@chromav/frizzle

中文文档
0.4.6发布于 2026/10/23MITTypeScriptES Module零依赖近 30 天下载 7084

A fun, type-safe query compiler with a parser-combinator grammar, pluggable code generators, and built-in validation. Filter, sort, and query your data.

原始使用文档
文档目录28 节
  1. Install
  2. Quick Start
  3. Filter a query
  4. Filter + sort with schema validation
  5. SQL builder alternative (Drizzle)
  6. Build filters in code, validate once
  7. Raw SQL target
  8. Interceptor pipelines
  9. Query Syntax
  10. Filter operators
  11. Logical operators
  12. Sorting and pagination
  13. Limits
  14. Error Handling
  15. Entry Points
  16. Descriptors
  17. Reference Interceptors
  18. Query interceptors
  19. Write interceptors
  20. SQL Target
  21. Dialect interface
  22. Write operations
  23. Architecture
  24. Documentation
  25. Core
  26. Database targets
  27. Guides
  28. License

frizzle

A type-safe query compiler and service layer toolkit. Parse URL-safe filter strings, compile to database queries (Drizzle ORM or raw SQL), and compose interceptor pipelines for scoping, soft-delete, and audit trails — with zero exceptions thrown.

?filter=status eq 'active' AND priority eq 'high'&sort=createdAt:desc&limit=50

Install

npm install @chromav/frizzle

Drizzle ORM is an optional peer dependency — only needed if you use the Drizzle code generators.

npm install drizzle-orm  # if using @chromav/frizzle/drizzle

The peer range is >=0.30.0 || >=1.0.0-beta.1. The RQB compileFilter needs drizzle-orm 1.0 (beta/rc), because its output uses the v2 relational-query object filter syntax (OR, AND, RAW), which drizzle-orm 0.x doesn't have. compileFilterToSQL and @chromav/frizzle/sql work across the whole range.

Quick Start

Filter a query

import { isSuccess } from "@chromav/frizzle";
import { compileFilter } from "@chromav/frizzle/drizzle";

const result = await compileFilter(
  "status eq 'active' and priority eq 'high'",
  { status: "status", priority: "priority" }
);

if (isSuccess(result)) {
  const tasks = await db.query.tasks.findMany({
    where: result.data,
  });
}

compileFilter targets Drizzle's relational queries (db.query.*) and requires drizzle-orm 1.0 (beta/rc); see Install. To add your own conditions, wrap both in AND — { AND: [{ workspaceId }, result.data] } — rather than spreading result.data, which would let a user filter replace your keys.

Filter + sort with schema validation

import { isSuccess } from "@chromav/frizzle";
import { compileFilter, compileSort } from "@chromav/frizzle/drizzle";
import type { Schema } from "@chromav/frizzle";

const schema: Schema = {
  status: { type: "enum", enumValues: ["active", "pending", "done"] },
  priority: { type: "enum", enumValues: ["low", "medium", "high"] },
  title: { type: "string" },
  createdAt: { type: "date" },
};

const columns = {
  status: "status",
  priority: "priority",
  title: "title",
  createdAt: "createdAt",
};

// Validates field names, operators, and value types before compiling
const filterResult = await compileFilter(
  "status eq 'active' and title contains 'deploy'",
  columns,
  { schema }
);

const sortResult = compileSort("createdAt:desc", columns);

if (isSuccess(filterResult) && isSuccess(sortResult)) {
  const tasks = await db.query.tasks.findMany({
    where: filterResult.data,
    orderBy: sortResult.data,
  });
}

SQL builder alternative (Drizzle)

import { isSuccess } from "@chromav/frizzle";
import { compileFilterToSQL } from "@chromav/frizzle/drizzle";

// Pass Drizzle column objects instead of strings
const result = compileFilterToSQL("status eq 'active'", {
  status: tasks.status,
  priority: tasks.priority,
});

if (isSuccess(result)) {
  const rows = await db.select().from(tasks).where(result.data);
}

Build filters in code, validate once

There are two levels to combine at, each with its own and / or:

You haveCombine withExamples
Filter trees, before compiling: what parseFilter returns, or nodes from the f buildersf.and, f.orAND shorthand params into a user filter, then validateFilter
Where objects, after compiling: compileFilter output, scopes, descriptor filtersandWhere, orWhereAND a compiled user filter with { workspaceId }

Prefer combining trees when you can: the result is validated (limits, schema) as one filter.

import { f, isError, validateFilter } from "@chromav/frizzle";
import { generateDrizzleQuery } from "@chromav/frizzle/drizzle";

// f.and / f.or skip undefined, and group an or inside an and so the tree means what it says
const filter = f.and(
  userFilter,                                        // e.g. from parseFilter(params.filter)
  region ? f.in("region", region.split(",")) : undefined,
);

// Schema checks plus the limits (maxParameters etc.) on the combined tree.
// On an error, reject the request: running the query without the filter
// would also drop the region restriction.
const valid = filter ? validateFilter(filter, schema) : undefined;
if (valid && isError(valid)) {
  return badRequest(valid.error);
}

// One field map filters and sorts: `name` is filtered through a resolver, sorted by its column
const query = generateDrizzleQuery(
  { filter: valid?.data, sort: [{ field: "name", direction: "asc" }] },
  db.select().from(countries).$dynamic(),
  { region: countries.region, name: { column: countries.name, where: nameOrAlias } },
  { db }, // passed to resolvers as their third argument
);

See Schema Validation and Drizzle Integration.

Raw SQL target

For databases Drizzle doesn't cover (e.g., Trino, Spark, Iceberg):

import { compileSelect } from "@chromav/frizzle/sql";
import { sqliteDialect } from "@chromav/frizzle/sql";

const columns = { status: "status", priority: "priority", createdAt: "created_at" };

const { sql, parameters } = compileSelect("tasks", {
  filter: { status: "active", priority: { gte: 3 } },
  sort: { createdAt: "desc" },
  take: 20,
}, columns, sqliteDialect);

// sql: SELECT * FROM "tasks" WHERE "status" = ? AND "priority" >= ? ORDER BY "created_at" DESC LIMIT ?
// parameters: ["active", 3, 20]

Interceptor pipelines

Compose cross-cutting concerns as reusable interceptors:

import {
  scopeQueryInterceptor,
  softDeleteQueryInterceptor,
  scopeWriteInterceptor,
  softDeleteWriteInterceptor,
  injectWriteInterceptor,
} from "@chromav/frizzle";

// Read pipeline: every query is scoped to workspace + excludes deleted
const queryInterceptors = [
  scopeQueryInterceptor({ workspaceId: "ws-123" }),
  softDeleteQueryInterceptor(),
];

// Write pipeline: scope + audit + soft-delete transform
const writeInterceptors = [
  scopeWriteInterceptor({ workspaceId: "ws-123" }),
  injectWriteInterceptor("user-456"),
  softDeleteWriteInterceptor("user-456"),
];

Query Syntax

Filter operators

OperatorCategoryExample
eq, neComparisonstatus eq 'active'
gt, gte, lt, lteComparisonage gte 18
like, ilikeString (substring)name like 'john'
contains, starts_with, ends_withStringtitle contains 'deploy'
in, not_inArraystatus in ['active', 'pending']
betweenRangeage between [18, 65]
is_null, is_not_nullNull checkassignedTo is_null

Symbol aliases work too: =, !=, >, >=, <, <=.

String operators match the value literally: % and _ are ordinary characters, not wildcards (title contains '50%' finds the text 50%).

Logical operators

status eq 'active' and priority eq 'high'
status eq 'active' or status eq 'pending'
(status eq 'active' or status eq 'pending') and priority eq 'high'

AND binds tighter than OR. Parentheses override precedence.

Sorting and pagination

sort=name:asc,createdAt:desc
limit=50
offset=100

Sort direction must be asc or desc; limit must be an integer from 1 to maxLimit (default 1000) and offset a non-negative integer. Anything else is an INVALID_VALUE error.

Limits

Filters are capped at 32 levels of parenthesis nesting (maxDepth), 100 values (maxParameters, matching Cloudflare D1's bound-parameter cap), and 1000 comparisons (maxConditions). Exceeding one returns a LIMIT_EXCEEDED error. Override them per call, e.g. compileFilter(expr, columns, { maxParameters: 50 }). See Limits.

Error Handling

All operations return Result<T> — no exceptions thrown.

import { isSuccess, isError } from "@chromav/frizzle";

const result = await compileFilter("bad ?? syntax", columns);

if (isError(result)) {
  result.error.message;     // "Expected operator, got '?'"
  result.error.position;    // 4
  result.error.code;        // "SYNTAX_ERROR"
  result.error.suggestions; // Possible fixes (e.g. close field names, for UNKNOWN_FIELD)
}

Entry Points

ImportContents
@chromav/frizzleCore: compiler, parser, AST types, result types, schema types, query string parsing, descriptors, interceptors
@chromav/frizzle/drizzleDrizzle ORM: compileFilter, compileFilterToSQL, compileSort, DrizzleCodeGenerator
@chromav/frizzle/sqlRaw SQL: compileSelect, compileFilter, compileFilterNode, compileInsert, compileUpdate, compileDelete, Dialect, sqliteDialect

The core package has zero dependencies. @chromav/frizzle/drizzle requires drizzle-orm as a peer dependency. @chromav/frizzle/sql has zero dependencies.

Descriptors

Frizzle uses database-agnostic intermediate representations that flow through interceptor pipelines before reaching a database adapter.

QueryDescriptor — describes a read operation:

import { mergeDescriptors } from "@chromav/frizzle";
import type { QueryDescriptor } from "@chromav/frizzle";

// A QueryDescriptor is a plain object; mergeDescriptors combines two
const base: QueryDescriptor = { filter: { workspaceId: "ws-1" } };
const descriptor = mergeDescriptors(base, {
  filter: { status: "active" },
  sort: { createdAt: "desc" },
  take: 20,
  skip: 0,
  select: { id: true, title: true, status: true },
  include: { assignee: true },
});

WriteDescriptor — describes a write operation:

import { createWriteDescriptor, mergeWriteDescriptor } from "@chromav/frizzle";

// Insert
const insert = createWriteDescriptor("insert", { title: "New task", status: "backlog" });

// Update
const update = mergeWriteDescriptor(
  createWriteDescriptor("update", { status: "done" }),
  { targetIds: ["task-1", "task-2"] },
);

// Delete
const remove = mergeWriteDescriptor(
  createWriteDescriptor("delete"),
  { targetIds: ["task-3"] },
);

Reference Interceptors

Reusable implementations for common service layer patterns. All exported from the main @chromav/frizzle entry point.

Query interceptors

InterceptorDescription
scopeQueryInterceptor(fields)Appends tenant/workspace filter to every query
softDeleteQueryInterceptor(config?)Excludes soft-deleted rows (isDeleted: false)
defaultIncludesInterceptor(defaults)Deep-merges default relation includes
extraScopeInterceptor(scopeFn)Adds entity-specific filter conditions from a factory

Write interceptors

InterceptorDescription
scopeWriteInterceptor(fields)Injects scope values into every write
softDeleteWriteInterceptor(userId, config?)Transforms delete → update with isDeleted: true
injectWriteInterceptor(userId, config?)Injects createdById on insert, updatedById on update
extraScopeWriteInterceptor(scopeFn)Injects additional scope fields on insert

All interceptors are generalized (configurable column names, generic scope fields) and use only frizzle's own types — zero external dependencies.

SQL Target

The @chromav/frizzle/sql entry point compiles descriptors to parameterized SQL strings. It supports any SQL database through the Dialect interface.

Dialect interface

import type { Dialect } from "@chromav/frizzle/sql";

const trinoDialect: Dialect = {
  name: "trino",
  quoteIdentifier: (name) => `"${name}"`,
  parameter: (index) => `$${index + 1}`,
  booleanLiteral: (v) => v ? "TRUE" : "FALSE",
  supportsILike: true,
  escapeLikePattern: (p) => p.replace(/[%_\\]/g, "\\$&"),
};

Write operations

import { compileInsert, compileUpdate, compileDelete } from "@chromav/frizzle/sql";
import { createWriteDescriptor, mergeWriteDescriptor } from "@chromav/frizzle";
import { sqliteDialect } from "@chromav/frizzle/sql";

const insert = createWriteDescriptor("insert", { title: "Task", status: "backlog" });
const { sql, parameters } = compileInsert("tasks", insert, sqliteDialect);
// sql: INSERT INTO "tasks" ("title", "status") VALUES (?, ?)

const update = mergeWriteDescriptor(
  createWriteDescriptor("update", { status: "done" }),
  { targetIds: ["t-1"] },
);
const { sql, parameters } = compileUpdate("tasks", update, sqliteDialect);
// sql: UPDATE "tasks" SET "status" = ? WHERE "id" = ?

Architecture

URL query string
    ↓ parseQueryString()
QueryDescriptor (database-agnostic IR)
    ↓ interceptor pipeline (scope, soft-delete, inject, ...)
QueryDescriptor (enriched)
    ↓ adapter
    ├── @chromav/frizzle/drizzle → Drizzle RQB findMany/findFirst
    └── @chromav/frizzle/sql     → parameterized SQL string

Documentation

Core

Database targets

Guides

  • Recipes — Express, Hono, Next.js, React Router examples + filter UI patterns
  • URL Examples — 50+ real-world URL query patterns

License

MIT

版本与发布记录

@chromav/frizzle 共发布 11 个版本,首次发布于 2026/08/25;过去 12 个月发布了 11 个版本。

版本发布日期距今
0.4.6latest2026/10/2317 天前
0.4.52026/10/1228 天前
0.4.42026/10/071 个月前
0.4.32026/10/061 个月前
0.4.22026/10/061 个月前
0.4.12026/10/051 个月前

关于 @chromav/frizzle 的常见问题

@chromav/frizzle 是什么?

A fun, type-safe query compiler with a parser-combinator grammar, pluggable code generators, and built-in validation. Filter, sort, and query your data.

如何安装 @chromav/frizzle?

在项目目录运行 npm install @chromav/frizzle,或使用 pnpm add / yarn add / bun add。

@chromav/frizzle 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@chromav/frizzle 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@chromav/frizzle 可以商用吗?

@chromav/frizzle 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@chromav/frizzle 的下载量有多少?

根据 npm 官方下载统计,@chromav/frizzle 在 2026/10/10 – 2026/11/08 这 30 天内被下载 7,084 次,日均约 236 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@chromav/frizzle 还在维护吗?

最新版本 0.4.6 发布于 2026/10/23(17 天前);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/08/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。