uql-codemod 0.48.1 → 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 +27 -16
- 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 +3 -10
- 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 +43 -11
- package/dist/edits.js +105 -26
- package/dist/entitySql.d.ts +15 -16
- package/dist/entitySql.js +33 -31
- package/dist/imports.d.ts +33 -16
- package/dist/imports.js +213 -99
- 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/statementSql.d.ts +2 -1
- package/dist/statementSql.js +10 -4
- package/dist/syntax.d.ts +54 -0
- package/dist/syntax.js +131 -1
- package/dist/transform.d.ts +11 -2
- package/dist/transform.js +174 -165
- package/dist/tsconfig.d.ts +3 -9
- package/dist/tsconfig.js +18 -22
- 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,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 {
|
|
6
|
-
import {
|
|
7
|
-
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';
|
|
8
|
+
import { columnExpressions, handNamedColumns, sqlTag, sqlWhereEdit, templateOf } from './entitySql.js';
|
|
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;
|
|
@@ -69,70 +65,22 @@ function renameSqlOption(options, from, to, node, owner, ctx) {
|
|
|
69
65
|
}
|
|
70
66
|
/** Notes SQL that names a column by hand, which a rename does not reach. */
|
|
71
67
|
function noteHandNamedColumns(sql, owner, ctx) {
|
|
72
|
-
const names = sql ? handNamedColumns(sql, memberNames(owner.entity, ctx.checker)) : [];
|
|
68
|
+
const names = sql ? handNamedColumns(sql, memberNames(owner.entity, ctx.checker), ctx.checker) : [];
|
|
73
69
|
if (!sql || !names.length) {
|
|
74
70
|
return;
|
|
75
71
|
}
|
|
76
72
|
ctx.note(sql, `SQL names ${names.map((name) => `'${name}'`).join(', ')} by hand, which a rename does not reach; ` +
|
|
77
|
-
`read each off a callback's refs instead: (${owner.param}) =>
|
|
78
|
-
}
|
|
79
|
-
/** Inserts `option` into the decorator's options object, creating one in an empty argument list. */
|
|
80
|
-
function insertOption(options, option) {
|
|
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} }`);
|
|
73
|
+
`read each off a callback's refs instead: (${owner.param}) => sql\`...\${${owner.param}.<member>}...\``);
|
|
87
74
|
}
|
|
88
|
-
/**
|
|
89
|
-
|
|
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);
|
|
224
|
+
}
|
|
225
|
+
else if (definer === 'defineIndex' && literal) {
|
|
226
|
+
rewriteIndexOptions(literal, owner, ctx);
|
|
266
227
|
}
|
|
267
|
-
else if (
|
|
268
|
-
rewriteRelationOptions(
|
|
269
|
-
addForeignKeyReference(
|
|
228
|
+
else if (definer === 'defineRelation' && literal) {
|
|
229
|
+
rewriteRelationOptions(literal, owner.param, ctx);
|
|
230
|
+
addForeignKeyReference(literal, key, owner, ctx);
|
|
270
231
|
}
|
|
271
|
-
else if (
|
|
272
|
-
renameOption(optionsOf(
|
|
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,25 +284,26 @@ 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
|
-
for (const sql of [...columnExpressions(columns), literal && propertyValue(literal, 'where')]) {
|
|
290
|
+
for (const sql of [...columnExpressions(columns, ctx.checker), literal && propertyValue(literal, 'where')]) {
|
|
315
291
|
noteHandNamedColumns(sql, owner, ctx);
|
|
316
292
|
}
|
|
317
293
|
}
|
|
318
|
-
/** A partial-index `where` string as `
|
|
294
|
+
/** A partial-index `where` string as `sql`, on an entity and in the migration builder alike, or reported. */
|
|
319
295
|
function rewriteIndexWhere(options, ctx) {
|
|
320
296
|
const where = options && propertyValue(options, 'where');
|
|
321
297
|
if (!where || !isStringTyped(ctx.checker.getTypeAtLocation(where))) {
|
|
322
298
|
return;
|
|
323
299
|
}
|
|
324
|
-
const edit =
|
|
300
|
+
const edit = sqlWhereEdit(where, sqlName(ctx.source));
|
|
325
301
|
if (edit) {
|
|
326
302
|
ctx.edits.push(edit);
|
|
327
|
-
ctx.imports.set(
|
|
303
|
+
ctx.imports.set(SQL_TAG, 'the sql`...`');
|
|
328
304
|
}
|
|
329
305
|
else {
|
|
330
|
-
ctx.report(where, "write the partial-index 'where' as
|
|
306
|
+
ctx.report(where, "write the partial-index 'where' as sql`...` or a predicate; this one could not be read");
|
|
331
307
|
}
|
|
332
308
|
}
|
|
333
309
|
/** Whether every value `type` admits is a string. */
|
|
@@ -360,7 +336,7 @@ function rewriteMethodCall(call, ctx) {
|
|
|
360
336
|
/**
|
|
361
337
|
* A querier's `run`/`all` handed SQL as a string, which now takes a tagged statement: a literal becomes
|
|
362
338
|
* `run`...``, its values interpolated where {@link statementTemplate} can place them, and SQL built at run
|
|
363
|
-
* time
|
|
339
|
+
* time `<sql>.text(...)`, which splices as the string did. Values it cannot place are reported.
|
|
364
340
|
*/
|
|
365
341
|
function rewriteSqlCall(call, method, ctx) {
|
|
366
342
|
const [sql, ...values] = call.arguments;
|
|
@@ -369,19 +345,21 @@ function rewriteSqlCall(call, method, ctx) {
|
|
|
369
345
|
}
|
|
370
346
|
const template = statementTemplate(sql, values);
|
|
371
347
|
if (template) {
|
|
372
|
-
const typeArguments = call.typeArguments
|
|
373
|
-
|
|
348
|
+
const typeArguments = (call.typeArguments ?? []).map(original);
|
|
349
|
+
const listed = typeArguments.flatMap((type, at) => (at ? [', ', type] : [type]));
|
|
350
|
+
const angle = typeArguments.length ? ['<', ...listed, '>'] : [];
|
|
351
|
+
ctx.edits.push(replaced(call, [original(call.expression), ...angle, ...template]));
|
|
374
352
|
return;
|
|
375
353
|
}
|
|
376
354
|
if (values.length) {
|
|
377
|
-
ctx.report(call, `${method}() takes one
|
|
355
|
+
ctx.report(call, `${method}() takes one statement: write the values into it, sql\`... \${value}\`, which binds them`);
|
|
378
356
|
return;
|
|
379
357
|
}
|
|
380
358
|
if (ts.isTemplateExpression(sql)) {
|
|
381
|
-
ctx.note(call, 'the values in this template are spliced into the SQL; write it as
|
|
359
|
+
ctx.note(call, 'the values in this template are spliced into the SQL; write it as sql`...` to bind them');
|
|
382
360
|
}
|
|
383
|
-
ctx.edits.push(replaced(sql,
|
|
384
|
-
ctx.imports.set(
|
|
361
|
+
ctx.edits.push(replaced(sql, [`${sqlName(ctx.source)}.text(`, original(sql), ')']));
|
|
362
|
+
ctx.imports.set(SQL_TAG, 'the sql.text(...)');
|
|
385
363
|
}
|
|
386
364
|
/** `const { id } = await q.upsertOne(...)`, which resolves to the id now, as the id: other keys are reported. */
|
|
387
365
|
function rewriteUpsertResult(declaration, ctx) {
|
|
@@ -412,7 +390,7 @@ const EXPR_VALUES = new Map([
|
|
|
412
390
|
...['currentDate', 'currentTime', 'uuid', 'uuidv7'].map((name) => [name, name]),
|
|
413
391
|
]);
|
|
414
392
|
/**
|
|
415
|
-
* Rewrites the migration builder's `expr.<helper>()` default as the value or `
|
|
393
|
+
* Rewrites the migration builder's `expr.<helper>()` default as the value or `sql` an entity declares, or reports
|
|
416
394
|
* one with no rewrite. Called only where the file imports `expr` from uql, so a variable of its own is left alone.
|
|
417
395
|
*/
|
|
418
396
|
function rewriteExprCall(call, ctx) {
|
|
@@ -428,8 +406,8 @@ function rewriteExprCall(call, ctx) {
|
|
|
428
406
|
ctx.imports.set(value, `the expr.${helper}()`);
|
|
429
407
|
}
|
|
430
408
|
else if (helper === 'raw' && sql && ts.isStringLiteralLike(sql)) {
|
|
431
|
-
ctx.edits.push(replaced(call,
|
|
432
|
-
ctx.imports.set(
|
|
409
|
+
ctx.edits.push(replaced(call, sqlTag(sqlName(ctx.source), sql.text)));
|
|
410
|
+
ctx.imports.set(SQL_TAG, 'the sql`...`');
|
|
433
411
|
}
|
|
434
412
|
else {
|
|
435
413
|
ctx.report(call, `expr.${helper}() is gone: declare the column with m.raw(...), or use a stamp on the entity`);
|
|
@@ -443,6 +421,7 @@ function rewriteBuilderCall(call, ctx) {
|
|
|
443
421
|
? BUILDER_INDEX_METHODS.get(call.expression.name.text)
|
|
444
422
|
: undefined;
|
|
445
423
|
if (at !== undefined) {
|
|
424
|
+
rewriteBuilderIndexLengths(call.arguments[at - 1], ctx);
|
|
446
425
|
rewriteIndexWhere(objectLiteral(call.arguments[at]), ctx);
|
|
447
426
|
}
|
|
448
427
|
}
|
|
@@ -458,7 +437,7 @@ function rewriteUpsertCall(call, ctx) {
|
|
|
458
437
|
const options = call.arguments[3];
|
|
459
438
|
const update = options && upsertUpdate(options);
|
|
460
439
|
if (name && UPSERT_CALLS.has(name) && update) {
|
|
461
|
-
ctx.edits.push(replaced(options, update
|
|
440
|
+
ctx.edits.push(replaced(options, [original(update)]));
|
|
462
441
|
}
|
|
463
442
|
}
|
|
464
443
|
/** The value of options holding `update` alone, `{ update: value }` or `{ update }`. */
|
|
@@ -494,6 +473,29 @@ function rewriteStatementKeys(node, ctx) {
|
|
|
494
473
|
else if (key === '$lock' && ts.isObjectLiteralExpression(node.initializer)) {
|
|
495
474
|
rewriteLock(node.initializer, ctx);
|
|
496
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');
|
|
497
499
|
}
|
|
498
500
|
/** `{ wait: 'skip' }` as `{ $wait: 'skip' }`, and `{}` or a `'block'` wait as `true`, which waits. */
|
|
499
501
|
function rewriteLock(lock, ctx) {
|
|
@@ -538,21 +540,6 @@ function dropDeclare(node, ctx) {
|
|
|
538
540
|
ctx.edits.push({ start: modifier.getStart(), end, text: '' });
|
|
539
541
|
}
|
|
540
542
|
}
|
|
541
|
-
/**
|
|
542
|
-
* Unwraps `Relation<Company>` to `Company`. The alias is `type Relation<T> = T`, and it existed only
|
|
543
|
-
* because reflection stored the property's type at class-definition time, which forced a real import and
|
|
544
|
-
* so created the circular-import problem it was working around.
|
|
545
|
-
*/
|
|
546
|
-
function unwrapRelationAlias(node, ctx) {
|
|
547
|
-
const declared = node.type;
|
|
548
|
-
if (declared &&
|
|
549
|
-
ts.isTypeReferenceNode(declared) &&
|
|
550
|
-
identifierText(declared.typeName) === 'Relation' &&
|
|
551
|
-
declared.typeArguments?.length === 1) {
|
|
552
|
-
ctx.edits.push(replaced(declared, declared.typeArguments[0].getText()));
|
|
553
|
-
ctx.rewritten.Relation += 1;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
543
|
/** The names a key is read from without help, mirroring `NamedIdKey` in `uql-orm`. */
|
|
557
544
|
const CONVENTIONAL_ID_NAMES = new Set(['id', '_id', 'uuid']);
|
|
558
545
|
/** Whether the member is an `[idKey]?: ...` brand, matched by the name it is written under. */
|
|
@@ -592,18 +579,23 @@ function rewriteProperty(node, ctx) {
|
|
|
592
579
|
for (const decorator of decorators) {
|
|
593
580
|
const name = decoratorName(decorator) ?? '';
|
|
594
581
|
const options = decoratorOptions(decorator);
|
|
595
|
-
if (FIELD_DECORATORS.has(name)) {
|
|
582
|
+
if (FIELD_DECORATORS.has(name) && !writesBuilder(decorator)) {
|
|
596
583
|
if (name === 'Field') {
|
|
597
584
|
admitNull(options, node, ctx);
|
|
598
585
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
});
|
|
603
591
|
renameSqlOption(options, 'virtual', 'computed', node, owner, ctx);
|
|
604
592
|
}
|
|
605
593
|
if (RELATION_DECORATORS.has(name)) {
|
|
606
|
-
|
|
594
|
+
reportMissingOption(decorator, node, ctx, {
|
|
595
|
+
option: 'entity',
|
|
596
|
+
satisfiedBy: ['entity'],
|
|
597
|
+
advice: 'name its target, `entity: () => Target`',
|
|
598
|
+
});
|
|
607
599
|
if (options.kind === 'literal') {
|
|
608
600
|
const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
|
|
609
601
|
rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
|
|
@@ -616,18 +608,23 @@ function rewriteProperty(node, ctx) {
|
|
|
616
608
|
if (decorators.length) {
|
|
617
609
|
dropDeclare(node, ctx);
|
|
618
610
|
}
|
|
619
|
-
|
|
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);
|
|
620
616
|
}
|
|
621
617
|
/**
|
|
622
618
|
* A column holds `null` unless `nullable: false` says otherwise, so the property says so too. A key is NOT NULL
|
|
623
619
|
* everywhere and a relation aggregate types its own, so neither is touched; nor are options it cannot read,
|
|
624
|
-
* which may state `nullable` and are reported by {@link
|
|
620
|
+
* which may state `nullable` and are reported by {@link reportMissingOption} already.
|
|
625
621
|
*/
|
|
626
622
|
function admitNull(options, node, ctx) {
|
|
627
623
|
if (options.kind === 'opaque' ||
|
|
628
624
|
!ctx.strictNullChecks ||
|
|
629
625
|
declaresNotNull(options) ||
|
|
630
626
|
findProperty(options, 'isId') ||
|
|
627
|
+
findProperty(options, 'version') ||
|
|
631
628
|
(findProperty(options, 'computed') && !findProperty(options, 'type'))) {
|
|
632
629
|
return;
|
|
633
630
|
}
|
|
@@ -638,18 +635,6 @@ function admitNull(options, node, ctx) {
|
|
|
638
635
|
ctx.edits.push(appended(node.type, ' | null'));
|
|
639
636
|
}
|
|
640
637
|
}
|
|
641
|
-
/** The same null rule where `defineEntity` names the field, on the property of the class the call takes. */
|
|
642
|
-
function admitNullOnDefined(field, owner, ctx) {
|
|
643
|
-
const key = propertyKey(field.name);
|
|
644
|
-
const declaration = key
|
|
645
|
-
? instanceTypeOf(owner.entity, ctx.checker)
|
|
646
|
-
.getProperty(key)
|
|
647
|
-
?.declarations?.find((it) => ts.isPropertyDeclaration(it))
|
|
648
|
-
: undefined;
|
|
649
|
-
if (declaration?.type) {
|
|
650
|
-
admitNull(optionsOf(field.initializer), declaration, ctx);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
638
|
/** Whether the options say the column refuses `null`; `nullable: true` is the default said out loud. */
|
|
654
639
|
function declaresNotNull(options) {
|
|
655
640
|
const nullable = findProperty(options, 'nullable');
|
|
@@ -676,37 +661,56 @@ function reportRemovedDecorators(node, ctx) {
|
|
|
676
661
|
}
|
|
677
662
|
}
|
|
678
663
|
/**
|
|
679
|
-
* Rewrites `raw('
|
|
680
|
-
* form's included. A computed string is left alone: a template cannot be built from a value not known here.
|
|
664
|
+
* Rewrites the deprecated `raw('...')` into the `sql` tagged template, and a second alias argument into `.as()`, the
|
|
665
|
+
* callback form's included. A computed string is left alone: a template cannot be built from a value not known here.
|
|
681
666
|
*/
|
|
682
667
|
function rewriteRawCall(node, ctx) {
|
|
683
|
-
if (!ts.isCallExpression(node) ||
|
|
668
|
+
if (!ts.isCallExpression(node) || uqlExport(node.expression, ctx.checker) !== LEGACY_SQL_TAG) {
|
|
684
669
|
return;
|
|
685
670
|
}
|
|
686
671
|
const [expression, alias] = node.arguments;
|
|
687
672
|
if (!expression || node.arguments.length > 2) {
|
|
688
673
|
return;
|
|
689
674
|
}
|
|
690
|
-
const suffix = alias ?
|
|
675
|
+
const suffix = alias ? ['.as(', original(alias), ')'] : [];
|
|
691
676
|
if (ts.isStringLiteral(expression)) {
|
|
692
|
-
ctx.edits.push(replaced(node,
|
|
677
|
+
ctx.edits.push(replaced(node, [original(node.expression), templateOf(expression.text), ...suffix]));
|
|
693
678
|
}
|
|
694
679
|
else if (alias) {
|
|
695
|
-
ctx.edits.push({ start: expression.getEnd(), end: node.getEnd(), text:
|
|
680
|
+
ctx.edits.push({ start: expression.getEnd(), end: node.getEnd(), text: [')', ...suffix] });
|
|
696
681
|
}
|
|
697
682
|
}
|
|
698
683
|
/**
|
|
699
|
-
* Rewrites one
|
|
700
|
-
*
|
|
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.
|
|
701
686
|
*/
|
|
702
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) {
|
|
703
707
|
const ctx = createContext(source, checker, options.strictNullChecks);
|
|
704
|
-
const rewritesRaw = uqlImport(source, 'raw') !== undefined;
|
|
705
708
|
const importsUql = uqlImportDeclarations(source, true).length > 0;
|
|
706
709
|
const importsExpr = uqlImport(source, 'expr', true) !== undefined;
|
|
710
|
+
const importsRaw = uqlImport(source, LEGACY_SQL_TAG, true) !== undefined;
|
|
707
711
|
const visit = (node) => {
|
|
708
712
|
reportRemovedDecorators(node, ctx);
|
|
709
|
-
if (
|
|
713
|
+
if (importsRaw) {
|
|
710
714
|
rewriteRawCall(node, ctx);
|
|
711
715
|
}
|
|
712
716
|
if (ts.isPropertyDeclaration(node)) {
|
|
@@ -715,6 +719,7 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
|
|
|
715
719
|
if (ts.isClassLike(node)) {
|
|
716
720
|
brandIdKey(node, ctx);
|
|
717
721
|
rewriteClassDecorators(node, ctx);
|
|
722
|
+
foldClassDecorators(node, ctx);
|
|
718
723
|
}
|
|
719
724
|
if (ts.isCallExpression(node)) {
|
|
720
725
|
rewriteDefineCall(node, ctx);
|
|
@@ -736,16 +741,20 @@ export function transformFile(source, checker, options = { strictNullChecks: tru
|
|
|
736
741
|
ts.forEachChild(node, visit);
|
|
737
742
|
};
|
|
738
743
|
visit(source);
|
|
744
|
+
const clash = clashingName(ctx);
|
|
745
|
+
if (clash) {
|
|
746
|
+
ctx.report(clash, `'${clash.text}' is already named here, and the codemod writes it: rename that, then run again`);
|
|
747
|
+
return { ...fileResult(ctx, source.getFullText()), ...unedited(source.getFullText()) };
|
|
748
|
+
}
|
|
739
749
|
const dead = new Set([...deadImportNames(ctx), ...rewriteColumnRefs(ctx)]);
|
|
740
|
-
|
|
750
|
+
rewriteImports(dead, ctx);
|
|
741
751
|
reportRemovedExports(ctx);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
};
|
|
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 };
|
|
757
|
+
}
|
|
758
|
+
function fileResult({ source, unresolved, notes }, text) {
|
|
759
|
+
return { fileName: source.fileName, text, changed: text !== source.getFullText(), unresolved, notes };
|
|
751
760
|
}
|
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,40 +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
|
-
'Change it to any dated target, such as `es2024`.');
|
|
32
|
-
}
|
|
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`.');
|
|
33
29
|
}
|
|
34
30
|
// A value the file inherits cannot be edited here, and silence would read as "there was none".
|
|
35
|
-
if (propertyValue(root, 'extends') && !
|
|
31
|
+
if (propertyValue(root, 'extends') && !legacy.length) {
|
|
36
32
|
unresolved.push(`${fileName}: extends another config, so check the base for 'experimentalDecorators', ` +
|
|
37
33
|
"'emitDecoratorMetadata' and 'target'.");
|
|
38
34
|
}
|
|
39
|
-
return
|
|
35
|
+
return unresolved;
|
|
40
36
|
}
|