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.
@@ -0,0 +1,588 @@
1
+ import ts from 'typescript';
2
+ import { call, flag, flagOrCall } from './columnMethods.js';
3
+ import { formatFindings, locationAt } from './context.js';
4
+ import { applyEdits, editedText, inserted, replaced, replacingUses, reparsedAgain, unedited, } from './edits.js';
5
+ import { deadImports, importRemovalEdits, importText, spelledNames, uqlImportDeclarations } from './imports.js';
6
+ import { isCallback, paramFor } from './keyMaps.js';
7
+ import { CHAINED_OPTIONS, FIRST_CALL_OPTIONS } from './schemaOptions.js';
8
+ import { chainOf, decoratorName, decoratorsOf, FIELD_DECORATORS, identifierText, identifierUses, indentOf, indentUnit, keyOf, objectLiteral, optionsOf, propertiesByKey, propertyAssignment, propertyValue, } from './syntax.js';
9
+ /** The column type each constructor implies, and the builder it is now. */
10
+ const CONSTRUCTORS = {
11
+ String: 'f.string()',
12
+ Number: 'f.number()',
13
+ BigInt: 'f.bigint()',
14
+ Boolean: 'f.boolean()',
15
+ Date: 'f.date()',
16
+ };
17
+ /** The column types a builder of their own names. */
18
+ const NAMED_TYPES = { text: 'f.text()', uuid: 'f.uuid()' };
19
+ /** The method a `false` states and a `true` says already, nothing; a value no literal states is `undefined`. */
20
+ const onlyFalse = (method) => (value) => value === 'false' ? `.${method}(false)` : value === 'true' ? '' : undefined;
21
+ /** Each option a method writes, in the order a builder chains them, with how its value becomes a call. */
22
+ const METHODS = [
23
+ ['defaultValue', call('default')],
24
+ ['onInsert', call('onInsert')],
25
+ ['onUpdate', call('onUpdate')],
26
+ ['updatable', onlyFalse('updatable')],
27
+ ['eager', onlyFalse('eager')],
28
+ ['softDelete', flagOrCall('softDelete')],
29
+ ['unique', flag('unique')],
30
+ ['index', (value) => (value === 'true' ? '.index()' : call('index')(value))],
31
+ ['autoIncrement', (value) => (value === 'true' ? '.autoIncrement()' : call('autoIncrement')(value))],
32
+ ['length', call('length')],
33
+ ['precision', call('precision')],
34
+ ['scale', call('scale')],
35
+ ['dimensions', call('dimensions')],
36
+ ['distance', call('distance')],
37
+ ['columnType', call('columnType')],
38
+ ['name', call('name')],
39
+ ['comment', call('comment')],
40
+ ['references', call('references')],
41
+ ['onDelete', call('onDelete')],
42
+ ];
43
+ /** What a constructor or `nullable()`/`id()` already said, so no method repeats it. */
44
+ const SAID_BY_SHAPE = new Set(['type', 'nullable', 'isId', 'enum', 'shape', 'version', 'computed', 'stored']);
45
+ const KNOWN = new Set([...SAID_BY_SHAPE, ...METHODS.map(([option]) => option)]);
46
+ /** The options a literal states, each by its key, or why it cannot be read. */
47
+ function optionsByKey(node) {
48
+ const options = optionsOf(node);
49
+ if (options.kind === 'opaque') {
50
+ return options.reason;
51
+ }
52
+ return propertiesByKey(options.node) ?? 'its options name one by a computed key';
53
+ }
54
+ /** `value` without an `as const`, which a builder's `const` type parameter makes redundant. */
55
+ function withoutAsConst(value) {
56
+ return ts.isAsExpression(value) && value.type.getText() === 'const' ? value.expression.getText() : value.getText();
57
+ }
58
+ /** The builder a column's options describe, `f.string().nullable()`, or why it cannot be written. */
59
+ function columnBuilder(options, key) {
60
+ const unknown = [...options.keys()].filter((option) => !KNOWN.has(option));
61
+ if (unknown.length) {
62
+ return { message: `has options no builder writes: ${unknown.join(', ')}` };
63
+ }
64
+ const consumed = new Set();
65
+ const text = (option) => {
66
+ const value = options.get(option);
67
+ if (value)
68
+ consumed.add(option);
69
+ return value && withoutAsConst(value);
70
+ };
71
+ const constructor = constructorOf(options, text);
72
+ if (typeof constructor !== 'string') {
73
+ return constructor;
74
+ }
75
+ const nullable = text('nullable');
76
+ if (nullable !== undefined && nullable !== 'true' && nullable !== 'false') {
77
+ return { message: `has 'nullable: ${nullable}', which no builder states unless it is a literal` };
78
+ }
79
+ const isKey = key || text('isId') === 'true';
80
+ const present = nullable === 'false' || isKey || options.has('version');
81
+ const shape = isKey ? '.id()' : present ? '' : '.nullable()';
82
+ const methods = [];
83
+ for (const [option, method] of METHODS) {
84
+ const value = consumed.has(option) ? undefined : text(option);
85
+ if (value === undefined)
86
+ continue;
87
+ const written = method(value);
88
+ if (written === undefined) {
89
+ return { message: `has '${option}: ${value}', which no builder method states unless it is a literal` };
90
+ }
91
+ methods.push(written);
92
+ }
93
+ return `${constructor}${shape}${methods.join('')}`;
94
+ }
95
+ /** `f.decimal()` with the precision and scale its options state. */
96
+ function decimalOf(text) {
97
+ return `f.decimal(${[text('precision'), text('scale')].filter(Boolean).join(', ')})`;
98
+ }
99
+ /** The builder a column starts from, its `type` read with whatever the constructor itself takes. */
100
+ function constructorOf(options, text) {
101
+ const type = text('type');
102
+ const members = text('enum');
103
+ if (options.has('shape') && type !== "'json'" && type !== "'jsonb'") {
104
+ return { message: "has a 'shape' on a column that is no JSON: only a JSON column holds a payload" };
105
+ }
106
+ if (options.has('version')) {
107
+ text('version');
108
+ if (type === undefined || type === 'Number')
109
+ return 'f.version()';
110
+ return type === 'BigInt' ? 'f.version(BigInt)' : { message: `is a version of type ${type}: use f.version()` };
111
+ }
112
+ if (members !== undefined) {
113
+ return type === 'String' || type === 'Number' || type === undefined
114
+ ? `f.enum(${members})`
115
+ : { message: `has an enum on a '${type}' column: declare it f.enum([...]).columnType(${type})` };
116
+ }
117
+ if (type === undefined) {
118
+ const references = text('references');
119
+ return references ? `f.references(${references})` : { message: "has no 'type': state it, f.string() and so on" };
120
+ }
121
+ const columnType = options.get('columnType')?.getText();
122
+ if (type === 'String' && (columnType === "'decimal'" || columnType === "'numeric'")) {
123
+ text('columnType');
124
+ return columnType === "'decimal'" ? decimalOf(text) : `f.column('numeric')`;
125
+ }
126
+ if (CONSTRUCTORS[type]) {
127
+ return CONSTRUCTORS[type];
128
+ }
129
+ const literal = /^'(.+)'$/.exec(type)?.[1];
130
+ if (!literal) {
131
+ return { message: `has a type written as '${type}': write its builder, f.string() and so on` };
132
+ }
133
+ if (NAMED_TYPES[literal]) {
134
+ return NAMED_TYPES[literal];
135
+ }
136
+ if (literal === 'json' || literal === 'jsonb') {
137
+ const shape = text('shape');
138
+ return `f.${literal}(${shape ?? ''})`;
139
+ }
140
+ if (literal === 'vector') {
141
+ const dimensions = text('dimensions');
142
+ return dimensions ? `f.vector(${dimensions})` : `f.column('vector')`;
143
+ }
144
+ if (literal === 'decimal') {
145
+ return decimalOf(text);
146
+ }
147
+ return `f.column('${literal}')`;
148
+ }
149
+ /** A computed field's builder: the column it reads as, then the SQL, kept where `stored` says so. */
150
+ function computedBuilder(options, expression) {
151
+ const stored = options.get('stored')?.getText();
152
+ const column = new Map([...options].filter(([option]) => option !== 'computed' && option !== 'stored'));
153
+ const built = columnBuilder(column, false);
154
+ if (typeof built !== 'string') {
155
+ return built;
156
+ }
157
+ const keep = stored === undefined || stored === 'false' ? '' : stored === 'true' ? '.stored()' : `.stored(${stored})`;
158
+ return `${built}.as(${expression})${keep}`;
159
+ }
160
+ /** `(post) => sql`...``'s body, its parameter renamed `param`; the expression itself where it is no callback. */
161
+ function bodyAs(value, param) {
162
+ if (!isCallback(value)) {
163
+ return value.getText();
164
+ }
165
+ const [first, ...rest] = value.parameters;
166
+ const body = value.body;
167
+ if (rest.length || ts.isBlock(body)) {
168
+ return undefined;
169
+ }
170
+ const name = first && identifierText(first.name);
171
+ if (!name || name === param) {
172
+ return parenthesized(body);
173
+ }
174
+ return parenthesized(body, identifierUses(body, name), param);
175
+ }
176
+ /** The text of `body`, each of `uses` written `replacement`, unwrapped of the parentheses an object body needs. */
177
+ function parenthesized(body, uses = [], replacement = '') {
178
+ const inner = ts.isParenthesizedExpression(body) ? body.expression : body;
179
+ return replacingUses(inner, uses, replacement);
180
+ }
181
+ /** `@Field({...})` and `@Id({...})` as builders, a computed one as `@Computed`. */
182
+ function decoratorEdits(property, findings, used) {
183
+ return decoratorsOf(property).flatMap((decorator) => {
184
+ const name = decoratorName(decorator);
185
+ if ((name !== 'Field' && name !== 'Id') || !ts.isCallExpression(decorator.expression)) {
186
+ return [];
187
+ }
188
+ const [argument] = decorator.expression.arguments;
189
+ if (!argument && name === 'Id') {
190
+ used.add('f');
191
+ return [replaced(decorator.expression, 'Field(f.number().id())')];
192
+ }
193
+ if (!argument || isBuilder(argument)) {
194
+ return [];
195
+ }
196
+ const options = optionsByKey(argument);
197
+ if (typeof options === 'string') {
198
+ findings.push({ node: decorator, message: `cannot write a builder because ${options}` });
199
+ return [];
200
+ }
201
+ const computed = options.get('computed');
202
+ if (computed) {
203
+ return computedDecorator(decorator.expression, options, computed, findings, used);
204
+ }
205
+ const built = columnBuilder(options, name === 'Id');
206
+ if (typeof built !== 'string') {
207
+ findings.push({ node: decorator, message: built.message });
208
+ return [];
209
+ }
210
+ used.add('f');
211
+ return [replaced(decorator.expression, `Field(${built})`)];
212
+ });
213
+ }
214
+ /** `@Computed((row) => ...)`: an aggregate as it is, SQL as the column it reads as. */
215
+ function computedDecorator(call, options, computed, findings, used) {
216
+ used.add('Computed');
217
+ if (!options.has('type') && !options.has('references')) {
218
+ return [replaced(call, `Computed(${computed.getText()})`)];
219
+ }
220
+ const param = (ts.isArrowFunction(computed) && identifierText(computed.parameters[0]?.name)) || 'row';
221
+ const expression = bodyAs(computed, param);
222
+ if (expression === undefined) {
223
+ findings.push({ node: call, message: 'cannot inline a computed callback with a block body' });
224
+ return [];
225
+ }
226
+ const built = computedBuilder(options, expression);
227
+ if (typeof built !== 'string') {
228
+ findings.push({ node: call, message: built.message });
229
+ return [];
230
+ }
231
+ used.add('f');
232
+ return [replaced(call, `Computed((${param}) => ${built})`)];
233
+ }
234
+ /** Whether `node` is already a builder, `f.string()...`. */
235
+ export function isBuilder(node) {
236
+ return identifierText(chainOf(node).root) === 'f';
237
+ }
238
+ /** The options a schema's first call keeps. */
239
+ const FIRST_CALL = new Set(FIRST_CALL_OPTIONS);
240
+ /** Every key a schema's `defineEntity` options may state, as its first call or as a section. */
241
+ const SCHEMA_KEYS = new Set([
242
+ ...FIRST_CALL,
243
+ ...CHAINED_OPTIONS,
244
+ 'relations',
245
+ 'aggregates',
246
+ ]);
247
+ /** `defineEntity({ fields: {...}, relations, ... })` as its first call and a chain of sections. */
248
+ function schemaEdits(call, findings, used) {
249
+ const options = objectLiteral(call.arguments[0]);
250
+ const fields = options && objectLiteral(propertyValue(options, 'fields'));
251
+ const chained = options?.properties.some((prop) => !FIRST_CALL.has(keyOf(prop)));
252
+ if (!options || !fields || (!chained && fields.properties.every((prop) => isSchemaColumn(prop)))) {
253
+ return [];
254
+ }
255
+ const param = schemaParam(call);
256
+ const unknown = options.properties.filter((prop) => !SCHEMA_KEYS.has(keyOf(prop)));
257
+ if (unknown.length) {
258
+ findings.push({
259
+ node: call,
260
+ message: `cannot write a chain: it states ${unknown.map((it) => it.getText()).join(', ')}`,
261
+ });
262
+ return [];
263
+ }
264
+ const references = [];
265
+ const field = propertyAssignment(options, 'fields');
266
+ const columns = fields.properties.map((prop) => {
267
+ if (!ts.isPropertyAssignment(prop)) {
268
+ findings.push({ node: prop, message: `cannot write '${prop.getText()}' as a column: write its options inline` });
269
+ return undefined;
270
+ }
271
+ const key = prop.name.getText();
272
+ const built = isBuilder(prop.initializer)
273
+ ? builtSchemaColumn(prop.initializer, key, references)
274
+ : schemaColumnOf(prop.initializer, key, references);
275
+ if (typeof built !== 'string') {
276
+ findings.push({ node: prop, message: `'${key}' ${built.message}` });
277
+ return undefined;
278
+ }
279
+ return withValue(prop, built);
280
+ });
281
+ const sections = CHAINED_OPTIONS.flatMap((section) => {
282
+ const value = propertyValue(options, section);
283
+ return value ? [chainedSection(section, value, param, findings)] : [];
284
+ });
285
+ if (references.length) {
286
+ sections.push(`.references({ ${references.join(', ')} })`);
287
+ }
288
+ const relations = propertyValue(options, 'relations');
289
+ const aggregates = propertyValue(options, 'aggregates');
290
+ if (relations || aggregates) {
291
+ sections.push(`.relations(${relations?.getText() ?? '() => ({})'}${aggregates ? `, ${aggregates.getText()}` : ''})`);
292
+ }
293
+ const written = columns.filter((it) => it !== undefined);
294
+ if (written.length !== columns.length || sections.some((it) => it === undefined)) {
295
+ return [];
296
+ }
297
+ used.add('f');
298
+ const kept = options.properties
299
+ .filter((prop) => FIRST_CALL.has(keyOf(prop)))
300
+ .map((prop) => (prop === field ? withValue(field, literal(fields, written)) : prop.getFullText()));
301
+ const indent = indentOf(call);
302
+ return [
303
+ replaced(call, `${call.expression.getText()}(${literal(options, kept)})${sections.map((it) => `\n${indent} ${it}`).join('')}`),
304
+ ];
305
+ }
306
+ /** The parameter a schema's callbacks are named after: its `name` where that is a literal, else its variable's. */
307
+ function schemaParam(call) {
308
+ const options = objectLiteral(call.arguments[0]);
309
+ const name = options && /^'(.+)'$/.exec(propertyValue(options, 'name')?.getText() ?? '')?.[1];
310
+ return paramFor(name ?? identifierText(variableOf(call)));
311
+ }
312
+ /** A schema column written as options, through {@link schemaColumn}, or why it cannot be. */
313
+ function schemaColumnOf(node, key, references) {
314
+ const column = optionsByKey(node);
315
+ return typeof column === 'string'
316
+ ? { message: `cannot be written as a builder because ${column}` }
317
+ : schemaColumn(column, key, references);
318
+ }
319
+ /**
320
+ * A schema column's builder, its foreign key moved into `.references()`, where its target is read once every entity
321
+ * exists: written in `fields`, it would read the target while that target's own type is still being inferred.
322
+ */
323
+ function schemaColumn(options, key, references) {
324
+ const target = options.get('references');
325
+ if (!target) {
326
+ return columnBuilder(options, false);
327
+ }
328
+ if (!options.has('type')) {
329
+ return { message: "references another entity but states no 'type': state the column's, as its key's" };
330
+ }
331
+ references.push(referenceEntry(key, target.getText(), options.get('onDelete')?.getText()));
332
+ return columnBuilder(new Map([...options].filter(([option]) => option !== 'references' && option !== 'onDelete')), false);
333
+ }
334
+ /** One entry of `.references()`: the target's getter, with its `onDelete` where one is stated. */
335
+ function referenceEntry(key, getter, action) {
336
+ return action ? `${key}: { entity: ${getter}, onDelete: ${action} }` : `${key}: ${getter}`;
337
+ }
338
+ /** Whether a schema column is a builder naming no foreign key, which a schema's `fields` keep as they are. */
339
+ function isSchemaColumn(prop) {
340
+ return (ts.isPropertyAssignment(prop) && isBuilder(prop.initializer) && !chainMethods(prop.initializer).has('references'));
341
+ }
342
+ /** Each method a builder chain calls, by name. */
343
+ function chainMethods(node) {
344
+ return new Map(chainOf(node).methods.map((method) => [method.name, method]));
345
+ }
346
+ /** A builder in `fields` with its foreign key, `.references(() => X)` and `.onDelete()`, moved into `.references()`. */
347
+ function builtSchemaColumn(node, key, references) {
348
+ const methods = chainMethods(node);
349
+ const target = methods.get('references');
350
+ if (!target) {
351
+ return node.getText();
352
+ }
353
+ if (identifierText(target.receiver) === 'f') {
354
+ return { message: "references another entity but states no type: write its builder, as its key's" };
355
+ }
356
+ const onDelete = methods.get('onDelete');
357
+ references.push(referenceEntry(key, target.call.arguments[0]?.getText(), onDelete?.call.arguments[0]?.getText()));
358
+ // Each method is cut out with the chain before it: from that receiver's end to the call's.
359
+ const cut = [onDelete, target].flatMap((method) => method
360
+ ? [{ start: method.receiver.getEnd() - node.getStart(), end: method.call.getEnd() - node.getStart(), text: '' }]
361
+ : []);
362
+ return applyEdits(node.getText(), cut);
363
+ }
364
+ /**
365
+ * An object literal of `properties`, each its full text with its comments, laid out as `node` was: on one line
366
+ * where it was written on one, a property a line otherwise.
367
+ */
368
+ function literal(node, properties) {
369
+ if (!properties.length) {
370
+ return '{}';
371
+ }
372
+ if (!node.getText().includes('\n')) {
373
+ return `{ ${properties.map((it) => it.trim()).join(', ')} }`;
374
+ }
375
+ return `{${properties.join(',')},\n${indentOf(node)}}`;
376
+ }
377
+ /** One chained section, or `undefined` where its findings say why not. */
378
+ function chainedSection(section, value, param, findings) {
379
+ if (isHelped(section)) {
380
+ const callback = sectionCallback(section, value, param, findings, value);
381
+ return callback && `.${section}(${callback})`;
382
+ }
383
+ if (section !== 'computed') {
384
+ return `.${section}(${value.getText()})`;
385
+ }
386
+ const computed = computedSection(value, param);
387
+ if (!computed)
388
+ findings.push({ node: value, message: "cannot write 'computed' as a section: inline each callback" });
389
+ return computed && `.computed((${param}) => (${computed}))`;
390
+ }
391
+ /** What each section's helper is named. */
392
+ const SECTION_HELPERS = { indexes: 'index', checks: 'check', triggers: 'trigger' };
393
+ const isHelped = (key) => Object.hasOwn(SECTION_HELPERS, key);
394
+ /** {@link listCallback}, reported against `node` where it cannot be written. */
395
+ function sectionCallback(section, list, param, findings, node) {
396
+ const callback = list && listCallback(section, list, param);
397
+ if (!callback) {
398
+ findings.push({
399
+ node,
400
+ message: `cannot write '${section}' as a section: write each entry as ${SECTION_HELPERS[section]}(...) over its refs`,
401
+ });
402
+ }
403
+ return callback;
404
+ }
405
+ /**
406
+ * A list of indexes, checks or triggers as one callback over the entity's refs, each entry through its helper, so
407
+ * TypeScript checks every key: `(post, index) => [index([post.title], { unique: true })]`. Comments above an entry
408
+ * stay above it.
409
+ */
410
+ function listCallback(section, list, param) {
411
+ if (!ts.isArrayLiteralExpression(list)) {
412
+ return undefined;
413
+ }
414
+ const helper = SECTION_HELPERS[section];
415
+ const entries = list.elements.map((entry) => {
416
+ const options = objectLiteral(entry);
417
+ const call = options && helperCall(section, options, param);
418
+ return call && `${leadingComments(entry)}${call}`;
419
+ });
420
+ if (!entries.every((entry) => entry !== undefined)) {
421
+ return undefined;
422
+ }
423
+ const multiline = list.getText().includes('\n');
424
+ const indent = `\n${indentOf(list)}${indentUnit(list.getSourceFile())}`;
425
+ const listed = multiline
426
+ ? `[${entries.map((entry) => (entry.startsWith('\n') ? entry : `${indent}${entry}`)).join(',')},\n${indentOf(list)}]`
427
+ : `[${entries.join(', ')}]`;
428
+ return `(${param}, ${helper}) => ${listed}`;
429
+ }
430
+ /** One entry as its helper's call: `index(columns, options)`, `check(where, name)` or `trigger({...})`. */
431
+ function helperCall(section, options, param) {
432
+ const props = optionsByKey(options);
433
+ if (typeof props === 'string') {
434
+ return undefined;
435
+ }
436
+ const read = (key) => {
437
+ const value = props.get(key);
438
+ return value && bodyAs(value, param);
439
+ };
440
+ if (section === 'checks') {
441
+ const where = read('where');
442
+ const name = props.get('name')?.getText();
443
+ const others = [...props.keys()].filter((key) => key !== 'where' && key !== 'name');
444
+ return where && !others.length ? `check(${[where, name].filter(Boolean).join(', ')})` : undefined;
445
+ }
446
+ const inlined = (keys) => {
447
+ const entries = [...props].map(([key, value]) => {
448
+ const text = keys.includes(key) ? bodyAs(value, param) : value.getText();
449
+ return text === undefined ? undefined : `${key}: ${text}`;
450
+ });
451
+ return entries.every((entry) => entry !== undefined) ? entries : undefined;
452
+ };
453
+ if (section === 'triggers') {
454
+ const entries = inlined(['of']);
455
+ return entries && `trigger({ ${entries.join(', ')} })`;
456
+ }
457
+ const columns = read('columns');
458
+ props.delete('columns');
459
+ const rest = inlined(['include', 'where']);
460
+ if (!columns || !rest) {
461
+ return undefined;
462
+ }
463
+ const name = props.get('name');
464
+ if (rest.length === 1 && name && ts.isStringLiteralLike(name)) {
465
+ return `index(${columns}, ${name.getText()})`;
466
+ }
467
+ return rest.length ? `index(${columns}, { ${rest.join(', ')} })` : `index(${columns})`;
468
+ }
469
+ /** The comments written above `node`, each on its own line, kept as they are; nothing where there are none. */
470
+ function leadingComments(node) {
471
+ const trivia = node.getFullText().slice(0, node.getStart() - node.getFullStart());
472
+ return /\/\/|\/\*/.test(trivia) ? trivia : '';
473
+ }
474
+ /** `@Entity({ indexes: [...] })`'s lists as sections over the class's refs. */
475
+ function entityEdits(cls, findings) {
476
+ const param = paramFor(identifierText(cls.name));
477
+ return decoratorsOf(cls).flatMap((decorator) => {
478
+ if (decoratorName(decorator) !== 'Entity' || !ts.isCallExpression(decorator.expression)) {
479
+ return [];
480
+ }
481
+ const options = objectLiteral(decorator.expression.arguments[0]);
482
+ return (options?.properties ?? []).flatMap((prop) => {
483
+ const key = keyOf(prop);
484
+ if (!key || !isHelped(key) || (ts.isPropertyAssignment(prop) && isCallback(prop.initializer))) {
485
+ return [];
486
+ }
487
+ const list = ts.isPropertyAssignment(prop) ? prop.initializer : undefined;
488
+ const callback = sectionCallback(key, list, param, findings, prop);
489
+ return callback && list ? [replaced(list, callback)] : [];
490
+ });
491
+ });
492
+ }
493
+ /** A schema's computed map as what its section returns: each a column `as()` its SQL. */
494
+ function computedSection(value, param) {
495
+ const map = objectLiteral(value);
496
+ const entries = map?.properties.map((prop) => {
497
+ if (!ts.isPropertyAssignment(prop))
498
+ return undefined;
499
+ const options = optionsByKey(prop.initializer);
500
+ if (typeof options === 'string')
501
+ return undefined;
502
+ const computed = options.get('computed');
503
+ const expression = computed && bodyAs(computed, param);
504
+ const built = expression && computedBuilder(options, expression);
505
+ return typeof built === 'string' ? `${prop.name.getText()}: ${built}` : undefined;
506
+ });
507
+ return entries?.every((entry) => entry !== undefined) ? `{ ${entries.join(', ')} }` : undefined;
508
+ }
509
+ /** The variable a call initializes, which names the entity where its `name` is no literal. */
510
+ function variableOf(call) {
511
+ let at = call;
512
+ while (ts.isPropertyAccessExpression(at.parent) || ts.isCallExpression(at.parent))
513
+ at = at.parent;
514
+ return ts.isVariableDeclaration(at.parent) ? at.parent.name : undefined;
515
+ }
516
+ /** A property's full text, its comments kept, its value written `value`. */
517
+ function withValue(prop, value) {
518
+ const full = prop.getFullText();
519
+ const at = prop.getFullStart();
520
+ return full.slice(0, prop.initializer.getStart() - at) + value + full.slice(prop.initializer.getEnd() - at);
521
+ }
522
+ /** The names a builder writes, which the file must not bind otherwise. */
523
+ export const BUILDER_NAMES = new Set(['f', 'Computed']);
524
+ /** Whether the file declares anything this pass rewrites. */
525
+ const REWRITES = /@(?:Field|Id)\(|\bdefineEntity\(\s*\{|@Entity\(\s*\{/;
526
+ /**
527
+ * Writes entity definitions as column builders, `@Field(f.string().nullable())`, and a schema as its columns and a
528
+ * chain of sections. Read off the text the other rewrites produced, so it only moves what is written; whatever it
529
+ * cannot write faithfully is left as it is and reported.
530
+ */
531
+ export function toBuilders(fileName, text, { locate, taken }) {
532
+ if (!REWRITES.test(text)) {
533
+ return { ...unedited(text), unresolved: [] };
534
+ }
535
+ const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true);
536
+ const findings = [];
537
+ const used = new Set();
538
+ const edits = [];
539
+ const visit = (node) => {
540
+ if (ts.isPropertyDeclaration(node)) {
541
+ edits.push(...decoratorEdits(node, findings, used));
542
+ }
543
+ if (ts.isClassLike(node)) {
544
+ edits.push(...entityEdits(node, findings));
545
+ }
546
+ if (ts.isCallExpression(node) && identifierText(node.expression) === 'defineEntity') {
547
+ const schema = schemaEdits(node, findings, used);
548
+ if (schema.length) {
549
+ edits.push(...schema);
550
+ return;
551
+ }
552
+ }
553
+ ts.forEachChild(node, visit);
554
+ };
555
+ visit(source);
556
+ const located = locate ?? ((at) => locationAt(source, at));
557
+ const unresolved = formatFindings(findings, located);
558
+ if (!edits.length) {
559
+ return { ...unedited(text), unresolved };
560
+ }
561
+ const clash = [...used].find((name) => taken.has(name));
562
+ if (clash) {
563
+ return {
564
+ ...unedited(text),
565
+ unresolved: [
566
+ ...unresolved,
567
+ `${located(0)}: '${clash}' is already named here, and the codemod writes it: rename that, then run again`,
568
+ ],
569
+ };
570
+ }
571
+ return { ...reparsedAgain(fileName, editedText(text, edits), (edited) => importEdits(edited, used)), unresolved };
572
+ }
573
+ /** Imports what the pass now writes, `f` and `Computed`, and no longer the field decorators it replaced, once unspelled. */
574
+ function importEdits(source, used) {
575
+ const [declaration] = uqlImportDeclarations(source, true);
576
+ if (!declaration) {
577
+ return used.size ? [{ start: 0, end: 0, text: `${importText('import', [...used].toSorted(), 'uql-orm')}\n` }] : [];
578
+ }
579
+ const names = new Set(declaration.elements.map((it) => it.name.text));
580
+ const added = [...used].filter((name) => !names.has(name)).toSorted();
581
+ const dead = deadImports(declaration.elements, FIELD_DECORATORS, spelledNames(source));
582
+ const first = declaration.elements.find((it) => !dead.includes(it));
583
+ if (!first) {
584
+ return [replaced(declaration.declaration, importText('import', added, 'uql-orm'))];
585
+ }
586
+ const removals = importRemovalEdits(declaration.declaration, declaration.elements, dead);
587
+ return added.length ? [...removals, inserted(first, `${added.join(', ')}, `)] : removals;
588
+ }
@@ -0,0 +1,9 @@
1
+ import ts from 'typescript';
2
+ import type { Context } from './context.js';
3
+ /**
4
+ * Folds `@Index`, `@Trigger` and `@Filter` into the class's `@Entity` options, `indexes`, `triggers` and `filters`.
5
+ * Triggers keep the order they register in, which is the order they run in: decorators apply bottom-up, so those
6
+ * below `@Entity` go ahead of its own and those above after them. Indexes and filters, whose order means nothing,
7
+ * follow `@Entity`'s own in the order written. A class with no `@Entity` becomes `@Entity({ abstract: true, ... })`.
8
+ */
9
+ export declare function foldClassDecorators(node: ts.ClassLikeDeclaration, ctx: Context): void;