uql-codemod 0.51.0 → 0.52.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
@@ -36,7 +36,7 @@ Only `--flag=value` is read, and anything unrecognised is an error. It exits `0`
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
- - 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`.
39
+ - An upsert's conflict keys to a query, `upsertOne(User, { email: true }, row)` to `upsertOne(User, { $conflict: { email: true } }, row)`, and its update, `{ update: X }` options or a fourth argument, to the query's `$update: X`, in `upsertOne`, `upsertMany` and a trigger's `upsertInto`; `const { id } = await upsertOne(...)` to `const id`.
40
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`.
41
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`.
42
42
  - The `[idKey]?: '...'` brand, and its import, on a key not called `id`, `_id` or `uuid` and on every composite.
@@ -3,7 +3,7 @@ import { type Pass } from './edits.js';
3
3
  export type BuildersResult = Pass & {
4
4
  readonly unresolved: string[];
5
5
  };
6
- /** Whether `node` is already a builder, `f.string()...`. */
6
+ /** Whether `node` is already a builder, `f.string()...`, or a chain off a `const` holding one. */
7
7
  export declare function isBuilder(node: ts.Expression): boolean;
8
8
  /** The names a builder writes, which the file must not bind otherwise. */
9
9
  export declare const BUILDER_NAMES: ReadonlySet<string>;
package/dist/builders.js CHANGED
@@ -6,7 +6,7 @@ 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';
8
8
  import { isCallback, paramFor } from './keyMaps.js';
9
- import { chainOf, decoratorName, decoratorsOf, FIELD_DECORATORS, identifierText, identifierUses, indentOf, indentUnit, keyOf, objectLiteral, objectProperties, optionsOf, propertiesByKey, propertyAssignment, propertyKey, propertyValue, } from './syntax.js';
9
+ import { chainOf, constValue, decoratorName, decoratorsOf, FIELD_DECORATORS, identifierText, identifierUses, indentOf, indentUnit, isNamedProperty, keyOf, namedValue, objectLiteral, objectProperties, optionsOf, propertiesByKey, propertyAssignment, propertyKey, propertyValue, } from './syntax.js';
10
10
  /** The column type each constructor implies, and the builder it is now. */
11
11
  const CONSTRUCTORS = {
12
12
  String: 'f.string()',
@@ -228,9 +228,14 @@ function computedDecorator(call, options, computed, findings, used) {
228
228
  used.add('f');
229
229
  return [replaced(call, `Computed((${param}) => ${built})`)];
230
230
  }
231
- /** Whether `node` is already a builder, `f.string()...`. */
231
+ /** Whether `node` is already a builder, `f.string()...`, or a chain off a `const` holding one. */
232
232
  export function isBuilder(node) {
233
- return identifierText(chainOf(node).root) === 'f';
233
+ const { root } = chainOf(node);
234
+ if (identifierText(root) === 'f') {
235
+ return true;
236
+ }
237
+ const held = ts.isIdentifier(root) ? constValue(root) : undefined;
238
+ return held !== undefined && isBuilder(held);
234
239
  }
235
240
  /** The options a defined entity's first call keeps. */
236
241
  const FIRST_CALL = new Set(FIRST_CALL_OPTIONS);
@@ -545,7 +550,7 @@ function appendedEntries(literal, entries, base) {
545
550
  }
546
551
  /** Whether a defined column is a builder naming no foreign key, which a defined entity's `fields` keep as they are. */
547
552
  function isDefinedColumn(prop) {
548
- return (ts.isPropertyAssignment(prop) && isBuilder(prop.initializer) && !chainMethods(prop.initializer).has('references'));
553
+ return isNamedProperty(prop) && isBuilder(namedValue(prop)) && !chainMethods(namedValue(prop)).has('references');
549
554
  }
550
555
  /** Each method a builder chain calls, by name. */
551
556
  function chainMethods(node) {
package/dist/syntax.d.ts CHANGED
@@ -6,6 +6,10 @@ export declare function propertyKey(name: ts.PropertyName): string | undefined;
6
6
  /** A property of an object literal written with its own name: a plain or quoted key, or a shorthand. */
7
7
  export type NamedProperty = ts.PropertyAssignment | ts.ShorthandPropertyAssignment;
8
8
  export declare function isNamedProperty(prop: ts.ObjectLiteralElementLike): prop is NamedProperty;
9
+ /** What a named property holds: a shorthand holds its name. */
10
+ export declare function namedValue(prop: NamedProperty): ts.Expression;
11
+ /** What the `const` `name` reads holds, where one is declared before it in a scope it sits in. */
12
+ export declare function constValue(name: ts.Identifier): ts.Expression | undefined;
9
13
  /** The property `name` of an object literal, a shorthand included. */
10
14
  export declare function namedProperty(node: ts.ObjectLiteralExpression, name: string): NamedProperty | undefined;
11
15
  /** The key a property is written under, a shorthand's included; nothing for a spread, a method or a computed key. */
package/dist/syntax.js CHANGED
@@ -10,6 +10,30 @@ export function propertyKey(name) {
10
10
  export function isNamedProperty(prop) {
11
11
  return ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop);
12
12
  }
13
+ /** What a named property holds: a shorthand holds its name. */
14
+ export function namedValue(prop) {
15
+ return ts.isShorthandPropertyAssignment(prop) ? prop.name : prop.initializer;
16
+ }
17
+ /** What the `const` `name` reads holds, where one is declared before it in a scope it sits in. */
18
+ export function constValue(name) {
19
+ for (let scope = name.parent; scope; scope = scope.parent) {
20
+ const statements = ts.isSourceFile(scope) || ts.isBlock(scope) || ts.isModuleBlock(scope) ? scope.statements : [];
21
+ for (const statement of statements) {
22
+ if (statement.end > name.pos) {
23
+ break;
24
+ }
25
+ if (!ts.isVariableStatement(statement)) {
26
+ continue;
27
+ }
28
+ const { declarationList } = statement;
29
+ const declared = declarationList.declarations.find((it) => identifierText(it.name) === name.text);
30
+ if (declared && declarationList.flags & ts.NodeFlags.Const) {
31
+ return declared.initializer;
32
+ }
33
+ }
34
+ }
35
+ return undefined;
36
+ }
13
37
  /** The property `name` of an object literal, a shorthand included. */
14
38
  export function namedProperty(node, name) {
15
39
  return node.properties.find((prop) => isNamedProperty(prop) && propertyKey(prop.name) === name);
@@ -45,7 +69,7 @@ export function propertiesByKey(node) {
45
69
  if (key === undefined) {
46
70
  return undefined;
47
71
  }
48
- props.set(key, ts.isShorthandPropertyAssignment(prop) ? prop.name : prop.initializer);
72
+ props.set(key, namedValue(prop));
49
73
  }
50
74
  return props;
51
75
  }
package/dist/transform.js CHANGED
@@ -14,7 +14,7 @@ import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByE
14
14
  import { toMigrationBuilders } from './migrationBuilders.js';
15
15
  import { relationTargetFor } from './relationTarget.js';
16
16
  import { statementTemplate } from './statementSql.js';
17
- import { arrayElements, decoratorName, decoratorsOf, FIELD_DECORATORS, identifierText, indentOf, namedProperty, objectLiteral, objectProperties, optionsOf, propertyKey, propertyValue, } from './syntax.js';
17
+ import { arrayElements, decoratorName, decoratorsOf, FIELD_DECORATORS, identifierText, indentOf, namedProperty, namedValue, objectLiteral, objectProperties, optionsOf, propertyKey, propertyValue, } from './syntax.js';
18
18
  const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
19
19
  const TO_ONE_DECORATORS = new Set(['OneToOne', 'ManyToOne']);
20
20
  const TO_ONE_CARDINALITIES = new Set(['11', 'm1']);
@@ -428,36 +428,65 @@ function rewriteBuilderCall(call, ctx) {
428
428
  }
429
429
  const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
430
430
  /**
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.
431
+ * Rewrites an upsert into one query: its conflict keys as `$conflict`, `{ $conflict: { sku: true } }`, and its update, a
432
+ * fourth argument or options holding `update` alone, as `$update`, the options after it kept. Read wherever it is
433
+ * called, since a querier is seldom imported from uql where it is used. A fourth argument holding only `returning` is
434
+ * already the options, and a query already naming `$conflict` keeps it, so a second run changes nothing.
435
435
  */
436
436
  function rewriteUpsertCall(call, ctx) {
437
437
  const callee = call.expression;
438
438
  const name = identifierText(ts.isPropertyAccessExpression(callee) ? callee.name : callee);
439
- if (!name || !UPSERT_CALLS.has(name)) {
439
+ const [, query, payload, fourth] = call.arguments;
440
+ if (!name || !UPSERT_CALLS.has(name) || !query || !payload || ts.isSpreadElement(query)) {
440
441
  return;
441
442
  }
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), ' }']));
443
+ const moved = fourth && !isWriteOptions(fourth, ctx) ? fourth : undefined;
444
+ const update = moved && !isUndefined(moved) ? (upsertUpdate(moved) ?? moved) : undefined;
445
+ const keys = isConflictKeys(query, ctx);
446
+ if (!keys && !moved) {
447
+ return;
448
+ }
449
+ const end = moved ?? query;
450
+ ctx.edits.push({
451
+ start: query.getStart(),
452
+ end: end.getEnd(),
453
+ text: [...upsertQuery(query, keys, update), ...(moved ? [{ start: query.getEnd(), end: payload.getEnd() }] : [])],
454
+ });
455
+ }
456
+ /** The parts of an upsert's query naming `update` as its `$update`: the old conflict map wrapped, a literal extended, else spread. */
457
+ function upsertQuery(query, keys, update) {
458
+ const named = update ? [', $update: ', original(update)] : [];
459
+ if (keys) {
460
+ return ['{ $conflict: ', original(query), ...named, ' }'];
445
461
  }
446
- const options = call.arguments[3];
447
- const update = options && upsertUpdate(options);
448
- if (update) {
449
- ctx.edits.push(replaced(options, [original(update)]));
462
+ if (!update) {
463
+ return [original(query)];
450
464
  }
465
+ const last = objectLiteral(query)?.properties.at(-1);
466
+ return last
467
+ ? [{ start: query.getStart(), end: last.getEnd() }, ...named, ' }']
468
+ : ['{ ...', original(query), ...named, ' }'];
451
469
  }
452
470
  /** Whether `keys` is the old conflict map, `{ sku: true }`: an argument of its own, not yet a query naming `$conflict`. */
453
471
  function isConflictKeys(keys, ctx) {
454
- return !ts.isSpreadElement(keys) && !ctx.checker.getTypeAtLocation(keys).getProperty('$conflict');
472
+ return !ctx.checker.getTypeAtLocation(keys).getProperty('$conflict');
473
+ }
474
+ /** Whether a fourth argument is already the write's options: something naming `returning` and nothing else. */
475
+ function isWriteOptions(node, ctx) {
476
+ const names = ctx.checker
477
+ .getTypeAtLocation(node)
478
+ .getProperties()
479
+ .map((property) => property.name);
480
+ return names.length > 0 && names.every((name) => name === 'returning');
481
+ }
482
+ function isUndefined(node) {
483
+ return identifierText(node) === 'undefined';
455
484
  }
456
485
  /** The value of options holding `update` alone, `{ update: value }` or `{ update }`. */
457
486
  function upsertUpdate(options) {
458
487
  const literal = objectLiteral(options);
459
488
  const property = literal?.properties.length === 1 ? namedProperty(literal, 'update') : undefined;
460
- return property && (ts.isPropertyAssignment(property) ? property.initializer : property.name);
489
+ return property && namedValue(property);
461
490
  }
462
491
  /**
463
492
  * `owner` is the declaring class's parameter and `target` the related one's, read off the `entity`
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.51.0",
5
+ "version": "0.52.0",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=22.18"