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

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 (60) 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-C2IALLla.d.mts +314 -0
  26. package/dist/sql-storage-C2IALLla.d.mts.map +1 -0
  27. package/dist/types-C2A2nkzv.d.mts +208 -0
  28. package/dist/types-C2A2nkzv.d.mts.map +1 -0
  29. package/dist/{types-DPkj4y3_.mjs → types-DqhaAjCH.mjs} +109 -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 +51 -14
  34. package/dist/validators.d.mts.map +1 -1
  35. package/dist/validators.mjs +116 -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/postgres-enum-storage-entry.ts +2 -0
  48. package/src/ir/sql-storage.ts +53 -50
  49. package/src/ir/sql-unbound-namespace.ts +11 -4
  50. package/src/ir/storage-column.ts +7 -1
  51. package/src/ir/storage-table.ts +10 -0
  52. package/src/ir/storage-type-instance.ts +5 -3
  53. package/src/ir/storage-value-set.ts +43 -0
  54. package/src/referential-action-sql.ts +14 -0
  55. package/src/resolve-storage-table.ts +61 -0
  56. package/src/types.ts +13 -0
  57. package/src/validators.ts +158 -46
  58. package/dist/types-ChlHcJCu.d.mts +0 -508
  59. package/dist/types-ChlHcJCu.d.mts.map +0 -1
  60. package/dist/types-DPkj4y3_.mjs.map +0 -1
@@ -0,0 +1,61 @@
1
+ import type { SqlNamespace, SqlStorage } from './ir/sql-storage';
2
+ import type { StorageTable } from './ir/storage-table';
3
+
4
+ export interface ResolvedStorageTable {
5
+ readonly namespaceId: string;
6
+ readonly table: StorageTable;
7
+ }
8
+
9
+ function tableInNamespace(
10
+ namespace: SqlNamespace | undefined,
11
+ tableName: string,
12
+ ): StorageTable | undefined {
13
+ if (namespace === undefined) {
14
+ return undefined;
15
+ }
16
+ const tables = namespace.entries.table;
17
+ if (!Object.hasOwn(tables, tableName)) {
18
+ return undefined;
19
+ }
20
+ return tables[tableName];
21
+ }
22
+
23
+ /**
24
+ * Resolve a bare storage table name to its namespace coordinate and table IR.
25
+ *
26
+ * When `namespaceId` is supplied, the table is resolved strictly within that
27
+ * namespace (no scan). When omitted, a bare name unique across namespaces
28
+ * resolves to its sole namespace; a bare name declared in more than one
29
+ * namespace throws a fail-fast diagnostic naming the candidate namespaces
30
+ * rather than silently selecting the first match.
31
+ */
32
+ export function resolveStorageTable(
33
+ storage: SqlStorage,
34
+ tableName: string,
35
+ namespaceId?: string,
36
+ ): ResolvedStorageTable | undefined {
37
+ if (namespaceId !== undefined) {
38
+ const table = tableInNamespace(storage.namespaces[namespaceId], tableName);
39
+ return table === undefined ? undefined : { namespaceId, table };
40
+ }
41
+
42
+ const matches: ResolvedStorageTable[] = [];
43
+ for (const candidateNamespaceId of Object.keys(storage.namespaces)) {
44
+ const table = tableInNamespace(storage.namespaces[candidateNamespaceId], tableName);
45
+ if (table !== undefined) {
46
+ matches.push({ namespaceId: candidateNamespaceId, table });
47
+ }
48
+ }
49
+
50
+ if (matches.length > 1) {
51
+ const candidates = matches
52
+ .map((match) => match.namespaceId)
53
+ .sort()
54
+ .join(', ');
55
+ throw new Error(
56
+ `Storage table "${tableName}" is ambiguous across namespaces [${candidates}]; qualify it with a namespace coordinate.`,
57
+ );
58
+ }
59
+
60
+ return matches[0];
61
+ }
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
@@ -33,6 +33,7 @@ type ColumnDefaultFunction = { readonly kind: 'function'; readonly expression: s
33
33
  const literalKindSchema = type("'literal'");
34
34
  const functionKindSchema = type("'function'");
35
35
  const generatorKindSchema = type("'generator'");
36
+ const ControlPolicySchema = type("'managed' | 'tolerated' | 'external' | 'observed'");
36
37
  const generatorIdSchema = type('string').narrow((value, ctx) => {
37
38
  return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value) ? true : ctx.mustBe('a flat generator id');
38
39
  });
@@ -76,6 +77,14 @@ const ExecutionSchema = type({
76
77
  },
77
78
  });
78
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
+
79
88
  const StorageColumnSchema = type({
80
89
  '+': 'reject',
81
90
  nativeType: 'string',
@@ -84,6 +93,8 @@ const StorageColumnSchema = type({
84
93
  'typeParams?': 'Record<string, unknown>',
85
94
  'typeRef?': 'string',
86
95
  'default?': ColumnDefaultSchema,
96
+ 'control?': ControlPolicySchema,
97
+ 'valueSet?': ValueSetRefSchema,
87
98
  }).narrow((col, ctx) => {
88
99
  if (col.typeParams !== undefined && col.typeRef !== undefined) {
89
100
  return ctx.mustBe('a column with either typeParams or typeRef, not both');
@@ -103,11 +114,11 @@ const StorageTypeInstanceSchema = type
103
114
  kind: "'codec-instance'",
104
115
  codecId: 'string',
105
116
  nativeType: 'string',
106
- typeParams: 'Record<string, unknown>',
117
+ 'typeParams?': 'Record<string, unknown>',
107
118
  });
108
119
 
109
120
  /**
110
- * Postgres native enum entry under `storage.namespaces[namespaceId].enum[name]`.
121
+ * Postgres native enum entry under `storage.namespaces[namespaceId].entries.type[name]`.
111
122
  * Document-scoped `storage.types` carries codec aliases only
112
123
  * (`DocumentScopedStorageTypeSchema`).
113
124
  */
@@ -116,11 +127,39 @@ const PostgresEnumTypeSchema = type({
116
127
  'name?': 'string',
117
128
  'nativeType?': 'string',
118
129
  values: type.string.array().readonly(),
130
+ 'control?': ControlPolicySchema,
119
131
  });
120
132
 
121
133
  /** Document-scoped `storage.types`: codec triples only. */
122
134
  const DocumentScopedStorageTypeSchema = StorageTypeInstanceSchema;
123
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 | number | boolean | null | unknown[] | Record<string, unknown>')
144
+ .array()
145
+ .readonly(),
146
+ });
147
+
148
+ /**
149
+ * Domain enum entry under `domain.namespaces[id].enum[name]`.
150
+ * Carries the codec id and an ordered `members` array of `{name, value}` pairs.
151
+ */
152
+ export const ContractEnumSchema = type({
153
+ '+': 'reject',
154
+ codecId: 'string',
155
+ members: type({
156
+ name: 'string',
157
+ value: 'string | number | boolean | null | unknown[] | Record<string, unknown>',
158
+ })
159
+ .array()
160
+ .readonly(),
161
+ });
162
+
124
163
  const PrimaryKeySchema = type.declare<PrimaryKeyInput>().type({
125
164
  columns: type.string.array().readonly(),
126
165
  'name?': 'string',
@@ -143,6 +182,14 @@ export const ForeignKeyReferenceSchema = type({
143
182
  namespaceId: 'string',
144
183
  tableName: 'string',
145
184
  columns: type.string.array().readonly(),
185
+ 'spaceId?': 'string',
186
+ }) satisfies Type<ForeignKeyReferenceInput>;
187
+
188
+ export const ForeignKeySourceSchema = type({
189
+ '+': 'reject',
190
+ namespaceId: 'string',
191
+ tableName: 'string',
192
+ columns: type.string.array().readonly(),
146
193
  }) satisfies Type<ForeignKeyReferenceInput>;
147
194
 
148
195
  export const ReferentialActionSchema = type
@@ -150,7 +197,7 @@ export const ReferentialActionSchema = type
150
197
  .type("'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault'");
151
198
 
152
199
  export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
153
- source: ForeignKeyReferenceSchema,
200
+ source: ForeignKeySourceSchema,
154
201
  target: ForeignKeyReferenceSchema,
155
202
  'name?': 'string',
156
203
  'onDelete?': ReferentialActionSchema,
@@ -159,6 +206,13 @@ export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
159
206
  index: 'boolean',
160
207
  });
161
208
 
209
+ export const CheckConstraintSchema = type({
210
+ '+': 'reject',
211
+ name: 'string',
212
+ column: 'string',
213
+ valueSet: ValueSetRefSchema,
214
+ });
215
+
162
216
  const StorageTableSchema = type({
163
217
  '+': 'reject',
164
218
  columns: type({ '[string]': StorageColumnSchema }),
@@ -166,6 +220,8 @@ const StorageTableSchema = type({
166
220
  uniques: UniqueConstraintSchema.array().readonly(),
167
221
  indexes: IndexSchema.array().readonly(),
168
222
  foreignKeys: ForeignKeySchema.array().readonly(),
223
+ 'control?': ControlPolicySchema,
224
+ 'checks?': CheckConstraintSchema.array().readonly(),
169
225
  });
170
226
 
171
227
  /**
@@ -236,7 +292,7 @@ function namespaceSlotEntrySchema(
236
292
  * Builds the per-namespace entry schema for `storage.namespaces[id]`.
237
293
  * Pack-contributed `validatorSchema` fragments — keyed by the
238
294
  * descriptor's `discriminator` — validate each entry by matching the
239
- * entry's `kind` field on the `'enum?'` slot.
295
+ * entry's `kind` field on the `'entries.type'` slot.
240
296
  */
241
297
  export function createNamespaceEntrySchema(
242
298
  fragments?: ReadonlyMap<string, Type<unknown>>,
@@ -245,9 +301,13 @@ export function createNamespaceEntrySchema(
245
301
  '+': 'reject',
246
302
  id: 'string',
247
303
  'kind?': 'string',
248
- 'tables?': type({ '[string]': StorageTableSchema }),
249
- 'enum?': type({
250
- '[string]': namespaceSlotEntrySchema(PostgresEnumTypeSchema, 'postgres-enum', fragments),
304
+ entries: type({
305
+ '+': 'reject',
306
+ 'table?': type({ '[string]': StorageTableSchema }),
307
+ 'type?': type({
308
+ '[string]': namespaceSlotEntrySchema(PostgresEnumTypeSchema, 'postgres-enum', fragments),
309
+ }),
310
+ 'valueSet?': type({ '[string]': StorageValueSetSchema }),
251
311
  }),
252
312
  }) as Type<unknown>;
253
313
  }
@@ -277,20 +337,22 @@ export function createSqlStorageSchema(
277
337
 
278
338
  const StorageSchema = createSqlStorageSchema();
279
339
 
280
- // SQL-specific namespace walk shape (`tables` is the SQL family's idiom —
281
- // the framework `Namespace` interface no longer carries it). The wider
282
- // `object` table value keeps this helper structurally compatible with
283
- // `SqlNamespace` (whose tables narrow to `StorageTable`) and the JSON
284
- // envelope variants that lose class identity.
340
+ // SQL-specific namespace walk shape (`entries.table` is the SQL family's
341
+ // idiom). The wider `object` table value keeps this helper structurally
342
+ // compatible with `SqlNamespace` and JSON envelope variants that lose class
343
+ // identity.
285
344
  type NamespacedStorageWalk = {
286
345
  readonly namespaces: Readonly<
287
- Record<string, Namespace & { readonly tables?: Readonly<Record<string, object>> }>
346
+ Record<
347
+ string,
348
+ Namespace & { readonly entries: { readonly table: Readonly<Record<string, object>> } }
349
+ >
288
350
  >;
289
351
  };
290
352
 
291
353
  function eachStorageTable(storage: NamespacedStorageWalk) {
292
354
  return Object.entries(storage.namespaces).flatMap(([namespaceId, ns]) =>
293
- Object.entries(ns.tables ?? {}).map(([tableName, table]) => ({
355
+ Object.entries(ns.entries.table).map(([tableName, table]) => ({
294
356
  namespaceId,
295
357
  tableName,
296
358
  table,
@@ -298,16 +360,6 @@ function eachStorageTable(storage: NamespacedStorageWalk) {
298
360
  );
299
361
  }
300
362
 
301
- function findStorageTableByTableName(storage: NamespacedStorageWalk, tableName: string): unknown {
302
- for (const ns of Object.values(storage.namespaces)) {
303
- const t = ns.tables?.[tableName];
304
- if (t !== undefined) {
305
- return t;
306
- }
307
- }
308
- return undefined;
309
- }
310
-
311
363
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
312
364
  return typeof value === 'object' && value !== null && !Array.isArray(value);
313
365
  }
@@ -353,6 +405,7 @@ const ModelFieldSchema = type({
353
405
  type: ContractFieldTypeSchema,
354
406
  'many?': 'true',
355
407
  'dict?': 'true',
408
+ 'valueSet?': ValueSetRefSchema,
356
409
  });
357
410
 
358
411
  const ModelStorageFieldSchema = type({
@@ -363,20 +416,44 @@ const ModelStorageFieldSchema = type({
363
416
 
364
417
  const ModelStorageSchema = type({
365
418
  table: 'string',
419
+ namespaceId: 'string',
366
420
  fields: type({ '[string]': ModelStorageFieldSchema }),
367
421
  });
368
422
 
369
- const ContractReferenceRelationSchema = type({
423
+ const ContractRelationThroughSchema = type({
424
+ '+': 'reject',
425
+ table: 'string',
426
+ namespaceId: 'string',
427
+ parentColumns: type.string.array().readonly(),
428
+ childColumns: type.string.array().readonly(),
429
+ targetColumns: type.string.array().readonly(),
430
+ });
431
+
432
+ const ContractRelationOnSchema = type({
433
+ '+': 'reject',
434
+ localFields: type.string.array().readonly(),
435
+ targetFields: type.string.array().readonly(),
436
+ });
437
+
438
+ const ContractManyToManyRelationSchema = type({
439
+ '+': 'reject',
440
+ to: CrossReferenceSchema,
441
+ cardinality: "'N:M'",
442
+ on: ContractRelationOnSchema,
443
+ through: ContractRelationThroughSchema,
444
+ });
445
+
446
+ const ContractNonJunctionRelationSchema = type({
370
447
  '+': 'reject',
371
448
  to: CrossReferenceSchema,
372
449
  cardinality: "'1:1' | '1:N' | 'N:1'",
373
- on: type({
374
- '+': 'reject',
375
- localFields: type.string.array().readonly(),
376
- targetFields: type.string.array().readonly(),
377
- }),
450
+ on: ContractRelationOnSchema,
378
451
  });
379
452
 
453
+ const ContractReferenceRelationSchema = ContractManyToManyRelationSchema.or(
454
+ ContractNonJunctionRelationSchema,
455
+ );
456
+
380
457
  const ContractEmbedRelationSchema = type({
381
458
  '+': 'reject',
382
459
  to: CrossReferenceSchema,
@@ -417,12 +494,14 @@ export function createSqlContractSchema(
417
494
  'capabilities?': 'Record<string, Record<string, boolean>>',
418
495
  'extensionPacks?': 'Record<string, unknown>',
419
496
  'meta?': ContractMetaSchema,
497
+ 'defaultControlPolicy?': ControlPolicySchema,
420
498
  'roots?': type({ '[string]': CrossReferenceSchema }),
421
499
  domain: type({
422
500
  namespaces: type({
423
501
  '[string]': type({
424
502
  models: type({ '[string]': ModelSchema }),
425
503
  'valueObjects?': 'Record<string, unknown>',
504
+ 'enum?': type({ '[string]': ContractEnumSchema }),
426
505
  }),
427
506
  }),
428
507
  }),
@@ -557,6 +636,9 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
557
636
  for (const fk of table.foreignKeys) {
558
637
  registerNamedObject('foreign key', fk.name);
559
638
  }
639
+ for (const check of table.checks ?? []) {
640
+ registerNamedObject('check constraint', check.name);
641
+ }
560
642
 
561
643
  for (const [name, kinds] of namedObjects) {
562
644
  if (kinds.length > 1) {
@@ -675,6 +757,18 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
675
757
  }
676
758
  }
677
759
  }
760
+
761
+ const seenCheckDefinitions = new Set<string>();
762
+ for (const check of table.checks ?? []) {
763
+ const signature = JSON.stringify({ column: check.column, valueSet: check.valueSet });
764
+ if (seenCheckDefinitions.has(signature)) {
765
+ errors.push(
766
+ `Namespace "${namespaceId}" table "${tableName}": duplicate check constraint definition on column "${check.column}"`,
767
+ );
768
+ continue;
769
+ }
770
+ seenCheckDefinitions.add(signature);
771
+ }
678
772
  }
679
773
 
680
774
  return errors;
@@ -691,12 +785,19 @@ export function validateModelStorageReferences(contract: Contract<SqlStorage>):
691
785
  const models = namespace.models as Record<string, ContractModel<SqlModelStorage>>;
692
786
  for (const [modelName, model] of Object.entries(models)) {
693
787
  const qualifiedName = `${namespaceId}:${modelName}`;
694
- const storageTable = model.storage.table;
788
+ const storageNamespaceId = model.storage.namespaceId;
789
+ if (storageNamespaceId !== namespaceId) {
790
+ throw new ContractValidationError(
791
+ `Model "${qualifiedName}" storage.namespaceId "${storageNamespaceId}" does not match domain namespace "${namespaceId}"`,
792
+ 'storage',
793
+ );
794
+ }
695
795
 
696
- const rawTable = findStorageTableByTableName(contract.storage, storageTable);
796
+ const storageTable = model.storage.table;
797
+ const rawTable = contract.storage.namespaces[storageNamespaceId]?.entries.table[storageTable];
697
798
  if (rawTable === undefined) {
698
799
  throw new ContractValidationError(
699
- `Model "${qualifiedName}" references non-existent table "${storageTable}"`,
800
+ `Model "${qualifiedName}" references non-existent table "${storageNamespaceId}.${storageTable}"`,
700
801
  'storage',
701
802
  );
702
803
  }
@@ -776,6 +877,15 @@ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): v
776
877
  }
777
878
  }
778
879
 
880
+ for (const check of table.checks ?? []) {
881
+ if (!columnNames.has(check.column)) {
882
+ throw new ContractValidationError(
883
+ `Namespace "${namespaceId}" table "${tableName}" check constraint "${check.name}" references non-existent column "${check.column}"`,
884
+ 'storage',
885
+ );
886
+ }
887
+ }
888
+
779
889
  for (const [colName, column] of Object.entries(table.columns)) {
780
890
  if (!column.nullable && column.default?.kind === 'literal' && column.default.value === null) {
781
891
  throw new ContractValidationError(
@@ -802,23 +912,25 @@ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): v
802
912
  }
803
913
  }
804
914
 
805
- const targetNamespace = contract.storage.namespaces[fk.target.namespaceId];
806
- const referencedRaw = targetNamespace?.tables?.[fk.target.tableName];
807
- if (referencedRaw === undefined) {
808
- throw new ContractValidationError(
809
- `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`,
810
- 'storage',
811
- );
812
- }
813
- const referencedTable = referencedRaw as StorageTable;
814
- const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
815
- for (const colName of fk.target.columns) {
816
- if (!referencedColumnNames.has(colName)) {
915
+ if (fk.target.spaceId === undefined) {
916
+ const targetNamespace = contract.storage.namespaces[fk.target.namespaceId];
917
+ const referencedRaw = targetNamespace?.entries.table[fk.target.tableName];
918
+ if (referencedRaw === undefined) {
817
919
  throw new ContractValidationError(
818
- `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.target.tableName}"`,
920
+ `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`,
819
921
  'storage',
820
922
  );
821
923
  }
924
+ const referencedTable = referencedRaw as StorageTable;
925
+ const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
926
+ for (const colName of fk.target.columns) {
927
+ if (!referencedColumnNames.has(colName)) {
928
+ throw new ContractValidationError(
929
+ `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.target.tableName}"`,
930
+ 'storage',
931
+ );
932
+ }
933
+ }
822
934
  }
823
935
 
824
936
  if (fk.source.columns.length !== fk.target.columns.length) {