@prisma-next/sql-contract-psl 0.12.0-dev.6 → 0.12.0-dev.60
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/README.md +10 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{interpreter-QE6eZZof.mjs → interpreter-1VmrYYbi.mjs} +373 -23
- package/dist/interpreter-1VmrYYbi.mjs.map +1 -0
- package/dist/provider.d.mts +5 -0
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +5 -3
- package/dist/provider.mjs.map +1 -1
- package/package.json +14 -14
- package/src/interpreter.ts +442 -20
- package/src/provider.ts +10 -1
- package/src/psl-attribute-parsing.ts +66 -0
- package/src/psl-column-resolution.ts +10 -3
- package/src/psl-field-resolution.ts +6 -0
- package/src/psl-relation-resolution.ts +3 -4
- package/dist/interpreter-QE6eZZof.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ This keeps core/CLI source-agnostic while giving PSL-first SQL users a one-line
|
|
|
14
14
|
## Responsibilities
|
|
15
15
|
|
|
16
16
|
- Interpret `ParsePslDocumentResult` into SQL `Contract`
|
|
17
|
-
- Interpret generic PSL attributes into SQL contract semantics (`@id`, `@unique`, `@default`, `@relation`, `@map`, `@@map`)
|
|
17
|
+
- Interpret generic PSL attributes into SQL contract semantics (`@id`, `@unique`, `@default`, `@relation`, `@map`, `@@map`, `@@control`)
|
|
18
18
|
- Interpret SQL timestamp semantics: `DateTime @default(now())` (or the equivalent `temporal.createdAt()` field-preset call) as a storage default, and `temporal.updatedAt()` as an execution mutation default
|
|
19
19
|
- Lower shared constructor expressions in both `types {}` blocks and inline field positions (for example `ShortName = sql.String(length: 35)` and `embedding pgvector.Vector(length: 1536)?`)
|
|
20
20
|
- Lower supported default functions through composed registry inputs
|
|
@@ -71,12 +71,20 @@ Supported timestamp authoring surface:
|
|
|
71
71
|
- The Prisma-flavored `@updatedAt` attribute is not supported; references produce `PSL_UNSUPPORTED_FIELD_ATTRIBUTE` with a migration hint pointing at `temporal.updatedAt()`. The hint is suppressed when the field already declares any `temporal.*` preset.
|
|
72
72
|
- `@createdAt` is not supported as a PSL alias.
|
|
73
73
|
|
|
74
|
+
Model-level control policy:
|
|
75
|
+
|
|
76
|
+
- `@@control(<policy>)` lowers to the storage table's `control` field. The argument is one positional lowercase literal: `managed`, `tolerated`, `external`, or `observed`. Omit `@@control` to leave per-table control unset (the framework default applies at runtime).
|
|
77
|
+
|
|
78
|
+
Contract-level default (specifier options bag):
|
|
79
|
+
|
|
80
|
+
- `defaultControlPolicy` on `prismaContract(...)` sets `Contract.defaultControlPolicy` at load time when the interpreted contract does not already define one (source wins when both are present).
|
|
81
|
+
|
|
74
82
|
## Public API
|
|
75
83
|
|
|
76
84
|
- `@prisma-next/sql-contract-psl`
|
|
77
85
|
- `interpretPslDocumentToSqlContract({ document, target, scalarTypeDescriptors, authoringContributions?, controlMutationDefaults?, composedExtensionPacks? })`
|
|
78
86
|
- `@prisma-next/sql-contract-psl/provider`
|
|
79
|
-
- `prismaContract(schemaPath, { output?, target, scalarTypeDescriptors, authoringContributions?, controlMutationDefaults?, composedExtensionPacks? })`
|
|
87
|
+
- `prismaContract(schemaPath, { output?, target, defaultControlPolicy?, scalarTypeDescriptors, authoringContributions?, controlMutationDefaults?, composedExtensionPacks? })`
|
|
80
88
|
- Provider input is fully preassembled by composition layers (for example `@prisma-next/family-sql/control` helpers).
|
|
81
89
|
|
|
82
90
|
## Dependencies
|
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Contract } from "@prisma-next/contract/types";
|
|
2
2
|
import { AuthoringContributions } from "@prisma-next/framework-components/authoring";
|
|
3
|
-
import { Namespace } from "@prisma-next/framework-components/ir";
|
|
4
3
|
import { SqlNamespaceTablesInput } from "@prisma-next/sql-contract/types";
|
|
5
4
|
import { Result } from "@prisma-next/utils/result";
|
|
6
5
|
import { ParsePslDocumentResult } from "@prisma-next/psl-parser";
|
|
7
6
|
import { ControlMutationDefaults, ControlMutationDefaults as ControlMutationDefaults$1, DefaultFunctionLoweringContext, DefaultFunctionLoweringHandler, DefaultFunctionRegistry, DefaultFunctionRegistryEntry, MutationDefaultGeneratorDescriptor } from "@prisma-next/framework-components/control";
|
|
8
7
|
import { ContractSourceDiagnostics } from "@prisma-next/config/config-types";
|
|
9
8
|
import { ExtensionPackRef, TargetPackRef } from "@prisma-next/framework-components/components";
|
|
9
|
+
import { Namespace } from "@prisma-next/framework-components/ir";
|
|
10
10
|
|
|
11
11
|
//#region src/psl-column-resolution.d.ts
|
|
12
12
|
type ColumnDescriptor = {
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/psl-column-resolution.ts","../src/interpreter.ts"],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/psl-column-resolution.ts","../src/interpreter.ts"],"mappings":";;;;;;;;;;;KA0CY,gBAAA;EAAA,SACD,OAAA;EAAA,SACA,UAAA;EAAA,SACA,OAAA;EAAA,SACA,UAAA,GAAa,MAAM;AAAA;;;UCkDb,sCAAA;EAAA,SACN,QAAA,EAAU,sBAAA;EAAA,SACV,MAAA,EAAQ,aAAA;EAAA,SACR,qBAAA,EAAuB,WAAA,SAAoB,gBAAA;EAAA,SAC3C,sBAAA;EAAA,SACA,yBAAA,YAAqC,gBAAA;EAAA,SACrC,uBAAA,GAA0B,yBAAA;EAAA,SAC1B,sBAAA,GAAyB,sBAAA;EDzDzB;;;AAAmB;;;;ACkD9B;;EDlDW,SCmEA,eAAA,IAAmB,KAAA,EAAO,uBAAA,KAA4B,SAAA;AAAA;AAAA,iBAgqDjD,iCAAA,CACd,KAAA,EAAO,sCAAA,GACN,MAAA,CAAO,QAAA,EAAU,yBAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as interpretPslDocumentToSqlContract } from "./interpreter-
|
|
1
|
+
import { t as interpretPslDocumentToSqlContract } from "./interpreter-1VmrYYbi.mjs";
|
|
2
2
|
export { interpretPslDocumentToSqlContract };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { crossRef } from "@prisma-next/contract/types";
|
|
2
2
|
import { hasRegisteredFieldNamespace, instantiateAuthoringEntityType, instantiateAuthoringFieldPreset, instantiateAuthoringTypeConstructor, isAuthoringEntityTypeDescriptor, isAuthoringFieldPresetDescriptor, isAuthoringTypeConstructorDescriptor, validateAuthoringHelperArguments } from "@prisma-next/framework-components/authoring";
|
|
3
|
-
import { UNBOUND_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
|
|
4
3
|
import { isPostgresEnumStorageEntry } from "@prisma-next/sql-contract/types";
|
|
5
4
|
import { buildSqlContractFromDefinition } from "@prisma-next/sql-contract-ts/contract-builder";
|
|
5
|
+
import { blindCast } from "@prisma-next/utils/casts";
|
|
6
6
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
7
7
|
import { notOk, ok } from "@prisma-next/utils/result";
|
|
8
8
|
import { getPositionalArgument, parseQuotedStringLiteral } from "@prisma-next/psl-parser";
|
|
@@ -279,6 +279,56 @@ function findDuplicateFieldName(fieldNames) {
|
|
|
279
279
|
seen.add(name);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
+
const CONTROL_POLICY_LITERAL_SET = new Set([
|
|
283
|
+
"managed",
|
|
284
|
+
"tolerated",
|
|
285
|
+
"external",
|
|
286
|
+
"observed"
|
|
287
|
+
]);
|
|
288
|
+
function isControlPolicyLiteral(value) {
|
|
289
|
+
return CONTROL_POLICY_LITERAL_SET.has(value);
|
|
290
|
+
}
|
|
291
|
+
function parseControlPolicyAttribute(input) {
|
|
292
|
+
if (input.attribute.args.filter((arg) => arg.kind === "named").length > 0) {
|
|
293
|
+
input.diagnostics.push({
|
|
294
|
+
code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
|
|
295
|
+
message: "`@@control` does not accept named arguments; pass the policy positionally as `@@control(external)`.",
|
|
296
|
+
sourceId: input.sourceId,
|
|
297
|
+
span: input.attribute.span
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const positionalArgs = getPositionalArguments(input.attribute);
|
|
302
|
+
if (positionalArgs.length === 0) {
|
|
303
|
+
input.diagnostics.push({
|
|
304
|
+
code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
|
|
305
|
+
message: "`@@control` requires exactly one positional argument: `managed`, `tolerated`, `external`, or `observed`.",
|
|
306
|
+
sourceId: input.sourceId,
|
|
307
|
+
span: input.attribute.span
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (positionalArgs.length > 1) {
|
|
312
|
+
input.diagnostics.push({
|
|
313
|
+
code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
|
|
314
|
+
message: `\`@@control\` accepts exactly one positional argument; got ${positionalArgs.length}.`,
|
|
315
|
+
sourceId: input.sourceId,
|
|
316
|
+
span: input.attribute.span
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const token = unquoteStringLiteral(positionalArgs[0] ?? "").trim();
|
|
321
|
+
if (!isControlPolicyLiteral(token)) {
|
|
322
|
+
input.diagnostics.push({
|
|
323
|
+
code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
|
|
324
|
+
message: `\`@@control\` argument \`${token}\` is not a known policy. Allowed: \`managed\`, \`tolerated\`, \`external\`, \`observed\`.`,
|
|
325
|
+
sourceId: input.sourceId,
|
|
326
|
+
span: input.attribute.span
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
return token;
|
|
331
|
+
}
|
|
282
332
|
function mapFieldNamesToColumns(input) {
|
|
283
333
|
const columns = [];
|
|
284
334
|
for (const fieldName of input.fieldNames) {
|
|
@@ -730,7 +780,7 @@ function getAuthoringTypeConstructor(contributions, path) {
|
|
|
730
780
|
let current = contributions?.type;
|
|
731
781
|
for (const segment of path) {
|
|
732
782
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return;
|
|
733
|
-
current = current[segment];
|
|
783
|
+
current = blindCast(current)[segment];
|
|
734
784
|
}
|
|
735
785
|
return isAuthoringTypeConstructorDescriptor(current) ? current : void 0;
|
|
736
786
|
}
|
|
@@ -749,7 +799,7 @@ function getAuthoringEntity(contributions, path) {
|
|
|
749
799
|
let current = contributions?.entityTypes;
|
|
750
800
|
for (const segment of path) {
|
|
751
801
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return;
|
|
752
|
-
current = current[segment];
|
|
802
|
+
current = blindCast(current)[segment];
|
|
753
803
|
}
|
|
754
804
|
return isAuthoringEntityTypeDescriptor(current) ? current : void 0;
|
|
755
805
|
}
|
|
@@ -762,7 +812,7 @@ function getAuthoringFieldPreset(contributions, path) {
|
|
|
762
812
|
let current = contributions?.field;
|
|
763
813
|
for (const segment of path) {
|
|
764
814
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return;
|
|
765
|
-
current = current[segment];
|
|
815
|
+
current = blindCast(current)[segment];
|
|
766
816
|
}
|
|
767
817
|
return isAuthoringFieldPresetDescriptor(current) ? current : void 0;
|
|
768
818
|
}
|
|
@@ -1344,6 +1394,7 @@ function collectResolvedFields(input) {
|
|
|
1344
1394
|
});
|
|
1345
1395
|
const relationAttribute = getAttribute(field.attributes, "relation");
|
|
1346
1396
|
if (isModelField && relationAttribute) continue;
|
|
1397
|
+
if (field.typeContractSpaceId !== void 0 && relationAttribute) continue;
|
|
1347
1398
|
const isValueObjectField = compositeTypeNames.has(field.typeName);
|
|
1348
1399
|
const isListField = field.list;
|
|
1349
1400
|
let descriptor;
|
|
@@ -1852,9 +1903,6 @@ const UNSPECIFIED_PSL_NAMESPACE_NAME = "__unspecified__";
|
|
|
1852
1903
|
* slot empty (which means the late-bound default at the `StorageTable`
|
|
1853
1904
|
* layer; emitted JSON omits the field).
|
|
1854
1905
|
*/
|
|
1855
|
-
function defaultSqlNamespaceIdForTarget(targetId) {
|
|
1856
|
-
return targetId === "postgres" ? "public" : UNBOUND_NAMESPACE_ID;
|
|
1857
|
-
}
|
|
1858
1906
|
function resolveNamespaceIdForSqlTarget(input) {
|
|
1859
1907
|
if (input.targetId !== "postgres") return;
|
|
1860
1908
|
if (input.bucketName === UNSPECIFIED_PSL_NAMESPACE_NAME) return "public";
|
|
@@ -2132,6 +2180,8 @@ function buildModelNodeFromPsl(input) {
|
|
|
2132
2180
|
} : void 0;
|
|
2133
2181
|
const hasInlinePrimaryKey = primaryKey !== void 0;
|
|
2134
2182
|
let blockPrimaryKeyDeclared = false;
|
|
2183
|
+
let controlPolicyDeclared = false;
|
|
2184
|
+
let controlPolicy;
|
|
2135
2185
|
const resultBackrelationCandidates = [];
|
|
2136
2186
|
for (const field of model.fields) {
|
|
2137
2187
|
if (!field.list || !input.modelNames.has(field.typeName)) continue;
|
|
@@ -2198,6 +2248,25 @@ function buildModelNodeFromPsl(input) {
|
|
|
2198
2248
|
for (const modelAttribute of model.attributes) {
|
|
2199
2249
|
if (modelAttribute.name === "map") continue;
|
|
2200
2250
|
if (modelAttribute.name === "discriminator" || modelAttribute.name === "base") continue;
|
|
2251
|
+
if (modelAttribute.name === "control") {
|
|
2252
|
+
if (controlPolicyDeclared) {
|
|
2253
|
+
diagnostics.push({
|
|
2254
|
+
code: "PSL_DUPLICATE_ATTRIBUTE",
|
|
2255
|
+
message: `\`@@control\` declared more than once on model "${model.name}".`,
|
|
2256
|
+
sourceId,
|
|
2257
|
+
span: modelAttribute.span
|
|
2258
|
+
});
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2261
|
+
controlPolicyDeclared = true;
|
|
2262
|
+
const parsed = parseControlPolicyAttribute({
|
|
2263
|
+
attribute: modelAttribute,
|
|
2264
|
+
sourceId,
|
|
2265
|
+
diagnostics
|
|
2266
|
+
});
|
|
2267
|
+
if (parsed !== void 0) controlPolicy = parsed;
|
|
2268
|
+
continue;
|
|
2269
|
+
}
|
|
2201
2270
|
const attributeLabel = `Model "${model.name}" @@${modelAttribute.name}`;
|
|
2202
2271
|
if (modelAttribute.name === "id") {
|
|
2203
2272
|
if (blockPrimaryKeyDeclared) {
|
|
@@ -2383,9 +2452,118 @@ function buildModelNodeFromPsl(input) {
|
|
|
2383
2452
|
});
|
|
2384
2453
|
}
|
|
2385
2454
|
const resultFkRelationMetadata = [];
|
|
2455
|
+
const resultCrossSpaceRelations = [];
|
|
2386
2456
|
for (const relationAttribute of relationAttributes) {
|
|
2387
|
-
|
|
2388
|
-
|
|
2457
|
+
const { typeName: fieldTypeName, typeNamespaceId: fieldTypeNamespaceId, typeContractSpaceId: fieldTypeContractSpaceId } = relationAttribute.field;
|
|
2458
|
+
if (relationAttribute.field.list) {
|
|
2459
|
+
if (fieldTypeContractSpaceId !== void 0) diagnostics.push({
|
|
2460
|
+
code: "PSL_UNSUPPORTED_CROSS_SPACE_LIST",
|
|
2461
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" is a cross-space list relation (type "${fieldTypeContractSpaceId}:${fieldTypeNamespaceId !== void 0 ? `${fieldTypeNamespaceId}.` : ""}${fieldTypeName}[]"). Cross-space relations must be singular in v0.1 — list cross-space relations are not supported.`,
|
|
2462
|
+
sourceId,
|
|
2463
|
+
span: relationAttribute.field.span
|
|
2464
|
+
});
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
if (fieldTypeContractSpaceId !== void 0) {
|
|
2468
|
+
if (!input.composedExtensions.has(fieldTypeContractSpaceId)) {
|
|
2469
|
+
diagnostics.push({
|
|
2470
|
+
code: "PSL_UNKNOWN_CONTRACT_SPACE",
|
|
2471
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" references contract space "${fieldTypeContractSpaceId}" which is not declared in extensionPacks. Add "${fieldTypeContractSpaceId}" to extensionPacks in prisma-next.config.ts.`,
|
|
2472
|
+
sourceId,
|
|
2473
|
+
span: relationAttribute.field.span,
|
|
2474
|
+
data: {
|
|
2475
|
+
space: fieldTypeContractSpaceId,
|
|
2476
|
+
suggestedPack: fieldTypeContractSpaceId
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
const parsedRelation = parseRelationAttribute({
|
|
2482
|
+
attribute: relationAttribute.relation,
|
|
2483
|
+
modelName: model.name,
|
|
2484
|
+
fieldName: relationAttribute.field.name,
|
|
2485
|
+
sourceId,
|
|
2486
|
+
diagnostics
|
|
2487
|
+
});
|
|
2488
|
+
if (!parsedRelation) continue;
|
|
2489
|
+
if (!parsedRelation.fields || !parsedRelation.references) {
|
|
2490
|
+
diagnostics.push({
|
|
2491
|
+
code: "PSL_INVALID_RELATION_ATTRIBUTE",
|
|
2492
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" requires fields and references arguments`,
|
|
2493
|
+
sourceId,
|
|
2494
|
+
span: relationAttribute.relation.span
|
|
2495
|
+
});
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
const localColumns = mapFieldNamesToColumns({
|
|
2499
|
+
modelName: model.name,
|
|
2500
|
+
fieldNames: parsedRelation.fields,
|
|
2501
|
+
mapping,
|
|
2502
|
+
sourceId,
|
|
2503
|
+
diagnostics,
|
|
2504
|
+
span: relationAttribute.relation.span,
|
|
2505
|
+
entityLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`
|
|
2506
|
+
});
|
|
2507
|
+
if (!localColumns) continue;
|
|
2508
|
+
const referencedColumns = parsedRelation.references;
|
|
2509
|
+
if (localColumns.length !== referencedColumns.length) {
|
|
2510
|
+
diagnostics.push({
|
|
2511
|
+
code: "PSL_INVALID_RELATION_ATTRIBUTE",
|
|
2512
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" must provide the same number of fields and references`,
|
|
2513
|
+
sourceId,
|
|
2514
|
+
span: relationAttribute.relation.span
|
|
2515
|
+
});
|
|
2516
|
+
continue;
|
|
2517
|
+
}
|
|
2518
|
+
const onDelete = parsedRelation.onDelete ? normalizeReferentialAction({
|
|
2519
|
+
modelName: model.name,
|
|
2520
|
+
fieldName: relationAttribute.field.name,
|
|
2521
|
+
actionName: "onDelete",
|
|
2522
|
+
actionToken: parsedRelation.onDelete,
|
|
2523
|
+
sourceId,
|
|
2524
|
+
span: relationAttribute.field.span,
|
|
2525
|
+
diagnostics
|
|
2526
|
+
}) : void 0;
|
|
2527
|
+
const onUpdate = parsedRelation.onUpdate ? normalizeReferentialAction({
|
|
2528
|
+
modelName: model.name,
|
|
2529
|
+
fieldName: relationAttribute.field.name,
|
|
2530
|
+
actionName: "onUpdate",
|
|
2531
|
+
actionToken: parsedRelation.onUpdate,
|
|
2532
|
+
sourceId,
|
|
2533
|
+
span: relationAttribute.field.span,
|
|
2534
|
+
diagnostics
|
|
2535
|
+
}) : void 0;
|
|
2536
|
+
const crossTargetNamespaceId = fieldTypeNamespaceId ?? "__unbound__";
|
|
2537
|
+
const crossTargetTableName = fieldTypeName.toLowerCase();
|
|
2538
|
+
foreignKeyNodes.push({
|
|
2539
|
+
columns: localColumns,
|
|
2540
|
+
references: {
|
|
2541
|
+
model: fieldTypeName,
|
|
2542
|
+
table: crossTargetTableName,
|
|
2543
|
+
columns: referencedColumns,
|
|
2544
|
+
namespaceId: crossTargetNamespaceId,
|
|
2545
|
+
spaceId: fieldTypeContractSpaceId
|
|
2546
|
+
},
|
|
2547
|
+
...ifDefined("name", parsedRelation.constraintName),
|
|
2548
|
+
...ifDefined("onDelete", onDelete),
|
|
2549
|
+
...ifDefined("onUpdate", onUpdate)
|
|
2550
|
+
});
|
|
2551
|
+
resultCrossSpaceRelations.push({
|
|
2552
|
+
fieldName: relationAttribute.field.name,
|
|
2553
|
+
toModel: fieldTypeName,
|
|
2554
|
+
toTable: crossTargetTableName,
|
|
2555
|
+
cardinality: "N:1",
|
|
2556
|
+
spaceId: fieldTypeContractSpaceId,
|
|
2557
|
+
namespaceId: crossTargetNamespaceId,
|
|
2558
|
+
on: {
|
|
2559
|
+
parentTable: tableName,
|
|
2560
|
+
parentColumns: localColumns,
|
|
2561
|
+
childTable: crossTargetTableName,
|
|
2562
|
+
childColumns: referencedColumns
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
continue;
|
|
2566
|
+
}
|
|
2389
2567
|
const qualifiedTypeName = fieldTypeNamespaceId ? `${fieldTypeNamespaceId}.${fieldTypeName}` : fieldTypeName;
|
|
2390
2568
|
if (!input.modelNames.has(fieldTypeName)) {
|
|
2391
2569
|
diagnostics.push({
|
|
@@ -2520,9 +2698,11 @@ function buildModelNodeFromPsl(input) {
|
|
|
2520
2698
|
...ifDefined("id", primaryKey),
|
|
2521
2699
|
...uniqueConstraints.length > 0 ? { uniques: uniqueConstraints } : {},
|
|
2522
2700
|
...indexNodes.length > 0 ? { indexes: indexNodes } : {},
|
|
2523
|
-
...foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {}
|
|
2701
|
+
...foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {},
|
|
2702
|
+
...ifDefined("control", controlPolicy)
|
|
2524
2703
|
},
|
|
2525
2704
|
fkRelationMetadata: resultFkRelationMetadata,
|
|
2705
|
+
crossSpaceRelations: resultCrossSpaceRelations,
|
|
2526
2706
|
backrelationCandidates: resultBackrelationCandidates,
|
|
2527
2707
|
resolvedFields
|
|
2528
2708
|
};
|
|
@@ -2688,8 +2868,16 @@ function collectPolymorphismDeclarations(models, sourceId, diagnostics) {
|
|
|
2688
2868
|
baseDeclarations
|
|
2689
2869
|
};
|
|
2690
2870
|
}
|
|
2691
|
-
function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds,
|
|
2871
|
+
function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, defaultNamespaceId, syntheticPkFieldsByVariant, stiBaseFieldsByBase, sourceId, diagnostics) {
|
|
2692
2872
|
let patched = models;
|
|
2873
|
+
for (const [baseName, fieldNames] of stiBaseFieldsByBase) {
|
|
2874
|
+
const baseModel = patched[baseName];
|
|
2875
|
+
if (!baseModel || fieldNames.length === 0) continue;
|
|
2876
|
+
patched = {
|
|
2877
|
+
...patched,
|
|
2878
|
+
[baseName]: stripStorageOnlyDomainFields(baseModel, fieldNames)
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2693
2881
|
for (const [modelName, decl] of discriminatorDeclarations) {
|
|
2694
2882
|
if (baseDeclarations.has(modelName)) {
|
|
2695
2883
|
diagnostics.push({
|
|
@@ -2771,20 +2959,168 @@ function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations
|
|
|
2771
2959
|
const baseMapping = modelMappings.get(baseDecl.baseName);
|
|
2772
2960
|
const variantMapping = modelMappings.get(variantName);
|
|
2773
2961
|
const resolvedTable = variantMapping?.model.attributes.some((attr) => attr.name === "map") ?? false ? variantMapping?.tableName : baseMapping?.tableName;
|
|
2962
|
+
const patchedVariant = {
|
|
2963
|
+
...variantModel,
|
|
2964
|
+
base: crossRef(baseDecl.baseName, modelNamespaceIds.get(baseDecl.baseName) ?? defaultNamespaceId),
|
|
2965
|
+
...resolvedTable ? { storage: {
|
|
2966
|
+
...variantModel.storage,
|
|
2967
|
+
table: resolvedTable
|
|
2968
|
+
} } : {}
|
|
2969
|
+
};
|
|
2774
2970
|
patched = {
|
|
2775
2971
|
...patched,
|
|
2776
|
-
[variantName]:
|
|
2777
|
-
...variantModel,
|
|
2778
|
-
base: crossRef(baseDecl.baseName, modelNamespaceIds.get(baseDecl.baseName) ?? defaultSqlNamespaceIdForTarget(targetId)),
|
|
2779
|
-
...resolvedTable ? { storage: {
|
|
2780
|
-
...variantModel.storage,
|
|
2781
|
-
table: resolvedTable
|
|
2782
|
-
} } : {}
|
|
2783
|
-
}
|
|
2972
|
+
[variantName]: stripStorageOnlyDomainFields(patchedVariant, syntheticPkFieldsByVariant.get(variantName) ?? [])
|
|
2784
2973
|
};
|
|
2785
2974
|
}
|
|
2786
2975
|
return patched;
|
|
2787
2976
|
}
|
|
2977
|
+
/**
|
|
2978
|
+
* Multi-table-inheritance variants (`@@base` + their own `@@map`) live in a
|
|
2979
|
+
* separate table from their base. The ORM joins that table to the base on the
|
|
2980
|
+
* shared primary key (`base.id = variant.id`), so the variant storage table
|
|
2981
|
+
* must carry the base PK column even though the variant domain model declares
|
|
2982
|
+
* only its own fields. This enriches each MTI variant's `ModelNode` with that
|
|
2983
|
+
* link column, a primary key on it, and a FK back to the base table.
|
|
2984
|
+
*
|
|
2985
|
+
* The link column is reported back per variant in `syntheticPkFieldsByVariant`
|
|
2986
|
+
* so the domain-model patch can drop it again — keeping the variant's domain
|
|
2987
|
+
* surface thin (its create/read inputs don't gain a redundant `id`) while the
|
|
2988
|
+
* storage table stays joinable. Single-table-inheritance variants (no own
|
|
2989
|
+
* table) are left untouched.
|
|
2990
|
+
*/
|
|
2991
|
+
function materializeMtiVariantStorageLinks(modelNodes, baseDeclarations, stiVariantNames) {
|
|
2992
|
+
const nodeByModel = new Map(modelNodes.map((node) => [node.modelName, node]));
|
|
2993
|
+
const syntheticPkFieldsByVariant = /* @__PURE__ */ new Map();
|
|
2994
|
+
return {
|
|
2995
|
+
modelNodes: modelNodes.map((node) => {
|
|
2996
|
+
const baseDecl = baseDeclarations.get(node.modelName);
|
|
2997
|
+
if (!baseDecl) return node;
|
|
2998
|
+
const baseNode = nodeByModel.get(baseDecl.baseName);
|
|
2999
|
+
if (!baseNode) return node;
|
|
3000
|
+
if (stiVariantNames.has(node.modelName)) return node;
|
|
3001
|
+
const basePrimaryKey = baseNode.id;
|
|
3002
|
+
if (!basePrimaryKey || basePrimaryKey.columns.length === 0) return node;
|
|
3003
|
+
const existingColumns = new Set(node.fields.map((field) => field.columnName));
|
|
3004
|
+
const linkFields = [];
|
|
3005
|
+
for (const pkColumn of basePrimaryKey.columns) {
|
|
3006
|
+
if (existingColumns.has(pkColumn)) continue;
|
|
3007
|
+
const baseField = baseNode.fields.find((field) => "descriptor" in field && field.columnName === pkColumn);
|
|
3008
|
+
if (!baseField) continue;
|
|
3009
|
+
linkFields.push({
|
|
3010
|
+
fieldName: baseField.fieldName,
|
|
3011
|
+
columnName: pkColumn,
|
|
3012
|
+
descriptor: baseField.descriptor,
|
|
3013
|
+
nullable: false
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
if (linkFields.length === 0) return node;
|
|
3017
|
+
syntheticPkFieldsByVariant.set(node.modelName, linkFields.map((field) => field.fieldName));
|
|
3018
|
+
const foreignKey = {
|
|
3019
|
+
columns: basePrimaryKey.columns,
|
|
3020
|
+
references: {
|
|
3021
|
+
model: baseNode.modelName,
|
|
3022
|
+
table: baseNode.tableName,
|
|
3023
|
+
columns: basePrimaryKey.columns,
|
|
3024
|
+
...ifDefined("namespaceId", baseNode.namespaceId)
|
|
3025
|
+
},
|
|
3026
|
+
constraint: true,
|
|
3027
|
+
index: false,
|
|
3028
|
+
onDelete: "cascade"
|
|
3029
|
+
};
|
|
3030
|
+
return {
|
|
3031
|
+
...node,
|
|
3032
|
+
fields: [...linkFields, ...node.fields],
|
|
3033
|
+
id: { columns: basePrimaryKey.columns },
|
|
3034
|
+
foreignKeys: [...node.foreignKeys ?? [], foreignKey]
|
|
3035
|
+
};
|
|
3036
|
+
}),
|
|
3037
|
+
syntheticPkFieldsByVariant
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
/**
|
|
3041
|
+
* Single-table-inheritance variants (`@@base` with no own `@@map`) share the
|
|
3042
|
+
* base table: `resolvePolymorphism` points the variant's `storage.table` at the
|
|
3043
|
+
* base, and the ORM reads variant-declared fields straight off the base table.
|
|
3044
|
+
* For that to validate and round-trip, the base storage table must physically
|
|
3045
|
+
* carry every STI variant's declared columns. This enriches the base
|
|
3046
|
+
* `ModelNode` with those columns.
|
|
3047
|
+
*
|
|
3048
|
+
* The materialised columns are always nullable in storage: the base table hosts
|
|
3049
|
+
* every variant's rows, so a column a variant declares as required is still
|
|
3050
|
+
* NULL on sibling-variant rows. The variant's domain field keeps its declared
|
|
3051
|
+
* nullability — required-in-domain / nullable-in-storage is the intended STI
|
|
3052
|
+
* shape.
|
|
3053
|
+
*
|
|
3054
|
+
* Collisions (two variants declaring the same column, or a variant column name
|
|
3055
|
+
* clashing with a base column) are resolved skip-if-exists here, mirroring the
|
|
3056
|
+
* MTI link guard; surfacing them as diagnostics is tracked separately
|
|
3057
|
+
* (TML-2827).
|
|
3058
|
+
*/
|
|
3059
|
+
function materializeStiVariantStorageColumns(modelNodes, baseDeclarations, stiVariantNames) {
|
|
3060
|
+
if (stiVariantNames.size === 0) return {
|
|
3061
|
+
modelNodes: [...modelNodes],
|
|
3062
|
+
stiBaseFieldsByBase: /* @__PURE__ */ new Map()
|
|
3063
|
+
};
|
|
3064
|
+
const nodeByModel = new Map(modelNodes.map((node) => [node.modelName, node]));
|
|
3065
|
+
const stiColumnsByBase = /* @__PURE__ */ new Map();
|
|
3066
|
+
for (const variantName of stiVariantNames) {
|
|
3067
|
+
const variantNode = nodeByModel.get(variantName);
|
|
3068
|
+
const baseDecl = baseDeclarations.get(variantName);
|
|
3069
|
+
if (!variantNode || !baseDecl) continue;
|
|
3070
|
+
const baseNode = nodeByModel.get(baseDecl.baseName);
|
|
3071
|
+
if (!baseNode) continue;
|
|
3072
|
+
const baseColumns = new Set(baseNode.fields.map((field) => field.columnName));
|
|
3073
|
+
const claimed = stiColumnsByBase.get(baseDecl.baseName) ?? [];
|
|
3074
|
+
const claimedColumns = new Set(claimed.map((field) => field.columnName));
|
|
3075
|
+
for (const field of variantNode.fields) {
|
|
3076
|
+
if (baseColumns.has(field.columnName) || claimedColumns.has(field.columnName)) continue;
|
|
3077
|
+
claimedColumns.add(field.columnName);
|
|
3078
|
+
claimed.push({
|
|
3079
|
+
...field,
|
|
3080
|
+
nullable: true
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
stiColumnsByBase.set(baseDecl.baseName, claimed);
|
|
3084
|
+
}
|
|
3085
|
+
const stiBaseFieldsByBase = /* @__PURE__ */ new Map();
|
|
3086
|
+
for (const [baseName, columns] of stiColumnsByBase) stiBaseFieldsByBase.set(baseName, columns.map((field) => field.fieldName));
|
|
3087
|
+
return {
|
|
3088
|
+
modelNodes: modelNodes.map((node) => {
|
|
3089
|
+
if (stiVariantNames.has(node.modelName)) return {
|
|
3090
|
+
...node,
|
|
3091
|
+
sharesBaseTable: true
|
|
3092
|
+
};
|
|
3093
|
+
const stiColumns = stiColumnsByBase.get(node.modelName);
|
|
3094
|
+
if (!stiColumns || stiColumns.length === 0) return node;
|
|
3095
|
+
return {
|
|
3096
|
+
...node,
|
|
3097
|
+
fields: [...node.fields, ...stiColumns]
|
|
3098
|
+
};
|
|
3099
|
+
}),
|
|
3100
|
+
stiBaseFieldsByBase
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
3104
|
+
* Drop the storage-only link fields (added by
|
|
3105
|
+
* {@link materializeMtiVariantStorageLinks}) from a variant's domain model, so
|
|
3106
|
+
* the domain surface stays thin while the storage table keeps the link column.
|
|
3107
|
+
*/
|
|
3108
|
+
function stripStorageOnlyDomainFields(model, fieldNames) {
|
|
3109
|
+
if (fieldNames.length === 0) return model;
|
|
3110
|
+
const fields = { ...model.fields };
|
|
3111
|
+
for (const name of fieldNames) delete fields[name];
|
|
3112
|
+
const storage = blindCast(model.storage);
|
|
3113
|
+
const storageFields = { ...storage.fields };
|
|
3114
|
+
for (const name of fieldNames) delete storageFields[name];
|
|
3115
|
+
return {
|
|
3116
|
+
...model,
|
|
3117
|
+
fields,
|
|
3118
|
+
storage: {
|
|
3119
|
+
...storage,
|
|
3120
|
+
fields: storageFields
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
2788
3124
|
function interpretPslDocumentToSqlContract(input) {
|
|
2789
3125
|
const sourceId = input.document.ast.sourceId;
|
|
2790
3126
|
if (!input.target) return notOk({
|
|
@@ -2889,6 +3225,7 @@ function interpretPslDocumentToSqlContract(input) {
|
|
|
2889
3225
|
const fkRelationMetadata = [];
|
|
2890
3226
|
const backrelationCandidates = [];
|
|
2891
3227
|
const modelResolvedFields = /* @__PURE__ */ new Map();
|
|
3228
|
+
const crossSpaceRelationsByModel = /* @__PURE__ */ new Map();
|
|
2892
3229
|
for (const model of models) {
|
|
2893
3230
|
const mapping = modelMappings.get(model.name);
|
|
2894
3231
|
if (!mapping) continue;
|
|
@@ -2919,6 +3256,10 @@ function interpretPslDocumentToSqlContract(input) {
|
|
|
2919
3256
|
fkRelationMetadata.push(...result.fkRelationMetadata);
|
|
2920
3257
|
backrelationCandidates.push(...result.backrelationCandidates);
|
|
2921
3258
|
modelResolvedFields.set(model.name, result.resolvedFields);
|
|
3259
|
+
if (result.crossSpaceRelations.length > 0) {
|
|
3260
|
+
const existing = crossSpaceRelationsByModel.get(model.name) ?? [];
|
|
3261
|
+
crossSpaceRelationsByModel.set(model.name, [...existing, ...result.crossSpaceRelations]);
|
|
3262
|
+
}
|
|
2922
3263
|
}
|
|
2923
3264
|
const { modelRelations, fkRelationsByPair } = indexFkRelations({ fkRelationMetadata });
|
|
2924
3265
|
applyBackrelationCandidates({
|
|
@@ -2928,7 +3269,16 @@ function interpretPslDocumentToSqlContract(input) {
|
|
|
2928
3269
|
diagnostics,
|
|
2929
3270
|
sourceId
|
|
2930
3271
|
});
|
|
3272
|
+
for (const [modelName, relations] of crossSpaceRelationsByModel) {
|
|
3273
|
+
const existing = modelRelations.get(modelName);
|
|
3274
|
+
if (existing) existing.push(...relations);
|
|
3275
|
+
else modelRelations.set(modelName, [...relations]);
|
|
3276
|
+
}
|
|
2931
3277
|
const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations(models, sourceId, diagnostics);
|
|
3278
|
+
const stiVariantNames = /* @__PURE__ */ new Set();
|
|
3279
|
+
for (const variantName of baseDeclarations.keys()) if (!(modelMappings.get(variantName)?.model.attributes.some((attr) => attr.name === "map") ?? false)) stiVariantNames.add(variantName);
|
|
3280
|
+
const { modelNodes: mtiLinkedModelNodes, syntheticPkFieldsByVariant } = materializeMtiVariantStorageLinks(modelNodes, baseDeclarations, stiVariantNames);
|
|
3281
|
+
const { modelNodes: stiColumnModelNodes, stiBaseFieldsByBase } = materializeStiVariantStorageColumns(mtiLinkedModelNodes, baseDeclarations, stiVariantNames);
|
|
2932
3282
|
const valueObjects = buildValueObjects({
|
|
2933
3283
|
compositeTypes,
|
|
2934
3284
|
enumTypeDescriptors: allEnumTypeDescriptors,
|
|
@@ -2951,7 +3301,7 @@ function interpretPslDocumentToSqlContract(input) {
|
|
|
2951
3301
|
...Object.keys(storageTypes).length > 0 ? { storageTypes } : {},
|
|
2952
3302
|
...Object.keys(namespaceEnumStorageTypes).length > 0 ? { namespaceTypes: namespaceEnumStorageTypes } : {},
|
|
2953
3303
|
...ifDefined("createNamespace", input.createNamespace),
|
|
2954
|
-
models:
|
|
3304
|
+
models: stiColumnModelNodes.map((model) => ({
|
|
2955
3305
|
...model,
|
|
2956
3306
|
...modelRelations.has(model.modelName) ? { relations: [...modelRelations.get(model.modelName) ?? []].sort((left, right) => compareStrings(left.fieldName, right.fieldName)) } : {}
|
|
2957
3307
|
}))
|
|
@@ -2963,7 +3313,7 @@ function interpretPslDocumentToSqlContract(input) {
|
|
|
2963
3313
|
}
|
|
2964
3314
|
let patchedModels = patchModelDomainFields(modelsForPatch, modelResolvedFields);
|
|
2965
3315
|
const polyDiagnostics = [];
|
|
2966
|
-
patchedModels = resolvePolymorphism(patchedModels, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, input.target.
|
|
3316
|
+
patchedModels = resolvePolymorphism(patchedModels, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, input.target.defaultNamespaceId, syntheticPkFieldsByVariant, stiBaseFieldsByBase, sourceId, polyDiagnostics);
|
|
2967
3317
|
if (polyDiagnostics.length > 0) return notOk({
|
|
2968
3318
|
summary: "PSL to SQL contract interpretation failed",
|
|
2969
3319
|
diagnostics: polyDiagnostics
|
|
@@ -2976,11 +3326,11 @@ function interpretPslDocumentToSqlContract(input) {
|
|
|
2976
3326
|
domain: { namespaces: Object.fromEntries(Object.entries(contract.domain.namespaces).map(([namespaceId, namespaceSlice]) => [namespaceId, {
|
|
2977
3327
|
models: Object.fromEntries(Object.entries(namespaceSlice.models).map(([modelName, model]) => [modelName, patchedModels[modelName] ?? model])),
|
|
2978
3328
|
...namespaceSlice.valueObjects !== void 0 ? { valueObjects: namespaceSlice.valueObjects } : {},
|
|
2979
|
-
...namespaceId ===
|
|
3329
|
+
...namespaceId === input.target.defaultNamespaceId && Object.keys(valueObjects).length > 0 ? { valueObjects } : {}
|
|
2980
3330
|
}])) }
|
|
2981
3331
|
});
|
|
2982
3332
|
}
|
|
2983
3333
|
//#endregion
|
|
2984
3334
|
export { interpretPslDocumentToSqlContract as t };
|
|
2985
3335
|
|
|
2986
|
-
//# sourceMappingURL=interpreter-
|
|
3336
|
+
//# sourceMappingURL=interpreter-1VmrYYbi.mjs.map
|