@prisma/orm-sqlite 8.0.0-rc.17-dev.43 → 8.0.0-rc.18

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,271 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Rewrites raw SQL written as a quoted string in PSL into a `sql` literal with the same text:
4
+ * the `where:` and `expression:` arguments of `@@index`, `@@fullTextIndex` and `@@check`, and
5
+ * `using =` and `withCheck =` in `policy_*` blocks.
6
+ *
7
+ * Usage:
8
+ * node scripts/codemods/rewrite-sql-strings.mjs <file-or-glob> [...more]
9
+ *
10
+ * Globs never descend into `node_modules` or `dist`. Files are rewritten in place, and each
11
+ * changed file is printed with its number of rewrites. Running it twice is a no-op.
12
+ */
13
+
14
+ import { readFileSync, writeFileSync } from 'node:fs';
15
+ import { glob } from 'node:fs/promises';
16
+ import { argv, exit, stderr, stdout } from 'node:process';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const ATTRIBUTE_START = /@@(index|fullTextIndex|check)\s*\(/g;
20
+ const ATTRIBUTE_ARGUMENT = /^(where|expression)\s*:\s*$/;
21
+ const POLICY_START = /^[ \t]*policy_[A-Za-z]+\s+[A-Za-z_][A-Za-z0-9_]*\s*\{/gm;
22
+ const POLICY_ENTRY = /(^|\n)([ \t]*(using|withCheck)\s*=\s*)$/;
23
+ const TAG = 'sql';
24
+
25
+ /** The end (exclusive) of the quoted string that starts at `start`, or -1 when it is not closed. */
26
+ function stringEnd(source, start) {
27
+ const quote = source[start];
28
+ for (let i = start + 1; i < source.length; i += 1) {
29
+ if (source[i] === '\\') {
30
+ i += 1;
31
+ continue;
32
+ }
33
+ if (source[i] === quote) return i + 1;
34
+ if (source[i] === '\n') return -1;
35
+ }
36
+ return -1;
37
+ }
38
+
39
+ /** The index of the line break that ends the `//` comment starting at `start`, or the source length. */
40
+ function commentEnd(source, start) {
41
+ const newline = source.indexOf('\n', start);
42
+ return newline === -1 ? source.length : newline;
43
+ }
44
+
45
+ /** The `[start, end)` ranges of `//` and `///` comments, quoted strings and backtick literals: text in which nothing declares an attribute or a policy. */
46
+ function inertRanges(source) {
47
+ const ranges = [];
48
+ for (let i = 0; i < source.length; i += 1) {
49
+ const ch = source[i];
50
+ if (ch === '"' || ch === "'") {
51
+ const end = stringEnd(source, i);
52
+ if (end !== -1) ranges.push([i, end]);
53
+ i = end === -1 ? commentEnd(source, i) : end - 1;
54
+ continue;
55
+ }
56
+ if (ch === '`') {
57
+ const end = source.indexOf('`', i + 1);
58
+ if (end === -1) continue;
59
+ ranges.push([i, end + 1]);
60
+ i = end;
61
+ continue;
62
+ }
63
+ if (ch === '/' && source[i + 1] === '/') {
64
+ const end = commentEnd(source, i);
65
+ ranges.push([i, end]);
66
+ i = end;
67
+ }
68
+ }
69
+ return ranges;
70
+ }
71
+
72
+ /** The end (exclusive) of the bracketed region opening at `open`, skipping strings and tagged literals. */
73
+ function closingEnd(source, open, openChar, closeChar) {
74
+ let depth = 0;
75
+ for (let i = open; i < source.length; i += 1) {
76
+ const ch = source[i];
77
+ if (ch === '"' || ch === "'" || ch === '`') {
78
+ const end = ch === '`' ? source.indexOf('`', i + 1) + 1 : stringEnd(source, i);
79
+ if (end <= 0) return -1;
80
+ i = end - 1;
81
+ continue;
82
+ }
83
+ if (ch === '/' && source[i + 1] === '/') {
84
+ const newline = source.indexOf('\n', i);
85
+ if (newline === -1) return -1;
86
+ i = newline;
87
+ continue;
88
+ }
89
+ if (ch === openChar) depth += 1;
90
+ if (ch === closeChar) {
91
+ depth -= 1;
92
+ if (depth === 0) return i + 1;
93
+ }
94
+ }
95
+ return -1;
96
+ }
97
+
98
+ function hexAt(raw, start, length) {
99
+ const digits = raw.slice(start, start + length);
100
+ return digits.length === length && /^[0-9A-Fa-f]+$/.test(digits)
101
+ ? String.fromCharCode(Number.parseInt(digits, 16))
102
+ : undefined;
103
+ }
104
+
105
+ const SIMPLE_ESCAPES = {
106
+ '/': '/',
107
+ b: '\b',
108
+ f: '\f',
109
+ n: '\n',
110
+ r: '\r',
111
+ t: '\t',
112
+ '"': '"',
113
+ "'": "'",
114
+ '\\': '\\',
115
+ };
116
+
117
+ /** The text of a PSL string literal body, decoded as the PSL parser decodes it. */
118
+ export function decodePslString(raw) {
119
+ let out = '';
120
+ for (let i = 0; i < raw.length; i += 1) {
121
+ const ch = raw[i];
122
+ if (ch !== '\\' || i + 1 >= raw.length) {
123
+ out += ch;
124
+ continue;
125
+ }
126
+ const next = raw[i + 1];
127
+ if (Object.hasOwn(SIMPLE_ESCAPES, next)) {
128
+ out += SIMPLE_ESCAPES[next];
129
+ i += 1;
130
+ continue;
131
+ }
132
+ const length = next === 'x' ? 2 : next === 'u' ? 4 : 0;
133
+ const decoded = length === 0 ? undefined : hexAt(raw, i + 2, length);
134
+ if (decoded === undefined) {
135
+ out += `\\${next}`;
136
+ i += 1;
137
+ continue;
138
+ }
139
+ out += decoded;
140
+ i += 1 + length;
141
+ }
142
+ return out;
143
+ }
144
+
145
+ /** A `sql` literal holding `text`: the backtick form, or the double-quote form when the text holds a backtick. */
146
+ export function printSqlLiteral(text) {
147
+ if (text.includes('`')) {
148
+ const escaped = text
149
+ .replace(/\\/g, '\\\\')
150
+ .replace(/"/g, '\\"')
151
+ .replace(/\n/g, '\\n')
152
+ .replace(/\r/g, '\\r');
153
+ return `${TAG}"${escaped}"`;
154
+ }
155
+ const fenced = text.replace(/\\/g, '\\\\');
156
+ return text.includes('\n') ? `${TAG}\`\n${fenced}\n\`` : `${TAG}\`${fenced}\``;
157
+ }
158
+
159
+ /** The quoted strings to rewrite, as `[start, end)` ranges of `source`. */
160
+ function stringRanges(source) {
161
+ const inert = inertRanges(source);
162
+ const isInert = (index) => inert.some(([start, end]) => index >= start && index < end);
163
+ const ranges = [];
164
+ for (const match of source.matchAll(ATTRIBUTE_START)) {
165
+ if (isInert(match.index)) continue;
166
+ const open = match.index + match[0].length - 1;
167
+ const close = closingEnd(source, open, '(', ')');
168
+ if (close === -1) continue;
169
+ let depth = 0;
170
+ let argumentStart = open + 1;
171
+ for (let i = open + 1; i < close - 1; i += 1) {
172
+ const ch = source[i];
173
+ if (ch === '/' && source[i + 1] === '/') {
174
+ i = commentEnd(source, i);
175
+ continue;
176
+ }
177
+ if (ch === '"' || ch === "'") {
178
+ const end = stringEnd(source, i);
179
+ if (end === -1) break;
180
+ const before = source.slice(argumentStart, i).trim();
181
+ if (depth === 0 && ATTRIBUTE_ARGUMENT.test(before)) ranges.push([i, end]);
182
+ i = end - 1;
183
+ continue;
184
+ }
185
+ if (ch === '`') {
186
+ i = source.indexOf('`', i + 1);
187
+ if (i === -1) break;
188
+ continue;
189
+ }
190
+ if (ch === '(' || ch === '[' || ch === '{') depth += 1;
191
+ if (ch === ')' || ch === ']' || ch === '}') depth -= 1;
192
+ if (ch === ',' && depth === 0) argumentStart = i + 1;
193
+ }
194
+ }
195
+ for (const match of source.matchAll(POLICY_START)) {
196
+ if (isInert(match.index + match[0].search(/\S/))) continue;
197
+ const open = match.index + match[0].length - 1;
198
+ const close = closingEnd(source, open, '{', '}');
199
+ if (close === -1) continue;
200
+ for (let i = open + 1; i < close - 1; i += 1) {
201
+ const ch = source[i];
202
+ if (ch === '/' && source[i + 1] === '/') {
203
+ i = commentEnd(source, i);
204
+ continue;
205
+ }
206
+ if (ch === '"' || ch === "'") {
207
+ const end = stringEnd(source, i);
208
+ if (end === -1) break;
209
+ if (POLICY_ENTRY.test(source.slice(open + 1, i))) ranges.push([i, end]);
210
+ i = end - 1;
211
+ continue;
212
+ }
213
+ if (ch === '`') {
214
+ i = source.indexOf('`', i + 1);
215
+ if (i === -1) break;
216
+ }
217
+ }
218
+ }
219
+ return ranges.sort((a, b) => a[0] - b[0]);
220
+ }
221
+
222
+ /** Rewrites `source` and returns it with the number of strings rewritten. */
223
+ export function rewriteSqlStringsWithCount(source) {
224
+ const ranges = stringRanges(source);
225
+ let out = '';
226
+ let position = 0;
227
+ for (const [start, end] of ranges) {
228
+ out += source.slice(position, start);
229
+ out += printSqlLiteral(decodePslString(source.slice(start + 1, end - 1)));
230
+ position = end;
231
+ }
232
+ out += source.slice(position);
233
+ return { source: out, count: ranges.length };
234
+ }
235
+
236
+ export function rewriteSqlStrings(source) {
237
+ return rewriteSqlStringsWithCount(source).source;
238
+ }
239
+
240
+ const SKIPPED_DIRECTORIES = /(^|\/)(node_modules|dist)\//;
241
+
242
+ async function filesMatching(patterns) {
243
+ const files = new Set();
244
+ for (const pattern of patterns) {
245
+ for await (const match of glob(pattern, {
246
+ exclude: (path) => /(^|\/)(node_modules|dist)$/.test(path),
247
+ })) {
248
+ if (!SKIPPED_DIRECTORIES.test(match)) files.add(match);
249
+ }
250
+ }
251
+ return [...files].sort();
252
+ }
253
+
254
+ async function main() {
255
+ const patterns = argv.slice(2);
256
+ if (patterns.length === 0) {
257
+ stderr.write('usage: node rewrite-sql-strings.mjs <file-or-glob> [...more]\n');
258
+ exit(2);
259
+ }
260
+ for (const file of await filesMatching(patterns)) {
261
+ const before = readFileSync(file, 'utf8');
262
+ const { source, count } = rewriteSqlStringsWithCount(before);
263
+ if (count === 0) continue;
264
+ writeFileSync(file, source);
265
+ stdout.write(`${file}: ${count} rewritten\n`);
266
+ }
267
+ }
268
+
269
+ if (argv[1] && fileURLToPath(import.meta.url) === argv[1]) {
270
+ await main();
271
+ }