uql-codemod 0.37.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,10 +28,11 @@ 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`) 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.
31
32
  - A partial-index `where` string to `raw`, importing it, on entities and in the migration builder.
32
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.
33
34
  - An aggregate's `$agg` to `$select`, and each field it or `$text`'s `$fields` names to a key: `{ $sum: { amount: true } }`.
34
- - Renamed exports, at the import and every use: `QueryWhereMap` to `QueryWhere`, `RelationKeyMap` to `KeyMap`, `FilterCondition` to `FilterWhere`, `SqlMigrationModuleOptions` to `MigrationModuleOptions` and `buildSqlQuerierMigrationModule` to `buildMigrationModule`, and the removed driver classes to the one each extended: `PgDialect`/`NeonDialect`/`PgliteDialect` to `PostgresDialect`, `CrdbQuerier`/`NeonQuerier` to `PgQuerier`, `LibsqlQuerier`/`TursoQuerier` to `HranaQuerier`, `TursoLocalQuerier` to `SqliteQuerier` and `TursoDatabase` to `SqliteDatabase`, `MySql2Dialect` to `MySqlDialect`, `MongodbNativeDialect` to `MongoDialect`, moving the import to the entry that exports it.
35
+ - Renamed exports, at the import and every use: `QueryWhereMap` to `QueryWhere`, `RelationKeyMap` to `KeyMap`, `FilterCondition` to `FilterWhere`, `SqlMigrationModuleOptions` to `MigrationModuleOptions`, `buildSqlQuerierMigrationModule` to `buildMigrationModule`, `QueryDialect` to `SqlQueryDialect`, `EngineFeatures` to `DialectFeatures`, `KnownMigratorDialect` to `DialectName` and `D1Preparer` to `D1Database`, and the removed driver classes to the one each extended: `PgDialect`/`NeonDialect`/`PgliteDialect` to `PostgresDialect`, `CrdbQuerier`/`NeonQuerier` to `PgQuerier`, `LibsqlQuerier`/`TursoQuerier` to `HranaQuerier`, `TursoLocalQuerier` to `SqliteQuerier` and `TursoDatabase` to `SqliteDatabase`, `MySql2Dialect` to `MySqlDialect`, `MongodbNativeDialect` to `MongoDialect`, moving the import to the entry that exports it.
35
36
  - The `[idKey]?: '...'` brand, and its import, on a key not called `id`, `_id` or `uuid` and on every composite.
36
37
  - `import 'reflect-metadata'`, and `experimentalDecorators`/`emitDecoratorMetadata` in `tsconfig.json`, comments and formatting kept.
37
38
 
@@ -43,6 +44,8 @@ Reported with what to do, never guessed:
43
44
  - `@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`: a `@Transactional()` method becomes a `pool.transaction()` around its body, and only you know which pool.
44
45
  - An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...).
45
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: 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`.
46
49
  - A partial-index `where` string that interpolates, since `raw` would bind what the template spliced in.
47
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.
48
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/edits.d.ts CHANGED
@@ -13,6 +13,7 @@ export type Edit = {
13
13
  export declare function applyEdits(text: string, edits: readonly Edit[]): string;
14
14
  export declare function replaced(node: ts.Node, text: string): Edit;
15
15
  export declare function inserted(node: ts.Node, text: string): Edit;
16
+ export declare function appended(node: ts.Node, text: string): Edit;
16
17
  /**
17
18
  * Removes one element of a comma-separated list - a parameter, a named import, a JSON property - taking
18
19
  * the separator that would otherwise dangle. The last element gives up the comma before it; any other
package/dist/edits.js CHANGED
@@ -16,6 +16,9 @@ export function replaced(node, text) {
16
16
  export function inserted(node, text) {
17
17
  return { start: node.getStart(), end: node.getStart(), text };
18
18
  }
19
+ export function appended(node, text) {
20
+ return { start: node.getEnd(), end: node.getEnd(), text };
21
+ }
19
22
  /**
20
23
  * Removes one element of a comma-separated list - a parameter, a named import, a JSON property - taking
21
24
  * the separator that would otherwise dangle. The last element gives up the comma before it; any other
package/dist/transform.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import ts from 'typescript';
2
- import { applyEdits, inserted, removeFromList, replaced } from './edits.js';
2
+ import { appended, applyEdits, inserted, removeFromList, replaced } from './edits.js';
3
3
  import { columnExpressions, handNamedColumns, isRaw, rawTag, rawWhereEdit } from './entitySql.js';
4
4
  import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
5
5
  import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, propertyKey, propertyValue, quoted, referencesEdits, } from './keyMaps.js';
6
6
  const FIELD_DECORATORS = new Set(['Field', 'Id']);
7
7
  const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
8
+ const TO_ONE_DECORATORS = new Set(['OneToOne', 'ManyToOne']);
9
+ const TO_ONE_CARDINALITIES = new Set(['11', 'm1']);
8
10
  /**
9
11
  * Decorators that no longer exist, mapped to what to do instead. None is removed for you: what to put
10
12
  * in its place is a judgement call, and removing only the import would swap a clear error for a
@@ -43,6 +45,17 @@ const REMOVED_EXPORTS = new Map([
43
45
  'IndexColumnOptions',
44
46
  "an entity's index entry is `EntityIndexColumnInput<E>`, the migration builder's `IndexColumnInput`",
45
47
  ],
48
+ ['isKnownMigratorDialect', 'every dialect has a migrator, so it always held: drop the check'],
49
+ ['QuerierPoolDialect', "read the pool's own: `P['dialect']`"],
50
+ ['QuerierPoolQuerier', "read the pool's own: `Awaited<ReturnType<P['getQuerier']>>`"],
51
+ [
52
+ 'POSTGRES_WIRE_DRIVER_CAPABILITIES',
53
+ 'pass `driverCapabilities: { nativeArrays: false, explicitJsonCast: true }` to the dialect',
54
+ ],
55
+ ['MysqlLikeSqlDialect', 'extend `MySqlDialect` (`uql-orm/mysql`) or `MariaDialect` (`uql-orm/maria`)'],
56
+ ['D1Meta', "uql reads `D1Result['meta']`; the rest of a binding is typed by `@cloudflare/workers-types`"],
57
+ ['D1ExecResult', 'the rest of a binding is typed by `@cloudflare/workers-types`'],
58
+ ['createSchemaGenerator', 'use `new SqlSchemaGenerator(dialect)`, or `migrator.getSchemaGenerator()`'],
46
59
  ]);
47
60
  /**
48
61
  * Exports renamed and nothing else - the driver classes were empty subclasses - so the import and every
@@ -65,6 +78,10 @@ const RENAMED_EXPORTS = new Map([
65
78
  ['TursoDatabase', { to: 'SqliteDatabase', from: 'uql-orm/sqlite' }],
66
79
  ['SqlMigrationModuleOptions', { to: 'MigrationModuleOptions' }],
67
80
  ['buildSqlQuerierMigrationModule', { to: 'buildMigrationModule' }],
81
+ ['QueryDialect', { to: 'SqlQueryDialect' }],
82
+ ['EngineFeatures', { to: 'DialectFeatures' }],
83
+ ['KnownMigratorDialect', { to: 'DialectName', from: 'uql-orm' }],
84
+ ['D1Preparer', { to: 'D1Database' }],
68
85
  ]);
69
86
  /** A node's decorators, or none where it is a kind that cannot carry them. */
70
87
  function decoratorsOf(node) {
@@ -210,6 +227,64 @@ function addRelationEntity(decorator, node, ctx) {
210
227
  },
211
228
  });
212
229
  }
230
+ /**
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.
234
+ */
235
+ function addForeignKeyReference(relation, key, owner, ctx, property) {
236
+ const options = { kind: 'literal', node: relation };
237
+ const cardinality = propertyValue(relation, 'cardinality');
238
+ const toOne = !cardinality || (ts.isStringLiteralLike(cardinality) && TO_ONE_CARDINALITIES.has(cardinality.text));
239
+ const joined = ['mappedBy', 'through', 'references'].some((option) => findProperty(options, option));
240
+ const last = relation.properties.at(-1);
241
+ if (!key || !last || !toOne || joined) {
242
+ return;
243
+ }
244
+ 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);
255
+ }
256
+ ctx.edits.push(appended(last, `, references: (${owner.param}) => ${memberAccess(owner.param, column)}`));
257
+ }
258
+ /** `@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;
261
+ const targetType = checker.getNonNullableType(checker.getTypeAtLocation(property));
262
+ const target = entityGetterTarget(relation) ?? relationTargetFor(targetType, checker);
263
+ 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;
271
+ }
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}`);
276
+ }
277
+ /** 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) {
279
+ const written = key.declarations?.find(isIdProperty)?.type;
280
+ if (written && written.getSourceFile() === at.getSourceFile()) {
281
+ return written.getText();
282
+ }
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');
287
+ }
213
288
  /**
214
289
  * Writes the edits `rewrite` reads off `value`, which still names members as strings, or reports it
215
290
  * where it could not be read: what it holds is only known at runtime. A callback or an object literal
@@ -270,6 +345,7 @@ function rewriteDefineCall(call, ctx) {
270
345
  }
271
346
  else if (name === 'defineRelation' && third && ts.isObjectLiteralExpression(third)) {
272
347
  rewriteRelationOptions(third, owner.param, ctx);
348
+ addForeignKeyReference(third, second && ts.isStringLiteralLike(second) ? second.text : undefined, owner, ctx);
273
349
  }
274
350
  else if (name === 'defineFilter' && third) {
275
351
  renameOption(optionsOf(third), 'condition', 'where', call, ctx);
@@ -296,6 +372,7 @@ function rewriteEntityOptions(options, owner, ctx) {
296
372
  for (const relation of objectProperties(propertyValue(options, 'relations'))) {
297
373
  if (ts.isObjectLiteralExpression(relation.initializer)) {
298
374
  rewriteRelationOptions(relation.initializer, owner.param, ctx);
375
+ addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx);
299
376
  }
300
377
  }
301
378
  }
@@ -590,6 +667,9 @@ function rewriteProperty(node, ctx) {
590
667
  const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
591
668
  rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
592
669
  }
670
+ if (TO_ONE_DECORATORS.has(name) && options.kind === 'literal') {
671
+ addForeignKeyReference(options.node, propertyKey(node.name), owner, ctx, node);
672
+ }
593
673
  }
594
674
  if (decorators.length) {
595
675
  dropDeclare(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.37.0",
5
+ "version": "0.39.0",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=24"