uql-codemod 0.47.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/dist/transform.js CHANGED
@@ -1,8 +1,13 @@
1
1
  import ts from 'typescript';
2
- import { appended, applyEdits, inserted, removeFromList, replaced } from './edits.js';
3
- import { columnExpressions, handNamedColumns, isRaw, rawTag, rawWhereEdit, templateOf } from './entitySql.js';
2
+ import { rewriteColumnRefs } from './columnRefs.js';
3
+ import { createContext, instanceTypeOf, memberNames, ownerOf } from './context.js';
4
+ import { appended, applyEdits, inserted, replaced } from './edits.js';
5
+ import { columnExpressions, handNamedColumns, rawTag, rawWhereEdit } from './entitySql.js';
4
6
  import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
5
- import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, propertyKey, propertyValue, quoted, referencesEdits, } from './keyMaps.js';
7
+ import { addImports, deadImportNames, dropDeadImports, renameExports, reportRemovedExports, uqlImport, uqlImportDeclarations, } from './imports.js';
8
+ import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, quoted, referencesEdits, } from './keyMaps.js';
9
+ import { statementTemplate } from './statementSql.js';
10
+ import { arrayElements, decoratorName, decoratorsOf, identifierText, namedProperty, objectLiteral, objectProperties, propertyKey, propertyValue, } from './syntax.js';
6
11
  const FIELD_DECORATORS = new Set(['Field', 'Id']);
7
12
  const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
8
13
  const TO_ONE_DECORATORS = new Set(['OneToOne', 'ManyToOne']);
@@ -18,145 +23,9 @@ const REMOVED_DECORATORS = new Map([
18
23
  ['Transactional', 'wrap the body in `pool.transaction(async (querier) => { ... })`'],
19
24
  ['InjectQuerier', 'take the querier from the enclosing `pool.transaction()` callback'],
20
25
  ]);
21
- /**
22
- * Exports that no longer exist, mapped to what to do instead. Reported rather than rewritten, for the
23
- * same reason as the decorators: what replaces each is decided at its call site.
24
- */
25
- const REMOVED_EXPORTS = new Map([
26
- [
27
- 'setQuerierPool',
28
- 'pass the pool where it is used: `createFetchHandler({ pool, include })`, `querierMiddleware({ pool, include })`',
29
- ],
30
- ['getQuerierPool', 'take the pool from the module that builds it, or from Nest DI'],
31
- ['getQuerier', 'use `pool.withQuerier(...)` / `pool.transaction(...)`, which release the connection'],
32
- ['ClientQuerierPool', "construct the `HttpQuerier` where it is used, `new HttpQuerier('/api')`"],
33
- ['QueryWhereFieldMap', 'use `QueryWhere`'],
34
- ['QueryStreamProjected', 'use `QueryProjected`'],
35
- ['RelationMappedBy', "a 'mappedBy' is `(keys: KeyMap<E>) => Key<E>` now"],
36
- ['RelationKeyMapper', 'it is `(keys: KeyMap<E>) => Key<E>` now'],
37
- ['augmentWhere', 'spread the two maps: `{ ...where, ...extra }`'],
38
- ['buildQueryWhereAsMap', 'a `$where` is a map already; name the key for ids: `{ id: [1, 2] }`'],
39
- ['AbstractPgQuerier', 'every pg-compatible pool returns `PgQuerier` (`uql-orm/postgres`), which is concrete'],
40
- ['AbstractHranaQuerierPool', 'extend `AbstractSqlQuerierPool` and hand every querier a `HranaQuerier`'],
41
- [
42
- 'PreparedSqliteQuerier',
43
- 'use `SqliteQuerier`, which takes any driver that prepares, or extend `AbstractSqliteQuerier`',
44
- ],
45
- ['toSqliteBindValues', 'pass the values as they are: every SQLite driver binds a `SqliteBindValue[]`'],
46
- ['libsqlUseRemoteForMigrations', '`LibsqlQuerierPool.getMigrationQuerier()` decides it'],
47
- ['SqlCallback', 'use `EntitySql<E>`, which holds the callback form'],
48
- [
49
- 'IndexColumnOptions',
50
- "an entity's index entry is `EntityIndexColumnInput<E>`, the migration builder's `IndexColumnInput`",
51
- ],
52
- ['isKnownMigratorDialect', 'every dialect has a migrator, so it always held: drop the check'],
53
- ['QuerierPoolDialect', "read the pool's own: `P['dialect']`"],
54
- ['QuerierPoolQuerier', "read the pool's own: `Awaited<ReturnType<P['getQuerier']>>`"],
55
- [
56
- 'POSTGRES_WIRE_DRIVER_CAPABILITIES',
57
- 'pass `driverCapabilities: { nativeArrays: false, explicitJsonCast: true }` to the dialect',
58
- ],
59
- ['MysqlLikeSqlDialect', 'extend `MySqlDialect` (`uql-orm/mysql`) or `MariaDialect` (`uql-orm/mariadb`)'],
60
- ['D1Meta', "uql reads `D1Result['meta']`; the rest of a binding is typed by `@cloudflare/workers-types`"],
61
- ['D1ExecResult', 'the rest of a binding is typed by `@cloudflare/workers-types`'],
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`']),
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
- ];
83
- /**
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.
86
- */
87
- const RENAMED_EXPORTS = new Map([
88
- ['QueryWhereMap', { to: 'QueryWhere' }],
89
- ['RelationKeyMap', { to: 'KeyMap' }],
90
- ['FilterCondition', { to: 'FilterWhere' }],
91
- ['PgDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
92
- ['NeonDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
93
- ['PgliteDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
94
- ['CrdbQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
95
- ['NeonQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
96
- ['MySql2Dialect', { to: 'MySqlDialect', from: 'uql-orm/mysql' }],
97
- ['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/mongodb' }],
98
- ['LibsqlQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
99
- ['TursoQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
100
- ['TursoLocalQuerier', { to: 'SqliteQuerier', from: 'uql-orm/sqlite' }],
101
- ['TursoDatabase', { to: 'SqliteDatabase', from: 'uql-orm/sqlite' }],
102
- ['SqlMigrationModuleOptions', { to: 'MigrationModuleOptions' }],
103
- ['buildSqlQuerierMigrationModule', { to: 'buildMigrationModule' }],
104
- ['QueryDialect', { to: 'SqlQueryDialect' }],
105
- ['EngineFeatures', { to: 'DialectFeatures' }],
106
- ['KnownMigratorDialect', { to: 'DialectName', from: 'uql-orm' }],
107
- ['D1Preparer', { to: 'D1Queryable' }],
108
- ['D1Database', { to: 'D1Queryable', from: 'uql-orm/d1' }],
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'],
146
- ]);
147
- /** A node's decorators, or none where it is a kind that cannot carry them. */
148
- function decoratorsOf(node) {
149
- return ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
150
- }
151
- function decoratorName(node) {
152
- const call = ts.isCallExpression(node.expression) ? node.expression : undefined;
153
- const target = call?.expression ?? node.expression;
154
- return ts.isIdentifier(target) ? target.text : undefined;
155
- }
26
+ /** A decorator's options; `@Field` uncalled is opaque, since adding a call would guess it is the factory. */
156
27
  function decoratorOptions(node) {
157
28
  if (!ts.isCallExpression(node.expression)) {
158
- // `@Field` rather than `@Field()`: there is no argument list to write into, and adding one would mean
159
- // guessing that this is the decorator factory the codemod thinks it is.
160
29
  return { kind: 'opaque', reason: 'it is used without being called' };
161
30
  }
162
31
  const [first] = node.expression.arguments;
@@ -171,18 +40,9 @@ function optionsOf(value) {
171
40
  ? { kind: 'opaque', reason: 'its options object spreads another' }
172
41
  : { kind: 'literal', node: value };
173
42
  }
174
- /**
175
- * The property `name` in the options, whether its key is plain, quoted or a shorthand.
176
- *
177
- * All three forms, because both callers care about the option being *there*: one to leave an option
178
- * alone that is already stated, the other to rename it. Matching only the plain key inserted a second
179
- * `type` beside a quoted one, and left a shorthand `virtual` behind while reporting nothing.
180
- */
43
+ /** The option `name`, however its key is written: a stated option is left alone, and a renamed one moved. */
181
44
  function findProperty(options, name) {
182
- if (options.kind !== 'literal') {
183
- return undefined;
184
- }
185
- return options.node.properties.find((prop) => (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && propertyKey(prop.name) === name);
45
+ return options.kind === 'literal' ? namedProperty(options.node, name) : undefined;
186
46
  }
187
47
  /**
188
48
  * Renames the option `from` to `to`. Only the key is replaced, so the value, its formatting and any comment
@@ -195,7 +55,7 @@ function renameOption(options, from, to, node, ctx) {
195
55
  return;
196
56
  }
197
57
  if (findProperty(options, to)) {
198
- ctx.unresolved.push(`${ctx.describe(node)}: gives both '${from}' and '${to}'; keep '${to}'.`);
58
+ ctx.report(node, `gives both '${from}' and '${to}'; keep '${to}'.`);
199
59
  return;
200
60
  }
201
61
  ctx.edits.push(replaced(property.name, ts.isShorthandPropertyAssignment(property) ? `${to}: ${from}` : to));
@@ -213,25 +73,12 @@ function noteHandNamedColumns(sql, owner, ctx) {
213
73
  if (!sql || !names.length) {
214
74
  return;
215
75
  }
216
- ctx.notes.push(`${ctx.describe(sql)}: SQL names ${names.map((name) => `'${name}'`).join(', ')} by hand, which a rename ` +
217
- `does not reach; read each off a callback's refs instead: (${owner.param}) => raw\`...\${${owner.param}.<member>}...\``);
218
- }
219
- /** The names of the members of the entity `node` declares or names. */
220
- function memberNames(node, checker) {
221
- return propertyNames(instanceTypeOf(node, checker));
76
+ ctx.note(sql, `SQL names ${names.map((name) => `'${name}'`).join(', ')} by hand, which a rename does not reach; ` +
77
+ `read each off a callback's refs instead: (${owner.param}) => raw\`...\${${owner.param}.<member>}...\``);
222
78
  }
223
- /** The instance type of the entity `node` declares or names. */
224
- function instanceTypeOf(node, checker) {
225
- const type = checker.getTypeAtLocation(node);
226
- return type.getConstructSignatures()[0]?.getReturnType() ?? type;
227
- }
228
- function propertyNames(type) {
229
- return new Set(type.getProperties().map(({ name }) => name));
230
- }
231
- /** Inserts `option` into the decorator's options object, creating one when the call has no arguments. */
79
+ /** Inserts `option` into the decorator's options object, creating one in an empty argument list. */
232
80
  function insertOption(options, option) {
233
81
  if (options.kind === 'empty') {
234
- // `@Field()` -> `@Field({ ... })`: the argument list is empty, so this only ever inserts.
235
82
  const { arguments: args } = options.call;
236
83
  return { start: args.pos, end: args.end, text: `{ ${option} }` };
237
84
  }
@@ -239,11 +86,8 @@ function insertOption(options, option) {
239
86
  return first ? inserted(first, `${option}, `) : replaced(options.node, `{ ${option} }`);
240
87
  }
241
88
  /**
242
- * Writes one option the decorator can no longer infer at runtime, or records why it could not.
243
- *
244
- * Returns the property's type when an edit was written, so a caller can say something about what was
245
- * just decided. Nothing is written for options the codemod cannot read: replacing them would drop
246
- * whatever the author put there.
89
+ * Writes one option the decorator can no longer infer at runtime, or records why it could not. Returns the
90
+ * property's type where it wrote one, for the caller to say more about.
247
91
  */
248
92
  function addInferredOption(decorator, node, ctx, spec) {
249
93
  const options = decoratorOptions(decorator);
@@ -251,34 +95,32 @@ function addInferredOption(decorator, node, ctx, spec) {
251
95
  return undefined;
252
96
  }
253
97
  if (options.kind === 'opaque') {
254
- ctx.unresolved.push(`${ctx.describe(node)}: cannot add '${spec.option}' because ${options.reason}`);
98
+ ctx.report(node, `cannot add '${spec.option}' because ${options.reason}`);
255
99
  return undefined;
256
100
  }
257
101
  const type = ctx.checker.getTypeAtLocation(node);
258
102
  const value = spec.infer(type);
259
103
  if (!value) {
260
- ctx.unresolved.push(`${ctx.describe(node)}: cannot infer '${spec.option}' for ${ctx.checker.typeToString(type)}`);
104
+ ctx.report(node, `cannot infer '${spec.option}' for ${ctx.checker.typeToString(type)}`);
261
105
  return undefined;
262
106
  }
263
107
  ctx.edits.push(insertOption(options, `${spec.option}: ${value}`));
264
108
  return type;
265
109
  }
266
- /** Writes the `type` that `design:type` used to supply at runtime. */
110
+ /**
111
+ * Writes the `type` that `design:type` supplied, unless `references` (the key it points at decides) or
112
+ * `computed` does. A branded id erased to `String`, which keeps the column; `'uuid'` would change it, so that
113
+ * is noted rather than decided.
114
+ */
267
115
  function addFieldType(decorator, node, ctx) {
268
116
  const type = addInferredOption(decorator, node, ctx, {
269
117
  option: 'type',
270
- // A `references` field deliberately has no `type`: the column resolves from the primary key it
271
- // points at, which also carries that key's `columnType` and length. A `computed` one is its SQL.
272
118
  satisfiedBy: ['type', 'references', 'computed'],
273
119
  infer: fieldTypeFor,
274
120
  });
275
- // A branded id type (`type UUID = `${string}-${string}-...``) erased to `String` under reflection, so
276
- // `String` is what keeps the existing column. It is often not what was wanted, though, and switching
277
- // to `'uuid'` changes the generated column, so say so rather than decide.
278
121
  if (type && isBrandedString(type)) {
279
- ctx.notes.push(`${ctx.describe(node)}: set 'type: ${fieldTypeFor(type)}' for ` +
280
- `${ctx.checker.typeToString(type)}, matching the previous behaviour. If the column should be ` +
281
- "'uuid', change it deliberately: that alters the schema.");
122
+ ctx.note(node, `set 'type: ${fieldTypeFor(type)}' for ${ctx.checker.typeToString(type)}, matching the previous behaviour. ` +
123
+ "If the column should be 'uuid', change it deliberately: that alters the schema.");
282
124
  }
283
125
  }
284
126
  /** Writes the `entity` getter relations can no longer infer. */
@@ -298,10 +140,9 @@ function addRelationEntity(decorator, node, ctx) {
298
140
  * is declared, or `defineEntity`'s `fields`. A column it cannot see declared is reported instead.
299
141
  */
300
142
  function addForeignKeyReference(relation, key, owner, ctx, site) {
301
- const options = { kind: 'literal', node: relation };
302
143
  const cardinality = propertyValue(relation, 'cardinality');
303
144
  const toOne = !cardinality || (ts.isStringLiteralLike(cardinality) && TO_ONE_CARDINALITIES.has(cardinality.text));
304
- const joined = ['mappedBy', 'through', 'references'].some((option) => findProperty(options, option));
145
+ const joined = ['mappedBy', 'through', 'references'].some((option) => namedProperty(relation, option));
305
146
  const last = relation.properties.at(-1);
306
147
  if (!key || !last || !toOne || joined) {
307
148
  return;
@@ -309,7 +150,7 @@ function addForeignKeyReference(relation, key, owner, ctx, site) {
309
150
  const column = `${key}Id`;
310
151
  const declaration = columnDeclaration(column, relation, owner, ctx.checker, site);
311
152
  if (typeof declaration === 'string') {
312
- ctx.unresolved.push(`${ctx.describe(site && ts.isPropertyDeclaration(site) ? site : relation)}: ${declaration}`);
153
+ ctx.report(site && ts.isPropertyDeclaration(site) ? site : relation, declaration);
313
154
  return;
314
155
  }
315
156
  if (declaration) {
@@ -331,7 +172,7 @@ function columnDeclaration(column, relation, owner, checker, site) {
331
172
  const declared = site ? objectProperties(site).some(({ name }) => propertyKey(name) === column) : member;
332
173
  return declared ? undefined : undeclaredColumn(column);
333
174
  }
334
- /** `@Field({ references: () => Target }) <column>?: <key type>;` ahead of the relation, or why it cannot be written. */
175
+ /** `@Field({ references: () => Target }) <column>?: <key type> | null;` ahead of the relation, or why it cannot be written. */
335
176
  function foreignKeyColumn(property, column, relation, checker) {
336
177
  const targetType = checker.getNonNullableType(checker.getTypeAtLocation(property));
337
178
  const target = entityGetterTarget(relation) ?? relationTargetFor(targetType, checker);
@@ -343,10 +184,12 @@ function foreignKeyColumn(property, column, relation, checker) {
343
184
  if (!target || !key) {
344
185
  return undeclaredColumn(column);
345
186
  }
346
- const indent = ' '.repeat(property.getSourceFile().getLineAndCharacterOfPosition(property.getStart()).character);
347
187
  const type = keyTypeSource(key, property, checker);
348
- // `| null`, as on every column the codemod touches: a foreign key holds one like any other.
349
- return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type} | null;\n${indent}`);
188
+ return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type} | null;\n${indentOf(property)}`);
189
+ }
190
+ /** The whitespace a line starting at `node` is indented by. */
191
+ function indentOf(node) {
192
+ return ' '.repeat(node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).character);
350
193
  }
351
194
  /** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
352
195
  function keyTypeSource(key, at, checker) {
@@ -374,7 +217,7 @@ function rewriteValue(value, rewrite, node, advice, ctx) {
374
217
  ctx.edits.push(...edits);
375
218
  }
376
219
  else {
377
- ctx.unresolved.push(`${ctx.describe(node)}: ${advice}; this one could not be read`);
220
+ ctx.report(node, `${advice}; this one could not be read`);
378
221
  }
379
222
  }
380
223
  function rewriteKeyList(value, param, node, what, ctx) {
@@ -385,21 +228,22 @@ function rewriteKeyList(value, param, node, what, ctx) {
385
228
  * members named by string read off a callback's parameter named after the class, and SQL written as it is taken.
386
229
  */
387
230
  function rewriteClassDecorators(node, ctx) {
388
- const owner = { param: paramFor(node.name?.text), entity: node };
231
+ const owner = ownerOf(node);
389
232
  for (const decorator of decoratorsOf(node)) {
390
233
  if (!ts.isCallExpression(decorator.expression)) {
391
234
  continue;
392
235
  }
393
236
  const [first, second] = decorator.expression.arguments;
394
237
  const name = decoratorName(decorator);
238
+ const options = objectLiteral(first);
395
239
  if (name === 'Index') {
396
240
  rewriteIndex(first, second, owner, decorator, "'@Index' columns", ctx);
397
241
  }
398
242
  else if (name === 'Filter' && second) {
399
243
  renameOption(optionsOf(second), 'condition', 'where', decorator, ctx);
400
244
  }
401
- else if (name === 'Entity' && first && ts.isObjectLiteralExpression(first)) {
402
- rewriteEntityOptions(first, owner, ctx);
245
+ else if (name === 'Entity' && options) {
246
+ rewriteEntityOptions(options, owner, ctx);
403
247
  }
404
248
  }
405
249
  }
@@ -409,18 +253,20 @@ function rewriteClassDecorators(node, ctx) {
409
253
  * Only uql's shapes, a class and a literal options object, are read.
410
254
  */
411
255
  function rewriteDefineCall(call, ctx) {
412
- const name = ts.isIdentifier(call.expression) ? call.expression.text : undefined;
256
+ const name = identifierText(call.expression);
413
257
  const [entity = call, second, third] = call.arguments;
414
- const owner = { param: paramFor(ts.isIdentifier(entity) ? entity.text : undefined), entity };
415
- if (name === 'defineEntity' && second && ts.isObjectLiteralExpression(second)) {
416
- rewriteEntityOptions(second, owner, ctx);
258
+ const owner = ownerOf(entity);
259
+ const options = objectLiteral(second);
260
+ const relation = objectLiteral(third);
261
+ if (name === 'defineEntity' && options) {
262
+ rewriteEntityOptions(options, owner, ctx);
417
263
  }
418
- else if (name === 'defineIndex' && second && ts.isObjectLiteralExpression(second)) {
419
- rewriteIndexOptions(second, owner, ctx);
264
+ else if (name === 'defineIndex' && options) {
265
+ rewriteIndexOptions(options, owner, ctx);
420
266
  }
421
- else if (name === 'defineRelation' && third && ts.isObjectLiteralExpression(third)) {
422
- rewriteRelationOptions(third, owner.param, ctx);
423
- addForeignKeyReference(third, second && ts.isStringLiteralLike(second) ? second.text : undefined, owner, ctx);
267
+ else if (name === 'defineRelation' && relation) {
268
+ rewriteRelationOptions(relation, owner.param, ctx);
269
+ addForeignKeyReference(relation, second && ts.isStringLiteralLike(second) ? second.text : undefined, owner, ctx);
424
270
  }
425
271
  else if (name === 'defineFilter' && third) {
426
272
  renameOption(optionsOf(third), 'condition', 'where', call, ctx);
@@ -446,11 +292,10 @@ function rewriteEntityOptions(options, owner, ctx) {
446
292
  for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
447
293
  rewriteKeyList(hook.initializer, owner.param, hook, 'a hook list', ctx);
448
294
  }
449
- const declared = fields && ts.isObjectLiteralExpression(fields) ? fields : undefined;
450
295
  for (const relation of objectProperties(propertyValue(options, 'relations'))) {
451
296
  if (ts.isObjectLiteralExpression(relation.initializer)) {
452
297
  rewriteRelationOptions(relation.initializer, owner.param, ctx);
453
- addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx, declared);
298
+ addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx, objectLiteral(fields));
454
299
  }
455
300
  }
456
301
  }
@@ -462,7 +307,7 @@ function rewriteIndexOptions(index, owner, ctx) {
462
307
  * `include` read off the entity's refs, its `where` written as {@link rewriteIndexWhere} writes it, its SQL noted.
463
308
  */
464
309
  function rewriteIndex(columns, options, owner, node, what, ctx) {
465
- const literal = options && ts.isObjectLiteralExpression(options) ? options : undefined;
310
+ const literal = objectLiteral(options);
466
311
  rewriteKeyList(columns, owner.param, node, what, ctx);
467
312
  rewriteKeyList(literal && propertyValue(literal, 'include'), owner.param, node, "'include'", ctx);
468
313
  rewriteIndexWhere(literal, ctx);
@@ -470,10 +315,7 @@ function rewriteIndex(columns, options, owner, node, what, ctx) {
470
315
  noteHandNamedColumns(sql, owner, ctx);
471
316
  }
472
317
  }
473
- /**
474
- * A partial-index `where` string as `raw`, on an entity and in the migration builder alike. A string it
475
- * cannot write as `raw` is reported.
476
- */
318
+ /** A partial-index `where` string as `raw`, on an entity and in the migration builder alike, or reported. */
477
319
  function rewriteIndexWhere(options, ctx) {
478
320
  const where = options && propertyValue(options, 'where');
479
321
  if (!where || !isStringTyped(ctx.checker.getTypeAtLocation(where))) {
@@ -485,82 +327,78 @@ function rewriteIndexWhere(options, ctx) {
485
327
  ctx.imports.set('raw', 'the raw`...`');
486
328
  }
487
329
  else {
488
- ctx.unresolved.push(`${ctx.describe(where)}: write the partial-index 'where' as raw\`...\` or a predicate; this one could not be read`);
330
+ ctx.report(where, "write the partial-index 'where' as raw`...` or a predicate; this one could not be read");
489
331
  }
490
332
  }
491
333
  /** Whether every value `type` admits is a string. */
492
334
  function isStringTyped(type) {
493
335
  return type.isUnion() ? type.types.every(isStringTyped) : Boolean(type.flags & ts.TypeFlags.StringLike);
494
336
  }
337
+ /** The name of the method `call` calls where uql-orm declares it, so a `run` of another library's is left alone. */
338
+ function uqlMethod(call, ctx) {
339
+ const callee = call.expression;
340
+ if (!ts.isPropertyAccessExpression(callee)) {
341
+ return undefined;
342
+ }
343
+ const declarations = ctx.checker.getSymbolAtLocation(callee.name)?.declarations ?? [];
344
+ const declared = declarations.some((it) => it.getSourceFile().fileName.includes('/node_modules/uql-orm/'));
345
+ return declared ? callee.name.text : undefined;
346
+ }
347
+ /** A uql method whose shape or meaning changed: a querier's `run`/`all`, and `Migrator`'s `pending`, `executed`, `down`. */
348
+ function rewriteMethodCall(call, ctx) {
349
+ const method = uqlMethod(call, ctx);
350
+ if (method === 'run' || method === 'all') {
351
+ rewriteSqlCall(call, method, ctx);
352
+ }
353
+ else if (method === 'pending' || method === 'executed') {
354
+ ctx.report(call, `'${method}()' was removed; read \`(await migrator.status()).${method}\``);
355
+ }
356
+ else if (method === 'down' && !call.arguments.length) {
357
+ ctx.note(call, 'down() with no options reverts only the last migration now; pass `{ step: Infinity }` to revert them all');
358
+ }
359
+ }
495
360
  /**
496
361
  * 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.
362
+ * `run`...``, its values interpolated where {@link statementTemplate} can place them, and SQL built at run
363
+ * time `raw.text`, which splices as the string did. Values it cannot place are reported.
498
364
  */
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
- }
365
+ function rewriteSqlCall(call, method, ctx) {
504
366
  const [sql, ...values] = call.arguments;
505
367
  if (!sql || !isStringTyped(ctx.checker.getTypeAtLocation(sql))) {
506
368
  return;
507
369
  }
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`);
370
+ const template = statementTemplate(sql, values);
371
+ if (template) {
372
+ const typeArguments = call.typeArguments ? `<${call.typeArguments.map((type) => type.getText()).join(', ')}>` : '';
373
+ ctx.edits.push(replaced(call, `${call.expression.getText()}${typeArguments}${template}`));
510
374
  return;
511
375
  }
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)}`));
376
+ if (values.length) {
377
+ ctx.report(call, `${method}() takes one raw statement: write the values into it, raw\`... \${value}\`, which binds them`);
515
378
  return;
516
379
  }
517
380
  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`);
381
+ ctx.note(call, 'the values in this template are spliced into the SQL; write it as raw`...` to bind them');
519
382
  }
520
383
  ctx.edits.push(replaced(sql, `raw.text(${sql.getText()})`));
521
384
  ctx.imports.set('raw', 'the raw.text(...)');
522
385
  }
523
- const SQL_METHODS = new Set(['run', 'all']);
524
386
  /** `const { id } = await q.upsertOne(...)`, which resolves to the id now, as the id: other keys are reported. */
525
387
  function rewriteUpsertResult(declaration, ctx) {
526
388
  const { name, initializer } = declaration;
527
389
  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)) {
390
+ if (!ts.isObjectBindingPattern(name) || !call || !ts.isCallExpression(call) || uqlMethod(call, ctx) !== 'upsertOne') {
534
391
  return;
535
392
  }
536
393
  const [element] = name.elements;
537
394
  const key = element && (element.propertyName ?? element.name);
538
- if (name.elements.length === 1 && key && ts.isIdentifier(key) && key.text === 'id' && ts.isIdentifier(element.name)) {
395
+ if (name.elements.length === 1 && key && identifierText(key) === 'id' && ts.isIdentifier(element.name)) {
539
396
  ctx.edits.push(replaced(name, element.name.text));
540
397
  }
541
398
  else {
542
- ctx.unresolved.push(`${ctx.describe(declaration)}: upsertOne() resolves to the id; 'created' and 'changes' are gone`);
399
+ ctx.report(declaration, "upsertOne() resolves to the id; 'created' and 'changes' are gone");
543
400
  }
544
401
  }
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
- }
564
402
  /** The migration builder's index methods, each with the position of the options it takes. */
565
403
  const BUILDER_INDEX_METHODS = new Map([
566
404
  ['index', 1],
@@ -568,22 +406,22 @@ const BUILDER_INDEX_METHODS = new Map([
568
406
  ['addIndex', 1],
569
407
  ['createIndex', 2],
570
408
  ]);
571
- /** The migration builder's `expr` helpers whose value an entity imports under the same name. */
572
- const EXPR_VALUES = new Set(['currentDate', 'currentTime', 'uuid', 'uuidv7']);
409
+ /** The migration builder's `expr` helpers an entity imports as values under the same name, `now` renamed. */
410
+ const EXPR_VALUES = new Map([
411
+ ['now', 'currentTimestamp'],
412
+ ...['currentDate', 'currentTime', 'uuid', 'uuidv7'].map((name) => [name, name]),
413
+ ]);
573
414
  /**
574
415
  * Rewrites the migration builder's `expr.<helper>()` default as the value or `raw` an entity declares, or reports
575
416
  * one with no rewrite. Called only where the file imports `expr` from uql, so a variable of its own is left alone.
576
417
  */
577
418
  function rewriteExprCall(call, ctx) {
578
419
  const callee = call.expression;
579
- if (!ts.isPropertyAccessExpression(callee) ||
580
- !ts.isIdentifier(callee.expression) ||
581
- callee.expression.text !== 'expr') {
420
+ if (!ts.isPropertyAccessExpression(callee) || identifierText(callee.expression) !== 'expr') {
582
421
  return;
583
422
  }
584
423
  const helper = callee.name.text;
585
- // The one renamed: an entity's clock is `currentTimestamp`.
586
- const value = helper === 'now' ? 'currentTimestamp' : EXPR_VALUES.has(helper) ? helper : undefined;
424
+ const value = EXPR_VALUES.get(helper);
587
425
  const [sql] = call.arguments;
588
426
  if (value) {
589
427
  ctx.edits.push(replaced(call, value));
@@ -594,7 +432,7 @@ function rewriteExprCall(call, ctx) {
594
432
  ctx.imports.set('raw', 'the raw`...`');
595
433
  }
596
434
  else {
597
- ctx.unresolved.push(`${ctx.describe(call)}: expr.${helper}() is gone: declare the column with m.raw(...), or use a stamp on the entity`);
435
+ ctx.report(call, `expr.${helper}() is gone: declare the column with m.raw(...), or use a stamp on the entity`);
598
436
  return;
599
437
  }
600
438
  ctx.rewritten.expr += 1;
@@ -604,11 +442,9 @@ function rewriteBuilderCall(call, ctx) {
604
442
  const at = ts.isPropertyAccessExpression(call.expression)
605
443
  ? BUILDER_INDEX_METHODS.get(call.expression.name.text)
606
444
  : undefined;
607
- if (at === undefined) {
608
- return;
445
+ if (at !== undefined) {
446
+ rewriteIndexWhere(objectLiteral(call.arguments[at]), ctx);
609
447
  }
610
- const options = call.arguments[at];
611
- rewriteIndexWhere(options && ts.isObjectLiteralExpression(options) ? options : undefined, ctx);
612
448
  }
613
449
  const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
614
450
  /**
@@ -618,22 +454,18 @@ const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
618
454
  */
619
455
  function rewriteUpsertCall(call, ctx) {
620
456
  const callee = call.expression;
621
- const name = ts.isPropertyAccessExpression(callee) ? callee.name : callee;
457
+ const name = identifierText(ts.isPropertyAccessExpression(callee) ? callee.name : callee);
622
458
  const options = call.arguments[3];
623
459
  const update = options && upsertUpdate(options);
624
- if (ts.isIdentifier(name) && UPSERT_CALLS.has(name.text) && options && update) {
460
+ if (name && UPSERT_CALLS.has(name) && update) {
625
461
  ctx.edits.push(replaced(options, update.getText()));
626
462
  }
627
463
  }
628
464
  /** The value of options holding `update` alone, `{ update: value }` or `{ update }`. */
629
465
  function upsertUpdate(options) {
630
- const [property] = ts.isObjectLiteralExpression(options) && options.properties.length === 1 ? options.properties : [];
631
- if (property && ts.isPropertyAssignment(property) && propertyKey(property.name) === 'update') {
632
- return property.initializer;
633
- }
634
- return property && ts.isShorthandPropertyAssignment(property) && property.name.text === 'update'
635
- ? property.name
636
- : undefined;
466
+ const literal = objectLiteral(options);
467
+ const property = literal?.properties.length === 1 ? namedProperty(literal, 'update') : undefined;
468
+ return property && (ts.isPropertyAssignment(property) ? property.initializer : property.name);
637
469
  }
638
470
  /**
639
471
  * `owner` is the declaring class's parameter and `target` the related one's, read off the `entity`
@@ -681,7 +513,7 @@ function isTextSearch(node) {
681
513
  }
682
514
  function rewriteAggregate(agg, ctx) {
683
515
  if (ts.isObjectLiteralExpression(agg.parent) && propertyValue(agg.parent, '$select')) {
684
- ctx.unresolved.push(`${ctx.describe(agg)}: merge '$agg' into the '$select' beside it`);
516
+ ctx.report(agg, "merge '$agg' into the '$select' beside it");
685
517
  return;
686
518
  }
687
519
  ctx.edits.push(replaced(agg.name, '$select'));
@@ -694,29 +526,15 @@ function rewriteAggregate(agg, ctx) {
694
526
  }
695
527
  }
696
528
  }
697
- /** The property assignments of an object literal, or none where `node` is not one. */
698
- function objectProperties(node) {
699
- return node && ts.isObjectLiteralExpression(node) ? node.properties.filter(ts.isPropertyAssignment) : [];
700
- }
701
- /** The elements of an array literal, or none where `node` is not one. */
702
- function arrayElements(node) {
703
- return node && ts.isArrayLiteralExpression(node) ? node.elements : [];
704
- }
705
529
  /**
706
- * Drops `declare` from a decorated field. A `declare` field emits nothing, so the standard spec has
707
- * nothing to decorate and rejects it ("Decorators are not valid here"). Legacy decorators tolerated it
708
- * because they only ever reached the class through `target.constructor`.
530
+ * Drops `declare` from a decorated field, with the whitespace after it: a `declare` field emits nothing, so
531
+ * the standard spec has nothing to decorate and rejects it ("Decorators are not valid here").
709
532
  */
710
533
  function dropDeclare(node, ctx) {
711
534
  const modifier = node.modifiers?.find((m) => m.kind === ts.SyntaxKind.DeclareKeyword);
712
535
  if (modifier) {
713
- // Through the whitespace that followed it, rather than a fixed one character: two spaces or a line
714
- // break left the rest behind.
715
- const text = node.getSourceFile().text;
716
- let end = modifier.getEnd();
717
- while (/\s/.test(text[end])) {
718
- end += 1;
719
- }
536
+ const rest = node.getSourceFile().text.slice(modifier.getEnd());
537
+ const end = modifier.getEnd() + rest.length - rest.trimStart().length;
720
538
  ctx.edits.push({ start: modifier.getStart(), end, text: '' });
721
539
  }
722
540
  }
@@ -729,85 +547,23 @@ function unwrapRelationAlias(node, ctx) {
729
547
  const declared = node.type;
730
548
  if (declared &&
731
549
  ts.isTypeReferenceNode(declared) &&
732
- ts.isIdentifier(declared.typeName) &&
733
- declared.typeName.text === 'Relation' &&
550
+ identifierText(declared.typeName) === 'Relation' &&
734
551
  declared.typeArguments?.length === 1) {
735
552
  ctx.edits.push(replaced(declared, declared.typeArguments[0].getText()));
736
553
  ctx.rewritten.Relation += 1;
737
554
  }
738
555
  }
739
- /** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
740
- function removeStatement(node) {
741
- const source = node.getSourceFile();
742
- const line = source.getLineAndCharacterOfPosition(node.getEnd()).line;
743
- const nextLineStart = source.getLineStarts()[line + 1];
744
- return { start: node.getStart(), end: nextLineStart ?? node.getEnd(), text: '' };
745
- }
746
- /**
747
- * Names `uql-orm` no longer exports and that the codemod does remove: `Relation` once every usage was
748
- * unwrapped, and `expr` once every default was rewritten. A removed *decorator* keeps its import on purpose (see
749
- * {@link REMOVED_DECORATORS}).
750
- */
751
- function deadImportNames(source, ctx) {
752
- const imported = uqlImports(source, true);
753
- // Every use of the name the file imports, the places no rewrite reaches included.
754
- const uses = (name) => {
755
- const element = imported.find((specifier) => importedName(specifier) === name);
756
- return element ? usesOf(source, element.name, ctx.checker).length : 0;
757
- };
758
- const { rewritten } = ctx;
759
- const relations = uses('Relation');
760
- if (relations > rewritten.Relation) {
761
- ctx.unresolved.push(`${source.fileName}: ${relations - rewritten.Relation} 'Relation<T>' reference(s) are somewhere this codemod ` +
762
- 'does not rewrite; unwrap them and drop the import by hand');
763
- }
764
- const names = ['Relation', 'expr'];
765
- return new Set(names.filter((name) => rewritten[name] > 0 && rewritten[name] === uses(name)));
766
- }
767
- /**
768
- * Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and each `dead` name
769
- * from the imports {@link renameExports} left as written, having left the dead names out of the rest.
770
- */
771
- function dropDeadImports(source, dead, rewritten, ctx) {
772
- for (const statement of source.statements) {
773
- if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
774
- continue;
775
- }
776
- if (statement.moduleSpecifier.text === 'reflect-metadata' && !statement.importClause) {
777
- ctx.edits.push(removeStatement(statement));
778
- continue;
779
- }
780
- const named = statement.importClause?.namedBindings;
781
- if (!named || !ts.isNamedImports(named) || rewritten.has(statement)) {
782
- continue;
783
- }
784
- const dropped = named.elements.filter((element) => dead.has(element.name.text));
785
- if (dropped.length === named.elements.length && !statement.importClause?.name) {
786
- ctx.edits.push(removeStatement(statement));
787
- continue;
788
- }
789
- for (const element of dropped) {
790
- ctx.edits.push(removeFromList(named.elements, element, source));
791
- }
792
- }
793
- }
794
556
  /** The names a key is read from without help, mirroring `NamedIdKey` in `uql-orm`. */
795
557
  const CONVENTIONAL_ID_NAMES = new Set(['id', '_id', 'uuid']);
796
558
  /** Whether the member is an `[idKey]?: ...` brand, matched by the name it is written under. */
797
559
  function isIdKeyBrand(member) {
798
560
  return (ts.isPropertyDeclaration(member) &&
799
561
  ts.isComputedPropertyName(member.name) &&
800
- ts.isIdentifier(member.name.expression) &&
801
- member.name.expression.text === 'idKey');
562
+ identifierText(member.name.expression) === 'idKey');
802
563
  }
803
564
  /**
804
- * Names the class's key with the `idKey` brand, where a conventional name does not already.
805
- *
806
- * Only the class's own `@Id` properties are read, which is also what makes an overriding key right:
807
- * a subclass that replaces an inherited `id` needs the brand precisely because the inherited name
808
- * would otherwise be taken for the key.
809
- *
810
- * One written makes `idKey` a value the file has to import, which is recorded for {@link addImports}.
565
+ * Names the class's key with the `idKey` brand where a conventional name does not. Only the class's own `@Id`
566
+ * properties are read: a subclass replacing an inherited `id` needs the brand, or that name is taken for the key.
811
567
  */
812
568
  function brandIdKey(node, ctx) {
813
569
  const ids = node.members.filter(isIdProperty);
@@ -816,69 +572,43 @@ function brandIdKey(node, ctx) {
816
572
  }
817
573
  const names = ids.map((m) => propertyKey(m.name)).filter((name) => name !== undefined);
818
574
  if (names.length !== ids.length) {
819
- ctx.unresolved.push(`${ctx.describe(node)}: a key written as a computed name; add the 'idKey' brand by hand`);
575
+ ctx.report(node, "a key written as a computed name; add the 'idKey' brand by hand");
820
576
  return;
821
577
  }
822
578
  if (names.length === 1 && CONVENTIONAL_ID_NAMES.has(names[0])) {
823
579
  return;
824
580
  }
825
- const indent = ' '.repeat(node.getSourceFile().getLineAndCharacterOfPosition(ids[0].getStart()).character);
826
- const brand = names.map(quoted).join(' | ');
827
581
  const start = node.members.pos;
828
- ctx.edits.push({ start, end: start, text: `\n${indent}[idKey]?: ${brand};` });
582
+ ctx.edits.push({ start, end: start, text: `\n${indentOf(ids[0])}[idKey]?: ${names.map(quoted).join(' | ')};` });
829
583
  ctx.imports.set('idKey', 'the brand(s)');
830
584
  }
831
585
  /**
832
- * Imports what the rewrites wrote (`idKey`, `raw`) into the file's own `uql-orm` import. Reported instead
833
- * where there is none: the package may be imported under a path this codemod does not recognise.
586
+ * Everything the standard spec needs written onto one decorated property, and the options renamed since.
587
+ * Options it cannot read may still hold `virtual`, so they are reported where they mention it.
834
588
  */
835
- function addImports(source, rewritten, ctx) {
836
- const imported = new Set(uqlImports(source).map(importedName));
837
- const missing = [...ctx.imports].filter(([name]) => !imported.has(name));
838
- if (!missing.length) {
839
- return;
840
- }
841
- const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration));
842
- if (!anchor) {
843
- for (const [name, what] of missing) {
844
- ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
845
- }
846
- return;
847
- }
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('')));
853
- }
854
- /** Everything the standard spec needs written onto one decorated property, and the options renamed since. */
855
589
  function rewriteProperty(node, ctx) {
856
- const owner = { param: paramFor(node.parent.name?.text), entity: node.parent };
590
+ const owner = ownerOf(node.parent);
857
591
  const decorators = decoratorsOf(node);
858
592
  for (const decorator of decorators) {
859
- const name = decoratorName(decorator);
860
- if (!name) {
861
- continue;
862
- }
593
+ const name = decoratorName(decorator) ?? '';
863
594
  const options = decoratorOptions(decorator);
864
595
  if (FIELD_DECORATORS.has(name)) {
865
596
  if (name === 'Field') {
866
597
  admitNull(options, node, ctx);
867
598
  }
868
599
  addFieldType(decorator, node, ctx);
869
- // Options it cannot read may still hold `virtual`, so they are reported where they mention it.
870
600
  if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
871
- ctx.unresolved.push(`${ctx.describe(node)}: ${options.reason}`);
601
+ ctx.report(node, options.reason);
872
602
  }
873
603
  renameSqlOption(options, 'virtual', 'computed', node, owner, ctx);
874
604
  }
875
605
  if (RELATION_DECORATORS.has(name)) {
876
606
  addRelationEntity(decorator, node, ctx);
877
- }
878
- if (RELATION_DECORATORS.has(name) && options.kind === 'literal') {
879
- const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
880
- rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
881
- if (TO_ONE_DECORATORS.has(name)) {
607
+ if (options.kind === 'literal') {
608
+ const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
609
+ rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
610
+ }
611
+ if (options.kind === 'literal' && TO_ONE_DECORATORS.has(name)) {
882
612
  addForeignKeyReference(options.node, propertyKey(node.name), owner, ctx, node);
883
613
  }
884
614
  }
@@ -889,34 +619,26 @@ function rewriteProperty(node, ctx) {
889
619
  unwrapRelationAlias(node, ctx);
890
620
  }
891
621
  /**
892
- * A column holds `null` unless `nullable: false` says otherwise, and a read hydrates one, so the property
893
- * says so too. A key is NOT NULL on every engine, and a relation aggregate is typed by the aggregate,
894
- * which declares its own `null` or none, so neither is touched.
622
+ * A column holds `null` unless `nullable: false` says otherwise, so the property says so too. A key is NOT NULL
623
+ * everywhere and a relation aggregate types its own, so neither is touched; nor are options it cannot read,
624
+ * which may state `nullable` and are reported by {@link addFieldType} already.
895
625
  */
896
626
  function admitNull(options, node, ctx) {
897
- // Options it cannot read may state `nullable` themselves, and appending to the property would then
898
- // contradict the column. `addFieldType` already reports the same options, so this adds no second note.
899
- if (options.kind === 'opaque' || !ctx.strictNullChecks) {
900
- return;
901
- }
902
- if (declaresNotNull(options) ||
627
+ if (options.kind === 'opaque' ||
628
+ !ctx.strictNullChecks ||
629
+ declaresNotNull(options) ||
903
630
  findProperty(options, 'isId') ||
904
631
  (findProperty(options, 'computed') && !findProperty(options, 'type'))) {
905
632
  return;
906
633
  }
907
634
  if (!node.type) {
908
- ctx.notes.push(`${ctx.describe(node)}: declare '${propertyKey(node.name)}' as a type that admits 'null', which the column holds`);
909
- return;
635
+ ctx.note(node, `declare '${propertyKey(node.name)}' as a type that admits 'null', which the column holds`);
910
636
  }
911
- if (admitsNull(node.type)) {
912
- return;
637
+ else if (!admitsNull(node.type)) {
638
+ ctx.edits.push(appended(node.type, ' | null'));
913
639
  }
914
- ctx.edits.push(appended(node.type, ' | null'));
915
640
  }
916
- /**
917
- * The same null rule where `defineEntity` names the field: the property is on the class the call takes,
918
- * so the declaration is found there rather than under a decorator.
919
- */
641
+ /** The same null rule where `defineEntity` names the field, on the property of the class the call takes. */
920
642
  function admitNullOnDefined(field, owner, ctx) {
921
643
  const key = propertyKey(field.name);
922
644
  const declaration = key
@@ -949,135 +671,23 @@ function reportRemovedDecorators(node, ctx) {
949
671
  const name = decoratorName(decorator);
950
672
  const advice = name && REMOVED_DECORATORS.get(name);
951
673
  if (advice) {
952
- ctx.unresolved.push(`${ctx.describe(decorator)}: '@${name}()' was removed; ${advice}`);
953
- }
954
- }
955
- }
956
- /** Names an export that no longer exists, where it is imported from the package or one of its entries. */
957
- function reportRemovedExports(source, ctx) {
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
- }
674
+ ctx.report(decorator, `'@${name}()' was removed; ${advice}`);
977
675
  }
978
676
  }
979
677
  }
980
- /**
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`
983
- * one. Returns the rewritten imports, which nothing else may edit.
984
- */
985
- function renameExports(source, dead, ctx) {
986
- const imports = uqlImportDeclarations(source, true);
987
- const kept = imports.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)));
988
- const bound = new Set(kept.map((element) => element.name.text));
989
- const rewritten = new Set();
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))) {
993
- continue;
994
- }
995
- const byEntry = new Map([[entry, []]]);
996
- for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
997
- const rename = renamedTo(element, entry);
998
- const local = element.propertyName || !rename ? element.name.text : rename.to;
999
- if (rename && !element.propertyName) {
1000
- ctx.edits.push(...usesOf(source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
1001
- }
1002
- if (rename && bound.has(local)) {
1003
- continue;
1004
- }
1005
- bound.add(local);
1006
- const target = rename?.from ?? entry;
1007
- const text = rename
1008
- ? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
1009
- : element.getText();
1010
- byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
1011
- }
1012
- const typeOnly = declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
1013
- const keyword = typeOnly ? 'import type' : 'import';
1014
- const quote = declaration.moduleSpecifier.getText()[0];
1015
- const statements = [...byEntry]
1016
- .filter(([, names]) => names.length)
1017
- .map(([from, names]) => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`);
1018
- ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
1019
- rewritten.add(declaration);
1020
- }
1021
- return rewritten;
1022
- }
1023
- /** The rename `element` still needs, none where it already names the export at its entry. */
1024
- function renamedTo(element, entry) {
1025
- const name = importedName(element);
1026
- const rename = RENAMED_EXPORTS.get(`${entry}#${name}`) ?? RENAMED_EXPORTS.get(name);
1027
- return rename?.to === name && rename.from === entry ? undefined : rename;
1028
- }
1029
- /** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
1030
- function usesOf(source, name, checker) {
1031
- const symbol = checker.getSymbolAtLocation(name);
1032
- const uses = [];
1033
- const visit = (node) => {
1034
- if (ts.isIdentifier(node) && node !== name && node.text === name.text) {
1035
- if (symbol && checker.getSymbolAtLocation(node) === symbol) {
1036
- uses.push(node);
1037
- }
1038
- }
1039
- ts.forEachChild(node, visit);
1040
- };
1041
- visit(source);
1042
- return uses;
1043
- }
1044
- /**
1045
- * The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
1046
- * for what is read by name only: `idKey` written into one would land on an entry that may not export it.
1047
- */
1048
- function uqlImportDeclarations(source, entries = false) {
1049
- return source.statements.flatMap((declaration) => {
1050
- if (!ts.isImportDeclaration(declaration) || !ts.isStringLiteral(declaration.moduleSpecifier)) {
1051
- return [];
1052
- }
1053
- const entry = declaration.moduleSpecifier.text;
1054
- if (entry !== 'uql-orm' && !(entries && entry.startsWith('uql-orm/'))) {
1055
- return [];
1056
- }
1057
- const bindings = declaration.importClause?.namedBindings;
1058
- return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
1059
- });
1060
- }
1061
- function uqlImports(source, entries = false) {
1062
- return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
1063
- }
1064
- /** The name an import specifier brings in, which is the original one where it was renamed. */
1065
- function importedName(element) {
1066
- return (element.propertyName ?? element.name).text;
1067
- }
1068
678
  /**
1069
679
  * Rewrites `raw('sql')` into the tagged template, and a second alias argument into `.as()`, the callback
1070
680
  * form's included. A computed string is left alone: a template cannot be built from a value not known here.
1071
681
  */
1072
- function rewriteRawCall(node, ctx, source) {
1073
- if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== 'raw') {
682
+ function rewriteRawCall(node, ctx) {
683
+ if (!ts.isCallExpression(node) || identifierText(node.expression) !== 'raw') {
1074
684
  return;
1075
685
  }
1076
686
  const [expression, alias] = node.arguments;
1077
687
  if (!expression || node.arguments.length > 2) {
1078
688
  return;
1079
689
  }
1080
- const suffix = alias ? `.as(${alias.getText(source)})` : '';
690
+ const suffix = alias ? `.as(${alias.getText()})` : '';
1081
691
  if (ts.isStringLiteral(expression)) {
1082
692
  ctx.edits.push(replaced(node, `${rawTag(expression.text)}${suffix}`));
1083
693
  }
@@ -1086,170 +696,23 @@ function rewriteRawCall(node, ctx, source) {
1086
696
  }
1087
697
  }
1088
698
  /**
1089
- * Rewrites each `col('x')` into the ref it names - `product.x` in a `computed` callback, `refs(Item).x` in a
1090
- * statement - where the entity its SQL renders against, and a member named `x`, can be told from here. Every
1091
- * other use is reported. Returns `col` as a dead import once no use is left.
1092
- */
1093
- function rewriteColumnRefs(source, ctx) {
1094
- const element = uqlImports(source).find((use) => importedName(use) === 'col');
1095
- if (!element) {
1096
- return [];
1097
- }
1098
- const computed = new Map();
1099
- const unread = [];
1100
- for (const use of usesOf(source, element.name, ctx.checker)) {
1101
- const call = ts.isCallExpression(use.parent) && use.parent.expression === use ? use.parent : undefined;
1102
- const sql = call && ts.findAncestor(call, isRaw);
1103
- const option = sql?.parent;
1104
- const owner = option && ts.isPropertyAssignment(option) && isComputedOption(option) ? computedOwner(option) : undefined;
1105
- const edit = call && !owner ? statementRefEdit(call, ctx) : undefined;
1106
- if (call && sql && ts.isTaggedTemplateExpression(sql) && owner) {
1107
- computed.set(sql, { owner, calls: [...(computed.get(sql)?.calls ?? []), call] });
1108
- }
1109
- else if (edit) {
1110
- ctx.edits.push(edit);
1111
- ctx.imports.set('refs', 'the refs(...)');
1112
- }
1113
- else {
1114
- unread.push(use);
1115
- }
1116
- }
1117
- for (const [sql, { owner, calls }] of computed) {
1118
- const edits = computedRefEdits(sql, calls, owner, ctx);
1119
- if (edits) {
1120
- ctx.edits.push(...edits);
1121
- }
1122
- else {
1123
- unread.push(...calls);
1124
- }
1125
- }
1126
- for (const node of unread.sort((a, b) => a.getStart() - b.getStart())) {
1127
- ctx.unresolved.push(`${ctx.describe(node)}: 'col' was removed; read the column off refs(Entity), which the codemod could not tell here`);
1128
- }
1129
- return unread.length ? [] : ['col'];
1130
- }
1131
- function isComputedOption(option) {
1132
- const key = propertyKey(option.name);
1133
- return key === 'computed' || key === 'virtual';
1134
- }
1135
- /** The entity a field's `computed` SQL is declared on: the decorated property's class, or `defineEntity`'s first argument. */
1136
- function computedOwner(option) {
1137
- const holder = ts.findAncestor(option, (node) => ts.isPropertyDeclaration(node) ||
1138
- (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'defineEntity'));
1139
- if (holder && ts.isPropertyDeclaration(holder)) {
1140
- return { param: paramFor(holder.parent.name?.text), entity: holder.parent };
1141
- }
1142
- const [entity] = holder?.arguments ?? [];
1143
- return entity && { param: paramFor(ts.isIdentifier(entity) ? entity.text : undefined), entity };
1144
- }
1145
- /** `(product) => ` before a `computed` raw, and `product.x` for each `col('x')` in it, where every one names a member. */
1146
- function computedRefEdits(sql, calls, owner, ctx) {
1147
- const members = memberNames(owner.entity, ctx.checker);
1148
- const names = calls.map((call) => columnMember(call, members));
1149
- if (mentions(sql, owner.param) || !names.every((name) => name !== undefined)) {
1150
- return undefined;
1151
- }
1152
- return [
1153
- inserted(sql, `(${owner.param}) => `),
1154
- ...calls.map((call, at) => replaced(call, memberAccess(owner.param, names[at]))),
1155
- ];
1156
- }
1157
- /** `refs(Item).x` for a `col('x')` in a statement, where the entity its SQL renders against can be told. */
1158
- function statementRefEdit(call, ctx) {
1159
- const entity = statementEntity(call, ctx.checker);
1160
- const name = entity && columnMember(call, propertyNames(entity.type));
1161
- return entity && name !== undefined ? replaced(call, memberAccess(`refs(${entity.name})`, name)) : undefined;
1162
- }
1163
- /** The member a `col('x')` names, where its one argument is a literal naming one of `members`. */
1164
- function columnMember(call, members) {
1165
- const [name] = call.arguments;
1166
- return call.arguments.length === 1 && ts.isStringLiteralLike(name) && members.has(name.text) ? name.text : undefined;
1167
- }
1168
- /** Whether `node` reads a binding named `name`, which a callback parameter of that name would shadow. */
1169
- function mentions(node, name) {
1170
- if (ts.isIdentifier(node)) {
1171
- return node.text === name && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node);
1172
- }
1173
- return ts.forEachChild(node, (child) => mentions(child, name) || undefined) ?? false;
1174
- }
1175
- /**
1176
- * The entity a statement's SQL renders against at `node`: the one its query names, or a relation a `$populate`
1177
- * reads there. Nothing where a function or a relation filter in between opens a scope of its own.
699
+ * Rewrites one source file for the standard decorator spec. The builder's and querier's methods are known by
700
+ * name, so they are read only in a file importing uql-orm, and `expr` only where it is uql's.
1178
701
  */
1179
- function statementEntity(node, checker) {
1180
- const path = [];
1181
- for (let at = node; at.parent; at = at.parent) {
1182
- const { parent } = at;
1183
- if (ts.isFunctionLike(parent)) {
1184
- return undefined;
1185
- }
1186
- if (ts.isPropertyAssignment(parent)) {
1187
- path.unshift(propertyKey(parent.name) ?? '');
1188
- }
1189
- const queried = ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === at);
1190
- const entity = queried ? queriedEntity(parent, checker) : undefined;
1191
- if (entity) {
1192
- return entityAlong(entity, path, node, checker);
1193
- }
1194
- }
1195
- return undefined;
1196
- }
1197
- /** The entity a call queries: its first argument's class, or the `$entity` its query names. */
1198
- function queriedEntity(call, checker) {
1199
- const [first] = call.arguments;
1200
- const entity = first && ts.isObjectLiteralExpression(first) ? propertyValue(first, '$entity') : first;
1201
- const instance = entity && checker.getTypeAtLocation(entity).getConstructSignatures()[0]?.getReturnType();
1202
- return entity && ts.isIdentifier(entity) && instance ? { name: entity.text, type: instance } : undefined;
1203
- }
1204
- /** `entity` followed along `path`: into each relation a `$populate` names, and nowhere a relation filter opens. */
1205
- function entityAlong(entity, path, node, checker) {
1206
- let current = entity;
1207
- for (const [at, key] of path.entries()) {
1208
- const property = current.type.getProperty(key);
1209
- const related = property && relatedClass(property, node, checker);
1210
- if (!related) {
1211
- continue;
1212
- }
1213
- const inScope = checker.getSymbolsInScope(node, ts.SymbolFlags.Class).some(({ name }) => name === related.name);
1214
- if (path[at - 1] !== '$populate' || !inScope) {
1215
- return undefined;
1216
- }
1217
- current = { name: related.name, type: checker.getDeclaredTypeOfSymbol(related) };
1218
- }
1219
- return current;
1220
- }
1221
- /** The class a relation property points at, one or many, or nothing where the property holds no class. */
1222
- function relatedClass(property, node, checker) {
1223
- const type = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(property, node));
1224
- const symbol = (checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type).getSymbol();
1225
- return symbol && symbol.flags & ts.SymbolFlags.Class ? symbol : undefined;
1226
- }
1227
- /** Rewrites one source file for the standard decorator spec. */
1228
702
  export function transformFile(source, checker, options = { strictNullChecks: true }) {
1229
- const ctx = {
1230
- checker,
1231
- strictNullChecks: options.strictNullChecks,
1232
- edits: [],
1233
- unresolved: [],
1234
- notes: [],
1235
- imports: new Map(),
1236
- rewritten: { Relation: 0, expr: 0 },
1237
- describe: (node) => `${source.fileName}:${lineOf(node) + 1}`,
1238
- };
1239
- const lineOf = (node) => source.getLineAndCharacterOfPosition(node.getStart()).line;
1240
- const rewritesRaw = uqlImports(source).some((element) => importedName(element) === 'raw');
1241
- // The builder's methods are known by name alone, so they are read only in a file that imports uql-orm.
703
+ const ctx = createContext(source, checker, options.strictNullChecks);
704
+ const rewritesRaw = uqlImport(source, 'raw') !== undefined;
1242
705
  const importsUql = uqlImportDeclarations(source, true).length > 0;
1243
- const importsExpr = uqlImports(source, true).some((element) => importedName(element) === 'expr');
706
+ const importsExpr = uqlImport(source, 'expr', true) !== undefined;
1244
707
  const visit = (node) => {
1245
708
  reportRemovedDecorators(node, ctx);
1246
709
  if (rewritesRaw) {
1247
- rewriteRawCall(node, ctx, source);
710
+ rewriteRawCall(node, ctx);
1248
711
  }
1249
712
  if (ts.isPropertyDeclaration(node)) {
1250
713
  rewriteProperty(node, ctx);
1251
714
  }
1252
- if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
715
+ if (ts.isClassLike(node)) {
1253
716
  brandIdKey(node, ctx);
1254
717
  rewriteClassDecorators(node, ctx);
1255
718
  }
@@ -1258,8 +721,7 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
1258
721
  rewriteUpsertCall(node, ctx);
1259
722
  if (importsUql) {
1260
723
  rewriteBuilderCall(node, ctx);
1261
- rewriteSqlCall(node, ctx);
1262
- reportMigratorCall(node, ctx);
724
+ rewriteMethodCall(node, ctx);
1263
725
  }
1264
726
  if (importsExpr) {
1265
727
  rewriteExprCall(node, ctx);
@@ -1274,11 +736,11 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
1274
736
  ts.forEachChild(node, visit);
1275
737
  };
1276
738
  visit(source);
1277
- const dead = new Set([...deadImportNames(source, ctx), ...rewriteColumnRefs(source, ctx)]);
1278
- const rewritten = renameExports(source, dead, ctx);
1279
- reportRemovedExports(source, ctx);
1280
- dropDeadImports(source, dead, rewritten, ctx);
1281
- addImports(source, rewritten, ctx);
739
+ const dead = new Set([...deadImportNames(ctx), ...rewriteColumnRefs(ctx)]);
740
+ const rewritten = renameExports(dead, ctx);
741
+ reportRemovedExports(ctx);
742
+ dropDeadImports(dead, rewritten, ctx);
743
+ addImports(rewritten, ctx);
1282
744
  return {
1283
745
  fileName: source.fileName,
1284
746
  text: applyEdits(source.getFullText(), ctx.edits),