@prisma-next/sql-contract-psl 0.5.0-dev.8 → 0.5.0-dev.81

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.
@@ -35,11 +35,13 @@ import {
35
35
  type ForeignKeyNode,
36
36
  type IndexNode,
37
37
  type ModelNode,
38
+ type PrimaryKeyNode,
38
39
  type UniqueConstraintNode,
39
40
  } from '@prisma-next/sql-contract-ts/contract-builder';
40
41
  import { ifDefined } from '@prisma-next/utils/defined';
41
42
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
42
43
  import {
44
+ findDuplicateFieldName,
43
45
  getAttribute,
44
46
  getPositionalArgument,
45
47
  mapFieldNamesToColumns,
@@ -77,19 +79,19 @@ import {
77
79
 
78
80
  export interface InterpretPslDocumentToSqlContractInput {
79
81
  readonly document: ParsePslDocumentResult;
80
- readonly target: TargetPackRef<'sql', 'postgres'>;
82
+ readonly target: TargetPackRef<'sql', string>;
81
83
  readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
82
84
  readonly composedExtensionPacks?: readonly string[];
83
- readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', 'postgres'>[];
85
+ readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', string>[];
84
86
  readonly controlMutationDefaults?: ControlMutationDefaults;
85
87
  readonly authoringContributions?: AuthoringContributions;
86
88
  }
87
89
 
88
90
  function buildComposedExtensionPackRefs(
89
- target: TargetPackRef<'sql', 'postgres'>,
91
+ target: TargetPackRef<'sql', string>,
90
92
  extensionIds: readonly string[],
91
- extensionPackRefs: readonly ExtensionPackRef<'sql', 'postgres'>[] = [],
92
- ): Record<string, ExtensionPackRef<'sql', 'postgres'>> | undefined {
93
+ extensionPackRefs: readonly ExtensionPackRef<'sql', string>[] = [],
94
+ ): Record<string, ExtensionPackRef<'sql', string>> | undefined {
93
95
  if (extensionIds.length === 0) {
94
96
  return undefined;
95
97
  }
@@ -106,7 +108,7 @@ function buildComposedExtensionPackRefs(
106
108
  familyId: target.familyId,
107
109
  targetId: target.targetId,
108
110
  version: '0.0.1',
109
- } satisfies ExtensionPackRef<'sql', 'postgres'>),
111
+ } satisfies ExtensionPackRef<'sql', string>),
110
112
  ]),
111
113
  );
112
114
  }
@@ -219,6 +221,7 @@ function validateNamedTypeAttributes(input: {
219
221
  readonly sourceId: string;
220
222
  readonly diagnostics: ContractSourceDiagnostic[];
221
223
  readonly composedExtensions: ReadonlySet<string>;
224
+ readonly authoringContributions: AuthoringContributions | undefined;
222
225
  readonly allowDbNativeType: boolean;
223
226
  readonly familyId: string;
224
227
  readonly targetId: string;
@@ -250,6 +253,7 @@ function validateNamedTypeAttributes(input: {
250
253
  const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
251
254
  familyId: input.familyId,
252
255
  targetId: input.targetId,
256
+ authoringContributions: input.authoringContributions,
253
257
  });
254
258
  if (uncomposedNamespace) {
255
259
  reportUncomposedNamespace({
@@ -289,6 +293,7 @@ function resolveNamedTypeDeclarations(input: ResolveNamedTypeDeclarationsInput):
289
293
  sourceId: input.sourceId,
290
294
  diagnostics: input.diagnostics,
291
295
  composedExtensions: input.composedExtensions,
296
+ authoringContributions: input.authoringContributions,
292
297
  allowDbNativeType: false,
293
298
  familyId: input.familyId,
294
299
  targetId: input.targetId,
@@ -367,6 +372,7 @@ function resolveNamedTypeDeclarations(input: ResolveNamedTypeDeclarationsInput):
367
372
  sourceId: input.sourceId,
368
373
  diagnostics: input.diagnostics,
369
374
  composedExtensions: input.composedExtensions,
375
+ authoringContributions: input.authoringContributions,
370
376
  allowDbNativeType: true,
371
377
  familyId: input.familyId,
372
378
  targetId: input.targetId,
@@ -460,18 +466,24 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
460
466
  scalarTypeDescriptors: input.scalarTypeDescriptors,
461
467
  });
462
468
 
463
- const primaryKeyFields = resolvedFields.filter((field) => field.isId);
464
- const primaryKeyColumns = primaryKeyFields.map((field) => field.columnName);
465
- const primaryKeyName = primaryKeyFields.length === 1 ? primaryKeyFields[0]?.idName : undefined;
466
- const isVariantModel = model.attributes.some((attr) => attr.name === 'base');
467
- if (primaryKeyColumns.length === 0 && !isVariantModel) {
469
+ const inlineIdFields = resolvedFields.filter((field) => field.isId);
470
+ if (inlineIdFields.length > 1) {
468
471
  diagnostics.push({
469
- code: 'PSL_MISSING_PRIMARY_KEY',
470
- message: `Model "${model.name}" must declare at least one @id field for SQL provider`,
472
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
473
+ message: `Model "${model.name}" cannot declare inline @id on multiple fields; use model-level @@id([...]) for composite identity`,
471
474
  sourceId,
472
475
  span: model.span,
473
476
  });
474
477
  }
478
+ const singleInlineIdField = inlineIdFields.length === 1 ? inlineIdFields[0] : undefined;
479
+ let primaryKey: PrimaryKeyNode | undefined = singleInlineIdField
480
+ ? {
481
+ columns: [singleInlineIdField.columnName],
482
+ ...ifDefined('name', singleInlineIdField.idName),
483
+ }
484
+ : undefined;
485
+ const hasInlinePrimaryKey = primaryKey !== undefined;
486
+ let blockPrimaryKeyDeclared = false;
475
487
 
476
488
  const resultBackrelationCandidates: ModelBackrelationCandidate[] = [];
477
489
  for (const field of model.fields) {
@@ -483,6 +495,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
483
495
  field,
484
496
  sourceId,
485
497
  composedExtensions: input.composedExtensions,
498
+ authoringContributions: input.authoringContributions,
486
499
  diagnostics,
487
500
  familyId: input.familyId,
488
501
  targetId: input.targetId,
@@ -557,17 +570,107 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
557
570
  if (modelAttribute.name === 'discriminator' || modelAttribute.name === 'base') {
558
571
  continue;
559
572
  }
573
+ const attributeLabel = `Model "${model.name}" @@${modelAttribute.name}`;
574
+ if (modelAttribute.name === 'id') {
575
+ if (blockPrimaryKeyDeclared) {
576
+ diagnostics.push({
577
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
578
+ message: `Model "${model.name}" declares @@id more than once`,
579
+ sourceId,
580
+ span: modelAttribute.span,
581
+ });
582
+ continue;
583
+ }
584
+ if (hasInlinePrimaryKey) {
585
+ diagnostics.push({
586
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
587
+ message: `Model "${model.name}" cannot declare both field-level @id and model-level @@id`,
588
+ sourceId,
589
+ span: modelAttribute.span,
590
+ });
591
+ blockPrimaryKeyDeclared = true;
592
+ continue;
593
+ }
594
+ const fieldNames = parseAttributeFieldList({
595
+ attribute: modelAttribute,
596
+ sourceId,
597
+ diagnostics,
598
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
599
+ entityLabel: attributeLabel,
600
+ });
601
+ if (!fieldNames) {
602
+ continue;
603
+ }
604
+ const duplicateFieldName = findDuplicateFieldName(fieldNames);
605
+ if (duplicateFieldName !== undefined) {
606
+ diagnostics.push({
607
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
608
+ message: `${attributeLabel} list contains duplicate field "${duplicateFieldName}"`,
609
+ sourceId,
610
+ span: modelAttribute.span,
611
+ });
612
+ continue;
613
+ }
614
+ const nullableFieldName = fieldNames.find(
615
+ (name) => model.fields.find((f) => f.name === name)?.optional === true,
616
+ );
617
+ if (nullableFieldName !== undefined) {
618
+ diagnostics.push({
619
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
620
+ message: `${attributeLabel} cannot include optional field "${nullableFieldName}"; primary key columns must be NOT NULL`,
621
+ sourceId,
622
+ span: modelAttribute.span,
623
+ });
624
+ continue;
625
+ }
626
+ const columnNames = mapFieldNamesToColumns({
627
+ modelName: model.name,
628
+ fieldNames,
629
+ mapping,
630
+ sourceId,
631
+ diagnostics,
632
+ span: modelAttribute.span,
633
+ entityLabel: attributeLabel,
634
+ });
635
+ if (!columnNames) {
636
+ continue;
637
+ }
638
+ const constraintName = parseConstraintMapArgument({
639
+ attribute: modelAttribute,
640
+ sourceId,
641
+ diagnostics,
642
+ entityLabel: attributeLabel,
643
+ span: modelAttribute.span,
644
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
645
+ });
646
+ primaryKey = {
647
+ columns: columnNames,
648
+ ...ifDefined('name', constraintName),
649
+ };
650
+ blockPrimaryKeyDeclared = true;
651
+ continue;
652
+ }
560
653
  if (modelAttribute.name === 'unique' || modelAttribute.name === 'index') {
561
654
  const fieldNames = parseAttributeFieldList({
562
655
  attribute: modelAttribute,
563
656
  sourceId,
564
657
  diagnostics,
565
658
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
566
- messagePrefix: `Model "${model.name}" @@${modelAttribute.name}`,
659
+ entityLabel: attributeLabel,
567
660
  });
568
661
  if (!fieldNames) {
569
662
  continue;
570
663
  }
664
+ const duplicateFieldName = findDuplicateFieldName(fieldNames);
665
+ if (duplicateFieldName !== undefined) {
666
+ diagnostics.push({
667
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
668
+ message: `${attributeLabel} list contains duplicate field "${duplicateFieldName}"`,
669
+ sourceId,
670
+ span: modelAttribute.span,
671
+ });
672
+ continue;
673
+ }
571
674
  const columnNames = mapFieldNamesToColumns({
572
675
  modelName: model.name,
573
676
  fieldNames,
@@ -575,7 +678,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
575
678
  sourceId,
576
679
  diagnostics,
577
680
  span: modelAttribute.span,
578
- contextLabel: `Model "${model.name}" @@${modelAttribute.name}`,
681
+ entityLabel: attributeLabel,
579
682
  });
580
683
  if (!columnNames) {
581
684
  continue;
@@ -584,7 +687,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
584
687
  attribute: modelAttribute,
585
688
  sourceId,
586
689
  diagnostics,
587
- entityLabel: `Model "${model.name}" @@${modelAttribute.name}`,
690
+ entityLabel: attributeLabel,
588
691
  span: modelAttribute.span,
589
692
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
590
693
  });
@@ -604,7 +707,11 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
604
707
  const uncomposedNamespace = checkUncomposedNamespace(
605
708
  modelAttribute.name,
606
709
  input.composedExtensions,
607
- { familyId: input.familyId, targetId: input.targetId },
710
+ {
711
+ familyId: input.familyId,
712
+ targetId: input.targetId,
713
+ authoringContributions: input.authoringContributions,
714
+ },
608
715
  );
609
716
  if (uncomposedNamespace) {
610
717
  reportUncomposedNamespace({
@@ -678,7 +785,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
678
785
  sourceId,
679
786
  diagnostics,
680
787
  span: relationAttribute.relation.span,
681
- contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
788
+ entityLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
682
789
  });
683
790
  if (!localColumns) {
684
791
  continue;
@@ -690,7 +797,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
690
797
  sourceId,
691
798
  diagnostics,
692
799
  span: relationAttribute.relation.span,
693
- contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
800
+ entityLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
694
801
  });
695
802
  if (!referencedColumns) {
696
803
  continue;
@@ -762,16 +869,9 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
762
869
  descriptor: resolvedField.descriptor,
763
870
  nullable: resolvedField.field.optional,
764
871
  ...ifDefined('default', resolvedField.defaultValue),
765
- ...ifDefined('executionDefault', resolvedField.executionDefault),
872
+ ...ifDefined('executionDefaults', resolvedField.executionDefaults),
766
873
  })),
767
- ...(primaryKeyColumns.length > 0
768
- ? {
769
- id: {
770
- columns: primaryKeyColumns,
771
- ...ifDefined('name', primaryKeyName),
772
- },
773
- }
774
- : {}),
874
+ ...ifDefined('id', primaryKey),
775
875
  ...(uniqueConstraints.length > 0 ? { uniques: uniqueConstraints } : {}),
776
876
  ...(indexNodes.length > 0 ? { indexes: indexNodes } : {}),
777
877
  ...(foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {}),
package/src/provider.ts CHANGED
@@ -10,8 +10,8 @@ import type { ColumnDescriptor } from './psl-column-resolution';
10
10
 
11
11
  export interface PrismaContractOptions {
12
12
  readonly output?: string;
13
- readonly target: TargetPackRef<'sql', 'postgres'>;
14
- readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', 'postgres'>[];
13
+ readonly target: TargetPackRef<'sql', string>;
14
+ readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', string>[];
15
15
  }
16
16
 
17
17
  function buildColumnDescriptorMap(
@@ -20,9 +20,7 @@ function buildColumnDescriptorMap(
20
20
  ): ReadonlyMap<string, ColumnDescriptor> {
21
21
  const result = new Map<string, ColumnDescriptor>();
22
22
  for (const [typeName, codecId] of scalarTypeDescriptors) {
23
- const codec = codecLookup.get(codecId);
24
- if (!codec) continue;
25
- const nativeType = codec.targetTypes[0];
23
+ const nativeType = codecLookup.targetTypesFor(codecId)?.[0];
26
24
  if (nativeType === undefined) continue;
27
25
  result.set(typeName, { codecId, nativeType });
28
26
  }
@@ -251,13 +251,13 @@ export function parseAttributeFieldList(input: {
251
251
  readonly sourceId: string;
252
252
  readonly diagnostics: ContractSourceDiagnostic[];
253
253
  readonly code: string;
254
- readonly messagePrefix: string;
254
+ readonly entityLabel: string;
255
255
  }): readonly string[] | undefined {
256
256
  const raw = getNamedArgument(input.attribute, 'fields') ?? getPositionalArgument(input.attribute);
257
257
  if (!raw) {
258
258
  input.diagnostics.push({
259
259
  code: input.code,
260
- message: `${input.messagePrefix} requires fields list argument`,
260
+ message: `${input.entityLabel} requires fields list argument`,
261
261
  sourceId: input.sourceId,
262
262
  span: input.attribute.span,
263
263
  });
@@ -267,7 +267,7 @@ export function parseAttributeFieldList(input: {
267
267
  if (!fields || fields.length === 0) {
268
268
  input.diagnostics.push({
269
269
  code: input.code,
270
- message: `${input.messagePrefix} requires bracketed field list argument`,
270
+ message: `${input.entityLabel} requires bracketed field list argument`,
271
271
  sourceId: input.sourceId,
272
272
  span: input.attribute.span,
273
273
  });
@@ -276,6 +276,15 @@ export function parseAttributeFieldList(input: {
276
276
  return fields;
277
277
  }
278
278
 
279
+ export function findDuplicateFieldName(fieldNames: readonly string[]): string | undefined {
280
+ const seen = new Set<string>();
281
+ for (const name of fieldNames) {
282
+ if (seen.has(name)) return name;
283
+ seen.add(name);
284
+ }
285
+ return undefined;
286
+ }
287
+
279
288
  export function mapFieldNamesToColumns(input: {
280
289
  readonly modelName: string;
281
290
  readonly fieldNames: readonly string[];
@@ -283,7 +292,7 @@ export function mapFieldNamesToColumns(input: {
283
292
  readonly sourceId: string;
284
293
  readonly diagnostics: ContractSourceDiagnostic[];
285
294
  readonly span: PslSpan;
286
- readonly contextLabel: string;
295
+ readonly entityLabel: string;
287
296
  }): readonly string[] | undefined {
288
297
  const columns: string[] = [];
289
298
  for (const fieldName of input.fieldNames) {
@@ -291,7 +300,7 @@ export function mapFieldNamesToColumns(input: {
291
300
  if (!columnName) {
292
301
  input.diagnostics.push({
293
302
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
294
- message: `${input.contextLabel} references unknown field "${input.modelName}.${fieldName}"`,
303
+ message: `${input.entityLabel} references unknown field "${input.modelName}.${fieldName}"`,
295
304
  sourceId: input.sourceId,
296
305
  span: input.span,
297
306
  });
@@ -311,6 +311,12 @@ function parsePslAuthoringArgumentValue(
311
311
  switch (descriptor.kind) {
312
312
  case 'string':
313
313
  return unquoteStringLiteral(rawValue);
314
+ case 'boolean': {
315
+ const trimmed = rawValue.trim();
316
+ if (trimmed === 'true') return true;
317
+ if (trimmed === 'false') return false;
318
+ return INVALID_AUTHORING_ARGUMENT;
319
+ }
314
320
  case 'number': {
315
321
  const parsed = Number(unquoteStringLiteral(rawValue));
316
322
  return Number.isNaN(parsed) ? INVALID_AUTHORING_ARGUMENT : parsed;