Skip to content

Commit c4fca2c

Browse files
committed
BUG#37965633 REST view data-mapping field optionality
Data-mapping fields in REST Views establish a link with a different REST View, in the form of an underlying table or view foreign key relationship. This allows to encode one-to-one, one-to-many and many-to-many relationships between REST objects. Such types of field should account for the same optionality rules established for root-level fields when creating or updating REST documents, when they are fully embedded as nested fields in the top-level document, or entirely disallowed when they are expanded as new unnested fields (since linked documents become read-only in that case). The MRS SDK code generator is producing creatable and updatable type aliases that do not account for these specific constraints. This patch introduces changes to address these shortcomings. Change-Id: I80724d36a87e9be1e35ab35dd19db82243be3f4e
1 parent fe74f01 commit c4fca2c

File tree

3 files changed

+132
-52
lines changed

3 files changed

+132
-52
lines changed

gui/extension/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
### Additions
66

7-
-
7+
-
88

99
### Fixes
1010

11-
-
11+
- BUG#37965633 Optional fields required when creating or updating nested documents
12+
-
1213

1314
## Changes in 1.19.11+9.3.1
1415

mrs_plugin/lib/sdk.py

Lines changed: 127 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ def get_interface_datatype(
857857
field,
858858
sdk_language,
859859
class_name="",
860-
reference_class_name_postfix="",
860+
reference_class_name_suffix="",
861861
enhanced_fields=False,
862862
nullable=True,
863863
):
@@ -877,7 +877,7 @@ def get_interface_datatype(
877877

878878
return maybe_null(client_datatype, sdk_language)
879879
class_name_postfix = generate_identifier(value=field.get("name"), primitive="class", existing_identifiers=[])
880-
return f"I{class_name}{reference_class_name_postfix}{class_name_postfix}"
880+
return f"I{class_name}{reference_class_name_suffix}{class_name_postfix}"
881881

882882

883883
def datatype_is_primitive(client_datatype, sdk_language):
@@ -940,9 +940,6 @@ def field_has_row_ownership(field, obj):
940940

941941

942942
def field_is_required(field, obj):
943-
if field.get("lev") != 1:
944-
return False
945-
946943
db_column_info = field.get("db_column")
947944

948945
if db_column_info is None:
@@ -1015,10 +1012,14 @@ def get_reduced_field_interface_datatype(field, fields, sdk_language, class_name
10151012

10161013
# If the reference mapping is "to_many", use an array
10171014
ref_mapping = obj_ref.get("reference_mapping")
1018-
is_array = "[]" if ref_mapping and ref_mapping.get(
1019-
"to_many") == True else ""
1015+
is_array = ref_mapping and ref_mapping.get("to_many", False)
10201016

1021-
return datatype + is_array
1017+
if is_array and sdk_language == "Python":
1018+
return f"list[{datatype}]"
1019+
if is_array:
1020+
return f"{datatype}[]"
1021+
1022+
return datatype
10221023

10231024
return None
10241025

@@ -1033,6 +1034,8 @@ def generate_type_declaration(
10331034
requires_placeholder=False,
10341035
is_unpacked=False,
10351036
readonly_fields: set[str] = set(),
1037+
nesting_fields: set[str] = set(),
1038+
nested_value_prefix: str = "",
10361039
):
10371040
if len(fields) == 0:
10381041
if not requires_placeholder:
@@ -1042,7 +1045,7 @@ def generate_type_declaration(
10421045
field_block = [
10431046
generate_type_declaration_field(
10441047
name,
1045-
value,
1048+
f"I{nested_value_prefix}{value.lstrip("I")}" if name in nesting_fields else value,
10461049
sdk_language,
10471050
non_mandatory=(name in non_mandatory_fields),
10481051
allowed_special_characters={"(", ")"},
@@ -1064,7 +1067,7 @@ def generate_type_declaration(
10641067
field_block = [
10651068
generate_type_declaration_field(
10661069
name,
1067-
value,
1070+
f"list[I{nested_value_prefix}{value.lstrip("list[I")}" if name in nesting_fields else value,
10681071
sdk_language,
10691072
non_mandatory=(
10701073
name in non_mandatory_fields
@@ -1271,6 +1274,7 @@ def generate_interfaces(
12711274
):
12721275
obj_interfaces: list[str] = []
12731276
interface_fields = {}
1277+
reduced_to_datatype_fields = {}
12741278
param_interface_fields = {}
12751279
out_params_interface_fields = {}
12761280
obj_unique_fields = {}
@@ -1279,6 +1283,8 @@ def generate_interfaces(
12791283
has_nested_fields = False
12801284
required_datatypes: set[str] = set()
12811285
nested_fields: set[str] = set()
1286+
nesting_fields: set[str] = set()
1287+
generated_type_aliases: set[str] = set()
12821288

12831289
# The I{class_name}, I{class_name}Params and I{class_name}Out interfaces
12841290
for field in fields:
@@ -1295,13 +1301,17 @@ def generate_interfaces(
12951301
)
12961302
# Handle references
12971303
if field.get("represents_reference_id"):
1304+
# nested field type aliases are already top-level type aliases for other REST objects
1305+
# we want to re-use them instead of clone them into a new ones
1306+
nested_class_name = class_name.rstrip(lib.core.convert_path_to_pascal_case(db_obj.get("name")))
1307+
datatype = get_interface_datatype(field, sdk_language, nested_class_name)
12981308
has_nested_fields = True
12991309
# Check if the field should be reduced to the value of another field
13001310
reduced_to_datatype = get_reduced_field_interface_datatype(
1301-
field, fields, sdk_language, class_name
1311+
field, fields, sdk_language, nested_class_name
13021312
)
13031313
if reduced_to_datatype:
1304-
interface_fields.update({field.get("name"): reduced_to_datatype})
1314+
reduced_to_datatype_fields.update({field.get("name"): reduced_to_datatype})
13051315
else:
13061316
obj_ref = field.get("object_reference")
13071317
# Add field if the referred table is not unnested
@@ -1342,16 +1352,19 @@ def generate_interfaces(
13421352
# Call recursive interface generation
13431353
generate_nested_interfaces(
13441354
obj_interfaces,
1345-
interface_fields,
1355+
interface_fields | reduced_to_datatype_fields,
13461356
field,
1347-
reference_class_name_postfix=lib.core.convert_path_to_pascal_case(
1357+
reference_class_name_suffix=lib.core.convert_path_to_pascal_case(
13481358
field.get("name")
13491359
),
13501360
fields=fields,
1351-
class_name=class_name,
1361+
class_name=nested_class_name,
13521362
sdk_language=sdk_language,
1353-
nested_fields=nested_fields,
1363+
nesting_fields=nesting_fields,
13541364
fully_qualified_parent_name=field.get("name"),
1365+
allowed_crud_ops=set(db_object_crud_ops),
1366+
reference_obj=obj,
1367+
generated_type_aliases=generated_type_aliases,
13551368
)
13561369
elif obj.get("kind") == "PARAMETERS":
13571370
# If this field represents an OUT parameter of a SP, add it to the
@@ -1390,6 +1403,9 @@ def generate_interfaces(
13901403

13911404
if not object_is_routine(db_obj):
13921405
# The object is a TABLE or a VIEW
1406+
creatable_type_alias_name = f"New{class_name}"
1407+
updatable_type_alias_name = f"Update{class_name}"
1408+
13931409
if sdk_language != "TypeScript":
13941410
# These type declarations are not needed for TypeScript because it uses a Proxy to replace the interface
13951411
# and not a wrapper class. This might change in the future.
@@ -1416,7 +1432,8 @@ def generate_interfaces(
14161432
)
14171433
)
14181434

1419-
if "CREATE" in db_object_crud_ops:
1435+
# Do not generate type aliases that have already been created whilst processing nested fields.
1436+
if "CREATE" in db_object_crud_ops and creatable_type_alias_name not in generated_type_aliases:
14201437
obj_non_mandatory_fields = set(
14211438
[
14221439
field.get("name")
@@ -1426,17 +1443,20 @@ def generate_interfaces(
14261443
and field_is_required(field, obj) is False
14271444
]
14281445
)
1429-
14301446
obj_interfaces.append(
14311447
generate_type_declaration(
1432-
name=f"New{class_name}",
1448+
name=creatable_type_alias_name,
14331449
fields=interface_fields,
14341450
sdk_language=sdk_language,
14351451
non_mandatory_fields=obj_non_mandatory_fields,
1452+
nesting_fields=nesting_fields,
1453+
nested_value_prefix="New",
14361454
)
14371455
)
1456+
generated_type_aliases.add(creatable_type_alias_name)
14381457

1439-
if "UPDATE" in db_object_crud_ops:
1458+
# Do not generate type aliases that have already been created whilst processing nested fields.
1459+
if "UPDATE" in db_object_crud_ops and updatable_type_alias_name not in generated_type_aliases:
14401460
# TODO: No partial update is supported yet. Once it is, the
14411461
# `non-mandatory_fields` argument should not change.
14421462
# This way, users can know what fields are required and which ones aren't.
@@ -1449,28 +1469,33 @@ def generate_interfaces(
14491469
]
14501470
obj_interfaces.append(
14511471
generate_type_declaration(
1452-
name=f"Update{class_name}",
1472+
name=updatable_type_alias_name,
14531473
fields=interface_fields,
14541474
sdk_language=sdk_language,
14551475
non_mandatory_fields=set(nullable_fields),
1476+
nesting_fields=nesting_fields,
1477+
nested_value_prefix="Update",
14561478
)
14571479
)
1480+
generated_type_aliases.add(updatable_type_alias_name)
14581481

1459-
primary_key_fields = [field.get("name") for field in fields if field_is_pk(field)]
1460-
obj_interfaces.append(
1461-
generate_data_class(
1462-
name=class_name,
1463-
fields=interface_fields,
1464-
sdk_language=sdk_language,
1465-
db_object_crud_ops=db_object_crud_ops,
1466-
obj_endpoint=obj_endpoint,
1467-
primary_key_fields=set(primary_key_fields),
1482+
if class_name not in generated_type_aliases:
1483+
primary_key_fields = [field.get("name") for field in fields if field_is_pk(field)]
1484+
obj_interfaces.append(
1485+
generate_data_class(
1486+
name=class_name,
1487+
fields=interface_fields | reduced_to_datatype_fields,
1488+
sdk_language=sdk_language,
1489+
db_object_crud_ops=db_object_crud_ops,
1490+
obj_endpoint=obj_endpoint,
1491+
primary_key_fields=set(primary_key_fields),
1492+
)
14681493
)
1469-
)
1494+
generated_type_aliases.add(class_name)
14701495

14711496
obj_interfaces.append(
14721497
generate_field_enum(
1473-
name=class_name, fields=interface_fields, sdk_language=sdk_language
1498+
name=class_name, fields=interface_fields | reduced_to_datatype_fields, sdk_language=sdk_language
14741499
)
14751500
)
14761501

@@ -1492,7 +1517,7 @@ def generate_interfaces(
14921517
)
14931518

14941519
obj_interfaces.append(
1495-
generate_selectable(class_name, interface_fields, sdk_language)
1520+
generate_selectable(class_name, interface_fields | reduced_to_datatype_fields, sdk_language)
14961521
)
14971522
obj_interfaces.append(
14981523
generate_sortable(class_name, obj_sortable_fields, sdk_language)
@@ -1616,15 +1641,22 @@ def generate_interfaces(
16161641
# For now, this function is not used for ${DatabaseObject}Params type declarations
16171642
def generate_nested_interfaces(
16181643
obj_interfaces, parent_interface_fields, parent_field,
1619-
reference_class_name_postfix,
1620-
fields, class_name, sdk_language, fully_qualified_parent_name: str = "", nested_fields: set[str] = set()):
1644+
reference_class_name_suffix,
1645+
fields, class_name, reference_obj,
1646+
sdk_language: Optional[Literal["TypeScript", "Python"]] = "TypeScript",
1647+
fully_qualified_parent_name: str = "",
1648+
nested_fields: set[str] = set(),
1649+
nesting_fields: set[str] = set(),
1650+
allowed_crud_ops: set[str] = set(),
1651+
generated_type_aliases: set[str] = set()):
16211652
# Build interface name
1622-
interface_name = f"{class_name}{reference_class_name_postfix}"
1653+
interface_name = f"{class_name}{reference_class_name_suffix}"
16231654

16241655
# Check if the reference has unnest set, and if so, use the parent_interface_fields
16251656
parent_obj_ref = parent_field.get("object_reference")
16261657
interface_fields = {} if not parent_obj_ref.get(
16271658
"unnest") else parent_interface_fields
1659+
reduced_to_datatype_fields = {}
16281660

16291661
for field in fields:
16301662
if (field.get("parent_reference_id") == parent_field.get("represents_reference_id") and
@@ -1635,36 +1667,83 @@ def generate_nested_interfaces(
16351667
reduced_to_datatype = get_reduced_field_interface_datatype(
16361668
field, fields, sdk_language, class_name)
16371669
if reduced_to_datatype:
1638-
interface_fields.update({ field.get("name"): reduced_to_datatype })
1670+
reduced_to_datatype_fields.update({ field.get("name"): reduced_to_datatype })
16391671
else:
16401672
obj_ref = field.get("object_reference")
16411673
field_interface_name = lib.core.convert_path_to_pascal_case(
16421674
field.get("name"))
16431675
# Add field if the referred table is not unnested
16441676
if not obj_ref.get("unnest"):
1645-
datatype = f"{class_name}{reference_class_name_postfix + field.get("name")}"
1677+
datatype = f"{class_name}{reference_class_name_suffix + field.get("name")}"
16461678
# Should use the corresponding nested field type.
16471679
interface_fields.update({ field.get("name"): f"I{interface_name}" + field_interface_name })
16481680

16491681
# If not, do recursive call
16501682
generate_nested_interfaces(
16511683
obj_interfaces, interface_fields, field,
1652-
reference_class_name_postfix=reference_class_name_postfix + field_interface_name,
1653-
fields=fields, class_name=class_name, sdk_language=sdk_language, nested_fields=nested_fields,
1654-
fully_qualified_parent_name=f"{parent_field.get("name")}.{field.get("name")}")
1684+
reference_class_name_suffix=reference_class_name_suffix + field_interface_name,
1685+
fields=fields, class_name=class_name, reference_obj=reference_obj, sdk_language=sdk_language, nested_fields=nested_fields,
1686+
fully_qualified_parent_name=f"{parent_field.get("name")}.{field.get("name")}", generated_type_aliases=generated_type_aliases)
16551687
else:
16561688
datatype = get_interface_datatype(field, sdk_language)
16571689
interface_fields.update({ field.get("name"): datatype })
16581690

16591691
if not parent_obj_ref.get("unnest"):
1660-
obj_interfaces.append(
1661-
generate_type_declaration(
1662-
name=interface_name,
1663-
fields=interface_fields,
1664-
sdk_language=sdk_language,
1665-
non_mandatory_fields=set(interface_fields),
1692+
creatable_type_alias_name = f"New{interface_name}"
1693+
updatable_type_alias_name = f"Update{interface_name}"
1694+
# Do not generate type aliases that have already been created whilst processing top-level fields.
1695+
if "CREATE" in allowed_crud_ops and creatable_type_alias_name not in generated_type_aliases:
1696+
non_mandatory_fields = [
1697+
field.get("name")
1698+
for field in fields
1699+
# exclude fields that are out of range (e.g. on different nesting levels)
1700+
if field.get("parent_reference_id") == parent_field.get("represents_reference_id")
1701+
and field_is_required(field, reference_obj) is False
1702+
]
1703+
obj_interfaces.append(
1704+
generate_type_declaration(
1705+
name=creatable_type_alias_name,
1706+
fields=interface_fields,
1707+
sdk_language=sdk_language,
1708+
non_mandatory_fields=set(non_mandatory_fields),
1709+
nesting_fields=nesting_fields,
1710+
nested_value_prefix="New",
1711+
)
16661712
)
1667-
)
1713+
generated_type_aliases.add(creatable_type_alias_name)
1714+
# Do not generate type aliases that have already been created whilst processing top-level fields.
1715+
if "UPDATE" in allowed_crud_ops and updatable_type_alias_name not in generated_type_aliases:
1716+
nullable_fields = [
1717+
field.get("name")
1718+
for field in fields
1719+
# exclude fields that are out of range (e.g. on different nesting levels)
1720+
if field.get("parent_reference_id") == parent_field.get("represents_reference_id")
1721+
and field_is_nullable(field) or field_has_row_ownership(field, reference_obj)
1722+
]
1723+
obj_interfaces.append(
1724+
generate_type_declaration(
1725+
name=updatable_type_alias_name,
1726+
fields=interface_fields,
1727+
sdk_language=sdk_language,
1728+
non_mandatory_fields=set(nullable_fields),
1729+
nesting_fields=nesting_fields,
1730+
nested_value_prefix="Update",
1731+
)
1732+
)
1733+
generated_type_aliases.add(updatable_type_alias_name)
1734+
# Do not generate type aliases that have already been created whilst processing top-level fields.
1735+
if interface_name not in generated_type_aliases:
1736+
readable_type_alias_fields = interface_fields | reduced_to_datatype_fields
1737+
obj_interfaces.append(
1738+
generate_type_declaration(
1739+
name=interface_name,
1740+
fields=readable_type_alias_fields,
1741+
sdk_language=sdk_language,
1742+
non_mandatory_fields=set(readable_type_alias_fields),
1743+
)
1744+
)
1745+
generated_type_aliases.add(interface_name)
1746+
nesting_fields.add(fully_qualified_parent_name)
16681747
nested_fields.update([f"{fully_qualified_parent_name}.{field}" for field in interface_fields])
16691748

16701749

mrs_plugin/sdk/python/mrs_service_template.py.template

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ class ${obj_class_name}DatabaseObject(MrsBaseObject):
178178
I${obj_class_name}UniqueFilterable,
179179
I${obj_class_name}Selectable,
180180
I${obj_class_name}Field,
181-
I${obj_class_name}NestedField
181+
I${obj_class_name}NestedField,
182182
]
183183
],
184184
) -> Optional[I${obj_class_name}]:
@@ -191,7 +191,7 @@ class ${obj_class_name}DatabaseObject(MrsBaseObject):
191191
I${obj_class_name}UniqueFilterable,
192192
I${obj_class_name}Selectable,
193193
I${obj_class_name}Field,
194-
I${obj_class_name}NestedField
194+
I${obj_class_name}NestedField,
195195
]
196196
],
197197
) -> I${obj_class_name}:

0 commit comments

Comments
 (0)