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/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 { appended, applyEdits, inserted, original, replaced } from './edits.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';
5
8
  import { columnExpressions, handNamedColumns, sqlTag, sqlWhereEdit, templateOf } from './entitySql.js';
6
- import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js';
7
- import { clashingName, deadImportNames, LEGACY_SQL_TAG, reportRemovedExports, rewriteImports, SQL_TAG, sqlName, uqlExport, uqlImport, uqlImportDeclarations, } from './imports.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';
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
- /** 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} }`);
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, `cannot add '${spec.option}' because ${options.reason}`);
99
- return undefined;
80
+ ctx.report(node, options.reason);
100
81
  }
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;
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 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);
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 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);
216
+ const read = definerCall(call);
217
+ if (!read) {
218
+ return;
263
219
  }
264
- else if (name === 'defineIndex' && options) {
265
- rewriteIndexOptions(options, owner, ctx);
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 (name === 'defineRelation' && relation) {
268
- rewriteRelationOptions(relation, owner.param, ctx);
269
- addForeignKeyReference(relation, second && ts.isStringLiteralLike(second) ? second.text : undefined, owner, ctx);
229
+ else if (definer === 'defineRelation' && literal) {
230
+ rewriteRelationOptions(literal, owner.param, ctx);
231
+ addForeignKeyReference(literal, key, owner, ctx);
270
232
  }
271
- else if (name === 'defineFilter' && third) {
272
- renameOption(optionsOf(third), 'condition', 'where', call, ctx);
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
- addFieldType(decorator, node, ctx);
602
- if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
603
- ctx.report(node, options.reason);
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
- addRelationEntity(decorator, node, ctx);
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
- unwrapRelationAlias(node, ctx);
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 addFieldType} already.
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 source file for the standard decorator spec. The builder's and querier's methods are known by
702
- * name, so they are read only in a file importing uql-orm, and `expr` only where it is uql's.
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, `'${clash.text}' is already named here, and the codemod writes it: rename that, then run again`);
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
- return fileResult(ctx, applyEdits(source.getFullText(), ctx.edits));
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 };
@@ -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,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
- * 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
- const legacy = LEGACY_FLAGS.flatMap((flag) => {
21
- const property = propertyAssignment(compilerOptions, flag);
22
- return property ? [property] : [];
23
- });
24
- edits.push(...removeFromList(compilerOptions.properties, legacy, source));
25
- // `target` is reported, not rewritten: removing it silently falls back to the compiler default
26
- // (`es5` for `tsc`), and choosing a replacement means guessing which era the project targets.
27
- const target = propertyValue(compilerOptions, 'target');
28
- if (target && ts.isStringLiteral(target) && target.text.toLowerCase() === 'esnext') {
29
- unresolved.push(`${fileName}: 'target' is 'esnext', the one target that emits decorator syntax untransformed. ` +
30
- 'Change it to any dated target, such as `es2024`.');
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') && !edits.length) {
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 { text: applyEdits(text, edits), changed: edits.length > 0, unresolved };
35
+ return unresolved;
39
36
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uql-codemod",
3
3
  "description": "Upgrade uql-orm across versions that introduce breaking changes",
4
4
  "license": "MIT",
5
- "version": "0.48.2",
5
+ "version": "0.50.0",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=22.18"
@@ -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
- }