@prisma-next/sql-contract-ts 0.16.0-dev.3 → 0.16.0-dev.31
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/README.md +1 -1
- package/dist/{build-contract-CAX6inbk.mjs → build-contract-DbUeVkiu.mjs} +94 -30
- package/dist/build-contract-DbUeVkiu.mjs.map +1 -0
- package/dist/config-types.d.mts.map +1 -1
- package/dist/config-types.mjs +7 -6
- package/dist/config-types.mjs.map +1 -1
- package/dist/contract-builder.d.mts +30 -27
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +244 -87
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +10 -10
- package/schemas/data-contract-sql-v1.json +4 -4
- package/src/authoring-helper-runtime.ts +16 -3
- package/src/build-contract.ts +102 -33
- package/src/composed-authoring-helpers.ts +26 -21
- package/src/config-types.ts +9 -5
- package/src/contract-builder.ts +94 -42
- package/src/contract-definition.ts +4 -1
- package/src/contract-dsl.ts +59 -15
- package/src/contract-errors.ts +39 -0
- package/src/contract-lowering.ts +116 -44
- package/src/contract-types.ts +7 -7
- package/dist/build-contract-CAX6inbk.mjs.map +0 -1
package/src/contract-builder.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type ScalarFieldBuilder,
|
|
34
34
|
type SqlStageSpec,
|
|
35
35
|
} from './contract-dsl';
|
|
36
|
+
import { contractError } from './contract-errors';
|
|
36
37
|
import { buildContractDefinition } from './contract-lowering';
|
|
37
38
|
import type { SqlContractResult } from './contract-types';
|
|
38
39
|
import type { EnumTypeHandle } from './enum-type';
|
|
@@ -57,7 +58,7 @@ type ContractDefinition<
|
|
|
57
58
|
Target extends TargetPackRef<'sql', string>,
|
|
58
59
|
Types extends Record<string, StorageTypeInstance>,
|
|
59
60
|
Models extends Record<string, ModelLike>,
|
|
60
|
-
|
|
61
|
+
Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
|
|
61
62
|
Naming extends ContractInput['naming'] | undefined,
|
|
62
63
|
StorageHash extends string | undefined,
|
|
63
64
|
ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined,
|
|
@@ -66,7 +67,7 @@ type ContractDefinition<
|
|
|
66
67
|
> = {
|
|
67
68
|
readonly family: Family;
|
|
68
69
|
readonly target: Target;
|
|
69
|
-
readonly
|
|
70
|
+
readonly extensions?: Extensions;
|
|
70
71
|
readonly naming?: Naming;
|
|
71
72
|
readonly storageHash?: StorageHash;
|
|
72
73
|
readonly foreignKeyDefaults?: ForeignKeyDefaults;
|
|
@@ -83,7 +84,7 @@ type ContractDefinition<
|
|
|
83
84
|
type ContractScaffold<
|
|
84
85
|
Family extends FamilyPackRef<string>,
|
|
85
86
|
Target extends TargetPackRef<'sql', string>,
|
|
86
|
-
|
|
87
|
+
Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
|
|
87
88
|
Naming extends ContractInput['naming'] | undefined,
|
|
88
89
|
StorageHash extends string | undefined,
|
|
89
90
|
ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined,
|
|
@@ -92,7 +93,7 @@ type ContractScaffold<
|
|
|
92
93
|
> = {
|
|
93
94
|
readonly family: Family;
|
|
94
95
|
readonly target: Target;
|
|
95
|
-
readonly
|
|
96
|
+
readonly extensions?: Extensions;
|
|
96
97
|
readonly naming?: Naming;
|
|
97
98
|
readonly storageHash?: StorageHash;
|
|
98
99
|
readonly foreignKeyDefaults?: ForeignKeyDefaults;
|
|
@@ -111,9 +112,9 @@ type ContractFactory<
|
|
|
111
112
|
Target extends TargetPackRef<'sql', string>,
|
|
112
113
|
Types extends Record<string, StorageTypeInstance>,
|
|
113
114
|
Models extends Record<string, ModelLike>,
|
|
114
|
-
|
|
115
|
+
Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
|
|
115
116
|
Enums extends Record<string, EnumTypeHandle> = Record<string, EnumTypeHandle>,
|
|
116
|
-
> = (helpers: ComposedAuthoringHelpers<Family, Target,
|
|
117
|
+
> = (helpers: ComposedAuthoringHelpers<Family, Target, Extensions>) => {
|
|
117
118
|
readonly types?: Types;
|
|
118
119
|
readonly models?: Models;
|
|
119
120
|
readonly enums?: Enums;
|
|
@@ -125,14 +126,24 @@ function validateTargetPackRef(
|
|
|
125
126
|
target: TargetPackRef<'sql', string>,
|
|
126
127
|
): void {
|
|
127
128
|
if (family.familyId !== 'sql') {
|
|
128
|
-
throw
|
|
129
|
+
throw contractError(
|
|
130
|
+
'CONTRACT.PACK_FAMILY_MISMATCH',
|
|
129
131
|
`defineContract only accepts SQL family packs. Received family "${family.familyId}".`,
|
|
132
|
+
{ meta: { packId: family.id, packFamilyId: family.familyId, contractFamilyId: 'sql' } },
|
|
130
133
|
);
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
if (target.familyId !== family.familyId) {
|
|
134
|
-
throw
|
|
137
|
+
throw contractError(
|
|
138
|
+
'CONTRACT.PACK_FAMILY_MISMATCH',
|
|
135
139
|
`target pack "${target.id}" targets family "${target.familyId}" but contract family is "${family.familyId}".`,
|
|
140
|
+
{
|
|
141
|
+
meta: {
|
|
142
|
+
packId: target.id,
|
|
143
|
+
packFamilyId: target.familyId,
|
|
144
|
+
contractFamilyId: family.familyId,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
136
147
|
);
|
|
137
148
|
}
|
|
138
149
|
}
|
|
@@ -161,33 +172,51 @@ function validateNamespaceDeclarations(
|
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
if (target.targetId === 'sqlite' && namespaces.length > 0) {
|
|
164
|
-
throw
|
|
175
|
+
throw contractError(
|
|
176
|
+
'CONTRACT.NAMESPACE_UNSUPPORTED',
|
|
165
177
|
`defineContract: SQLite contracts cannot declare namespaces (SQLite has no schema concept; emitted DDL is always unqualified). Received namespaces: [${namespaces
|
|
166
178
|
.map((name) => `"${name}"`)
|
|
167
179
|
.join(', ')}].`,
|
|
180
|
+
{ meta: { namespaces, targetId: target.targetId } },
|
|
168
181
|
);
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
const seen = new Set<string>();
|
|
172
185
|
for (const namespace of namespaces) {
|
|
173
186
|
if (namespace.length === 0) {
|
|
174
|
-
throw
|
|
187
|
+
throw contractError(
|
|
188
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
189
|
+
'defineContract: namespace names cannot be empty.',
|
|
190
|
+
{ meta: { namespace, reason: 'empty' } },
|
|
191
|
+
);
|
|
175
192
|
}
|
|
176
193
|
if (namespace.trim().length === 0) {
|
|
177
|
-
throw
|
|
194
|
+
throw contractError(
|
|
195
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
196
|
+
`defineContract: namespace name "${namespace}" cannot be whitespace-only.`,
|
|
197
|
+
{ meta: { namespace, reason: 'whitespace-only' } },
|
|
198
|
+
);
|
|
178
199
|
}
|
|
179
200
|
if (namespace === '__unbound__' || namespace === '__unspecified__') {
|
|
180
|
-
throw
|
|
201
|
+
throw contractError(
|
|
202
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
181
203
|
`defineContract: namespace name "${namespace}" is a reserved IR sentinel and cannot appear in the declared namespaces list.`,
|
|
204
|
+
{ meta: { namespace, reason: 'reserved-ir-sentinel' } },
|
|
182
205
|
);
|
|
183
206
|
}
|
|
184
207
|
if (target.targetId === 'postgres' && namespace === 'unbound') {
|
|
185
|
-
throw
|
|
208
|
+
throw contractError(
|
|
209
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
186
210
|
`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).`,
|
|
211
|
+
{ meta: { namespace, reason: 'reserved-by-postgres' } },
|
|
187
212
|
);
|
|
188
213
|
}
|
|
189
214
|
if (seen.has(namespace)) {
|
|
190
|
-
throw
|
|
215
|
+
throw contractError(
|
|
216
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
217
|
+
`defineContract: namespaces list contains duplicate entry "${namespace}".`,
|
|
218
|
+
{ meta: { kind: 'namespace', name: namespace } },
|
|
219
|
+
);
|
|
191
220
|
}
|
|
192
221
|
seen.add(namespace);
|
|
193
222
|
}
|
|
@@ -228,20 +257,26 @@ function validatePerModelNamespaces(
|
|
|
228
257
|
}
|
|
229
258
|
|
|
230
259
|
if (target.targetId === 'sqlite') {
|
|
231
|
-
throw
|
|
260
|
+
throw contractError(
|
|
261
|
+
'CONTRACT.NAMESPACE_UNSUPPORTED',
|
|
232
262
|
`defineContract: model "${modelKey}" sets \`namespace: "${perModelNamespace}"\` but the target is SQLite (SQLite has no schema concept; remove the per-model \`namespace\` field).`,
|
|
263
|
+
{ meta: { modelKey, namespace: perModelNamespace, targetId: target.targetId } },
|
|
233
264
|
);
|
|
234
265
|
}
|
|
235
266
|
|
|
236
267
|
if (perModelNamespace === '__unbound__' || perModelNamespace === '__unspecified__') {
|
|
237
|
-
throw
|
|
268
|
+
throw contractError(
|
|
269
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
238
270
|
`defineContract: model "${modelKey}" sets \`namespace: "${perModelNamespace}"\` but that name is a reserved IR sentinel and cannot appear in user code.`,
|
|
271
|
+
{ meta: { modelKey, namespace: perModelNamespace, reason: 'reserved-ir-sentinel' } },
|
|
239
272
|
);
|
|
240
273
|
}
|
|
241
274
|
|
|
242
275
|
if (target.targetId === 'postgres' && perModelNamespace === 'unbound') {
|
|
243
|
-
throw
|
|
276
|
+
throw contractError(
|
|
277
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
244
278
|
`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).`,
|
|
279
|
+
{ meta: { modelKey, namespace: perModelNamespace, reason: 'reserved-by-postgres' } },
|
|
245
280
|
);
|
|
246
281
|
}
|
|
247
282
|
|
|
@@ -250,8 +285,10 @@ function validatePerModelNamespaces(
|
|
|
250
285
|
declaredNamespaces.size > 0
|
|
251
286
|
? ` Declared namespaces: [${[...declaredNamespaces].map((name) => `"${name}"`).join(', ')}].`
|
|
252
287
|
: ' The contract does not declare any namespaces; add `namespaces: ["…"]` to `defineContract` first.';
|
|
253
|
-
throw
|
|
288
|
+
throw contractError(
|
|
289
|
+
'CONTRACT.NAMESPACE_UNKNOWN',
|
|
254
290
|
`defineContract: model "${modelKey}" references namespace "${perModelNamespace}" but that name does not appear in the contract's declared \`namespaces\` list.${hint}`,
|
|
291
|
+
{ meta: { modelKey, namespace: perModelNamespace, declared: [...declaredNamespaces] } },
|
|
255
292
|
);
|
|
256
293
|
}
|
|
257
294
|
}
|
|
@@ -259,28 +296,46 @@ function validatePerModelNamespaces(
|
|
|
259
296
|
|
|
260
297
|
function validateExtensionPackRefs(
|
|
261
298
|
target: TargetPackRef<'sql', string>,
|
|
262
|
-
|
|
299
|
+
extensions?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
263
300
|
): void {
|
|
264
|
-
if (!
|
|
301
|
+
if (!extensions) {
|
|
265
302
|
return;
|
|
266
303
|
}
|
|
267
304
|
|
|
268
|
-
for (const packRef of Object.values(
|
|
305
|
+
for (const packRef of Object.values(extensions)) {
|
|
269
306
|
if (packRef.kind !== 'extension') {
|
|
270
|
-
throw
|
|
271
|
-
|
|
307
|
+
throw contractError(
|
|
308
|
+
'CONTRACT.PACK_REF_INVALID',
|
|
309
|
+
`defineContract only accepts extension pack refs in extensions. Received kind "${packRef.kind}".`,
|
|
310
|
+
{ meta: { packId: packRef.id, kind: packRef.kind } },
|
|
272
311
|
);
|
|
273
312
|
}
|
|
274
313
|
|
|
275
314
|
if (packRef.familyId !== target.familyId) {
|
|
276
|
-
throw
|
|
315
|
+
throw contractError(
|
|
316
|
+
'CONTRACT.PACK_FAMILY_MISMATCH',
|
|
277
317
|
`extension pack "${packRef.id}" targets family "${packRef.familyId}" but contract target family is "${target.familyId}".`,
|
|
318
|
+
{
|
|
319
|
+
meta: {
|
|
320
|
+
packId: packRef.id,
|
|
321
|
+
packFamilyId: packRef.familyId,
|
|
322
|
+
contractFamilyId: target.familyId,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
278
325
|
);
|
|
279
326
|
}
|
|
280
327
|
|
|
281
328
|
if (packRef.targetId && packRef.targetId !== target.targetId) {
|
|
282
|
-
throw
|
|
329
|
+
throw contractError(
|
|
330
|
+
'CONTRACT.PACK_TARGET_MISMATCH',
|
|
283
331
|
`extension pack "${packRef.id}" targets "${packRef.targetId}" but contract target is "${target.targetId}".`,
|
|
332
|
+
{
|
|
333
|
+
meta: {
|
|
334
|
+
packId: packRef.id,
|
|
335
|
+
packTargetId: packRef.targetId,
|
|
336
|
+
contractTargetId: target.targetId,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
284
339
|
);
|
|
285
340
|
}
|
|
286
341
|
}
|
|
@@ -290,7 +345,7 @@ function buildContractFromDsl<Definition extends ContractInput>(
|
|
|
290
345
|
definition: Definition,
|
|
291
346
|
): SqlContractResult<Definition> {
|
|
292
347
|
validateTargetPackRef(definition.family, definition.target);
|
|
293
|
-
validateExtensionPackRefs(definition.target, definition.
|
|
348
|
+
validateExtensionPackRefs(definition.target, definition.extensions);
|
|
294
349
|
validateNamespaceDeclarations(definition.target, definition.namespaces);
|
|
295
350
|
validatePerModelNamespaces(
|
|
296
351
|
definition.target,
|
|
@@ -309,13 +364,13 @@ function buildContractFromDsl<Definition extends ContractInput>(
|
|
|
309
364
|
type BoundDefinitionInput<
|
|
310
365
|
Types extends Record<string, StorageTypeInstance> = Record<never, never>,
|
|
311
366
|
Models extends Record<string, ModelLike> = Record<never, never>,
|
|
312
|
-
|
|
367
|
+
Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined = undefined,
|
|
313
368
|
Naming extends ContractInput['naming'] | undefined = undefined,
|
|
314
369
|
StorageHash extends string | undefined = undefined,
|
|
315
370
|
ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined = undefined,
|
|
316
371
|
Namespaces extends readonly string[] | undefined = undefined,
|
|
317
372
|
> = {
|
|
318
|
-
readonly
|
|
373
|
+
readonly extensions?: Extensions;
|
|
319
374
|
readonly naming?: Naming;
|
|
320
375
|
readonly storageHash?: StorageHash;
|
|
321
376
|
readonly foreignKeyDefaults?: ForeignKeyDefaults;
|
|
@@ -402,7 +457,7 @@ export function buildBoundContract<
|
|
|
402
457
|
target: T,
|
|
403
458
|
definition: Definition,
|
|
404
459
|
factory: (
|
|
405
|
-
helpers: ComposedAuthoringHelpers<F, T, NonNullable<Definition['
|
|
460
|
+
helpers: ComposedAuthoringHelpers<F, T, NonNullable<Definition['extensions']>>,
|
|
406
461
|
) => Built,
|
|
407
462
|
): SqlContractResult<WithFamilyTarget<Definition & Built, F, T>>;
|
|
408
463
|
/** Implementation. */
|
|
@@ -432,7 +487,7 @@ export function buildBoundContract(
|
|
|
432
487
|
createComposedAuthoringHelpers({
|
|
433
488
|
family,
|
|
434
489
|
target,
|
|
435
|
-
|
|
490
|
+
extensions: definition.extensions,
|
|
436
491
|
}),
|
|
437
492
|
);
|
|
438
493
|
const mergedEnums = { ...(definition.enums ?? {}), ...built.enums };
|
|
@@ -454,9 +509,7 @@ export function defineContract<
|
|
|
454
509
|
const Target extends TargetPackRef<'sql', string>,
|
|
455
510
|
const Types extends Record<string, StorageTypeInstance> = Record<never, never>,
|
|
456
511
|
const Models extends Record<string, ModelLike> = Record<never, never>,
|
|
457
|
-
const
|
|
458
|
-
| Record<string, ExtensionPackRef<'sql', string>>
|
|
459
|
-
| undefined = undefined,
|
|
512
|
+
const Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined = undefined,
|
|
460
513
|
const Naming extends ContractInput['naming'] | undefined = undefined,
|
|
461
514
|
const StorageHash extends string | undefined = undefined,
|
|
462
515
|
const ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined = undefined,
|
|
@@ -468,7 +521,7 @@ export function defineContract<
|
|
|
468
521
|
Target,
|
|
469
522
|
Types,
|
|
470
523
|
Models,
|
|
471
|
-
|
|
524
|
+
Extensions,
|
|
472
525
|
Naming,
|
|
473
526
|
StorageHash,
|
|
474
527
|
ForeignKeyDefaults,
|
|
@@ -481,7 +534,7 @@ export function defineContract<
|
|
|
481
534
|
Target,
|
|
482
535
|
Types,
|
|
483
536
|
Models,
|
|
484
|
-
|
|
537
|
+
Extensions,
|
|
485
538
|
Naming,
|
|
486
539
|
StorageHash,
|
|
487
540
|
ForeignKeyDefaults,
|
|
@@ -494,9 +547,7 @@ export function defineContract<
|
|
|
494
547
|
const Target extends TargetPackRef<'sql', string>,
|
|
495
548
|
const Types extends Record<string, StorageTypeInstance> = Record<never, never>,
|
|
496
549
|
const Models extends Record<string, ModelLike> = Record<never, never>,
|
|
497
|
-
const
|
|
498
|
-
| Record<string, ExtensionPackRef<'sql', string>>
|
|
499
|
-
| undefined = undefined,
|
|
550
|
+
const Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined = undefined,
|
|
500
551
|
const Naming extends ContractInput['naming'] | undefined = undefined,
|
|
501
552
|
const StorageHash extends string | undefined = undefined,
|
|
502
553
|
const ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined = undefined,
|
|
@@ -507,21 +558,21 @@ export function defineContract<
|
|
|
507
558
|
definition: ContractScaffold<
|
|
508
559
|
Family,
|
|
509
560
|
Target,
|
|
510
|
-
|
|
561
|
+
Extensions,
|
|
511
562
|
Naming,
|
|
512
563
|
StorageHash,
|
|
513
564
|
ForeignKeyDefaults,
|
|
514
565
|
Namespaces,
|
|
515
566
|
ScaffoldEnums
|
|
516
567
|
>,
|
|
517
|
-
factory: ContractFactory<Family, Target, Types, Models,
|
|
568
|
+
factory: ContractFactory<Family, Target, Types, Models, Extensions, FactoryEnums>,
|
|
518
569
|
): SqlContractResult<
|
|
519
570
|
ContractDefinition<
|
|
520
571
|
Family,
|
|
521
572
|
Target,
|
|
522
573
|
Types,
|
|
523
574
|
Models,
|
|
524
|
-
|
|
575
|
+
Extensions,
|
|
525
576
|
Naming,
|
|
526
577
|
StorageHash,
|
|
527
578
|
ForeignKeyDefaults,
|
|
@@ -540,7 +591,8 @@ export function defineContract(
|
|
|
540
591
|
>,
|
|
541
592
|
): SqlContractResult<ContractInput> {
|
|
542
593
|
if (!isContractInput(definition)) {
|
|
543
|
-
throw
|
|
594
|
+
throw contractError(
|
|
595
|
+
'CONTRACT.ARGUMENT_INVALID',
|
|
544
596
|
'defineContract expects a contract definition object. Define your contract with defineContract({ family, target, models, ... }).',
|
|
545
597
|
);
|
|
546
598
|
}
|
|
@@ -57,6 +57,9 @@ export interface UniqueConstraintNode {
|
|
|
57
57
|
|
|
58
58
|
export interface IndexNode {
|
|
59
59
|
readonly columns: readonly string[];
|
|
60
|
+
/** Exact physical name (PSL `map:`) — adopted verbatim, no wire hash. */
|
|
61
|
+
readonly map?: string;
|
|
62
|
+
/** Managed wire-name prefix (TS `name:`) — lowers to `<name>_<8hex>`. */
|
|
60
63
|
readonly name?: string;
|
|
61
64
|
readonly type?: string;
|
|
62
65
|
readonly options?: Record<string, unknown>;
|
|
@@ -183,7 +186,7 @@ export interface ModelNode {
|
|
|
183
186
|
export interface ContractDefinition {
|
|
184
187
|
readonly target: TargetPackRef<'sql', string>;
|
|
185
188
|
readonly defaultControlPolicy?: ControlPolicy;
|
|
186
|
-
readonly
|
|
189
|
+
readonly extensions?: Record<string, ExtensionPackRef<'sql', string>>;
|
|
187
190
|
readonly storageHash?: string;
|
|
188
191
|
readonly foreignKeyDefaults?: ForeignKeyDefaultsState;
|
|
189
192
|
readonly storageTypes?: Record<string, StorageTypeInstance>;
|
package/src/contract-dsl.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
24
24
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
25
25
|
import type { NamedConstraintSpec } from './authoring-type-utils';
|
|
26
|
+
import { contractError } from './contract-errors';
|
|
26
27
|
import type { EnumTypeHandle } from './enum-type';
|
|
27
28
|
import { isEnumTypeHandle } from './enum-type';
|
|
28
29
|
|
|
@@ -402,10 +403,14 @@ export class ScalarFieldBuilder<State extends AnyScalarFieldState = AnyScalarFie
|
|
|
402
403
|
const uniqueSpec = 'unique' in spec ? spec.unique : undefined;
|
|
403
404
|
|
|
404
405
|
if (idSpec && !this.state.id) {
|
|
405
|
-
throw
|
|
406
|
+
throw contractError(
|
|
407
|
+
'CONTRACT.ARGUMENT_INVALID',
|
|
408
|
+
'field.sql({ id }) requires an existing inline .id(...) declaration.',
|
|
409
|
+
);
|
|
406
410
|
}
|
|
407
411
|
if (uniqueSpec && !this.state.unique) {
|
|
408
|
-
throw
|
|
412
|
+
throw contractError(
|
|
413
|
+
'CONTRACT.ARGUMENT_INVALID',
|
|
409
414
|
'field.sql({ unique }) requires an existing inline .unique(...) declaration.',
|
|
410
415
|
);
|
|
411
416
|
}
|
|
@@ -457,8 +462,10 @@ export class EnumScalarFieldBuilder<
|
|
|
457
462
|
}
|
|
458
463
|
|
|
459
464
|
override defaultSql(_expression: never): never {
|
|
460
|
-
throw
|
|
465
|
+
throw contractError(
|
|
466
|
+
'CONTRACT.DEFAULT_INVALID',
|
|
461
467
|
'defaultSql is not available on an enum field; use .default(members.X) instead',
|
|
468
|
+
{ meta: { reason: 'defaultSql-on-enum-field' } },
|
|
462
469
|
);
|
|
463
470
|
}
|
|
464
471
|
}
|
|
@@ -674,7 +681,11 @@ export class RelationBuilder<State extends RelationState = AnyRelationState> {
|
|
|
674
681
|
spec: SqlSpec,
|
|
675
682
|
): RelationBuilder<ApplyBelongsToRelationSqlSpec<State, SqlSpec>> {
|
|
676
683
|
if (this.state.kind !== 'belongsTo') {
|
|
677
|
-
throw
|
|
684
|
+
throw contractError(
|
|
685
|
+
'CONTRACT.RELATION_INVALID',
|
|
686
|
+
'relation.sql(...) is only supported for belongsTo relations.',
|
|
687
|
+
{ meta: { relationKind: this.state.kind } },
|
|
688
|
+
);
|
|
678
689
|
}
|
|
679
690
|
|
|
680
691
|
return new RelationBuilder({
|
|
@@ -860,27 +871,45 @@ function normalizeTargetFieldRefInput(input: TargetFieldRef | readonly TargetFie
|
|
|
860
871
|
const refs = Array.isArray(input) ? input : [input];
|
|
861
872
|
const [first] = refs;
|
|
862
873
|
if (!first) {
|
|
863
|
-
throw
|
|
874
|
+
throw contractError('CONTRACT.FOREIGN_KEY_INVALID', 'Expected at least one target ref', {
|
|
875
|
+
meta: { reason: 'empty-target-refs' },
|
|
876
|
+
});
|
|
864
877
|
}
|
|
865
878
|
if (refs.some((ref) => ref.modelName !== first.modelName)) {
|
|
866
|
-
throw
|
|
879
|
+
throw contractError(
|
|
880
|
+
'CONTRACT.FOREIGN_KEY_INVALID',
|
|
881
|
+
'All target refs in a foreign key must point to the same model',
|
|
882
|
+
{ meta: { mismatch: 'modelName', models: refs.map((ref) => ref.modelName) } },
|
|
883
|
+
);
|
|
867
884
|
}
|
|
868
885
|
// F-compound: all refs in a compound FK must share the same cross-space coordinate.
|
|
869
886
|
// A mismatch in spaceId, namespaceId, or tableName means the refs come from
|
|
870
887
|
// different spaces despite having the same modelName — an impossible FK.
|
|
871
888
|
if (refs.some((ref) => ref.spaceId !== first.spaceId)) {
|
|
872
|
-
throw
|
|
889
|
+
throw contractError(
|
|
890
|
+
'CONTRACT.FOREIGN_KEY_INVALID',
|
|
873
891
|
`All target refs in a compound foreign key must share the same spaceId (found mismatch: "${first.spaceId ?? '<local>'}" vs "${refs.find((r) => r.spaceId !== first.spaceId)?.spaceId ?? '<local>'}")`,
|
|
892
|
+
{
|
|
893
|
+
meta: {
|
|
894
|
+
mismatch: 'spaceId',
|
|
895
|
+
first: first.spaceId,
|
|
896
|
+
second: refs.find((r) => r.spaceId !== first.spaceId)?.spaceId,
|
|
897
|
+
},
|
|
898
|
+
},
|
|
874
899
|
);
|
|
875
900
|
}
|
|
876
901
|
if (refs.some((ref) => ref.namespaceId !== first.namespaceId)) {
|
|
877
|
-
throw
|
|
902
|
+
throw contractError(
|
|
903
|
+
'CONTRACT.FOREIGN_KEY_INVALID',
|
|
878
904
|
'All target refs in a compound foreign key must share the same namespaceId (found mismatch)',
|
|
905
|
+
{ meta: { mismatch: 'namespaceId' } },
|
|
879
906
|
);
|
|
880
907
|
}
|
|
881
908
|
if (refs.some((ref) => ref.tableName !== first.tableName)) {
|
|
882
|
-
throw
|
|
909
|
+
throw contractError(
|
|
910
|
+
'CONTRACT.FOREIGN_KEY_INVALID',
|
|
883
911
|
'All target refs in a compound foreign key must share the same tableName (found mismatch)',
|
|
912
|
+
{ meta: { mismatch: 'tableName' } },
|
|
884
913
|
);
|
|
885
914
|
}
|
|
886
915
|
return {
|
|
@@ -1322,7 +1351,10 @@ export class ContractModelBuilder<
|
|
|
1322
1351
|
): TargetFieldRef<ModelName & string, FieldName> {
|
|
1323
1352
|
const modelName = this.stageOne.modelName;
|
|
1324
1353
|
if (!modelName) {
|
|
1325
|
-
throw
|
|
1354
|
+
throw contractError(
|
|
1355
|
+
'CONTRACT.MODEL_TOKEN_INVALID',
|
|
1356
|
+
'Model tokens require model("ModelName", ...) before calling .ref(...)',
|
|
1357
|
+
);
|
|
1326
1358
|
}
|
|
1327
1359
|
|
|
1328
1360
|
return {
|
|
@@ -1346,8 +1378,16 @@ export class ContractModelBuilder<
|
|
|
1346
1378
|
> {
|
|
1347
1379
|
const duplicateRelationName = findDuplicateRelationName(this.stageOne.relations, relations);
|
|
1348
1380
|
if (duplicateRelationName) {
|
|
1349
|
-
throw
|
|
1381
|
+
throw contractError(
|
|
1382
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
1350
1383
|
`Model "${this.stageOne.modelName ?? '<anonymous>'}" already defines relation "${duplicateRelationName}".`,
|
|
1384
|
+
{
|
|
1385
|
+
meta: {
|
|
1386
|
+
kind: 'relation',
|
|
1387
|
+
name: duplicateRelationName,
|
|
1388
|
+
modelName: this.stageOne.modelName,
|
|
1389
|
+
},
|
|
1390
|
+
},
|
|
1351
1391
|
);
|
|
1352
1392
|
}
|
|
1353
1393
|
|
|
@@ -1501,7 +1541,8 @@ function resolveNamedModelTokenName(token: {
|
|
|
1501
1541
|
}): string {
|
|
1502
1542
|
const modelName = token.stageOne.modelName;
|
|
1503
1543
|
if (!modelName) {
|
|
1504
|
-
throw
|
|
1544
|
+
throw contractError(
|
|
1545
|
+
'CONTRACT.MODEL_TOKEN_INVALID',
|
|
1505
1546
|
'Relation targets require named model tokens. Use model("ModelName", ...) before passing a token to rel.*(...).',
|
|
1506
1547
|
);
|
|
1507
1548
|
}
|
|
@@ -1557,11 +1598,11 @@ export type ContractInput<
|
|
|
1557
1598
|
SqlStageSpec | undefined
|
|
1558
1599
|
>
|
|
1559
1600
|
> = Record<never, never>,
|
|
1560
|
-
|
|
1601
|
+
Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined = undefined,
|
|
1561
1602
|
> = {
|
|
1562
1603
|
readonly family: Family;
|
|
1563
1604
|
readonly target: Target;
|
|
1564
|
-
readonly
|
|
1605
|
+
readonly extensions?: Extensions;
|
|
1565
1606
|
readonly naming?: NamingConfig;
|
|
1566
1607
|
readonly storageHash?: string;
|
|
1567
1608
|
readonly foreignKeyDefaults?: ForeignKeyDefaultsState;
|
|
@@ -1668,7 +1709,10 @@ export function model<
|
|
|
1668
1709
|
const input = typeof modelNameOrInput === 'string' ? maybeInput : modelNameOrInput;
|
|
1669
1710
|
|
|
1670
1711
|
if (!input) {
|
|
1671
|
-
throw
|
|
1712
|
+
throw contractError(
|
|
1713
|
+
'CONTRACT.ARGUMENT_INVALID',
|
|
1714
|
+
'model("ModelName", ...) requires a model definition.',
|
|
1715
|
+
);
|
|
1672
1716
|
}
|
|
1673
1717
|
|
|
1674
1718
|
return new ContractModelBuilder({
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { StructuredError, StructuredErrorOptions } from '@prisma-next/utils/structured-error';
|
|
2
|
+
import { structuredError } from '@prisma-next/utils/structured-error';
|
|
3
|
+
|
|
4
|
+
export type ContractCode = `CONTRACT.${ContractSubcode}`;
|
|
5
|
+
|
|
6
|
+
type ContractSubcode =
|
|
7
|
+
| 'VALIDATION_FAILED'
|
|
8
|
+
| 'NAME_DUPLICATE'
|
|
9
|
+
| 'MODEL_UNKNOWN'
|
|
10
|
+
| 'PACK_CONTRIBUTION_INVALID'
|
|
11
|
+
| 'PACK_FAMILY_MISMATCH'
|
|
12
|
+
| 'PACK_TARGET_MISMATCH'
|
|
13
|
+
| 'PACK_REF_INVALID'
|
|
14
|
+
| 'PACK_MISSING'
|
|
15
|
+
| 'NAMESPACE_INVALID'
|
|
16
|
+
| 'NAMESPACE_UNSUPPORTED'
|
|
17
|
+
| 'NAMESPACE_UNKNOWN'
|
|
18
|
+
| 'ARGUMENT_INVALID'
|
|
19
|
+
| 'FOREIGN_KEY_INVALID'
|
|
20
|
+
| 'RELATION_INVALID'
|
|
21
|
+
| 'IDENTITY_INVALID'
|
|
22
|
+
| 'CONSTRAINT_INVALID'
|
|
23
|
+
| 'DEFAULT_INVALID'
|
|
24
|
+
| 'ENUM_INVALID'
|
|
25
|
+
| 'TYPE_UNKNOWN'
|
|
26
|
+
| 'FIELD_UNKNOWN'
|
|
27
|
+
| 'MODEL_TOKEN_INVALID'
|
|
28
|
+
| 'MODULE_EXPORT_MISSING'
|
|
29
|
+
| 'ENTITY_KIND_UNKNOWN'
|
|
30
|
+
| 'ENTITY_KIND_INVALID'
|
|
31
|
+
| 'TABLE_MISMATCH';
|
|
32
|
+
|
|
33
|
+
export function contractError(
|
|
34
|
+
code: ContractCode,
|
|
35
|
+
message: string,
|
|
36
|
+
options?: StructuredErrorOptions,
|
|
37
|
+
): StructuredError {
|
|
38
|
+
return structuredError(code, message, options);
|
|
39
|
+
}
|