Skip to content

rmarganti/filter-def

Repository files navigation

filter-def

A TypeScript library for defining type-safe data filters. Define your filters once, use them across different data backends.

Packages

This is a monorepo containing the following packages:

Package Description NPM
@filter-def/core Core types and utilities npm
@filter-def/in-memory In-memory filtering with native array methods npm
@filter-def/drizzle Drizzle ORM adapter for SQL databases npm
@filter-def/bigquery BigQuery adapter for parameterized SQL npm

Features

  • Type-safe filters: Full TypeScript inference for filter inputs and entity fields
  • Multiple backends: Use the same filter patterns for in-memory arrays or SQL databases
  • Composable: Combine filters with AND/OR logic
  • Custom filters: Define complex business logic with custom filter functions
  • Zero lock-in: Each adapter works independently

Quick Start

In-Memory Filtering

import { inMemoryFilter } from "@filter-def/in-memory";

interface User {
    name: string;
    email: string;
    age: number;
}

const userFilter = inMemoryFilter<User>().def({
    name: { kind: "eq" },
    emailContains: { kind: "contains", field: "email" },
    minAge: { kind: "gte", field: "age" },
});

// Create predicates for native array methods
const users: User[] = [
    /* ... */
];
const results = users.filter(
    userFilter({
        name: "John",
        emailContains: "@example.com",
        minAge: 18,
    }),
);

Drizzle ORM

import { drizzleFilter } from "@filter-def/drizzle";
import { pgTable, text, integer, boolean } from "drizzle-orm/pg-core";

const usersTable = pgTable("users", {
    id: integer("id").primaryKey(),
    name: text("name").notNull(),
    email: text("email").notNull(),
    age: integer("age").notNull(),
});

const userFilter = drizzleFilter(usersTable).def({
    name: { kind: "eq" },
    emailContains: { kind: "contains", field: "email" },
    minAge: { kind: "gte", field: "age" },
});

// Create SQL WHERE clauses
const where = userFilter({
    name: "John",
    emailContains: "@example.com",
    minAge: 18,
});

const results = await db.select().from(usersTable).where(where);

BigQuery

import { bigqueryFilter } from "@filter-def/bigquery";
import { BigQuery } from "@google-cloud/bigquery";

const userFilter = bigqueryFilter<User>("myproject.dataset.users").def({
    name: { kind: "eq" },
    emailContains: { kind: "contains", field: "email" },
    minAge: { kind: "gte", field: "age" },
});

// Create parameterized SQL
const where = userFilter({
    name: "John",
    emailContains: "@example.com",
    minAge: 18,
});

const bigquery = new BigQuery();
const [rows] = await bigquery.query({
    query: `SELECT * FROM \`myproject.dataset.users\` WHERE ${where.sql}`,
    params: where.params,
});

Filter Types

All adapters support the same core filter types:

Primitive Filters

Kind Description Input Type
eq Exact equality Field type
neq Not equal Field type
contains String contains substring string
inArray Value is in array Field type[]
gt Greater than Field type
gte Greater than or equal Field type
lt Less than Field type
lte Less than or equal Field type
isNull Check if null/undefined boolean
isNotNull Check if not null/undefined boolean

Boolean Filters

Combine conditions with AND/OR logic:

const filter = inMemoryFilter<User>().def({
    // OR: match any condition
    searchTerm: {
        kind: "or",
        conditions: [
            { kind: "contains", field: "name" },
            { kind: "contains", field: "email" },
        ],
    },

    // AND: match all conditions
    ageRange: {
        kind: "and",
        conditions: [
            { kind: "gte", field: "age" },
            { kind: "lte", field: "age" },
        ],
    },
});

Custom Filters

Each adapter supports custom filters with adapter-specific implementations:

Memory (predicate function):

const filter = inMemoryFilter<User>().def({
    hasRole: (user, role: string) => user.roles.includes(role),
});

Drizzle (SQL expression):

const filter = drizzleFilter(usersTable).def({
    ageDivisibleBy: (divisor: number) =>
        sql`${usersTable.age} % ${divisor} = 0`,
});

BigQuery (parameterized SQL):

const filter = bigqueryFilter<User>("dataset.users").def({
    ageDivisibleBy: (divisor: number) => ({
        sql: "MOD(age, @divisor) = 0",
        params: { divisor },
    }),
});

Field Inference

When the filter name matches a field/column name, the field property is optional:

const filter = inMemoryFilter<User>().def({
    name: { kind: "eq" }, // field: "name" inferred
    email: { kind: "contains" }, // field: "email" inferred
    minAge: { kind: "gte", field: "age" }, // explicit field required
});

Type Utilities

Extract the input type from any filter definition:

import type { InMemoryFilterInput } from "@filter-def/in-memory";
// or
import type { DrizzleFilterInput } from "@filter-def/drizzle";
// or
import type { BigQueryFilterInput } from "@filter-def/bigquery";

const userFilter = inMemoryFilter<User>().def({
    name: { kind: "eq" },
    minAge: { kind: "gte", field: "age" },
});

type UserFilterInput = InMemoryFilterInput<typeof userFilter>;
// { name?: string; minAge?: number }

Installation

# For in-memory filtering
npm install @filter-def/in-memory

# For Drizzle ORM
npm install @filter-def/drizzle drizzle-orm

# For BigQuery
npm install @filter-def/bigquery @google-cloud/bigquery

Documentation

License

MIT

About

Easy, type-safe data filters in Typescript

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •