uql-codemod 0.35.0 → 0.35.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.
- package/README.md +23 -28
- package/dist/transform.js +69 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
# uql-codemod
|
|
9
9
|
|
|
10
|
-
Rewrites [uql-orm](https://uql-orm.dev)
|
|
10
|
+
Rewrites a [uql-orm](https://uql-orm.dev) project across the breaking changes that can be made mechanically. Dry run first:
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
13
|
npx uql-codemod --dry-run
|
|
@@ -20,42 +20,37 @@ npx uql-codemod
|
|
|
20
20
|
| `--dry-run` | Report what would change and touch nothing. |
|
|
21
21
|
| `--include=<a,b>` | Only files whose path contains one of these fragments. |
|
|
22
22
|
|
|
23
|
-
Only
|
|
23
|
+
Only `--flag=value` is read, and anything unrecognised is an error. It exits `0` when nothing is left for you, `1` when something is, and `2` when it could not start. Edits are spliced into the text, so everything it does not touch stays byte for byte as written.
|
|
24
24
|
|
|
25
25
|
## What it rewrites
|
|
26
26
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- `
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
- `
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
- **An export that no longer exists** (`setQuerierPool`, `getQuerier`, `augmentWhere`, `RelationMappedBy`, ...) is reported with what replaces it. A `$where` given an id, a list of them or a bare `raw()` needs no report: it is a compile error on that value, fixed by naming the key (`{ id: [1, 2] }`) or wrapping the raw in `$and`.
|
|
47
|
-
- **A key written under a computed name** (`@Id() [KEY]?: string`) is reported: the brand has to spell the name out, and only you know what that expression evaluates to.
|
|
48
|
-
- **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.
|
|
49
|
-
- **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.
|
|
50
|
-
- **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'` a native one, and only you know which the database has.
|
|
27
|
+
- `type` on a `@Field`/`@Id` that has none, from the property's declared type; not beside `references`, whose column comes from the key it points at.
|
|
28
|
+
- `entity: () => X` on a bare relation decorator, and `Relation<T>` to `T`, its import going with the last use.
|
|
29
|
+
- `virtual` to `computed`, and `raw('sql')` to the tagged template.
|
|
30
|
+
- Members named by string to key-map callbacks: `mappedBy: 'author'` to `(post) => post.author`, and `@Index`, `include`, `references` and `@Entity`/`defineEntity`'s `indexes` and `hooks` likewise: `@Index((post) => [post.title])`.
|
|
31
|
+
- An aggregate's `$agg` to `$select`, and each field it or `$text`'s `$fields` names to a key: `{ $sum: { amount: true } }`.
|
|
32
|
+
- Renamed exports, at the import and every use: `QueryWhereMap` to `QueryWhere`, `RelationKeyMap` to `KeyMap`, and the removed driver classes to the one each extended: `PgDialect`/`NeonDialect`/`PgliteDialect` to `PostgresDialect`, `CrdbQuerier`/`NeonQuerier` to `PgQuerier`, `LibsqlQuerier`/`TursoQuerier` to `HranaQuerier`, `MySql2Dialect` to `MySqlDialect`, `MongodbNativeDialect` to `MongoDialect`, moving the import to the entry that exports it.
|
|
33
|
+
- The `[idKey]?: '...'` brand, and its import, on a key not called `id`, `_id` or `uuid` and on every composite.
|
|
34
|
+
- `import 'reflect-metadata'`, and `experimentalDecorators`/`emitDecoratorMetadata` in `tsconfig.json`, comments and formatting kept.
|
|
35
|
+
|
|
36
|
+
## What it leaves to you
|
|
37
|
+
|
|
38
|
+
Reported with what to do, never guessed:
|
|
39
|
+
|
|
40
|
+
- `target: esnext`, the one target that leaves decorators untransformed, and a value inherited through `extends`.
|
|
41
|
+
- `@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`: a `@Transactional()` method becomes a `pool.transaction()` around its body, and only you know which pool.
|
|
42
|
+
- An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...).
|
|
43
|
+
- A value it cannot read: options passed as a variable or a spread, a list that is not a literal, a key under a computed name.
|
|
44
|
+
- A property whose type maps to no column (`Json<T>`, a vector, a mixed union).
|
|
45
|
+
- A branded string id, written as `type: String` to keep the column you have, with a note: `'uuid'` is a native column, and a migration.
|
|
51
46
|
|
|
52
47
|
## After it runs
|
|
53
48
|
|
|
54
|
-
`tsc` is the rest of the migration
|
|
49
|
+
`tsc` is the rest of the migration: what the codemod wrote is checked against the property it describes, so anything it got wrong is a compile error. See the [upgrade guide](https://uql-orm.dev/upgrade-guide).
|
|
55
50
|
|
|
56
51
|
## Why TypeScript 6
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
Reading each property's type needs the checker, so TypeScript is a runtime dependency. 7's entry point exports no compiler API yet and has no standalone parser, which the `tsconfig.json` rewrite needs to keep your formatting; revisit once that API is stable.
|
|
59
54
|
|
|
60
55
|
## License
|
|
61
56
|
|
package/dist/transform.js
CHANGED
|
@@ -29,21 +29,24 @@ const REMOVED_EXPORTS = new Map([
|
|
|
29
29
|
['RelationKeyMapper', 'it is `(keys: KeyMap<E>) => Key<E>` now'],
|
|
30
30
|
['augmentWhere', 'spread the two maps: `{ ...where, ...extra }`'],
|
|
31
31
|
['buildQueryWhereAsMap', 'a `$where` is a map already; name the key for ids: `{ id: [1, 2] }`'],
|
|
32
|
-
['PgDialect', 'the pools build `PostgresDialect` (`uql-orm/postgres`); name that where a dialect is typed'],
|
|
33
|
-
['NeonDialect', 'the pools build `PostgresDialect` (`uql-orm/postgres`); name that where a dialect is typed'],
|
|
34
|
-
['PgliteDialect', 'the pools build `PostgresDialect` (`uql-orm/postgres`); name that where a dialect is typed'],
|
|
35
|
-
['MySql2Dialect', 'the pool builds `MySqlDialect` (`uql-orm/mysql`)'],
|
|
36
|
-
['MongodbNativeDialect', 'the pool builds `MongoDialect` (`uql-orm/mongo`)'],
|
|
37
32
|
['AbstractPgQuerier', 'every pg-compatible pool returns `PgQuerier` (`uql-orm/postgres`), which is concrete'],
|
|
38
|
-
['CrdbQuerier', 'every pg-compatible pool returns `PgQuerier` (`uql-orm/postgres`)'],
|
|
39
|
-
['NeonQuerier', 'every pg-compatible pool returns `PgQuerier` (`uql-orm/postgres`)'],
|
|
40
|
-
['LibsqlQuerier', 'the libSQL and Turso pools return `HranaQuerier` (`uql-orm/sqlite`)'],
|
|
41
|
-
['TursoQuerier', 'the libSQL and Turso pools return `HranaQuerier` (`uql-orm/sqlite`)'],
|
|
42
33
|
]);
|
|
43
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Exports renamed and nothing else - the driver classes were empty subclasses - so the import and every
|
|
36
|
+
* use follow, the import moving to `from` where the new name lives in another entry.
|
|
37
|
+
*/
|
|
44
38
|
const RENAMED_EXPORTS = new Map([
|
|
45
|
-
['QueryWhereMap', 'QueryWhere'],
|
|
46
|
-
['RelationKeyMap', 'KeyMap'],
|
|
39
|
+
['QueryWhereMap', { to: 'QueryWhere' }],
|
|
40
|
+
['RelationKeyMap', { to: 'KeyMap' }],
|
|
41
|
+
['PgDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
42
|
+
['NeonDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
43
|
+
['PgliteDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
44
|
+
['CrdbQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
45
|
+
['NeonQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
46
|
+
['MySql2Dialect', { to: 'MySqlDialect', from: 'uql-orm/mysql' }],
|
|
47
|
+
['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/mongo' }],
|
|
48
|
+
['LibsqlQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
49
|
+
['TursoQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
47
50
|
]);
|
|
48
51
|
/** A node's decorators, or none where it is a kind that cannot carry them. */
|
|
49
52
|
function decoratorsOf(node) {
|
|
@@ -433,12 +436,11 @@ function brandIdKey(node, ctx) {
|
|
|
433
436
|
* Imports `idKey` where a brand was written, into the file's own `uql-orm` import. Reported instead
|
|
434
437
|
* where there is none: the package may be imported under a path this codemod does not recognise.
|
|
435
438
|
*/
|
|
436
|
-
function addIdKeyImport(source, ctx) {
|
|
437
|
-
|
|
438
|
-
if (imported.some((element) => importedName(element) === 'idKey')) {
|
|
439
|
+
function addIdKeyImport(source, rewritten, ctx) {
|
|
440
|
+
if (uqlImports(source).some((element) => importedName(element) === 'idKey')) {
|
|
439
441
|
return;
|
|
440
442
|
}
|
|
441
|
-
const
|
|
443
|
+
const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration))?.elements[0];
|
|
442
444
|
if (!anchor) {
|
|
443
445
|
ctx.unresolved.push(`${source.fileName}: import 'idKey' from 'uql-orm' for the brand(s) written here`);
|
|
444
446
|
return;
|
|
@@ -493,24 +495,49 @@ function reportRemovedExports(source, ctx) {
|
|
|
493
495
|
}
|
|
494
496
|
}
|
|
495
497
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
+
* Rewrites each import naming a {@link RENAMED_EXPORTS} export, renaming every use of it: a name moving
|
|
499
|
+
* entries gets an import from its new one, and a name the file already imports is dropped. Returns the
|
|
500
|
+
* rewritten imports, which nothing else may edit.
|
|
498
501
|
*/
|
|
499
502
|
function renameExports(source, ctx) {
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
503
|
+
const imports = uqlImportDeclarations(source, true);
|
|
504
|
+
const kept = imports.flatMap(({ elements }) => elements).filter((element) => !renamedTo(element));
|
|
505
|
+
const bound = new Set(kept.map((element) => element.name.text));
|
|
506
|
+
const rewritten = new Set();
|
|
507
|
+
for (const { declaration, entry, elements } of imports) {
|
|
508
|
+
if (!elements.some(renamedTo)) {
|
|
504
509
|
continue;
|
|
505
510
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
511
|
+
const byEntry = new Map([[entry, []]]);
|
|
512
|
+
for (const element of elements) {
|
|
513
|
+
const rename = renamedTo(element);
|
|
514
|
+
const local = element.propertyName || !rename ? element.name.text : rename.to;
|
|
515
|
+
if (rename && !element.propertyName) {
|
|
516
|
+
ctx.edits.push(...usesOf(source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
|
|
517
|
+
}
|
|
518
|
+
if (rename && bound.has(local)) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
bound.add(local);
|
|
522
|
+
const target = rename?.from ?? entry;
|
|
523
|
+
const text = rename
|
|
524
|
+
? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
|
|
525
|
+
: element.getText();
|
|
526
|
+
byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
|
|
509
527
|
}
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
528
|
+
const typeOnly = declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
|
|
529
|
+
const keyword = typeOnly ? 'import type' : 'import';
|
|
530
|
+
const quote = declaration.moduleSpecifier.getText()[0];
|
|
531
|
+
const statements = [...byEntry]
|
|
532
|
+
.filter(([, names]) => names.length)
|
|
533
|
+
.map(([from, names]) => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`);
|
|
534
|
+
ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
|
|
535
|
+
rewritten.add(declaration);
|
|
513
536
|
}
|
|
537
|
+
return rewritten;
|
|
538
|
+
}
|
|
539
|
+
function renamedTo(element) {
|
|
540
|
+
return RENAMED_EXPORTS.get(importedName(element));
|
|
514
541
|
}
|
|
515
542
|
/** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
|
|
516
543
|
function usesOf(source, name, checker) {
|
|
@@ -528,23 +555,25 @@ function usesOf(source, name, checker) {
|
|
|
528
555
|
return uses;
|
|
529
556
|
}
|
|
530
557
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
* into one of those would land on an entry that may not export it.
|
|
558
|
+
* The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
|
|
559
|
+
* for what is read by name only: `idKey` written into one would land on an entry that may not export it.
|
|
534
560
|
*/
|
|
535
|
-
function
|
|
536
|
-
return source.statements.flatMap((
|
|
537
|
-
if (!ts.isImportDeclaration(
|
|
561
|
+
function uqlImportDeclarations(source, entries = false) {
|
|
562
|
+
return source.statements.flatMap((declaration) => {
|
|
563
|
+
if (!ts.isImportDeclaration(declaration) || !ts.isStringLiteral(declaration.moduleSpecifier)) {
|
|
538
564
|
return [];
|
|
539
565
|
}
|
|
540
|
-
const
|
|
541
|
-
if (
|
|
566
|
+
const entry = declaration.moduleSpecifier.text;
|
|
567
|
+
if (entry !== 'uql-orm' && !(entries && entry.startsWith('uql-orm/'))) {
|
|
542
568
|
return [];
|
|
543
569
|
}
|
|
544
|
-
const bindings =
|
|
545
|
-
return bindings && ts.isNamedImports(bindings) ? [
|
|
570
|
+
const bindings = declaration.importClause?.namedBindings;
|
|
571
|
+
return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
|
|
546
572
|
});
|
|
547
573
|
}
|
|
574
|
+
function uqlImports(source, entries = false) {
|
|
575
|
+
return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
|
|
576
|
+
}
|
|
548
577
|
/** The name an import specifier brings in, which is the original one where it was renamed. */
|
|
549
578
|
function importedName(element) {
|
|
550
579
|
return (element.propertyName ?? element.name).text;
|
|
@@ -608,11 +637,11 @@ export function transformFile(source, checker) {
|
|
|
608
637
|
ts.forEachChild(node, visit);
|
|
609
638
|
};
|
|
610
639
|
visit(source);
|
|
611
|
-
renameExports(source, ctx);
|
|
640
|
+
const rewritten = renameExports(source, ctx);
|
|
612
641
|
reportRemovedExports(source, ctx);
|
|
613
642
|
dropDeadImports(source, ctx);
|
|
614
643
|
if (branded) {
|
|
615
|
-
addIdKeyImport(source, ctx);
|
|
644
|
+
addIdKeyImport(source, rewritten, ctx);
|
|
616
645
|
}
|
|
617
646
|
return {
|
|
618
647
|
fileName: source.fileName,
|