@prisma-next/sql-contract 0.11.0 → 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-DZpIXwK4.d.mts → types-ChlHcJCu.d.mts} +17 -20
- package/dist/types-ChlHcJCu.d.mts.map +1 -0
- package/dist/{types-L8p7B1dP.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 +9 -9
- package/dist/validators.d.mts.map +1 -1
- package/dist/validators.mjs +73 -42
- 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 +13 -95
- package/src/types.ts +1 -0
- package/src/validators.ts +108 -56
- package/dist/types-DZpIXwK4.d.mts.map +0 -1
- package/dist/types-L8p7B1dP.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "SQL contract types, validators, and IR factories for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/contract": "0.
|
|
10
|
-
"@prisma-next/framework-components": "0.
|
|
9
|
+
"@prisma-next/contract": "0.12.0",
|
|
10
|
+
"@prisma-next/framework-components": "0.12.0",
|
|
11
|
+
"@prisma-next/utils": "0.12.0",
|
|
11
12
|
"arktype": "^2.2.0"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
|
-
"@prisma-next/test-utils": "0.
|
|
15
|
-
"@prisma-next/tsconfig": "0.
|
|
16
|
-
"@prisma-next/tsdown": "0.
|
|
15
|
+
"@prisma-next/test-utils": "0.12.0",
|
|
16
|
+
"@prisma-next/tsconfig": "0.12.0",
|
|
17
|
+
"@prisma-next/tsdown": "0.12.0",
|
|
17
18
|
"tsdown": "0.22.0",
|
|
18
19
|
"typescript": "5.9.3",
|
|
19
20
|
"vitest": "4.1.6"
|
|
20
21
|
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"typescript": ">=5.9"
|
|
24
|
+
},
|
|
25
|
+
"peerDependenciesMeta": {
|
|
26
|
+
"typescript": {
|
|
27
|
+
"optional": true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
21
30
|
"files": [
|
|
22
31
|
"dist",
|
|
23
32
|
"src"
|
|
24
33
|
],
|
|
25
34
|
"exports": {
|
|
35
|
+
"./canonicalization-hooks": "./dist/canonicalization-hooks.mjs",
|
|
26
36
|
"./factories": "./dist/factories.mjs",
|
|
27
37
|
"./index-type-validation": "./dist/index-type-validation.mjs",
|
|
28
38
|
"./index-types": "./dist/index-types.mjs",
|
|
@@ -31,6 +41,9 @@
|
|
|
31
41
|
"./validators": "./dist/validators.mjs",
|
|
32
42
|
"./package.json": "./package.json"
|
|
33
43
|
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=24"
|
|
46
|
+
},
|
|
34
47
|
"repository": {
|
|
35
48
|
"type": "git",
|
|
36
49
|
"url": "https://github.com/prisma/prisma-next.git",
|
|
@@ -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,
|
|
@@ -23,79 +16,23 @@ import {
|
|
|
23
16
|
* Polymorphic value type for document-scoped `SqlStorage.types` entries
|
|
24
17
|
* (codec aliases / parameterised native type registrations). Postgres
|
|
25
18
|
* native enum registrations live under
|
|
26
|
-
* `storage.namespaces[namespaceId].
|
|
19
|
+
* `storage.namespaces[namespaceId].enum` instead.
|
|
27
20
|
*/
|
|
28
21
|
export type SqlStorageTypeEntry =
|
|
29
22
|
| StorageTypeInstance
|
|
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>;
|
|
40
|
-
readonly
|
|
29
|
+
readonly enum?: Record<string, PostgresEnumStorageEntry>;
|
|
41
30
|
}
|
|
42
31
|
|
|
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 types?: 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.types !== undefined && Object.keys(input.types).length > 0) {
|
|
68
|
-
Object.defineProperty(this, 'types', {
|
|
69
|
-
value: Object.freeze({ ...input.types }),
|
|
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.types ?? {}).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,39 +61,25 @@ 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
|
|
136
68
|
// the slot without a class-instance check.
|
|
137
69
|
export type SqlNamespace = Namespace & {
|
|
138
70
|
readonly tables: Readonly<Record<string, StorageTable>>;
|
|
139
|
-
readonly
|
|
71
|
+
readonly enum?: Readonly<Record<string, PostgresEnumStorageEntry>>;
|
|
140
72
|
};
|
|
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,
|
|
@@ -98,7 +107,7 @@ const StorageTypeInstanceSchema = type
|
|
|
98
107
|
});
|
|
99
108
|
|
|
100
109
|
/**
|
|
101
|
-
* Postgres native enum entry under `storage.namespaces[namespaceId].
|
|
110
|
+
* Postgres native enum entry under `storage.namespaces[namespaceId].enum[name]`.
|
|
102
111
|
* Document-scoped `storage.types` carries codec aliases only
|
|
103
112
|
* (`DocumentScopedStorageTypeSchema`).
|
|
104
113
|
*/
|
|
@@ -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>()
|
|
@@ -226,11 +236,7 @@ function namespaceSlotEntrySchema(
|
|
|
226
236
|
* Builds the per-namespace entry schema for `storage.namespaces[id]`.
|
|
227
237
|
* Pack-contributed `validatorSchema` fragments — keyed by the
|
|
228
238
|
* descriptor's `discriminator` — validate each entry by matching the
|
|
229
|
-
* entry's `kind` field
|
|
230
|
-
* unconditionally: it coexists additively with any contributed fragment
|
|
231
|
-
* that validates the same shape today. The full rename of `types` →
|
|
232
|
-
* `postgresEnums` lands later; until then, the redundancy is the F1 cure
|
|
233
|
-
* (no relocated dual-shape probe).
|
|
239
|
+
* entry's `kind` field on the `'enum?'` slot.
|
|
234
240
|
*/
|
|
235
241
|
export function createNamespaceEntrySchema(
|
|
236
242
|
fragments?: ReadonlyMap<string, Type<unknown>>,
|
|
@@ -240,7 +246,7 @@ export function createNamespaceEntrySchema(
|
|
|
240
246
|
id: 'string',
|
|
241
247
|
'kind?': 'string',
|
|
242
248
|
'tables?': type({ '[string]': StorageTableSchema }),
|
|
243
|
-
'
|
|
249
|
+
'enum?': type({
|
|
244
250
|
'[string]': namespaceSlotEntrySchema(PostgresEnumTypeSchema, 'postgres-enum', fragments),
|
|
245
251
|
}),
|
|
246
252
|
}) as Type<unknown>;
|
|
@@ -260,6 +266,11 @@ export function createSqlStorageSchema(
|
|
|
260
266
|
'+': 'reject',
|
|
261
267
|
storageHash: 'string',
|
|
262
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.
|
|
263
274
|
'namespaces?': type({ '[string]': namespaceEntry }),
|
|
264
275
|
}) as Type<unknown>;
|
|
265
276
|
}
|
|
@@ -355,13 +366,32 @@ const ModelStorageSchema = type({
|
|
|
355
366
|
fields: type({ '[string]': ModelStorageFieldSchema }),
|
|
356
367
|
});
|
|
357
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
|
+
|
|
358
388
|
const ModelSchema = type({
|
|
359
389
|
storage: ModelStorageSchema,
|
|
360
390
|
'fields?': type({ '[string]': ModelFieldSchema }),
|
|
361
|
-
'relations?': type({ '[string]':
|
|
391
|
+
'relations?': type({ '[string]': ContractRelationSchema }),
|
|
362
392
|
'discriminator?': 'unknown',
|
|
363
393
|
'variants?': 'unknown',
|
|
364
|
-
'base?':
|
|
394
|
+
'base?': CrossReferenceSchema,
|
|
365
395
|
'owner?': 'string',
|
|
366
396
|
});
|
|
367
397
|
|
|
@@ -387,10 +417,15 @@ export function createSqlContractSchema(
|
|
|
387
417
|
'capabilities?': 'Record<string, Record<string, boolean>>',
|
|
388
418
|
'extensionPacks?': 'Record<string, unknown>',
|
|
389
419
|
'meta?': ContractMetaSchema,
|
|
390
|
-
'roots?': '
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
+
}),
|
|
394
429
|
storage,
|
|
395
430
|
'execution?': ExecutionSchema,
|
|
396
431
|
}) as Type<unknown>;
|
|
@@ -417,11 +452,26 @@ export function validateStorage(value: unknown): SqlStorage {
|
|
|
417
452
|
const messages = result.map((p: { message: string }) => p.message).join('; ');
|
|
418
453
|
throw new Error(`Storage validation failed: ${messages}`);
|
|
419
454
|
}
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
// (
|
|
423
|
-
//
|
|
424
|
-
|
|
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
|
+
});
|
|
425
475
|
}
|
|
426
476
|
|
|
427
477
|
export function validateModel(value: unknown): unknown {
|
|
@@ -637,43 +687,46 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
|
|
|
637
687
|
* columns. Throws `ContractValidationError` on the first mismatch.
|
|
638
688
|
*/
|
|
639
689
|
export function validateModelStorageReferences(contract: Contract<SqlStorage>): void {
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
'storage',
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
const table = rawTable as StorageTable;
|
|
653
|
-
|
|
654
|
-
const columnNames = new Set(Object.keys(table.columns));
|
|
655
|
-
for (const [fieldName, field] of Object.entries(model.storage.fields)) {
|
|
656
|
-
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) {
|
|
657
698
|
throw new ContractValidationError(
|
|
658
|
-
`Model "${
|
|
699
|
+
`Model "${qualifiedName}" references non-existent table "${storageTable}"`,
|
|
659
700
|
'storage',
|
|
660
701
|
);
|
|
661
702
|
}
|
|
662
|
-
}
|
|
663
703
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
+
}
|
|
677
730
|
}
|
|
678
731
|
}
|
|
679
732
|
}
|
|
@@ -813,8 +866,7 @@ export function validateSqlContractFully<T extends Contract<SqlStorage>>(
|
|
|
813
866
|
const validated = validateSqlContractStructure<T>(stripped, schema);
|
|
814
867
|
validateContractDomain({
|
|
815
868
|
roots: validated.roots,
|
|
816
|
-
|
|
817
|
-
...(validated.valueObjects ? { valueObjects: validated.valueObjects } : {}),
|
|
869
|
+
domain: validated.domain,
|
|
818
870
|
});
|
|
819
871
|
validateSqlStorageConsistency(validated);
|
|
820
872
|
const semanticErrors = validateStorageSemantics(validated.storage);
|