uql-codemod 0.46.0 → 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 } 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,93 +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/maria`)'],
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
- ]);
64
- /**
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.
67
- */
68
- const RENAMED_EXPORTS = new Map([
69
- ['QueryWhereMap', { to: 'QueryWhere' }],
70
- ['RelationKeyMap', { to: 'KeyMap' }],
71
- ['FilterCondition', { to: 'FilterWhere' }],
72
- ['PgDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
73
- ['NeonDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
74
- ['PgliteDialect', { to: 'PostgresDialect', from: 'uql-orm/postgres' }],
75
- ['CrdbQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
76
- ['NeonQuerier', { to: 'PgQuerier', from: 'uql-orm/postgres' }],
77
- ['MySql2Dialect', { to: 'MySqlDialect', from: 'uql-orm/mysql' }],
78
- ['MongodbNativeDialect', { to: 'MongoDialect', from: 'uql-orm/mongo' }],
79
- ['LibsqlQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
80
- ['TursoQuerier', { to: 'HranaQuerier', from: 'uql-orm/sqlite' }],
81
- ['TursoLocalQuerier', { to: 'SqliteQuerier', from: 'uql-orm/sqlite' }],
82
- ['TursoDatabase', { to: 'SqliteDatabase', from: 'uql-orm/sqlite' }],
83
- ['SqlMigrationModuleOptions', { to: 'MigrationModuleOptions' }],
84
- ['buildSqlQuerierMigrationModule', { to: 'buildMigrationModule' }],
85
- ['QueryDialect', { to: 'SqlQueryDialect' }],
86
- ['EngineFeatures', { to: 'DialectFeatures' }],
87
- ['KnownMigratorDialect', { to: 'DialectName', from: 'uql-orm' }],
88
- ['D1Preparer', { to: 'D1Queryable' }],
89
- ['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' }],
94
- ]);
95
- /** A node's decorators, or none where it is a kind that cannot carry them. */
96
- function decoratorsOf(node) {
97
- return ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
98
- }
99
- function decoratorName(node) {
100
- const call = ts.isCallExpression(node.expression) ? node.expression : undefined;
101
- const target = call?.expression ?? node.expression;
102
- return ts.isIdentifier(target) ? target.text : undefined;
103
- }
26
+ /** A decorator's options; `@Field` uncalled is opaque, since adding a call would guess it is the factory. */
104
27
  function decoratorOptions(node) {
105
28
  if (!ts.isCallExpression(node.expression)) {
106
- // `@Field` rather than `@Field()`: there is no argument list to write into, and adding one would mean
107
- // guessing that this is the decorator factory the codemod thinks it is.
108
29
  return { kind: 'opaque', reason: 'it is used without being called' };
109
30
  }
110
31
  const [first] = node.expression.arguments;
@@ -119,18 +40,9 @@ function optionsOf(value) {
119
40
  ? { kind: 'opaque', reason: 'its options object spreads another' }
120
41
  : { kind: 'literal', node: value };
121
42
  }
122
- /**
123
- * The property `name` in the options, whether its key is plain, quoted or a shorthand.
124
- *
125
- * All three forms, because both callers care about the option being *there*: one to leave an option
126
- * alone that is already stated, the other to rename it. Matching only the plain key inserted a second
127
- * `type` beside a quoted one, and left a shorthand `virtual` behind while reporting nothing.
128
- */
43
+ /** The option `name`, however its key is written: a stated option is left alone, and a renamed one moved. */
129
44
  function findProperty(options, name) {
130
- if (options.kind !== 'literal') {
131
- return undefined;
132
- }
133
- 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;
134
46
  }
135
47
  /**
136
48
  * Renames the option `from` to `to`. Only the key is replaced, so the value, its formatting and any comment
@@ -143,7 +55,7 @@ function renameOption(options, from, to, node, ctx) {
143
55
  return;
144
56
  }
145
57
  if (findProperty(options, to)) {
146
- ctx.unresolved.push(`${ctx.describe(node)}: gives both '${from}' and '${to}'; keep '${to}'.`);
58
+ ctx.report(node, `gives both '${from}' and '${to}'; keep '${to}'.`);
147
59
  return;
148
60
  }
149
61
  ctx.edits.push(replaced(property.name, ts.isShorthandPropertyAssignment(property) ? `${to}: ${from}` : to));
@@ -161,25 +73,12 @@ function noteHandNamedColumns(sql, owner, ctx) {
161
73
  if (!sql || !names.length) {
162
74
  return;
163
75
  }
164
- ctx.notes.push(`${ctx.describe(sql)}: SQL names ${names.map((name) => `'${name}'`).join(', ')} by hand, which a rename ` +
165
- `does not reach; read each off a callback's refs instead: (${owner.param}) => raw\`...\${${owner.param}.<member>}...\``);
166
- }
167
- /** The names of the members of the entity `node` declares or names. */
168
- function memberNames(node, checker) {
169
- return propertyNames(instanceTypeOf(node, checker));
170
- }
171
- /** The instance type of the entity `node` declares or names. */
172
- function instanceTypeOf(node, checker) {
173
- const type = checker.getTypeAtLocation(node);
174
- return type.getConstructSignatures()[0]?.getReturnType() ?? type;
175
- }
176
- function propertyNames(type) {
177
- return new Set(type.getProperties().map(({ name }) => name));
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>}...\``);
178
78
  }
179
- /** 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. */
180
80
  function insertOption(options, option) {
181
81
  if (options.kind === 'empty') {
182
- // `@Field()` -> `@Field({ ... })`: the argument list is empty, so this only ever inserts.
183
82
  const { arguments: args } = options.call;
184
83
  return { start: args.pos, end: args.end, text: `{ ${option} }` };
185
84
  }
@@ -187,11 +86,8 @@ function insertOption(options, option) {
187
86
  return first ? inserted(first, `${option}, `) : replaced(options.node, `{ ${option} }`);
188
87
  }
189
88
  /**
190
- * Writes one option the decorator can no longer infer at runtime, or records why it could not.
191
- *
192
- * Returns the property's type when an edit was written, so a caller can say something about what was
193
- * just decided. Nothing is written for options the codemod cannot read: replacing them would drop
194
- * 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.
195
91
  */
196
92
  function addInferredOption(decorator, node, ctx, spec) {
197
93
  const options = decoratorOptions(decorator);
@@ -199,34 +95,32 @@ function addInferredOption(decorator, node, ctx, spec) {
199
95
  return undefined;
200
96
  }
201
97
  if (options.kind === 'opaque') {
202
- ctx.unresolved.push(`${ctx.describe(node)}: cannot add '${spec.option}' because ${options.reason}`);
98
+ ctx.report(node, `cannot add '${spec.option}' because ${options.reason}`);
203
99
  return undefined;
204
100
  }
205
101
  const type = ctx.checker.getTypeAtLocation(node);
206
102
  const value = spec.infer(type);
207
103
  if (!value) {
208
- 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)}`);
209
105
  return undefined;
210
106
  }
211
107
  ctx.edits.push(insertOption(options, `${spec.option}: ${value}`));
212
108
  return type;
213
109
  }
214
- /** 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
+ */
215
115
  function addFieldType(decorator, node, ctx) {
216
116
  const type = addInferredOption(decorator, node, ctx, {
217
117
  option: 'type',
218
- // 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'],
118
+ satisfiedBy: ['type', 'references', 'computed'],
221
119
  infer: fieldTypeFor,
222
120
  });
223
- // A branded id type (`type UUID = `${string}-${string}-...``) erased to `String` under reflection, so
224
- // `String` is what keeps the existing column. It is often not what was wanted, though, and switching
225
- // to `'uuid'` changes the generated column, so say so rather than decide.
226
121
  if (type && isBrandedString(type)) {
227
- ctx.notes.push(`${ctx.describe(node)}: set 'type: ${fieldTypeFor(type)}' for ` +
228
- `${ctx.checker.typeToString(type)}, matching the previous behaviour. If the column should be ` +
229
- "'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.");
230
124
  }
231
125
  }
232
126
  /** Writes the `entity` getter relations can no longer infer. */
@@ -246,10 +140,9 @@ function addRelationEntity(decorator, node, ctx) {
246
140
  * is declared, or `defineEntity`'s `fields`. A column it cannot see declared is reported instead.
247
141
  */
248
142
  function addForeignKeyReference(relation, key, owner, ctx, site) {
249
- const options = { kind: 'literal', node: relation };
250
143
  const cardinality = propertyValue(relation, 'cardinality');
251
144
  const toOne = !cardinality || (ts.isStringLiteralLike(cardinality) && TO_ONE_CARDINALITIES.has(cardinality.text));
252
- const joined = ['mappedBy', 'through', 'references'].some((option) => findProperty(options, option));
145
+ const joined = ['mappedBy', 'through', 'references'].some((option) => namedProperty(relation, option));
253
146
  const last = relation.properties.at(-1);
254
147
  if (!key || !last || !toOne || joined) {
255
148
  return;
@@ -257,7 +150,7 @@ function addForeignKeyReference(relation, key, owner, ctx, site) {
257
150
  const column = `${key}Id`;
258
151
  const declaration = columnDeclaration(column, relation, owner, ctx.checker, site);
259
152
  if (typeof declaration === 'string') {
260
- ctx.unresolved.push(`${ctx.describe(site && ts.isPropertyDeclaration(site) ? site : relation)}: ${declaration}`);
153
+ ctx.report(site && ts.isPropertyDeclaration(site) ? site : relation, declaration);
261
154
  return;
262
155
  }
263
156
  if (declaration) {
@@ -279,7 +172,7 @@ function columnDeclaration(column, relation, owner, checker, site) {
279
172
  const declared = site ? objectProperties(site).some(({ name }) => propertyKey(name) === column) : member;
280
173
  return declared ? undefined : undeclaredColumn(column);
281
174
  }
282
- /** `@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. */
283
176
  function foreignKeyColumn(property, column, relation, checker) {
284
177
  const targetType = checker.getNonNullableType(checker.getTypeAtLocation(property));
285
178
  const target = entityGetterTarget(relation) ?? relationTargetFor(targetType, checker);
@@ -291,10 +184,12 @@ function foreignKeyColumn(property, column, relation, checker) {
291
184
  if (!target || !key) {
292
185
  return undeclaredColumn(column);
293
186
  }
294
- const indent = ' '.repeat(property.getSourceFile().getLineAndCharacterOfPosition(property.getStart()).character);
295
187
  const type = keyTypeSource(key, property, checker);
296
- // `| null`, as on every column the codemod touches: a foreign key holds one like any other.
297
- 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);
298
193
  }
299
194
  /** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
300
195
  function keyTypeSource(key, at, checker) {
@@ -322,7 +217,7 @@ function rewriteValue(value, rewrite, node, advice, ctx) {
322
217
  ctx.edits.push(...edits);
323
218
  }
324
219
  else {
325
- ctx.unresolved.push(`${ctx.describe(node)}: ${advice}; this one could not be read`);
220
+ ctx.report(node, `${advice}; this one could not be read`);
326
221
  }
327
222
  }
328
223
  function rewriteKeyList(value, param, node, what, ctx) {
@@ -333,21 +228,22 @@ function rewriteKeyList(value, param, node, what, ctx) {
333
228
  * members named by string read off a callback's parameter named after the class, and SQL written as it is taken.
334
229
  */
335
230
  function rewriteClassDecorators(node, ctx) {
336
- const owner = { param: paramFor(node.name?.text), entity: node };
231
+ const owner = ownerOf(node);
337
232
  for (const decorator of decoratorsOf(node)) {
338
233
  if (!ts.isCallExpression(decorator.expression)) {
339
234
  continue;
340
235
  }
341
236
  const [first, second] = decorator.expression.arguments;
342
237
  const name = decoratorName(decorator);
238
+ const options = objectLiteral(first);
343
239
  if (name === 'Index') {
344
240
  rewriteIndex(first, second, owner, decorator, "'@Index' columns", ctx);
345
241
  }
346
242
  else if (name === 'Filter' && second) {
347
243
  renameOption(optionsOf(second), 'condition', 'where', decorator, ctx);
348
244
  }
349
- else if (name === 'Entity' && first && ts.isObjectLiteralExpression(first)) {
350
- rewriteEntityOptions(first, owner, ctx);
245
+ else if (name === 'Entity' && options) {
246
+ rewriteEntityOptions(options, owner, ctx);
351
247
  }
352
248
  }
353
249
  }
@@ -357,18 +253,20 @@ function rewriteClassDecorators(node, ctx) {
357
253
  * Only uql's shapes, a class and a literal options object, are read.
358
254
  */
359
255
  function rewriteDefineCall(call, ctx) {
360
- const name = ts.isIdentifier(call.expression) ? call.expression.text : undefined;
256
+ const name = identifierText(call.expression);
361
257
  const [entity = call, second, third] = call.arguments;
362
- const owner = { param: paramFor(ts.isIdentifier(entity) ? entity.text : undefined), entity };
363
- if (name === 'defineEntity' && second && ts.isObjectLiteralExpression(second)) {
364
- 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);
365
263
  }
366
- else if (name === 'defineIndex' && second && ts.isObjectLiteralExpression(second)) {
367
- rewriteIndexOptions(second, owner, ctx);
264
+ else if (name === 'defineIndex' && options) {
265
+ rewriteIndexOptions(options, owner, ctx);
368
266
  }
369
- else if (name === 'defineRelation' && third && ts.isObjectLiteralExpression(third)) {
370
- rewriteRelationOptions(third, owner.param, ctx);
371
- 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);
372
270
  }
373
271
  else if (name === 'defineFilter' && third) {
374
272
  renameOption(optionsOf(third), 'condition', 'where', call, ctx);
@@ -394,11 +292,10 @@ function rewriteEntityOptions(options, owner, ctx) {
394
292
  for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
395
293
  rewriteKeyList(hook.initializer, owner.param, hook, 'a hook list', ctx);
396
294
  }
397
- const declared = fields && ts.isObjectLiteralExpression(fields) ? fields : undefined;
398
295
  for (const relation of objectProperties(propertyValue(options, 'relations'))) {
399
296
  if (ts.isObjectLiteralExpression(relation.initializer)) {
400
297
  rewriteRelationOptions(relation.initializer, owner.param, ctx);
401
- addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx, declared);
298
+ addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx, objectLiteral(fields));
402
299
  }
403
300
  }
404
301
  }
@@ -410,7 +307,7 @@ function rewriteIndexOptions(index, owner, ctx) {
410
307
  * `include` read off the entity's refs, its `where` written as {@link rewriteIndexWhere} writes it, its SQL noted.
411
308
  */
412
309
  function rewriteIndex(columns, options, owner, node, what, ctx) {
413
- const literal = options && ts.isObjectLiteralExpression(options) ? options : undefined;
310
+ const literal = objectLiteral(options);
414
311
  rewriteKeyList(columns, owner.param, node, what, ctx);
415
312
  rewriteKeyList(literal && propertyValue(literal, 'include'), owner.param, node, "'include'", ctx);
416
313
  rewriteIndexWhere(literal, ctx);
@@ -418,10 +315,7 @@ function rewriteIndex(columns, options, owner, node, what, ctx) {
418
315
  noteHandNamedColumns(sql, owner, ctx);
419
316
  }
420
317
  }
421
- /**
422
- * A partial-index `where` string as `raw`, on an entity and in the migration builder alike. A string it
423
- * cannot write as `raw` is reported.
424
- */
318
+ /** A partial-index `where` string as `raw`, on an entity and in the migration builder alike, or reported. */
425
319
  function rewriteIndexWhere(options, ctx) {
426
320
  const where = options && propertyValue(options, 'where');
427
321
  if (!where || !isStringTyped(ctx.checker.getTypeAtLocation(where))) {
@@ -433,13 +327,78 @@ function rewriteIndexWhere(options, ctx) {
433
327
  ctx.imports.set('raw', 'the raw`...`');
434
328
  }
435
329
  else {
436
- 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");
437
331
  }
438
332
  }
439
333
  /** Whether every value `type` admits is a string. */
440
334
  function isStringTyped(type) {
441
335
  return type.isUnion() ? type.types.every(isStringTyped) : Boolean(type.flags & ts.TypeFlags.StringLike);
442
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
+ }
360
+ /**
361
+ * A querier's `run`/`all` handed SQL as a string, which now takes a tagged statement: a literal becomes
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.
364
+ */
365
+ function rewriteSqlCall(call, method, ctx) {
366
+ const [sql, ...values] = call.arguments;
367
+ if (!sql || !isStringTyped(ctx.checker.getTypeAtLocation(sql))) {
368
+ return;
369
+ }
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}`));
374
+ return;
375
+ }
376
+ if (values.length) {
377
+ ctx.report(call, `${method}() takes one raw statement: write the values into it, raw\`... \${value}\`, which binds them`);
378
+ return;
379
+ }
380
+ if (ts.isTemplateExpression(sql)) {
381
+ ctx.note(call, 'the values in this template are spliced into the SQL; write it as raw`...` to bind them');
382
+ }
383
+ ctx.edits.push(replaced(sql, `raw.text(${sql.getText()})`));
384
+ ctx.imports.set('raw', 'the raw.text(...)');
385
+ }
386
+ /** `const { id } = await q.upsertOne(...)`, which resolves to the id now, as the id: other keys are reported. */
387
+ function rewriteUpsertResult(declaration, ctx) {
388
+ const { name, initializer } = declaration;
389
+ const call = initializer && ts.isAwaitExpression(initializer) ? initializer.expression : initializer;
390
+ if (!ts.isObjectBindingPattern(name) || !call || !ts.isCallExpression(call) || uqlMethod(call, ctx) !== 'upsertOne') {
391
+ return;
392
+ }
393
+ const [element] = name.elements;
394
+ const key = element && (element.propertyName ?? element.name);
395
+ if (name.elements.length === 1 && key && identifierText(key) === 'id' && ts.isIdentifier(element.name)) {
396
+ ctx.edits.push(replaced(name, element.name.text));
397
+ }
398
+ else {
399
+ ctx.report(declaration, "upsertOne() resolves to the id; 'created' and 'changes' are gone");
400
+ }
401
+ }
443
402
  /** The migration builder's index methods, each with the position of the options it takes. */
444
403
  const BUILDER_INDEX_METHODS = new Map([
445
404
  ['index', 1],
@@ -447,22 +406,22 @@ const BUILDER_INDEX_METHODS = new Map([
447
406
  ['addIndex', 1],
448
407
  ['createIndex', 2],
449
408
  ]);
450
- /** The migration builder's `expr` helpers whose value an entity imports under the same name. */
451
- 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
+ ]);
452
414
  /**
453
415
  * Rewrites the migration builder's `expr.<helper>()` default as the value or `raw` an entity declares, or reports
454
416
  * one with no rewrite. Called only where the file imports `expr` from uql, so a variable of its own is left alone.
455
417
  */
456
418
  function rewriteExprCall(call, ctx) {
457
419
  const callee = call.expression;
458
- if (!ts.isPropertyAccessExpression(callee) ||
459
- !ts.isIdentifier(callee.expression) ||
460
- callee.expression.text !== 'expr') {
420
+ if (!ts.isPropertyAccessExpression(callee) || identifierText(callee.expression) !== 'expr') {
461
421
  return;
462
422
  }
463
423
  const helper = callee.name.text;
464
- // The one renamed: an entity's clock is `currentTimestamp`.
465
- const value = helper === 'now' ? 'currentTimestamp' : EXPR_VALUES.has(helper) ? helper : undefined;
424
+ const value = EXPR_VALUES.get(helper);
466
425
  const [sql] = call.arguments;
467
426
  if (value) {
468
427
  ctx.edits.push(replaced(call, value));
@@ -473,7 +432,7 @@ function rewriteExprCall(call, ctx) {
473
432
  ctx.imports.set('raw', 'the raw`...`');
474
433
  }
475
434
  else {
476
- 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`);
477
436
  return;
478
437
  }
479
438
  ctx.rewritten.expr += 1;
@@ -483,11 +442,9 @@ function rewriteBuilderCall(call, ctx) {
483
442
  const at = ts.isPropertyAccessExpression(call.expression)
484
443
  ? BUILDER_INDEX_METHODS.get(call.expression.name.text)
485
444
  : undefined;
486
- if (at === undefined) {
487
- return;
445
+ if (at !== undefined) {
446
+ rewriteIndexWhere(objectLiteral(call.arguments[at]), ctx);
488
447
  }
489
- const options = call.arguments[at];
490
- rewriteIndexWhere(options && ts.isObjectLiteralExpression(options) ? options : undefined, ctx);
491
448
  }
492
449
  const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
493
450
  /**
@@ -497,22 +454,18 @@ const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
497
454
  */
498
455
  function rewriteUpsertCall(call, ctx) {
499
456
  const callee = call.expression;
500
- const name = ts.isPropertyAccessExpression(callee) ? callee.name : callee;
457
+ const name = identifierText(ts.isPropertyAccessExpression(callee) ? callee.name : callee);
501
458
  const options = call.arguments[3];
502
459
  const update = options && upsertUpdate(options);
503
- if (ts.isIdentifier(name) && UPSERT_CALLS.has(name.text) && options && update) {
460
+ if (name && UPSERT_CALLS.has(name) && update) {
504
461
  ctx.edits.push(replaced(options, update.getText()));
505
462
  }
506
463
  }
507
464
  /** The value of options holding `update` alone, `{ update: value }` or `{ update }`. */
508
465
  function upsertUpdate(options) {
509
- const [property] = ts.isObjectLiteralExpression(options) && options.properties.length === 1 ? options.properties : [];
510
- if (property && ts.isPropertyAssignment(property) && propertyKey(property.name) === 'update') {
511
- return property.initializer;
512
- }
513
- return property && ts.isShorthandPropertyAssignment(property) && property.name.text === 'update'
514
- ? property.name
515
- : 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);
516
469
  }
517
470
  /**
518
471
  * `owner` is the declaring class's parameter and `target` the related one's, read off the `entity`
@@ -560,7 +513,7 @@ function isTextSearch(node) {
560
513
  }
561
514
  function rewriteAggregate(agg, ctx) {
562
515
  if (ts.isObjectLiteralExpression(agg.parent) && propertyValue(agg.parent, '$select')) {
563
- ctx.unresolved.push(`${ctx.describe(agg)}: merge '$agg' into the '$select' beside it`);
516
+ ctx.report(agg, "merge '$agg' into the '$select' beside it");
564
517
  return;
565
518
  }
566
519
  ctx.edits.push(replaced(agg.name, '$select'));
@@ -573,29 +526,15 @@ function rewriteAggregate(agg, ctx) {
573
526
  }
574
527
  }
575
528
  }
576
- /** The property assignments of an object literal, or none where `node` is not one. */
577
- function objectProperties(node) {
578
- return node && ts.isObjectLiteralExpression(node) ? node.properties.filter(ts.isPropertyAssignment) : [];
579
- }
580
- /** The elements of an array literal, or none where `node` is not one. */
581
- function arrayElements(node) {
582
- return node && ts.isArrayLiteralExpression(node) ? node.elements : [];
583
- }
584
529
  /**
585
- * Drops `declare` from a decorated field. A `declare` field emits nothing, so the standard spec has
586
- * nothing to decorate and rejects it ("Decorators are not valid here"). Legacy decorators tolerated it
587
- * 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").
588
532
  */
589
533
  function dropDeclare(node, ctx) {
590
534
  const modifier = node.modifiers?.find((m) => m.kind === ts.SyntaxKind.DeclareKeyword);
591
535
  if (modifier) {
592
- // Through the whitespace that followed it, rather than a fixed one character: two spaces or a line
593
- // break left the rest behind.
594
- const text = node.getSourceFile().text;
595
- let end = modifier.getEnd();
596
- while (/\s/.test(text[end])) {
597
- end += 1;
598
- }
536
+ const rest = node.getSourceFile().text.slice(modifier.getEnd());
537
+ const end = modifier.getEnd() + rest.length - rest.trimStart().length;
599
538
  ctx.edits.push({ start: modifier.getStart(), end, text: '' });
600
539
  }
601
540
  }
@@ -608,85 +547,23 @@ function unwrapRelationAlias(node, ctx) {
608
547
  const declared = node.type;
609
548
  if (declared &&
610
549
  ts.isTypeReferenceNode(declared) &&
611
- ts.isIdentifier(declared.typeName) &&
612
- declared.typeName.text === 'Relation' &&
550
+ identifierText(declared.typeName) === 'Relation' &&
613
551
  declared.typeArguments?.length === 1) {
614
552
  ctx.edits.push(replaced(declared, declared.typeArguments[0].getText()));
615
553
  ctx.rewritten.Relation += 1;
616
554
  }
617
555
  }
618
- /** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
619
- function removeStatement(node) {
620
- const source = node.getSourceFile();
621
- const line = source.getLineAndCharacterOfPosition(node.getEnd()).line;
622
- const nextLineStart = source.getLineStarts()[line + 1];
623
- return { start: node.getStart(), end: nextLineStart ?? node.getEnd(), text: '' };
624
- }
625
- /**
626
- * Names `uql-orm` no longer exports and that the codemod does remove: `Relation` once every usage was
627
- * unwrapped, and `expr` once every default was rewritten. A removed *decorator* keeps its import on purpose (see
628
- * {@link REMOVED_DECORATORS}).
629
- */
630
- function deadImportNames(source, ctx) {
631
- const imported = uqlImports(source, true);
632
- // Every use of the name the file imports, the places no rewrite reaches included.
633
- const uses = (name) => {
634
- const element = imported.find((specifier) => importedName(specifier) === name);
635
- return element ? usesOf(source, element.name, ctx.checker).length : 0;
636
- };
637
- const { rewritten } = ctx;
638
- const relations = uses('Relation');
639
- if (relations > rewritten.Relation) {
640
- ctx.unresolved.push(`${source.fileName}: ${relations - rewritten.Relation} 'Relation<T>' reference(s) are somewhere this codemod ` +
641
- 'does not rewrite; unwrap them and drop the import by hand');
642
- }
643
- const names = ['Relation', 'expr'];
644
- return new Set(names.filter((name) => rewritten[name] > 0 && rewritten[name] === uses(name)));
645
- }
646
- /**
647
- * Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and each `dead` name
648
- * from the imports {@link renameExports} left as written, having left the dead names out of the rest.
649
- */
650
- function dropDeadImports(source, dead, rewritten, ctx) {
651
- for (const statement of source.statements) {
652
- if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
653
- continue;
654
- }
655
- if (statement.moduleSpecifier.text === 'reflect-metadata' && !statement.importClause) {
656
- ctx.edits.push(removeStatement(statement));
657
- continue;
658
- }
659
- const named = statement.importClause?.namedBindings;
660
- if (!named || !ts.isNamedImports(named) || rewritten.has(statement)) {
661
- continue;
662
- }
663
- const dropped = named.elements.filter((element) => dead.has(element.name.text));
664
- if (dropped.length === named.elements.length && !statement.importClause?.name) {
665
- ctx.edits.push(removeStatement(statement));
666
- continue;
667
- }
668
- for (const element of dropped) {
669
- ctx.edits.push(removeFromList(named.elements, element, source));
670
- }
671
- }
672
- }
673
556
  /** The names a key is read from without help, mirroring `NamedIdKey` in `uql-orm`. */
674
557
  const CONVENTIONAL_ID_NAMES = new Set(['id', '_id', 'uuid']);
675
558
  /** Whether the member is an `[idKey]?: ...` brand, matched by the name it is written under. */
676
559
  function isIdKeyBrand(member) {
677
560
  return (ts.isPropertyDeclaration(member) &&
678
561
  ts.isComputedPropertyName(member.name) &&
679
- ts.isIdentifier(member.name.expression) &&
680
- member.name.expression.text === 'idKey');
562
+ identifierText(member.name.expression) === 'idKey');
681
563
  }
682
564
  /**
683
- * Names the class's key with the `idKey` brand, where a conventional name does not already.
684
- *
685
- * Only the class's own `@Id` properties are read, which is also what makes an overriding key right:
686
- * a subclass that replaces an inherited `id` needs the brand precisely because the inherited name
687
- * would otherwise be taken for the key.
688
- *
689
- * 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.
690
567
  */
691
568
  function brandIdKey(node, ctx) {
692
569
  const ids = node.members.filter(isIdProperty);
@@ -695,65 +572,43 @@ function brandIdKey(node, ctx) {
695
572
  }
696
573
  const names = ids.map((m) => propertyKey(m.name)).filter((name) => name !== undefined);
697
574
  if (names.length !== ids.length) {
698
- 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");
699
576
  return;
700
577
  }
701
578
  if (names.length === 1 && CONVENTIONAL_ID_NAMES.has(names[0])) {
702
579
  return;
703
580
  }
704
- const indent = ' '.repeat(node.getSourceFile().getLineAndCharacterOfPosition(ids[0].getStart()).character);
705
- const brand = names.map(quoted).join(' | ');
706
581
  const start = node.members.pos;
707
- 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(' | ')};` });
708
583
  ctx.imports.set('idKey', 'the brand(s)');
709
584
  }
710
585
  /**
711
- * Imports what the rewrites wrote (`idKey`, `raw`) into the file's own `uql-orm` import. Reported instead
712
- * 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.
713
588
  */
714
- function addImports(source, rewritten, ctx) {
715
- const imported = new Set(uqlImports(source).map(importedName));
716
- const missing = [...ctx.imports].filter(([name]) => !imported.has(name));
717
- if (!missing.length) {
718
- return;
719
- }
720
- const anchor = uqlImportDeclarations(source).find(({ declaration }) => !rewritten.has(declaration))?.elements[0];
721
- if (!anchor) {
722
- for (const [name, what] of missing) {
723
- ctx.unresolved.push(`${source.fileName}: import '${name}' from 'uql-orm' for ${what} written here`);
724
- }
725
- return;
726
- }
727
- ctx.edits.push(inserted(anchor, missing.map(([name]) => `${name}, `).join('')));
728
- }
729
- /** Everything the standard spec needs written onto one decorated property, and the options renamed since. */
730
589
  function rewriteProperty(node, ctx) {
731
- const owner = { param: paramFor(node.parent.name?.text), entity: node.parent };
590
+ const owner = ownerOf(node.parent);
732
591
  const decorators = decoratorsOf(node);
733
592
  for (const decorator of decorators) {
734
- const name = decoratorName(decorator);
735
- if (!name) {
736
- continue;
737
- }
593
+ const name = decoratorName(decorator) ?? '';
738
594
  const options = decoratorOptions(decorator);
739
595
  if (FIELD_DECORATORS.has(name)) {
740
596
  if (name === 'Field') {
741
597
  admitNull(options, node, ctx);
742
598
  }
743
599
  addFieldType(decorator, node, ctx);
744
- // Options it cannot read may still hold `virtual`, so they are reported where they mention it.
745
600
  if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
746
- ctx.unresolved.push(`${ctx.describe(node)}: ${options.reason}`);
601
+ ctx.report(node, options.reason);
747
602
  }
748
603
  renameSqlOption(options, 'virtual', 'computed', node, owner, ctx);
749
604
  }
750
605
  if (RELATION_DECORATORS.has(name)) {
751
606
  addRelationEntity(decorator, node, ctx);
752
- }
753
- if (RELATION_DECORATORS.has(name) && options.kind === 'literal') {
754
- const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
755
- rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
756
- 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)) {
757
612
  addForeignKeyReference(options.node, propertyKey(node.name), owner, ctx, node);
758
613
  }
759
614
  }
@@ -764,34 +619,26 @@ function rewriteProperty(node, ctx) {
764
619
  unwrapRelationAlias(node, ctx);
765
620
  }
766
621
  /**
767
- * A column holds `null` unless `nullable: false` says otherwise, and a read hydrates one, so the property
768
- * says so too. A key is NOT NULL on every engine, and a relation aggregate is typed by the aggregate,
769
- * 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.
770
625
  */
771
626
  function admitNull(options, node, ctx) {
772
- // Options it cannot read may state `nullable` themselves, and appending to the property would then
773
- // contradict the column. `addFieldType` already reports the same options, so this adds no second note.
774
- if (options.kind === 'opaque') {
775
- return;
776
- }
777
- if (declaresNotNull(options) ||
627
+ if (options.kind === 'opaque' ||
628
+ !ctx.strictNullChecks ||
629
+ declaresNotNull(options) ||
778
630
  findProperty(options, 'isId') ||
779
631
  (findProperty(options, 'computed') && !findProperty(options, 'type'))) {
780
632
  return;
781
633
  }
782
634
  if (!node.type) {
783
- ctx.notes.push(`${ctx.describe(node)}: declare '${propertyKey(node.name)}' as a type that admits 'null', which the column holds`);
784
- return;
635
+ ctx.note(node, `declare '${propertyKey(node.name)}' as a type that admits 'null', which the column holds`);
785
636
  }
786
- if (admitsNull(node.type)) {
787
- return;
637
+ else if (!admitsNull(node.type)) {
638
+ ctx.edits.push(appended(node.type, ' | null'));
788
639
  }
789
- ctx.edits.push(appended(node.type, ' | null'));
790
640
  }
791
- /**
792
- * The same null rule where `defineEntity` names the field: the property is on the class the call takes,
793
- * so the declaration is found there rather than under a decorator.
794
- */
641
+ /** The same null rule where `defineEntity` names the field, on the property of the class the call takes. */
795
642
  function admitNullOnDefined(field, owner, ctx) {
796
643
  const key = propertyKey(field.name);
797
644
  const declaration = key
@@ -824,120 +671,23 @@ function reportRemovedDecorators(node, ctx) {
824
671
  const name = decoratorName(decorator);
825
672
  const advice = name && REMOVED_DECORATORS.get(name);
826
673
  if (advice) {
827
- ctx.unresolved.push(`${ctx.describe(decorator)}: '@${name}()' was removed; ${advice}`);
674
+ ctx.report(decorator, `'@${name}()' was removed; ${advice}`);
828
675
  }
829
676
  }
830
677
  }
831
- /** Names an export that no longer exists, where it is imported from the package or one of its entries. */
832
- function reportRemovedExports(source, ctx) {
833
- for (const element of uqlImports(source, true)) {
834
- const name = importedName(element);
835
- const advice = REMOVED_EXPORTS.get(name);
836
- if (advice) {
837
- ctx.unresolved.push(`${ctx.describe(element)}: '${name}' was removed; ${advice}`);
838
- }
839
- }
840
- }
841
- /**
842
- * Rewrites each import naming a {@link RENAMED_EXPORTS} export, renaming every use of it: a name moving
843
- * entries gets an import from its new one, and a name the file already imports is dropped, as is a `dead`
844
- * one. Returns the rewritten imports, which nothing else may edit.
845
- */
846
- function renameExports(source, dead, ctx) {
847
- const imports = uqlImportDeclarations(source, true);
848
- const kept = imports.flatMap(({ entry, elements }) => elements.filter((element) => !renamedTo(element, entry)));
849
- const bound = new Set(kept.map((element) => element.name.text));
850
- const rewritten = new Set();
851
- for (const { declaration, entry, elements } of imports) {
852
- if (!elements.some((element) => renamedTo(element, entry))) {
853
- continue;
854
- }
855
- const byEntry = new Map([[entry, []]]);
856
- for (const element of elements.filter(({ name }) => !dead.has(name.text))) {
857
- const rename = renamedTo(element, entry);
858
- const local = element.propertyName || !rename ? element.name.text : rename.to;
859
- if (rename && !element.propertyName) {
860
- ctx.edits.push(...usesOf(source, element.name, ctx.checker).map((use) => replaced(use, rename.to)));
861
- }
862
- if (rename && bound.has(local)) {
863
- continue;
864
- }
865
- bound.add(local);
866
- const target = rename?.from ?? entry;
867
- const text = rename
868
- ? `${element.isTypeOnly ? 'type ' : ''}${rename.to}${element.propertyName ? ` as ${local}` : ''}`
869
- : element.getText();
870
- byEntry.set(target, [...(byEntry.get(target) ?? []), text]);
871
- }
872
- const typeOnly = declaration.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
873
- const keyword = typeOnly ? 'import type' : 'import';
874
- const quote = declaration.moduleSpecifier.getText()[0];
875
- const statements = [...byEntry]
876
- .filter(([, names]) => names.length)
877
- .map(([from, names]) => `${keyword} { ${names.join(', ')} } from ${quote}${from}${quote};`);
878
- ctx.edits.push(statements.length ? replaced(declaration, statements.join('\n')) : removeStatement(declaration));
879
- rewritten.add(declaration);
880
- }
881
- return rewritten;
882
- }
883
- /** The rename `element` still needs, none where it already names the export at its entry. */
884
- function renamedTo(element, entry) {
885
- const name = importedName(element);
886
- const rename = RENAMED_EXPORTS.get(name);
887
- return rename?.to === name && rename.from === entry ? undefined : rename;
888
- }
889
- /** Every identifier in the file bound to the same symbol as `name`, besides `name` itself. */
890
- function usesOf(source, name, checker) {
891
- const symbol = checker.getSymbolAtLocation(name);
892
- const uses = [];
893
- const visit = (node) => {
894
- if (ts.isIdentifier(node) && node !== name && node.text === name.text) {
895
- if (symbol && checker.getSymbolAtLocation(node) === symbol) {
896
- uses.push(node);
897
- }
898
- }
899
- ts.forEachChild(node, visit);
900
- };
901
- visit(source);
902
- return uses;
903
- }
904
- /**
905
- * The file's named imports from `uql-orm`. `entries` adds the driver entries (`uql-orm/postgres`, ...),
906
- * for what is read by name only: `idKey` written into one would land on an entry that may not export it.
907
- */
908
- function uqlImportDeclarations(source, entries = false) {
909
- return source.statements.flatMap((declaration) => {
910
- if (!ts.isImportDeclaration(declaration) || !ts.isStringLiteral(declaration.moduleSpecifier)) {
911
- return [];
912
- }
913
- const entry = declaration.moduleSpecifier.text;
914
- if (entry !== 'uql-orm' && !(entries && entry.startsWith('uql-orm/'))) {
915
- return [];
916
- }
917
- const bindings = declaration.importClause?.namedBindings;
918
- return bindings && ts.isNamedImports(bindings) ? [{ declaration, entry, elements: bindings.elements }] : [];
919
- });
920
- }
921
- function uqlImports(source, entries = false) {
922
- return uqlImportDeclarations(source, entries).flatMap(({ elements }) => elements);
923
- }
924
- /** The name an import specifier brings in, which is the original one where it was renamed. */
925
- function importedName(element) {
926
- return (element.propertyName ?? element.name).text;
927
- }
928
678
  /**
929
679
  * Rewrites `raw('sql')` into the tagged template, and a second alias argument into `.as()`, the callback
930
680
  * form's included. A computed string is left alone: a template cannot be built from a value not known here.
931
681
  */
932
- function rewriteRawCall(node, ctx, source) {
933
- 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') {
934
684
  return;
935
685
  }
936
686
  const [expression, alias] = node.arguments;
937
687
  if (!expression || node.arguments.length > 2) {
938
688
  return;
939
689
  }
940
- const suffix = alias ? `.as(${alias.getText(source)})` : '';
690
+ const suffix = alias ? `.as(${alias.getText()})` : '';
941
691
  if (ts.isStringLiteral(expression)) {
942
692
  ctx.edits.push(replaced(node, `${rawTag(expression.text)}${suffix}`));
943
693
  }
@@ -946,169 +696,23 @@ function rewriteRawCall(node, ctx, source) {
946
696
  }
947
697
  }
948
698
  /**
949
- * Rewrites each `col('x')` into the ref it names - `product.x` in a `computed` callback, `refs(Item).x` in a
950
- * statement - where the entity its SQL renders against, and a member named `x`, can be told from here. Every
951
- * other use is reported. Returns `col` as a dead import once no use is left.
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.
952
701
  */
953
- function rewriteColumnRefs(source, ctx) {
954
- const element = uqlImports(source).find((use) => importedName(use) === 'col');
955
- if (!element) {
956
- return [];
957
- }
958
- const computed = new Map();
959
- const unread = [];
960
- for (const use of usesOf(source, element.name, ctx.checker)) {
961
- const call = ts.isCallExpression(use.parent) && use.parent.expression === use ? use.parent : undefined;
962
- const sql = call && ts.findAncestor(call, isRaw);
963
- const option = sql?.parent;
964
- const owner = option && ts.isPropertyAssignment(option) && isComputedOption(option) ? computedOwner(option) : undefined;
965
- const edit = call && !owner ? statementRefEdit(call, ctx) : undefined;
966
- if (call && sql && ts.isTaggedTemplateExpression(sql) && owner) {
967
- computed.set(sql, { owner, calls: [...(computed.get(sql)?.calls ?? []), call] });
968
- }
969
- else if (edit) {
970
- ctx.edits.push(edit);
971
- ctx.imports.set('refs', 'the refs(...)');
972
- }
973
- else {
974
- unread.push(use);
975
- }
976
- }
977
- for (const [sql, { owner, calls }] of computed) {
978
- const edits = computedRefEdits(sql, calls, owner, ctx);
979
- if (edits) {
980
- ctx.edits.push(...edits);
981
- }
982
- else {
983
- unread.push(...calls);
984
- }
985
- }
986
- for (const node of unread.sort((a, b) => a.getStart() - b.getStart())) {
987
- ctx.unresolved.push(`${ctx.describe(node)}: 'col' was removed; read the column off refs(Entity), which the codemod could not tell here`);
988
- }
989
- return unread.length ? [] : ['col'];
990
- }
991
- function isComputedOption(option) {
992
- const key = propertyKey(option.name);
993
- return key === 'computed' || key === 'virtual';
994
- }
995
- /** The entity a field's `computed` SQL is declared on: the decorated property's class, or `defineEntity`'s first argument. */
996
- function computedOwner(option) {
997
- const holder = ts.findAncestor(option, (node) => ts.isPropertyDeclaration(node) ||
998
- (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'defineEntity'));
999
- if (holder && ts.isPropertyDeclaration(holder)) {
1000
- return { param: paramFor(holder.parent.name?.text), entity: holder.parent };
1001
- }
1002
- const [entity] = holder?.arguments ?? [];
1003
- return entity && { param: paramFor(ts.isIdentifier(entity) ? entity.text : undefined), entity };
1004
- }
1005
- /** `(product) => ` before a `computed` raw, and `product.x` for each `col('x')` in it, where every one names a member. */
1006
- function computedRefEdits(sql, calls, owner, ctx) {
1007
- const members = memberNames(owner.entity, ctx.checker);
1008
- const names = calls.map((call) => columnMember(call, members));
1009
- if (mentions(sql, owner.param) || !names.every((name) => name !== undefined)) {
1010
- return undefined;
1011
- }
1012
- return [
1013
- inserted(sql, `(${owner.param}) => `),
1014
- ...calls.map((call, at) => replaced(call, memberAccess(owner.param, names[at]))),
1015
- ];
1016
- }
1017
- /** `refs(Item).x` for a `col('x')` in a statement, where the entity its SQL renders against can be told. */
1018
- function statementRefEdit(call, ctx) {
1019
- const entity = statementEntity(call, ctx.checker);
1020
- const name = entity && columnMember(call, propertyNames(entity.type));
1021
- return entity && name !== undefined ? replaced(call, memberAccess(`refs(${entity.name})`, name)) : undefined;
1022
- }
1023
- /** The member a `col('x')` names, where its one argument is a literal naming one of `members`. */
1024
- function columnMember(call, members) {
1025
- const [name] = call.arguments;
1026
- return call.arguments.length === 1 && ts.isStringLiteralLike(name) && members.has(name.text) ? name.text : undefined;
1027
- }
1028
- /** Whether `node` reads a binding named `name`, which a callback parameter of that name would shadow. */
1029
- function mentions(node, name) {
1030
- if (ts.isIdentifier(node)) {
1031
- return node.text === name && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node);
1032
- }
1033
- return ts.forEachChild(node, (child) => mentions(child, name) || undefined) ?? false;
1034
- }
1035
- /**
1036
- * The entity a statement's SQL renders against at `node`: the one its query names, or a relation a `$populate`
1037
- * reads there. Nothing where a function or a relation filter in between opens a scope of its own.
1038
- */
1039
- function statementEntity(node, checker) {
1040
- const path = [];
1041
- for (let at = node; at.parent; at = at.parent) {
1042
- const { parent } = at;
1043
- if (ts.isFunctionLike(parent)) {
1044
- return undefined;
1045
- }
1046
- if (ts.isPropertyAssignment(parent)) {
1047
- path.unshift(propertyKey(parent.name) ?? '');
1048
- }
1049
- const queried = ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === at);
1050
- const entity = queried ? queriedEntity(parent, checker) : undefined;
1051
- if (entity) {
1052
- return entityAlong(entity, path, node, checker);
1053
- }
1054
- }
1055
- return undefined;
1056
- }
1057
- /** The entity a call queries: its first argument's class, or the `$entity` its query names. */
1058
- function queriedEntity(call, checker) {
1059
- const [first] = call.arguments;
1060
- const entity = first && ts.isObjectLiteralExpression(first) ? propertyValue(first, '$entity') : first;
1061
- const instance = entity && checker.getTypeAtLocation(entity).getConstructSignatures()[0]?.getReturnType();
1062
- return entity && ts.isIdentifier(entity) && instance ? { name: entity.text, type: instance } : undefined;
1063
- }
1064
- /** `entity` followed along `path`: into each relation a `$populate` names, and nowhere a relation filter opens. */
1065
- function entityAlong(entity, path, node, checker) {
1066
- let current = entity;
1067
- for (const [at, key] of path.entries()) {
1068
- const property = current.type.getProperty(key);
1069
- const related = property && relatedClass(property, node, checker);
1070
- if (!related) {
1071
- continue;
1072
- }
1073
- const inScope = checker.getSymbolsInScope(node, ts.SymbolFlags.Class).some(({ name }) => name === related.name);
1074
- if (path[at - 1] !== '$populate' || !inScope) {
1075
- return undefined;
1076
- }
1077
- current = { name: related.name, type: checker.getDeclaredTypeOfSymbol(related) };
1078
- }
1079
- return current;
1080
- }
1081
- /** The class a relation property points at, one or many, or nothing where the property holds no class. */
1082
- function relatedClass(property, node, checker) {
1083
- const type = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(property, node));
1084
- const symbol = (checker.getIndexTypeOfType(type, ts.IndexKind.Number) ?? type).getSymbol();
1085
- return symbol && symbol.flags & ts.SymbolFlags.Class ? symbol : undefined;
1086
- }
1087
- /** Rewrites one source file for the standard decorator spec. */
1088
- export function transformFile(source, checker) {
1089
- const ctx = {
1090
- checker,
1091
- edits: [],
1092
- unresolved: [],
1093
- notes: [],
1094
- imports: new Map(),
1095
- rewritten: { Relation: 0, expr: 0 },
1096
- describe: (node) => `${source.fileName}:${lineOf(node) + 1}`,
1097
- };
1098
- const lineOf = (node) => source.getLineAndCharacterOfPosition(node.getStart()).line;
1099
- const rewritesRaw = uqlImports(source).some((element) => importedName(element) === 'raw');
1100
- // The builder's methods are known by name alone, so they are read only in a file that imports uql-orm.
702
+ export function transformFile(source, checker, options = { strictNullChecks: true }) {
703
+ const ctx = createContext(source, checker, options.strictNullChecks);
704
+ const rewritesRaw = uqlImport(source, 'raw') !== undefined;
1101
705
  const importsUql = uqlImportDeclarations(source, true).length > 0;
1102
- const importsExpr = uqlImports(source, true).some((element) => importedName(element) === 'expr');
706
+ const importsExpr = uqlImport(source, 'expr', true) !== undefined;
1103
707
  const visit = (node) => {
1104
708
  reportRemovedDecorators(node, ctx);
1105
709
  if (rewritesRaw) {
1106
- rewriteRawCall(node, ctx, source);
710
+ rewriteRawCall(node, ctx);
1107
711
  }
1108
712
  if (ts.isPropertyDeclaration(node)) {
1109
713
  rewriteProperty(node, ctx);
1110
714
  }
1111
- if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
715
+ if (ts.isClassLike(node)) {
1112
716
  brandIdKey(node, ctx);
1113
717
  rewriteClassDecorators(node, ctx);
1114
718
  }
@@ -1117,6 +721,7 @@ export function transformFile(source, checker) {
1117
721
  rewriteUpsertCall(node, ctx);
1118
722
  if (importsUql) {
1119
723
  rewriteBuilderCall(node, ctx);
724
+ rewriteMethodCall(node, ctx);
1120
725
  }
1121
726
  if (importsExpr) {
1122
727
  rewriteExprCall(node, ctx);
@@ -1125,14 +730,17 @@ export function transformFile(source, checker) {
1125
730
  if (ts.isPropertyAssignment(node)) {
1126
731
  rewriteStatementKeys(node, ctx);
1127
732
  }
733
+ if (importsUql && ts.isVariableDeclaration(node)) {
734
+ rewriteUpsertResult(node, ctx);
735
+ }
1128
736
  ts.forEachChild(node, visit);
1129
737
  };
1130
738
  visit(source);
1131
- const dead = new Set([...deadImportNames(source, ctx), ...rewriteColumnRefs(source, ctx)]);
1132
- const rewritten = renameExports(source, dead, ctx);
1133
- reportRemovedExports(source, ctx);
1134
- dropDeadImports(source, dead, rewritten, ctx);
1135
- 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);
1136
744
  return {
1137
745
  fileName: source.fileName,
1138
746
  text: applyEdits(source.getFullText(), ctx.edits),