uql-codemod 0.39.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
@@ -45,7 +45,7 @@ Reported with what to do, never guessed:
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
47
  - A to-one onto a composite key: declare a column per key and pair each with it in `references`.
48
- - A to-one in `defineEntity`/`defineRelation` whose entity has no foreign key column: declare it with `defineField`.
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.
49
49
  - A partial-index `where` string that interpolates, since `raw` would bind what the template spliced in.
50
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.
51
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,11 +232,11 @@ 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
- * A key the entity neither declares nor inherits is declared first, on a decorated property, typed as the
233
- * target's key; elsewhere it is reported.
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.
234
238
  */
235
- function addForeignKeyReference(relation, key, owner, ctx, property) {
239
+ function addForeignKeyReference(relation, key, owner, ctx, site) {
236
240
  const options = { kind: 'literal', node: relation };
237
241
  const cardinality = propertyValue(relation, 'cardinality');
238
242
  const toOne = !cardinality || (ts.isStringLiteralLike(cardinality) && TO_ONE_CARDINALITIES.has(cardinality.text));
@@ -242,49 +246,58 @@ function addForeignKeyReference(relation, key, owner, ctx, property) {
242
246
  return;
243
247
  }
244
248
  const column = `${key}Id`;
245
- if (!memberNames(owner.entity, ctx.checker).has(column)) {
246
- if (!property) {
247
- ctx.unresolved.push(`${ctx.describe(relation)}: declare the foreign key column '${column}' and name it in 'references'`);
248
- return;
249
- }
250
- const declared = foreignKeyColumn(property, column, relation, ctx);
251
- if (!declared) {
252
- return;
253
- }
254
- ctx.edits.push(declared);
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}`);
252
+ return;
253
+ }
254
+ if (declaration) {
255
+ ctx.edits.push(declaration);
256
+ ctx.imports.set('Field', 'the foreign key column');
255
257
  }
256
258
  ctx.edits.push(appended(last, `, references: (${owner.param}) => ${memberAccess(owner.param, column)}`));
257
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
+ }
258
273
  /** `@Field({ references: () => Target }) <column>?: <key type>;` ahead of the relation, or why it cannot be written. */
259
- function foreignKeyColumn(property, column, relation, ctx) {
260
- const { checker } = ctx;
274
+ function foreignKeyColumn(property, column, relation, checker) {
261
275
  const targetType = checker.getNonNullableType(checker.getTypeAtLocation(property));
262
276
  const target = entityGetterTarget(relation) ?? relationTargetFor(targetType, checker);
263
277
  const keys = targetType.getProperties().filter((member) => member.declarations?.some(isIdProperty));
264
- const [soleKey] = keys;
265
- if (!target || !soleKey || keys.length > 1) {
266
- const advice = keys.length > 1
267
- ? `'${target}' has a composite key: declare a column per key and pair each in 'references'`
268
- : `declare the foreign key column '${column}' and name it in 'references'`;
269
- ctx.unresolved.push(`${ctx.describe(property)}: ${advice}`);
270
- return undefined;
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);
271
284
  }
272
- const source = property.getSourceFile();
273
- const indent = ' '.repeat(source.getLineAndCharacterOfPosition(property.getStart()).character);
274
- ctx.imports.set('Field', 'the foreign key column');
275
- return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${keyTypeSource(soleKey, property, ctx)};\n${indent}`);
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}`);
276
288
  }
277
289
  /** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
278
- function keyTypeSource(key, at, ctx) {
290
+ function keyTypeSource(key, at, checker) {
279
291
  const written = key.declarations?.find(isIdProperty)?.type;
280
292
  if (written && written.getSourceFile() === at.getSourceFile()) {
281
293
  return written.getText();
282
294
  }
283
- return ctx.checker.typeToString(ctx.checker.getNonNullableType(ctx.checker.getTypeOfSymbolAtLocation(key, at)), at);
284
- }
285
- function isIdProperty(node) {
286
- return ts.isPropertyDeclaration(node) && decoratorsOf(node).some((decorator) => decoratorName(decorator) === 'Id');
295
+ return checker.typeToString(checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(key, at)), at);
287
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);
288
301
  /**
289
302
  * Writes the edits `rewrite` reads off `value`, which still names members as strings, or reports it
290
303
  * where it could not be read: what it holds is only known at runtime. A callback or an object literal
@@ -352,6 +365,7 @@ function rewriteDefineCall(call, ctx) {
352
365
  }
353
366
  }
354
367
  function rewriteEntityOptions(options, owner, ctx) {
368
+ const fields = propertyValue(options, 'fields');
355
369
  for (const index of arrayElements(propertyValue(options, 'indexes'))) {
356
370
  if (ts.isObjectLiteralExpression(index)) {
357
371
  rewriteIndexOptions(index, owner, ctx);
@@ -360,7 +374,7 @@ function rewriteEntityOptions(options, owner, ctx) {
360
374
  for (const check of arrayElements(propertyValue(options, 'checks'))) {
361
375
  renameSqlOption(optionsOf(check), 'expression', 'where', check, owner, ctx);
362
376
  }
363
- for (const field of objectProperties(propertyValue(options, 'fields'))) {
377
+ for (const field of objectProperties(fields)) {
364
378
  renameSqlOption(optionsOf(field.initializer), 'virtual', 'computed', field, owner, ctx);
365
379
  }
366
380
  for (const filter of objectProperties(propertyValue(options, 'filters'))) {
@@ -369,10 +383,11 @@ function rewriteEntityOptions(options, owner, ctx) {
369
383
  for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
370
384
  rewriteKeyList(hook.initializer, owner.param, hook, 'a hook list', ctx);
371
385
  }
386
+ const declared = fields && ts.isObjectLiteralExpression(fields) ? fields : undefined;
372
387
  for (const relation of objectProperties(propertyValue(options, 'relations'))) {
373
388
  if (ts.isObjectLiteralExpression(relation.initializer)) {
374
389
  rewriteRelationOptions(relation.initializer, owner.param, ctx);
375
- addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx);
390
+ addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx, declared);
376
391
  }
377
392
  }
378
393
  }
@@ -605,7 +620,7 @@ function isIdKeyBrand(member) {
605
620
  * One written makes `idKey` a value the file has to import, which is recorded for {@link addImports}.
606
621
  */
607
622
  function brandIdKey(node, ctx) {
608
- const ids = node.members.filter((m) => ts.isPropertyDeclaration(m) && decoratorsOf(m).some((d) => decoratorName(d) === 'Id'));
623
+ const ids = node.members.filter(isIdProperty);
609
624
  if (!ids.length || node.members.some(isIdKeyBrand)) {
610
625
  return;
611
626
  }
@@ -666,9 +681,9 @@ function rewriteProperty(node, ctx) {
666
681
  if (RELATION_DECORATORS.has(name) && options.kind === 'literal') {
667
682
  const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
668
683
  rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
669
- }
670
- if (TO_ONE_DECORATORS.has(name) && options.kind === 'literal') {
671
- addForeignKeyReference(options.node, propertyKey(node.name), owner, ctx, node);
684
+ if (TO_ONE_DECORATORS.has(name)) {
685
+ addForeignKeyReference(options.node, propertyKey(node.name), owner, ctx, node);
686
+ }
672
687
  }
673
688
  }
674
689
  if (decorators.length) {
@@ -969,9 +984,9 @@ export function transformFile(source, checker) {
969
984
  }
970
985
  if (ts.isCallExpression(node)) {
971
986
  rewriteDefineCall(node, ctx);
972
- }
973
- if (importsUql && ts.isCallExpression(node)) {
974
- rewriteBuilderCall(node, ctx);
987
+ if (importsUql) {
988
+ rewriteBuilderCall(node, ctx);
989
+ }
975
990
  }
976
991
  if (ts.isPropertyAssignment(node)) {
977
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.39.0",
5
+ "version": "0.39.1",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=24"