@prisma-next/sql-contract-psl 0.12.0-dev.47 → 0.12.0-dev.49
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/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{interpreter-BTfcjVFM.mjs → interpreter-Rn_Vzvt-.mjs} +173 -13
- package/dist/interpreter-Rn_Vzvt-.mjs.map +1 -0
- package/dist/provider.mjs +1 -1
- package/package.json +12 -12
- package/src/interpreter.ts +239 -9
- package/dist/interpreter-BTfcjVFM.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-contract-psl",
|
|
3
|
-
"version": "0.12.0-dev.
|
|
3
|
+
"version": "0.12.0-dev.49",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "PSL-to-SQL ContractIR interpreter for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/config": "0.12.0-dev.
|
|
10
|
-
"@prisma-next/contract": "0.12.0-dev.
|
|
11
|
-
"@prisma-next/framework-components": "0.12.0-dev.
|
|
12
|
-
"@prisma-next/psl-parser": "0.12.0-dev.
|
|
13
|
-
"@prisma-next/sql-contract": "0.12.0-dev.
|
|
14
|
-
"@prisma-next/sql-contract-ts": "0.12.0-dev.
|
|
15
|
-
"@prisma-next/utils": "0.12.0-dev.
|
|
9
|
+
"@prisma-next/config": "0.12.0-dev.49",
|
|
10
|
+
"@prisma-next/contract": "0.12.0-dev.49",
|
|
11
|
+
"@prisma-next/framework-components": "0.12.0-dev.49",
|
|
12
|
+
"@prisma-next/psl-parser": "0.12.0-dev.49",
|
|
13
|
+
"@prisma-next/sql-contract": "0.12.0-dev.49",
|
|
14
|
+
"@prisma-next/sql-contract-ts": "0.12.0-dev.49",
|
|
15
|
+
"@prisma-next/utils": "0.12.0-dev.49",
|
|
16
16
|
"pathe": "^2.0.3"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@prisma-next/contract-authoring": "0.12.0-dev.
|
|
20
|
-
"@prisma-next/test-utils": "0.12.0-dev.
|
|
21
|
-
"@prisma-next/tsconfig": "0.12.0-dev.
|
|
22
|
-
"@prisma-next/tsdown": "0.12.0-dev.
|
|
19
|
+
"@prisma-next/contract-authoring": "0.12.0-dev.49",
|
|
20
|
+
"@prisma-next/test-utils": "0.12.0-dev.49",
|
|
21
|
+
"@prisma-next/tsconfig": "0.12.0-dev.49",
|
|
22
|
+
"@prisma-next/tsdown": "0.12.0-dev.49",
|
|
23
23
|
"arktype": "^2.2.0",
|
|
24
24
|
"tsdown": "0.22.0",
|
|
25
25
|
"typescript": "5.9.3",
|
package/src/interpreter.ts
CHANGED
|
@@ -37,17 +37,20 @@ import type {
|
|
|
37
37
|
import {
|
|
38
38
|
isPostgresEnumStorageEntry,
|
|
39
39
|
type PostgresEnumStorageEntry,
|
|
40
|
+
type SqlModelStorage,
|
|
40
41
|
type SqlNamespaceTablesInput,
|
|
41
42
|
type StorageTypeInstance,
|
|
42
43
|
} from '@prisma-next/sql-contract/types';
|
|
43
44
|
import {
|
|
44
45
|
buildSqlContractFromDefinition,
|
|
46
|
+
type FieldNode,
|
|
45
47
|
type ForeignKeyNode,
|
|
46
48
|
type IndexNode,
|
|
47
49
|
type ModelNode,
|
|
48
50
|
type PrimaryKeyNode,
|
|
49
51
|
type UniqueConstraintNode,
|
|
50
52
|
} from '@prisma-next/sql-contract-ts/contract-builder';
|
|
53
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
51
54
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
52
55
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
53
56
|
import {
|
|
@@ -1332,11 +1335,26 @@ function resolvePolymorphism(
|
|
|
1332
1335
|
modelMappings: ReadonlyMap<string, ModelNameMapping>,
|
|
1333
1336
|
modelNamespaceIds: ReadonlyMap<string, string>,
|
|
1334
1337
|
defaultNamespaceId: string,
|
|
1338
|
+
syntheticPkFieldsByVariant: ReadonlyMap<string, readonly string[]>,
|
|
1339
|
+
stiBaseFieldsByBase: ReadonlyMap<string, readonly string[]>,
|
|
1335
1340
|
sourceId: string,
|
|
1336
1341
|
diagnostics: ContractSourceDiagnostic[],
|
|
1337
1342
|
): Record<string, ContractModel> {
|
|
1338
1343
|
let patched = models;
|
|
1339
1344
|
|
|
1345
|
+
// STI variant columns were materialised onto the base storage table so the
|
|
1346
|
+
// variants' `storage.fields` resolve. They are storage-only on the base — the
|
|
1347
|
+
// domain field belongs to the variant — so strip them from the base model's
|
|
1348
|
+
// domain + storage field maps (the table column, built upstream, stays).
|
|
1349
|
+
for (const [baseName, fieldNames] of stiBaseFieldsByBase) {
|
|
1350
|
+
const baseModel = patched[baseName];
|
|
1351
|
+
if (!baseModel || fieldNames.length === 0) continue;
|
|
1352
|
+
patched = {
|
|
1353
|
+
...patched,
|
|
1354
|
+
[baseName]: stripStorageOnlyDomainFields(baseModel, fieldNames),
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1340
1358
|
for (const [modelName, decl] of discriminatorDeclarations) {
|
|
1341
1359
|
if (baseDeclarations.has(modelName)) {
|
|
1342
1360
|
diagnostics.push({
|
|
@@ -1431,22 +1449,211 @@ function resolvePolymorphism(
|
|
|
1431
1449
|
variantMapping?.model.attributes.some((attr) => attr.name === 'map') ?? false;
|
|
1432
1450
|
const resolvedTable = hasExplicitMap ? variantMapping?.tableName : baseMapping?.tableName;
|
|
1433
1451
|
|
|
1452
|
+
const patchedVariant: ContractModel = {
|
|
1453
|
+
...variantModel,
|
|
1454
|
+
base: crossRef(
|
|
1455
|
+
baseDecl.baseName,
|
|
1456
|
+
modelNamespaceIds.get(baseDecl.baseName) ?? defaultNamespaceId,
|
|
1457
|
+
),
|
|
1458
|
+
...(resolvedTable ? { storage: { ...variantModel.storage, table: resolvedTable } } : {}),
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1434
1461
|
patched = {
|
|
1435
1462
|
...patched,
|
|
1436
|
-
[variantName]:
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
modelNamespaceIds.get(baseDecl.baseName) ?? defaultNamespaceId,
|
|
1441
|
-
),
|
|
1442
|
-
...(resolvedTable ? { storage: { ...variantModel.storage, table: resolvedTable } } : {}),
|
|
1443
|
-
},
|
|
1463
|
+
[variantName]: stripStorageOnlyDomainFields(
|
|
1464
|
+
patchedVariant,
|
|
1465
|
+
syntheticPkFieldsByVariant.get(variantName) ?? [],
|
|
1466
|
+
),
|
|
1444
1467
|
};
|
|
1445
1468
|
}
|
|
1446
1469
|
|
|
1447
1470
|
return patched;
|
|
1448
1471
|
}
|
|
1449
1472
|
|
|
1473
|
+
/**
|
|
1474
|
+
* Multi-table-inheritance variants (`@@base` + their own `@@map`) live in a
|
|
1475
|
+
* separate table from their base. The ORM joins that table to the base on the
|
|
1476
|
+
* shared primary key (`base.id = variant.id`), so the variant storage table
|
|
1477
|
+
* must carry the base PK column even though the variant domain model declares
|
|
1478
|
+
* only its own fields. This enriches each MTI variant's `ModelNode` with that
|
|
1479
|
+
* link column, a primary key on it, and a FK back to the base table.
|
|
1480
|
+
*
|
|
1481
|
+
* The link column is reported back per variant in `syntheticPkFieldsByVariant`
|
|
1482
|
+
* so the domain-model patch can drop it again — keeping the variant's domain
|
|
1483
|
+
* surface thin (its create/read inputs don't gain a redundant `id`) while the
|
|
1484
|
+
* storage table stays joinable. Single-table-inheritance variants (no own
|
|
1485
|
+
* table) are left untouched.
|
|
1486
|
+
*/
|
|
1487
|
+
function materializeMtiVariantStorageLinks(
|
|
1488
|
+
modelNodes: readonly ModelNode[],
|
|
1489
|
+
baseDeclarations: ReadonlyMap<string, BaseDeclaration>,
|
|
1490
|
+
stiVariantNames: ReadonlySet<string>,
|
|
1491
|
+
): { modelNodes: ModelNode[]; syntheticPkFieldsByVariant: Map<string, readonly string[]> } {
|
|
1492
|
+
const nodeByModel = new Map(modelNodes.map((node) => [node.modelName, node]));
|
|
1493
|
+
const syntheticPkFieldsByVariant = new Map<string, readonly string[]>();
|
|
1494
|
+
|
|
1495
|
+
const enriched = modelNodes.map((node): ModelNode => {
|
|
1496
|
+
const baseDecl = baseDeclarations.get(node.modelName);
|
|
1497
|
+
if (!baseDecl) return node;
|
|
1498
|
+
const baseNode = nodeByModel.get(baseDecl.baseName);
|
|
1499
|
+
if (!baseNode) return node;
|
|
1500
|
+
// Single-table inheritance (no own `@@map`) shares the base table; it gets
|
|
1501
|
+
// its columns materialised onto the base instead (see
|
|
1502
|
+
// {@link materializeStiVariantStorageColumns}), never a link column.
|
|
1503
|
+
if (stiVariantNames.has(node.modelName)) return node;
|
|
1504
|
+
const basePrimaryKey = baseNode.id;
|
|
1505
|
+
if (!basePrimaryKey || basePrimaryKey.columns.length === 0) return node;
|
|
1506
|
+
|
|
1507
|
+
const existingColumns = new Set(node.fields.map((field) => field.columnName));
|
|
1508
|
+
const linkFields: FieldNode[] = [];
|
|
1509
|
+
for (const pkColumn of basePrimaryKey.columns) {
|
|
1510
|
+
if (existingColumns.has(pkColumn)) continue;
|
|
1511
|
+
const baseField = baseNode.fields.find(
|
|
1512
|
+
(field): field is FieldNode => 'descriptor' in field && field.columnName === pkColumn,
|
|
1513
|
+
);
|
|
1514
|
+
if (!baseField) continue;
|
|
1515
|
+
linkFields.push({
|
|
1516
|
+
fieldName: baseField.fieldName,
|
|
1517
|
+
columnName: pkColumn,
|
|
1518
|
+
descriptor: baseField.descriptor,
|
|
1519
|
+
nullable: false,
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
if (linkFields.length === 0) return node;
|
|
1523
|
+
|
|
1524
|
+
syntheticPkFieldsByVariant.set(
|
|
1525
|
+
node.modelName,
|
|
1526
|
+
linkFields.map((field) => field.fieldName),
|
|
1527
|
+
);
|
|
1528
|
+
|
|
1529
|
+
const foreignKey: ForeignKeyNode = {
|
|
1530
|
+
columns: basePrimaryKey.columns,
|
|
1531
|
+
references: {
|
|
1532
|
+
model: baseNode.modelName,
|
|
1533
|
+
table: baseNode.tableName,
|
|
1534
|
+
columns: basePrimaryKey.columns,
|
|
1535
|
+
...ifDefined('namespaceId', baseNode.namespaceId),
|
|
1536
|
+
},
|
|
1537
|
+
constraint: true,
|
|
1538
|
+
// The link columns are the variant's own primary key, which already
|
|
1539
|
+
// carries a unique index — a separate FK backing index would be redundant.
|
|
1540
|
+
index: false,
|
|
1541
|
+
// Deleting a base row must delete its variant extension row — classic
|
|
1542
|
+
// multi-table-inheritance semantics.
|
|
1543
|
+
onDelete: 'cascade',
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
return {
|
|
1547
|
+
...node,
|
|
1548
|
+
fields: [...linkFields, ...node.fields],
|
|
1549
|
+
id: { columns: basePrimaryKey.columns },
|
|
1550
|
+
foreignKeys: [...(node.foreignKeys ?? []), foreignKey],
|
|
1551
|
+
};
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
return { modelNodes: enriched, syntheticPkFieldsByVariant };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Single-table-inheritance variants (`@@base` with no own `@@map`) share the
|
|
1559
|
+
* base table: `resolvePolymorphism` points the variant's `storage.table` at the
|
|
1560
|
+
* base, and the ORM reads variant-declared fields straight off the base table.
|
|
1561
|
+
* For that to validate and round-trip, the base storage table must physically
|
|
1562
|
+
* carry every STI variant's declared columns. This enriches the base
|
|
1563
|
+
* `ModelNode` with those columns.
|
|
1564
|
+
*
|
|
1565
|
+
* The materialised columns are always nullable in storage: the base table hosts
|
|
1566
|
+
* every variant's rows, so a column a variant declares as required is still
|
|
1567
|
+
* NULL on sibling-variant rows. The variant's domain field keeps its declared
|
|
1568
|
+
* nullability — required-in-domain / nullable-in-storage is the intended STI
|
|
1569
|
+
* shape.
|
|
1570
|
+
*
|
|
1571
|
+
* Collisions (two variants declaring the same column, or a variant column name
|
|
1572
|
+
* clashing with a base column) are resolved skip-if-exists here, mirroring the
|
|
1573
|
+
* MTI link guard; surfacing them as diagnostics is tracked separately
|
|
1574
|
+
* (TML-2827).
|
|
1575
|
+
*/
|
|
1576
|
+
function materializeStiVariantStorageColumns(
|
|
1577
|
+
modelNodes: readonly ModelNode[],
|
|
1578
|
+
baseDeclarations: ReadonlyMap<string, BaseDeclaration>,
|
|
1579
|
+
stiVariantNames: ReadonlySet<string>,
|
|
1580
|
+
): { modelNodes: ModelNode[]; stiBaseFieldsByBase: Map<string, readonly string[]> } {
|
|
1581
|
+
if (stiVariantNames.size === 0) {
|
|
1582
|
+
return { modelNodes: [...modelNodes], stiBaseFieldsByBase: new Map() };
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
const nodeByModel = new Map(modelNodes.map((node) => [node.modelName, node]));
|
|
1586
|
+
type StiColumn = ModelNode['fields'][number];
|
|
1587
|
+
const stiColumnsByBase = new Map<string, StiColumn[]>();
|
|
1588
|
+
|
|
1589
|
+
for (const variantName of stiVariantNames) {
|
|
1590
|
+
const variantNode = nodeByModel.get(variantName);
|
|
1591
|
+
const baseDecl = baseDeclarations.get(variantName);
|
|
1592
|
+
if (!variantNode || !baseDecl) continue;
|
|
1593
|
+
const baseNode = nodeByModel.get(baseDecl.baseName);
|
|
1594
|
+
if (!baseNode) continue;
|
|
1595
|
+
|
|
1596
|
+
const baseColumns = new Set(baseNode.fields.map((field) => field.columnName));
|
|
1597
|
+
const claimed = stiColumnsByBase.get(baseDecl.baseName) ?? [];
|
|
1598
|
+
const claimedColumns = new Set(claimed.map((field) => field.columnName));
|
|
1599
|
+
|
|
1600
|
+
for (const field of variantNode.fields) {
|
|
1601
|
+
if (baseColumns.has(field.columnName) || claimedColumns.has(field.columnName)) {
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
claimedColumns.add(field.columnName);
|
|
1605
|
+
claimed.push({ ...field, nullable: true });
|
|
1606
|
+
}
|
|
1607
|
+
stiColumnsByBase.set(baseDecl.baseName, claimed);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// The materialised columns exist on the base STORAGE table so the variants'
|
|
1611
|
+
// `storage.fields` resolve, but they are NOT base DOMAIN fields — `severity`
|
|
1612
|
+
// belongs to `Bug`, not to `Task`. Report the materialised field names per
|
|
1613
|
+
// base so the domain patch can strip them from the base model (the table
|
|
1614
|
+
// column stays); this is the STI analogue of `syntheticPkFieldsByVariant`.
|
|
1615
|
+
const stiBaseFieldsByBase = new Map<string, readonly string[]>();
|
|
1616
|
+
for (const [baseName, columns] of stiColumnsByBase) {
|
|
1617
|
+
stiBaseFieldsByBase.set(
|
|
1618
|
+
baseName,
|
|
1619
|
+
columns.map((field) => field.fieldName),
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const enriched = modelNodes.map((node): ModelNode => {
|
|
1624
|
+
// STI variant: contributes a domain model but no storage table of its own.
|
|
1625
|
+
if (stiVariantNames.has(node.modelName)) {
|
|
1626
|
+
return { ...node, sharesBaseTable: true };
|
|
1627
|
+
}
|
|
1628
|
+
const stiColumns = stiColumnsByBase.get(node.modelName);
|
|
1629
|
+
if (!stiColumns || stiColumns.length === 0) return node;
|
|
1630
|
+
return { ...node, fields: [...node.fields, ...stiColumns] };
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
return { modelNodes: enriched, stiBaseFieldsByBase };
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Drop the storage-only link fields (added by
|
|
1638
|
+
* {@link materializeMtiVariantStorageLinks}) from a variant's domain model, so
|
|
1639
|
+
* the domain surface stays thin while the storage table keeps the link column.
|
|
1640
|
+
*/
|
|
1641
|
+
function stripStorageOnlyDomainFields(
|
|
1642
|
+
model: ContractModel,
|
|
1643
|
+
fieldNames: readonly string[],
|
|
1644
|
+
): ContractModel {
|
|
1645
|
+
if (fieldNames.length === 0) return model;
|
|
1646
|
+
const fields = { ...model.fields };
|
|
1647
|
+
for (const name of fieldNames) delete fields[name];
|
|
1648
|
+
const storage = blindCast<
|
|
1649
|
+
SqlModelStorage,
|
|
1650
|
+
'SQL interpreter domain models always carry SqlModelStorage'
|
|
1651
|
+
>(model.storage);
|
|
1652
|
+
const storageFields = { ...storage.fields };
|
|
1653
|
+
for (const name of fieldNames) delete storageFields[name];
|
|
1654
|
+
return { ...model, fields, storage: { ...storage, fields: storageFields } };
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1450
1657
|
export function interpretPslDocumentToSqlContract(
|
|
1451
1658
|
input: InterpretPslDocumentToSqlContractInput,
|
|
1452
1659
|
): Result<Contract, ContractSourceDiagnostics> {
|
|
@@ -1653,6 +1860,27 @@ export function interpretPslDocumentToSqlContract(
|
|
|
1653
1860
|
diagnostics,
|
|
1654
1861
|
);
|
|
1655
1862
|
|
|
1863
|
+
// A variant with `@@base` but no own `@@map` is single-table inheritance:
|
|
1864
|
+
// it shares the base table. (`@@map` ⇒ multi-table inheritance.) This is the
|
|
1865
|
+
// authoritative STI/MTI signal — the variant's resolved table name is not,
|
|
1866
|
+
// because a no-`@@map` STI variant still gets a `lowerFirst(name)` default
|
|
1867
|
+
// table name that differs from the base before `resolvePolymorphism` rewrites
|
|
1868
|
+
// it onto the base table.
|
|
1869
|
+
const stiVariantNames = new Set<string>();
|
|
1870
|
+
for (const variantName of baseDeclarations.keys()) {
|
|
1871
|
+
const variantMapping = modelMappings.get(variantName);
|
|
1872
|
+
const hasExplicitMap =
|
|
1873
|
+
variantMapping?.model.attributes.some((attr) => attr.name === 'map') ?? false;
|
|
1874
|
+
if (!hasExplicitMap) {
|
|
1875
|
+
stiVariantNames.add(variantName);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
const { modelNodes: mtiLinkedModelNodes, syntheticPkFieldsByVariant } =
|
|
1880
|
+
materializeMtiVariantStorageLinks(modelNodes, baseDeclarations, stiVariantNames);
|
|
1881
|
+
const { modelNodes: stiColumnModelNodes, stiBaseFieldsByBase } =
|
|
1882
|
+
materializeStiVariantStorageColumns(mtiLinkedModelNodes, baseDeclarations, stiVariantNames);
|
|
1883
|
+
|
|
1656
1884
|
const valueObjects = buildValueObjects({
|
|
1657
1885
|
compositeTypes,
|
|
1658
1886
|
enumTypeDescriptors: allEnumTypeDescriptors,
|
|
@@ -1688,7 +1916,7 @@ export function interpretPslDocumentToSqlContract(
|
|
|
1688
1916
|
? { namespaceTypes: namespaceEnumStorageTypes }
|
|
1689
1917
|
: {}),
|
|
1690
1918
|
...ifDefined('createNamespace', input.createNamespace),
|
|
1691
|
-
models:
|
|
1919
|
+
models: stiColumnModelNodes.map((model) => ({
|
|
1692
1920
|
...model,
|
|
1693
1921
|
...(modelRelations.has(model.modelName)
|
|
1694
1922
|
? {
|
|
@@ -1722,6 +1950,8 @@ export function interpretPslDocumentToSqlContract(
|
|
|
1722
1950
|
modelMappings,
|
|
1723
1951
|
modelNamespaceIds,
|
|
1724
1952
|
input.target.defaultNamespaceId,
|
|
1953
|
+
syntheticPkFieldsByVariant,
|
|
1954
|
+
stiBaseFieldsByBase,
|
|
1725
1955
|
sourceId,
|
|
1726
1956
|
polyDiagnostics,
|
|
1727
1957
|
);
|