@prisma-next/psl-printer 0.5.0-dev.8 → 0.5.0-dev.80
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 +8 -12
- package/dist/index.d.mts +3 -41
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +205 -873
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -12
- package/src/ast-to-print-document.ts +293 -0
- package/src/exports/index.ts +1 -8
- package/src/print-document.ts +14 -0
- package/src/print-psl.ts +6 -879
- package/src/serialize-print-document.ts +182 -0
- package/src/types.ts +14 -83
- package/dist/postgres.d.mts +0 -32
- package/dist/postgres.d.mts.map +0 -1
- package/dist/postgres.mjs +0 -346
- package/dist/postgres.mjs.map +0 -1
- package/dist/types-BmnVaMF1.d.mts +0 -54
- package/dist/types-BmnVaMF1.d.mts.map +0 -1
- package/src/default-mapping.ts +0 -66
- package/src/exports/postgres.ts +0 -8
- package/src/name-transforms.ts +0 -234
- package/src/postgres-default-mapping.ts +0 -16
- package/src/postgres-type-map.ts +0 -204
- package/src/raw-default-parser.ts +0 -98
- package/src/relation-inference.ts +0 -239
- package/src/schema-validation.ts +0 -308
package/dist/index.mjs
CHANGED
|
@@ -1,352 +1,4 @@
|
|
|
1
|
-
//#region src/
|
|
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
|
-
|
|
361
|
-
"
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
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
|
-
|
|
421
|
-
for (const
|
|
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
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
|
824
|
-
|
|
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
|
|
842
|
-
|
|
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
|
|
858
|
-
|
|
859
|
-
|
|
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
|
-
|
|
862
|
-
|
|
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
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
892
|
-
|
|
893
|
-
|
|
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
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
|
910
|
-
|
|
911
|
-
|
|
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
|
|
914
|
-
|
|
915
|
-
if (
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
|
938
|
-
|
|
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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
|
949
|
-
|
|
950
|
-
|
|
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
|
|
953
|
-
|
|
954
|
-
|
|
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
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
|
297
|
+
export { printPslFromAst as printPsl };
|
|
298
|
+
|
|
967
299
|
//# sourceMappingURL=index.mjs.map
|