uql-codemod 0.24.2 → 0.25.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/README.md CHANGED
@@ -27,7 +27,6 @@ Only the `--flag=value` form is read, and anything unrecognised is an error: a m
27
27
  - `@Field()` and `@Field({ ... })` with no `type`, and `@Id()`: inserts the `type` the property's declared TypeScript type implies. Skipped when `references` is present, because schema generation resolves that column from the referenced primary key.
28
28
  - Bare relation decorators: inserts `entity: () => X`, unwrapping arrays and the old `Relation<T>` alias.
29
29
  - `Relation<T>` becomes `T`, and its import goes with the last usage - `uql-orm` no longer exports it. A usage somewhere the codemod does not reach, such as a type alias, keeps the import and is reported.
30
- - `@Transactional()` methods: drops the `@InjectQuerier() querier?: Querier` parameter and rewrites the body to `currentQuerier()`.
31
30
  - `import 'reflect-metadata'` goes. Removing the package from your `package.json` is left to you.
32
31
  - `tsconfig.json`: removes `experimentalDecorators` and `emitDecoratorMetadata`, keeping the rest of the file - comments and formatting included - exactly as written.
33
32
 
@@ -37,7 +36,7 @@ It reports rather than guesses, and exits non-zero when anything is left for you
37
36
 
38
37
  - **`target: esnext`** is reported, not changed. Removing the line falls back to the compiler default (`es5` for `tsc`), and choosing a replacement means guessing which era the project targets. Any dated target works; `esnext` is the one where TypeScript emits decorator syntax untransformed.
39
38
  - **A value inherited through `extends`** cannot be edited here, so the base config is reported instead.
40
- - **`@Log()` and `@Serialized()`** are reported and left in place. They no longer exist, and what to do instead is a judgement call.
39
+ - **`@Log()`, `@Serialized()`, `@Transactional()` and `@InjectQuerier()`** are reported and left in place. They no longer exist, and what to do instead is a judgement call - a `@Transactional()` method becomes a `pool.transaction(async (querier) => { ... })` around its body, and the codemod cannot know which pool.
41
40
  - **Options it cannot read** (`@Field(sharedOptions)`, a spread, or a `@Field` never called at all) are left exactly as written. Replacing an argument it cannot parse would silently drop the options.
42
41
  - **A property whose type it cannot map to a column** (a `Json<T>`, a vector, a union that disagrees with itself) is reported as `needs a decision`. Those always had to declare `type` by hand anyway, because `design:type` reported the useless `Object`/`Array` for them.
43
42
  - **A branded string id**, e.g. `type UUID = \`${string}-${string}\``, gets `type: String` plus a `worth a look` note. That is a real fork: `String` generates a text column, `'uuid'` generates a native one, and only you know which the database has.
package/dist/transform.js CHANGED
@@ -4,13 +4,16 @@ import { fieldTypeFor, isBrandedString, relationTargetFor } from './fieldType.js
4
4
  const FIELD_DECORATORS = new Set(['Field', 'Id']);
5
5
  const RELATION_DECORATORS = new Set(['OneToOne', 'ManyToOne', 'OneToMany', 'ManyToMany']);
6
6
  /**
7
- * Decorators that no longer exist, and that the codemod does not remove for you.
8
- *
9
- * A standard-spec decorator cannot preserve a generic method's signature, which is why these went. What
10
- * to do instead is a judgement call - inline the logging, drop the serialisation - so the usage is
11
- * reported and left in place. Removing only the import would swap a clear error for a puzzling one.
7
+ * Decorators that no longer exist, mapped to what to do instead. None is removed for you: what to put
8
+ * in its place is a judgement call, and removing only the import would swap a clear error for a
9
+ * puzzling one.
12
10
  */
13
- const REMOVED_DECORATORS = new Set(['Log', 'Serialized']);
11
+ const REMOVED_DECORATORS = new Map([
12
+ ['Log', 'delete it and its import'],
13
+ ['Serialized', 'delete it and its import'],
14
+ ['Transactional', 'wrap the body in `pool.transaction(async (querier) => { ... })`'],
15
+ ['InjectQuerier', 'take the querier from the enclosing `pool.transaction()` callback'],
16
+ ]);
14
17
  function decoratorName(node) {
15
18
  const call = ts.isCallExpression(node.expression) ? node.expression : undefined;
16
19
  const target = call?.expression ?? node.expression;
@@ -143,36 +146,6 @@ function countRelationAlias(node, ctx) {
143
146
  ctx.relationAlias.seen += 1;
144
147
  }
145
148
  }
146
- /**
147
- * Rewrites an `@InjectQuerier()` parameter into a `currentQuerier()` call.
148
- *
149
- * The standard spec has no parameter decorators, so the querier travels through async-local storage and
150
- * is read inside the body. Only the plain shape is rewritten - a named parameter in a method with a body.
151
- * A default value or a destructured parameter is reported: the author was doing something the codemod
152
- * would have to guess at.
153
- */
154
- function rewriteInjectQuerier(method, ctx) {
155
- for (const parameter of method.parameters) {
156
- if (!ts.getDecorators(parameter)?.some((d) => decoratorName(d) === 'InjectQuerier')) {
157
- continue;
158
- }
159
- if (!method.body || !ts.isIdentifier(parameter.name) || parameter.initializer) {
160
- ctx.unresolved.push(`${ctx.describe(parameter)}: cannot rewrite this '@InjectQuerier()' parameter`);
161
- continue;
162
- }
163
- ctx.edits.push(removeFromList(method.parameters, parameter, method.getSourceFile()));
164
- // The querier is read at the top of the body, keeping the name the parameter had.
165
- const body = method.body;
166
- const first = body.statements[0];
167
- const indent = first ? ' '.repeat(first.getStart() - body.getSourceFile().getLineStarts()[ctx.lineOf(first)]) : '';
168
- ctx.edits.push({
169
- start: body.getStart() + 1,
170
- end: body.getStart() + 1,
171
- text: `\n${indent}const ${parameter.name.text} = currentQuerier();`,
172
- });
173
- ctx.imports.add('currentQuerier');
174
- }
175
- }
176
149
  /** Removes a statement together with the rest of its line, so nothing is left blank behind it. */
177
150
  function removeStatement(node) {
178
151
  const source = node.getSourceFile();
@@ -181,44 +154,29 @@ function removeStatement(node) {
181
154
  return { start: node.getStart(), end: nextLineStart ?? node.getEnd(), text: '' };
182
155
  }
183
156
  /**
184
- * Brings the file's imports in line with what the rewrites left behind, in one pass:
185
- *
186
- * - `import 'reflect-metadata'` goes, since the polyfill only ever fed `design:type`.
187
- * - `InjectQuerier` goes with it, and its import would otherwise not compile: the standard spec has no
188
- * parameter decorators, so `uql-orm` no longer exports one.
189
- * - whatever a rewrite needs (`currentQuerier`) is added, extending an existing `uql-orm` import rather
190
- * than adding a second one.
191
- *
192
- * One function because all three edit the same statements, and doing them separately meant an import
193
- * could be dropped and re-added, or a name added to a statement another edit had already removed.
157
+ * Names `uql-orm` no longer exports and that the codemod does remove: only `Relation`, and only once
158
+ * every usage was unwrapped. A removed *decorator* keeps its import on purpose (see
159
+ * {@link REMOVED_DECORATORS}).
194
160
  */
195
- function reconcileImports(source, ctx) {
196
- // Dropping runs first and hands over the `uql-orm` import it left standing: adding a name to a
197
- // statement the other pass had already removed is the bug that made these one step to begin with.
198
- addNeededImports(source, ctx, dropDeadImports(source, ctx));
199
- }
200
- /** Names `uql-orm` no longer exports. `Relation` joins them only when every usage was rewritten. */
201
161
  function deadImportNames(source, ctx) {
202
162
  const { seen, unwrapped } = ctx.relationAlias;
203
163
  if (seen > unwrapped) {
204
164
  ctx.unresolved.push(`${source.fileName}: ${seen - unwrapped} 'Relation<T>' reference(s) are somewhere this codemod does ` +
205
165
  'not rewrite; unwrap them and drop the import by hand');
206
166
  }
207
- return new Set(seen > 0 && seen === unwrapped ? ['InjectQuerier', 'Relation'] : ['InjectQuerier']);
167
+ return new Set(seen > 0 && seen === unwrapped ? ['Relation'] : []);
208
168
  }
209
169
  /**
210
- * Removes `import 'reflect-metadata'` and any name that no longer exists, and returns the `uql-orm`
211
- * named imports still standing - `undefined` when there are none to extend.
170
+ * Removes `import 'reflect-metadata'`, since the polyfill only ever fed `design:type`, and any name
171
+ * `uql-orm` no longer exports.
212
172
  */
213
173
  function dropDeadImports(source, ctx) {
214
174
  const dead = deadImportNames(source, ctx);
215
- let host;
216
175
  for (const statement of source.statements) {
217
176
  if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
218
177
  continue;
219
178
  }
220
- const module = statement.moduleSpecifier.text;
221
- if (module === 'reflect-metadata' && !statement.importClause) {
179
+ if (statement.moduleSpecifier.text === 'reflect-metadata' && !statement.importClause) {
222
180
  ctx.edits.push(removeStatement(statement));
223
181
  continue;
224
182
  }
@@ -234,25 +192,7 @@ function dropDeadImports(source, ctx) {
234
192
  for (const element of dropped) {
235
193
  ctx.edits.push(removeFromList(named.elements, element, source));
236
194
  }
237
- if (module === 'uql-orm') {
238
- host = named;
239
- }
240
- }
241
- return host;
242
- }
243
- /** Adds what the rewrites need, extending `host` when there is one rather than adding a second import. */
244
- function addNeededImports(source, ctx, host) {
245
- const missing = [...ctx.imports].filter((name) => !host?.elements.some((element) => element.name.text === name));
246
- if (!missing.length) {
247
- return;
248
195
  }
249
- if (host) {
250
- const last = host.elements[host.elements.length - 1];
251
- ctx.edits.push({ start: last.getEnd(), end: last.getEnd(), text: `, ${missing.join(', ')}` });
252
- return;
253
- }
254
- const position = source.statements[0]?.getStart() ?? 0;
255
- ctx.edits.push({ start: position, end: position, text: `import { ${missing.join(', ')} } from 'uql-orm';\n` });
256
196
  }
257
197
  /** Everything the standard spec needs written onto one decorated property. */
258
198
  function rewriteProperty(node, ctx) {
@@ -281,8 +221,9 @@ function reportRemovedDecorators(node, ctx) {
281
221
  }
282
222
  for (const decorator of ts.getDecorators(node) ?? []) {
283
223
  const name = decoratorName(decorator);
284
- if (name && REMOVED_DECORATORS.has(name)) {
285
- ctx.unresolved.push(`${ctx.describe(decorator)}: '@${name}()' was removed; delete it and its import`);
224
+ const advice = name && REMOVED_DECORATORS.get(name);
225
+ if (advice) {
226
+ ctx.unresolved.push(`${ctx.describe(decorator)}: '@${name}()' was removed; ${advice}`);
286
227
  }
287
228
  }
288
229
  }
@@ -293,24 +234,20 @@ export function transformFile(source, checker) {
293
234
  edits: [],
294
235
  unresolved: [],
295
236
  notes: [],
296
- imports: new Set(),
297
237
  relationAlias: { seen: 0, unwrapped: 0 },
298
- describe: (node) => `${source.fileName}:${ctx.lineOf(node) + 1}`,
299
- lineOf: (node) => source.getLineAndCharacterOfPosition(node.getStart()).line,
238
+ describe: (node) => `${source.fileName}:${lineOf(node) + 1}`,
300
239
  };
240
+ const lineOf = (node) => source.getLineAndCharacterOfPosition(node.getStart()).line;
301
241
  const visit = (node) => {
302
242
  reportRemovedDecorators(node, ctx);
303
243
  countRelationAlias(node, ctx);
304
244
  if (ts.isPropertyDeclaration(node)) {
305
245
  rewriteProperty(node, ctx);
306
246
  }
307
- else if (ts.isMethodDeclaration(node)) {
308
- rewriteInjectQuerier(node, ctx);
309
- }
310
247
  ts.forEachChild(node, visit);
311
248
  };
312
249
  visit(source);
313
- reconcileImports(source, ctx);
250
+ dropDeadImports(source, ctx);
314
251
  return {
315
252
  fileName: source.fileName,
316
253
  text: applyEdits(source.getFullText(), ctx.edits),
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uql-codemod",
3
3
  "description": "Migrates uql-orm entities from legacy decorators to the TC39 standard spec",
4
4
  "license": "MIT",
5
- "version": "0.24.2",
5
+ "version": "0.25.0",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=24"