@prisma-next/sql-contract-psl 0.5.0-dev.52 → 0.5.0-dev.54
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/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{interpreter-DJrrH8Ee.mjs → interpreter-BChe-9vN.mjs} +122 -21
- package/dist/interpreter-BChe-9vN.mjs.map +1 -0
- package/dist/provider.mjs +1 -1
- package/package.json +11 -11
- package/src/interpreter.ts +111 -20
- package/src/psl-attribute-parsing.ts +14 -5
- package/src/psl-field-resolution.ts +11 -1
- package/dist/interpreter-DJrrH8Ee.mjs.map +0 -1
package/src/interpreter.ts
CHANGED
|
@@ -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,
|
|
@@ -464,18 +466,24 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
|
|
|
464
466
|
scalarTypeDescriptors: input.scalarTypeDescriptors,
|
|
465
467
|
});
|
|
466
468
|
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
const primaryKeyName = primaryKeyFields.length === 1 ? primaryKeyFields[0]?.idName : undefined;
|
|
470
|
-
const isVariantModel = model.attributes.some((attr) => attr.name === 'base');
|
|
471
|
-
if (primaryKeyColumns.length === 0 && !isVariantModel) {
|
|
469
|
+
const inlineIdFields = resolvedFields.filter((field) => field.isId);
|
|
470
|
+
if (inlineIdFields.length > 1) {
|
|
472
471
|
diagnostics.push({
|
|
473
|
-
code: '
|
|
474
|
-
message: `Model "${model.name}"
|
|
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`,
|
|
475
474
|
sourceId,
|
|
476
475
|
span: model.span,
|
|
477
476
|
});
|
|
478
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;
|
|
479
487
|
|
|
480
488
|
const resultBackrelationCandidates: ModelBackrelationCandidate[] = [];
|
|
481
489
|
for (const field of model.fields) {
|
|
@@ -562,17 +570,107 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
|
|
|
562
570
|
if (modelAttribute.name === 'discriminator' || modelAttribute.name === 'base') {
|
|
563
571
|
continue;
|
|
564
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
|
+
}
|
|
565
653
|
if (modelAttribute.name === 'unique' || modelAttribute.name === 'index') {
|
|
566
654
|
const fieldNames = parseAttributeFieldList({
|
|
567
655
|
attribute: modelAttribute,
|
|
568
656
|
sourceId,
|
|
569
657
|
diagnostics,
|
|
570
658
|
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
571
|
-
|
|
659
|
+
entityLabel: attributeLabel,
|
|
572
660
|
});
|
|
573
661
|
if (!fieldNames) {
|
|
574
662
|
continue;
|
|
575
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
|
+
}
|
|
576
674
|
const columnNames = mapFieldNamesToColumns({
|
|
577
675
|
modelName: model.name,
|
|
578
676
|
fieldNames,
|
|
@@ -580,7 +678,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
|
|
|
580
678
|
sourceId,
|
|
581
679
|
diagnostics,
|
|
582
680
|
span: modelAttribute.span,
|
|
583
|
-
|
|
681
|
+
entityLabel: attributeLabel,
|
|
584
682
|
});
|
|
585
683
|
if (!columnNames) {
|
|
586
684
|
continue;
|
|
@@ -589,7 +687,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
|
|
|
589
687
|
attribute: modelAttribute,
|
|
590
688
|
sourceId,
|
|
591
689
|
diagnostics,
|
|
592
|
-
entityLabel:
|
|
690
|
+
entityLabel: attributeLabel,
|
|
593
691
|
span: modelAttribute.span,
|
|
594
692
|
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
595
693
|
});
|
|
@@ -687,7 +785,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
|
|
|
687
785
|
sourceId,
|
|
688
786
|
diagnostics,
|
|
689
787
|
span: relationAttribute.relation.span,
|
|
690
|
-
|
|
788
|
+
entityLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
|
|
691
789
|
});
|
|
692
790
|
if (!localColumns) {
|
|
693
791
|
continue;
|
|
@@ -699,7 +797,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
|
|
|
699
797
|
sourceId,
|
|
700
798
|
diagnostics,
|
|
701
799
|
span: relationAttribute.relation.span,
|
|
702
|
-
|
|
800
|
+
entityLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
|
|
703
801
|
});
|
|
704
802
|
if (!referencedColumns) {
|
|
705
803
|
continue;
|
|
@@ -773,14 +871,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
|
|
|
773
871
|
...ifDefined('default', resolvedField.defaultValue),
|
|
774
872
|
...ifDefined('executionDefaults', resolvedField.executionDefaults),
|
|
775
873
|
})),
|
|
776
|
-
...(
|
|
777
|
-
? {
|
|
778
|
-
id: {
|
|
779
|
-
columns: primaryKeyColumns,
|
|
780
|
-
...ifDefined('name', primaryKeyName),
|
|
781
|
-
},
|
|
782
|
-
}
|
|
783
|
-
: {}),
|
|
874
|
+
...ifDefined('id', primaryKey),
|
|
784
875
|
...(uniqueConstraints.length > 0 ? { uniques: uniqueConstraints } : {}),
|
|
785
876
|
...(indexNodes.length > 0 ? { indexes: indexNodes } : {}),
|
|
786
877
|
...(foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {}),
|
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
303
|
+
message: `${input.entityLabel} references unknown field "${input.modelName}.${fieldName}"`,
|
|
295
304
|
sourceId: input.sourceId,
|
|
296
305
|
span: input.span,
|
|
297
306
|
});
|
|
@@ -366,6 +366,16 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
366
366
|
sourceId,
|
|
367
367
|
diagnostics,
|
|
368
368
|
});
|
|
369
|
+
let isIdField = Boolean(idAttribute);
|
|
370
|
+
if (idAttribute && field.optional) {
|
|
371
|
+
diagnostics.push({
|
|
372
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
373
|
+
message: `Field "${model.name}.${field.name}" @id cannot be optional; primary key columns must be NOT NULL`,
|
|
374
|
+
sourceId,
|
|
375
|
+
span: idAttribute.span,
|
|
376
|
+
});
|
|
377
|
+
isIdField = false;
|
|
378
|
+
}
|
|
369
379
|
|
|
370
380
|
// Field presets contribute their own default / executionDefaults / id /
|
|
371
381
|
// unique. They take precedence over attribute-derived contributions for
|
|
@@ -394,7 +404,7 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
394
404
|
descriptor,
|
|
395
405
|
...ifDefined('defaultValue', fieldDefaultValue),
|
|
396
406
|
...ifDefined('executionDefaults', fieldExecutionDefaults),
|
|
397
|
-
isId:
|
|
407
|
+
isId: isIdField || Boolean(presetContributions?.id),
|
|
398
408
|
isUnique: Boolean(uniqueAttribute) || Boolean(presetContributions?.unique),
|
|
399
409
|
...ifDefined('idName', idName),
|
|
400
410
|
...ifDefined('uniqueName', uniqueName),
|