@rafikidota/iroh-query-builder
v0.1.0
Published
TypeORM query builder extracted from iroh: type-safe where/order/select/relations from a plain query object.
Maintainers
Readme
@rafikidota/iroh-query-builder
Sometimes, the best way to solve your own problems is to help someone else.
Type-safe query builder for TypeORM: build where / order / select / relations from a plain, serializable query object instead of chaining TypeORM's QueryBuilder API by hand. Extracted from the iroh project.
Useful when a query shape needs to travel across a boundary (HTTP request body, message queue, RPC payload) and get validated/typed before hitting the database.
Install
npm install @rafikidota/iroh-query-builderRequires typeorm@^1.0.0 as a peer dependency.
Usage
import { QueryBuilder, type QueryForm } from '@rafikidota/iroh-query-builder';
import { dataSource } from './data-source';
import { User } from './user.entity';
const form: QueryForm<User> = {
relations: { profile: true },
select: { name: true, profile: { bio: true } },
where: {
age: { $gte: 18 },
profile: { verified: true },
$or: [{ name: 'john' }, { name: 'jane' }],
},
order: { name: 'ASC' },
};
const sqb = dataSource.getRepository(User).createQueryBuilder('u');
const qb = new QueryBuilder(sqb);
qb.addOptions(form);
qb.addLimit(20);
qb.addOffset(0);
const [rows, count] = await qb.getManyAndCount();QueryBuilder wraps a TypeORM SelectQueryBuilder<T> and reads its root alias from sqb.alias (whatever you passed to createQueryBuilder(...)). Nested relations are namespaced under it as <alias>_<relation>, <alias>_<relation>_<nested>, ...
QueryForm<T>
type QueryForm<T> = {
select?: SelectOption<T>;
relations?: RelationOption<T>;
where?: WhereOption<T>;
order?: OrderOption<T>;
};Each option can be applied independently via its own method (addSelect, addRelations, addWhere, addOrder) or all at once via addOptions(form).
select— nested object oftrueflags. If no field is set totrueat a given level, that whole alias is selected (addSelect('alias')).relations— nested object oftrue/ nested objects, mirrors TypeORM'sFindOptionsRelations. Each relation becomes aLEFT JOINunder a namespaced alias, and anyselectfields nested under it are merged in automatically.order— nested object of'ASC' | 'DESC'. Falsy entries are skipped.where— see below.
where operators
A bare value is an implicit $eq. Supported operators:
| Operator | SQL |
| --- | --- |
| $eq | = |
| $ne | <> |
| $gt / $gte | > / >= |
| $lt / $lte | < / <= |
| $in / $nin | IN / NOT IN |
| $like / $nlike | LIKE / NOT LIKE |
| $ilike / $nilike | ILIKE / NOT ILIKE |
| $between / $nbetween | BETWEEN / NOT BETWEEN |
| $regexp / $iregexp | ~ / ~* |
| $isnull / $notnull | IS NULL / IS NOT NULL |
Top-level fields are ANDed together. Explicit boolean composition is available via $and, $or, $not:
const where: WhereOption<User> = {
$or: [{ name: 'john' }, { $and: [{ age: { $gte: 18 } }, { age: { $lte: 65 } }] }],
$not: { deletedAt: { $isnull: true } },
};Nested relation fields use dotted paths under the hood (profile.age) and resolve to the namespaced alias (alias_profile.age).
Operator helper functions are also available for building conditions programmatically instead of writing raw operator objects:
import { Equal, In, IsNull, Between } from '@rafikidota/iroh-query-builder';
const where = {
name: Equal('john'),
id: In([1, 2, 3]),
deletedAt: IsNull(),
createdAt: Between(from, to),
};Terminal methods
QueryBuilder delegates to the underlying SelectQueryBuilder: getMany(), getManyAndCount(), getOne(), getCount(), and the sql getter (getSql()).
Development
pnpm install
pnpm build # bundle with tsdown -> dist/
pnpm typecheck # tsc --noEmit
pnpm lint # eslint --fix
pnpm lint:check # eslint, no fixes
pnpm format # prettier --write
pnpm test # vitest run
pnpm test:watch # vitest watch mode
pnpm test:types # vitest run --typecheck.only (*.test-d.ts)Run a single test file: pnpm vitest run test/query-builder.test.ts.
Node version is pinned in .nvmrc; package manager is pnpm (pnpm-workspace.yaml / .npmrc).
License
MIT
