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

@pimbay/search-query

v2.1.0

Published

Framework-agnostic, strict search/pagination contracts (page, slice, cursor) with no runtime magic — a foundation for datasource-specific extensions, starting with Drizzle.

Readme

@pimbay/search-query

npm version Node Version License Code Coverage Mutation Score

Framework-agnostic pagination and search-terms parsing for TypeScript/JavaScript.

No query-builder or ORM dependency — you implement one small adapter interface (pageView/pageSlice/pageAfter/count/ids/head/all, whichever you need) against your own data source, and this library gives you back a typed, immutable result object.

Three pagination styles are supported, each with its own result type and adapter interface:

  • Page (paginatePage) — classic page-numbered navigation with totalCount/pageCount. Needs a count query.
  • Slice (paginateSlice) — page-numbered navigation without a count query (hasMore is determined by over-fetching by one row). Cheaper than Page when you don't need totalCount.
  • Cursor (paginateCursor) — forward-only cursor navigation, no totalCount/pageCount/currentPage concepts at all.

Plus parseSearchTerms/parseSearchTermsString — pure string parsing of a search-terms input into equals/notEquals/likes/notLikes buckets (leading negation marker, embedded wildcard marker), with no SQL or column awareness of its own. A datasource-specific adapter package (e.g. @pimbay/search-query-drizzle) turns the parsed result into actual query conditions.

Installation

npm install @pimbay/search-query

Usage

paginatePage

Classic page-numbered navigation. Needs a count query.

import { paginatePage, type PageAdapter, type PageChunk } from '@pimbay/search-query';

class MyAdapter implements PageAdapter<MyRow> {
  async pageView(offset: number, size: number): Promise<PageChunk<MyRow>> {
    const [results, totalCount] = await Promise.all([myQuery(offset, size), myCount()]);

    return { results, totalCount };
  }
}

const page = await paginatePage(new MyAdapter(), /* page */ 1, /* size */ 20);

page.getData(); // MyRow[]
page.getTotalCount();
page.getPageCount();
page.hasNextPage();

paginateSlice/paginateCursor follow the same shape against SliceAdapter/CursorAdapter instead — see docs/usage.md for a Slice example against a real datasource.

If a hard failure is exactly what you want, paginatePageOrThrow/paginateSliceOrThrow do the isOutOfRange() check for you and throw SearchQueryError (outOfRange) instead of returning a result you'd have to check yourself.

An InMemoryArrayAdapter is included — it implements every adapter interface at once, useful for tests and small, already-loaded collections without writing your own adapter.

parseSearchTermsString

Parses a search-terms input string into equals/notEquals/likes/notLikes buckets, with no SQL or column awareness of its own.

import { createSearchTermsConfig, parseSearchTermsString } from '@pimbay/search-query';

const config = createSearchTermsConfig();
const parsed = parseSearchTermsString('dog hors* -cow', config);

parsed.equals; // ['dog']
parsed.likes; // ['hors*']
parsed.notEquals; // ['cow']

createSearchTermsConfig validates its markers (non-empty, unique, the two sets disjoint) and throws SearchQueryError.invalidSearchTermsConfig() on a bad combination.

| Option | Default | Notes | | :---------------------- | :----------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | anywhere | true | Pass-through hint only — this package never reads it; a datasource package decides what it means for its own LIKE pattern. | | minLength | 3 | Measured on the raw term, so a negation marker counts towards it: -ab needs minLength <= 3. | | likeMarkers | ['*'] | Markers a term may embed to become a wildcard match; every one of them maps to the same wildcard. [] disables wildcards. | | ignoreMarkers | ['-', '!'] | Markers a term may start with to become a negation. Longest first wins, so -- beats -. [] disables negation, which is what data with legitimately leading -/! needs. | | ignoredTermsMatchNull | true | Pass-through hint only, like anywhere. Whether a negated term should also keep records whose value is absent: -red means "not red", and under SQL's three-valued logic a bare value != 'red' drops every NULL row silently. false restores that stricter reading. |

Full usage reference (real-datasource example, advanced usage, edge cases): docs/usage.md.

Adapters for a specific datasource

This package only defines the adapter interfaces — it has zero runtime dependencies. Implementing PageAdapter/SliceAdapter/etc. against your own query layer is normal and expected. For Drizzle ORM specifically, see @pimbay/search-query-drizzle, which provides DrizzleSimpleAdapter/DrizzleIdentityAdapter plus a search-terms-to-SQL-condition builder.

Errors

Every failure mode is a named constructor on SearchQueryError.

| Error | Raised by | | --------------------------------------------- | ---------------------------------------------------------------------------------------------- | | SearchQueryError.invalidPageIndex() | paginatePage()/paginateSlice() — page not a positive integer | | SearchQueryError.invalidSize() | paginatePage()/paginateSlice()/paginateCursor() — size not a positive integer | | SearchQueryError.emptyCursor() | paginateCursor() — empty string passed as cursor (use null instead) | | SearchQueryError.outOfRange() | paginatePageOrThrow()/paginateSliceOrThrow() — requested page past the last available page | | SearchQueryError.invalidSearchTermsConfig() | parseSearchTerms()/parseSearchTermsString() — invalid SearchTermsConfig |

Extended notes: docs/errors.md.

Testing

npm run test:22        # docker compose run node22 — npm install + build + unit tests, no coverage
npm run test:24        # docker compose run node24 — npm install + build + unit tests, no coverage
npm run test:26        # docker compose run node26 — npm install + build + unit tests, no coverage
npm run test:all       # test:22 + test:24 + test:26
npm run test:coverage  # vitest run --coverage test/unit
npm run test:mutation  # stryker run — min MSI 100%

npm install is sufficient — no external services to start first.

| Node | Status | | :----- | :----- | | 22 | ✅ | | 24 | ✅ | | 26 | ✅ |

Development Helpers

npm run js:lint       # eslint (check only)
npm run js:lint:fix   # same, applies the fix
npm run js:format     # prettier --check .
npm run js:format:fix # same, applies the fix
npm run js:typecheck  # tsc --noEmit

Architecture & Decisions

Packages in the stack

| Package | Description | | ------------------------------ | ----------------------------------------------------------------------------------------- | | @pimbay/search-query | This package — framework-agnostic pagination and search-terms parsing, no ORM dependency. | | @pimbay/search-query-drizzle | Drizzle ORM adapters and a search-terms-to-SQL condition builder built on this package. |

License

Unlicense — public domain. Part of the PimBay ecosystem.

Bundled third-party dependencies and their licenses: docs/THIRD-PARTY-NOTICES.md.