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.
@@ -0,0 +1,21 @@
1
+ import ts from 'typescript';
2
+ /** Reads a union by requiring its arms to agree, `undefined` and `null` dropped first: `Company | null` is `Company`. */
3
+ function foldUnion(type, read) {
4
+ const arms = type.types.filter((arm) => !(arm.getFlags() & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)));
5
+ const readings = new Set(arms.map(read));
6
+ return readings.size === 1 ? [...readings][0] : undefined;
7
+ }
8
+ /**
9
+ * The entity name a relation property points at: `Company` for `Company` and `Company[]` alike, since the
10
+ * element of an array is what a to-many relation targets.
11
+ */
12
+ export function relationTargetFor(type, checker) {
13
+ if (type.isUnion()) {
14
+ return foldUnion(type, (arm) => relationTargetFor(arm, checker));
15
+ }
16
+ const elementType = checker.getIndexTypeOfType(type, ts.IndexKind.Number);
17
+ const target = elementType ?? type;
18
+ const symbol = target.getSymbol();
19
+ const name = symbol?.getName();
20
+ return name && name !== '__type' && /^[A-Z][A-Za-z0-9_]*$/.test(name) ? name : undefined;
21
+ }
@@ -0,0 +1,15 @@
1
+ import { type Pass } from './edits.js';
2
+ export type SchemaResult = Pass & {
3
+ readonly unresolved: string[];
4
+ readonly notes: string[];
5
+ };
6
+ /**
7
+ * Declares each class `defineEntity(Class, options)` registered as the schema `defineEntity({ name, fields })` and its
8
+ * chain of sections, folding the string-keyed definers of the same class in. Read off the text the other rewrites
9
+ * produced, whose options already name members by callback, so it only moves what is written. An entity it cannot
10
+ * declare faithfully, `taken` naming a builder it writes among them, is left as written and reported.
11
+ */
12
+ export declare function toSchemaEntities(fileName: string, text: string, { locate, taken }: {
13
+ readonly locate?: (at: number) => string;
14
+ readonly taken: ReadonlySet<string>;
15
+ }): SchemaResult;
@@ -0,0 +1,578 @@
1
+ import ts from 'typescript';
2
+ import { chainedSchema } from './builders.js';
3
+ import { formatFindings, locationAt } from './context.js';
4
+ import { CLASS_DEFINERS, definerCall } from './definers.js';
5
+ import { editedText, removeStatement, replaced, replacingUses, reparsedAgain, unedited, } from './edits.js';
6
+ import { importEdits } from './imports.js';
7
+ import { memberKey, paramFor, quoted } from './keyMaps.js';
8
+ import { nameClash } from './schemaMessages.js';
9
+ import { KEPT_OPTIONS } from './schemaOptions.js';
10
+ import { arrayElements, decoratorsOf, freeName, hasModifier, identifierText, identifierUses, indentOf, indentUnit, isNamedProperty, objectLiteral, objectProperties, propertyKey, propertyValue, } from './syntax.js';
11
+ /** Imports a converted file may no longer read: the definers folded in, and the brands a schema derives. */
12
+ const FOLDED_IMPORTS = new Set([
13
+ ...Object.keys(CLASS_DEFINERS).filter((name) => name !== 'defineEntity'),
14
+ 'idKey',
15
+ 'versionKey',
16
+ 'filledKey',
17
+ ]);
18
+ const RELATION_KINDS = new Map([
19
+ ['11', 'oneToOne'],
20
+ ['m1', 'manyToOne'],
21
+ ['1m', 'oneToMany'],
22
+ ['mm', 'manyToMany'],
23
+ ]);
24
+ const READ_OPTIONS = new Set([
25
+ 'name',
26
+ 'schema',
27
+ 'extends',
28
+ 'fields',
29
+ 'relations',
30
+ 'hooks',
31
+ ...KEPT_OPTIONS,
32
+ ]);
33
+ /** How each option holding entries is written for its entries to be read one by one: a map of plain properties, or a list. */
34
+ const ENTRY_OPTIONS = {
35
+ fields: 'map',
36
+ relations: 'map',
37
+ hooks: 'map',
38
+ filters: 'map',
39
+ indexes: 'list',
40
+ checks: 'list',
41
+ triggers: 'list',
42
+ };
43
+ /** Whether `value` is written as `shape` says, so no entry hides in a variable, a spread or a shorthand. */
44
+ function readsAs(value, shape) {
45
+ return shape === 'list'
46
+ ? ts.isArrayLiteralExpression(value)
47
+ : ts.isObjectLiteralExpression(value) && value.properties.every(ts.isPropertyAssignment);
48
+ }
49
+ /** What a class-form relation said that a `rel` builder says by its name and first argument. */
50
+ const RELATION_SHAPE = new Set(['cardinality', 'entity']);
51
+ /**
52
+ * Declares each class `defineEntity(Class, options)` registered as the schema `defineEntity({ name, fields })` and its
53
+ * chain of sections, folding the string-keyed definers of the same class in. Read off the text the other rewrites
54
+ * produced, whose options already name members by callback, so it only moves what is written. An entity it cannot
55
+ * declare faithfully, `taken` naming a builder it writes among them, is left as written and reported.
56
+ */
57
+ export function toSchemaEntities(fileName, text, { locate, taken }) {
58
+ if (!CLASS_FORM_CALL.test(text)) {
59
+ return { ...unedited(text), unresolved: [], notes: [] };
60
+ }
61
+ const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true);
62
+ const located = locate ?? ((at) => locationAt(source, at));
63
+ const unit = indentUnit(source);
64
+ const unresolved = [];
65
+ const notes = [];
66
+ const edits = [];
67
+ const written = new Set();
68
+ const registrations = collectRegistrations(source, unresolved);
69
+ const entityCallOf = (calls) => calls.find(({ definer }) => definer === 'defineEntity')?.call;
70
+ for (const [name, { cls, calls }] of registrations) {
71
+ const entityCall = entityCallOf(calls);
72
+ if (!entityCall) {
73
+ unresolved.push(...calls.map(({ call, definer }) => ({ node: call, message: removedDefiner(definer, name) })));
74
+ continue;
75
+ }
76
+ const base = baseName(entityCall);
77
+ const baseCall = base && entityCallOf(registrations.get(base)?.calls ?? []);
78
+ if (baseCall && baseCall.getStart() > entityCall.getStart()) {
79
+ unresolved.push({
80
+ node: entityCall,
81
+ message: `'${name}' extends '${base}', declared below it: move defineEntity(${base}, ...) above this one, then run again`,
82
+ });
83
+ continue;
84
+ }
85
+ const schema = schemaOf(name, cls, calls, entityCall, unit);
86
+ if ('message' in schema) {
87
+ unresolved.push(schema);
88
+ continue;
89
+ }
90
+ const chained = chainedSchema(schema.text);
91
+ if (!('text' in chained)) {
92
+ unresolved.push(...chained.reasons.map((reason) => ({ node: entityCall, message: `'${name}' is left as written: ${reason}` })));
93
+ continue;
94
+ }
95
+ const clash = [...chained.builders].find((builder) => taken.has(builder));
96
+ if (clash !== undefined) {
97
+ unresolved.push({ node: entityCall, message: `'${name}' is left as written: ${nameClash(clash)}` });
98
+ continue;
99
+ }
100
+ for (const builder of chained.builders)
101
+ written.add(builder);
102
+ notes.push(...schema.notes);
103
+ edits.push(removeStatement(cls));
104
+ for (const { call } of calls) {
105
+ edits.push(call === entityCall ? replaced(call.parent, indented(chained.text, call.parent)) : removeStatement(call.parent));
106
+ }
107
+ }
108
+ return {
109
+ ...(edits.length
110
+ ? reparsedAgain(fileName, editedText(text, edits), (edited) => importEdits(edited, written, FOLDED_IMPORTS))
111
+ : unedited(text)),
112
+ unresolved: formatFindings(unresolved, located),
113
+ notes: formatFindings(notes, located),
114
+ };
115
+ }
116
+ /** The class a `defineEntity(Class, { extends })` names as its base, which its schema's const reads where it is declared. */
117
+ function baseName(entityCall) {
118
+ const options = objectLiteral(entityCall.arguments[1]);
119
+ return identifierText(options && propertyValue(options, 'extends'));
120
+ }
121
+ /** A call a class-form definer makes, its entity named first: what the pass has anything to do with. */
122
+ const CLASS_FORM_CALL = new RegExp(`\\b(?:${Object.keys(CLASS_DEFINERS).join('|')})\\(\\s*[\\w$.]+\\s*[,)]`);
123
+ /**
124
+ * Each class the file declares that a class-form definer names, with those calls, in source order. A call naming
125
+ * anything else, or not written as a statement of its own, is reported: there is no class here to declare.
126
+ */
127
+ function collectRegistrations(source, unresolved) {
128
+ const classes = new Map(source.statements
129
+ .filter((statement) => ts.isClassDeclaration(statement) && !!statement.name)
130
+ .map((cls) => [cls.name.text, cls]));
131
+ const registrations = new Map();
132
+ const visit = (node) => {
133
+ const read = ts.isCallExpression(node) ? definerCall(node) : undefined;
134
+ if (read) {
135
+ const name = identifierText(read.entity);
136
+ const cls = name ? classes.get(name) : undefined;
137
+ if (name && cls && ts.isExpressionStatement(node.parent) && node.parent.parent === source) {
138
+ const registration = registrations.get(name) ?? { cls, calls: [] };
139
+ registration.calls.push(read);
140
+ registrations.set(name, registration);
141
+ }
142
+ else if (read.definer === 'defineEntity') {
143
+ unresolved.push({ node, message: 'defineEntity takes a schema now: defineEntity({ name, fields, ... })' });
144
+ }
145
+ else {
146
+ unresolved.push({ node, message: removedDefiner(read.definer, textOf(read.entity)) });
147
+ }
148
+ }
149
+ ts.forEachChild(node, visit);
150
+ };
151
+ visit(source);
152
+ return registrations;
153
+ }
154
+ /** Why an incremental definer naming `entity` has nowhere to fold into. */
155
+ function removedDefiner(definer, entity) {
156
+ return `${definer} was removed: register '${entity}' with defineEntity in this file, or decorate it (${CLASS_DEFINERS[definer]})`;
157
+ }
158
+ function schemaOf(name, cls, calls, entityCall, unit) {
159
+ const refused = (message) => ({ node: entityCall, message });
160
+ const options = objectLiteral(entityCall.arguments[1]) ?? (entityCall.arguments[1] ? undefined : emptyOptions());
161
+ if (!options || options.properties.some((prop) => !ts.isPropertyAssignment(prop))) {
162
+ return refused("defineEntity's options could not be read: write them as an object literal");
163
+ }
164
+ if (cls.heritageClauses?.length) {
165
+ return refused(`'${name}' extends a class: declare the base with defineEntity too and pass it as 'extends'`);
166
+ }
167
+ if (decoratorsOf(cls).length) {
168
+ return refused(`'${name}' is decorated as well: declare it one way, decorators or a schema`);
169
+ }
170
+ const exported = hasModifier(cls, ts.SyntaxKind.ExportKeyword);
171
+ if (hasModifier(cls, ts.SyntaxKind.DefaultKeyword)) {
172
+ return refused(`'${name}' is a default export: export the schema's const by name instead`);
173
+ }
174
+ const unread = objectProperties(options).find(({ name: key }) => !READ_OPTIONS.has(propertyKey(key)));
175
+ if (unread) {
176
+ return refused(`'${textOf(unread.name)}' has no place in a schema: move it by hand`);
177
+ }
178
+ for (const [option, shape] of Object.entries(ENTRY_OPTIONS)) {
179
+ const value = propertyValue(options, option);
180
+ if (value && !readsAs(value, shape)) {
181
+ const literal = shape === 'map' ? 'an object literal of its entries' : 'an array literal';
182
+ return { node: value, message: `'${option}' could not be read: write it as ${literal}` };
183
+ }
184
+ }
185
+ const scope = {
186
+ name,
187
+ param: paramFor(name),
188
+ rel: freeName('rel', calls.map(({ call }) => call)),
189
+ self: freeName(paramFor(name), outerReads(paramFor(name), options, calls)),
190
+ declared: declaredTypes(cls),
191
+ relations: new Set([
192
+ ...objectProperties(propertyValue(options, 'relations')).map((it) => propertyKey(it.name)),
193
+ ...calls.filter((read) => read.definer === 'defineRelation').map((read) => read.key),
194
+ ]),
195
+ methods: new Map(cls.members
196
+ .filter((member) => ts.isMethodDeclaration(member))
197
+ .map((method) => [textOf(method.name), method])),
198
+ hooked: new Map(),
199
+ };
200
+ const gathered = {
201
+ fields: [],
202
+ typed: false,
203
+ computed: [],
204
+ aggregates: [],
205
+ relations: new Map(),
206
+ hooks: new Map(),
207
+ kept: new Map(),
208
+ };
209
+ const { hooked } = scope;
210
+ const notes = [];
211
+ for (const read of calls) {
212
+ const problem = gather(read, read.call === entityCall ? options : undefined, gathered, scope);
213
+ if (problem) {
214
+ return problem;
215
+ }
216
+ }
217
+ const member = cls.members.find((it) => !(ts.isPropertyDeclaration(it) && !hasModifier(it, ts.SyntaxKind.StaticKeyword)) &&
218
+ !(ts.isMethodDeclaration(it) && hooked.has(textOf(it.name))));
219
+ if (member) {
220
+ const what = ts.isMethodDeclaration(member) ? 'method' : 'member';
221
+ const label = member.name ? textOf(member.name) : textOf(member).split('\n')[0];
222
+ return refused(`'${name}' has the ${what} '${label}': rows are plain objects, so make it a function of the row, then run again`);
223
+ }
224
+ for (const [method, declaration] of hooked) {
225
+ notes.push({
226
+ node: declaration,
227
+ message: `'${method}' runs as a hook of the row now, no longer a method of '${name}': call sites need the function`,
228
+ });
229
+ }
230
+ notes.push(...looserTypes(cls, options));
231
+ const table = propertyValue(options, 'name');
232
+ const lines = [`name: ${quoted(name)}`, ...(table ? [`table: ${textOf(table)}`] : [])];
233
+ for (const key of ['schema', 'extends']) {
234
+ const value = propertyValue(options, key);
235
+ if (value) {
236
+ lines.push(`${key}: ${textOf(value)}`);
237
+ }
238
+ }
239
+ const fieldsValue = propertyValue(options, 'fields');
240
+ const untouched = fieldsValue &&
241
+ gathered.fields.length === objectProperties(fieldsValue).length &&
242
+ !gathered.typed &&
243
+ !gathered.computed.length &&
244
+ !gathered.aggregates.length;
245
+ lines.push(`fields: ${untouched ? textOf(fieldsValue) : block(gathered.fields, unit)}`);
246
+ if (gathered.computed.length) {
247
+ lines.push(`computed: ${block(gathered.computed, unit)}`);
248
+ }
249
+ if (gathered.relations.size) {
250
+ lines.push(`relations: (${scope.rel}) => (${block([...gathered.relations.values()], unit)})`);
251
+ }
252
+ if (gathered.aggregates.length) {
253
+ lines.push(`aggregates: (${scope.self}) => (${block(gathered.aggregates, unit)})`);
254
+ }
255
+ for (const key of KEPT_OPTIONS) {
256
+ const entries = gathered.kept.get(key);
257
+ if (entries) {
258
+ lines.push(`${key}: ${keptValue(key, entries, propertyValue(options, key))}`);
259
+ }
260
+ }
261
+ if (gathered.hooks.size) {
262
+ lines.push(`hooks: ${block([...gathered.hooks].map(([event, runs]) => `${event}: [${runs.join(', ')}]`), unit)}`);
263
+ }
264
+ const prefix = exported ? 'export ' : '';
265
+ return {
266
+ text: `${prefix}const ${name} = defineEntity({\n${lines.map((line) => `${unit}${line},`).join('\n')}\n});\n${prefix}type ${name} = InstanceType<typeof ${name}>;`,
267
+ notes,
268
+ };
269
+ }
270
+ const emptyOptions = () => ts.factory.createObjectLiteralExpression();
271
+ /** Entries as an object literal a level in: `{\n a,\n }`. */
272
+ function block(entries, unit) {
273
+ return entries.length ? `{\n${entries.map((entry) => `${unit}${unit}${entry},\n`).join('')}${unit}}` : '{}';
274
+ }
275
+ /** `node`'s text, its lines after the first indented relative to the statement holding it. */
276
+ function textOf(node) {
277
+ return rebased(node.getText(), node);
278
+ }
279
+ /** `text`, written inside `node`, with the indentation of the statement holding it taken off its lines. */
280
+ function rebased(text, node) {
281
+ return text.replaceAll(`\n${indentOf(statementOf(node))}`, '\n');
282
+ }
283
+ function statementOf(node) {
284
+ return ts.isSourceFile(node.parent) ? node : statementOf(node.parent);
285
+ }
286
+ /** `text` replacing `node`, each line after its first indented as `node` is. */
287
+ function indented(text, node) {
288
+ return text.replaceAll('\n', `\n${indentOf(node)}`);
289
+ }
290
+ /** A kept option's value: as written where nothing was folded into it, its entries joined otherwise. */
291
+ function keptValue(key, entries, written) {
292
+ if (written && entries.length === entriesOfValue(key, written).length) {
293
+ return textOf(written);
294
+ }
295
+ return ENTRY_OPTIONS[key] === 'map' ? `{ ${entries.join(', ')} }` : `[${entries.join(', ')}]`;
296
+ }
297
+ /** The entries of a kept option as written: a map's properties, a list's elements. */
298
+ function entriesOfValue(key, value) {
299
+ return ENTRY_OPTIONS[key] === 'map' ? objectProperties(value) : arrayElements(value);
300
+ }
301
+ /**
302
+ * Adds what one registration declares to `gathered`: `options` for the `defineEntity` call, its arguments for an
303
+ * incremental definer. Returns why it cannot, where it cannot.
304
+ */
305
+ function gather({ call, definer, key, options: third, rest }, options, gathered, { name, param, rel, self, relations, methods, hooked, declared }) {
306
+ const unreadable = {
307
+ node: call,
308
+ message: `${definer}'s arguments could not be read: fold them into defineEntity by hand`,
309
+ };
310
+ const addHook = (event, method) => {
311
+ const declaration = methods.get(method);
312
+ if (!declaration) {
313
+ return { node: call, message: `the hook '${method}' names no method of the class` };
314
+ }
315
+ const run = hookFunction(declaration, param);
316
+ if (!run) {
317
+ return {
318
+ node: declaration,
319
+ message: `'${method}' takes more than the hook context: make it a function of the row by hand`,
320
+ };
321
+ }
322
+ hooked.set(method, declaration);
323
+ const runs = gathered.hooks.get(event) ?? [];
324
+ runs.push(run);
325
+ gathered.hooks.set(event, runs);
326
+ return undefined;
327
+ };
328
+ const addField = (key, label, value, fieldOptions, node) => {
329
+ const aggregate = key && aggregateEntry(`${name}.${key}`, key, fieldOptions, relations, self);
330
+ if (typeof aggregate === 'string') {
331
+ gathered.aggregates.push(aggregate);
332
+ return undefined;
333
+ }
334
+ if (aggregate) {
335
+ return { node, message: aggregate.message };
336
+ }
337
+ const type = key === undefined ? undefined : untyped(fieldOptions) && declared.get(key);
338
+ gathered.typed ||= !!type;
339
+ const entry = `${label}: ${type ? withOption(value, `type: ${type}`) : value}`;
340
+ (isComputed(fieldOptions) ? gathered.computed : gathered.fields).push(entry);
341
+ return undefined;
342
+ };
343
+ const addRelation = (key, value) => {
344
+ const built = key && relationEntry(key, value, rel);
345
+ if (built) {
346
+ gathered.relations.set(key, built);
347
+ }
348
+ return !!built;
349
+ };
350
+ const addKept = (option, entries) => {
351
+ const kept = gathered.kept.get(option) ?? [];
352
+ kept.push(...entries);
353
+ gathered.kept.set(option, kept);
354
+ };
355
+ if (options) {
356
+ for (const field of objectProperties(propertyValue(options, 'fields'))) {
357
+ const problem = addField(propertyKey(field.name), textOf(field.name), textOf(field.initializer), field.initializer, field);
358
+ if (problem)
359
+ return problem;
360
+ }
361
+ for (const relation of objectProperties(propertyValue(options, 'relations'))) {
362
+ if (!addRelation(propertyKey(relation.name), relation.initializer)) {
363
+ return {
364
+ node: relation,
365
+ message: 'a relation could not be read: write it as rel.<kind>(Target, { ... }) by hand',
366
+ };
367
+ }
368
+ }
369
+ for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
370
+ const listed = hookMethods(hook.initializer);
371
+ if (!listed) {
372
+ return { node: hook, message: 'a hook list could not be read: write its functions by hand' };
373
+ }
374
+ for (const method of listed) {
375
+ const problem = addHook(textOf(hook.name), method);
376
+ if (problem)
377
+ return problem;
378
+ }
379
+ }
380
+ for (const option of KEPT_OPTIONS) {
381
+ const value = propertyValue(options, option);
382
+ if (value) {
383
+ addKept(option, entriesOfValue(option, value).map((it) => textOf(it)));
384
+ }
385
+ }
386
+ return undefined;
387
+ }
388
+ switch (definer) {
389
+ case 'defineEntity':
390
+ return unreadable;
391
+ case 'defineField':
392
+ case 'defineId': {
393
+ if (!key || !third)
394
+ return unreadable;
395
+ const literal = objectLiteral(third);
396
+ const value = definer === 'defineId' ? literal && withOption(textOf(literal), 'isId: true') : textOf(third);
397
+ if (!value)
398
+ return unreadable;
399
+ return addField(key, memberKey(key), value, third, call);
400
+ }
401
+ case 'defineRelation':
402
+ return third && addRelation(key, third) ? undefined : unreadable;
403
+ case 'defineHook': {
404
+ const event = third && ts.isStringLiteralLike(third) ? third.text : undefined;
405
+ return key && event ? addHook(event, key) : unreadable;
406
+ }
407
+ case 'defineIndex':
408
+ if (!third)
409
+ return unreadable;
410
+ addKept('indexes', [textOf(third)]);
411
+ return undefined;
412
+ case 'defineTrigger':
413
+ if (!rest.length)
414
+ return unreadable;
415
+ addKept('triggers', rest.map((it) => textOf(it)));
416
+ return undefined;
417
+ case 'defineFilter':
418
+ if (!key || !third)
419
+ return unreadable;
420
+ addKept('filters', [`${memberKey(key)}: ${textOf(third)}`]);
421
+ return undefined;
422
+ }
423
+ }
424
+ /** Whether a field's options compute it, which a schema declares under `computed`. */
425
+ function isComputed(options) {
426
+ const literal = objectLiteral(options);
427
+ return !!literal && propertyValue(literal, 'computed') !== undefined;
428
+ }
429
+ /**
430
+ * The bodies of the field callbacks whose parameter is not `name`, where a `name` they read is the file's: a
431
+ * callback taking `name` reads its own under it, which the aggregate it becomes reads under the same name.
432
+ */
433
+ function outerReads(name, options, calls) {
434
+ const declared = [
435
+ ...objectProperties(propertyValue(options, 'fields')).map((field) => field.initializer),
436
+ ...calls.flatMap(({ options: third }) => (third ? [third] : [])),
437
+ ];
438
+ return declared.flatMap((field) => {
439
+ const literal = objectLiteral(field);
440
+ const computed = literal && propertyValue(literal, 'computed');
441
+ return computed && ts.isArrowFunction(computed) && identifierText(computed.parameters[0]?.name) !== name
442
+ ? [computed.body]
443
+ : [];
444
+ });
445
+ }
446
+ /** An options literal's text with `option` written first: `{ isId: true, type: String }`. */
447
+ function withOption(literal, option) {
448
+ return /^\{\s*\}$/.test(literal) ? `{ ${option} }` : literal.replace(/^\{\s*/, `{ ${option}, `);
449
+ }
450
+ /** Whether a field's options are a literal stating no column type, which the class's declaration may say instead. */
451
+ function untyped(options) {
452
+ const literal = objectLiteral(options);
453
+ return !!literal && ['type', 'enum', 'version'].every((option) => propertyValue(literal, option) === undefined);
454
+ }
455
+ /** The constructors the TypeScript types of a class's properties name: `number | null` is `Number`. */
456
+ const DECLARED_TYPES = {
457
+ number: 'Number',
458
+ string: 'String',
459
+ boolean: 'Boolean',
460
+ bigint: 'BigInt',
461
+ Date: 'Date',
462
+ };
463
+ /** Each property a class declares as one of {@link DECLARED_TYPES}, with that type's constructor. */
464
+ function declaredTypes(cls) {
465
+ return new Map(cls.members.filter(ts.isPropertyDeclaration).flatMap((member) => {
466
+ const key = propertyKey(member.name);
467
+ const type = member.type;
468
+ const arms = type && ts.isUnionTypeNode(type) ? type.types : type ? [type] : [];
469
+ const named = arms.map((arm) => arm.getText()).filter((arm) => arm !== 'null' && arm !== 'undefined');
470
+ const constructor = named.length === 1 ? DECLARED_TYPES[named[0]] : undefined;
471
+ return key && constructor ? [[key, constructor]] : [];
472
+ }));
473
+ }
474
+ /** `author: () => rel.manyToOne(User, { references })` from `{ cardinality: 'm1', entity: () => User, references }`. */
475
+ function relationEntry(key, value, rel) {
476
+ const relation = objectLiteral(value);
477
+ const cardinality = relation && propertyValue(relation, 'cardinality');
478
+ const getter = relation && propertyValue(relation, 'entity');
479
+ const kind = cardinality && ts.isStringLiteralLike(cardinality) ? RELATION_KINDS.get(cardinality.text) : undefined;
480
+ if (!relation || !kind || !getter || !ts.isArrowFunction(getter) || ts.isBlock(getter.body)) {
481
+ return undefined;
482
+ }
483
+ const properties = relation.properties.filter(isNamedProperty);
484
+ if (properties.length !== relation.properties.length) {
485
+ return undefined;
486
+ }
487
+ const rest = properties.filter((prop) => !RELATION_SHAPE.has(propertyKey(prop.name))).map((prop) => textOf(prop));
488
+ const args = [textOf(getter.body), ...(rest.length ? [`{ ${rest.join(', ')} }`] : [])];
489
+ return `${memberKey(key)}: () => ${rel}.${kind}(${args.join(', ')})`;
490
+ }
491
+ /** The methods a hook list names, `(post) => [post.stamp]`, or nothing where it is not one. */
492
+ function hookMethods(list) {
493
+ if (!ts.isArrowFunction(list) || !ts.isArrayLiteralExpression(list.body) || list.parameters.length !== 1) {
494
+ return undefined;
495
+ }
496
+ const param = identifierText(list.parameters[0].name);
497
+ const names = list.body.elements.map((entry) => ts.isPropertyAccessExpression(entry) && identifierText(entry.expression) === param ? entry.name.text : undefined);
498
+ return names.every((it) => it !== undefined) ? names : undefined;
499
+ }
500
+ /** A hook method as a function of the row: `this` read as the row, named `preferred` where the body leaves it free. */
501
+ function hookFunction(method, preferred) {
502
+ if (!method.body || method.parameters.length > 1) {
503
+ return undefined;
504
+ }
505
+ const row = freeName(preferred, [method]);
506
+ const isAsync = hasModifier(method, ts.SyntaxKind.AsyncKeyword);
507
+ const params = [row, ...method.parameters.map((it) => textOf(it))].join(', ');
508
+ const body = method.body;
509
+ const uses = [];
510
+ const visit = (node) => {
511
+ if (node.kind === ts.SyntaxKind.ThisKeyword) {
512
+ uses.push(node);
513
+ }
514
+ else if (!ts.isFunctionLike(node) || ts.isArrowFunction(node)) {
515
+ ts.forEachChild(node, visit);
516
+ }
517
+ };
518
+ ts.forEachChild(body, visit);
519
+ return `${isAsync ? 'async ' : ''}(${params}) => ${substituted(body, uses, row)}`;
520
+ }
521
+ /**
522
+ * `total: () => rel.items.count()` from a field computed over a relation, `{ computed: (counted) => counted.items.count() }`,
523
+ * which a schema declares among its aggregates; nothing for a field reading no relation, and why where it cannot move.
524
+ */
525
+ function aggregateEntry(label, key, options, relations, rel) {
526
+ const literal = objectLiteral(options);
527
+ const computed = literal && propertyValue(literal, 'computed');
528
+ const param = computed && ts.isArrowFunction(computed) ? identifierText(computed.parameters[0]?.name) : undefined;
529
+ if (!computed || !ts.isArrowFunction(computed) || !param) {
530
+ return undefined;
531
+ }
532
+ const uses = identifierUses(computed.body, param);
533
+ const overRelation = (use) => ts.isPropertyAccessExpression(use.parent) && use.parent.expression === use && relations.has(use.parent.name.text);
534
+ if (!uses.some(overRelation)) {
535
+ return undefined;
536
+ }
537
+ if (literal.properties.length > 1) {
538
+ return { message: `'${label}' aggregates a relation with options an aggregate takes none of: move it by hand` };
539
+ }
540
+ if (ts.isBlock(computed.body) || !uses.every(overRelation)) {
541
+ return { message: `'${label}' reads more than a relation's aggregate: move it by hand` };
542
+ }
543
+ return `${memberKey(key)}: () => ${substituted(computed.body, uses, rel)}`;
544
+ }
545
+ /** `body`'s text, rebased, with each of `uses` written as `replacement`. */
546
+ function substituted(body, uses, replacement) {
547
+ return rebased(replacingUses(body, uses, replacement), body);
548
+ }
549
+ /** Notes each property the class typed more narrowly than its schema derives, with the option that keeps it. */
550
+ function looserTypes(cls, options) {
551
+ const fields = objectLiteral(propertyValue(options, 'fields'));
552
+ return cls.members.flatMap((member) => {
553
+ const key = ts.isPropertyDeclaration(member) ? propertyKey(member.name) : undefined;
554
+ const declared = ts.isPropertyDeclaration(member) ? member.type : undefined;
555
+ const field = key && fields ? objectLiteral(propertyValue(fields, key)) : undefined;
556
+ if (!key || !declared || !field) {
557
+ return [];
558
+ }
559
+ const written = textOf(declared);
560
+ const said = (option) => propertyValue(field, option) !== undefined;
561
+ if (/\bJson</.test(written) && !said('schema')) {
562
+ return [
563
+ {
564
+ node: member,
565
+ message: `'${key}' was declared ${written}: describe its payload with a Standard Schema, 'f.json(schema)', to keep it typed`,
566
+ },
567
+ ];
568
+ }
569
+ const literals = ts.isUnionTypeNode(declared) &&
570
+ declared.types.filter((type) => ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal));
571
+ if (literals && literals.length > 1 && !said('enum')) {
572
+ return [
573
+ { node: member, message: `'${key}' was declared ${written}: list its values in 'enum' to keep them typed` },
574
+ ];
575
+ }
576
+ return [];
577
+ });
578
+ }
@@ -0,0 +1,8 @@
1
+ /** Why a name the codemod writes is refused: a file already binds it. */
2
+ export declare const nameClash: (name: string) => string;
3
+ /** Why a schema column's foreign key is refused: no relation joins on it, and none can be written for it. */
4
+ export declare const FOREIGN_KEY_IN_SCHEMA = "references another entity, and no relation can be named for it: declare one in '.relations()', as 'rel.manyToOne(Target, { references: (row) => row.key })'";
5
+ /** Why a joined column's `onDelete` is refused: its relation states another, or takes options the codemod cannot write into. */
6
+ export declare const ON_DELETE_ON_RELATION = "joins through a relation that cannot take its 'onDelete': settle the action in that relation's options";
7
+ /** Why a foreign key with no type is refused: a schema column states its own, which only its target's key implied. */
8
+ export declare const NO_KEY_TYPE = "takes its type from its target: write its builder, f.number() and so on, as its key's type";
@@ -0,0 +1,8 @@
1
+ /** Why a name the codemod writes is refused: a file already binds it. */
2
+ export const nameClash = (name) => `'${name}' is already named here, and the codemod writes it`;
3
+ /** Why a schema column's foreign key is refused: no relation joins on it, and none can be written for it. */
4
+ export const FOREIGN_KEY_IN_SCHEMA = "references another entity, and no relation can be named for it: declare one in '.relations()', as 'rel.manyToOne(Target, { references: (row) => row.key })'";
5
+ /** Why a joined column's `onDelete` is refused: its relation states another, or takes options the codemod cannot write into. */
6
+ export const ON_DELETE_ON_RELATION = "joins through a relation that cannot take its 'onDelete': settle the action in that relation's options";
7
+ /** Why a foreign key with no type is refused: a schema column states its own, which only its target's key implied. */
8
+ export const NO_KEY_TYPE = "takes its type from its target: write its builder, f.number() and so on, as its key's type";
@@ -0,0 +1,6 @@
1
+ /** The options a schema's first call keeps, each as it is written there. */
2
+ export declare const FIRST_CALL_OPTIONS: readonly ["name", "table", "schema", "abstract", "extends", "fields"];
3
+ /** The sections a schema chains after its first call, in the order it chains them, `relations` last. */
4
+ export declare const CHAINED_OPTIONS: readonly ["computed", "indexes", "checks", "triggers", "filters", "hooks"];
5
+ /** The options a class-form `defineEntity` took that a schema keeps as they are, and that incremental definers add to. */
6
+ export declare const KEPT_OPTIONS: readonly ["indexes", "checks", "filters", "triggers"];
@@ -0,0 +1,6 @@
1
+ /** The options a schema's first call keeps, each as it is written there. */
2
+ export const FIRST_CALL_OPTIONS = ['name', 'table', 'schema', 'abstract', 'extends', 'fields'];
3
+ /** The sections a schema chains after its first call, in the order it chains them, `relations` last. */
4
+ export const CHAINED_OPTIONS = ['computed', 'indexes', 'checks', 'triggers', 'filters', 'hooks'];
5
+ /** The options a class-form `defineEntity` took that a schema keeps as they are, and that incremental definers add to. */
6
+ export const KEPT_OPTIONS = ['indexes', 'checks', 'filters', 'triggers'];