This file provides guidance to agentic agents like Claude Code when working with code in this repository.
npm test— Run the full Jest suite. Test files live next to their source as*.test.js(the/test/directory is ignored as a test path; it only holds fixtures).npm test -- --watch— Re-run on file change during development.npm test -- src/parsers/filter.test.js— Run a single test file.npm test -- -t "parses an object"— Run tests matching a name pattern.npm run lint— Runs bothlint:format(Prettier--check) andlint:quality(ESLint).npm run lint:format:fix/npm run lint:quality:fix— Auto-fix Prettier / ESLint issues.npm run release— Release It!-driven release. Run fromdevelopment; after release, mergedevelopmentintomain(which always reflects the latest release).
CI runs npm run lint then npm test -- --ci --coverage --runInBand against
Node 20.x, 22.x, and 24.x. The package targets node >= 20 and is CommonJS
(require / module.exports).
QueryQL turns a parsed query-string object (e.g. Express's req.query) into the
appropriate calls on a query builder / ORM. The user-facing surface is a single
class, QueryQL, that they subclass into a querier per resource. The library
is structured as a pipeline orchestrated by src/index.js:
query (object) → Schema (whitelist) → Orchestrators → Parsers → Validators → Adapter → builder (mutated)
Key collaborators and the contracts between them:
QueryQL(src/index.js) — Constructor wiresConfig,Schema, three orchestrators (Filterer,Sorter,Pager), anadapterinstance, and avalidator.run()validates, then runs each orchestrator in order, then returns the (now-modified)builder. The user only overridesdefineSchema(); everything else has sensible defaults.Schema(src/schema.js) — In-memory whitelist.filter(name, operator, options)keys filters by${name}[${operator}].sort(name, options)keys by name.page(boolOrOptions)toggles pagination. The schema is the source of truth for what's allowed; anything not whitelisted causes aValidationError.- Orchestrators (
src/orchestrators/) — One per query type (filterer,sorter,pager), all extendingBaseOrchestrator. Each owns aqueryKey('filter'/'sort'/'page'), aschemaslice, anisEnabledcheck, abuildParser(), avalidate(), and arun().apply()is the dispatch point: it callsquerier[querierMethod]if defined (e.g.'filter:id[in]','sort:name'), otherwise falls back toadapter[queryKey]. - Parsers (
src/parsers/) — Convert the raw query input into a normalizedMapkeyed by${queryKey}:${name}[${operator}](filter),${queryKey}:${name}(sort), or${queryKey}:${field}(page). All extendBaseParser.defineValidation(Joi)returns a Joi schema used byParserValidatorto validate the structure of the input. The parser also resolves thefieldoption (defaulting toname) so downstream consumers always have a database column to target. - Adapters (
src/adapters/) — Bridge to specific query builders.KnexAdapteris the default;BaseAdapterdefines the contract. An adapter declaresFILTER_OPERATORSandDEFAULT_FILTER_OPERATOR, plus dispatch methodsfilter:*(catch-all), optionalfilter:<op>overrides,sort, andpage.BaseAdapter.filter()looks up the right operator-specific method or falls back tofilter:*. Adapters also supply per-operator value-type validation viadefineValidation(Joi)(e.g.'filter:in'requires an array). - Validators — Three layers, run on every
run():ParserValidator(src/validators/parser.js) — Validates raw query structure against the parser's Joi schema.AdapterValidator(src/validators/adapter.js) — Validates each filter/sort/page value against the adapter's per-operator Joi schemas (so the operator's value-type contract is enforced before hitting the DB).- Querier validator (
src/validators/querier/, defaultJoiValidator) — User-defined app-specific validation fromdefineValidation(schema)on the querier (e.g.'filter:status[=]': schema.string().valid('open', 'closed')). All three throwValidationError(fromsrc/errors/); a Joi error is converted to a friendly path-prefixed message viasrc/services/joi_validation_error_converter.js.
- Per-name/operator overrides on the querier — Define
'filter:<name>[<operator>]'(builder, { name, field, operator, value })or'sort:<name>'(builder, { name, field, order })to bypass the adapter for a specific entry (e.g. a fuzzy search filter that fans out across multiple columns). The orchestrator'sapply()discovers these by name. - Per-operator overrides on the adapter — Define
'filter:<operator>'to specialize behavior for an operator across all filters; otherwise'filter:*'handles the operator generically.
The querier exposes two distinct customization patterns that are easy to confuse:
get defaultFilter / defaultSort / defaultPage— A fallback value used when the client doesn't sendfilter/sort/pagein the query.get filterDefaults / sortDefaults / pageDefaults— Defaults merged into every parsed entry (e.g.pageDefaults = { size: 10 }to change the default page size). These overlay thestatic DEFAULTSon each parser; client-provided values can still override them, so enforce caps with validation.
validate() and parse() on every orchestrator, plus validate() on every
parser, are wrapped by src/services/cache_function.js, which memoizes
zero-argument methods. They get called multiple times in a single run()
(validate → parse → apply), so this avoids redoing successful validation and
parse work. Thrown errors are not cached.
Config (src/config.js) holds the adapter and validator classes.
Config.defaults = { ... } mutates the global default; the third constructor
arg new Querier(query, builder, { adapter }) overrides per-instance.
All custom errors extend BaseError (src/errors/base.js), which sets name
from the constructor and captures the stack. Throw ValidationError for
user-facing validation failures and NotImplementedError for abstract methods
that subclasses must override (this is how BaseAdapter, BaseParser,
BaseOrchestrator, and BaseValidator enforce their contracts).