@prisma-next/sql-contract-ts 0.9.0 → 0.10.0
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/contract-builder.d.mts +123 -11
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +155 -18
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +10 -10
- package/src/build-contract.ts +138 -12
- package/src/composed-authoring-helpers.ts +2 -1
- package/src/contract-builder.ts +145 -4
- package/src/contract-definition.ts +40 -0
- package/src/contract-dsl.ts +53 -0
- package/src/contract-lowering.ts +5 -0
- package/src/contract-types.ts +51 -6
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { computeExecutionHash, computeProfileHash, computeStorageHash } from "@prisma-next/contract/hashing";
|
|
2
2
|
import { coreHash, isColumnDefault } from "@prisma-next/contract/types";
|
|
3
|
-
import {
|
|
3
|
+
import { UNBOUND_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
|
|
4
4
|
import { validateIndexTypes } from "@prisma-next/sql-contract/index-type-validation";
|
|
5
5
|
import { createIndexTypeRegistry } from "@prisma-next/sql-contract/index-types";
|
|
6
|
-
import { SqlStorage,
|
|
6
|
+
import { SqlStorage, StorageTable, applyFkDefaults, isPostgresEnumStorageEntry, toStorageTypeInstance } from "@prisma-next/sql-contract/types";
|
|
7
7
|
import { validateStorageSemantics } from "@prisma-next/sql-contract/validators";
|
|
8
8
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
9
9
|
import { createEntityHelpersFromNamespace } from "@prisma-next/contract-authoring";
|
|
@@ -95,11 +95,50 @@ function buildDomainField(field, column) {
|
|
|
95
95
|
...field.many ? { many: true } : {}
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
|
+
function collectStorageNamespaceCoordinateIds(definition) {
|
|
99
|
+
const ids = /* @__PURE__ */ new Set();
|
|
100
|
+
ids.add(UNBOUND_NAMESPACE_ID);
|
|
101
|
+
for (const id of definition.namespaces ?? []) if (id.length > 0) ids.add(id);
|
|
102
|
+
for (const model of definition.models) if (model.namespaceId !== void 0 && model.namespaceId.length > 0) ids.add(model.namespaceId);
|
|
103
|
+
return ids;
|
|
104
|
+
}
|
|
105
|
+
const POSTGRES_ENUM_NAMESPACE_ID = "public";
|
|
106
|
+
function partitionStorageTypesForTarget(targetId, types, namespaceTypes) {
|
|
107
|
+
const documentTypes = {};
|
|
108
|
+
const namespaceEnumTypesById = {};
|
|
109
|
+
for (const [name, entry] of Object.entries(types)) {
|
|
110
|
+
if (isPostgresEnumStorageEntry(entry)) {
|
|
111
|
+
if (targetId !== "postgres") throw new Error(`buildSqlContractFromDefinition: postgres enum "${name}" is only valid when target is "postgres" (got "${targetId}").`);
|
|
112
|
+
let slot = namespaceEnumTypesById[POSTGRES_ENUM_NAMESPACE_ID];
|
|
113
|
+
if (slot === void 0) {
|
|
114
|
+
slot = {};
|
|
115
|
+
namespaceEnumTypesById[POSTGRES_ENUM_NAMESPACE_ID] = slot;
|
|
116
|
+
}
|
|
117
|
+
slot[name] = entry;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
documentTypes[name] = entry;
|
|
121
|
+
}
|
|
122
|
+
if (namespaceTypes !== void 0) for (const [nsId, enumsInNs] of Object.entries(namespaceTypes)) for (const [name, entry] of Object.entries(enumsInNs)) {
|
|
123
|
+
if (targetId !== "postgres") throw new Error(`buildSqlContractFromDefinition: postgres enum "${name}" is only valid when target is "postgres" (got "${targetId}").`);
|
|
124
|
+
let slot = namespaceEnumTypesById[nsId];
|
|
125
|
+
if (slot === void 0) {
|
|
126
|
+
slot = {};
|
|
127
|
+
namespaceEnumTypesById[nsId] = slot;
|
|
128
|
+
}
|
|
129
|
+
slot[name] = entry;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
documentTypes,
|
|
133
|
+
namespaceEnumTypesById
|
|
134
|
+
};
|
|
135
|
+
}
|
|
98
136
|
function buildSqlContractFromDefinition(definition, codecLookup) {
|
|
99
137
|
const target = definition.target.targetId;
|
|
100
138
|
const targetFamily = "sql";
|
|
101
139
|
const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));
|
|
102
|
-
const
|
|
140
|
+
const tablesByNamespace = {};
|
|
141
|
+
const tableNameToNamespaceId = /* @__PURE__ */ new Map();
|
|
103
142
|
const executionDefaults = [];
|
|
104
143
|
const models = {};
|
|
105
144
|
const roots = {};
|
|
@@ -138,13 +177,20 @@ function buildSqlContractFromDefinition(definition, codecLookup) {
|
|
|
138
177
|
...ifDefined("onUpdate", executionDefaultPhases.onUpdate)
|
|
139
178
|
});
|
|
140
179
|
}
|
|
180
|
+
const namespaceId = semanticModel.namespaceId !== void 0 && semanticModel.namespaceId.length > 0 ? semanticModel.namespaceId : UNBOUND_NAMESPACE_ID;
|
|
141
181
|
const foreignKeys = (semanticModel.foreignKeys ?? []).map((fk) => {
|
|
142
182
|
const targetModel = assertKnownTargetModel(modelsByName, semanticModel.modelName, fk.references.model, "Foreign key");
|
|
143
183
|
assertTargetTableMatches(semanticModel.modelName, targetModel, fk.references.table, "Foreign key");
|
|
184
|
+
const targetNamespaceId = fk.references.namespaceId ?? (targetModel.namespaceId !== void 0 && targetModel.namespaceId.length > 0 ? targetModel.namespaceId : UNBOUND_NAMESPACE_ID);
|
|
144
185
|
return {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
186
|
+
source: {
|
|
187
|
+
namespaceId,
|
|
188
|
+
tableName,
|
|
189
|
+
columns: fk.columns
|
|
190
|
+
},
|
|
191
|
+
target: {
|
|
192
|
+
namespaceId: targetNamespaceId,
|
|
193
|
+
tableName: fk.references.table,
|
|
148
194
|
columns: fk.references.columns
|
|
149
195
|
},
|
|
150
196
|
...applyFkDefaults({
|
|
@@ -156,7 +202,10 @@ function buildSqlContractFromDefinition(definition, codecLookup) {
|
|
|
156
202
|
...ifDefined("onUpdate", fk.onUpdate)
|
|
157
203
|
};
|
|
158
204
|
});
|
|
159
|
-
|
|
205
|
+
const existingNs = tableNameToNamespaceId.get(tableName);
|
|
206
|
+
if (existingNs !== void 0 && existingNs !== namespaceId) throw new Error(`buildSqlContractFromDefinition: table "${tableName}" is mapped in namespace "${namespaceId}" but already exists in namespace "${existingNs}".`);
|
|
207
|
+
tableNameToNamespaceId.set(tableName, namespaceId);
|
|
208
|
+
const tableInput = {
|
|
160
209
|
columns,
|
|
161
210
|
uniques: (semanticModel.uniques ?? []).map((u) => ({
|
|
162
211
|
columns: u.columns,
|
|
@@ -174,6 +223,13 @@ function buildSqlContractFromDefinition(definition, codecLookup) {
|
|
|
174
223
|
...ifDefined("name", semanticModel.id.name)
|
|
175
224
|
} } : {}
|
|
176
225
|
};
|
|
226
|
+
let nsTables = tablesByNamespace[namespaceId];
|
|
227
|
+
if (nsTables === void 0) {
|
|
228
|
+
nsTables = {};
|
|
229
|
+
tablesByNamespace[namespaceId] = nsTables;
|
|
230
|
+
}
|
|
231
|
+
if (nsTables[tableName] !== void 0) throw new Error(`buildSqlContractFromDefinition: duplicate table "${tableName}" in namespace "${namespaceId}".`);
|
|
232
|
+
nsTables[tableName] = new StorageTable(tableInput);
|
|
177
233
|
const storageFields = {};
|
|
178
234
|
for (const [fieldName, columnName] of Object.entries(fieldToColumn)) storageFields[fieldName] = { column: columnName };
|
|
179
235
|
const columnToField = new Map(Object.entries(fieldToColumn).map(([field, col]) => [col, field]));
|
|
@@ -207,18 +263,29 @@ function buildSqlContractFromDefinition(definition, codecLookup) {
|
|
|
207
263
|
};
|
|
208
264
|
}
|
|
209
265
|
const rawStorageTypes = definition.storageTypes ?? {};
|
|
266
|
+
const { documentTypes, namespaceEnumTypesById } = partitionStorageTypesForTarget(target, Object.fromEntries(Object.entries(rawStorageTypes).map(([name, entry]) => {
|
|
267
|
+
if (isPostgresEnumStorageEntry(entry)) return [name, entry];
|
|
268
|
+
if (entry.kind === "codec-instance") return [name, entry];
|
|
269
|
+
return [name, toStorageTypeInstance({
|
|
270
|
+
codecId: entry.codecId,
|
|
271
|
+
nativeType: entry.nativeType,
|
|
272
|
+
typeParams: entry.typeParams ?? {}
|
|
273
|
+
})];
|
|
274
|
+
})), definition.namespaceTypes);
|
|
275
|
+
const namespaceCoordinateIds = collectStorageNamespaceCoordinateIds(definition);
|
|
276
|
+
for (const id of Object.keys(namespaceEnumTypesById)) namespaceCoordinateIds.add(id);
|
|
277
|
+
const { createNamespace } = definition;
|
|
210
278
|
const storageWithoutHash = {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}))
|
|
221
|
-
namespaces: { [UNSPECIFIED_NAMESPACE_ID]: SqlUnspecifiedNamespace.instance }
|
|
279
|
+
types: documentTypes,
|
|
280
|
+
namespaces: Object.fromEntries([...namespaceCoordinateIds].sort().map((id) => {
|
|
281
|
+
const enumTypes = namespaceEnumTypesById[id];
|
|
282
|
+
const nsInput = {
|
|
283
|
+
id,
|
|
284
|
+
tables: tablesByNamespace[id] ?? {},
|
|
285
|
+
...ifDefined("types", enumTypes)
|
|
286
|
+
};
|
|
287
|
+
return [id, createNamespace ? createNamespace(nsInput) : nsInput];
|
|
288
|
+
}))
|
|
222
289
|
};
|
|
223
290
|
const storageHash = definition.storageHash ? coreHash(definition.storageHash) : computeStorageHash({
|
|
224
291
|
target,
|
|
@@ -675,6 +742,7 @@ function model(modelNameOrInput, maybeInput) {
|
|
|
675
742
|
if (!input) throw new Error("model(\"ModelName\", ...) requires a model definition.");
|
|
676
743
|
return new ContractModelBuilder({
|
|
677
744
|
...typeof modelNameOrInput === "string" ? { modelName: modelNameOrInput } : {},
|
|
745
|
+
...input.namespace !== void 0 ? { namespace: input.namespace } : {},
|
|
678
746
|
fields: input.fields,
|
|
679
747
|
relations: input.relations ?? {}
|
|
680
748
|
});
|
|
@@ -1210,6 +1278,7 @@ function resolveModelNode(spec, allSpecs, storageTypes, storageTypeReverseLookup
|
|
|
1210
1278
|
return {
|
|
1211
1279
|
modelName: spec.modelName,
|
|
1212
1280
|
tableName: spec.tableName,
|
|
1281
|
+
...spec.namespace !== void 0 ? { namespaceId: spec.namespace } : {},
|
|
1213
1282
|
fields,
|
|
1214
1283
|
...idConstraint ? { id: {
|
|
1215
1284
|
columns: mapFieldNamesToColumnNames(spec.modelName, idConstraint.fields, spec.fieldToColumn),
|
|
@@ -1254,6 +1323,7 @@ function collectRuntimeModelSpecs(definition) {
|
|
|
1254
1323
|
modelSpecs.set(modelName, {
|
|
1255
1324
|
modelName,
|
|
1256
1325
|
tableName,
|
|
1326
|
+
namespace: modelDefinition.stageOne.namespace,
|
|
1257
1327
|
fieldBuilders,
|
|
1258
1328
|
fieldToColumn,
|
|
1259
1329
|
relations: modelDefinition.stageOne.relations,
|
|
@@ -1283,6 +1353,8 @@ function buildContractDefinition(definition) {
|
|
|
1283
1353
|
...definition.storageHash ? { storageHash: definition.storageHash } : {},
|
|
1284
1354
|
...definition.foreignKeyDefaults ? { foreignKeyDefaults: definition.foreignKeyDefaults } : {},
|
|
1285
1355
|
...Object.keys(collection.storageTypes).length > 0 ? { storageTypes: collection.storageTypes } : {},
|
|
1356
|
+
...definition.namespaces ? { namespaces: definition.namespaces } : {},
|
|
1357
|
+
...definition.createNamespace ? { createNamespace: definition.createNamespace } : {},
|
|
1286
1358
|
models
|
|
1287
1359
|
};
|
|
1288
1360
|
}
|
|
@@ -1292,6 +1364,69 @@ function validateTargetPackRef(family, target) {
|
|
|
1292
1364
|
if (family.familyId !== "sql") throw new Error(`defineContract only accepts SQL family packs. Received family "${family.familyId}".`);
|
|
1293
1365
|
if (target.familyId !== family.familyId) throw new Error(`target pack "${target.id}" targets family "${target.familyId}" but contract family is "${family.familyId}".`);
|
|
1294
1366
|
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Per-target reserved namespace names enforced by `defineContract` for
|
|
1369
|
+
* SQL family contracts. Two categories:
|
|
1370
|
+
*
|
|
1371
|
+
* 1. **IR sentinels** (`__unbound__`, `__unspecified__`) — reserved on
|
|
1372
|
+
* every SQL target. The double-underscore decoration marks them as
|
|
1373
|
+
* framework-reserved coordinates; user code must not declare them
|
|
1374
|
+
* explicitly.
|
|
1375
|
+
* 2. **Target-specific PSL keywords** — Postgres reserves the bare
|
|
1376
|
+
* `unbound` identifier for the late-binding opt-in
|
|
1377
|
+
* (`namespace unbound { … }`) so the TS surface must reject it from
|
|
1378
|
+
* `defineContract({ namespaces })` lists. SQLite has no schema
|
|
1379
|
+
* concept and rejects every non-empty namespaces list outright;
|
|
1380
|
+
* callers should declare `namespaces: []` or omit the field.
|
|
1381
|
+
*/
|
|
1382
|
+
function validateNamespaceDeclarations(target, namespaces) {
|
|
1383
|
+
if (!namespaces) return;
|
|
1384
|
+
if (target.targetId === "sqlite" && namespaces.length > 0) throw new Error(`defineContract: SQLite contracts cannot declare namespaces (SQLite has no schema concept; emitted DDL is always unqualified). Received namespaces: [${namespaces.map((name) => `"${name}"`).join(", ")}].`);
|
|
1385
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1386
|
+
for (const namespace of namespaces) {
|
|
1387
|
+
if (namespace.length === 0) throw new Error("defineContract: namespace names cannot be empty.");
|
|
1388
|
+
if (namespace.trim().length === 0) throw new Error(`defineContract: namespace name "${namespace}" cannot be whitespace-only.`);
|
|
1389
|
+
if (namespace === "__unbound__" || namespace === "__unspecified__") throw new Error(`defineContract: namespace name "${namespace}" is a reserved IR sentinel and cannot appear in the declared namespaces list.`);
|
|
1390
|
+
if (target.targetId === "postgres" && namespace === "unbound") throw new Error(`defineContract: namespace name "unbound" is reserved by Postgres for the late-binding opt-in (use \`namespace unbound { … }\` in PSL instead of declaring it as a regular schema).`);
|
|
1391
|
+
if (seen.has(namespace)) throw new Error(`defineContract: namespaces list contains duplicate entry "${namespace}".`);
|
|
1392
|
+
seen.add(namespace);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Per-model `namespace` validation paired with
|
|
1397
|
+
* {@link validateNamespaceDeclarations}. Mirrors the reserved-name
|
|
1398
|
+
* rules so the per-model surface stays consistent with the contract-
|
|
1399
|
+
* level surface:
|
|
1400
|
+
*
|
|
1401
|
+
* - `__unbound__` / `__unspecified__` — reserved IR sentinels on
|
|
1402
|
+
* every SQL target.
|
|
1403
|
+
* - `unbound` on Postgres — reserved for the PSL
|
|
1404
|
+
* `namespace unbound { … }` opt-in.
|
|
1405
|
+
*
|
|
1406
|
+
* Additionally enforces that each per-model `namespace` either
|
|
1407
|
+
* references an entry in the contract's declared `namespaces` list or
|
|
1408
|
+
* names the Postgres late-binding keyword (`unbound`) — the latter is
|
|
1409
|
+
* not a "declared namespace" but is a legal opt-in only via PSL today,
|
|
1410
|
+
* so the TS surface also rejects it on the per-model side and points
|
|
1411
|
+
* authors at the PSL `namespace unbound { … }` block.
|
|
1412
|
+
*
|
|
1413
|
+
* The SQLite per-model `namespace` field is rejected outright (SQLite
|
|
1414
|
+
* has no schema concept).
|
|
1415
|
+
*/
|
|
1416
|
+
function validatePerModelNamespaces(target, namespaces, models) {
|
|
1417
|
+
const declaredNamespaces = new Set(namespaces ?? []);
|
|
1418
|
+
for (const [modelKey, modelBuilder] of Object.entries(models)) {
|
|
1419
|
+
const perModelNamespace = modelBuilder.stageOne.namespace;
|
|
1420
|
+
if (perModelNamespace === void 0) continue;
|
|
1421
|
+
if (target.targetId === "sqlite") throw new Error(`defineContract: model "${modelKey}" sets \`namespace: "${perModelNamespace}"\` but the target is SQLite (SQLite has no schema concept; remove the per-model \`namespace\` field).`);
|
|
1422
|
+
if (perModelNamespace === "__unbound__" || perModelNamespace === "__unspecified__") throw new Error(`defineContract: model "${modelKey}" sets \`namespace: "${perModelNamespace}"\` but that name is a reserved IR sentinel and cannot appear in user code.`);
|
|
1423
|
+
if (target.targetId === "postgres" && perModelNamespace === "unbound") throw new Error(`defineContract: model "${modelKey}" sets \`namespace: "unbound"\` but that name is reserved by Postgres for the late-binding opt-in (use \`namespace unbound { … }\` in PSL instead — there is no equivalent surface in the TS builder today).`);
|
|
1424
|
+
if (!declaredNamespaces.has(perModelNamespace)) {
|
|
1425
|
+
const hint = declaredNamespaces.size > 0 ? ` Declared namespaces: [${[...declaredNamespaces].map((name) => `"${name}"`).join(", ")}].` : " The contract does not declare any namespaces; add `namespaces: [\"…\"]` to `defineContract` first.";
|
|
1426
|
+
throw new Error(`defineContract: model "${modelKey}" references namespace "${perModelNamespace}" but that name does not appear in the contract's declared \`namespaces\` list.${hint}`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1295
1430
|
function validateExtensionPackRefs(target, extensionPacks) {
|
|
1296
1431
|
if (!extensionPacks) return;
|
|
1297
1432
|
for (const packRef of Object.values(extensionPacks)) {
|
|
@@ -1303,6 +1438,8 @@ function validateExtensionPackRefs(target, extensionPacks) {
|
|
|
1303
1438
|
function buildContractFromDsl(definition) {
|
|
1304
1439
|
validateTargetPackRef(definition.family, definition.target);
|
|
1305
1440
|
validateExtensionPackRefs(definition.target, definition.extensionPacks);
|
|
1441
|
+
validateNamespaceDeclarations(definition.target, definition.namespaces);
|
|
1442
|
+
validatePerModelNamespaces(definition.target, definition.namespaces, definition.models ?? {});
|
|
1306
1443
|
return buildSqlContractFromDefinition(buildContractDefinition(definition), definition.codecLookup);
|
|
1307
1444
|
}
|
|
1308
1445
|
function defineContract(definition, factory) {
|