@rapiq/typeorm
Applies a parsed Query directly to a TypeORM SelectQueryBuilder — filters become parameterized WHERE conditions, relations become joins, fields/sort/pagination map to select/orderBy/take+skip.
npm install @rapiq/core @rapiq/sql @rapiq/typeormUsage
import { TypeormAdapter } from '@rapiq/typeorm';
const queryBuilder = dataSource.getRepository(User).createQueryBuilder('user');
const adapter = new TypeormAdapter({
queryBuilder,
relations: { joinAndSelect: true },
});
const { pagination } = adapter.execute(query);
const [entities, total] = await queryBuilder.getManyAndCount();The queryBuilder (the builder to write into) is bound at construction; execute(query) then walks the parsed Query, collects the state into its sub-adapters, and applies it to that builder — returning the applied pagination (e.g. for the response meta block).
Construct the adapter per request, just like the SelectQueryBuilder you hand it — it holds per-call state. The shareable, long-lived part is your config, which you spread into the per-request options:
// module scope — the reusable config
const config = { relations: { joinAndSelect: true } };
// per request — add the request's builder as `queryBuilder`
new TypeormAdapter({ ...config, queryBuilder }).execute(query);By default each call clears the adapter's own accumulated state. The bound queryBuilder, however, is mutated in place — joins and the selected projection are not rolled back — so build a fresh adapter (with a fresh builder) per query rather than re-running one onto an already-applied builder. Pass { clear: false } as the second argument to accumulate several queries' conditions onto the same builder, and { visitor } to forward options to the underlying visitors:
adapter.execute(query, { clear: false });Options
new TypeormAdapter({
relations: {
joinAndSelect: true,
joinType: 'left',
onJoin: (path, alias, queryBuilder) => {
queryBuilder.addGroupBy(`${alias}.id`);
},
},
});| Option | Description |
|---|---|
relations.joinAndSelect | Join and select (hydrate the related entities) instead of joining for filtering/sorting only. |
relations.joinType | 'left' (default) or 'inner'. Left joins keep records whose relation is absent. |
relations.onJoin | Invoked as (path, alias, queryBuilder) for every join the adapter applies — e.g. to addGroupBy per join when the root query is grouped. Skipped (pre-existing) joins don't trigger it. |
Relations are validated against the entity metadata of the attached query builder — a requested relation that doesn't exist on the entity is ignored. Joins are applied idempotently: relations already joined on the query builder (by the adapter or by your own code, matched by alias) are skipped, so applying a query twice does not duplicate joins.
Alias convention
Joins are aliased by the relation path's last segment: role.realm joins as alias realm — the same convention filter/sort/field references resolve against. Two relation paths ending in the same segment (e.g. realm and role.realm) therefore collide: the later join is skipped and references resolve against the first one. Path-qualified aliases are tracked in #744.
Dialect detection
The adapter resolves the SQL dialect from the attached query builder's connection type (postgres, mysql/mariadb, sqlite/better-sqlite3, mssql, oracle, …). Field escaping is delegated to the query builder itself; regex conditions use the matching dialect preset — on regex-less dialects (SQLite, SQL Server) the contains / startsWith / endsWith operators fall back to LIKE, and the regex operator throws a typed AdapterError. When the connection type has no matching preset, the postgres preset is the documented last-resort default.
Applying a single parameter
A Query with only some parameters set applies just those — the rest are empty and become no-ops. To apply, say, only the filters of a parsed query:
import { Query } from '@rapiq/core';
const adapter = new TypeormAdapter({ queryBuilder });
adapter.execute(new Query({ filters: query.filters }));For lower-level control, each per-parameter sub-adapter (adapter.filters, adapter.fields, adapter.sort, adapter.pagination, adapter.relations) pairs with the matching @rapiq/sql visitor (FiltersVisitor, FieldsVisitor, SortsVisitor, PaginationVisitor, RelationsVisitor) and applies via its own execute() — the query builder is already bound from the adapter's construction.
End-to-end example
The complete Express endpoint — schemas, decoding, error handling, response meta — lives in the Express & TypeORM recipe.
Migrating from typeorm-extension
applyQuery used leftJoinAndSelect and returned the parsed pagination — joinType: 'left' (the default) and the execute(query) return value mirror that contract. See the migration guide.