npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@queryjs/drizzle

v0.2.0

Published

Drizzle adapter for QueryJS: translates validated ResourceQuery objects into Drizzle SQL where and orderBy fragments plus skip and take.

Readme

@queryjs/drizzle

npm version CI License: MIT TypeScript

Drizzle adapter for QueryJS: translates validated ResourceQuery objects into Drizzle SQL fragments for where and orderBy, plus skip/take integers.

The adapter builds parameterized Drizzle SQL chunks — pass them straight to .where() / .orderBy(). A debug-only toDrizzleSQL() helper flattens a fragment back to text for tests and logs.

Why this adapter?

  • No stringly-typed conditions. Filters, relation filters, and search compile into Drizzle SQL values using parameter binding, so nothing is interpolated into SQL text.
  • Portable case-insensitive matching. Substring operators use LOWER(column) LIKE LOWER(pattern) ESCAPE '\' — no database-specific collations, works on SQLite, PostgreSQL, and MySQL alike.
  • Literal LIKE input. User-supplied %, _, and \ are escaped, and SQLite (unlike PostgreSQL/MySQL) has no implicit backslash escaping — the explicit ESCAPE '\' makes it safe everywhere.
  • Relation filters with aliases. ?filter[member][accountNo]=AC-1 compiles to member.accountNo = 'AC-1', so a relation filter works when you have joined that relation under an alias matching the path.
  • Identity with the other adapters. The same query produces semantically identical results across the Prisma, Drizzle, and TypeORM adapters (parity tests in tests/adapters/operator-parity.spec.ts).

Installation

bun add @queryjs/core @queryjs/drizzle drizzle-orm
# or
npm install @queryjs/core @queryjs/drizzle drizzle-orm
# or
pnpm add @queryjs/core @queryjs/drizzle drizzle-orm
# or
yarn add @queryjs/core @queryjs/drizzle drizzle-orm

@queryjs/core and drizzle-orm are peer dependencies (the adapter imports Drizzle's and, or, sql, and the SQL type). Install any driver you need on top (better-sqlite3, pg, mysql2, …).

Quick Start

Request: GET /users?filter[status]=ACTIVE&filter[age][gte]=18&sort=-createdAt&page=2&limit=25

import { defineQuery, q } from '@queryjs/core';
import { drizzleQueryAdapter } from '@queryjs/drizzle';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import { users } from './schema';

const db = drizzle(new Database('app.db'));

const usersQuery = defineQuery({
  fields: {
    status: q.enum(['ACTIVE', 'INACTIVE']).sortable().searchable(),
    age: q.number().sortable().min(0).max(150),
    createdAt: q.date().sortable(),
  },
});

const query = usersQuery.parse(req.query);
const { where, orderBy, skip, take } = drizzleQueryAdapter.map(query);

const rows = await db
  .select()
  .from(users)
  .where(where)               // SQL fragment | undefined
  .orderBy(...(orderBy ?? []))// spread SQL[]
  .limit(take)
  .offset(skip);

// const { where, orderBy, skip, take } →
// {
//   where: SQL fragment — `users.status = 'ACTIVE' AND users.age >= 18`,
//   orderBy: [ SQL fragment — `users.createdAt DESC` ],
//   skip: 25,
//   take: 25,
// }

Pass undefined where clauses freely — Drizzle omits them. For an empty result set ordering, there's no orderBy (spread of []).

Full Fastify Example

import Fastify from 'fastify';
import { defineQuery, QueryJSError, q } from '@queryjs/core';
import { drizzleQueryAdapter } from '@queryjs/drizzle';
import { eq } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import { orgs, users } from './schema';

const db = drizzle(new Database('app.db'));
const app = Fastify();

const usersQuery = defineQuery({
  fields: {
    name: q.string().sortable().searchable().max(100),
    email: q.string().sortable().searchable().email(),
    age: q.number().sortable().min(0).max(150).integer(),
    role: q.enum(['admin', 'editor', 'viewer']).searchable(),
  },
  relations: {
    org: defineRelation({ fields: { name: q.string().sortable().searchable() } }),
  },
});

app.get('/users', async (req, reply) => {
  try {
    const query = usersQuery.parse(req.query as Record<string, unknown>);
    const { where, orderBy, skip, take } = drizzleQueryAdapter.map(query);
    const data = await db.select().from(users).where(where).orderBy(...(orderBy ?? [])).limit(take).offset(skip);
    reply.send({ data, meta: { page: query.pagination.page, limit: query.pagination.limit } });
  } catch (error) {
    if (error instanceof QueryJSError) {
      const { message, code, field, operator, path, details } = error;
      return reply.status(error.statusCode).send({ error: { message, code, field, operator, path, details } });
    }
    throw error;
  }
});

await app.listen({ port: 3000 });

Relation filters (?filter[org][name]=Acme) reference org.name — join the relation under a matching alias in your query:

// Alias the join as `org` so the generated `org.name` references it.
const data = await db
  .select()
  .from(users)
  .leftJoin(org, eq(users.orgId, org.id))
  .where(where)
  .orderBy(...(orderBy ?? []))
  .limit(take)
  .offset(skip);

API Reference

drizzleQueryAdapter.map(query)

import { drizzleQueryAdapter, toDrizzleSQL } from '@queryjs/drizzle';

const mapped: {
  where: SQL | undefined;
  orderBy: SQL[] | undefined;
  skip: number;
  take: number;
} = drizzleQueryAdapter.map(query);

where and each orderBy element are Drizzle SQL fragments (import type { SQL } from 'drizzle-orm'). All values are bound parameters.

Operator mapping

The wire ids come from @queryjs/core (FilterOperator):

| Builder method | Wire id | Example URL | | --- | --- | --- | | q.op.equal() | eq | ?filter[status][eq]=ACTIVE | | q.op.notEqual() | neq | ?filter[status][neq]=ACTIVE | | q.op.greaterThan() | gt | ?filter[age][gt]=18 | | q.op.greaterThanOrEqual() | gte | ?filter[age][gte]=18 | | q.op.lessThan() | lt | ?filter[age][lt]=65 | | q.op.lessThanOrEqual() | lte | ?filter[age][lte]=65 | | q.op.in() | in | ?filter[status][in]=ACTIVE,INACTIVE | | q.op.notIn() | notIn | ?filter[status][notIn]=ACTIVE,INACTIVE | | q.op.contains() | contains | ?filter[name][contains]=abe | | q.op.startsWith() | startsWith | ?filter[name][startsWith]=abe | | q.op.endsWith() | endsWith | ?filter[name][endsWith]=beke | | q.op.isNull() | isNull | ?filter[email][isNull]=true | | q.op.isNotNull() | isNotNull | ?filter[email][isNotNull]=true |

SQL translation:

| Wire id | Drizzle SQL | | --- | --- | | eq | col = ? · col IS NULL for a null input | | neq | col != ? · col IS NOT NULL for a null input | | in | col IN (?, ?, …) · 1=0 for an empty list | | notIn | col NOT IN (?, ?, …) · 1=1 for an empty list | | gt / gte / lt / lte | col > ? / col >= ? / col < ? / col <= ? | | contains | LOWER(col) LIKE LOWER(? pattern) ESCAPE '\' (see note) | | startsWith | LOWER(col) LIKE ? ESCAPE '\' (see note) | | endsWith | LOWER(col) LIKE ? ESCAPE '\' (see note) | | isNull / isNotNull | col IS NULL / col IS NOT NULL |

Case sensitivity. For the substring family, matching is case-insensitive via LOWER() on both sides unless the field was declared .caseSensitive(), in which case LOWER() is omitted. eq/neq never wrap in LOWER() (they must stay plain for numeric/date columns).

Multiple operators on one field combine with Drizzle's and(...) — ?filter[age][gte]=18&filter[age][lt]=65 → age >= ? AND age < ?.

Search expands to or(...) across every searchable field (or the field: target), AND-joined with scalar filters. Phrase and bare terms compile to contains; field:value* to startsWith.

Relation filters qualify the column with the relation path: ?filter[org][name]=Acme → org.name = ?. In your query, join the related table under an alias matching that path (e.g. leftJoin(org, eq(users.orgId, org.id))).

toDrizzleSQL(fragment)

import { toDrizzleSQL } from '@queryjs/drizzle';

const text = toDrizzleSQL(where); // e.g. "users.status = 'ACTIVE' AND users.age >= 18"

DEBUG ONLY. The returned string is a flattened representation of the fragment, not runnable SQL — parameter values are inlined literally without quoting. Use it for tests, logs, and eyeballing; never execute it and never feed it back into sql.raw().

Error Handling

All parse and validation errors come from @queryjs/core as QueryJSError (see its error reference). The adapter adds a defensive QueryJSError with ErrorCode.UNSUPPORTED_OPERATOR if it ever receives an operator it has no SQL for — which cannot happen for queries produced by parse().

import { QueryJSError } from '@queryjs/core';

app.setErrorHandler((error, req, reply) => {
  if (error instanceof QueryJSError) {
    return reply.status(error.statusCode).send({ error });
  }
  throw error;
});

Security Notes

  • Parameterized everywhere. No user input is interpolated into SQL text; values are bound by Drizzle. The only text built from strings is the column name itself — quoted via quoteIdentifier (safe identifiers pass through, unsafe ones are double-quoted with escaped ").
  • LIKE literal matching. %, _, and \ in user input are escaped via escapeLikeValue, and ESCAPE '\' is always emitted — required for SQLite, harmless on PostgreSQL/MySQL.
  • Relation filter columns. The relation path is also identifier-quoted, so it cannot inject SQL.
  • toDrizzleSQL is not for production. It's a debug representation only (see above).
  • Respect the core security notes, especially maxPage/maxLimit, so offset/limit stay bounded.

Custom Adapters

Don't use Drizzle? The QueryMapperAdapter interface in @queryjs/core/compiler lets you target any data layer. See the custom adapter example in the core README.

Compatibility

  • Runtime: Node.js ≥ 18, Bun.
  • Modules: dual ESM + CJS ("type": "module"). bun run smoke-pack verifies both import and require under Bun and Node. Use with Drizzle's own ESM build (or the CJS fallback) together with this package.
  • Peers: @queryjs/core ^0.1.0 and drizzle-orm ^0.38.3 || ^0.45.3.
  • Drivers: driver-agnostic — the adapter emits SQL, so it works with better-sqlite3, node-postgres, mysql2, and the other Drizzle drivers.
  • Types: the map result is typed with Drizzle's SQL, so where/orderBy type-check against your queries.

Example Application

Run a full Fastify + Drizzle + SQLite server: examples/fastify-drizzle

Links

License

MIT © Yohannes Getachew — see LICENSE.