Skip to content

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.

Terminal window
npx uql-codemod --project=tsconfig.json

Add --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.

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().

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.

engines.node is now >=24. Node 20 reached end of life in April 2026 and Node 24 is the active LTS.

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 release
await pool.transaction((querier) => save(querier, user)); // same function, inside a transaction

See 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.

Two things became available with the same release:

  • NodeSqliteQuerierPool runs 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.

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.