uql-codemod 0.35.0 → 0.36.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 +28 -28
- package/dist/edits.d.ts +5 -4
- package/dist/edits.js +10 -5
- package/dist/entitySql.d.ts +23 -0
- package/dist/entitySql.js +67 -0
- package/dist/keyMaps.d.ts +9 -6
- package/dist/keyMaps.js +29 -45
- package/dist/transform.js +418 -143
- 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,42 @@ 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
|
-
- An aggregate's `$agg
|
|
34
|
-
- `QueryWhereMap`
|
|
35
|
-
-
|
|
36
|
-
- `import 'reflect-metadata'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
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` (on `@Field` and in `defineEntity`'s `fields`), a check's `expression` and a filter's `condition` to `where` (`@Entity`/`defineEntity`, `@Filter`, `defineFilter`), `raw('sql')` to the tagged template, and a `raw` alias argument onto `.as()`.
|
|
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 index expression to the callback an index takes, as an entry or a `column`: ``raw`lower("email")` `` to ``() => raw`lower("email")` ``, and a partial-index `where` string to `raw`, importing it; on entities and in the migration builder.
|
|
32
|
+
- `col('cost')` to the ref it names, ``(product) => raw`${product.cost}` `` in a `computed` and `refs(Item).cost` in a statement, and `$lock: { wait: 'skip' }` to `{ $wait: 'skip' }`, or to `true` for `{}` and a `'block'` wait.
|
|
33
|
+
- An aggregate's `$agg` to `$select`, and each field it or `$text`'s `$fields` names to a key: `{ $sum: { amount: true } }`.
|
|
34
|
+
- Renamed exports, at the import and every use: `QueryWhereMap` to `QueryWhere`, `RelationKeyMap` to `KeyMap`, `FilterCondition` to `FilterWhere`, 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.
|
|
35
|
+
- The `[idKey]?: '...'` brand, and its import, on a key not called `id`, `_id` or `uuid` and on every composite.
|
|
36
|
+
- `import 'reflect-metadata'`, and `experimentalDecorators`/`emitDecoratorMetadata` in `tsconfig.json`, comments and formatting kept.
|
|
37
|
+
|
|
38
|
+
## What it leaves to you
|
|
39
|
+
|
|
40
|
+
Reported with what to do, never guessed:
|
|
41
|
+
|
|
42
|
+
- `target: esnext`, the one target that leaves decorators untransformed, and a value inherited through `extends`.
|
|
43
|
+
- `@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`: a `@Transactional()` method becomes a `pool.transaction()` around its body, and only you know which pool.
|
|
44
|
+
- An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...).
|
|
45
|
+
- 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.
|
|
46
|
+
- A partial-index `where` string that interpolates, since `raw` would bind what the template spliced in.
|
|
47
|
+
- A `col()` whose entity it cannot tell: a raw built apart from its query, one under a relation filter, or a column no field maps.
|
|
48
|
+
- SQL in a definition that names a column by hand, as a note: a rename reaches a callback's refs, ``(user) => raw`lower(${user.email})` ``, and never the text.
|
|
49
|
+
- A property whose type maps to no column (`Json<T>`, a vector, a mixed union).
|
|
50
|
+
- 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
51
|
|
|
52
52
|
## After it runs
|
|
53
53
|
|
|
54
|
-
`tsc` is the rest of the migration
|
|
54
|
+
`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
55
|
|
|
56
56
|
## Why TypeScript 6
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
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
59
|
|
|
60
60
|
## License
|
|
61
61
|
|
package/dist/edits.d.ts
CHANGED
|
@@ -6,12 +6,13 @@ export type Edit = {
|
|
|
6
6
|
readonly text: string;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
|
-
* Applies edits back to front, so
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* that reformats everything it touches buries its own change in the diff.
|
|
9
|
+
* Applies edits back to front, so none shifts the offsets of another, and at a shared offset the replacement
|
|
10
|
+
* first, so an insertion there lands before it. Splicing rather than reprinting keeps the rest of the file
|
|
11
|
+
* byte for byte: a codemod that reformats everything it touches buries its own change in the diff.
|
|
13
12
|
*/
|
|
14
13
|
export declare function applyEdits(text: string, edits: readonly Edit[]): string;
|
|
14
|
+
export declare function replaced(node: ts.Node, text: string): Edit;
|
|
15
|
+
export declare function inserted(node: ts.Node, text: string): Edit;
|
|
15
16
|
/**
|
|
16
17
|
* Removes one element of a comma-separated list - a parameter, a named import, a JSON property - taking
|
|
17
18
|
* the separator that would otherwise dangle. The last element gives up the comma before it; any other
|
package/dist/edits.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Applies edits back to front, so
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* that reformats everything it touches buries its own change in the diff.
|
|
2
|
+
* Applies edits back to front, so none shifts the offsets of another, and at a shared offset the replacement
|
|
3
|
+
* first, so an insertion there lands before it. Splicing rather than reprinting keeps the rest of the file
|
|
4
|
+
* byte for byte: a codemod that reformats everything it touches buries its own change in the diff.
|
|
6
5
|
*/
|
|
7
6
|
export function applyEdits(text, edits) {
|
|
8
7
|
let result = text;
|
|
9
|
-
for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
|
|
8
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start || b.end - a.end)) {
|
|
10
9
|
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
11
10
|
}
|
|
12
11
|
return result;
|
|
13
12
|
}
|
|
13
|
+
export function replaced(node, text) {
|
|
14
|
+
return { start: node.getStart(), end: node.getEnd(), text };
|
|
15
|
+
}
|
|
16
|
+
export function inserted(node, text) {
|
|
17
|
+
return { start: node.getStart(), end: node.getStart(), text };
|
|
18
|
+
}
|
|
14
19
|
/**
|
|
15
20
|
* Removes one element of a comma-separated list - a parameter, a named import, a JSON property - taking
|
|
16
21
|
* the separator that would otherwise dangle. The last element gives up the comma before it; any other
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { type Edit } from './edits.js';
|
|
3
|
+
/** `raw\`...\`` or `raw(...)`. */
|
|
4
|
+
type RawSql = ts.TaggedTemplateExpression | ts.CallExpression;
|
|
5
|
+
export declare function isRaw(node: ts.Node): node is RawSql;
|
|
6
|
+
/**
|
|
7
|
+
* `() => ` before each expression a column list indexes, which an index takes as a callback now. One that
|
|
8
|
+
* interpolates takes the list callback's parameter as its refs: `(user) => [raw\`lower(${user.email})\`]`
|
|
9
|
+
* spliced in the key `'email'`, where the ref it reads now renders the column.
|
|
10
|
+
*/
|
|
11
|
+
export declare function expressionEntryEdits(list: ts.Expression | undefined): readonly Edit[];
|
|
12
|
+
/** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `raw`. */
|
|
13
|
+
export declare function columnExpressions(list: ts.Expression | undefined): readonly RawSql[];
|
|
14
|
+
/** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
|
|
15
|
+
export declare function rawTag(sql: string): string;
|
|
16
|
+
/** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
|
|
17
|
+
export declare function rawWhereEdit(where: ts.Expression): Edit | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The columns SQL names by hand, which a rename does not reach: each quoted identifier, and each bare word
|
|
20
|
+
* naming one of `members`. What a string literal holds is left out, as is a word called as a function.
|
|
21
|
+
*/
|
|
22
|
+
export declare function handNamedColumns(sql: ts.Expression, members: ReadonlySet<string>): readonly string[];
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { inserted, replaced } from './edits.js';
|
|
3
|
+
import { propertyValue } from './keyMaps.js';
|
|
4
|
+
export function isRaw(node) {
|
|
5
|
+
const callee = ts.isTaggedTemplateExpression(node)
|
|
6
|
+
? node.tag
|
|
7
|
+
: ts.isCallExpression(node)
|
|
8
|
+
? node.expression
|
|
9
|
+
: undefined;
|
|
10
|
+
return callee !== undefined && ts.isIdentifier(callee) && callee.text === 'raw';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `() => ` before each expression a column list indexes, which an index takes as a callback now. One that
|
|
14
|
+
* interpolates takes the list callback's parameter as its refs: `(user) => [raw\`lower(${user.email})\`]`
|
|
15
|
+
* spliced in the key `'email'`, where the ref it reads now renders the column.
|
|
16
|
+
*/
|
|
17
|
+
export function expressionEntryEdits(list) {
|
|
18
|
+
const param = list && ts.isArrowFunction(list) ? list.parameters[0]?.name : undefined;
|
|
19
|
+
const refs = param && ts.isIdentifier(param) ? param.text : '';
|
|
20
|
+
return columnExpressions(list).map((sql) => inserted(sql, `(${interpolates(sql) ? refs : ''}) => `));
|
|
21
|
+
}
|
|
22
|
+
/** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `raw`. */
|
|
23
|
+
export function columnExpressions(list) {
|
|
24
|
+
const body = list && ts.isArrowFunction(list) ? list.body : list;
|
|
25
|
+
const entries = body && ts.isArrayLiteralExpression(body) ? body.elements : [];
|
|
26
|
+
return entries.flatMap((entry) => {
|
|
27
|
+
const sql = ts.isObjectLiteralExpression(entry) ? propertyValue(entry, 'column') : entry;
|
|
28
|
+
return sql && isRaw(sql) ? [sql] : [];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function interpolates(sql) {
|
|
32
|
+
return ts.isTaggedTemplateExpression(sql) && ts.isTemplateExpression(sql.template);
|
|
33
|
+
}
|
|
34
|
+
/** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
|
|
35
|
+
export function rawTag(sql) {
|
|
36
|
+
return `raw\`${sql.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\``;
|
|
37
|
+
}
|
|
38
|
+
/** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
|
|
39
|
+
export function rawWhereEdit(where) {
|
|
40
|
+
if (ts.isNoSubstitutionTemplateLiteral(where)) {
|
|
41
|
+
return inserted(where, 'raw');
|
|
42
|
+
}
|
|
43
|
+
return ts.isStringLiteral(where) ? replaced(where, rawTag(where.text)) : undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The columns SQL names by hand, which a rename does not reach: each quoted identifier, and each bare word
|
|
47
|
+
* naming one of `members`. What a string literal holds is left out, as is a word called as a function.
|
|
48
|
+
*/
|
|
49
|
+
export function handNamedColumns(sql, members) {
|
|
50
|
+
const code = sqlText(sql)?.replace(/'(?:[^']|'')*'/g, "''") ?? '';
|
|
51
|
+
const quoted = Array.from(code.matchAll(/"[^"]+"|`[^`]+`/g), ([token]) => token.slice(1, -1));
|
|
52
|
+
const bare = Array.from(code.matchAll(/\b[A-Za-z_]\w*\b(?!\s*\()/g), ([word]) => word);
|
|
53
|
+
return [...new Set([...quoted, ...bare.filter((word) => members.has(word))])];
|
|
54
|
+
}
|
|
55
|
+
/** The text of a `raw` or a string, `?` standing in for each interpolation. */
|
|
56
|
+
function sqlText(sql) {
|
|
57
|
+
if (!sql) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (isRaw(sql)) {
|
|
61
|
+
return sqlText(ts.isTaggedTemplateExpression(sql) ? sql.template : sql.arguments[0]);
|
|
62
|
+
}
|
|
63
|
+
if (ts.isTemplateExpression(sql)) {
|
|
64
|
+
return [sql.head, ...sql.templateSpans.map((span) => span.literal)].map((part) => part.text).join(' ? ');
|
|
65
|
+
}
|
|
66
|
+
return ts.isStringLiteralLike(sql) ? sql.text : undefined;
|
|
67
|
+
}
|
package/dist/keyMaps.d.ts
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import type
|
|
2
|
+
import { type Edit } from './edits.js';
|
|
3
|
+
/** The edits of one rewrite, or `undefined` where the value it reads could not be read. */
|
|
4
|
+
export type Edits = readonly Edit[] | undefined;
|
|
3
5
|
/** A callback's parameter, named after the class it reads: `Post` -> `post`. */
|
|
4
6
|
export declare function paramFor(className: string | undefined): string;
|
|
7
|
+
/** `post.title`, or `post['first-name']` for a key that is no identifier. */
|
|
8
|
+
export declare function memberAccess(param: string, key: string): string;
|
|
5
9
|
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
6
10
|
export declare function propertyKey(name: ts.PropertyName): string | undefined;
|
|
7
11
|
/** The initializer of the property `name` in an object literal, however its key is written. */
|
|
8
12
|
export declare function propertyValue(node: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined;
|
|
9
13
|
/** `'amount'` or `['title', 'body']` -> `{ amount: true }`, the key map a statement names fields by. */
|
|
10
|
-
export declare function
|
|
14
|
+
export declare function fieldKeysEdits(value: ts.Expression): Edits;
|
|
11
15
|
/** Whether `value` is already a function, the form every rewrite here produces. */
|
|
12
16
|
export declare function isCallback(value: ts.Expression): boolean;
|
|
13
17
|
/** `['title', { column: 'createdAt' }]` -> `(post) => [post.title, { column: post.createdAt }]`. */
|
|
14
|
-
export declare function
|
|
18
|
+
export declare function keyListEdits(list: ts.Expression, param: string): Edits;
|
|
15
19
|
/** `'author'` -> `(post) => post.author`, `param` being the relation's target. */
|
|
16
|
-
export declare function
|
|
20
|
+
export declare function mappedByEdits(value: ts.Expression, param: string): Edits;
|
|
17
21
|
/**
|
|
18
22
|
* `[{ local: 'customerCode', foreign: 'code' }]` -> `(order, customer) => [{ local: order.customerCode,
|
|
19
23
|
* foreign: customer.code }]`. A self-relation would give both parameters one name, so it reads
|
|
20
24
|
* `(local, foreign)` instead.
|
|
21
25
|
*/
|
|
22
|
-
export declare function
|
|
26
|
+
export declare function referencesEdits(value: ts.Expression, owner: string, target: string): Edits;
|
|
23
27
|
/** The class an `entity: () => Post` getter returns, as written. */
|
|
24
28
|
export declare function entityGetterTarget(relation: ts.ObjectLiteralExpression): string | undefined;
|
|
25
|
-
export declare function replaced(node: ts.Node, text: string): Edit;
|
package/dist/keyMaps.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
+
import { inserted, replaced } from './edits.js';
|
|
2
3
|
/** A callback's parameter, named after the class it reads: `Post` -> `post`. */
|
|
3
4
|
export function paramFor(className) {
|
|
4
5
|
return className ? className[0].toLowerCase() + className.slice(1) : 'entity';
|
|
@@ -7,7 +8,7 @@ function isIdentifierName(key) {
|
|
|
7
8
|
return /^[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*$/u.test(key);
|
|
8
9
|
}
|
|
9
10
|
/** `post.title`, or `post['first-name']` for a key that is no identifier. */
|
|
10
|
-
function memberAccess(param, key) {
|
|
11
|
+
export function memberAccess(param, key) {
|
|
11
12
|
return isIdentifierName(key) ? `${param}.${key}` : `${param}['${key}']`;
|
|
12
13
|
}
|
|
13
14
|
/** A key's name where it is spelled out, which is every form but a computed one (`{ [k]: v }`). */
|
|
@@ -20,93 +21,76 @@ export function propertyValue(node, name) {
|
|
|
20
21
|
return property?.initializer;
|
|
21
22
|
}
|
|
22
23
|
/** `'amount'` or `['title', 'body']` -> `{ amount: true }`, the key map a statement names fields by. */
|
|
23
|
-
export function
|
|
24
|
+
export function fieldKeysEdits(value) {
|
|
24
25
|
const names = ts.isArrayLiteralExpression(value) ? value.elements : [value];
|
|
25
26
|
if (!names.every((name) => ts.isStringLiteralLike(name))) {
|
|
26
27
|
return undefined;
|
|
27
28
|
}
|
|
28
29
|
const keys = names.map(({ text }) => `${isIdentifierName(text) ? text : `'${text}'`}: true`);
|
|
29
|
-
return replaced(value, `{ ${keys.join(', ')} }`);
|
|
30
|
+
return [replaced(value, `{ ${keys.join(', ')} }`)];
|
|
30
31
|
}
|
|
31
32
|
/** Whether `value` is already a function, the form every rewrite here produces. */
|
|
32
33
|
export function isCallback(value) {
|
|
33
34
|
return ts.isArrowFunction(value) || ts.isFunctionExpression(value);
|
|
34
35
|
}
|
|
35
36
|
/** `['title', { column: 'createdAt' }]` -> `(post) => [post.title, { column: post.createdAt }]`. */
|
|
36
|
-
export function
|
|
37
|
-
|
|
38
|
-
?
|
|
37
|
+
export function keyListEdits(list, param) {
|
|
38
|
+
return ts.isArrayLiteralExpression(list)
|
|
39
|
+
? callbackEdits(list, param, list.elements.map((entry) => keyListEntryEdits(entry, param)))
|
|
39
40
|
: undefined;
|
|
40
|
-
return callbackEdit(list, param, text);
|
|
41
41
|
}
|
|
42
42
|
/** `'author'` -> `(post) => post.author`, `param` being the relation's target. */
|
|
43
|
-
export function
|
|
44
|
-
return
|
|
43
|
+
export function mappedByEdits(value, param) {
|
|
44
|
+
return ts.isStringLiteralLike(value)
|
|
45
|
+
? [replaced(value, `(${param}) => ${memberAccess(param, value.text)}`)]
|
|
46
|
+
: undefined;
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
47
49
|
* `[{ local: 'customerCode', foreign: 'code' }]` -> `(order, customer) => [{ local: order.customerCode,
|
|
48
50
|
* foreign: customer.code }]`. A self-relation would give both parameters one name, so it reads
|
|
49
51
|
* `(local, foreign)` instead.
|
|
50
52
|
*/
|
|
51
|
-
export function
|
|
53
|
+
export function referencesEdits(value, owner, target) {
|
|
52
54
|
const [local, foreign] = owner === target ? ['local', 'foreign'] : [owner, target];
|
|
53
|
-
|
|
54
|
-
?
|
|
55
|
+
return ts.isArrayLiteralExpression(value)
|
|
56
|
+
? callbackEdits(value, `${local}, ${foreign}`, value.elements.map((pair) => referenceEdits(pair, local, foreign)))
|
|
55
57
|
: undefined;
|
|
56
|
-
return callbackEdit(value, `${local}, ${foreign}`, text);
|
|
57
58
|
}
|
|
58
59
|
/** The class an `entity: () => Post` getter returns, as written. */
|
|
59
60
|
export function entityGetterTarget(relation) {
|
|
60
61
|
const getter = propertyValue(relation, 'entity');
|
|
61
62
|
return getter && ts.isArrowFunction(getter) && ts.isIdentifier(getter.body) ? getter.body.text : undefined;
|
|
62
63
|
}
|
|
63
|
-
/**
|
|
64
|
-
function
|
|
64
|
+
/** A column-list entry, its name read off the key map: an expression, or a `column` given otherwise, has none. */
|
|
65
|
+
function keyListEntryEdits(entry, param) {
|
|
65
66
|
if (ts.isStringLiteralLike(entry)) {
|
|
66
|
-
return memberAccess(param, entry.text);
|
|
67
|
+
return [replaced(entry, memberAccess(param, entry.text))];
|
|
67
68
|
}
|
|
68
69
|
if (ts.isTaggedTemplateExpression(entry) || ts.isCallExpression(entry)) {
|
|
69
|
-
return
|
|
70
|
+
return [];
|
|
70
71
|
}
|
|
71
72
|
if (!ts.isObjectLiteralExpression(entry)) {
|
|
72
73
|
return undefined;
|
|
73
74
|
}
|
|
74
75
|
const column = propertyValue(entry, 'column');
|
|
75
|
-
return column && ts.isStringLiteralLike(column)
|
|
76
|
-
? spliced(entry, [[column, memberAccess(param, column.text)]])
|
|
77
|
-
: entry.getText();
|
|
76
|
+
return column && ts.isStringLiteralLike(column) ? [replaced(column, memberAccess(param, column.text))] : [];
|
|
78
77
|
}
|
|
79
|
-
function
|
|
78
|
+
function referenceEdits(pair, local, foreign) {
|
|
80
79
|
if (!ts.isObjectLiteralExpression(pair)) {
|
|
81
80
|
return undefined;
|
|
82
81
|
}
|
|
83
82
|
const localKey = propertyValue(pair, 'local');
|
|
84
83
|
const foreignKey = propertyValue(pair, 'foreign');
|
|
85
84
|
return localKey && foreignKey && ts.isStringLiteralLike(localKey) && ts.isStringLiteralLike(foreignKey)
|
|
86
|
-
?
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
]
|
|
85
|
+
? [
|
|
86
|
+
replaced(localKey, memberAccess(local, localKey.text)),
|
|
87
|
+
replaced(foreignKey, memberAccess(foreign, foreignKey.text)),
|
|
88
|
+
]
|
|
90
89
|
: undefined;
|
|
91
90
|
}
|
|
92
|
-
/** `
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
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 };
|
|
91
|
+
/** `(params) => ` before `body`, with the edits of its parts, or nothing where one could not be read. */
|
|
92
|
+
function callbackEdits(body, params, parts) {
|
|
93
|
+
return parts.every((part) => part !== undefined)
|
|
94
|
+
? [inserted(body, `(${params}) => `), ...parts.flat()]
|
|
95
|
+
: undefined;
|
|
112
96
|
}
|
package/dist/transform.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import { applyEdits, removeFromList } from './edits.js';
|
|
2
|
+
import { applyEdits, inserted, removeFromList, replaced } from './edits.js';
|
|
3
|
+
import { columnExpressions, expressionEntryEdits, handNamedColumns, isRaw, rawTag, rawWhereEdit } from './entitySql.js';
|
|
3
4
|
import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
|
|
4
|
-
import { entityGetterTarget,
|
|
5
|
+
import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, propertyKey, propertyValue, referencesEdits, } from './keyMaps.js';
|
|
5
6
|
const FIELD_DECORATORS = new Set(['Field', 'Id']);
|
|
6
7
|
const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
|
|
7
8
|
/**
|
|
@@ -29,21 +30,25 @@ const REMOVED_EXPORTS = new Map([
|
|
|
29
30
|
['RelationKeyMapper', 'it is `(keys: KeyMap<E>) => Key<E>` now'],
|
|
30
31
|
['augmentWhere', 'spread the two maps: `{ ...where, ...extra }`'],
|
|
31
32
|
['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
33
|
['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
34
|
]);
|
|
43
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Exports renamed and nothing else - the driver classes were empty subclasses - so the import and every
|
|
37
|
+
* use follow, the import moving to `from` where the new name lives in another entry.
|
|
38
|
+
*/
|
|
44
39
|
const RENAMED_EXPORTS = new Map([
|
|
45
|
-
['QueryWhereMap', 'QueryWhere'],
|
|
46
|
-
['RelationKeyMap', 'KeyMap'],
|
|
40
|
+
['QueryWhereMap', { to: 'QueryWhere' }],
|
|
41
|
+
['RelationKeyMap', { to: 'KeyMap' }],
|
|
42
|
+
['FilterCondition', { to: 'FilterWhere' }],
|
|
43
|
+
['PgDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
44
|
+
['NeonDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
45
|
+
['PgliteDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
|
|
46
|
+
['CrdbQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
47
|
+
['NeonQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
|
|
48
|
+
['MySql2Dialect', { to: 'MySqlDialect', from: 'uql-orm/mysql' }],
|
|
49
|
+
['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/mongo' }],
|
|
50
|
+
['LibsqlQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
51
|
+
['TursoQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
|
|
47
52
|
]);
|
|
48
53
|
/** A node's decorators, or none where it is a kind that cannot carry them. */
|
|
49
54
|
function decoratorsOf(node) {
|
|
@@ -61,18 +66,19 @@ function decoratorOptions(node) {
|
|
|
61
66
|
return { kind: 'opaque', reason: 'it is used without being called' };
|
|
62
67
|
}
|
|
63
68
|
const [first] = node.expression.arguments;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
return first ? optionsOf(first) : { kind: 'empty', call: node.expression };
|
|
70
|
+
}
|
|
71
|
+
/** The options `value` writes: a literal to read and write into, or why it is not one. */
|
|
72
|
+
function optionsOf(value) {
|
|
73
|
+
if (!ts.isObjectLiteralExpression(value)) {
|
|
74
|
+
return { kind: 'opaque', reason: `its options are passed as '${value.getText()}'` };
|
|
69
75
|
}
|
|
70
|
-
return
|
|
76
|
+
return value.properties.some(ts.isSpreadAssignment)
|
|
71
77
|
? { kind: 'opaque', reason: 'its options object spreads another' }
|
|
72
|
-
: { kind: 'literal', node:
|
|
78
|
+
: { kind: 'literal', node: value };
|
|
73
79
|
}
|
|
74
80
|
/**
|
|
75
|
-
* The property `name` in the
|
|
81
|
+
* The property `name` in the options, whether its key is plain, quoted or a shorthand.
|
|
76
82
|
*
|
|
77
83
|
* All three forms, because both callers care about the option being *there*: one to leave an option
|
|
78
84
|
* alone that is already stated, the other to rename it. Matching only the plain key inserted a second
|
|
@@ -85,27 +91,44 @@ function findProperty(options, name) {
|
|
|
85
91
|
return options.node.properties.find((prop) => (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && propertyKey(prop.name) === name);
|
|
86
92
|
}
|
|
87
93
|
/**
|
|
88
|
-
* Renames the `
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* object survive untouched. A shorthand has no value to keep, so it becomes `computed: virtual` -
|
|
92
|
-
* renaming the key alone would rebind it to a local that does not exist.
|
|
94
|
+
* Renames the option `from` to `to`. Only the key is replaced, so the value, its formatting and any comment
|
|
95
|
+
* inside survive; a shorthand has no value to keep, so it becomes `to: from`, since renaming the key alone
|
|
96
|
+
* would rebind it to a local that does not exist.
|
|
93
97
|
*/
|
|
94
|
-
function
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
if (!virtual) {
|
|
98
|
-
if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
|
|
99
|
-
ctx.unresolved.push(`${ctx.describe(node)}: ${options.reason}`);
|
|
100
|
-
}
|
|
98
|
+
function renameOption(options, from, to, node, ctx) {
|
|
99
|
+
const property = findProperty(options, from);
|
|
100
|
+
if (!property) {
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
|
-
if (findProperty(options,
|
|
104
|
-
ctx.unresolved.push(`${ctx.describe(node)}: gives both '
|
|
103
|
+
if (findProperty(options, to)) {
|
|
104
|
+
ctx.unresolved.push(`${ctx.describe(node)}: gives both '${from}' and '${to}'; keep '${to}'.`);
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
ctx.edits.push(replaced(property.name, ts.isShorthandPropertyAssignment(property) ? `${to}: ${from}` : to));
|
|
108
|
+
}
|
|
109
|
+
/** {@link renameOption} for an option holding SQL - a field's `virtual`, a check's `expression` - noting that SQL. */
|
|
110
|
+
function renameSqlOption(options, from, to, node, owner, ctx) {
|
|
111
|
+
renameOption(options, from, to, node, ctx);
|
|
112
|
+
if (options.kind === 'literal') {
|
|
113
|
+
noteHandNamedColumns(propertyValue(options.node, from) ?? propertyValue(options.node, to), owner, ctx);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Notes SQL that names a column by hand, which a rename does not reach. */
|
|
117
|
+
function noteHandNamedColumns(sql, owner, ctx) {
|
|
118
|
+
const names = sql ? handNamedColumns(sql, memberNames(owner.entity, ctx.checker)) : [];
|
|
119
|
+
if (!sql || !names.length) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
ctx.notes.push(`${ctx.describe(sql)}: SQL names ${names.map((name) => `'${name}'`).join(', ')} by hand, which a rename ` +
|
|
123
|
+
`does not reach; read each off a callback's refs instead: (${owner.param}) => raw\`...\${${owner.param}.<member>}...\``);
|
|
124
|
+
}
|
|
125
|
+
/** The names of the members of the entity `node` declares or names. */
|
|
126
|
+
function memberNames(node, checker) {
|
|
127
|
+
const type = checker.getTypeAtLocation(node);
|
|
128
|
+
return propertyNames(type.getConstructSignatures()[0]?.getReturnType() ?? type);
|
|
129
|
+
}
|
|
130
|
+
function propertyNames(type) {
|
|
131
|
+
return new Set(type.getProperties().map(({ name }) => name));
|
|
109
132
|
}
|
|
110
133
|
/** Inserts `option` into the decorator's options object, creating one when the call has no arguments. */
|
|
111
134
|
function insertOption(options, option) {
|
|
@@ -115,9 +138,7 @@ function insertOption(options, option) {
|
|
|
115
138
|
return { start: args.pos, end: args.end, text: `{ ${option} }` };
|
|
116
139
|
}
|
|
117
140
|
const first = options.node.properties[0];
|
|
118
|
-
return first
|
|
119
|
-
? { start: first.getStart(), end: first.getStart(), text: `${option}, ` }
|
|
120
|
-
: { start: options.node.getStart(), end: options.node.getEnd(), text: `{ ${option} }` };
|
|
141
|
+
return first ? inserted(first, `${option}, `) : replaced(options.node, `{ ${option} }`);
|
|
121
142
|
}
|
|
122
143
|
/**
|
|
123
144
|
* Writes one option the decorator can no longer infer at runtime, or records why it could not.
|
|
@@ -174,7 +195,7 @@ function addRelationEntity(decorator, node, ctx) {
|
|
|
174
195
|
});
|
|
175
196
|
}
|
|
176
197
|
/**
|
|
177
|
-
* Writes the
|
|
198
|
+
* Writes the edits `rewrite` reads off `value`, which still names members as strings, or reports it
|
|
178
199
|
* where it could not be read: what it holds is only known at runtime. A callback or an object literal
|
|
179
200
|
* is already the new form, and kept.
|
|
180
201
|
*/
|
|
@@ -182,23 +203,23 @@ function rewriteValue(value, rewrite, node, advice, ctx) {
|
|
|
182
203
|
if (!value || isCallback(value) || ts.isObjectLiteralExpression(value)) {
|
|
183
204
|
return;
|
|
184
205
|
}
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
ctx.edits.push(
|
|
206
|
+
const edits = rewrite(value);
|
|
207
|
+
if (edits) {
|
|
208
|
+
ctx.edits.push(...edits);
|
|
188
209
|
}
|
|
189
210
|
else {
|
|
190
211
|
ctx.unresolved.push(`${ctx.describe(node)}: ${advice}; this one could not be read`);
|
|
191
212
|
}
|
|
192
213
|
}
|
|
193
214
|
function rewriteKeyList(value, param, node, what, ctx) {
|
|
194
|
-
rewriteValue(value, (list) =>
|
|
215
|
+
rewriteValue(value, (list) => keyListEdits(list, param), node, `write ${what} as a key-map callback`, ctx);
|
|
195
216
|
}
|
|
196
217
|
/**
|
|
197
|
-
* Rewrites
|
|
198
|
-
*
|
|
218
|
+
* Rewrites a class's decorators - `@Index`, `@Filter` and `@Entity`'s options - for what they define now:
|
|
219
|
+
* members named by string read off key maps named after the class, and SQL written as it is taken.
|
|
199
220
|
*/
|
|
200
221
|
function rewriteClassDecorators(node, ctx) {
|
|
201
|
-
const
|
|
222
|
+
const owner = { param: paramFor(node.name?.text), entity: node };
|
|
202
223
|
for (const decorator of decoratorsOf(node)) {
|
|
203
224
|
if (!ts.isCallExpression(decorator.expression)) {
|
|
204
225
|
continue;
|
|
@@ -206,54 +227,118 @@ function rewriteClassDecorators(node, ctx) {
|
|
|
206
227
|
const [first, second] = decorator.expression.arguments;
|
|
207
228
|
const name = decoratorName(decorator);
|
|
208
229
|
if (name === 'Index') {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
rewriteIndex(first, second, owner, decorator, "'@Index' columns", ctx);
|
|
231
|
+
}
|
|
232
|
+
else if (name === 'Filter' && second) {
|
|
233
|
+
renameOption(optionsOf(second), 'condition', 'where', decorator, ctx);
|
|
213
234
|
}
|
|
214
235
|
else if (name === 'Entity' && first && ts.isObjectLiteralExpression(first)) {
|
|
215
|
-
rewriteEntityOptions(first,
|
|
236
|
+
rewriteEntityOptions(first, owner, ctx);
|
|
216
237
|
}
|
|
217
238
|
}
|
|
218
239
|
}
|
|
219
240
|
/**
|
|
220
|
-
* Rewrites
|
|
221
|
-
* `
|
|
222
|
-
*
|
|
241
|
+
* Rewrites the imperative API the same way: `defineEntity(Post, { ... })`, `defineIndex(Post, { columns })`,
|
|
242
|
+
* `defineRelation(Post, 'tag', { mappedBy, references })` and `defineFilter(Post, 'live', { condition })`.
|
|
243
|
+
* Only uql's shapes, a class and a literal options object, are read.
|
|
223
244
|
*/
|
|
224
245
|
function rewriteDefineCall(call, ctx) {
|
|
225
246
|
const name = ts.isIdentifier(call.expression) ? call.expression.text : undefined;
|
|
226
|
-
const [entity, second, third] = call.arguments;
|
|
227
|
-
const
|
|
247
|
+
const [entity = call, second, third] = call.arguments;
|
|
248
|
+
const owner = { param: paramFor(ts.isIdentifier(entity) ? entity.text : undefined), entity };
|
|
228
249
|
if (name === 'defineEntity' && second && ts.isObjectLiteralExpression(second)) {
|
|
229
|
-
rewriteEntityOptions(second,
|
|
250
|
+
rewriteEntityOptions(second, owner, ctx);
|
|
230
251
|
}
|
|
231
252
|
else if (name === 'defineIndex' && second && ts.isObjectLiteralExpression(second)) {
|
|
232
|
-
rewriteIndexOptions(second,
|
|
253
|
+
rewriteIndexOptions(second, owner, ctx);
|
|
233
254
|
}
|
|
234
255
|
else if (name === 'defineRelation' && third && ts.isObjectLiteralExpression(third)) {
|
|
235
|
-
rewriteRelationOptions(third, param, ctx);
|
|
256
|
+
rewriteRelationOptions(third, owner.param, ctx);
|
|
257
|
+
}
|
|
258
|
+
else if (name === 'defineFilter' && third) {
|
|
259
|
+
renameOption(optionsOf(third), 'condition', 'where', call, ctx);
|
|
236
260
|
}
|
|
237
261
|
}
|
|
238
|
-
function rewriteEntityOptions(options,
|
|
239
|
-
const
|
|
240
|
-
for (const index of indexes && ts.isArrayLiteralExpression(indexes) ? indexes.elements : []) {
|
|
262
|
+
function rewriteEntityOptions(options, owner, ctx) {
|
|
263
|
+
for (const index of arrayElements(propertyValue(options, 'indexes'))) {
|
|
241
264
|
if (ts.isObjectLiteralExpression(index)) {
|
|
242
|
-
rewriteIndexOptions(index,
|
|
265
|
+
rewriteIndexOptions(index, owner, ctx);
|
|
243
266
|
}
|
|
244
267
|
}
|
|
268
|
+
for (const check of arrayElements(propertyValue(options, 'checks'))) {
|
|
269
|
+
renameSqlOption(optionsOf(check), 'expression', 'where', check, owner, ctx);
|
|
270
|
+
}
|
|
271
|
+
for (const field of objectProperties(propertyValue(options, 'fields'))) {
|
|
272
|
+
renameSqlOption(optionsOf(field.initializer), 'virtual', 'computed', field, owner, ctx);
|
|
273
|
+
}
|
|
274
|
+
for (const filter of objectProperties(propertyValue(options, 'filters'))) {
|
|
275
|
+
renameOption(optionsOf(filter.initializer), 'condition', 'where', filter, ctx);
|
|
276
|
+
}
|
|
245
277
|
for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
|
|
246
|
-
rewriteKeyList(hook.initializer, param, hook, 'a hook list', ctx);
|
|
278
|
+
rewriteKeyList(hook.initializer, owner.param, hook, 'a hook list', ctx);
|
|
247
279
|
}
|
|
248
280
|
for (const relation of objectProperties(propertyValue(options, 'relations'))) {
|
|
249
281
|
if (ts.isObjectLiteralExpression(relation.initializer)) {
|
|
250
|
-
rewriteRelationOptions(relation.initializer, param, ctx);
|
|
282
|
+
rewriteRelationOptions(relation.initializer, owner.param, ctx);
|
|
251
283
|
}
|
|
252
284
|
}
|
|
253
285
|
}
|
|
254
|
-
function rewriteIndexOptions(index,
|
|
255
|
-
|
|
256
|
-
|
|
286
|
+
function rewriteIndexOptions(index, owner, ctx) {
|
|
287
|
+
rewriteIndex(propertyValue(index, 'columns'), index, owner, index, "'columns'", ctx);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* An index, as `@Index(columns, options)` or a `defineIndex`/`indexes` entry holding both: its columns and
|
|
291
|
+
* `include` read off the key map, its SQL written as {@link rewriteIndexSql} writes it, and noted.
|
|
292
|
+
*/
|
|
293
|
+
function rewriteIndex(columns, options, owner, node, what, ctx) {
|
|
294
|
+
const literal = options && ts.isObjectLiteralExpression(options) ? options : undefined;
|
|
295
|
+
rewriteKeyList(columns, owner.param, node, what, ctx);
|
|
296
|
+
rewriteKeyList(literal && propertyValue(literal, 'include'), owner.param, node, "'include'", ctx);
|
|
297
|
+
rewriteIndexSql(columns, literal, ctx);
|
|
298
|
+
for (const sql of [...columnExpressions(columns), literal && propertyValue(literal, 'where')]) {
|
|
299
|
+
noteHandNamedColumns(sql, owner, ctx);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* An index's SQL as an index takes it now, on an entity and in the migration builder alike: each expression
|
|
304
|
+
* a callback, and a `where` string `raw`. A string it cannot write as `raw` is reported.
|
|
305
|
+
*/
|
|
306
|
+
function rewriteIndexSql(columns, options, ctx) {
|
|
307
|
+
ctx.edits.push(...expressionEntryEdits(columns));
|
|
308
|
+
const where = options && propertyValue(options, 'where');
|
|
309
|
+
if (!where || !isStringTyped(ctx.checker.getTypeAtLocation(where))) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const edit = rawWhereEdit(where);
|
|
313
|
+
if (edit) {
|
|
314
|
+
ctx.edits.push(edit);
|
|
315
|
+
ctx.imports.set('raw', 'the raw`...`');
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
ctx.unresolved.push(`${ctx.describe(where)}: write the partial-index 'where' as raw\`...\` or a predicate; this one could not be read`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** Whether every value `type` admits is a string. */
|
|
322
|
+
function isStringTyped(type) {
|
|
323
|
+
return type.isUnion() ? type.types.every(isStringTyped) : Boolean(type.flags & ts.TypeFlags.StringLike);
|
|
324
|
+
}
|
|
325
|
+
/** The migration builder's index methods, each with the position of the columns it takes. */
|
|
326
|
+
const BUILDER_INDEX_METHODS = new Map([
|
|
327
|
+
['index', 0],
|
|
328
|
+
['unique', 0],
|
|
329
|
+
['addIndex', 0],
|
|
330
|
+
['createIndex', 1],
|
|
331
|
+
]);
|
|
332
|
+
/** Rewrites the SQL of the migration builder's `t.index()`, `t.unique()`, `t.addIndex()` and `m.createIndex()`. */
|
|
333
|
+
function rewriteBuilderCall(call, ctx) {
|
|
334
|
+
const at = ts.isPropertyAccessExpression(call.expression)
|
|
335
|
+
? BUILDER_INDEX_METHODS.get(call.expression.name.text)
|
|
336
|
+
: undefined;
|
|
337
|
+
if (at === undefined) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const options = call.arguments[at + 1];
|
|
341
|
+
rewriteIndexSql(call.arguments[at], options && ts.isObjectLiteralExpression(options) ? options : undefined, ctx);
|
|
257
342
|
}
|
|
258
343
|
/**
|
|
259
344
|
* `owner` is the declaring class's parameter and `target` the related one's, read off the `entity`
|
|
@@ -261,15 +346,15 @@ function rewriteIndexOptions(index, param, ctx) {
|
|
|
261
346
|
*/
|
|
262
347
|
function rewriteRelationOptions(relation, owner, ctx, target = paramFor(entityGetterTarget(relation))) {
|
|
263
348
|
const advice = (what) => `write '${what}' as a key-map callback`;
|
|
264
|
-
const mappedBy = (value) =>
|
|
265
|
-
const references = (value) =>
|
|
349
|
+
const mappedBy = (value) => mappedByEdits(value, target);
|
|
350
|
+
const references = (value) => referencesEdits(value, owner, target);
|
|
266
351
|
rewriteValue(propertyValue(relation, 'mappedBy'), mappedBy, relation, advice('mappedBy'), ctx);
|
|
267
352
|
rewriteValue(propertyValue(relation, 'references'), references, relation, advice('references'), ctx);
|
|
268
353
|
}
|
|
269
354
|
/**
|
|
270
|
-
* Rewrites
|
|
271
|
-
*
|
|
272
|
-
* `
|
|
355
|
+
* Rewrites a statement's keys: an aggregate's `$agg: { total: { $sum: 'amount' } }` into
|
|
356
|
+
* `$select: { total: { $sum: { amount: true } } }`, `$text`'s `$fields: ['title']` into `{ title: true }`, and
|
|
357
|
+
* `$lock`'s `wait` into `$wait`. Each key is uql's own, so it is read wherever a literal holds it.
|
|
273
358
|
*/
|
|
274
359
|
function rewriteStatementKeys(node, ctx) {
|
|
275
360
|
const key = propertyKey(node.name);
|
|
@@ -277,7 +362,21 @@ function rewriteStatementKeys(node, ctx) {
|
|
|
277
362
|
rewriteAggregate(node, ctx);
|
|
278
363
|
}
|
|
279
364
|
else if (key === '$fields' && isTextSearch(node.parent)) {
|
|
280
|
-
rewriteValue(node.initializer,
|
|
365
|
+
rewriteValue(node.initializer, fieldKeysEdits, node, "write '$fields' as { field: true }", ctx);
|
|
366
|
+
}
|
|
367
|
+
else if (key === '$lock' && ts.isObjectLiteralExpression(node.initializer)) {
|
|
368
|
+
rewriteLock(node.initializer, ctx);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/** `{ wait: 'skip' }` as `{ $wait: 'skip' }`, and `{}` or a `'block'` wait as `true`, which waits. */
|
|
372
|
+
function rewriteLock(lock, ctx) {
|
|
373
|
+
const wait = propertyValue(lock, 'wait');
|
|
374
|
+
const blocks = wait && ts.isStringLiteralLike(wait) && wait.text === 'block';
|
|
375
|
+
if (!lock.properties.length || (blocks && lock.properties.length === 1)) {
|
|
376
|
+
ctx.edits.push(replaced(lock, 'true'));
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
renameOption(optionsOf(lock), 'wait', '$wait', lock, ctx);
|
|
281
380
|
}
|
|
282
381
|
}
|
|
283
382
|
function isTextSearch(node) {
|
|
@@ -295,7 +394,7 @@ function rewriteAggregate(agg, ctx) {
|
|
|
295
394
|
for (const op of objectProperties(fn.initializer)) {
|
|
296
395
|
const isStar = ts.isStringLiteralLike(op.initializer) && op.initializer.text === '*';
|
|
297
396
|
if (!isStar) {
|
|
298
|
-
rewriteValue(op.initializer,
|
|
397
|
+
rewriteValue(op.initializer, fieldKeysEdits, op, `write '${op.name.getText()}' as { field: true }`, ctx);
|
|
299
398
|
}
|
|
300
399
|
}
|
|
301
400
|
}
|
|
@@ -304,6 +403,10 @@ function rewriteAggregate(agg, ctx) {
|
|
|
304
403
|
function objectProperties(node) {
|
|
305
404
|
return node && ts.isObjectLiteralExpression(node) ? node.properties.filter(ts.isPropertyAssignment) : [];
|
|
306
405
|
}
|
|
406
|
+
/** The elements of an array literal, or none where `node` is not one. */
|
|
407
|
+
function arrayElements(node) {
|
|
408
|
+
return node && ts.isArrayLiteralExpression(node) ? node.elements : [];
|
|
409
|
+
}
|
|
307
410
|
/**
|
|
308
411
|
* Drops `declare` from a decorated field. A `declare` field emits nothing, so the standard spec has
|
|
309
412
|
* nothing to decorate and rejects it ("Decorators are not valid here"). Legacy decorators tolerated it
|
|
@@ -334,7 +437,7 @@ function unwrapRelationAlias(node, ctx) {
|
|
|
334
437
|
ts.isIdentifier(declared.typeName) &&
|
|
335
438
|
declared.typeName.text === 'Relation' &&
|
|
336
439
|
declared.typeArguments?.length === 1) {
|
|
337
|
-
ctx.edits.push(
|
|
440
|
+
ctx.edits.push(replaced(declared, declared.typeArguments[0].getText()));
|
|
338
441
|
ctx.relationAlias.unwrapped += 1;
|
|
339
442
|
}
|
|
340
443
|
}
|
|
@@ -365,11 +468,10 @@ function deadImportNames(source, ctx) {
|
|
|
365
468
|
return new Set(seen > 0 && seen === unwrapped ? ['Relation'] : []);
|
|
366
469
|
}
|
|
367
470
|
/**
|
|
368
|
-
* Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and
|
|
369
|
-
*
|
|
471
|
+
* Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and each `dead` name
|
|
472
|
+
* from the imports {@link renameExports} left as written, having left the dead names out of the rest.
|
|
370
473
|
*/
|
|
371
|
-
function dropDeadImports(source, ctx) {
|
|
372
|
-
const dead = deadImportNames(source, ctx);
|
|
474
|
+
function dropDeadImports(source, dead, rewritten, ctx) {
|
|
373
475
|
for (const statement of source.statements) {
|
|
374
476
|
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
375
477
|
continue;
|
|
@@ -379,7 +481,7 @@ function dropDeadImports(source, ctx) {
|
|
|
379
481
|
continue;
|
|
380
482
|
}
|
|
381
483
|
const named = statement.importClause?.namedBindings;
|
|
382
|
-
if (!named || !ts.isNamedImports(named)) {
|
|
484
|
+
if (!named || !ts.isNamedImports(named) || rewritten.has(statement)) {
|
|
383
485
|
continue;
|
|
384
486
|
}
|
|
385
487
|
const dropped = named.elements.filter((element) => dead.has(element.name.text));
|
|
@@ -408,63 +510,70 @@ function isIdKeyBrand(member) {
|
|
|
408
510
|
* a subclass that replaces an inherited `id` needs the brand precisely because the inherited name
|
|
409
511
|
* would otherwise be taken for the key.
|
|
410
512
|
*
|
|
411
|
-
*
|
|
513
|
+
* One written makes `idKey` a value the file has to import, which is recorded for {@link addImports}.
|
|
412
514
|
*/
|
|
413
515
|
function brandIdKey(node, ctx) {
|
|
414
516
|
const ids = node.members.filter((m) => ts.isPropertyDeclaration(m) && decoratorsOf(m).some((d) => decoratorName(d) === 'Id'));
|
|
415
517
|
if (!ids.length || node.members.some(isIdKeyBrand)) {
|
|
416
|
-
return
|
|
518
|
+
return;
|
|
417
519
|
}
|
|
418
520
|
const names = ids.map((m) => propertyKey(m.name)).filter((name) => name !== undefined);
|
|
419
521
|
if (names.length !== ids.length) {
|
|
420
522
|
ctx.unresolved.push(`${ctx.describe(node)}: a key written as a computed name; add the 'idKey' brand by hand`);
|
|
421
|
-
return
|
|
523
|
+
return;
|
|
422
524
|
}
|
|
423
525
|
if (names.length === 1 && CONVENTIONAL_ID_NAMES.has(names[0])) {
|
|
424
|
-
return
|
|
526
|
+
return;
|
|
425
527
|
}
|
|
426
528
|
const indent = ' '.repeat(node.getSourceFile().getLineAndCharacterOfPosition(ids[0].getStart()).character);
|
|
427
529
|
const brand = names.map((name) => `'${name}'`).join(' | ');
|
|
428
530
|
const start = node.members.pos;
|
|
429
531
|
ctx.edits.push({ start, end: start, text: `\n${indent}[idKey]?: ${brand};` });
|
|
430
|
-
|
|
532
|
+
ctx.imports.set('idKey', 'the brand(s)');
|
|
431
533
|
}
|
|
432
534
|
/**
|
|
433
|
-
* Imports
|
|
535
|
+
* Imports what the rewrites wrote (`idKey`, `raw`) into the file's own `uql-orm` import. Reported instead
|
|
434
536
|
* where there is none: the package may be imported under a path this codemod does not recognise.
|
|
435
537
|
*/
|
|
436
|
-
function
|
|
437
|
-
const imported = uqlImports(source);
|
|
438
|
-
|
|
538
|
+
function addImports(source, rewritten, ctx) {
|
|
539
|
+
const imported = new Set(uqlImports(source).map(importedName));
|
|
540
|
+
const missing = [...ctx.imports].filter(([name]) => !imported.has(name));
|
|
541
|
+
if (!missing.length) {
|
|
439
542
|
return;
|
|
440
543
|
}
|
|
441
|
-
const
|
|
544
|
+
const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration))?.elements[0];
|
|
442
545
|
if (!anchor) {
|
|
443
|
-
|
|
546
|
+
for (const [name, what] of missing) {
|
|
547
|
+
ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
|
|
548
|
+
}
|
|
444
549
|
return;
|
|
445
550
|
}
|
|
446
|
-
ctx.edits.push(
|
|
551
|
+
ctx.edits.push(inserted(anchor, missing.map(([name]) => `${name}, `).join('')));
|
|
447
552
|
}
|
|
448
|
-
/** Everything the standard spec needs written onto one decorated property. */
|
|
553
|
+
/** Everything the standard spec needs written onto one decorated property, and the options renamed since. */
|
|
449
554
|
function rewriteProperty(node, ctx) {
|
|
555
|
+
const owner = { param: paramFor(node.parent.name?.text), entity: node.parent };
|
|
450
556
|
const decorators = decoratorsOf(node);
|
|
451
557
|
for (const decorator of decorators) {
|
|
452
558
|
const name = decoratorName(decorator);
|
|
453
559
|
if (!name) {
|
|
454
560
|
continue;
|
|
455
561
|
}
|
|
562
|
+
const options = decoratorOptions(decorator);
|
|
456
563
|
if (FIELD_DECORATORS.has(name)) {
|
|
457
564
|
addFieldType(decorator, node, ctx);
|
|
458
|
-
|
|
565
|
+
// Options it cannot read may still hold `virtual`, so they are reported where they mention it.
|
|
566
|
+
if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
|
|
567
|
+
ctx.unresolved.push(`${ctx.describe(node)}: ${options.reason}`);
|
|
568
|
+
}
|
|
569
|
+
renameSqlOption(options, 'virtual', 'computed', node, owner, ctx);
|
|
459
570
|
}
|
|
460
|
-
const options = decoratorOptions(decorator);
|
|
461
571
|
if (RELATION_DECORATORS.has(name)) {
|
|
462
572
|
addRelationEntity(decorator, node, ctx);
|
|
463
573
|
}
|
|
464
574
|
if (RELATION_DECORATORS.has(name) && options.kind === 'literal') {
|
|
465
|
-
const owner = ts.isClassLike(node.parent) ? node.parent.name?.text : undefined;
|
|
466
575
|
const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
|
|
467
|
-
rewriteRelationOptions(options.node,
|
|
576
|
+
rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
|
|
468
577
|
}
|
|
469
578
|
}
|
|
470
579
|
if (decorators.length) {
|
|
@@ -493,24 +602,49 @@ function reportRemovedExports(source, ctx) {
|
|
|
493
602
|
}
|
|
494
603
|
}
|
|
495
604
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
605
|
+
* Rewrites each import naming a {@link RENAMED_EXPORTS} export, renaming every use of it: a name moving
|
|
606
|
+
* entries gets an import from its new one, and a name the file already imports is dropped, as is a `dead`
|
|
607
|
+
* one. Returns the rewritten imports, which nothing else may edit.
|
|
498
608
|
*/
|
|
499
|
-
function renameExports(source, ctx) {
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
609
|
+
function renameExports(source, dead, ctx) {
|
|
610
|
+
const imports = uqlImportDeclarations(source, true);
|
|
611
|
+
const kept = imports.flatMap(({ elements }) => elements).filter((element) => !renamedTo(element));
|
|
612
|
+
const bound = new Set(kept.map((element) => element.name.text));
|
|
613
|
+
const rewritten = new Set();
|
|
614
|
+
for (const { declaration, entry, elements } of imports) {
|
|
615
|
+
if (!elements.some(renamedTo)) {
|
|
504
616
|
continue;
|
|
505
617
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
618
|
+
const byEntry = new Map([[entry, []]]);
|
|
619
|
+
for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
|
|
620
|
+
const rename = renamedTo(element);
|
|
621
|
+
const local = element.propertyName || !rename ? element.name.text : rename.to;
|
|
622
|
+
if (rename && !element.propertyName) {
|
|
623
|
+
ctx.edits.push(...usesOf(source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
|
|
624
|
+
}
|
|
625
|
+
if (rename && bound.has(local)) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
bound.add(local);
|
|
629
|
+
const target = rename?.from ?? entry;
|
|
630
|
+
const text = rename
|
|
631
|
+
? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
|
|
632
|
+
: element.getText();
|
|
633
|
+
byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
|
|
509
634
|
}
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
635
|
+
const typeOnly = declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
|
|
636
|
+
const keyword = typeOnly ? 'import type' : 'import';
|
|
637
|
+
const quote = declaration.moduleSpecifier.getText()[0];
|
|
638
|
+
const statements = [...byEntry]
|
|
639
|
+
.filter(([, names]) => names.length)
|
|
640
|
+
.map(([from, names]) => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`);
|
|
641
|
+
ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
|
|
642
|
+
rewritten.add(declaration);
|
|
513
643
|
}
|
|
644
|
+
return rewritten;
|
|
645
|
+
}
|
|
646
|
+
function renamedTo(element) {
|
|
647
|
+
return RENAMED_EXPORTS.get(importedName(element));
|
|
514
648
|
}
|
|
515
649
|
/** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
|
|
516
650
|
function usesOf(source, name, checker) {
|
|
@@ -528,50 +662,187 @@ function usesOf(source, name, checker) {
|
|
|
528
662
|
return uses;
|
|
529
663
|
}
|
|
530
664
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
* into one of those would land on an entry that may not export it.
|
|
665
|
+
* The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
|
|
666
|
+
* for what is read by name only: `idKey` written into one would land on an entry that may not export it.
|
|
534
667
|
*/
|
|
535
|
-
function
|
|
536
|
-
return source.statements.flatMap((
|
|
537
|
-
if (!ts.isImportDeclaration(
|
|
668
|
+
function uqlImportDeclarations(source, entries = false) {
|
|
669
|
+
return source.statements.flatMap((declaration) => {
|
|
670
|
+
if (!ts.isImportDeclaration(declaration) || !ts.isStringLiteral(declaration.moduleSpecifier)) {
|
|
538
671
|
return [];
|
|
539
672
|
}
|
|
540
|
-
const
|
|
541
|
-
if (
|
|
673
|
+
const entry = declaration.moduleSpecifier.text;
|
|
674
|
+
if (entry !== 'uql-orm' && !(entries && entry.startsWith('uql-orm/'))) {
|
|
542
675
|
return [];
|
|
543
676
|
}
|
|
544
|
-
const bindings =
|
|
545
|
-
return bindings && ts.isNamedImports(bindings) ? [
|
|
677
|
+
const bindings = declaration.importClause?.namedBindings;
|
|
678
|
+
return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
|
|
546
679
|
});
|
|
547
680
|
}
|
|
681
|
+
function uqlImports(source, entries = false) {
|
|
682
|
+
return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
|
|
683
|
+
}
|
|
548
684
|
/** The name an import specifier brings in, which is the original one where it was renamed. */
|
|
549
685
|
function importedName(element) {
|
|
550
686
|
return (element.propertyName ?? element.name).text;
|
|
551
687
|
}
|
|
552
688
|
/**
|
|
553
|
-
* Rewrites `raw('sql')` into the tagged template, and a second alias argument into `.as()
|
|
554
|
-
* string
|
|
555
|
-
* left alone because a template cannot be built from a value that is not known here.
|
|
689
|
+
* Rewrites `raw('sql')` into the tagged template, and a second alias argument into `.as()`, the callback
|
|
690
|
+
* form's included. A computed string is left alone: a template cannot be built from a value not known here.
|
|
556
691
|
*/
|
|
557
692
|
function rewriteRawCall(node, ctx, source) {
|
|
558
693
|
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== 'raw') {
|
|
559
694
|
return;
|
|
560
695
|
}
|
|
561
696
|
const [expression, alias] = node.arguments;
|
|
562
|
-
if (!expression ||
|
|
697
|
+
if (!expression || node.arguments.length > 2) {
|
|
563
698
|
return;
|
|
564
699
|
}
|
|
565
700
|
const suffix = alias ? `.as(${alias.getText(source)})` : '';
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
701
|
+
if (ts.isStringLiteral(expression)) {
|
|
702
|
+
ctx.edits.push(replaced(node, `${rawTag(expression.text)}${suffix}`));
|
|
703
|
+
}
|
|
704
|
+
else if (alias) {
|
|
705
|
+
ctx.edits.push({ start: expression.getEnd(), end: node.getEnd(), text: `)${suffix}` });
|
|
706
|
+
}
|
|
571
707
|
}
|
|
572
|
-
/**
|
|
573
|
-
|
|
574
|
-
|
|
708
|
+
/**
|
|
709
|
+
* Rewrites each `col('x')` into the ref it names - `product.x` in a `computed` callback, `refs(Item).x` in a
|
|
710
|
+
* statement - where the entity its SQL renders against, and a member named `x`, can be told from here. Every
|
|
711
|
+
* other use is reported. Returns `col` as a dead import once no use is left.
|
|
712
|
+
*/
|
|
713
|
+
function rewriteColumnRefs(source, ctx) {
|
|
714
|
+
const element = uqlImports(source).find((use) => importedName(use) === 'col');
|
|
715
|
+
if (!element) {
|
|
716
|
+
return [];
|
|
717
|
+
}
|
|
718
|
+
const computed = new Map();
|
|
719
|
+
const unread = [];
|
|
720
|
+
for (const use of usesOf(source, element.name, ctx.checker)) {
|
|
721
|
+
const call = ts.isCallExpression(use.parent) && use.parent.expression === use ? use.parent : undefined;
|
|
722
|
+
const sql = call && ts.findAncestor(call, isRaw);
|
|
723
|
+
const option = sql?.parent;
|
|
724
|
+
const owner = option && ts.isPropertyAssignment(option) && isComputedOption(option) ? computedOwner(option) : undefined;
|
|
725
|
+
const edit = call && !owner ? statementRefEdit(call, ctx) : undefined;
|
|
726
|
+
if (call && sql && ts.isTaggedTemplateExpression(sql) && owner) {
|
|
727
|
+
computed.set(sql, { owner, calls: [...(computed.get(sql)?.calls ?? []), call] });
|
|
728
|
+
}
|
|
729
|
+
else if (edit) {
|
|
730
|
+
ctx.edits.push(edit);
|
|
731
|
+
ctx.imports.set('refs', 'the refs(...)');
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
unread.push(use);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
for (const [sql, { owner, calls }] of computed) {
|
|
738
|
+
const edits = computedRefEdits(sql, calls, owner, ctx);
|
|
739
|
+
if (edits) {
|
|
740
|
+
ctx.edits.push(...edits);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
unread.push(...calls);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const node of unread.sort((a, b) => a.getStart() - b.getStart())) {
|
|
747
|
+
ctx.unresolved.push(`${ctx.describe(node)}: 'col' was removed; read the column off refs(Entity), which the codemod could not tell here`);
|
|
748
|
+
}
|
|
749
|
+
return unread.length ? [] : ['col'];
|
|
750
|
+
}
|
|
751
|
+
function isComputedOption(option) {
|
|
752
|
+
const key = propertyKey(option.name);
|
|
753
|
+
return key === 'computed' || key === 'virtual';
|
|
754
|
+
}
|
|
755
|
+
/** The entity a field's `computed` SQL is declared on: the decorated property's class, or `defineEntity`'s first argument. */
|
|
756
|
+
function computedOwner(option) {
|
|
757
|
+
const holder = ts.findAncestor(option, (node) => ts.isPropertyDeclaration(node) ||
|
|
758
|
+
(ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'defineEntity'));
|
|
759
|
+
if (holder && ts.isPropertyDeclaration(holder)) {
|
|
760
|
+
return { param: paramFor(holder.parent.name?.text), entity: holder.parent };
|
|
761
|
+
}
|
|
762
|
+
const [entity] = holder?.arguments ?? [];
|
|
763
|
+
return entity && { param: paramFor(ts.isIdentifier(entity) ? entity.text : undefined), entity };
|
|
764
|
+
}
|
|
765
|
+
/** `(product) => ` before a `computed` raw, and `product.x` for each `col('x')` in it, where every one names a member. */
|
|
766
|
+
function computedRefEdits(sql, calls, owner, ctx) {
|
|
767
|
+
const members = memberNames(owner.entity, ctx.checker);
|
|
768
|
+
const names = calls.map((call) => columnMember(call, members));
|
|
769
|
+
if (mentions(sql, owner.param) || !names.every((name) => name !== undefined)) {
|
|
770
|
+
return undefined;
|
|
771
|
+
}
|
|
772
|
+
return [
|
|
773
|
+
inserted(sql, `(${owner.param}) => `),
|
|
774
|
+
...calls.map((call, at) => replaced(call, memberAccess(owner.param, names[at]))),
|
|
775
|
+
];
|
|
776
|
+
}
|
|
777
|
+
/** `refs(Item).x` for a `col('x')` in a statement, where the entity its SQL renders against can be told. */
|
|
778
|
+
function statementRefEdit(call, ctx) {
|
|
779
|
+
const entity = statementEntity(call, ctx.checker);
|
|
780
|
+
const name = entity && columnMember(call, propertyNames(entity.type));
|
|
781
|
+
return entity && name !== undefined ? replaced(call, memberAccess(`refs(${entity.name})`, name)) : undefined;
|
|
782
|
+
}
|
|
783
|
+
/** The member a `col('x')` names, where its one argument is a literal naming one of `members`. */
|
|
784
|
+
function columnMember(call, members) {
|
|
785
|
+
const [name] = call.arguments;
|
|
786
|
+
return call.arguments.length === 1 && ts.isStringLiteralLike(name) && members.has(name.text) ? name.text : undefined;
|
|
787
|
+
}
|
|
788
|
+
/** Whether `node` reads a binding named `name`, which a callback parameter of that name would shadow. */
|
|
789
|
+
function mentions(node, name) {
|
|
790
|
+
if (ts.isIdentifier(node)) {
|
|
791
|
+
return node.text === name && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node);
|
|
792
|
+
}
|
|
793
|
+
return ts.forEachChild(node, (child) => mentions(child, name) || undefined) ?? false;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* The entity a statement's SQL renders against at `node`: the one its query names, or a relation a `$populate`
|
|
797
|
+
* reads there. Nothing where a function or a relation filter in between opens a scope of its own.
|
|
798
|
+
*/
|
|
799
|
+
function statementEntity(node, checker) {
|
|
800
|
+
const path = [];
|
|
801
|
+
for (let at = node; at.parent; at = at.parent) {
|
|
802
|
+
const { parent } = at;
|
|
803
|
+
if (ts.isFunctionLike(parent)) {
|
|
804
|
+
return undefined;
|
|
805
|
+
}
|
|
806
|
+
if (ts.isPropertyAssignment(parent)) {
|
|
807
|
+
path.unshift(propertyKey(parent.name) ?? '');
|
|
808
|
+
}
|
|
809
|
+
const queried = ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === at);
|
|
810
|
+
const entity = queried ? queriedEntity(parent, checker) : undefined;
|
|
811
|
+
if (entity) {
|
|
812
|
+
return entityAlong(entity, path, node, checker);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return undefined;
|
|
816
|
+
}
|
|
817
|
+
/** The entity a call queries: its first argument's class, or the `$entity` its query names. */
|
|
818
|
+
function queriedEntity(call, checker) {
|
|
819
|
+
const [first] = call.arguments;
|
|
820
|
+
const entity = first && ts.isObjectLiteralExpression(first) ? propertyValue(first, '$entity') : first;
|
|
821
|
+
const instance = entity && checker.getTypeAtLocation(entity).getConstructSignatures()[0]?.getReturnType();
|
|
822
|
+
return entity && ts.isIdentifier(entity) && instance ? { name: entity.text, type: instance } : undefined;
|
|
823
|
+
}
|
|
824
|
+
/** `entity` followed along `path`: into each relation a `$populate` names, and nowhere a relation filter opens. */
|
|
825
|
+
function entityAlong(entity, path, node, checker) {
|
|
826
|
+
let current = entity;
|
|
827
|
+
for (const [at, key] of path.entries()) {
|
|
828
|
+
const property = current.type.getProperty(key);
|
|
829
|
+
const related = property && relatedClass(property, node, checker);
|
|
830
|
+
if (!related) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const inScope = checker.getSymbolsInScope(node, ts.SymbolFlags.Class).some(({ name }) => name === related.name);
|
|
834
|
+
if (path[at - 1] !== '$populate' || !inScope) {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
current = { name: related.name, type: checker.getDeclaredTypeOfSymbol(related) };
|
|
838
|
+
}
|
|
839
|
+
return current;
|
|
840
|
+
}
|
|
841
|
+
/** The class a relation property points at, one or many, or nothing where the property holds no class. */
|
|
842
|
+
function relatedClass(property, node, checker) {
|
|
843
|
+
const type = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(property, node));
|
|
844
|
+
const symbol = (checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type).getSymbol();
|
|
845
|
+
return symbol && symbol.flags & ts.SymbolFlags.Class ? symbol : undefined;
|
|
575
846
|
}
|
|
576
847
|
/** Rewrites one source file for the standard decorator spec. */
|
|
577
848
|
export function transformFile(source, checker) {
|
|
@@ -580,12 +851,14 @@ export function transformFile(source, checker) {
|
|
|
580
851
|
edits: [],
|
|
581
852
|
unresolved: [],
|
|
582
853
|
notes: [],
|
|
854
|
+
imports: new Map(),
|
|
583
855
|
relationAlias: { seen: 0, unwrapped: 0 },
|
|
584
856
|
describe: (node) => `${source.fileName}:${lineOf(node) + 1}`,
|
|
585
857
|
};
|
|
586
858
|
const lineOf = (node) => source.getLineAndCharacterOfPosition(node.getStart()).line;
|
|
587
859
|
const rewritesRaw = uqlImports(source).some((element) => importedName(element) === 'raw');
|
|
588
|
-
|
|
860
|
+
// The builder's methods are known by name alone, so they are read only in a file that imports uql-orm.
|
|
861
|
+
const importsUql = uqlImportDeclarations(source, true).length > 0;
|
|
589
862
|
const visit = (node) => {
|
|
590
863
|
reportRemovedDecorators(node, ctx);
|
|
591
864
|
countRelationAlias(node, ctx);
|
|
@@ -596,24 +869,26 @@ export function transformFile(source, checker) {
|
|
|
596
869
|
rewriteProperty(node, ctx);
|
|
597
870
|
}
|
|
598
871
|
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
|
599
|
-
|
|
872
|
+
brandIdKey(node, ctx);
|
|
600
873
|
rewriteClassDecorators(node, ctx);
|
|
601
874
|
}
|
|
602
875
|
if (ts.isCallExpression(node)) {
|
|
603
876
|
rewriteDefineCall(node, ctx);
|
|
604
877
|
}
|
|
878
|
+
if (importsUql && ts.isCallExpression(node)) {
|
|
879
|
+
rewriteBuilderCall(node, ctx);
|
|
880
|
+
}
|
|
605
881
|
if (ts.isPropertyAssignment(node)) {
|
|
606
882
|
rewriteStatementKeys(node, ctx);
|
|
607
883
|
}
|
|
608
884
|
ts.forEachChild(node, visit);
|
|
609
885
|
};
|
|
610
886
|
visit(source);
|
|
611
|
-
|
|
887
|
+
const dead = new Set([...deadImportNames(source, ctx), ...rewriteColumnRefs(source, ctx)]);
|
|
888
|
+
const rewritten = renameExports(source, dead, ctx);
|
|
612
889
|
reportRemovedExports(source, ctx);
|
|
613
|
-
dropDeadImports(source, ctx);
|
|
614
|
-
|
|
615
|
-
addIdKeyImport(source, ctx);
|
|
616
|
-
}
|
|
890
|
+
dropDeadImports(source, dead, rewritten, ctx);
|
|
891
|
+
addImports(source, rewritten, ctx);
|
|
617
892
|
return {
|
|
618
893
|
fileName: source.fileName,
|
|
619
894
|
text: applyEdits(source.getFullText(), ctx.edits),
|