@prisma-next/sql-contract 0.11.0-dev.9 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canonicalization-hooks.d.mts +10 -0
- package/dist/canonicalization-hooks.d.mts.map +1 -0
- package/dist/canonicalization-hooks.mjs +58 -0
- package/dist/canonicalization-hooks.mjs.map +1 -0
- package/dist/factories.d.mts +1 -1
- package/dist/factories.d.mts.map +1 -1
- package/dist/factories.mjs +3 -2
- package/dist/factories.mjs.map +1 -1
- package/dist/index-type-validation.d.mts +1 -1
- package/dist/index-type-validation.mjs.map +1 -1
- package/dist/index-types-B1cf5N0F.d.mts.map +1 -1
- package/dist/index-types.mjs.map +1 -1
- package/dist/{types-m5NX52zE.d.mts → types-ChlHcJCu.d.mts} +14 -17
- package/dist/types-ChlHcJCu.d.mts.map +1 -0
- package/dist/{types-C7t-bmro.mjs → types-DPkj4y3_.mjs} +123 -112
- package/dist/types-DPkj4y3_.mjs.map +1 -0
- package/dist/types.d.mts +2 -2
- package/dist/types.mjs +2 -2
- package/dist/validators.d.mts +7 -3
- package/dist/validators.d.mts.map +1 -1
- package/dist/validators.mjs +70 -35
- package/dist/validators.mjs.map +1 -1
- package/package.json +19 -6
- package/src/canonicalization-hooks.ts +32 -0
- package/src/exports/canonicalization-hooks.ts +1 -0
- package/src/exports/types.ts +2 -0
- package/src/factories.ts +2 -2
- package/src/ir/build-sql-namespace.ts +89 -0
- package/src/ir/foreign-key-reference.ts +3 -2
- package/src/ir/sql-storage.ts +10 -92
- package/src/types.ts +1 -0
- package/src/validators.ts +105 -49
- package/dist/types-C7t-bmro.mjs.map +0 -1
- package/dist/types-m5NX52zE.d.mts.map +0 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PreserveEmptyPredicate, StorageSort } from '@prisma-next/contract/hashing';
|
|
2
|
+
import {
|
|
3
|
+
createPreserveEmptyPredicate,
|
|
4
|
+
createStorageSort,
|
|
5
|
+
type NamedArraySortTarget,
|
|
6
|
+
type PathPattern,
|
|
7
|
+
} from '@prisma-next/contract/hashing-utils';
|
|
8
|
+
|
|
9
|
+
const preserveEmptyPatterns = [
|
|
10
|
+
['storage', 'namespaces', '*', 'tables'],
|
|
11
|
+
['storage', 'namespaces', '*', 'tables', '*'],
|
|
12
|
+
['storage', 'namespaces', '*', 'tables', '*', ['uniques', 'indexes', 'foreignKeys']],
|
|
13
|
+
['storage', 'namespaces', '*', 'tables', '*', 'foreignKeys', ['constraint', 'index']],
|
|
14
|
+
['storage', 'types', '*', 'typeParams'],
|
|
15
|
+
] as const satisfies readonly PathPattern[];
|
|
16
|
+
|
|
17
|
+
const sortTargets = [
|
|
18
|
+
{ path: ['namespaces', '*', 'tables', '*'], arrayKeys: ['indexes', 'uniques'] },
|
|
19
|
+
] as const satisfies readonly NamedArraySortTarget[];
|
|
20
|
+
|
|
21
|
+
const shouldPreserveEmpty: PreserveEmptyPredicate =
|
|
22
|
+
createPreserveEmptyPredicate(preserveEmptyPatterns);
|
|
23
|
+
|
|
24
|
+
const sortStorage: StorageSort = createStorageSort(sortTargets);
|
|
25
|
+
|
|
26
|
+
export const sqlContractCanonicalizationHooks: {
|
|
27
|
+
readonly shouldPreserveEmpty: PreserveEmptyPredicate;
|
|
28
|
+
readonly sortStorage: StorageSort;
|
|
29
|
+
} = {
|
|
30
|
+
shouldPreserveEmpty,
|
|
31
|
+
sortStorage,
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { sqlContractCanonicalizationHooks } from '../canonicalization-hooks';
|
package/src/exports/types.ts
CHANGED
package/src/factories.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { asNamespaceId, type ScalarFieldType } from '@prisma-next/contract/types';
|
|
2
2
|
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
3
3
|
import {
|
|
4
4
|
applyFkDefaults,
|
|
@@ -38,7 +38,7 @@ export function fk(
|
|
|
38
38
|
opts?: ForeignKeyOptions & { constraint?: boolean; index?: boolean; namespaceId?: string },
|
|
39
39
|
): ForeignKey {
|
|
40
40
|
const defaults = applyFkDefaults({ constraint: opts?.constraint, index: opts?.index });
|
|
41
|
-
const namespaceId = opts?.namespaceId ?? UNBOUND_NAMESPACE_ID;
|
|
41
|
+
const namespaceId = asNamespaceId(opts?.namespaceId ?? UNBOUND_NAMESPACE_ID);
|
|
42
42
|
return new ForeignKey({
|
|
43
43
|
source: { namespaceId, tableName: srcTableName, columns: srcColumns },
|
|
44
44
|
target: { namespaceId, tableName: targetTableName, columns: targetColumns },
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
freezeNode,
|
|
3
|
+
type Namespace,
|
|
4
|
+
NamespaceBase,
|
|
5
|
+
UNBOUND_NAMESPACE_ID,
|
|
6
|
+
} from '@prisma-next/framework-components/ir';
|
|
7
|
+
import { blindCast, castAs } from '@prisma-next/utils/casts';
|
|
8
|
+
import type { PostgresEnumStorageEntry } from './postgres-enum-storage-entry';
|
|
9
|
+
import type { SqlNamespace, SqlNamespaceTablesInput } from './sql-storage';
|
|
10
|
+
import { SqlUnboundNamespace } from './sql-unbound-namespace';
|
|
11
|
+
import { StorageTable } from './storage-table';
|
|
12
|
+
|
|
13
|
+
const SQL_NAMESPACE_KIND = 'sql-namespace' as const;
|
|
14
|
+
|
|
15
|
+
function isMaterializedSqlNamespace(ns: Namespace | SqlNamespaceTablesInput): ns is SqlNamespace {
|
|
16
|
+
if (typeof ns !== 'object' || ns === null) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const proto = Object.getPrototypeOf(ns);
|
|
20
|
+
if (proto === Object.prototype || proto === null) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return (ns as Namespace).kind === SQL_NAMESPACE_KIND;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class SqlBoundNamespace extends NamespaceBase {
|
|
27
|
+
declare readonly kind: string;
|
|
28
|
+
declare readonly enum?: Readonly<Record<string, PostgresEnumStorageEntry>>;
|
|
29
|
+
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly tables: Readonly<Record<string, StorageTable>>;
|
|
32
|
+
|
|
33
|
+
static fromTablesInput(input: SqlNamespaceTablesInput): SqlNamespace {
|
|
34
|
+
const tableCount = Object.keys(input.tables ?? {}).length;
|
|
35
|
+
const enumCount = Object.keys(input.enum ?? {}).length;
|
|
36
|
+
if (input.id === UNBOUND_NAMESPACE_ID && tableCount === 0 && enumCount === 0) {
|
|
37
|
+
return castAs<SqlNamespace>(SqlUnboundNamespace.instance);
|
|
38
|
+
}
|
|
39
|
+
return castAs<SqlNamespace>(new SqlBoundNamespace(input));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private constructor(input: SqlNamespaceTablesInput) {
|
|
43
|
+
super();
|
|
44
|
+
this.id = input.id;
|
|
45
|
+
this.tables = Object.freeze(
|
|
46
|
+
Object.fromEntries(
|
|
47
|
+
Object.entries(input.tables ?? {}).map(([name, t]) => [
|
|
48
|
+
name,
|
|
49
|
+
t instanceof StorageTable ? t : new StorageTable(t),
|
|
50
|
+
]),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
if (input.enum !== undefined && Object.keys(input.enum).length > 0) {
|
|
54
|
+
Object.defineProperty(this, 'enum', {
|
|
55
|
+
value: Object.freeze({ ...input.enum }),
|
|
56
|
+
writable: false,
|
|
57
|
+
enumerable: true,
|
|
58
|
+
configurable: false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
Object.defineProperty(this, 'kind', {
|
|
62
|
+
value: SQL_NAMESPACE_KIND,
|
|
63
|
+
writable: false,
|
|
64
|
+
enumerable: false,
|
|
65
|
+
configurable: true,
|
|
66
|
+
});
|
|
67
|
+
freezeNode(this);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildSqlNamespace(input: SqlNamespaceTablesInput): SqlNamespace {
|
|
72
|
+
return SqlBoundNamespace.fromTablesInput(input);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildSqlNamespaceMap(
|
|
76
|
+
namespaces: Readonly<Record<string, Namespace | SqlNamespaceTablesInput>>,
|
|
77
|
+
): Readonly<Record<string, SqlNamespace>> {
|
|
78
|
+
return Object.fromEntries(
|
|
79
|
+
Object.entries(namespaces).map(([nsKey, ns]) => [
|
|
80
|
+
nsKey,
|
|
81
|
+
isMaterializedSqlNamespace(ns)
|
|
82
|
+
? blindCast<
|
|
83
|
+
SqlNamespace,
|
|
84
|
+
'a materialised SQL-family namespace entry in a namespace map is a SqlNamespace'
|
|
85
|
+
>(ns)
|
|
86
|
+
: SqlBoundNamespace.fromTablesInput(ns),
|
|
87
|
+
]),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asNamespaceId, type NamespaceId } from '@prisma-next/contract/types';
|
|
1
2
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
2
3
|
import { SqlNode } from './sql-node';
|
|
3
4
|
|
|
@@ -15,13 +16,13 @@ export interface ForeignKeyReferenceInput {
|
|
|
15
16
|
* as the sentinel `namespaceId` for single-namespace (unbound) references.
|
|
16
17
|
*/
|
|
17
18
|
export class ForeignKeyReference extends SqlNode {
|
|
18
|
-
readonly namespaceId:
|
|
19
|
+
readonly namespaceId: NamespaceId;
|
|
19
20
|
readonly tableName: string;
|
|
20
21
|
readonly columns: readonly string[];
|
|
21
22
|
|
|
22
23
|
constructor(input: ForeignKeyReferenceInput) {
|
|
23
24
|
super();
|
|
24
|
-
this.namespaceId = input.namespaceId;
|
|
25
|
+
this.namespaceId = asNamespaceId(input.namespaceId);
|
|
25
26
|
this.tableName = input.tableName;
|
|
26
27
|
this.columns = input.columns;
|
|
27
28
|
freezeNode(this);
|
package/src/ir/sql-storage.ts
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import type { StorageHashBase } from '@prisma-next/contract/types';
|
|
2
|
-
import {
|
|
3
|
-
freezeNode,
|
|
4
|
-
type Namespace,
|
|
5
|
-
NamespaceBase,
|
|
6
|
-
type Storage,
|
|
7
|
-
UNBOUND_NAMESPACE_ID,
|
|
8
|
-
} from '@prisma-next/framework-components/ir';
|
|
2
|
+
import { freezeNode, type Namespace, type Storage } from '@prisma-next/framework-components/ir';
|
|
9
3
|
import {
|
|
10
4
|
isPostgresEnumStorageEntry,
|
|
11
5
|
type PostgresEnumStorageEntry,
|
|
12
6
|
} from './postgres-enum-storage-entry';
|
|
13
7
|
import { SqlNode } from './sql-node';
|
|
14
|
-
import {
|
|
15
|
-
import { StorageTable, type StorageTableInput } from './storage-table';
|
|
8
|
+
import type { StorageTable, StorageTableInput } from './storage-table';
|
|
16
9
|
import {
|
|
17
10
|
isStorageTypeInstance,
|
|
18
11
|
type StorageTypeInstance,
|
|
@@ -30,10 +23,6 @@ export type SqlStorageTypeEntry =
|
|
|
30
23
|
| StorageTypeInstanceInput
|
|
31
24
|
| PostgresEnumStorageEntry;
|
|
32
25
|
|
|
33
|
-
const DEFAULT_NAMESPACES: Readonly<Record<string, Namespace>> = Object.freeze({
|
|
34
|
-
[UNBOUND_NAMESPACE_ID]: SqlUnboundNamespace.instance,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
26
|
export interface SqlNamespaceTablesInput {
|
|
38
27
|
readonly id: string;
|
|
39
28
|
readonly tables?: Record<string, StorageTable | StorageTableInput>;
|
|
@@ -43,59 +32,7 @@ export interface SqlNamespaceTablesInput {
|
|
|
43
32
|
export interface SqlStorageInput<THash extends string = string> {
|
|
44
33
|
readonly storageHash: StorageHashBase<THash>;
|
|
45
34
|
readonly types?: Record<string, SqlStorageTypeEntry>;
|
|
46
|
-
readonly namespaces
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
class SqlNamespacePayload extends NamespaceBase {
|
|
50
|
-
declare readonly kind: string;
|
|
51
|
-
declare readonly enum?: Readonly<Record<string, PostgresEnumStorageEntry>>;
|
|
52
|
-
|
|
53
|
-
readonly id: string;
|
|
54
|
-
readonly tables: Readonly<Record<string, StorageTable>>;
|
|
55
|
-
|
|
56
|
-
constructor(input: SqlNamespaceTablesInput) {
|
|
57
|
-
super();
|
|
58
|
-
this.id = input.id;
|
|
59
|
-
this.tables = Object.freeze(
|
|
60
|
-
Object.fromEntries(
|
|
61
|
-
Object.entries(input.tables ?? {}).map(([name, t]) => [
|
|
62
|
-
name,
|
|
63
|
-
t instanceof StorageTable ? t : new StorageTable(t),
|
|
64
|
-
]),
|
|
65
|
-
),
|
|
66
|
-
);
|
|
67
|
-
if (input.enum !== undefined && Object.keys(input.enum).length > 0) {
|
|
68
|
-
Object.defineProperty(this, 'enum', {
|
|
69
|
-
value: Object.freeze({ ...input.enum }),
|
|
70
|
-
writable: false,
|
|
71
|
-
enumerable: true,
|
|
72
|
-
configurable: false,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
Object.defineProperty(this, 'kind', {
|
|
76
|
-
value: 'sql-namespace',
|
|
77
|
-
writable: false,
|
|
78
|
-
enumerable: false,
|
|
79
|
-
configurable: true,
|
|
80
|
-
});
|
|
81
|
-
freezeNode(this);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function normaliseNamespaceEntry(
|
|
86
|
-
nsKey: string,
|
|
87
|
-
ns: Namespace | SqlNamespaceTablesInput,
|
|
88
|
-
): Namespace {
|
|
89
|
-
if (ns instanceof NamespaceBase) {
|
|
90
|
-
return ns;
|
|
91
|
-
}
|
|
92
|
-
const input = ns as SqlNamespaceTablesInput; // JSON namespace payloads match SqlNamespaceTablesInput before SqlNamespacePayload materialises StorageTable instances.
|
|
93
|
-
const tableCount = Object.keys(input.tables ?? {}).length;
|
|
94
|
-
const typeCount = Object.keys(input.enum ?? {}).length;
|
|
95
|
-
if (nsKey === UNBOUND_NAMESPACE_ID && tableCount === 0 && typeCount === 0) {
|
|
96
|
-
return SqlUnboundNamespace.instance;
|
|
97
|
-
}
|
|
98
|
-
return new SqlNamespacePayload(input);
|
|
35
|
+
readonly namespaces: Readonly<Record<string, SqlNamespace>>;
|
|
99
36
|
}
|
|
100
37
|
|
|
101
38
|
/**
|
|
@@ -108,16 +45,11 @@ function normaliseNamespaceEntry(
|
|
|
108
45
|
* target-specific storage extensions).
|
|
109
46
|
*
|
|
110
47
|
* Honours the framework `Storage` interface: every SQL IR carries a
|
|
111
|
-
* `namespaces` map keyed by namespace id.
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* land; per-target namespace classes (`PostgresSchema.unbound`,
|
|
115
|
-
* `SqliteUnboundDatabase.instance`) earn their slots when each
|
|
116
|
-
* target's namespace shape lands.
|
|
48
|
+
* `namespaces` map keyed by namespace id. Callers must supply fully
|
|
49
|
+
* constructed `Namespace` instances — construction discipline lives
|
|
50
|
+
* in the authoring builders and deserializer hydration paths.
|
|
117
51
|
*
|
|
118
|
-
* The constructor normalises optional `types` into class instances
|
|
119
|
-
* materialises plain namespace envelope objects into `Namespace` class
|
|
120
|
-
* instances so downstream walks see a uniform AST.
|
|
52
|
+
* The constructor normalises optional `types` into class instances.
|
|
121
53
|
* `types` is polymorphic per Decision 18 Option B: codec-triple inputs
|
|
122
54
|
* are stamped with `kind: 'codec-instance'`; class-instance kinds
|
|
123
55
|
* (e.g. Postgres-enum entries satisfying `PostgresEnumStorageEntry`)
|
|
@@ -129,7 +61,7 @@ function normaliseNamespaceEntry(
|
|
|
129
61
|
// SQL concretions always store `StorageTable`-shaped values in `tables`.
|
|
130
62
|
// `tables` is a SQL-family idiom — the framework `Namespace` contract no
|
|
131
63
|
// longer mandates this field; Mongo namespaces carry `collections`
|
|
132
|
-
// instead. The `
|
|
64
|
+
// instead. The `tables` slot uses the same narrowing as every other
|
|
133
65
|
// SQL namespace; the wider `Record<string, object>` on `StorageTable` is
|
|
134
66
|
// only there so emitted `contract.d.ts` table literals (which lack the
|
|
135
67
|
// runtime `kind` discriminator on `StorageTable`) structurally satisfy
|
|
@@ -141,27 +73,13 @@ export type SqlNamespace = Namespace & {
|
|
|
141
73
|
|
|
142
74
|
export class SqlStorage<THash extends string = string> extends SqlNode implements Storage {
|
|
143
75
|
readonly storageHash: StorageHashBase<THash>;
|
|
144
|
-
readonly namespaces: Readonly<Record<string, SqlNamespace
|
|
145
|
-
readonly __unbound__: SqlNamespace;
|
|
146
|
-
};
|
|
76
|
+
readonly namespaces: Readonly<Record<string, SqlNamespace>>;
|
|
147
77
|
declare readonly types?: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
|
|
148
78
|
|
|
149
79
|
constructor(input: SqlStorageInput<THash>) {
|
|
150
80
|
super();
|
|
151
81
|
this.storageHash = input.storageHash;
|
|
152
|
-
|
|
153
|
-
const normalised: Record<string, SqlNamespace> = Object.fromEntries(
|
|
154
|
-
Object.entries(inputNamespaces).map(([nsKey, ns]) => [
|
|
155
|
-
nsKey,
|
|
156
|
-
normaliseNamespaceEntry(nsKey, ns) as SqlNamespace,
|
|
157
|
-
]),
|
|
158
|
-
);
|
|
159
|
-
if (!normalised[UNBOUND_NAMESPACE_ID]) {
|
|
160
|
-
normalised[UNBOUND_NAMESPACE_ID] = SqlUnboundNamespace.instance as SqlNamespace;
|
|
161
|
-
}
|
|
162
|
-
this.namespaces = Object.freeze(normalised) as Readonly<Record<string, SqlNamespace>> & {
|
|
163
|
-
readonly __unbound__: SqlNamespace;
|
|
164
|
-
};
|
|
82
|
+
this.namespaces = Object.freeze(input.namespaces);
|
|
165
83
|
if (input.types !== undefined) {
|
|
166
84
|
this.types = Object.freeze(
|
|
167
85
|
Object.fromEntries(
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CodecTrait } from '@prisma-next/framework-components/codec';
|
|
2
2
|
import type { ReferentialAction } from './ir/foreign-key';
|
|
3
3
|
|
|
4
|
+
export { buildSqlNamespace, buildSqlNamespaceMap } from './ir/build-sql-namespace';
|
|
4
5
|
export {
|
|
5
6
|
ForeignKey,
|
|
6
7
|
type ForeignKeyInput,
|
package/src/validators.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { ContractValidationError } from '@prisma-next/contract/contract-validation-error';
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
type Contract,
|
|
4
|
+
type ContractField,
|
|
5
|
+
type ContractModel,
|
|
6
|
+
CrossReferenceSchema,
|
|
7
|
+
} from '@prisma-next/contract/types';
|
|
3
8
|
import { validateContractDomain } from '@prisma-next/contract/validate-domain';
|
|
4
|
-
import type
|
|
9
|
+
import { type Namespace, UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
10
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
11
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
5
12
|
import { type Type, type } from 'arktype';
|
|
13
|
+
import { buildSqlNamespaceMap } from './ir/build-sql-namespace';
|
|
14
|
+
import { SqlUnboundNamespace } from './ir/sql-unbound-namespace';
|
|
6
15
|
import {
|
|
7
16
|
type ForeignKeyInput,
|
|
8
17
|
type ForeignKeyReferenceInput,
|
|
@@ -129,11 +138,12 @@ export const IndexSchema = type({
|
|
|
129
138
|
'options?': 'Record<string, unknown>',
|
|
130
139
|
});
|
|
131
140
|
|
|
132
|
-
export const ForeignKeyReferenceSchema = type
|
|
141
|
+
export const ForeignKeyReferenceSchema = type({
|
|
142
|
+
'+': 'reject',
|
|
133
143
|
namespaceId: 'string',
|
|
134
144
|
tableName: 'string',
|
|
135
145
|
columns: type.string.array().readonly(),
|
|
136
|
-
})
|
|
146
|
+
}) satisfies Type<ForeignKeyReferenceInput>;
|
|
137
147
|
|
|
138
148
|
export const ReferentialActionSchema = type
|
|
139
149
|
.declare<ReferentialAction>()
|
|
@@ -256,6 +266,11 @@ export function createSqlStorageSchema(
|
|
|
256
266
|
'+': 'reject',
|
|
257
267
|
storageHash: 'string',
|
|
258
268
|
'types?': type({ '[string]': DocumentScopedStorageTypeSchema }),
|
|
269
|
+
// `__unbound__` is NOT required here: cross-namespace contracts can
|
|
270
|
+
// declare only named namespaces (see cross-namespace FK fixtures). The
|
|
271
|
+
// `__unbound__` brand on `SqlStorageInput['namespaces']` is kept sound at
|
|
272
|
+
// construction time by injecting the unbound singleton when absent
|
|
273
|
+
// (see `validateStorage` / `hydrateSqlStorage`), not by structural require.
|
|
259
274
|
'namespaces?': type({ '[string]': namespaceEntry }),
|
|
260
275
|
}) as Type<unknown>;
|
|
261
276
|
}
|
|
@@ -351,13 +366,32 @@ const ModelStorageSchema = type({
|
|
|
351
366
|
fields: type({ '[string]': ModelStorageFieldSchema }),
|
|
352
367
|
});
|
|
353
368
|
|
|
369
|
+
const ContractReferenceRelationSchema = type({
|
|
370
|
+
'+': 'reject',
|
|
371
|
+
to: CrossReferenceSchema,
|
|
372
|
+
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
|
+
}),
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const ContractEmbedRelationSchema = type({
|
|
381
|
+
'+': 'reject',
|
|
382
|
+
to: CrossReferenceSchema,
|
|
383
|
+
cardinality: "'1:1' | '1:N'",
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const ContractRelationSchema = ContractReferenceRelationSchema.or(ContractEmbedRelationSchema);
|
|
387
|
+
|
|
354
388
|
const ModelSchema = type({
|
|
355
389
|
storage: ModelStorageSchema,
|
|
356
390
|
'fields?': type({ '[string]': ModelFieldSchema }),
|
|
357
|
-
'relations?': type({ '[string]':
|
|
391
|
+
'relations?': type({ '[string]': ContractRelationSchema }),
|
|
358
392
|
'discriminator?': 'unknown',
|
|
359
393
|
'variants?': 'unknown',
|
|
360
|
-
'base?':
|
|
394
|
+
'base?': CrossReferenceSchema,
|
|
361
395
|
'owner?': 'string',
|
|
362
396
|
});
|
|
363
397
|
|
|
@@ -383,10 +417,15 @@ export function createSqlContractSchema(
|
|
|
383
417
|
'capabilities?': 'Record<string, Record<string, boolean>>',
|
|
384
418
|
'extensionPacks?': 'Record<string, unknown>',
|
|
385
419
|
'meta?': ContractMetaSchema,
|
|
386
|
-
'roots?': '
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
420
|
+
'roots?': type({ '[string]': CrossReferenceSchema }),
|
|
421
|
+
domain: type({
|
|
422
|
+
namespaces: type({
|
|
423
|
+
'[string]': type({
|
|
424
|
+
models: type({ '[string]': ModelSchema }),
|
|
425
|
+
'valueObjects?': 'Record<string, unknown>',
|
|
426
|
+
}),
|
|
427
|
+
}),
|
|
428
|
+
}),
|
|
390
429
|
storage,
|
|
391
430
|
'execution?': ExecutionSchema,
|
|
392
431
|
}) as Type<unknown>;
|
|
@@ -413,11 +452,26 @@ export function validateStorage(value: unknown): SqlStorage {
|
|
|
413
452
|
const messages = result.map((p: { message: string }) => p.message).join('; ');
|
|
414
453
|
throw new Error(`Storage validation failed: ${messages}`);
|
|
415
454
|
}
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
// (
|
|
419
|
-
//
|
|
420
|
-
|
|
455
|
+
// Arktype validates the JSON-safe envelope, but the `ColumnDefault`
|
|
456
|
+
// union carries runtime-only `bigint | Date` that the validation DSL
|
|
457
|
+
// can't express (see NOTE above), so bridge the validated shape to the
|
|
458
|
+
// input type. Construction below re-materialises nested IR fields.
|
|
459
|
+
const validated = blindCast<
|
|
460
|
+
SqlStorageInput & { readonly namespaces?: SqlStorageInput['namespaces'] },
|
|
461
|
+
'arktype validated the JSON envelope but its output type is unknown (ColumnDefault carries runtime-only bigint|Date); bridge to the input shape'
|
|
462
|
+
>(result);
|
|
463
|
+
const namespaces = buildSqlNamespaceMap(validated.namespaces ?? {});
|
|
464
|
+
// Compatibility shim: inject the empty unbound singleton when absent so that
|
|
465
|
+
// production code paths which address __unbound__ for table metadata have a
|
|
466
|
+
// slot to read or write into. The `SqlStorageInput['namespaces']` type no
|
|
467
|
+
// longer requires __unbound__, so this is a runtime convenience, not a type
|
|
468
|
+
// invariant.
|
|
469
|
+
const unbound = namespaces[UNBOUND_NAMESPACE_ID] ?? SqlUnboundNamespace.instance;
|
|
470
|
+
return new SqlStorage({
|
|
471
|
+
storageHash: validated.storageHash,
|
|
472
|
+
...ifDefined('types', validated.types),
|
|
473
|
+
namespaces: { ...namespaces, [UNBOUND_NAMESPACE_ID]: unbound },
|
|
474
|
+
});
|
|
421
475
|
}
|
|
422
476
|
|
|
423
477
|
export function validateModel(value: unknown): unknown {
|
|
@@ -633,43 +687,46 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
|
|
|
633
687
|
* columns. Throws `ContractValidationError` on the first mismatch.
|
|
634
688
|
*/
|
|
635
689
|
export function validateModelStorageReferences(contract: Contract<SqlStorage>): void {
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
'storage',
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const table = rawTable as StorageTable;
|
|
649
|
-
|
|
650
|
-
const columnNames = new Set(Object.keys(table.columns));
|
|
651
|
-
for (const [fieldName, field] of Object.entries(model.storage.fields)) {
|
|
652
|
-
if (!columnNames.has(field.column)) {
|
|
690
|
+
for (const [namespaceId, namespace] of Object.entries(contract.domain.namespaces)) {
|
|
691
|
+
const models = namespace.models as Record<string, ContractModel<SqlModelStorage>>;
|
|
692
|
+
for (const [modelName, model] of Object.entries(models)) {
|
|
693
|
+
const qualifiedName = `${namespaceId}:${modelName}`;
|
|
694
|
+
const storageTable = model.storage.table;
|
|
695
|
+
|
|
696
|
+
const rawTable = findStorageTableByTableName(contract.storage, storageTable);
|
|
697
|
+
if (rawTable === undefined) {
|
|
653
698
|
throw new ContractValidationError(
|
|
654
|
-
`Model "${
|
|
699
|
+
`Model "${qualifiedName}" references non-existent table "${storageTable}"`,
|
|
655
700
|
'storage',
|
|
656
701
|
);
|
|
657
702
|
}
|
|
658
|
-
}
|
|
659
703
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
704
|
+
const table = rawTable as StorageTable;
|
|
705
|
+
|
|
706
|
+
const columnNames = new Set(Object.keys(table.columns));
|
|
707
|
+
for (const [fieldName, field] of Object.entries(model.storage.fields)) {
|
|
708
|
+
if (!columnNames.has(field.column)) {
|
|
709
|
+
throw new ContractValidationError(
|
|
710
|
+
`Model "${qualifiedName}" field "${fieldName}" references non-existent column "${field.column}" in table "${storageTable}"`,
|
|
711
|
+
'storage',
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const JSON_NATIVE_TYPES = new Set(['json', 'jsonb']);
|
|
717
|
+
for (const [fieldName, domainField] of Object.entries(model.fields ?? {})) {
|
|
718
|
+
const f = domainField as ContractField;
|
|
719
|
+
if (f.type?.kind !== 'valueObject') continue;
|
|
720
|
+
const storageField = model.storage.fields[fieldName];
|
|
721
|
+
if (!storageField) continue;
|
|
722
|
+
const column = table.columns[storageField.column];
|
|
723
|
+
if (!column) continue;
|
|
724
|
+
if (!JSON_NATIVE_TYPES.has(column.nativeType)) {
|
|
725
|
+
throw new ContractValidationError(
|
|
726
|
+
`Model "${qualifiedName}" field "${fieldName}" is a value object but storage column "${storageField.column}" has nativeType "${column.nativeType}" (expected json or jsonb)`,
|
|
727
|
+
'storage',
|
|
728
|
+
);
|
|
729
|
+
}
|
|
673
730
|
}
|
|
674
731
|
}
|
|
675
732
|
}
|
|
@@ -809,8 +866,7 @@ export function validateSqlContractFully<T extends Contract<SqlStorage>>(
|
|
|
809
866
|
const validated = validateSqlContractStructure<T>(stripped, schema);
|
|
810
867
|
validateContractDomain({
|
|
811
868
|
roots: validated.roots,
|
|
812
|
-
|
|
813
|
-
...(validated.valueObjects ? { valueObjects: validated.valueObjects } : {}),
|
|
869
|
+
domain: validated.domain,
|
|
814
870
|
});
|
|
815
871
|
validateSqlStorageConsistency(validated);
|
|
816
872
|
const semanticErrors = validateStorageSemantics(validated.storage);
|