uql-codemod 0.48.2 → 0.50.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 +23 -12
- package/dist/builders.d.ts +28 -0
- package/dist/builders.js +783 -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 +21 -5
- package/dist/imports.js +103 -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 +15 -0
- package/dist/schemaEntity.js +578 -0
- package/dist/schemaMessages.d.ts +8 -0
- package/dist/schemaMessages.js +8 -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 +142 -131
- 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/transform.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
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';
|
|
15
|
+
import { nameClash } from './schemaMessages.js';
|
|
9
16
|
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']);
|
|
17
|
+
import { arrayElements, decoratorName, decoratorsOf, FIELD_DECORATORS, identifierText, indentOf, namedProperty, objectLiteral, objectProperties, optionsOf, propertyKey, propertyValue, } from './syntax.js';
|
|
12
18
|
const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
|
|
13
19
|
const TO_ONE_DECORATORS = new Set(['OneToOne', 'ManyToOne']);
|
|
14
20
|
const TO_ONE_CARDINALITIES = new Set(['11', 'm1']);
|
|
@@ -31,15 +37,6 @@ function decoratorOptions(node) {
|
|
|
31
37
|
const [first] = node.expression.arguments;
|
|
32
38
|
return first ? optionsOf(first) : { kind: 'empty', call: node.expression };
|
|
33
39
|
}
|
|
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
40
|
/** The option `name`, however its key is written: a stated option is left alone, and a renamed one moved. */
|
|
44
41
|
function findProperty(options, name) {
|
|
45
42
|
return options.kind === 'literal' ? namedProperty(options.node, name) : undefined;
|
|
@@ -76,63 +73,15 @@ function noteHandNamedColumns(sql, owner, ctx) {
|
|
|
76
73
|
ctx.note(sql, `SQL names ${names.map((name) => `'${name}'`).join(', ')} by hand, which a rename does not reach; ` +
|
|
77
74
|
`read each off a callback's refs instead: (${owner.param}) => sql\`...\${${owner.param}.<member>}...\``);
|
|
78
75
|
}
|
|
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) {
|
|
76
|
+
/** Reports a decorator lacking `option` unless one of `satisfiedBy` states it, or options it cannot read, which may. */
|
|
77
|
+
function reportMissingOption(decorator, node, ctx, spec) {
|
|
93
78
|
const options = decoratorOptions(decorator);
|
|
94
|
-
if (spec.satisfiedBy.some((name) => findProperty(options, name))) {
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
79
|
if (options.kind === 'opaque') {
|
|
98
|
-
ctx.report(node,
|
|
99
|
-
return undefined;
|
|
80
|
+
ctx.report(node, options.reason);
|
|
100
81
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!value) {
|
|
104
|
-
ctx.report(node, `cannot infer '${spec.option}' for ${ctx.checker.typeToString(type)}`);
|
|
105
|
-
return undefined;
|
|
82
|
+
else if (!spec.satisfiedBy.some((name) => findProperty(options, name))) {
|
|
83
|
+
ctx.report(node, `has no '${spec.option}': ${spec.advice}`);
|
|
106
84
|
}
|
|
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
85
|
}
|
|
137
86
|
/**
|
|
138
87
|
* Names the foreign key a to-one used to find by name, `references: (post) => post.authorId` beside `author`.
|
|
@@ -187,10 +136,6 @@ function foreignKeyColumn(property, column, relation, checker) {
|
|
|
187
136
|
const type = keyTypeSource(key, property, checker);
|
|
188
137
|
return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type} | null;\n${indentOf(property)}`);
|
|
189
138
|
}
|
|
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
139
|
/** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
|
|
195
140
|
function keyTypeSource(key, at, checker) {
|
|
196
141
|
const written = key.declarations?.find(isIdProperty)?.type;
|
|
@@ -199,10 +144,24 @@ function keyTypeSource(key, at, checker) {
|
|
|
199
144
|
}
|
|
200
145
|
return checker.typeToString(checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(key, at)), at);
|
|
201
146
|
}
|
|
202
|
-
/** Whether a declaration is a property
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
147
|
+
/** Whether a declaration is a property declaring a column, `@Field(...)` or `@Id(...)`. */
|
|
148
|
+
function isFieldProperty(node) {
|
|
149
|
+
return (ts.isPropertyDeclaration(node) &&
|
|
150
|
+
decoratorsOf(node).some((decorator) => FIELD_DECORATORS.has(decoratorName(decorator))));
|
|
151
|
+
}
|
|
152
|
+
/** Whether a declaration is a key property: `@Id(...)`, or `@Field({ isId: true, ... })`. */
|
|
153
|
+
function isIdProperty(node) {
|
|
154
|
+
return ts.isPropertyDeclaration(node) && decoratorsOf(node).some(declaresKey);
|
|
155
|
+
}
|
|
156
|
+
function declaresKey(decorator) {
|
|
157
|
+
const name = decoratorName(decorator);
|
|
158
|
+
if (name !== 'Field') {
|
|
159
|
+
return name === 'Id';
|
|
160
|
+
}
|
|
161
|
+
const options = decoratorOptions(decorator);
|
|
162
|
+
const isId = options.kind === 'literal' ? propertyValue(options.node, 'isId') : undefined;
|
|
163
|
+
return isId?.kind === ts.SyntaxKind.TrueKeyword;
|
|
164
|
+
}
|
|
206
165
|
/**
|
|
207
166
|
* Writes the edits `rewrite` reads off `value`, which still names members as strings, or reports it
|
|
208
167
|
* where it could not be read: what it holds is only known at runtime. A callback or an object literal
|
|
@@ -244,6 +203,7 @@ function rewriteClassDecorators(node, ctx) {
|
|
|
244
203
|
}
|
|
245
204
|
else if (name === 'Entity' && options) {
|
|
246
205
|
rewriteEntityOptions(options, owner, ctx);
|
|
206
|
+
reportEntityMembers(options, ctx);
|
|
247
207
|
}
|
|
248
208
|
}
|
|
249
209
|
}
|
|
@@ -253,23 +213,25 @@ function rewriteClassDecorators(node, ctx) {
|
|
|
253
213
|
* Only uql's shapes, a class and a literal options object, are read.
|
|
254
214
|
*/
|
|
255
215
|
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);
|
|
216
|
+
const read = definerCall(call);
|
|
217
|
+
if (!read) {
|
|
218
|
+
return;
|
|
263
219
|
}
|
|
264
|
-
|
|
265
|
-
|
|
220
|
+
const { definer, key, options } = read;
|
|
221
|
+
const owner = ownerOf(read.entity);
|
|
222
|
+
const literal = objectLiteral(options);
|
|
223
|
+
if (definer === 'defineEntity' && literal) {
|
|
224
|
+
rewriteEntityOptions(literal, owner, ctx);
|
|
225
|
+
}
|
|
226
|
+
else if (definer === 'defineIndex' && literal) {
|
|
227
|
+
rewriteIndexOptions(literal, owner, ctx);
|
|
266
228
|
}
|
|
267
|
-
else if (
|
|
268
|
-
rewriteRelationOptions(
|
|
269
|
-
addForeignKeyReference(
|
|
229
|
+
else if (definer === 'defineRelation' && literal) {
|
|
230
|
+
rewriteRelationOptions(literal, owner.param, ctx);
|
|
231
|
+
addForeignKeyReference(literal, key, owner, ctx);
|
|
270
232
|
}
|
|
271
|
-
else if (
|
|
272
|
-
renameOption(optionsOf(
|
|
233
|
+
else if (definer === 'defineFilter' && options) {
|
|
234
|
+
renameOption(optionsOf(options), 'condition', 'where', call, ctx);
|
|
273
235
|
}
|
|
274
236
|
}
|
|
275
237
|
function rewriteEntityOptions(options, owner, ctx) {
|
|
@@ -284,7 +246,6 @@ function rewriteEntityOptions(options, owner, ctx) {
|
|
|
284
246
|
}
|
|
285
247
|
for (const field of objectProperties(fields)) {
|
|
286
248
|
renameSqlOption(optionsOf(field.initializer), 'virtual', 'computed', field, owner, ctx);
|
|
287
|
-
admitNullOnDefined(field, owner, ctx);
|
|
288
249
|
}
|
|
289
250
|
for (const filter of objectProperties(propertyValue(options, 'filters'))) {
|
|
290
251
|
renameOption(optionsOf(filter.initializer), 'condition', 'where', filter, ctx);
|
|
@@ -299,6 +260,21 @@ function rewriteEntityOptions(options, owner, ctx) {
|
|
|
299
260
|
}
|
|
300
261
|
}
|
|
301
262
|
}
|
|
263
|
+
/** The options `@Entity` no longer takes: the members, which the decorators declare, and a base, which the class extends. */
|
|
264
|
+
const DECORATED_MEMBER_OPTIONS = new Map([
|
|
265
|
+
['fields', 'declare each with @Field or @Id'],
|
|
266
|
+
['relations', 'declare each with its relation decorator'],
|
|
267
|
+
['hooks', 'mark each method with its hook decorator'],
|
|
268
|
+
['extends', 'extend the base class itself'],
|
|
269
|
+
]);
|
|
270
|
+
function reportEntityMembers(options, ctx) {
|
|
271
|
+
for (const [option, advice] of DECORATED_MEMBER_OPTIONS) {
|
|
272
|
+
const property = namedProperty(options, option);
|
|
273
|
+
if (property) {
|
|
274
|
+
ctx.report(property, `@Entity takes no '${option}' now: ${advice}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
302
278
|
function rewriteIndexOptions(index, owner, ctx) {
|
|
303
279
|
rewriteIndex(propertyValue(index, 'columns'), index, owner, index, "'columns'", ctx);
|
|
304
280
|
}
|
|
@@ -309,6 +285,7 @@ function rewriteIndexOptions(index, owner, ctx) {
|
|
|
309
285
|
function rewriteIndex(columns, options, owner, node, what, ctx) {
|
|
310
286
|
const literal = objectLiteral(options);
|
|
311
287
|
rewriteKeyList(columns, owner.param, node, what, ctx);
|
|
288
|
+
rewriteJsonIndexEntries(columns, ctx);
|
|
312
289
|
rewriteKeyList(literal && propertyValue(literal, 'include'), owner.param, node, "'include'", ctx);
|
|
313
290
|
rewriteIndexWhere(literal, ctx);
|
|
314
291
|
for (const sql of [...columnExpressions(columns, ctx.checker), literal && propertyValue(literal, 'where')]) {
|
|
@@ -445,6 +422,7 @@ function rewriteBuilderCall(call, ctx) {
|
|
|
445
422
|
? BUILDER_INDEX_METHODS.get(call.expression.name.text)
|
|
446
423
|
: undefined;
|
|
447
424
|
if (at !== undefined) {
|
|
425
|
+
rewriteBuilderIndexLengths(call.arguments[at - 1], ctx);
|
|
448
426
|
rewriteIndexWhere(objectLiteral(call.arguments[at]), ctx);
|
|
449
427
|
}
|
|
450
428
|
}
|
|
@@ -496,6 +474,29 @@ function rewriteStatementKeys(node, ctx) {
|
|
|
496
474
|
else if (key === '$lock' && ts.isObjectLiteralExpression(node.initializer)) {
|
|
497
475
|
rewriteLock(node.initializer, ctx);
|
|
498
476
|
}
|
|
477
|
+
else if (key === '$exclude') {
|
|
478
|
+
rewriteExclude(node, ctx);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/** `$exclude: { body: true }` as `$select: { body: false }`, the one way left to leave a field out. */
|
|
482
|
+
function rewriteExclude(exclude, ctx) {
|
|
483
|
+
if (ts.isObjectLiteralExpression(exclude.parent) && propertyValue(exclude.parent, '$select')) {
|
|
484
|
+
ctx.report(exclude, "merge '$exclude' into the '$select' beside it, each field as `field: false`");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const fields = objectLiteral(exclude.initializer);
|
|
488
|
+
const entries = objectProperties(fields);
|
|
489
|
+
if (!fields ||
|
|
490
|
+
entries.length !== fields.properties.length ||
|
|
491
|
+
!entries.every((it) => isTruthyLiteral(it.initializer))) {
|
|
492
|
+
ctx.report(exclude, "write '$exclude' as `$select: { field: false }`");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
ctx.edits.push(replaced(exclude.name, '$select'), ...entries.map((entry) => replaced(entry.initializer, 'false')));
|
|
496
|
+
}
|
|
497
|
+
/** `true` or `1`, how an `$exclude` entry leaves its field out. */
|
|
498
|
+
function isTruthyLiteral(node) {
|
|
499
|
+
return node.kind === ts.SyntaxKind.TrueKeyword || (ts.isNumericLiteral(node) && node.text === '1');
|
|
499
500
|
}
|
|
500
501
|
/** `{ wait: 'skip' }` as `{ $wait: 'skip' }`, and `{}` or a `'block'` wait as `true`, which waits. */
|
|
501
502
|
function rewriteLock(lock, ctx) {
|
|
@@ -540,21 +541,6 @@ function dropDeclare(node, ctx) {
|
|
|
540
541
|
ctx.edits.push({ start: modifier.getStart(), end, text: '' });
|
|
541
542
|
}
|
|
542
543
|
}
|
|
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
544
|
/** The names a key is read from without help, mirroring `NamedIdKey` in `uql-orm`. */
|
|
559
545
|
const CONVENTIONAL_ID_NAMES = new Set(['id', '_id', 'uuid']);
|
|
560
546
|
/** Whether the member is an `[idKey]?: ...` brand, matched by the name it is written under. */
|
|
@@ -594,18 +580,23 @@ function rewriteProperty(node, ctx) {
|
|
|
594
580
|
for (const decorator of decorators) {
|
|
595
581
|
const name = decoratorName(decorator) ?? '';
|
|
596
582
|
const options = decoratorOptions(decorator);
|
|
597
|
-
if (FIELD_DECORATORS.has(name)) {
|
|
583
|
+
if (FIELD_DECORATORS.has(name) && !writesBuilder(decorator)) {
|
|
598
584
|
if (name === 'Field') {
|
|
599
585
|
admitNull(options, node, ctx);
|
|
600
586
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
587
|
+
reportMissingOption(decorator, node, ctx, {
|
|
588
|
+
option: 'type',
|
|
589
|
+
satisfiedBy: ['type', 'references', 'computed'],
|
|
590
|
+
advice: 'declare its column as a builder, such as `f.string()`',
|
|
591
|
+
});
|
|
605
592
|
renameSqlOption(options, 'virtual', 'computed', node, owner, ctx);
|
|
606
593
|
}
|
|
607
594
|
if (RELATION_DECORATORS.has(name)) {
|
|
608
|
-
|
|
595
|
+
reportMissingOption(decorator, node, ctx, {
|
|
596
|
+
option: 'entity',
|
|
597
|
+
satisfiedBy: ['entity'],
|
|
598
|
+
advice: 'name its target, `entity: () => Target`',
|
|
599
|
+
});
|
|
609
600
|
if (options.kind === 'literal') {
|
|
610
601
|
const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
|
|
611
602
|
rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
|
|
@@ -618,18 +609,23 @@ function rewriteProperty(node, ctx) {
|
|
|
618
609
|
if (decorators.length) {
|
|
619
610
|
dropDeclare(node, ctx);
|
|
620
611
|
}
|
|
621
|
-
|
|
612
|
+
}
|
|
613
|
+
/** Whether a field decorator already declares its column with a builder, which states everything the rewrites add. */
|
|
614
|
+
function writesBuilder(decorator) {
|
|
615
|
+
const [first] = ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
|
|
616
|
+
return first !== undefined && isBuilder(first);
|
|
622
617
|
}
|
|
623
618
|
/**
|
|
624
619
|
* A column holds `null` unless `nullable: false` says otherwise, so the property says so too. A key is NOT NULL
|
|
625
620
|
* 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
|
|
621
|
+
* which may state `nullable` and are reported by {@link reportMissingOption} already.
|
|
627
622
|
*/
|
|
628
623
|
function admitNull(options, node, ctx) {
|
|
629
624
|
if (options.kind === 'opaque' ||
|
|
630
625
|
!ctx.strictNullChecks ||
|
|
631
626
|
declaresNotNull(options) ||
|
|
632
627
|
findProperty(options, 'isId') ||
|
|
628
|
+
findProperty(options, 'version') ||
|
|
633
629
|
(findProperty(options, 'computed') && !findProperty(options, 'type'))) {
|
|
634
630
|
return;
|
|
635
631
|
}
|
|
@@ -640,18 +636,6 @@ function admitNull(options, node, ctx) {
|
|
|
640
636
|
ctx.edits.push(appended(node.type, ' | null'));
|
|
641
637
|
}
|
|
642
638
|
}
|
|
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
639
|
/** Whether the options say the column refuses `null`; `nullable: true` is the default said out loud. */
|
|
656
640
|
function declaresNotNull(options) {
|
|
657
641
|
const nullable = findProperty(options, 'nullable');
|
|
@@ -698,10 +682,29 @@ function rewriteRawCall(node, ctx) {
|
|
|
698
682
|
}
|
|
699
683
|
}
|
|
700
684
|
/**
|
|
701
|
-
* Rewrites one
|
|
702
|
-
*
|
|
685
|
+
* Rewrites one file across every breaking change: the earlier ones off the checker, then column builders and schema
|
|
686
|
+
* chains off the text those produced, so neither pass edits what the other writes.
|
|
703
687
|
*/
|
|
704
688
|
export function transformFile(source, checker, options = { strictNullChecks: true }) {
|
|
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));
|
|
692
|
+
const builders = toBuilders(source.fileName, legacy.text, { locate: located(legacy), taken });
|
|
693
|
+
const { text, unresolved } = toMigrationBuilders(source.fileName, builders.text, located(followed(legacy, builders)));
|
|
694
|
+
return {
|
|
695
|
+
fileName: source.fileName,
|
|
696
|
+
text,
|
|
697
|
+
changed: text !== source.getFullText(),
|
|
698
|
+
unresolved: [...legacy.unresolved, ...builders.unresolved, ...unresolved],
|
|
699
|
+
notes: legacy.notes,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* The rewrites the checker reads types for, up to the schema entities a class-form registration declares. The
|
|
704
|
+
* builder's and querier's methods are known by name, so they are read only in a file importing uql-orm, and `expr`
|
|
705
|
+
* only where it is uql's.
|
|
706
|
+
*/
|
|
707
|
+
export function rewriteLegacy(source, checker, options, taken = foreignNames(source, BUILDER_NAMES, checker)) {
|
|
705
708
|
const ctx = createContext(source, checker, options.strictNullChecks);
|
|
706
709
|
const importsUql = uqlImportDeclarations(source, true).length > 0;
|
|
707
710
|
const importsExpr = uqlImport(source, 'expr', true) !== undefined;
|
|
@@ -717,6 +720,7 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
|
|
|
717
720
|
if (ts.isClassLike(node)) {
|
|
718
721
|
brandIdKey(node, ctx);
|
|
719
722
|
rewriteClassDecorators(node, ctx);
|
|
723
|
+
foldClassDecorators(node, ctx);
|
|
720
724
|
}
|
|
721
725
|
if (ts.isCallExpression(node)) {
|
|
722
726
|
rewriteDefineCall(node, ctx);
|
|
@@ -740,13 +744,20 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
|
|
|
740
744
|
visit(source);
|
|
741
745
|
const clash = clashingName(ctx);
|
|
742
746
|
if (clash) {
|
|
743
|
-
ctx.report(clash,
|
|
744
|
-
return fileResult(ctx, source.getFullText());
|
|
747
|
+
ctx.report(clash, `${nameClash(clash.text)}: rename that, then run again`);
|
|
748
|
+
return { ...fileResult(ctx, source.getFullText()), ...unedited(source.getFullText()) };
|
|
745
749
|
}
|
|
746
750
|
const dead = new Set([...deadImportNames(ctx), ...rewriteColumnRefs(ctx)]);
|
|
747
751
|
rewriteImports(dead, ctx);
|
|
748
752
|
reportRemovedExports(ctx);
|
|
749
|
-
|
|
753
|
+
const edited = editedText(source.getFullText(), ctx.edits);
|
|
754
|
+
const schema = toSchemaEntities(source.fileName, edited.text, {
|
|
755
|
+
locate: (at) => locationAt(source, edited.sourceOffset(at)),
|
|
756
|
+
taken,
|
|
757
|
+
});
|
|
758
|
+
ctx.unresolved.push(...schema.unresolved);
|
|
759
|
+
ctx.notes.push(...schema.notes);
|
|
760
|
+
return { ...fileResult(ctx, schema.text), sourceOffset: followed(edited, schema).sourceOffset };
|
|
750
761
|
}
|
|
751
762
|
function fileResult({ source, unresolved, notes }, text) {
|
|
752
763
|
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[];
|
package/dist/tsconfig.js
CHANGED
|
@@ -1,39 +1,36 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import { applyEdits, removeFromList } from './edits.js';
|
|
3
2
|
import { propertyAssignment, propertyValue } from './syntax.js';
|
|
4
3
|
/** The flags the standard spec has no use for. Their presence is what keeps a project on the old one. */
|
|
5
4
|
const LEGACY_FLAGS = ['experimentalDecorators', 'emitDecoratorMetadata'];
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* What a `tsconfig.json` needs decided by hand for the standard decorator spec. The file is only read, never
|
|
7
|
+
* written: its flags are the project's to remove, and a rewrite would have to keep its comments and formatting.
|
|
9
8
|
*/
|
|
10
|
-
export function
|
|
9
|
+
export function checkTsconfig(fileName, text) {
|
|
11
10
|
const source = ts.parseJsonText(fileName, text);
|
|
12
11
|
const root = source.statements[0]?.expression;
|
|
13
12
|
if (!root || !ts.isObjectLiteralExpression(root)) {
|
|
14
|
-
return
|
|
13
|
+
return [`${fileName}: not an object, so nothing was checked`];
|
|
15
14
|
}
|
|
16
|
-
const edits = [];
|
|
17
15
|
const unresolved = [];
|
|
18
16
|
const compilerOptions = propertyValue(root, 'compilerOptions');
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
17
|
+
const options = compilerOptions && ts.isObjectLiteralExpression(compilerOptions) ? compilerOptions : undefined;
|
|
18
|
+
const legacy = LEGACY_FLAGS.filter((flag) => options && propertyAssignment(options, flag));
|
|
19
|
+
if (legacy.length) {
|
|
20
|
+
unresolved.push(`${fileName}: remove ${legacy.map((flag) => `'${flag}'`).join(' and ')} by hand, ` +
|
|
21
|
+
'since the standard spec is the only one uql-orm supports');
|
|
22
|
+
}
|
|
23
|
+
// `target` is only reported: removing it silently falls back to the compiler default (`es5` for `tsc`), and
|
|
24
|
+
// choosing a replacement means guessing which era the project targets.
|
|
25
|
+
const target = options && propertyValue(options, 'target');
|
|
26
|
+
if (target && ts.isStringLiteral(target) && target.text.toLowerCase() === 'esnext') {
|
|
27
|
+
unresolved.push(`${fileName}: 'target' is 'esnext', the one target that emits decorator syntax untransformed. ` +
|
|
28
|
+
'Change it to any dated target, such as `es2024`.');
|
|
32
29
|
}
|
|
33
30
|
// A value the file inherits cannot be edited here, and silence would read as "there was none".
|
|
34
|
-
if (propertyValue(root, 'extends') && !
|
|
31
|
+
if (propertyValue(root, 'extends') && !legacy.length) {
|
|
35
32
|
unresolved.push(`${fileName}: extends another config, so check the base for 'experimentalDecorators', ` +
|
|
36
33
|
"'emitDecoratorMetadata' and 'target'.");
|
|
37
34
|
}
|
|
38
|
-
return
|
|
35
|
+
return unresolved;
|
|
39
36
|
}
|
package/package.json
CHANGED
package/dist/fieldType.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript';
|
|
2
|
-
export declare function isBrandedString(type: ts.Type): boolean;
|
|
3
|
-
/**
|
|
4
|
-
* The `type` option reflection supplied for a property of `type`, mirroring `TypeFor` in `uql-orm`, or nothing
|
|
5
|
-
* for a shape a human decides. Only what reflection could produce is read: a `Json<T>` or vector property got
|
|
6
|
-
* `Object`/`Array` from it, so it already declared its `type`.
|
|
7
|
-
*/
|
|
8
|
-
export declare function fieldTypeFor(type: ts.Type): string | undefined;
|
|
9
|
-
/**
|
|
10
|
-
* The entity name a relation property points at: `Company` for `Company`, `Company[]`, and
|
|
11
|
-
* `Relation<Company>` alike, since `Relation<T>` is an alias for `T` and erases before this runs.
|
|
12
|
-
*/
|
|
13
|
-
export declare function relationTargetFor(type: ts.Type, checker: ts.TypeChecker): string | undefined;
|
package/dist/fieldType.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript';
|
|
2
|
-
/** The string types carrying more than `string`, e.g. `` type UUID = `${string}-${string}` ``, still string columns. */
|
|
3
|
-
const BRANDED_STRING = ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping;
|
|
4
|
-
export function isBrandedString(type) {
|
|
5
|
-
return (type.isUnion() ? type.types : [type]).some((t) => !!(t.getFlags() & BRANDED_STRING));
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Reads a union by requiring its arms to agree, which is what an `'a' | 'b'` enum or a `T | null` looks
|
|
9
|
-
* like. `undefined` and `null` arms are dropped first: they carry no column type of their own.
|
|
10
|
-
*/
|
|
11
|
-
function foldUnion(type, read) {
|
|
12
|
-
const arms = type.types.filter((arm) => !(arm.getFlags() & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)));
|
|
13
|
-
const readings = new Set(arms.map(read));
|
|
14
|
-
return readings.size === 1 ? [...readings][0] : undefined;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* The `type` option reflection supplied for a property of `type`, mirroring `TypeFor` in `uql-orm`, or nothing
|
|
18
|
-
* for a shape a human decides. Only what reflection could produce is read: a `Json<T>` or vector property got
|
|
19
|
-
* `Object`/`Array` from it, so it already declared its `type`.
|
|
20
|
-
*/
|
|
21
|
-
export function fieldTypeFor(type) {
|
|
22
|
-
const flags = type.getFlags();
|
|
23
|
-
if (type.isUnion()) {
|
|
24
|
-
return foldUnion(type, fieldTypeFor);
|
|
25
|
-
}
|
|
26
|
-
if (flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral | BRANDED_STRING))
|
|
27
|
-
return 'String';
|
|
28
|
-
if (flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))
|
|
29
|
-
return 'Number';
|
|
30
|
-
if (flags & (ts.TypeFlags.BigInt | ts.TypeFlags.BigIntLiteral))
|
|
31
|
-
return 'BigInt';
|
|
32
|
-
if (flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral))
|
|
33
|
-
return 'Boolean';
|
|
34
|
-
// Matched by declared symbol rather than by the printed type: both of these became generic in the
|
|
35
|
-
// modern lib, so `Uint8Array` prints as `Uint8Array<ArrayBufferLike>` and never compared equal.
|
|
36
|
-
const name = type.getSymbol()?.getName();
|
|
37
|
-
if (name === 'Date')
|
|
38
|
-
return 'Date';
|
|
39
|
-
if (name === 'Buffer' || name === 'Uint8Array')
|
|
40
|
-
return "'blob'";
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* The entity name a relation property points at: `Company` for `Company`, `Company[]`, and
|
|
45
|
-
* `Relation<Company>` alike, since `Relation<T>` is an alias for `T` and erases before this runs.
|
|
46
|
-
*/
|
|
47
|
-
export function relationTargetFor(type, checker) {
|
|
48
|
-
if (type.isUnion()) {
|
|
49
|
-
return foldUnion(type, (arm) => relationTargetFor(arm, checker));
|
|
50
|
-
}
|
|
51
|
-
const elementType = checker.getIndexTypeOfType(type, ts.IndexKind.Number);
|
|
52
|
-
const target = elementType ?? type;
|
|
53
|
-
const symbol = target.getSymbol();
|
|
54
|
-
const name = symbol?.getName();
|
|
55
|
-
return name && name !== '__type' && /^[A-Z][A-Za-z0-9_]*$/.test(name) ? name : undefined;
|
|
56
|
-
}
|