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