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

@qfilter/gridify

v0.0.1

Published

Adapter that translates @qfilter/core query results to and from Gridify filter and orderBy syntax.

Readme

@qfilter/gridify

Adapter that translates @qfilter/core query results to and from Gridify filter and orderBy syntax.

Build a filter with qfilter on the client, send it to a .NET API that uses Gridify, and read Gridify query strings back into qfilter.

Install

@qfilter/core is a peer dependency:

pnpm add @qfilter/gridify @qfilter/core
npm install @qfilter/gridify @qfilter/core

Quick start

import { Op, Query } from "@qfilter/core";
import { fromGridify, toGridify } from "@qfilter/gridify";

type User = {
  name: string;
  age: number;
  active: boolean;
  deletedAt: Date | null;
  address: { city: string };
};

const result = new Query<User>()
  .where("age", Op.GREATER_THAN_OR_EQUAL, 18)
  .and()
  .where("address.city", Op.EQUAL, "Madrid")
  .orderBy("name", "desc")
  .build();

toGridify(result);
// { filter: "age>=18,address.city=Madrid", orderBy: "name desc" }

fromGridify<User>({ filter: "age>=18,address.city=Madrid", orderBy: "name desc" });
// the same QueryResult as `result`

Send the strings as query parameters:

const { filter, orderBy } = toGridify(result);
const params = new URLSearchParams({ filter, ...(orderBy && { orderBy }) });

await fetch(`/api/users?${params}`);

API

toGridify(result)

function toGridify<T>(result: QueryResult<T>): GridifyQuery;

Writes a QueryResult from Query.build() as Gridify strings. orderBy is left out when the result has no order. An empty query gives { filter: "" }.

fromGridify(query, options?)

function fromGridify<T>(query: GridifyQuery, options?: FromGridifyOptions<T>): QueryResult<T>;

Parses Gridify strings into a QueryResult. The filter is rebuilt with a Query<T>, so it goes through core's validation. An empty or missing orderBy gives order: null.

GridifyQuery

type GridifyQuery = {
  filter: string;
  orderBy?: string;
};

FromGridifyOptions

type FromGridifyOptions<T> = {
  types?: Partial<Record<OrderBy<T>["property"], ValueType>>; // any dot-path of T
};

See Value types.

Operators

Every qfilter operator has a Gridify spelling:

| Op | qfilter value | Gridify | | -------------------------- | ----------------- | ------- | | Op.EQUAL | "eq" | = | | Op.NOT_EQUAL | "neq" | != | | Op.LESS_THAN | "lt" | < | | Op.GREATER_THAN | "gt" | > | | Op.LESS_THAN_OR_EQUAL | "lte" | <= | | Op.GREATER_THAN_OR_EQUAL | "gte" | >= | | Op.CONTAINS | "contains" | =* | | Op.NOT_CONTAINS | "notContains" | !* | | Op.STARTS_WITH | "startsWith" | ^ | | Op.NOT_STARTS_WITH | "notStartsWith" | !^ | | Op.ENDS_WITH | "endsWith" | $ | | Op.NOT_ENDS_WITH | "notEndsWith" | !$ |

Gridify's spellings differ from core's string tokens: core writes contains as *= and starts with as ^=, Gridify as =* and ^. Pass core's tokens to Query.where and Gridify's to fromGridify.

fromGridify accepts spaces around operators, so age > 18 and age>18 both parse. toGridify writes them without spaces.

Connectors and groups

| qfilter | Gridify | | ------------ | ------- | | and() | , | | or() | \| | | group(...) | ( ) |

// name = "Ana" OR (active = true AND (age < 18 OR age > 65))
const result = new Query<User>()
  .where("name", Op.EQUAL, "Ana")
  .or()
  .group((g) => {
    g.where("active", Op.EQUAL, true)
      .and()
      .group((inner) => {
        inner.where("age", Op.LESS_THAN, 18).or().where("age", Op.GREATER_THAN, 65);
      });
  })
  .build();

toGridify(result).filter; // "name=Ana|(active=true,(age<18|age>65))"

Null checks

Gridify reads an empty value as "null or default". A comparison with null is written with no value:

const result = new Query<User>()
  .where("deletedAt", Op.EQUAL, null)
  .or()
  .where("deletedAt", Op.NOT_EQUAL, null)
  .build();

toGridify(result).filter; // "deletedAt=|deletedAt!="

fromGridify reads an empty value as null.

Case-insensitive strings

A condition with caseInsensitive: true gets Gridify's /i suffix:

const result = new Query<User>()
  .where("name", Op.CONTAINS, "jo", { caseInsensitive: true })
  .build();

toGridify(result).filter; // "name=*jo/i"

fromGridify reads a trailing /i back as caseInsensitive: true. On a value that is not a string, such as age=18/i, the /i is dropped.

Escaping

Gridify gives (, ), ,, |, \ and /i a meaning, so toGridify escapes them in values with a backslash, and fromGridify unescapes them:

const result = new Query<User>().where("name", Op.EQUAL, "Smith, (J|K)").build();

toGridify(result).filter; // "name=Smith\, \(J\|K\)"

Value types

Gridify strings carry no types, so fromGridify guesses each value's type from its text:

| Text | Value | Example | | ------------------------------------------------------- | --------- | ------------------------------------ | | Empty | null | deletedAt= | | true or false | boolean | active=true | | A decimal number | number | age>-1.5 | | An ISO 8601 date-time with a timezone (Z or ±hh:mm) | Date | createdAt>2024-01-02T03:04:05.000Z | | Anything else | string | name=Ana |

A date without a time or timezone, such as 2024-01-02, stays a string. toGridify writes dates in the full ISO form, so they read back as Date.

When a guess would be wrong, for example a zip code 01234 that should stay a string, pass the types in options.types. Properties in the map are parsed as that type, and the rest are still guessed:

import { ValueType } from "@qfilter/core";

fromGridify<User & { zip: string }>(
  { filter: "zip=01234,createdAt>2024-01-02" },
  { types: { zip: ValueType.STRING, createdAt: ValueType.DATE } },
);
// zip = "01234" (string), createdAt > new Date("2024-01-02")

A value that does not fit its mapped type throws, for example age=abc with age: ValueType.NUMBER.

Ordering

toGridify(new Query<User>().orderBy("address.city", "desc").build());
// { filter: "", orderBy: "address.city desc" }

fromGridify<User>({ filter: "", orderBy: "name" }).order;
// { property: "name", direction: "asc" }

The direction defaults to asc when it is left out.

Errors

A badly ordered filter is rebuilt with Query, so it throws core's ValidationError with the same codes:

| Filter | Code | | -------------------- | ------------------------------- | | ,age=1 | INVALID_START | | age=1\| | INVALID_END | | age=1,\|age=2 | CONSECUTIVE_LOGICAL_OPERATORS | | age=1,() | EMPTY_GROUP | | age=1(age=2) | MISSING_CONNECTOR | | (age=1 or age=1) | UNBALANCED_GROUP |

import { ValidationError } from "@qfilter/core";

try {
  fromGridify<User>({ filter: "(age=1" });
} catch (error) {
  if (error instanceof ValidationError) {
    error.code; // "UNBALANCED_GROUP"
  }
}

Syntax that qfilter cannot represent throws a plain Error:

| Input | Why | | ----------------------------------------- | ------------------------------------------ | | age, age #in 1 | Missing or unknown operator | | age=*5 | The operator does not fit the value's type | | name=x\ (a trailing backslash) | Dangling escape | | orderBy: "name asc, age desc" | qfilter orders by one property | | orderBy: "deletedAt?" or "deletedAt!" | Nullable order markers are not supported | | orderBy: "name up" | The direction must be asc or desc |

Round trip

For any QueryResult, fromGridify(toGridify(result)) gives back the same result, including escaped values, /i, null, dates and nested groups. The other way is not guaranteed: Gridify text is normalized (spaces removed), and values without a mapped type are guessed.

Development

vp install   # install dependencies and the git hooks
vp test      # run the tests
vp check     # format, lint and type check
vp pack      # build to dist/

A pre-push hook runs vp test, so a push with failing tests is stopped. Skip it once with git push --no-verify.

Releasing

Releases use standard-version and Conventional Commits:

vp run release            # bump the version, update CHANGELOG.md, commit and tag
git push --follow-tags
pnpm publish              # builds through prepublishOnly, then publishes

License

MIT. Report issues at github.com/jhael07/qf-gridify/issues.