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

@zeroxsolutions/query

v0.5.1

Published

The query contract for a read - one permit-list declaration and one guard for a collection and a single resource, in the JSON:API spelling (page, sort, filter, fields, include); the guard answers every permitted fieldset, and a permit-list declared as con

Readme

@zeroxsolutions/query

What a client may ask of a read: page, sort and filter a collection, and select and include on either kind - declared once, enforced once, in the JSON:API spelling.

Install

pnpm add @zeroxsolutions/query

Peer dep: zod. Nothing else - no framework, no envelope, no driver.

One declaration, one guard, one spelling

| Holds | Wire | Read by | | --- | --- | --- | | the permit-list, the guard, the parsed shape, the errors | ?page[limit]=20&filter[f0][condition][path]=name&fields[classes]=name&include=subject | a bracket tokenizer, by hand |

One wire spelling, so a call site has nothing to choose. A second encoding of the same GuardedCollectionQuery bought no capability and made every schema file a decision nobody had a rule for; renderCollectionQuery puts the same families on a service-to-service hop.

import { collectionQuerySchema } from '@zeroxsolutions/query';
import type { CollectionQueryPermits } from '@zeroxsolutions/query';

const classQueryPermits = {
  sortable: ['name', 'createdAt'],
  filterable: { name: { operators: ['equals', 'contains'] } },
  filterItem: filterValueSchema, // the executor's vocabulary - this package names no operator
} satisfies CollectionQueryPermits<typeof filterValueSchema, keyof ClassRow & string, FilterOperator>;

createRoute({ request: { query: collectionQuerySchema(classQueryPermits) } });
const { offset, limit, sort, filter } = c.req.valid('query'); // already narrowed

The schema pipes through the guard, so there is no second call to remember and no way to read the query while skipping the permit-list.

collectionLinks answers the same query back as the five links a page carries, each the caller's own query at a different offset - so the filter, sort and fieldset that produced the page ride along, and the page a collection has none of is null rather than a link to itself.

import { collectionLinks } from '@zeroxsolutions/query';

const links = collectionLinks(c.req.path, query, total); // { self, first, prev, next, last }

A read-one has no page to walk and no ordering over one row, so resourceQuerySchema declares the two families that are left - the ones that shape the document rather than the page. CollectionQueryPermits extends ResourceQueryPermits, so a type gaining an attribute cannot become selectable on one endpoint and not the other.

import { resourceQuerySchema } from '@zeroxsolutions/query';

const classQueryPermits = {
  fields: { classes: ['name', 'subject'], subjects: ['name'] }, // columns and relations, as the table names them
  computed: { classes: ['openState'] },
  include: ['subject'],
} as const satisfies TableQuery<typeof schema, 'classesTable'> & ResourceQueryPermits;

createRoute({ request: { query: resourceQuerySchema(classQueryPermits) } });
const { fields, include } = c.req.valid('query'); // PermittedResourceQuery<typeof classQueryPermits>

The guard answers every permitted type: one the request gave no fieldset reads under its whole permitted list, so a finder handed the query loads what the resource publishes and nothing the permit-list left out. computed names the fields a route computes rather than reads - a client may name one in a fieldset, and the guarded query never carries it, so a finder never looks for a column of that name. include is never filled: an unsent include stays unsent.

Declared as const, or checked with satisfies against a type naming each field such as @zeroxsolutions/db's TableQuery, the permit-list types the result: each fieldset holds only the names it permits, so the guarded query passes to @zeroxsolutions/db's finders without a cast. A caller forwarding a query to another service guards it again under that service's permit-list with guardResourceQuery, which narrows the fieldsets the same way.

Why the reading is hand-written

The grammar puts its information in the query-string key - filter[f0][condition][path]=name - which no schema can express, so bracket-params and filter-params tokenize it by hand. That reading stops the moment it has produced a tree.

Everything after the tree is guardCollectionQuery, and it is where every bound lives: a maxDepth enforced inside the reader would be enforced against the shape the reader happened to build, rather than against the permit-list the endpoint declared.

An omitted permit-list permits nothing

A column added to a table later is refused until someone names it, rather than exposed until someone hides it. sortable carries the same weight as filterable: paging a hidden column's order reveals it without ever reading the field. Bind CollectionQueryPermits's Field and Op type parameters and a dropped column turns every collection read naming it red, while a mistyped operator is a compile error rather than a 400 nobody sees until a client sends that filter.

Two kinds of refusal, answered differently

A shape breach - a family that is not readable, a parameter the permit-list never declared - is a ZodError a validation hook renders field by field. A permit-list breach raises QueryNotPermitted (carrying the offending parameter), which travels past safeParse so the transport maps it by type to its own status and code. Neither error carries a status or a wire code of its own, so both work under any error shape - pair them with @zeroxsolutions/response's defineErrorMap where that is what you answer with.

The page window is the one family that does not refuse: a limit past maxLimit is clamped to it, so a client asking for more is served fewer without being told.

API reference

The version-accurate API is the shipped types - every export carries TSDoc, compiled into dist/**/*.d.ts.

Building & testing

pnpm nx build @zeroxsolutions/query
pnpm nx test @zeroxsolutions/query