Skip to content

Commit 065878d

Browse files
authored
Merge pull request #110 from forcedotcom/deploy-dmo-package
fix deploy for dmo package
2 parents fb05a4f + 41229a1 commit 065878d

2 files changed

Lines changed: 487 additions & 28 deletions

File tree

src/datacustomcode/deploy.py

Lines changed: 109 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@
2626
Callable,
2727
Dict,
2828
List,
29+
Optional,
2930
Union,
3031
)
3132

3233
from loguru import logger
3334
import pydantic
34-
from pydantic import BaseModel
35+
from pydantic import BaseModel, model_validator
3536
import requests
3637

3738
from datacustomcode.cmd import cmd_output
@@ -366,23 +367,86 @@ class BaseConfig(BaseModel):
366367
entryPoint: str
367368

368369

370+
class DataObjectField(BaseModel):
371+
name: str
372+
label: str
373+
dataType: str
374+
isPrimaryKey: bool = False
375+
keyQualifierFieldName: Optional[str] = None
376+
377+
378+
class DataObject(BaseModel):
379+
name: str
380+
label: str
381+
type: str
382+
category: str
383+
fields: list[DataObjectField]
384+
385+
369386
class DataTransformConfig(BaseConfig):
370387
sdkVersion: str
371388
dataspace: str
372389
permissions: Permissions
390+
dataObjects: Optional[list[DataObject]] = None
373391

374392

375393
class FunctionConfig(BaseConfig):
376394
pass
377395

378396

397+
class DloPermission(BaseModel):
398+
dlo: list[str]
399+
400+
401+
class DmoPermission(BaseModel):
402+
dmo: list[str]
403+
404+
379405
class Permissions(BaseModel):
380-
read: Union[DloPermission]
381-
write: Union[DloPermission]
406+
read: Union[DloPermission, DmoPermission]
407+
write: Union[DloPermission, DmoPermission]
408+
409+
@model_validator(mode="after")
410+
def _no_mixed_layers(self) -> "Permissions":
411+
read_is_dlo = isinstance(self.read, DloPermission)
412+
write_is_dlo = isinstance(self.write, DloPermission)
413+
if read_is_dlo != write_is_dlo:
414+
raise ValueError(
415+
"permissions.read and permissions.write must both reference "
416+
"DLOs or both reference DMOs (got "
417+
f"read={type(self.read).__name__}, "
418+
f"write={type(self.write).__name__})"
419+
)
420+
return self
382421

383422

384-
class DloPermission(BaseModel):
385-
dlo: list[str]
423+
def _permission_entries(perm: Union[DloPermission, DmoPermission]) -> list[str]:
424+
"""Return the list of object names regardless of layer (DLO or DMO)."""
425+
if isinstance(perm, DloPermission):
426+
return perm.dlo
427+
return perm.dmo
428+
429+
430+
def _data_object_to_output(obj: DataObject) -> dict[str, Any]:
431+
"""Convert a config.json DataObject into an outputDataObjects entry."""
432+
fields: list[dict[str, Any]] = []
433+
for field in obj.fields:
434+
entry: dict[str, Any] = {
435+
"isPrimaryKey": field.isPrimaryKey,
436+
"label": field.label,
437+
"name": field.name,
438+
"type": field.dataType,
439+
}
440+
if field.keyQualifierFieldName is not None:
441+
entry["keyQualifierField"] = field.keyQualifierFieldName
442+
fields.append(entry)
443+
return {
444+
"category": obj.category,
445+
"fields": fields,
446+
"label": obj.label,
447+
"name": obj.name,
448+
"type": obj.type,
449+
}
386450

387451

388452
def get_config(directory: str) -> BaseConfig:
@@ -404,10 +468,17 @@ def get_config(directory: str) -> BaseConfig:
404468
except json.JSONDecodeError as err:
405469
raise ValueError(f"config.json at {config_path} is not valid JSON") from err
406470
except pydantic.ValidationError as err:
407-
missing_fields = [str(err["loc"][0]) for err in err.errors()]
471+
errors = err.errors()
472+
missing = [e for e in errors if e.get("type") == "missing"]
473+
if missing and len(missing) == len(errors):
474+
missing_fields = [str(e["loc"][0]) for e in missing]
475+
raise ValueError(
476+
f"config.json at {config_path} is missing required "
477+
f"fields: {', '.join(missing_fields)}"
478+
) from err
479+
messages = [str(e.get("msg", "")) for e in errors]
408480
raise ValueError(
409-
f"config.json at {config_path} is missing required "
410-
f"fields: {', '.join(missing_fields)}"
481+
f"config.json at {config_path} is invalid: {'; '.join(messages)}"
411482
) from err
412483

413484

@@ -421,26 +492,45 @@ def create_data_transform(
421492
script_name = metadata.name
422493
request_hydrated = DATA_TRANSFORM_REQUEST_TEMPLATE.copy()
423494

424-
# Add nodes for each write DLO
425-
for i, dlo in enumerate(data_transform_config.permissions.write.dlo, 1):
495+
# Add nodes for each write entry (DLO or DMO)
496+
for i, name in enumerate(
497+
_permission_entries(data_transform_config.permissions.write), 1
498+
):
426499
request_hydrated["nodes"][f"node{i}"] = {
427-
"relation_name": dlo,
500+
"relation_name": name,
428501
"config": {"materialized": "table"},
429502
"compiled_code": "",
430503
}
431504

432-
# Add sources for each read DLO
433-
for i, dlo in enumerate(data_transform_config.permissions.read.dlo, 1):
434-
request_hydrated["sources"][f"source{i}"] = {"relation_name": dlo}
505+
# Add sources for each read entry (DLO or DMO)
506+
for i, name in enumerate(
507+
_permission_entries(data_transform_config.permissions.read), 1
508+
):
509+
request_hydrated["sources"][f"source{i}"] = {"relation_name": name}
435510

436511
request_hydrated["macros"]["macro.byoc"]["arguments"][0]["name"] = script_name
437512

513+
definition: dict[str, Any] = {
514+
"type": "DCSQL",
515+
"manifest": request_hydrated,
516+
"version": "56.0",
517+
}
518+
519+
# outputDataObjects is only set for DMO-backed transforms. The server requires
520+
# the schema of any DMO created/updated by the transform; DLO transforms use
521+
# an existing materialized table and must not include this field.
522+
if isinstance(data_transform_config.permissions.write, DmoPermission):
523+
if not data_transform_config.dataObjects:
524+
raise ValueError(
525+
"DMO transforms require 'dataObjects' in config.json describing "
526+
"the schema of each output DMO."
527+
)
528+
definition["outputDataObjects"] = [
529+
_data_object_to_output(obj) for obj in data_transform_config.dataObjects
530+
]
531+
438532
body = {
439-
"definition": {
440-
"type": "DCSQL",
441-
"manifest": request_hydrated,
442-
"version": "56.0",
443-
},
533+
"definition": definition,
444534
"label": f"{metadata.name}",
445535
"name": f"{metadata.name}",
446536
"type": "BATCH",

0 commit comments

Comments
 (0)