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 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`, and `raw('sql')` to the tagged template.
30
- - Members named by string to key-map callbacks: `mappedBy: 'author'` to `(post) => post.author`, and `@Index`, `include`, `references` and `@Entity`/`defineEntity`'s `indexes` and `hooks` likewise: `@Index((post) => [post.title])`.
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 an earlier edit cannot shift the offsets of a later one.
10
- *
11
- * Replacing text rather than reprinting the file is what keeps the rest of it byte-for-byte: a codemod
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 an earlier edit cannot shift the offsets of a later one.
3
- *
4
- * Replacing text rather than reprinting the file is what keeps the rest of it byte-for-byte: a codemod
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 { Edit } from './edits.js';
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 fieldKeysEdit(value: ts.Expression): Edit | undefined;
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 keyListEdit(list: ts.Expression, param: string): Edit | undefined;
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 mappedByEdit(value: ts.Expression, param: string): Edit | undefined;
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 referencesEdit(value: ts.Expression, owner: string, target: string): Edit | undefined;
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}['${key}']`;
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 fieldKeysEdit(value) {
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 : `'${text}'`}: true`);
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 keyListEdit(list, param) {
37
- const text = ts.isArrayLiteralExpression(list)
38
- ? spliced(list, list.elements.map((entry) => [entry, keyListEntry(entry, param)]))
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 mappedByEdit(value, param) {
44
- return callbackEdit(value, param, ts.isStringLiteralLike(value) ? memberAccess(param, value.text) : undefined);
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 referencesEdit(value, owner, target) {
57
+ export function referencesEdits(value, owner, target) {
52
58
  const [local, foreign] = owner === target ? ['local', 'foreign'] : [owner, target];
53
- const text = ts.isArrayLiteralExpression(value)
54
- ? spliced(value, value.elements.map((pair) => [pair, referenceText(pair, local, foreign)]))
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
- /** An expression entry (`raw\`...\``) and a `column` that is not a string stay as written. */
64
- function keyListEntry(entry, param) {
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 entry.getText();
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 referenceText(pair, local, foreign) {
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
- ? spliced(pair, [
87
- [localKey, memberAccess(local, localKey.text)],
88
- [foreignKey, memberAccess(foreign, foreignKey.text)],
89
- ])
89
+ ? [
90
+ replaced(localKey, memberAccess(local, localKey.text)),
91
+ replaced(foreignKey, memberAccess(foreign, foreignKey.text)),
92
+ ]
90
93
  : undefined;
91
94
  }
92
- /** `node`'s text with each part swapped for its own, everything between kept as written. */
93
- function spliced(node, parts) {
94
- if (!parts.every((part) => part[1] !== undefined)) {
95
- return undefined;
96
- }
97
- const start = node.getStart();
98
- const source = node.getText();
99
- const sorted = [...parts].sort(([a], [b]) => a.getStart() - b.getStart());
100
- const { text, at } = sorted.reduce((acc, [part, replacement]) => ({
101
- text: acc.text + source.slice(acc.at, part.getStart() - start) + replacement,
102
- at: part.getEnd() - start,
103
- }), { text: '', at: 0 });
104
- return text + source.slice(at);
105
- }
106
- /** `value` replaced by `(params) => body`, or nothing when the body could not be read. */
107
- function callbackEdit(value, params, body) {
108
- return body === undefined ? undefined : replaced(value, `(${params}) => ${body}`);
109
- }
110
- export function replaced(node, text) {
111
- return { start: node.getStart(), end: node.getEnd(), text };
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, fieldKeysEdit, isCallback, keyListEdit, mappedByEdit, paramFor, propertyKey, propertyValue, referencesEdit, replaced, } from './keyMaps.js';
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
- if (!first) {
68
- return { kind: 'empty', call: node.expression };
69
- }
70
- if (!ts.isObjectLiteralExpression(first)) {
71
- return { kind: 'opaque', reason: `its options are passed as '${first.getText()}'` };
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 first.properties.some(ts.isSpreadAssignment)
92
+ return value.properties.some(ts.isSpreadAssignment)
74
93
  ? { kind: 'opaque', reason: 'its options object spreads another' }
75
- : { kind: 'literal', node: first };
94
+ : { kind: 'literal', node: value };
76
95
  }
77
96
  /**
78
- * The property `name` in the decorator's options, whether its key is plain, quoted or a shorthand.
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 `virtual` option to `computed`, the one it was renamed to.
92
- *
93
- * Only the key is replaced, so the expression, its formatting and any comment inside the options
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 renameVirtualOption(decorator, node, ctx) {
98
- const options = decoratorOptions(decorator);
99
- const virtual = findProperty(options, 'virtual');
100
- if (!virtual) {
101
- if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
102
- ctx.unresolved.push(`${ctx.describe(node)}: ${options.reason}`);
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
- if (findProperty(options, 'computed')) {
107
- ctx.unresolved.push(`${ctx.describe(node)}: gives both 'virtual' and 'computed'; keep 'computed'.`);
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
- const text = ts.isShorthandPropertyAssignment(virtual) ? 'computed: virtual' : 'computed';
111
- ctx.edits.push({ start: virtual.name.getStart(), end: virtual.name.getEnd(), text });
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 edit `rewrite` reads off `value`, which still names members as strings, or reports it
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 edit = rewrite(value);
189
- if (edit) {
190
- ctx.edits.push(edit);
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) => keyListEdit(list, param), node, `write ${what} as a key-map callback`, ctx);
231
+ rewriteValue(value, (list) => keyListEdits(list, param), node, `write ${what} as a callback`, ctx);
198
232
  }
199
233
  /**
200
- * Rewrites what a class decorator names by string - `@Index` columns and `include`, and `@Entity`'s
201
- * `indexes`, `hooks` and `relations` - into key-map callbacks named after the class.
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 param = paramFor(node.name?.text);
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
- rewriteKeyList(first, param, decorator, "'@Index' columns", ctx);
213
- if (second && ts.isObjectLiteralExpression(second)) {
214
- rewriteKeyList(propertyValue(second, 'include'), param, decorator, "'include'", ctx);
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, param, ctx);
252
+ rewriteEntityOptions(first, owner, ctx);
219
253
  }
220
254
  }
221
255
  }
222
256
  /**
223
- * Rewrites what the imperative API names by string - `defineEntity(Post, { indexes, hooks, relations })`,
224
- * `defineIndex(Post, { columns, include })` and `defineRelation(Post, 'tag', { mappedBy, references })` -
225
- * into key-map callbacks. Only uql's shapes, a class and a literal options object, are read.
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 param = paramFor(entity && ts.isIdentifier(entity) ? entity.text : undefined);
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, param, ctx);
266
+ rewriteEntityOptions(second, owner, ctx);
233
267
  }
234
268
  else if (name === 'defineIndex' && second && ts.isObjectLiteralExpression(second)) {
235
- rewriteIndexOptions(second, param, ctx);
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, param, ctx) {
242
- const indexes = propertyValue(options, 'indexes');
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, param, ctx);
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, param, ctx) {
258
- rewriteKeyList(propertyValue(index, 'columns'), param, index, "'columns'", ctx);
259
- rewriteKeyList(propertyValue(index, 'include'), param, index, "'include'", ctx);
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 key-map callback`;
267
- const mappedBy = (value) => mappedByEdit(value, target);
268
- const references = (value) => referencesEdit(value, owner, target);
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 what a statement names by string into keys: an aggregate's `$agg: { total: { $sum: 'amount' } }`
274
- * into `$select: { total: { $sum: { amount: true } } }`, and `$text`'s `$fields: ['title']` into
275
- * `{ title: true }`. Both keys are uql's own, so they are read wherever a literal holds them.
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, fieldKeysEdit, node, "write '$fields' as { field: true }", ctx);
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, fieldKeysEdit, op, `write '${op.name.getText()}' as { field: true }`, ctx);
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({ start: declared.getStart(), end: declared.getEnd(), text: declared.typeArguments[0].getText() });
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 any name
372
- * `uql-orm` no longer exports.
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
- * Reports whether one was written, since `idKey` is then a value the file has to import.
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 false;
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 false;
538
+ return;
425
539
  }
426
540
  if (names.length === 1 && CONVENTIONAL_ID_NAMES.has(names[0])) {
427
- return false;
541
+ return;
428
542
  }
429
543
  const indent = ' '.repeat(node.getSourceFile().getLineAndCharacterOfPosition(ids[0].getStart()).character);
430
- const brand = names.map((name) => `'${name}'`).join(' | ');
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
- return true;
547
+ ctx.imports.set('idKey', 'the brand(s)');
434
548
  }
435
549
  /**
436
- * Imports `idKey` where a brand was written, into the file's own `uql-orm` import. Reported instead
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 addIdKeyImport(source, rewritten, ctx) {
440
- if (uqlImports(source).some((element) => importedName(element) === 'idKey')) {
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
- ctx.unresolved.push(`${source.fileName}: import 'idKey' from 'uql-orm' for the brand(s) written here`);
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({ start: anchor.getStart(), end: anchor.getStart(), text: 'idKey, ' });
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
- renameVirtualOption(decorator, node, ctx);
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, paramFor(owner), ctx, paramFor(target));
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. Returns the
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()`. Only a
583
- * string-literal first argument qualifies: the callback form is unchanged, and a computed string is
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 || !ts.isStringLiteral(expression) || node.arguments.length > 2) {
712
+ if (!expression || node.arguments.length > 2) {
592
713
  return;
593
714
  }
594
715
  const suffix = alias ? `.as(${alias.getText(source)})` : '';
595
- ctx.edits.push({
596
- start: node.getStart(source),
597
- end: node.getEnd(),
598
- text: `raw\`${escapeForTemplate(expression.text)}\`${suffix}`,
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
- /** A backtick or a `${` inside the old string literal would end or interpolate the template. */
602
- function escapeForTemplate(text) {
603
- return text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
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
- let branded = false;
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
- branded = brandIdKey(node, ctx) || branded;
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 rewritten = renameExports(source, ctx);
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
- if (branded) {
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),
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uql-codemod",
3
3
  "description": "Upgrade uql-orm across versions that introduce breaking changes",
4
4
  "license": "MIT",
5
- "version": "0.35.1",
5
+ "version": "0.37.0",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=24"