@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.
@@ -0,0 +1,454 @@
1
+ import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
2
+ import type { AuthoringArgumentDescriptor } from '@prisma-next/framework-components/authoring';
3
+ import type { PslAttributeArgument, PslSpan } from '@prisma-next/psl-parser';
4
+ import { unquoteStringLiteral } from './psl-attribute-parsing';
5
+
6
+ const INVALID_AUTHORING_ARGUMENT = Symbol('invalidAuthoringArgument');
7
+
8
+ type ParsedPslLiteral =
9
+ | string
10
+ | number
11
+ | boolean
12
+ | null
13
+ | ParsedPslLiteral[]
14
+ | { [key: string]: ParsedPslLiteral };
15
+
16
+ function isIdentifierStartCharacter(character: string | undefined): boolean {
17
+ return character !== undefined && /[A-Za-z_$]/.test(character);
18
+ }
19
+
20
+ function isIdentifierCharacter(character: string | undefined): boolean {
21
+ return character !== undefined && /[A-Za-z0-9_$]/.test(character);
22
+ }
23
+
24
+ function parseJsLikeLiteral(value: string): ParsedPslLiteral | typeof INVALID_AUTHORING_ARGUMENT {
25
+ let index = 0;
26
+
27
+ function skipWhitespace() {
28
+ while (/\s/.test(value[index] ?? '')) {
29
+ index += 1;
30
+ }
31
+ }
32
+
33
+ function parseIdentifier(): string | typeof INVALID_AUTHORING_ARGUMENT {
34
+ const first = value[index];
35
+ if (!isIdentifierStartCharacter(first)) {
36
+ return INVALID_AUTHORING_ARGUMENT;
37
+ }
38
+
39
+ let end = index + 1;
40
+ while (isIdentifierCharacter(value[end])) {
41
+ end += 1;
42
+ }
43
+
44
+ const identifier = value.slice(index, end);
45
+ index = end;
46
+ return identifier;
47
+ }
48
+
49
+ function parseString(): string | typeof INVALID_AUTHORING_ARGUMENT {
50
+ const quote = value[index];
51
+ if (quote !== '"' && quote !== "'") {
52
+ return INVALID_AUTHORING_ARGUMENT;
53
+ }
54
+
55
+ index += 1;
56
+ let result = '';
57
+
58
+ while (index < value.length) {
59
+ const character = value[index];
60
+ index += 1;
61
+
62
+ if (character === undefined) {
63
+ return INVALID_AUTHORING_ARGUMENT;
64
+ }
65
+
66
+ if (character === quote) {
67
+ return result;
68
+ }
69
+
70
+ if (character !== '\\') {
71
+ result += character;
72
+ continue;
73
+ }
74
+
75
+ const escaped = value[index];
76
+ index += 1;
77
+
78
+ if (escaped === undefined) {
79
+ return INVALID_AUTHORING_ARGUMENT;
80
+ }
81
+
82
+ switch (escaped) {
83
+ case "'":
84
+ case '"':
85
+ case '\\':
86
+ case '/':
87
+ result += escaped;
88
+ break;
89
+ case 'b':
90
+ result += '\b';
91
+ break;
92
+ case 'f':
93
+ result += '\f';
94
+ break;
95
+ case 'n':
96
+ result += '\n';
97
+ break;
98
+ case 'r':
99
+ result += '\r';
100
+ break;
101
+ case 't':
102
+ result += '\t';
103
+ break;
104
+ case 'u': {
105
+ const hex = value.slice(index, index + 4);
106
+ if (!/^[0-9A-Fa-f]{4}$/.test(hex)) {
107
+ return INVALID_AUTHORING_ARGUMENT;
108
+ }
109
+ result += String.fromCharCode(Number.parseInt(hex, 16));
110
+ index += 4;
111
+ break;
112
+ }
113
+ default:
114
+ return INVALID_AUTHORING_ARGUMENT;
115
+ }
116
+ }
117
+
118
+ return INVALID_AUTHORING_ARGUMENT;
119
+ }
120
+
121
+ function parseNumber(): number | typeof INVALID_AUTHORING_ARGUMENT {
122
+ const match = value.slice(index).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/);
123
+ const raw = match?.[0];
124
+ if (!raw) {
125
+ return INVALID_AUTHORING_ARGUMENT;
126
+ }
127
+
128
+ const parsed = Number(raw);
129
+ if (!Number.isFinite(parsed)) {
130
+ return INVALID_AUTHORING_ARGUMENT;
131
+ }
132
+
133
+ index += raw.length;
134
+ return parsed;
135
+ }
136
+
137
+ function parseArray(): ParsedPslLiteral[] | typeof INVALID_AUTHORING_ARGUMENT {
138
+ if (value[index] !== '[') {
139
+ return INVALID_AUTHORING_ARGUMENT;
140
+ }
141
+
142
+ index += 1;
143
+ const result: ParsedPslLiteral[] = [];
144
+
145
+ skipWhitespace();
146
+ if (value[index] === ']') {
147
+ index += 1;
148
+ return result;
149
+ }
150
+
151
+ while (index < value.length) {
152
+ const entry = parseValue();
153
+ if (entry === INVALID_AUTHORING_ARGUMENT) {
154
+ return INVALID_AUTHORING_ARGUMENT;
155
+ }
156
+ result.push(entry);
157
+
158
+ skipWhitespace();
159
+ if (value[index] === ',') {
160
+ index += 1;
161
+ skipWhitespace();
162
+ continue;
163
+ }
164
+ if (value[index] === ']') {
165
+ index += 1;
166
+ return result;
167
+ }
168
+ return INVALID_AUTHORING_ARGUMENT;
169
+ }
170
+
171
+ return INVALID_AUTHORING_ARGUMENT;
172
+ }
173
+
174
+ function parseObject(): { [key: string]: ParsedPslLiteral } | typeof INVALID_AUTHORING_ARGUMENT {
175
+ if (value[index] !== '{') {
176
+ return INVALID_AUTHORING_ARGUMENT;
177
+ }
178
+
179
+ index += 1;
180
+ const result: { [key: string]: ParsedPslLiteral } = {};
181
+
182
+ skipWhitespace();
183
+ if (value[index] === '}') {
184
+ index += 1;
185
+ return result;
186
+ }
187
+
188
+ while (index < value.length) {
189
+ skipWhitespace();
190
+ const key = value[index] === '"' || value[index] === "'" ? parseString() : parseIdentifier();
191
+ if (key === INVALID_AUTHORING_ARGUMENT) {
192
+ return INVALID_AUTHORING_ARGUMENT;
193
+ }
194
+
195
+ skipWhitespace();
196
+ if (value[index] !== ':') {
197
+ return INVALID_AUTHORING_ARGUMENT;
198
+ }
199
+
200
+ index += 1;
201
+ const entry = parseValue();
202
+ if (entry === INVALID_AUTHORING_ARGUMENT) {
203
+ return INVALID_AUTHORING_ARGUMENT;
204
+ }
205
+ result[key] = entry;
206
+
207
+ skipWhitespace();
208
+ if (value[index] === ',') {
209
+ index += 1;
210
+ skipWhitespace();
211
+ continue;
212
+ }
213
+ if (value[index] === '}') {
214
+ index += 1;
215
+ return result;
216
+ }
217
+ return INVALID_AUTHORING_ARGUMENT;
218
+ }
219
+
220
+ return INVALID_AUTHORING_ARGUMENT;
221
+ }
222
+
223
+ function parseValue(): ParsedPslLiteral | typeof INVALID_AUTHORING_ARGUMENT {
224
+ skipWhitespace();
225
+ const character = value[index];
226
+ if (character === '{') {
227
+ return parseObject();
228
+ }
229
+ if (character === '[') {
230
+ return parseArray();
231
+ }
232
+ if (character === '"' || character === "'") {
233
+ return parseString();
234
+ }
235
+ if (character === '-' || /\d/.test(character ?? '')) {
236
+ return parseNumber();
237
+ }
238
+
239
+ const identifier = parseIdentifier();
240
+ if (identifier === INVALID_AUTHORING_ARGUMENT) {
241
+ return INVALID_AUTHORING_ARGUMENT;
242
+ }
243
+ if (identifier === 'true') {
244
+ return true;
245
+ }
246
+ if (identifier === 'false') {
247
+ return false;
248
+ }
249
+ if (identifier === 'null') {
250
+ return null;
251
+ }
252
+ return INVALID_AUTHORING_ARGUMENT;
253
+ }
254
+
255
+ skipWhitespace();
256
+ const parsed = parseValue();
257
+ if (parsed === INVALID_AUTHORING_ARGUMENT) {
258
+ return parsed;
259
+ }
260
+
261
+ skipWhitespace();
262
+ return index === value.length ? parsed : INVALID_AUTHORING_ARGUMENT;
263
+ }
264
+
265
+ function parseStringArrayLiteral(
266
+ value: string,
267
+ ): readonly string[] | typeof INVALID_AUTHORING_ARGUMENT {
268
+ const parsed = parseJsLikeLiteral(value);
269
+ if (parsed === INVALID_AUTHORING_ARGUMENT || !Array.isArray(parsed)) {
270
+ return INVALID_AUTHORING_ARGUMENT;
271
+ }
272
+ if (!parsed.every((item): item is string => typeof item === 'string')) {
273
+ return INVALID_AUTHORING_ARGUMENT;
274
+ }
275
+ return parsed;
276
+ }
277
+
278
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
279
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
280
+ }
281
+
282
+ function parsePslObjectLiteral(
283
+ value: string,
284
+ ): Record<string, unknown> | typeof INVALID_AUTHORING_ARGUMENT {
285
+ const trimmed = value.trim();
286
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
287
+ return INVALID_AUTHORING_ARGUMENT;
288
+ }
289
+
290
+ let parsed: unknown;
291
+ try {
292
+ parsed = JSON.parse(trimmed);
293
+ } catch {
294
+ parsed = parseJsLikeLiteral(trimmed);
295
+ if (parsed === INVALID_AUTHORING_ARGUMENT) {
296
+ return INVALID_AUTHORING_ARGUMENT;
297
+ }
298
+ }
299
+
300
+ if (!isPlainObject(parsed)) {
301
+ return INVALID_AUTHORING_ARGUMENT;
302
+ }
303
+
304
+ return parsed;
305
+ }
306
+
307
+ function parsePslAuthoringArgumentValue(
308
+ descriptor: AuthoringArgumentDescriptor,
309
+ rawValue: string,
310
+ ): unknown | typeof INVALID_AUTHORING_ARGUMENT {
311
+ switch (descriptor.kind) {
312
+ case 'string':
313
+ return unquoteStringLiteral(rawValue);
314
+ case 'number': {
315
+ const parsed = Number(unquoteStringLiteral(rawValue));
316
+ return Number.isNaN(parsed) ? INVALID_AUTHORING_ARGUMENT : parsed;
317
+ }
318
+ case 'stringArray':
319
+ return parseStringArrayLiteral(rawValue);
320
+ case 'object':
321
+ return parsePslObjectLiteral(rawValue);
322
+ default: {
323
+ const _exhaustive: never = descriptor;
324
+ void _exhaustive;
325
+ return INVALID_AUTHORING_ARGUMENT;
326
+ }
327
+ }
328
+ }
329
+
330
+ function pushInvalidPslHelperArgument(input: {
331
+ readonly diagnostics: ContractSourceDiagnostic[];
332
+ readonly sourceId: string;
333
+ readonly span: PslSpan;
334
+ readonly entityLabel: string;
335
+ readonly helperLabel: string;
336
+ readonly message: string;
337
+ }): undefined {
338
+ input.diagnostics.push({
339
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
340
+ message: `${input.entityLabel} ${input.helperLabel} ${input.message}`,
341
+ sourceId: input.sourceId,
342
+ span: input.span,
343
+ });
344
+ return undefined;
345
+ }
346
+
347
+ export function mapPslHelperArgs(input: {
348
+ readonly args: readonly PslAttributeArgument[];
349
+ readonly descriptors: readonly AuthoringArgumentDescriptor[];
350
+ readonly helperLabel: string;
351
+ readonly span: PslSpan;
352
+ readonly diagnostics: ContractSourceDiagnostic[];
353
+ readonly sourceId: string;
354
+ readonly entityLabel: string;
355
+ }): readonly unknown[] | undefined {
356
+ const mappedArgs: unknown[] = input.descriptors.map(() => undefined);
357
+
358
+ const positionalArgs = input.args.filter((arg) => arg.kind === 'positional');
359
+ const namedArgs = input.args.filter((arg) => arg.kind === 'named');
360
+
361
+ if (positionalArgs.length > input.descriptors.length) {
362
+ return pushInvalidPslHelperArgument({
363
+ diagnostics: input.diagnostics,
364
+ sourceId: input.sourceId,
365
+ span: input.span,
366
+ entityLabel: input.entityLabel,
367
+ helperLabel: input.helperLabel,
368
+ message: `accepts at most ${input.descriptors.length} argument(s), received ${positionalArgs.length}.`,
369
+ });
370
+ }
371
+
372
+ for (const [index, argument] of positionalArgs.entries()) {
373
+ const descriptor = input.descriptors[index];
374
+ if (!descriptor) {
375
+ return pushInvalidPslHelperArgument({
376
+ diagnostics: input.diagnostics,
377
+ sourceId: input.sourceId,
378
+ span: argument.span,
379
+ entityLabel: input.entityLabel,
380
+ helperLabel: input.helperLabel,
381
+ message: `does not define positional argument #${index + 1}.`,
382
+ });
383
+ }
384
+
385
+ const value = parsePslAuthoringArgumentValue(descriptor, argument.value);
386
+ if (value === INVALID_AUTHORING_ARGUMENT) {
387
+ return pushInvalidPslHelperArgument({
388
+ diagnostics: input.diagnostics,
389
+ sourceId: input.sourceId,
390
+ span: argument.span,
391
+ entityLabel: input.entityLabel,
392
+ helperLabel: input.helperLabel,
393
+ message: `cannot parse argument #${index + 1} for descriptor kind "${descriptor.kind}".`,
394
+ });
395
+ }
396
+
397
+ mappedArgs[index] = value;
398
+ }
399
+
400
+ for (const argument of namedArgs) {
401
+ const descriptorIndex = input.descriptors.findIndex(
402
+ (descriptor) => descriptor.name === argument.name,
403
+ );
404
+ if (descriptorIndex < 0) {
405
+ return pushInvalidPslHelperArgument({
406
+ diagnostics: input.diagnostics,
407
+ sourceId: input.sourceId,
408
+ span: argument.span,
409
+ entityLabel: input.entityLabel,
410
+ helperLabel: input.helperLabel,
411
+ message: `received unknown named argument "${argument.name}".`,
412
+ });
413
+ }
414
+
415
+ if (mappedArgs[descriptorIndex] !== undefined) {
416
+ return pushInvalidPslHelperArgument({
417
+ diagnostics: input.diagnostics,
418
+ sourceId: input.sourceId,
419
+ span: argument.span,
420
+ entityLabel: input.entityLabel,
421
+ helperLabel: input.helperLabel,
422
+ message: `received duplicate value for argument "${argument.name}".`,
423
+ });
424
+ }
425
+
426
+ const descriptor = input.descriptors[descriptorIndex];
427
+ if (!descriptor) {
428
+ return pushInvalidPslHelperArgument({
429
+ diagnostics: input.diagnostics,
430
+ sourceId: input.sourceId,
431
+ span: argument.span,
432
+ entityLabel: input.entityLabel,
433
+ helperLabel: input.helperLabel,
434
+ message: `does not define named argument "${argument.name}".`,
435
+ });
436
+ }
437
+
438
+ const value = parsePslAuthoringArgumentValue(descriptor, argument.value);
439
+ if (value === INVALID_AUTHORING_ARGUMENT) {
440
+ return pushInvalidPslHelperArgument({
441
+ diagnostics: input.diagnostics,
442
+ sourceId: input.sourceId,
443
+ span: argument.span,
444
+ entityLabel: input.entityLabel,
445
+ helperLabel: input.helperLabel,
446
+ message: `cannot parse named argument "${argument.name}" for descriptor kind "${descriptor.kind}".`,
447
+ });
448
+ }
449
+
450
+ mappedArgs[descriptorIndex] = value;
451
+ }
452
+
453
+ return mappedArgs;
454
+ }