@prisma-next/sql-contract-psl 0.3.0-dev.71 → 0.3.0

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/src/provider.ts CHANGED
@@ -1,34 +1,35 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import type { ContractConfig } from '@prisma-next/config/config-types';
3
- import type { TargetPackRef } from '@prisma-next/contract/framework-components';
2
+ import type { ContractConfig, ContractSourceContext } from '@prisma-next/config/config-types';
3
+ import type { AuthoringContributions } from '@prisma-next/framework-components/authoring';
4
+ import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';
4
5
  import { parsePslDocument } from '@prisma-next/psl-parser';
5
6
  import { ifDefined } from '@prisma-next/utils/defined';
6
- import { notOk } from '@prisma-next/utils/result';
7
+ import { notOk, ok } from '@prisma-next/utils/result';
7
8
  import { resolve } from 'pathe';
8
- import { createBuiltinDefaultFunctionRegistry } from './default-function-registry';
9
- import { interpretPslDocumentToSqlContractIR } from './interpreter';
9
+ import type { ControlMutationDefaults } from './default-function-registry';
10
+ import { interpretPslDocumentToSqlContract } from './interpreter';
10
11
 
11
12
  export interface PrismaContractOptions {
12
13
  readonly output?: string;
13
- readonly target?: TargetPackRef<'sql', 'postgres'>;
14
- /**
15
- * Milestone-local namespace availability hook.
16
- *
17
- * This currently models composed extension packs by id only (for example `["pgvector"]`),
18
- * and is sufficient for namespace presence checks in the PSL interpreter.
19
- *
20
- * Future milestones can evolve this to richer composed pack metadata/manifests when
21
- * attribute-level schema/argument validation needs to move beyond namespace existence.
22
- */
14
+ readonly target: TargetPackRef<'sql', 'postgres'>;
15
+ readonly authoringContributions?: AuthoringContributions;
16
+ readonly scalarTypeDescriptors: ReadonlyMap<
17
+ string,
18
+ {
19
+ readonly codecId: string;
20
+ readonly nativeType: string;
21
+ readonly typeRef?: string;
22
+ readonly typeParams?: Record<string, unknown>;
23
+ }
24
+ >;
25
+ readonly controlMutationDefaults?: ControlMutationDefaults;
23
26
  readonly composedExtensionPacks?: readonly string[];
27
+ readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', 'postgres'>[];
24
28
  }
25
29
 
26
- export function prismaContract(
27
- schemaPath: string,
28
- options?: PrismaContractOptions,
29
- ): ContractConfig {
30
+ export function prismaContract(schemaPath: string, options: PrismaContractOptions): ContractConfig {
30
31
  return {
31
- source: async () => {
32
+ source: async (context: ContractSourceContext) => {
32
33
  const absoluteSchemaPath = resolve(schemaPath);
33
34
  let schema: string;
34
35
  try {
@@ -52,14 +53,32 @@ export function prismaContract(
52
53
  schema,
53
54
  sourceId: schemaPath,
54
55
  });
56
+ const composedExtensionPacks = [
57
+ ...(context.composedExtensionPacks ?? []),
58
+ ...(options.composedExtensionPacks ?? []),
59
+ ];
55
60
 
56
- return interpretPslDocumentToSqlContractIR({
61
+ const interpreted = interpretPslDocumentToSqlContract({
57
62
  document,
58
- ...ifDefined('target', options?.target),
59
- ...ifDefined('composedExtensionPacks', options?.composedExtensionPacks),
60
- defaultFunctionRegistry: createBuiltinDefaultFunctionRegistry(),
63
+ target: options.target,
64
+ ...ifDefined('authoringContributions', options.authoringContributions),
65
+ scalarTypeDescriptors: options.scalarTypeDescriptors,
66
+ ...ifDefined(
67
+ 'composedExtensionPacks',
68
+ composedExtensionPacks.length > 0 ? composedExtensionPacks : undefined,
69
+ ),
70
+ ...ifDefined(
71
+ 'composedExtensionPackRefs',
72
+ options.composedExtensionPackRefs?.length ? options.composedExtensionPackRefs : undefined,
73
+ ),
74
+ ...ifDefined('controlMutationDefaults', options.controlMutationDefaults),
61
75
  });
76
+ if (!interpreted.ok) {
77
+ return interpreted;
78
+ }
79
+
80
+ return ok(interpreted.value);
62
81
  },
63
- ...ifDefined('output', options?.output),
82
+ ...ifDefined('output', options.output),
64
83
  };
65
84
  }
@@ -0,0 +1,303 @@
1
+ import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
2
+ import type { PslAttribute, PslSpan } from '@prisma-next/psl-parser';
3
+ import { getPositionalArgument, parseQuotedStringLiteral } from '@prisma-next/psl-parser';
4
+
5
+ export { getPositionalArgument, parseQuotedStringLiteral };
6
+
7
+ export function lowerFirst(value: string): string {
8
+ if (value.length === 0) return value;
9
+ return value[0]?.toLowerCase() + value.slice(1);
10
+ }
11
+
12
+ export function getAttribute(
13
+ attributes: readonly PslAttribute[] | undefined,
14
+ name: string,
15
+ ): PslAttribute | undefined {
16
+ return attributes?.find((attribute) => attribute.name === name);
17
+ }
18
+
19
+ export function getNamedArgument(attribute: PslAttribute, name: string): string | undefined {
20
+ const entry = attribute.args.find((arg) => arg.kind === 'named' && arg.name === name);
21
+ if (!entry || entry.kind !== 'named') {
22
+ return undefined;
23
+ }
24
+ return entry.value;
25
+ }
26
+
27
+ export function getPositionalArgumentEntry(
28
+ attribute: PslAttribute,
29
+ index = 0,
30
+ ): { value: string; span: PslSpan } | undefined {
31
+ const entries = attribute.args.filter((arg) => arg.kind === 'positional');
32
+ const entry = entries[index];
33
+ if (!entry || entry.kind !== 'positional') {
34
+ return undefined;
35
+ }
36
+ return {
37
+ value: entry.value,
38
+ span: entry.span,
39
+ };
40
+ }
41
+
42
+ export function unquoteStringLiteral(value: string): string {
43
+ const trimmed = value.trim();
44
+ const match = trimmed.match(/^(['"])(.*)\1$/);
45
+ if (!match) {
46
+ return trimmed;
47
+ }
48
+ return match[2] ?? '';
49
+ }
50
+
51
+ export function parseFieldList(value: string): readonly string[] | undefined {
52
+ const trimmed = value.trim();
53
+ if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {
54
+ return undefined;
55
+ }
56
+ const body = trimmed.slice(1, -1);
57
+ const parts = body
58
+ .split(',')
59
+ .map((entry) => entry.trim())
60
+ .filter((entry) => entry.length > 0);
61
+ return parts;
62
+ }
63
+
64
+ export function parseMapName(input: {
65
+ readonly attribute: PslAttribute | undefined;
66
+ readonly defaultValue: string;
67
+ readonly sourceId: string;
68
+ readonly diagnostics: ContractSourceDiagnostic[];
69
+ readonly entityLabel: string;
70
+ readonly span: PslSpan;
71
+ }): string {
72
+ if (!input.attribute) {
73
+ return input.defaultValue;
74
+ }
75
+
76
+ const value = getPositionalArgument(input.attribute);
77
+ if (!value) {
78
+ input.diagnostics.push({
79
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
80
+ message: `${input.entityLabel} @map requires a positional quoted string literal argument`,
81
+ sourceId: input.sourceId,
82
+ span: input.attribute.span,
83
+ });
84
+ return input.defaultValue;
85
+ }
86
+ const parsed = parseQuotedStringLiteral(value);
87
+ if (parsed === undefined) {
88
+ input.diagnostics.push({
89
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
90
+ message: `${input.entityLabel} @map requires a positional quoted string literal argument`,
91
+ sourceId: input.sourceId,
92
+ span: input.attribute.span,
93
+ });
94
+ return input.defaultValue;
95
+ }
96
+ return parsed;
97
+ }
98
+
99
+ export function parseConstraintMapArgument(input: {
100
+ readonly attribute: PslAttribute | undefined;
101
+ readonly sourceId: string;
102
+ readonly diagnostics: ContractSourceDiagnostic[];
103
+ readonly entityLabel: string;
104
+ readonly span: PslSpan;
105
+ readonly code: string;
106
+ }): string | undefined {
107
+ if (!input.attribute) {
108
+ return undefined;
109
+ }
110
+
111
+ const raw = getNamedArgument(input.attribute, 'map');
112
+ if (!raw) {
113
+ return undefined;
114
+ }
115
+
116
+ const parsed = parseQuotedStringLiteral(raw);
117
+ if (parsed !== undefined) {
118
+ return parsed;
119
+ }
120
+
121
+ input.diagnostics.push({
122
+ code: input.code,
123
+ message: `${input.entityLabel} map argument must be a quoted string literal`,
124
+ sourceId: input.sourceId,
125
+ span: input.span,
126
+ });
127
+ return undefined;
128
+ }
129
+
130
+ export function getPositionalArguments(attribute: PslAttribute): readonly string[] {
131
+ return attribute.args
132
+ .filter((arg) => arg.kind === 'positional')
133
+ .map((arg) => (arg.kind === 'positional' ? arg.value : ''));
134
+ }
135
+
136
+ export function pushInvalidAttributeArgument(input: {
137
+ readonly diagnostics: ContractSourceDiagnostic[];
138
+ readonly sourceId: string;
139
+ readonly span: PslSpan;
140
+ readonly message: string;
141
+ }): undefined {
142
+ input.diagnostics.push({
143
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
144
+ message: input.message,
145
+ sourceId: input.sourceId,
146
+ span: input.span,
147
+ });
148
+ return undefined;
149
+ }
150
+
151
+ export function parseOptionalSingleIntegerArgument(input: {
152
+ readonly attribute: PslAttribute;
153
+ readonly diagnostics: ContractSourceDiagnostic[];
154
+ readonly sourceId: string;
155
+ readonly entityLabel: string;
156
+ readonly minimum: number;
157
+ readonly valueLabel: string;
158
+ }): number | null | undefined {
159
+ if (input.attribute.args.some((arg) => arg.kind === 'named')) {
160
+ return pushInvalidAttributeArgument({
161
+ diagnostics: input.diagnostics,
162
+ sourceId: input.sourceId,
163
+ span: input.attribute.span,
164
+ message: `${input.entityLabel} @${input.attribute.name} accepts zero or one positional integer argument.`,
165
+ });
166
+ }
167
+
168
+ const positionalArguments = getPositionalArguments(input.attribute);
169
+ if (positionalArguments.length > 1) {
170
+ return pushInvalidAttributeArgument({
171
+ diagnostics: input.diagnostics,
172
+ sourceId: input.sourceId,
173
+ span: input.attribute.span,
174
+ message: `${input.entityLabel} @${input.attribute.name} accepts zero or one positional integer argument.`,
175
+ });
176
+ }
177
+ if (positionalArguments.length === 0) {
178
+ return null;
179
+ }
180
+
181
+ const parsed = Number(unquoteStringLiteral(positionalArguments[0] ?? ''));
182
+ if (!Number.isInteger(parsed) || parsed < input.minimum) {
183
+ return pushInvalidAttributeArgument({
184
+ diagnostics: input.diagnostics,
185
+ sourceId: input.sourceId,
186
+ span: input.attribute.span,
187
+ message: `${input.entityLabel} @${input.attribute.name} requires a ${input.valueLabel}.`,
188
+ });
189
+ }
190
+
191
+ return parsed;
192
+ }
193
+
194
+ export function parseOptionalNumericArguments(input: {
195
+ readonly attribute: PslAttribute;
196
+ readonly diagnostics: ContractSourceDiagnostic[];
197
+ readonly sourceId: string;
198
+ readonly entityLabel: string;
199
+ }): { precision: number; scale?: number } | null | undefined {
200
+ if (input.attribute.args.some((arg) => arg.kind === 'named')) {
201
+ return pushInvalidAttributeArgument({
202
+ diagnostics: input.diagnostics,
203
+ sourceId: input.sourceId,
204
+ span: input.attribute.span,
205
+ message: `${input.entityLabel} @${input.attribute.name} accepts zero, one, or two positional integer arguments.`,
206
+ });
207
+ }
208
+
209
+ const positionalArguments = getPositionalArguments(input.attribute);
210
+ if (positionalArguments.length > 2) {
211
+ return pushInvalidAttributeArgument({
212
+ diagnostics: input.diagnostics,
213
+ sourceId: input.sourceId,
214
+ span: input.attribute.span,
215
+ message: `${input.entityLabel} @${input.attribute.name} accepts zero, one, or two positional integer arguments.`,
216
+ });
217
+ }
218
+ if (positionalArguments.length === 0) {
219
+ return null;
220
+ }
221
+
222
+ const precision = Number(unquoteStringLiteral(positionalArguments[0] ?? ''));
223
+ if (!Number.isInteger(precision) || precision < 1) {
224
+ return pushInvalidAttributeArgument({
225
+ diagnostics: input.diagnostics,
226
+ sourceId: input.sourceId,
227
+ span: input.attribute.span,
228
+ message: `${input.entityLabel} @${input.attribute.name} requires a positive integer precision.`,
229
+ });
230
+ }
231
+
232
+ if (positionalArguments.length === 1) {
233
+ return { precision };
234
+ }
235
+
236
+ const scale = Number(unquoteStringLiteral(positionalArguments[1] ?? ''));
237
+ if (!Number.isInteger(scale) || scale < 0) {
238
+ return pushInvalidAttributeArgument({
239
+ diagnostics: input.diagnostics,
240
+ sourceId: input.sourceId,
241
+ span: input.attribute.span,
242
+ message: `${input.entityLabel} @${input.attribute.name} requires a non-negative integer scale.`,
243
+ });
244
+ }
245
+
246
+ return { precision, scale };
247
+ }
248
+
249
+ export function parseAttributeFieldList(input: {
250
+ readonly attribute: PslAttribute;
251
+ readonly sourceId: string;
252
+ readonly diagnostics: ContractSourceDiagnostic[];
253
+ readonly code: string;
254
+ readonly messagePrefix: string;
255
+ }): readonly string[] | undefined {
256
+ const raw = getNamedArgument(input.attribute, 'fields') ?? getPositionalArgument(input.attribute);
257
+ if (!raw) {
258
+ input.diagnostics.push({
259
+ code: input.code,
260
+ message: `${input.messagePrefix} requires fields list argument`,
261
+ sourceId: input.sourceId,
262
+ span: input.attribute.span,
263
+ });
264
+ return undefined;
265
+ }
266
+ const fields = parseFieldList(raw);
267
+ if (!fields || fields.length === 0) {
268
+ input.diagnostics.push({
269
+ code: input.code,
270
+ message: `${input.messagePrefix} requires bracketed field list argument`,
271
+ sourceId: input.sourceId,
272
+ span: input.attribute.span,
273
+ });
274
+ return undefined;
275
+ }
276
+ return fields;
277
+ }
278
+
279
+ export function mapFieldNamesToColumns(input: {
280
+ readonly modelName: string;
281
+ readonly fieldNames: readonly string[];
282
+ readonly mapping: { readonly fieldColumns: Map<string, string> };
283
+ readonly sourceId: string;
284
+ readonly diagnostics: ContractSourceDiagnostic[];
285
+ readonly span: PslSpan;
286
+ readonly contextLabel: string;
287
+ }): readonly string[] | undefined {
288
+ const columns: string[] = [];
289
+ for (const fieldName of input.fieldNames) {
290
+ const columnName = input.mapping.fieldColumns.get(fieldName);
291
+ if (!columnName) {
292
+ input.diagnostics.push({
293
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
294
+ message: `${input.contextLabel} references unknown field "${input.modelName}.${fieldName}"`,
295
+ sourceId: input.sourceId,
296
+ span: input.span,
297
+ });
298
+ return undefined;
299
+ }
300
+ columns.push(columnName);
301
+ }
302
+ return columns;
303
+ }