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.
Files changed (43) hide show
  1. package/README.md +27 -16
  2. package/dist/builders.d.ts +18 -0
  3. package/dist/builders.js +588 -0
  4. package/dist/classOptions.d.ts +9 -0
  5. package/dist/classOptions.js +240 -0
  6. package/dist/columnMethods.d.ts +11 -0
  7. package/dist/columnMethods.js +6 -0
  8. package/dist/columnRefs.js +3 -10
  9. package/dist/context.d.ts +18 -5
  10. package/dist/context.js +22 -4
  11. package/dist/definers.d.ts +24 -0
  12. package/dist/definers.js +34 -0
  13. package/dist/edits.d.ts +43 -11
  14. package/dist/edits.js +105 -26
  15. package/dist/entitySql.d.ts +15 -16
  16. package/dist/entitySql.js +33 -31
  17. package/dist/imports.d.ts +33 -16
  18. package/dist/imports.js +213 -99
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +9 -12
  21. package/dist/jsonIndex.d.ts +11 -0
  22. package/dist/jsonIndex.js +78 -0
  23. package/dist/keyMaps.d.ts +3 -1
  24. package/dist/keyMaps.js +5 -1
  25. package/dist/migrationBuilders.d.ts +13 -0
  26. package/dist/migrationBuilders.js +182 -0
  27. package/dist/relationTarget.d.ts +6 -0
  28. package/dist/relationTarget.js +21 -0
  29. package/dist/schemaEntity.d.ts +12 -0
  30. package/dist/schemaEntity.js +518 -0
  31. package/dist/schemaOptions.d.ts +6 -0
  32. package/dist/schemaOptions.js +6 -0
  33. package/dist/statementSql.d.ts +2 -1
  34. package/dist/statementSql.js +10 -4
  35. package/dist/syntax.d.ts +54 -0
  36. package/dist/syntax.js +131 -1
  37. package/dist/transform.d.ts +11 -2
  38. package/dist/transform.js +174 -165
  39. package/dist/tsconfig.d.ts +3 -9
  40. package/dist/tsconfig.js +18 -22
  41. package/package.json +1 -1
  42. package/dist/fieldType.d.ts +0 -13
  43. 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 { appended, applyEdits, inserted, replaced } from './edits.js';
5
- import { columnExpressions, handNamedColumns, rawTag, rawWhereEdit } from './entitySql.js';
6
- import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
7
- import { addImports, deadImportNames, dropDeadImports, renameExports, reportRemovedExports, uqlImport, uqlImportDeclarations, } from './imports.js';
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}) => raw\`...\${${owner.param}.<member>}...\``);
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
- * 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, `cannot add '${spec.option}' because ${options.reason}`);
99
- return undefined;
79
+ ctx.report(node, options.reason);
100
80
  }
101
- const type = ctx.checker.getTypeAtLocation(node);
102
- const value = spec.infer(type);
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 carrying one of `names` as a decorator. */
203
- const decoratedWith = (names) => (node) => ts.isPropertyDeclaration(node) && decoratorsOf(node).some((decorator) => names.has(decoratorName(decorator) ?? ''));
204
- const isIdProperty = decoratedWith(new Set(['Id']));
205
- const isFieldProperty = decoratedWith(FIELD_DECORATORS);
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 name = identifierText(call.expression);
257
- const [entity = call, second, third] = call.arguments;
258
- const owner = ownerOf(entity);
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
- else if (name === 'defineIndex' && options) {
265
- rewriteIndexOptions(options, owner, ctx);
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 (name === 'defineRelation' && relation) {
268
- rewriteRelationOptions(relation, owner.param, ctx);
269
- addForeignKeyReference(relation, second && ts.isStringLiteralLike(second) ? second.text : undefined, owner, ctx);
228
+ else if (definer === 'defineRelation' && literal) {
229
+ rewriteRelationOptions(literal, owner.param, ctx);
230
+ addForeignKeyReference(literal, key, owner, ctx);
270
231
  }
271
- else if (name === 'defineFilter' && third) {
272
- renameOption(optionsOf(third), 'condition', 'where', call, ctx);
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 `raw`, on an entity and in the migration builder alike, or reported. */
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 = rawWhereEdit(where);
300
+ const edit = sqlWhereEdit(where, sqlName(ctx.source));
325
301
  if (edit) {
326
302
  ctx.edits.push(edit);
327
- ctx.imports.set('raw', 'the raw`...`');
303
+ ctx.imports.set(SQL_TAG, 'the sql`...`');
328
304
  }
329
305
  else {
330
- ctx.report(where, "write the partial-index 'where' as raw`...` or a predicate; this one could not be read");
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 `raw.text`, which splices as the string did. Values it cannot place are reported.
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 ? `<${call.typeArguments.map((type) => type.getText()).join(', ')}>` : '';
373
- ctx.edits.push(replaced(call, `${call.expression.getText()}${typeArguments}${template}`));
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 raw statement: write the values into it, raw\`... \${value}\`, which binds them`);
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 raw`...` to bind them');
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, `raw.text(${sql.getText()})`));
384
- ctx.imports.set('raw', 'the raw.text(...)');
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 `raw` an entity declares, or reports
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, rawTag(sql.text)));
432
- ctx.imports.set('raw', 'the raw`...`');
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.getText()));
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
- addFieldType(decorator, node, ctx);
600
- if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
601
- ctx.report(node, options.reason);
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
- addRelationEntity(decorator, node, ctx);
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
- unwrapRelationAlias(node, ctx);
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 addFieldType} already.
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('sql')` into the tagged template, and a second alias argument into `.as()`, the callback
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) || identifierText(node.expression) !== 'raw') {
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 ? `.as(${alias.getText()})` : '';
675
+ const suffix = alias ? ['.as(', original(alias), ')'] : [];
691
676
  if (ts.isStringLiteral(expression)) {
692
- ctx.edits.push(replaced(node, `${rawTag(expression.text)}${suffix}`));
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: `)${suffix}` });
680
+ ctx.edits.push({ start: expression.getEnd(), end: node.getEnd(), text: [')', ...suffix] });
696
681
  }
697
682
  }
698
683
  /**
699
- * Rewrites one source file for the standard decorator spec. The builder's and querier's methods are known by
700
- * name, so they are read only in a file importing uql-orm, and `expr` only where it is uql's.
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 (rewritesRaw) {
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
- const rewritten = renameExports(dead, ctx);
750
+ rewriteImports(dead, ctx);
741
751
  reportRemovedExports(ctx);
742
- dropDeadImports(dead, rewritten, ctx);
743
- addImports(rewritten, ctx);
744
- return {
745
- fileName: source.fileName,
746
- text: applyEdits(source.getFullText(), ctx.edits),
747
- changed: ctx.edits.length > 0,
748
- unresolved: ctx.unresolved,
749
- notes: ctx.notes,
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
  }
@@ -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
- * Rewrites a `tsconfig.json` for the standard decorator spec, as text through `ts.parseJsonText` rather than
9
- * re-serialised: a tsconfig is JSON with comments and someone's formatting, which a rewrite would throw away.
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 transformTsconfig(fileName: string, text: string): TsconfigResult;
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
- * Rewrites a `tsconfig.json` for the standard decorator spec, as text through `ts.parseJsonText` rather than
8
- * re-serialised: a tsconfig is JSON with comments and someone's formatting, which a rewrite would throw away.
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 transformTsconfig(fileName, text) {
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 { text, changed: false, unresolved: [`${fileName}: not an object, so nothing was changed`] };
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
- if (compilerOptions && ts.isObjectLiteralExpression(compilerOptions)) {
20
- for (const flag of LEGACY_FLAGS) {
21
- const property = propertyAssignment(compilerOptions, flag);
22
- if (property) {
23
- edits.push(removeFromList(compilerOptions.properties, property, source));
24
- }
25
- }
26
- // `target` is reported, not rewritten: removing it silently falls back to the compiler default
27
- // (`es5` for `tsc`), and choosing a replacement means guessing which era the project targets.
28
- const target = propertyValue(compilerOptions, 'target');
29
- if (target && ts.isStringLiteral(target) && target.text.toLowerCase() === 'esnext') {
30
- unresolved.push(`${fileName}: 'target' is 'esnext', the one target that emits decorator syntax untransformed. ` +
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') && !edits.length) {
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 { text: applyEdits(text, edits), changed: edits.length > 0, unresolved };
35
+ return unresolved;
40
36
  }