uql-codemod 0.48.2 → 0.49.0

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