uql-codemod 0.26.0 → 0.28.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 +6 -6
- package/dist/transform.js +73 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,11 +14,11 @@ npx uql-codemod --dry-run
|
|
|
14
14
|
npx uql-codemod
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
| Flag
|
|
18
|
-
|
|
17
|
+
| Flag | |
|
|
18
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
19
19
|
| `--project=<path>` | `tsconfig.json` to read, defaulting to `./tsconfig.json`. It is a real program and not a parser, because the codemod writes down the types `design:type` used to report at runtime, so a config that covers your entities is what makes the run work. |
|
|
20
|
-
| `--dry-run`
|
|
21
|
-
| `--include=<a,b>`
|
|
20
|
+
| `--dry-run` | Report what would change and touch nothing. |
|
|
21
|
+
| `--include=<a,b>` | Only files whose path contains one of these fragments. |
|
|
22
22
|
|
|
23
23
|
Only the `--flag=value` form is read, and anything unrecognised is an error: a misspelled `--dry-run` would otherwise rewrite the project for real. It exits `0` when nothing is left for you, `1` when something is, and `2` when it could not start at all - a bad argument, or a project it cannot read.
|
|
24
24
|
|
|
@@ -39,11 +39,11 @@ It reports rather than guesses, and exits non-zero when anything is left for you
|
|
|
39
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.
|
|
40
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.
|
|
41
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.
|
|
42
|
-
- **A branded string id**, e.g. `type UUID = \`${string}-${string}\``, gets `type: String`
|
|
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.
|
|
43
43
|
|
|
44
44
|
## After it runs
|
|
45
45
|
|
|
46
|
-
`tsc` is the rest of the migration, and that is the point: the annotations the codemod inserts are now
|
|
46
|
+
`tsc` is the rest of the migration, and that is the point: the annotations the codemod inserts are now _checked_ against the properties they describe, so anything it got wrong is a compile error rather than a silently wrong column. See the [upgrade guide](https://uql-orm.dev/upgrade-guide).
|
|
47
47
|
|
|
48
48
|
## Why TypeScript 6
|
|
49
49
|
|
package/dist/transform.js
CHANGED
|
@@ -14,6 +14,15 @@ const REMOVED_DECORATORS = new Map([
|
|
|
14
14
|
['Transactional', 'wrap the body in `pool.transaction(async (querier) => { ... })`'],
|
|
15
15
|
['InjectQuerier', 'take the querier from the enclosing `pool.transaction()` callback'],
|
|
16
16
|
]);
|
|
17
|
+
/**
|
|
18
|
+
* Exports that no longer exist, mapped to what to do instead. Reported rather than rewritten, for the
|
|
19
|
+
* same reason as the decorators: which pool belongs at a given call site is a judgement call.
|
|
20
|
+
*/
|
|
21
|
+
const REMOVED_EXPORTS = new Map([
|
|
22
|
+
['setQuerierPool', 'pass the pool where it is used: `createFetchHandler({ pool })`, `querierMiddleware({ pool })`'],
|
|
23
|
+
['getQuerierPool', 'take the pool from the module that builds it, or from Nest DI'],
|
|
24
|
+
['getQuerier', 'use `pool.withQuerier(...)` / `pool.transaction(...)`, which release the connection'],
|
|
25
|
+
]);
|
|
17
26
|
function decoratorName(node) {
|
|
18
27
|
const call = ts.isCallExpression(node.expression) ? node.expression : undefined;
|
|
19
28
|
const target = call?.expression ?? node.expression;
|
|
@@ -227,6 +236,65 @@ function reportRemovedDecorators(node, ctx) {
|
|
|
227
236
|
}
|
|
228
237
|
}
|
|
229
238
|
}
|
|
239
|
+
/** Names an export that no longer exists, where it is imported from the package. */
|
|
240
|
+
function reportRemovedExports(node, ctx) {
|
|
241
|
+
if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (node.moduleSpecifier.text !== 'uql-orm') {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const bindings = node.importClause?.namedBindings;
|
|
248
|
+
if (!bindings || !ts.isNamedImports(bindings)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
for (const element of bindings.elements) {
|
|
252
|
+
const name = (element.propertyName ?? element.name).text;
|
|
253
|
+
const advice = REMOVED_EXPORTS.get(name);
|
|
254
|
+
if (advice) {
|
|
255
|
+
ctx.unresolved.push(`${ctx.describe(element)}: '${name}' was removed; ${advice}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Whether this file's `raw` is the ORM's, so another library's function of that name is left alone. */
|
|
260
|
+
function importsRaw(source) {
|
|
261
|
+
return source.statements.some((statement) => {
|
|
262
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
if (statement.moduleSpecifier.text !== 'uql-orm') {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
const bindings = statement.importClause?.namedBindings;
|
|
269
|
+
return (!!bindings &&
|
|
270
|
+
ts.isNamedImports(bindings) &&
|
|
271
|
+
bindings.elements.some((element) => (element.propertyName ?? element.name).text === 'raw'));
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Rewrites `raw('sql')` into the tagged template, and a second alias argument into `.as()`. Only a
|
|
276
|
+
* string-literal first argument qualifies: the callback form is unchanged, and a computed string is
|
|
277
|
+
* left alone because a template cannot be built from a value that is not known here.
|
|
278
|
+
*/
|
|
279
|
+
function rewriteRawCall(node, ctx, source) {
|
|
280
|
+
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== 'raw') {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const [expression, alias] = node.arguments;
|
|
284
|
+
if (!expression || !ts.isStringLiteral(expression) || node.arguments.length > 2) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const suffix = alias ? `.as(${alias.getText(source)})` : '';
|
|
288
|
+
ctx.edits.push({
|
|
289
|
+
start: node.getStart(source),
|
|
290
|
+
end: node.getEnd(),
|
|
291
|
+
text: `raw\`${escapeForTemplate(expression.text)}\`${suffix}`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/** A backtick or a `${` inside the old string literal would end or interpolate the template. */
|
|
295
|
+
function escapeForTemplate(text) {
|
|
296
|
+
return text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
297
|
+
}
|
|
230
298
|
/** Rewrites one source file for the standard decorator spec. */
|
|
231
299
|
export function transformFile(source, checker) {
|
|
232
300
|
const ctx = {
|
|
@@ -238,9 +306,14 @@ export function transformFile(source, checker) {
|
|
|
238
306
|
describe: (node) => `${source.fileName}:${lineOf(node) + 1}`,
|
|
239
307
|
};
|
|
240
308
|
const lineOf = (node) => source.getLineAndCharacterOfPosition(node.getStart()).line;
|
|
309
|
+
const rewritesRaw = importsRaw(source);
|
|
241
310
|
const visit = (node) => {
|
|
242
311
|
reportRemovedDecorators(node, ctx);
|
|
312
|
+
reportRemovedExports(node, ctx);
|
|
243
313
|
countRelationAlias(node, ctx);
|
|
314
|
+
if (rewritesRaw) {
|
|
315
|
+
rewriteRawCall(node, ctx, source);
|
|
316
|
+
}
|
|
244
317
|
if (ts.isPropertyDeclaration(node)) {
|
|
245
318
|
rewriteProperty(node, ctx);
|
|
246
319
|
}
|