@dudousxd/nestjs-filter-drizzle
Version:
Drizzle ORM adapter for @dudousxd/nestjs-filter.
249 lines • 11.7 kB
TypeScript
import { type AnyColumn, Column, SQL, type SQLWrapper, type Table } from 'drizzle-orm';
import type { DrizzleDialect } from './operator-resolver.js';
import { type DrizzleSchemaMetadata, type ResolvedRelation } from './schema-metadata.js';
import type { DrizzleDatabase } from './types.js';
/**
* The slice of a drizzle select builder the query uses. Every dialect's builder
* has this shape at runtime; the structural type sidesteps the union of three
* dialect-specific overload sets, which TypeScript cannot call through.
*/
interface SelectChain extends SQLWrapper {
where(condition: SQL | undefined): SelectChain;
orderBy(...columns: Array<SQL | SQL.Aliased | AnyColumn>): SelectChain;
groupBy(...columns: Array<SQL | AnyColumn>): SelectChain;
limit(limit: number): SelectChain;
offset(offset: number): SelectChain;
as(alias: string): Table;
toSQL(): {
sql: string;
params: unknown[];
};
then<R>(onfulfilled: (rows: Record<string, unknown>[]) => R): Promise<R>;
}
interface QueryDatabase {
select(fields?: Record<string, unknown>): {
from(source: unknown): SelectChain;
};
selectDistinct(fields?: Record<string, unknown>): {
from(source: unknown): SelectChain;
};
}
/** A row of `TTable`, plus whatever includes / computed aliases were attached. */
export type DrizzleRow<TTable extends Table> = TTable['$inferSelect'] & Record<string, unknown>;
/** A projected field: a column, a raw expression, or an aliased expression. */
export type SelectionValue = AnyColumn | SQL | SQL.Aliased;
/**
* State shared by a root query and every child it spawns (relation constraints,
* nested `EXISTS`): the database, the dialect, the schema metadata, and ONE
* alias counter — so two subqueries over the same table never collide, however
* deeply they nest.
*/
export declare class DrizzleQueryContext {
readonly db: DrizzleDatabase;
readonly dialect: DrizzleDialect;
readonly metadata: DrizzleSchemaMetadata;
private aliasSeq;
constructor(db: DrizzleDatabase, dialect: DrizzleDialect, metadata: DrizzleSchemaMetadata);
/** A fresh, SQL-safe table alias (`posts_1`, `manager_2`, …). */
nextAlias(base: string): string;
/** @internal — the untyped select entry points. */
get queryDb(): QueryDatabase;
}
/**
* The query state the core hands to a Drizzle filter as `this.$query`.
*
* Drizzle's select builders are immutable-ish and single-shot (`.where()`
* replaces, the builder is bound to one projection at creation time), which is
* the opposite of what a filter pipeline needs: many independent methods each
* contributing a condition. So filters write into this accumulator instead —
* conditions, ordering, a page window, a projection, includes — and the
* accumulated state is materialized into ONE `db.select().from(table)` at
* execution time ({@link toSelect}, {@link execute}).
*
* ```ts
* @FilterFor('minAge')
* applyMinAge(value: number) {
* this.$query.where(gte(users.age, value));
* }
* ```
*
* Relations never become joins on the root query. A relation constraint is an
* `EXISTS (…)` subquery and an include is a second, batched query — so the
* root query always returns exactly one row per matching entity, and
* `LIMIT`/`OFFSET`/`COUNT(*)` stay correct with any number of to-many
* relations involved.
*/
export declare class DrizzleQuery<TTable extends Table = Table> {
readonly context: DrizzleQueryContext;
readonly baseTable: TTable;
private readonly conditions;
private readonly orderings;
private limitValue;
private offsetValue;
private projection;
private readonly extras;
private distinctFlag;
private readonly includePaths;
/**
* @param context - Shared db/dialect/metadata/alias state.
* @param baseTable - The ORIGINAL table object (metadata is keyed by it).
* @param aliased - When this query targets an aliased copy of the table (a
* relation constraint's subquery), that alias; columns are read off it.
*/
constructor(context: DrizzleQueryContext, baseTable: TTable, aliased?: TTable);
/** The table the query reads — the original, or its alias inside a subquery. */
readonly table: TTable;
/** The drizzle database the query executes against. */
get db(): DrizzleDatabase;
/** `'postgres' | 'mysql' | 'sqlite'`. */
get dialect(): DrizzleDialect;
/** The table's columns keyed by property name — `this.$query.columns.email`. */
get columns(): TTable['_']['columns'];
/**
* ANDs one or more conditions onto the query. `undefined` entries are
* ignored, so drizzle's `and()`/`or()` (which return `undefined` for an empty
* list) compose without guards.
*/
where(...conditions: Array<SQL | undefined>): this;
/**
* Like {@link where}, but also accepts an equality map keyed by column
* property — `andWhere({ status: 'active', role: ['a', 'b'] })` — which is
* the shape the core's `related()` helper and `@TenantScoped` emit. Array
* values become `IN`, `null` becomes `IS NULL`; keys that are not columns of
* the table are ignored.
*/
andWhere(condition: SQL | Record<string, unknown> | undefined): this;
/**
* Constrains the query to rows that HAVE a related row — `EXISTS (SELECT 1
* FROM <relation> WHERE <correlation> AND <build(target)>)`. The callback
* receives the related table (an alias — reference its columns through the
* argument, not the imported table object). A dotted path follows several
* relations (`'posts.comments'`).
*
* ```ts
* this.$query.whereHas('posts', (posts) => eq(posts.status, 'published'));
* ```
*
* An unknown relation throws — silently ignoring a constraint would return
* rows the caller asked to exclude.
*/
whereHas(relationPath: string, build?: (target: Table) => SQL | undefined): this;
/** The negation of {@link whereHas}: rows with NO matching related row. */
whereDoesntHave(relationPath: string, build?: (target: Table) => SQL | undefined): this;
/** The accumulated WHERE — every condition ANDed — or `undefined` when empty. */
getWhere(): SQL | undefined;
/**
* Appends ORDER BY terms. A bare column sorts ascending; use drizzle's
* `asc()`/`desc()` for an explicit direction.
*/
orderBy(...terms: Array<SQL | SQL.Aliased | AnyColumn>): this;
/** Drops every ORDER BY term accumulated so far. */
clearOrderBy(): this;
limit(limit: number | undefined): this;
offset(offset: number | undefined): this;
/**
* Replaces the projection with the given fields (keyed by output name). The
* default projection is every column of the table.
*/
select(fields: Record<string, SelectionValue>): this;
/**
* Adds fields to the projection without replacing it — computed values that
* should come back on each row next to the table's own columns.
*/
addSelect(fields: Record<string, SelectionValue>): this;
/** Marks the projection `SELECT DISTINCT`. */
distinct(on?: boolean): this;
/**
* Relations to load onto the fetched rows (dotted for nesting:
* `'posts.comments'`). Loaded by {@link execute} in separate batched
* queries after the page is fetched, never joined — see the class doc.
*/
include(...relationPaths: string[]): this;
isDistinct(): boolean;
getIncludes(): readonly string[];
getOrderBy(): ReadonlyArray<SQL | SQL.Aliased | AnyColumn>;
getLimit(): number | undefined;
getOffset(): number | undefined;
/** The projection currently in effect (explicit, or all columns), plus additive fields. */
getSelection(): Record<string, SelectionValue>;
/** True when the projection was narrowed (`select`/`distinct`) rather than all columns. */
hasExplicitProjection(): boolean;
/**
* Materializes the accumulated state as a drizzle select builder:
* `db.select(<projection>).from(table).where(…).orderBy(…).limit(…).offset(…)`.
* Use it to extend the query with anything the accumulator does not model,
* or to hand it to drizzle APIs that take a builder.
*/
toSelect(opts?: {
withWindow?: boolean;
withOrder?: boolean;
}): SelectChain;
/** The SQL + bound params the query would run. */
toSQL(): {
sql: string;
params: unknown[];
};
/** Runs the query and loads every {@link include}d relation onto the rows. */
execute(): Promise<DrizzleRow<TTable>[]>;
/**
* `COUNT(*)` over the same WHERE, ignoring ordering and the page window. For
* a DISTINCT projection, counts distinct TUPLES of the projection (via a
* derived table, the one form every dialect accepts for several columns).
*/
count(): Promise<number>;
/** {@link execute} + {@link count} — the page and the total it was cut from. */
executeAndCount(): Promise<{
rows: DrizzleRow<TTable>[];
total: number;
}>;
/**
* The projection sent to the database. Includes need the key they join on:
* when a narrowed projection dropped it, it is added back so the include can
* still be resolved.
*/
private selectionForExecution;
/**
* A column of this query's table by property key — the aliased column when
* the query runs over an alias.
*/
column(key: string): Column | undefined;
/**
* `EXISTS` over a relation chain starting at this query's table. The last
* hop's (aliased) table is handed to `build` for the inner condition.
* Returns `undefined` when any segment is not a relation.
*/
relationExists(segments: string[], build?: (target: Table) => SQL | undefined): SQL | undefined;
/**
* A scalar expression for a to-one relation path's column
* (`manager.name` → `(SELECT m.name FROM users m WHERE m.id = users.manager_id)`),
* used for ORDER BY / DISTINCT on relation fields without joining. Returns
* `undefined` when the path crosses a to-many relation (it has no single
* value) or does not resolve.
*/
relationScalar(segments: string[]): SQL | undefined;
/**
* Creates the child query a relation constraint's filter writes into: it
* targets a fresh alias of the related table, and {@link relationExistsFor}
* later folds its WHERE into an `EXISTS` on this query.
*/
childFor(relation: ResolvedRelation): DrizzleQuery<Table>;
/**
* The predicate correlating `targetView` (an alias of `relation.target`) to
* this query's table — the WHERE of a correlated subquery over the relation.
*/
correlate(relation: ResolvedRelation, targetView: Table): SQL | undefined;
/** `EXISTS` correlating `child` (made by {@link childFor}) to this query's table. */
relationExistsFor(relation: ResolvedRelation, child: DrizzleQuery<Table>): SQL;
}
/**
* Loads relation paths onto already-fetched rows with one batched
* `SELECT … WHERE <key> IN (…)` per relation (and per nesting level), then
* grafts the results back: a to-many relation becomes an array (empty when
* nothing matched), a to-one relation the row or `null`. The shape matches
* drizzle's relational queries (`with: { posts: true }`).
*
* Unknown relations and composite-key relations are skipped.
*/
export declare function loadRelations(context: DrizzleQueryContext, table: Table, rows: Record<string, unknown>[], paths: readonly string[]): Promise<void>;
export {};
//# sourceMappingURL=drizzle-query.d.ts.map