uql-codemod 0.50.1 → 0.51.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
@@ -31,12 +31,13 @@ Only `--flag=value` is read, and anything unrecognised is an error. It exits `0`
31
31
  - Members named by string to callbacks: `mappedBy: 'author'` to `(post) => post.author`, and `include`, `references` and `@Entity`/`defineEntity`'s `indexes` and `hooks` likewise: `{ columns: (post) => [post.title] }`.
32
32
  - `references: (post) => post.authorId` on a to-one (`@ManyToOne`, `@OneToOne`, or in `defineEntity`) 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.
33
33
  - A partial-index `where` string to `sql`, importing it, on entities and in the migration builder.
34
- - A class registered with `defineEntity(Post, { ... })` to the `defineEntity({ ... })` call it stands for, whole or not at all: `export const Post = defineEntity({ name: 'Post', fields })` with its sections chained, as a defined entity's options are above, and `export type Post = InstanceType<typeof Post>`. The class is removed and a table it named kept as `table`; a field stating no `type` takes the one its property declares (`authorId?: number | null` is `f.number().nullable()`), each relation becomes `author: () => rel.manyToOne(User, { ... })`, a relation aggregate a derived field (`(post) => ({ total: () => post.comments.count() })`), a hook's method a function of the row (`this` read as the row), and the same class's `defineField`, `defineId`, `defineRelation`, `defineHook`, `defineIndex`, `defineTrigger` and `defineFilter` are folded in.
34
+ - A class registered with `defineEntity(Post, { ... })` to the `defineEntity({ ... })` call it stands for, whole or not at all: `export const Post = defineEntity({ name: 'Post', fields })` with its sections chained, as a defined entity's options are above, and `export type Post = InstanceType<typeof Post>`. The class is removed and a table it named kept as `table`; a field stating no `type` takes the one its property declares (`authorId?: number | null` is `f.number().nullable()`), each relation becomes `author: () => rel.manyToOne(User, { ... })`, a relation aggregate an aggregate field (`(post) => ({ total: () => post.comments.count() })`), a hook's method a function of the row (`this` read as the row), and the same class's `defineField`, `defineId`, `defineRelation`, `defineHook`, `defineIndex`, `defineTrigger` and `defineFilter` are folded in.
35
35
  - A JSON index entry to the path read off the column's ref: `{ column: post.kind, jsonPath: { path: 'theme', type: Number } }` to `{ column: post.kind.theme, type: Number }`, and a `jsonArray`'s element type to `type: [String]`.
36
36
  - `col('cost')` to the ref it names, ``(product) => sql`${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.
37
37
  - `$exclude` to a `$select` leaving its fields out: `$exclude: { password: true }` to `$select: { password: false }`, at every depth of `$populate`.
38
38
  - An aggregate's `$agg` to `$select`, and each field it or `$text`'s `$fields` names to a key: `{ $sum: { amount: true } }`.
39
- - Renamed exports, at the import and every use: `raw` to `sql`, `QueryWhereMap` to `QueryWhere`, `IndexColumnSchema` to `IndexEntry`, `IndexColumnModifiers` to `IndexEntryModifiers`, `RelationKeyMap` to `KeyMap`, `FilterCondition` to `FilterWhere`, `SqlMigrationModuleOptions` to `MigrationModuleOptions`, `buildSqlQuerierMigrationModule` to `buildMigrationModule`, `QueryDialect` to `SqlQueryDialect`, `EngineFeatures` to `DialectFeatures`, `KnownMigratorDialect` to `DialectName`, `D1Preparer`/`D1Database` to `D1Queryable`, 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`, `Sqlite3QuerierPool` to `SqliteQuerierPool`, the migration builder's `I`-prefixed types to the unprefixed name (`IColumnBuilder`/`IColumnFactory`, and `uql-orm/migrate`'s `ColumnBuilder`/`ColumnFactory`, to `TableColumnBuilder`/`TableColumnFactory`), moving the import to the entry that exports it; and `MongoQuerier`, `isMongoQuerier` and `MongoSchemaIntrospector` to `uql-orm/mongodb`.
39
+ - An upsert's conflict keys to a query, `upsertOne(User, { email: true }, row)` to `upsertOne(User, { $conflict: { email: true } }, row)`, and its `{ update: X }` options to `X` as its fourth argument, in `upsertOne`, `upsertMany` and a trigger's `upsertInto`; `const { id } = await upsertOne(...)` to `const id`.
40
+ - Renamed exports, at the import and every use: `raw` to `sql`, `QueryWhereMap` to `QueryWhere`, `IndexColumnSchema` to `IndexEntry`, `IndexColumnModifiers` to `IndexEntryModifiers`, `RelationKeyMap` to `KeyMap`, `FilterCondition` to `FilterWhere`, `SqlMigrationModuleOptions` to `MigrationModuleOptions`, `buildSqlQuerierMigrationModule` to `buildMigrationModule`, `QueryDialect` to `SqlQueryDialect`, `EngineFeatures` to `DialectFeatures`, `KnownMigratorDialect` to `DialectName`, `QueryConflictPaths` to `QueryConflictKeys`, `D1Preparer`/`D1Database` to `D1Queryable`, 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`, `Sqlite3QuerierPool` to `SqliteQuerierPool`, the migration builder's `I`-prefixed types to the unprefixed name (`IColumnBuilder`/`IColumnFactory`, and `uql-orm/migrate`'s `ColumnBuilder`/`ColumnFactory`, to `TableColumnBuilder`/`TableColumnFactory`), moving the import to the entry that exports it; and `MongoQuerier`, `isMongoQuerier` and `MongoSchemaIntrospector` to `uql-orm/mongodb`.
40
41
  - Imports off `uql-orm/maria`, `uql-orm/mongo` and `uql-orm/bunSql` to `uql-orm/mariadb`, `uql-orm/mongodb` and `uql-orm/bun-sql`, and off `uql-orm/type`, `uql-orm/dialect`, `uql-orm/entity`, `uql-orm/querier` and `uql-orm/namingStrategy` to `uql-orm`.
41
42
  - The `[idKey]?: '...'` brand, and its import, on a key not called `id`, `_id` or `uuid` and on every composite.
42
43
 
package/dist/builders.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import ts from 'typescript';
2
2
  import { call, flag, flagOrCall } from './columnMethods.js';
3
3
  import { formatFindings, locationAt } from './context.js';
4
- import { FOREIGN_KEY_IN_DEFINED, NO_KEY_TYPE, nameClash, ON_DELETE_ON_RELATION } from './definedMessages.js';
4
+ import { UNJOINED_FOREIGN_KEY, NO_KEY_TYPE, nameClash, ON_DELETE_ON_RELATION } from './definedMessages.js';
5
5
  import { CHAINED_OPTIONS, FIRST_CALL_OPTIONS } from './definedOptions.js';
6
6
  import { applyEdits, editedText, replaced, replacingUses, reparsedAgain, unedited, } from './edits.js';
7
7
  import { importEdits } from './imports.js';
@@ -340,7 +340,7 @@ function definedColumn(node) {
340
340
  }
341
341
  const target = targetOf(references.call.arguments[0]);
342
342
  if (identifierText(references.receiver) === 'f' || !target) {
343
- return { message: target ? NO_KEY_TYPE : FOREIGN_KEY_IN_DEFINED };
343
+ return { message: target ? NO_KEY_TYPE : UNJOINED_FOREIGN_KEY };
344
344
  }
345
345
  const onDelete = methods.get('onDelete');
346
346
  const cut = [references, ...(onDelete ? [onDelete] : [])].map((method) => ({
@@ -360,7 +360,7 @@ function definedColumn(node) {
360
360
  const references = options.get('references');
361
361
  const target = references && targetOf(references);
362
362
  if (references && !target) {
363
- return { message: FOREIGN_KEY_IN_DEFINED };
363
+ return { message: UNJOINED_FOREIGN_KEY };
364
364
  }
365
365
  if (target && !options.has('type')) {
366
366
  return { message: NO_KEY_TYPE };
@@ -408,7 +408,7 @@ function claimKey(name, { target, onDelete }, claims, targets) {
408
408
  }
409
409
  const relation = ts.isIdentifier(name) ? /^(.+?)(?:Id|_id)$/.exec(key)?.[1] : undefined;
410
410
  if (!relation || claims.named.has(relation) || !targets.insertable) {
411
- return { claims, problem: FOREIGN_KEY_IN_DEFINED };
411
+ return { claims, problem: UNJOINED_FOREIGN_KEY };
412
412
  }
413
413
  const written = [
414
414
  `references: (${targets.param}) => ${targets.param}.${key}`,
@@ -1,7 +1,7 @@
1
1
  /** Why a name the codemod writes is refused: a file already binds it. */
2
2
  export declare const nameClash: (name: string) => string;
3
3
  /** Why a defined column's foreign key is refused: no relation joins on it, and none can be written for it. */
4
- export declare const FOREIGN_KEY_IN_DEFINED = "references another entity, and no relation can be named for it: declare one in '.relations()', as 'rel.manyToOne(Target, { references: (row) => row.key })'";
4
+ export declare const UNJOINED_FOREIGN_KEY = "references another entity, and no relation can be named for it: declare one in '.relations()', as 'rel.manyToOne(Target, { references: (row) => row.key })'";
5
5
  /** Why a joined column's `onDelete` is refused: its relation states another, or takes options the codemod cannot write into. */
6
6
  export declare const ON_DELETE_ON_RELATION = "joins through a relation that cannot take its 'onDelete': settle the action in that relation's options";
7
7
  /** Why a foreign key with no type is refused: a defined column states its own, which only its target's key implied. */
@@ -1,7 +1,7 @@
1
1
  /** Why a name the codemod writes is refused: a file already binds it. */
2
2
  export const nameClash = (name) => `'${name}' is already named here, and the codemod writes it`;
3
3
  /** Why a defined column's foreign key is refused: no relation joins on it, and none can be written for it. */
4
- export const FOREIGN_KEY_IN_DEFINED = "references another entity, and no relation can be named for it: declare one in '.relations()', as 'rel.manyToOne(Target, { references: (row) => row.key })'";
4
+ export const UNJOINED_FOREIGN_KEY = "references another entity, and no relation can be named for it: declare one in '.relations()', as 'rel.manyToOne(Target, { references: (row) => row.key })'";
5
5
  /** Why a joined column's `onDelete` is refused: its relation states another, or takes options the codemod cannot write into. */
6
6
  export const ON_DELETE_ON_RELATION = "joins through a relation that cannot take its 'onDelete': settle the action in that relation's options";
7
7
  /** Why a foreign key with no type is refused: a defined column states its own, which only its target's key implied. */
package/dist/imports.js CHANGED
@@ -76,6 +76,7 @@ export const SQL_TAG = 'sql';
76
76
  */
77
77
  const RENAMED_EXPORTS = new Map([
78
78
  ['QueryWhereMap', { to: 'QueryWhere' }],
79
+ ['QueryConflictPaths', { to: 'QueryConflictKeys' }],
79
80
  ['IndexColumnSchema', { to: 'IndexEntry' }],
80
81
  ['IndexColumnModifiers', { to: 'IndexEntryModifiers' }],
81
82
  ['RelationKeyMap', { to: 'KeyMap' }],
package/dist/transform.js CHANGED
@@ -428,19 +428,31 @@ function rewriteBuilderCall(call, ctx) {
428
428
  }
429
429
  const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
430
430
  /**
431
- * Rewrites an upsert's options holding `update` alone into that update, its fourth argument now. Read wherever it is
432
- * called, since a querier is seldom imported from uql where it is used. Any other fourth argument is already an
433
- * update, `{}` included, so a second run changes nothing.
431
+ * Rewrites an upsert's conflict keys into its query's `$conflict`, `{ $conflict: { sku: true } }`, and its options
432
+ * holding `update` alone into that update, its fourth argument now. Read wherever it is called, since a querier is
433
+ * seldom imported from uql where it is used. A query already naming `$conflict`, or typed with one, is left as it is,
434
+ * and any other fourth argument is already an update, `{}` included, so a second run changes nothing.
434
435
  */
435
436
  function rewriteUpsertCall(call, ctx) {
436
437
  const callee = call.expression;
437
438
  const name = identifierText(ts.isPropertyAccessExpression(callee) ? callee.name : callee);
439
+ if (!name || !UPSERT_CALLS.has(name)) {
440
+ return;
441
+ }
442
+ const keys = call.arguments[1];
443
+ if (call.arguments.length >= 3 && keys && isConflictKeys(keys, ctx)) {
444
+ ctx.edits.push(replaced(keys, ['{ $conflict: ', original(keys), ' }']));
445
+ }
438
446
  const options = call.arguments[3];
439
447
  const update = options && upsertUpdate(options);
440
- if (name && UPSERT_CALLS.has(name) && update) {
448
+ if (update) {
441
449
  ctx.edits.push(replaced(options, [original(update)]));
442
450
  }
443
451
  }
452
+ /** Whether `keys` is the old conflict map, `{ sku: true }`: an argument of its own, not yet a query naming `$conflict`. */
453
+ function isConflictKeys(keys, ctx) {
454
+ return !ts.isSpreadElement(keys) && !ctx.checker.getTypeAtLocation(keys).getProperty('$conflict');
455
+ }
444
456
  /** The value of options holding `update` alone, `{ update: value }` or `{ update }`. */
445
457
  function upsertUpdate(options) {
446
458
  const literal = objectLiteral(options);
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.50.1",
5
+ "version": "0.51.0",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=22.18"