uql-codemod 0.24.1 → 0.25.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 +1 -2
- package/dist/transform.js +22 -85
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -27,7 +27,6 @@ Only the `--flag=value` form is read, and anything unrecognised is an error: a m
|
|
|
27
27
|
- `@Field()` and `@Field({ ... })` with no `type`, and `@Id()`: inserts the `type` the property's declared TypeScript type implies. Skipped when `references` is present, because schema generation resolves that column from the referenced primary key.
|
|
28
28
|
- Bare relation decorators: inserts `entity: () => X`, unwrapping arrays and the old `Relation<T>` alias.
|
|
29
29
|
- `Relation<T>` becomes `T`, and its import goes with the last usage - `uql-orm` no longer exports it. A usage somewhere the codemod does not reach, such as a type alias, keeps the import and is reported.
|
|
30
|
-
- `@Transactional()` methods: drops the `@InjectQuerier() querier?: Querier` parameter and rewrites the body to `currentQuerier()`.
|
|
31
30
|
- `import 'reflect-metadata'` goes. Removing the package from your `package.json` is left to you.
|
|
32
31
|
- `tsconfig.json`: removes `experimentalDecorators` and `emitDecoratorMetadata`, keeping the rest of the file - comments and formatting included - exactly as written.
|
|
33
32
|
|
|
@@ -37,7 +36,7 @@ It reports rather than guesses, and exits non-zero when anything is left for you
|
|
|
37
36
|
|
|
38
37
|
- **`target: esnext`** is reported, not changed. Removing the line falls back to the compiler default (`es5` for `tsc`), and choosing a replacement means guessing which era the project targets. Any dated target works; `esnext` is the one where TypeScript emits decorator syntax untransformed.
|
|
39
38
|
- **A value inherited through `extends`** cannot be edited here, so the base config is reported instead.
|
|
40
|
-
- **`@Log()` and `@
|
|
39
|
+
- **`@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`** are reported and left in place. They no longer exist, and what to do instead is a judgement call - a `@Transactional()` method becomes a `pool.transaction(async (querier) => { ... })` around its body, and the codemod cannot know which pool.
|
|
41
40
|
- **Options it cannot read** (`@Field(sharedOptions)`, a spread, or a `@Field` never called at all) are left exactly as written. Replacing an argument it cannot parse would silently drop the options.
|
|
42
41
|
- **A property whose type it cannot map to a column** (a `Json<T>`, a vector, a union that disagrees with itself) is reported as `needs a decision`. Those always had to declare `type` by hand anyway, because `design:type` reported the useless `Object`/`Array` for them.
|
|
43
42
|
- **A branded string id**, e.g. `type UUID = \`${string}-${string}\``, gets `type: String` plus a `worth a look` note. That is a real fork: `String` generates a text column, `'uuid'` generates a native one, and only you know which the database has.
|
package/dist/transform.js
CHANGED
|
@@ -4,13 +4,16 @@ import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js
|
|
|
4
4
|
const FIELD_DECORATORS = new Set(['Field', 'Id']);
|
|
5
5
|
const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
|
|
6
6
|
/**
|
|
7
|
-
* Decorators that no longer exist,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* to do instead is a judgement call - inline the logging, drop the serialisation - so the usage is
|
|
11
|
-
* reported and left in place. Removing only the import would swap a clear error for a puzzling one.
|
|
7
|
+
* Decorators that no longer exist, mapped to what to do instead. None is removed for you: what to put
|
|
8
|
+
* in its place is a judgement call, and removing only the import would swap a clear error for a
|
|
9
|
+
* puzzling one.
|
|
12
10
|
*/
|
|
13
|
-
const REMOVED_DECORATORS = new
|
|
11
|
+
const REMOVED_DECORATORS = new Map([
|
|
12
|
+
['Log', 'delete it and its import'],
|
|
13
|
+
['Serialized', 'delete it and its import'],
|
|
14
|
+
['Transactional', 'wrap the body in `pool.transaction(async (querier) => { ... })`'],
|
|
15
|
+
['InjectQuerier', 'take the querier from the enclosing `pool.transaction()` callback'],
|
|
16
|
+
]);
|
|
14
17
|
function decoratorName(node) {
|
|
15
18
|
const call = ts.isCallExpression(node.expression) ? node.expression : undefined;
|
|
16
19
|
const target = call?.expression ?? node.expression;
|
|
@@ -143,36 +146,6 @@ function countRelationAlias(node, ctx) {
|
|
|
143
146
|
ctx.relationAlias.seen += 1;
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Rewrites an `@InjectQuerier()` parameter into a `currentQuerier()` call.
|
|
148
|
-
*
|
|
149
|
-
* The standard spec has no parameter decorators, so the querier travels through async-local storage and
|
|
150
|
-
* is read inside the body. Only the plain shape is rewritten - a named parameter in a method with a body.
|
|
151
|
-
* A default value or a destructured parameter is reported: the author was doing something the codemod
|
|
152
|
-
* would have to guess at.
|
|
153
|
-
*/
|
|
154
|
-
function rewriteInjectQuerier(method, ctx) {
|
|
155
|
-
for (const parameter of method.parameters) {
|
|
156
|
-
if (!ts.getDecorators(parameter)?.some((d) => decoratorName(d) === 'InjectQuerier')) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
if (!method.body || !ts.isIdentifier(parameter.name) || parameter.initializer) {
|
|
160
|
-
ctx.unresolved.push(`${ctx.describe(parameter)}: cannot rewrite this '@InjectQuerier()' parameter`);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
ctx.edits.push(removeFromList(method.parameters, parameter, method.getSourceFile()));
|
|
164
|
-
// The querier is read at the top of the body, keeping the name the parameter had.
|
|
165
|
-
const body = method.body;
|
|
166
|
-
const first = body.statements[0];
|
|
167
|
-
const indent = first ? ' '.repeat(first.getStart() - body.getSourceFile().getLineStarts()[ctx.lineOf(first)]) : '';
|
|
168
|
-
ctx.edits.push({
|
|
169
|
-
start: body.getStart() + 1,
|
|
170
|
-
end: body.getStart() + 1,
|
|
171
|
-
text: `\n${indent}const ${parameter.name.text} = currentQuerier();`,
|
|
172
|
-
});
|
|
173
|
-
ctx.imports.add('currentQuerier');
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
149
|
/** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
|
|
177
150
|
function removeStatement(node) {
|
|
178
151
|
const source = node.getSourceFile();
|
|
@@ -181,44 +154,29 @@ function removeStatement(node) {
|
|
|
181
154
|
return { start: node.getStart(), end: nextLineStart ?? node.getEnd(), text: '' };
|
|
182
155
|
}
|
|
183
156
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* - `InjectQuerier` goes with it, and its import would otherwise not compile: the standard spec has no
|
|
188
|
-
* parameter decorators, so `uql-orm` no longer exports one.
|
|
189
|
-
* - whatever a rewrite needs (`currentQuerier`) is added, extending an existing `uql-orm` import rather
|
|
190
|
-
* than adding a second one.
|
|
191
|
-
*
|
|
192
|
-
* One function because all three edit the same statements, and doing them separately meant an import
|
|
193
|
-
* could be dropped and re-added, or a name added to a statement another edit had already removed.
|
|
157
|
+
* Names `uql-orm` no longer exports and that the codemod does remove: only `Relation`, and only once
|
|
158
|
+
* every usage was unwrapped. A removed *decorator* keeps its import on purpose (see
|
|
159
|
+
* {@link REMOVED_DECORATORS}).
|
|
194
160
|
*/
|
|
195
|
-
function reconcileImports(source, ctx) {
|
|
196
|
-
// Dropping runs first and hands over the `uql-orm` import it left standing: adding a name to a
|
|
197
|
-
// statement the other pass had already removed is the bug that made these one step to begin with.
|
|
198
|
-
addNeededImports(source, ctx, dropDeadImports(source, ctx));
|
|
199
|
-
}
|
|
200
|
-
/** Names `uql-orm` no longer exports. `Relation` joins them only when every usage was rewritten. */
|
|
201
161
|
function deadImportNames(source, ctx) {
|
|
202
162
|
const { seen, unwrapped } = ctx.relationAlias;
|
|
203
163
|
if (seen > unwrapped) {
|
|
204
164
|
ctx.unresolved.push(`${source.fileName}: ${seen - unwrapped} 'Relation<T>' reference(s) are somewhere this codemod does ` +
|
|
205
165
|
'not rewrite; unwrap them and drop the import by hand');
|
|
206
166
|
}
|
|
207
|
-
return new Set(seen > 0 && seen === unwrapped ? ['
|
|
167
|
+
return new Set(seen > 0 && seen === unwrapped ? ['Relation'] : []);
|
|
208
168
|
}
|
|
209
169
|
/**
|
|
210
|
-
* Removes `import 'reflect-metadata'
|
|
211
|
-
*
|
|
170
|
+
* Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and any name
|
|
171
|
+
* `uql-orm` no longer exports.
|
|
212
172
|
*/
|
|
213
173
|
function dropDeadImports(source, ctx) {
|
|
214
174
|
const dead = deadImportNames(source, ctx);
|
|
215
|
-
let host;
|
|
216
175
|
for (const statement of source.statements) {
|
|
217
176
|
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
218
177
|
continue;
|
|
219
178
|
}
|
|
220
|
-
|
|
221
|
-
if (module === 'reflect-metadata' && !statement.importClause) {
|
|
179
|
+
if (statement.moduleSpecifier.text === 'reflect-metadata' && !statement.importClause) {
|
|
222
180
|
ctx.edits.push(removeStatement(statement));
|
|
223
181
|
continue;
|
|
224
182
|
}
|
|
@@ -234,25 +192,7 @@ function dropDeadImports(source, ctx) {
|
|
|
234
192
|
for (const element of dropped) {
|
|
235
193
|
ctx.edits.push(removeFromList(named.elements, element, source));
|
|
236
194
|
}
|
|
237
|
-
if (module === 'uql-orm') {
|
|
238
|
-
host = named;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return host;
|
|
242
|
-
}
|
|
243
|
-
/** Adds what the rewrites need, extending `host` when there is one rather than adding a second import. */
|
|
244
|
-
function addNeededImports(source, ctx, host) {
|
|
245
|
-
const missing = [...ctx.imports].filter((name) => !host?.elements.some((element) => element.name.text === name));
|
|
246
|
-
if (!missing.length) {
|
|
247
|
-
return;
|
|
248
195
|
}
|
|
249
|
-
if (host) {
|
|
250
|
-
const last = host.elements[host.elements.length - 1];
|
|
251
|
-
ctx.edits.push({ start: last.getEnd(), end: last.getEnd(), text: `, ${missing.join(', ')}` });
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
const position = source.statements[0]?.getStart() ?? 0;
|
|
255
|
-
ctx.edits.push({ start: position, end: position, text: `import { ${missing.join(', ')} } from 'uql-orm';\n` });
|
|
256
196
|
}
|
|
257
197
|
/** Everything the standard spec needs written onto one decorated property. */
|
|
258
198
|
function rewriteProperty(node, ctx) {
|
|
@@ -281,8 +221,9 @@ function reportRemovedDecorators(node, ctx) {
|
|
|
281
221
|
}
|
|
282
222
|
for (const decorator of ts.getDecorators(node) ?? []) {
|
|
283
223
|
const name = decoratorName(decorator);
|
|
284
|
-
|
|
285
|
-
|
|
224
|
+
const advice = name && REMOVED_DECORATORS.get(name);
|
|
225
|
+
if (advice) {
|
|
226
|
+
ctx.unresolved.push(`${ctx.describe(decorator)}: '@${name}()' was removed; ${advice}`);
|
|
286
227
|
}
|
|
287
228
|
}
|
|
288
229
|
}
|
|
@@ -293,24 +234,20 @@ export function transformFile(source, checker) {
|
|
|
293
234
|
edits: [],
|
|
294
235
|
unresolved: [],
|
|
295
236
|
notes: [],
|
|
296
|
-
imports: new Set(),
|
|
297
237
|
relationAlias: { seen: 0, unwrapped: 0 },
|
|
298
|
-
describe: (node) => `${source.fileName}:${
|
|
299
|
-
lineOf: (node) => source.getLineAndCharacterOfPosition(node.getStart()).line,
|
|
238
|
+
describe: (node) => `${source.fileName}:${lineOf(node) + 1}`,
|
|
300
239
|
};
|
|
240
|
+
const lineOf = (node) => source.getLineAndCharacterOfPosition(node.getStart()).line;
|
|
301
241
|
const visit = (node) => {
|
|
302
242
|
reportRemovedDecorators(node, ctx);
|
|
303
243
|
countRelationAlias(node, ctx);
|
|
304
244
|
if (ts.isPropertyDeclaration(node)) {
|
|
305
245
|
rewriteProperty(node, ctx);
|
|
306
246
|
}
|
|
307
|
-
else if (ts.isMethodDeclaration(node)) {
|
|
308
|
-
rewriteInjectQuerier(node, ctx);
|
|
309
|
-
}
|
|
310
247
|
ts.forEachChild(node, visit);
|
|
311
248
|
};
|
|
312
249
|
visit(source);
|
|
313
|
-
|
|
250
|
+
dropDeadImports(source, ctx);
|
|
314
251
|
return {
|
|
315
252
|
fileName: source.fileName,
|
|
316
253
|
text: applyEdits(source.getFullText(), ctx.edits),
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "uql-codemod",
|
|
3
3
|
"description": "Migrates uql-orm entities from legacy decorators to the TC39 standard spec",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.25.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=24"
|
|
@@ -35,9 +35,24 @@
|
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
|
37
37
|
"uql",
|
|
38
|
+
"uql-orm",
|
|
39
|
+
"orm",
|
|
38
40
|
"codemod",
|
|
39
41
|
"decorators",
|
|
40
|
-
"
|
|
42
|
+
"tc39",
|
|
43
|
+
"migration",
|
|
44
|
+
"migrate",
|
|
45
|
+
"refactor",
|
|
46
|
+
"typescript",
|
|
47
|
+
"ts",
|
|
48
|
+
"javascript",
|
|
49
|
+
"database",
|
|
50
|
+
"sql",
|
|
51
|
+
"postgres",
|
|
52
|
+
"postgresql",
|
|
53
|
+
"mysql",
|
|
54
|
+
"sqlite",
|
|
55
|
+
"mongodb"
|
|
41
56
|
],
|
|
42
57
|
"publishConfig": {
|
|
43
58
|
"access": "public"
|