@prisma-next/sql-contract-psl 0.5.0-dev.9 → 0.5.1
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 +9 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -3
- package/dist/{interpreter-iFCRN9nb.mjs → interpreter-ijCjxhaU.mjs} +507 -80
- package/dist/interpreter-ijCjxhaU.mjs.map +1 -0
- package/dist/provider.d.mts +2 -2
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +19 -7
- package/dist/provider.mjs.map +1 -1
- package/package.json +15 -13
- package/src/interpreter.ts +172 -28
- package/src/provider.ts +26 -6
- package/src/psl-attribute-parsing.ts +140 -5
- package/src/psl-authoring-arguments.ts +6 -0
- package/src/psl-column-resolution.ts +228 -37
- package/src/psl-field-resolution.ts +138 -17
- package/src/psl-relation-resolution.ts +3 -0
- package/dist/interpreter-iFCRN9nb.mjs.map +0 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
|
|
2
|
-
import type { ColumnDefault,
|
|
2
|
+
import type { ColumnDefault, ExecutionMutationDefaultPhases } from '@prisma-next/contract/types';
|
|
3
3
|
import type {
|
|
4
4
|
AuthoringContributions,
|
|
5
|
+
AuthoringFieldPresetDescriptor,
|
|
5
6
|
AuthoringTypeConstructorDescriptor,
|
|
6
7
|
} from '@prisma-next/framework-components/authoring';
|
|
7
8
|
import {
|
|
9
|
+
hasRegisteredFieldNamespace,
|
|
10
|
+
instantiateAuthoringFieldPreset,
|
|
8
11
|
instantiateAuthoringTypeConstructor,
|
|
12
|
+
isAuthoringFieldPresetDescriptor,
|
|
9
13
|
isAuthoringTypeConstructorDescriptor,
|
|
10
14
|
validateAuthoringHelperArguments,
|
|
11
15
|
} from '@prisma-next/framework-components/authoring';
|
|
@@ -37,7 +41,7 @@ export type ColumnDescriptor = {
|
|
|
37
41
|
readonly codecId: string;
|
|
38
42
|
readonly nativeType: string;
|
|
39
43
|
readonly typeRef?: string;
|
|
40
|
-
readonly typeParams?: Record<string, unknown
|
|
44
|
+
readonly typeParams?: Record<string, unknown> | undefined;
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
export function toNamedTypeFieldDescriptor(
|
|
@@ -68,23 +72,45 @@ export function getAuthoringTypeConstructor(
|
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
75
|
+
* Walks `authoringContributions.field` segment-by-segment and returns the field-preset descriptor at the resolved path, or `undefined` if no descriptor is registered.
|
|
76
|
+
*
|
|
77
|
+
* Symmetric with `getAuthoringTypeConstructor`. Field presets are strictly richer than type constructors — they can contribute `default` / `executionDefaults` / `id` / `unique` / `nullable` in addition to the `codecId` / `nativeType` / `typeParams` triple. PSL resolution tries field presets first, then falls back to type constructors on miss (see `resolveFieldTypeDescriptor`).
|
|
78
|
+
*/
|
|
79
|
+
export function getAuthoringFieldPreset(
|
|
80
|
+
contributions: AuthoringContributions | undefined,
|
|
81
|
+
path: readonly string[],
|
|
82
|
+
): AuthoringFieldPresetDescriptor | undefined {
|
|
83
|
+
let current: unknown = contributions?.field;
|
|
84
|
+
|
|
85
|
+
for (const segment of path) {
|
|
86
|
+
if (typeof current !== 'object' || current === null || Array.isArray(current)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
current = (current as Record<string, unknown>)[segment];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return isAuthoringFieldPresetDescriptor(current) ? current : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns the namespace prefix of `attributeName` if it references an unrecognized extension namespace, otherwise `undefined`. A namespace is considered recognized when it is:
|
|
74
97
|
*
|
|
75
98
|
* - `db` (native-type spec, always allowed),
|
|
76
99
|
* - the active family id (e.g. `sql`),
|
|
77
100
|
* - the active target id (e.g. `postgres`),
|
|
101
|
+
* - a registered field-preset namespace (e.g. `temporal`),
|
|
78
102
|
* - present in `composedExtensions`.
|
|
79
103
|
*
|
|
80
|
-
* Family/target namespaces are exempted so that e.g. `@sql.foo` surfaces as
|
|
81
|
-
* PSL_UNSUPPORTED_*_ATTRIBUTE (the attribute isn't defined) rather than
|
|
82
|
-
* PSL_EXTENSION_NAMESPACE_NOT_COMPOSED (the namespace is already composed).
|
|
104
|
+
* Family/target/field-preset namespaces are exempted so that e.g. `@sql.foo` surfaces as PSL_UNSUPPORTED_*_ATTRIBUTE (the attribute isn't defined) rather than PSL_EXTENSION_NAMESPACE_NOT_COMPOSED (the namespace is already composed).
|
|
83
105
|
*/
|
|
84
106
|
export function checkUncomposedNamespace(
|
|
85
107
|
attributeName: string,
|
|
86
108
|
composedExtensions: ReadonlySet<string>,
|
|
87
|
-
context?: {
|
|
109
|
+
context?: {
|
|
110
|
+
readonly familyId?: string;
|
|
111
|
+
readonly targetId?: string;
|
|
112
|
+
readonly authoringContributions?: AuthoringContributions | undefined;
|
|
113
|
+
},
|
|
88
114
|
): string | undefined {
|
|
89
115
|
const dotIndex = attributeName.indexOf('.');
|
|
90
116
|
if (dotIndex <= 0 || dotIndex === attributeName.length - 1) {
|
|
@@ -95,6 +121,7 @@ export function checkUncomposedNamespace(
|
|
|
95
121
|
namespace === 'db' ||
|
|
96
122
|
namespace === context?.familyId ||
|
|
97
123
|
namespace === context?.targetId ||
|
|
124
|
+
hasRegisteredFieldNamespace(context?.authoringContributions, namespace) ||
|
|
98
125
|
composedExtensions.has(namespace)
|
|
99
126
|
) {
|
|
100
127
|
return undefined;
|
|
@@ -103,12 +130,9 @@ export function checkUncomposedNamespace(
|
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
/**
|
|
106
|
-
* Pushes the canonical `PSL_EXTENSION_NAMESPACE_NOT_COMPOSED` diagnostic for a
|
|
107
|
-
* subject (attribute, model attribute, or type constructor) that references an
|
|
108
|
-
* extension namespace which is not composed in the current contract.
|
|
133
|
+
* Pushes the canonical `PSL_EXTENSION_NAMESPACE_NOT_COMPOSED` diagnostic for a subject (attribute, model attribute, or type constructor) that references an extension namespace which is not composed in the current contract.
|
|
109
134
|
*
|
|
110
|
-
* The `data` payload carries the missing namespace so machine consumers
|
|
111
|
-
* (agents, IDE extensions, CLI auto-fix) don't have to parse the prose.
|
|
135
|
+
* The `data` payload carries the missing namespace so machine consumers (agents, IDE extensions, CLI auto-fix) don't have to parse the prose.
|
|
112
136
|
*/
|
|
113
137
|
export function reportUncomposedNamespace(input: {
|
|
114
138
|
readonly subjectLabel: string;
|
|
@@ -126,6 +150,26 @@ export function reportUncomposedNamespace(input: {
|
|
|
126
150
|
});
|
|
127
151
|
}
|
|
128
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Pushes the canonical `PSL_UNKNOWN_FIELD_PRESET` diagnostic when a typoed preset name is referenced inside a registered field-preset namespace. The `data` payload exposes the namespace and full helper path so machine consumers (agents, IDE extensions) don't have to parse the prose.
|
|
155
|
+
*/
|
|
156
|
+
export function reportUnknownFieldPreset(input: {
|
|
157
|
+
readonly entityLabel: string;
|
|
158
|
+
readonly namespace: string;
|
|
159
|
+
readonly helperPath: string;
|
|
160
|
+
readonly sourceId: string;
|
|
161
|
+
readonly span: PslSpan;
|
|
162
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
163
|
+
}): void {
|
|
164
|
+
input.diagnostics.push({
|
|
165
|
+
code: 'PSL_UNKNOWN_FIELD_PRESET',
|
|
166
|
+
message: `${input.entityLabel} references unknown field preset "${input.helperPath}". Check the spelling against the available presets in the "${input.namespace}" namespace.`,
|
|
167
|
+
sourceId: input.sourceId,
|
|
168
|
+
span: input.span,
|
|
169
|
+
data: { namespace: input.namespace, helperPath: input.helperPath },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
129
173
|
export function instantiatePslTypeConstructor(input: {
|
|
130
174
|
readonly call: PslTypeConstructorCall;
|
|
131
175
|
readonly descriptor: AuthoringTypeConstructorDescriptor;
|
|
@@ -200,17 +244,19 @@ export function resolvePslTypeConstructorDescriptor(input: {
|
|
|
200
244
|
return descriptor;
|
|
201
245
|
}
|
|
202
246
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
247
|
+
const uncomposedNamespace = checkUncomposedNamespace(
|
|
248
|
+
input.call.path.join('.'),
|
|
249
|
+
input.composedExtensions,
|
|
250
|
+
{
|
|
251
|
+
familyId: input.familyId,
|
|
252
|
+
targetId: input.targetId,
|
|
253
|
+
authoringContributions: input.authoringContributions,
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
if (uncomposedNamespace) {
|
|
211
257
|
reportUncomposedNamespace({
|
|
212
258
|
subjectLabel: `Type constructor "${input.call.path.join('.')}"`,
|
|
213
|
-
namespace,
|
|
259
|
+
namespace: uncomposedNamespace,
|
|
214
260
|
sourceId: input.sourceId,
|
|
215
261
|
span: input.call.span,
|
|
216
262
|
diagnostics: input.diagnostics,
|
|
@@ -227,8 +273,89 @@ export function resolvePslTypeConstructorDescriptor(input: {
|
|
|
227
273
|
});
|
|
228
274
|
}
|
|
229
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Instantiates a field-preset call against its descriptor, coercing PSL AST arguments into the descriptor's typed argument shape and returning the preset's full set of contract contributions.
|
|
278
|
+
*
|
|
279
|
+
* Symmetric with `instantiatePslTypeConstructor` but richer: a field preset can contribute `default`, `executionDefaults`, `id`, `unique`, and `nullable` in addition to the storage-type triple. PSL → typed-args coercion happens here (via `mapPslHelperArgs`) so that `instantiateAuthoringFieldPreset` itself stays typed-input-only and TS keeps its zero-runtime-validation cost.
|
|
280
|
+
*/
|
|
281
|
+
export function instantiatePslFieldPreset(input: {
|
|
282
|
+
readonly call: PslTypeConstructorCall;
|
|
283
|
+
readonly descriptor: AuthoringFieldPresetDescriptor;
|
|
284
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
285
|
+
readonly sourceId: string;
|
|
286
|
+
readonly entityLabel: string;
|
|
287
|
+
}):
|
|
288
|
+
| {
|
|
289
|
+
readonly descriptor: ColumnDescriptor;
|
|
290
|
+
readonly nullable: boolean;
|
|
291
|
+
readonly default?: ColumnDefault;
|
|
292
|
+
readonly executionDefaults?: ExecutionMutationDefaultPhases;
|
|
293
|
+
readonly id: boolean;
|
|
294
|
+
readonly unique: boolean;
|
|
295
|
+
}
|
|
296
|
+
| undefined {
|
|
297
|
+
const helperPath = input.call.path.join('.');
|
|
298
|
+
const args = mapPslHelperArgs({
|
|
299
|
+
args: input.call.args,
|
|
300
|
+
descriptors: input.descriptor.args ?? [],
|
|
301
|
+
helperLabel: `preset "${helperPath}"`,
|
|
302
|
+
span: input.call.span,
|
|
303
|
+
diagnostics: input.diagnostics,
|
|
304
|
+
sourceId: input.sourceId,
|
|
305
|
+
entityLabel: input.entityLabel,
|
|
306
|
+
});
|
|
307
|
+
if (!args) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
validateAuthoringHelperArguments(helperPath, input.descriptor.args, args);
|
|
313
|
+
const instantiated = instantiateAuthoringFieldPreset(input.descriptor, args);
|
|
314
|
+
return {
|
|
315
|
+
descriptor: {
|
|
316
|
+
codecId: instantiated.descriptor.codecId,
|
|
317
|
+
nativeType: instantiated.descriptor.nativeType,
|
|
318
|
+
...(instantiated.descriptor.typeParams !== undefined
|
|
319
|
+
? { typeParams: instantiated.descriptor.typeParams }
|
|
320
|
+
: {}),
|
|
321
|
+
},
|
|
322
|
+
nullable: instantiated.nullable,
|
|
323
|
+
...(instantiated.default !== undefined ? { default: instantiated.default } : {}),
|
|
324
|
+
...(instantiated.executionDefaults !== undefined
|
|
325
|
+
? { executionDefaults: instantiated.executionDefaults }
|
|
326
|
+
: {}),
|
|
327
|
+
id: instantiated.id,
|
|
328
|
+
unique: instantiated.unique,
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
332
|
+
input.diagnostics.push({
|
|
333
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
334
|
+
message: `${input.entityLabel} preset "${helperPath}" ${message}`,
|
|
335
|
+
sourceId: input.sourceId,
|
|
336
|
+
span: input.call.span,
|
|
337
|
+
});
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Contract contributions a field preset adds beyond the bare storage-type triple. Set when a field is resolved through the field-preset dispatch path; absent when resolved through the type-constructor path or as a scalar/enum/named-type lookup.
|
|
344
|
+
*/
|
|
345
|
+
export type FieldPresetContributions = {
|
|
346
|
+
readonly nullable: boolean;
|
|
347
|
+
readonly id: boolean;
|
|
348
|
+
readonly unique: boolean;
|
|
349
|
+
readonly default?: ColumnDefault;
|
|
350
|
+
readonly executionDefaults?: ExecutionMutationDefaultPhases;
|
|
351
|
+
};
|
|
352
|
+
|
|
230
353
|
export type ResolveFieldTypeResult =
|
|
231
|
-
| {
|
|
354
|
+
| {
|
|
355
|
+
readonly ok: true;
|
|
356
|
+
readonly descriptor: ColumnDescriptor;
|
|
357
|
+
readonly presetContributions?: FieldPresetContributions;
|
|
358
|
+
}
|
|
232
359
|
| { readonly ok: false; readonly alreadyReported: boolean };
|
|
233
360
|
|
|
234
361
|
export function resolveFieldTypeDescriptor(input: {
|
|
@@ -245,18 +372,71 @@ export function resolveFieldTypeDescriptor(input: {
|
|
|
245
372
|
readonly entityLabel: string;
|
|
246
373
|
}): ResolveFieldTypeResult {
|
|
247
374
|
if (input.field.typeConstructor) {
|
|
375
|
+
// Field presets carry richer semantics than type constructors, so a field preset match is the complete answer. Shared composition rejects exact cross-registry collisions before PSL resolution can observe them.
|
|
376
|
+
const presetDescriptor = getAuthoringFieldPreset(
|
|
377
|
+
input.authoringContributions,
|
|
378
|
+
input.field.typeConstructor.path,
|
|
379
|
+
);
|
|
380
|
+
if (presetDescriptor) {
|
|
381
|
+
const instantiated = instantiatePslFieldPreset({
|
|
382
|
+
call: input.field.typeConstructor,
|
|
383
|
+
descriptor: presetDescriptor,
|
|
384
|
+
diagnostics: input.diagnostics,
|
|
385
|
+
sourceId: input.sourceId,
|
|
386
|
+
entityLabel: input.entityLabel,
|
|
387
|
+
});
|
|
388
|
+
if (!instantiated) {
|
|
389
|
+
return { ok: false, alreadyReported: true };
|
|
390
|
+
}
|
|
391
|
+
const presetContributions: FieldPresetContributions = {
|
|
392
|
+
nullable: instantiated.nullable,
|
|
393
|
+
id: instantiated.id,
|
|
394
|
+
unique: instantiated.unique,
|
|
395
|
+
...(instantiated.default !== undefined ? { default: instantiated.default } : {}),
|
|
396
|
+
...(instantiated.executionDefaults !== undefined
|
|
397
|
+
? { executionDefaults: instantiated.executionDefaults }
|
|
398
|
+
: {}),
|
|
399
|
+
};
|
|
400
|
+
return { ok: true, descriptor: instantiated.descriptor, presetContributions };
|
|
401
|
+
}
|
|
402
|
+
|
|
248
403
|
const helperPath = input.field.typeConstructor.path.join('.');
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
404
|
+
const namespacePrefix =
|
|
405
|
+
input.field.typeConstructor.path.length > 1 ? input.field.typeConstructor.path[0] : undefined;
|
|
406
|
+
const typeDescriptor = getAuthoringTypeConstructor(
|
|
407
|
+
input.authoringContributions,
|
|
408
|
+
input.field.typeConstructor.path,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
if (
|
|
412
|
+
!typeDescriptor &&
|
|
413
|
+
namespacePrefix &&
|
|
414
|
+
hasRegisteredFieldNamespace(input.authoringContributions, namespacePrefix)
|
|
415
|
+
) {
|
|
416
|
+
reportUnknownFieldPreset({
|
|
417
|
+
entityLabel: input.entityLabel,
|
|
418
|
+
namespace: namespacePrefix,
|
|
419
|
+
helperPath,
|
|
420
|
+
sourceId: input.sourceId,
|
|
421
|
+
span: input.field.typeConstructor.span,
|
|
422
|
+
diagnostics: input.diagnostics,
|
|
423
|
+
});
|
|
424
|
+
return { ok: false, alreadyReported: true };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const descriptor =
|
|
428
|
+
typeDescriptor ??
|
|
429
|
+
resolvePslTypeConstructorDescriptor({
|
|
430
|
+
call: input.field.typeConstructor,
|
|
431
|
+
authoringContributions: input.authoringContributions,
|
|
432
|
+
composedExtensions: input.composedExtensions,
|
|
433
|
+
familyId: input.familyId,
|
|
434
|
+
targetId: input.targetId,
|
|
435
|
+
diagnostics: input.diagnostics,
|
|
436
|
+
sourceId: input.sourceId,
|
|
437
|
+
unsupportedCode: 'PSL_UNSUPPORTED_FIELD_TYPE',
|
|
438
|
+
unsupportedMessage: `${input.entityLabel} type constructor "${helperPath}" is not supported in SQL PSL provider v1`,
|
|
439
|
+
});
|
|
260
440
|
if (!descriptor) {
|
|
261
441
|
return { ok: false, alreadyReported: true };
|
|
262
442
|
}
|
|
@@ -495,7 +675,7 @@ export function lowerDefaultForField(input: {
|
|
|
495
675
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
496
676
|
}): {
|
|
497
677
|
readonly defaultValue?: ColumnDefault;
|
|
498
|
-
readonly
|
|
678
|
+
readonly executionDefaults?: ExecutionMutationDefaultPhases;
|
|
499
679
|
} {
|
|
500
680
|
const positionalEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'positional');
|
|
501
681
|
const namedEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'named');
|
|
@@ -568,6 +748,17 @@ export function lowerDefaultForField(input: {
|
|
|
568
748
|
return {};
|
|
569
749
|
}
|
|
570
750
|
|
|
751
|
+
// Preset-only generators (e.g. `timestampNow`) co-register their codec through the preset descriptor, so they don't carry an `applicableCodecIds` list. Such a generator surfacing on the `@default(...)` lowering path is itself the bug — emit a diagnostic pointing the user at the correct authoring surface.
|
|
752
|
+
if (generatorDescriptor.applicableCodecIds === undefined) {
|
|
753
|
+
input.diagnostics.push({
|
|
754
|
+
code: 'PSL_INVALID_DEFAULT_APPLICABILITY',
|
|
755
|
+
message: `Default generator "${generatorDescriptor.id}" is not applicable to "@default(...)" lowering. Use the corresponding field preset (e.g. \`temporal.${generatorDescriptor.id === 'timestampNow' ? 'updatedAt' : generatorDescriptor.id}()\`) instead.`,
|
|
756
|
+
sourceId: input.sourceId,
|
|
757
|
+
span: expressionEntry.span,
|
|
758
|
+
});
|
|
759
|
+
return {};
|
|
760
|
+
}
|
|
761
|
+
|
|
571
762
|
if (!generatorDescriptor.applicableCodecIds.includes(input.columnDescriptor.codecId)) {
|
|
572
763
|
input.diagnostics.push({
|
|
573
764
|
code: 'PSL_INVALID_DEFAULT_APPLICABILITY',
|
|
@@ -578,7 +769,7 @@ export function lowerDefaultForField(input: {
|
|
|
578
769
|
return {};
|
|
579
770
|
}
|
|
580
771
|
|
|
581
|
-
return {
|
|
772
|
+
return { executionDefaults: { onCreate: lowered.value.generated } };
|
|
582
773
|
}
|
|
583
774
|
|
|
584
775
|
export function resolveColumnDescriptor(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
|
|
2
|
-
import type { ColumnDefault,
|
|
2
|
+
import type { ColumnDefault, ExecutionMutationDefaultPhases } from '@prisma-next/contract/types';
|
|
3
3
|
import type { AuthoringContributions } from '@prisma-next/framework-components/authoring';
|
|
4
4
|
import type {
|
|
5
5
|
ControlMutationDefaultRegistry,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
parseConstraintMapArgument,
|
|
14
14
|
parseMapName,
|
|
15
15
|
} from './psl-attribute-parsing';
|
|
16
|
-
import type { ColumnDescriptor } from './psl-column-resolution';
|
|
16
|
+
import type { ColumnDescriptor, FieldPresetContributions } from './psl-column-resolution';
|
|
17
17
|
import {
|
|
18
18
|
checkUncomposedNamespace,
|
|
19
19
|
lowerDefaultForField,
|
|
@@ -26,7 +26,7 @@ export type ResolvedField = {
|
|
|
26
26
|
readonly columnName: string;
|
|
27
27
|
readonly descriptor: ColumnDescriptor;
|
|
28
28
|
readonly defaultValue?: ColumnDefault;
|
|
29
|
-
readonly
|
|
29
|
+
readonly executionDefaults?: ExecutionMutationDefaultPhases;
|
|
30
30
|
readonly isId: boolean;
|
|
31
31
|
readonly isUnique: boolean;
|
|
32
32
|
readonly idName?: string;
|
|
@@ -68,10 +68,54 @@ const BUILTIN_FIELD_ATTRIBUTE_NAMES: ReadonlySet<string> = new Set([
|
|
|
68
68
|
'map',
|
|
69
69
|
]);
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Per-attribute migration rule for attributes that have been removed
|
|
73
|
+
* from PSL in favor of the field-preset surface. The `hint` text is
|
|
74
|
+
* appended to the `PSL_UNSUPPORTED_FIELD_ATTRIBUTE` message so users
|
|
75
|
+
* porting Prisma 6 schemas see "use this preset instead" inline; the
|
|
76
|
+
* `suppressWhen` predicate skips the hint when the user has already
|
|
77
|
+
* migrated (so they don't get told to do what they just did).
|
|
78
|
+
*
|
|
79
|
+
* Pairing the suppression predicate with the hint makes each entry
|
|
80
|
+
* self-contained: a future entry for, say, `@id` ↔ `id.uuidv7()` cannot
|
|
81
|
+
* silently inherit the wrong predicate when added.
|
|
82
|
+
*/
|
|
83
|
+
interface RemovedAttributeRule {
|
|
84
|
+
readonly hint: string;
|
|
85
|
+
readonly suppressWhen: (field: PslField) => boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const REMOVED_ATTRIBUTE_RULES: ReadonlyMap<string, RemovedAttributeRule> = new Map([
|
|
89
|
+
[
|
|
90
|
+
'updatedAt',
|
|
91
|
+
{
|
|
92
|
+
hint: 'Use `temporal.updatedAt()` as a field-preset call instead.',
|
|
93
|
+
suppressWhen: (field) => field.typeConstructor?.path[0] === 'temporal',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
// `validateFieldAttributes` short-circuits on `BUILTIN_FIELD_ATTRIBUTE_NAMES`
|
|
99
|
+
// before consulting `REMOVED_ATTRIBUTE_RULES`. A name appearing in both sets
|
|
100
|
+
// would silently suppress its migration hint, defeating the purpose of the
|
|
101
|
+
// hint table. Fail at module load with a clear message — the table is
|
|
102
|
+
// designed to grow and this is the cheap insurance against future drift.
|
|
103
|
+
{
|
|
104
|
+
const overlap = [...REMOVED_ATTRIBUTE_RULES.keys()].filter((name) =>
|
|
105
|
+
BUILTIN_FIELD_ATTRIBUTE_NAMES.has(name),
|
|
106
|
+
);
|
|
107
|
+
if (overlap.length > 0) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`BUILTIN_FIELD_ATTRIBUTE_NAMES and REMOVED_ATTRIBUTE_RULES must not overlap. Names in both: ${overlap.join(', ')}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
71
114
|
function validateFieldAttributes(input: {
|
|
72
115
|
readonly model: PslModel;
|
|
73
116
|
readonly field: PslField;
|
|
74
117
|
readonly composedExtensions: ReadonlySet<string>;
|
|
118
|
+
readonly authoringContributions: AuthoringContributions | undefined;
|
|
75
119
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
76
120
|
readonly sourceId: string;
|
|
77
121
|
readonly familyId: string;
|
|
@@ -85,6 +129,7 @@ function validateFieldAttributes(input: {
|
|
|
85
129
|
const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
|
|
86
130
|
familyId: input.familyId,
|
|
87
131
|
targetId: input.targetId,
|
|
132
|
+
authoringContributions: input.authoringContributions,
|
|
88
133
|
});
|
|
89
134
|
if (uncomposedNamespace) {
|
|
90
135
|
reportUncomposedNamespace({
|
|
@@ -97,9 +142,16 @@ function validateFieldAttributes(input: {
|
|
|
97
142
|
continue;
|
|
98
143
|
}
|
|
99
144
|
|
|
145
|
+
const baseMessage = `Field "${input.model.name}.${input.field.name}" uses unsupported attribute "@${attribute.name}"`;
|
|
146
|
+
const removedRule = REMOVED_ATTRIBUTE_RULES.get(attribute.name);
|
|
147
|
+
const message =
|
|
148
|
+
removedRule && !removedRule.suppressWhen(input.field)
|
|
149
|
+
? `${baseMessage}. ${removedRule.hint}`
|
|
150
|
+
: baseMessage;
|
|
151
|
+
|
|
100
152
|
input.diagnostics.push({
|
|
101
153
|
code: 'PSL_UNSUPPORTED_FIELD_ATTRIBUTE',
|
|
102
|
-
message
|
|
154
|
+
message,
|
|
103
155
|
sourceId: input.sourceId,
|
|
104
156
|
span: attribute.span,
|
|
105
157
|
});
|
|
@@ -159,7 +211,9 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
159
211
|
const resolvedFields: ResolvedField[] = [];
|
|
160
212
|
|
|
161
213
|
for (const field of model.fields) {
|
|
162
|
-
|
|
214
|
+
const isModelField = modelNames.has(field.typeName);
|
|
215
|
+
|
|
216
|
+
if (field.list && isModelField) {
|
|
163
217
|
continue;
|
|
164
218
|
}
|
|
165
219
|
|
|
@@ -167,6 +221,7 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
167
221
|
model,
|
|
168
222
|
field,
|
|
169
223
|
composedExtensions,
|
|
224
|
+
authoringContributions,
|
|
170
225
|
diagnostics,
|
|
171
226
|
sourceId,
|
|
172
227
|
familyId,
|
|
@@ -174,7 +229,7 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
174
229
|
});
|
|
175
230
|
|
|
176
231
|
const relationAttribute = getAttribute(field.attributes, 'relation');
|
|
177
|
-
if (
|
|
232
|
+
if (isModelField && relationAttribute) {
|
|
178
233
|
continue;
|
|
179
234
|
}
|
|
180
235
|
|
|
@@ -183,6 +238,7 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
183
238
|
|
|
184
239
|
let descriptor: ColumnDescriptor | undefined;
|
|
185
240
|
let scalarCodecId: string | undefined;
|
|
241
|
+
let presetContributions: FieldPresetContributions | undefined;
|
|
186
242
|
const resolveInput = {
|
|
187
243
|
field,
|
|
188
244
|
enumTypeDescriptors,
|
|
@@ -212,6 +268,17 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
212
268
|
}
|
|
213
269
|
continue;
|
|
214
270
|
}
|
|
271
|
+
// Field presets are complete declarations — they carry their own codec
|
|
272
|
+
// and do not compose with `[]` list-of semantics. Reject early.
|
|
273
|
+
if (resolved.presetContributions) {
|
|
274
|
+
diagnostics.push({
|
|
275
|
+
code: 'PSL_PRESET_NOT_LIST',
|
|
276
|
+
message: `Field "${model.name}.${field.name}" uses a field-preset call as a list element type. Presets cannot be list elements; remove "[]" or use a scalar type.`,
|
|
277
|
+
sourceId,
|
|
278
|
+
span: field.span,
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
215
282
|
scalarCodecId = resolved.descriptor.codecId;
|
|
216
283
|
descriptor = scalarTypeDescriptors.get('Json');
|
|
217
284
|
} else {
|
|
@@ -228,13 +295,37 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
228
295
|
continue;
|
|
229
296
|
}
|
|
230
297
|
descriptor = resolved.descriptor;
|
|
298
|
+
presetContributions = resolved.presetContributions;
|
|
231
299
|
}
|
|
232
300
|
|
|
233
301
|
if (!descriptor) {
|
|
234
302
|
continue;
|
|
235
303
|
}
|
|
236
304
|
|
|
305
|
+
// Field presets are complete declarations: the preset names its own codec
|
|
306
|
+
// and contributes any combination of default / executionDefaults / id /
|
|
307
|
+
// unique. Optional and `@default(...)` modifiers contradict that, so they
|
|
308
|
+
// are hard errors per spec FR7.
|
|
309
|
+
if (presetContributions && field.optional) {
|
|
310
|
+
diagnostics.push({
|
|
311
|
+
code: 'PSL_PRESET_NOT_OPTIONAL',
|
|
312
|
+
message: `Field "${model.name}.${field.name}" uses a field-preset call and cannot be optional. Remove "?" or use a different field type.`,
|
|
313
|
+
sourceId,
|
|
314
|
+
span: field.span,
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
237
319
|
const defaultAttribute = getAttribute(field.attributes, 'default');
|
|
320
|
+
if (presetContributions && defaultAttribute) {
|
|
321
|
+
diagnostics.push({
|
|
322
|
+
code: 'PSL_PRESET_AND_DEFAULT_CONFLICT',
|
|
323
|
+
message: `Field "${model.name}.${field.name}" uses a field-preset call and cannot also declare @default(...). The preset already specifies the default value.`,
|
|
324
|
+
sourceId,
|
|
325
|
+
span: defaultAttribute.span,
|
|
326
|
+
});
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
238
329
|
const loweredDefault = defaultAttribute
|
|
239
330
|
? lowerDefaultForField({
|
|
240
331
|
modelName: model.name,
|
|
@@ -247,11 +338,10 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
247
338
|
diagnostics,
|
|
248
339
|
})
|
|
249
340
|
: {};
|
|
250
|
-
|
|
341
|
+
const loweredOnCreate = loweredDefault.executionDefaults?.onCreate;
|
|
342
|
+
if (field.optional && loweredOnCreate) {
|
|
251
343
|
const generatorDescription =
|
|
252
|
-
|
|
253
|
-
? `"${loweredDefault.executionDefault.id}"`
|
|
254
|
-
: 'for this field';
|
|
344
|
+
loweredOnCreate.kind === 'generator' ? `"${loweredOnCreate.id}"` : 'for this field';
|
|
255
345
|
diagnostics.push({
|
|
256
346
|
code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
|
|
257
347
|
message: `Field "${model.name}.${field.name}" cannot be optional when using execution default ${generatorDescription}. Remove "?" or use a storage default.`,
|
|
@@ -260,10 +350,10 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
260
350
|
});
|
|
261
351
|
continue;
|
|
262
352
|
}
|
|
263
|
-
if (
|
|
264
|
-
const generatorDescriptor = generatorDescriptorById.get(
|
|
353
|
+
if (loweredOnCreate) {
|
|
354
|
+
const generatorDescriptor = generatorDescriptorById.get(loweredOnCreate.id);
|
|
265
355
|
const generatedDescriptor = generatorDescriptor?.resolveGeneratedColumnDescriptor?.({
|
|
266
|
-
generated:
|
|
356
|
+
generated: loweredOnCreate,
|
|
267
357
|
});
|
|
268
358
|
if (generatedDescriptor) {
|
|
269
359
|
descriptor = generatedDescriptor;
|
|
@@ -276,15 +366,46 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
|
|
|
276
366
|
sourceId,
|
|
277
367
|
diagnostics,
|
|
278
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
|
+
}
|
|
379
|
+
|
|
380
|
+
// Field presets contribute their own default / executionDefaults / id /
|
|
381
|
+
// unique. They take precedence over attribute-derived contributions for
|
|
382
|
+
// this field, since a preset *is* the field declaration. Conflicts with
|
|
383
|
+
// `@default` and optional are already rejected above; explicit `@id`
|
|
384
|
+
// would be redundant noise on the resolved field, so we surface it as
|
|
385
|
+
// a hard error here for symmetry.
|
|
386
|
+
if (presetContributions && idAttribute && !presetContributions.id) {
|
|
387
|
+
diagnostics.push({
|
|
388
|
+
code: 'PSL_PRESET_AND_ID_CONFLICT',
|
|
389
|
+
message: `Field "${model.name}.${field.name}" uses a field-preset call and cannot also declare @id. Use a preset that contributes id semantics, or drop @id.`,
|
|
390
|
+
sourceId,
|
|
391
|
+
span: idAttribute.span,
|
|
392
|
+
});
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
279
395
|
|
|
396
|
+
// Field-preset contributions take precedence over attribute-derived
|
|
397
|
+
// sources when present.
|
|
398
|
+
const fieldExecutionDefaults =
|
|
399
|
+
presetContributions?.executionDefaults ?? loweredDefault.executionDefaults;
|
|
400
|
+
const fieldDefaultValue = presetContributions?.default ?? loweredDefault.defaultValue;
|
|
280
401
|
resolvedFields.push({
|
|
281
402
|
field,
|
|
282
403
|
columnName: mappedColumnName,
|
|
283
404
|
descriptor,
|
|
284
|
-
...ifDefined('defaultValue',
|
|
285
|
-
...ifDefined('
|
|
286
|
-
isId: Boolean(
|
|
287
|
-
isUnique: Boolean(uniqueAttribute),
|
|
405
|
+
...ifDefined('defaultValue', fieldDefaultValue),
|
|
406
|
+
...ifDefined('executionDefaults', fieldExecutionDefaults),
|
|
407
|
+
isId: isIdField || Boolean(presetContributions?.id),
|
|
408
|
+
isUnique: Boolean(uniqueAttribute) || Boolean(presetContributions?.unique),
|
|
288
409
|
...ifDefined('idName', idName),
|
|
289
410
|
...ifDefined('uniqueName', uniqueName),
|
|
290
411
|
...ifDefined('many', isListField ? (true as const) : undefined),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
|
|
2
|
+
import type { AuthoringContributions } from '@prisma-next/framework-components/authoring';
|
|
2
3
|
import type { PslAttribute, PslField, PslSpan } from '@prisma-next/psl-parser';
|
|
3
4
|
import type { ReferentialAction } from '@prisma-next/sql-contract/types';
|
|
4
5
|
import type { RelationNode } from '@prisma-next/sql-contract-ts/contract-builder';
|
|
@@ -334,6 +335,7 @@ export function validateNavigationListFieldAttributes(input: {
|
|
|
334
335
|
readonly field: PslField;
|
|
335
336
|
readonly sourceId: string;
|
|
336
337
|
readonly composedExtensions: Set<string>;
|
|
338
|
+
readonly authoringContributions: AuthoringContributions | undefined;
|
|
337
339
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
338
340
|
readonly familyId: string;
|
|
339
341
|
readonly targetId: string;
|
|
@@ -347,6 +349,7 @@ export function validateNavigationListFieldAttributes(input: {
|
|
|
347
349
|
const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
|
|
348
350
|
familyId: input.familyId,
|
|
349
351
|
targetId: input.targetId,
|
|
352
|
+
authoringContributions: input.authoringContributions,
|
|
350
353
|
});
|
|
351
354
|
if (uncomposedNamespace) {
|
|
352
355
|
reportUncomposedNamespace({
|