@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.
- package/dist/factories.d.mts +2 -2
- package/dist/factories.mjs +1 -1
- package/dist/index-type-validation.d.mts +1 -1
- package/dist/index-type-validation.mjs +8 -11
- package/dist/index-type-validation.mjs.map +1 -1
- package/dist/resolve-storage-table.d.mts +1 -1
- package/dist/resolve-storage-table.d.mts.map +1 -1
- package/dist/resolve-storage-table.mjs +11 -8
- package/dist/resolve-storage-table.mjs.map +1 -1
- package/dist/{sql-storage-DChMDNZo.d.mts → sql-storage-Cply4666.d.mts} +21 -11
- package/dist/{sql-storage-DChMDNZo.d.mts.map → sql-storage-Cply4666.d.mts.map} +1 -1
- package/dist/{types-DA_Jsl9r.mjs → types-DkYKvPMB.mjs} +39 -20
- package/dist/types-DkYKvPMB.mjs.map +1 -0
- package/dist/{types-DiR-feh1.d.mts → types-XKDJNOD1.d.mts} +4 -5
- package/dist/types-XKDJNOD1.d.mts.map +1 -0
- package/dist/types.d.mts +3 -3
- package/dist/types.mjs +2 -2
- package/dist/validators.d.mts +22 -21
- package/dist/validators.d.mts.map +1 -1
- package/dist/validators.mjs +53 -84
- package/dist/validators.mjs.map +1 -1
- package/package.json +7 -7
- package/src/exports/types.ts +2 -1
- package/src/index-type-validation.ts +2 -3
- package/src/ir/build-sql-namespace.ts +62 -32
- package/src/ir/sql-storage.ts +22 -24
- package/src/ir/sql-unbound-namespace.ts +15 -3
- package/src/resolve-storage-table.ts +12 -17
- package/src/types.ts +2 -1
- package/src/validators.ts +71 -100
- package/dist/types-DA_Jsl9r.mjs.map +0 -1
- package/dist/types-DiR-feh1.d.mts.map +0 -1
|
@@ -4,11 +4,12 @@ import {
|
|
|
4
4
|
NamespaceBase,
|
|
5
5
|
UNBOUND_NAMESPACE_ID,
|
|
6
6
|
} from '@prisma-next/framework-components/ir';
|
|
7
|
-
import { blindCast
|
|
8
|
-
import
|
|
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:
|
|
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
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Object.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
?
|
|
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,
|
package/src/ir/sql-storage.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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:
|
|
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:
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
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
|
-
|
|
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:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
'
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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`
|
|
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
|
-
|
|
302
|
+
registry: ReadonlyMap<string, Type<unknown>>,
|
|
331
303
|
): Type<unknown> {
|
|
332
|
-
const namespaceEntry = createNamespaceEntrySchema(
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
463
|
+
registry: ReadonlyMap<string, Type<unknown>>,
|
|
494
464
|
): Type<unknown> {
|
|
495
|
-
const storage = createSqlStorageSchema(
|
|
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
|
|
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}"`,
|