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