Upgrade guide
UQL’s decorators are now the standard TC39 ones, and the legacy experimentalDecorators spec is no
longer supported. A codemod handles the mechanical part.
npx uql-codemod --project=tsconfig.jsonAdd --dry-run first to see what it would change. It reports anything it will not guess at rather than
guessing, so read its output before committing.
What the codemod does
Section titled “What the codemod does”| Change | Why |
|---|---|
Adds type to every @Field / @Id |
Nothing reflects any more, so the type has to be stated. It is now checked against the property, so a wrong one is a compile error rather than a silently wrong column. |
Adds entity: () => Other to relations |
Same reason: there is no reflected type to infer the target from. |
Unwraps Relation<T> to T |
The alias only existed because reflection stored the property type at class-definition time, which forced the circular imports it was working around. |
Removes declare from decorated fields |
A declare field emits nothing, so the standard spec has nothing to decorate and rejects it. |
Rewrites @InjectQuerier() to currentQuerier() |
Parameter decorators do not exist in the standard spec. |
Removes both decorator flags from tsconfig.json |
Neither is used now. The rest of the file, comments included, is left as written. |
Removes import 'reflect-metadata' |
Unused. |
It reports rather than edits where a choice is yours: target: esnext (removing the line would fall back to es5, and any dated target works), a value inherited through extends from a base tsconfig.json, and any remaining @Log() or @Serialized().
What to do by hand
Section titled “What to do by hand”Drop reflect-metadata. It is no longer a peer dependency.
Check target. It must not be esnext. Any dated target works.
Confirm your build transforms standard decorators. esbuild, SWC, Babel with
version: '2023-11', Bun and tsc all do. Oxc does not, so a Vite 8 project needs one of the
others through a plugin.
A uql.config.ts needs a runtime that transforms TypeScript. Node strips types on its own, which is enough for a config that is only types and an object, but decorators are not erasable syntax, so a config that imports entity classes needs more. Run the CLI with bun, or with node --import tsx
(npm i -D tsx). A JavaScript config, or passing the config inline, needs neither. UQL deliberately bundles no transpiler: only your runtime knows your tsconfig.json.
NestJS projects: switch entities to defineEntity. Nest’s DI needs parameter decorators, so those projects must keep experimentalDecorators: true, and one tsconfig.json cannot mix specs. See NestJS. The codemod cannot help here.
Node 24 is the minimum
Section titled “Node 24 is the minimum”engines.node is now >=24. Node 20 reached end of life in April 2026 and Node 24 is the active LTS.
The pool runs every operation
Section titled “The pool runs every operation”QuerierPool implements the whole UniversalQuerier, so a function that writes can take “a querier, or the pool”:
import type { UniversalQuerier } from 'uql-orm';
const save = (querier: UniversalQuerier, user: User) => querier.saveOne(User, user);
await save(pool, user); // one acquisition, one releaseawait pool.transaction((querier) => save(querier, user)); // same function, inside a transactionSee pool vs. querier. Only a hand-written QuerierPool or a partial UniversalQuerier has anything to do: insertMany, updateMany, upsertOne, upsertMany and saveMany are not optional.
While you are here
Section titled “While you are here”Two things became available with the same release:
NodeSqliteQuerierPoolruns SQLite on Node’s built-in driver, with no native module.await using querier = await pool.getQuerier()releases the querier on the way out, so the try/finally around manual transactions is optional. See Transactions.
Branded id types resolve to String
Section titled “Branded id types resolve to String”If your ids use a branded string type, the codemod writes type: String:
import { Id } from 'uql-orm';
type UUID = `${string}-${string}-${string}-${string}-${string}`;
@Id({ type: String }) id?: UUID;That is deliberate. Reflection erased such a type to String at runtime, so String is the column your database already has. Writing 'uuid' instead would change the generated column from string to uuid, which the next drift:check would report as a difference needing a migration.
'uuid' is often what was actually wanted, so the codemod prints a worth a look: line for each one. Change them if you want the narrower column, and generate a migration for the change.