uql-codemod 0.48.1 → 0.48.2

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,13 +26,13 @@ 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` (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()`.
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('...')` to the `sql` tagged template, and a second argument onto `.as()`.
30
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
31
  - `references: (post) => post.authorId` on a to-one (`@ManyToOne`, `@OneToOne`, or `defineEntity`/`defineRelation`) that names no foreign key, declaring `@Field({ references: () => User }) authorId` first on a decorated class that has no such column, typed as the key it points at.
32
- - A partial-index `where` string to `raw`, importing it, on entities and in the migration builder.
33
- - `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.
32
+ - A partial-index `where` string to `sql`, importing it, on entities and in the migration builder.
33
+ - `col('cost')` to the ref it names, ``(product) => sql`${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.
34
34
  - An aggregate's `$agg` to `$select`, and each field it or `$text`'s `$fields` names to a key: `{ $sum: { amount: true } }`.
35
- - Renamed exports, at the import and every use: `QueryWhereMap` to `QueryWhere`, `RelationKeyMap` to `KeyMap`, `FilterCondition` to `FilterWhere`, `SqlMigrationModuleOptions` to `MigrationModuleOptions`, `buildSqlQuerierMigrationModule` to `buildMigrationModule`, `QueryDialect` to `SqlQueryDialect`, `EngineFeatures` to `DialectFeatures`, `KnownMigratorDialect` to `DialectName`, `D1Preparer`/`D1Database` to `D1Queryable`, 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`, `Sqlite3QuerierPool` to `SqliteQuerierPool`, the migration builder's `I`-prefixed types to the unprefixed name, moving the import to the entry that exports it; and `MongoQuerier`, `isMongoQuerier` and `MongoSchemaIntrospector` to `uql-orm/mongodb`.
35
+ - Renamed exports, at the import and every use: `raw` to `sql`, `QueryWhereMap` to `QueryWhere`, `RelationKeyMap` to `KeyMap`, `FilterCondition` to `FilterWhere`, `SqlMigrationModuleOptions` to `MigrationModuleOptions`, `buildSqlQuerierMigrationModule` to `buildMigrationModule`, `QueryDialect` to `SqlQueryDialect`, `EngineFeatures` to `DialectFeatures`, `KnownMigratorDialect` to `DialectName`, `D1Preparer`/`D1Database` to `D1Queryable`, 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`, `Sqlite3QuerierPool` to `SqliteQuerierPool`, the migration builder's `I`-prefixed types to the unprefixed name, moving the import to the entry that exports it; and `MongoQuerier`, `isMongoQuerier` and `MongoSchemaIntrospector` to `uql-orm/mongodb`.
36
36
  - Imports off `uql-orm/maria`, `uql-orm/mongo` and `uql-orm/bunSql` to `uql-orm/mariadb`, `uql-orm/mongodb` and `uql-orm/bun-sql`, and off `uql-orm/type`, `uql-orm/dialect`, `uql-orm/entity`, `uql-orm/querier` and `uql-orm/namingStrategy` to `uql-orm`.
37
37
  - The `[idKey]?: '...'` brand, and its import, on a key not called `id`, `_id` or `uuid` and on every composite.
38
38
  - `import 'reflect-metadata'`, and `experimentalDecorators`/`emitDecoratorMetadata` in `tsconfig.json`, comments and formatting kept.
@@ -43,13 +43,14 @@ Reported with what to do, never guessed:
43
43
 
44
44
  - `target: esnext`, the one target that leaves decorators untransformed, and a value inherited through `extends`.
45
45
  - `@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`: a `@Transactional()` method becomes a `pool.transaction()` around its body, and only you know which pool.
46
- - An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...), an internal the root no longer exports (`AbstractSqlQuerier`, `fieldOf`, ...), the migration storages, and any import from `uql-orm/util` the root does not export (`raw`, `refs`, ... move to `uql-orm`).
46
+ - An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...), an internal the root no longer exports (`AbstractSqlQuerier`, `fieldOf`, ...), the migration storages, and any import from `uql-orm/util` the root does not export (`refs`, ... move to `uql-orm`).
47
47
  - 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.
48
48
  - A to-one onto a composite key: declare a column per key and pair each with it in `references`.
49
49
  - A to-one whose `<relation>Id` is not a declared column: an undecorated property, a `defineEntity` whose `fields` leave it out, or a `defineRelation` whose entity has no such member.
50
- - A partial-index `where` string that interpolates, since `raw` would bind what the template spliced in.
50
+ - A partial-index `where` string that interpolates, since `sql` would bind what the template spliced in.
51
51
  - 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.
52
- - 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.
52
+ - SQL in a definition that names a column by hand, as a note: a rename reaches a callback's refs, ``(user) => sql`lower(${user.email})` ``, and never the text.
53
+ - A file that already names something the codemod would write (`sql`, `refs`, `idKey`, a renamed export): left unchanged and reported, since the new import would shadow it or be shadowed.
53
54
  - A property whose type maps to no column (`Json<T>`, a vector, a mixed union).
54
55
  - 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.
55
56
 
@@ -1,7 +1,7 @@
1
1
  import ts from 'typescript';
2
2
  import { constructedType, memberNames, ownerOf, propertyNames } from './context.js';
3
3
  import { inserted, replaced } from './edits.js';
4
- import { isRaw } from './entitySql.js';
4
+ import { isSqlCall } from './entitySql.js';
5
5
  import { uqlImport, usesOf } from './imports.js';
6
6
  import { memberAccess } from './keyMaps.js';
7
7
  import { identifierText, propertyKey, propertyValue } from './syntax.js';
@@ -19,7 +19,7 @@ export function rewriteColumnRefs(ctx) {
19
19
  const unread = [];
20
20
  for (const use of usesOf(ctx.source, element.name, ctx.checker)) {
21
21
  const call = ts.isCallExpression(use.parent) && use.parent.expression === use ? use.parent : undefined;
22
- const sql = call && ts.findAncestor(call, isRaw);
22
+ const sql = call && ts.findAncestor(call, (node) => isSqlCall(node, ctx.checker));
23
23
  const option = sql?.parent;
24
24
  const owner = option && ts.isPropertyAssignment(option) && isComputedOption(option) ? computedOwner(option) : undefined;
25
25
  const edit = call && !owner ? statementRefEdit(call, ctx) : undefined;
package/dist/edits.d.ts CHANGED
@@ -1,24 +1,35 @@
1
1
  import type ts from 'typescript';
2
+ /** A stretch of the original text, which a replacement can keep. */
3
+ export type Span = {
4
+ readonly start: number;
5
+ readonly end: number;
6
+ };
7
+ /** A piece of a replacement: text written, or a stretch of the original kept. */
8
+ export type Part = string | Span;
9
+ /** What replaces a range: text, or text with stretches of the original in it, edits inside them included. */
10
+ export type Replacement = string | readonly Part[];
2
11
  /** One text replacement in a file. */
3
12
  export type Edit = {
4
13
  readonly start: number;
5
14
  readonly end: number;
6
- readonly text: string;
15
+ readonly text: Replacement;
7
16
  };
17
+ /** The text of `node` as written, for a replacement to keep. */
18
+ export declare function original(node: ts.Node): Span;
8
19
  /**
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.
20
+ * Applies edits to `source`, which is spliced rather than reprinted: a codemod that reformats everything it
21
+ * touches buries its own change in the diff. An edit inside a range another replaces is moot, unless that
22
+ * replacement keeps the stretch it sits in; edits that overlap without one holding the other are a bug.
12
23
  */
13
- export declare function applyEdits(text: string, edits: readonly Edit[]): string;
14
- export declare function replaced(node: ts.Node, text: string): Edit;
24
+ export declare function applyEdits(source: string, edits: readonly Edit[]): string;
25
+ export declare function replaced(node: ts.Node, text: Replacement): Edit;
15
26
  export declare function inserted(node: ts.Node, text: string): Edit;
16
27
  export declare function appended(node: ts.Node, text: string): Edit;
17
28
  /**
18
- * Removes one element of a comma-separated list with the separator that would dangle: the comma before the
19
- * last element, the one after any other. `source` is passed because a tree from `ts.parseJsonText` carries
20
- * no back-reference for `getStart()` to find.
29
+ * Removes elements of a comma-separated list with the separators that would dangle. Each removed element takes
30
+ * the text up to the next item; the removed run at the end takes the separator before it, so the ranges never
31
+ * overlap. `source` is passed because a tree from `ts.parseJsonText` carries no back-reference for `getStart()`.
21
32
  */
22
- export declare function removeFromList(items: readonly ts.Node[], item: ts.Node, source: ts.SourceFile): Edit;
33
+ export declare function removeFromList(items: readonly ts.Node[], removed: readonly ts.Node[], source: ts.SourceFile): readonly Edit[];
23
34
  /** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
24
35
  export declare function removeStatement(node: ts.Node): Edit;
package/dist/edits.js CHANGED
@@ -1,17 +1,51 @@
1
+ /** The text of `node` as written, for a replacement to keep. */
2
+ export function original(node) {
3
+ return { start: node.getStart(), end: node.getEnd() };
4
+ }
5
+ const isInsertion = (edit) => edit.start === edit.end;
1
6
  /**
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.
7
+ * Applies edits to `source`, which is spliced rather than reprinted: a codemod that reformats everything it
8
+ * touches buries its own change in the diff. An edit inside a range another replaces is moot, unless that
9
+ * replacement keeps the stretch it sits in; edits that overlap without one holding the other are a bug.
5
10
  */
6
- export function applyEdits(text, edits) {
7
- let result = text;
8
- for (const edit of [...edits].sort((a, b) => b.start - a.start || b.end - a.end)) {
9
- result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
11
+ export function applyEdits(source, edits) {
12
+ const ordered = edits
13
+ .map((edit, index) => ({ edit, index }))
14
+ .sort((a, b) => a.edit.start - b.edit.start ||
15
+ Number(!isInsertion(a.edit)) - Number(!isInsertion(b.edit)) ||
16
+ (isInsertion(a.edit) ? b.index - a.index : b.edit.end - a.edit.end))
17
+ .map(({ edit }) => edit);
18
+ return render(source, ordered, { start: 0, end: source.length });
19
+ }
20
+ /**
21
+ * `range` of `source` with `edits` (ordered outermost first, an insertion before a replacement at its offset, a
22
+ * later insertion before an earlier) applied. `parent` is the edit keeping `range`: an insertion at its edges
23
+ * is written beside it, so it is not written again inside.
24
+ */
25
+ function render(source, edits, range, parent) {
26
+ const within = (edit) => edit !== parent &&
27
+ range.start <= edit.start &&
28
+ edit.end <= range.end &&
29
+ !(parent && isInsertion(edit) && (edit.start === parent.start || edit.start === parent.end));
30
+ const inside = edits.filter(within);
31
+ let written = '';
32
+ let at = range.start;
33
+ for (const edit of inside) {
34
+ if (edit.start < at) {
35
+ if (edit.end > at) {
36
+ throw new Error(`overlapping edits at ${edit.start}`);
37
+ }
38
+ continue;
39
+ }
40
+ const parts = typeof edit.text === 'string' ? [edit.text] : edit.text;
41
+ written += source.slice(at, edit.start);
42
+ written += parts.map((part) => (typeof part === 'string' ? part : render(source, inside, part, edit))).join('');
43
+ at = edit.end;
10
44
  }
11
- return result;
45
+ return written + source.slice(at, range.end);
12
46
  }
13
47
  export function replaced(node, text) {
14
- return { start: node.getStart(), end: node.getEnd(), text };
48
+ return { ...original(node), text };
15
49
  }
16
50
  export function inserted(node, text) {
17
51
  return { start: node.getStart(), end: node.getStart(), text };
@@ -20,19 +54,22 @@ export function appended(node, text) {
20
54
  return { start: node.getEnd(), end: node.getEnd(), text };
21
55
  }
22
56
  /**
23
- * Removes one element of a comma-separated list with the separator that would dangle: the comma before the
24
- * last element, the one after any other. `source` is passed because a tree from `ts.parseJsonText` carries
25
- * no back-reference for `getStart()` to find.
57
+ * Removes elements of a comma-separated list with the separators that would dangle. Each removed element takes
58
+ * the text up to the next item; the removed run at the end takes the separator before it, so the ranges never
59
+ * overlap. `source` is passed because a tree from `ts.parseJsonText` carries no back-reference for `getStart()`.
26
60
  */
27
- export function removeFromList(items, item, source) {
28
- const at = items.indexOf(item);
29
- const next = items[at + 1];
30
- const previous = items[at - 1];
31
- return {
32
- start: previous && !next ? previous.getEnd() : item.getStart(source),
33
- end: next ? next.getStart(source) : item.getEnd(),
34
- text: '',
35
- };
61
+ export function removeFromList(items, removed, source) {
62
+ const kept = items.map((item) => !removed.includes(item));
63
+ const lastKept = kept.lastIndexOf(true);
64
+ const between = items
65
+ .slice(0, lastKept + 1)
66
+ .flatMap((item, at) => kept[at] ? [] : [{ start: item.getStart(source), end: items[at + 1].getStart(source), text: '' }]);
67
+ const trailing = items.slice(lastKept + 1);
68
+ if (!trailing.length) {
69
+ return between;
70
+ }
71
+ const from = lastKept < 0 ? trailing[0].getStart(source) : items[lastKept].getEnd();
72
+ return [...between, { start: from, end: items[items.length - 1].getEnd(), text: '' }];
36
73
  }
37
74
  /** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
38
75
  export function removeStatement(node) {
@@ -1,22 +1,21 @@
1
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
- /**
11
- * A template literal reading `texts` with the source of each of `values` interpolated between them, one fewer
12
- * than `texts`: in the texts, a backslash, a backtick and a `${` are escaped.
13
- */
14
- export declare function templateOf(texts: readonly string[], values?: readonly string[]): string;
15
- /** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
16
- export declare function rawWhereEdit(where: ts.Expression): Edit | undefined;
2
+ import { type Edit, type Part, type Span } from './edits.js';
3
+ /** `sql\`...\`` or `sql(...)`, or the deprecated `raw` of either. */
4
+ type SqlCall = ts.TaggedTemplateExpression | ts.CallExpression;
5
+ export declare function isSqlCall(node: ts.Node, checker: ts.TypeChecker): node is SqlCall;
6
+ /** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `sql`. */
7
+ export declare function columnExpressions(list: ts.Expression | undefined, checker: ts.TypeChecker): readonly SqlCall[];
8
+ /** SQL text as a template tagged by `name`, escaping what would end or interpolate it. */
9
+ export declare function sqlTag(name: string, sql: string): string;
10
+ /** `text` as a template literal binding nothing: a backslash, a backtick and a `${` in it are escaped. */
11
+ export declare function templateOf(text: string): string;
12
+ /** A template literal reading `texts`, the `values` kept from the original interpolated between them. */
13
+ export declare function templateWith(texts: readonly string[], values: readonly Span[]): readonly Part[];
14
+ /** A partial-index `where` string as `sql`, or nothing for a template that interpolates, whose values `sql` would bind. */
15
+ export declare function sqlWhereEdit(where: ts.Expression, name: string): Edit | undefined;
17
16
  /**
18
17
  * The columns SQL names by hand, which a rename does not reach: each quoted identifier, and each bare word
19
18
  * naming one of `members`. What a string literal holds is left out, as is a word called as a function.
20
19
  */
21
- export declare function handNamedColumns(sql: ts.Expression, members: ReadonlySet<string>): readonly string[];
20
+ export declare function handNamedColumns(sql: ts.Expression, members: ReadonlySet<string>, checker: ts.TypeChecker): readonly string[];
22
21
  export {};
package/dist/entitySql.js CHANGED
@@ -1,60 +1,64 @@
1
1
  import ts from 'typescript';
2
2
  import { inserted, replaced } from './edits.js';
3
- import { identifierText, propertyValue } from './syntax.js';
4
- export function isRaw(node) {
3
+ import { isSqlExport, uqlExport } from './imports.js';
4
+ import { propertyValue } from './syntax.js';
5
+ export function isSqlCall(node, checker) {
5
6
  const callee = ts.isTaggedTemplateExpression(node)
6
7
  ? node.tag
7
8
  : ts.isCallExpression(node)
8
9
  ? node.expression
9
10
  : undefined;
10
- const name = identifierText(callee);
11
- return name === 'raw' || name === 'sql';
11
+ return isSqlExport(uqlExport(callee, checker));
12
12
  }
13
- /** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `raw`. */
14
- export function columnExpressions(list) {
13
+ /** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `sql`. */
14
+ export function columnExpressions(list, checker) {
15
15
  const body = list && ts.isArrowFunction(list) ? list.body : list;
16
16
  const entries = body && ts.isArrayLiteralExpression(body) ? body.elements : [];
17
17
  return entries.flatMap((entry) => {
18
18
  const sql = ts.isObjectLiteralExpression(entry) ? propertyValue(entry, 'column') : entry;
19
- return sql && isRaw(sql) ? [sql] : [];
19
+ return sql && isSqlCall(sql, checker) ? [sql] : [];
20
20
  });
21
21
  }
22
- /** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
23
- export function rawTag(sql) {
24
- return `raw${templateOf([sql])}`;
22
+ /** SQL text as a template tagged by `name`, escaping what would end or interpolate it. */
23
+ export function sqlTag(name, sql) {
24
+ return `${name}${templateOf(sql)}`;
25
25
  }
26
- /**
27
- * A template literal reading `texts` with the source of each of `values` interpolated between them, one fewer
28
- * than `texts`: in the texts, a backslash, a backtick and a `${` are escaped.
29
- */
30
- export function templateOf(texts, values = []) {
31
- const escaped = texts.map((text) => text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${'));
32
- return `\`${escaped.reduce((template, text, at) => `${template}\${${values[at - 1]}}${text}`)}\``;
26
+ /** `text` as a template literal binding nothing: a backslash, a backtick and a `${` in it are escaped. */
27
+ export function templateOf(text) {
28
+ return `\`${escapedIn(text)}\``;
29
+ }
30
+ /** A template literal reading `texts`, the `values` kept from the original interpolated between them. */
31
+ export function templateWith(texts, values) {
32
+ const [first, ...rest] = texts.map(escapedIn);
33
+ return ['`', first, ...rest.flatMap((text, at) => ['${', values[at], '}', text]), '`'];
34
+ }
35
+ function escapedIn(text) {
36
+ return text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
33
37
  }
34
- /** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
35
- export function rawWhereEdit(where) {
38
+ /** A partial-index `where` string as `sql`, or nothing for a template that interpolates, whose values `sql` would bind. */
39
+ export function sqlWhereEdit(where, name) {
36
40
  if (ts.isNoSubstitutionTemplateLiteral(where)) {
37
- return inserted(where, 'raw');
41
+ return inserted(where, name);
38
42
  }
39
- return ts.isStringLiteral(where) ? replaced(where, rawTag(where.text)) : undefined;
43
+ return ts.isStringLiteral(where) ? replaced(where, sqlTag(name, where.text)) : undefined;
40
44
  }
41
45
  /**
42
46
  * The columns SQL names by hand, which a rename does not reach: each quoted identifier, and each bare word
43
47
  * naming one of `members`. What a string literal holds is left out, as is a word called as a function.
44
48
  */
45
- export function handNamedColumns(sql, members) {
46
- const code = sqlText(sql)?.replace(/'(?:[^']|'')*'/g, "''") ?? '';
49
+ export function handNamedColumns(sql, members, checker) {
50
+ const code = sqlText(sql, checker)?.replace(/'(?:[^']|'')*'/g, "''") ?? '';
47
51
  const quoted = Array.from(code.matchAll(/"[^"]+"|`[^`]+`/g), ([token]) => token.slice(1, -1));
48
52
  const bare = Array.from(code.matchAll(/\b[A-Za-z_]\w*\b(?!\s*\()/g), ([word]) => word);
49
53
  return [...new Set([...quoted, ...bare.filter((word) => members.has(word))])];
50
54
  }
51
- /** The text of a `raw` or a string, `?` standing in for each interpolation. */
52
- function sqlText(sql) {
55
+ /** The text of a `sql` or a string, `?` standing in for each interpolation. */
56
+ function sqlText(sql, checker) {
53
57
  if (!sql) {
54
58
  return undefined;
55
59
  }
56
- if (isRaw(sql)) {
57
- return sqlText(ts.isTaggedTemplateExpression(sql) ? sql.template : sql.arguments[0]);
60
+ if (isSqlCall(sql, checker)) {
61
+ return sqlText(ts.isTaggedTemplateExpression(sql) ? sql.template : sql.arguments[0], checker);
58
62
  }
59
63
  if (ts.isTemplateExpression(sql)) {
60
64
  return [sql.head, ...sql.templateSpans.map((span) => span.literal)].map((part) => part.text).join(' ? ');
package/dist/imports.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import ts from 'typescript';
2
2
  import type { Context } from './context.js';
3
+ /** The name `uql-orm` gave its SQL tag, and gives it now. */
4
+ export declare const LEGACY_SQL_TAG = "raw";
5
+ export declare const SQL_TAG = "sql";
3
6
  type UqlImport = {
4
7
  readonly declaration: ts.ImportDeclaration;
5
8
  readonly entry: string;
@@ -16,25 +19,26 @@ export declare function uqlImport(source: ts.SourceFile, name: string, entries?:
16
19
  export declare function usesOf(source: ts.SourceFile, name: ts.Identifier, checker: ts.TypeChecker): ts.Identifier[];
17
20
  /** Names an export that no longer exists, where it is imported from the package or one of its entries. */
18
21
  export declare function reportRemovedExports(ctx: Context): void;
19
- /**
20
- * Rewrites each import naming a {@link RENAMED_EXPORTS} export, or from a {@link MOVED_ENTRIES} entry, renaming
21
- * every use: a name moving entries gets an import from its new one, and one the file already imports is dropped,
22
- * as is a `dead` one. Returns the rewritten imports, which nothing else may edit.
23
- */
24
- export declare function renameExports(dead: ReadonlySet<string>, ctx: Context): ReadonlySet<ts.ImportDeclaration>;
25
22
  /**
26
23
  * The imported names the codemod removes once every use was rewritten: `Relation` unwrapped and `expr` defaults.
27
24
  * A removed decorator keeps its import on purpose: deleting it would swap a clear error for a puzzling one.
28
25
  */
29
26
  export declare function deadImportNames(ctx: Context): ReadonlySet<string>;
27
+ /** The `uql-orm` export `node` names, where its symbol is an import of it or of one of its entries. */
28
+ export declare function uqlExport(node: ts.Node | undefined, checker: ts.TypeChecker): string | undefined;
29
+ /** Whether `name` is the SQL tag, by either of its names. */
30
+ export declare function isSqlExport(name: string | undefined): boolean;
31
+ /** The name the file calls the SQL tag by once `raw` is renamed: the alias it imports it as, else `sql`. */
32
+ export declare function sqlName(source: ts.SourceFile): string;
30
33
  /**
31
- * Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and each `dead` name
32
- * from the imports {@link renameExports} left as written, having left the dead names out of the rest.
34
+ * The first identifier of the file already naming something a rewrite writes: a name it imports (`sql`, `refs`,
35
+ * `idKey`) or a rename brings in. The write would shadow it or be shadowed, so the file is left to its owner.
33
36
  */
34
- export declare function dropDeadImports(dead: ReadonlySet<string>, rewritten: ReadonlySet<ts.ImportDeclaration>, ctx: Context): void;
37
+ export declare function clashingName(ctx: Context): ts.Identifier | undefined;
35
38
  /**
36
- * Imports what the rewrites wrote (`idKey`, `raw`) into the file's own `uql-orm` import. Reported instead
37
- * where there is none: the package may be imported under a path this codemod does not recognise.
39
+ * Rewrites the file's imports in one pass: a name {@link RENAMED_EXPORTS} renames or a {@link MOVED_ENTRIES} entry
40
+ * moves is imported again where it lives now, every use renamed, and one already imported or `dead` is dropped.
41
+ * The names the rewrites wrote join the first `uql-orm` import, or are reported when there is none.
38
42
  */
39
- export declare function addImports(rewritten: ReadonlySet<ts.ImportDeclaration>, ctx: Context): void;
43
+ export declare function rewriteImports(dead: ReadonlySet<string>, ctx: Context): void;
40
44
  export {};
package/dist/imports.js CHANGED
@@ -49,7 +49,6 @@ const UTIL_ROOT_EXPORTS = [
49
49
  'currentDate',
50
50
  'currentTime',
51
51
  'currentTimestamp',
52
- 'raw',
53
52
  'sql',
54
53
  'refs',
55
54
  'uuid',
@@ -63,6 +62,9 @@ const UTIL_ROOT_EXPORTS = [
63
62
  'HookContext',
64
63
  'DefaultLogger',
65
64
  ];
65
+ /** The name `uql-orm` gave its SQL tag, and gives it now. */
66
+ export const LEGACY_SQL_TAG = 'raw';
67
+ export const SQL_TAG = 'sql';
66
68
  /**
67
69
  * Exports renamed or moved and nothing else, so the import and every use follow, the import moving to `from`
68
70
  * where the name lives in another entry. Keyed by name, or by `entry#name` where the name stays elsewhere.
@@ -102,6 +104,8 @@ const RENAMED_EXPORTS = new Map([
102
104
  ['IForeignKeyBuilder', { to: 'ForeignKeyBuilder' }],
103
105
  ['ITableForeignKeyBuilder', { to: 'TableForeignKeyBuilder' }],
104
106
  ['UqlLockUsageError', { to: 'UqlUsageError' }],
107
+ [LEGACY_SQL_TAG, { to: SQL_TAG }],
108
+ [`uql-orm/util#${LEGACY_SQL_TAG}`, { to: SQL_TAG, from: 'uql-orm' }],
105
109
  ['uql-orm/http#Hook', { to: 'RequestHook' }],
106
110
  ['uql-orm/http#HookContext', { to: 'RequestHookContext' }],
107
111
  ...UTIL_ROOT_EXPORTS.map((name) => [`uql-orm/util#${name}`, { to: name, from: 'uql-orm' }]),
@@ -127,6 +131,8 @@ const MOVED_ENTRIES = new Map([
127
131
  ['uql-orm/querier', 'uql-orm'],
128
132
  ['uql-orm/type', 'uql-orm'],
129
133
  ]);
134
+ /** The entry an import from `entry` lives in now. */
135
+ const entryOf = (entry) => MOVED_ENTRIES.get(entry) ?? entry;
130
136
  /**
131
137
  * The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
132
138
  * for what is read by name only: `idKey` written into one would land on an entry that may not export it.
@@ -137,13 +143,15 @@ export function uqlImportDeclarations(source, entries = false) {
137
143
  return [];
138
144
  }
139
145
  const entry = declaration.moduleSpecifier.text;
140
- if (entry !== 'uql-orm' && !(entries && entry.startsWith('uql-orm/'))) {
146
+ if (entries ? !isUqlEntry(entry) : entry !== 'uql-orm') {
141
147
  return [];
142
148
  }
143
149
  const bindings = declaration.importClause?.namedBindings;
144
150
  return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
145
151
  });
146
152
  }
153
+ /** Whether `entry` is `uql-orm` itself or one of its `uql-orm/<driver>` entries. */
154
+ const isUqlEntry = (entry) => entry === 'uql-orm' || entry.startsWith('uql-orm/');
147
155
  function uqlImports(source, entries = false) {
148
156
  return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
149
157
  }
@@ -186,7 +194,7 @@ export function reportRemovedExports(ctx) {
186
194
  }
187
195
  continue;
188
196
  }
189
- const root = (MOVED_ENTRIES.get(entry) ?? entry) === 'uql-orm';
197
+ const root = entryOf(entry) === 'uql-orm';
190
198
  for (const element of elements) {
191
199
  const name = importedName(element);
192
200
  const removed = REMOVED_EXPORTS.get(name);
@@ -200,48 +208,6 @@ export function reportRemovedExports(ctx) {
200
208
  }
201
209
  }
202
210
  }
203
- /**
204
- * Rewrites each import naming a {@link RENAMED_EXPORTS} export, or from a {@link MOVED_ENTRIES} entry, renaming
205
- * every use: a name moving entries gets an import from its new one, and one the file already imports is dropped,
206
- * as is a `dead` one. Returns the rewritten imports, which nothing else may edit.
207
- */
208
- export function renameExports(dead, ctx) {
209
- const imports = uqlImportDeclarations(ctx.source, true);
210
- const kept = imports.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)));
211
- const bound = new Set(kept.map((element) => element.name.text));
212
- const rewritten = new Set();
213
- for (const { declaration, entry: imported, elements } of imports) {
214
- const entry = MOVED_ENTRIES.get(imported) ?? imported;
215
- if (entry === imported && !elements.some((element) => renamedTo(element, entry))) {
216
- continue;
217
- }
218
- const byEntry = new Map([[entry, []]]);
219
- for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
220
- const rename = renamedTo(element, entry);
221
- const local = element.propertyName || !rename ? element.name.text : rename.to;
222
- if (rename && !element.propertyName) {
223
- ctx.edits.push(...usesOf(ctx.source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
224
- }
225
- if (rename && bound.has(local)) {
226
- continue;
227
- }
228
- bound.add(local);
229
- const target = rename?.from ?? entry;
230
- const text = rename
231
- ? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
232
- : element.getText();
233
- byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
234
- }
235
- const keyword = isTypeOnly(declaration) ? 'import type' : 'import';
236
- const quote = declaration.moduleSpecifier.getText()[0];
237
- const statements = [...byEntry]
238
- .filter(([, names]) => names.length)
239
- .map(([from, names]) => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`);
240
- ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
241
- rewritten.add(declaration);
242
- }
243
- return rewritten;
244
- }
245
211
  function isTypeOnly(declaration) {
246
212
  return declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
247
213
  }
@@ -263,12 +229,77 @@ export function deadImportNames(ctx) {
263
229
  const names = ['Relation', 'expr'];
264
230
  return new Set(names.filter((name) => rewritten[name] > 0 && rewritten[name] === uses(name)));
265
231
  }
232
+ /** The `uql-orm` export `node` names, where its symbol is an import of it or of one of its entries. */
233
+ export function uqlExport(node, checker) {
234
+ const declaration = node && ts.isIdentifier(node) ? checker.getSymbolAtLocation(node)?.declarations?.[0] : undefined;
235
+ return declaration && isUqlSpecifier(declaration) ? importedName(declaration) : undefined;
236
+ }
237
+ function isUqlSpecifier(declaration) {
238
+ if (!ts.isImportSpecifier(declaration)) {
239
+ return false;
240
+ }
241
+ const { moduleSpecifier } = declaration.parent.parent.parent;
242
+ return ts.isStringLiteral(moduleSpecifier) && isUqlEntry(moduleSpecifier.text);
243
+ }
244
+ /** Whether `name` is the SQL tag, by either of its names. */
245
+ export function isSqlExport(name) {
246
+ return name === SQL_TAG || name === LEGACY_SQL_TAG;
247
+ }
248
+ /** The name the file calls the SQL tag by once `raw` is renamed: the alias it imports it as, else `sql`. */
249
+ export function sqlName(source) {
250
+ const element = uqlImports(source, true).find((it) => isSqlExport(importedName(it)));
251
+ return element?.propertyName ? element.name.text : SQL_TAG;
252
+ }
253
+ /** The names the rewrites wrote that the file does not import from `uql-orm` yet, each with what wrote it. */
254
+ function missingNames(ctx) {
255
+ const imported = new Set(uqlImports(ctx.source, true).map(importedName));
256
+ const bound = [...imported].some(isSqlExport);
257
+ return [...ctx.imports].filter(([name]) => !imported.has(name) && !(bound && name === SQL_TAG));
258
+ }
259
+ const MEMBER = ts.SymbolFlags.Property | ts.SymbolFlags.Method | ts.SymbolFlags.Accessor | ts.SymbolFlags.EnumMember;
260
+ /** Whether `node` names a binding or a reference that is not `uql-orm`'s, which an import of the same name would capture. */
261
+ function isForeignName(node, checker) {
262
+ const symbol = checker.getSymbolAtLocation(node);
263
+ if (!symbol) {
264
+ const { parent } = node;
265
+ return !((ts.isPropertyAccessExpression(parent) || ts.isPropertyAssignment(parent)) && parent.name === node);
266
+ }
267
+ return !(symbol.flags & MEMBER) && !symbol.declarations?.every(isUqlSpecifier);
268
+ }
269
+ /**
270
+ * The first identifier of the file already naming something a rewrite writes: a name it imports (`sql`, `refs`,
271
+ * `idKey`) or a rename brings in. The write would shadow it or be shadowed, so the file is left to its owner.
272
+ */
273
+ export function clashingName(ctx) {
274
+ const renamed = uqlImportDeclarations(ctx.source, true).flatMap(({ entry, elements }) => elements.flatMap((element) => {
275
+ const rename = renamedTo(element, entry);
276
+ return rename && !element.propertyName ? [rename.to] : [];
277
+ }));
278
+ const names = new Set([...missingNames(ctx).map(([name]) => name), ...renamed]);
279
+ const visit = (node) => ts.isIdentifier(node) && names.has(node.text) && isForeignName(node, ctx.checker)
280
+ ? node
281
+ : ts.forEachChild(node, visit);
282
+ return names.size ? visit(ctx.source) : undefined;
283
+ }
266
284
  /**
267
- * Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and each `dead` name
268
- * from the imports {@link renameExports} left as written, having left the dead names out of the rest.
285
+ * Rewrites the file's imports in one pass: a name {@link RENAMED_EXPORTS} renames or a {@link MOVED_ENTRIES} entry
286
+ * moves is imported again where it lives now, every use renamed, and one already imported or `dead` is dropped.
287
+ * The names the rewrites wrote join the first `uql-orm` import, or are reported when there is none.
269
288
  */
270
- export function dropDeadImports(dead, rewritten, ctx) {
271
- for (const statement of ctx.source.statements) {
289
+ export function rewriteImports(dead, ctx) {
290
+ const { source } = ctx;
291
+ const imports = new Map(uqlImportDeclarations(source, true).map((it) => [it.declaration, it]));
292
+ const bound = new Set([...imports.values()]
293
+ .flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)))
294
+ .map((element) => element.name.text));
295
+ const home = [...imports.values()].find(({ entry }) => entryOf(entry) === 'uql-orm');
296
+ const written = missingNames(ctx);
297
+ if (!home) {
298
+ for (const [name, what] of written) {
299
+ ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
300
+ }
301
+ }
302
+ for (const statement of source.statements) {
272
303
  if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
273
304
  continue;
274
305
  }
@@ -276,40 +307,69 @@ export function dropDeadImports(dead, rewritten, ctx) {
276
307
  ctx.edits.push(removeStatement(statement));
277
308
  continue;
278
309
  }
279
- const named = statement.importClause?.namedBindings;
280
- if (!named || !ts.isNamedImports(named) || rewritten.has(statement)) {
281
- continue;
310
+ const uql = imports.get(statement);
311
+ const join = uql === home ? written.map(([name]) => name) : [];
312
+ const entry = uql && entryOf(uql.entry);
313
+ if (uql && (entry !== uql.entry || uql.elements.some((element) => renamedTo(element, entry)))) {
314
+ rebuildImport(uql, join, dead, bound, ctx);
282
315
  }
283
- const dropped = named.elements.filter((element) => dead.has(element.name.text));
284
- if (dropped.length === named.elements.length && !statement.importClause?.name) {
285
- ctx.edits.push(removeStatement(statement));
286
- continue;
287
- }
288
- for (const element of dropped) {
289
- ctx.edits.push(removeFromList(named.elements, element, ctx.source));
316
+ else {
317
+ patchImport(statement, join, dead, ctx);
290
318
  }
291
319
  }
292
320
  }
321
+ /** An import of `names` from `from`, quoted as the file quotes its imports. */
322
+ const importText = (keyword, names, from, quote = "'") => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`;
293
323
  /**
294
- * Imports what the rewrites wrote (`idKey`, `raw`) into the file's own `uql-orm` import. Reported instead
295
- * where there is none: the package may be imported under a path this codemod does not recognise.
324
+ * The import written again: each name under the entry it lives in, `join` first under `uql-orm`. `join` is values,
325
+ * so in a type-only declaration it becomes a value import of its own.
296
326
  */
297
- export function addImports(rewritten, ctx) {
298
- const { source } = ctx;
299
- const imported = new Set(uqlImports(source).map(importedName));
300
- const missing = [...ctx.imports].filter(([name]) => !imported.has(name));
301
- if (!missing.length) {
327
+ function rebuildImport({ declaration, entry: imported, elements }, join, dead, bound, ctx) {
328
+ const entry = entryOf(imported);
329
+ const typeOnly = isTypeOnly(declaration);
330
+ const byEntry = new Map([[entry, typeOnly ? [] : [...join]]]);
331
+ for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
332
+ const rename = renamedTo(element, entry);
333
+ const local = element.propertyName || !rename ? element.name.text : rename.to;
334
+ if (rename && !element.propertyName) {
335
+ ctx.edits.push(...usesOf(ctx.source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
336
+ }
337
+ if (rename && bound.has(local)) {
338
+ continue;
339
+ }
340
+ bound.add(local);
341
+ const target = rename?.from ?? entry;
342
+ const text = rename
343
+ ? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
344
+ : element.getText();
345
+ byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
346
+ }
347
+ const keyword = typeOnly ? 'import type' : 'import';
348
+ const quote = declaration.moduleSpecifier.getText()[0];
349
+ const statements = [...byEntry]
350
+ .filter(([, names]) => names.length)
351
+ .map(([from, names]) => importText(keyword, names, from, quote));
352
+ if (typeOnly && join.length) {
353
+ statements.unshift(importText('import', join, 'uql-orm', quote));
354
+ }
355
+ ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
356
+ }
357
+ /** The import with the `dead` names out and `join` in, nothing else of it touched. */
358
+ function patchImport(declaration, join, dead, ctx) {
359
+ const named = declaration.importClause?.namedBindings;
360
+ if (!named || !ts.isNamedImports(named)) {
302
361
  return;
303
362
  }
304
- const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration));
305
- if (!anchor) {
306
- for (const [name, what] of missing) {
307
- ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
308
- }
363
+ const dropped = named.elements.filter((element) => dead.has(element.name.text));
364
+ const statement = importText('import', join, 'uql-orm');
365
+ if (dropped.length === named.elements.length && !declaration.importClause?.name) {
366
+ ctx.edits.push(join.length ? replaced(declaration, statement) : removeStatement(declaration));
309
367
  return;
310
368
  }
311
- const names = missing.map(([name]) => name);
312
- ctx.edits.push(isTypeOnly(anchor.declaration)
313
- ? inserted(anchor.declaration, `import { ${names.join(', ')} } from 'uql-orm';\n`)
314
- : inserted(anchor.elements[0], names.map((name) => `${name}, `).join('')));
369
+ ctx.edits.push(...removeFromList(named.elements, dropped, ctx.source));
370
+ if (join.length) {
371
+ ctx.edits.push(isTypeOnly(declaration)
372
+ ? inserted(declaration, `${statement}\n`)
373
+ : inserted(named.elements[0], join.map((name) => `${name}, `).join('')));
374
+ }
315
375
  }
@@ -1,7 +1,8 @@
1
1
  import ts from 'typescript';
2
+ import { type Part } from './edits.js';
2
3
  /**
3
4
  * A querier's `run(sql, values)` statement as the template its tag takes, each placeholder replaced by the value
4
5
  * it binds, or nothing where that is not certain: the SQL is no literal, or the values no array literal of
5
6
  * plain elements, or its placeholders cannot be told apart from the rest of the text.
6
7
  */
7
- export declare function statementTemplate(sql: ts.Expression, values: readonly ts.Expression[]): string | undefined;
8
+ export declare function statementTemplate(sql: ts.Expression, values: readonly ts.Expression[]): readonly Part[] | undefined;
@@ -1,5 +1,6 @@
1
1
  import ts from 'typescript';
2
- import { templateOf } from './entitySql.js';
2
+ import { original } from './edits.js';
3
+ import { templateOf, templateWith } from './entitySql.js';
3
4
  /**
4
5
  * A quoted string or identifier, or a comment, skipped; a `?` or `$n` placeholder; or a character a placeholder
5
6
  * could hide behind that this does not read: an unclosed quote or comment, a backslash, a `#`, any other `$`.
@@ -40,7 +41,7 @@ export function statementTemplate(sql, values) {
40
41
  return undefined;
41
42
  }
42
43
  if (!values.length) {
43
- return templateOf([sql.text]);
44
+ return [templateOf(sql.text)];
44
45
  }
45
46
  const [list] = values;
46
47
  const elements = ts.isArrayLiteralExpression(list) ? list.elements : undefined;
@@ -48,6 +49,11 @@ export function statementTemplate(sql, values) {
48
49
  return undefined;
49
50
  }
50
51
  const placeholders = placeholdersOf(sql.text, elements.length);
51
- return (placeholders &&
52
- templateOf(placeholders.texts, placeholders.values.map((at) => elements[at].getText())));
52
+ if (!placeholders || placeholders.values.some((at) => isReused(placeholders.values, at) && !isPlain(elements[at]))) {
53
+ return undefined;
54
+ }
55
+ return templateWith(placeholders.texts, placeholders.values.map((at) => original(elements[at])));
53
56
  }
57
+ /** A value read twice in the template is evaluated twice, which only a name or a literal can do unseen. */
58
+ const isPlain = (value) => ts.isIdentifier(value) || ts.isNumericLiteral(value) || ts.isStringLiteral(value);
59
+ const isReused = (values, at) => values.filter((value) => value === at).length > 1;
package/dist/transform.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import ts from 'typescript';
2
2
  import { rewriteColumnRefs } from './columnRefs.js';
3
3
  import { createContext, instanceTypeOf, memberNames, ownerOf } from './context.js';
4
- import { appended, applyEdits, inserted, replaced } from './edits.js';
5
- import { columnExpressions, handNamedColumns, rawTag, rawWhereEdit } from './entitySql.js';
4
+ import { appended, applyEdits, inserted, original, replaced } from './edits.js';
5
+ import { columnExpressions, handNamedColumns, sqlTag, sqlWhereEdit, templateOf } from './entitySql.js';
6
6
  import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
7
- import { addImports, deadImportNames, dropDeadImports, renameExports, reportRemovedExports, uqlImport, uqlImportDeclarations, } from './imports.js';
7
+ import { clashingName, deadImportNames, LEGACY_SQL_TAG, reportRemovedExports, rewriteImports, SQL_TAG, sqlName, uqlExport, uqlImport, uqlImportDeclarations, } from './imports.js';
8
8
  import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, quoted, referencesEdits, } from './keyMaps.js';
9
9
  import { statementTemplate } from './statementSql.js';
10
10
  import { arrayElements, decoratorName, decoratorsOf, identifierText, namedProperty, objectLiteral, objectProperties, propertyKey, propertyValue, } from './syntax.js';
@@ -69,12 +69,12 @@ function renameSqlOption(options, from, to, node, owner, ctx) {
69
69
  }
70
70
  /** Notes SQL that names a column by hand, which a rename does not reach. */
71
71
  function noteHandNamedColumns(sql, owner, ctx) {
72
- const names = sql ? handNamedColumns(sql, memberNames(owner.entity, ctx.checker)) : [];
72
+ const names = sql ? handNamedColumns(sql, memberNames(owner.entity, ctx.checker), ctx.checker) : [];
73
73
  if (!sql || !names.length) {
74
74
  return;
75
75
  }
76
76
  ctx.note(sql, `SQL names ${names.map((name) => `'${name}'`).join(', ')} by hand, which a rename does not reach; ` +
77
- `read each off a callback's refs instead: (${owner.param}) => raw\`...\${${owner.param}.<member>}...\``);
77
+ `read each off a callback's refs instead: (${owner.param}) => sql\`...\${${owner.param}.<member>}...\``);
78
78
  }
79
79
  /** Inserts `option` into the decorator's options object, creating one in an empty argument list. */
80
80
  function insertOption(options, option) {
@@ -311,23 +311,23 @@ function rewriteIndex(columns, options, owner, node, what, ctx) {
311
311
  rewriteKeyList(columns, owner.param, node, what, ctx);
312
312
  rewriteKeyList(literal && propertyValue(literal, 'include'), owner.param, node, "'include'", ctx);
313
313
  rewriteIndexWhere(literal, ctx);
314
- for (const sql of [...columnExpressions(columns), literal && propertyValue(literal, 'where')]) {
314
+ for (const sql of [...columnExpressions(columns, ctx.checker), literal && propertyValue(literal, 'where')]) {
315
315
  noteHandNamedColumns(sql, owner, ctx);
316
316
  }
317
317
  }
318
- /** A partial-index `where` string as `raw`, on an entity and in the migration builder alike, or reported. */
318
+ /** A partial-index `where` string as `sql`, on an entity and in the migration builder alike, or reported. */
319
319
  function rewriteIndexWhere(options, ctx) {
320
320
  const where = options && propertyValue(options, 'where');
321
321
  if (!where || !isStringTyped(ctx.checker.getTypeAtLocation(where))) {
322
322
  return;
323
323
  }
324
- const edit = rawWhereEdit(where);
324
+ const edit = sqlWhereEdit(where, sqlName(ctx.source));
325
325
  if (edit) {
326
326
  ctx.edits.push(edit);
327
- ctx.imports.set('raw', 'the raw`...`');
327
+ ctx.imports.set(SQL_TAG, 'the sql`...`');
328
328
  }
329
329
  else {
330
- ctx.report(where, "write the partial-index 'where' as raw`...` or a predicate; this one could not be read");
330
+ ctx.report(where, "write the partial-index 'where' as sql`...` or a predicate; this one could not be read");
331
331
  }
332
332
  }
333
333
  /** Whether every value `type` admits is a string. */
@@ -360,7 +360,7 @@ function rewriteMethodCall(call, ctx) {
360
360
  /**
361
361
  * A querier's `run`/`all` handed SQL as a string, which now takes a tagged statement: a literal becomes
362
362
  * `run`...``, its values interpolated where {@link statementTemplate} can place them, and SQL built at run
363
- * time `raw.text`, which splices as the string did. Values it cannot place are reported.
363
+ * time `<sql>.text(...)`, which splices as the string did. Values it cannot place are reported.
364
364
  */
365
365
  function rewriteSqlCall(call, method, ctx) {
366
366
  const [sql, ...values] = call.arguments;
@@ -369,19 +369,21 @@ function rewriteSqlCall(call, method, ctx) {
369
369
  }
370
370
  const template = statementTemplate(sql, values);
371
371
  if (template) {
372
- const typeArguments = call.typeArguments ? `<${call.typeArguments.map((type) => type.getText()).join(', ')}>` : '';
373
- ctx.edits.push(replaced(call, `${call.expression.getText()}${typeArguments}${template}`));
372
+ const typeArguments = (call.typeArguments ?? []).map(original);
373
+ const listed = typeArguments.flatMap((type, at) => (at ? [', ', type] : [type]));
374
+ const angle = typeArguments.length ? ['<', ...listed, '>'] : [];
375
+ ctx.edits.push(replaced(call, [original(call.expression), ...angle, ...template]));
374
376
  return;
375
377
  }
376
378
  if (values.length) {
377
- ctx.report(call, `${method}() takes one raw statement: write the values into it, raw\`... \${value}\`, which binds them`);
379
+ ctx.report(call, `${method}() takes one statement: write the values into it, sql\`... \${value}\`, which binds them`);
378
380
  return;
379
381
  }
380
382
  if (ts.isTemplateExpression(sql)) {
381
- ctx.note(call, 'the values in this template are spliced into the SQL; write it as raw`...` to bind them');
383
+ ctx.note(call, 'the values in this template are spliced into the SQL; write it as sql`...` to bind them');
382
384
  }
383
- ctx.edits.push(replaced(sql, `raw.text(${sql.getText()})`));
384
- ctx.imports.set('raw', 'the raw.text(...)');
385
+ ctx.edits.push(replaced(sql, [`${sqlName(ctx.source)}.text(`, original(sql), ')']));
386
+ ctx.imports.set(SQL_TAG, 'the sql.text(...)');
385
387
  }
386
388
  /** `const { id } = await q.upsertOne(...)`, which resolves to the id now, as the id: other keys are reported. */
387
389
  function rewriteUpsertResult(declaration, ctx) {
@@ -412,7 +414,7 @@ const EXPR_VALUES = new Map([
412
414
  ...['currentDate', 'currentTime', 'uuid', 'uuidv7'].map((name) => [name, name]),
413
415
  ]);
414
416
  /**
415
- * Rewrites the migration builder's `expr.<helper>()` default as the value or `raw` an entity declares, or reports
417
+ * Rewrites the migration builder's `expr.<helper>()` default as the value or `sql` an entity declares, or reports
416
418
  * one with no rewrite. Called only where the file imports `expr` from uql, so a variable of its own is left alone.
417
419
  */
418
420
  function rewriteExprCall(call, ctx) {
@@ -428,8 +430,8 @@ function rewriteExprCall(call, ctx) {
428
430
  ctx.imports.set(value, `the expr.${helper}()`);
429
431
  }
430
432
  else if (helper === 'raw' && sql && ts.isStringLiteralLike(sql)) {
431
- ctx.edits.push(replaced(call, rawTag(sql.text)));
432
- ctx.imports.set('raw', 'the raw`...`');
433
+ ctx.edits.push(replaced(call, sqlTag(sqlName(ctx.source), sql.text)));
434
+ ctx.imports.set(SQL_TAG, 'the sql`...`');
433
435
  }
434
436
  else {
435
437
  ctx.report(call, `expr.${helper}() is gone: declare the column with m.raw(...), or use a stamp on the entity`);
@@ -458,7 +460,7 @@ function rewriteUpsertCall(call, ctx) {
458
460
  const options = call.arguments[3];
459
461
  const update = options && upsertUpdate(options);
460
462
  if (name && UPSERT_CALLS.has(name) && update) {
461
- ctx.edits.push(replaced(options, update.getText()));
463
+ ctx.edits.push(replaced(options, [original(update)]));
462
464
  }
463
465
  }
464
466
  /** The value of options holding `update` alone, `{ update: value }` or `{ update }`. */
@@ -549,7 +551,7 @@ function unwrapRelationAlias(node, ctx) {
549
551
  ts.isTypeReferenceNode(declared) &&
550
552
  identifierText(declared.typeName) === 'Relation' &&
551
553
  declared.typeArguments?.length === 1) {
552
- ctx.edits.push(replaced(declared, declared.typeArguments[0].getText()));
554
+ ctx.edits.push(replaced(declared, [original(declared.typeArguments[0])]));
553
555
  ctx.rewritten.Relation += 1;
554
556
  }
555
557
  }
@@ -676,23 +678,23 @@ function reportRemovedDecorators(node, ctx) {
676
678
  }
677
679
  }
678
680
  /**
679
- * Rewrites `raw('sql')` into the tagged template, and a second alias argument into `.as()`, the callback
680
- * form's included. A computed string is left alone: a template cannot be built from a value not known here.
681
+ * Rewrites the deprecated `raw('...')` into the `sql` tagged template, and a second alias argument into `.as()`, the
682
+ * callback form's included. A computed string is left alone: a template cannot be built from a value not known here.
681
683
  */
682
684
  function rewriteRawCall(node, ctx) {
683
- if (!ts.isCallExpression(node) || identifierText(node.expression) !== 'raw') {
685
+ if (!ts.isCallExpression(node) || uqlExport(node.expression, ctx.checker) !== LEGACY_SQL_TAG) {
684
686
  return;
685
687
  }
686
688
  const [expression, alias] = node.arguments;
687
689
  if (!expression || node.arguments.length > 2) {
688
690
  return;
689
691
  }
690
- const suffix = alias ? `.as(${alias.getText()})` : '';
692
+ const suffix = alias ? ['.as(', original(alias), ')'] : [];
691
693
  if (ts.isStringLiteral(expression)) {
692
- ctx.edits.push(replaced(node, `${rawTag(expression.text)}${suffix}`));
694
+ ctx.edits.push(replaced(node, [original(node.expression), templateOf(expression.text), ...suffix]));
693
695
  }
694
696
  else if (alias) {
695
- ctx.edits.push({ start: expression.getEnd(), end: node.getEnd(), text: `)${suffix}` });
697
+ ctx.edits.push({ start: expression.getEnd(), end: node.getEnd(), text: [')', ...suffix] });
696
698
  }
697
699
  }
698
700
  /**
@@ -701,12 +703,12 @@ function rewriteRawCall(node, ctx) {
701
703
  */
702
704
  export function transformFile(source, checker, options = { strictNullChecks: true }) {
703
705
  const ctx = createContext(source, checker, options.strictNullChecks);
704
- const rewritesRaw = uqlImport(source, 'raw') !== undefined;
705
706
  const importsUql = uqlImportDeclarations(source, true).length > 0;
706
707
  const importsExpr = uqlImport(source, 'expr', true) !== undefined;
708
+ const importsRaw = uqlImport(source, LEGACY_SQL_TAG, true) !== undefined;
707
709
  const visit = (node) => {
708
710
  reportRemovedDecorators(node, ctx);
709
- if (rewritesRaw) {
711
+ if (importsRaw) {
710
712
  rewriteRawCall(node, ctx);
711
713
  }
712
714
  if (ts.isPropertyDeclaration(node)) {
@@ -736,16 +738,16 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
736
738
  ts.forEachChild(node, visit);
737
739
  };
738
740
  visit(source);
741
+ const clash = clashingName(ctx);
742
+ if (clash) {
743
+ ctx.report(clash, `'${clash.text}' is already named here, and the codemod writes it: rename that, then run again`);
744
+ return fileResult(ctx, source.getFullText());
745
+ }
739
746
  const dead = new Set([...deadImportNames(ctx), ...rewriteColumnRefs(ctx)]);
740
- const rewritten = renameExports(dead, ctx);
747
+ rewriteImports(dead, ctx);
741
748
  reportRemovedExports(ctx);
742
- dropDeadImports(dead, rewritten, ctx);
743
- addImports(rewritten, ctx);
744
- return {
745
- fileName: source.fileName,
746
- text: applyEdits(source.getFullText(), ctx.edits),
747
- changed: ctx.edits.length > 0,
748
- unresolved: ctx.unresolved,
749
- notes: ctx.notes,
750
- };
749
+ return fileResult(ctx, applyEdits(source.getFullText(), ctx.edits));
750
+ }
751
+ function fileResult({ source, unresolved, notes }, text) {
752
+ return { fileName: source.fileName, text, changed: text !== source.getFullText(), unresolved, notes };
751
753
  }
package/dist/tsconfig.js CHANGED
@@ -17,12 +17,11 @@ export function transformTsconfig(fileName, text) {
17
17
  const unresolved = [];
18
18
  const compilerOptions = propertyValue(root, 'compilerOptions');
19
19
  if (compilerOptions && ts.isObjectLiteralExpression(compilerOptions)) {
20
- for (const flag of LEGACY_FLAGS) {
20
+ const legacy = LEGACY_FLAGS.flatMap((flag) => {
21
21
  const property = propertyAssignment(compilerOptions, flag);
22
- if (property) {
23
- edits.push(removeFromList(compilerOptions.properties, property, source));
24
- }
25
- }
22
+ return property ? [property] : [];
23
+ });
24
+ edits.push(...removeFromList(compilerOptions.properties, legacy, source));
26
25
  // `target` is reported, not rewritten: removing it silently falls back to the compiler default
27
26
  // (`es5` for `tsc`), and choosing a replacement means guessing which era the project targets.
28
27
  const target = propertyValue(compilerOptions, 'target');
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.48.1",
5
+ "version": "0.48.2",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=22.18"