@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.
@@ -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 { UNSPECIFIED_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
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, SqlUnspecifiedNamespace, applyFkDefaults, isPostgresEnumStorageEntry, toStorageTypeInstance } from "@prisma-next/sql-contract/types";
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 storageTables = {};
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
- columns: fk.columns,
146
- references: {
147
- table: fk.references.table,
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
- storageTables[tableName] = {
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
- tables: storageTables,
212
- types: Object.fromEntries(Object.entries(rawStorageTypes).map(([name, entry]) => {
213
- if (isPostgresEnumStorageEntry(entry)) return [name, entry];
214
- if (entry.kind === "codec-instance") return [name, entry];
215
- return [name, toStorageTypeInstance({
216
- codecId: entry.codecId,
217
- nativeType: entry.nativeType,
218
- typeParams: entry.typeParams ?? {}
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) {