@prisma/orm-sqlite 8.0.0-rc.11-dev.56 → 8.0.0-rc.12-dev.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.
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Adds `@@map("<model name with its first letter lowered>")` to every PSL
4
+ * `model` block that has no `@@map`, so a schema written before Prisma 8 made
5
+ * model names verbatim keeps the table (or collection) names it already has.
6
+ *
7
+ * Usage:
8
+ * node <path to this file> <file-or-glob> [...more]
9
+ *
10
+ * Globs never descend into `node_modules` or `dist`.
11
+ *
12
+ * A model with `@@base(...)` and no `@@map` shares its base's storage, so it
13
+ * is left alone: adding `@@map` there would split it into its own table or
14
+ * collection.
15
+ *
16
+ * Files are rewritten in place; every mapped model is printed as
17
+ * `<file>: model <Name> -> @@map("<name>")` for review. Running it twice is a
18
+ * no-op. Only run it on a schema written against the previous release: a
19
+ * schema inferred after upgrading already names its tables verbatim.
20
+ */
21
+
22
+ import { readFileSync, writeFileSync } from 'node:fs';
23
+ import { glob } from 'node:fs/promises';
24
+ import { argv, exit, stderr, stdout } from 'node:process';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const MODEL_LINE = /^\s*model\s/;
28
+ const MODEL_START = /^(\s*)model\s+([A-Za-z_][A-Za-z0-9_]*)\s*(\{)?(.*)$/;
29
+ const OPEN_BRACE_LINE = /^\s*\{\s*(\/\/.*)?$/;
30
+ const BLANK_OR_COMMENT = /^\s*(\/\/.*)?$/;
31
+ const BLOCK_CLOSE = /^\s*\}\s*(\/\/.*)?$/;
32
+ const MAP_ATTRIBUTE = /^\s*@@map\s*\(/;
33
+ const BASE_ATTRIBUTE = /^\s*@@base\s*\(/;
34
+ const OWN_STORAGE_ATTRIBUTE = /@@(map|base)\s*\(/;
35
+
36
+ function lowerFirst(name) {
37
+ return name.charAt(0).toLowerCase() + name.slice(1);
38
+ }
39
+
40
+ function bodyIndent(lines, start, end, headerIndent) {
41
+ for (let i = start; i < end; i += 1) {
42
+ const match = /^(\s*)\S/.exec(lines[i]);
43
+ if (match) return match[1];
44
+ }
45
+ return `${headerIndent} `;
46
+ }
47
+
48
+ function stripCr(line) {
49
+ return line.replace(/\r$/, '');
50
+ }
51
+
52
+ /**
53
+ * Classifies the `model` line at `index`. Returns the header indent, the model
54
+ * name, the index of the first body line, any body text that shares the
55
+ * header line, and whether the whole block sits on this one line.
56
+ */
57
+ function nextNonBlankIndex(lines, index) {
58
+ let i = index + 1;
59
+ while (i < lines.length && BLANK_OR_COMMENT.test(stripCr(lines[i]))) i += 1;
60
+ return i;
61
+ }
62
+
63
+ /**
64
+ * A `model` line is a declaration only when the block's `{` follows the model
65
+ * name on that line, or opens the next line that is not blank or a comment.
66
+ * Any other `model` line is a field named `model` inside some block and is
67
+ * left alone.
68
+ */
69
+ function isModelDeclaration(lines, index) {
70
+ const match = MODEL_START.exec(stripCr(lines[index]));
71
+ if (!match) return false;
72
+ if (match[3] !== undefined) return true;
73
+ const next = lines[nextNonBlankIndex(lines, index)];
74
+ return next !== undefined && OPEN_BRACE_LINE.test(stripCr(next));
75
+ }
76
+
77
+ function readModelStart(lines, index) {
78
+ const match = MODEL_START.exec(stripCr(lines[index]));
79
+ if (!match) return undefined;
80
+ const [, indent, modelName, brace, rest] = match;
81
+ if (brace === undefined) {
82
+ if (!BLANK_OR_COMMENT.test(rest)) return undefined;
83
+ const braceAt = nextNonBlankIndex(lines, index);
84
+ const next = lines[braceAt];
85
+ if (next === undefined || !OPEN_BRACE_LINE.test(stripCr(next))) return undefined;
86
+ return { indent, modelName, bodyStart: braceAt + 1, inlineBody: '', singleLine: false };
87
+ }
88
+ const closeAt = closingBraceIndex(rest);
89
+ if (closeAt !== -1) {
90
+ return { indent, modelName, bodyStart: index + 1, inlineBody: rest, singleLine: true };
91
+ }
92
+ return {
93
+ indent,
94
+ modelName,
95
+ bodyStart: index + 1,
96
+ inlineBody: BLANK_OR_COMMENT.test(rest) ? '' : rest,
97
+ singleLine: false,
98
+ };
99
+ }
100
+
101
+ /** The line without a trailing `//` comment; a `//` inside a double-quoted string is content. */
102
+ function stripLineComment(text) {
103
+ let quoted = false;
104
+ for (let i = 0; i < text.length; i += 1) {
105
+ const char = text[i];
106
+ if (quoted) {
107
+ if (char === '\\') i += 1;
108
+ else if (char === '"') quoted = false;
109
+ } else if (char === '"') {
110
+ quoted = true;
111
+ } else if (char === '/' && text[i + 1] === '/') {
112
+ return text.slice(0, i).trimEnd();
113
+ }
114
+ }
115
+ return text;
116
+ }
117
+
118
+ /** Index of a `}` that ends the block on this line (only whitespace or a `//` comment may follow), else -1. */
119
+ function closingBraceIndex(text) {
120
+ const code = stripLineComment(text);
121
+ const closeAt = code.lastIndexOf('}');
122
+ return closeAt !== -1 && /^\s*$/.test(code.slice(closeAt + 1)) ? closeAt : -1;
123
+ }
124
+
125
+ function addMapToSingleLineModel(line, modelName) {
126
+ const raw = stripCr(line);
127
+ const closeAt = closingBraceIndex(raw);
128
+ const body = raw.slice(0, closeAt);
129
+ if (OWN_STORAGE_ATTRIBUTE.test(body)) return line;
130
+ return `${body.trimEnd()} @@map("${lowerFirst(modelName)}") ${line.slice(closeAt)}`;
131
+ }
132
+
133
+ /** Thrown when a `model` declaration is written in a shape the codemod does not recognise. */
134
+ export class UnhandledModelError extends Error {
135
+ constructor(lineNumbers) {
136
+ super(`model block(s) not understood at line(s) ${lineNumbers.join(', ')}`);
137
+ this.lineNumbers = lineNumbers;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Rewrites `source` and also returns the models that received an `@@map`, so a
143
+ * caller can show the user what was mapped. The codemod cannot see storage:
144
+ * it assumes every unmapped model was written against the release whose
145
+ * default lowered the first letter.
146
+ */
147
+ export function addModelMapsWithReport(source) {
148
+ const newline = source.includes('\r\n') ? '\r' : '';
149
+ const lines = source.split('\n');
150
+ const out = [];
151
+ const unhandled = [];
152
+ const mapped = [];
153
+ let i = 0;
154
+ while (i < lines.length) {
155
+ if (!MODEL_LINE.test(lines[i]) || !isModelDeclaration(lines, i)) {
156
+ out.push(lines[i]);
157
+ i += 1;
158
+ continue;
159
+ }
160
+ const start = readModelStart(lines, i);
161
+ if (start === undefined) {
162
+ unhandled.push(i + 1);
163
+ out.push(lines[i]);
164
+ i += 1;
165
+ continue;
166
+ }
167
+ if (start.singleLine) {
168
+ const rewritten = addMapToSingleLineModel(lines[i], start.modelName);
169
+ if (rewritten !== lines[i]) mapped.push(start.modelName);
170
+ out.push(rewritten);
171
+ i += 1;
172
+ continue;
173
+ }
174
+ let close = start.bodyStart;
175
+ while (close < lines.length && !BLOCK_CLOSE.test(lines[close])) close += 1;
176
+ if (close >= lines.length) {
177
+ unhandled.push(i + 1);
178
+ out.push(...lines.slice(i));
179
+ break;
180
+ }
181
+ const body = [start.inlineBody, ...lines.slice(start.bodyStart, close)];
182
+ out.push(...lines.slice(i, close));
183
+ if (!body.some((line) => MAP_ATTRIBUTE.test(line) || BASE_ATTRIBUTE.test(line))) {
184
+ const indent = bodyIndent(lines, start.bodyStart, close, start.indent);
185
+ out.push(`${indent}@@map("${lowerFirst(start.modelName)}")${newline}`);
186
+ mapped.push(start.modelName);
187
+ }
188
+ out.push(lines[close]);
189
+ i = close + 1;
190
+ }
191
+ if (unhandled.length > 0) throw new UnhandledModelError(unhandled);
192
+ return { source: out.join('\n'), mapped };
193
+ }
194
+
195
+ export function addModelMaps(source) {
196
+ return addModelMapsWithReport(source).source;
197
+ }
198
+
199
+ const SKIPPED_DIRECTORIES = /(^|\/)(node_modules|dist)\//;
200
+
201
+ async function expandPatterns(patterns) {
202
+ const files = new Set();
203
+ for (const pattern of patterns) {
204
+ for await (const match of glob(pattern, {
205
+ exclude: (path) => SKIPPED_DIRECTORIES.test(`${path}/`),
206
+ })) {
207
+ files.add(match);
208
+ }
209
+ }
210
+ return [...files].sort();
211
+ }
212
+
213
+ async function main() {
214
+ const patterns = argv.slice(2);
215
+ if (patterns.length === 0) {
216
+ stderr.write('usage: node add-model-map.mjs <file-or-glob> [...more]\n');
217
+ exit(2);
218
+ }
219
+ const files = await expandPatterns(patterns);
220
+ if (files.length === 0) {
221
+ stderr.write('no files matched\n');
222
+ exit(1);
223
+ }
224
+ let failed = false;
225
+ let mappedCount = 0;
226
+ for (const file of files) {
227
+ const before = readFileSync(file, 'utf8');
228
+ let result;
229
+ try {
230
+ result = addModelMapsWithReport(before);
231
+ } catch (error) {
232
+ if (!(error instanceof UnhandledModelError)) throw error;
233
+ for (const line of error.lineNumbers)
234
+ stderr.write(`${file}:${line}: model block not understood\n`);
235
+ failed = true;
236
+ continue;
237
+ }
238
+ if (result.source !== before) {
239
+ writeFileSync(file, result.source);
240
+ for (const modelName of result.mapped) {
241
+ stdout.write(`${file}: model ${modelName} -> @@map("${lowerFirst(modelName)}")\n`);
242
+ }
243
+ mappedCount += result.mapped.length;
244
+ }
245
+ }
246
+ if (mappedCount > 0) {
247
+ stdout.write(
248
+ `${mappedCount} model(s) mapped. Review the list: every one must be a model whose table or collection was created under the previous release's lowered-first-letter default. A schema written or inferred after upgrading must not be run through this codemod.\n`,
249
+ );
250
+ }
251
+ if (failed) exit(1);
252
+ }
253
+
254
+ if (argv[1] && fileURLToPath(import.meta.url) === argv[1]) {
255
+ await main();
256
+ }