@prisma-next/sql-contract 0.14.0-dev.5 → 0.14.0-dev.50
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/contract-view.d.mts +61 -0
- package/dist/contract-view.d.mts.map +1 -0
- package/dist/contract-view.mjs +39 -0
- package/dist/contract-view.mjs.map +1 -0
- package/dist/entity-kinds-CHiydmat.mjs +131 -0
- package/dist/entity-kinds-CHiydmat.mjs.map +1 -0
- package/dist/entity-kinds.d.mts +2 -1
- package/dist/entity-kinds.d.mts.map +1 -1
- package/dist/entity-kinds.mjs +1 -1
- package/dist/factories.d.mts +6 -4
- package/dist/factories.d.mts.map +1 -1
- package/dist/factories.mjs +5 -4
- package/dist/factories.mjs.map +1 -1
- package/dist/{foreign-key-BATxB95l.d.mts → foreign-key-DM1UTydh.d.mts} +3 -41
- package/dist/foreign-key-DM1UTydh.d.mts.map +1 -0
- package/dist/index-type-validation.d.mts +1 -1
- package/dist/referential-action-sql.d.mts +1 -1
- package/dist/resolve-storage-table.d.mts +2 -2
- package/dist/sql-node-V214WXQD.d.mts +42 -0
- package/dist/sql-node-V214WXQD.d.mts.map +1 -0
- package/dist/{sql-storage-Dga0jwP2.d.mts → sql-storage-BRB55sCP.d.mts} +62 -20
- package/dist/sql-storage-BRB55sCP.d.mts.map +1 -0
- package/dist/{storage-value-set-WnYsIFM8.d.mts → storage-table-Bj1ZN1P7.d.mts} +16 -39
- package/dist/storage-table-Bj1ZN1P7.d.mts.map +1 -0
- package/dist/{entity-kinds-Cl36zL5j.mjs → storage-value-set-C4XInPlX.mjs} +21 -128
- package/dist/storage-value-set-C4XInPlX.mjs.map +1 -0
- package/dist/storage-value-set-D-jww77l.d.mts +41 -0
- package/dist/storage-value-set-D-jww77l.d.mts.map +1 -0
- package/dist/types-KiOT4SAV.mjs +114 -0
- package/dist/types-KiOT4SAV.mjs.map +1 -0
- package/dist/{types-B1N8w0I2.d.mts → types-zBvpNCmg.d.mts} +25 -55
- package/dist/types-zBvpNCmg.d.mts.map +1 -0
- package/dist/types.d.mts +13 -5
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +3 -3
- package/dist/validators.d.mts +10 -7
- package/dist/validators.d.mts.map +1 -1
- package/dist/validators.mjs +109 -17
- package/dist/validators.mjs.map +1 -1
- package/dist/value-set-derivation-hook.d.mts +35 -0
- package/dist/value-set-derivation-hook.d.mts.map +1 -0
- package/dist/value-set-derivation-hook.mjs +22 -0
- package/dist/value-set-derivation-hook.mjs.map +1 -0
- package/package.json +9 -7
- package/src/column-type-resolution.ts +26 -0
- package/src/contract-view.ts +98 -0
- package/src/exports/contract-view.ts +11 -0
- package/src/exports/types.ts +17 -4
- package/src/exports/value-set-derivation-hook.ts +2 -0
- package/src/factories.ts +12 -2
- package/src/ir/sql-storage.ts +80 -17
- package/src/ir/storage-column.ts +3 -0
- package/src/ir/storage-entry-schemas.ts +1 -0
- package/src/ir/storage-table.ts +21 -0
- package/src/ir/storage-value-set.ts +5 -0
- package/src/types.ts +51 -8
- package/src/validators.ts +215 -40
- package/src/value-set-derivation-hook.ts +47 -0
- package/dist/entity-kinds-Cl36zL5j.mjs.map +0 -1
- package/dist/foreign-key-BATxB95l.d.mts.map +0 -1
- package/dist/sql-storage-Dga0jwP2.d.mts.map +0 -1
- package/dist/storage-value-set-WnYsIFM8.d.mts.map +0 -1
- package/dist/types-B-eiQXff.mjs +0 -191
- package/dist/types-B-eiQXff.mjs.map +0 -1
- package/dist/types-B1N8w0I2.d.mts.map +0 -1
- package/src/ir/build-sql-namespace.ts +0 -110
- package/src/ir/sql-unbound-namespace.ts +0 -74
package/src/ir/sql-storage.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { StorageHashBase } from '@prisma-next/contract/types';
|
|
2
|
-
import
|
|
2
|
+
import type { AuthoringContributions } from '@prisma-next/framework-components/authoring';
|
|
3
|
+
import {
|
|
4
|
+
freezeNode,
|
|
5
|
+
isPlainRecord,
|
|
6
|
+
type Namespace,
|
|
7
|
+
NamespaceBase,
|
|
8
|
+
type Storage,
|
|
9
|
+
} from '@prisma-next/framework-components/ir';
|
|
3
10
|
import { SqlNode } from './sql-node';
|
|
4
11
|
import type { StorageTable } from './storage-table';
|
|
5
12
|
import {
|
|
@@ -19,15 +26,45 @@ import type { StorageValueSet } from './storage-value-set';
|
|
|
19
26
|
*/
|
|
20
27
|
export type SqlStorageTypeEntry = StorageTypeInstance | StorageTypeInstanceInput;
|
|
21
28
|
|
|
22
|
-
export interface
|
|
29
|
+
export interface SqlNamespaceInput {
|
|
23
30
|
readonly id: string;
|
|
24
31
|
readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Target-supplied factory that materializes a `Namespace` from a SQL
|
|
36
|
+
* `SqlNamespaceInput` (used to populate `SqlStorage.namespaces`).
|
|
37
|
+
*/
|
|
38
|
+
export type SqlNamespaceFactory = (input: SqlNamespaceInput) => Namespace;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* SQL-family extension of the framework `AuthoringContributions`. SQL target
|
|
42
|
+
* packs add a `createNamespace` factory so the PSL/TS authoring paths can
|
|
43
|
+
* materialize namespaces (and merge lowered extension-block entities) without
|
|
44
|
+
* each consumer re-specifying it. The factory is SQL-specific, so it lives here
|
|
45
|
+
* rather than on the framework `AuthoringContributions` base.
|
|
46
|
+
*/
|
|
47
|
+
export interface SqlAuthoringContributions extends AuthoringContributions {
|
|
48
|
+
readonly createNamespace?: SqlNamespaceFactory;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Narrows framework `AuthoringContributions` to the SQL-family shape by testing
|
|
53
|
+
* for the SQL-specific `createNamespace` capability.
|
|
54
|
+
*/
|
|
55
|
+
export function isSqlAuthoringContributions(
|
|
56
|
+
authoring: AuthoringContributions | undefined,
|
|
57
|
+
): authoring is SqlAuthoringContributions {
|
|
58
|
+
if (authoring === undefined || !Object.hasOwn(authoring, 'createNamespace')) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return typeof Reflect.get(authoring, 'createNamespace') === 'function';
|
|
62
|
+
}
|
|
63
|
+
|
|
27
64
|
export interface SqlStorageInput<THash extends string = string> {
|
|
28
65
|
readonly storageHash: StorageHashBase<THash>;
|
|
29
66
|
readonly types?: Record<string, SqlStorageTypeEntry>;
|
|
30
|
-
readonly namespaces: Readonly<Record<string,
|
|
67
|
+
readonly namespaces: Readonly<Record<string, SqlNamespaceBase>>;
|
|
31
68
|
}
|
|
32
69
|
|
|
33
70
|
/**
|
|
@@ -63,22 +100,48 @@ export type SqlNamespaceEntries = Readonly<Record<string, Readonly<Record<string
|
|
|
63
100
|
};
|
|
64
101
|
|
|
65
102
|
/**
|
|
66
|
-
* SQL family
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
103
|
+
* Structural interface for SQL family namespaces. Generated `.d.ts` contract
|
|
104
|
+
* types satisfy this structurally (no prototype methods). The runtime
|
|
105
|
+
* abstract class `SqlNamespaceBase` extends this.
|
|
106
|
+
*
|
|
107
|
+
* `qualifyTable` is optional so JSON-shaped contract types (which carry no
|
|
108
|
+
* methods) are accepted where `SqlNamespace` is required. Hydrated
|
|
109
|
+
* `SqlNamespaceBase` instances always have it.
|
|
71
110
|
*/
|
|
72
|
-
export
|
|
111
|
+
export interface SqlNamespace {
|
|
112
|
+
readonly kind: string;
|
|
113
|
+
readonly id: string;
|
|
73
114
|
readonly entries: SqlNamespaceEntries;
|
|
74
|
-
/**
|
|
75
|
-
* Render a dialect-qualified table reference for runtime SQL emission.
|
|
76
|
-
* Present on materialised target concretions (`PostgresSchema`,
|
|
77
|
-
* `SqliteDatabase`, …) and family placeholders; omitted on emitted
|
|
78
|
-
* contract structural namespace literals (methods are not serialised).
|
|
79
|
-
*/
|
|
80
115
|
qualifyTable?(tableName: string): string;
|
|
81
|
-
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Abstract SQL family namespace base class. Target concretions (`PostgresSchema`,
|
|
120
|
+
* `SqliteDatabase`, …) extend this — it is never instantiated directly.
|
|
121
|
+
* `entries` is the open ADR 224 dictionary: `entries[entityKind][entityName]`
|
|
122
|
+
* addresses any entity.
|
|
123
|
+
*/
|
|
124
|
+
export abstract class SqlNamespaceBase extends NamespaceBase implements SqlNamespace {
|
|
125
|
+
abstract override readonly id: string;
|
|
126
|
+
abstract override readonly entries: SqlNamespaceEntries;
|
|
127
|
+
|
|
128
|
+
abstract qualifyTable(tableName: string): string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Realm-safe guard for hydrated `SqlNamespaceBase` concretions. Checks
|
|
133
|
+
* `qualifyTable` structurally instead of `instanceof NamespaceBase`, so it
|
|
134
|
+
* survives duplicate-module boundaries (e.g. dist e2e where the target and
|
|
135
|
+
* the family carry separate copies of `@prisma-next/framework-components`).
|
|
136
|
+
*
|
|
137
|
+
* Every concrete `SqlNamespaceBase` subclass (`PostgresSchema`, `SqliteDatabase`,
|
|
138
|
+
* `TestSqlNamespace`, …) implements `qualifyTable`. Raw `SqlNamespaceInput`
|
|
139
|
+
* objects (`{ id, entries }`) do not.
|
|
140
|
+
*/
|
|
141
|
+
export function isMaterializedSqlNamespace(x: unknown): x is SqlNamespaceBase {
|
|
142
|
+
if (typeof x !== 'object' || x === null || !('qualifyTable' in x)) return false;
|
|
143
|
+
return typeof x.qualifyTable === 'function';
|
|
144
|
+
}
|
|
82
145
|
|
|
83
146
|
export class SqlStorage<THash extends string = string> extends SqlNode implements Storage {
|
|
84
147
|
readonly storageHash: StorageHashBase<THash>;
|
|
@@ -128,7 +191,7 @@ function normaliseTypeEntry(name: string, entry: SqlStorageTypeEntry): StorageTy
|
|
|
128
191
|
}
|
|
129
192
|
return toStorageTypeInstance(entry);
|
|
130
193
|
}
|
|
131
|
-
const rawKind = (entry
|
|
194
|
+
const rawKind = isPlainRecord(entry) ? entry['kind'] : undefined;
|
|
132
195
|
const kindDescription =
|
|
133
196
|
rawKind === undefined
|
|
134
197
|
? 'missing `kind` discriminator'
|
package/src/ir/storage-column.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface StorageColumnInput {
|
|
|
16
16
|
readonly nativeType: string;
|
|
17
17
|
readonly codecId: string;
|
|
18
18
|
readonly nullable: boolean;
|
|
19
|
+
readonly many?: boolean;
|
|
19
20
|
readonly typeParams?: Record<string, unknown>;
|
|
20
21
|
readonly typeRef?: string;
|
|
21
22
|
readonly default?: ColumnDefault;
|
|
@@ -40,6 +41,7 @@ export class StorageColumn extends SqlNode {
|
|
|
40
41
|
readonly nativeType: string;
|
|
41
42
|
readonly codecId: string;
|
|
42
43
|
readonly nullable: boolean;
|
|
44
|
+
declare readonly many?: boolean;
|
|
43
45
|
declare readonly typeParams?: Record<string, unknown>;
|
|
44
46
|
declare readonly typeRef?: string;
|
|
45
47
|
declare readonly default?: ColumnDefault;
|
|
@@ -51,6 +53,7 @@ export class StorageColumn extends SqlNode {
|
|
|
51
53
|
this.nativeType = input.nativeType;
|
|
52
54
|
this.codecId = input.codecId;
|
|
53
55
|
this.nullable = input.nullable;
|
|
56
|
+
if (input.many !== undefined) this.many = input.many;
|
|
54
57
|
if (input.typeParams !== undefined) this.typeParams = input.typeParams;
|
|
55
58
|
if (input.typeRef !== undefined) this.typeRef = input.typeRef;
|
|
56
59
|
if (input.default !== undefined) this.default = input.default;
|
package/src/ir/storage-table.ts
CHANGED
|
@@ -68,4 +68,25 @@ export class StorageTable extends SqlNode {
|
|
|
68
68
|
}
|
|
69
69
|
freezeNode(this);
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Runtime guard that a namespace `table` entry is really a `StorageTable`.
|
|
74
|
+
* The compiler already types the entry as `StorageTable`, but a
|
|
75
|
+
* freshly-deserialized contract may carry plain JSON at that slot until
|
|
76
|
+
* hydration; this duck-types the structural shape. Accepts `undefined` so
|
|
77
|
+
* optional-chained entry lookups pass straight through.
|
|
78
|
+
*/
|
|
79
|
+
static is(value: StorageTable | undefined): value is StorageTable {
|
|
80
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
81
|
+
return 'columns' in value && 'uniques' in value && 'indexes' in value && 'foreignKeys' in value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static assert(
|
|
85
|
+
value: StorageTable | undefined,
|
|
86
|
+
coordinate: string,
|
|
87
|
+
): asserts value is StorageTable {
|
|
88
|
+
if (!StorageTable.is(value)) {
|
|
89
|
+
throw new Error(`Expected a StorageTable at ${coordinate}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
71
92
|
}
|
|
@@ -41,3 +41,8 @@ export class StorageValueSet extends SqlNode {
|
|
|
41
41
|
freezeNode(this);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
export function isStorageValueSet(value: unknown): value is StorageValueSet {
|
|
46
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
47
|
+
return 'kind' in value && value.kind === 'valueSet' && 'values' in value;
|
|
48
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -10,7 +10,6 @@ export interface SqlControlDriverInstance<T extends string = string>
|
|
|
10
10
|
): Promise<{ readonly rows: Row[] }>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export { buildSqlNamespace, buildSqlNamespaceMap } from './ir/build-sql-namespace';
|
|
14
13
|
export { CheckConstraint, type CheckConstraintInput } from './ir/check-constraint';
|
|
15
14
|
export {
|
|
16
15
|
ForeignKey,
|
|
@@ -25,14 +24,18 @@ export { PrimaryKey, type PrimaryKeyInput } from './ir/primary-key';
|
|
|
25
24
|
export { Index, type IndexInput } from './ir/sql-index';
|
|
26
25
|
export { SqlNode } from './ir/sql-node';
|
|
27
26
|
export {
|
|
27
|
+
isMaterializedSqlNamespace,
|
|
28
|
+
isSqlAuthoringContributions,
|
|
29
|
+
type SqlAuthoringContributions,
|
|
28
30
|
type SqlNamespace,
|
|
31
|
+
SqlNamespaceBase,
|
|
29
32
|
type SqlNamespaceEntries,
|
|
30
|
-
type
|
|
33
|
+
type SqlNamespaceFactory,
|
|
34
|
+
type SqlNamespaceInput,
|
|
31
35
|
SqlStorage,
|
|
32
36
|
type SqlStorageInput,
|
|
33
37
|
type SqlStorageTypeEntry,
|
|
34
38
|
} from './ir/sql-storage';
|
|
35
|
-
export { SqlUnboundNamespace } from './ir/sql-unbound-namespace';
|
|
36
39
|
export { StorageColumn, type StorageColumnInput } from './ir/storage-column';
|
|
37
40
|
export { StorageTable, type StorageTableInput } from './ir/storage-table';
|
|
38
41
|
export {
|
|
@@ -42,7 +45,11 @@ export {
|
|
|
42
45
|
type StorageTypeInstanceInput,
|
|
43
46
|
toStorageTypeInstance,
|
|
44
47
|
} from './ir/storage-type-instance';
|
|
45
|
-
export {
|
|
48
|
+
export {
|
|
49
|
+
isStorageValueSet,
|
|
50
|
+
StorageValueSet,
|
|
51
|
+
type StorageValueSetInput,
|
|
52
|
+
} from './ir/storage-value-set';
|
|
46
53
|
export {
|
|
47
54
|
UniqueConstraint,
|
|
48
55
|
type UniqueConstraintInput,
|
|
@@ -83,16 +90,25 @@ export function applyFkDefaults(
|
|
|
83
90
|
// Shared by the output and input field-type maps and their extractors.
|
|
84
91
|
export type NamespacedFieldTypeMap = Record<string, Record<string, Record<string, unknown>>>;
|
|
85
92
|
|
|
93
|
+
export type NamespacedStorageColumnTypeMap = Record<
|
|
94
|
+
string,
|
|
95
|
+
Record<string, Record<string, unknown>>
|
|
96
|
+
>;
|
|
97
|
+
|
|
86
98
|
export type TypeMaps<
|
|
87
99
|
TCodecTypes extends Record<string, { output: unknown }> = Record<string, never>,
|
|
88
100
|
TQueryOperationTypes extends Record<string, unknown> = Record<string, never>,
|
|
89
101
|
TFieldOutputTypes extends NamespacedFieldTypeMap = Record<string, never>,
|
|
90
102
|
TFieldInputTypes extends NamespacedFieldTypeMap = Record<string, never>,
|
|
103
|
+
TStorageColumnTypes extends NamespacedStorageColumnTypeMap = Record<string, never>,
|
|
104
|
+
TStorageColumnInputTypes extends NamespacedStorageColumnTypeMap = Record<string, never>,
|
|
91
105
|
> = {
|
|
92
106
|
readonly codecTypes: TCodecTypes;
|
|
93
107
|
readonly queryOperationTypes: TQueryOperationTypes;
|
|
94
108
|
readonly fieldOutputTypes: TFieldOutputTypes;
|
|
95
109
|
readonly fieldInputTypes: TFieldInputTypes;
|
|
110
|
+
readonly storageColumnTypes: TStorageColumnTypes;
|
|
111
|
+
readonly storageColumnInputTypes: TStorageColumnInputTypes;
|
|
96
112
|
};
|
|
97
113
|
|
|
98
114
|
export type CodecTypesOf<T> = [T] extends [never]
|
|
@@ -107,12 +123,19 @@ export type CodecTypesOf<T> = [T] extends [never]
|
|
|
107
123
|
* Dispatch hint identifying the first-argument target of an operation.
|
|
108
124
|
*
|
|
109
125
|
* Used by ORM column helpers to decide whether an operation is reachable on a
|
|
110
|
-
* field.
|
|
111
|
-
*
|
|
126
|
+
* field. Names a concrete codec identity, a set of capability traits the
|
|
127
|
+
* field's codec must carry, or targets list-typed (`many`) fields. Element
|
|
128
|
+
* capability gating for list ops travels in `elementTraits`.
|
|
112
129
|
*/
|
|
113
130
|
export type QueryOperationSelfSpec =
|
|
114
|
-
| { readonly codecId: string; readonly traits?: never }
|
|
115
|
-
| { readonly traits: readonly CodecTrait[]; readonly codecId?: never }
|
|
131
|
+
| { readonly codecId: string; readonly traits?: never; readonly many?: never }
|
|
132
|
+
| { readonly traits: readonly CodecTrait[]; readonly codecId?: never; readonly many?: never }
|
|
133
|
+
| {
|
|
134
|
+
readonly many: true;
|
|
135
|
+
readonly elementTraits?: readonly CodecTrait[];
|
|
136
|
+
readonly codecId?: never;
|
|
137
|
+
readonly traits?: never;
|
|
138
|
+
};
|
|
116
139
|
|
|
117
140
|
/**
|
|
118
141
|
* Structural shape an operation's impl must return: any value carrying a
|
|
@@ -172,10 +195,30 @@ export type FieldInputTypesOf<T> = [T] extends [never]
|
|
|
172
195
|
: Record<string, never>
|
|
173
196
|
: Record<string, never>;
|
|
174
197
|
|
|
198
|
+
export type StorageColumnTypesOf<T> = [T] extends [never]
|
|
199
|
+
? Record<string, never>
|
|
200
|
+
: T extends { readonly storageColumnTypes: infer F }
|
|
201
|
+
? F extends NamespacedStorageColumnTypeMap
|
|
202
|
+
? F
|
|
203
|
+
: Record<string, never>
|
|
204
|
+
: Record<string, never>;
|
|
205
|
+
|
|
206
|
+
export type StorageColumnInputTypesOf<T> = [T] extends [never]
|
|
207
|
+
? Record<string, never>
|
|
208
|
+
: T extends { readonly storageColumnInputTypes: infer F }
|
|
209
|
+
? F extends NamespacedStorageColumnTypeMap
|
|
210
|
+
? F
|
|
211
|
+
: Record<string, never>
|
|
212
|
+
: Record<string, never>;
|
|
213
|
+
|
|
175
214
|
export type ExtractCodecTypes<T> = CodecTypesOf<ExtractTypeMapsFromContract<T>>;
|
|
176
215
|
export type ExtractQueryOperationTypes<T> = QueryOperationTypesOf<ExtractTypeMapsFromContract<T>>;
|
|
177
216
|
export type ExtractFieldOutputTypes<T> = FieldOutputTypesOf<ExtractTypeMapsFromContract<T>>;
|
|
178
217
|
export type ExtractFieldInputTypes<T> = FieldInputTypesOf<ExtractTypeMapsFromContract<T>>;
|
|
218
|
+
export type ExtractStorageColumnTypes<T> = StorageColumnTypesOf<ExtractTypeMapsFromContract<T>>;
|
|
219
|
+
export type ExtractStorageColumnInputTypes<T> = StorageColumnInputTypesOf<
|
|
220
|
+
ExtractTypeMapsFromContract<T>
|
|
221
|
+
>;
|
|
179
222
|
|
|
180
223
|
export type ResolveCodecTypes<TContract, TTypeMaps> = [TTypeMaps] extends [never]
|
|
181
224
|
? ExtractCodecTypes<TContract>
|
package/src/validators.ts
CHANGED
|
@@ -10,13 +10,11 @@ import {
|
|
|
10
10
|
type AnyEntityKindDescriptor,
|
|
11
11
|
isPlainRecord,
|
|
12
12
|
type Namespace,
|
|
13
|
-
UNBOUND_NAMESPACE_ID,
|
|
14
13
|
} from '@prisma-next/framework-components/ir';
|
|
15
14
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
16
15
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
17
16
|
import { type Type, type } from 'arktype';
|
|
18
17
|
import { composeSqlEntityKinds } from './entity-kinds';
|
|
19
|
-
import { buildSqlNamespaceMap } from './ir/build-sql-namespace';
|
|
20
18
|
|
|
21
19
|
export {
|
|
22
20
|
CheckConstraintSchema,
|
|
@@ -32,13 +30,12 @@ export {
|
|
|
32
30
|
StorageValueSetSchema,
|
|
33
31
|
} from './ir/storage-entry-schemas';
|
|
34
32
|
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
type SqlModelStorage,
|
|
33
|
+
import type {
|
|
34
|
+
SqlModelStorage,
|
|
38
35
|
SqlStorage,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
StorageColumn,
|
|
37
|
+
StorageTable,
|
|
38
|
+
StorageTypeInstanceInput,
|
|
42
39
|
} from './types';
|
|
43
40
|
|
|
44
41
|
const generatorKindSchema = type("'generator'");
|
|
@@ -192,9 +189,8 @@ export function createSqlStorageSchema(
|
|
|
192
189
|
'types?': type({ '[string]': DocumentScopedStorageTypeSchema }),
|
|
193
190
|
// `__unbound__` is NOT required here: cross-namespace contracts can
|
|
194
191
|
// declare only named namespaces (see cross-namespace FK fixtures). The
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
// (see `validateStorage` / `hydrateSqlStorage`), not by structural require.
|
|
192
|
+
// unbound slot is injected when absent by `ensureUnboundNamespaceSlot`
|
|
193
|
+
// in `build-contract.ts`, not enforced here structurally.
|
|
198
194
|
'namespaces?': type({ '[string]': namespaceEntry }),
|
|
199
195
|
}) as Type<unknown>;
|
|
200
196
|
}
|
|
@@ -368,45 +364,21 @@ export function createSqlContractSchema(
|
|
|
368
364
|
|
|
369
365
|
const SqlContractSchema = createSqlContractSchema(DEFAULT_SQL_KINDS);
|
|
370
366
|
|
|
371
|
-
// NOTE: StorageColumnSchema, StorageTableSchema, and StorageSchema use bare type()
|
|
372
|
-
// instead of type.declare<T>().type() because the ColumnDefault union's value field
|
|
373
|
-
// includes bigint | Date (runtime-only types after decoding) which cannot be expressed
|
|
374
|
-
// in Arktype's JSON validation DSL. The `as SqlStorage` cast in validateStorage() bridges
|
|
375
|
-
// the gap between the JSON-safe Arktype output and the runtime TypeScript type.
|
|
376
|
-
|
|
377
367
|
/**
|
|
378
|
-
* Validates the structural shape of SqlStorage using Arktype.
|
|
368
|
+
* Validates the structural shape of SqlStorage using Arktype. Pure
|
|
369
|
+
* structural check: namespace IR is never materialized here (that needs
|
|
370
|
+
* a target concretion via the serializer hydration path), so this throws
|
|
371
|
+
* on invalid input and constructs nothing.
|
|
379
372
|
*
|
|
380
373
|
* @param value - The storage value to validate
|
|
381
|
-
* @returns The validated storage if structure is valid
|
|
382
374
|
* @throws Error if the storage structure is invalid
|
|
383
375
|
*/
|
|
384
|
-
export function validateStorage(value: unknown):
|
|
376
|
+
export function validateStorage(value: unknown): void {
|
|
385
377
|
const result = StorageSchema(value);
|
|
386
378
|
if (result instanceof type.errors) {
|
|
387
379
|
const messages = result.map((p: { message: string }) => p.message).join('; ');
|
|
388
380
|
throw new Error(`Storage validation failed: ${messages}`);
|
|
389
381
|
}
|
|
390
|
-
// Arktype validates the JSON-safe envelope, but the `ColumnDefault`
|
|
391
|
-
// union carries runtime-only `bigint | Date` that the validation DSL
|
|
392
|
-
// can't express (see NOTE above), so bridge the validated shape to the
|
|
393
|
-
// input type. Construction below re-materialises nested IR fields.
|
|
394
|
-
const validated = blindCast<
|
|
395
|
-
SqlStorageInput & { readonly namespaces?: SqlStorageInput['namespaces'] },
|
|
396
|
-
'arktype validated the JSON envelope but its output type is unknown (ColumnDefault carries runtime-only bigint|Date); bridge to the input shape'
|
|
397
|
-
>(result);
|
|
398
|
-
const namespaces = buildSqlNamespaceMap(validated.namespaces ?? {});
|
|
399
|
-
// Compatibility shim: inject the empty unbound singleton when absent so that
|
|
400
|
-
// production code paths which address __unbound__ for table metadata have a
|
|
401
|
-
// slot to read or write into. The `SqlStorageInput['namespaces']` type no
|
|
402
|
-
// longer requires __unbound__, so this is a runtime convenience, not a type
|
|
403
|
-
// invariant.
|
|
404
|
-
const unbound = namespaces[UNBOUND_NAMESPACE_ID] ?? SqlUnboundNamespace.instance;
|
|
405
|
-
return new SqlStorage({
|
|
406
|
-
storageHash: validated.storageHash,
|
|
407
|
-
...ifDefined('types', validated.types),
|
|
408
|
-
namespaces: { ...namespaces, [UNBOUND_NAMESPACE_ID]: unbound },
|
|
409
|
-
});
|
|
410
382
|
}
|
|
411
383
|
|
|
412
384
|
export function validateModel(value: unknown): unknown {
|
|
@@ -846,5 +818,208 @@ export function validateSqlContractFully<T extends Contract<SqlStorage>>(
|
|
|
846
818
|
);
|
|
847
819
|
}
|
|
848
820
|
validateModelStorageReferences(validated);
|
|
821
|
+
validateRelationThroughConsistency(validated);
|
|
849
822
|
return validated;
|
|
850
823
|
}
|
|
824
|
+
|
|
825
|
+
/** Storage column lookup for through-consistency validation. */
|
|
826
|
+
function lookupStorageColumn(
|
|
827
|
+
contract: Contract<SqlStorage>,
|
|
828
|
+
namespaceId: string,
|
|
829
|
+
tableName: string,
|
|
830
|
+
columnName: string,
|
|
831
|
+
): StorageColumn | undefined {
|
|
832
|
+
const rawTable = contract.storage.namespaces[namespaceId]?.entries.table?.[tableName];
|
|
833
|
+
if (rawTable === undefined) {
|
|
834
|
+
return undefined;
|
|
835
|
+
}
|
|
836
|
+
const table = blindCast<StorageTable, 'structurally validated storage table'>(rawTable);
|
|
837
|
+
return table.columns[columnName];
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Two storage columns share a type when their `nativeType` and `typeParams`
|
|
842
|
+
* match. The contract is canonicalized, so `typeParams` key order is stable and
|
|
843
|
+
* a JSON comparison is exact. `codecId` and `nullable` are intentionally not
|
|
844
|
+
* compared: they do not change the database-level type that governs a join.
|
|
845
|
+
*/
|
|
846
|
+
function sameStorageType(a: StorageColumn, b: StorageColumn): boolean {
|
|
847
|
+
return (
|
|
848
|
+
a.nativeType === b.nativeType &&
|
|
849
|
+
JSON.stringify(a.typeParams ?? null) === JSON.stringify(b.typeParams ?? null)
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function describeColumnType(column: StorageColumn): string {
|
|
854
|
+
return column.typeParams === undefined
|
|
855
|
+
? column.nativeType
|
|
856
|
+
: `${column.nativeType} ${JSON.stringify(column.typeParams)}`;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Validates one side of an N:M join: the junction columns and the model
|
|
861
|
+
* columns they pair against positionally must be equal in number, exist in
|
|
862
|
+
* their tables, and share the same storage type (`nativeType` + `typeParams`).
|
|
863
|
+
* The junction's storage foreign keys already guarantee this for user-declared
|
|
864
|
+
* FK constraints, but `through` is a logical descriptor never tied to them by
|
|
865
|
+
* the rest of validation — and the TS builder accepts explicit join columns
|
|
866
|
+
* without requiring a junction FK at all — so this checks the columns directly
|
|
867
|
+
* against storage, one path regardless of how the junction was authored.
|
|
868
|
+
*
|
|
869
|
+
* Joined columns must be the *same* storage type, not merely compatible:
|
|
870
|
+
* relying on implicit conversion (e.g. `text`↔`character`) is unsafe on writes
|
|
871
|
+
* — `character(n)` space-padding makes such coercions non-associative — and no
|
|
872
|
+
* ADR sanctions heterogeneous junction columns. Equality is the conservative
|
|
873
|
+
* default; it can be relaxed deliberately if a real use case ever appears.
|
|
874
|
+
*/
|
|
875
|
+
function validateThroughJoinSide(input: {
|
|
876
|
+
readonly contract: Contract<SqlStorage>;
|
|
877
|
+
readonly qualifiedName: string;
|
|
878
|
+
readonly modelColumns: readonly string[];
|
|
879
|
+
readonly modelColumnsLabel: string;
|
|
880
|
+
/**
|
|
881
|
+
* The model side of the join, when resolvable. Absent for a cross-space
|
|
882
|
+
* target whose storage lives in another contract: the length and
|
|
883
|
+
* junction-column-existence checks still run, but the target-column
|
|
884
|
+
* existence and type checks are skipped because the target table is not
|
|
885
|
+
* available here. `fieldToColumn` is present when `modelColumns` are domain
|
|
886
|
+
* field names (the `on.*Fields` arrays), absent when they are already storage
|
|
887
|
+
* column names (the `through.*Columns` arrays).
|
|
888
|
+
*/
|
|
889
|
+
readonly model?: {
|
|
890
|
+
readonly namespaceId: string;
|
|
891
|
+
readonly table: string;
|
|
892
|
+
readonly fieldToColumn?: Readonly<Record<string, { readonly column: string }>>;
|
|
893
|
+
};
|
|
894
|
+
readonly junctionColumns: readonly string[];
|
|
895
|
+
readonly junctionColumnsLabel: string;
|
|
896
|
+
readonly junctionNamespaceId: string;
|
|
897
|
+
readonly junctionTable: string;
|
|
898
|
+
}): void {
|
|
899
|
+
const fail = (detail: string): ContractValidationError =>
|
|
900
|
+
new ContractValidationError(
|
|
901
|
+
`Many-to-many relation "${input.qualifiedName}" ${detail}`,
|
|
902
|
+
'storage',
|
|
903
|
+
);
|
|
904
|
+
if (input.junctionColumns.length !== input.modelColumns.length) {
|
|
905
|
+
throw fail(
|
|
906
|
+
`pairs ${input.junctionColumnsLabel} (${input.junctionColumns.length}) with ${input.modelColumnsLabel} (${input.modelColumns.length}) of differing length; they join positionally and must match.`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
for (const [index, junctionColumnName] of input.junctionColumns.entries()) {
|
|
910
|
+
const modelColumnRef = input.modelColumns[index];
|
|
911
|
+
if (modelColumnRef === undefined) {
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const junctionColumn = lookupStorageColumn(
|
|
915
|
+
input.contract,
|
|
916
|
+
input.junctionNamespaceId,
|
|
917
|
+
input.junctionTable,
|
|
918
|
+
junctionColumnName,
|
|
919
|
+
);
|
|
920
|
+
if (junctionColumn === undefined) {
|
|
921
|
+
throw fail(
|
|
922
|
+
`${input.junctionColumnsLabel} references column "${junctionColumnName}" absent from junction table "${input.junctionNamespaceId}.${input.junctionTable}".`,
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
// Cross-space target: its storage lives in another contract, so the target
|
|
926
|
+
// column's existence and type cannot be checked here. Length and
|
|
927
|
+
// junction-column existence have already been validated above.
|
|
928
|
+
const model = input.model;
|
|
929
|
+
if (model === undefined) {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
let modelColumnName = modelColumnRef;
|
|
933
|
+
if (model.fieldToColumn !== undefined) {
|
|
934
|
+
const mapped = model.fieldToColumn[modelColumnRef];
|
|
935
|
+
if (mapped === undefined) {
|
|
936
|
+
throw fail(
|
|
937
|
+
`${input.modelColumnsLabel} references field "${modelColumnRef}" absent from model on table "${model.namespaceId}.${model.table}".`,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
modelColumnName = mapped.column;
|
|
941
|
+
}
|
|
942
|
+
const modelColumn = lookupStorageColumn(
|
|
943
|
+
input.contract,
|
|
944
|
+
model.namespaceId,
|
|
945
|
+
model.table,
|
|
946
|
+
modelColumnName,
|
|
947
|
+
);
|
|
948
|
+
if (modelColumn === undefined) {
|
|
949
|
+
throw fail(
|
|
950
|
+
`${input.modelColumnsLabel} references column "${modelColumnName}" absent from table "${model.namespaceId}.${model.table}".`,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (!sameStorageType(junctionColumn, modelColumn)) {
|
|
954
|
+
throw fail(
|
|
955
|
+
`joins "${input.junctionTable}.${junctionColumnName}" (${describeColumnType(junctionColumn)}) with "${model.table}.${modelColumnName}" (${describeColumnType(modelColumn)}) of differing storage type; junction columns must match the type of the column they reference.`,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Validates that every N:M relation's `through` descriptor is consistent with
|
|
963
|
+
* the storage columns it joins: both join sides match in column count,
|
|
964
|
+
* reference columns that exist in their tables, and pair columns of the same
|
|
965
|
+
* storage type. Without this, a `through` that disagrees with storage surfaces
|
|
966
|
+
* as a silently wrong JOIN at query time rather than a validation error here.
|
|
967
|
+
*/
|
|
968
|
+
function validateRelationThroughConsistency(contract: Contract<SqlStorage>): void {
|
|
969
|
+
for (const [namespaceId, namespace] of Object.entries(contract.domain.namespaces)) {
|
|
970
|
+
for (const [modelName, model] of Object.entries(namespace.models)) {
|
|
971
|
+
for (const [relationName, relation] of Object.entries(model.relations)) {
|
|
972
|
+
if (relation.cardinality !== 'N:M') continue;
|
|
973
|
+
const qualifiedName = `${namespaceId}.${modelName}.${relationName}`;
|
|
974
|
+
const { on, through } = relation;
|
|
975
|
+
const modelStorage = blindCast<SqlModelStorage, 'SQL contract model storage'>(
|
|
976
|
+
model.storage,
|
|
977
|
+
);
|
|
978
|
+
// Parent side: the owning model's localFields (domain field names) join
|
|
979
|
+
// the junction's parentColumns (storage columns).
|
|
980
|
+
validateThroughJoinSide({
|
|
981
|
+
contract,
|
|
982
|
+
qualifiedName,
|
|
983
|
+
modelColumns: on.localFields,
|
|
984
|
+
modelColumnsLabel: 'on.localFields',
|
|
985
|
+
model: {
|
|
986
|
+
namespaceId,
|
|
987
|
+
table: modelStorage.table,
|
|
988
|
+
fieldToColumn: modelStorage.fields,
|
|
989
|
+
},
|
|
990
|
+
junctionColumns: through.parentColumns,
|
|
991
|
+
junctionColumnsLabel: 'through.parentColumns',
|
|
992
|
+
junctionNamespaceId: through.namespaceId,
|
|
993
|
+
junctionTable: through.table,
|
|
994
|
+
});
|
|
995
|
+
// Child side: the junction's childColumns join the target model's
|
|
996
|
+
// targetColumns. Length and junction-column existence are checked
|
|
997
|
+
// regardless; a cross-space target lives in another contract, so its
|
|
998
|
+
// column existence and type are checked only when it is resolvable here.
|
|
999
|
+
const targetModel =
|
|
1000
|
+
relation.to.space === undefined
|
|
1001
|
+
? contract.domain.namespaces[relation.to.namespace]?.models[relation.to.model]
|
|
1002
|
+
: undefined;
|
|
1003
|
+
const targetModelSide =
|
|
1004
|
+
targetModel === undefined
|
|
1005
|
+
? undefined
|
|
1006
|
+
: {
|
|
1007
|
+
namespaceId: relation.to.namespace,
|
|
1008
|
+
table: blindCast<SqlModelStorage, 'SQL contract model storage'>(targetModel.storage)
|
|
1009
|
+
.table,
|
|
1010
|
+
};
|
|
1011
|
+
validateThroughJoinSide({
|
|
1012
|
+
contract,
|
|
1013
|
+
qualifiedName,
|
|
1014
|
+
modelColumns: through.targetColumns,
|
|
1015
|
+
modelColumnsLabel: 'through.targetColumns',
|
|
1016
|
+
...ifDefined('model', targetModelSide),
|
|
1017
|
+
junctionColumns: through.childColumns,
|
|
1018
|
+
junctionColumnsLabel: 'through.childColumns',
|
|
1019
|
+
junctionNamespaceId: through.namespaceId,
|
|
1020
|
+
junctionTable: through.table,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { StorageValueSetInput } from './ir/storage-value-set';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SQL-family extension to the framework entity-type authoring registry: a pack's entity-type
|
|
5
|
+
* `factory` output (framework `AuthoringEntityTypeFactoryOutput`) may derive a value-set from an
|
|
6
|
+
* entity it built (e.g. Postgres's `native_enum` deriving its ordered member values). The generic
|
|
7
|
+
* extension-block lowering pass (`contract-psl`'s interpreter) probes each entity-type descriptor's
|
|
8
|
+
* output for this hook after building the entity, and folds a non-undefined result into the same
|
|
9
|
+
* namespace's `valueSet` slot, keyed by the entity's block name.
|
|
10
|
+
*
|
|
11
|
+
* SQL-family concept — the returned shape ({@link StorageValueSetInput}) is SQL-specific, so this
|
|
12
|
+
* hook lives here instead of on the framework `AuthoringEntityTypeFactoryOutput` itself.
|
|
13
|
+
*/
|
|
14
|
+
export interface SqlValueSetDerivingEntityTypeOutput {
|
|
15
|
+
/**
|
|
16
|
+
* TypeScript treats method-syntax declarations bivariantly, so a pack's concretely-typed
|
|
17
|
+
* `deriveValueSet(entity: PostgresNativeEnum) => …` is structurally compatible with this
|
|
18
|
+
* interface's `unknown` parameter — no `never`-parameter contravariance bridge needed.
|
|
19
|
+
*/
|
|
20
|
+
deriveValueSet(entity: unknown): StorageValueSetInput | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Structural check for {@link SqlValueSetDerivingEntityTypeOutput}: no casts. */
|
|
24
|
+
export function providesValueSetDerivation(
|
|
25
|
+
output: unknown,
|
|
26
|
+
): output is SqlValueSetDerivingEntityTypeOutput {
|
|
27
|
+
if (typeof output !== 'object' || output === null || !('deriveValueSet' in output)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const { deriveValueSet } = output;
|
|
31
|
+
return typeof deriveValueSet === 'function';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* If `output` (an entity-type descriptor's factory output) provides
|
|
36
|
+
* {@link SqlValueSetDerivingEntityTypeOutput.deriveValueSet}, invoke it on `entity` and return the
|
|
37
|
+
* derived value-set; otherwise return `undefined`. `contract-psl`'s generic extension-block
|
|
38
|
+
* lowering pass calls this after building each entity so a value-set-carrying pack entity can
|
|
39
|
+
* contribute its value-set without the pass naming any target discriminator.
|
|
40
|
+
*/
|
|
41
|
+
export function deriveValueSetFromEntity(
|
|
42
|
+
output: unknown,
|
|
43
|
+
entity: unknown,
|
|
44
|
+
): StorageValueSetInput | undefined {
|
|
45
|
+
if (!providesValueSetDerivation(output)) return undefined;
|
|
46
|
+
return output.deriveValueSet(entity);
|
|
47
|
+
}
|