@prisma-next/sql-contract-ts 0.9.0 → 0.10.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/contract-builder.d.mts +123 -11
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +155 -18
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +10 -10
- package/src/build-contract.ts +138 -12
- package/src/composed-authoring-helpers.ts +2 -1
- package/src/contract-builder.ts +145 -4
- package/src/contract-definition.ts +40 -0
- package/src/contract-dsl.ts +53 -0
- package/src/contract-lowering.ts +5 -0
- package/src/contract-types.ts +51 -6
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-contract-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "SQL-specific TypeScript contract authoring surface for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/config": "0.
|
|
10
|
-
"@prisma-next/contract": "0.
|
|
11
|
-
"@prisma-next/contract-authoring": "0.
|
|
12
|
-
"@prisma-next/framework-components": "0.
|
|
13
|
-
"@prisma-next/sql-contract": "0.
|
|
14
|
-
"@prisma-next/utils": "0.
|
|
9
|
+
"@prisma-next/config": "0.10.0",
|
|
10
|
+
"@prisma-next/contract": "0.10.0",
|
|
11
|
+
"@prisma-next/contract-authoring": "0.10.0",
|
|
12
|
+
"@prisma-next/framework-components": "0.10.0",
|
|
13
|
+
"@prisma-next/sql-contract": "0.10.0",
|
|
14
|
+
"@prisma-next/utils": "0.10.0",
|
|
15
15
|
"arktype": "^2.2.0",
|
|
16
16
|
"pathe": "^2.0.3",
|
|
17
17
|
"ts-toolbelt": "^9.6.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@prisma-next/test-utils": "0.
|
|
21
|
-
"@prisma-next/tsconfig": "0.
|
|
20
|
+
"@prisma-next/test-utils": "0.10.0",
|
|
21
|
+
"@prisma-next/tsconfig": "0.10.0",
|
|
22
22
|
"@types/pg": "8.20.0",
|
|
23
23
|
"pg": "8.20.0",
|
|
24
|
-
"@prisma-next/tsdown": "0.
|
|
24
|
+
"@prisma-next/tsdown": "0.10.0",
|
|
25
25
|
"tsdown": "0.22.0",
|
|
26
26
|
"typescript": "5.9.3",
|
|
27
27
|
"vitest": "4.1.6"
|
package/src/build-contract.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type StorageHashBase,
|
|
18
18
|
} from '@prisma-next/contract/types';
|
|
19
19
|
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
20
|
-
import {
|
|
20
|
+
import { type Namespace, UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
21
21
|
import { validateIndexTypes } from '@prisma-next/sql-contract/index-type-validation';
|
|
22
22
|
import {
|
|
23
23
|
createIndexTypeRegistry,
|
|
@@ -28,10 +28,12 @@ import {
|
|
|
28
28
|
applyFkDefaults,
|
|
29
29
|
isPostgresEnumStorageEntry,
|
|
30
30
|
type PostgresEnumStorageEntry,
|
|
31
|
+
type SqlNamespaceTablesInput,
|
|
31
32
|
SqlStorage,
|
|
32
|
-
|
|
33
|
+
type SqlStorageInput,
|
|
33
34
|
type StorageColumn,
|
|
34
|
-
|
|
35
|
+
StorageTable,
|
|
36
|
+
type StorageTableInput,
|
|
35
37
|
type StorageTypeInstance,
|
|
36
38
|
toStorageTypeInstance,
|
|
37
39
|
} from '@prisma-next/sql-contract/types';
|
|
@@ -209,6 +211,71 @@ function buildDomainField(
|
|
|
209
211
|
};
|
|
210
212
|
}
|
|
211
213
|
|
|
214
|
+
function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): Set<string> {
|
|
215
|
+
const ids = new Set<string>();
|
|
216
|
+
ids.add(UNBOUND_NAMESPACE_ID);
|
|
217
|
+
for (const id of definition.namespaces ?? []) {
|
|
218
|
+
if (id.length > 0) {
|
|
219
|
+
ids.add(id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const model of definition.models) {
|
|
223
|
+
if (model.namespaceId !== undefined && model.namespaceId.length > 0) {
|
|
224
|
+
ids.add(model.namespaceId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return ids;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const POSTGRES_ENUM_NAMESPACE_ID = 'public';
|
|
231
|
+
|
|
232
|
+
function partitionStorageTypesForTarget(
|
|
233
|
+
targetId: string,
|
|
234
|
+
types: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
|
|
235
|
+
namespaceTypes?: Readonly<Record<string, Readonly<Record<string, PostgresEnumStorageEntry>>>>,
|
|
236
|
+
): {
|
|
237
|
+
readonly documentTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
|
|
238
|
+
readonly namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>>;
|
|
239
|
+
} {
|
|
240
|
+
const documentTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {};
|
|
241
|
+
const namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>> = {};
|
|
242
|
+
for (const [name, entry] of Object.entries(types)) {
|
|
243
|
+
if (isPostgresEnumStorageEntry(entry)) {
|
|
244
|
+
if (targetId !== 'postgres') {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`buildSqlContractFromDefinition: postgres enum "${name}" is only valid when target is "postgres" (got "${targetId}").`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
let slot = namespaceEnumTypesById[POSTGRES_ENUM_NAMESPACE_ID];
|
|
250
|
+
if (slot === undefined) {
|
|
251
|
+
slot = {};
|
|
252
|
+
namespaceEnumTypesById[POSTGRES_ENUM_NAMESPACE_ID] = slot;
|
|
253
|
+
}
|
|
254
|
+
slot[name] = entry;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
documentTypes[name] = entry;
|
|
258
|
+
}
|
|
259
|
+
if (namespaceTypes !== undefined) {
|
|
260
|
+
for (const [nsId, enumsInNs] of Object.entries(namespaceTypes)) {
|
|
261
|
+
for (const [name, entry] of Object.entries(enumsInNs)) {
|
|
262
|
+
if (targetId !== 'postgres') {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`buildSqlContractFromDefinition: postgres enum "${name}" is only valid when target is "postgres" (got "${targetId}").`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
let slot = namespaceEnumTypesById[nsId];
|
|
268
|
+
if (slot === undefined) {
|
|
269
|
+
slot = {};
|
|
270
|
+
namespaceEnumTypesById[nsId] = slot;
|
|
271
|
+
}
|
|
272
|
+
slot[name] = entry;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { documentTypes, namespaceEnumTypesById };
|
|
277
|
+
}
|
|
278
|
+
|
|
212
279
|
export function buildSqlContractFromDefinition(
|
|
213
280
|
definition: ContractDefinition,
|
|
214
281
|
codecLookup?: CodecLookup,
|
|
@@ -217,7 +284,8 @@ export function buildSqlContractFromDefinition(
|
|
|
217
284
|
const targetFamily = 'sql';
|
|
218
285
|
const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));
|
|
219
286
|
|
|
220
|
-
const
|
|
287
|
+
const tablesByNamespace: Record<string, Record<string, StorageTable>> = {};
|
|
288
|
+
const tableNameToNamespaceId = new Map<string, string>();
|
|
221
289
|
const executionDefaults: ExecutionMutationDefault[] = [];
|
|
222
290
|
const models: Record<string, ContractModel> = {};
|
|
223
291
|
const roots: Record<string, string> = {};
|
|
@@ -276,6 +344,11 @@ export function buildSqlContractFromDefinition(
|
|
|
276
344
|
}
|
|
277
345
|
}
|
|
278
346
|
|
|
347
|
+
const namespaceId =
|
|
348
|
+
semanticModel.namespaceId !== undefined && semanticModel.namespaceId.length > 0
|
|
349
|
+
? semanticModel.namespaceId
|
|
350
|
+
: UNBOUND_NAMESPACE_ID;
|
|
351
|
+
|
|
279
352
|
const foreignKeys = (semanticModel.foreignKeys ?? []).map((fk) => {
|
|
280
353
|
const targetModel = assertKnownTargetModel(
|
|
281
354
|
modelsByName,
|
|
@@ -289,9 +362,18 @@ export function buildSqlContractFromDefinition(
|
|
|
289
362
|
fk.references.table,
|
|
290
363
|
'Foreign key',
|
|
291
364
|
);
|
|
365
|
+
const targetNamespaceId =
|
|
366
|
+
fk.references.namespaceId ??
|
|
367
|
+
(targetModel.namespaceId !== undefined && targetModel.namespaceId.length > 0
|
|
368
|
+
? targetModel.namespaceId
|
|
369
|
+
: UNBOUND_NAMESPACE_ID);
|
|
292
370
|
return {
|
|
293
|
-
columns: fk.columns,
|
|
294
|
-
|
|
371
|
+
source: { namespaceId, tableName, columns: fk.columns },
|
|
372
|
+
target: {
|
|
373
|
+
namespaceId: targetNamespaceId,
|
|
374
|
+
tableName: fk.references.table,
|
|
375
|
+
columns: fk.references.columns,
|
|
376
|
+
},
|
|
295
377
|
...applyFkDefaults(
|
|
296
378
|
{
|
|
297
379
|
...ifDefined('constraint', fk.constraint),
|
|
@@ -305,7 +387,15 @@ export function buildSqlContractFromDefinition(
|
|
|
305
387
|
};
|
|
306
388
|
});
|
|
307
389
|
|
|
308
|
-
|
|
390
|
+
const existingNs = tableNameToNamespaceId.get(tableName);
|
|
391
|
+
if (existingNs !== undefined && existingNs !== namespaceId) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`buildSqlContractFromDefinition: table "${tableName}" is mapped in namespace "${namespaceId}" but already exists in namespace "${existingNs}".`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
tableNameToNamespaceId.set(tableName, namespaceId);
|
|
397
|
+
|
|
398
|
+
const tableInput: StorageTableInput = {
|
|
309
399
|
columns,
|
|
310
400
|
uniques: (semanticModel.uniques ?? []).map((u) => ({
|
|
311
401
|
columns: u.columns,
|
|
@@ -328,6 +418,18 @@ export function buildSqlContractFromDefinition(
|
|
|
328
418
|
: {}),
|
|
329
419
|
};
|
|
330
420
|
|
|
421
|
+
let nsTables = tablesByNamespace[namespaceId];
|
|
422
|
+
if (nsTables === undefined) {
|
|
423
|
+
nsTables = {};
|
|
424
|
+
tablesByNamespace[namespaceId] = nsTables;
|
|
425
|
+
}
|
|
426
|
+
if (nsTables[tableName] !== undefined) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`buildSqlContractFromDefinition: duplicate table "${tableName}" in namespace "${namespaceId}".`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
nsTables[tableName] = new StorageTable(tableInput);
|
|
432
|
+
|
|
331
433
|
// --- Build contract model ---
|
|
332
434
|
|
|
333
435
|
const storageFields: Record<string, { readonly column: string }> = {};
|
|
@@ -414,15 +516,39 @@ export function buildSqlContractFromDefinition(
|
|
|
414
516
|
];
|
|
415
517
|
}),
|
|
416
518
|
);
|
|
519
|
+
const { documentTypes, namespaceEnumTypesById } = partitionStorageTypesForTarget(
|
|
520
|
+
target,
|
|
521
|
+
storageTypes,
|
|
522
|
+
definition.namespaceTypes,
|
|
523
|
+
);
|
|
524
|
+
const namespaceCoordinateIds = collectStorageNamespaceCoordinateIds(definition);
|
|
525
|
+
for (const id of Object.keys(namespaceEnumTypesById)) {
|
|
526
|
+
namespaceCoordinateIds.add(id);
|
|
527
|
+
}
|
|
528
|
+
const { createNamespace } = definition;
|
|
529
|
+
const namespaces: Record<string, SqlNamespaceTablesInput | Namespace> = Object.fromEntries(
|
|
530
|
+
[...namespaceCoordinateIds].sort().map((id) => {
|
|
531
|
+
const enumTypes = namespaceEnumTypesById[id];
|
|
532
|
+
const nsInput: SqlNamespaceTablesInput = {
|
|
533
|
+
id,
|
|
534
|
+
tables: tablesByNamespace[id] ?? {},
|
|
535
|
+
...ifDefined('types', enumTypes),
|
|
536
|
+
};
|
|
537
|
+
return [id, createNamespace ? createNamespace(nsInput) : nsInput];
|
|
538
|
+
}),
|
|
539
|
+
);
|
|
417
540
|
const storageWithoutHash = {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
namespaces: { [UNSPECIFIED_NAMESPACE_ID]: SqlUnspecifiedNamespace.instance },
|
|
541
|
+
types: documentTypes,
|
|
542
|
+
namespaces,
|
|
421
543
|
};
|
|
422
544
|
const storageHash: StorageHashBase<string> = definition.storageHash
|
|
423
545
|
? coreHash(definition.storageHash)
|
|
424
|
-
: computeStorageHash({
|
|
425
|
-
|
|
546
|
+
: computeStorageHash({
|
|
547
|
+
target,
|
|
548
|
+
targetFamily,
|
|
549
|
+
storage: storageWithoutHash as Record<string, unknown>,
|
|
550
|
+
});
|
|
551
|
+
const storage = new SqlStorage({ ...storageWithoutHash, storageHash } as SqlStorageInput); // Builder types are wider than SqlStorageInput until SqlStorage normalises document types.
|
|
426
552
|
|
|
427
553
|
const executionSection =
|
|
428
554
|
executionDefaults.length > 0
|
|
@@ -119,7 +119,7 @@ type PackAwareModel<IndexTypes extends IndexTypeMap> = {
|
|
|
119
119
|
Relations extends Record<string, AnyRelationBuilder> = Record<never, never>,
|
|
120
120
|
>(
|
|
121
121
|
modelName: ModelName,
|
|
122
|
-
input: { readonly fields: Fields; readonly relations?: Relations },
|
|
122
|
+
input: { readonly fields: Fields; readonly relations?: Relations; readonly namespace?: string },
|
|
123
123
|
): ContractModelBuilder<ModelName, Fields, Relations, undefined, undefined, IndexTypes>;
|
|
124
124
|
<
|
|
125
125
|
Fields extends Record<string, ScalarFieldBuilder>,
|
|
@@ -127,6 +127,7 @@ type PackAwareModel<IndexTypes extends IndexTypeMap> = {
|
|
|
127
127
|
>(input: {
|
|
128
128
|
readonly fields: Fields;
|
|
129
129
|
readonly relations?: Relations;
|
|
130
|
+
readonly namespace?: string;
|
|
130
131
|
}): ContractModelBuilder<undefined, Fields, Relations, undefined, undefined, IndexTypes>;
|
|
131
132
|
};
|
|
132
133
|
|
package/src/contract-builder.ts
CHANGED
|
@@ -5,8 +5,10 @@ import type {
|
|
|
5
5
|
FamilyPackRef,
|
|
6
6
|
TargetPackRef,
|
|
7
7
|
} from '@prisma-next/framework-components/components';
|
|
8
|
+
import type { Namespace } from '@prisma-next/framework-components/ir';
|
|
8
9
|
import type {
|
|
9
10
|
PostgresEnumStorageEntry,
|
|
11
|
+
SqlNamespaceTablesInput,
|
|
10
12
|
StorageTypeInstance,
|
|
11
13
|
} from '@prisma-next/sql-contract/types';
|
|
12
14
|
import { buildSqlContractFromDefinition } from './build-contract';
|
|
@@ -35,6 +37,7 @@ export { buildSqlContractFromDefinition } from './build-contract';
|
|
|
35
37
|
type ModelLike = {
|
|
36
38
|
readonly stageOne: {
|
|
37
39
|
readonly modelName?: string;
|
|
40
|
+
readonly namespace?: string;
|
|
38
41
|
readonly fields: Record<string, ScalarFieldBuilder>;
|
|
39
42
|
readonly relations: Record<string, RelationBuilder<RelationState>>;
|
|
40
43
|
};
|
|
@@ -54,6 +57,7 @@ type ContractDefinition<
|
|
|
54
57
|
Naming extends ContractInput['naming'] | undefined,
|
|
55
58
|
StorageHash extends string | undefined,
|
|
56
59
|
ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined,
|
|
60
|
+
Namespaces extends readonly string[] | undefined = undefined,
|
|
57
61
|
> = {
|
|
58
62
|
readonly family: Family;
|
|
59
63
|
readonly target: Target;
|
|
@@ -62,6 +66,8 @@ type ContractDefinition<
|
|
|
62
66
|
readonly storageHash?: StorageHash;
|
|
63
67
|
readonly foreignKeyDefaults?: ForeignKeyDefaults;
|
|
64
68
|
readonly capabilities?: Capabilities;
|
|
69
|
+
readonly namespaces?: Namespaces;
|
|
70
|
+
readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;
|
|
65
71
|
readonly types?: Types;
|
|
66
72
|
readonly models?: Models;
|
|
67
73
|
readonly codecLookup?: CodecLookup;
|
|
@@ -75,6 +81,7 @@ type ContractScaffold<
|
|
|
75
81
|
Naming extends ContractInput['naming'] | undefined,
|
|
76
82
|
StorageHash extends string | undefined,
|
|
77
83
|
ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined,
|
|
84
|
+
Namespaces extends readonly string[] | undefined = undefined,
|
|
78
85
|
> = {
|
|
79
86
|
readonly family: Family;
|
|
80
87
|
readonly target: Target;
|
|
@@ -83,6 +90,8 @@ type ContractScaffold<
|
|
|
83
90
|
readonly storageHash?: StorageHash;
|
|
84
91
|
readonly foreignKeyDefaults?: ForeignKeyDefaults;
|
|
85
92
|
readonly capabilities?: Capabilities;
|
|
93
|
+
readonly namespaces?: Namespaces;
|
|
94
|
+
readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;
|
|
86
95
|
readonly codecLookup?: CodecLookup;
|
|
87
96
|
};
|
|
88
97
|
|
|
@@ -114,6 +123,126 @@ function validateTargetPackRef(
|
|
|
114
123
|
}
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Per-target reserved namespace names enforced by `defineContract` for
|
|
128
|
+
* SQL family contracts. Two categories:
|
|
129
|
+
*
|
|
130
|
+
* 1. **IR sentinels** (`__unbound__`, `__unspecified__`) — reserved on
|
|
131
|
+
* every SQL target. The double-underscore decoration marks them as
|
|
132
|
+
* framework-reserved coordinates; user code must not declare them
|
|
133
|
+
* explicitly.
|
|
134
|
+
* 2. **Target-specific PSL keywords** — Postgres reserves the bare
|
|
135
|
+
* `unbound` identifier for the late-binding opt-in
|
|
136
|
+
* (`namespace unbound { … }`) so the TS surface must reject it from
|
|
137
|
+
* `defineContract({ namespaces })` lists. SQLite has no schema
|
|
138
|
+
* concept and rejects every non-empty namespaces list outright;
|
|
139
|
+
* callers should declare `namespaces: []` or omit the field.
|
|
140
|
+
*/
|
|
141
|
+
function validateNamespaceDeclarations(
|
|
142
|
+
target: TargetPackRef<'sql', string>,
|
|
143
|
+
namespaces: readonly string[] | undefined,
|
|
144
|
+
): void {
|
|
145
|
+
if (!namespaces) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (target.targetId === 'sqlite' && namespaces.length > 0) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`defineContract: SQLite contracts cannot declare namespaces (SQLite has no schema concept; emitted DDL is always unqualified). Received namespaces: [${namespaces
|
|
152
|
+
.map((name) => `"${name}"`)
|
|
153
|
+
.join(', ')}].`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const seen = new Set<string>();
|
|
158
|
+
for (const namespace of namespaces) {
|
|
159
|
+
if (namespace.length === 0) {
|
|
160
|
+
throw new Error('defineContract: namespace names cannot be empty.');
|
|
161
|
+
}
|
|
162
|
+
if (namespace.trim().length === 0) {
|
|
163
|
+
throw new Error(`defineContract: namespace name "${namespace}" cannot be whitespace-only.`);
|
|
164
|
+
}
|
|
165
|
+
if (namespace === '__unbound__' || namespace === '__unspecified__') {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`defineContract: namespace name "${namespace}" is a reserved IR sentinel and cannot appear in the declared namespaces list.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (target.targetId === 'postgres' && namespace === 'unbound') {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`defineContract: namespace name "unbound" is reserved by Postgres for the late-binding opt-in (use \`namespace unbound { … }\` in PSL instead of declaring it as a regular schema).`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (seen.has(namespace)) {
|
|
176
|
+
throw new Error(`defineContract: namespaces list contains duplicate entry "${namespace}".`);
|
|
177
|
+
}
|
|
178
|
+
seen.add(namespace);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Per-model `namespace` validation paired with
|
|
184
|
+
* {@link validateNamespaceDeclarations}. Mirrors the reserved-name
|
|
185
|
+
* rules so the per-model surface stays consistent with the contract-
|
|
186
|
+
* level surface:
|
|
187
|
+
*
|
|
188
|
+
* - `__unbound__` / `__unspecified__` — reserved IR sentinels on
|
|
189
|
+
* every SQL target.
|
|
190
|
+
* - `unbound` on Postgres — reserved for the PSL
|
|
191
|
+
* `namespace unbound { … }` opt-in.
|
|
192
|
+
*
|
|
193
|
+
* Additionally enforces that each per-model `namespace` either
|
|
194
|
+
* references an entry in the contract's declared `namespaces` list or
|
|
195
|
+
* names the Postgres late-binding keyword (`unbound`) — the latter is
|
|
196
|
+
* not a "declared namespace" but is a legal opt-in only via PSL today,
|
|
197
|
+
* so the TS surface also rejects it on the per-model side and points
|
|
198
|
+
* authors at the PSL `namespace unbound { … }` block.
|
|
199
|
+
*
|
|
200
|
+
* The SQLite per-model `namespace` field is rejected outright (SQLite
|
|
201
|
+
* has no schema concept).
|
|
202
|
+
*/
|
|
203
|
+
function validatePerModelNamespaces(
|
|
204
|
+
target: TargetPackRef<'sql', string>,
|
|
205
|
+
namespaces: readonly string[] | undefined,
|
|
206
|
+
models: Record<string, ModelLike>,
|
|
207
|
+
): void {
|
|
208
|
+
const declaredNamespaces = new Set<string>(namespaces ?? []);
|
|
209
|
+
|
|
210
|
+
for (const [modelKey, modelBuilder] of Object.entries(models)) {
|
|
211
|
+
const perModelNamespace = modelBuilder.stageOne.namespace;
|
|
212
|
+
if (perModelNamespace === undefined) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (target.targetId === 'sqlite') {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`defineContract: model "${modelKey}" sets \`namespace: "${perModelNamespace}"\` but the target is SQLite (SQLite has no schema concept; remove the per-model \`namespace\` field).`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (perModelNamespace === '__unbound__' || perModelNamespace === '__unspecified__') {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`defineContract: model "${modelKey}" sets \`namespace: "${perModelNamespace}"\` but that name is a reserved IR sentinel and cannot appear in user code.`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (target.targetId === 'postgres' && perModelNamespace === 'unbound') {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`defineContract: model "${modelKey}" sets \`namespace: "unbound"\` but that name is reserved by Postgres for the late-binding opt-in (use \`namespace unbound { … }\` in PSL instead — there is no equivalent surface in the TS builder today).`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!declaredNamespaces.has(perModelNamespace)) {
|
|
235
|
+
const hint =
|
|
236
|
+
declaredNamespaces.size > 0
|
|
237
|
+
? ` Declared namespaces: [${[...declaredNamespaces].map((name) => `"${name}"`).join(', ')}].`
|
|
238
|
+
: ' The contract does not declare any namespaces; add `namespaces: ["…"]` to `defineContract` first.';
|
|
239
|
+
throw new Error(
|
|
240
|
+
`defineContract: model "${modelKey}" references namespace "${perModelNamespace}" but that name does not appear in the contract's declared \`namespaces\` list.${hint}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
117
246
|
function validateExtensionPackRefs(
|
|
118
247
|
target: TargetPackRef<'sql', string>,
|
|
119
248
|
extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
@@ -152,6 +281,12 @@ function buildContractFromDsl(
|
|
|
152
281
|
): ReturnType<typeof buildSqlContractFromDefinition> {
|
|
153
282
|
validateTargetPackRef(definition.family, definition.target);
|
|
154
283
|
validateExtensionPackRefs(definition.target, definition.extensionPacks);
|
|
284
|
+
validateNamespaceDeclarations(definition.target, definition.namespaces);
|
|
285
|
+
validatePerModelNamespaces(
|
|
286
|
+
definition.target,
|
|
287
|
+
definition.namespaces,
|
|
288
|
+
(definition.models ?? {}) as Record<string, ModelLike>,
|
|
289
|
+
);
|
|
155
290
|
|
|
156
291
|
return buildSqlContractFromDefinition(
|
|
157
292
|
buildContractDefinition(definition),
|
|
@@ -174,6 +309,7 @@ export function defineContract<
|
|
|
174
309
|
const Naming extends ContractInput['naming'] | undefined = undefined,
|
|
175
310
|
const StorageHash extends string | undefined = undefined,
|
|
176
311
|
const ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined = undefined,
|
|
312
|
+
const Namespaces extends readonly string[] | undefined = undefined,
|
|
177
313
|
>(
|
|
178
314
|
definition: ContractDefinition<
|
|
179
315
|
Family,
|
|
@@ -184,7 +320,8 @@ export function defineContract<
|
|
|
184
320
|
Capabilities,
|
|
185
321
|
Naming,
|
|
186
322
|
StorageHash,
|
|
187
|
-
ForeignKeyDefaults
|
|
323
|
+
ForeignKeyDefaults,
|
|
324
|
+
Namespaces
|
|
188
325
|
>,
|
|
189
326
|
): SqlContractResult<
|
|
190
327
|
ContractDefinition<
|
|
@@ -196,7 +333,8 @@ export function defineContract<
|
|
|
196
333
|
Capabilities,
|
|
197
334
|
Naming,
|
|
198
335
|
StorageHash,
|
|
199
|
-
ForeignKeyDefaults
|
|
336
|
+
ForeignKeyDefaults,
|
|
337
|
+
Namespaces
|
|
200
338
|
>
|
|
201
339
|
>;
|
|
202
340
|
export function defineContract<
|
|
@@ -214,6 +352,7 @@ export function defineContract<
|
|
|
214
352
|
const Naming extends ContractInput['naming'] | undefined = undefined,
|
|
215
353
|
const StorageHash extends string | undefined = undefined,
|
|
216
354
|
const ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined = undefined,
|
|
355
|
+
const Namespaces extends readonly string[] | undefined = undefined,
|
|
217
356
|
>(
|
|
218
357
|
definition: ContractScaffold<
|
|
219
358
|
Family,
|
|
@@ -222,7 +361,8 @@ export function defineContract<
|
|
|
222
361
|
Capabilities,
|
|
223
362
|
Naming,
|
|
224
363
|
StorageHash,
|
|
225
|
-
ForeignKeyDefaults
|
|
364
|
+
ForeignKeyDefaults,
|
|
365
|
+
Namespaces
|
|
226
366
|
>,
|
|
227
367
|
factory: ContractFactory<Family, Target, Types, Models, ExtensionPacks>,
|
|
228
368
|
): SqlContractResult<
|
|
@@ -235,7 +375,8 @@ export function defineContract<
|
|
|
235
375
|
Capabilities,
|
|
236
376
|
Naming,
|
|
237
377
|
StorageHash,
|
|
238
|
-
ForeignKeyDefaults
|
|
378
|
+
ForeignKeyDefaults,
|
|
379
|
+
Namespaces
|
|
239
380
|
>
|
|
240
381
|
>;
|
|
241
382
|
export function defineContract(
|
|
@@ -2,9 +2,11 @@ import type { ColumnDefault, ExecutionMutationDefaultPhases } from '@prisma-next
|
|
|
2
2
|
import type { ForeignKeyDefaultsState } from '@prisma-next/contract-authoring';
|
|
3
3
|
import type { ColumnTypeDescriptor } from '@prisma-next/framework-components/codec';
|
|
4
4
|
import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';
|
|
5
|
+
import type { Namespace } from '@prisma-next/framework-components/ir';
|
|
5
6
|
import type {
|
|
6
7
|
PostgresEnumStorageEntry,
|
|
7
8
|
ReferentialAction,
|
|
9
|
+
SqlNamespaceTablesInput,
|
|
8
10
|
StorageTypeInstance,
|
|
9
11
|
} from '@prisma-next/sql-contract/types';
|
|
10
12
|
|
|
@@ -43,6 +45,13 @@ export interface ForeignKeyNode {
|
|
|
43
45
|
readonly model: string;
|
|
44
46
|
readonly table: string;
|
|
45
47
|
readonly columns: readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* Namespace coordinate of the referenced table. When omitted the
|
|
50
|
+
* assembler resolves the coordinate from the referenced model node's
|
|
51
|
+
* own `namespaceId`; the field exists so authoring paths that already
|
|
52
|
+
* know the target namespace can stamp it explicitly.
|
|
53
|
+
*/
|
|
54
|
+
readonly namespaceId?: string;
|
|
46
55
|
};
|
|
47
56
|
readonly name?: string;
|
|
48
57
|
readonly onDelete?: ReferentialAction;
|
|
@@ -87,6 +96,17 @@ export interface ValueObjectNode {
|
|
|
87
96
|
export interface ModelNode {
|
|
88
97
|
readonly modelName: string;
|
|
89
98
|
readonly tableName: string;
|
|
99
|
+
/**
|
|
100
|
+
* Resolved namespace coordinate for this model — the key into the
|
|
101
|
+
* parent contract's `SqlStorage.namespaces` map. Omitting the field
|
|
102
|
+
* (or setting it to the framework's `UNBOUND_NAMESPACE_ID` sentinel)
|
|
103
|
+
* selects the late-bound slot, which renders as unqualified DDL.
|
|
104
|
+
*
|
|
105
|
+
* Populated by per-target PSL interpreters from the resolved
|
|
106
|
+
* `namespace { … }` AST bucket; the TS builder also sets it from the
|
|
107
|
+
* per-model `namespace` field once that authoring surface lands.
|
|
108
|
+
*/
|
|
109
|
+
readonly namespaceId?: string;
|
|
90
110
|
readonly fields: readonly (FieldNode | ValueObjectFieldNode)[];
|
|
91
111
|
readonly id?: PrimaryKeyNode;
|
|
92
112
|
readonly uniques?: readonly UniqueConstraintNode[];
|
|
@@ -102,6 +122,26 @@ export interface ContractDefinition {
|
|
|
102
122
|
readonly storageHash?: string;
|
|
103
123
|
readonly foreignKeyDefaults?: ForeignKeyDefaultsState;
|
|
104
124
|
readonly storageTypes?: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
|
|
125
|
+
/**
|
|
126
|
+
* Enum types declared inside a named `namespace { enum … }` block,
|
|
127
|
+
* keyed first by namespace id then by type name. These are routed to
|
|
128
|
+
* `storage.namespaces[nsId].types` rather than the implicit fallback
|
|
129
|
+
* namespace used for top-level `storageTypes` enums.
|
|
130
|
+
*/
|
|
131
|
+
readonly namespaceTypes?: Readonly<
|
|
132
|
+
Record<string, Readonly<Record<string, PostgresEnumStorageEntry>>>
|
|
133
|
+
>;
|
|
134
|
+
/**
|
|
135
|
+
* Declared namespace coordinates for this contract — populates
|
|
136
|
+
* `SqlStorage.namespaces` together with `createNamespace`.
|
|
137
|
+
*/
|
|
138
|
+
readonly namespaces?: readonly string[];
|
|
139
|
+
/**
|
|
140
|
+
* Target-supplied factory that materialises a `Namespace` concretion
|
|
141
|
+
* for a declared namespace coordinate. Mirrors
|
|
142
|
+
* `ContractInput.createNamespace`.
|
|
143
|
+
*/
|
|
144
|
+
readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;
|
|
105
145
|
readonly models: readonly ModelNode[];
|
|
106
146
|
readonly valueObjects?: readonly ValueObjectNode[];
|
|
107
147
|
}
|