uql-codemod 0.49.0 → 0.50.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 +6 -5
- package/dist/builders.d.ts +11 -1
- package/dist/builders.js +296 -101
- package/dist/definedEntity.d.ts +15 -0
- package/dist/{schemaEntity.js → definedEntity.js} +98 -38
- package/dist/definedMessages.d.ts +8 -0
- package/dist/definedMessages.js +8 -0
- package/dist/definedOptions.d.ts +6 -0
- package/dist/definedOptions.js +6 -0
- package/dist/definers.d.ts +1 -1
- package/dist/definers.js +1 -1
- package/dist/imports.d.ts +5 -2
- package/dist/imports.js +22 -4
- package/dist/transform.d.ts +3 -3
- package/dist/transform.js +15 -11
- package/package.json +1 -1
- package/dist/schemaEntity.d.ts +0 -12
- package/dist/schemaOptions.d.ts +0 -6
- package/dist/schemaOptions.js +0 -6
package/README.md
CHANGED
|
@@ -25,13 +25,13 @@ Only `--flag=value` is read, and anything unrecognised is an error. It exits `0`
|
|
|
25
25
|
## What it rewrites
|
|
26
26
|
|
|
27
27
|
- `virtual` to `computed` (on `@Field` and in `defineEntity`'s `fields`), a check's `expression` and a filter's `condition` to `where` (`@Entity`, `defineEntity`, `@Filter`), `raw('...')` to the `sql` tagged template, and a second argument onto `.as()`.
|
|
28
|
-
- A column's options to its builder, last, after the rewrites above: `@Id()` and `@Field({ type: String, nullable: true, length: 80 })` to `@Field(f.number().id())` and `@Field(f.string().nullable().length(80))`, a NOT NULL column losing its `nullable: false`, `computed` to `@Computed((row) => f.string().as(...))` or `@Computed((row) => row.books.count())`, and a
|
|
28
|
+
- A column's options to its builder, last, after the rewrites above: `@Id()` and `@Field({ type: String, nullable: true, length: 80 })` to `@Field(f.number().id())` and `@Field(f.string().nullable().length(80))`, a NOT NULL column losing its `nullable: false`, `computed` to `@Computed((row) => f.string().as(...))` or `@Computed((row) => row.books.count())`, and a defined entity's `fields` likewise; its other options to the sections chained after it, and its `aggregates` to `.relations()`' second callback. A defined column's foreign key becomes a relation: its `references` is dropped where a relation joins on the column (by `(order) => order.customerId`, by `[{ local, foreign }]` pairs, or as the ORM infers the one column naming its target, written into it), its `onDelete` moved into that relation, and where none joins, a `manyToOne` named as the column less its `Id` is written for it: `customer: () => rel.manyToOne(Customer, { references: (order) => order.customerId })`. Then each index, check and trigger entry, in `@Entity` and `defineEntity`, goes to the helper its callback is handed: `(post, index) => [index([post.title], 'by_title')]`.
|
|
29
29
|
- A migration's columns to the same methods: `table.string('email', { length: 80, nullable: true })` to `table.string('email').length(80).nullable()`, `.defaultValue()` to `.default()`, `.primaryKey()` to `.id()`, `.computed()` to `.as()`, and `.notNullable()` dropped.
|
|
30
30
|
- `@Index`, `@Trigger` and `@Filter` into `@Entity`'s `indexes`, `triggers` and `filters`, each decorator's comments kept above its entry; a class with no `@Entity` becomes `@Entity({ abstract: true, ... })`, a base that is never a table.
|
|
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
|
|
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.
|
|
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`.
|
|
@@ -51,16 +51,17 @@ Reported with what to do, never guessed:
|
|
|
51
51
|
- An `$exclude` it cannot read, or one beside a `$select`: merge it in, each field as `field: false`.
|
|
52
52
|
- A to-one onto a composite key: declare a column per key and pair each with it in `references`.
|
|
53
53
|
- A to-one whose `<relation>Id` is not a declared column: an undecorated property, or a `defineEntity` whose `fields` leave it out.
|
|
54
|
-
- A class `defineEntity` registers that
|
|
54
|
+
- A class `defineEntity` registers that `defineEntity({ ... })` cannot declare as it is: one that extends another class, has a method no hook runs or is decorated as well; options it cannot read, or a column or section its chain cannot write; a class registered in another file, or an entity that is no class declared in the file. It is left as written, each reason reported.
|
|
55
|
+
- A defined column's foreign key no relation can take: one whose relation name (the column less its `Id`) is taken or cannot be read off it, a key with no type of its own (`f.references(() => Order)`), or an `onDelete` its relation contradicts.
|
|
55
56
|
- A definer on a decorated class (`defineIndex(Post, ...)`), and `@Entity`'s `fields`, `relations`, `hooks` and `extends`: each is a decorator, or the class's own `extends`, now.
|
|
56
|
-
- A property typed more narrowly than
|
|
57
|
+
- A property typed more narrowly than `defineEntity` derives, as a note: a `Json<T>` column wants a Standard Schema, a string-literal union `enum`.
|
|
57
58
|
- A partial-index `where` string that interpolates, since `sql` would bind what the template spliced in.
|
|
58
59
|
- A JSON index entry whose `path` is not a string literal, or that takes an option besides `path`, `type` and `length`.
|
|
59
60
|
- 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.
|
|
60
61
|
- SQL in a definition that names a column by hand, as a note: a rename reaches a callback's refs, ``(user) => sql`lower(${user.email})` ``, and never the text.
|
|
61
62
|
- A file that already names something the codemod would write (`sql`, `refs`, `idKey`, `f`, a renamed export): left unchanged and reported, since the new import would shadow it or be shadowed.
|
|
62
63
|
- A property whose type maps to no column (`Json<T>`, a vector, a mixed union).
|
|
63
|
-
- A column option it cannot write as a method: `shape`
|
|
64
|
+
- A column option it cannot write as a method: a `shape` a JSON column no longer takes, a migration column's options passed as a variable, or a flag such as `unique: isUnique` that no literal states.
|
|
64
65
|
- A `@Field`/`@Id` with no `type`, and a relation with no `entity`: declare the column as a builder (`f.string()`) and name the target (`entity: () => Target`). A branded string id's column, `f.uuid()` or `f.string()`, is yours to pick.
|
|
65
66
|
- `Relation<T>` imported from `uql-orm`, which no longer exports it: write the property's type without it, and drop the import.
|
|
66
67
|
|
package/dist/builders.d.ts
CHANGED
|
@@ -8,7 +8,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>;
|
|
10
10
|
/**
|
|
11
|
-
* Writes entity definitions as column builders, `@Field(f.string().nullable())`, and a
|
|
11
|
+
* Writes entity definitions as column builders, `@Field(f.string().nullable())`, and a defined entity as its columns and a
|
|
12
12
|
* chain of sections. Read off the text the other rewrites produced, so it only moves what is written; whatever it
|
|
13
13
|
* cannot write faithfully is left as it is and reported.
|
|
14
14
|
*/
|
|
@@ -16,3 +16,13 @@ export declare function toBuilders(fileName: string, text: string, { locate, tak
|
|
|
16
16
|
readonly locate?: (at: number) => string;
|
|
17
17
|
readonly taken: ReadonlySet<string>;
|
|
18
18
|
}): BuildersResult;
|
|
19
|
+
/**
|
|
20
|
+
* A defined entity's statement, `const X = defineEntity({...})`, its call written as a chain and the builders it writes, or why
|
|
21
|
+
* it cannot be.
|
|
22
|
+
*/
|
|
23
|
+
export declare function chainedEntity(text: string): {
|
|
24
|
+
readonly text: string;
|
|
25
|
+
readonly builders: ReadonlySet<string>;
|
|
26
|
+
} | {
|
|
27
|
+
readonly reasons: readonly string[];
|
|
28
|
+
};
|
package/dist/builders.js
CHANGED
|
@@ -1,11 +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 {
|
|
5
|
-
import {
|
|
4
|
+
import { FOREIGN_KEY_IN_DEFINED, NO_KEY_TYPE, nameClash, ON_DELETE_ON_RELATION } from './definedMessages.js';
|
|
5
|
+
import { CHAINED_OPTIONS, FIRST_CALL_OPTIONS } from './definedOptions.js';
|
|
6
|
+
import { applyEdits, editedText, replaced, replacingUses, reparsedAgain, unedited, } from './edits.js';
|
|
7
|
+
import { importEdits } from './imports.js';
|
|
6
8
|
import { isCallback, paramFor } from './keyMaps.js';
|
|
7
|
-
import {
|
|
8
|
-
import { chainOf, decoratorName, decoratorsOf, FIELD_DECORATORS, identifierText, identifierUses, indentOf, indentUnit, keyOf, objectLiteral, optionsOf, propertiesByKey, propertyAssignment, propertyValue, } from './syntax.js';
|
|
9
|
+
import { chainOf, decoratorName, decoratorsOf, FIELD_DECORATORS, identifierText, identifierUses, indentOf, indentUnit, keyOf, objectLiteral, objectProperties, optionsOf, propertiesByKey, propertyAssignment, propertyKey, propertyValue, } from './syntax.js';
|
|
9
10
|
/** The column type each constructor implies, and the builder it is now. */
|
|
10
11
|
const CONSTRUCTORS = {
|
|
11
12
|
String: 'f.string()',
|
|
@@ -41,8 +42,8 @@ const METHODS = [
|
|
|
41
42
|
['onDelete', call('onDelete')],
|
|
42
43
|
];
|
|
43
44
|
/** What a constructor or `nullable()`/`id()` already said, so no method repeats it. */
|
|
44
|
-
const
|
|
45
|
-
const KNOWN = new Set([...
|
|
45
|
+
const SAID_BY_CONSTRUCTOR = new Set(['type', 'nullable', 'isId', 'enum', 'version', 'computed', 'stored']);
|
|
46
|
+
const KNOWN = new Set([...SAID_BY_CONSTRUCTOR, ...METHODS.map(([option]) => option)]);
|
|
46
47
|
/** The options a literal states, each by its key, or why it cannot be read. */
|
|
47
48
|
function optionsByKey(node) {
|
|
48
49
|
const options = optionsOf(node);
|
|
@@ -100,9 +101,6 @@ function decimalOf(text) {
|
|
|
100
101
|
function constructorOf(options, text) {
|
|
101
102
|
const type = text('type');
|
|
102
103
|
const members = text('enum');
|
|
103
|
-
if (options.has('shape') && type !== "'json'" && type !== "'jsonb'") {
|
|
104
|
-
return { message: "has a 'shape' on a column that is no JSON: only a JSON column holds a payload" };
|
|
105
|
-
}
|
|
106
104
|
if (options.has('version')) {
|
|
107
105
|
text('version');
|
|
108
106
|
if (type === undefined || type === 'Number')
|
|
@@ -134,8 +132,7 @@ function constructorOf(options, text) {
|
|
|
134
132
|
return NAMED_TYPES[literal];
|
|
135
133
|
}
|
|
136
134
|
if (literal === 'json' || literal === 'jsonb') {
|
|
137
|
-
|
|
138
|
-
return `f.${literal}(${shape ?? ''})`;
|
|
135
|
+
return `f.${literal}()`;
|
|
139
136
|
}
|
|
140
137
|
if (literal === 'vector') {
|
|
141
138
|
const dimensions = text('dimensions');
|
|
@@ -235,9 +232,9 @@ function computedDecorator(call, options, computed, findings, used) {
|
|
|
235
232
|
export function isBuilder(node) {
|
|
236
233
|
return identifierText(chainOf(node).root) === 'f';
|
|
237
234
|
}
|
|
238
|
-
/** The options a
|
|
235
|
+
/** The options a defined entity's first call keeps. */
|
|
239
236
|
const FIRST_CALL = new Set(FIRST_CALL_OPTIONS);
|
|
240
|
-
/** Every key
|
|
237
|
+
/** Every key `defineEntity`'s options may state, as its first call or as a section. */
|
|
241
238
|
const SCHEMA_KEYS = new Set([
|
|
242
239
|
...FIRST_CALL,
|
|
243
240
|
...CHAINED_OPTIONS,
|
|
@@ -245,14 +242,14 @@ const SCHEMA_KEYS = new Set([
|
|
|
245
242
|
'aggregates',
|
|
246
243
|
]);
|
|
247
244
|
/** `defineEntity({ fields: {...}, relations, ... })` as its first call and a chain of sections. */
|
|
248
|
-
function
|
|
245
|
+
function definedEdits(call, findings, used) {
|
|
249
246
|
const options = objectLiteral(call.arguments[0]);
|
|
250
247
|
const fields = options && objectLiteral(propertyValue(options, 'fields'));
|
|
251
248
|
const chained = options?.properties.some((prop) => !FIRST_CALL.has(keyOf(prop)));
|
|
252
|
-
if (!options || !fields || (!chained && fields.properties.every((prop) =>
|
|
249
|
+
if (!options || !fields || (!chained && fields.properties.every((prop) => isDefinedColumn(prop)))) {
|
|
253
250
|
return [];
|
|
254
251
|
}
|
|
255
|
-
const param =
|
|
252
|
+
const param = definedParam(call);
|
|
256
253
|
const unknown = options.properties.filter((prop) => !SCHEMA_KEYS.has(keyOf(prop)));
|
|
257
254
|
if (unknown.length) {
|
|
258
255
|
findings.push({
|
|
@@ -261,34 +258,59 @@ function schemaEdits(call, findings, used) {
|
|
|
261
258
|
});
|
|
262
259
|
return [];
|
|
263
260
|
}
|
|
264
|
-
const references = [];
|
|
265
261
|
const field = propertyAssignment(options, 'fields');
|
|
266
|
-
const
|
|
267
|
-
|
|
262
|
+
const relations = propertyValue(options, 'relations');
|
|
263
|
+
const aggregates = propertyValue(options, 'aggregates');
|
|
264
|
+
const read = fields.properties.map((prop) => ({
|
|
265
|
+
prop,
|
|
266
|
+
column: ts.isPropertyAssignment(prop) ? definedColumn(prop.initializer) : undefined,
|
|
267
|
+
}));
|
|
268
|
+
const { joins, inferred } = joinsOf(relations, targetsOf(read), param);
|
|
269
|
+
const targets = {
|
|
270
|
+
param,
|
|
271
|
+
rel: (relations && ts.isArrowFunction(relations) && identifierText(relations.parameters[0]?.name)) || 'rel',
|
|
272
|
+
joins,
|
|
273
|
+
insertable: !relations || returnedLiteral(relations) !== undefined,
|
|
274
|
+
};
|
|
275
|
+
const start = {
|
|
276
|
+
named: new Set([
|
|
277
|
+
fields,
|
|
278
|
+
objectLiteral(propertyValue(options, 'computed')),
|
|
279
|
+
returnedLiteral(relations),
|
|
280
|
+
returnedLiteral(aggregates),
|
|
281
|
+
]
|
|
282
|
+
.flatMap((literal) => objectProperties(literal))
|
|
283
|
+
.map((prop) => propertyKey(prop.name))),
|
|
284
|
+
added: [],
|
|
285
|
+
options: new Map([...inferred].map(([relation, reference]) => [relation, [reference]])),
|
|
286
|
+
};
|
|
287
|
+
const claimed = claimColumns(read, start, targets);
|
|
288
|
+
const columns = read.map(({ prop, column }) => {
|
|
289
|
+
if (!ts.isPropertyAssignment(prop) || !column) {
|
|
268
290
|
findings.push({ node: prop, message: `cannot write '${prop.getText()}' as a column: write its options inline` });
|
|
269
291
|
return undefined;
|
|
270
292
|
}
|
|
271
|
-
const key = prop.name.getText();
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
293
|
+
const key = propertyKey(prop.name) ?? prop.name.getText();
|
|
294
|
+
if ('message' in column) {
|
|
295
|
+
findings.push({ node: prop, message: `'${key}' ${column.message}` });
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
const problem = claimed.problems.get(prop);
|
|
299
|
+
if (problem) {
|
|
300
|
+
findings.push({ node: prop, message: `'${key}' ${problem}` });
|
|
277
301
|
return undefined;
|
|
278
302
|
}
|
|
279
|
-
return withValue(prop,
|
|
303
|
+
return withValue(prop, column.builder);
|
|
280
304
|
});
|
|
281
305
|
const sections = CHAINED_OPTIONS.flatMap((section) => {
|
|
282
306
|
const value = propertyValue(options, section);
|
|
283
307
|
return value ? [chainedSection(section, value, param, findings)] : [];
|
|
284
308
|
});
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (relations || aggregates) {
|
|
291
|
-
sections.push(`.relations(${relations?.getText() ?? '() => ({})'}${aggregates ? `, ${aggregates.getText()}` : ''})`);
|
|
309
|
+
const indent = indentOf(call);
|
|
310
|
+
const unit = indentUnit(call.getSourceFile());
|
|
311
|
+
const related = relationsText(relations, targets.rel, claimed.claims, fields.getText().includes('\n') ? `${indent}${unit}` : undefined, unit);
|
|
312
|
+
if (related || aggregates) {
|
|
313
|
+
sections.push(`.relations(${related ?? '() => ({})'}${aggregates ? `, ${aggregates.getText()}` : ''})`);
|
|
292
314
|
}
|
|
293
315
|
const written = columns.filter((it) => it !== undefined);
|
|
294
316
|
if (written.length !== columns.length || sections.some((it) => it === undefined)) {
|
|
@@ -298,69 +320,237 @@ function schemaEdits(call, findings, used) {
|
|
|
298
320
|
const kept = options.properties
|
|
299
321
|
.filter((prop) => FIRST_CALL.has(keyOf(prop)))
|
|
300
322
|
.map((prop) => (prop === field ? withValue(field, literal(fields, written)) : prop.getFullText()));
|
|
301
|
-
const indent = indentOf(call);
|
|
302
323
|
return [
|
|
303
|
-
replaced(call, `${call.expression.getText()}(${literal(options, kept)})${sections.map((it) => `\n${indent}
|
|
324
|
+
replaced(call, `${call.expression.getText()}(${literal(options, kept)})${sections.map((it) => `\n${indent}${unit}${it}`).join('')}`),
|
|
304
325
|
];
|
|
305
326
|
}
|
|
306
|
-
/** The parameter a
|
|
307
|
-
function
|
|
327
|
+
/** The parameter a defined entity's callbacks are named after: its `name` where that is a literal, else its variable's. */
|
|
328
|
+
function definedParam(call) {
|
|
308
329
|
const options = objectLiteral(call.arguments[0]);
|
|
309
330
|
const name = options && /^'(.+)'$/.exec(propertyValue(options, 'name')?.getText() ?? '')?.[1];
|
|
310
331
|
return paramFor(name ?? identifierText(variableOf(call)));
|
|
311
332
|
}
|
|
312
|
-
/** A
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
333
|
+
/** A defined column's builder off its options or chain, its `references` and `onDelete` taken out to move to a relation. */
|
|
334
|
+
function definedColumn(node) {
|
|
335
|
+
if (isBuilder(node)) {
|
|
336
|
+
const methods = chainMethods(node);
|
|
337
|
+
const references = methods.get('references');
|
|
338
|
+
if (!references) {
|
|
339
|
+
return { builder: node.getText() };
|
|
340
|
+
}
|
|
341
|
+
const target = targetOf(references.call.arguments[0]);
|
|
342
|
+
if (identifierText(references.receiver) === 'f' || !target) {
|
|
343
|
+
return { message: target ? NO_KEY_TYPE : FOREIGN_KEY_IN_DEFINED };
|
|
344
|
+
}
|
|
345
|
+
const onDelete = methods.get('onDelete');
|
|
346
|
+
const cut = [references, ...(onDelete ? [onDelete] : [])].map((method) => ({
|
|
347
|
+
start: method.receiver.getEnd() - node.getStart(),
|
|
348
|
+
end: method.call.getEnd() - node.getStart(),
|
|
349
|
+
text: '',
|
|
350
|
+
}));
|
|
351
|
+
return {
|
|
352
|
+
builder: applyEdits(node.getText(), cut),
|
|
353
|
+
key: { target, onDelete: onDelete?.call.arguments[0]?.getText() },
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const options = optionsByKey(node);
|
|
357
|
+
if (typeof options === 'string') {
|
|
358
|
+
return { message: `cannot be written as a builder because ${options}` };
|
|
359
|
+
}
|
|
360
|
+
const references = options.get('references');
|
|
361
|
+
const target = references && targetOf(references);
|
|
362
|
+
if (references && !target) {
|
|
363
|
+
return { message: FOREIGN_KEY_IN_DEFINED };
|
|
364
|
+
}
|
|
365
|
+
if (target && !options.has('type')) {
|
|
366
|
+
return { message: NO_KEY_TYPE };
|
|
367
|
+
}
|
|
368
|
+
const built = columnBuilder(new Map([...options].filter(([option]) => !target || (option !== 'references' && option !== 'onDelete'))), false);
|
|
369
|
+
if (typeof built !== 'string') {
|
|
370
|
+
return built;
|
|
371
|
+
}
|
|
372
|
+
return target
|
|
373
|
+
? { builder: built, key: { target, onDelete: options.get('onDelete')?.getText() } }
|
|
374
|
+
: { builder: built };
|
|
375
|
+
}
|
|
376
|
+
/** The entity a `() => Target` getter names, as its text. */
|
|
377
|
+
function targetOf(getter) {
|
|
378
|
+
return getter && ts.isArrowFunction(getter) && !ts.isBlock(getter.body) ? getter.body.getText() : undefined;
|
|
379
|
+
}
|
|
380
|
+
/** Each column's foreign key target, for the columns naming one. */
|
|
381
|
+
function targetsOf(read) {
|
|
382
|
+
return new Map(read.flatMap(({ prop, column }) => column && 'key' in column && column.key ? [[prop.name?.getText(), column.key.target]] : []));
|
|
383
|
+
}
|
|
384
|
+
/** The defined entity's columns' foreign keys claimed in turn, the problem of each column by its name node. */
|
|
385
|
+
function claimColumns(read, start, targets) {
|
|
386
|
+
const problems = new Map();
|
|
387
|
+
const claims = read.reduce((state, { prop, column }) => {
|
|
388
|
+
if (!ts.isPropertyAssignment(prop) || !column || !('key' in column) || !column.key) {
|
|
389
|
+
return state;
|
|
390
|
+
}
|
|
391
|
+
const outcome = claimKey(prop.name, column.key, state, targets);
|
|
392
|
+
if (outcome.problem) {
|
|
393
|
+
problems.set(prop, outcome.problem);
|
|
394
|
+
}
|
|
395
|
+
return outcome.claims;
|
|
396
|
+
}, start);
|
|
397
|
+
return { claims, problems };
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* A column's foreign key moved into the to-one joining on it, with its `onDelete`, else into a `manyToOne` written for
|
|
401
|
+
* it, named as the column less its `Id`; or why it cannot be.
|
|
402
|
+
*/
|
|
403
|
+
function claimKey(name, { target, onDelete }, claims, targets) {
|
|
404
|
+
const key = name.getText();
|
|
405
|
+
const join = targets.joins.get(key);
|
|
406
|
+
if (join) {
|
|
407
|
+
return onDelete ? movedOnDelete(join, onDelete, claims) : { claims };
|
|
408
|
+
}
|
|
409
|
+
const relation = ts.isIdentifier(name) ? /^(.+?)(?:Id|_id)$/.exec(key)?.[1] : undefined;
|
|
410
|
+
if (!relation || claims.named.has(relation) || !targets.insertable) {
|
|
411
|
+
return { claims, problem: FOREIGN_KEY_IN_DEFINED };
|
|
412
|
+
}
|
|
413
|
+
const written = [
|
|
414
|
+
`references: (${targets.param}) => ${targets.param}.${key}`,
|
|
415
|
+
...(onDelete ? [`onDelete: ${onDelete}`] : []),
|
|
416
|
+
];
|
|
417
|
+
return {
|
|
418
|
+
claims: {
|
|
419
|
+
...claims,
|
|
420
|
+
named: new Set([...claims.named, relation]),
|
|
421
|
+
added: [...claims.added, `${relation}: () => ${targets.rel}.manyToOne(${target}, { ${written.join(', ')} })`],
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
/** Writes a column's `onDelete` into the to-one joining on it, or says why not: it states another, or options none can be written into. */
|
|
426
|
+
function movedOnDelete(relation, onDelete, claims) {
|
|
427
|
+
const written = `onDelete: ${onDelete}`;
|
|
428
|
+
const options = relation.arguments[1];
|
|
429
|
+
const literal = objectLiteral(options);
|
|
430
|
+
const stated = literal ? propertyValue(literal, 'onDelete')?.getText() : undefined;
|
|
431
|
+
const current = claims.options.get(relation) ?? [];
|
|
432
|
+
const pending = current.find((option) => option.startsWith('onDelete: '));
|
|
433
|
+
if ((options && !literal) || (stated ?? onDelete) !== onDelete || (pending ?? written) !== written) {
|
|
434
|
+
return { claims, problem: ON_DELETE_ON_RELATION };
|
|
435
|
+
}
|
|
436
|
+
return stated || pending
|
|
437
|
+
? { claims }
|
|
438
|
+
: { claims: { ...claims, options: new Map(claims.options).set(relation, [...current, written]) } };
|
|
439
|
+
}
|
|
440
|
+
/** The relation calls a defined entity's `relations` returns, one per relation. */
|
|
441
|
+
function relationCalls(relations) {
|
|
442
|
+
return objectProperties(returnedLiteral(relations)).flatMap((prop) => {
|
|
443
|
+
const relation = ts.isArrowFunction(prop.initializer) ? prop.initializer.body : undefined;
|
|
444
|
+
return relation && ts.isCallExpression(relation) ? [relation] : [];
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/** The object literal a callback returns, `(rel) => ({ ... })`, or nothing where it returns none so. */
|
|
448
|
+
function returnedLiteral(callback) {
|
|
449
|
+
const body = callback && ts.isArrowFunction(callback) && !ts.isBlock(callback.body) ? callback.body : undefined;
|
|
450
|
+
return objectLiteral(body && ts.isParenthesizedExpression(body) ? body.expression : body);
|
|
318
451
|
}
|
|
319
452
|
/**
|
|
320
|
-
*
|
|
321
|
-
*
|
|
453
|
+
* The to-one joining on each column: by the `references` it names, `(row) => row.column` or pairs of `local`s, else by
|
|
454
|
+
* the ORM's inference, the one column whose foreign key names its target, whose `references` is written as inferred.
|
|
322
455
|
*/
|
|
323
|
-
function
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
456
|
+
function joinsOf(relations, targets, param) {
|
|
457
|
+
const joins = new Map();
|
|
458
|
+
const inferred = new Map();
|
|
459
|
+
for (const relation of relationCalls(relations)) {
|
|
460
|
+
const named = namedColumns(relation);
|
|
461
|
+
for (const column of named ?? []) {
|
|
462
|
+
if (!joins.has(column))
|
|
463
|
+
joins.set(column, relation);
|
|
464
|
+
}
|
|
465
|
+
const target = relation.arguments[0]?.getText();
|
|
466
|
+
const matching = [...targets].filter(([, named]) => named === target).map(([key]) => key);
|
|
467
|
+
const column = matching.length === 1 ? matching[0] : undefined;
|
|
468
|
+
if (!named && isToOne(relation) && writableOptions(relation) && column && !joins.has(column)) {
|
|
469
|
+
inferred.set(relation, `references: (${param}) => ${param}.${column}`);
|
|
470
|
+
joins.set(column, relation);
|
|
471
|
+
}
|
|
327
472
|
}
|
|
328
|
-
|
|
329
|
-
|
|
473
|
+
return { joins, inferred };
|
|
474
|
+
}
|
|
475
|
+
/** The columns a relation's `references` names on its local side, or nothing where it names none it can read. */
|
|
476
|
+
function namedColumns(relation) {
|
|
477
|
+
const options = objectLiteral(relation.arguments[1]);
|
|
478
|
+
const references = options && propertyValue(options, 'references');
|
|
479
|
+
if (!references) {
|
|
480
|
+
return undefined;
|
|
330
481
|
}
|
|
331
|
-
|
|
332
|
-
|
|
482
|
+
const local = ts.isArrowFunction(references) ? identifierText(references.parameters[0]?.name) : undefined;
|
|
483
|
+
const column = (node) => node && ts.isPropertyAccessExpression(node) && identifierText(node.expression) === local
|
|
484
|
+
? node.name.text
|
|
485
|
+
: undefined;
|
|
486
|
+
const body = local && ts.isArrowFunction(references) ? references.body : undefined;
|
|
487
|
+
if (body && ts.isArrayLiteralExpression(body)) {
|
|
488
|
+
const locals = body.elements.map((pair) => {
|
|
489
|
+
const literal = objectLiteral(pair);
|
|
490
|
+
return column(literal && propertyValue(literal, 'local'));
|
|
491
|
+
});
|
|
492
|
+
return locals.every((it) => it !== undefined) ? locals : [];
|
|
493
|
+
}
|
|
494
|
+
const single = body && ts.isExpression(body) ? column(body) : undefined;
|
|
495
|
+
return single ? [single] : [];
|
|
496
|
+
}
|
|
497
|
+
function isToOne(relation) {
|
|
498
|
+
return ts.isPropertyAccessExpression(relation.expression) && relation.expression.name.text === 'manyToOne';
|
|
499
|
+
}
|
|
500
|
+
/** Whether a relation's options are absent or a literal, the only kind an option can be written into. */
|
|
501
|
+
function writableOptions(relation) {
|
|
502
|
+
return relation.arguments.length < 2 || objectLiteral(relation.arguments[1]) !== undefined;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* The relations' text, each to-one's options written into its relation and each relation added for a foreign key
|
|
506
|
+
* appended, or nothing where there are none. A section of its own is laid out a line per relation, a `unit` in from
|
|
507
|
+
* the section's `indent`, where one is given.
|
|
508
|
+
*/
|
|
509
|
+
function relationsText(relations, rel, { options, added }, indent, unit) {
|
|
510
|
+
if (!relations) {
|
|
511
|
+
if (!added.length)
|
|
512
|
+
return undefined;
|
|
513
|
+
const listed = indent === undefined
|
|
514
|
+
? `{ ${added.join(', ')} }`
|
|
515
|
+
: `{\n${added.map((entry) => `${indent}${unit}${entry},\n`).join('')}${indent}}`;
|
|
516
|
+
return `(${rel}) => (${listed})`;
|
|
517
|
+
}
|
|
518
|
+
const base = relations.getStart();
|
|
519
|
+
const edits = [...options].flatMap(([relation, written]) => written.length ? [withOptions(relation, written, base)] : []);
|
|
520
|
+
const literal = returnedLiteral(relations);
|
|
521
|
+
if (literal && added.length) {
|
|
522
|
+
edits.push(appendedEntries(literal, added, base));
|
|
523
|
+
}
|
|
524
|
+
return applyEdits(relations.getText(), edits);
|
|
525
|
+
}
|
|
526
|
+
/** The edit writing `options` into a relation call: after its options' last, else as options after its target. */
|
|
527
|
+
function withOptions(relation, options, base) {
|
|
528
|
+
const literal = objectLiteral(relation.arguments[1]);
|
|
529
|
+
const last = literal?.properties.at(-1);
|
|
530
|
+
if (literal && !last) {
|
|
531
|
+
return { start: literal.getStart() - base, end: literal.getEnd() - base, text: `{ ${options.join(', ')} }` };
|
|
532
|
+
}
|
|
533
|
+
const at = (last ?? relation.arguments[0]).getEnd() - base;
|
|
534
|
+
return { start: at, end: at, text: last ? `, ${options.join(', ')}` : `, { ${options.join(', ')} }` };
|
|
333
535
|
}
|
|
334
|
-
/**
|
|
335
|
-
function
|
|
336
|
-
|
|
536
|
+
/** The edit appending `entries` to an object literal, laid out as its own entries are. */
|
|
537
|
+
function appendedEntries(literal, entries, base) {
|
|
538
|
+
const last = literal.properties.at(-1);
|
|
539
|
+
if (!last) {
|
|
540
|
+
return { start: literal.getStart() - base, end: literal.getEnd() - base, text: `{ ${entries.join(', ')} }` };
|
|
541
|
+
}
|
|
542
|
+
const separator = literal.getText().includes('\n') ? `,\n${indentOf(last)}` : ', ';
|
|
543
|
+
const at = last.getEnd() - base;
|
|
544
|
+
return { start: at, end: at, text: entries.map((entry) => `${separator}${entry}`).join('') };
|
|
337
545
|
}
|
|
338
|
-
/** Whether a
|
|
339
|
-
function
|
|
546
|
+
/** Whether a defined column is a builder naming no foreign key, which a defined entity's `fields` keep as they are. */
|
|
547
|
+
function isDefinedColumn(prop) {
|
|
340
548
|
return (ts.isPropertyAssignment(prop) && isBuilder(prop.initializer) && !chainMethods(prop.initializer).has('references'));
|
|
341
549
|
}
|
|
342
550
|
/** Each method a builder chain calls, by name. */
|
|
343
551
|
function chainMethods(node) {
|
|
344
552
|
return new Map(chainOf(node).methods.map((method) => [method.name, method]));
|
|
345
553
|
}
|
|
346
|
-
/** A builder in `fields` with its foreign key, `.references(() => X)` and `.onDelete()`, moved into `.references()`. */
|
|
347
|
-
function builtSchemaColumn(node, key, references) {
|
|
348
|
-
const methods = chainMethods(node);
|
|
349
|
-
const target = methods.get('references');
|
|
350
|
-
if (!target) {
|
|
351
|
-
return node.getText();
|
|
352
|
-
}
|
|
353
|
-
if (identifierText(target.receiver) === 'f') {
|
|
354
|
-
return { message: "references another entity but states no type: write its builder, as its key's" };
|
|
355
|
-
}
|
|
356
|
-
const onDelete = methods.get('onDelete');
|
|
357
|
-
references.push(referenceEntry(key, target.call.arguments[0]?.getText(), onDelete?.call.arguments[0]?.getText()));
|
|
358
|
-
// Each method is cut out with the chain before it: from that receiver's end to the call's.
|
|
359
|
-
const cut = [onDelete, target].flatMap((method) => method
|
|
360
|
-
? [{ start: method.receiver.getEnd() - node.getStart(), end: method.call.getEnd() - node.getStart(), text: '' }]
|
|
361
|
-
: []);
|
|
362
|
-
return applyEdits(node.getText(), cut);
|
|
363
|
-
}
|
|
364
554
|
/**
|
|
365
555
|
* An object literal of `properties`, each its full text with its comments, laid out as `node` was: on one line
|
|
366
556
|
* where it was written on one, a property a line otherwise.
|
|
@@ -490,7 +680,7 @@ function entityEdits(cls, findings) {
|
|
|
490
680
|
});
|
|
491
681
|
});
|
|
492
682
|
}
|
|
493
|
-
/** A
|
|
683
|
+
/** A defined entity's computed map as what its section returns: each a column `as()` its SQL. */
|
|
494
684
|
function computedSection(value, param) {
|
|
495
685
|
const map = objectLiteral(value);
|
|
496
686
|
const entries = map?.properties.map((prop) => {
|
|
@@ -524,7 +714,7 @@ export const BUILDER_NAMES = new Set(['f', 'Computed']);
|
|
|
524
714
|
/** Whether the file declares anything this pass rewrites. */
|
|
525
715
|
const REWRITES = /@(?:Field|Id)\(|\bdefineEntity\(\s*\{|@Entity\(\s*\{/;
|
|
526
716
|
/**
|
|
527
|
-
* Writes entity definitions as column builders, `@Field(f.string().nullable())`, and a
|
|
717
|
+
* Writes entity definitions as column builders, `@Field(f.string().nullable())`, and a defined entity as its columns and a
|
|
528
718
|
* chain of sections. Read off the text the other rewrites produced, so it only moves what is written; whatever it
|
|
529
719
|
* cannot write faithfully is left as it is and reported.
|
|
530
720
|
*/
|
|
@@ -544,9 +734,9 @@ export function toBuilders(fileName, text, { locate, taken }) {
|
|
|
544
734
|
edits.push(...entityEdits(node, findings));
|
|
545
735
|
}
|
|
546
736
|
if (ts.isCallExpression(node) && identifierText(node.expression) === 'defineEntity') {
|
|
547
|
-
const
|
|
548
|
-
if (
|
|
549
|
-
edits.push(...
|
|
737
|
+
const defined = definedEdits(node, findings, used);
|
|
738
|
+
if (defined.length) {
|
|
739
|
+
edits.push(...defined);
|
|
550
740
|
return;
|
|
551
741
|
}
|
|
552
742
|
}
|
|
@@ -562,27 +752,32 @@ export function toBuilders(fileName, text, { locate, taken }) {
|
|
|
562
752
|
if (clash) {
|
|
563
753
|
return {
|
|
564
754
|
...unedited(text),
|
|
565
|
-
unresolved: [
|
|
566
|
-
...unresolved,
|
|
567
|
-
`${located(0)}: '${clash}' is already named here, and the codemod writes it: rename that, then run again`,
|
|
568
|
-
],
|
|
755
|
+
unresolved: [...unresolved, `${located(0)}: ${nameClash(clash)}: rename that, then run again`],
|
|
569
756
|
};
|
|
570
757
|
}
|
|
571
|
-
return {
|
|
758
|
+
return {
|
|
759
|
+
...reparsedAgain(fileName, editedText(text, edits), (edited) => importEdits(edited, used, FIELD_DECORATORS)),
|
|
760
|
+
unresolved,
|
|
761
|
+
};
|
|
572
762
|
}
|
|
573
|
-
/**
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
763
|
+
/**
|
|
764
|
+
* A defined entity's statement, `const X = defineEntity({...})`, its call written as a chain and the builders it writes, or why
|
|
765
|
+
* it cannot be.
|
|
766
|
+
*/
|
|
767
|
+
export function chainedEntity(text) {
|
|
768
|
+
const source = ts.createSourceFile('/entity.ts', text, ts.ScriptTarget.Latest, true);
|
|
769
|
+
const findings = [];
|
|
770
|
+
const used = new Set();
|
|
771
|
+
const edits = [];
|
|
772
|
+
const visit = (node) => {
|
|
773
|
+
if (ts.isCallExpression(node) && identifierText(node.expression) === 'defineEntity') {
|
|
774
|
+
edits.push(...definedEdits(node, findings, used));
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
ts.forEachChild(node, visit);
|
|
778
|
+
};
|
|
779
|
+
visit(source);
|
|
780
|
+
return findings.length
|
|
781
|
+
? { reasons: findings.map(({ message }) => message) }
|
|
782
|
+
: { text: applyEdits(text, edits), builders: used };
|
|
588
783
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Pass } from './edits.js';
|
|
2
|
+
export type DefinedResult = Pass & {
|
|
3
|
+
readonly unresolved: string[];
|
|
4
|
+
readonly notes: string[];
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Declares each class `defineEntity(Class, options)` registered as `defineEntity({ name, fields })` and its
|
|
8
|
+
* chain of sections, folding the string-keyed definers of the same class in. Read off the text the other rewrites
|
|
9
|
+
* produced, whose options already name members by callback, so it only moves what is written. An entity it cannot
|
|
10
|
+
* declare faithfully, `taken` naming a builder it writes among them, is left as written and reported.
|
|
11
|
+
*/
|
|
12
|
+
export declare function toDefinedEntities(fileName: string, text: string, { locate, taken }: {
|
|
13
|
+
readonly locate?: (at: number) => string;
|
|
14
|
+
readonly taken: ReadonlySet<string>;
|
|
15
|
+
}): DefinedResult;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
+
import { chainedEntity } from './builders.js';
|
|
2
3
|
import { formatFindings, locationAt } from './context.js';
|
|
4
|
+
import { nameClash } from './definedMessages.js';
|
|
5
|
+
import { KEPT_OPTIONS } from './definedOptions.js';
|
|
3
6
|
import { CLASS_DEFINERS, definerCall } from './definers.js';
|
|
4
7
|
import { editedText, removeStatement, replaced, replacingUses, reparsedAgain, unedited, } from './edits.js';
|
|
5
|
-
import {
|
|
8
|
+
import { importEdits } from './imports.js';
|
|
6
9
|
import { memberKey, paramFor, quoted } from './keyMaps.js';
|
|
7
|
-
import { KEPT_OPTIONS } from './schemaOptions.js';
|
|
8
10
|
import { arrayElements, decoratorsOf, freeName, hasModifier, identifierText, identifierUses, indentOf, indentUnit, isNamedProperty, objectLiteral, objectProperties, propertyKey, propertyValue, } from './syntax.js';
|
|
9
|
-
/** Imports a converted file may no longer read: the definers folded in, and the brands a
|
|
11
|
+
/** Imports a converted file may no longer read: the definers folded in, and the brands a defined entity derives. */
|
|
10
12
|
const FOLDED_IMPORTS = new Set([
|
|
11
13
|
...Object.keys(CLASS_DEFINERS).filter((name) => name !== 'defineEntity'),
|
|
12
14
|
'idKey',
|
|
@@ -47,12 +49,12 @@ function readsAs(value, shape) {
|
|
|
47
49
|
/** What a class-form relation said that a `rel` builder says by its name and first argument. */
|
|
48
50
|
const RELATION_SHAPE = new Set(['cardinality', 'entity']);
|
|
49
51
|
/**
|
|
50
|
-
* Declares each class `defineEntity(Class, options)` registered as
|
|
51
|
-
* folding the string-keyed definers of the same class in. Read off the text the other rewrites
|
|
52
|
-
* whose options already name members by callback, so it only moves what is written. An entity it cannot
|
|
53
|
-
* declare faithfully is left as written and reported.
|
|
52
|
+
* Declares each class `defineEntity(Class, options)` registered as `defineEntity({ name, fields })` and its
|
|
53
|
+
* chain of sections, folding the string-keyed definers of the same class in. Read off the text the other rewrites
|
|
54
|
+
* produced, whose options already name members by callback, so it only moves what is written. An entity it cannot
|
|
55
|
+
* declare faithfully, `taken` naming a builder it writes among them, is left as written and reported.
|
|
54
56
|
*/
|
|
55
|
-
export function
|
|
57
|
+
export function toDefinedEntities(fileName, text, { locate, taken }) {
|
|
56
58
|
if (!CLASS_FORM_CALL.test(text)) {
|
|
57
59
|
return { ...unedited(text), unresolved: [], notes: [] };
|
|
58
60
|
}
|
|
@@ -62,6 +64,7 @@ export function toSchemaEntities(fileName, text, locate) {
|
|
|
62
64
|
const unresolved = [];
|
|
63
65
|
const notes = [];
|
|
64
66
|
const edits = [];
|
|
67
|
+
const written = new Set();
|
|
65
68
|
const registrations = collectRegistrations(source, unresolved);
|
|
66
69
|
const entityCallOf = (calls) => calls.find(({ definer }) => definer === 'defineEntity')?.call;
|
|
67
70
|
for (const [name, { cls, calls }] of registrations) {
|
|
@@ -79,26 +82,38 @@ export function toSchemaEntities(fileName, text, locate) {
|
|
|
79
82
|
});
|
|
80
83
|
continue;
|
|
81
84
|
}
|
|
82
|
-
const
|
|
83
|
-
if ('message' in
|
|
84
|
-
unresolved.push(
|
|
85
|
+
const defined = definedOf(name, cls, calls, entityCall, unit);
|
|
86
|
+
if ('message' in defined) {
|
|
87
|
+
unresolved.push(defined);
|
|
85
88
|
continue;
|
|
86
89
|
}
|
|
87
|
-
|
|
90
|
+
const chained = chainedEntity(defined.text);
|
|
91
|
+
if (!('text' in chained)) {
|
|
92
|
+
unresolved.push(...chained.reasons.map((reason) => ({ node: entityCall, message: `'${name}' is left as written: ${reason}` })));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const clash = [...chained.builders].find((builder) => taken.has(builder));
|
|
96
|
+
if (clash !== undefined) {
|
|
97
|
+
unresolved.push({ node: entityCall, message: `'${name}' is left as written: ${nameClash(clash)}` });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const builder of chained.builders)
|
|
101
|
+
written.add(builder);
|
|
102
|
+
notes.push(...defined.notes);
|
|
88
103
|
edits.push(removeStatement(cls));
|
|
89
104
|
for (const { call } of calls) {
|
|
90
|
-
edits.push(call === entityCall ? replaced(call.parent, indented(
|
|
105
|
+
edits.push(call === entityCall ? replaced(call.parent, indented(chained.text, call.parent)) : removeStatement(call.parent));
|
|
91
106
|
}
|
|
92
107
|
}
|
|
93
108
|
return {
|
|
94
109
|
...(edits.length
|
|
95
|
-
? reparsedAgain(fileName, editedText(text, edits), (edited) =>
|
|
110
|
+
? reparsedAgain(fileName, editedText(text, edits), (edited) => importEdits(edited, written, FOLDED_IMPORTS))
|
|
96
111
|
: unedited(text)),
|
|
97
112
|
unresolved: formatFindings(unresolved, located),
|
|
98
113
|
notes: formatFindings(notes, located),
|
|
99
114
|
};
|
|
100
115
|
}
|
|
101
|
-
/** The class a `defineEntity(Class, { extends })` names as its base, which its
|
|
116
|
+
/** The class a `defineEntity(Class, { extends })` names as its base, which its defined const reads where it is declared. */
|
|
102
117
|
function baseName(entityCall) {
|
|
103
118
|
const options = objectLiteral(entityCall.arguments[1]);
|
|
104
119
|
return identifierText(options && propertyValue(options, 'extends'));
|
|
@@ -125,7 +140,7 @@ function collectRegistrations(source, unresolved) {
|
|
|
125
140
|
registrations.set(name, registration);
|
|
126
141
|
}
|
|
127
142
|
else if (read.definer === 'defineEntity') {
|
|
128
|
-
unresolved.push({ node, message: 'defineEntity takes
|
|
143
|
+
unresolved.push({ node, message: 'defineEntity takes options alone now: defineEntity({ name, fields, ... })' });
|
|
129
144
|
}
|
|
130
145
|
else {
|
|
131
146
|
unresolved.push({ node, message: removedDefiner(read.definer, textOf(read.entity)) });
|
|
@@ -140,7 +155,7 @@ function collectRegistrations(source, unresolved) {
|
|
|
140
155
|
function removedDefiner(definer, entity) {
|
|
141
156
|
return `${definer} was removed: register '${entity}' with defineEntity in this file, or decorate it (${CLASS_DEFINERS[definer]})`;
|
|
142
157
|
}
|
|
143
|
-
function
|
|
158
|
+
function definedOf(name, cls, calls, entityCall, unit) {
|
|
144
159
|
const refused = (message) => ({ node: entityCall, message });
|
|
145
160
|
const options = objectLiteral(entityCall.arguments[1]) ?? (entityCall.arguments[1] ? undefined : emptyOptions());
|
|
146
161
|
if (!options || options.properties.some((prop) => !ts.isPropertyAssignment(prop))) {
|
|
@@ -150,15 +165,15 @@ function schemaOf(name, cls, calls, entityCall, unit) {
|
|
|
150
165
|
return refused(`'${name}' extends a class: declare the base with defineEntity too and pass it as 'extends'`);
|
|
151
166
|
}
|
|
152
167
|
if (decoratorsOf(cls).length) {
|
|
153
|
-
return refused(`'${name}' is decorated as well: declare it one way, decorators or
|
|
168
|
+
return refused(`'${name}' is decorated as well: declare it one way, decorators or defineEntity`);
|
|
154
169
|
}
|
|
155
170
|
const exported = hasModifier(cls, ts.SyntaxKind.ExportKeyword);
|
|
156
171
|
if (hasModifier(cls, ts.SyntaxKind.DefaultKeyword)) {
|
|
157
|
-
return refused(`'${name}' is a default export: export
|
|
172
|
+
return refused(`'${name}' is a default export: export its defineEntity const by name instead`);
|
|
158
173
|
}
|
|
159
174
|
const unread = objectProperties(options).find(({ name: key }) => !READ_OPTIONS.has(propertyKey(key)));
|
|
160
175
|
if (unread) {
|
|
161
|
-
return refused(`'${textOf(unread.name)}' has no place in
|
|
176
|
+
return refused(`'${textOf(unread.name)}' has no place in defineEntity: move it by hand`);
|
|
162
177
|
}
|
|
163
178
|
for (const [option, shape] of Object.entries(ENTRY_OPTIONS)) {
|
|
164
179
|
const value = propertyValue(options, option);
|
|
@@ -171,6 +186,8 @@ function schemaOf(name, cls, calls, entityCall, unit) {
|
|
|
171
186
|
name,
|
|
172
187
|
param: paramFor(name),
|
|
173
188
|
rel: freeName('rel', calls.map(({ call }) => call)),
|
|
189
|
+
self: freeName(paramFor(name), outerReads(paramFor(name), options, calls)),
|
|
190
|
+
declared: declaredTypes(cls),
|
|
174
191
|
relations: new Set([
|
|
175
192
|
...objectProperties(propertyValue(options, 'relations')).map((it) => propertyKey(it.name)),
|
|
176
193
|
...calls.filter((read) => read.definer === 'defineRelation').map((read) => read.key),
|
|
@@ -182,6 +199,7 @@ function schemaOf(name, cls, calls, entityCall, unit) {
|
|
|
182
199
|
};
|
|
183
200
|
const gathered = {
|
|
184
201
|
fields: [],
|
|
202
|
+
typed: false,
|
|
185
203
|
computed: [],
|
|
186
204
|
aggregates: [],
|
|
187
205
|
relations: new Map(),
|
|
@@ -221,6 +239,7 @@ function schemaOf(name, cls, calls, entityCall, unit) {
|
|
|
221
239
|
const fieldsValue = propertyValue(options, 'fields');
|
|
222
240
|
const untouched = fieldsValue &&
|
|
223
241
|
gathered.fields.length === objectProperties(fieldsValue).length &&
|
|
242
|
+
!gathered.typed &&
|
|
224
243
|
!gathered.computed.length &&
|
|
225
244
|
!gathered.aggregates.length;
|
|
226
245
|
lines.push(`fields: ${untouched ? textOf(fieldsValue) : block(gathered.fields, unit)}`);
|
|
@@ -231,7 +250,7 @@ function schemaOf(name, cls, calls, entityCall, unit) {
|
|
|
231
250
|
lines.push(`relations: (${scope.rel}) => (${block([...gathered.relations.values()], unit)})`);
|
|
232
251
|
}
|
|
233
252
|
if (gathered.aggregates.length) {
|
|
234
|
-
lines.push(`aggregates: (${scope.
|
|
253
|
+
lines.push(`aggregates: (${scope.self}) => (${block(gathered.aggregates, unit)})`);
|
|
235
254
|
}
|
|
236
255
|
for (const key of KEPT_OPTIONS) {
|
|
237
256
|
const entries = gathered.kept.get(key);
|
|
@@ -283,7 +302,7 @@ function entriesOfValue(key, value) {
|
|
|
283
302
|
* Adds what one registration declares to `gathered`: `options` for the `defineEntity` call, its arguments for an
|
|
284
303
|
* incremental definer. Returns why it cannot, where it cannot.
|
|
285
304
|
*/
|
|
286
|
-
function gather({ call, definer, key, options: third, rest }, options, gathered, { name, param, rel, relations, methods, hooked }) {
|
|
305
|
+
function gather({ call, definer, key, options: third, rest }, options, gathered, { name, param, rel, self, relations, methods, hooked, declared }) {
|
|
287
306
|
const unreadable = {
|
|
288
307
|
node: call,
|
|
289
308
|
message: `${definer}'s arguments could not be read: fold them into defineEntity by hand`,
|
|
@@ -306,8 +325,8 @@ function gather({ call, definer, key, options: third, rest }, options, gathered,
|
|
|
306
325
|
gathered.hooks.set(event, runs);
|
|
307
326
|
return undefined;
|
|
308
327
|
};
|
|
309
|
-
const addField = (key,
|
|
310
|
-
const aggregate = key && aggregateEntry(`${name}.${key}`, key, fieldOptions, relations,
|
|
328
|
+
const addField = (key, label, value, fieldOptions, node) => {
|
|
329
|
+
const aggregate = key && aggregateEntry(`${name}.${key}`, key, fieldOptions, relations, self);
|
|
311
330
|
if (typeof aggregate === 'string') {
|
|
312
331
|
gathered.aggregates.push(aggregate);
|
|
313
332
|
return undefined;
|
|
@@ -315,6 +334,9 @@ function gather({ call, definer, key, options: third, rest }, options, gathered,
|
|
|
315
334
|
if (aggregate) {
|
|
316
335
|
return { node, message: aggregate.message };
|
|
317
336
|
}
|
|
337
|
+
const type = key === undefined ? undefined : untyped(fieldOptions) && declared.get(key);
|
|
338
|
+
gathered.typed ||= !!type;
|
|
339
|
+
const entry = `${label}: ${type ? withOption(value, `type: ${type}`) : value}`;
|
|
318
340
|
(isComputed(fieldOptions) ? gathered.computed : gathered.fields).push(entry);
|
|
319
341
|
return undefined;
|
|
320
342
|
};
|
|
@@ -332,7 +354,7 @@ function gather({ call, definer, key, options: third, rest }, options, gathered,
|
|
|
332
354
|
};
|
|
333
355
|
if (options) {
|
|
334
356
|
for (const field of objectProperties(propertyValue(options, 'fields'))) {
|
|
335
|
-
const problem = addField(propertyKey(field.name), textOf(field), field.initializer, field);
|
|
357
|
+
const problem = addField(propertyKey(field.name), textOf(field.name), textOf(field.initializer), field.initializer, field);
|
|
336
358
|
if (problem)
|
|
337
359
|
return problem;
|
|
338
360
|
}
|
|
@@ -370,10 +392,11 @@ function gather({ call, definer, key, options: third, rest }, options, gathered,
|
|
|
370
392
|
case 'defineId': {
|
|
371
393
|
if (!key || !third)
|
|
372
394
|
return unreadable;
|
|
373
|
-
const
|
|
395
|
+
const literal = objectLiteral(third);
|
|
396
|
+
const value = definer === 'defineId' ? literal && withOption(textOf(literal), 'isId: true') : textOf(third);
|
|
374
397
|
if (!value)
|
|
375
398
|
return unreadable;
|
|
376
|
-
return addField(key,
|
|
399
|
+
return addField(key, memberKey(key), value, third, call);
|
|
377
400
|
}
|
|
378
401
|
case 'defineRelation':
|
|
379
402
|
return third && addRelation(key, third) ? undefined : unreadable;
|
|
@@ -398,18 +421,55 @@ function gather({ call, definer, key, options: third, rest }, options, gathered,
|
|
|
398
421
|
return undefined;
|
|
399
422
|
}
|
|
400
423
|
}
|
|
401
|
-
/** Whether a field's options compute it, which a
|
|
424
|
+
/** Whether a field's options compute it, which a defined entity declares under `computed`. */
|
|
402
425
|
function isComputed(options) {
|
|
403
426
|
const literal = objectLiteral(options);
|
|
404
427
|
return !!literal && propertyValue(literal, 'computed') !== undefined;
|
|
405
428
|
}
|
|
406
|
-
/**
|
|
407
|
-
|
|
429
|
+
/**
|
|
430
|
+
* The bodies of the field callbacks whose parameter is not `name`, where a `name` they read is the file's: a
|
|
431
|
+
* callback taking `name` reads its own under it, which the aggregate it becomes reads under the same name.
|
|
432
|
+
*/
|
|
433
|
+
function outerReads(name, options, calls) {
|
|
434
|
+
const declared = [
|
|
435
|
+
...objectProperties(propertyValue(options, 'fields')).map((field) => field.initializer),
|
|
436
|
+
...calls.flatMap(({ options: third }) => (third ? [third] : [])),
|
|
437
|
+
];
|
|
438
|
+
return declared.flatMap((field) => {
|
|
439
|
+
const literal = objectLiteral(field);
|
|
440
|
+
const computed = literal && propertyValue(literal, 'computed');
|
|
441
|
+
return computed && ts.isArrowFunction(computed) && identifierText(computed.parameters[0]?.name) !== name
|
|
442
|
+
? [computed.body]
|
|
443
|
+
: [];
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/** An options literal's text with `option` written first: `{ isId: true, type: String }`. */
|
|
447
|
+
function withOption(literal, option) {
|
|
448
|
+
return /^\{\s*\}$/.test(literal) ? `{ ${option} }` : literal.replace(/^\{\s*/, `{ ${option}, `);
|
|
449
|
+
}
|
|
450
|
+
/** Whether a field's options are a literal stating no column type, which the class's declaration may say instead. */
|
|
451
|
+
function untyped(options) {
|
|
408
452
|
const literal = objectLiteral(options);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
453
|
+
return !!literal && ['type', 'enum', 'version'].every((option) => propertyValue(literal, option) === undefined);
|
|
454
|
+
}
|
|
455
|
+
/** The constructors the TypeScript types of a class's properties name: `number | null` is `Number`. */
|
|
456
|
+
const DECLARED_TYPES = {
|
|
457
|
+
number: 'Number',
|
|
458
|
+
string: 'String',
|
|
459
|
+
boolean: 'Boolean',
|
|
460
|
+
bigint: 'BigInt',
|
|
461
|
+
Date: 'Date',
|
|
462
|
+
};
|
|
463
|
+
/** Each property a class declares as one of {@link DECLARED_TYPES}, with that type's constructor. */
|
|
464
|
+
function declaredTypes(cls) {
|
|
465
|
+
return new Map(cls.members.filter(ts.isPropertyDeclaration).flatMap((member) => {
|
|
466
|
+
const key = propertyKey(member.name);
|
|
467
|
+
const type = member.type;
|
|
468
|
+
const arms = type && ts.isUnionTypeNode(type) ? type.types : type ? [type] : [];
|
|
469
|
+
const named = arms.map((arm) => arm.getText()).filter((arm) => arm !== 'null' && arm !== 'undefined');
|
|
470
|
+
const constructor = named.length === 1 ? DECLARED_TYPES[named[0]] : undefined;
|
|
471
|
+
return key && constructor ? [[key, constructor]] : [];
|
|
472
|
+
}));
|
|
413
473
|
}
|
|
414
474
|
/** `author: () => rel.manyToOne(User, { references })` from `{ cardinality: 'm1', entity: () => User, references }`. */
|
|
415
475
|
function relationEntry(key, value, rel) {
|
|
@@ -460,7 +520,7 @@ function hookFunction(method, preferred) {
|
|
|
460
520
|
}
|
|
461
521
|
/**
|
|
462
522
|
* `total: () => rel.items.count()` from a field computed over a relation, `{ computed: (counted) => counted.items.count() }`,
|
|
463
|
-
* which a
|
|
523
|
+
* which a defined entity declares among its aggregates; nothing for a field reading no relation, and why where it cannot move.
|
|
464
524
|
*/
|
|
465
525
|
function aggregateEntry(label, key, options, relations, rel) {
|
|
466
526
|
const literal = objectLiteral(options);
|
|
@@ -486,7 +546,7 @@ function aggregateEntry(label, key, options, relations, rel) {
|
|
|
486
546
|
function substituted(body, uses, replacement) {
|
|
487
547
|
return rebased(replacingUses(body, uses, replacement), body);
|
|
488
548
|
}
|
|
489
|
-
/** Notes each property the class typed more narrowly than
|
|
549
|
+
/** Notes each property the class typed more narrowly than defineEntity derives, with the option that keeps it. */
|
|
490
550
|
function looserTypes(cls, options) {
|
|
491
551
|
const fields = objectLiteral(propertyValue(options, 'fields'));
|
|
492
552
|
return cls.members.flatMap((member) => {
|
|
@@ -498,11 +558,11 @@ function looserTypes(cls, options) {
|
|
|
498
558
|
}
|
|
499
559
|
const written = textOf(declared);
|
|
500
560
|
const said = (option) => propertyValue(field, option) !== undefined;
|
|
501
|
-
if (/\bJson</.test(written) && !said('
|
|
561
|
+
if (/\bJson</.test(written) && !said('schema')) {
|
|
502
562
|
return [
|
|
503
563
|
{
|
|
504
564
|
node: member,
|
|
505
|
-
message: `'${key}' was declared ${written}: describe its payload with '
|
|
565
|
+
message: `'${key}' was declared ${written}: describe its payload with a Standard Schema, 'f.json(schema)', to keep it typed`,
|
|
506
566
|
},
|
|
507
567
|
];
|
|
508
568
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Why a name the codemod writes is refused: a file already binds it. */
|
|
2
|
+
export declare const nameClash: (name: string) => string;
|
|
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 })'";
|
|
5
|
+
/** Why a joined column's `onDelete` is refused: its relation states another, or takes options the codemod cannot write into. */
|
|
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
|
+
/** Why a foreign key with no type is refused: a defined column states its own, which only its target's key implied. */
|
|
8
|
+
export declare const NO_KEY_TYPE = "takes its type from its target: write its builder, f.number() and so on, as its key's type";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Why a name the codemod writes is refused: a file already binds it. */
|
|
2
|
+
export const nameClash = (name) => `'${name}' is already named here, and the codemod writes it`;
|
|
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 })'";
|
|
5
|
+
/** Why a joined column's `onDelete` is refused: its relation states another, or takes options the codemod cannot write into. */
|
|
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
|
+
/** Why a foreign key with no type is refused: a defined column states its own, which only its target's key implied. */
|
|
8
|
+
export const NO_KEY_TYPE = "takes its type from its target: write its builder, f.number() and so on, as its key's type";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The options a defined entity's first call keeps, each as it is written there. */
|
|
2
|
+
export declare const FIRST_CALL_OPTIONS: readonly ["name", "table", "schema", "abstract", "extends", "fields"];
|
|
3
|
+
/** The sections a defined entity chains after its first call, in the order it chains them, `relations` last. */
|
|
4
|
+
export declare const CHAINED_OPTIONS: readonly ["computed", "indexes", "checks", "triggers", "filters", "hooks"];
|
|
5
|
+
/** The options a class-form `defineEntity` took that a defined entity keeps as they are, and that incremental definers add to. */
|
|
6
|
+
export declare const KEPT_OPTIONS: readonly ["indexes", "checks", "filters", "triggers"];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The options a defined entity's first call keeps, each as it is written there. */
|
|
2
|
+
export const FIRST_CALL_OPTIONS = ['name', 'table', 'schema', 'abstract', 'extends', 'fields'];
|
|
3
|
+
/** The sections a defined entity chains after its first call, in the order it chains them, `relations` last. */
|
|
4
|
+
export const CHAINED_OPTIONS = ['computed', 'indexes', 'checks', 'triggers', 'filters', 'hooks'];
|
|
5
|
+
/** The options a class-form `defineEntity` took that a defined entity keeps as they are, and that incremental definers add to. */
|
|
6
|
+
export const KEPT_OPTIONS = ['indexes', 'checks', 'filters', 'triggers'];
|
package/dist/definers.d.ts
CHANGED
|
@@ -20,5 +20,5 @@ export type DefinerCall = {
|
|
|
20
20
|
readonly options: ts.Expression | undefined;
|
|
21
21
|
readonly rest: readonly ts.Expression[];
|
|
22
22
|
};
|
|
23
|
-
/** `call` read as a class-form definer, or nothing where it is another call, the
|
|
23
|
+
/** `call` read as a class-form definer, or nothing where it is another call, the defined form included. */
|
|
24
24
|
export declare function definerCall(call: ts.CallExpression): DefinerCall | undefined;
|
package/dist/definers.js
CHANGED
|
@@ -20,7 +20,7 @@ const KEYED_DEFINERS = new Set([
|
|
|
20
20
|
'defineHook',
|
|
21
21
|
'defineFilter',
|
|
22
22
|
]);
|
|
23
|
-
/** `call` read as a class-form definer, or nothing where it is another call, the
|
|
23
|
+
/** `call` read as a class-form definer, or nothing where it is another call, the defined form included. */
|
|
24
24
|
export function definerCall(call) {
|
|
25
25
|
const definer = identifierText(call.expression);
|
|
26
26
|
const [entity, ...rest] = call.arguments;
|
package/dist/imports.d.ts
CHANGED
|
@@ -52,6 +52,9 @@ export declare function importRemovalEdits(declaration: ts.ImportDeclaration, el
|
|
|
52
52
|
export declare function spelledNames(source: ts.SourceFile): ReadonlySet<string>;
|
|
53
53
|
/** The import names in `candidates` that the file no longer spells. */
|
|
54
54
|
export declare function deadImports(elements: readonly ts.ImportSpecifier[], candidates: ReadonlySet<string | undefined>, spelled: ReadonlySet<string>): readonly ts.ImportSpecifier[];
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The file's `uql-orm` imports with `added` joined to the first, a new import where there is none, and each name in
|
|
57
|
+
* `candidates` the file no longer spells dropped.
|
|
58
|
+
*/
|
|
59
|
+
export declare function importEdits(source: ts.SourceFile, added: ReadonlySet<string>, candidates: ReadonlySet<string | undefined>): readonly Edit[];
|
|
57
60
|
export {};
|
package/dist/imports.js
CHANGED
|
@@ -400,12 +400,30 @@ export function spelledNames(source) {
|
|
|
400
400
|
export function deadImports(elements, candidates, spelled) {
|
|
401
401
|
return elements.filter((it) => candidates.has(it.name.text) && !spelled.has(it.name.text));
|
|
402
402
|
}
|
|
403
|
-
/**
|
|
404
|
-
|
|
403
|
+
/**
|
|
404
|
+
* The file's `uql-orm` imports with `added` joined to the first, a new import where there is none, and each name in
|
|
405
|
+
* `candidates` the file no longer spells dropped.
|
|
406
|
+
*/
|
|
407
|
+
export function importEdits(source, added, candidates) {
|
|
408
|
+
const declarations = uqlImportDeclarations(source, true);
|
|
409
|
+
const bound = new Set(declarations.flatMap(({ elements }) => elements.map((it) => it.name.text)));
|
|
410
|
+
const joined = [...added].filter((name) => !bound.has(name)).toSorted();
|
|
411
|
+
const [home] = declarations;
|
|
412
|
+
if (!home) {
|
|
413
|
+
return joined.length ? [{ start: 0, end: 0, text: `${importText('import', joined, 'uql-orm')}\n` }] : [];
|
|
414
|
+
}
|
|
405
415
|
const spelled = spelledNames(source);
|
|
406
|
-
return
|
|
416
|
+
return declarations.flatMap(({ declaration, elements }) => {
|
|
407
417
|
const dead = deadImports(elements, candidates, spelled);
|
|
408
|
-
|
|
418
|
+
const join = declaration === home.declaration ? joined : [];
|
|
419
|
+
const removals = dead.length ? importRemovalEdits(declaration, elements, dead) : [];
|
|
420
|
+
const first = elements.find((it) => !dead.includes(it));
|
|
421
|
+
if (!join.length) {
|
|
422
|
+
return removals;
|
|
423
|
+
}
|
|
424
|
+
return first
|
|
425
|
+
? [...removals, inserted(first, `${join.join(', ')}, `)]
|
|
426
|
+
: [replaced(declaration, importText('import', join, 'uql-orm'))];
|
|
409
427
|
});
|
|
410
428
|
}
|
|
411
429
|
/** The import with the `dead` names out and `join` in, nothing else of it touched. */
|
package/dist/transform.d.ts
CHANGED
|
@@ -10,17 +10,17 @@ export type FileResult = {
|
|
|
10
10
|
readonly notes: readonly string[];
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
|
-
* Rewrites one file across every breaking change: the earlier ones off the checker, then column builders and
|
|
13
|
+
* Rewrites one file across every breaking change: the earlier ones off the checker, then column builders and defineEntity
|
|
14
14
|
* chains off the text those produced, so neither pass edits what the other writes.
|
|
15
15
|
*/
|
|
16
16
|
export declare function transformFile(source: ts.SourceFile, checker: ts.TypeChecker, options?: {
|
|
17
17
|
readonly strictNullChecks: boolean;
|
|
18
18
|
}): FileResult;
|
|
19
19
|
/**
|
|
20
|
-
* The rewrites the checker reads types for, up to the
|
|
20
|
+
* The rewrites the checker reads types for, up to the defined entities a class-form registration declares. The
|
|
21
21
|
* builder's and querier's methods are known by name, so they are read only in a file importing uql-orm, and `expr`
|
|
22
22
|
* only where it is uql's.
|
|
23
23
|
*/
|
|
24
24
|
export declare function rewriteLegacy(source: ts.SourceFile, checker: ts.TypeChecker, options: {
|
|
25
25
|
readonly strictNullChecks: boolean;
|
|
26
|
-
}): FileResult & Pass;
|
|
26
|
+
}, taken?: ReadonlySet<string>): FileResult & Pass;
|
package/dist/transform.js
CHANGED
|
@@ -3,6 +3,8 @@ import { BUILDER_NAMES, isBuilder, toBuilders } from './builders.js';
|
|
|
3
3
|
import { foldClassDecorators } from './classOptions.js';
|
|
4
4
|
import { rewriteColumnRefs } from './columnRefs.js';
|
|
5
5
|
import { createContext, instanceTypeOf, locationAt, memberNames, ownerOf, } from './context.js';
|
|
6
|
+
import { toDefinedEntities } from './definedEntity.js';
|
|
7
|
+
import { nameClash } from './definedMessages.js';
|
|
6
8
|
import { definerCall } from './definers.js';
|
|
7
9
|
import { appended, editedText, followed, inserted, original, replaced, unedited, } from './edits.js';
|
|
8
10
|
import { columnExpressions, handNamedColumns, sqlTag, sqlWhereEdit, templateOf } from './entitySql.js';
|
|
@@ -11,7 +13,6 @@ import { rewriteBuilderIndexLengths, rewriteJsonIndexEntries } from './jsonIndex
|
|
|
11
13
|
import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, quoted, referencesEdits, } from './keyMaps.js';
|
|
12
14
|
import { toMigrationBuilders } from './migrationBuilders.js';
|
|
13
15
|
import { relationTargetFor } from './relationTarget.js';
|
|
14
|
-
import { toSchemaEntities } from './schemaEntity.js';
|
|
15
16
|
import { statementTemplate } from './statementSql.js';
|
|
16
17
|
import { arrayElements, decoratorName, decoratorsOf, FIELD_DECORATORS, identifierText, indentOf, namedProperty, objectLiteral, objectProperties, optionsOf, propertyKey, propertyValue, } from './syntax.js';
|
|
17
18
|
const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
|
|
@@ -681,13 +682,13 @@ function rewriteRawCall(node, ctx) {
|
|
|
681
682
|
}
|
|
682
683
|
}
|
|
683
684
|
/**
|
|
684
|
-
* Rewrites one file across every breaking change: the earlier ones off the checker, then column builders and
|
|
685
|
+
* Rewrites one file across every breaking change: the earlier ones off the checker, then column builders and defineEntity
|
|
685
686
|
* chains off the text those produced, so neither pass edits what the other writes.
|
|
686
687
|
*/
|
|
687
688
|
export function transformFile(source, checker, options = { strictNullChecks: true }) {
|
|
688
|
-
const legacy = rewriteLegacy(source, checker, options);
|
|
689
|
-
const located = (pass) => (at) => locationAt(source, pass.sourceOffset(at));
|
|
690
689
|
const taken = foreignNames(source, BUILDER_NAMES, checker);
|
|
690
|
+
const legacy = rewriteLegacy(source, checker, options, taken);
|
|
691
|
+
const located = (pass) => (at) => locationAt(source, pass.sourceOffset(at));
|
|
691
692
|
const builders = toBuilders(source.fileName, legacy.text, { locate: located(legacy), taken });
|
|
692
693
|
const { text, unresolved } = toMigrationBuilders(source.fileName, builders.text, located(followed(legacy, builders)));
|
|
693
694
|
return {
|
|
@@ -699,11 +700,11 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
|
|
|
699
700
|
};
|
|
700
701
|
}
|
|
701
702
|
/**
|
|
702
|
-
* The rewrites the checker reads types for, up to the
|
|
703
|
+
* The rewrites the checker reads types for, up to the defined entities a class-form registration declares. The
|
|
703
704
|
* builder's and querier's methods are known by name, so they are read only in a file importing uql-orm, and `expr`
|
|
704
705
|
* only where it is uql's.
|
|
705
706
|
*/
|
|
706
|
-
export function rewriteLegacy(source, checker, options) {
|
|
707
|
+
export function rewriteLegacy(source, checker, options, taken = foreignNames(source, BUILDER_NAMES, checker)) {
|
|
707
708
|
const ctx = createContext(source, checker, options.strictNullChecks);
|
|
708
709
|
const importsUql = uqlImportDeclarations(source, true).length > 0;
|
|
709
710
|
const importsExpr = uqlImport(source, 'expr', true) !== undefined;
|
|
@@ -743,17 +744,20 @@ export function rewriteLegacy(source, checker, options) {
|
|
|
743
744
|
visit(source);
|
|
744
745
|
const clash = clashingName(ctx);
|
|
745
746
|
if (clash) {
|
|
746
|
-
ctx.report(clash,
|
|
747
|
+
ctx.report(clash, `${nameClash(clash.text)}: rename that, then run again`);
|
|
747
748
|
return { ...fileResult(ctx, source.getFullText()), ...unedited(source.getFullText()) };
|
|
748
749
|
}
|
|
749
750
|
const dead = new Set([...deadImportNames(ctx), ...rewriteColumnRefs(ctx)]);
|
|
750
751
|
rewriteImports(dead, ctx);
|
|
751
752
|
reportRemovedExports(ctx);
|
|
752
753
|
const edited = editedText(source.getFullText(), ctx.edits);
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
754
|
+
const defined = toDefinedEntities(source.fileName, edited.text, {
|
|
755
|
+
locate: (at) => locationAt(source, edited.sourceOffset(at)),
|
|
756
|
+
taken,
|
|
757
|
+
});
|
|
758
|
+
ctx.unresolved.push(...defined.unresolved);
|
|
759
|
+
ctx.notes.push(...defined.notes);
|
|
760
|
+
return { ...fileResult(ctx, defined.text), sourceOffset: followed(edited, defined).sourceOffset };
|
|
757
761
|
}
|
|
758
762
|
function fileResult({ source, unresolved, notes }, text) {
|
|
759
763
|
return { fileName: source.fileName, text, changed: text !== source.getFullText(), unresolved, notes };
|
package/package.json
CHANGED
package/dist/schemaEntity.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type Pass } from './edits.js';
|
|
2
|
-
export type SchemaResult = Pass & {
|
|
3
|
-
readonly unresolved: string[];
|
|
4
|
-
readonly notes: string[];
|
|
5
|
-
};
|
|
6
|
-
/**
|
|
7
|
-
* Declares each class `defineEntity(Class, options)` registered as the schema `defineEntity({ name, ... })`,
|
|
8
|
-
* folding the string-keyed definers of the same class in. Read off the text the other rewrites produced,
|
|
9
|
-
* whose options already name members by callback, so it only moves what is written. An entity it cannot
|
|
10
|
-
* declare faithfully is left as written and reported.
|
|
11
|
-
*/
|
|
12
|
-
export declare function toSchemaEntities(fileName: string, text: string, locate?: (at: number) => string): SchemaResult;
|
package/dist/schemaOptions.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/** The options a schema's first call keeps, each as it is written there. */
|
|
2
|
-
export declare const FIRST_CALL_OPTIONS: readonly ["name", "table", "schema", "abstract", "extends", "fields"];
|
|
3
|
-
/** The sections a schema chains after its first call, in the order it chains them, `relations` last. */
|
|
4
|
-
export declare const CHAINED_OPTIONS: readonly ["computed", "indexes", "checks", "triggers", "filters", "hooks"];
|
|
5
|
-
/** The options a class-form `defineEntity` took that a schema keeps as they are, and that incremental definers add to. */
|
|
6
|
-
export declare const KEPT_OPTIONS: readonly ["indexes", "checks", "filters", "triggers"];
|
package/dist/schemaOptions.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/** The options a schema's first call keeps, each as it is written there. */
|
|
2
|
-
export const FIRST_CALL_OPTIONS = ['name', 'table', 'schema', 'abstract', 'extends', 'fields'];
|
|
3
|
-
/** The sections a schema chains after its first call, in the order it chains them, `relations` last. */
|
|
4
|
-
export const CHAINED_OPTIONS = ['computed', 'indexes', 'checks', 'triggers', 'filters', 'hooks'];
|
|
5
|
-
/** The options a class-form `defineEntity` took that a schema keeps as they are, and that incremental definers add to. */
|
|
6
|
-
export const KEPT_OPTIONS = ['indexes', 'checks', 'filters', 'triggers'];
|