uql-codemod 0.50.1 → 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 +3 -2
- package/dist/builders.d.ts +1 -1
- package/dist/builders.js +13 -8
- package/dist/definedMessages.d.ts +1 -1
- package/dist/definedMessages.js +1 -1
- package/dist/imports.js +1 -0
- package/dist/syntax.d.ts +4 -0
- package/dist/syntax.js +25 -1
- package/dist/transform.js +50 -9
- package/package.json +1 -1
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
|
|
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
|
-
-
|
|
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
|
+
- 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.d.ts
CHANGED
|
@@ -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
|
@@ -1,12 +1,12 @@
|
|
|
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 {
|
|
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';
|
|
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
|
-
|
|
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);
|
|
@@ -340,7 +345,7 @@ function definedColumn(node) {
|
|
|
340
345
|
}
|
|
341
346
|
const target = targetOf(references.call.arguments[0]);
|
|
342
347
|
if (identifierText(references.receiver) === 'f' || !target) {
|
|
343
|
-
return { message: target ? NO_KEY_TYPE :
|
|
348
|
+
return { message: target ? NO_KEY_TYPE : UNJOINED_FOREIGN_KEY };
|
|
344
349
|
}
|
|
345
350
|
const onDelete = methods.get('onDelete');
|
|
346
351
|
const cut = [references, ...(onDelete ? [onDelete] : [])].map((method) => ({
|
|
@@ -360,7 +365,7 @@ function definedColumn(node) {
|
|
|
360
365
|
const references = options.get('references');
|
|
361
366
|
const target = references && targetOf(references);
|
|
362
367
|
if (references && !target) {
|
|
363
|
-
return { message:
|
|
368
|
+
return { message: UNJOINED_FOREIGN_KEY };
|
|
364
369
|
}
|
|
365
370
|
if (target && !options.has('type')) {
|
|
366
371
|
return { message: NO_KEY_TYPE };
|
|
@@ -408,7 +413,7 @@ function claimKey(name, { target, onDelete }, claims, targets) {
|
|
|
408
413
|
}
|
|
409
414
|
const relation = ts.isIdentifier(name) ? /^(.+?)(?:Id|_id)$/.exec(key)?.[1] : undefined;
|
|
410
415
|
if (!relation || claims.named.has(relation) || !targets.insertable) {
|
|
411
|
-
return { claims, problem:
|
|
416
|
+
return { claims, problem: UNJOINED_FOREIGN_KEY };
|
|
412
417
|
}
|
|
413
418
|
const written = [
|
|
414
419
|
`references: (${targets.param}) => ${targets.param}.${key}`,
|
|
@@ -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 (
|
|
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) {
|
|
@@ -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
|
|
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. */
|
package/dist/definedMessages.js
CHANGED
|
@@ -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
|
|
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/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,
|
|
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,24 +428,65 @@ function rewriteBuilderCall(call, ctx) {
|
|
|
428
428
|
}
|
|
429
429
|
const UPSERT_CALLS = new Set(['upsertOne', 'upsertMany', 'upsertInto']);
|
|
430
430
|
/**
|
|
431
|
-
* Rewrites an upsert
|
|
432
|
-
*
|
|
433
|
-
*
|
|
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.
|
|
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);
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
439
|
+
const [, query, payload, fourth] = call.arguments;
|
|
440
|
+
if (!name || !UPSERT_CALLS.has(name) || !query || !payload || ts.isSpreadElement(query)) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
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, ' }'];
|
|
461
|
+
}
|
|
462
|
+
if (!update) {
|
|
463
|
+
return [original(query)];
|
|
442
464
|
}
|
|
465
|
+
const last = objectLiteral(query)?.properties.at(-1);
|
|
466
|
+
return last
|
|
467
|
+
? [{ start: query.getStart(), end: last.getEnd() }, ...named, ' }']
|
|
468
|
+
: ['{ ...', original(query), ...named, ' }'];
|
|
469
|
+
}
|
|
470
|
+
/** Whether `keys` is the old conflict map, `{ sku: true }`: an argument of its own, not yet a query naming `$conflict`. */
|
|
471
|
+
function isConflictKeys(keys, ctx) {
|
|
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';
|
|
443
484
|
}
|
|
444
485
|
/** The value of options holding `update` alone, `{ update: value }` or `{ update }`. */
|
|
445
486
|
function upsertUpdate(options) {
|
|
446
487
|
const literal = objectLiteral(options);
|
|
447
488
|
const property = literal?.properties.length === 1 ? namedProperty(literal, 'update') : undefined;
|
|
448
|
-
return property && (
|
|
489
|
+
return property && namedValue(property);
|
|
449
490
|
}
|
|
450
491
|
/**
|
|
451
492
|
* `owner` is the declaring class's parameter and `target` the related one's, read off the `entity`
|