@prisma-next/psl-printer 0.5.0-dev.9 → 0.5.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/dist/index.mjs CHANGED
@@ -1,352 +1,4 @@
1
- //#region src/default-mapping.ts
2
- const DEFAULT_FUNCTION_ATTRIBUTES = {
3
- "autoincrement()": "@default(autoincrement())",
4
- "now()": "@default(now())"
5
- };
6
- /**
7
- * Maps a normalized ColumnDefault to a PSL @default(...) attribute string,
8
- * or a comment for unrecognized expressions.
9
- */
10
- function mapDefault(columnDefault, options) {
11
- switch (columnDefault.kind) {
12
- case "literal": return { attribute: `@default(${formatLiteralValue(columnDefault.value)})` };
13
- case "function": {
14
- const attribute = options?.functionAttributes?.[columnDefault.expression] ?? DEFAULT_FUNCTION_ATTRIBUTES[columnDefault.expression] ?? options?.fallbackFunctionAttribute?.(columnDefault.expression);
15
- return attribute ? { attribute } : { comment: `// Raw default: ${columnDefault.expression.replace(/[\r\n]+/g, " ")}` };
16
- }
17
- }
18
- }
19
- /**
20
- * Formats a literal value for use in @default(...).
21
- */
22
- function formatLiteralValue(value) {
23
- if (value === null) return "null";
24
- switch (typeof value) {
25
- case "boolean":
26
- case "number": return String(value);
27
- case "string": return quoteString(value);
28
- default: return quoteString(JSON.stringify(value));
29
- }
30
- }
31
- function quoteString(str) {
32
- return `"${escapeString(str)}"`;
33
- }
34
- function escapeString(str) {
35
- return JSON.stringify(str).slice(1, -1);
36
- }
37
-
38
- //#endregion
39
- //#region src/name-transforms.ts
40
- /**
41
- * PSL reserved words that cannot be used as identifiers without escaping.
42
- */
43
- const PSL_RESERVED_WORDS = new Set([
44
- "model",
45
- "enum",
46
- "types",
47
- "type",
48
- "generator",
49
- "datasource"
50
- ]);
51
- const IDENTIFIER_PART_PATTERN = /[A-Za-z0-9]+/g;
52
- /**
53
- * Checks whether normalization needs to split or sanitize the identifier.
54
- */
55
- function hasSeparators(input) {
56
- return /[^A-Za-z0-9]/.test(input);
57
- }
58
- function extractIdentifierParts(input) {
59
- return input.match(IDENTIFIER_PART_PATTERN) ?? [];
60
- }
61
- function createSyntheticIdentifier(input) {
62
- let hash = 2166136261;
63
- for (const char of input) {
64
- hash ^= char.codePointAt(0) ?? 0;
65
- hash = Math.imul(hash, 16777619);
66
- }
67
- return `x${(hash >>> 0).toString(16)}`;
68
- }
69
- function sanitizeIdentifierCharacters(input) {
70
- const sanitized = input.replace(/[^\w]/g, "");
71
- return sanitized.length > 0 ? sanitized : createSyntheticIdentifier(input);
72
- }
73
- function capitalize(word) {
74
- return word.charAt(0).toUpperCase() + word.slice(1);
75
- }
76
- /**
77
- * Converts a normalized identifier to PascalCase.
78
- */
79
- function snakeToPascalCase(input) {
80
- const parts = extractIdentifierParts(input);
81
- if (parts.length === 0) return capitalize(sanitizeIdentifierCharacters(input));
82
- return parts.map(capitalize).join("");
83
- }
84
- /**
85
- * Converts a normalized identifier to camelCase.
86
- */
87
- function snakeToCamelCase(input) {
88
- const parts = extractIdentifierParts(input);
89
- if (parts.length === 0) return sanitizeIdentifierCharacters(input);
90
- const [firstPart = input, ...rest] = parts;
91
- return firstPart.charAt(0).toLowerCase() + firstPart.slice(1) + rest.map(capitalize).join("");
92
- }
93
- /**
94
- * Checks if a name needs escaping (reserved word or starts with digit).
95
- */
96
- function needsEscaping(name) {
97
- return PSL_RESERVED_WORDS.has(name.toLowerCase()) || /^\d/.test(name);
98
- }
99
- /**
100
- * Escapes a name by prefixing with underscore.
101
- */
102
- function escapeName(name) {
103
- return `_${name}`;
104
- }
105
- function escapeIfNeeded(name) {
106
- return needsEscaping(name) ? escapeName(name) : name;
107
- }
108
- /**
109
- * Converts a database table name to a PSL model name.
110
- * snake_case → PascalCase, with @@map("db_name") when the name was transformed.
111
- * Names that are already PascalCase (no separators, start with uppercase) are kept as-is.
112
- */
113
- function toModelName(tableName) {
114
- let name;
115
- if (hasSeparators(tableName)) name = snakeToPascalCase(tableName);
116
- else name = tableName.charAt(0).toUpperCase() + tableName.slice(1);
117
- if (needsEscaping(name)) return {
118
- name: escapeName(name),
119
- map: tableName
120
- };
121
- if (name !== tableName) return {
122
- name,
123
- map: tableName
124
- };
125
- return { name };
126
- }
127
- /**
128
- * Converts a database column name to a PSL field name.
129
- * snake_case → camelCase, with @map("db_col") when the name was transformed.
130
- * Names that are already camelCase (no separators, start with lowercase) are kept as-is.
131
- */
132
- function toFieldName(columnName) {
133
- let name;
134
- if (hasSeparators(columnName)) name = snakeToCamelCase(columnName);
135
- else name = columnName.charAt(0).toLowerCase() + columnName.slice(1);
136
- if (needsEscaping(name)) return {
137
- name: escapeName(name),
138
- map: columnName
139
- };
140
- if (name !== columnName) return {
141
- name,
142
- map: columnName
143
- };
144
- return { name };
145
- }
146
- /**
147
- * Converts a Postgres enum type name to a PSL enum name.
148
- * snake_case → PascalCase, with @@map when transformed.
149
- */
150
- function toEnumName(pgTypeName) {
151
- let name;
152
- if (hasSeparators(pgTypeName)) name = snakeToPascalCase(pgTypeName);
153
- else name = pgTypeName.charAt(0).toUpperCase() + pgTypeName.slice(1);
154
- if (needsEscaping(name)) return {
155
- name: escapeName(name),
156
- map: pgTypeName
157
- };
158
- if (name !== pgTypeName) return {
159
- name,
160
- map: pgTypeName
161
- };
162
- return { name };
163
- }
164
- /**
165
- * Simple English pluralization for back-relation field names.
166
- * Handles: s→ses, y→ies, default→s
167
- */
168
- function pluralize(word) {
169
- if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) return `${word}es`;
170
- if (word.endsWith("y") && !/[aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;
171
- return `${word}s`;
172
- }
173
- /**
174
- * Derives a relation field name from FK column names.
175
- *
176
- * For single-column FKs: strip _id/Id suffix, camelCase the result.
177
- * For composite FKs: use the referenced table name (lowercased, camelCased).
178
- */
179
- function deriveRelationFieldName(fkColumns, referencedTableName) {
180
- if (fkColumns.length === 1) {
181
- const [col = referencedTableName] = fkColumns;
182
- const stripped = col.replace(/_id$/i, "").replace(/Id$/, "");
183
- if (stripped.length > 0 && stripped !== col) return escapeIfNeeded(snakeToCamelCase(stripped));
184
- return escapeIfNeeded(snakeToCamelCase(referencedTableName));
185
- }
186
- return escapeIfNeeded(snakeToCamelCase(referencedTableName));
187
- }
188
- /**
189
- * Derives a back-relation field name.
190
- * For 1:N: pluralize the child model name (lowercased first char).
191
- * For 1:1: lowercase first char of child model name.
192
- */
193
- function deriveBackRelationFieldName(childModelName, isOneToOne) {
194
- const base = childModelName.charAt(0).toLowerCase() + childModelName.slice(1);
195
- return isOneToOne ? base : pluralize(base);
196
- }
197
- /**
198
- * Converts a column name to a named type name for the types block.
199
- * E.g., column "email" with type "character varying(255)" → "Email"
200
- */
201
- function toNamedTypeName(columnName) {
202
- let name;
203
- if (hasSeparators(columnName)) name = snakeToPascalCase(columnName);
204
- else name = columnName.charAt(0).toUpperCase() + columnName.slice(1);
205
- return escapeIfNeeded(name);
206
- }
207
-
208
- //#endregion
209
- //#region src/relation-inference.ts
210
- /**
211
- * Default referential actions — when the FK uses these, we omit them from the PSL output.
212
- */
213
- const DEFAULT_ON_DELETE = "noAction";
214
- const DEFAULT_ON_UPDATE = "noAction";
215
- /**
216
- * Maps SqlReferentialAction to PSL-compatible casing.
217
- */
218
- const REFERENTIAL_ACTION_PSL = {
219
- noAction: "NoAction",
220
- restrict: "Restrict",
221
- cascade: "Cascade",
222
- setNull: "SetNull",
223
- setDefault: "SetDefault"
224
- };
225
- /**
226
- * Infers relation fields from foreign keys across all tables.
227
- *
228
- * For each FK:
229
- * 1. Creates a relation field on the child table (the table with the FK)
230
- * 2. Creates a back-relation field on the parent table (the referenced table)
231
- * 3. Detects 1:1 vs 1:N cardinality
232
- * 4. Handles multiple FKs to the same parent (named relations)
233
- * 5. Handles self-referencing FKs
234
- */
235
- function inferRelations(tables, modelNameMap) {
236
- const relationsByTable = /* @__PURE__ */ new Map();
237
- const fkCountByPair = /* @__PURE__ */ new Map();
238
- for (const table of Object.values(tables)) for (const fk of table.foreignKeys) {
239
- const pairKey = `${table.name}→${fk.referencedTable}`;
240
- fkCountByPair.set(pairKey, (fkCountByPair.get(pairKey) ?? 0) + 1);
241
- }
242
- const usedFieldNames = /* @__PURE__ */ new Map();
243
- for (const table of Object.values(tables)) {
244
- const names = /* @__PURE__ */ new Set();
245
- for (const col of Object.values(table.columns)) names.add(col.name);
246
- usedFieldNames.set(table.name, names);
247
- }
248
- for (const table of Object.values(tables)) for (const fk of table.foreignKeys) {
249
- const childTableName = table.name;
250
- const parentTableName = fk.referencedTable;
251
- const childUsed = usedFieldNames.get(childTableName);
252
- const childModelName = modelNameMap.get(childTableName) ?? childTableName;
253
- const parentModelName = modelNameMap.get(parentTableName) ?? parentTableName;
254
- const pairKey = `${childTableName}→${parentTableName}`;
255
- const isSelfRelation = childTableName === parentTableName;
256
- const needsRelationName = fkCountByPair.get(pairKey) > 1 || isSelfRelation;
257
- const isOneToOne = detectOneToOne(fk, table);
258
- const childRelFieldName = resolveUniqueFieldName(deriveRelationFieldName(fk.columns, parentTableName), childUsed, parentModelName);
259
- const relationName = needsRelationName ? deriveRelationName(fk, childRelFieldName, parentModelName, isSelfRelation) : void 0;
260
- addRelationField(relationsByTable, childTableName, buildChildRelationField(childRelFieldName, parentModelName, fk, fk.columns.some((columnName) => table.columns[columnName]?.nullable ?? false), relationName));
261
- childUsed.add(childRelFieldName);
262
- const parentUsed = usedFieldNames.get(parentTableName) ?? /* @__PURE__ */ new Set();
263
- usedFieldNames.set(parentTableName, parentUsed);
264
- const backRelFieldName = resolveUniqueFieldName(deriveBackRelationFieldName(childModelName, isOneToOne), parentUsed, childModelName);
265
- addRelationField(relationsByTable, parentTableName, {
266
- fieldName: backRelFieldName,
267
- typeName: childModelName,
268
- optional: isOneToOne,
269
- list: !isOneToOne,
270
- relationName
271
- });
272
- parentUsed.add(backRelFieldName);
273
- }
274
- return { relationsByTable };
275
- }
276
- /**
277
- * Detects whether a FK represents a 1:1 relationship.
278
- * A FK is 1:1 if:
279
- * - The FK columns exactly match the table's PK columns, OR
280
- * - The FK columns exactly match a unique constraint
281
- */
282
- function detectOneToOne(fk, table) {
283
- const fkCols = [...fk.columns].sort();
284
- if (table.primaryKey) {
285
- const pkCols = [...table.primaryKey.columns].sort();
286
- if (pkCols.length === fkCols.length && pkCols.every((c, i) => c === fkCols[i])) return true;
287
- }
288
- for (const unique of table.uniques) {
289
- const uniqueCols = [...unique.columns].sort();
290
- if (uniqueCols.length === fkCols.length && uniqueCols.every((c, i) => c === fkCols[i])) return true;
291
- }
292
- return false;
293
- }
294
- /**
295
- * Derives a relation name for disambiguation.
296
- * Uses the FK constraint name if available, otherwise generates one from the relation shape.
297
- */
298
- function deriveRelationName(fk, childRelationFieldName, parentModelName, isSelfRelation) {
299
- if (fk.name) return fk.name;
300
- if (isSelfRelation) return `${childRelationFieldName.charAt(0).toUpperCase() + childRelationFieldName.slice(1)}${pluralize(parentModelName)}`;
301
- return fk.columns.join("_");
302
- }
303
- /**
304
- * Builds a child-side relation field with @relation attributes.
305
- */
306
- function buildChildRelationField(fieldName, parentModelName, fk, optional, relationName) {
307
- const onDelete = fk.onDelete && fk.onDelete !== DEFAULT_ON_DELETE ? fk.onDelete : void 0;
308
- const onUpdate = fk.onUpdate && fk.onUpdate !== DEFAULT_ON_UPDATE ? fk.onUpdate : void 0;
309
- return {
310
- fieldName,
311
- typeName: parentModelName,
312
- referencedTableName: fk.referencedTable,
313
- optional,
314
- list: false,
315
- relationName,
316
- fkName: fk.name,
317
- fields: fk.columns,
318
- references: fk.referencedColumns,
319
- onDelete: onDelete ? REFERENTIAL_ACTION_PSL[onDelete] : void 0,
320
- onUpdate: onUpdate ? REFERENTIAL_ACTION_PSL[onUpdate] : void 0
321
- };
322
- }
323
- /**
324
- * Resolves a unique field name by appending the model name if there's a collision.
325
- */
326
- function resolveUniqueFieldName(desired, usedNames, fallbackSuffix) {
327
- if (!usedNames.has(desired)) return desired;
328
- const withSuffix = `${desired}${fallbackSuffix}`;
329
- if (!usedNames.has(withSuffix)) return withSuffix;
330
- let counter = 2;
331
- while (usedNames.has(`${desired}${counter}`)) counter++;
332
- return `${desired}${counter}`;
333
- }
334
- function addRelationField(map, tableName, field) {
335
- const existing = map.get(tableName);
336
- if (existing) existing.push(field);
337
- else map.set(tableName, [field]);
338
- }
339
-
340
- //#endregion
341
- //#region src/print-psl.ts
342
- const DEFAULT_HEADER = "// This file was introspected from the database. Do not edit manually.";
343
- /**
344
- * Escapes a string for use inside a PSL double-quoted context (e.g., @map("..."), @relation(name: "...")).
345
- * Prevents malformed PSL when database identifiers contain `"` or newlines.
346
- */
347
- function escapePslString(value) {
348
- return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
349
- }
1
+ //#region src/serialize-print-document.ts
350
2
  const PSL_IDENTIFIER_PATTERN = /^[A-Za-z_]\w*$/;
351
3
  const ENUM_MEMBER_RESERVED_WORDS = new Set([
352
4
  "datasource",
@@ -357,377 +9,19 @@ const ENUM_MEMBER_RESERVED_WORDS = new Set([
357
9
  "type",
358
10
  "types"
359
11
  ]);
360
- const PSL_SCALAR_TYPE_NAMES = new Set([
361
- "String",
362
- "Boolean",
363
- "Int",
364
- "BigInt",
365
- "Float",
366
- "Decimal",
367
- "DateTime",
368
- "Json",
369
- "Bytes"
370
- ]);
371
- /**
372
- * Converts a SqlSchemaIR to a PSL (Prisma Schema Language) string.
373
- *
374
- * The output follows PSL formatting conventions:
375
- * - Header comment
376
- * - `types` block (if parameterized types exist)
377
- * - `enum` blocks (alphabetical)
378
- * - `model` blocks (topologically sorted by FK deps, alphabetical fallback)
379
- *
380
- * @param schemaIR - The introspected schema IR
381
- * @param options - Printer configuration (type map, header)
382
- * @returns A valid PSL string
383
- */
384
- function printPsl(schemaIR, options) {
385
- const { typeMap, header, defaultMapping, enumInfo, parseRawDefault } = options;
386
- const headerComment = header ?? DEFAULT_HEADER;
387
- const { typeNames: enumTypeNames, definitions: enumDefinitions } = enumInfo ?? {
388
- typeNames: /* @__PURE__ */ new Set(),
389
- definitions: /* @__PURE__ */ new Map()
390
- };
391
- const modelNames = buildTopLevelNameMap(Object.keys(schemaIR.tables), toModelName, "model", "table");
392
- const enumNames = buildTopLevelNameMap(enumTypeNames, toEnumName, "enum", "enum type");
393
- assertNoCrossKindNameCollisions(modelNames, enumNames);
394
- const modelNameMap = new Map([...modelNames].map(([tableName, result]) => [tableName, result.name]));
395
- const enumNameMap = new Map([...enumNames].map(([pgTypeName, result]) => [pgTypeName, result.name]));
396
- const reservedNamedTypeNames = createReservedNamedTypeNames(modelNames, enumNames);
397
- const fieldNamesByTable = buildFieldNamesByTable(schemaIR.tables);
398
- const { relationsByTable } = inferRelations(schemaIR.tables, modelNameMap);
399
- const namedTypes = seedNamedTypeRegistry(schemaIR, typeMap, enumNameMap, reservedNamedTypeNames);
400
- const models = [];
401
- for (const table of Object.values(schemaIR.tables)) {
402
- const model = processTable(table, typeMap, enumNameMap, fieldNamesByTable, namedTypes, defaultMapping, parseRawDefault, relationsByTable.get(table.name) ?? []);
403
- models.push(model);
404
- }
405
- const enums = [];
406
- for (const [pgTypeName, values] of enumDefinitions) {
407
- const enumName = enumNames.get(pgTypeName);
408
- enums.push({
409
- name: enumName.name,
410
- mapName: enumName.map,
411
- values
412
- });
413
- }
414
- enums.sort((a, b) => a.name.localeCompare(b.name));
415
- const sortedModels = topologicalSort(models, schemaIR.tables, modelNameMap);
12
+ function escapePslString(value) {
13
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
14
+ }
15
+ function serializePrintDocument(doc) {
416
16
  const sections = [];
417
- sections.push(headerComment);
418
- const namedTypeEntries = [...namedTypes.entriesByKey.values()].sort((a, b) => a.name.localeCompare(b.name));
17
+ sections.push(doc.headerComment);
18
+ const namedTypeEntries = [...doc.namedTypes].sort((a, b) => a.name.localeCompare(b.name));
419
19
  if (namedTypeEntries.length > 0) sections.push(serializeTypesBlock(namedTypeEntries));
420
- for (const e of enums) sections.push(serializeEnum(e));
421
- for (const model of sortedModels) sections.push(serializeModel(model));
20
+ const enumsSorted = [...doc.enums].sort((a, b) => a.name.localeCompare(b.name));
21
+ for (const e of enumsSorted) sections.push(serializeEnum(e));
22
+ for (const model of doc.models) sections.push(serializeModel(model));
422
23
  return `${sections.join("\n\n")}\n`;
423
24
  }
424
- /**
425
- * Processes a SQL table into a PrinterModel.
426
- */
427
- function processTable(table, typeMap, enumNameMap, fieldNamesByTable, namedTypes, defaultMapping, rawDefaultParser, relationFields) {
428
- const { name: modelName, map: mapName } = toModelName(table.name);
429
- const fieldNameMap = fieldNamesByTable.get(table.name);
430
- const pkColumns = new Set(table.primaryKey?.columns ?? []);
431
- const isSinglePk = pkColumns.size === 1;
432
- const singlePkConstraintName = isSinglePk ? table.primaryKey?.name : void 0;
433
- const uniqueColumns = /* @__PURE__ */ new Map();
434
- for (const unique of table.uniques) if (unique.columns.length === 1) {
435
- const [columnName = ""] = unique.columns;
436
- const existingConstraintName = uniqueColumns.get(columnName);
437
- if (!uniqueColumns.has(columnName) || existingConstraintName === void 0 && unique.name) uniqueColumns.set(columnName, unique.name);
438
- }
439
- const fields = [];
440
- const columnEntries = Object.values(table.columns);
441
- for (const column of columnEntries) {
442
- const resolvedField = fieldNameMap?.get(column.name);
443
- const fieldName = resolvedField?.fieldName ?? toFieldName(column.name).name;
444
- const fieldMap = resolvedField?.fieldMap;
445
- const resolution = typeMap.resolve(column.nativeType, table.annotations);
446
- if ("unsupported" in resolution) {
447
- fields.push({
448
- name: fieldName,
449
- typeName: `Unsupported("${escapePslString(resolution.nativeType)}")`,
450
- optional: column.nullable,
451
- list: false,
452
- attributes: fieldMap ? [`@map("${escapePslString(fieldMap)}")`] : [],
453
- mapName: fieldMap ?? void 0,
454
- isId: false,
455
- isRelation: false,
456
- isUnsupported: true
457
- });
458
- continue;
459
- }
460
- let typeName = resolution.pslType;
461
- const enumPslName = enumNameMap.get(column.nativeType);
462
- if (enumPslName) typeName = enumPslName;
463
- if (resolution.nativeTypeAttribute && !enumPslName) typeName = resolveNamedTypeName(namedTypes, resolution);
464
- const attributes = [];
465
- const isId = isSinglePk && pkColumns.has(column.name);
466
- if (isId) attributes.push(formatFieldConstraintAttribute("@id", singlePkConstraintName));
467
- let comment;
468
- if (column.default !== void 0) {
469
- const parsed = parseDefaultIfNeeded(column.default, column.nativeType, rawDefaultParser);
470
- if (parsed) {
471
- const result = mapDefault(parsed, defaultMapping);
472
- if ("attribute" in result) attributes.push(result.attribute);
473
- else comment = result.comment;
474
- }
475
- }
476
- const uniqueConstraintName = uniqueColumns.get(column.name);
477
- if (uniqueConstraintName !== void 0 || uniqueColumns.has(column.name)) {
478
- if (!isId) attributes.push(formatFieldConstraintAttribute("@unique", uniqueConstraintName));
479
- }
480
- if (fieldMap) attributes.push(`@map("${escapePslString(fieldMap)}")`);
481
- fields.push({
482
- name: fieldName,
483
- typeName,
484
- optional: column.nullable,
485
- list: false,
486
- attributes,
487
- mapName: fieldMap ?? void 0,
488
- isId,
489
- isRelation: false,
490
- isUnsupported: false,
491
- comment
492
- });
493
- }
494
- const usedFieldNames = new Set(fields.map((field) => field.name));
495
- for (const rel of relationFields) {
496
- const relationFieldName = createUniqueFieldName(rel.fieldName, usedFieldNames);
497
- const relAttributes = [];
498
- if (rel.fields && rel.references) {
499
- const parts = [];
500
- if (rel.relationName) parts.push(`name: "${escapePslString(rel.relationName)}"`);
501
- parts.push(`fields: [${rel.fields.map((fieldName) => resolveColumnFieldName(fieldNamesByTable, table.name, fieldName)).join(", ")}]`);
502
- parts.push(`references: [${rel.references.map((fieldName) => resolveColumnFieldName(fieldNamesByTable, rel.referencedTableName ?? "", fieldName)).join(", ")}]`);
503
- if (rel.onDelete) parts.push(`onDelete: ${rel.onDelete}`);
504
- if (rel.onUpdate) parts.push(`onUpdate: ${rel.onUpdate}`);
505
- if (rel.fkName) parts.push(`map: "${escapePslString(rel.fkName)}"`);
506
- relAttributes.push(`@relation(${parts.join(", ")})`);
507
- } else if (rel.relationName) relAttributes.push(`@relation(name: "${escapePslString(rel.relationName)}")`);
508
- fields.push({
509
- name: relationFieldName,
510
- typeName: rel.typeName,
511
- optional: rel.optional,
512
- list: rel.list,
513
- attributes: relAttributes,
514
- isId: false,
515
- isRelation: true,
516
- isUnsupported: false
517
- });
518
- usedFieldNames.add(relationFieldName);
519
- }
520
- const modelAttributes = [];
521
- if (table.primaryKey && table.primaryKey.columns.length > 1) {
522
- const pkFieldNames = table.primaryKey.columns.map((columnName) => resolveColumnFieldName(fieldNamesByTable, table.name, columnName));
523
- modelAttributes.push(formatModelConstraintAttribute("@@id", pkFieldNames, table.primaryKey.name));
524
- }
525
- for (const unique of table.uniques) if (unique.columns.length > 1) {
526
- const fieldNames = unique.columns.map((columnName) => resolveColumnFieldName(fieldNamesByTable, table.name, columnName));
527
- modelAttributes.push(formatModelConstraintAttribute("@@unique", fieldNames, unique.name));
528
- }
529
- for (const index of table.indexes) if (!index.unique) {
530
- const fieldNames = index.columns.map((columnName) => resolveColumnFieldName(fieldNamesByTable, table.name, columnName));
531
- modelAttributes.push(formatModelConstraintAttribute("@@index", fieldNames, index.name));
532
- }
533
- if (mapName) modelAttributes.push(`@@map("${escapePslString(mapName)}")`);
534
- const tableComment = !table.primaryKey ? "// WARNING: This table has no primary key in the database" : void 0;
535
- return {
536
- name: modelName,
537
- mapName: mapName ?? void 0,
538
- fields,
539
- modelAttributes,
540
- comment: tableComment
541
- };
542
- }
543
- function parseDefaultIfNeeded(value, nativeType, rawDefaultParser) {
544
- if (typeof value === "string") return rawDefaultParser ? rawDefaultParser(value, nativeType) : void 0;
545
- return value;
546
- }
547
- function formatFieldConstraintAttribute(attribute, constraintName) {
548
- return constraintName ? `${attribute}(map: "${escapePslString(constraintName)}")` : attribute;
549
- }
550
- function formatModelConstraintAttribute(attribute, fields, constraintName) {
551
- const parts = [`[${fields.join(", ")}]`];
552
- if (constraintName) parts.push(`map: "${escapePslString(constraintName)}"`);
553
- return `${attribute}(${parts.join(", ")})`;
554
- }
555
- function buildFieldNamesByTable(tables) {
556
- const fieldNamesByTable = /* @__PURE__ */ new Map();
557
- for (const table of Object.values(tables)) {
558
- const assignmentOrder = [...Object.values(table.columns).map((column, index) => {
559
- const { name, map } = toFieldName(column.name);
560
- return {
561
- columnName: column.name,
562
- desiredFieldName: name,
563
- fieldMap: map,
564
- index
565
- };
566
- })].sort((left, right) => {
567
- const mapComparison = Number(left.fieldMap !== void 0) - Number(right.fieldMap !== void 0);
568
- if (mapComparison !== 0) return mapComparison;
569
- return left.index - right.index;
570
- });
571
- const usedFieldNames = /* @__PURE__ */ new Set();
572
- const tableFieldNames = /* @__PURE__ */ new Map();
573
- for (const column of assignmentOrder) {
574
- const fieldName = createUniqueFieldName(column.desiredFieldName, usedFieldNames);
575
- usedFieldNames.add(fieldName);
576
- tableFieldNames.set(column.columnName, {
577
- fieldName,
578
- fieldMap: column.fieldMap
579
- });
580
- }
581
- fieldNamesByTable.set(table.name, tableFieldNames);
582
- }
583
- return fieldNamesByTable;
584
- }
585
- function resolveColumnFieldName(fieldNamesByTable, tableName, columnName) {
586
- return fieldNamesByTable.get(tableName)?.get(columnName)?.fieldName ?? toFieldName(columnName).name;
587
- }
588
- function createUniqueFieldName(desiredName, usedFieldNames) {
589
- if (!usedFieldNames.has(desiredName)) return desiredName;
590
- let counter = 2;
591
- while (usedFieldNames.has(`${desiredName}${counter}`)) counter++;
592
- return `${desiredName}${counter}`;
593
- }
594
- function buildTopLevelNameMap(sources, normalize, kind, sourceKind) {
595
- const results = /* @__PURE__ */ new Map();
596
- const normalizedToSources = /* @__PURE__ */ new Map();
597
- for (const source of sources) {
598
- const normalized = normalize(source);
599
- results.set(source, normalized);
600
- normalizedToSources.set(normalized.name, [...normalizedToSources.get(normalized.name) ?? [], source]);
601
- }
602
- const duplicates = [...normalizedToSources.entries()].filter(([, conflictingSources]) => conflictingSources.length > 1);
603
- if (duplicates.length > 0) {
604
- const details = duplicates.map(([normalizedName, conflictingSources]) => `- ${kind} "${normalizedName}" from ${sourceKind}s ${conflictingSources.map((source) => `"${source}"`).join(", ")}`);
605
- throw new Error(`PSL ${kind} name collisions detected:\n${details.join("\n")}`);
606
- }
607
- return results;
608
- }
609
- function assertNoCrossKindNameCollisions(modelNames, enumNames) {
610
- const enumSourceByName = new Map([...enumNames].map(([source, result]) => [result.name, source]));
611
- const collisions = [...modelNames.entries()].map(([tableName, result]) => {
612
- const enumSource = enumSourceByName.get(result.name);
613
- return enumSource ? `- identifier "${result.name}" from table "${tableName}" collides with enum type "${enumSource}"` : void 0;
614
- }).filter((detail) => detail !== void 0);
615
- if (collisions.length > 0) throw new Error(`PSL top-level name collisions detected:\n${collisions.join("\n")}`);
616
- }
617
- function createReservedNamedTypeNames(modelNames, enumNames) {
618
- const reservedNames = new Set(PSL_SCALAR_TYPE_NAMES);
619
- for (const result of modelNames.values()) reservedNames.add(result.name);
620
- for (const result of enumNames.values()) reservedNames.add(result.name);
621
- return reservedNames;
622
- }
623
- function seedNamedTypeRegistry(schemaIR, typeMap, enumNameMap, reservedNames) {
624
- const seeds = /* @__PURE__ */ new Map();
625
- for (const tableName of Object.keys(schemaIR.tables).sort()) {
626
- const table = schemaIR.tables[tableName];
627
- if (!table) continue;
628
- for (const columnName of Object.keys(table.columns).sort()) {
629
- const column = table.columns[columnName];
630
- if (!column) continue;
631
- const resolution = typeMap.resolve(column.nativeType, table.annotations);
632
- if ("unsupported" in resolution || enumNameMap.has(column.nativeType) || !resolution.nativeTypeAttribute) continue;
633
- const signatureKey = createNamedTypeSignatureKey(resolution);
634
- if (!seeds.has(signatureKey)) seeds.set(signatureKey, {
635
- baseType: resolution.pslType,
636
- desiredName: toNamedTypeName(column.name),
637
- attributes: [renderNativeTypeAttribute(resolution.nativeTypeAttribute)]
638
- });
639
- }
640
- }
641
- const registry = {
642
- entriesByKey: /* @__PURE__ */ new Map(),
643
- usedNames: new Set(reservedNames)
644
- };
645
- const sortedSeeds = [...seeds.entries()].sort((left, right) => {
646
- const desiredNameComparison = left[1].desiredName.localeCompare(right[1].desiredName);
647
- if (desiredNameComparison !== 0) return desiredNameComparison;
648
- return left[0].localeCompare(right[0]);
649
- });
650
- for (const [signatureKey, seed] of sortedSeeds) {
651
- const name = createUniqueFieldName(seed.desiredName, registry.usedNames);
652
- registry.entriesByKey.set(signatureKey, {
653
- name,
654
- baseType: seed.baseType,
655
- attributes: seed.attributes
656
- });
657
- registry.usedNames.add(name);
658
- }
659
- return registry;
660
- }
661
- function resolveNamedTypeName(registry, resolution) {
662
- const key = createNamedTypeSignatureKey(resolution);
663
- const existing = registry.entriesByKey.get(key);
664
- if (existing) return existing.name;
665
- throw new Error(`Named type registry was not seeded for native type "${resolution.nativeType}"`);
666
- }
667
- function createNamedTypeSignatureKey(resolution) {
668
- return JSON.stringify({
669
- baseType: resolution.pslType,
670
- nativeTypeAttribute: resolution.nativeTypeAttribute ? {
671
- name: resolution.nativeTypeAttribute.name,
672
- args: resolution.nativeTypeAttribute.args ?? null
673
- } : null
674
- });
675
- }
676
- function isNormalizedEnumMemberReservedWord(value) {
677
- return ENUM_MEMBER_RESERVED_WORDS.has(value.toLowerCase());
678
- }
679
- function normalizeEnumMemberName(value, usedNames) {
680
- return createUniqueFieldName(PSL_IDENTIFIER_PATTERN.test(value) && !isNormalizedEnumMemberReservedWord(value) ? value : createNormalizedEnumMemberBaseName(value), usedNames);
681
- }
682
- function createNormalizedEnumMemberBaseName(value) {
683
- const tokens = value.match(/[A-Za-z0-9]+/g)?.map((token) => token.toLowerCase()) ?? [];
684
- let normalized = tokens[0] ?? "value";
685
- for (const token of tokens.slice(1)) normalized += token.charAt(0).toUpperCase() + token.slice(1);
686
- if (isNormalizedEnumMemberReservedWord(normalized) || /^\d/.test(normalized)) normalized = `_${normalized}`;
687
- return normalized;
688
- }
689
- /**
690
- * Topologically sorts models by FK dependencies.
691
- * Parent tables (those referenced by FKs) come before child tables.
692
- * Alphabetical fallback for cycles.
693
- */
694
- function topologicalSort(models, tables, modelNameMap) {
695
- const modelByName = /* @__PURE__ */ new Map();
696
- for (const model of models) modelByName.set(model.name, model);
697
- const deps = /* @__PURE__ */ new Map();
698
- const tableToModel = /* @__PURE__ */ new Map();
699
- for (const tableName of Object.keys(tables)) {
700
- const modelName = modelNameMap.get(tableName);
701
- tableToModel.set(tableName, modelName);
702
- deps.set(modelName, /* @__PURE__ */ new Set());
703
- }
704
- for (const [tableName, table] of Object.entries(tables)) {
705
- const modelName = tableToModel.get(tableName);
706
- for (const fk of table.foreignKeys) {
707
- const refModelName = tableToModel.get(fk.referencedTable);
708
- if (refModelName && refModelName !== modelName) deps.get(modelName).add(refModelName);
709
- }
710
- }
711
- const result = [];
712
- const visited = /* @__PURE__ */ new Set();
713
- const visiting = /* @__PURE__ */ new Set();
714
- const sortedNames = [...deps.keys()].sort();
715
- function visit(name) {
716
- if (visited.has(name)) return;
717
- if (visiting.has(name)) return;
718
- visiting.add(name);
719
- const sortedDeps = [...deps.get(name)].sort();
720
- for (const dep of sortedDeps) visit(dep);
721
- visiting.delete(name);
722
- visited.add(name);
723
- result.push(modelByName.get(name));
724
- }
725
- for (const name of sortedNames) visit(name);
726
- return result;
727
- }
728
- /**
729
- * Serializes the `types` block.
730
- */
731
25
  function serializeTypesBlock(namedTypes) {
732
26
  const lines = ["types {"];
733
27
  for (const nt of namedTypes) {
@@ -737,19 +31,15 @@ function serializeTypesBlock(namedTypes) {
737
31
  lines.push("}");
738
32
  return lines.join("\n");
739
33
  }
740
- function renderNativeTypeAttribute(attribute) {
741
- if (!attribute.args || attribute.args.length === 0) return `@${attribute.name}`;
742
- return `@${attribute.name}(${attribute.args.join(", ")})`;
743
- }
744
- /**
745
- * Serializes an enum block.
746
- */
747
34
  function serializeEnum(e) {
748
35
  const lines = [`enum ${e.name} {`];
749
36
  const usedNames = /* @__PURE__ */ new Set();
750
37
  for (const value of e.values) {
751
- const memberName = normalizeEnumMemberName(value, usedNames);
752
- lines.push(` ${memberName}`);
38
+ const memberName = normalizeEnumMemberName(value.name, usedNames);
39
+ const explicitMap = value.mapName;
40
+ const storageLabel = explicitMap !== void 0 ? explicitMap : memberName !== value.name ? value.name : void 0;
41
+ if (storageLabel !== void 0) lines.push(` ${memberName} @map("${escapePslString(storageLabel)}")`);
42
+ else lines.push(` ${memberName}`);
753
43
  usedNames.add(memberName);
754
44
  }
755
45
  if (e.mapName) {
@@ -759,9 +49,6 @@ function serializeEnum(e) {
759
49
  lines.push("}");
760
50
  return lines.join("\n");
761
51
  }
762
- /**
763
- * Serializes a model block with column-aligned fields.
764
- */
765
52
  function serializeModel(model) {
766
53
  const lines = [];
767
54
  if (model.comment) lines.push(model.comment);
@@ -793,175 +80,220 @@ function serializeModel(model) {
793
80
  lines.push("}");
794
81
  return lines.join("\n");
795
82
  }
796
- /**
797
- * Formats a field type string with optional/list modifiers.
798
- */
799
83
  function formatFieldType(field) {
800
84
  let type = field.typeName;
801
85
  if (field.list) type += "[]";
802
86
  else if (field.optional) type += "?";
803
87
  return type;
804
88
  }
805
-
806
- //#endregion
807
- //#region src/schema-validation.ts
808
- const REFERENTIAL_ACTIONS = new Set([
809
- "noAction",
810
- "restrict",
811
- "cascade",
812
- "setNull",
813
- "setDefault"
814
- ]);
815
- function validatePrintableSqlSchemaIR(value) {
816
- const root = expectRecord(value, "schema");
817
- return {
818
- tables: validateTables(root["tables"], "schema.tables"),
819
- dependencies: validateDependencies(root["dependencies"], "schema.dependencies"),
820
- ...ifDefined("annotations", validateAnnotations(root["annotations"], "schema.annotations"))
821
- };
89
+ function createUniqueFieldName(desiredName, usedFieldNames) {
90
+ if (!usedFieldNames.has(desiredName)) return desiredName;
91
+ let counter = 2;
92
+ while (usedFieldNames.has(`${desiredName}${counter}`)) counter++;
93
+ return `${desiredName}${counter}`;
822
94
  }
823
- function validateTables(value, path) {
824
- const tables = expectRecord(value, path);
825
- const validated = {};
826
- for (const [tableName, tableValue] of Object.entries(tables)) {
827
- const tablePath = `${path}.${tableName}`;
828
- const table = expectRecord(tableValue, tablePath);
829
- validated[tableName] = {
830
- name: expectString(table["name"], `${tablePath}.name`),
831
- columns: validateColumns(table["columns"], `${tablePath}.columns`),
832
- foreignKeys: validateForeignKeys(table["foreignKeys"], `${tablePath}.foreignKeys`),
833
- uniques: validateUniques(table["uniques"], `${tablePath}.uniques`),
834
- indexes: validateIndexes(table["indexes"], `${tablePath}.indexes`),
835
- ...ifDefined("primaryKey", validatePrimaryKey(table["primaryKey"], `${tablePath}.primaryKey`)),
836
- ...ifDefined("annotations", validateAnnotations(table["annotations"], `${tablePath}.annotations`))
837
- };
838
- }
839
- return validated;
95
+ function isNormalizedEnumMemberReservedWord(value) {
96
+ return ENUM_MEMBER_RESERVED_WORDS.has(value.toLowerCase());
840
97
  }
841
- function validateColumns(value, path) {
842
- const columns = expectRecord(value, path);
843
- const validated = {};
844
- for (const [columnName, columnValue] of Object.entries(columns)) {
845
- const columnPath = `${path}.${columnName}`;
846
- const column = expectRecord(columnValue, columnPath);
847
- validated[columnName] = {
848
- name: expectString(column["name"], `${columnPath}.name`),
849
- nativeType: expectString(column["nativeType"], `${columnPath}.nativeType`),
850
- nullable: expectBoolean(column["nullable"], `${columnPath}.nullable`),
851
- ...ifDefined("default", validateColumnDefault(column["default"], `${columnPath}.default`)),
852
- ...ifDefined("annotations", validateAnnotations(column["annotations"], `${columnPath}.annotations`))
853
- };
854
- }
855
- return validated;
98
+ function normalizeEnumMemberName(value, usedNames) {
99
+ return createUniqueFieldName(PSL_IDENTIFIER_PATTERN.test(value) && !isNormalizedEnumMemberReservedWord(value) ? value : createNormalizedEnumMemberBaseName(value), usedNames);
856
100
  }
857
- function validatePrimaryKey(value, path) {
858
- if (value === void 0) return;
859
- const primaryKey = expectRecord(value, path);
101
+ function createNormalizedEnumMemberBaseName(value) {
102
+ const tokens = value.match(/[A-Za-z0-9]+/g)?.map((token) => token.toLowerCase()) ?? [];
103
+ let normalized = tokens[0] ?? "value";
104
+ for (const token of tokens.slice(1)) normalized += token.charAt(0).toUpperCase() + token.slice(1);
105
+ if (isNormalizedEnumMemberReservedWord(normalized) || /^\d/.test(normalized)) normalized = `_${normalized}`;
106
+ return normalized;
107
+ }
108
+ //#endregion
109
+ //#region src/ast-to-print-document.ts
110
+ const DEFAULT_AST_PRINT_HEADER = "// Contract inferred from the live database schema. Edit as needed, then run `prisma-next contract emit`.";
111
+ function astDocumentToPrintDocument(ast) {
112
+ const modelNames = new Set(ast.models.map((m) => m.name));
113
+ const deps = buildModelFkDeps(ast.models, modelNames);
114
+ const sortedModels = topologicalSortModels(ast.models, deps);
115
+ const namedTypes = ast.types ? ast.types.declarations.map(namedTypeDeclarationToPrinterNamedType) : [];
116
+ const enums = ast.enums.map(enumToPrinterEnum);
117
+ const printerModels = sortedModels.map((m) => modelToPrinterModel(m));
860
118
  return {
861
- columns: validateStringArray(primaryKey["columns"], `${path}.columns`),
862
- ...ifDefined("name", validateOptionalString(primaryKey["name"], `${path}.name`))
119
+ headerComment: DEFAULT_AST_PRINT_HEADER,
120
+ namedTypes,
121
+ enums: enums.map((e) => ({
122
+ name: e.name,
123
+ mapName: e.mapName,
124
+ values: e.values
125
+ })),
126
+ models: printerModels
863
127
  };
864
128
  }
865
- function validateForeignKeys(value, path) {
866
- return expectArray(value, path).map((foreignKey, index) => {
867
- const foreignKeyPath = `${path}[${index}]`;
868
- const record = expectRecord(foreignKey, foreignKeyPath);
869
- return {
870
- columns: validateStringArray(record["columns"], `${foreignKeyPath}.columns`),
871
- referencedTable: expectString(record["referencedTable"], `${foreignKeyPath}.referencedTable`),
872
- referencedColumns: validateStringArray(record["referencedColumns"], `${foreignKeyPath}.referencedColumns`),
873
- ...ifDefined("name", validateOptionalString(record["name"], `${foreignKeyPath}.name`)),
874
- ...ifDefined("onDelete", validateReferentialAction(record["onDelete"], `${foreignKeyPath}.onDelete`)),
875
- ...ifDefined("onUpdate", validateReferentialAction(record["onUpdate"], `${foreignKeyPath}.onUpdate`)),
876
- ...ifDefined("annotations", validateAnnotations(record["annotations"], `${foreignKeyPath}.annotations`))
877
- };
878
- });
879
- }
880
- function validateUniques(value, path) {
881
- return expectArray(value, path).map((uniqueValue, index) => {
882
- const uniquePath = `${path}[${index}]`;
883
- const unique = expectRecord(uniqueValue, uniquePath);
884
- return {
885
- columns: validateStringArray(unique["columns"], `${uniquePath}.columns`),
886
- ...ifDefined("name", validateOptionalString(unique["name"], `${uniquePath}.name`)),
887
- ...ifDefined("annotations", validateAnnotations(unique["annotations"], `${uniquePath}.annotations`))
888
- };
889
- });
129
+ function renderPslAttribute(attr) {
130
+ const prefix = attr.target === "model" || attr.target === "enum" ? "@@" : "@";
131
+ if (attr.args.length === 0) return `${prefix}${attr.name}`;
132
+ const inner = attr.args.map(renderAttributeArgument).join(", ");
133
+ return `${prefix}${attr.name}(${inner})`;
890
134
  }
891
- function validateIndexes(value, path) {
892
- return expectArray(value, path).map((indexValue, index) => {
893
- const indexPath = `${path}[${index}]`;
894
- const record = expectRecord(indexValue, indexPath);
895
- return {
896
- columns: validateStringArray(record["columns"], `${indexPath}.columns`),
897
- unique: expectBoolean(record["unique"], `${indexPath}.unique`),
898
- ...ifDefined("name", validateOptionalString(record["name"], `${indexPath}.name`)),
899
- ...ifDefined("annotations", validateAnnotations(record["annotations"], `${indexPath}.annotations`))
900
- };
901
- });
135
+ function renderAttributeArgument(arg) {
136
+ if (arg.kind === "positional") return arg.value;
137
+ return `${arg.name}: ${arg.value}`;
902
138
  }
903
- function validateDependencies(value, path) {
904
- return expectArray(value, path).map((dependencyValue, index) => {
905
- const dependencyPath = `${path}[${index}]`;
906
- return { id: expectString(expectRecord(dependencyValue, dependencyPath)["id"], `${dependencyPath}.id`) };
907
- });
139
+ function namedTypeDeclarationToPrinterNamedType(decl) {
140
+ const base = decl.baseType ?? (decl.typeConstructor !== void 0 ? formatTypeConstructor(decl.typeConstructor) : "");
141
+ const attributes = decl.attributes.map(renderPslAttribute);
142
+ return {
143
+ name: decl.name,
144
+ baseType: base,
145
+ attributes
146
+ };
908
147
  }
909
- function validateAnnotations(value, path) {
910
- if (value === void 0) return;
911
- return expectRecord(value, path);
148
+ function formatTypeConstructor(tc) {
149
+ const path = tc.path.join(".");
150
+ if (tc.args.length === 0) return path;
151
+ return `${path}(${tc.args.map(renderAttributeArgument).join(", ")})`;
912
152
  }
913
- function validateColumnDefault(value, path) {
914
- if (value === void 0) return;
915
- if (typeof value === "string") return value;
916
- const columnDefault = expectRecord(value, path);
917
- const kind = expectString(columnDefault["kind"], `${path}.kind`);
918
- if (kind === "literal") {
919
- if (!Object.hasOwn(columnDefault, "value")) throw new Error(`${path}.value must be present for literal defaults`);
920
- return {
921
- kind: "literal",
922
- value: columnDefault["value"]
923
- };
153
+ function enumToPrinterEnum(en) {
154
+ let mapName;
155
+ for (const a of en.attributes) if (a.name === "map" && a.target === "enum") {
156
+ const quoted = getPositionalStringArg(a, 0);
157
+ if (quoted !== void 0) mapName = quoted;
924
158
  }
925
- if (kind === "function") return {
926
- kind: "function",
927
- expression: expectString(columnDefault["expression"], `${path}.expression`)
159
+ return {
160
+ name: en.name,
161
+ mapName,
162
+ values: en.values.map((v) => ({
163
+ name: v.name,
164
+ ...v.mapName !== void 0 ? { mapName: v.mapName } : {}
165
+ }))
928
166
  };
929
- throw new Error(`${path}.kind must be "literal" or "function"`);
930
- }
931
- function validateReferentialAction(value, path) {
932
- if (value === void 0) return;
933
- const action = expectString(value, path);
934
- if (!REFERENTIAL_ACTIONS.has(action)) throw new Error(`${path} must be one of ${[...REFERENTIAL_ACTIONS].map((item) => `"${item}"`).join(", ")}`);
935
- return action;
936
167
  }
937
- function validateStringArray(value, path) {
938
- return expectArray(value, path).map((item, index) => expectString(item, `${path}[${index}]`));
168
+ function getPositionalStringArg(attr, index) {
169
+ const raw = attr.args.filter((a) => a.kind === "positional")[index]?.value.trim();
170
+ if (!raw) return void 0;
171
+ const m = raw.match(/^(['"])(.*)\1$/);
172
+ if (!m) return void 0;
173
+ return unescapePslString(m[2]);
939
174
  }
940
- function validateOptionalString(value, path) {
941
- if (value === void 0) return;
942
- return expectString(value, path);
943
- }
944
- function expectRecord(value, path) {
945
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
946
- return value;
175
+ /**
176
+ * Inverse of `escapePslString`. The parser stores quoted-literal arguments with
177
+ * their PSL escape sequences (`\\`, `\"`, `\n`, `\r`) intact; when we round-trip
178
+ * a value through `getPositionalStringArg` and re-render via `escapePslString`,
179
+ * we must decode it once on extraction to avoid double-escaping the same
180
+ * sequences on output.
181
+ */
182
+ function unescapePslString(value) {
183
+ let result = "";
184
+ for (let i = 0; i < value.length; i++) {
185
+ if (value.charCodeAt(i) !== 92 || i + 1 >= value.length) {
186
+ result += value[i];
187
+ continue;
188
+ }
189
+ const next = value[i + 1];
190
+ if (next === "\\" || next === "\"" || next === "'") result += next;
191
+ else if (next === "n") result += "\n";
192
+ else if (next === "r") result += "\r";
193
+ else {
194
+ result += "\\";
195
+ result += next;
196
+ }
197
+ i++;
198
+ }
199
+ return result;
947
200
  }
948
- function expectArray(value, path) {
949
- if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
950
- return value;
201
+ function modelToPrinterModel(model) {
202
+ let mapName;
203
+ const modelAttrStrings = [];
204
+ for (const a of model.attributes) {
205
+ if (a.name === "map" && a.target === "model") {
206
+ mapName = getPositionalStringArg(a, 0) ?? mapName;
207
+ continue;
208
+ }
209
+ modelAttrStrings.push(renderPslAttribute(a));
210
+ }
211
+ if (mapName !== void 0) modelAttrStrings.push(`@@map("${escapePslString(mapName)}")`);
212
+ const printerFields = model.fields.map((f) => fieldToPrinterField(f));
213
+ return {
214
+ name: model.name,
215
+ mapName,
216
+ fields: printerFields,
217
+ modelAttributes: modelAttrStrings,
218
+ comment: model.comment
219
+ };
951
220
  }
952
- function expectString(value, path) {
953
- if (typeof value !== "string") throw new Error(`${path} must be a string`);
954
- return value;
221
+ function fieldToPrinterField(field) {
222
+ const typeName = field.typeConstructor !== void 0 ? formatTypeConstructor(field.typeConstructor) : field.typeName;
223
+ let mapName;
224
+ const attrStrings = [];
225
+ for (const a of field.attributes) {
226
+ if (a.name === "map" && a.target === "field") {
227
+ mapName = getPositionalStringArg(a, 0) ?? mapName;
228
+ continue;
229
+ }
230
+ attrStrings.push(renderPslAttribute(a));
231
+ }
232
+ if (mapName !== void 0) attrStrings.push(`@map("${escapePslString(mapName)}")`);
233
+ const isRelation = field.attributes.some((a) => a.name === "relation" && a.target === "field");
234
+ const isUnsupported = typeName.startsWith("Unsupported(");
235
+ const isId = field.attributes.some((a) => a.name === "id" && a.target === "field");
236
+ return {
237
+ name: field.name,
238
+ typeName,
239
+ optional: field.optional,
240
+ list: field.list,
241
+ attributes: attrStrings,
242
+ mapName,
243
+ isId,
244
+ isRelation,
245
+ isUnsupported,
246
+ comment: void 0
247
+ };
955
248
  }
956
- function expectBoolean(value, path) {
957
- if (typeof value !== "boolean") throw new Error(`${path} must be a boolean`);
958
- return value;
249
+ function buildModelFkDeps(models, modelNames) {
250
+ const deps = /* @__PURE__ */ new Map();
251
+ for (const m of models) deps.set(m.name, /* @__PURE__ */ new Set());
252
+ for (const m of models) for (const field of m.fields) {
253
+ const refModel = relationReferencedModel(field, modelNames);
254
+ if (!refModel || refModel === m.name) continue;
255
+ if (!hasFullRelation(field)) continue;
256
+ deps.get(m.name).add(refModel);
257
+ }
258
+ return deps;
259
+ }
260
+ function hasFullRelation(field) {
261
+ const rel = field.attributes.find((a) => a.name === "relation" && a.target === "field");
262
+ if (!rel) return false;
263
+ const named = Object.fromEntries(rel.args.filter((a) => a.kind === "named").map((a) => [a.name, a.value.trim()]));
264
+ return named["fields"] !== void 0 && named["references"] !== void 0;
265
+ }
266
+ function relationReferencedModel(field, modelNames) {
267
+ const raw = field.typeConstructor?.path[0] ?? field.typeName.replace(/\?$/, "").replace(/\[\]$/, "");
268
+ if (raw.length === 0) return;
269
+ return modelNames.has(raw) ? raw : void 0;
270
+ }
271
+ function topologicalSortModels(models, deps) {
272
+ const byName = new Map(models.map((m) => [m.name, m]));
273
+ const result = [];
274
+ const visited = /* @__PURE__ */ new Set();
275
+ const visiting = /* @__PURE__ */ new Set();
276
+ const sortedNames = [...deps.keys()].sort();
277
+ function visit(name) {
278
+ if (visited.has(name)) return;
279
+ if (visiting.has(name)) return;
280
+ visiting.add(name);
281
+ const sortedDeps = [...deps.get(name) ?? /* @__PURE__ */ new Set()].sort();
282
+ for (const dep of sortedDeps) visit(dep);
283
+ visiting.delete(name);
284
+ visited.add(name);
285
+ const model = byName.get(name);
286
+ if (model) result.push(model);
287
+ }
288
+ for (const name of sortedNames) visit(name);
289
+ return result;
959
290
  }
960
- function ifDefined(key, value) {
961
- if (value === void 0) return {};
962
- return { [key]: value };
291
+ //#endregion
292
+ //#region src/print-psl.ts
293
+ function printPslFromAst(ast) {
294
+ return serializePrintDocument(astDocumentToPrintDocument(ast));
963
295
  }
964
-
965
296
  //#endregion
966
- export { printPsl, validatePrintableSqlSchemaIR };
297
+ export { printPslFromAst as printPsl };
298
+
967
299
  //# sourceMappingURL=index.mjs.map