uql-codemod 0.34.0 → 0.35.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 +12 -12
- package/dist/keyMaps.d.ts +25 -0
- package/dist/keyMaps.js +112 -0
- package/dist/transform.js +189 -5
- 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
|
-
| ------------------ |
|
|
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
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
|
|
|
@@ -28,6 +28,10 @@ Only the `--flag=value` form is read, and anything unrecognised is an error: a m
|
|
|
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
30
|
- `@Field({ virtual })` becomes `@Field({ computed })`, the name it was renamed to - written as a plain key, a quoted one, or a shorthand, which becomes `computed: virtual` so it keeps referring to the same local. A decorator given both names is reported instead, because which one wins is your call.
|
|
31
|
+
- `mappedBy: 'author'` becomes the callback `(post) => post.author`, the one form left, named after the relation's target.
|
|
32
|
+
- `@Index(['title', { column: 'createdAt', order: 'desc' }], { include: ['slug'] })` becomes `@Index((post) => [post.title, { column: post.createdAt, order: 'desc' }], { include: (post) => [post.slug] })`, and `references` pairs and the `@Entity`/`defineEntity`/`defineIndex`/`defineRelation` string forms likewise. A value that is not a literal is reported.
|
|
33
|
+
- An aggregate's `$agg: { total: { $sum: 'amount' } }` becomes `$select: { total: { $sum: { amount: true } } }`, and `$text`'s `$fields: ['title']` becomes `$fields: { title: true }`.
|
|
34
|
+
- `QueryWhereMap` and `RelationKeyMap`, renamed to `QueryWhere` and `KeyMap`, are renamed at their import and every use.
|
|
31
35
|
- A primary key not called `id`, `_id` or `uuid`, and every composite key, gets the `[idKey]?: '...'` brand naming it, plus the import. `@Id` refuses a key it cannot name, since one that resolves to any column types `findOneById` against the wrong one.
|
|
32
36
|
- `import 'reflect-metadata'` goes. Removing the package from your `package.json` is left to you.
|
|
33
37
|
- `tsconfig.json`: removes `experimentalDecorators` and `emitDecoratorMetadata`, keeping the rest of the file - comments and formatting included - exactly as written.
|
|
@@ -39,11 +43,11 @@ It reports rather than guesses, and exits non-zero when anything is left for you
|
|
|
39
43
|
- **`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.
|
|
40
44
|
- **A value inherited through `extends`** cannot be edited here, so the base config is reported instead.
|
|
41
45
|
- **`@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.
|
|
42
|
-
- **An export that no longer exists** (`setQuerierPool`, `getQuerier`, `
|
|
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`.
|
|
43
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.
|
|
44
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.
|
|
45
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.
|
|
46
|
-
- **A branded string id**, e.g.
|
|
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.
|
|
47
51
|
|
|
48
52
|
## After it runs
|
|
49
53
|
|
|
@@ -51,11 +55,7 @@ It reports rather than guesses, and exits non-zero when anything is left for you
|
|
|
51
55
|
|
|
52
56
|
## Why TypeScript 6
|
|
53
57
|
|
|
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.
|
|
58
|
+
TypeScript is a runtime dependency here: reading each property's type needs the checker. Version 6 is pinned because 7's `typescript` entry point exports no compiler API yet (it moved to `typescript/unstable/*`), and 7 has no standalone parser, which the `tsconfig.json` rewrite needs to keep your comments and formatting. A one-shot tool for a spec `uql-orm` no longer supports needs nothing newer; revisit once that API is stable.
|
|
59
59
|
|
|
60
60
|
## License
|
|
61
61
|
|
|
@@ -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,8 +23,10 @@ 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
32
|
['PgDialect', 'the pools build `PostgresDialect` (`uql-orm/postgres`); name that where a dialect is typed'],
|
|
@@ -37,6 +40,11 @@ const REMOVED_EXPORTS = new Map([
|
|
|
37
40
|
['LibsqlQuerier', 'the libSQL and Turso pools return `HranaQuerier` (`uql-orm/sqlite`)'],
|
|
38
41
|
['TursoQuerier', 'the libSQL and Turso pools return `HranaQuerier` (`uql-orm/sqlite`)'],
|
|
39
42
|
]);
|
|
43
|
+
/** Exports renamed and nothing else, so the import and every use of it in the file follow. */
|
|
44
|
+
const RENAMED_EXPORTS = new Map([
|
|
45
|
+
['QueryWhereMap', 'QueryWhere'],
|
|
46
|
+
['RelationKeyMap', 'KeyMap'],
|
|
47
|
+
]);
|
|
40
48
|
/** A node's decorators, or none where it is a kind that cannot carry them. */
|
|
41
49
|
function decoratorsOf(node) {
|
|
42
50
|
return ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
|
|
@@ -76,10 +84,6 @@ function findProperty(options, name) {
|
|
|
76
84
|
}
|
|
77
85
|
return options.node.properties.find((prop) => (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && propertyKey(prop.name) === name);
|
|
78
86
|
}
|
|
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
87
|
/**
|
|
84
88
|
* Renames the `virtual` option to `computed`, the one it was renamed to.
|
|
85
89
|
*
|
|
@@ -169,6 +173,137 @@ function addRelationEntity(decorator, node, ctx) {
|
|
|
169
173
|
},
|
|
170
174
|
});
|
|
171
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Writes the edit `rewrite` reads off `value`, which still names members as strings, or reports it
|
|
178
|
+
* where it could not be read: what it holds is only known at runtime. A callback or an object literal
|
|
179
|
+
* is already the new form, and kept.
|
|
180
|
+
*/
|
|
181
|
+
function rewriteValue(value, rewrite, node, advice, ctx) {
|
|
182
|
+
if (!value || isCallback(value) || ts.isObjectLiteralExpression(value)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const edit = rewrite(value);
|
|
186
|
+
if (edit) {
|
|
187
|
+
ctx.edits.push(edit);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
ctx.unresolved.push(`${ctx.describe(node)}: ${advice}; this one could not be read`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function rewriteKeyList(value, param, node, what, ctx) {
|
|
194
|
+
rewriteValue(value, (list) => keyListEdit(list, param), node, `write ${what} as a key-map callback`, ctx);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Rewrites what a class decorator names by string - `@Index` columns and `include`, and `@Entity`'s
|
|
198
|
+
* `indexes`, `hooks` and `relations` - into key-map callbacks named after the class.
|
|
199
|
+
*/
|
|
200
|
+
function rewriteClassDecorators(node, ctx) {
|
|
201
|
+
const param = paramFor(node.name?.text);
|
|
202
|
+
for (const decorator of decoratorsOf(node)) {
|
|
203
|
+
if (!ts.isCallExpression(decorator.expression)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const [first, second] = decorator.expression.arguments;
|
|
207
|
+
const name = decoratorName(decorator);
|
|
208
|
+
if (name === 'Index') {
|
|
209
|
+
rewriteKeyList(first, param, decorator, "'@Index' columns", ctx);
|
|
210
|
+
if (second && ts.isObjectLiteralExpression(second)) {
|
|
211
|
+
rewriteKeyList(propertyValue(second, 'include'), param, decorator, "'include'", ctx);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (name === 'Entity' && first && ts.isObjectLiteralExpression(first)) {
|
|
215
|
+
rewriteEntityOptions(first, param, ctx);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Rewrites what the imperative API names by string - `defineEntity(Post, { indexes, hooks, relations })`,
|
|
221
|
+
* `defineIndex(Post, { columns, include })` and `defineRelation(Post, 'tag', { mappedBy, references })` -
|
|
222
|
+
* into key-map callbacks. Only uql's shapes, a class and a literal options object, are read.
|
|
223
|
+
*/
|
|
224
|
+
function rewriteDefineCall(call, ctx) {
|
|
225
|
+
const name = ts.isIdentifier(call.expression) ? call.expression.text : undefined;
|
|
226
|
+
const [entity, second, third] = call.arguments;
|
|
227
|
+
const param = paramFor(entity && ts.isIdentifier(entity) ? entity.text : undefined);
|
|
228
|
+
if (name === 'defineEntity' && second && ts.isObjectLiteralExpression(second)) {
|
|
229
|
+
rewriteEntityOptions(second, param, ctx);
|
|
230
|
+
}
|
|
231
|
+
else if (name === 'defineIndex' && second && ts.isObjectLiteralExpression(second)) {
|
|
232
|
+
rewriteIndexOptions(second, param, ctx);
|
|
233
|
+
}
|
|
234
|
+
else if (name === 'defineRelation' && third && ts.isObjectLiteralExpression(third)) {
|
|
235
|
+
rewriteRelationOptions(third, param, ctx);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function rewriteEntityOptions(options, param, ctx) {
|
|
239
|
+
const indexes = propertyValue(options, 'indexes');
|
|
240
|
+
for (const index of indexes && ts.isArrayLiteralExpression(indexes) ? indexes.elements : []) {
|
|
241
|
+
if (ts.isObjectLiteralExpression(index)) {
|
|
242
|
+
rewriteIndexOptions(index, param, ctx);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
|
|
246
|
+
rewriteKeyList(hook.initializer, param, hook, 'a hook list', ctx);
|
|
247
|
+
}
|
|
248
|
+
for (const relation of objectProperties(propertyValue(options, 'relations'))) {
|
|
249
|
+
if (ts.isObjectLiteralExpression(relation.initializer)) {
|
|
250
|
+
rewriteRelationOptions(relation.initializer, param, ctx);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function rewriteIndexOptions(index, param, ctx) {
|
|
255
|
+
rewriteKeyList(propertyValue(index, 'columns'), param, index, "'columns'", ctx);
|
|
256
|
+
rewriteKeyList(propertyValue(index, 'include'), param, index, "'include'", ctx);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* `owner` is the declaring class's parameter and `target` the related one's, read off the `entity`
|
|
260
|
+
* getter unless the caller knows it from the property's type.
|
|
261
|
+
*/
|
|
262
|
+
function rewriteRelationOptions(relation, owner, ctx, target = paramFor(entityGetterTarget(relation))) {
|
|
263
|
+
const advice = (what) => `write '${what}' as a key-map callback`;
|
|
264
|
+
const mappedBy = (value) => mappedByEdit(value, target);
|
|
265
|
+
const references = (value) => referencesEdit(value, owner, target);
|
|
266
|
+
rewriteValue(propertyValue(relation, 'mappedBy'), mappedBy, relation, advice('mappedBy'), ctx);
|
|
267
|
+
rewriteValue(propertyValue(relation, 'references'), references, relation, advice('references'), ctx);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Rewrites what a statement names by string into keys: an aggregate's `$agg: { total: { $sum: 'amount' } }`
|
|
271
|
+
* into `$select: { total: { $sum: { amount: true } } }`, and `$text`'s `$fields: ['title']` into
|
|
272
|
+
* `{ title: true }`. Both keys are uql's own, so they are read wherever a literal holds them.
|
|
273
|
+
*/
|
|
274
|
+
function rewriteStatementKeys(node, ctx) {
|
|
275
|
+
const key = propertyKey(node.name);
|
|
276
|
+
if (key === '$agg') {
|
|
277
|
+
rewriteAggregate(node, ctx);
|
|
278
|
+
}
|
|
279
|
+
else if (key === '$fields' && isTextSearch(node.parent)) {
|
|
280
|
+
rewriteValue(node.initializer, fieldKeysEdit, node, "write '$fields' as { field: true }", ctx);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function isTextSearch(node) {
|
|
284
|
+
return (ts.isObjectLiteralExpression(node) &&
|
|
285
|
+
ts.isPropertyAssignment(node.parent) &&
|
|
286
|
+
propertyKey(node.parent.name) === '$text');
|
|
287
|
+
}
|
|
288
|
+
function rewriteAggregate(agg, ctx) {
|
|
289
|
+
if (ts.isObjectLiteralExpression(agg.parent) && propertyValue(agg.parent, '$select')) {
|
|
290
|
+
ctx.unresolved.push(`${ctx.describe(agg)}: merge '$agg' into the '$select' beside it`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
ctx.edits.push(replaced(agg.name, '$select'));
|
|
294
|
+
for (const fn of objectProperties(agg.initializer)) {
|
|
295
|
+
for (const op of objectProperties(fn.initializer)) {
|
|
296
|
+
const isStar = ts.isStringLiteralLike(op.initializer) && op.initializer.text === '*';
|
|
297
|
+
if (!isStar) {
|
|
298
|
+
rewriteValue(op.initializer, fieldKeysEdit, op, `write '${op.name.getText()}' as { field: true }`, ctx);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** The property assignments of an object literal, or none where `node` is not one. */
|
|
304
|
+
function objectProperties(node) {
|
|
305
|
+
return node && ts.isObjectLiteralExpression(node) ? node.properties.filter(ts.isPropertyAssignment) : [];
|
|
306
|
+
}
|
|
172
307
|
/**
|
|
173
308
|
* Drops `declare` from a decorated field. A `declare` field emits nothing, so the standard spec has
|
|
174
309
|
* nothing to decorate and rejects it ("Decorators are not valid here"). Legacy decorators tolerated it
|
|
@@ -322,9 +457,15 @@ function rewriteProperty(node, ctx) {
|
|
|
322
457
|
addFieldType(decorator, node, ctx);
|
|
323
458
|
renameVirtualOption(decorator, node, ctx);
|
|
324
459
|
}
|
|
460
|
+
const options = decoratorOptions(decorator);
|
|
325
461
|
if (RELATION_DECORATORS.has(name)) {
|
|
326
462
|
addRelationEntity(decorator, node, ctx);
|
|
327
463
|
}
|
|
464
|
+
if (RELATION_DECORATORS.has(name) && options.kind === 'literal') {
|
|
465
|
+
const owner = ts.isClassLike(node.parent) ? node.parent.name?.text : undefined;
|
|
466
|
+
const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
|
|
467
|
+
rewriteRelationOptions(options.node, paramFor(owner), ctx, paramFor(target));
|
|
468
|
+
}
|
|
328
469
|
}
|
|
329
470
|
if (decorators.length) {
|
|
330
471
|
dropDeclare(node, ctx);
|
|
@@ -351,6 +492,41 @@ function reportRemovedExports(source, ctx) {
|
|
|
351
492
|
}
|
|
352
493
|
}
|
|
353
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Renames each import of a {@link RENAMED_EXPORTS} name and every use of it in the file. An aliased
|
|
497
|
+
* import only changes the name it imports; one whose new name the file already imports is dropped.
|
|
498
|
+
*/
|
|
499
|
+
function renameExports(source, ctx) {
|
|
500
|
+
const imported = uqlImports(source);
|
|
501
|
+
for (const element of imported) {
|
|
502
|
+
const renamed = RENAMED_EXPORTS.get(importedName(element));
|
|
503
|
+
if (!renamed) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (element.propertyName) {
|
|
507
|
+
ctx.edits.push(replaced(element.propertyName, renamed));
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
ctx.edits.push(...usesOf(source, element.name, ctx.checker).map((use) => replaced(use, renamed)));
|
|
511
|
+
const duplicate = imported.some((other) => other.name.text === renamed);
|
|
512
|
+
ctx.edits.push(duplicate ? removeFromList(element.parent.elements, element, source) : replaced(element.name, renamed));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
|
|
516
|
+
function usesOf(source, name, checker) {
|
|
517
|
+
const symbol = checker.getSymbolAtLocation(name);
|
|
518
|
+
const uses = [];
|
|
519
|
+
const visit = (node) => {
|
|
520
|
+
if (ts.isIdentifier(node) && node !== name && node.text === name.text) {
|
|
521
|
+
if (symbol && checker.getSymbolAtLocation(node) === symbol) {
|
|
522
|
+
uses.push(node);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
ts.forEachChild(node, visit);
|
|
526
|
+
};
|
|
527
|
+
visit(source);
|
|
528
|
+
return uses;
|
|
529
|
+
}
|
|
354
530
|
/**
|
|
355
531
|
* What the file imports from `uql-orm` by name, which is every import this codemod reads or writes.
|
|
356
532
|
* `entries` adds the driver entries (`uql-orm/postgres`, ...), for reporting only: an import written
|
|
@@ -421,10 +597,18 @@ export function transformFile(source, checker) {
|
|
|
421
597
|
}
|
|
422
598
|
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
|
423
599
|
branded = brandIdKey(node, ctx) || branded;
|
|
600
|
+
rewriteClassDecorators(node, ctx);
|
|
601
|
+
}
|
|
602
|
+
if (ts.isCallExpression(node)) {
|
|
603
|
+
rewriteDefineCall(node, ctx);
|
|
604
|
+
}
|
|
605
|
+
if (ts.isPropertyAssignment(node)) {
|
|
606
|
+
rewriteStatementKeys(node, ctx);
|
|
424
607
|
}
|
|
425
608
|
ts.forEachChild(node, visit);
|
|
426
609
|
};
|
|
427
610
|
visit(source);
|
|
611
|
+
renameExports(source, ctx);
|
|
428
612
|
reportRemovedExports(source, ctx);
|
|
429
613
|
dropDeadImports(source, ctx);
|
|
430
614
|
if (branded) {
|