uql-codemod 0.38.0 → 0.39.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/README.md CHANGED
@@ -28,7 +28,7 @@ Only `--flag=value` is read, and anything unrecognised is an error. It exits `0`
28
28
  - `entity: () => X` on a bare relation decorator, and `Relation<T>` to `T`, its import going with the last use.
29
29
  - `virtual` to `computed` (on `@Field` and in `defineEntity`'s `fields`), a check's `expression` and a filter's `condition` to `where` (`@Entity`/`defineEntity`, `@Filter`, `defineFilter`), `raw('sql')` to the tagged template, and a `raw` alias argument onto `.as()`.
30
30
  - Members named by string to callbacks: `mappedBy: 'author'` to `(post) => post.author`, and `@Index`, `include`, `references` and `@Entity`/`defineEntity`'s `indexes` and `hooks` likewise: `@Index((post) => [post.title])`.
31
- - `references: (post) => post.authorId` on a to-one (`@ManyToOne`, `@OneToOne`, or `defineEntity`/`defineRelation`) whose entity declares or inherits `authorId`, which it used to join by name.
31
+ - `references: (post) => post.authorId` on a to-one (`@ManyToOne`, `@OneToOne`, or `defineEntity`/`defineRelation`) that names no foreign key, declaring `@Field({ references: () => User }) authorId` first on a decorated class that has no such column, typed as the key it points at.
32
32
  - A partial-index `where` string to `raw`, importing it, on entities and in the migration builder.
33
33
  - `col('cost')` to the ref it names, ``(product) => raw`${product.cost}` `` in a `computed` and `refs(Item).cost` in a statement, and `$lock: { wait: 'skip' }` to `{ $wait: 'skip' }`, or to `true` for `{}` and a `'block'` wait.
34
34
  - An aggregate's `$agg` to `$select`, and each field it or `$text`'s `$fields` names to a key: `{ $sum: { amount: true } }`.
@@ -44,7 +44,8 @@ Reported with what to do, never guessed:
44
44
  - `@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`: a `@Transactional()` method becomes a `pool.transaction()` around its body, and only you know which pool.
45
45
  - An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...).
46
46
  - A value it cannot read: options passed as a variable or a spread, a list that is not a literal, a key under a computed name.
47
- - A to-one onto a composite key whose columns the entity declares: pair each with its key in `references`, which the error at startup spells out.
47
+ - A to-one onto a composite key: declare a column per key and pair each with it in `references`.
48
+ - A to-one whose `<relation>Id` is not a declared column: an undecorated property, a `defineEntity` whose `fields` leave it out, or a `defineRelation` whose entity has no such member.
48
49
  - A partial-index `where` string that interpolates, since `raw` would bind what the template spliced in.
49
50
  - A `col()` whose entity it cannot tell: a raw built apart from its query, one under a relation filter, or a column no field maps.
50
51
  - SQL in a definition that names a column by hand, as a note: a rename reaches a callback's refs, ``(user) => raw`lower(${user.email})` ``, and never the text.
package/dist/transform.js CHANGED
@@ -157,8 +157,12 @@ function noteHandNamedColumns(sql, owner, ctx) {
157
157
  }
158
158
  /** The names of the members of the entity `node` declares or names. */
159
159
  function memberNames(node, checker) {
160
+ return propertyNames(instanceTypeOf(node, checker));
161
+ }
162
+ /** The instance type of the entity `node` declares or names. */
163
+ function instanceTypeOf(node, checker) {
160
164
  const type = checker.getTypeAtLocation(node);
161
- return propertyNames(type.getConstructSignatures()[0]?.getReturnType() ?? type);
165
+ return type.getConstructSignatures()[0]?.getReturnType() ?? type;
162
166
  }
163
167
  function propertyNames(type) {
164
168
  return new Set(type.getProperties().map(({ name }) => name));
@@ -228,21 +232,72 @@ function addRelationEntity(decorator, node, ctx) {
228
232
  });
229
233
  }
230
234
  /**
231
- * Names the foreign key a to-one found by name alone, `references: (post) => post.authorId` beside `author`,
232
- * where its entity declares or inherits that key: a declared key is no longer joined by its name.
235
+ * Names the foreign key a to-one used to find by name, `references: (post) => post.authorId` beside `author`.
236
+ * `site` says where the entity declares its columns: the decorated property, ahead of which a missing column
237
+ * is declared, or `defineEntity`'s `fields`. A column it cannot see declared is reported instead.
233
238
  */
234
- function addForeignKeyReference(relation, key, owner, ctx) {
239
+ function addForeignKeyReference(relation, key, owner, ctx, site) {
235
240
  const options = { kind: 'literal', node: relation };
236
241
  const cardinality = propertyValue(relation, 'cardinality');
237
242
  const toOne = !cardinality || (ts.isStringLiteralLike(cardinality) && TO_ONE_CARDINALITIES.has(cardinality.text));
238
243
  const joined = ['mappedBy', 'through', 'references'].some((option) => findProperty(options, option));
239
244
  const last = relation.properties.at(-1);
245
+ if (!key || !last || !toOne || joined) {
246
+ return;
247
+ }
240
248
  const column = `${key}Id`;
241
- if (!key || !last || !toOne || joined || !memberNames(owner.entity, ctx.checker).has(column)) {
249
+ const declaration = columnDeclaration(column, relation, owner, ctx.checker, site);
250
+ if (typeof declaration === 'string') {
251
+ ctx.unresolved.push(`${ctx.describe(site && ts.isPropertyDeclaration(site) ? site : relation)}: ${declaration}`);
242
252
  return;
243
253
  }
254
+ if (declaration) {
255
+ ctx.edits.push(declaration);
256
+ ctx.imports.set('Field', 'the foreign key column');
257
+ }
244
258
  ctx.edits.push(appended(last, `, references: (${owner.param}) => ${memberAccess(owner.param, column)}`));
245
259
  }
260
+ const undeclaredColumn = (column) => `declare the foreign key column '${column}' and name it in 'references'`;
261
+ /** Nothing where `column` is declared, the declaration to write where a decorated class lacks it, or why neither. */
262
+ function columnDeclaration(column, relation, owner, checker, site) {
263
+ const member = instanceTypeOf(owner.entity, checker).getProperty(column);
264
+ if (site && ts.isPropertyDeclaration(site)) {
265
+ if (!member) {
266
+ return foreignKeyColumn(site, column, relation, checker);
267
+ }
268
+ return member.declarations?.some(isFieldProperty) ? undefined : undeclaredColumn(column);
269
+ }
270
+ const declared = site ? objectProperties(site).some(({ name }) => propertyKey(name) === column) : member;
271
+ return declared ? undefined : undeclaredColumn(column);
272
+ }
273
+ /** `@Field({ references: () => Target }) <column>?: <key type>;` ahead of the relation, or why it cannot be written. */
274
+ function foreignKeyColumn(property, column, relation, checker) {
275
+ const targetType = checker.getNonNullableType(checker.getTypeAtLocation(property));
276
+ const target = entityGetterTarget(relation) ?? relationTargetFor(targetType, checker);
277
+ const keys = targetType.getProperties().filter((member) => member.declarations?.some(isIdProperty));
278
+ if (keys.length > 1) {
279
+ return `'${target}' has a composite key: declare a column per key and pair each in 'references'`;
280
+ }
281
+ const [key] = keys;
282
+ if (!target || !key) {
283
+ return undeclaredColumn(column);
284
+ }
285
+ const indent = ' '.repeat(property.getSourceFile().getLineAndCharacterOfPosition(property.getStart()).character);
286
+ const type = keyTypeSource(key, property, checker);
287
+ return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type};\n${indent}`);
288
+ }
289
+ /** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
290
+ function keyTypeSource(key, at, checker) {
291
+ const written = key.declarations?.find(isIdProperty)?.type;
292
+ if (written && written.getSourceFile() === at.getSourceFile()) {
293
+ return written.getText();
294
+ }
295
+ return checker.typeToString(checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(key, at)), at);
296
+ }
297
+ /** Whether a declaration is a property carrying one of `names` as a decorator. */
298
+ const decoratedWith = (names) => (node) => ts.isPropertyDeclaration(node) && decoratorsOf(node).some((decorator) => names.has(decoratorName(decorator) ?? ''));
299
+ const isIdProperty = decoratedWith(new Set(['Id']));
300
+ const isFieldProperty = decoratedWith(FIELD_DECORATORS);
246
301
  /**
247
302
  * Writes the edits `rewrite` reads off `value`, which still names members as strings, or reports it
248
303
  * where it could not be read: what it holds is only known at runtime. A callback or an object literal
@@ -310,6 +365,7 @@ function rewriteDefineCall(call, ctx) {
310
365
  }
311
366
  }
312
367
  function rewriteEntityOptions(options, owner, ctx) {
368
+ const fields = propertyValue(options, 'fields');
313
369
  for (const index of arrayElements(propertyValue(options, 'indexes'))) {
314
370
  if (ts.isObjectLiteralExpression(index)) {
315
371
  rewriteIndexOptions(index, owner, ctx);
@@ -318,7 +374,7 @@ function rewriteEntityOptions(options, owner, ctx) {
318
374
  for (const check of arrayElements(propertyValue(options, 'checks'))) {
319
375
  renameSqlOption(optionsOf(check), 'expression', 'where', check, owner, ctx);
320
376
  }
321
- for (const field of objectProperties(propertyValue(options, 'fields'))) {
377
+ for (const field of objectProperties(fields)) {
322
378
  renameSqlOption(optionsOf(field.initializer), 'virtual', 'computed', field, owner, ctx);
323
379
  }
324
380
  for (const filter of objectProperties(propertyValue(options, 'filters'))) {
@@ -327,10 +383,11 @@ function rewriteEntityOptions(options, owner, ctx) {
327
383
  for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
328
384
  rewriteKeyList(hook.initializer, owner.param, hook, 'a hook list', ctx);
329
385
  }
386
+ const declared = fields && ts.isObjectLiteralExpression(fields) ? fields : undefined;
330
387
  for (const relation of objectProperties(propertyValue(options, 'relations'))) {
331
388
  if (ts.isObjectLiteralExpression(relation.initializer)) {
332
389
  rewriteRelationOptions(relation.initializer, owner.param, ctx);
333
- addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx);
390
+ addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx, declared);
334
391
  }
335
392
  }
336
393
  }
@@ -563,7 +620,7 @@ function isIdKeyBrand(member) {
563
620
  * One written makes `idKey` a value the file has to import, which is recorded for {@link addImports}.
564
621
  */
565
622
  function brandIdKey(node, ctx) {
566
- const ids = node.members.filter((m) => ts.isPropertyDeclaration(m) && decoratorsOf(m).some((d) => decoratorName(d) === 'Id'));
623
+ const ids = node.members.filter(isIdProperty);
567
624
  if (!ids.length || node.members.some(isIdKeyBrand)) {
568
625
  return;
569
626
  }
@@ -624,9 +681,9 @@ function rewriteProperty(node, ctx) {
624
681
  if (RELATION_DECORATORS.has(name) && options.kind === 'literal') {
625
682
  const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
626
683
  rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
627
- }
628
- if (TO_ONE_DECORATORS.has(name) && options.kind === 'literal') {
629
- addForeignKeyReference(options.node, propertyKey(node.name), owner, ctx);
684
+ if (TO_ONE_DECORATORS.has(name)) {
685
+ addForeignKeyReference(options.node, propertyKey(node.name), owner, ctx, node);
686
+ }
630
687
  }
631
688
  }
632
689
  if (decorators.length) {
@@ -927,9 +984,9 @@ export function transformFile(source, checker) {
927
984
  }
928
985
  if (ts.isCallExpression(node)) {
929
986
  rewriteDefineCall(node, ctx);
930
- }
931
- if (importsUql && ts.isCallExpression(node)) {
932
- rewriteBuilderCall(node, ctx);
987
+ if (importsUql) {
988
+ rewriteBuilderCall(node, ctx);
989
+ }
933
990
  }
934
991
  if (ts.isPropertyAssignment(node)) {
935
992
  rewriteStatementKeys(node, ctx);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uql-codemod",
3
3
  "description": "Upgrade uql-orm across versions that introduce breaking changes",
4
4
  "license": "MIT",
5
- "version": "0.38.0",
5
+ "version": "0.39.1",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=24"