uql-codemod 0.46.0 → 0.47.1

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/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015-present UQL Contributors
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -32,7 +32,8 @@ Only `--flag=value` is read, and anything unrecognised is an error. It exits `0`
32
32
  - A partial-index `where` string to `raw`, importing it, on entities and in the migration builder.
33
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.
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`, moving the import to the entry that exports it; and `MongoQuerier`, `isMongoQuerier`, `MongoMigrationStorage` and `MongoSchemaIntrospector` to `uql-orm/mongo`.
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`.
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`.
36
37
  - The `[idKey]?: '...'` brand, and its import, on a key not called `id`, `_id` or `uuid` and on every composite.
37
38
  - `import 'reflect-metadata'`, and `experimentalDecorators`/`emitDecoratorMetadata` in `tsconfig.json`, comments and formatting kept.
38
39
 
@@ -42,7 +43,7 @@ Reported with what to do, never guessed:
42
43
 
43
44
  - `target: esnext`, the one target that leaves decorators untransformed, and a value inherited through `extends`.
44
45
  - `@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`: a `@Transactional()` method becomes a `pool.transaction()` around its body, and only you know which pool.
45
- - An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...).
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
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.
47
48
  - A to-one onto a composite key: declare a column per key and pair each with it in `references`.
48
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.
package/dist/bin.js CHANGED
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from './cli.js';
3
3
  import { run } from './index.js';
4
- const USAGE = 'usage: uql-codemod [--project=<path>] [--include=<a,b>] [--dry-run]';
4
+ const USAGE = 'usage: uql-codemod [--project=<path>] [--include=<a,b>] [--dry-run] [--help]';
5
5
  // Exit 2 is "could not start", so a script can tell it apart from exit 1, "there is work left".
6
6
  const stop = (message) => {
7
7
  console.error(message);
8
8
  process.exit(2);
9
9
  };
10
- const { options, errors } = parseArgs(process.argv.slice(2));
10
+ const { options, help, errors } = parseArgs(process.argv.slice(2));
11
+ if (help) {
12
+ console.log(USAGE);
13
+ process.exit(0);
14
+ }
11
15
  if (errors.length) {
12
16
  stop([...errors, USAGE].join('\n'));
13
17
  }
@@ -19,8 +23,11 @@ for (const line of summary.notes) {
19
23
  for (const line of summary.unresolved) {
20
24
  console.error(`needs a decision: ${line}`);
21
25
  }
26
+ for (const file of summary.changed) {
27
+ console.log(`${options.dryRun ? 'would change' : 'changed'}: ${file}`);
28
+ }
22
29
  console.log(`${summary.changed.length} file(s) ${options.dryRun ? 'would change' : 'changed'}`);
23
30
  if (summary.unresolved.length) {
24
- console.error(`${summary.unresolved.length} property(ies) left untouched; see above`);
31
+ console.error(`${summary.unresolved.length} item(s) need a decision; see above`);
25
32
  process.exit(1);
26
33
  }
package/dist/cli.d.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  import type { RunOptions } from './index.js';
2
2
  export type ParsedArgs = {
3
3
  readonly options: RunOptions;
4
+ /** Whether to print the usage and stop. */
5
+ readonly help: boolean;
4
6
  /** Why the arguments were rejected. Nothing runs while this is non-empty. */
5
7
  readonly errors: readonly string[];
6
8
  };
7
9
  /**
8
- * Reads the command line, rejecting anything it does not recognise.
9
- *
10
- * Unknown arguments are an error rather than being ignored: a misspelled `--dry-run` would otherwise
11
- * rewrite the project for real, which is the one mistake this tool must not let someone make quietly.
12
- * Only the `--flag=value` form is accepted, so a stray `--project foo` cannot silently fall back to the
13
- * default config either.
10
+ * Reads the command line, rejecting anything it does not recognise: a misspelled `--dry-run` would otherwise
11
+ * rewrite the project for real. Only `--flag=value` is accepted, so a stray `--project foo` cannot fall back
12
+ * to the default config either.
14
13
  */
15
14
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
package/dist/cli.js CHANGED
@@ -1,21 +1,23 @@
1
1
  const VALUE_FLAGS = new Set(['project', 'include']);
2
2
  /**
3
- * Reads the command line, rejecting anything it does not recognise.
4
- *
5
- * Unknown arguments are an error rather than being ignored: a misspelled `--dry-run` would otherwise
6
- * rewrite the project for real, which is the one mistake this tool must not let someone make quietly.
7
- * Only the `--flag=value` form is accepted, so a stray `--project foo` cannot silently fall back to the
8
- * default config either.
3
+ * Reads the command line, rejecting anything it does not recognise: a misspelled `--dry-run` would otherwise
4
+ * rewrite the project for real. Only `--flag=value` is accepted, so a stray `--project foo` cannot fall back
5
+ * to the default config either.
9
6
  */
10
7
  export function parseArgs(argv) {
11
8
  const errors = [];
12
9
  const values = new Map();
13
10
  let dryRun = false;
11
+ let help = false;
14
12
  for (const arg of argv) {
15
13
  if (arg === '--dry-run') {
16
14
  dryRun = true;
17
15
  continue;
18
16
  }
17
+ if (arg === '--help' || arg === '-h') {
18
+ help = true;
19
+ continue;
20
+ }
19
21
  const separator = arg.indexOf('=');
20
22
  const name = arg.slice(2, separator === -1 ? undefined : separator);
21
23
  if (!arg.startsWith('--') || !VALUE_FLAGS.has(name)) {
@@ -35,6 +37,7 @@ export function parseArgs(argv) {
35
37
  dryRun,
36
38
  include: values.get('include')?.split(',').filter(Boolean),
37
39
  },
40
+ help,
38
41
  errors,
39
42
  };
40
43
  }
@@ -0,0 +1,7 @@
1
+ import { type Context } from './context.js';
2
+ /**
3
+ * Rewrites each `col('x')` into the ref it names - `product.x` in a `computed` callback, `refs(Item).x` in a
4
+ * statement - where the entity its SQL renders against, and a member named `x`, can be told from here. Every
5
+ * other use is reported. Returns `col` as a dead import once no use is left.
6
+ */
7
+ export declare function rewriteColumnRefs(ctx: Context): readonly string[];
@@ -0,0 +1,146 @@
1
+ import ts from 'typescript';
2
+ import { constructedType, memberNames, ownerOf, propertyNames } from './context.js';
3
+ import { inserted, replaced } from './edits.js';
4
+ import { isRaw } from './entitySql.js';
5
+ import { uqlImport, usesOf } from './imports.js';
6
+ import { memberAccess } from './keyMaps.js';
7
+ import { identifierText, propertyKey, propertyValue } from './syntax.js';
8
+ /**
9
+ * Rewrites each `col('x')` into the ref it names - `product.x` in a `computed` callback, `refs(Item).x` in a
10
+ * statement - where the entity its SQL renders against, and a member named `x`, can be told from here. Every
11
+ * other use is reported. Returns `col` as a dead import once no use is left.
12
+ */
13
+ export function rewriteColumnRefs(ctx) {
14
+ const element = uqlImport(ctx.source, 'col');
15
+ if (!element) {
16
+ return [];
17
+ }
18
+ const computed = new Map();
19
+ const unread = [];
20
+ for (const use of usesOf(ctx.source, element.name, ctx.checker)) {
21
+ const call = ts.isCallExpression(use.parent) && use.parent.expression === use ? use.parent : undefined;
22
+ const sql = call && ts.findAncestor(call, isRaw);
23
+ const option = sql?.parent;
24
+ const owner = option && ts.isPropertyAssignment(option) && isComputedOption(option) ? computedOwner(option) : undefined;
25
+ const edit = call && !owner ? statementRefEdit(call, ctx) : undefined;
26
+ if (call && sql && ts.isTaggedTemplateExpression(sql) && owner) {
27
+ computed.set(sql, { owner, calls: [...(computed.get(sql)?.calls ?? []), call] });
28
+ }
29
+ else if (edit) {
30
+ ctx.edits.push(edit);
31
+ ctx.imports.set('refs', 'the refs(...)');
32
+ }
33
+ else {
34
+ unread.push(use);
35
+ }
36
+ }
37
+ for (const [sql, { owner, calls }] of computed) {
38
+ const edits = computedRefEdits(sql, calls, owner, ctx);
39
+ if (edits) {
40
+ ctx.edits.push(...edits);
41
+ }
42
+ else {
43
+ unread.push(...calls);
44
+ }
45
+ }
46
+ for (const node of unread.sort((a, b) => a.getStart() - b.getStart())) {
47
+ ctx.report(node, "'col' was removed; read the column off refs(Entity), which the codemod could not tell here");
48
+ }
49
+ return unread.length ? [] : ['col'];
50
+ }
51
+ function isComputedOption(option) {
52
+ const key = propertyKey(option.name);
53
+ return key === 'computed' || key === 'virtual';
54
+ }
55
+ /** The entity a field's `computed` SQL is declared on: the decorated property's class, or `defineEntity`'s first argument. */
56
+ function computedOwner(option) {
57
+ const holder = ts.findAncestor(option, (node) => ts.isPropertyDeclaration(node) ||
58
+ (ts.isCallExpression(node) && identifierText(node.expression) === 'defineEntity'));
59
+ if (holder && ts.isPropertyDeclaration(holder)) {
60
+ return ownerOf(holder.parent);
61
+ }
62
+ const [entity] = holder?.arguments ?? [];
63
+ return entity && ownerOf(entity);
64
+ }
65
+ /** `(product) => ` before a `computed` raw, and `product.x` for each `col('x')` in it, where every one names a member. */
66
+ function computedRefEdits(sql, calls, owner, ctx) {
67
+ const members = memberNames(owner.entity, ctx.checker);
68
+ const names = calls.map((call) => columnMember(call, members));
69
+ if (mentions(sql, owner.param) || !names.every((name) => name !== undefined)) {
70
+ return undefined;
71
+ }
72
+ return [
73
+ inserted(sql, `(${owner.param}) => `),
74
+ ...calls.map((call, at) => replaced(call, memberAccess(owner.param, names[at]))),
75
+ ];
76
+ }
77
+ /** `refs(Item).x` for a `col('x')` in a statement, where the entity its SQL renders against can be told. */
78
+ function statementRefEdit(call, ctx) {
79
+ const entity = statementEntity(call, ctx.checker);
80
+ const name = entity && columnMember(call, propertyNames(entity.type));
81
+ return entity && name !== undefined ? replaced(call, memberAccess(`refs(${entity.name})`, name)) : undefined;
82
+ }
83
+ /** The member a `col('x')` names, where its one argument is a literal naming one of `members`. */
84
+ function columnMember(call, members) {
85
+ const [name] = call.arguments;
86
+ return call.arguments.length === 1 && ts.isStringLiteralLike(name) && members.has(name.text) ? name.text : undefined;
87
+ }
88
+ /** Whether `node` reads a binding named `name`, which a callback parameter of that name would shadow. */
89
+ function mentions(node, name) {
90
+ if (ts.isIdentifier(node)) {
91
+ return node.text === name && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node);
92
+ }
93
+ return ts.forEachChild(node, (child) => mentions(child, name) || undefined) ?? false;
94
+ }
95
+ /**
96
+ * The entity a statement's SQL renders against at `node`: the one its query names, or a relation a `$populate`
97
+ * reads there. Nothing where a function or a relation filter in between opens a scope of its own.
98
+ */
99
+ function statementEntity(node, checker) {
100
+ const path = [];
101
+ for (let at = node; at.parent; at = at.parent) {
102
+ const { parent } = at;
103
+ if (ts.isFunctionLike(parent)) {
104
+ return undefined;
105
+ }
106
+ if (ts.isPropertyAssignment(parent)) {
107
+ path.unshift(propertyKey(parent.name) ?? '');
108
+ }
109
+ const queried = ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === at);
110
+ const entity = queried ? queriedEntity(parent, checker) : undefined;
111
+ if (entity) {
112
+ return entityAlong(entity, path, node, checker);
113
+ }
114
+ }
115
+ return undefined;
116
+ }
117
+ /** The entity a call queries: its first argument's class, or the `$entity` its query names. */
118
+ function queriedEntity(call, checker) {
119
+ const [first] = call.arguments;
120
+ const entity = first && ts.isObjectLiteralExpression(first) ? propertyValue(first, '$entity') : first;
121
+ const instance = entity && constructedType(checker.getTypeAtLocation(entity));
122
+ return entity && ts.isIdentifier(entity) && instance ? { name: entity.text, type: instance } : undefined;
123
+ }
124
+ /** `entity` followed along `path`: into each relation a `$populate` names, and nowhere a relation filter opens. */
125
+ function entityAlong(entity, path, node, checker) {
126
+ let current = entity;
127
+ for (const [at, key] of path.entries()) {
128
+ const property = current.type.getProperty(key);
129
+ const related = property && relatedClass(property, node, checker);
130
+ if (!related) {
131
+ continue;
132
+ }
133
+ const inScope = checker.getSymbolsInScope(node, ts.SymbolFlags.Class).some(({ name }) => name === related.name);
134
+ if (path[at - 1] !== '$populate' || !inScope) {
135
+ return undefined;
136
+ }
137
+ current = { name: related.name, type: checker.getDeclaredTypeOfSymbol(related) };
138
+ }
139
+ return current;
140
+ }
141
+ /** The class a relation property points at, one or many, or nothing where the property holds no class. */
142
+ function relatedClass(property, node, checker) {
143
+ const type = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(property, node));
144
+ const symbol = (checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type).getSymbol();
145
+ return symbol && symbol.flags & ts.SymbolFlags.Class ? symbol : undefined;
146
+ }
@@ -0,0 +1,38 @@
1
+ import ts from 'typescript';
2
+ import type { Edit } from './edits.js';
3
+ /** What a rewrite of one file reads and writes. */
4
+ export type Context = {
5
+ readonly source: ts.SourceFile;
6
+ readonly checker: ts.TypeChecker;
7
+ /** Whether the project checks nulls; without it a `| null` written on a property changes nothing. */
8
+ readonly strictNullChecks: boolean;
9
+ readonly edits: Edit[];
10
+ readonly unresolved: string[];
11
+ readonly notes: string[];
12
+ /** Names written that the file has to import from `uql-orm`, each with what it was written for. */
13
+ readonly imports: Map<string, string>;
14
+ /** Uses rewritten of each name whose import goes once every use was: `Relation<T>` unwrapped, `expr` defaults. */
15
+ readonly rewritten: {
16
+ Relation: number;
17
+ expr: number;
18
+ };
19
+ /** Records what needs a human, at `node`. */
20
+ readonly report: (node: ts.Node, message: string) => void;
21
+ /** Records a rewrite that is correct but worth a second look, at `node`. */
22
+ readonly note: (node: ts.Node, message: string) => void;
23
+ };
24
+ export declare function createContext(source: ts.SourceFile, checker: ts.TypeChecker, strictNullChecks: boolean): Context;
25
+ /** The entity a definition describes: the parameter its callbacks are named after, and the node naming it. */
26
+ export type Owner = {
27
+ readonly param: string;
28
+ readonly entity: ts.Node;
29
+ };
30
+ /** The owner a class, or an expression naming one, defines. */
31
+ export declare function ownerOf(entity: ts.Node): Owner;
32
+ /** The instance type a class type constructs, if it constructs one. */
33
+ export declare function constructedType(type: ts.Type): ts.Type | undefined;
34
+ /** The instance type of the entity `node` declares or names. */
35
+ export declare function instanceTypeOf(node: ts.Node, checker: ts.TypeChecker): ts.Type;
36
+ export declare function propertyNames(type: ts.Type): ReadonlySet<string>;
37
+ /** The names of the members of the entity `node` declares or names. */
38
+ export declare function memberNames(node: ts.Node, checker: ts.TypeChecker): ReadonlySet<string>;
@@ -0,0 +1,44 @@
1
+ import ts from 'typescript';
2
+ import { paramFor } from './keyMaps.js';
3
+ import { identifierText } from './syntax.js';
4
+ export function createContext(source, checker, strictNullChecks) {
5
+ const unresolved = [];
6
+ const notes = [];
7
+ const at = (node) => `${source.fileName}:${source.getLineAndCharacterOfPosition(node.getStart()).line + 1}`;
8
+ return {
9
+ source,
10
+ checker,
11
+ strictNullChecks,
12
+ edits: [],
13
+ unresolved,
14
+ notes,
15
+ imports: new Map(),
16
+ rewritten: { Relation: 0, expr: 0 },
17
+ report: (node, message) => {
18
+ unresolved.push(`${at(node)}: ${message}`);
19
+ },
20
+ note: (node, message) => {
21
+ notes.push(`${at(node)}: ${message}`);
22
+ },
23
+ };
24
+ }
25
+ /** The owner a class, or an expression naming one, defines. */
26
+ export function ownerOf(entity) {
27
+ return { param: paramFor(identifierText(ts.isClassLike(entity) ? entity.name : entity)), entity };
28
+ }
29
+ /** The instance type a class type constructs, if it constructs one. */
30
+ export function constructedType(type) {
31
+ return type.getConstructSignatures()[0]?.getReturnType();
32
+ }
33
+ /** The instance type of the entity `node` declares or names. */
34
+ export function instanceTypeOf(node, checker) {
35
+ const type = checker.getTypeAtLocation(node);
36
+ return constructedType(type) ?? type;
37
+ }
38
+ export function propertyNames(type) {
39
+ return new Set(type.getProperties().map(({ name }) => name));
40
+ }
41
+ /** The names of the members of the entity `node` declares or names. */
42
+ export function memberNames(node, checker) {
43
+ return propertyNames(instanceTypeOf(node, checker));
44
+ }
package/dist/edits.d.ts CHANGED
@@ -15,11 +15,10 @@ export declare function replaced(node: ts.Node, text: string): Edit;
15
15
  export declare function inserted(node: ts.Node, text: string): Edit;
16
16
  export declare function appended(node: ts.Node, text: string): Edit;
17
17
  /**
18
- * Removes one element of a comma-separated list - a parameter, a named import, a JSON property - taking
19
- * the separator that would otherwise dangle. The last element gives up the comma before it; any other
20
- * gives up the one after.
21
- *
22
- * The source file is passed explicitly because a tree from `ts.parseJsonText` carries no
23
- * back-reference, so `getStart()` cannot find one on its own.
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.
24
21
  */
25
22
  export declare function removeFromList(items: readonly ts.Node[], item: ts.Node, source: ts.SourceFile): Edit;
23
+ /** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
24
+ export declare function removeStatement(node: ts.Node): Edit;
package/dist/edits.js CHANGED
@@ -20,12 +20,9 @@ export function appended(node, text) {
20
20
  return { start: node.getEnd(), end: node.getEnd(), text };
21
21
  }
22
22
  /**
23
- * Removes one element of a comma-separated list - a parameter, a named import, a JSON property - taking
24
- * the separator that would otherwise dangle. The last element gives up the comma before it; any other
25
- * gives up the one after.
26
- *
27
- * The source file is passed explicitly because a tree from `ts.parseJsonText` carries no
28
- * back-reference, so `getStart()` cannot find one on its own.
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.
29
26
  */
30
27
  export function removeFromList(items, item, source) {
31
28
  const at = items.indexOf(item);
@@ -37,3 +34,10 @@ export function removeFromList(items, item, source) {
37
34
  text: '',
38
35
  };
39
36
  }
37
+ /** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
38
+ export function removeStatement(node) {
39
+ const source = node.getSourceFile();
40
+ const line = source.getLineAndCharacterOfPosition(node.getEnd()).line;
41
+ const nextLineStart = source.getLineStarts()[line + 1];
42
+ return { start: node.getStart(), end: nextLineStart ?? node.getEnd(), text: '' };
43
+ }
@@ -7,6 +7,11 @@ export declare function isRaw(node: ts.Node): node is RawSql;
7
7
  export declare function columnExpressions(list: ts.Expression | undefined): readonly RawSql[];
8
8
  /** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
9
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;
10
15
  /** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
11
16
  export declare function rawWhereEdit(where: ts.Expression): Edit | undefined;
12
17
  /**
package/dist/entitySql.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import ts from 'typescript';
2
2
  import { inserted, replaced } from './edits.js';
3
- import { propertyValue } from './keyMaps.js';
3
+ import { identifierText, propertyValue } from './syntax.js';
4
4
  export function isRaw(node) {
5
5
  const callee = ts.isTaggedTemplateExpression(node)
6
6
  ? node.tag
7
7
  : ts.isCallExpression(node)
8
8
  ? node.expression
9
9
  : undefined;
10
- return callee !== undefined && ts.isIdentifier(callee) && callee.text === 'raw';
10
+ return identifierText(callee) === 'raw';
11
11
  }
12
12
  /** The expressions of a column list, written as one or returned by its callback: each entry or `column` that is `raw`. */
13
13
  export function columnExpressions(list) {
@@ -20,7 +20,15 @@ export function columnExpressions(list) {
20
20
  }
21
21
  /** SQL text as a `raw` tagged template, escaping what would end or interpolate it. */
22
22
  export function rawTag(sql) {
23
- return `raw\`${sql.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\``;
23
+ return `raw${templateOf([sql])}`;
24
+ }
25
+ /**
26
+ * A template literal reading `texts` with the source of each of `values` interpolated between them, one fewer
27
+ * than `texts`: in the texts, a backslash, a backtick and a `${` are escaped.
28
+ */
29
+ export function templateOf(texts, values = []) {
30
+ const escaped = texts.map((text) => text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${'));
31
+ return `\`${escaped.reduce((template, text, at) => `${template}\${${values[at - 1]}}${text}`)}\``;
24
32
  }
25
33
  /** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
26
34
  export function rawWhereEdit(where) {
@@ -1,15 +1,9 @@
1
1
  import ts from 'typescript';
2
- /** A string type carrying more than `string`, e.g. `` type UUID = `${string}-${string}` ``. */
3
2
  export declare function isBrandedString(type: ts.Type): boolean;
4
3
  /**
5
- * The `FieldType` expression to write for a property declared as `type`, or `undefined` when the shape is
6
- * one a human has to decide on.
7
- *
8
- * Mirrors `TypeFor` in `uql-orm`, in the direction the codemod needs: from the property's TypeScript type
9
- * to the `type` option that reflection used to supply. Only the cases reflection could actually produce
10
- * are handled. A `Json<T>` or vector property already had to declare its `type` explicitly, because
11
- * `design:type` reported the useless `Object`/`Array` for them, so any such property reaching here is
12
- * genuinely ambiguous and is reported rather than guessed.
4
+ * The `type` option reflection supplied for a property of `type`, mirroring `TypeFor` in `uql-orm`, or nothing
5
+ * for a shape a human decides. Only what reflection could produce is read: a `Json<T>` or vector property got
6
+ * `Object`/`Array` from it, so it already declared its `type`.
13
7
  */
14
8
  export declare function fieldTypeFor(type: ts.Type): string | undefined;
15
9
  /**
package/dist/fieldType.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import ts from 'typescript';
2
- /** A string type carrying more than `string`, e.g. `` type UUID = `${string}-${string}` ``. */
2
+ /** The string types carrying more than `string`, e.g. `` type UUID = `${string}-${string}` ``, still string columns. */
3
+ const BRANDED_STRING = ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping;
3
4
  export function isBrandedString(type) {
4
- const branded = ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping;
5
- return type.isUnion() ? type.types.some((t) => !!(t.getFlags() & branded)) : !!(type.getFlags() & branded);
5
+ return (type.isUnion() ? type.types : [type]).some((t) => !!(t.getFlags() & BRANDED_STRING));
6
6
  }
7
7
  /**
8
8
  * Reads a union by requiring its arms to agree, which is what an `'a' | 'b'` enum or a `T | null` looks
@@ -14,24 +14,16 @@ function foldUnion(type, read) {
14
14
  return readings.size === 1 ? [...readings][0] : undefined;
15
15
  }
16
16
  /**
17
- * The `FieldType` expression to write for a property declared as `type`, or `undefined` when the shape is
18
- * one a human has to decide on.
19
- *
20
- * Mirrors `TypeFor` in `uql-orm`, in the direction the codemod needs: from the property's TypeScript type
21
- * to the `type` option that reflection used to supply. Only the cases reflection could actually produce
22
- * are handled. A `Json<T>` or vector property already had to declare its `type` explicitly, because
23
- * `design:type` reported the useless `Object`/`Array` for them, so any such property reaching here is
24
- * genuinely ambiguous and is reported rather than guessed.
17
+ * The `type` option reflection supplied for a property of `type`, mirroring `TypeFor` in `uql-orm`, or nothing
18
+ * for a shape a human decides. Only what reflection could produce is read: a `Json<T>` or vector property got
19
+ * `Object`/`Array` from it, so it already declared its `type`.
25
20
  */
26
21
  export function fieldTypeFor(type) {
27
22
  const flags = type.getFlags();
28
23
  if (type.isUnion()) {
29
24
  return foldUnion(type, fieldTypeFor);
30
25
  }
31
- // `TemplateLiteral` and `StringMapping` cover the branded-string types projects use for ids, e.g.
32
- // `type UUID = `${string}-${string}-${string}-${string}-${string}``, which are still string columns.
33
- const stringLike = ts.TypeFlags.String | ts.TypeFlags.StringLiteral | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping;
34
- if (flags & stringLike)
26
+ if (flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral | BRANDED_STRING))
35
27
  return 'String';
36
28
  if (flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))
37
29
  return 'Number';
@@ -0,0 +1,40 @@
1
+ import ts from 'typescript';
2
+ import type { Context } from './context.js';
3
+ type UqlImport = {
4
+ readonly declaration: ts.ImportDeclaration;
5
+ readonly entry: string;
6
+ readonly elements: readonly ts.ImportSpecifier[];
7
+ };
8
+ /**
9
+ * The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
10
+ * for what is read by name only: `idKey` written into one would land on an entry that may not export it.
11
+ */
12
+ export declare function uqlImportDeclarations(source: ts.SourceFile, entries?: boolean): readonly UqlImport[];
13
+ /** The specifier importing the export `name` from `uql-orm`, or with `entries` from any of its entries. */
14
+ export declare function uqlImport(source: ts.SourceFile, name: string, entries?: boolean): ts.ImportSpecifier | undefined;
15
+ /** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
16
+ export declare function usesOf(source: ts.SourceFile, name: ts.Identifier, checker: ts.TypeChecker): ts.Identifier[];
17
+ /** Names an export that no longer exists, where it is imported from the package or one of its entries. */
18
+ 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
+ /**
26
+ * The imported names the codemod removes once every use was rewritten: `Relation` unwrapped and `expr` defaults.
27
+ * A removed decorator keeps its import on purpose: deleting it would swap a clear error for a puzzling one.
28
+ */
29
+ export declare function deadImportNames(ctx: Context): ReadonlySet<string>;
30
+ /**
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.
33
+ */
34
+ export declare function dropDeadImports(dead: ReadonlySet<string>, rewritten: ReadonlySet<ts.ImportDeclaration>, ctx: Context): void;
35
+ /**
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.
38
+ */
39
+ export declare function addImports(rewritten: ReadonlySet<ts.ImportDeclaration>, ctx: Context): void;
40
+ export {};