@githolon/dsl 0.102.2 → 0.102.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/codegen_dart.ts +2 -2
- package/src/codegen_dart_application.ts +104 -37
- package/src/codegen_dart_client.ts +9 -5
- package/src/codegen_ts.ts +1 -1
- package/src/fields.ts +8 -1
- package/src/law_check.ts +5 -5
- package/src/value_object.ts +8 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.102.
|
|
3
|
+
"version": "0.102.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/codegen_dart.ts
CHANGED
|
@@ -1707,7 +1707,7 @@ function dartTypeOfVoField(f: ValueObjectFieldDescriptor, voName: string, fieldN
|
|
|
1707
1707
|
switch (f.type) {
|
|
1708
1708
|
case "string":
|
|
1709
1709
|
return f.idOf !== undefined
|
|
1710
|
-
? `NomosRef<${concreteClassName(f.idOf)}>`
|
|
1710
|
+
? `NomosRef<${f.idOfContract === true ? contractTypeName(f.idOf) : concreteClassName(f.idOf)}>`
|
|
1711
1711
|
: f.brand ?? "String"; // a brandedId(...) → the generated extension-type wrapper
|
|
1712
1712
|
case "number":
|
|
1713
1713
|
// a UNIT-tagged number is the generated zero-cost extension type; `.int()` is Dart int.
|
|
@@ -1742,7 +1742,7 @@ function voFromExpr(
|
|
|
1742
1742
|
switch (f.type) {
|
|
1743
1743
|
case "string":
|
|
1744
1744
|
if (f.idOf !== undefined) {
|
|
1745
|
-
const target = concreteClassName(f.idOf);
|
|
1745
|
+
const target = f.idOfContract === true ? contractTypeName(f.idOf) : concreteClassName(f.idOf);
|
|
1746
1746
|
return `NmsInternalNomosRefs.create<${target}>(workspace: '', type: '${f.idOf}', id: ${expr} as String)`;
|
|
1747
1747
|
}
|
|
1748
1748
|
return f.brand !== undefined ? `${f.brand}(${expr} as String)` : `${expr} as String`;
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "./framework/framework_aggregates.js";
|
|
32
32
|
import { isBusinessIntent } from "./intent.js";
|
|
33
33
|
import { contractTypeName } from "./contract.js";
|
|
34
|
+
import { admitsNull } from "./null_meta.js";
|
|
34
35
|
import type {
|
|
35
36
|
WorkspaceSharingPolicy,
|
|
36
37
|
WorkspaceSharingReceipt,
|
|
@@ -720,6 +721,11 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
720
721
|
const targetFile = owner?.name ?? mod.name;
|
|
721
722
|
const targetApplication = dartApplicationClassName(targetDomain);
|
|
722
723
|
const opener = `nomosInternalOpen${targetApplication}`;
|
|
724
|
+
const rootAggregate = aggregates.get(birth.aggregateId);
|
|
725
|
+
const rootType = concreteClassName(birth.aggregateId);
|
|
726
|
+
const rootCollection = member(plural(rootAggregate === undefined
|
|
727
|
+
? birth.aggregateId
|
|
728
|
+
: concreteClassName(rootAggregate.id)));
|
|
723
729
|
if (targetFile !== mod.name) {
|
|
724
730
|
addSibling(`${targetFile}.application.dart`, targetApplication);
|
|
725
731
|
addSibling(`${targetFile}.application.dart`, opener);
|
|
@@ -734,7 +740,9 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
734
740
|
` /// or call a lower-level client.`,
|
|
735
741
|
` Future<${targetApplication}> ${method}(${dartActionInputClassName(directive)} facts) async {`,
|
|
736
742
|
` final reference = await _client.${method}(facts);`,
|
|
737
|
-
|
|
743
|
+
opts.supportImport === "package"
|
|
744
|
+
? ` return ${rootCollection}._nmsOpenReference(reference);`
|
|
745
|
+
: ` return ${opener}(_client.bridge, reference, _client.actor);`,
|
|
738
746
|
` }`,
|
|
739
747
|
``,
|
|
740
748
|
);
|
|
@@ -743,31 +751,47 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
743
751
|
` /// Give one ${birth.aggregateId} its own durable life.`,
|
|
744
752
|
` Future<${targetApplication}> birth(Create${cap(camel(birth.aggregateId))}Input facts) async {`,
|
|
745
753
|
` final reference = await _client.birth(facts);`,
|
|
746
|
-
|
|
754
|
+
opts.supportImport === "package"
|
|
755
|
+
? ` return _nmsOpenReference(reference);`
|
|
756
|
+
: ` return ${opener}(_client.bridge, reference, _client.actor);`,
|
|
747
757
|
` }`,
|
|
748
758
|
``,
|
|
749
759
|
]);
|
|
750
760
|
}
|
|
751
761
|
if (opts.supportImport === "package") {
|
|
752
762
|
usesWorkspaceName = true;
|
|
753
|
-
const rootAggregate = aggregates.get(birth.aggregateId);
|
|
754
763
|
const rootModel = rootAggregate === undefined
|
|
755
764
|
? `${concreteClassName(birth.aggregateId)}ReadModel`
|
|
756
765
|
: readModelClassName(rootAggregate.id);
|
|
757
|
-
const rootType = concreteClassName(birth.aggregateId);
|
|
758
|
-
const rootCollection = member(plural(rootAggregate === undefined
|
|
759
|
-
? birth.aggregateId
|
|
760
|
-
: concreteClassName(rootAggregate.id)));
|
|
761
766
|
addCollectionMember(birth.aggregateId, "open", [
|
|
767
|
+
` final Map<String, Future<${targetApplication}>> _nmsApplications = {};`,
|
|
768
|
+
``,
|
|
769
|
+
` Future<${targetApplication}> _nmsOpenReference(NomosRef<${rootType}> reference) {`,
|
|
770
|
+
` final workspace = NmsInternalNomosRefs.workspace(reference);`,
|
|
771
|
+
` final existing = _nmsApplications[workspace];`,
|
|
772
|
+
` if (existing != null) return existing;`,
|
|
773
|
+
` late final Future<${targetApplication}> opened;`,
|
|
774
|
+
` opened = (() async {`,
|
|
775
|
+
` try {`,
|
|
776
|
+
` return await ${opener}(_client.bridge, reference, _client.actor);`,
|
|
777
|
+
` } catch (_) {`,
|
|
778
|
+
` if (identical(_nmsApplications[workspace], opened)) {`,
|
|
779
|
+
` _nmsApplications.remove(workspace);`,
|
|
780
|
+
` }`,
|
|
781
|
+
` rethrow;`,
|
|
782
|
+
` }`,
|
|
783
|
+
` })();`,
|
|
784
|
+
` _nmsApplications[workspace] = opened;`,
|
|
785
|
+
` return opened;`,
|
|
786
|
+
` }`,
|
|
787
|
+
``,
|
|
762
788
|
` /// Open the ${birth.aggregateId} whose ${birth.keyField} is [key].`,
|
|
763
|
-
` Future<${targetApplication}> open(String key) =>
|
|
764
|
-
` _client.bridge,`,
|
|
789
|
+
` Future<${targetApplication}> open(String key) => _nmsOpenReference(`,
|
|
765
790
|
` NmsInternalNomosRefs.create<${rootType}>(`,
|
|
766
791
|
` workspace: deriveWorkspaceName(${JSON.stringify(birth.namespace)}, key),`,
|
|
767
792
|
` type: ${JSON.stringify(birth.aggregateId)},`,
|
|
768
793
|
` id: ${JSON.stringify(birth.rootId)},`,
|
|
769
794
|
` ),`,
|
|
770
|
-
` _client.actor,`,
|
|
771
795
|
` );`,
|
|
772
796
|
``,
|
|
773
797
|
]);
|
|
@@ -789,14 +813,9 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
789
813
|
` if (raw is! Map) continue;`,
|
|
790
814
|
` final child = (raw['child'] ?? '').toString();`,
|
|
791
815
|
` if (!child.startsWith(${JSON.stringify(`${birth.namespace}-`)})) continue;`,
|
|
792
|
-
` final
|
|
793
|
-
`
|
|
794
|
-
`
|
|
795
|
-
` _client.actor,`,
|
|
796
|
-
` );`,
|
|
797
|
-
` final record = await application.${rootCollection}.find(`,
|
|
798
|
-
` NmsInternalNomosRefs.create<${rootType}>(workspace: child, type: ${JSON.stringify(birth.aggregateId)}, id: ${JSON.stringify(birth.rootId)}),`,
|
|
799
|
-
` );`,
|
|
816
|
+
` final reference = NmsInternalNomosRefs.create<${rootType}>(workspace: child, type: ${JSON.stringify(birth.aggregateId)}, id: ${JSON.stringify(birth.rootId)});`,
|
|
817
|
+
` final application = await _nmsOpenReference(reference);`,
|
|
818
|
+
` final record = await application.${rootCollection}.find(reference);`,
|
|
800
819
|
` if (record != null) records.add(record);`,
|
|
801
820
|
` }`,
|
|
802
821
|
` return records;`,
|
|
@@ -832,14 +851,9 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
832
851
|
` object != 'workspace:self' ||`,
|
|
833
852
|
` prunedAt.isNotEmpty ||`,
|
|
834
853
|
` !seen.add(workspace)) continue;`,
|
|
835
|
-
` final
|
|
836
|
-
`
|
|
837
|
-
`
|
|
838
|
-
` _client.actor,`,
|
|
839
|
-
` );`,
|
|
840
|
-
` final record = await application.${rootCollection}.find(`,
|
|
841
|
-
` NmsInternalNomosRefs.create<${rootType}>(workspace: workspace, type: ${JSON.stringify(birth.aggregateId)}, id: ${JSON.stringify(birth.rootId)}),`,
|
|
842
|
-
` );`,
|
|
854
|
+
` final reference = NmsInternalNomosRefs.create<${rootType}>(workspace: workspace, type: ${JSON.stringify(birth.aggregateId)}, id: ${JSON.stringify(birth.rootId)});`,
|
|
855
|
+
` final application = await _nmsOpenReference(reference);`,
|
|
856
|
+
` final record = await application.${rootCollection}.find(reference);`,
|
|
843
857
|
` if (record != null) records.add(record);`,
|
|
844
858
|
` }`,
|
|
845
859
|
` return records;`,
|
|
@@ -854,10 +868,8 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
854
868
|
` /// Continue working in the business application that produced [record].`,
|
|
855
869
|
` Future<${targetApplication}> applicationOf(${rootModel} record) {`,
|
|
856
870
|
` final origin = NmsInternalRecordOrigins.require(record);`,
|
|
857
|
-
` return
|
|
858
|
-
` _client.bridge,`,
|
|
871
|
+
` return _nmsOpenReference(`,
|
|
859
872
|
` NmsInternalNomosRefs.create<${rootType}>(workspace: origin.workspace, type: ${JSON.stringify(birth.aggregateId)}, id: origin.id),`,
|
|
860
|
-
` _client.actor,`,
|
|
861
873
|
` );`,
|
|
862
874
|
` }`,
|
|
863
875
|
``,
|
|
@@ -1027,6 +1039,55 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1027
1039
|
(candidate as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
1028
1040
|
)) {
|
|
1029
1041
|
const sourceModel = readModelClassName(aggregate.id);
|
|
1042
|
+
for (const contractId of aggregate.implementsContracts ?? []) {
|
|
1043
|
+
const contract = contractTypeName(contractId);
|
|
1044
|
+
const method = member(`as${contract}`);
|
|
1045
|
+
addCollectionMember(aggregate.id, method, [
|
|
1046
|
+
` /// Use [record] wherever another domain declares [${contract}].`,
|
|
1047
|
+
` ///`,
|
|
1048
|
+
` /// Nomos preserves the record's workspace-qualified identity while exposing only`,
|
|
1049
|
+
` /// the contract that this aggregate explicitly implements. This does not grant access.`,
|
|
1050
|
+
` NomosRef<${contract}> ${method}(${sourceModel} record) {`,
|
|
1051
|
+
` final reference = record.ref;`,
|
|
1052
|
+
` _nmsRequireReference(reference, ${JSON.stringify(aggregate.id)}, '');`,
|
|
1053
|
+
` return NmsInternalNomosRefs.create<${contract}>(`,
|
|
1054
|
+
` workspace: reference.workspace,`,
|
|
1055
|
+
` type: ${JSON.stringify(contractId)},`,
|
|
1056
|
+
` id: reference.id,`,
|
|
1057
|
+
` );`,
|
|
1058
|
+
` }`,
|
|
1059
|
+
``,
|
|
1060
|
+
]);
|
|
1061
|
+
const resolveMethod = member(`resolve${contract}`);
|
|
1062
|
+
addCollectionMember(aggregate.id, resolveMethod, [
|
|
1063
|
+
` /// Resolve [reference] as the concrete ${sourceModel} that implements [${contract}].`,
|
|
1064
|
+
` ///`,
|
|
1065
|
+
` /// Use this after following a generated cross-application reference and opening`,
|
|
1066
|
+
` /// its business application. Nomos checks the declared contract and access on read.`,
|
|
1067
|
+
` Future<${sourceModel}?> ${resolveMethod}(ForeignRef<${contract}> reference) {`,
|
|
1068
|
+
` _nmsRequireReference(reference.ref, ${JSON.stringify(contractId)}, '');`,
|
|
1069
|
+
` return find(NmsInternalNomosRefs.create<${concreteClassName(aggregate.id)}>(`,
|
|
1070
|
+
` workspace: reference.workspace,`,
|
|
1071
|
+
` type: ${JSON.stringify(aggregate.id)},`,
|
|
1072
|
+
` id: reference.id,`,
|
|
1073
|
+
` ));`,
|
|
1074
|
+
` }`,
|
|
1075
|
+
``,
|
|
1076
|
+
]);
|
|
1077
|
+
const watchMethod = member(`watch${contract}`);
|
|
1078
|
+
addCollectionMember(aggregate.id, watchMethod, [
|
|
1079
|
+
` /// React when the concrete record behind [reference] changes.`,
|
|
1080
|
+
` Stream<${sourceModel}?> ${watchMethod}(ForeignRef<${contract}> reference) {`,
|
|
1081
|
+
` _nmsRequireReference(reference.ref, ${JSON.stringify(contractId)}, '');`,
|
|
1082
|
+
` return watchOne(NmsInternalNomosRefs.create<${concreteClassName(aggregate.id)}>(`,
|
|
1083
|
+
` workspace: reference.workspace,`,
|
|
1084
|
+
` type: ${JSON.stringify(aggregate.id)},`,
|
|
1085
|
+
` id: reference.id,`,
|
|
1086
|
+
` ));`,
|
|
1087
|
+
` }`,
|
|
1088
|
+
``,
|
|
1089
|
+
]);
|
|
1090
|
+
}
|
|
1030
1091
|
const sameDomainRefs = Object.entries(aggregate.fields).filter(([, field]) =>
|
|
1031
1092
|
field.kind === "ref" &&
|
|
1032
1093
|
field.foreignRef === undefined &&
|
|
@@ -1051,7 +1112,10 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1051
1112
|
? sourceFieldMember.slice(0, -2)
|
|
1052
1113
|
: `${sourceFieldMember}Ref`;
|
|
1053
1114
|
const method = member(stem);
|
|
1054
|
-
const memberNullable = field.isFromBirth !== true;
|
|
1115
|
+
const memberNullable = admitsNull(field) || field.isFromBirth !== true;
|
|
1116
|
+
const sourceFieldValue = admitsNull(field)
|
|
1117
|
+
? `record.${sourceFieldMember}.valueOrNull`
|
|
1118
|
+
: `record.${sourceFieldMember}`;
|
|
1055
1119
|
const isSameDomain = sameDomainRefs.some(([name]) => name === fieldName);
|
|
1056
1120
|
const isCrossDomain = crossDomainRefs.some(([name]) => name === fieldName);
|
|
1057
1121
|
if (!isSameDomain && !isCrossDomain) continue;
|
|
@@ -1064,7 +1128,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1064
1128
|
` Future<${targetModel}?> ${method}(${sourceModel} record) async {`,
|
|
1065
1129
|
...(memberNullable
|
|
1066
1130
|
? [
|
|
1067
|
-
` final id =
|
|
1131
|
+
` final id = ${sourceFieldValue};`,
|
|
1068
1132
|
` if (id == null) return null;`,
|
|
1069
1133
|
` final rows = await _client.${targetById}(NmsInternalNomosRefs.id(id));`,
|
|
1070
1134
|
]
|
|
@@ -1095,7 +1159,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1095
1159
|
` Future<${targetModel}?> ${method}(${sourceModel} record) async {`,
|
|
1096
1160
|
...(memberNullable
|
|
1097
1161
|
? [
|
|
1098
|
-
` final id =
|
|
1162
|
+
` final id = ${sourceFieldValue};`,
|
|
1099
1163
|
` if (id == null) return null;`,
|
|
1100
1164
|
` final application = await ${opener}(`,
|
|
1101
1165
|
` _client.bridge,`,
|
|
@@ -1107,7 +1171,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1107
1171
|
` );`,
|
|
1108
1172
|
]
|
|
1109
1173
|
: [
|
|
1110
|
-
` final id =
|
|
1174
|
+
` final id = ${sourceFieldValue};`,
|
|
1111
1175
|
` final application = await ${opener}(`,
|
|
1112
1176
|
` _client.bridge,`,
|
|
1113
1177
|
` id,`,
|
|
@@ -1126,7 +1190,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1126
1190
|
` Future<${targetApplication}?> ${applicationMethod}(${sourceModel} record) {`,
|
|
1127
1191
|
...(memberNullable
|
|
1128
1192
|
? [
|
|
1129
|
-
` final id =
|
|
1193
|
+
` final id = ${sourceFieldValue};`,
|
|
1130
1194
|
` if (id == null) return Future<${targetApplication}?>.value(null);`,
|
|
1131
1195
|
` return ${opener}(`,
|
|
1132
1196
|
` _client.bridge,`,
|
|
@@ -1151,12 +1215,15 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1151
1215
|
? contractTypeName(foreign.targetAggregateId)
|
|
1152
1216
|
: concreteClassName(foreign.targetAggregateId);
|
|
1153
1217
|
const fieldMember = member(fieldName);
|
|
1218
|
+
const fieldValue = admitsNull(field)
|
|
1219
|
+
? `record.${fieldMember}.valueOrNull`
|
|
1220
|
+
: `record.${fieldMember}`;
|
|
1154
1221
|
const stem = fieldMember.endsWith("Id") ? fieldMember.slice(0, -2) : fieldMember;
|
|
1155
1222
|
const method = member(`${stem}Of`);
|
|
1156
1223
|
addCollectionMember(aggregate.id, method, [
|
|
1157
1224
|
` /// Follow [record]'s ${fieldName} to its typed business record.`,
|
|
1158
1225
|
` Future<${target}?> ${method}(${sourceModel} record) async {`,
|
|
1159
|
-
` final reference =
|
|
1226
|
+
` final reference = ${fieldValue};`,
|
|
1160
1227
|
` if (reference == null) return null;`,
|
|
1161
1228
|
` final target = await NmsInternalForeignRefs.resolve(reference, _client.bridge.foreignReader());`,
|
|
1162
1229
|
` if (target == null) return null;`,
|
|
@@ -1179,7 +1246,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1179
1246
|
addCollectionMember(aggregate.id, applicationMethod, [
|
|
1180
1247
|
` /// Open the business application containing [record]'s ${fieldName}.`,
|
|
1181
1248
|
` Future<${targetApplication}?> ${applicationMethod}(${sourceModel} record) {`,
|
|
1182
|
-
` final reference =
|
|
1249
|
+
` final reference = ${fieldValue};`,
|
|
1183
1250
|
` if (reference == null) return Future<${targetApplication}?>.value(null);`,
|
|
1184
1251
|
` return ${opener}(`,
|
|
1185
1252
|
` _client.bridge,`,
|
|
@@ -1294,7 +1361,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1294
1361
|
`/// a repository or parallel application state.`,
|
|
1295
1362
|
`final class ${collection} {`,
|
|
1296
1363
|
` final ${client} _client;`,
|
|
1297
|
-
`
|
|
1364
|
+
` ${collection}._(this._client);`,
|
|
1298
1365
|
``,
|
|
1299
1366
|
...members,
|
|
1300
1367
|
`}`,
|
|
@@ -62,6 +62,7 @@ import { buildNomosDescribeModel, type NomosDescribeModel } from "./describe_mod
|
|
|
62
62
|
import { assertModelClientMemberAvailable } from "./generated_client_names.js";
|
|
63
63
|
import { ownedRecordActionDirectiveId } from "./framework/owned_custody.js";
|
|
64
64
|
import { contractTypeName } from "./contract.js";
|
|
65
|
+
import { admitsNull } from "./null_meta.js";
|
|
65
66
|
|
|
66
67
|
const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
|
|
67
68
|
const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
|
|
@@ -1268,14 +1269,15 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
1268
1269
|
// the DSL schema's `.optional()` — a partial fold decodes every scalar `null` when absent
|
|
1269
1270
|
// (`emitReadModel`'s own doc: "PARTIAL folds"), so the member is nullable UNLESS the field is
|
|
1270
1271
|
// `.fromBirth()` (birth-presence law — the ONE case the projection types non-null).
|
|
1271
|
-
const memberNullable = f.isFromBirth !== true;
|
|
1272
|
+
const memberNullable = admitsNull(f) || f.isFromBirth !== true;
|
|
1273
|
+
const memberValue = admitsNull(f) ? `${member}.valueOrNull` : member;
|
|
1272
1274
|
lines.push(
|
|
1273
1275
|
` /// Resolve \`${fieldName}\` (${agg.id}.${fieldName} -> ${targetType}) by dispatching to`,
|
|
1274
1276
|
` /// [${className}.${byIdMethod}] — the existing by-id read lane, never a second one.`,
|
|
1275
1277
|
` Future<${targetModel}?> ${method}(${className} client) async {`,
|
|
1276
1278
|
...(memberNullable
|
|
1277
1279
|
? [
|
|
1278
|
-
` final id = ${
|
|
1280
|
+
` final id = ${memberValue};`,
|
|
1279
1281
|
` if (id == null) return null;`,
|
|
1280
1282
|
` final rows = await client.${byIdMethod}(NmsInternalNomosRefs.id(id));`,
|
|
1281
1283
|
]
|
|
@@ -1301,7 +1303,8 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
1301
1303
|
const member = dartMemberName(fieldName);
|
|
1302
1304
|
const methodStem = member.length > 2 && member.endsWith("Id") ? member.slice(0, -2) : `${member}Ref`;
|
|
1303
1305
|
const method = safeMethod(methodStem);
|
|
1304
|
-
const memberNullable = f.isFromBirth !== true;
|
|
1306
|
+
const memberNullable = admitsNull(f) || f.isFromBirth !== true;
|
|
1307
|
+
const memberValue = admitsNull(f) ? `${member}.valueOrNull` : member;
|
|
1305
1308
|
lines.push(
|
|
1306
1309
|
` /// Resolve \`${fieldName}\` (${agg.id}.${fieldName} -> ${targetType}, a CROSS-DOMAIN ref) by`,
|
|
1307
1310
|
` /// dispatching to [${targetClient}.${byIdMethod}] on the target domain's client — same by-id`,
|
|
@@ -1310,7 +1313,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
1310
1313
|
` Future<${targetModel}?> ${method}(${targetClient} client) async {`,
|
|
1311
1314
|
...(memberNullable
|
|
1312
1315
|
? [
|
|
1313
|
-
` final id = ${
|
|
1316
|
+
` final id = ${memberValue};`,
|
|
1314
1317
|
` if (id == null) return null;`,
|
|
1315
1318
|
` final rows = await client.${byIdMethod}(NmsInternalNomosRefs.id(id));`,
|
|
1316
1319
|
]
|
|
@@ -1341,6 +1344,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
1341
1344
|
}
|
|
1342
1345
|
const targetConcrete = concreteClassName(targetType);
|
|
1343
1346
|
const member = dartMemberName(fieldName);
|
|
1347
|
+
const memberValue = admitsNull(f) ? `${member}.valueOrNull` : member;
|
|
1344
1348
|
// The stored locator member is a typed `ForeignRef<Target>?` getter, so the resolver takes a
|
|
1345
1349
|
// `remote…` name to avoid colliding with it (and to read as "the far side of the reference").
|
|
1346
1350
|
const method = safeMethod(`remote${cap(camel(member))}`);
|
|
@@ -1350,7 +1354,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
1350
1354
|
` /// implementation opens the foreign workspace on demand; tenant code sees only this business method.`,
|
|
1351
1355
|
` /// Null locator (absent / empty id or workspace) ⇒ null.`,
|
|
1352
1356
|
` Future<${targetConcrete}?> ${method}(NomosEngine engine, {String? cloud}) {`,
|
|
1353
|
-
` final ref = ${
|
|
1357
|
+
` final ref = ${memberValue};`,
|
|
1354
1358
|
` if (ref == null) return Future<${targetConcrete}?>.value(null);`,
|
|
1355
1359
|
` return NmsInternalForeignRefs.resolve(ref, engine.foreignReader(cloud: cloud));`,
|
|
1356
1360
|
` }`,
|
package/src/codegen_ts.ts
CHANGED
|
@@ -598,7 +598,7 @@ function tsTypeOfVoField(f: ValueObjectFieldDescriptor): string {
|
|
|
598
598
|
switch (f.type) {
|
|
599
599
|
case "string":
|
|
600
600
|
return f.idOf !== undefined
|
|
601
|
-
? `NomosRef<${f.idOf}>`
|
|
601
|
+
? `NomosRef<${f.idOfContract === true ? contractTypeName(f.idOf) : f.idOf}>`
|
|
602
602
|
: f.brand ?? "string"; // a brandedId(...) → the emitted branded alias
|
|
603
603
|
case "number":
|
|
604
604
|
return "number"; // `.int()`/units are Dart-side nominal products; TS keeps one number
|
package/src/fields.ts
CHANGED
|
@@ -171,7 +171,14 @@ export type ValueObjectFieldDescriptor =
|
|
|
171
171
|
/** A plain string, OR a BRANDED nominal string when `brand` is present (a `brandedId(...)`
|
|
172
172
|
* — the generated TS branded alias / Dart extension-type wrapper's name). Wire-identical
|
|
173
173
|
* to a plain string; the brand is a compile-time nominal tag only. */
|
|
174
|
-
| {
|
|
174
|
+
| {
|
|
175
|
+
readonly type: "string";
|
|
176
|
+
readonly brand?: string;
|
|
177
|
+
readonly idOf?: string;
|
|
178
|
+
/** Present only for an `idOf(recordContract(...))` target. Aggregate is the default. */
|
|
179
|
+
readonly idOfContract?: true;
|
|
180
|
+
readonly optional?: true;
|
|
181
|
+
}
|
|
175
182
|
/** `int: true` when the zod carried `.int()` (Dart `int`, TS `number`); `unit` when the field
|
|
176
183
|
* was declared through `unit(...)` — the generated Dart extension-type wrapper's name. */
|
|
177
184
|
| { readonly type: "number"; readonly int?: true; readonly unit?: string; readonly optional?: true }
|
package/src/law_check.ts
CHANGED
|
@@ -729,7 +729,7 @@ function dottedQueryKeyFindings(mod: DomainModule, aggById: ReadonlyMap<string,
|
|
|
729
729
|
// admits before a null is lawful (t.optional / t.clearable / t.nullable / t.json({null})). These
|
|
730
730
|
// twins pre-fire the three refusals the gate would raise: (a) a payload field that ADMITS null with
|
|
731
731
|
// no null-admitting receiver; (b) a null-admitting field used where a null is structurally illegal
|
|
732
|
-
// (authority gate,
|
|
732
|
+
// (authority gate, spatial index, read key); (c) a plan op writing literal null into a
|
|
733
733
|
// field that never declared null a meaning. All PAYLOAD-INDEPENDENT (pure declarations) ⇒ REFUSE.
|
|
734
734
|
|
|
735
735
|
/** The verbatim null-semantics guidance (`null_semantics.md`) — the `fix` on twin N-a/N-c. */
|
|
@@ -843,7 +843,10 @@ function nullBanFindings(mod: DomainModule, aggById: ReadonlyMap<string, Aggrega
|
|
|
843
843
|
});
|
|
844
844
|
};
|
|
845
845
|
|
|
846
|
-
// Facet gates (authority/standing)
|
|
846
|
+
// Facet gates (authority/standing) — a null in these breaks the very thing they decide. A plain
|
|
847
|
+
// typed ref is an association, not structural identity: clearing `asset.spaceId`, for example,
|
|
848
|
+
// means the asset is no longer in a space. If a ref participates in a query/group/spatial key the
|
|
849
|
+
// corresponding structural check below still refuses it.
|
|
847
850
|
for (const agg of mod.aggregates) {
|
|
848
851
|
for (const [name, f0] of Object.entries(agg.fields)) {
|
|
849
852
|
const f = f0 as Field;
|
|
@@ -851,9 +854,6 @@ function nullBanFindings(mod: DomainModule, aggById: ReadonlyMap<string, Aggrega
|
|
|
851
854
|
if (f.facet === "authority" || f.facet === "standing") {
|
|
852
855
|
ban(agg.id, name, "an authority/standing gate", "drop the null semantic — an authority sink must always resolve to a subject.");
|
|
853
856
|
}
|
|
854
|
-
if (f.kind === "ref") {
|
|
855
|
-
ban(agg.id, name, "an identity reference", "an identity ref must always point somewhere — model absence with a separate presence flag, not a null ref.");
|
|
856
|
-
}
|
|
857
857
|
}
|
|
858
858
|
}
|
|
859
859
|
// Read keys / group-bys / spatial indexes — a null key row is a silent dead index.
|
package/src/value_object.ts
CHANGED
|
@@ -396,12 +396,17 @@ function describeZod(
|
|
|
396
396
|
switch (zodKind(schema)) {
|
|
397
397
|
case "string": {
|
|
398
398
|
const brandedBrand = (schema as unknown as Record<symbol, unknown>)[NOMOS_BRANDED_ID_BRAND];
|
|
399
|
-
const
|
|
399
|
+
const idOfTarget = idOfMetaOf(schema);
|
|
400
400
|
// idOf(Agg) REFERENCES the aggregate's canonical `NomosRef<Agg>`. Carry `idOf` in the stable
|
|
401
401
|
// descriptor so every emitter can recover the aggregate type while the wire remains a string.
|
|
402
402
|
// A standalone `brandedId("X")` remains an unrelated nominal string and declares its own wrapper.
|
|
403
|
-
if (
|
|
404
|
-
return {
|
|
403
|
+
if (idOfTarget !== undefined) {
|
|
404
|
+
return {
|
|
405
|
+
type: "string",
|
|
406
|
+
brand: `${idOfTarget}Id`,
|
|
407
|
+
idOf: idOfTarget,
|
|
408
|
+
...(idOfTargetKindOf(schema) === "contract" ? { idOfContract: true as const } : {}),
|
|
409
|
+
};
|
|
405
410
|
}
|
|
406
411
|
return typeof brandedBrand === "string" ? { type: "string", brand: brandedBrand } : { type: "string" };
|
|
407
412
|
}
|