uql-codemod 0.48.2 → 0.49.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 +22 -12
- package/dist/builders.d.ts +18 -0
- package/dist/builders.js +588 -0
- package/dist/classOptions.d.ts +9 -0
- package/dist/classOptions.js +240 -0
- package/dist/columnMethods.d.ts +11 -0
- package/dist/columnMethods.js +6 -0
- package/dist/columnRefs.js +1 -8
- package/dist/context.d.ts +18 -5
- package/dist/context.js +22 -4
- package/dist/definers.d.ts +24 -0
- package/dist/definers.js +34 -0
- package/dist/edits.d.ts +26 -5
- package/dist/edits.js +54 -12
- package/dist/entitySql.js +2 -4
- package/dist/imports.d.ts +18 -5
- package/dist/imports.js +85 -31
- package/dist/index.d.ts +3 -3
- package/dist/index.js +9 -12
- package/dist/jsonIndex.d.ts +11 -0
- package/dist/jsonIndex.js +78 -0
- package/dist/keyMaps.d.ts +3 -1
- package/dist/keyMaps.js +5 -1
- package/dist/migrationBuilders.d.ts +13 -0
- package/dist/migrationBuilders.js +182 -0
- package/dist/relationTarget.d.ts +6 -0
- package/dist/relationTarget.js +21 -0
- package/dist/schemaEntity.d.ts +12 -0
- package/dist/schemaEntity.js +518 -0
- package/dist/schemaOptions.d.ts +6 -0
- package/dist/schemaOptions.js +6 -0
- package/dist/syntax.d.ts +54 -0
- package/dist/syntax.js +131 -1
- package/dist/transform.d.ts +11 -2
- package/dist/transform.js +137 -130
- package/dist/tsconfig.d.ts +3 -9
- package/dist/tsconfig.js +18 -21
- package/package.json +1 -1
- package/dist/fieldType.d.ts +0 -13
- package/dist/fieldType.js +0 -56
package/dist/syntax.js
CHANGED
|
@@ -7,9 +7,91 @@ export function identifierText(node) {
|
|
|
7
7
|
export function propertyKey(name) {
|
|
8
8
|
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
9
9
|
}
|
|
10
|
+
export function isNamedProperty(prop) {
|
|
11
|
+
return ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop);
|
|
12
|
+
}
|
|
10
13
|
/** The property `name` of an object literal, a shorthand included. */
|
|
11
14
|
export function namedProperty(node, name) {
|
|
12
|
-
return node.properties.find((prop) => (
|
|
15
|
+
return node.properties.find((prop) => isNamedProperty(prop) && propertyKey(prop.name) === name);
|
|
16
|
+
}
|
|
17
|
+
/** The key a property is written under, a shorthand's included; nothing for a spread, a method or a computed key. */
|
|
18
|
+
export function keyOf(prop) {
|
|
19
|
+
return isNamedProperty(prop) ? propertyKey(prop.name) : undefined;
|
|
20
|
+
}
|
|
21
|
+
/** The options `value` writes: a literal to read and write into, or why it is not one. */
|
|
22
|
+
export function optionsOf(value) {
|
|
23
|
+
if (!ts.isObjectLiteralExpression(value)) {
|
|
24
|
+
return { kind: 'opaque', reason: `its options are passed as '${value.getText()}'` };
|
|
25
|
+
}
|
|
26
|
+
return value.properties.some(ts.isSpreadAssignment)
|
|
27
|
+
? { kind: 'opaque', reason: 'its options object spreads another' }
|
|
28
|
+
: { kind: 'literal', node: value };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* An object literal's named properties by key, each as its value (a shorthand's value is its name), or `undefined`
|
|
32
|
+
* where `node` is no literal or one of its properties is spread, computed or a method.
|
|
33
|
+
*/
|
|
34
|
+
export function propertiesByKey(node) {
|
|
35
|
+
const literal = objectLiteral(node);
|
|
36
|
+
if (!literal) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const props = new Map();
|
|
40
|
+
for (const prop of literal.properties) {
|
|
41
|
+
if (!isNamedProperty(prop)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const key = propertyKey(prop.name);
|
|
45
|
+
if (key === undefined) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
props.set(key, ts.isShorthandPropertyAssignment(prop) ? prop.name : prop.initializer);
|
|
49
|
+
}
|
|
50
|
+
return props;
|
|
51
|
+
}
|
|
52
|
+
/** The root a method chain starts from, and its methods in source order: `f.string().nullable()` is `f` and two. */
|
|
53
|
+
export function chainOf(node) {
|
|
54
|
+
const methods = [];
|
|
55
|
+
let root = node;
|
|
56
|
+
while (ts.isCallExpression(root) && ts.isPropertyAccessExpression(root.expression)) {
|
|
57
|
+
methods.unshift({ call: root, name: root.expression.name.text, receiver: root.expression.expression });
|
|
58
|
+
root = root.expression.expression;
|
|
59
|
+
}
|
|
60
|
+
return { root, methods };
|
|
61
|
+
}
|
|
62
|
+
export function hasModifier(node, kind) {
|
|
63
|
+
return ts.canHaveModifiers(node) && !!ts.getModifiers(node)?.some((modifier) => modifier.kind === kind);
|
|
64
|
+
}
|
|
65
|
+
/** `preferred`, or it numbered past every name `nodes` mention, so a parameter named so shadows nothing they read. */
|
|
66
|
+
export function freeName(preferred, nodes) {
|
|
67
|
+
const taken = (name) => nodes.some((node) => mentions(node, name));
|
|
68
|
+
let name = preferred;
|
|
69
|
+
for (let n = 2; taken(name); n++) {
|
|
70
|
+
name = `${preferred}${n}`;
|
|
71
|
+
}
|
|
72
|
+
return name;
|
|
73
|
+
}
|
|
74
|
+
/** Whether `node` is an identifier reading `name`, not a member named so. */
|
|
75
|
+
function readsName(node, name) {
|
|
76
|
+
return (ts.isIdentifier(node) &&
|
|
77
|
+
node.text === name &&
|
|
78
|
+
!(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node));
|
|
79
|
+
}
|
|
80
|
+
/** Each identifier in `node` naming `name`, in source order, nested functions included, but none a member named so. */
|
|
81
|
+
export function identifierUses(node, name) {
|
|
82
|
+
const uses = [];
|
|
83
|
+
const visit = (child) => {
|
|
84
|
+
if (readsName(child, name)) {
|
|
85
|
+
uses.push(child);
|
|
86
|
+
}
|
|
87
|
+
ts.forEachChild(child, visit);
|
|
88
|
+
};
|
|
89
|
+
visit(node);
|
|
90
|
+
return uses;
|
|
91
|
+
}
|
|
92
|
+
/** Whether `node` reads a binding named `name`, which a callback parameter of that name would shadow: stops at the first. */
|
|
93
|
+
export function mentions(node, name) {
|
|
94
|
+
return readsName(node, name) || !!ts.forEachChild(node, (child) => mentions(child, name) || undefined);
|
|
13
95
|
}
|
|
14
96
|
/** The property `name` of an object literal that is written with a value. */
|
|
15
97
|
export function propertyAssignment(node, name) {
|
|
@@ -22,6 +104,13 @@ export function propertyValue(node, name) {
|
|
|
22
104
|
export function objectProperties(node) {
|
|
23
105
|
return node && ts.isObjectLiteralExpression(node) ? node.properties.filter(ts.isPropertyAssignment) : [];
|
|
24
106
|
}
|
|
107
|
+
/** The entries of a column list, written as an array or returned by its callback. */
|
|
108
|
+
export function columnListEntries(list) {
|
|
109
|
+
if (list && ts.isArrowFunction(list) && !ts.isBlock(list.body)) {
|
|
110
|
+
return arrayElements(list.body);
|
|
111
|
+
}
|
|
112
|
+
return arrayElements(list);
|
|
113
|
+
}
|
|
25
114
|
/** The elements of an array literal, or none where `node` is not one. */
|
|
26
115
|
export function arrayElements(node) {
|
|
27
116
|
return node && ts.isArrayLiteralExpression(node) ? node.elements : [];
|
|
@@ -30,6 +119,35 @@ export function arrayElements(node) {
|
|
|
30
119
|
export function objectLiteral(node) {
|
|
31
120
|
return node && ts.isObjectLiteralExpression(node) ? node : undefined;
|
|
32
121
|
}
|
|
122
|
+
const indentUnits = new WeakMap();
|
|
123
|
+
/** Whether `node` starts a line of code whose indent a file's unit is read from: a member, a property, a statement. */
|
|
124
|
+
const startsCodeLine = (node) => ts.isClassElement(node) ||
|
|
125
|
+
ts.isPropertyAssignment(node) ||
|
|
126
|
+
ts.isShorthandPropertyAssignment(node) ||
|
|
127
|
+
ts.isVariableStatement(node) ||
|
|
128
|
+
ts.isExpressionStatement(node) ||
|
|
129
|
+
ts.isClassDeclaration(node) ||
|
|
130
|
+
ts.isFunctionDeclaration(node) ||
|
|
131
|
+
ts.isImportDeclaration(node);
|
|
132
|
+
/** One level of indentation as the file writes it: the shallowest indent of its code, two spaces where it has none. */
|
|
133
|
+
export function indentUnit(source) {
|
|
134
|
+
const known = indentUnits.get(source);
|
|
135
|
+
if (known !== undefined) {
|
|
136
|
+
return known;
|
|
137
|
+
}
|
|
138
|
+
const indents = [];
|
|
139
|
+
const visit = (node) => {
|
|
140
|
+
const indent = startsCodeLine(node) ? indentOf(node) : '';
|
|
141
|
+
if (indent) {
|
|
142
|
+
indents.push(indent);
|
|
143
|
+
}
|
|
144
|
+
ts.forEachChild(node, visit);
|
|
145
|
+
};
|
|
146
|
+
visit(source);
|
|
147
|
+
const unit = indents.reduce((shallow, indent) => (indent.length < shallow.length ? indent : shallow), indents[0] ?? ' ');
|
|
148
|
+
indentUnits.set(source, unit);
|
|
149
|
+
return unit;
|
|
150
|
+
}
|
|
33
151
|
/** A node's decorators, or none where it is a kind that cannot carry them. */
|
|
34
152
|
export function decoratorsOf(node) {
|
|
35
153
|
return ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
|
|
@@ -39,3 +157,15 @@ export function decoratorName(node) {
|
|
|
39
157
|
const { expression } = node;
|
|
40
158
|
return identifierText(ts.isCallExpression(expression) ? expression.expression : expression);
|
|
41
159
|
}
|
|
160
|
+
/** The decorators declaring a field: `@Field`, and `@Id` for the key the class names. */
|
|
161
|
+
export const FIELD_DECORATORS = new Set(['Field', 'Id']);
|
|
162
|
+
/** Where the line holding offset `at` of `text` starts. */
|
|
163
|
+
export function lineStartOf(text, at) {
|
|
164
|
+
return text.lastIndexOf('\n', at - 1) + 1;
|
|
165
|
+
}
|
|
166
|
+
/** The whitespace the line holding `node` starts with. */
|
|
167
|
+
export function indentOf(node) {
|
|
168
|
+
const text = node.getSourceFile().text;
|
|
169
|
+
const line = text.slice(lineStartOf(text, node.getStart()), node.getStart());
|
|
170
|
+
return line.slice(0, line.length - line.trimStart().length);
|
|
171
|
+
}
|
package/dist/transform.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
+
import { type Pass } from './edits.js';
|
|
2
3
|
export type FileResult = {
|
|
3
4
|
readonly fileName: string;
|
|
4
5
|
readonly text: string;
|
|
@@ -9,9 +10,17 @@ export type FileResult = {
|
|
|
9
10
|
readonly notes: readonly string[];
|
|
10
11
|
};
|
|
11
12
|
/**
|
|
12
|
-
* Rewrites one
|
|
13
|
-
*
|
|
13
|
+
* Rewrites one file across every breaking change: the earlier ones off the checker, then column builders and schema
|
|
14
|
+
* chains off the text those produced, so neither pass edits what the other writes.
|
|
14
15
|
*/
|
|
15
16
|
export declare function transformFile(source: ts.SourceFile, checker: ts.TypeChecker, options?: {
|
|
16
17
|
readonly strictNullChecks: boolean;
|
|
17
18
|
}): FileResult;
|
|
19
|
+
/**
|
|
20
|
+
* The rewrites the checker reads types for, up to the schema entities a class-form registration declares. The
|
|
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
|
+
* only where it is uql's.
|
|
23
|
+
*/
|
|
24
|
+
export declare function rewriteLegacy(source: ts.SourceFile, checker: ts.TypeChecker, options: {
|
|
25
|
+
readonly strictNullChecks: boolean;
|
|
26
|
+
}): FileResult & Pass;
|
package/dist/transform.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
+
import { BUILDER_NAMES, isBuilder, toBuilders } from './builders.js';
|
|
3
|
+
import { foldClassDecorators } from './classOptions.js';
|
|
2
4
|
import { rewriteColumnRefs } from './columnRefs.js';
|
|
3
|
-
import { createContext, instanceTypeOf, memberNames, ownerOf } from './context.js';
|
|
4
|
-
import {
|
|
5
|
+
import { createContext, instanceTypeOf, locationAt, memberNames, ownerOf, } from './context.js';
|
|
6
|
+
import { definerCall } from './definers.js';
|
|
7
|
+
import { appended, editedText, followed, inserted, original, replaced, unedited, } from './edits.js';
|
|
5
8
|
import { columnExpressions, handNamedColumns, sqlTag, sqlWhereEdit, templateOf } from './entitySql.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
9
|
+
import { clashingName, deadImportNames, foreignNames, LEGACY_SQL_TAG, reportRemovedExports, rewriteImports, SQL_TAG, sqlName, uqlExport, uqlImport, uqlImportDeclarations, } from './imports.js';
|
|
10
|
+
import { rewriteBuilderIndexLengths, rewriteJsonIndexEntries } from './jsonIndex.js';
|
|
8
11
|
import { entityGetterTarget, fieldKeysEdits, isCallback, keyListEdits, mappedByEdits, memberAccess, paramFor, quoted, referencesEdits, } from './keyMaps.js';
|
|
12
|
+
import { toMigrationBuilders } from './migrationBuilders.js';
|
|
13
|
+
import { relationTargetFor } from './relationTarget.js';
|
|
14
|
+
import { toSchemaEntities } from './schemaEntity.js';
|
|
9
15
|
import { statementTemplate } from './statementSql.js';
|
|
10
|
-
import { arrayElements, decoratorName, decoratorsOf, identifierText, namedProperty, objectLiteral, objectProperties, propertyKey, propertyValue, } from './syntax.js';
|
|
11
|
-
const FIELD_DECORATORS = new Set(['Field', 'Id']);
|
|
16
|
+
import { arrayElements, decoratorName, decoratorsOf, FIELD_DECORATORS, identifierText, indentOf, namedProperty, objectLiteral, objectProperties, optionsOf, propertyKey, propertyValue, } from './syntax.js';
|
|
12
17
|
const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
|
|
13
18
|
const TO_ONE_DECORATORS = new Set(['OneToOne', 'ManyToOne']);
|
|
14
19
|
const TO_ONE_CARDINALITIES = new Set(['11', 'm1']);
|
|
@@ -31,15 +36,6 @@ function decoratorOptions(node) {
|
|
|
31
36
|
const [first] = node.expression.arguments;
|
|
32
37
|
return first ? optionsOf(first) : { kind: 'empty', call: node.expression };
|
|
33
38
|
}
|
|
34
|
-
/** The options `value` writes: a literal to read and write into, or why it is not one. */
|
|
35
|
-
function optionsOf(value) {
|
|
36
|
-
if (!ts.isObjectLiteralExpression(value)) {
|
|
37
|
-
return { kind: 'opaque', reason: `its options are passed as '${value.getText()}'` };
|
|
38
|
-
}
|
|
39
|
-
return value.properties.some(ts.isSpreadAssignment)
|
|
40
|
-
? { kind: 'opaque', reason: 'its options object spreads another' }
|
|
41
|
-
: { kind: 'literal', node: value };
|
|
42
|
-
}
|
|
43
39
|
/** The option `name`, however its key is written: a stated option is left alone, and a renamed one moved. */
|
|
44
40
|
function findProperty(options, name) {
|
|
45
41
|
return options.kind === 'literal' ? namedProperty(options.node, name) : undefined;
|
|
@@ -76,63 +72,15 @@ function noteHandNamedColumns(sql, owner, ctx) {
|
|
|
76
72
|
ctx.note(sql, `SQL names ${names.map((name) => `'${name}'`).join(', ')} by hand, which a rename does not reach; ` +
|
|
77
73
|
`read each off a callback's refs instead: (${owner.param}) => sql\`...\${${owner.param}.<member>}...\``);
|
|
78
74
|
}
|
|
79
|
-
/**
|
|
80
|
-
function
|
|
81
|
-
if (options.kind === 'empty') {
|
|
82
|
-
const { arguments: args } = options.call;
|
|
83
|
-
return { start: args.pos, end: args.end, text: `{ ${option} }` };
|
|
84
|
-
}
|
|
85
|
-
const first = options.node.properties[0];
|
|
86
|
-
return first ? inserted(first, `${option}, `) : replaced(options.node, `{ ${option} }`);
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Writes one option the decorator can no longer infer at runtime, or records why it could not. Returns the
|
|
90
|
-
* property's type where it wrote one, for the caller to say more about.
|
|
91
|
-
*/
|
|
92
|
-
function addInferredOption(decorator, node, ctx, spec) {
|
|
75
|
+
/** Reports a decorator lacking `option` unless one of `satisfiedBy` states it, or options it cannot read, which may. */
|
|
76
|
+
function reportMissingOption(decorator, node, ctx, spec) {
|
|
93
77
|
const options = decoratorOptions(decorator);
|
|
94
|
-
if (spec.satisfiedBy.some((name) => findProperty(options, name))) {
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
78
|
if (options.kind === 'opaque') {
|
|
98
|
-
ctx.report(node,
|
|
99
|
-
return undefined;
|
|
79
|
+
ctx.report(node, options.reason);
|
|
100
80
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!value) {
|
|
104
|
-
ctx.report(node, `cannot infer '${spec.option}' for ${ctx.checker.typeToString(type)}`);
|
|
105
|
-
return undefined;
|
|
81
|
+
else if (!spec.satisfiedBy.some((name) => findProperty(options, name))) {
|
|
82
|
+
ctx.report(node, `has no '${spec.option}': ${spec.advice}`);
|
|
106
83
|
}
|
|
107
|
-
ctx.edits.push(insertOption(options, `${spec.option}: ${value}`));
|
|
108
|
-
return type;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Writes the `type` that `design:type` supplied, unless `references` (the key it points at decides) or
|
|
112
|
-
* `computed` does. A branded id erased to `String`, which keeps the column; `'uuid'` would change it, so that
|
|
113
|
-
* is noted rather than decided.
|
|
114
|
-
*/
|
|
115
|
-
function addFieldType(decorator, node, ctx) {
|
|
116
|
-
const type = addInferredOption(decorator, node, ctx, {
|
|
117
|
-
option: 'type',
|
|
118
|
-
satisfiedBy: ['type', 'references', 'computed'],
|
|
119
|
-
infer: fieldTypeFor,
|
|
120
|
-
});
|
|
121
|
-
if (type && isBrandedString(type)) {
|
|
122
|
-
ctx.note(node, `set 'type: ${fieldTypeFor(type)}' for ${ctx.checker.typeToString(type)}, matching the previous behaviour. ` +
|
|
123
|
-
"If the column should be 'uuid', change it deliberately: that alters the schema.");
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
/** Writes the `entity` getter relations can no longer infer. */
|
|
127
|
-
function addRelationEntity(decorator, node, ctx) {
|
|
128
|
-
addInferredOption(decorator, node, ctx, {
|
|
129
|
-
option: 'entity',
|
|
130
|
-
satisfiedBy: ['entity'],
|
|
131
|
-
infer: (t) => {
|
|
132
|
-
const target = relationTargetFor(t, ctx.checker);
|
|
133
|
-
return target && `() => ${target}`;
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
84
|
}
|
|
137
85
|
/**
|
|
138
86
|
* Names the foreign key a to-one used to find by name, `references: (post) => post.authorId` beside `author`.
|
|
@@ -187,10 +135,6 @@ function foreignKeyColumn(property, column, relation, checker) {
|
|
|
187
135
|
const type = keyTypeSource(key, property, checker);
|
|
188
136
|
return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type} | null;\n${indentOf(property)}`);
|
|
189
137
|
}
|
|
190
|
-
/** The whitespace a line starting at `node` is indented by. */
|
|
191
|
-
function indentOf(node) {
|
|
192
|
-
return ' '.repeat(node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).character);
|
|
193
|
-
}
|
|
194
138
|
/** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
|
|
195
139
|
function keyTypeSource(key, at, checker) {
|
|
196
140
|
const written = key.declarations?.find(isIdProperty)?.type;
|
|
@@ -199,10 +143,24 @@ function keyTypeSource(key, at, checker) {
|
|
|
199
143
|
}
|
|
200
144
|
return checker.typeToString(checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(key, at)), at);
|
|
201
145
|
}
|
|
202
|
-
/** Whether a declaration is a property
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
146
|
+
/** Whether a declaration is a property declaring a column, `@Field(...)` or `@Id(...)`. */
|
|
147
|
+
function isFieldProperty(node) {
|
|
148
|
+
return (ts.isPropertyDeclaration(node) &&
|
|
149
|
+
decoratorsOf(node).some((decorator) => FIELD_DECORATORS.has(decoratorName(decorator))));
|
|
150
|
+
}
|
|
151
|
+
/** Whether a declaration is a key property: `@Id(...)`, or `@Field({ isId: true, ... })`. */
|
|
152
|
+
function isIdProperty(node) {
|
|
153
|
+
return ts.isPropertyDeclaration(node) && decoratorsOf(node).some(declaresKey);
|
|
154
|
+
}
|
|
155
|
+
function declaresKey(decorator) {
|
|
156
|
+
const name = decoratorName(decorator);
|
|
157
|
+
if (name !== 'Field') {
|
|
158
|
+
return name === 'Id';
|
|
159
|
+
}
|
|
160
|
+
const options = decoratorOptions(decorator);
|
|
161
|
+
const isId = options.kind === 'literal' ? propertyValue(options.node, 'isId') : undefined;
|
|
162
|
+
return isId?.kind === ts.SyntaxKind.TrueKeyword;
|
|
163
|
+
}
|
|
206
164
|
/**
|
|
207
165
|
* Writes the edits `rewrite` reads off `value`, which still names members as strings, or reports it
|
|
208
166
|
* where it could not be read: what it holds is only known at runtime. A callback or an object literal
|
|
@@ -244,6 +202,7 @@ function rewriteClassDecorators(node, ctx) {
|
|
|
244
202
|
}
|
|
245
203
|
else if (name === 'Entity' && options) {
|
|
246
204
|
rewriteEntityOptions(options, owner, ctx);
|
|
205
|
+
reportEntityMembers(options, ctx);
|
|
247
206
|
}
|
|
248
207
|
}
|
|
249
208
|
}
|
|
@@ -253,23 +212,25 @@ function rewriteClassDecorators(node, ctx) {
|
|
|
253
212
|
* Only uql's shapes, a class and a literal options object, are read.
|
|
254
213
|
*/
|
|
255
214
|
function rewriteDefineCall(call, ctx) {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const options = objectLiteral(second);
|
|
260
|
-
const relation = objectLiteral(third);
|
|
261
|
-
if (name === 'defineEntity' && options) {
|
|
262
|
-
rewriteEntityOptions(options, owner, ctx);
|
|
215
|
+
const read = definerCall(call);
|
|
216
|
+
if (!read) {
|
|
217
|
+
return;
|
|
263
218
|
}
|
|
264
|
-
|
|
265
|
-
|
|
219
|
+
const { definer, key, options } = read;
|
|
220
|
+
const owner = ownerOf(read.entity);
|
|
221
|
+
const literal = objectLiteral(options);
|
|
222
|
+
if (definer === 'defineEntity' && literal) {
|
|
223
|
+
rewriteEntityOptions(literal, owner, ctx);
|
|
266
224
|
}
|
|
267
|
-
else if (
|
|
268
|
-
|
|
269
|
-
addForeignKeyReference(relation, second && ts.isStringLiteralLike(second) ? second.text : undefined, owner, ctx);
|
|
225
|
+
else if (definer === 'defineIndex' && literal) {
|
|
226
|
+
rewriteIndexOptions(literal, owner, ctx);
|
|
270
227
|
}
|
|
271
|
-
else if (
|
|
272
|
-
|
|
228
|
+
else if (definer === 'defineRelation' && literal) {
|
|
229
|
+
rewriteRelationOptions(literal, owner.param, ctx);
|
|
230
|
+
addForeignKeyReference(literal, key, owner, ctx);
|
|
231
|
+
}
|
|
232
|
+
else if (definer === 'defineFilter' && options) {
|
|
233
|
+
renameOption(optionsOf(options), 'condition', 'where', call, ctx);
|
|
273
234
|
}
|
|
274
235
|
}
|
|
275
236
|
function rewriteEntityOptions(options, owner, ctx) {
|
|
@@ -284,7 +245,6 @@ function rewriteEntityOptions(options, owner, ctx) {
|
|
|
284
245
|
}
|
|
285
246
|
for (const field of objectProperties(fields)) {
|
|
286
247
|
renameSqlOption(optionsOf(field.initializer), 'virtual', 'computed', field, owner, ctx);
|
|
287
|
-
admitNullOnDefined(field, owner, ctx);
|
|
288
248
|
}
|
|
289
249
|
for (const filter of objectProperties(propertyValue(options, 'filters'))) {
|
|
290
250
|
renameOption(optionsOf(filter.initializer), 'condition', 'where', filter, ctx);
|
|
@@ -299,6 +259,21 @@ function rewriteEntityOptions(options, owner, ctx) {
|
|
|
299
259
|
}
|
|
300
260
|
}
|
|
301
261
|
}
|
|
262
|
+
/** The options `@Entity` no longer takes: the members, which the decorators declare, and a base, which the class extends. */
|
|
263
|
+
const DECORATED_MEMBER_OPTIONS = new Map([
|
|
264
|
+
['fields', 'declare each with @Field or @Id'],
|
|
265
|
+
['relations', 'declare each with its relation decorator'],
|
|
266
|
+
['hooks', 'mark each method with its hook decorator'],
|
|
267
|
+
['extends', 'extend the base class itself'],
|
|
268
|
+
]);
|
|
269
|
+
function reportEntityMembers(options, ctx) {
|
|
270
|
+
for (const [option, advice] of DECORATED_MEMBER_OPTIONS) {
|
|
271
|
+
const property = namedProperty(options, option);
|
|
272
|
+
if (property) {
|
|
273
|
+
ctx.report(property, `@Entity takes no '${option}' now: ${advice}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
302
277
|
function rewriteIndexOptions(index, owner, ctx) {
|
|
303
278
|
rewriteIndex(propertyValue(index, 'columns'), index, owner, index, "'columns'", ctx);
|
|
304
279
|
}
|
|
@@ -309,6 +284,7 @@ function rewriteIndexOptions(index, owner, ctx) {
|
|
|
309
284
|
function rewriteIndex(columns, options, owner, node, what, ctx) {
|
|
310
285
|
const literal = objectLiteral(options);
|
|
311
286
|
rewriteKeyList(columns, owner.param, node, what, ctx);
|
|
287
|
+
rewriteJsonIndexEntries(columns, ctx);
|
|
312
288
|
rewriteKeyList(literal && propertyValue(literal, 'include'), owner.param, node, "'include'", ctx);
|
|
313
289
|
rewriteIndexWhere(literal, ctx);
|
|
314
290
|
for (const sql of [...columnExpressions(columns, ctx.checker), literal && propertyValue(literal, 'where')]) {
|
|
@@ -445,6 +421,7 @@ function rewriteBuilderCall(call, ctx) {
|
|
|
445
421
|
? BUILDER_INDEX_METHODS.get(call.expression.name.text)
|
|
446
422
|
: undefined;
|
|
447
423
|
if (at !== undefined) {
|
|
424
|
+
rewriteBuilderIndexLengths(call.arguments[at - 1], ctx);
|
|
448
425
|
rewriteIndexWhere(objectLiteral(call.arguments[at]), ctx);
|
|
449
426
|
}
|
|
450
427
|
}
|
|
@@ -496,6 +473,29 @@ function rewriteStatementKeys(node, ctx) {
|
|
|
496
473
|
else if (key === '$lock' && ts.isObjectLiteralExpression(node.initializer)) {
|
|
497
474
|
rewriteLock(node.initializer, ctx);
|
|
498
475
|
}
|
|
476
|
+
else if (key === '$exclude') {
|
|
477
|
+
rewriteExclude(node, ctx);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/** `$exclude: { body: true }` as `$select: { body: false }`, the one way left to leave a field out. */
|
|
481
|
+
function rewriteExclude(exclude, ctx) {
|
|
482
|
+
if (ts.isObjectLiteralExpression(exclude.parent) && propertyValue(exclude.parent, '$select')) {
|
|
483
|
+
ctx.report(exclude, "merge '$exclude' into the '$select' beside it, each field as `field: false`");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const fields = objectLiteral(exclude.initializer);
|
|
487
|
+
const entries = objectProperties(fields);
|
|
488
|
+
if (!fields ||
|
|
489
|
+
entries.length !== fields.properties.length ||
|
|
490
|
+
!entries.every((it) => isTruthyLiteral(it.initializer))) {
|
|
491
|
+
ctx.report(exclude, "write '$exclude' as `$select: { field: false }`");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
ctx.edits.push(replaced(exclude.name, '$select'), ...entries.map((entry) => replaced(entry.initializer, 'false')));
|
|
495
|
+
}
|
|
496
|
+
/** `true` or `1`, how an `$exclude` entry leaves its field out. */
|
|
497
|
+
function isTruthyLiteral(node) {
|
|
498
|
+
return node.kind === ts.SyntaxKind.TrueKeyword || (ts.isNumericLiteral(node) && node.text === '1');
|
|
499
499
|
}
|
|
500
500
|
/** `{ wait: 'skip' }` as `{ $wait: 'skip' }`, and `{}` or a `'block'` wait as `true`, which waits. */
|
|
501
501
|
function rewriteLock(lock, ctx) {
|
|
@@ -540,21 +540,6 @@ function dropDeclare(node, ctx) {
|
|
|
540
540
|
ctx.edits.push({ start: modifier.getStart(), end, text: '' });
|
|
541
541
|
}
|
|
542
542
|
}
|
|
543
|
-
/**
|
|
544
|
-
* Unwraps `Relation<Company>` to `Company`. The alias is `type Relation<T> = T`, and it existed only
|
|
545
|
-
* because reflection stored the property's type at class-definition time, which forced a real import and
|
|
546
|
-
* so created the circular-import problem it was working around.
|
|
547
|
-
*/
|
|
548
|
-
function unwrapRelationAlias(node, ctx) {
|
|
549
|
-
const declared = node.type;
|
|
550
|
-
if (declared &&
|
|
551
|
-
ts.isTypeReferenceNode(declared) &&
|
|
552
|
-
identifierText(declared.typeName) === 'Relation' &&
|
|
553
|
-
declared.typeArguments?.length === 1) {
|
|
554
|
-
ctx.edits.push(replaced(declared, [original(declared.typeArguments[0])]));
|
|
555
|
-
ctx.rewritten.Relation += 1;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
543
|
/** The names a key is read from without help, mirroring `NamedIdKey` in `uql-orm`. */
|
|
559
544
|
const CONVENTIONAL_ID_NAMES = new Set(['id', '_id', 'uuid']);
|
|
560
545
|
/** Whether the member is an `[idKey]?: ...` brand, matched by the name it is written under. */
|
|
@@ -594,18 +579,23 @@ function rewriteProperty(node, ctx) {
|
|
|
594
579
|
for (const decorator of decorators) {
|
|
595
580
|
const name = decoratorName(decorator) ?? '';
|
|
596
581
|
const options = decoratorOptions(decorator);
|
|
597
|
-
if (FIELD_DECORATORS.has(name)) {
|
|
582
|
+
if (FIELD_DECORATORS.has(name) && !writesBuilder(decorator)) {
|
|
598
583
|
if (name === 'Field') {
|
|
599
584
|
admitNull(options, node, ctx);
|
|
600
585
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
586
|
+
reportMissingOption(decorator, node, ctx, {
|
|
587
|
+
option: 'type',
|
|
588
|
+
satisfiedBy: ['type', 'references', 'computed'],
|
|
589
|
+
advice: 'declare its column as a builder, such as `f.string()`',
|
|
590
|
+
});
|
|
605
591
|
renameSqlOption(options, 'virtual', 'computed', node, owner, ctx);
|
|
606
592
|
}
|
|
607
593
|
if (RELATION_DECORATORS.has(name)) {
|
|
608
|
-
|
|
594
|
+
reportMissingOption(decorator, node, ctx, {
|
|
595
|
+
option: 'entity',
|
|
596
|
+
satisfiedBy: ['entity'],
|
|
597
|
+
advice: 'name its target, `entity: () => Target`',
|
|
598
|
+
});
|
|
609
599
|
if (options.kind === 'literal') {
|
|
610
600
|
const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
|
|
611
601
|
rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
|
|
@@ -618,18 +608,23 @@ function rewriteProperty(node, ctx) {
|
|
|
618
608
|
if (decorators.length) {
|
|
619
609
|
dropDeclare(node, ctx);
|
|
620
610
|
}
|
|
621
|
-
|
|
611
|
+
}
|
|
612
|
+
/** Whether a field decorator already declares its column with a builder, which states everything the rewrites add. */
|
|
613
|
+
function writesBuilder(decorator) {
|
|
614
|
+
const [first] = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
|
|
615
|
+
return first !== undefined && isBuilder(first);
|
|
622
616
|
}
|
|
623
617
|
/**
|
|
624
618
|
* A column holds `null` unless `nullable: false` says otherwise, so the property says so too. A key is NOT NULL
|
|
625
619
|
* everywhere and a relation aggregate types its own, so neither is touched; nor are options it cannot read,
|
|
626
|
-
* which may state `nullable` and are reported by {@link
|
|
620
|
+
* which may state `nullable` and are reported by {@link reportMissingOption} already.
|
|
627
621
|
*/
|
|
628
622
|
function admitNull(options, node, ctx) {
|
|
629
623
|
if (options.kind === 'opaque' ||
|
|
630
624
|
!ctx.strictNullChecks ||
|
|
631
625
|
declaresNotNull(options) ||
|
|
632
626
|
findProperty(options, 'isId') ||
|
|
627
|
+
findProperty(options, 'version') ||
|
|
633
628
|
(findProperty(options, 'computed') && !findProperty(options, 'type'))) {
|
|
634
629
|
return;
|
|
635
630
|
}
|
|
@@ -640,18 +635,6 @@ function admitNull(options, node, ctx) {
|
|
|
640
635
|
ctx.edits.push(appended(node.type, ' | null'));
|
|
641
636
|
}
|
|
642
637
|
}
|
|
643
|
-
/** The same null rule where `defineEntity` names the field, on the property of the class the call takes. */
|
|
644
|
-
function admitNullOnDefined(field, owner, ctx) {
|
|
645
|
-
const key = propertyKey(field.name);
|
|
646
|
-
const declaration = key
|
|
647
|
-
? instanceTypeOf(owner.entity, ctx.checker)
|
|
648
|
-
.getProperty(key)
|
|
649
|
-
?.declarations?.find((it) => ts.isPropertyDeclaration(it))
|
|
650
|
-
: undefined;
|
|
651
|
-
if (declaration?.type) {
|
|
652
|
-
admitNull(optionsOf(field.initializer), declaration, ctx);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
638
|
/** Whether the options say the column refuses `null`; `nullable: true` is the default said out loud. */
|
|
656
639
|
function declaresNotNull(options) {
|
|
657
640
|
const nullable = findProperty(options, 'nullable');
|
|
@@ -698,10 +681,29 @@ function rewriteRawCall(node, ctx) {
|
|
|
698
681
|
}
|
|
699
682
|
}
|
|
700
683
|
/**
|
|
701
|
-
* Rewrites one
|
|
702
|
-
*
|
|
684
|
+
* Rewrites one file across every breaking change: the earlier ones off the checker, then column builders and schema
|
|
685
|
+
* chains off the text those produced, so neither pass edits what the other writes.
|
|
703
686
|
*/
|
|
704
687
|
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
|
+
const taken = foreignNames(source, BUILDER_NAMES, checker);
|
|
691
|
+
const builders = toBuilders(source.fileName, legacy.text, { locate: located(legacy), taken });
|
|
692
|
+
const { text, unresolved } = toMigrationBuilders(source.fileName, builders.text, located(followed(legacy, builders)));
|
|
693
|
+
return {
|
|
694
|
+
fileName: source.fileName,
|
|
695
|
+
text,
|
|
696
|
+
changed: text !== source.getFullText(),
|
|
697
|
+
unresolved: [...legacy.unresolved, ...builders.unresolved, ...unresolved],
|
|
698
|
+
notes: legacy.notes,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* The rewrites the checker reads types for, up to the schema entities a class-form registration declares. The
|
|
703
|
+
* builder's and querier's methods are known by name, so they are read only in a file importing uql-orm, and `expr`
|
|
704
|
+
* only where it is uql's.
|
|
705
|
+
*/
|
|
706
|
+
export function rewriteLegacy(source, checker, options) {
|
|
705
707
|
const ctx = createContext(source, checker, options.strictNullChecks);
|
|
706
708
|
const importsUql = uqlImportDeclarations(source, true).length > 0;
|
|
707
709
|
const importsExpr = uqlImport(source, 'expr', true) !== undefined;
|
|
@@ -717,6 +719,7 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
|
|
|
717
719
|
if (ts.isClassLike(node)) {
|
|
718
720
|
brandIdKey(node, ctx);
|
|
719
721
|
rewriteClassDecorators(node, ctx);
|
|
722
|
+
foldClassDecorators(node, ctx);
|
|
720
723
|
}
|
|
721
724
|
if (ts.isCallExpression(node)) {
|
|
722
725
|
rewriteDefineCall(node, ctx);
|
|
@@ -741,12 +744,16 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
|
|
|
741
744
|
const clash = clashingName(ctx);
|
|
742
745
|
if (clash) {
|
|
743
746
|
ctx.report(clash, `'${clash.text}' is already named here, and the codemod writes it: rename that, then run again`);
|
|
744
|
-
return fileResult(ctx, source.getFullText());
|
|
747
|
+
return { ...fileResult(ctx, source.getFullText()), ...unedited(source.getFullText()) };
|
|
745
748
|
}
|
|
746
749
|
const dead = new Set([...deadImportNames(ctx), ...rewriteColumnRefs(ctx)]);
|
|
747
750
|
rewriteImports(dead, ctx);
|
|
748
751
|
reportRemovedExports(ctx);
|
|
749
|
-
|
|
752
|
+
const edited = editedText(source.getFullText(), ctx.edits);
|
|
753
|
+
const schema = toSchemaEntities(source.fileName, edited.text, (at) => locationAt(source, edited.sourceOffset(at)));
|
|
754
|
+
ctx.unresolved.push(...schema.unresolved);
|
|
755
|
+
ctx.notes.push(...schema.notes);
|
|
756
|
+
return { ...fileResult(ctx, schema.text), sourceOffset: followed(edited, schema).sourceOffset };
|
|
750
757
|
}
|
|
751
758
|
function fileResult({ source, unresolved, notes }, text) {
|
|
752
759
|
return { fileName: source.fileName, text, changed: text !== source.getFullText(), unresolved, notes };
|
package/dist/tsconfig.d.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
export type TsconfigResult = {
|
|
2
|
-
readonly text: string;
|
|
3
|
-
readonly changed: boolean;
|
|
4
|
-
/** What the codemod will not decide, each needing a human. */
|
|
5
|
-
readonly unresolved: readonly string[];
|
|
6
|
-
};
|
|
7
1
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* What a `tsconfig.json` needs decided by hand for the standard decorator spec. The file is only read, never
|
|
3
|
+
* written: its flags are the project's to remove, and a rewrite would have to keep its comments and formatting.
|
|
10
4
|
*/
|
|
11
|
-
export declare function
|
|
5
|
+
export declare function checkTsconfig(fileName: string, text: string): readonly string[];
|