uql-codemod 0.48.0 → 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 +8 -7
- package/dist/columnRefs.js +2 -2
- package/dist/edits.d.ts +21 -10
- package/dist/edits.js +58 -21
- package/dist/entitySql.d.ts +15 -16
- package/dist/entitySql.js +31 -26
- package/dist/imports.d.ts +16 -12
- package/dist/imports.js +136 -75
- package/dist/statementSql.d.ts +2 -1
- package/dist/statementSql.js +10 -4
- package/dist/transform.js +42 -40
- package/dist/tsconfig.js +4 -5
- package/package.json +1 -1
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('
|
|
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 `
|
|
33
|
-
- `col('cost')` to the ref it names, ``(product) =>
|
|
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 (`
|
|
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 `
|
|
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) =>
|
|
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
|
|
package/dist/columnRefs.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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:
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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(
|
|
14
|
-
export declare function replaced(node: ts.Node, text:
|
|
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
|
|
19
|
-
*
|
|
20
|
-
* no back-reference for `getStart()
|
|
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[],
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
45
|
+
return written + source.slice(at, range.end);
|
|
12
46
|
}
|
|
13
47
|
export function replaced(node, text) {
|
|
14
|
-
return {
|
|
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
|
|
24
|
-
*
|
|
25
|
-
* no back-reference for `getStart()
|
|
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,
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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) {
|
package/dist/entitySql.d.ts
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import { type Edit } from './edits.js';
|
|
3
|
-
/** `
|
|
4
|
-
type
|
|
5
|
-
export declare function
|
|
6
|
-
/** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `
|
|
7
|
-
export declare function columnExpressions(list: ts.Expression | undefined): readonly
|
|
8
|
-
/** SQL text as a
|
|
9
|
-
export declare function
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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,59 +1,64 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { inserted, replaced } from './edits.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
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
|
-
return
|
|
11
|
+
return isSqlExport(uqlExport(callee, checker));
|
|
11
12
|
}
|
|
12
|
-
/** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `
|
|
13
|
-
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) {
|
|
14
15
|
const body = list && ts.isArrowFunction(list) ? list.body : list;
|
|
15
16
|
const entries = body && ts.isArrayLiteralExpression(body) ? body.elements : [];
|
|
16
17
|
return entries.flatMap((entry) => {
|
|
17
18
|
const sql = ts.isObjectLiteralExpression(entry) ? propertyValue(entry, 'column') : entry;
|
|
18
|
-
return sql &&
|
|
19
|
+
return sql && isSqlCall(sql, checker) ? [sql] : [];
|
|
19
20
|
});
|
|
20
21
|
}
|
|
21
|
-
/** SQL text as a
|
|
22
|
-
export function
|
|
23
|
-
return
|
|
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)}`;
|
|
24
25
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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, '\\${');
|
|
32
37
|
}
|
|
33
|
-
/** A partial-index `where` string as `
|
|
34
|
-
export function
|
|
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) {
|
|
35
40
|
if (ts.isNoSubstitutionTemplateLiteral(where)) {
|
|
36
|
-
return inserted(where,
|
|
41
|
+
return inserted(where, name);
|
|
37
42
|
}
|
|
38
|
-
return ts.isStringLiteral(where) ? replaced(where,
|
|
43
|
+
return ts.isStringLiteral(where) ? replaced(where, sqlTag(name, where.text)) : undefined;
|
|
39
44
|
}
|
|
40
45
|
/**
|
|
41
46
|
* The columns SQL names by hand, which a rename does not reach: each quoted identifier, and each bare word
|
|
42
47
|
* naming one of `members`. What a string literal holds is left out, as is a word called as a function.
|
|
43
48
|
*/
|
|
44
|
-
export function handNamedColumns(sql, members) {
|
|
45
|
-
const code = sqlText(sql)?.replace(/'(?:[^']|'')*'/g, "''") ?? '';
|
|
49
|
+
export function handNamedColumns(sql, members, checker) {
|
|
50
|
+
const code = sqlText(sql, checker)?.replace(/'(?:[^']|'')*'/g, "''") ?? '';
|
|
46
51
|
const quoted = Array.from(code.matchAll(/"[^"]+"|`[^`]+`/g), ([token]) => token.slice(1, -1));
|
|
47
52
|
const bare = Array.from(code.matchAll(/\b[A-Za-z_]\w*\b(?!\s*\()/g), ([word]) => word);
|
|
48
53
|
return [...new Set([...quoted, ...bare.filter((word) => members.has(word))])];
|
|
49
54
|
}
|
|
50
|
-
/** The text of a `
|
|
51
|
-
function sqlText(sql) {
|
|
55
|
+
/** The text of a `sql` or a string, `?` standing in for each interpolation. */
|
|
56
|
+
function sqlText(sql, checker) {
|
|
52
57
|
if (!sql) {
|
|
53
58
|
return undefined;
|
|
54
59
|
}
|
|
55
|
-
if (
|
|
56
|
-
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);
|
|
57
62
|
}
|
|
58
63
|
if (ts.isTemplateExpression(sql)) {
|
|
59
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
|
-
*
|
|
32
|
-
*
|
|
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
|
|
37
|
+
export declare function clashingName(ctx: Context): ts.Identifier | undefined;
|
|
35
38
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
43
|
+
export declare function rewriteImports(dead: ReadonlySet<string>, ctx: Context): void;
|
|
40
44
|
export {};
|
package/dist/imports.js
CHANGED
|
@@ -49,7 +49,7 @@ const UTIL_ROOT_EXPORTS = [
|
|
|
49
49
|
'currentDate',
|
|
50
50
|
'currentTime',
|
|
51
51
|
'currentTimestamp',
|
|
52
|
-
'
|
|
52
|
+
'sql',
|
|
53
53
|
'refs',
|
|
54
54
|
'uuid',
|
|
55
55
|
'uuidv7',
|
|
@@ -62,6 +62,9 @@ const UTIL_ROOT_EXPORTS = [
|
|
|
62
62
|
'HookContext',
|
|
63
63
|
'DefaultLogger',
|
|
64
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';
|
|
65
68
|
/**
|
|
66
69
|
* Exports renamed or moved and nothing else, so the import and every use follow, the import moving to `from`
|
|
67
70
|
* where the name lives in another entry. Keyed by name, or by `entry#name` where the name stays elsewhere.
|
|
@@ -101,6 +104,8 @@ const RENAMED_EXPORTS = new Map([
|
|
|
101
104
|
['IForeignKeyBuilder', { to: 'ForeignKeyBuilder' }],
|
|
102
105
|
['ITableForeignKeyBuilder', { to: 'TableForeignKeyBuilder' }],
|
|
103
106
|
['UqlLockUsageError', { to: 'UqlUsageError' }],
|
|
107
|
+
[LEGACY_SQL_TAG, { to: SQL_TAG }],
|
|
108
|
+
[`uql-orm/util#${LEGACY_SQL_TAG}`, { to: SQL_TAG, from: 'uql-orm' }],
|
|
104
109
|
['uql-orm/http#Hook', { to: 'RequestHook' }],
|
|
105
110
|
['uql-orm/http#HookContext', { to: 'RequestHookContext' }],
|
|
106
111
|
...UTIL_ROOT_EXPORTS.map((name) => [`uql-orm/util#${name}`, { to: name, from: 'uql-orm' }]),
|
|
@@ -126,6 +131,8 @@ const MOVED_ENTRIES = new Map([
|
|
|
126
131
|
['uql-orm/querier', 'uql-orm'],
|
|
127
132
|
['uql-orm/type', 'uql-orm'],
|
|
128
133
|
]);
|
|
134
|
+
/** The entry an import from `entry` lives in now. */
|
|
135
|
+
const entryOf = (entry) => MOVED_ENTRIES.get(entry) ?? entry;
|
|
129
136
|
/**
|
|
130
137
|
* The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
|
|
131
138
|
* for what is read by name only: `idKey` written into one would land on an entry that may not export it.
|
|
@@ -136,13 +143,15 @@ export function uqlImportDeclarations(source, entries = false) {
|
|
|
136
143
|
return [];
|
|
137
144
|
}
|
|
138
145
|
const entry = declaration.moduleSpecifier.text;
|
|
139
|
-
if (
|
|
146
|
+
if (entries ? !isUqlEntry(entry) : entry !== 'uql-orm') {
|
|
140
147
|
return [];
|
|
141
148
|
}
|
|
142
149
|
const bindings = declaration.importClause?.namedBindings;
|
|
143
150
|
return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
|
|
144
151
|
});
|
|
145
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/');
|
|
146
155
|
function uqlImports(source, entries = false) {
|
|
147
156
|
return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
|
|
148
157
|
}
|
|
@@ -185,7 +194,7 @@ export function reportRemovedExports(ctx) {
|
|
|
185
194
|
}
|
|
186
195
|
continue;
|
|
187
196
|
}
|
|
188
|
-
const root = (
|
|
197
|
+
const root = entryOf(entry) === 'uql-orm';
|
|
189
198
|
for (const element of elements) {
|
|
190
199
|
const name = importedName(element);
|
|
191
200
|
const removed = REMOVED_EXPORTS.get(name);
|
|
@@ -199,48 +208,6 @@ export function reportRemovedExports(ctx) {
|
|
|
199
208
|
}
|
|
200
209
|
}
|
|
201
210
|
}
|
|
202
|
-
/**
|
|
203
|
-
* Rewrites each import naming a {@link RENAMED_EXPORTS} export, or from a {@link MOVED_ENTRIES} entry, renaming
|
|
204
|
-
* every use: a name moving entries gets an import from its new one, and one the file already imports is dropped,
|
|
205
|
-
* as is a `dead` one. Returns the rewritten imports, which nothing else may edit.
|
|
206
|
-
*/
|
|
207
|
-
export function renameExports(dead, ctx) {
|
|
208
|
-
const imports = uqlImportDeclarations(ctx.source, true);
|
|
209
|
-
const kept = imports.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)));
|
|
210
|
-
const bound = new Set(kept.map((element) => element.name.text));
|
|
211
|
-
const rewritten = new Set();
|
|
212
|
-
for (const { declaration, entry: imported, elements } of imports) {
|
|
213
|
-
const entry = MOVED_ENTRIES.get(imported) ?? imported;
|
|
214
|
-
if (entry === imported && !elements.some((element) => renamedTo(element, entry))) {
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
const byEntry = new Map([[entry, []]]);
|
|
218
|
-
for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
|
|
219
|
-
const rename = renamedTo(element, entry);
|
|
220
|
-
const local = element.propertyName || !rename ? element.name.text : rename.to;
|
|
221
|
-
if (rename && !element.propertyName) {
|
|
222
|
-
ctx.edits.push(...usesOf(ctx.source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
|
|
223
|
-
}
|
|
224
|
-
if (rename && bound.has(local)) {
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
bound.add(local);
|
|
228
|
-
const target = rename?.from ?? entry;
|
|
229
|
-
const text = rename
|
|
230
|
-
? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
|
|
231
|
-
: element.getText();
|
|
232
|
-
byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
|
|
233
|
-
}
|
|
234
|
-
const keyword = isTypeOnly(declaration) ? 'import type' : 'import';
|
|
235
|
-
const quote = declaration.moduleSpecifier.getText()[0];
|
|
236
|
-
const statements = [...byEntry]
|
|
237
|
-
.filter(([, names]) => names.length)
|
|
238
|
-
.map(([from, names]) => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`);
|
|
239
|
-
ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
|
|
240
|
-
rewritten.add(declaration);
|
|
241
|
-
}
|
|
242
|
-
return rewritten;
|
|
243
|
-
}
|
|
244
211
|
function isTypeOnly(declaration) {
|
|
245
212
|
return declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
|
|
246
213
|
}
|
|
@@ -262,12 +229,77 @@ export function deadImportNames(ctx) {
|
|
|
262
229
|
const names = ['Relation', 'expr'];
|
|
263
230
|
return new Set(names.filter((name) => rewritten[name] > 0 && rewritten[name] === uses(name)));
|
|
264
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
|
+
}
|
|
265
284
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
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.
|
|
268
288
|
*/
|
|
269
|
-
export function
|
|
270
|
-
|
|
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) {
|
|
271
303
|
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
272
304
|
continue;
|
|
273
305
|
}
|
|
@@ -275,40 +307,69 @@ export function dropDeadImports(dead, rewritten, ctx) {
|
|
|
275
307
|
ctx.edits.push(removeStatement(statement));
|
|
276
308
|
continue;
|
|
277
309
|
}
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
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);
|
|
281
315
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
ctx.edits.push(removeStatement(statement));
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
for (const element of dropped) {
|
|
288
|
-
ctx.edits.push(removeFromList(named.elements, element, ctx.source));
|
|
316
|
+
else {
|
|
317
|
+
patchImport(statement, join, dead, ctx);
|
|
289
318
|
}
|
|
290
319
|
}
|
|
291
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};`;
|
|
292
323
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
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.
|
|
295
326
|
*/
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
|
|
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)) {
|
|
301
361
|
return;
|
|
302
362
|
}
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
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));
|
|
308
367
|
return;
|
|
309
368
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
375
|
}
|
package/dist/statementSql.d.ts
CHANGED
|
@@ -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[]):
|
|
8
|
+
export declare function statementTemplate(sql: ts.Expression, values: readonly ts.Expression[]): readonly Part[] | undefined;
|
package/dist/statementSql.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import {
|
|
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(
|
|
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
|
-
|
|
52
|
-
|
|
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,
|
|
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 {
|
|
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}) =>
|
|
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 `
|
|
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 =
|
|
324
|
+
const edit = sqlWhereEdit(where, sqlName(ctx.source));
|
|
325
325
|
if (edit) {
|
|
326
326
|
ctx.edits.push(edit);
|
|
327
|
-
ctx.imports.set(
|
|
327
|
+
ctx.imports.set(SQL_TAG, 'the sql`...`');
|
|
328
328
|
}
|
|
329
329
|
else {
|
|
330
|
-
ctx.report(where, "write the partial-index 'where' as
|
|
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
|
|
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
|
|
373
|
-
|
|
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
|
|
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
|
|
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,
|
|
384
|
-
ctx.imports.set(
|
|
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 `
|
|
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,
|
|
432
|
-
ctx.imports.set(
|
|
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
|
|
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]
|
|
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('
|
|
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) ||
|
|
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 ?
|
|
692
|
+
const suffix = alias ? ['.as(', original(alias), ')'] : [];
|
|
691
693
|
if (ts.isStringLiteral(expression)) {
|
|
692
|
-
ctx.edits.push(replaced(node,
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
747
|
+
rewriteImports(dead, ctx);
|
|
741
748
|
reportRemovedExports(ctx);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
20
|
+
const legacy = LEGACY_FLAGS.flatMap((flag) => {
|
|
21
21
|
const property = propertyAssignment(compilerOptions, flag);
|
|
22
|
-
|
|
23
|
-
|
|
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');
|