@prisma-next/sql-contract 0.12.0-dev.9 → 0.13.0-dev.1

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.
Files changed (59) hide show
  1. package/dist/canonicalization-hooks.d.mts.map +1 -1
  2. package/dist/canonicalization-hooks.mjs +10 -11
  3. package/dist/canonicalization-hooks.mjs.map +1 -1
  4. package/dist/factories.d.mts +4 -2
  5. package/dist/factories.d.mts.map +1 -1
  6. package/dist/factories.mjs +3 -2
  7. package/dist/factories.mjs.map +1 -1
  8. package/dist/foreign-key-BATxB95l.d.mts +121 -0
  9. package/dist/foreign-key-BATxB95l.d.mts.map +1 -0
  10. package/dist/index-type-validation.d.mts +2 -2
  11. package/dist/index-type-validation.mjs +1 -1
  12. package/dist/index-type-validation.mjs.map +1 -1
  13. package/dist/{index-types-B1cf5N0F.d.mts → index-types-Czsyu7Iw.d.mts} +1 -1
  14. package/dist/{index-types-B1cf5N0F.d.mts.map → index-types-Czsyu7Iw.d.mts.map} +1 -1
  15. package/dist/index-types.d.mts +1 -1
  16. package/dist/pack-types.d.mts +1 -1
  17. package/dist/referential-action-sql.d.mts +12 -0
  18. package/dist/referential-action-sql.d.mts.map +1 -0
  19. package/dist/referential-action-sql.mjs +17 -0
  20. package/dist/referential-action-sql.mjs.map +1 -0
  21. package/dist/resolve-storage-table.d.mts +20 -0
  22. package/dist/resolve-storage-table.d.mts.map +1 -0
  23. package/dist/resolve-storage-table.mjs +42 -0
  24. package/dist/resolve-storage-table.mjs.map +1 -0
  25. package/dist/sql-storage-CXf9xjAL.d.mts +314 -0
  26. package/dist/sql-storage-CXf9xjAL.d.mts.map +1 -0
  27. package/dist/types-DEnWD3xB.d.mts +208 -0
  28. package/dist/types-DEnWD3xB.d.mts.map +1 -0
  29. package/dist/{types-YQrDHy-b.mjs → types-DqhaAjCH.mjs} +107 -28
  30. package/dist/types-DqhaAjCH.mjs.map +1 -0
  31. package/dist/types.d.mts +4 -2
  32. package/dist/types.mjs +2 -2
  33. package/dist/validators.d.mts +50 -14
  34. package/dist/validators.d.mts.map +1 -1
  35. package/dist/validators.mjs +111 -32
  36. package/dist/validators.mjs.map +1 -1
  37. package/package.json +11 -9
  38. package/src/canonicalization-hooks.ts +5 -6
  39. package/src/exports/referential-action-sql.ts +1 -0
  40. package/src/exports/resolve-storage-table.ts +1 -0
  41. package/src/exports/types.ts +6 -0
  42. package/src/factories.ts +2 -1
  43. package/src/index-type-validation.ts +1 -1
  44. package/src/ir/build-sql-namespace.ts +33 -19
  45. package/src/ir/check-constraint.ts +42 -0
  46. package/src/ir/foreign-key-reference.ts +23 -0
  47. package/src/ir/sql-storage.ts +53 -50
  48. package/src/ir/sql-unbound-namespace.ts +11 -4
  49. package/src/ir/storage-column.ts +4 -1
  50. package/src/ir/storage-table.ts +6 -0
  51. package/src/ir/storage-type-instance.ts +5 -3
  52. package/src/ir/storage-value-set.ts +42 -0
  53. package/src/referential-action-sql.ts +14 -0
  54. package/src/resolve-storage-table.ts +61 -0
  55. package/src/types.ts +13 -0
  56. package/src/validators.ts +152 -47
  57. package/dist/types-Cx_5A_L0.d.mts +0 -513
  58. package/dist/types-Cx_5A_L0.d.mts.map +0 -1
  59. package/dist/types-YQrDHy-b.mjs.map +0 -1
package/src/types.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  import type { CodecTrait } from '@prisma-next/framework-components/codec';
2
+ import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
2
3
  import type { ReferentialAction } from './ir/foreign-key';
3
4
 
5
+ export interface SqlControlDriverInstance<T extends string = string>
6
+ extends ControlDriverInstance<'sql', T> {
7
+ query<Row = Record<string, unknown>>(
8
+ sql: string,
9
+ params?: readonly unknown[],
10
+ ): Promise<{ readonly rows: Row[] }>;
11
+ }
12
+
4
13
  export { buildSqlNamespace, buildSqlNamespaceMap } from './ir/build-sql-namespace';
14
+ export { CheckConstraint, type CheckConstraintInput } from './ir/check-constraint';
5
15
  export {
6
16
  ForeignKey,
7
17
  type ForeignKeyInput,
@@ -24,6 +34,7 @@ export {
24
34
  SqlStorage,
25
35
  type SqlStorageInput,
26
36
  type SqlStorageTypeEntry,
37
+ storageTableAt,
27
38
  } from './ir/sql-storage';
28
39
  export { SqlUnboundNamespace } from './ir/sql-unbound-namespace';
29
40
  export { StorageColumn, type StorageColumnInput } from './ir/storage-column';
@@ -35,6 +46,7 @@ export {
35
46
  type StorageTypeInstanceInput,
36
47
  toStorageTypeInstance,
37
48
  } from './ir/storage-type-instance';
49
+ export { StorageValueSet, type StorageValueSetInput } from './ir/storage-value-set';
38
50
  export {
39
51
  UniqueConstraint,
40
52
  type UniqueConstraintInput,
@@ -54,6 +66,7 @@ export type SqlModelFieldStorage = {
54
66
 
55
67
  export type SqlModelStorage = {
56
68
  readonly table: string;
69
+ readonly namespaceId: string;
57
70
  readonly fields: Record<string, SqlModelFieldStorage>;
58
71
  };
59
72
 
package/src/validators.ts CHANGED
@@ -77,6 +77,14 @@ const ExecutionSchema = type({
77
77
  },
78
78
  });
79
79
 
80
+ const ValueSetRefSchema = type({
81
+ plane: "'domain' | 'storage'",
82
+ namespaceId: 'string',
83
+ entityKind: "'enum' | 'value-set'",
84
+ name: 'string',
85
+ 'spaceId?': 'string',
86
+ });
87
+
80
88
  const StorageColumnSchema = type({
81
89
  '+': 'reject',
82
90
  nativeType: 'string',
@@ -86,6 +94,7 @@ const StorageColumnSchema = type({
86
94
  'typeRef?': 'string',
87
95
  'default?': ColumnDefaultSchema,
88
96
  'control?': ControlPolicySchema,
97
+ 'valueSet?': ValueSetRefSchema,
89
98
  }).narrow((col, ctx) => {
90
99
  if (col.typeParams !== undefined && col.typeRef !== undefined) {
91
100
  return ctx.mustBe('a column with either typeParams or typeRef, not both');
@@ -105,11 +114,11 @@ const StorageTypeInstanceSchema = type
105
114
  kind: "'codec-instance'",
106
115
  codecId: 'string',
107
116
  nativeType: 'string',
108
- typeParams: 'Record<string, unknown>',
117
+ 'typeParams?': 'Record<string, unknown>',
109
118
  });
110
119
 
111
120
  /**
112
- * Postgres native enum entry under `storage.namespaces[namespaceId].enum[name]`.
121
+ * Postgres native enum entry under `storage.namespaces[namespaceId].entries.type[name]`.
113
122
  * Document-scoped `storage.types` carries codec aliases only
114
123
  * (`DocumentScopedStorageTypeSchema`).
115
124
  */
@@ -124,6 +133,31 @@ const PostgresEnumTypeSchema = type({
124
133
  /** Document-scoped `storage.types`: codec triples only. */
125
134
  const DocumentScopedStorageTypeSchema = StorageTypeInstanceSchema;
126
135
 
136
+ /**
137
+ * Storage value-set entry under `storage.namespaces[id].entries.valueSet[name]`.
138
+ * Carries a `kind: 'value-set'` discriminator (enumerable, survives JSON) and an
139
+ * ordered `values` array of codec-encoded permitted values.
140
+ */
141
+ export const StorageValueSetSchema = type({
142
+ kind: "'value-set'",
143
+ values: type.string.array().readonly(),
144
+ });
145
+
146
+ /**
147
+ * Domain enum entry under `domain.namespaces[id].enum[name]`.
148
+ * Carries the codec id and an ordered `members` array of `{name, value}` pairs.
149
+ */
150
+ export const ContractEnumSchema = type({
151
+ '+': 'reject',
152
+ codecId: 'string',
153
+ members: type({
154
+ name: 'string',
155
+ value: 'string',
156
+ })
157
+ .array()
158
+ .readonly(),
159
+ });
160
+
127
161
  const PrimaryKeySchema = type.declare<PrimaryKeyInput>().type({
128
162
  columns: type.string.array().readonly(),
129
163
  'name?': 'string',
@@ -146,6 +180,14 @@ export const ForeignKeyReferenceSchema = type({
146
180
  namespaceId: 'string',
147
181
  tableName: 'string',
148
182
  columns: type.string.array().readonly(),
183
+ 'spaceId?': 'string',
184
+ }) satisfies Type<ForeignKeyReferenceInput>;
185
+
186
+ export const ForeignKeySourceSchema = type({
187
+ '+': 'reject',
188
+ namespaceId: 'string',
189
+ tableName: 'string',
190
+ columns: type.string.array().readonly(),
149
191
  }) satisfies Type<ForeignKeyReferenceInput>;
150
192
 
151
193
  export const ReferentialActionSchema = type
@@ -153,7 +195,7 @@ export const ReferentialActionSchema = type
153
195
  .type("'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault'");
154
196
 
155
197
  export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
156
- source: ForeignKeyReferenceSchema,
198
+ source: ForeignKeySourceSchema,
157
199
  target: ForeignKeyReferenceSchema,
158
200
  'name?': 'string',
159
201
  'onDelete?': ReferentialActionSchema,
@@ -162,6 +204,13 @@ export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
162
204
  index: 'boolean',
163
205
  });
164
206
 
207
+ export const CheckConstraintSchema = type({
208
+ '+': 'reject',
209
+ name: 'string',
210
+ column: 'string',
211
+ valueSet: ValueSetRefSchema,
212
+ });
213
+
165
214
  const StorageTableSchema = type({
166
215
  '+': 'reject',
167
216
  columns: type({ '[string]': StorageColumnSchema }),
@@ -170,6 +219,7 @@ const StorageTableSchema = type({
170
219
  indexes: IndexSchema.array().readonly(),
171
220
  foreignKeys: ForeignKeySchema.array().readonly(),
172
221
  'control?': ControlPolicySchema,
222
+ 'checks?': CheckConstraintSchema.array().readonly(),
173
223
  });
174
224
 
175
225
  /**
@@ -240,7 +290,7 @@ function namespaceSlotEntrySchema(
240
290
  * Builds the per-namespace entry schema for `storage.namespaces[id]`.
241
291
  * Pack-contributed `validatorSchema` fragments — keyed by the
242
292
  * descriptor's `discriminator` — validate each entry by matching the
243
- * entry's `kind` field on the `'enum?'` slot.
293
+ * entry's `kind` field on the `'entries.type'` slot.
244
294
  */
245
295
  export function createNamespaceEntrySchema(
246
296
  fragments?: ReadonlyMap<string, Type<unknown>>,
@@ -249,9 +299,13 @@ export function createNamespaceEntrySchema(
249
299
  '+': 'reject',
250
300
  id: 'string',
251
301
  'kind?': 'string',
252
- 'tables?': type({ '[string]': StorageTableSchema }),
253
- 'enum?': type({
254
- '[string]': namespaceSlotEntrySchema(PostgresEnumTypeSchema, 'postgres-enum', fragments),
302
+ entries: type({
303
+ '+': 'reject',
304
+ 'table?': type({ '[string]': StorageTableSchema }),
305
+ 'type?': type({
306
+ '[string]': namespaceSlotEntrySchema(PostgresEnumTypeSchema, 'postgres-enum', fragments),
307
+ }),
308
+ 'valueSet?': type({ '[string]': StorageValueSetSchema }),
255
309
  }),
256
310
  }) as Type<unknown>;
257
311
  }
@@ -281,20 +335,22 @@ export function createSqlStorageSchema(
281
335
 
282
336
  const StorageSchema = createSqlStorageSchema();
283
337
 
284
- // SQL-specific namespace walk shape (`tables` is the SQL family's idiom —
285
- // the framework `Namespace` interface no longer carries it). The wider
286
- // `object` table value keeps this helper structurally compatible with
287
- // `SqlNamespace` (whose tables narrow to `StorageTable`) and the JSON
288
- // envelope variants that lose class identity.
338
+ // SQL-specific namespace walk shape (`entries.table` is the SQL family's
339
+ // idiom). The wider `object` table value keeps this helper structurally
340
+ // compatible with `SqlNamespace` and JSON envelope variants that lose class
341
+ // identity.
289
342
  type NamespacedStorageWalk = {
290
343
  readonly namespaces: Readonly<
291
- Record<string, Namespace & { readonly tables?: Readonly<Record<string, object>> }>
344
+ Record<
345
+ string,
346
+ Namespace & { readonly entries: { readonly table: Readonly<Record<string, object>> } }
347
+ >
292
348
  >;
293
349
  };
294
350
 
295
351
  function eachStorageTable(storage: NamespacedStorageWalk) {
296
352
  return Object.entries(storage.namespaces).flatMap(([namespaceId, ns]) =>
297
- Object.entries(ns.tables ?? {}).map(([tableName, table]) => ({
353
+ Object.entries(ns.entries.table).map(([tableName, table]) => ({
298
354
  namespaceId,
299
355
  tableName,
300
356
  table,
@@ -302,16 +358,6 @@ function eachStorageTable(storage: NamespacedStorageWalk) {
302
358
  );
303
359
  }
304
360
 
305
- function findStorageTableByTableName(storage: NamespacedStorageWalk, tableName: string): unknown {
306
- for (const ns of Object.values(storage.namespaces)) {
307
- const t = ns.tables?.[tableName];
308
- if (t !== undefined) {
309
- return t;
310
- }
311
- }
312
- return undefined;
313
- }
314
-
315
361
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
316
362
  return typeof value === 'object' && value !== null && !Array.isArray(value);
317
363
  }
@@ -357,6 +403,7 @@ const ModelFieldSchema = type({
357
403
  type: ContractFieldTypeSchema,
358
404
  'many?': 'true',
359
405
  'dict?': 'true',
406
+ 'valueSet?': ValueSetRefSchema,
360
407
  });
361
408
 
362
409
  const ModelStorageFieldSchema = type({
@@ -367,20 +414,44 @@ const ModelStorageFieldSchema = type({
367
414
 
368
415
  const ModelStorageSchema = type({
369
416
  table: 'string',
417
+ namespaceId: 'string',
370
418
  fields: type({ '[string]': ModelStorageFieldSchema }),
371
419
  });
372
420
 
373
- const ContractReferenceRelationSchema = type({
421
+ const ContractRelationThroughSchema = type({
422
+ '+': 'reject',
423
+ table: 'string',
424
+ namespaceId: 'string',
425
+ parentColumns: type.string.array().readonly(),
426
+ childColumns: type.string.array().readonly(),
427
+ targetColumns: type.string.array().readonly(),
428
+ });
429
+
430
+ const ContractRelationOnSchema = type({
431
+ '+': 'reject',
432
+ localFields: type.string.array().readonly(),
433
+ targetFields: type.string.array().readonly(),
434
+ });
435
+
436
+ const ContractManyToManyRelationSchema = type({
437
+ '+': 'reject',
438
+ to: CrossReferenceSchema,
439
+ cardinality: "'N:M'",
440
+ on: ContractRelationOnSchema,
441
+ through: ContractRelationThroughSchema,
442
+ });
443
+
444
+ const ContractNonJunctionRelationSchema = type({
374
445
  '+': 'reject',
375
446
  to: CrossReferenceSchema,
376
447
  cardinality: "'1:1' | '1:N' | 'N:1'",
377
- on: type({
378
- '+': 'reject',
379
- localFields: type.string.array().readonly(),
380
- targetFields: type.string.array().readonly(),
381
- }),
448
+ on: ContractRelationOnSchema,
382
449
  });
383
450
 
451
+ const ContractReferenceRelationSchema = ContractManyToManyRelationSchema.or(
452
+ ContractNonJunctionRelationSchema,
453
+ );
454
+
384
455
  const ContractEmbedRelationSchema = type({
385
456
  '+': 'reject',
386
457
  to: CrossReferenceSchema,
@@ -421,13 +492,14 @@ export function createSqlContractSchema(
421
492
  'capabilities?': 'Record<string, Record<string, boolean>>',
422
493
  'extensionPacks?': 'Record<string, unknown>',
423
494
  'meta?': ContractMetaSchema,
424
- 'defaultControl?': ControlPolicySchema,
495
+ 'defaultControlPolicy?': ControlPolicySchema,
425
496
  'roots?': type({ '[string]': CrossReferenceSchema }),
426
497
  domain: type({
427
498
  namespaces: type({
428
499
  '[string]': type({
429
500
  models: type({ '[string]': ModelSchema }),
430
501
  'valueObjects?': 'Record<string, unknown>',
502
+ 'enum?': type({ '[string]': ContractEnumSchema }),
431
503
  }),
432
504
  }),
433
505
  }),
@@ -562,6 +634,9 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
562
634
  for (const fk of table.foreignKeys) {
563
635
  registerNamedObject('foreign key', fk.name);
564
636
  }
637
+ for (const check of table.checks ?? []) {
638
+ registerNamedObject('check constraint', check.name);
639
+ }
565
640
 
566
641
  for (const [name, kinds] of namedObjects) {
567
642
  if (kinds.length > 1) {
@@ -680,6 +755,18 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
680
755
  }
681
756
  }
682
757
  }
758
+
759
+ const seenCheckDefinitions = new Set<string>();
760
+ for (const check of table.checks ?? []) {
761
+ const signature = JSON.stringify({ column: check.column, valueSet: check.valueSet });
762
+ if (seenCheckDefinitions.has(signature)) {
763
+ errors.push(
764
+ `Namespace "${namespaceId}" table "${tableName}": duplicate check constraint definition on column "${check.column}"`,
765
+ );
766
+ continue;
767
+ }
768
+ seenCheckDefinitions.add(signature);
769
+ }
683
770
  }
684
771
 
685
772
  return errors;
@@ -696,12 +783,19 @@ export function validateModelStorageReferences(contract: Contract<SqlStorage>):
696
783
  const models = namespace.models as Record<string, ContractModel<SqlModelStorage>>;
697
784
  for (const [modelName, model] of Object.entries(models)) {
698
785
  const qualifiedName = `${namespaceId}:${modelName}`;
699
- const storageTable = model.storage.table;
786
+ const storageNamespaceId = model.storage.namespaceId;
787
+ if (storageNamespaceId !== namespaceId) {
788
+ throw new ContractValidationError(
789
+ `Model "${qualifiedName}" storage.namespaceId "${storageNamespaceId}" does not match domain namespace "${namespaceId}"`,
790
+ 'storage',
791
+ );
792
+ }
700
793
 
701
- const rawTable = findStorageTableByTableName(contract.storage, storageTable);
794
+ const storageTable = model.storage.table;
795
+ const rawTable = contract.storage.namespaces[storageNamespaceId]?.entries.table[storageTable];
702
796
  if (rawTable === undefined) {
703
797
  throw new ContractValidationError(
704
- `Model "${qualifiedName}" references non-existent table "${storageTable}"`,
798
+ `Model "${qualifiedName}" references non-existent table "${storageNamespaceId}.${storageTable}"`,
705
799
  'storage',
706
800
  );
707
801
  }
@@ -781,6 +875,15 @@ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): v
781
875
  }
782
876
  }
783
877
 
878
+ for (const check of table.checks ?? []) {
879
+ if (!columnNames.has(check.column)) {
880
+ throw new ContractValidationError(
881
+ `Namespace "${namespaceId}" table "${tableName}" check constraint "${check.name}" references non-existent column "${check.column}"`,
882
+ 'storage',
883
+ );
884
+ }
885
+ }
886
+
784
887
  for (const [colName, column] of Object.entries(table.columns)) {
785
888
  if (!column.nullable && column.default?.kind === 'literal' && column.default.value === null) {
786
889
  throw new ContractValidationError(
@@ -807,23 +910,25 @@ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): v
807
910
  }
808
911
  }
809
912
 
810
- const targetNamespace = contract.storage.namespaces[fk.target.namespaceId];
811
- const referencedRaw = targetNamespace?.tables?.[fk.target.tableName];
812
- if (referencedRaw === undefined) {
813
- throw new ContractValidationError(
814
- `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`,
815
- 'storage',
816
- );
817
- }
818
- const referencedTable = referencedRaw as StorageTable;
819
- const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
820
- for (const colName of fk.target.columns) {
821
- if (!referencedColumnNames.has(colName)) {
913
+ if (fk.target.spaceId === undefined) {
914
+ const targetNamespace = contract.storage.namespaces[fk.target.namespaceId];
915
+ const referencedRaw = targetNamespace?.entries.table[fk.target.tableName];
916
+ if (referencedRaw === undefined) {
822
917
  throw new ContractValidationError(
823
- `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.target.tableName}"`,
918
+ `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`,
824
919
  'storage',
825
920
  );
826
921
  }
922
+ const referencedTable = referencedRaw as StorageTable;
923
+ const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
924
+ for (const colName of fk.target.columns) {
925
+ if (!referencedColumnNames.has(colName)) {
926
+ throw new ContractValidationError(
927
+ `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.target.tableName}"`,
928
+ 'storage',
929
+ );
930
+ }
931
+ }
827
932
  }
828
933
 
829
934
  if (fk.source.columns.length !== fk.target.columns.length) {