uql-codemod 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,6 +1,8 @@
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
  };
package/dist/cli.js CHANGED
@@ -11,11 +11,16 @@ export function parseArgs(argv) {
11
11
  const errors = [];
12
12
  const values = new Map();
13
13
  let dryRun = false;
14
+ let help = false;
14
15
  for (const arg of argv) {
15
16
  if (arg === '--dry-run') {
16
17
  dryRun = true;
17
18
  continue;
18
19
  }
20
+ if (arg === '--help' || arg === '-h') {
21
+ help = true;
22
+ continue;
23
+ }
19
24
  const separator = arg.indexOf('=');
20
25
  const name = arg.slice(2, separator === -1 ? undefined : separator);
21
26
  if (!arg.startsWith('--') || !VALUE_FLAGS.has(name)) {
@@ -35,6 +40,7 @@ export function parseArgs(argv) {
35
40
  dryRun,
36
41
  include: values.get('include')?.split(',').filter(Boolean),
37
42
  },
43
+ help,
38
44
  errors,
39
45
  };
40
46
  }
@@ -7,6 +7,8 @@ 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
+ /** `sql` as a template literal reading the same text: a backslash, a backtick and a `${` escaped. */
11
+ export declare function templateOf(sql: string): string;
10
12
  /** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
11
13
  export declare function rawWhereEdit(where: ts.Expression): Edit | undefined;
12
14
  /**
package/dist/entitySql.js CHANGED
@@ -20,7 +20,11 @@ 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
+ /** `sql` as a template literal reading the same text: a backslash, a backtick and a `${` escaped. */
26
+ export function templateOf(sql) {
27
+ return `\`${sql.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\``;
24
28
  }
25
29
  /** A partial-index `where` string as `raw`, or nothing for a template that interpolates, whose values `raw` would bind. */
26
30
  export function rawWhereEdit(where) {
package/dist/index.js CHANGED
@@ -29,13 +29,15 @@ export async function run({ project, dryRun, include }) {
29
29
  noEmit: true,
30
30
  });
31
31
  const checker = program.getTypeChecker();
32
+ const { strictNullChecks, strict } = program.getCompilerOptions();
33
+ const nulls = { strictNullChecks: strictNullChecks ?? strict ?? false };
32
34
  const results = [];
33
35
  for (const source of program.getSourceFiles()) {
34
36
  if (source.isDeclarationFile || /[/\\]node_modules[/\\]/.test(source.fileName))
35
37
  continue;
36
38
  if (include?.length && !include.some((fragment) => source.fileName.includes(fragment)))
37
39
  continue;
38
- const result = transformFile(source, checker);
40
+ const result = transformFile(source, checker, nulls);
39
41
  if (result.changed || result.unresolved.length || result.notes.length)
40
42
  results.push(result);
41
43
  }
@@ -9,4 +9,6 @@ export type FileResult = {
9
9
  readonly notes: readonly string[];
10
10
  };
11
11
  /** Rewrites one source file for the standard decorator spec. */
12
- export declare function transformFile(source: ts.SourceFile, checker: ts.TypeChecker): FileResult;
12
+ export declare function transformFile(source: ts.SourceFile, checker: ts.TypeChecker, options?: {
13
+ readonly strictNullChecks: boolean;
14
+ }): FileResult;
package/dist/transform.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import ts from 'typescript';
2
2
  import { appended, applyEdits, inserted, removeFromList, replaced } from './edits.js';
3
- import { columnExpressions, handNamedColumns, isRaw, rawTag, rawWhereEdit } from './entitySql.js';
3
+ import { columnExpressions, handNamedColumns, isRaw, rawTag, rawWhereEdit, templateOf } from './entitySql.js';
4
4
  import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
5
5
  import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, propertyKey, propertyValue, quoted, referencesEdits, } from './keyMaps.js';
6
6
  const FIELD_DECORATORS = new Set(['Field', 'Id']);
@@ -56,14 +56,33 @@ const REMOVED_EXPORTS = new Map([
56
56
  'POSTGRES_WIRE_DRIVER_CAPABILITIES',
57
57
  'pass `driverCapabilities: { nativeArrays: false, explicitJsonCast: true }` to the dialect',
58
58
  ],
59
- ['MysqlLikeSqlDialect', 'extend `MySqlDialect` (`uql-orm/mysql`) or `MariaDialect` (`uql-orm/maria`)'],
59
+ ['MysqlLikeSqlDialect', 'extend `MySqlDialect` (`uql-orm/mysql`) or `MariaDialect` (`uql-orm/mariadb`)'],
60
60
  ['D1Meta', "uql reads `D1Result['meta']`; the rest of a binding is typed by `@cloudflare/workers-types`"],
61
61
  ['D1ExecResult', 'the rest of a binding is typed by `@cloudflare/workers-types`'],
62
62
  ['createSchemaGenerator', 'use `new SqlSchemaGenerator(dialect)`, or `migrator.getSchemaGenerator()`'],
63
+ ...['JsonMigrationStorage', 'DatabaseMigrationStorage', 'MongoMigrationStorage'].map((name) => [name, 'the migrator keeps its journal in the database itself, named by `tableName`']),
63
64
  ]);
65
+ /** What `uql-orm/util` exported that the root still does. */
66
+ const UTIL_ROOT_EXPORTS = [
67
+ 'currentDate',
68
+ 'currentTime',
69
+ 'currentTimestamp',
70
+ 'raw',
71
+ 'refs',
72
+ 'uuid',
73
+ 'uuidv7',
74
+ 'deleteFrom',
75
+ 'insertInto',
76
+ 'refuse',
77
+ 'updateTable',
78
+ 'upsertInto',
79
+ 'withDeleted',
80
+ 'HookContext',
81
+ 'DefaultLogger',
82
+ ];
64
83
  /**
65
- * Exports renamed or moved and nothing else - the driver classes were empty subclasses - so the import and
66
- * every use follow, the import moving to `from` where the name lives in another entry.
84
+ * Exports renamed or moved and nothing else, so the import and every use follow, the import moving to `from`
85
+ * where the name lives in another entry. Keyed by name, or by `entry#name` where the name stays elsewhere.
67
86
  */
68
87
  const RENAMED_EXPORTS = new Map([
69
88
  ['QueryWhereMap', { to: 'QueryWhere' }],
@@ -75,7 +94,7 @@ const RENAMED_EXPORTS = new Map([
75
94
  ['CrdbQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
76
95
  ['NeonQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
77
96
  ['MySql2Dialect', { to: 'MySqlDialect', from: 'uql-orm/mysql' }],
78
- ['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/mongo' }],
97
+ ['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/mongodb' }],
79
98
  ['LibsqlQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
80
99
  ['TursoQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
81
100
  ['TursoLocalQuerier', { to: 'SqliteQuerier', from: 'uql-orm/sqlite' }],
@@ -87,10 +106,43 @@ const RENAMED_EXPORTS = new Map([
87
106
  ['KnownMigratorDialect', { to: 'DialectName', from: 'uql-orm' }],
88
107
  ['D1Preparer', { to: 'D1Queryable' }],
89
108
  ['D1Database', { to: 'D1Queryable', from: 'uql-orm/d1' }],
90
- ['MongoQuerier', { to: 'MongoQuerier', from: 'uql-orm/mongo' }],
91
- ['isMongoQuerier', { to: 'isMongoQuerier', from: 'uql-orm/mongo' }],
92
- ['MongoMigrationStorage', { to: 'MongoMigrationStorage', from: 'uql-orm/mongo' }],
93
- ['MongoSchemaIntrospector', { to: 'MongoSchemaIntrospector', from: 'uql-orm/mongo' }],
109
+ ['MongoQuerier', { to: 'MongoQuerier', from: 'uql-orm/mongodb' }],
110
+ ['isMongoQuerier', { to: 'isMongoQuerier', from: 'uql-orm/mongodb' }],
111
+ ['MongoSchemaIntrospector', { to: 'MongoSchemaIntrospector', from: 'uql-orm/mongodb' }],
112
+ ['Sqlite3QuerierPool', { to: 'SqliteQuerierPool' }],
113
+ ['Sqlite3PoolOptions', { to: 'SqlitePoolOptions' }],
114
+ ['IMigrationBuilder', { to: 'MigrationBuilder' }],
115
+ ['ITableBuilder', { to: 'TableBuilder' }],
116
+ ['IAlterTableBuilder', { to: 'AlterTableBuilder' }],
117
+ ['IColumnBuilder', { to: 'ColumnBuilder' }],
118
+ ['IColumnFactory', { to: 'ColumnFactory' }],
119
+ ['IForeignKeyBuilder', { to: 'ForeignKeyBuilder' }],
120
+ ['ITableForeignKeyBuilder', { to: 'TableForeignKeyBuilder' }],
121
+ ['UqlLockUsageError', { to: 'UqlUsageError' }],
122
+ ['uql-orm/http#Hook', { to: 'RequestHook' }],
123
+ ['uql-orm/http#HookContext', { to: 'RequestHookContext' }],
124
+ ...UTIL_ROOT_EXPORTS.map((name) => [`uql-orm/util#${name}`, { to: name, from: 'uql-orm' }]),
125
+ ]);
126
+ /** What the root exported that is internal now, and what to write instead. */
127
+ const INTERNAL_EXPORTS = new Map([
128
+ ...['AbstractDialect', 'AbstractSqlDialect', 'AbstractQuerier', 'AbstractQuerierPool'].map((name) => [name, "extend a driver's dialect or pool from its entry instead"]),
129
+ ...['AbstractSqlQuerier', 'AbstractSqlQuerierPool'].map((name) => [name, "extend a driver's querier or pool from its entry instead"]),
130
+ ['SqlQueryContext', 'make one with `dialect.createContext()`, typed `QueryContext`'],
131
+ ['dialectOptionsFrom', "pass the options to the driver's dialect"],
132
+ ...['fieldOf', 'relationOf', 'soleIdOf', 'assertSoleId'].map((name) => [name, 'read the field off `getMeta(Entity).fields`']),
133
+ ['namesKey', 'read the keys off `getMeta(Entity).ids`'],
134
+ ['getEntities', 'list your entities in the config'],
135
+ ]);
136
+ /** Entries renamed, or dropped for repeating the root, each to the one now exporting what it did. */
137
+ const MOVED_ENTRIES = new Map([
138
+ ['uql-orm/maria', 'uql-orm/mariadb'],
139
+ ['uql-orm/mongo', 'uql-orm/mongodb'],
140
+ ['uql-orm/bunSql', 'uql-orm/bun-sql'],
141
+ ['uql-orm/dialect', 'uql-orm'],
142
+ ['uql-orm/entity', 'uql-orm'],
143
+ ['uql-orm/namingStrategy', 'uql-orm'],
144
+ ['uql-orm/querier', 'uql-orm'],
145
+ ['uql-orm/type', 'uql-orm'],
94
146
  ]);
95
147
  /** A node's decorators, or none where it is a kind that cannot carry them. */
96
148
  function decoratorsOf(node) {
@@ -216,8 +268,8 @@ function addFieldType(decorator, node, ctx) {
216
268
  const type = addInferredOption(decorator, node, ctx, {
217
269
  option: 'type',
218
270
  // A `references` field deliberately has no `type`: the column resolves from the primary key it
219
- // points at, which also carries that key's `columnType` and length.
220
- satisfiedBy: ['type', 'references'],
271
+ // points at, which also carries that key's `columnType` and length. A `computed` one is its SQL.
272
+ satisfiedBy: ['type', 'references', 'computed'],
221
273
  infer: fieldTypeFor,
222
274
  });
223
275
  // A branded id type (`type UUID = `${string}-${string}-...``) erased to `String` under reflection, so
@@ -440,6 +492,75 @@ function rewriteIndexWhere(options, ctx) {
440
492
  function isStringTyped(type) {
441
493
  return type.isUnion() ? type.types.every(isStringTyped) : Boolean(type.flags & ts.TypeFlags.StringLike);
442
494
  }
495
+ /**
496
+ * A querier's `run`/`all` handed SQL as a string, which now takes a tagged statement: a literal becomes
497
+ * `run`...``, SQL built at run time `raw.text`, which splices as the string did. One passed with values is reported.
498
+ */
499
+ function rewriteSqlCall(call, ctx) {
500
+ const callee = call.expression;
501
+ if (!ts.isPropertyAccessExpression(callee) || !SQL_METHODS.has(callee.name.text) || !isUqlMember(callee, ctx)) {
502
+ return;
503
+ }
504
+ const [sql, ...values] = call.arguments;
505
+ if (!sql || !isStringTyped(ctx.checker.getTypeAtLocation(sql))) {
506
+ return;
507
+ }
508
+ if (values.length) {
509
+ ctx.unresolved.push(`${ctx.describe(call)}: ${callee.name.text}() takes one raw statement: write the values into it, raw\`... \${value}\`, which binds them`);
510
+ return;
511
+ }
512
+ if (ts.isStringLiteral(sql) || ts.isNoSubstitutionTemplateLiteral(sql)) {
513
+ const typeArguments = call.typeArguments ? `<${call.typeArguments.map((type) => type.getText()).join(', ')}>` : '';
514
+ ctx.edits.push(replaced(call, `${callee.getText()}${typeArguments}${templateOf(sql.text)}`));
515
+ return;
516
+ }
517
+ if (ts.isTemplateExpression(sql)) {
518
+ ctx.notes.push(`${ctx.describe(call)}: the values in this template are spliced into the SQL; write it as raw\`...\` to bind them`);
519
+ }
520
+ ctx.edits.push(replaced(sql, `raw.text(${sql.getText()})`));
521
+ ctx.imports.set('raw', 'the raw.text(...)');
522
+ }
523
+ const SQL_METHODS = new Set(['run', 'all']);
524
+ /** `const { id } = await q.upsertOne(...)`, which resolves to the id now, as the id: other keys are reported. */
525
+ function rewriteUpsertResult(declaration, ctx) {
526
+ const { name, initializer } = declaration;
527
+ const call = initializer && ts.isAwaitExpression(initializer) ? initializer.expression : initializer;
528
+ if (!ts.isObjectBindingPattern(name) ||
529
+ !call ||
530
+ !ts.isCallExpression(call) ||
531
+ !ts.isPropertyAccessExpression(call.expression) ||
532
+ call.expression.name.text !== 'upsertOne' ||
533
+ !isUqlMember(call.expression, ctx)) {
534
+ return;
535
+ }
536
+ const [element] = name.elements;
537
+ const key = element && (element.propertyName ?? element.name);
538
+ if (name.elements.length === 1 && key && ts.isIdentifier(key) && key.text === 'id' && ts.isIdentifier(element.name)) {
539
+ ctx.edits.push(replaced(name, element.name.text));
540
+ }
541
+ else {
542
+ ctx.unresolved.push(`${ctx.describe(declaration)}: upsertOne() resolves to the id; 'created' and 'changes' are gone`);
543
+ }
544
+ }
545
+ /** A `Migrator` call whose name or meaning changed: `pending()`/`executed()` are `status()`, and `down()` reverts one. */
546
+ function reportMigratorCall(call, ctx) {
547
+ const callee = call.expression;
548
+ if (!ts.isPropertyAccessExpression(callee) || !isUqlMember(callee, ctx)) {
549
+ return;
550
+ }
551
+ const method = callee.name.text;
552
+ if (method === 'pending' || method === 'executed') {
553
+ ctx.unresolved.push(`${ctx.describe(call)}: '${method}()' was removed; read \`(await migrator.status()).${method}\``);
554
+ }
555
+ else if (method === 'down' && !call.arguments.length) {
556
+ ctx.notes.push(`${ctx.describe(call)}: down() with no options reverts only the last migration now; pass \`{ step: Infinity }\` to revert them all`);
557
+ }
558
+ }
559
+ /** Whether `member` is declared by uql-orm, so a `run` of another library's is left alone. */
560
+ function isUqlMember(member, ctx) {
561
+ const declarations = ctx.checker.getSymbolAtLocation(member.name)?.declarations ?? [];
562
+ return declarations.some((declaration) => declaration.getSourceFile().fileName.includes('/node_modules/uql-orm/'));
563
+ }
443
564
  /** The migration builder's index methods, each with the position of the options it takes. */
444
565
  const BUILDER_INDEX_METHODS = new Map([
445
566
  ['index', 1],
@@ -717,14 +838,18 @@ function addImports(source, rewritten, ctx) {
717
838
  if (!missing.length) {
718
839
  return;
719
840
  }
720
- const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration))?.elements[0];
841
+ const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration));
721
842
  if (!anchor) {
722
843
  for (const [name, what] of missing) {
723
844
  ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
724
845
  }
725
846
  return;
726
847
  }
727
- ctx.edits.push(inserted(anchor, missing.map(([name]) => `${name}, `).join('')));
848
+ const names = missing.map(([name]) => name);
849
+ const typeOnly = anchor.declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
850
+ ctx.edits.push(typeOnly
851
+ ? inserted(anchor.declaration, `import { ${names.join(', ')} } from 'uql-orm';\n`)
852
+ : inserted(anchor.elements[0], names.map((name) => `${name}, `).join('')));
728
853
  }
729
854
  /** Everything the standard spec needs written onto one decorated property, and the options renamed since. */
730
855
  function rewriteProperty(node, ctx) {
@@ -771,7 +896,7 @@ function rewriteProperty(node, ctx) {
771
896
  function admitNull(options, node, ctx) {
772
897
  // Options it cannot read may state `nullable` themselves, and appending to the property would then
773
898
  // contradict the column. `addFieldType` already reports the same options, so this adds no second note.
774
- if (options.kind === 'opaque') {
899
+ if (options.kind === 'opaque' || !ctx.strictNullChecks) {
775
900
  return;
776
901
  }
777
902
  if (declaresNotNull(options) ||
@@ -793,9 +918,6 @@ function admitNull(options, node, ctx) {
793
918
  * so the declaration is found there rather than under a decorator.
794
919
  */
795
920
  function admitNullOnDefined(field, owner, ctx) {
796
- if (!ts.isPropertyAssignment(field)) {
797
- return;
798
- }
799
921
  const key = propertyKey(field.name);
800
922
  const declaration = key
801
923
  ? instanceTypeOf(owner.entity, ctx.checker)
@@ -833,17 +955,31 @@ function reportRemovedDecorators(node, ctx) {
833
955
  }
834
956
  /** Names an export that no longer exists, where it is imported from the package or one of its entries. */
835
957
  function reportRemovedExports(source, ctx) {
836
- for (const element of uqlImports(source, true)) {
837
- const name = importedName(element);
838
- const advice = REMOVED_EXPORTS.get(name);
839
- if (advice) {
840
- ctx.unresolved.push(`${ctx.describe(element)}: '${name}' was removed; ${advice}`);
958
+ for (const { declaration, entry, elements } of uqlImportDeclarations(source, true)) {
959
+ if (entry === 'uql-orm/util') {
960
+ const names = elements.filter((element) => !renamedTo(element, entry)).map(importedName);
961
+ if (names.length) {
962
+ ctx.unresolved.push(`${ctx.describe(declaration)}: 'uql-orm/util' was removed; its helpers are internal, so write your own \`${names.join('`, `')}\``);
963
+ }
964
+ continue;
965
+ }
966
+ const root = (MOVED_ENTRIES.get(entry) ?? entry) === 'uql-orm';
967
+ for (const element of elements) {
968
+ const name = importedName(element);
969
+ const removed = REMOVED_EXPORTS.get(name);
970
+ const internal = root ? INTERNAL_EXPORTS.get(name) : undefined;
971
+ if (removed) {
972
+ ctx.unresolved.push(`${ctx.describe(element)}: '${name}' was removed; ${removed}`);
973
+ }
974
+ else if (internal) {
975
+ ctx.unresolved.push(`${ctx.describe(element)}: '${name}' is internal; ${internal}`);
976
+ }
841
977
  }
842
978
  }
843
979
  }
844
980
  /**
845
- * Rewrites each import naming a {@link RENAMED_EXPORTS} export, renaming every use of it: a name moving
846
- * entries gets an import from its new one, and a name the file already imports is dropped, as is a `dead`
981
+ * Rewrites each import naming a {@link RENAMED_EXPORTS} export, or from a {@link MOVED_ENTRIES} entry, renaming
982
+ * every use of it: a name moving entries gets an import from its new one, and a name the file already imports is dropped, as is a `dead`
847
983
  * one. Returns the rewritten imports, which nothing else may edit.
848
984
  */
849
985
  function renameExports(source, dead, ctx) {
@@ -851,8 +987,9 @@ function renameExports(source, dead, ctx) {
851
987
  const kept = imports.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)));
852
988
  const bound = new Set(kept.map((element) => element.name.text));
853
989
  const rewritten = new Set();
854
- for (const { declaration, entry, elements } of imports) {
855
- if (!elements.some((element) => renamedTo(element, entry))) {
990
+ for (const { declaration, entry: imported, elements } of imports) {
991
+ const entry = MOVED_ENTRIES.get(imported) ?? imported;
992
+ if (entry === imported && !elements.some((element) => renamedTo(element, entry))) {
856
993
  continue;
857
994
  }
858
995
  const byEntry = new Map([[entry, []]]);
@@ -886,8 +1023,8 @@ function renameExports(source, dead, ctx) {
886
1023
  /** The rename `element` still needs, none where it already names the export at its entry. */
887
1024
  function renamedTo(element, entry) {
888
1025
  const name = importedName(element);
889
- const rename = RENAMED_EXPORTS.get(name);
890
- return rename?.to === name && (rename.from ?? entry) === entry ? undefined : rename;
1026
+ const rename = RENAMED_EXPORTS.get(`${entry}#${name}`) ?? RENAMED_EXPORTS.get(name);
1027
+ return rename?.to === name && rename.from === entry ? undefined : rename;
891
1028
  }
892
1029
  /** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
893
1030
  function usesOf(source, name, checker) {
@@ -1088,9 +1225,10 @@ function relatedClass(property, node, checker) {
1088
1225
  return symbol && symbol.flags & ts.SymbolFlags.Class ? symbol : undefined;
1089
1226
  }
1090
1227
  /** Rewrites one source file for the standard decorator spec. */
1091
- export function transformFile(source, checker) {
1228
+ export function transformFile(source, checker, options = { strictNullChecks: true }) {
1092
1229
  const ctx = {
1093
1230
  checker,
1231
+ strictNullChecks: options.strictNullChecks,
1094
1232
  edits: [],
1095
1233
  unresolved: [],
1096
1234
  notes: [],
@@ -1120,6 +1258,8 @@ export function transformFile(source, checker) {
1120
1258
  rewriteUpsertCall(node, ctx);
1121
1259
  if (importsUql) {
1122
1260
  rewriteBuilderCall(node, ctx);
1261
+ rewriteSqlCall(node, ctx);
1262
+ reportMigratorCall(node, ctx);
1123
1263
  }
1124
1264
  if (importsExpr) {
1125
1265
  rewriteExprCall(node, ctx);
@@ -1128,6 +1268,9 @@ export function transformFile(source, checker) {
1128
1268
  if (ts.isPropertyAssignment(node)) {
1129
1269
  rewriteStatementKeys(node, ctx);
1130
1270
  }
1271
+ if (importsUql && ts.isVariableDeclaration(node)) {
1272
+ rewriteUpsertResult(node, ctx);
1273
+ }
1131
1274
  ts.forEachChild(node, visit);
1132
1275
  };
1133
1276
  visit(source);
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.45.0",
5
+ "version": "0.47.0",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=24"