@azghr/filterkit
v0.3.13
Published
Framework-agnostic, type-safe filtering for TypeScript: one filter definition that runs in memory on the client and translates to Prisma, MongoDB, or SQL on the server.
Downloads
300
Maintainers
Readme
filterkit
Framework-agnostic, type-safe filtering for TypeScript. Define a filter once — run it in memory on the client (React, Vue, Angular, vanilla) or translate it into a database query on the server (Prisma, MongoDB, raw SQL, Drizzle, or your own adapter).
- Zero runtime dependencies, ~2.5 KB gzipped core
- Full type safety: fields, operators, and values are all inferred from your data type, including nested objects and arrays (
"posts.comments.author"autocompletes) - Serializable filter AST: plain JSON — build on the client,
POSTit, validate, translate on the server - Extensible: register custom operators at runtime, type them via declaration merging, translate them per adapter
- Tree-shakable: adapters live behind subpath exports; if you never import
filterkit/adapters/sql, it never ships
The problem
Building and maintaining consistent filtering logic across client and server is complex. You need to:
- Ensure filters work the same in browser memory and database queries
- Maintain type safety across the stack
- Support multiple database backends with different query syntaxes
- Validate untrusted filter inputs from API clients
Each adapter (Prisma, MongoDB, SQL) has its own query format, forcing you to duplicate logic or build fragile translation layers.
Install
npm install @azghr/filterkit
# or
pnpm add @azghr/filterkit
# or
yarn add @azghr/filterkitWith adapters:
# For Drizzle ORM
pnpm add @azghr/filterkit drizzle-orm
# For Prisma
pnpm add @azghr/filterkit @prisma/client
# For MongoDB
pnpm add @azghr/filterkit mongodb
# SQL adapter has no additional dependenciesUse
Quick start
import { createFilter, createEngine } from "@azghr/filterkit";
interface User {
name: string;
age: number;
email: string;
profile: { city: string; score: number };
posts: { title: string; likes: number }[];
}
const filter = createFilter<User>()
.where("age", "greaterThanOrEquals", 18)
.where("email", "endsWith", "@example.com")
.or((b) => b.where("profile.city", "equals", "Berlin"))
.build();
const sort = [{ field: "age", direction: "desc" }];
// Client side: filter + sort in memory
const engine = createEngine<User>();
const visible = engine.filter(users, filter, { sort });
// The same filter is plain JSON — send it to your API
await fetch("/api/users/search", {
method: "POST",
body: JSON.stringify({ filter, sort }),
});Server-side translation
// Server side: translate the SAME filter + sort into a database query
import { toDrizzleWhere, toDrizzleOrderBy } from "@azghr/filterkit/adapters/drizzle";
import { toPrismaWhere, toPrismaOrderBy } from "@azghr/filterkit/adapters/prisma";
import { toMongoQuery, toMongoSort } from "@azghr/filterkit/adapters/mongo";
import { toSqlWhere, toSqlOrderBy } from "@azghr/filterkit/adapters/sql";
// Drizzle
const results = await db
.select()
.from(users)
.where(toDrizzleWhere(filter, { column: (f) => users[f] }))
.orderBy(...toDrizzleOrderBy(sort, (f) => users[f]));
// Prisma
const users = await prisma.user.findMany({
where: toPrismaWhere(filter, { listRelations: ["posts"] }),
orderBy: toPrismaOrderBy(sort),
});
// MongoDB
const docs = await db.collection("users")
.find(toMongoQuery(filter))
.sort(toMongoSort(sort))
.toArray();
// SQL
const { text, params } = toSqlWhere(filter, { placeholder: "$n" });
const orderBy = toSqlOrderBy(sort);
const rows = await pg.query(`SELECT * FROM users WHERE ${text} ORDER BY ${orderBy}`, params);API
Core functions
| Export | Purpose |
| --- | --- |
| createFilter<T>() | Fluent, typed builder → Filter<T> AST |
| createEngine<T>(opts?) | Engine with custom operators + getter cache |
| engine.filter(data, filter, { sort }) | Filter + sort arrays in memory |
| validateFilter(input, opts?) | Validate untrusted ASTs before translating |
| applyFilter / matchesFilter | One-shot helpers, built-ins only |
Detailed API surface
| Export | Purpose |
| --- | --- |
| allOf(...filters) | All conditions must match (alias for and) |
| anyOf(...filters) | At least one condition must match (alias for or) |
| noneOf(...filters) | No conditions must match (negated or) |
| oneOf(...filters) | Exactly one condition must match (XOR) |
| minus(a, b) | Items matching a but not b (set difference) |
| xor(a, b) | Items matching exactly one of a or b (symmetric difference) |
| simplify(filter) | Algebraic simplification: dedup, double negation collapse |
| eq() / neq() / gt() / gte() / lt() / lte() | Functional filter constructors |
| inArray() / notInArray() | Array membership filters |
| contains() / startsWith() / endsWith() | String operation filters |
| between() / exists() | Range and presence filters |
| like() / iLike() | SQL LIKE pattern matching (case-sensitive / insensitive) |
| isNull() / isNotNull() | Null/undefined presence checks |
| isEmpty() / isNotEmpty() | Empty array/string checks |
| fuzzy(data, field, query, opts?) | Fuzzy match with relevance scores |
| search(data, query, { fields }) | Multi-field weighted search |
| ScoredItem<T> | Result type: { item: T; score: number } |
| validateFilter(input, opts?) | Validate untrusted ASTs before translating |
| toDrizzleOrderBy(sort) | Sort → Drizzle orderBy objects |
| toPrismaOrderBy(sort) | Sort → Prisma orderBy |
| toMongoSort(sort) | Sort → MongoDB sort |
| toSqlOrderBy(sort) | Sort → SQL ORDER BY clause |
Built-in operators: equals, notEquals, in, notIn, exists, isNull, isNotNull, isEmpty, isNotEmpty, contains, notContains, startsWith, endsWith, matches, like, iLike, greaterThan, greaterThanOrEquals, lessThan, lessThanOrEquals, between.
Logical operators
| Export | Purpose |
| --- | --- |
| allOf(...filters) | All conditions must match (alias for and) |
| anyOf(...filters) | At least one condition must match (alias for or) |
| noneOf(...filters) | No conditions must match (negated or) |
| oneOf(...filters) | Exactly one condition must match (XOR) |
| xor(a, b) | Items matching exactly one of a or b |
| minus(a, b) | Items matching a but not b (set difference) |
| simplify(filter) | Algebraic simplification: dedup, double negation collapse |
Comparison operators
| Export | Purpose |
| --- | --- |
| eq() / neq() / gt() / gte() / lt() / lte() | Functional filter constructors |
| inArray() / notInArray() | Array membership filters |
| contains() / startsWith() / endsWith() | String operation filters |
| between() / exists() | Range and presence filters |
| like() / iLike() | SQL LIKE pattern matching |
| isNull() / isNotNull() | Null/undefined presence checks |
| isEmpty() / isNotEmpty() | Empty array/string checks |
Advanced features
| Export | Purpose |
| --- | --- |
| fuzzy(data, field, query, opts?) | Fuzzy match with relevance scores |
| search(data, query, { fields }) | Multi-field weighted search |
| adaptive(filter, opts?) | Memory filter: excludes previously matched items |
| mcda(data, criteria, opts?) | Multi-criteria weighted ranking |
| filterDiff(a, b) | Compare two filters: added, removed, changed, unchanged |
| createReplay(filter, data, opts?) | Time-travel replay: track filter results as data evolves |
Adapters
| Export | Purpose |
| --- | --- |
| filterkit/adapters/{drizzle,mongo,prisma,sql} | Query translators (tree-shaken subpaths) |
Client + Server example
// ── Client (React) ──────────────────────────────────────
import { createFilter } from "@azghr/filterkit";
interface User {
name: string;
age: number;
email: string;
profile: { city: string; };
}
const filter = createFilter<User>()
.where("age", "greaterThanOrEquals", 18)
.where("email", "endsWith", "@example.com")
.or((b) => b.where("profile.city", "equals", "Berlin"))
.build();
const sort = [{ field: "age", direction: "desc" }];
// Send filter + sort to your API
const res = await fetch("/api/users/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filter, sort }),
});
const users = await res.json();// ── Server (Express) ────────────────────────────────────
import express from "express";
import { validateFilter } from "@azghr/filterkit";
import { toPrismaWhere, toPrismaOrderBy } from "@azghr/filterkit/adapters/prisma";
const app = express();
app.use(express.json());
app.post("/api/users/search", async (req, res) => {
const { filter, sort } = req.body;
const result = validateFilter(filter, {
fields: ["age", "email", "profile.city"],
operators: ["equals", "greaterThanOrEquals", "endsWith"],
});
if (!result.valid) {
return res.status(400).json({ error: result.error });
}
const users = await prisma.user.findMany({
where: toPrismaWhere(result.filter),
orderBy: sort ? toPrismaOrderBy(sort) : undefined,
});
res.json(users);
});// ── Server (Hono + Drizzle) ─────────────────────────────
import { Hono } from "hono";
import { validateFilter } from "@azghr/filterkit";
import { toDrizzleWhere, toDrizzleOrderBy } from "@azghr/filterkit/adapters/drizzle";
const app = new Hono();
app.post("/api/users/search", async (c) => {
const { filter, sort } = await c.req.json();
const result = validateFilter(filter, {
fields: ["age", "email", "profile.city"],
operators: ["equals", "greaterThanOrEquals", "endsWith"],
});
if (!result.valid) {
return c.json({ error: result.error }, 400);
}
const users = await db
.select()
.from(usersTable)
.where(toDrizzleWhere(result.filter, { column: (f) => usersTable[f] }))
.orderBy(...(sort ? toDrizzleOrderBy(sort, (f) => usersTable[f]) : []));
return c.json(users);
});Non-goals
filterkit focuses on filtering and query translation. These features are explicitly out of scope:
- UI components — Use with React/Vue/Angular components
- Form handling — Integrate with form libraries separately
- Authentication/authorization — Handle auth logic separately
- Data validation — Use validation libraries for input validation
- Pagination — Compose with pagination libraries
Related Packages
Caching & Concurrency:
- @azghr/singlet — Deduplicate concurrent async calls
- staleness — Stale-while-revalidate caching for async functions
Text Processing:
- @azghr/shorn — Truncate strings by byte budget without breaking graphemes
- seriatim — Sequential processing utilities
HTTP & Network:
- forbear — Read server rate-limit instructions from HTTP responses
- forestall — Delay execution until a condition is met
- obviate — Render operations unnecessary through caching
System & Process:
- quiesce — Ordered, timeboxed graceful shutdown for Node
- sortition — Deterministic percentage rollouts and A/B bucketing
- stanch — Stop flows or operations based on conditions
Utilities:
- expunge — Remove or exclude items from collections
- occlude — Hide or mask data and functionality
- placemark — Geographic location and mapping utilities
- specie — Currency and financial calculations
License
MIT
