@prisma-next/sql-contract 0.13.0-dev.26 → 0.13.0-dev.28

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.
@@ -4,11 +4,12 @@ import {
4
4
  NamespaceBase,
5
5
  UNBOUND_NAMESPACE_ID,
6
6
  } from '@prisma-next/framework-components/ir';
7
- import { blindCast, castAs } from '@prisma-next/utils/casts';
8
- import type { SqlNamespace, SqlNamespaceTablesInput } from './sql-storage';
7
+ import { blindCast } from '@prisma-next/utils/casts';
8
+ import { ifDefined } from '@prisma-next/utils/defined';
9
+ import type { SqlNamespace, SqlNamespaceEntries, SqlNamespaceTablesInput } from './sql-storage';
9
10
  import { SqlUnboundNamespace } from './sql-unbound-namespace';
10
- import { StorageTable } from './storage-table';
11
- import { StorageValueSet } from './storage-value-set';
11
+ import { StorageTable, type StorageTableInput } from './storage-table';
12
+ import { StorageValueSet, type StorageValueSetInput } from './storage-value-set';
12
13
 
13
14
  const SQL_NAMESPACE_KIND = 'sql-namespace' as const;
14
15
 
@@ -27,39 +28,63 @@ class SqlBoundNamespace extends NamespaceBase {
27
28
  declare readonly kind: string;
28
29
 
29
30
  readonly id: string;
30
- readonly entries: Readonly<{
31
- readonly table: Readonly<Record<string, StorageTable>>;
32
- readonly valueSet?: Readonly<Record<string, StorageValueSet>>;
33
- }>;
31
+ readonly entries: SqlNamespaceEntries;
34
32
 
35
33
  static fromTablesInput(input: SqlNamespaceTablesInput): SqlNamespace {
36
- const tableCount = Object.keys(input.entries.table).length;
37
- const hasValueSets =
38
- input.entries.valueSet !== undefined && Object.keys(input.entries.valueSet).length > 0;
39
- if (input.id === UNBOUND_NAMESPACE_ID && tableCount === 0 && !hasValueSets) {
40
- return castAs<SqlNamespace>(SqlUnboundNamespace.instance);
34
+ const tableKind = input.entries['table'];
35
+ const tableCount = tableKind !== undefined ? Object.keys(tableKind).length : 0;
36
+ const valueSetKind = input.entries['valueSet'];
37
+ const hasValueSets = valueSetKind !== undefined && Object.keys(valueSetKind).length > 0;
38
+ const hasUnknownKinds = Object.keys(input.entries).some(
39
+ (kind) => kind !== 'table' && kind !== 'valueSet',
40
+ );
41
+ if (
42
+ input.id === UNBOUND_NAMESPACE_ID &&
43
+ tableCount === 0 &&
44
+ !hasValueSets &&
45
+ !hasUnknownKinds
46
+ ) {
47
+ return SqlUnboundNamespace.instance;
41
48
  }
42
- return castAs<SqlNamespace>(new SqlBoundNamespace(input));
49
+ return new SqlBoundNamespace(input);
43
50
  }
44
51
 
45
52
  private constructor(input: SqlNamespaceTablesInput) {
46
53
  super();
47
54
  this.id = input.id;
48
- const table = Object.freeze(
49
- Object.fromEntries(
50
- Object.entries(input.entries.table).map(([k, v]) => [k, new StorageTable(v)]),
51
- ),
52
- );
53
- if (input.entries.valueSet !== undefined) {
54
- const valueSet = Object.freeze(
55
- Object.fromEntries(
56
- Object.entries(input.entries.valueSet).map(([k, v]) => [k, new StorageValueSet(v)]),
57
- ),
58
- );
59
- this.entries = Object.freeze({ table, valueSet });
60
- } else {
61
- this.entries = Object.freeze({ table });
55
+
56
+ const carried: Record<string, Readonly<Record<string, unknown>>> = {};
57
+ let table: Readonly<Record<string, StorageTable>> = Object.freeze({});
58
+ let valueSet: Readonly<Record<string, StorageValueSet>> | undefined;
59
+ for (const [kind, rawMap] of Object.entries(input.entries)) {
60
+ if (kind === 'table') {
61
+ const tableMap: Record<string, StorageTable> = {};
62
+ for (const [name, v] of Object.entries(
63
+ blindCast<
64
+ Record<string, StorageTableInput>,
65
+ 'entries[table] holds StorageTableInput by construction'
66
+ >(rawMap),
67
+ )) {
68
+ tableMap[name] = new StorageTable(v);
69
+ }
70
+ table = Object.freeze(tableMap);
71
+ } else if (kind === 'valueSet') {
72
+ const vsMap: Record<string, StorageValueSet> = {};
73
+ for (const [name, v] of Object.entries(
74
+ blindCast<
75
+ Record<string, StorageValueSetInput>,
76
+ 'entries[valueSet] holds StorageValueSetInput by construction'
77
+ >(rawMap),
78
+ )) {
79
+ vsMap[name] = new StorageValueSet(v);
80
+ }
81
+ valueSet = Object.freeze(vsMap);
82
+ } else {
83
+ carried[kind] = Object.freeze(rawMap);
84
+ }
62
85
  }
86
+
87
+ this.entries = Object.freeze({ ...carried, table, ...ifDefined('valueSet', valueSet) });
63
88
  Object.defineProperty(this, 'kind', {
64
89
  value: SQL_NAMESPACE_KIND,
65
90
  writable: false,
@@ -69,6 +94,14 @@ class SqlBoundNamespace extends NamespaceBase {
69
94
  freezeNode(this);
70
95
  }
71
96
 
97
+ get table(): Readonly<Record<string, StorageTable>> {
98
+ return this.entries.table ?? Object.freeze({});
99
+ }
100
+
101
+ get valueSet(): Readonly<Record<string, StorageValueSet>> | undefined {
102
+ return this.entries.valueSet;
103
+ }
104
+
72
105
  qualifyTable(tableName: string): string {
73
106
  if (this.id === UNBOUND_NAMESPACE_ID) {
74
107
  return `"${tableName}"`;
@@ -88,10 +121,7 @@ export function buildSqlNamespaceMap(
88
121
  Object.entries(namespaces).map(([nsKey, ns]) => [
89
122
  nsKey,
90
123
  isMaterializedSqlNamespace(ns)
91
- ? blindCast<
92
- SqlNamespace,
93
- 'a materialised SQL-family namespace entry in a namespace map is a SqlNamespace'
94
- >(ns)
124
+ ? ns
95
125
  : SqlBoundNamespace.fromTablesInput(
96
126
  blindCast<
97
127
  SqlNamespaceTablesInput,
@@ -1,14 +1,14 @@
1
1
  import type { StorageHashBase } from '@prisma-next/contract/types';
2
2
  import { freezeNode, type Namespace, type Storage } from '@prisma-next/framework-components/ir';
3
3
  import { SqlNode } from './sql-node';
4
- import type { StorageTable, StorageTableInput } from './storage-table';
4
+ import type { StorageTable } from './storage-table';
5
5
  import {
6
6
  isStorageTypeInstance,
7
7
  type StorageTypeInstance,
8
8
  type StorageTypeInstanceInput,
9
9
  toStorageTypeInstance,
10
10
  } from './storage-type-instance';
11
- import type { StorageValueSet, StorageValueSetInput } from './storage-value-set';
11
+ import type { StorageValueSet } from './storage-value-set';
12
12
 
13
13
  /**
14
14
  * Polymorphic value type for document-scoped `SqlStorage.types` entries
@@ -21,10 +21,7 @@ export type SqlStorageTypeEntry = StorageTypeInstance | StorageTypeInstanceInput
21
21
 
22
22
  export interface SqlNamespaceTablesInput {
23
23
  readonly id: string;
24
- readonly entries: {
25
- readonly table: Record<string, StorageTable | StorageTableInput>;
26
- readonly valueSet?: Record<string, StorageValueSet | StorageValueSetInput>;
27
- };
24
+ readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
28
25
  }
29
26
 
30
27
  export interface SqlStorageInput<THash extends string = string> {
@@ -54,17 +51,26 @@ export interface SqlStorageInput<THash extends string = string> {
54
51
  * the per-target serializer's responsibility (so the family base does
55
52
  * not import target-specific subclasses).
56
53
  */
57
- // SQL concretions store `StorageTable` values under `entries.table`.
58
- // Mongo namespaces carry `entries.collection` instead. The wider
59
- // `Record<string, object>` on `StorageTable` is only there so emitted
60
- // `contract.d.ts` table literals (which lack the runtime `kind`
61
- // discriminator on `StorageTable`) structurally satisfy the slot without
62
- // a class-instance check.
54
+ /**
55
+ * The typed `entries` shape for SQL family namespaces. The open dictionary
56
+ * is intersected with optional known-kind maps so that `ns.entries.table`
57
+ * and `ns.entries.valueSet` resolve without a cast, while unknown pack-
58
+ * contributed kinds remain valid (the `Record` part allows any string key).
59
+ */
60
+ export type SqlNamespaceEntries = Readonly<Record<string, Readonly<Record<string, unknown>>>> & {
61
+ readonly table?: Readonly<Record<string, StorageTable>>;
62
+ readonly valueSet?: Readonly<Record<string, StorageValueSet>>;
63
+ };
64
+
65
+ /**
66
+ * SQL family namespace. `entries` is the open ADR 224 dictionary —
67
+ * `entries[entityKind][entityName]` addresses any entity. Emitted
68
+ * contract literals satisfy this structurally (no prototype getters
69
+ * needed). For typed access to specific kinds, use the class getters
70
+ * on the concretion or `ns.entries.table` / `ns.entries.valueSet` directly.
71
+ */
63
72
  export type SqlNamespace = Namespace & {
64
- readonly entries: Readonly<{
65
- readonly table: Readonly<Record<string, StorageTable>>;
66
- readonly valueSet?: Readonly<Record<string, StorageValueSet>>;
67
- }>;
73
+ readonly entries: SqlNamespaceEntries;
68
74
  /**
69
75
  * Render a dialect-qualified table reference for runtime SQL emission.
70
76
  * Present on materialised target concretions (`PostgresSchema`,
@@ -94,14 +100,6 @@ export class SqlStorage<THash extends string = string> extends SqlNode implement
94
100
  }
95
101
  }
96
102
 
97
- export function storageTableAt(
98
- storage: SqlStorage,
99
- namespaceId: string,
100
- tableName: string,
101
- ): StorageTable | undefined {
102
- return storage.namespaces[namespaceId]?.entries.table[tableName];
103
- }
104
-
105
103
  /**
106
104
  * Strict polymorphic-slot dispatch for `SqlStorage.types` entries.
107
105
  * Every entry must carry a `kind: 'codec-instance'` discriminator or
@@ -3,6 +3,8 @@ import {
3
3
  NamespaceBase,
4
4
  UNBOUND_NAMESPACE_ID,
5
5
  } from '@prisma-next/framework-components/ir';
6
+ import { blindCast } from '@prisma-next/utils/casts';
7
+ import type { SqlNamespaceEntries } from './sql-storage';
6
8
  import type { StorageTable } from './storage-table';
7
9
 
8
10
  /**
@@ -40,9 +42,12 @@ export class SqlUnboundNamespace extends NamespaceBase {
40
42
  static readonly instance: SqlUnboundNamespace = new SqlUnboundNamespace();
41
43
 
42
44
  readonly id = UNBOUND_NAMESPACE_ID;
43
- readonly entries: Readonly<{
44
- readonly table: Readonly<Record<string, StorageTable>>;
45
- }> = Object.freeze({ table: Object.freeze({}) });
45
+ readonly entries: SqlNamespaceEntries = Object.freeze({
46
+ table: blindCast<
47
+ Readonly<Record<string, StorageTable>>,
48
+ 'empty frozen map is a valid Readonly<Record<string, StorageTable>>'
49
+ >(Object.freeze({})),
50
+ });
46
51
  declare readonly kind: string;
47
52
 
48
53
  private constructor() {
@@ -56,6 +61,13 @@ export class SqlUnboundNamespace extends NamespaceBase {
56
61
  freezeNode(this);
57
62
  }
58
63
 
64
+ get table(): Readonly<Record<string, StorageTable>> {
65
+ return blindCast<
66
+ Readonly<Record<string, StorageTable>>,
67
+ 'entries[table] holds only StorageTable by construction'
68
+ >(this.entries['table']);
69
+ }
70
+
59
71
  qualifyTable(tableName: string): string {
60
72
  return `"${tableName}"`;
61
73
  }
@@ -1,4 +1,5 @@
1
- import type { SqlNamespace, SqlStorage } from './ir/sql-storage';
1
+ import { entityAt } from '@prisma-next/framework-components/ir';
2
+ import type { SqlStorage } from './ir/sql-storage';
2
3
  import type { StorageTable } from './ir/storage-table';
3
4
 
4
5
  export interface ResolvedStorageTable {
@@ -6,20 +7,6 @@ export interface ResolvedStorageTable {
6
7
  readonly table: StorageTable;
7
8
  }
8
9
 
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
10
  /**
24
11
  * Resolve a bare storage table name to its namespace coordinate and table IR.
25
12
  *
@@ -35,13 +22,21 @@ export function resolveStorageTable(
35
22
  namespaceId?: string,
36
23
  ): ResolvedStorageTable | undefined {
37
24
  if (namespaceId !== undefined) {
38
- const table = tableInNamespace(storage.namespaces[namespaceId], tableName);
25
+ const table = entityAt<StorageTable>(storage, {
26
+ namespaceId,
27
+ entityKind: 'table',
28
+ entityName: tableName,
29
+ });
39
30
  return table === undefined ? undefined : { namespaceId, table };
40
31
  }
41
32
 
42
33
  const matches: ResolvedStorageTable[] = [];
43
34
  for (const candidateNamespaceId of Object.keys(storage.namespaces)) {
44
- const table = tableInNamespace(storage.namespaces[candidateNamespaceId], tableName);
35
+ const table = entityAt<StorageTable>(storage, {
36
+ namespaceId: candidateNamespaceId,
37
+ entityKind: 'table',
38
+ entityName: tableName,
39
+ });
45
40
  if (table !== undefined) {
46
41
  matches.push({ namespaceId: candidateNamespaceId, table });
47
42
  }
package/src/types.ts CHANGED
@@ -30,11 +30,12 @@ export { PrimaryKey, type PrimaryKeyInput } from './ir/primary-key';
30
30
  export { Index, type IndexInput } from './ir/sql-index';
31
31
  export { SqlNode } from './ir/sql-node';
32
32
  export {
33
+ type SqlNamespace,
34
+ type SqlNamespaceEntries,
33
35
  type SqlNamespaceTablesInput,
34
36
  SqlStorage,
35
37
  type SqlStorageInput,
36
38
  type SqlStorageTypeEntry,
37
- storageTableAt,
38
39
  } from './ir/sql-storage';
39
40
  export { SqlUnboundNamespace } from './ir/sql-unbound-namespace';
40
41
  export { StorageColumn, type StorageColumnInput } from './ir/storage-column';
package/src/validators.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  } from '@prisma-next/contract/types';
8
8
  import { validateContractDomain } from '@prisma-next/contract/validate-domain';
9
9
  import { type Namespace, UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
10
- import { blindCast } from '@prisma-next/utils/casts';
10
+ import { blindCast, castAs } from '@prisma-next/utils/casts';
11
11
  import { ifDefined } from '@prisma-next/utils/defined';
12
12
  import { type Type, type } from 'arktype';
13
13
  import { buildSqlNamespaceMap } from './ir/build-sql-namespace';
@@ -125,19 +125,6 @@ const StorageTypeInstanceSchema = type
125
125
  'typeParams?': 'Record<string, unknown>',
126
126
  });
127
127
 
128
- /**
129
- * Postgres native enum entry under `storage.namespaces[namespaceId].entries.type[name]`.
130
- * Document-scoped `storage.types` carries codec aliases only
131
- * (`DocumentScopedStorageTypeSchema`).
132
- */
133
- const PostgresEnumTypeSchema = type({
134
- kind: "'postgres-enum'",
135
- 'name?': 'string',
136
- 'nativeType?': 'string',
137
- values: type.string.array().readonly(),
138
- 'control?': ControlPolicySchema,
139
- });
140
-
141
128
  /** Document-scoped `storage.types`: codec triples only. */
142
129
  const DocumentScopedStorageTypeSchema = StorageTypeInstanceSchema;
143
130
 
@@ -233,103 +220,88 @@ const StorageTableSchema = type({
233
220
  });
234
221
 
235
222
  /**
236
- * Re-exported so target packs can register their `validatorSchema`
237
- * fragment without re-declaring the schema for the kinds the family
238
- * core already validates. Full extraction of enum-specific schemas
239
- * into the Postgres pack is a follow-up; today the symbol lives here.
240
- */
241
- export { PostgresEnumTypeSchema };
242
-
243
- /**
244
- * Composes a hardcoded family `fallback` schema with optional
245
- * pack-contributed `fragments` keyed by the entry's `kind`
246
- * discriminator. The composition is **additive**, not substitutive:
247
- *
248
- * - No fragments registered → entries are validated by `fallback`
249
- * alone (the unchanged baseline).
250
- * - An entry's `kind` matches `fallbackKind` AND a fragment for that
251
- * kind is registered → the entry must pass **both** `fallback` and
252
- * the fragment. This preserves family-owned invariants (e.g. the
253
- * built-in `PostgresEnumType` shape) even when a pack contributes
254
- * its own schema for the same kind.
255
- * - An entry's `kind` matches a registered fragment for some
256
- * non-fallback kind → the fragment alone validates the entry.
257
- * `fallback` is family-specific (validates a single hardcoded kind)
258
- * and would reject any other kind, so it does not apply here.
259
- * - An entry's `kind` matches no fragment → fall through to
260
- * `fallback`.
223
+ * Composes the single entry-validator registry consulted during
224
+ * structural validation. SQL core registers its own kinds (`'table'`,
225
+ * `'valueSet'`) into the same registry targets extend — there is no
226
+ * separate built-in fallback tier. Target packs pass their contributed
227
+ * kinds (e.g. postgres passes `'type'` → the postgres enum schema).
261
228
  */
262
- function namespaceSlotEntrySchema(
263
- fallback: Type<unknown>,
264
- fallbackKind: string,
265
- fragments?: ReadonlyMap<string, Type<unknown>>,
266
- ): Type<unknown> {
267
- if (fragments === undefined || fragments.size === 0) {
268
- return fallback;
269
- }
270
- return type('unknown').narrow((entry, ctx) => {
271
- if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
272
- return ctx.mustBe('an object');
273
- }
274
- const kind = (entry as { kind?: unknown }).kind;
275
- if (typeof kind === 'string') {
276
- const fragment = fragments.get(kind);
277
- if (fragment !== undefined) {
278
- if (kind === fallbackKind) {
279
- const baseParsed = fallback(entry);
280
- if (baseParsed instanceof type.errors) {
281
- return ctx.reject({ expected: baseParsed.summary });
282
- }
283
- }
284
- const parsed = fragment(entry);
285
- if (parsed instanceof type.errors) {
286
- return ctx.reject({ expected: parsed.summary });
287
- }
288
- return true;
229
+ export function createSqlEntrySchemaRegistry(
230
+ packSchemas?: ReadonlyMap<string, Type<unknown>>,
231
+ ): ReadonlyMap<string, Type<unknown>> {
232
+ const registry = new Map<string, Type<unknown>>([
233
+ ['table', castAs<Type<unknown>>(StorageTableSchema)],
234
+ ['valueSet', castAs<Type<unknown>>(StorageValueSetSchema)],
235
+ ]);
236
+ if (packSchemas !== undefined) {
237
+ for (const [kind, schema] of packSchemas) {
238
+ if (registry.has(kind)) {
239
+ throw new Error(
240
+ `createSqlEntrySchemaRegistry: pack schema "${kind}" collides with a core kind — pack schemas cannot override "table" or "valueSet"`,
241
+ );
289
242
  }
243
+ registry.set(kind, schema);
290
244
  }
291
- const parsed = fallback(entry);
292
- if (parsed instanceof type.errors) {
293
- return ctx.reject({ expected: parsed.summary });
294
- }
295
- return true;
296
- });
245
+ }
246
+ return registry;
297
247
  }
298
248
 
299
249
  /**
300
250
  * Builds the per-namespace entry schema for `storage.namespaces[id]`.
301
- * Pack-contributed `validatorSchema` fragments — keyed by the
302
- * descriptor's `discriminator` validate each entry by matching the
303
- * entry's `kind` field on the `'entries.type'` slot.
251
+ *
252
+ * Validation is registry-driven: the `registry` parameter maps each
253
+ * entries key to an arktype schema that validates a single inner-map
254
+ * value for that kind. Compose the registry with
255
+ * {@link createSqlEntrySchemaRegistry} — SQL core's kinds and pack
256
+ * contributions live in the same map. An unregistered key fails
257
+ * validation naming the kind and the namespace id, so validation fails
258
+ * closed.
304
259
  */
305
260
  export function createNamespaceEntrySchema(
306
- fragments?: ReadonlyMap<string, Type<unknown>>,
261
+ registry: ReadonlyMap<string, Type<unknown>>,
307
262
  ): Type<unknown> {
308
263
  return type({
309
264
  '+': 'reject',
310
265
  id: 'string',
311
266
  'kind?': 'string',
312
- entries: type({
313
- '+': 'reject',
314
- 'table?': type({ '[string]': StorageTableSchema }),
315
- 'type?': type({
316
- '[string]': namespaceSlotEntrySchema(PostgresEnumTypeSchema, 'postgres-enum', fragments),
317
- }),
318
- 'valueSet?': type({ '[string]': StorageValueSetSchema }),
319
- }),
267
+ entries: 'object',
268
+ }).narrow((ns, ctx) => {
269
+ if (!isPlainRecord(ns.entries)) {
270
+ return ctx.mustBe('an entries object');
271
+ }
272
+ for (const [key, innerMap] of Object.entries(ns.entries)) {
273
+ const entrySchema = registry.get(key);
274
+ if (entrySchema === undefined) {
275
+ return ctx.reject({
276
+ expected: `entries key "${key}" in namespace "${ns.id}" is not a registered entity kind`,
277
+ });
278
+ }
279
+ if (!isPlainRecord(innerMap)) {
280
+ return ctx.reject({
281
+ expected: `entries["${key}"] in namespace "${ns.id}" must be an object`,
282
+ });
283
+ }
284
+ for (const [, value] of Object.entries(innerMap)) {
285
+ const parsed = entrySchema(value);
286
+ if (parsed instanceof type.errors) {
287
+ return ctx.reject({ expected: parsed.summary });
288
+ }
289
+ }
290
+ }
291
+ return true;
320
292
  }) as Type<unknown>;
321
293
  }
322
294
 
323
295
  /**
324
296
  * Builds the storage schema. Pack contributions reach the per-namespace
325
297
  * entry shape through {@link createNamespaceEntrySchema}; the
326
- * document-scoped `storage.types` slot (codec triples only) and the
298
+ * document-scoped `storage.types` field (codec triples only) and the
327
299
  * storage hash stay family-shared.
328
300
  */
329
301
  export function createSqlStorageSchema(
330
- fragments?: ReadonlyMap<string, Type<unknown>>,
302
+ registry: ReadonlyMap<string, Type<unknown>>,
331
303
  ): Type<unknown> {
332
- const namespaceEntry = createNamespaceEntrySchema(fragments);
304
+ const namespaceEntry = createNamespaceEntrySchema(registry);
333
305
  return type({
334
306
  '+': 'reject',
335
307
  storageHash: 'string',
@@ -343,24 +315,20 @@ export function createSqlStorageSchema(
343
315
  }) as Type<unknown>;
344
316
  }
345
317
 
346
- const StorageSchema = createSqlStorageSchema();
318
+ const StorageSchema = createSqlStorageSchema(createSqlEntrySchemaRegistry());
347
319
 
348
- // SQL-specific namespace walk shape (`entries.table` is the SQL family's
349
- // idiom). The wider `object` table value keeps this helper structurally
350
- // compatible with `SqlNamespace` and JSON envelope variants that lose class
351
- // identity.
352
320
  type NamespacedStorageWalk = {
353
321
  readonly namespaces: Readonly<
354
322
  Record<
355
323
  string,
356
- Namespace & { readonly entries: { readonly table: Readonly<Record<string, object>> } }
324
+ Namespace & { readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>> }
357
325
  >
358
326
  >;
359
327
  };
360
328
 
361
329
  function eachStorageTable(storage: NamespacedStorageWalk) {
362
330
  return Object.entries(storage.namespaces).flatMap(([namespaceId, ns]) =>
363
- Object.entries(ns.entries.table).map(([tableName, table]) => ({
331
+ Object.entries(ns.entries['table'] ?? {}).map(([tableName, table]) => ({
364
332
  namespaceId,
365
333
  tableName,
366
334
  table,
@@ -369,7 +337,9 @@ function eachStorageTable(storage: NamespacedStorageWalk) {
369
337
  }
370
338
 
371
339
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
372
- return typeof value === 'object' && value !== null && !Array.isArray(value);
340
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
341
+ const proto = Object.getPrototypeOf(value) as unknown;
342
+ return proto === Object.prototype || proto === null;
373
343
  }
374
344
 
375
345
  function findDuplicateValue(values: readonly string[]): string | undefined {
@@ -490,9 +460,9 @@ const ContractMetaSchema = type({
490
460
  * of the contract envelope is family-shared.
491
461
  */
492
462
  export function createSqlContractSchema(
493
- fragments?: ReadonlyMap<string, Type<unknown>>,
463
+ registry: ReadonlyMap<string, Type<unknown>>,
494
464
  ): Type<unknown> {
495
- const storage = createSqlStorageSchema(fragments);
465
+ const storage = createSqlStorageSchema(registry);
496
466
  return type({
497
467
  '+': 'reject',
498
468
  target: 'string',
@@ -518,7 +488,7 @@ export function createSqlContractSchema(
518
488
  }) as Type<unknown>;
519
489
  }
520
490
 
521
- const SqlContractSchema = createSqlContractSchema();
491
+ const SqlContractSchema = createSqlContractSchema(createSqlEntrySchemaRegistry());
522
492
 
523
493
  // NOTE: StorageColumnSchema, StorageTableSchema, and StorageSchema use bare type()
524
494
  // instead of type.declare<T>().type() because the ColumnDefault union's value field
@@ -802,7 +772,8 @@ export function validateModelStorageReferences(contract: Contract<SqlStorage>):
802
772
  }
803
773
 
804
774
  const storageTable = model.storage.table;
805
- const rawTable = contract.storage.namespaces[storageNamespaceId]?.entries.table[storageTable];
775
+ const storageNs = contract.storage.namespaces[storageNamespaceId];
776
+ const rawTable = storageNs?.entries.table?.[storageTable];
806
777
  if (rawTable === undefined) {
807
778
  throw new ContractValidationError(
808
779
  `Model "${qualifiedName}" references non-existent table "${storageNamespaceId}.${storageTable}"`,
@@ -922,7 +893,7 @@ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): v
922
893
 
923
894
  if (fk.target.spaceId === undefined) {
924
895
  const targetNamespace = contract.storage.namespaces[fk.target.namespaceId];
925
- const referencedRaw = targetNamespace?.entries.table[fk.target.tableName];
896
+ const referencedRaw = targetNamespace?.entries.table?.[fk.target.tableName];
926
897
  if (referencedRaw === undefined) {
927
898
  throw new ContractValidationError(
928
899
  `Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`,