uql-codemod 0.34.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 +25 -30
- package/dist/keyMaps.d.ts +25 -0
- package/dist/keyMaps.js +112 -0
- package/dist/transform.js +242 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,55 +7,50 @@
|
|
|
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
|
|
14
14
|
npx uql-codemod
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
| Flag |
|
|
18
|
-
| ------------------ |
|
|
19
|
-
| `--project=<path>` | `tsconfig.json` to read,
|
|
20
|
-
| `--dry-run` | Report what would change and touch nothing.
|
|
21
|
-
| `--include=<a,b>` | Only files whose path contains one of these fragments.
|
|
17
|
+
| Flag | |
|
|
18
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `--project=<path>` | The `tsconfig.json` to read, `./tsconfig.json` by default. It has to cover your entities: the type checker is what says each property's type. |
|
|
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
|
-
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
|
-
- `
|
|
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.
|
|
34
35
|
|
|
35
|
-
## What it
|
|
36
|
+
## What it leaves to you
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
Reported with what to do, never guessed:
|
|
38
39
|
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
- **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.
|
|
46
|
-
- **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.
|
|
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.
|
|
47
46
|
|
|
48
47
|
## After it runs
|
|
49
48
|
|
|
50
|
-
`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).
|
|
51
50
|
|
|
52
51
|
## Why TypeScript 6
|
|
53
52
|
|
|
54
|
-
TypeScript is a
|
|
55
|
-
|
|
56
|
-
TypeScript 7 is not a version bump away. Its `typescript` entry point exports only the version number, and the compiler API moved to `typescript/unstable/*` in a different shape. The gap that matters is the parser: 7 has no standalone one, and `ts.parseJsonText` is what lets the `tsconfig.json` rewrite keep your comments and formatting instead of reserialising the file.
|
|
57
|
-
|
|
58
|
-
Staying on 6 costs nothing. This is a one-shot tool for a spec `uql-orm` no longer supports, so it never needs a new language feature, and 7 reads `experimentalDecorators` projects perfectly well when the time comes. Worth revisiting once that API drops the `unstable/` prefix and regains a parser.
|
|
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
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { Edit } from './edits.js';
|
|
3
|
+
/** A callback's parameter, named after the class it reads: `Post` -> `post`. */
|
|
4
|
+
export declare function paramFor(className: string | undefined): string;
|
|
5
|
+
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
6
|
+
export declare function propertyKey(name: ts.PropertyName): string | undefined;
|
|
7
|
+
/** The initializer of the property `name` in an object literal, however its key is written. */
|
|
8
|
+
export declare function propertyValue(node: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined;
|
|
9
|
+
/** `'amount'` or `['title', 'body']` -> `{ amount: true }`, the key map a statement names fields by. */
|
|
10
|
+
export declare function fieldKeysEdit(value: ts.Expression): Edit | undefined;
|
|
11
|
+
/** Whether `value` is already a function, the form every rewrite here produces. */
|
|
12
|
+
export declare function isCallback(value: ts.Expression): boolean;
|
|
13
|
+
/** `['title', { column: 'createdAt' }]` -> `(post) => [post.title, { column: post.createdAt }]`. */
|
|
14
|
+
export declare function keyListEdit(list: ts.Expression, param: string): Edit | undefined;
|
|
15
|
+
/** `'author'` -> `(post) => post.author`, `param` being the relation's target. */
|
|
16
|
+
export declare function mappedByEdit(value: ts.Expression, param: string): Edit | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* `[{ local: 'customerCode', foreign: 'code' }]` -> `(order, customer) => [{ local: order.customerCode,
|
|
19
|
+
* foreign: customer.code }]`. A self-relation would give both parameters one name, so it reads
|
|
20
|
+
* `(local, foreign)` instead.
|
|
21
|
+
*/
|
|
22
|
+
export declare function referencesEdit(value: ts.Expression, owner: string, target: string): Edit | undefined;
|
|
23
|
+
/** The class an `entity: () => Post` getter returns, as written. */
|
|
24
|
+
export declare function entityGetterTarget(relation: ts.ObjectLiteralExpression): string | undefined;
|
|
25
|
+
export declare function replaced(node: ts.Node, text: string): Edit;
|
package/dist/keyMaps.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
/** A callback's parameter, named after the class it reads: `Post` -> `post`. */
|
|
3
|
+
export function paramFor(className) {
|
|
4
|
+
return className ? className[0].toLowerCase() + className.slice(1) : 'entity';
|
|
5
|
+
}
|
|
6
|
+
function isIdentifierName(key) {
|
|
7
|
+
return /^[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*$/u.test(key);
|
|
8
|
+
}
|
|
9
|
+
/** `post.title`, or `post['first-name']` for a key that is no identifier. */
|
|
10
|
+
function memberAccess(param, key) {
|
|
11
|
+
return isIdentifierName(key) ? `${param}.${key}` : `${param}['${key}']`;
|
|
12
|
+
}
|
|
13
|
+
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
14
|
+
export function propertyKey(name) {
|
|
15
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
16
|
+
}
|
|
17
|
+
/** The initializer of the property `name` in an object literal, however its key is written. */
|
|
18
|
+
export function propertyValue(node, name) {
|
|
19
|
+
const property = node.properties.find((prop) => ts.isPropertyAssignment(prop) && propertyKey(prop.name) === name);
|
|
20
|
+
return property?.initializer;
|
|
21
|
+
}
|
|
22
|
+
/** `'amount'` or `['title', 'body']` -> `{ amount: true }`, the key map a statement names fields by. */
|
|
23
|
+
export function fieldKeysEdit(value) {
|
|
24
|
+
const names = ts.isArrayLiteralExpression(value) ? value.elements : [value];
|
|
25
|
+
if (!names.every((name) => ts.isStringLiteralLike(name))) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const keys = names.map(({ text }) => `${isIdentifierName(text) ? text : `'${text}'`}: true`);
|
|
29
|
+
return replaced(value, `{ ${keys.join(', ')} }`);
|
|
30
|
+
}
|
|
31
|
+
/** Whether `value` is already a function, the form every rewrite here produces. */
|
|
32
|
+
export function isCallback(value) {
|
|
33
|
+
return ts.isArrowFunction(value) || ts.isFunctionExpression(value);
|
|
34
|
+
}
|
|
35
|
+
/** `['title', { column: 'createdAt' }]` -> `(post) => [post.title, { column: post.createdAt }]`. */
|
|
36
|
+
export function keyListEdit(list, param) {
|
|
37
|
+
const text = ts.isArrayLiteralExpression(list)
|
|
38
|
+
? spliced(list, list.elements.map((entry) => [entry, keyListEntry(entry, param)]))
|
|
39
|
+
: undefined;
|
|
40
|
+
return callbackEdit(list, param, text);
|
|
41
|
+
}
|
|
42
|
+
/** `'author'` -> `(post) => post.author`, `param` being the relation's target. */
|
|
43
|
+
export function mappedByEdit(value, param) {
|
|
44
|
+
return callbackEdit(value, param, ts.isStringLiteralLike(value) ? memberAccess(param, value.text) : undefined);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `[{ local: 'customerCode', foreign: 'code' }]` -> `(order, customer) => [{ local: order.customerCode,
|
|
48
|
+
* foreign: customer.code }]`. A self-relation would give both parameters one name, so it reads
|
|
49
|
+
* `(local, foreign)` instead.
|
|
50
|
+
*/
|
|
51
|
+
export function referencesEdit(value, owner, target) {
|
|
52
|
+
const [local, foreign] = owner === target ? ['local', 'foreign'] : [owner, target];
|
|
53
|
+
const text = ts.isArrayLiteralExpression(value)
|
|
54
|
+
? spliced(value, value.elements.map((pair) => [pair, referenceText(pair, local, foreign)]))
|
|
55
|
+
: undefined;
|
|
56
|
+
return callbackEdit(value, `${local}, ${foreign}`, text);
|
|
57
|
+
}
|
|
58
|
+
/** The class an `entity: () => Post` getter returns, as written. */
|
|
59
|
+
export function entityGetterTarget(relation) {
|
|
60
|
+
const getter = propertyValue(relation, 'entity');
|
|
61
|
+
return getter && ts.isArrowFunction(getter) && ts.isIdentifier(getter.body) ? getter.body.text : undefined;
|
|
62
|
+
}
|
|
63
|
+
/** An expression entry (`raw\`...\``) and a `column` that is not a string stay as written. */
|
|
64
|
+
function keyListEntry(entry, param) {
|
|
65
|
+
if (ts.isStringLiteralLike(entry)) {
|
|
66
|
+
return memberAccess(param, entry.text);
|
|
67
|
+
}
|
|
68
|
+
if (ts.isTaggedTemplateExpression(entry) || ts.isCallExpression(entry)) {
|
|
69
|
+
return entry.getText();
|
|
70
|
+
}
|
|
71
|
+
if (!ts.isObjectLiteralExpression(entry)) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const column = propertyValue(entry, 'column');
|
|
75
|
+
return column && ts.isStringLiteralLike(column)
|
|
76
|
+
? spliced(entry, [[column, memberAccess(param, column.text)]])
|
|
77
|
+
: entry.getText();
|
|
78
|
+
}
|
|
79
|
+
function referenceText(pair, local, foreign) {
|
|
80
|
+
if (!ts.isObjectLiteralExpression(pair)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const localKey = propertyValue(pair, 'local');
|
|
84
|
+
const foreignKey = propertyValue(pair, 'foreign');
|
|
85
|
+
return localKey && foreignKey && ts.isStringLiteralLike(localKey) && ts.isStringLiteralLike(foreignKey)
|
|
86
|
+
? spliced(pair, [
|
|
87
|
+
[localKey, memberAccess(local, localKey.text)],
|
|
88
|
+
[foreignKey, memberAccess(foreign, foreignKey.text)],
|
|
89
|
+
])
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
92
|
+
/** `node`'s text with each part swapped for its own, everything between kept as written. */
|
|
93
|
+
function spliced(node, parts) {
|
|
94
|
+
if (!parts.every((part) => part[1] !== undefined)) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const start = node.getStart();
|
|
98
|
+
const source = node.getText();
|
|
99
|
+
const sorted = [...parts].sort(([a], [b]) => a.getStart() - b.getStart());
|
|
100
|
+
const { text, at } = sorted.reduce((acc, [part, replacement]) => ({
|
|
101
|
+
text: acc.text + source.slice(acc.at, part.getStart() - start) + replacement,
|
|
102
|
+
at: part.getEnd() - start,
|
|
103
|
+
}), { text: '', at: 0 });
|
|
104
|
+
return text + source.slice(at);
|
|
105
|
+
}
|
|
106
|
+
/** `value` replaced by `(params) => body`, or nothing when the body could not be read. */
|
|
107
|
+
function callbackEdit(value, params, body) {
|
|
108
|
+
return body === undefined ? undefined : replaced(value, `(${params}) => ${body}`);
|
|
109
|
+
}
|
|
110
|
+
export function replaced(node, text) {
|
|
111
|
+
return { start: node.getStart(), end: node.getEnd(), text };
|
|
112
|
+
}
|
package/dist/transform.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { applyEdits, removeFromList } from './edits.js';
|
|
3
3
|
import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
|
|
4
|
+
import { entityGetterTarget, fieldKeysEdit, isCallback, keyListEdit, mappedByEdit, paramFor, propertyKey, propertyValue, referencesEdit, replaced, } from './keyMaps.js';
|
|
4
5
|
const FIELD_DECORATORS = new Set(['Field', 'Id']);
|
|
5
6
|
const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
|
|
6
7
|
/**
|
|
@@ -22,20 +23,30 @@ const REMOVED_EXPORTS = new Map([
|
|
|
22
23
|
['setQuerierPool', 'pass the pool where it is used: `createFetchHandler({ pool })`, `querierMiddleware({ pool })`'],
|
|
23
24
|
['getQuerierPool', 'take the pool from the module that builds it, or from Nest DI'],
|
|
24
25
|
['getQuerier', 'use `pool.withQuerier(...)` / `pool.transaction(...)`, which release the connection'],
|
|
25
|
-
['QueryWhereMap', 'it is `QueryWhere` now'],
|
|
26
26
|
['QueryWhereFieldMap', 'use `QueryWhere`'],
|
|
27
|
+
['QueryStreamProjected', 'use `QueryProjected`'],
|
|
28
|
+
['RelationMappedBy', "a 'mappedBy' is `(keys: KeyMap<E>) => Key<E>` now"],
|
|
29
|
+
['RelationKeyMapper', 'it is `(keys: KeyMap<E>) => Key<E>` now'],
|
|
27
30
|
['augmentWhere', 'spread the two maps: `{ ...where, ...extra }`'],
|
|
28
31
|
['buildQueryWhereAsMap', 'a `$where` is a map already; name the key for ids: `{ id: [1, 2] }`'],
|
|
29
|
-
['PgDialect', 'the pools build `PostgresDialect` (`uql-orm/postgres`); name that where a dialect is typed'],
|
|
30
|
-
['NeonDialect', 'the pools build `PostgresDialect` (`uql-orm/postgres`); name that where a dialect is typed'],
|
|
31
|
-
['PgliteDialect', 'the pools build `PostgresDialect` (`uql-orm/postgres`); name that where a dialect is typed'],
|
|
32
|
-
['MySql2Dialect', 'the pool builds `MySqlDialect` (`uql-orm/mysql`)'],
|
|
33
|
-
['MongodbNativeDialect', 'the pool builds `MongoDialect` (`uql-orm/mongo`)'],
|
|
34
32
|
['AbstractPgQuerier', 'every pg-compatible pool returns `PgQuerier` (`uql-orm/postgres`), which is concrete'],
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
]);
|
|
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
|
+
*/
|
|
38
|
+
const RENAMED_EXPORTS = new Map([
|
|
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' }],
|
|
39
50
|
]);
|
|
40
51
|
/** A node's decorators, or none where it is a kind that cannot carry them. */
|
|
41
52
|
function decoratorsOf(node) {
|
|
@@ -76,10 +87,6 @@ function findProperty(options, name) {
|
|
|
76
87
|
}
|
|
77
88
|
return options.node.properties.find((prop) => (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && propertyKey(prop.name) === name);
|
|
78
89
|
}
|
|
79
|
-
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
80
|
-
function propertyKey(name) {
|
|
81
|
-
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
82
|
-
}
|
|
83
90
|
/**
|
|
84
91
|
* Renames the `virtual` option to `computed`, the one it was renamed to.
|
|
85
92
|
*
|
|
@@ -169,6 +176,137 @@ function addRelationEntity(decorator, node, ctx) {
|
|
|
169
176
|
},
|
|
170
177
|
});
|
|
171
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Writes the edit `rewrite` reads off `value`, which still names members as strings, or reports it
|
|
181
|
+
* where it could not be read: what it holds is only known at runtime. A callback or an object literal
|
|
182
|
+
* is already the new form, and kept.
|
|
183
|
+
*/
|
|
184
|
+
function rewriteValue(value, rewrite, node, advice, ctx) {
|
|
185
|
+
if (!value || isCallback(value) || ts.isObjectLiteralExpression(value)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const edit = rewrite(value);
|
|
189
|
+
if (edit) {
|
|
190
|
+
ctx.edits.push(edit);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
ctx.unresolved.push(`${ctx.describe(node)}: ${advice}; this one could not be read`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function rewriteKeyList(value, param, node, what, ctx) {
|
|
197
|
+
rewriteValue(value, (list) => keyListEdit(list, param), node, `write ${what} as a key-map callback`, ctx);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Rewrites what a class decorator names by string - `@Index` columns and `include`, and `@Entity`'s
|
|
201
|
+
* `indexes`, `hooks` and `relations` - into key-map callbacks named after the class.
|
|
202
|
+
*/
|
|
203
|
+
function rewriteClassDecorators(node, ctx) {
|
|
204
|
+
const param = paramFor(node.name?.text);
|
|
205
|
+
for (const decorator of decoratorsOf(node)) {
|
|
206
|
+
if (!ts.isCallExpression(decorator.expression)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const [first, second] = decorator.expression.arguments;
|
|
210
|
+
const name = decoratorName(decorator);
|
|
211
|
+
if (name === 'Index') {
|
|
212
|
+
rewriteKeyList(first, param, decorator, "'@Index' columns", ctx);
|
|
213
|
+
if (second && ts.isObjectLiteralExpression(second)) {
|
|
214
|
+
rewriteKeyList(propertyValue(second, 'include'), param, decorator, "'include'", ctx);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (name === 'Entity' && first && ts.isObjectLiteralExpression(first)) {
|
|
218
|
+
rewriteEntityOptions(first, param, ctx);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Rewrites what the imperative API names by string - `defineEntity(Post, { indexes, hooks, relations })`,
|
|
224
|
+
* `defineIndex(Post, { columns, include })` and `defineRelation(Post, 'tag', { mappedBy, references })` -
|
|
225
|
+
* into key-map callbacks. Only uql's shapes, a class and a literal options object, are read.
|
|
226
|
+
*/
|
|
227
|
+
function rewriteDefineCall(call, ctx) {
|
|
228
|
+
const name = ts.isIdentifier(call.expression) ? call.expression.text : undefined;
|
|
229
|
+
const [entity, second, third] = call.arguments;
|
|
230
|
+
const param = paramFor(entity && ts.isIdentifier(entity) ? entity.text : undefined);
|
|
231
|
+
if (name === 'defineEntity' && second && ts.isObjectLiteralExpression(second)) {
|
|
232
|
+
rewriteEntityOptions(second, param, ctx);
|
|
233
|
+
}
|
|
234
|
+
else if (name === 'defineIndex' && second && ts.isObjectLiteralExpression(second)) {
|
|
235
|
+
rewriteIndexOptions(second, param, ctx);
|
|
236
|
+
}
|
|
237
|
+
else if (name === 'defineRelation' && third && ts.isObjectLiteralExpression(third)) {
|
|
238
|
+
rewriteRelationOptions(third, param, ctx);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function rewriteEntityOptions(options, param, ctx) {
|
|
242
|
+
const indexes = propertyValue(options, 'indexes');
|
|
243
|
+
for (const index of indexes && ts.isArrayLiteralExpression(indexes) ? indexes.elements : []) {
|
|
244
|
+
if (ts.isObjectLiteralExpression(index)) {
|
|
245
|
+
rewriteIndexOptions(index, param, ctx);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
|
|
249
|
+
rewriteKeyList(hook.initializer, param, hook, 'a hook list', ctx);
|
|
250
|
+
}
|
|
251
|
+
for (const relation of objectProperties(propertyValue(options, 'relations'))) {
|
|
252
|
+
if (ts.isObjectLiteralExpression(relation.initializer)) {
|
|
253
|
+
rewriteRelationOptions(relation.initializer, param, ctx);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function rewriteIndexOptions(index, param, ctx) {
|
|
258
|
+
rewriteKeyList(propertyValue(index, 'columns'), param, index, "'columns'", ctx);
|
|
259
|
+
rewriteKeyList(propertyValue(index, 'include'), param, index, "'include'", ctx);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* `owner` is the declaring class's parameter and `target` the related one's, read off the `entity`
|
|
263
|
+
* getter unless the caller knows it from the property's type.
|
|
264
|
+
*/
|
|
265
|
+
function rewriteRelationOptions(relation, owner, ctx, target = paramFor(entityGetterTarget(relation))) {
|
|
266
|
+
const advice = (what) => `write '${what}' as a key-map callback`;
|
|
267
|
+
const mappedBy = (value) => mappedByEdit(value, target);
|
|
268
|
+
const references = (value) => referencesEdit(value, owner, target);
|
|
269
|
+
rewriteValue(propertyValue(relation, 'mappedBy'), mappedBy, relation, advice('mappedBy'), ctx);
|
|
270
|
+
rewriteValue(propertyValue(relation, 'references'), references, relation, advice('references'), ctx);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Rewrites what a statement names by string into keys: an aggregate's `$agg: { total: { $sum: 'amount' } }`
|
|
274
|
+
* into `$select: { total: { $sum: { amount: true } } }`, and `$text`'s `$fields: ['title']` into
|
|
275
|
+
* `{ title: true }`. Both keys are uql's own, so they are read wherever a literal holds them.
|
|
276
|
+
*/
|
|
277
|
+
function rewriteStatementKeys(node, ctx) {
|
|
278
|
+
const key = propertyKey(node.name);
|
|
279
|
+
if (key === '$agg') {
|
|
280
|
+
rewriteAggregate(node, ctx);
|
|
281
|
+
}
|
|
282
|
+
else if (key === '$fields' && isTextSearch(node.parent)) {
|
|
283
|
+
rewriteValue(node.initializer, fieldKeysEdit, node, "write '$fields' as { field: true }", ctx);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function isTextSearch(node) {
|
|
287
|
+
return (ts.isObjectLiteralExpression(node) &&
|
|
288
|
+
ts.isPropertyAssignment(node.parent) &&
|
|
289
|
+
propertyKey(node.parent.name) === '$text');
|
|
290
|
+
}
|
|
291
|
+
function rewriteAggregate(agg, ctx) {
|
|
292
|
+
if (ts.isObjectLiteralExpression(agg.parent) && propertyValue(agg.parent, '$select')) {
|
|
293
|
+
ctx.unresolved.push(`${ctx.describe(agg)}: merge '$agg' into the '$select' beside it`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
ctx.edits.push(replaced(agg.name, '$select'));
|
|
297
|
+
for (const fn of objectProperties(agg.initializer)) {
|
|
298
|
+
for (const op of objectProperties(fn.initializer)) {
|
|
299
|
+
const isStar = ts.isStringLiteralLike(op.initializer) && op.initializer.text === '*';
|
|
300
|
+
if (!isStar) {
|
|
301
|
+
rewriteValue(op.initializer, fieldKeysEdit, op, `write '${op.name.getText()}' as { field: true }`, ctx);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** The property assignments of an object literal, or none where `node` is not one. */
|
|
307
|
+
function objectProperties(node) {
|
|
308
|
+
return node && ts.isObjectLiteralExpression(node) ? node.properties.filter(ts.isPropertyAssignment) : [];
|
|
309
|
+
}
|
|
172
310
|
/**
|
|
173
311
|
* Drops `declare` from a decorated field. A `declare` field emits nothing, so the standard spec has
|
|
174
312
|
* nothing to decorate and rejects it ("Decorators are not valid here"). Legacy decorators tolerated it
|
|
@@ -298,12 +436,11 @@ function brandIdKey(node, ctx) {
|
|
|
298
436
|
* Imports `idKey` where a brand was written, into the file's own `uql-orm` import. Reported instead
|
|
299
437
|
* where there is none: the package may be imported under a path this codemod does not recognise.
|
|
300
438
|
*/
|
|
301
|
-
function addIdKeyImport(source, ctx) {
|
|
302
|
-
|
|
303
|
-
if (imported.some((element) => importedName(element) === 'idKey')) {
|
|
439
|
+
function addIdKeyImport(source, rewritten, ctx) {
|
|
440
|
+
if (uqlImports(source).some((element) => importedName(element) === 'idKey')) {
|
|
304
441
|
return;
|
|
305
442
|
}
|
|
306
|
-
const
|
|
443
|
+
const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration))?.elements[0];
|
|
307
444
|
if (!anchor) {
|
|
308
445
|
ctx.unresolved.push(`${source.fileName}: import 'idKey' from 'uql-orm' for the brand(s) written here`);
|
|
309
446
|
return;
|
|
@@ -322,9 +459,15 @@ function rewriteProperty(node, ctx) {
|
|
|
322
459
|
addFieldType(decorator, node, ctx);
|
|
323
460
|
renameVirtualOption(decorator, node, ctx);
|
|
324
461
|
}
|
|
462
|
+
const options = decoratorOptions(decorator);
|
|
325
463
|
if (RELATION_DECORATORS.has(name)) {
|
|
326
464
|
addRelationEntity(decorator, node, ctx);
|
|
327
465
|
}
|
|
466
|
+
if (RELATION_DECORATORS.has(name) && options.kind === 'literal') {
|
|
467
|
+
const owner = ts.isClassLike(node.parent) ? node.parent.name?.text : undefined;
|
|
468
|
+
const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
|
|
469
|
+
rewriteRelationOptions(options.node, paramFor(owner), ctx, paramFor(target));
|
|
470
|
+
}
|
|
328
471
|
}
|
|
329
472
|
if (decorators.length) {
|
|
330
473
|
dropDeclare(node, ctx);
|
|
@@ -352,23 +495,85 @@ function reportRemovedExports(source, ctx) {
|
|
|
352
495
|
}
|
|
353
496
|
}
|
|
354
497
|
/**
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
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.
|
|
358
501
|
*/
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
502
|
+
function renameExports(source, ctx) {
|
|
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)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
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]);
|
|
527
|
+
}
|
|
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);
|
|
536
|
+
}
|
|
537
|
+
return rewritten;
|
|
538
|
+
}
|
|
539
|
+
function renamedTo(element) {
|
|
540
|
+
return RENAMED_EXPORTS.get(importedName(element));
|
|
541
|
+
}
|
|
542
|
+
/** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
|
|
543
|
+
function usesOf(source, name, checker) {
|
|
544
|
+
const symbol = checker.getSymbolAtLocation(name);
|
|
545
|
+
const uses = [];
|
|
546
|
+
const visit = (node) => {
|
|
547
|
+
if (ts.isIdentifier(node) && node !== name && node.text === name.text) {
|
|
548
|
+
if (symbol && checker.getSymbolAtLocation(node) === symbol) {
|
|
549
|
+
uses.push(node);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
ts.forEachChild(node, visit);
|
|
553
|
+
};
|
|
554
|
+
visit(source);
|
|
555
|
+
return uses;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
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.
|
|
560
|
+
*/
|
|
561
|
+
function uqlImportDeclarations(source, entries = false) {
|
|
562
|
+
return source.statements.flatMap((declaration) => {
|
|
563
|
+
if (!ts.isImportDeclaration(declaration) || !ts.isStringLiteral(declaration.moduleSpecifier)) {
|
|
362
564
|
return [];
|
|
363
565
|
}
|
|
364
|
-
const
|
|
365
|
-
if (
|
|
566
|
+
const entry = declaration.moduleSpecifier.text;
|
|
567
|
+
if (entry !== 'uql-orm' && !(entries && entry.startsWith('uql-orm/'))) {
|
|
366
568
|
return [];
|
|
367
569
|
}
|
|
368
|
-
const bindings =
|
|
369
|
-
return bindings && ts.isNamedImports(bindings) ? [
|
|
570
|
+
const bindings = declaration.importClause?.namedBindings;
|
|
571
|
+
return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
|
|
370
572
|
});
|
|
371
573
|
}
|
|
574
|
+
function uqlImports(source, entries = false) {
|
|
575
|
+
return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
|
|
576
|
+
}
|
|
372
577
|
/** The name an import specifier brings in, which is the original one where it was renamed. */
|
|
373
578
|
function importedName(element) {
|
|
374
579
|
return (element.propertyName ?? element.name).text;
|
|
@@ -421,14 +626,22 @@ export function transformFile(source, checker) {
|
|
|
421
626
|
}
|
|
422
627
|
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
|
423
628
|
branded = brandIdKey(node, ctx) || branded;
|
|
629
|
+
rewriteClassDecorators(node, ctx);
|
|
630
|
+
}
|
|
631
|
+
if (ts.isCallExpression(node)) {
|
|
632
|
+
rewriteDefineCall(node, ctx);
|
|
633
|
+
}
|
|
634
|
+
if (ts.isPropertyAssignment(node)) {
|
|
635
|
+
rewriteStatementKeys(node, ctx);
|
|
424
636
|
}
|
|
425
637
|
ts.forEachChild(node, visit);
|
|
426
638
|
};
|
|
427
639
|
visit(source);
|
|
640
|
+
const rewritten = renameExports(source, ctx);
|
|
428
641
|
reportRemovedExports(source, ctx);
|
|
429
642
|
dropDeadImports(source, ctx);
|
|
430
643
|
if (branded) {
|
|
431
|
-
addIdKeyImport(source, ctx);
|
|
644
|
+
addIdKeyImport(source, rewritten, ctx);
|
|
432
645
|
}
|
|
433
646
|
return {
|
|
434
647
|
fileName: source.fileName,
|