uql-codemod 0.38.0 → 0.39.1
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/README.md +3 -2
- package/dist/transform.js +71 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Only `--flag=value` is read, and anything unrecognised is an error. It exits `0`
|
|
|
28
28
|
- `entity: () => X` on a bare relation decorator, and `Relation<T>` to `T`, its import going with the last use.
|
|
29
29
|
- `virtual` to `computed` (on `@Field` and in `defineEntity`'s `fields`), a check's `expression` and a filter's `condition` to `where` (`@Entity`/`defineEntity`, `@Filter`, `defineFilter`), `raw('sql')` to the tagged template, and a `raw` alias argument onto `.as()`.
|
|
30
30
|
- Members named by string to callbacks: `mappedBy: 'author'` to `(post) => post.author`, and `@Index`, `include`, `references` and `@Entity`/`defineEntity`'s `indexes` and `hooks` likewise: `@Index((post) => [post.title])`.
|
|
31
|
-
- `references: (post) => post.authorId` on a to-one (`@ManyToOne`, `@OneToOne`, or `defineEntity`/`defineRelation`)
|
|
31
|
+
- `references: (post) => post.authorId` on a to-one (`@ManyToOne`, `@OneToOne`, or `defineEntity`/`defineRelation`) that names no foreign key, declaring `@Field({ references: () => User }) authorId` first on a decorated class that has no such column, typed as the key it points at.
|
|
32
32
|
- A partial-index `where` string to `raw`, importing it, on entities and in the migration builder.
|
|
33
33
|
- `col('cost')` to the ref it names, ``(product) => raw`${product.cost}` `` in a `computed` and `refs(Item).cost` in a statement, and `$lock: { wait: 'skip' }` to `{ $wait: 'skip' }`, or to `true` for `{}` and a `'block'` wait.
|
|
34
34
|
- An aggregate's `$agg` to `$select`, and each field it or `$text`'s `$fields` names to a key: `{ $sum: { amount: true } }`.
|
|
@@ -44,7 +44,8 @@ Reported with what to do, never guessed:
|
|
|
44
44
|
- `@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`: a `@Transactional()` method becomes a `pool.transaction()` around its body, and only you know which pool.
|
|
45
45
|
- An export removed with no one-to-one replacement (`setQuerierPool`, `getQuerier`, `augmentWhere`, `AbstractPgQuerier`, ...).
|
|
46
46
|
- A value it cannot read: options passed as a variable or a spread, a list that is not a literal, a key under a computed name.
|
|
47
|
-
- A to-one onto a composite key
|
|
47
|
+
- A to-one onto a composite key: declare a column per key and pair each with it in `references`.
|
|
48
|
+
- A to-one whose `<relation>Id` is not a declared column: an undecorated property, a `defineEntity` whose `fields` leave it out, or a `defineRelation` whose entity has no such member.
|
|
48
49
|
- A partial-index `where` string that interpolates, since `raw` would bind what the template spliced in.
|
|
49
50
|
- A `col()` whose entity it cannot tell: a raw built apart from its query, one under a relation filter, or a column no field maps.
|
|
50
51
|
- SQL in a definition that names a column by hand, as a note: a rename reaches a callback's refs, ``(user) => raw`lower(${user.email})` ``, and never the text.
|
package/dist/transform.js
CHANGED
|
@@ -157,8 +157,12 @@ function noteHandNamedColumns(sql, owner, ctx) {
|
|
|
157
157
|
}
|
|
158
158
|
/** The names of the members of the entity `node` declares or names. */
|
|
159
159
|
function memberNames(node, checker) {
|
|
160
|
+
return propertyNames(instanceTypeOf(node, checker));
|
|
161
|
+
}
|
|
162
|
+
/** The instance type of the entity `node` declares or names. */
|
|
163
|
+
function instanceTypeOf(node, checker) {
|
|
160
164
|
const type = checker.getTypeAtLocation(node);
|
|
161
|
-
return
|
|
165
|
+
return type.getConstructSignatures()[0]?.getReturnType() ?? type;
|
|
162
166
|
}
|
|
163
167
|
function propertyNames(type) {
|
|
164
168
|
return new Set(type.getProperties().map(({ name }) => name));
|
|
@@ -228,21 +232,72 @@ function addRelationEntity(decorator, node, ctx) {
|
|
|
228
232
|
});
|
|
229
233
|
}
|
|
230
234
|
/**
|
|
231
|
-
* Names the foreign key a to-one
|
|
232
|
-
* where
|
|
235
|
+
* Names the foreign key a to-one used to find by name, `references: (post) => post.authorId` beside `author`.
|
|
236
|
+
* `site` says where the entity declares its columns: the decorated property, ahead of which a missing column
|
|
237
|
+
* is declared, or `defineEntity`'s `fields`. A column it cannot see declared is reported instead.
|
|
233
238
|
*/
|
|
234
|
-
function addForeignKeyReference(relation, key, owner, ctx) {
|
|
239
|
+
function addForeignKeyReference(relation, key, owner, ctx, site) {
|
|
235
240
|
const options = { kind: 'literal', node: relation };
|
|
236
241
|
const cardinality = propertyValue(relation, 'cardinality');
|
|
237
242
|
const toOne = !cardinality || (ts.isStringLiteralLike(cardinality) && TO_ONE_CARDINALITIES.has(cardinality.text));
|
|
238
243
|
const joined = ['mappedBy', 'through', 'references'].some((option) => findProperty(options, option));
|
|
239
244
|
const last = relation.properties.at(-1);
|
|
245
|
+
if (!key || !last || !toOne || joined) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
240
248
|
const column = `${key}Id`;
|
|
241
|
-
|
|
249
|
+
const declaration = columnDeclaration(column, relation, owner, ctx.checker, site);
|
|
250
|
+
if (typeof declaration === 'string') {
|
|
251
|
+
ctx.unresolved.push(`${ctx.describe(site && ts.isPropertyDeclaration(site) ? site : relation)}: ${declaration}`);
|
|
242
252
|
return;
|
|
243
253
|
}
|
|
254
|
+
if (declaration) {
|
|
255
|
+
ctx.edits.push(declaration);
|
|
256
|
+
ctx.imports.set('Field', 'the foreign key column');
|
|
257
|
+
}
|
|
244
258
|
ctx.edits.push(appended(last, `, references: (${owner.param}) => ${memberAccess(owner.param, column)}`));
|
|
245
259
|
}
|
|
260
|
+
const undeclaredColumn = (column) => `declare the foreign key column '${column}' and name it in 'references'`;
|
|
261
|
+
/** Nothing where `column` is declared, the declaration to write where a decorated class lacks it, or why neither. */
|
|
262
|
+
function columnDeclaration(column, relation, owner, checker, site) {
|
|
263
|
+
const member = instanceTypeOf(owner.entity, checker).getProperty(column);
|
|
264
|
+
if (site && ts.isPropertyDeclaration(site)) {
|
|
265
|
+
if (!member) {
|
|
266
|
+
return foreignKeyColumn(site, column, relation, checker);
|
|
267
|
+
}
|
|
268
|
+
return member.declarations?.some(isFieldProperty) ? undefined : undeclaredColumn(column);
|
|
269
|
+
}
|
|
270
|
+
const declared = site ? objectProperties(site).some(({ name }) => propertyKey(name) === column) : member;
|
|
271
|
+
return declared ? undefined : undeclaredColumn(column);
|
|
272
|
+
}
|
|
273
|
+
/** `@Field({ references: () => Target }) <column>?: <key type>;` ahead of the relation, or why it cannot be written. */
|
|
274
|
+
function foreignKeyColumn(property, column, relation, checker) {
|
|
275
|
+
const targetType = checker.getNonNullableType(checker.getTypeAtLocation(property));
|
|
276
|
+
const target = entityGetterTarget(relation) ?? relationTargetFor(targetType, checker);
|
|
277
|
+
const keys = targetType.getProperties().filter((member) => member.declarations?.some(isIdProperty));
|
|
278
|
+
if (keys.length > 1) {
|
|
279
|
+
return `'${target}' has a composite key: declare a column per key and pair each in 'references'`;
|
|
280
|
+
}
|
|
281
|
+
const [key] = keys;
|
|
282
|
+
if (!target || !key) {
|
|
283
|
+
return undeclaredColumn(column);
|
|
284
|
+
}
|
|
285
|
+
const indent = ' '.repeat(property.getSourceFile().getLineAndCharacterOfPosition(property.getStart()).character);
|
|
286
|
+
const type = keyTypeSource(key, property, checker);
|
|
287
|
+
return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type};\n${indent}`);
|
|
288
|
+
}
|
|
289
|
+
/** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
|
|
290
|
+
function keyTypeSource(key, at, checker) {
|
|
291
|
+
const written = key.declarations?.find(isIdProperty)?.type;
|
|
292
|
+
if (written && written.getSourceFile() === at.getSourceFile()) {
|
|
293
|
+
return written.getText();
|
|
294
|
+
}
|
|
295
|
+
return checker.typeToString(checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(key, at)), at);
|
|
296
|
+
}
|
|
297
|
+
/** Whether a declaration is a property carrying one of `names` as a decorator. */
|
|
298
|
+
const decoratedWith = (names) => (node) => ts.isPropertyDeclaration(node) && decoratorsOf(node).some((decorator) => names.has(decoratorName(decorator) ?? ''));
|
|
299
|
+
const isIdProperty = decoratedWith(new Set(['Id']));
|
|
300
|
+
const isFieldProperty = decoratedWith(FIELD_DECORATORS);
|
|
246
301
|
/**
|
|
247
302
|
* Writes the edits `rewrite` reads off `value`, which still names members as strings, or reports it
|
|
248
303
|
* where it could not be read: what it holds is only known at runtime. A callback or an object literal
|
|
@@ -310,6 +365,7 @@ function rewriteDefineCall(call, ctx) {
|
|
|
310
365
|
}
|
|
311
366
|
}
|
|
312
367
|
function rewriteEntityOptions(options, owner, ctx) {
|
|
368
|
+
const fields = propertyValue(options, 'fields');
|
|
313
369
|
for (const index of arrayElements(propertyValue(options, 'indexes'))) {
|
|
314
370
|
if (ts.isObjectLiteralExpression(index)) {
|
|
315
371
|
rewriteIndexOptions(index, owner, ctx);
|
|
@@ -318,7 +374,7 @@ function rewriteEntityOptions(options, owner, ctx) {
|
|
|
318
374
|
for (const check of arrayElements(propertyValue(options, 'checks'))) {
|
|
319
375
|
renameSqlOption(optionsOf(check), 'expression', 'where', check, owner, ctx);
|
|
320
376
|
}
|
|
321
|
-
for (const field of objectProperties(
|
|
377
|
+
for (const field of objectProperties(fields)) {
|
|
322
378
|
renameSqlOption(optionsOf(field.initializer), 'virtual', 'computed', field, owner, ctx);
|
|
323
379
|
}
|
|
324
380
|
for (const filter of objectProperties(propertyValue(options, 'filters'))) {
|
|
@@ -327,10 +383,11 @@ function rewriteEntityOptions(options, owner, ctx) {
|
|
|
327
383
|
for (const hook of objectProperties(propertyValue(options, 'hooks'))) {
|
|
328
384
|
rewriteKeyList(hook.initializer, owner.param, hook, 'a hook list', ctx);
|
|
329
385
|
}
|
|
386
|
+
const declared = fields && ts.isObjectLiteralExpression(fields) ? fields : undefined;
|
|
330
387
|
for (const relation of objectProperties(propertyValue(options, 'relations'))) {
|
|
331
388
|
if (ts.isObjectLiteralExpression(relation.initializer)) {
|
|
332
389
|
rewriteRelationOptions(relation.initializer, owner.param, ctx);
|
|
333
|
-
addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx);
|
|
390
|
+
addForeignKeyReference(relation.initializer, propertyKey(relation.name), owner, ctx, declared);
|
|
334
391
|
}
|
|
335
392
|
}
|
|
336
393
|
}
|
|
@@ -563,7 +620,7 @@ function isIdKeyBrand(member) {
|
|
|
563
620
|
* One written makes `idKey` a value the file has to import, which is recorded for {@link addImports}.
|
|
564
621
|
*/
|
|
565
622
|
function brandIdKey(node, ctx) {
|
|
566
|
-
const ids = node.members.filter(
|
|
623
|
+
const ids = node.members.filter(isIdProperty);
|
|
567
624
|
if (!ids.length || node.members.some(isIdKeyBrand)) {
|
|
568
625
|
return;
|
|
569
626
|
}
|
|
@@ -624,9 +681,9 @@ function rewriteProperty(node, ctx) {
|
|
|
624
681
|
if (RELATION_DECORATORS.has(name) && options.kind === 'literal') {
|
|
625
682
|
const target = relationTargetFor(ctx.checker.getTypeAtLocation(node), ctx.checker);
|
|
626
683
|
rewriteRelationOptions(options.node, owner.param, ctx, paramFor(target));
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
684
|
+
if (TO_ONE_DECORATORS.has(name)) {
|
|
685
|
+
addForeignKeyReference(options.node, propertyKey(node.name), owner, ctx, node);
|
|
686
|
+
}
|
|
630
687
|
}
|
|
631
688
|
}
|
|
632
689
|
if (decorators.length) {
|
|
@@ -927,9 +984,9 @@ export function transformFile(source, checker) {
|
|
|
927
984
|
}
|
|
928
985
|
if (ts.isCallExpression(node)) {
|
|
929
986
|
rewriteDefineCall(node, ctx);
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
987
|
+
if (importsUql) {
|
|
988
|
+
rewriteBuilderCall(node, ctx);
|
|
989
|
+
}
|
|
933
990
|
}
|
|
934
991
|
if (ts.isPropertyAssignment(node)) {
|
|
935
992
|
rewriteStatementKeys(node, ctx);
|