@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.
@@ -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
- ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
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 extensionPacks?: ExtensionPacks;
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
- ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
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 extensionPacks?: ExtensionPacks;
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
- ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
115
+ Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
115
116
  Enums extends Record<string, EnumTypeHandle> = Record<string, EnumTypeHandle>,
116
- > = (helpers: ComposedAuthoringHelpers<Family, Target, ExtensionPacks>) => {
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 new Error(
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 new Error(
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 new Error(
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 new Error('defineContract: namespace names cannot be empty.');
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 new Error(`defineContract: namespace name "${namespace}" cannot be whitespace-only.`);
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 new Error(
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 new Error(
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 new Error(`defineContract: namespaces list contains duplicate entry "${namespace}".`);
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 new Error(
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 new Error(
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 new Error(
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 new Error(
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
- extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
299
+ extensions?: Record<string, ExtensionPackRef<'sql', string>>,
263
300
  ): void {
264
- if (!extensionPacks) {
301
+ if (!extensions) {
265
302
  return;
266
303
  }
267
304
 
268
- for (const packRef of Object.values(extensionPacks)) {
305
+ for (const packRef of Object.values(extensions)) {
269
306
  if (packRef.kind !== 'extension') {
270
- throw new Error(
271
- `defineContract only accepts extension pack refs in extensionPacks. Received kind "${packRef.kind}".`,
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 new Error(
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 new Error(
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.extensionPacks);
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
- ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined = undefined,
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 extensionPacks?: ExtensionPacks;
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['extensionPacks']>>,
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
- extensionPacks: definition.extensionPacks,
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 ExtensionPacks extends
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
- ExtensionPacks,
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
- ExtensionPacks,
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 ExtensionPacks extends
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
- ExtensionPacks,
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, ExtensionPacks, FactoryEnums>,
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
- ExtensionPacks,
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 new TypeError(
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 extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>;
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>;
@@ -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 new Error('field.sql({ id }) requires an existing inline .id(...) declaration.');
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 new Error(
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 new Error(
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 new Error('relation.sql(...) is only supported for belongsTo relations.');
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 new Error('Expected at least one target ref');
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 new Error('All target refs in a foreign key must point to the same model');
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 new Error(
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 new Error(
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 new Error(
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 new Error('Model tokens require model("ModelName", ...) before calling .ref(...)');
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 new Error(
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 new Error(
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
- ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined = undefined,
1601
+ Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined = undefined,
1561
1602
  > = {
1562
1603
  readonly family: Family;
1563
1604
  readonly target: Target;
1564
- readonly extensionPacks?: ExtensionPacks;
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 new Error('model("ModelName", ...) requires a model definition.');
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
+ }