nestjs-rest-querynestjs-rest-query
AdaptersAdapters

Adapters

How nestjs-rest-query talks to TypeORM, Prisma and Drizzle - one semantic core, three compilers, and the limits each one declares.

nestjs-rest-query separates what a request means from how one ORM executes it. The core parses the query string, authorises it against the endpoint rules, coerces every value by its declared kind and produces a frozen TypedQueryPlan. An adapter only compiles that plan.

The plan, then the adapter

Rendering Mermaid diagram...

Two consequences worth stating plainly:

  • Normalisation is the core's job, not the adapter's. The adapter hydrates rows; the core turns them into the canonical JSON. That is the only place where PostgreSQL's bigint, MySQL's number and SQL Server's string for the same column become the same output.
  • There is no global adapter setting. forRoot({ adapter }) is refused at startup. The adapter arrives with the source you pass to execute().

Choosing an adapter

ORMSource factorySubpathWhat you declare
TypeORMtypeormSource(repository)nestjs-rest-query/typeormThe logical schema, or derive it with buildSchemaRegistry(repository). Companion columns follow the _folded / _order convention.
PrismaprismaSource({ client, model, manifest })nestjs-rest-query/prismaThe logical schema and a hand-written manifest (provider, registry, models).
DrizzledrizzleSource({ db, dialect, table, relations })nestjs-rest-query/drizzleA logical table descriptor plus relations by dotted path; drizzleDatabase({ client, dialect }) once.

All three factories are generic in the row type. typeormSource<T>(repository) infers T from the repository; drizzleSource<TRow> takes it from the executor you built with drizzleDatabase<TRow>({ ... }), so it is inferred too; prismaSource<TRow> is the one you name explicitly. A service can annotate Promise<NormalizedQueryResult<UserDto>> without a cast anywhere.

Parity, and how it is measured

The promise is that the same request produces the same observable outcome in nine combinations — TypeORM, Prisma and Drizzle × PostgreSQL, MySQL and SQL Server — measured by one corpus with one set of expectations, including byte-for-byte 400 messages.

SQLite is not a matrix cell. It is the reference dialect: it proves each adapter's compiler implements the plan's semantics, it runs without a container, and a green result there is not parity.

The current per-cell state, including which gates are still open, is in docs/v3/status.md; the supported version matrix is in docs/v3/versions.md.

Declared divergences and limits

A divergence — same input, different observable outcome by adapter — is allowed only where the underlying model makes "the same as TypeORM" ambiguous or unsafe. Each one is declared as data on the corpus case itself, with a mandatory reason, so an adapter that starts agreeing again breaks the build and forces the exception to be deleted.

AdapterCaseOutcome
Prismalike, notLike, ilike, notIlike, search on sqlite and sqlserverRefused, 400 CAPABILITY_UNAVAILABLE. Prisma compiles contains with no ESCAPE clause and those two dialects have no default escape character, so % and _ cannot be literal. Works on postgresql and mysql.
DrizzleProjecting a to-many collection nested under another relationRefused, ADAPTER_CONTRACT_VIOLATION. First-level collections are supported.

Existential conditions are not on that list. A filter or search target that crosses a many relation compiles in all three adapters at any depth — one hop, a further one hop (posts.author.name), a second collection, or a many-to-many (posts.tags.label) — and each of those is a corpus case with no declared divergence. TypeORM emits one EXISTS correlated to the root exactly once, with the later hops as INNER JOINs inside the subquery; for a many-to-many the subquery's FROM is the junction table and the target joins in from there.

The search whitelist in 02-app-with-postgres is a good illustration: it keeps user.firstName (one one hop) and items.company.name (through the items collection and then company), and the same rules run unchanged under Prisma and Drizzle. items.company.cnpj stays out for an unrelated reason — search compares through a folded column, and cnpj declares none.

Everything else — filter operators, search, pagination shape, paginate=false, the customize hook, isNull on a one or many relation, repeated filters, whitelist rejection, dotted-path filters — is exercised by the parity corpus and produces identical outcomes across all three adapters.

Get started

Edit this page on GitHub

On this page