@adonis-agora/filter
Version:
Server-side query filtering/sorting/pagination for AdonisJS — Spatie-style input, a Lucid adapter, and field allow-listing. Part of the Agora ecosystem.
107 lines • 5.93 kB
TypeScript
import type { FilterSpec } from './filter_spec.js';
/**
* Codegen for the typed client — the AdonisJS-idiomatic port of the NestJS
* `nestjs-filter` codegen. Where the NestJS package ships a standalone
* `reflect-metadata` bin that reads decorator metadata off entity classes and
* emits `filterQuery: () => filterQueryTyped<Fields, TypeMap>()` members into a
* generated `api.ts`, the Adonis port has no decorators: the filter declaration
* is already a plain runtime {@link FilterSpec} produced by `defineFilter(...)`.
*
* So generation is a **pure function of the spec** — string in, string out, no
* AST walk, no metadata reflection, no framework. The ace command
* (`make:filter-client`) is the thin IO wrapper that loads the app's declared
* specs and writes the emitted module to disk. Keeping the generator pure keeps
* it trivially testable (snapshot/string assertions) and framework-free, exactly
* like the rest of this core.
*
* The emitted module targets `@adonis-agora/filter-client`'s `filterQueryTyped<Fields,
* FieldTypes>()` factory — the same client the NestJS codegen emitted against —
* so the two ecosystems share one browser runtime.
*/
import type { FilterFieldKind } from './types.js';
export type { FilterFieldKind };
/**
* Optional per-field type information. A {@link FilterSpec} carries the
* allow-list but not column value types (Lucid models aren't reflected here), so
* the caller supplies types to unlock the client's type-aware operator narrowing.
* Without it the emitted client is still field-name-safe, just operator-permissive.
*/
export interface FilterFieldTypeInfo {
/** Classified value kind. Ignored when {@link typeRef} or {@link enumValues} is set. */
kind?: FilterFieldKind;
/** Enum literal members — emitted as a union (`"A" | "B"` or `1 | 2`). Wins over {@link kind}. */
enumValues?: readonly (string | number)[];
/** A named TS type (e.g. `'Role'`) — emitted verbatim. Wins over everything. */
typeRef?: string;
/** Nullable column — appends `| null` to the emitted type. */
nullable?: boolean;
}
/** Options for {@link generateFilterClient}. */
export interface GenerateFilterClientOptions {
/**
* Base name for the emitted identifiers — e.g. `'people'` yields
* `PeopleFilterFields`, `PeopleFilterFieldTypes`, `peopleFilterMeta` and
* `peopleFilterQuery()`. Non-identifier characters are stripped.
*/
name: string;
/** Per-field value types keyed by (dotted) field path. Unlocks operator narrowing. */
fieldTypes?: Record<string, FilterFieldTypeInfo>;
/** Import specifier for `filterQueryTyped`. Default `'@adonis-agora/filter-client'`. */
clientModule?: string;
/** Cap relation-path depth of the emitted field union. Default {@link FilterSpec.maxDepth}. */
maxDepth?: number;
/** Emit the "generated — do not edit" banner. Default `true`. */
banner?: boolean;
}
/** Enumerate the filterable field paths a spec admits, capped at `maxDepth` relation hops. */
export declare function filterableFieldPaths(spec: FilterSpec, maxDepth?: number): string[];
/** Enumerate the sortable field paths a spec admits, capped at `maxDepth` relation hops. */
export declare function sortableFieldPaths(spec: FilterSpec, maxDepth?: number): string[];
/**
* Generate a typed filter client module for one {@link FilterSpec} — a pure
* string transform. The emitted module exports, for a spec named `people`:
*
* - `type PeopleFilterFields` — the union of filterable field paths (the security
* boundary, as concrete string literals);
* - `interface PeopleFilterFieldTypes` — the per-field value-type map (only when
* `fieldTypes` is supplied), which drives the client's operator/value narrowing;
* - `const peopleFilterMeta` — runtime metadata (filterable/sortable/searchable
* fields, whitelisted relations, per-field kinds, and the default sort / page
* size / max size + cursor keyset the runner paginates by);
* - `function peopleFilterQuery()` — a `filterQueryTyped<Fields, FieldTypes>()`
* factory returning a type-safe builder scoped to this spec.
*/
export declare function generateFilterClient(spec: FilterSpec, options: GenerateFilterClientOptions): string;
/**
* One entry in a {@link FilterClientManifest}: a declared spec plus the optional
* type info / codegen overrides the emitter needs. This is what an app exports
* from `config/filter.ts` (as `filters`) for the `make:filter-client` command to
* consume — the specs are already `defineFilter(...)` results, so no reflection.
*/
export interface FilterClientEntry {
/** The `defineFilter(...)` result to generate a client for. */
spec: FilterSpec;
/** Per-field value types (see {@link GenerateFilterClientOptions.fieldTypes}). */
fieldTypes?: Record<string, FilterFieldTypeInfo>;
/** Override the client import specifier for this entry. */
clientModule?: string;
/** Override the relation-path depth cap for this entry. */
maxDepth?: number;
}
/** A map of client name → declared spec, keyed by the emitted client's base name. */
export type FilterClientManifest = Record<string, FilterClientEntry>;
/** A single generated client: its name, its AdonisJS-conventional file name, and its code. */
export interface GeneratedFilterClient {
name: string;
/** `snake_case`d file name, e.g. `blog_post_filter_client.ts`. */
filename: string;
code: string;
}
/**
* Expand a whole {@link FilterClientManifest} into per-spec generated modules —
* the pure core the `make:filter-client` ace command writes to disk. Kept pure
* (manifest in, modules out) so the command stays a thin IO wrapper and the
* expansion is itself testable without a running app.
*/
export declare function generateFilterClients(manifest: FilterClientManifest): GeneratedFilterClient[];
//# sourceMappingURL=generate_client.d.ts.map