uql-codemod 0.48.1 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -16
- package/dist/builders.d.ts +18 -0
- package/dist/builders.js +588 -0
- package/dist/classOptions.d.ts +9 -0
- package/dist/classOptions.js +240 -0
- package/dist/columnMethods.d.ts +11 -0
- package/dist/columnMethods.js +6 -0
- package/dist/columnRefs.js +3 -10
- package/dist/context.d.ts +18 -5
- package/dist/context.js +22 -4
- package/dist/definers.d.ts +24 -0
- package/dist/definers.js +34 -0
- package/dist/edits.d.ts +43 -11
- package/dist/edits.js +105 -26
- package/dist/entitySql.d.ts +15 -16
- package/dist/entitySql.js +33 -31
- package/dist/imports.d.ts +33 -16
- package/dist/imports.js +213 -99
- package/dist/index.d.ts +3 -3
- package/dist/index.js +9 -12
- package/dist/jsonIndex.d.ts +11 -0
- package/dist/jsonIndex.js +78 -0
- package/dist/keyMaps.d.ts +3 -1
- package/dist/keyMaps.js +5 -1
- package/dist/migrationBuilders.d.ts +13 -0
- package/dist/migrationBuilders.js +182 -0
- package/dist/relationTarget.d.ts +6 -0
- package/dist/relationTarget.js +21 -0
- package/dist/schemaEntity.d.ts +12 -0
- package/dist/schemaEntity.js +518 -0
- package/dist/schemaOptions.d.ts +6 -0
- package/dist/schemaOptions.js +6 -0
- package/dist/statementSql.d.ts +2 -1
- package/dist/statementSql.js +10 -4
- package/dist/syntax.d.ts +54 -0
- package/dist/syntax.js +131 -1
- package/dist/transform.d.ts +11 -2
- package/dist/transform.js +174 -165
- package/dist/tsconfig.d.ts +3 -9
- package/dist/tsconfig.js +18 -22
- package/package.json +1 -1
- package/dist/fieldType.d.ts +0 -13
- package/dist/fieldType.js +0 -56
package/dist/imports.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
+
import { REWRITTEN } from './context.js';
|
|
2
3
|
import { inserted, removeFromList, removeStatement, replaced } from './edits.js';
|
|
3
4
|
/**
|
|
4
5
|
* Exports that no longer exist, mapped to what to do instead. Reported rather than rewritten: what replaces
|
|
5
6
|
* each is decided at its call site.
|
|
6
7
|
*/
|
|
7
8
|
const REMOVED_EXPORTS = new Map([
|
|
9
|
+
['Relation', "write the property's type without it, and drop the import"],
|
|
8
10
|
[
|
|
9
11
|
'setQuerierPool',
|
|
10
12
|
'pass the pool where it is used: `createFetchHandler({ pool, include })`, `querierMiddleware({ pool, include })`',
|
|
@@ -29,8 +31,11 @@ const REMOVED_EXPORTS = new Map([
|
|
|
29
31
|
['SqlCallback', 'use `EntitySql<E>`, which holds the callback form'],
|
|
30
32
|
[
|
|
31
33
|
'IndexColumnOptions',
|
|
32
|
-
"an entity's index entry is `EntityIndexColumnInput
|
|
34
|
+
"an entity's index entry is `EntityIndexColumnInput`, the migration builder's `IndexColumnInput`",
|
|
33
35
|
],
|
|
36
|
+
['EntityIndexColumn', "an entity's index entry is `EntityIndexColumnInput`, the migration builder's `IndexEntry`"],
|
|
37
|
+
['IndexJsonPath', "read the path off the column's ref: `{ column: post.meta.lang, type: Number }`"],
|
|
38
|
+
['IndexJsonArray', "give the elements' type: `{ column: post.tags, type: [String] }`"],
|
|
34
39
|
['isKnownMigratorDialect', 'every dialect has a migrator, so it always held: drop the check'],
|
|
35
40
|
['QuerierPoolDialect', "read the pool's own: `P['dialect']`"],
|
|
36
41
|
['QuerierPoolQuerier', "read the pool's own: `Awaited<ReturnType<P['getQuerier']>>`"],
|
|
@@ -49,7 +54,6 @@ const UTIL_ROOT_EXPORTS = [
|
|
|
49
54
|
'currentDate',
|
|
50
55
|
'currentTime',
|
|
51
56
|
'currentTimestamp',
|
|
52
|
-
'raw',
|
|
53
57
|
'sql',
|
|
54
58
|
'refs',
|
|
55
59
|
'uuid',
|
|
@@ -63,12 +67,17 @@ const UTIL_ROOT_EXPORTS = [
|
|
|
63
67
|
'HookContext',
|
|
64
68
|
'DefaultLogger',
|
|
65
69
|
];
|
|
70
|
+
/** The name `uql-orm` gave its SQL tag, and gives it now. */
|
|
71
|
+
export const LEGACY_SQL_TAG = 'raw';
|
|
72
|
+
export const SQL_TAG = 'sql';
|
|
66
73
|
/**
|
|
67
74
|
* Exports renamed or moved and nothing else, so the import and every use follow, the import moving to `from`
|
|
68
75
|
* where the name lives in another entry. Keyed by name, or by `entry#name` where the name stays elsewhere.
|
|
69
76
|
*/
|
|
70
77
|
const RENAMED_EXPORTS = new Map([
|
|
71
78
|
['QueryWhereMap', { to: 'QueryWhere' }],
|
|
79
|
+
['IndexColumnSchema', { to: 'IndexEntry' }],
|
|
80
|
+
['IndexColumnModifiers', { to: 'IndexEntryModifiers' }],
|
|
72
81
|
['RelationKeyMap', { to: 'KeyMap' }],
|
|
73
82
|
['FilterCondition', { to: 'FilterWhere' }],
|
|
74
83
|
['PgDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
@@ -97,11 +106,15 @@ const RENAMED_EXPORTS = new Map([
|
|
|
97
106
|
['IMigrationBuilder', { to: 'MigrationBuilder' }],
|
|
98
107
|
['ITableBuilder', { to: 'TableBuilder' }],
|
|
99
108
|
['IAlterTableBuilder', { to: 'AlterTableBuilder' }],
|
|
100
|
-
['IColumnBuilder', { to: '
|
|
101
|
-
['IColumnFactory', { to: '
|
|
109
|
+
['IColumnBuilder', { to: 'TableColumnBuilder' }],
|
|
110
|
+
['IColumnFactory', { to: 'TableColumnFactory' }],
|
|
111
|
+
['uql-orm/migrate#ColumnBuilder', { to: 'TableColumnBuilder' }],
|
|
112
|
+
['uql-orm/migrate#ColumnFactory', { to: 'TableColumnFactory' }],
|
|
102
113
|
['IForeignKeyBuilder', { to: 'ForeignKeyBuilder' }],
|
|
103
114
|
['ITableForeignKeyBuilder', { to: 'TableForeignKeyBuilder' }],
|
|
104
115
|
['UqlLockUsageError', { to: 'UqlUsageError' }],
|
|
116
|
+
[LEGACY_SQL_TAG, { to: SQL_TAG }],
|
|
117
|
+
[`uql-orm/util#${LEGACY_SQL_TAG}`, { to: SQL_TAG, from: 'uql-orm' }],
|
|
105
118
|
['uql-orm/http#Hook', { to: 'RequestHook' }],
|
|
106
119
|
['uql-orm/http#HookContext', { to: 'RequestHookContext' }],
|
|
107
120
|
...UTIL_ROOT_EXPORTS.map((name) => [`uql-orm/util#${name}`, { to: name, from: 'uql-orm' }]),
|
|
@@ -127,6 +140,8 @@ const MOVED_ENTRIES = new Map([
|
|
|
127
140
|
['uql-orm/querier', 'uql-orm'],
|
|
128
141
|
['uql-orm/type', 'uql-orm'],
|
|
129
142
|
]);
|
|
143
|
+
/** The entry an import from `entry` lives in now. */
|
|
144
|
+
const entryOf = (entry) => MOVED_ENTRIES.get(entry) ?? entry;
|
|
130
145
|
/**
|
|
131
146
|
* The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
|
|
132
147
|
* for what is read by name only: `idKey` written into one would land on an entry that may not export it.
|
|
@@ -137,13 +152,15 @@ export function uqlImportDeclarations(source, entries = false) {
|
|
|
137
152
|
return [];
|
|
138
153
|
}
|
|
139
154
|
const entry = declaration.moduleSpecifier.text;
|
|
140
|
-
if (
|
|
155
|
+
if (entries ? !isUqlEntry(entry) : entry !== 'uql-orm') {
|
|
141
156
|
return [];
|
|
142
157
|
}
|
|
143
158
|
const bindings = declaration.importClause?.namedBindings;
|
|
144
159
|
return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
|
|
145
160
|
});
|
|
146
161
|
}
|
|
162
|
+
/** Whether `entry` is `uql-orm` itself or one of its `uql-orm/<driver>` entries. */
|
|
163
|
+
const isUqlEntry = (entry) => entry === 'uql-orm' || entry.startsWith('uql-orm/');
|
|
147
164
|
function uqlImports(source, entries = false) {
|
|
148
165
|
return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
|
|
149
166
|
}
|
|
@@ -157,18 +174,35 @@ function importedName(element) {
|
|
|
157
174
|
}
|
|
158
175
|
/** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
|
|
159
176
|
export function usesOf(source, name, checker) {
|
|
160
|
-
|
|
161
|
-
|
|
177
|
+
return usesOfEach(source, [name], checker)[0];
|
|
178
|
+
}
|
|
179
|
+
/** {@link usesOf} for each of `names`, in their order, from one walk of the file. */
|
|
180
|
+
function usesOfEach(source, names, checker) {
|
|
181
|
+
const bound = names.map((name) => ({ name, symbol: checker.getSymbolAtLocation(name), uses: [] }));
|
|
162
182
|
const visit = (node) => {
|
|
163
|
-
if (ts.isIdentifier(node)
|
|
164
|
-
|
|
165
|
-
|
|
183
|
+
if (ts.isIdentifier(node)) {
|
|
184
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
185
|
+
for (const binding of bound) {
|
|
186
|
+
if (symbol && binding.symbol === symbol && binding.name !== node)
|
|
187
|
+
binding.uses.push(node);
|
|
166
188
|
}
|
|
167
189
|
}
|
|
168
190
|
ts.forEachChild(node, visit);
|
|
169
191
|
};
|
|
170
192
|
visit(source);
|
|
171
|
-
return uses;
|
|
193
|
+
return bound.map(({ uses }) => uses);
|
|
194
|
+
}
|
|
195
|
+
/** The names among `names` the file binds or reads as something other than uql-orm's export, which a rewrite would shadow. */
|
|
196
|
+
export function foreignNames(source, names, checker) {
|
|
197
|
+
const found = new Set();
|
|
198
|
+
const visit = (node) => {
|
|
199
|
+
if (ts.isIdentifier(node) && names.has(node.text) && isForeignName(node, checker)) {
|
|
200
|
+
found.add(node.text);
|
|
201
|
+
}
|
|
202
|
+
ts.forEachChild(node, visit);
|
|
203
|
+
};
|
|
204
|
+
visit(source);
|
|
205
|
+
return found;
|
|
172
206
|
}
|
|
173
207
|
/** The rename `element` still needs, none where it already names the export at its entry. */
|
|
174
208
|
function renamedTo(element, entry) {
|
|
@@ -186,7 +220,7 @@ export function reportRemovedExports(ctx) {
|
|
|
186
220
|
}
|
|
187
221
|
continue;
|
|
188
222
|
}
|
|
189
|
-
const root = (
|
|
223
|
+
const root = entryOf(entry) === 'uql-orm';
|
|
190
224
|
for (const element of elements) {
|
|
191
225
|
const name = importedName(element);
|
|
192
226
|
const removed = REMOVED_EXPORTS.get(name);
|
|
@@ -200,116 +234,196 @@ export function reportRemovedExports(ctx) {
|
|
|
200
234
|
}
|
|
201
235
|
}
|
|
202
236
|
}
|
|
203
|
-
/**
|
|
204
|
-
* Rewrites each import naming a {@link RENAMED_EXPORTS} export, or from a {@link MOVED_ENTRIES} entry, renaming
|
|
205
|
-
* every use: a name moving entries gets an import from its new one, and one the file already imports is dropped,
|
|
206
|
-
* as is a `dead` one. Returns the rewritten imports, which nothing else may edit.
|
|
207
|
-
*/
|
|
208
|
-
export function renameExports(dead, ctx) {
|
|
209
|
-
const imports = uqlImportDeclarations(ctx.source, true);
|
|
210
|
-
const kept = imports.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)));
|
|
211
|
-
const bound = new Set(kept.map((element) => element.name.text));
|
|
212
|
-
const rewritten = new Set();
|
|
213
|
-
for (const { declaration, entry: imported, elements } of imports) {
|
|
214
|
-
const entry = MOVED_ENTRIES.get(imported) ?? imported;
|
|
215
|
-
if (entry === imported && !elements.some((element) => renamedTo(element, entry))) {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
const byEntry = new Map([[entry, []]]);
|
|
219
|
-
for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
|
|
220
|
-
const rename = renamedTo(element, entry);
|
|
221
|
-
const local = element.propertyName || !rename ? element.name.text : rename.to;
|
|
222
|
-
if (rename && !element.propertyName) {
|
|
223
|
-
ctx.edits.push(...usesOf(ctx.source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
|
|
224
|
-
}
|
|
225
|
-
if (rename && bound.has(local)) {
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
bound.add(local);
|
|
229
|
-
const target = rename?.from ?? entry;
|
|
230
|
-
const text = rename
|
|
231
|
-
? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
|
|
232
|
-
: element.getText();
|
|
233
|
-
byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
|
|
234
|
-
}
|
|
235
|
-
const keyword = isTypeOnly(declaration) ? 'import type' : 'import';
|
|
236
|
-
const quote = declaration.moduleSpecifier.getText()[0];
|
|
237
|
-
const statements = [...byEntry]
|
|
238
|
-
.filter(([, names]) => names.length)
|
|
239
|
-
.map(([from, names]) => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`);
|
|
240
|
-
ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
|
|
241
|
-
rewritten.add(declaration);
|
|
242
|
-
}
|
|
243
|
-
return rewritten;
|
|
244
|
-
}
|
|
245
237
|
function isTypeOnly(declaration) {
|
|
246
238
|
return declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
|
|
247
239
|
}
|
|
248
|
-
/**
|
|
249
|
-
* The imported names the codemod removes once every use was rewritten: `Relation` unwrapped and `expr` defaults.
|
|
250
|
-
* A removed decorator keeps its import on purpose: deleting it would swap a clear error for a puzzling one.
|
|
251
|
-
*/
|
|
240
|
+
/** The imported names of `REWRITTEN` the codemod removes, once every use was rewritten. */
|
|
252
241
|
export function deadImportNames(ctx) {
|
|
253
242
|
const { source, rewritten } = ctx;
|
|
254
|
-
const
|
|
243
|
+
const imported = REWRITTEN.flatMap((name) => {
|
|
255
244
|
const element = uqlImport(source, name, true);
|
|
256
|
-
return element ?
|
|
257
|
-
};
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
245
|
+
return element ? [[name, element.name]] : [];
|
|
246
|
+
});
|
|
247
|
+
const found = usesOfEach(source, imported.map(([, element]) => element), ctx.checker);
|
|
248
|
+
const uses = new Map(imported.map(([name], at) => [name, found[at].length]));
|
|
249
|
+
return new Set(REWRITTEN.filter((name) => rewritten[name] > 0 && rewritten[name] === (uses.get(name) ?? 0)));
|
|
250
|
+
}
|
|
251
|
+
/** The `uql-orm` export `node` names, where its symbol is an import of it or of one of its entries. */
|
|
252
|
+
export function uqlExport(node, checker) {
|
|
253
|
+
const declaration = node && ts.isIdentifier(node) ? checker.getSymbolAtLocation(node)?.declarations?.[0] : undefined;
|
|
254
|
+
return declaration && isUqlSpecifier(declaration) ? importedName(declaration) : undefined;
|
|
255
|
+
}
|
|
256
|
+
function isUqlSpecifier(declaration) {
|
|
257
|
+
if (!ts.isImportSpecifier(declaration)) {
|
|
258
|
+
return false;
|
|
262
259
|
}
|
|
263
|
-
const
|
|
264
|
-
return
|
|
260
|
+
const { moduleSpecifier } = declaration.parent.parent.parent;
|
|
261
|
+
return ts.isStringLiteral(moduleSpecifier) && isUqlEntry(moduleSpecifier.text);
|
|
262
|
+
}
|
|
263
|
+
/** Whether `name` is the SQL tag, by either of its names. */
|
|
264
|
+
export function isSqlExport(name) {
|
|
265
|
+
return name === SQL_TAG || name === LEGACY_SQL_TAG;
|
|
266
|
+
}
|
|
267
|
+
/** The name the file calls the SQL tag by once `raw` is renamed: the alias it imports it as, else `sql`. */
|
|
268
|
+
export function sqlName(source) {
|
|
269
|
+
const element = uqlImports(source, true).find((it) => isSqlExport(importedName(it)));
|
|
270
|
+
return element?.propertyName ? element.name.text : SQL_TAG;
|
|
271
|
+
}
|
|
272
|
+
/** The names the rewrites wrote that the file does not import from `uql-orm` yet, each with what wrote it. */
|
|
273
|
+
function missingNames(ctx) {
|
|
274
|
+
const imported = new Set(uqlImports(ctx.source, true).map(importedName));
|
|
275
|
+
const bound = [...imported].some(isSqlExport);
|
|
276
|
+
return [...ctx.imports].filter(([name]) => !imported.has(name) && !(bound && name === SQL_TAG));
|
|
277
|
+
}
|
|
278
|
+
const MEMBER = ts.SymbolFlags.Property | ts.SymbolFlags.Method | ts.SymbolFlags.Accessor | ts.SymbolFlags.EnumMember;
|
|
279
|
+
/** Whether `node` names a binding or a reference that is not `uql-orm`'s, which an import of the same name would capture. */
|
|
280
|
+
function isForeignName(node, checker) {
|
|
281
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
282
|
+
if (!symbol) {
|
|
283
|
+
const { parent } = node;
|
|
284
|
+
return !((ts.isPropertyAccessExpression(parent) || ts.isPropertyAssignment(parent)) && parent.name === node);
|
|
285
|
+
}
|
|
286
|
+
return !(symbol.flags & MEMBER) && !symbol.declarations?.every(isUqlSpecifier);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The first identifier of the file already naming something a rewrite writes: a name it imports (`sql`, `refs`,
|
|
290
|
+
* `idKey`) or a rename brings in. The write would shadow it or be shadowed, so the file is left to its owner.
|
|
291
|
+
*/
|
|
292
|
+
export function clashingName(ctx) {
|
|
293
|
+
const renamed = uqlImportDeclarations(ctx.source, true).flatMap(({ entry, elements }) => elements.flatMap((element) => {
|
|
294
|
+
const rename = renamedTo(element, entry);
|
|
295
|
+
return rename && !element.propertyName ? [rename.to] : [];
|
|
296
|
+
}));
|
|
297
|
+
const names = new Set([...missingNames(ctx).map(([name]) => name), ...renamed]);
|
|
298
|
+
const visit = (node) => ts.isIdentifier(node) && names.has(node.text) && isForeignName(node, ctx.checker)
|
|
299
|
+
? node
|
|
300
|
+
: ts.forEachChild(node, visit);
|
|
301
|
+
return names.size ? visit(ctx.source) : undefined;
|
|
265
302
|
}
|
|
266
303
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
304
|
+
* Rewrites the file's imports in one pass: a name {@link RENAMED_EXPORTS} renames or a {@link MOVED_ENTRIES} entry
|
|
305
|
+
* moves is imported again where it lives now, every use renamed, and one already imported or `dead` is dropped.
|
|
306
|
+
* The names the rewrites wrote join the first `uql-orm` import, or are reported when there is none.
|
|
269
307
|
*/
|
|
270
|
-
export function
|
|
271
|
-
|
|
308
|
+
export function rewriteImports(dead, ctx) {
|
|
309
|
+
const { source } = ctx;
|
|
310
|
+
const imports = new Map(uqlImportDeclarations(source, true).map((it) => [it.declaration, it]));
|
|
311
|
+
const bound = new Set([...imports.values()]
|
|
312
|
+
.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)))
|
|
313
|
+
.map((element) => element.name.text));
|
|
314
|
+
const home = [...imports.values()].find(({ entry }) => entryOf(entry) === 'uql-orm');
|
|
315
|
+
const written = missingNames(ctx);
|
|
316
|
+
if (!home) {
|
|
317
|
+
for (const [name, what] of written) {
|
|
318
|
+
ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const statement of source.statements) {
|
|
272
322
|
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
273
323
|
continue;
|
|
274
324
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
325
|
+
const uql = imports.get(statement);
|
|
326
|
+
const join = uql === home ? written.map(([name]) => name) : [];
|
|
327
|
+
const entry = uql && entryOf(uql.entry);
|
|
328
|
+
if (uql && (entry !== uql.entry || uql.elements.some((element) => renamedTo(element, entry)))) {
|
|
329
|
+
rebuildImport(uql, join, dead, bound, ctx);
|
|
278
330
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
continue;
|
|
331
|
+
else {
|
|
332
|
+
patchImport(statement, join, dead, ctx);
|
|
282
333
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** An import of `names` from `from`, quoted as the file quotes its imports. */
|
|
337
|
+
export const importText = (keyword, names, from, quote = "'") => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`;
|
|
338
|
+
/**
|
|
339
|
+
* The import written again: each name under the entry it lives in, `join` first under `uql-orm`. `join` is values,
|
|
340
|
+
* so in a type-only declaration it becomes a value import of its own.
|
|
341
|
+
*/
|
|
342
|
+
function rebuildImport({ declaration, entry: imported, elements }, join, dead, bound, ctx) {
|
|
343
|
+
const entry = entryOf(imported);
|
|
344
|
+
const typeOnly = isTypeOnly(declaration);
|
|
345
|
+
const byEntry = new Map([[entry, typeOnly ? [] : [...join]]]);
|
|
346
|
+
for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
|
|
347
|
+
const rename = renamedTo(element, entry);
|
|
348
|
+
const local = element.propertyName || !rename ? element.name.text : rename.to;
|
|
349
|
+
if (rename && !element.propertyName) {
|
|
350
|
+
ctx.edits.push(...usesOf(ctx.source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
|
|
287
351
|
}
|
|
288
|
-
|
|
289
|
-
|
|
352
|
+
if (rename && bound.has(local)) {
|
|
353
|
+
continue;
|
|
290
354
|
}
|
|
355
|
+
bound.add(local);
|
|
356
|
+
const target = rename?.from ?? entry;
|
|
357
|
+
const text = rename
|
|
358
|
+
? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
|
|
359
|
+
: element.getText();
|
|
360
|
+
byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
|
|
361
|
+
}
|
|
362
|
+
const keyword = typeOnly ? 'import type' : 'import';
|
|
363
|
+
const quote = declaration.moduleSpecifier.getText()[0];
|
|
364
|
+
const statements = [...byEntry]
|
|
365
|
+
.filter(([, names]) => names.length)
|
|
366
|
+
.map(([from, names]) => importText(keyword, names, from, quote));
|
|
367
|
+
if (typeOnly && join.length) {
|
|
368
|
+
statements.unshift(importText('import', join, 'uql-orm', quote));
|
|
291
369
|
}
|
|
370
|
+
ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
|
|
292
371
|
}
|
|
293
372
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
373
|
+
* The import without `dropped` of its `elements`: the whole statement where nothing else is left of it, and the
|
|
374
|
+
* braces with them where a default import stays.
|
|
296
375
|
*/
|
|
297
|
-
export function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const missing = [...ctx.imports].filter(([name]) => !imported.has(name));
|
|
301
|
-
if (!missing.length) {
|
|
302
|
-
return;
|
|
376
|
+
export function importRemovalEdits(declaration, elements, dropped) {
|
|
377
|
+
if (dropped.length !== elements.length) {
|
|
378
|
+
return removeFromList(elements, dropped, declaration.getSourceFile());
|
|
303
379
|
}
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
380
|
+
const clause = declaration.importClause;
|
|
381
|
+
return clause?.name
|
|
382
|
+
? [{ start: clause.name.getEnd(), end: clause.getEnd(), text: '' }]
|
|
383
|
+
: [removeStatement(declaration)];
|
|
384
|
+
}
|
|
385
|
+
/** Every name the file spells outside its imports, which an import it binds is still used under if listed here. */
|
|
386
|
+
export function spelledNames(source) {
|
|
387
|
+
const names = new Set();
|
|
388
|
+
const visit = (node) => {
|
|
389
|
+
if (ts.isIdentifier(node)) {
|
|
390
|
+
names.add(node.text);
|
|
308
391
|
}
|
|
392
|
+
if (!ts.isImportDeclaration(node)) {
|
|
393
|
+
ts.forEachChild(node, visit);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
visit(source);
|
|
397
|
+
return names;
|
|
398
|
+
}
|
|
399
|
+
/** The import names in `candidates` that the file no longer spells. */
|
|
400
|
+
export function deadImports(elements, candidates, spelled) {
|
|
401
|
+
return elements.filter((it) => candidates.has(it.name.text) && !spelled.has(it.name.text));
|
|
402
|
+
}
|
|
403
|
+
/** The edits dropping each `uql-orm` import name in `candidates` that the file no longer spells. */
|
|
404
|
+
export function unusedImportEdits(source, candidates) {
|
|
405
|
+
const spelled = spelledNames(source);
|
|
406
|
+
return uqlImportDeclarations(source, true).flatMap(({ declaration, elements }) => {
|
|
407
|
+
const dead = deadImports(elements, candidates, spelled);
|
|
408
|
+
return dead.length ? importRemovalEdits(declaration, elements, dead) : [];
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/** The import with the `dead` names out and `join` in, nothing else of it touched. */
|
|
412
|
+
function patchImport(declaration, join, dead, ctx) {
|
|
413
|
+
const named = declaration.importClause?.namedBindings;
|
|
414
|
+
if (!named || !ts.isNamedImports(named)) {
|
|
309
415
|
return;
|
|
310
416
|
}
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
417
|
+
const dropped = named.elements.filter((element) => dead.has(element.name.text));
|
|
418
|
+
const statement = importText('import', join, 'uql-orm');
|
|
419
|
+
if (join.length && dropped.length === named.elements.length && !declaration.importClause?.name) {
|
|
420
|
+
ctx.edits.push(replaced(declaration, statement));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
ctx.edits.push(...importRemovalEdits(declaration, named.elements, dropped));
|
|
424
|
+
if (join.length) {
|
|
425
|
+
ctx.edits.push(isTypeOnly(declaration)
|
|
426
|
+
? inserted(declaration, `${statement}\n`)
|
|
427
|
+
: inserted(named.elements[0], join.map((name) => `${name}, `).join('')));
|
|
428
|
+
}
|
|
315
429
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,9 +14,9 @@ export type RunSummary = {
|
|
|
14
14
|
readonly notes: readonly string[];
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
* Rewrites a project
|
|
17
|
+
* Rewrites a project across the breaking changes of uql-orm that can be made mechanically.
|
|
18
18
|
*
|
|
19
|
-
* @remarks Needs a type checker, not just a parser: the
|
|
20
|
-
*
|
|
19
|
+
* @remarks Needs a type checker, not just a parser: the codemod reads each property's type to name a relation's
|
|
20
|
+
* target and to admit `null`, and only the checker resolves `role?: Role` to its arms.
|
|
21
21
|
*/
|
|
22
22
|
export declare function run({ project, dryRun, include }: RunOptions): Promise<RunSummary>;
|
package/dist/index.js
CHANGED
|
@@ -2,14 +2,14 @@ import { writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import ts from 'typescript';
|
|
4
4
|
import { transformFile } from './transform.js';
|
|
5
|
-
import {
|
|
5
|
+
import { checkTsconfig } from './tsconfig.js';
|
|
6
6
|
export * from './transform.js';
|
|
7
7
|
export * from './tsconfig.js';
|
|
8
8
|
/**
|
|
9
|
-
* Rewrites a project
|
|
9
|
+
* Rewrites a project across the breaking changes of uql-orm that can be made mechanically.
|
|
10
10
|
*
|
|
11
|
-
* @remarks Needs a type checker, not just a parser: the
|
|
12
|
-
*
|
|
11
|
+
* @remarks Needs a type checker, not just a parser: the codemod reads each property's type to name a relation's
|
|
12
|
+
* target and to admit `null`, and only the checker resolves `role?: Role` to its arms.
|
|
13
13
|
*/
|
|
14
14
|
export async function run({ project, dryRun, include }) {
|
|
15
15
|
// Absolute, because `parseJsonConfigFileContent` resolves every discovered file against the base path
|
|
@@ -41,19 +41,16 @@ export async function run({ project, dryRun, include }) {
|
|
|
41
41
|
if (result.changed || result.unresolved.length || result.notes.length)
|
|
42
42
|
results.push(result);
|
|
43
43
|
}
|
|
44
|
-
// The config is rewritten too: leaving `experimentalDecorators` on is what keeps a project compiling
|
|
45
|
-
// against the old spec, so a codemod that only touched the sources would leave it half-migrated.
|
|
46
|
-
const tsconfig = transformTsconfig(configPath, ts.sys.readFile(configPath) ?? '');
|
|
47
44
|
const rewritten = results.filter((r) => r.changed);
|
|
48
45
|
if (!dryRun) {
|
|
49
46
|
await Promise.all(rewritten.map((r) => writeFile(r.fileName, r.text, 'utf8')));
|
|
50
|
-
if (tsconfig.changed) {
|
|
51
|
-
await writeFile(configPath, tsconfig.text, 'utf8');
|
|
52
|
-
}
|
|
53
47
|
}
|
|
54
48
|
return {
|
|
55
|
-
changed:
|
|
56
|
-
unresolved: [
|
|
49
|
+
changed: rewritten.map((r) => r.fileName),
|
|
50
|
+
unresolved: [
|
|
51
|
+
...results.flatMap((r) => r.unresolved),
|
|
52
|
+
...checkTsconfig(configPath, ts.sys.readFile(configPath) ?? ''),
|
|
53
|
+
],
|
|
57
54
|
notes: results.flatMap((r) => r.notes),
|
|
58
55
|
};
|
|
59
56
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { Context } from './context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Rewrites each JSON entry of an index's columns into the path read off the column's ref, as a `$where` key
|
|
5
|
+
* spells it: `{ column: post.kind, jsonPath: { path: 'theme.color', type: String } }` becomes
|
|
6
|
+
* `{ column: post.kind.theme.color, type: String }`, and a `jsonArray`'s element type becomes `type: [String]`.
|
|
7
|
+
* Its `length` becomes `charLength`, and a column entry's becomes `prefixLength`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function rewriteJsonIndexEntries(columns: ts.Expression | undefined, ctx: Context): void;
|
|
10
|
+
/** A migration builder's index entries, each `length` as the option it names now: `charLength` on JSON, `prefixLength` on a column. */
|
|
11
|
+
export declare function rewriteBuilderIndexLengths(columns: ts.Expression | undefined, ctx: Context): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { original, replaced } from './edits.js';
|
|
3
|
+
import { memberAccess } from './keyMaps.js';
|
|
4
|
+
import { arrayElements, columnListEntries, namedProperty, propertyAssignment, propertyKey, propertyValue, } from './syntax.js';
|
|
5
|
+
/** The entry's own keys the rewritten entry writes itself rather than carrying over. */
|
|
6
|
+
const MOVED_KEYS = new Set(['column', 'length']);
|
|
7
|
+
/**
|
|
8
|
+
* Rewrites each JSON entry of an index's columns into the path read off the column's ref, as a `$where` key
|
|
9
|
+
* spells it: `{ column: post.kind, jsonPath: { path: 'theme.color', type: String } }` becomes
|
|
10
|
+
* `{ column: post.kind.theme.color, type: String }`, and a `jsonArray`'s element type becomes `type: [String]`.
|
|
11
|
+
* Its `length` becomes `charLength`, and a column entry's becomes `prefixLength`.
|
|
12
|
+
*/
|
|
13
|
+
export function rewriteJsonIndexEntries(columns, ctx) {
|
|
14
|
+
for (const entry of columnListEntries(columns).filter(ts.isObjectLiteralExpression)) {
|
|
15
|
+
const read = propertyAssignment(entry, 'jsonPath') ?? propertyAssignment(entry, 'jsonArray');
|
|
16
|
+
const edit = read ? jsonEntryEdit(entry, read) : prefixLengthEdit(entry);
|
|
17
|
+
if (edit) {
|
|
18
|
+
ctx.edits.push(edit);
|
|
19
|
+
}
|
|
20
|
+
else if (read) {
|
|
21
|
+
ctx.report(entry, "read the JSON path off the column's ref, `{ column: post.kind.theme, type: Number }`; this entry could not be read");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The read's options as the path's keys, and the type and length as written, `undefined` where it takes more than these. */
|
|
26
|
+
function readOptions(options) {
|
|
27
|
+
const path = propertyValue(options, 'path');
|
|
28
|
+
const type = propertyValue(options, 'type');
|
|
29
|
+
const length = propertyValue(options, 'length');
|
|
30
|
+
const known = [path, type, length].filter(Boolean).length;
|
|
31
|
+
if (!type || (path && !ts.isStringLiteralLike(path)) || options.properties.length !== known) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const keys = path && ts.isStringLiteralLike(path) ? path.text.split('.') : [];
|
|
35
|
+
return { keys, type, length };
|
|
36
|
+
}
|
|
37
|
+
/** The entry rewritten, or `undefined` where its read names a path or an option this cannot carry over. */
|
|
38
|
+
function jsonEntryEdit(entry, read) {
|
|
39
|
+
const column = propertyValue(entry, 'column');
|
|
40
|
+
const options = read.initializer;
|
|
41
|
+
const parsed = column && ts.isObjectLiteralExpression(options) ? readOptions(options) : undefined;
|
|
42
|
+
const entryLength = propertyValue(entry, 'length');
|
|
43
|
+
if (!column || !parsed || (parsed.length && entryLength)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const { keys, type } = parsed;
|
|
47
|
+
const length = parsed.length ?? entryLength;
|
|
48
|
+
const elements = propertyKey(read.name) === 'jsonArray';
|
|
49
|
+
const rest = entry.properties.filter((property) => property !== read && !(ts.isPropertyAssignment(property) && MOVED_KEYS.has(propertyKey(property.name))));
|
|
50
|
+
const parts = [
|
|
51
|
+
'{ column: ',
|
|
52
|
+
original(column),
|
|
53
|
+
keys.reduce(memberAccess, ''),
|
|
54
|
+
', type: ',
|
|
55
|
+
...(elements ? ['[', original(type), ']'] : [original(type)]),
|
|
56
|
+
...(length ? [', charLength: ', original(length)] : []),
|
|
57
|
+
...rest.flatMap((property) => [', ', original(property)]),
|
|
58
|
+
' }',
|
|
59
|
+
];
|
|
60
|
+
return replaced(entry, parts);
|
|
61
|
+
}
|
|
62
|
+
/** A column entry's `length` as the `prefixLength` it now names, `undefined` where it states none. */
|
|
63
|
+
function prefixLengthEdit(entry) {
|
|
64
|
+
const length = propertyAssignment(entry, 'length');
|
|
65
|
+
return length && replaced(length.name, 'prefixLength');
|
|
66
|
+
}
|
|
67
|
+
/** The keys only a migration builder's JSON entry states, whose `length` is the `CHAR(n)` it is read as. */
|
|
68
|
+
const JSON_ENTRY_KEYS = ['json', 'path', 'type'];
|
|
69
|
+
/** A migration builder's index entries, each `length` as the option it names now: `charLength` on JSON, `prefixLength` on a column. */
|
|
70
|
+
export function rewriteBuilderIndexLengths(columns, ctx) {
|
|
71
|
+
for (const entry of arrayElements(columns).filter(ts.isObjectLiteralExpression)) {
|
|
72
|
+
const length = propertyAssignment(entry, 'length');
|
|
73
|
+
const json = JSON_ENTRY_KEYS.some((key) => namedProperty(entry, key));
|
|
74
|
+
if (length) {
|
|
75
|
+
ctx.edits.push(replaced(length.name, json ? 'charLength' : 'prefixLength'));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/keyMaps.d.ts
CHANGED
|
@@ -8,10 +8,12 @@ export declare function paramFor(className: string | undefined): string;
|
|
|
8
8
|
export declare function quoted(text: string): string;
|
|
9
9
|
/** `post.title`, or `post['first-name']` for a key that is no identifier. */
|
|
10
10
|
export declare function memberAccess(param: string, key: string): string;
|
|
11
|
+
/** A key as an object literal writes it: `title`, or `'first-name'` for one that is no identifier. */
|
|
12
|
+
export declare function memberKey(key: string): string;
|
|
11
13
|
/** `'amount'` or `['title', 'body']` -> `{ amount: true }`, the key map a statement names fields by. */
|
|
12
14
|
export declare function fieldKeysEdits(value: ts.Expression): Edits;
|
|
13
15
|
/** Whether `value` is already a function, the form every rewrite here produces. */
|
|
14
|
-
export declare function isCallback(value: ts.Expression):
|
|
16
|
+
export declare function isCallback(value: ts.Expression): value is ts.ArrowFunction | ts.FunctionExpression;
|
|
15
17
|
/** `['title', { column: 'createdAt' }]` -> `(post) => [post.title, { column: post.createdAt }]`. */
|
|
16
18
|
export declare function keyListEdits(list: ts.Expression, param: string): Edits;
|
|
17
19
|
/** `'author'` -> `(post) => post.author`, `param` being the relation's target. */
|
package/dist/keyMaps.js
CHANGED
|
@@ -16,13 +16,17 @@ export function quoted(text) {
|
|
|
16
16
|
export function memberAccess(param, key) {
|
|
17
17
|
return isIdentifierName(key) ? `${param}.${key}` : `${param}[${quoted(key)}]`;
|
|
18
18
|
}
|
|
19
|
+
/** A key as an object literal writes it: `title`, or `'first-name'` for one that is no identifier. */
|
|
20
|
+
export function memberKey(key) {
|
|
21
|
+
return isIdentifierName(key) ? key : quoted(key);
|
|
22
|
+
}
|
|
19
23
|
/** `'amount'` or `['title', 'body']` -> `{ amount: true }`, the key map a statement names fields by. */
|
|
20
24
|
export function fieldKeysEdits(value) {
|
|
21
25
|
const names = ts.isArrayLiteralExpression(value) ? value.elements : [value];
|
|
22
26
|
if (!names.every((name) => ts.isStringLiteralLike(name))) {
|
|
23
27
|
return undefined;
|
|
24
28
|
}
|
|
25
|
-
const keys = names.map(({ text }) => `${
|
|
29
|
+
const keys = names.map(({ text }) => `${memberKey(text)}: true`);
|
|
26
30
|
return [replaced(value, `{ ${keys.join(', ')} }`)];
|
|
27
31
|
}
|
|
28
32
|
/** Whether `value` is already a function, the form every rewrite here produces. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The column factories of a migration's table builder. */
|
|
2
|
+
export declare const FACTORY_NAMES: readonly ["id", "integer", "smallint", "bigint", "float", "double", "decimal", "string", "char", "text", "boolean", "date", "time", "timestamp", "timestamptz", "json", "jsonb", "uuid", "blob", "vector"];
|
|
3
|
+
export type MigrationBuildersResult = {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly unresolved: string[];
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Writes a migration's columns in an entity column's vocabulary: `table.string('email', { length: 80 })` as
|
|
9
|
+
* `table.string('email').length(80)`, `.defaultValue()` as `.default()`, `.primaryKey()` as `.id()`, `.computed()`
|
|
10
|
+
* as `.as()`, and `.notNullable()` dropped, a column being NOT NULL already. Only a chain off the builder a
|
|
11
|
+
* `createTable`, `alterTable`, `addColumn` or `alterColumn` callback is handed, so no other `.string()` is touched.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toMigrationBuilders(fileName: string, text: string, locate?: (at: number) => string): MigrationBuildersResult;
|