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

@generazioneai/genquery

v0.17.0

Published

ORM-agnostic JSON query language with a pluggable Prisma adapter

Readme

genquery

ORM-agnostic JSON query language with a pluggable Prisma adapter.

Frontends send a GenQueryInput object. The backend validates it against a Schema and an adapter translates the result into ORM operations. The shipped adapter targets Prisma.

Install

# npm pubblico (registry.npmjs.org), con provenienza
npm install @generazioneai/genquery
# Prisma client (peer; you almost certainly already have it)
npm install @prisma/client

Quick start (Prisma)

import { Prisma, PrismaClient } from "@prisma/client";
import { createPrismaEngine } from "@generazioneai/genquery/prisma";

const prisma = new PrismaClient();

// 1. One line to set up the schema (from the DMMF), adapter, and engine
const engine = createPrismaEngine(Prisma.dmmf.datamodel);

// 2. Run a query from a request body against a model delegate
const { data, current, total } = await engine.run(
  {
    searchBy: { firstName: "mario" },
    orderBy:  "createdAt",
    pagination: { page: 0, perPage: 20 },
  },
  "User",        // root entity name — Prisma delegates don't expose it
  prisma.user,   // target model delegate; entity type flows from this
);
// data:    User[]
// current: data.length   (omitted if pagination.showNumber === false)
// total:   match count   (via a parallel count; omitted if pagination.showTotal === false)

engine.run is async and returns { data, current?, total? }. current and total are populated according to pagination.showNumber / pagination.showTotal (both default to true); setting showTotal: false skips the extra count.

createPrismaEngine is a thin wrapper around schemaFromPrisma → new PrismaAdapter → new GenQueryEngine. The root entity ("User") must be passed explicitly — Prisma delegates don't expose their model name on a stable public API. The TS entity type is read from the delegate's findMany return.

If you only want the Prisma args object (custom chaining, transactions, your own findMany call), parse separately and call runParsed, which returns the args without executing:

const parsed = engine.parse(input, "User");
const args   = engine.runParsed(parsed, prisma.user);   // { where, orderBy, skip, take, include, ... }
const rows   = await prisma.user.findMany(args);

Need fine-grained control? You can still build it manually:

const schema  = schemaFromPrisma(Prisma.dmmf.datamodel, { overrides: { User: { meta: "string" } } });
const adapter = new PrismaAdapter(schema, { parallelCount: false });
const engine  = new GenQueryEngine({ adapter });   // schema is read from the adapter

Or pass the same options to createPrismaEngine:

const engine = createPrismaEngine(Prisma.dmmf.datamodel, {
  schema:  { overrides: { User: { meta: "string" } } },
  adapter: { parallelCount: false },
});

Core concepts

Schema

The Schema describes your data model independently of any ORM. The parser uses it to reject unknown fields; the adapter uses it to know which fields are dates vs strings vs relations.

With Prisma, derive it from the generated DMMF — no duplication:

import { schemaFromPrisma } from "@generazioneai/genquery/prisma";

const schema = schemaFromPrisma(Prisma.dmmf.datamodel);
// optional: restrict to specific models
const schema = schemaFromPrisma(Prisma.dmmf.datamodel, { models: ["User", "Post"] });
// optional: override fields with non-standard column types
const schema = schemaFromPrisma(Prisma.dmmf.datamodel, {
  overrides: { User: { preferences: "string" } },
});

Or declare one explicitly (no ORM, or fine-grained control):

const schema: Schema = {
  entities: {
    EntityName: {
      name: "EntityName",
      primaryKey: "id",           // optional, defaults to "id"
      fields: {
        fieldName: { type: "string" | "number" | "boolean" | "date" },
      },
      relations: {
        relationName: { target: "OtherEntity", kind: "one" | "many" },
      },
    },
  },
};

Query input

A GenQueryInput is a plain JSON object with five optional top-level keys.

The entity type is inferred automatically from the target argument when it has a recognizable shape (a Prisma model delegate exposes findMany(args?): Promise<T[]>). No explicit generic is required — autocomplete and value-shape checking flow from the delegate's entity type:

// prisma.user is Prisma.UserDelegate — entity type flows into the call below
await engine.run(
  {
    searchBy: {
      firstName: "mario",                              // OK
      age: { operation: ">=", value: 18 },             // OK — number → comparison
      birthDate: { after: "2000-01-01T00:00:00Z" },    // OK — date → range
      posts: { title: "typescript" },                  // OK — relation → recursive
      // age: "x",       // ✗ type error: number field can't take a string
      // nope: "x",      // ✗ type error: 'nope' isn't a field on User
    },
    orderBy: { field: "lastName", order: "asc" },     // ✓ field constrained to User keys
    select:  { firstName: true },                      // ✓ only primitive fields
    include: { posts: "all" },                         // ✓ only relations
  },
  "User",
  prisma.user,
);

The inference distinguishes fields (primitives → searchable / selectable) from relations (objects/arrays → includable / recursive search), and picks the right value shape per field type (string/number/boolean/Date/enum).

If your target type doesn't expose the entity, the input falls back to a loose form where any key/value is accepted — the runtime parser still validates everything against the schema.

Top-level keys:

| Key | Default | Purpose | |-----|---------|---------| | searchBy | — | Filter conditions (AND + OR) | | orderBy | — | Sort field and direction | | select | "all" | Which fields to return (policy-denied fields are stripped from "all") | | include | "none" | Which relations to join | | pagination | "all" | Page / limit |

Full query language reference: docs/query-reference.md

Engine

GenQueryEngine is the public entry point. It reads the schema from the adapter — there is one source of truth.

const engine = new GenQueryEngine({ adapter });   // schema comes from the adapter

// parse + apply + execute → Promise<{ data, current?, total? }>
await engine.run(input, rootEntity, target);

// parse only (requires explicit rootEntity)
const parsed = engine.parse(input, rootEntity);

// apply a previously parsed query without executing (returns the raw args object)
engine.runParsed(parsed, target);

Policy (allowlist)

By default every field/relation that exists in the schema is queryable. A policy restricts that surface per entity, so a frontend can read a field but not filter/sort on it, not include an expensive relation, or not over-fetch a huge page. Enforced by the parser (throws QueryValidationError); maxPerPage is clamped, not rejected.

As of 0.14.0, fields denied for select are also stripped from the default selection: a query without an explicit select (i.e. select: "all") gets a Prisma omit of the denied fields, at the root and inside included relations — denied/secret fields never come back, even unasked, while every other column (including ones the genquery schema doesn't model, like Json or scalar arrays) keeps Prisma's default selection.

Low-level: optional flags on the schema (filterable / sortable / selectable on fields, includable / filterable on relations, maxPerPage on entities — all default to allowed).

Ergonomic: declare allowlists by name and project them with applyPolicy (ORM-agnostic) or the policy option of schemaFromPrisma.

import { applyPolicy } from "@generazioneai/genquery";

const restricted = applyPolicy(schema, {
  User: {
    filterable: ["name", "email"],   // only these allowed in searchBy
    sortable:   ["createdAt"],        // only this in orderBy
    includable: ["posts"],            // only this relation in include
    maxPerPage: 100,                  // perPage > 100 is clamped to 100
  },
});
// per axis: array omitted = unrestricted · present = only-listed · empty = none
// Prisma: build + restrict in one call
const schema = schemaFromPrisma(Prisma.dmmf.datamodel, {
  policy: { User: { filterable: ["name"], includable: ["posts"], maxPerPage: 100 } },
});

Errors

Parse failures throw QueryValidationError with a path field pointing to the offending location in the input (e.g. "searchBy.posts.title.value").

import { QueryValidationError } from "@generazioneai/genquery";

try {
  await engine.run(input, "User", prisma.user);
} catch (e) {
  if (e instanceof QueryValidationError) {
    console.error(e.path, e.message);
  }
}

Examples

See docs/examples.md for full worked examples covering:

  • String search modes (splitword, exact — nativeregex is rejected by the Prisma adapter)
  • Date ranges
  • Numeric comparisons
  • OR conditions
  • Relation filtering and inclusion
  • Pagination and sorting

Limits and secrets (0.15, 0.15.1, 0.16)

Every limit has two modes, GENQUERY_LIMIT_MODE=log|enforce (default log): log measures and logs (wouldCap / wouldReject) and changes nothing; enforce cuts or rejects. Secrets are the exception: they are closed in both modes.

| Setting | Default | Effect in enforce | |---|---|---| | limits.maxRows / GENQUERY_MAX_ROWS | 2000 | pagination: "all" (and no pagination) gets take: cap+1; the result is cut to cap and carries truncated: true | | entityMaxRows / EntityDefinition.maxRows / EntityPolicy.maxRows | — | per-entity cap; null = explicit "no cap" exception | | limits.maxDepth / GENQUERY_MAX_DEPTH | 3 | include or nested relation filter deeper than N hops: QueryValidationError | | limits.maxFilterDepth / GENQUERY_MAX_FILTER_DEPTH (0.16) | maxDepth | depth of nested relation filters (searchBy) only; violations are filterDepth events (wouldReject in log). Include depth stays on maxDepth | | limits.toManyAfterToOne | true | no to-many include after a to-one (rule ported from SKILLHR-BE's include-select guard) | | limits.sharedRoots | [] | root entities with no to-many include at all (global/shared rows) | | limits.includablePaths / EntityPolicy.includablePaths / buildGenQueryPolicy({ includablePaths }) (0.15.1) | — | per root, the nested include paths allowed (see below) | | limits.onEvent | console.warn JSON | sink for the events (rowCap, depth, filterDepth, toManyAfterToOne, includablePath, baseIncludeDropped, includeMergeConflict) |

new PrismaAdapter(schema, { limits: { mode: "enforce", maxRows: 2000, sharedRoots: ["AbsenceType"] } });

Nested includes: includablePaths (0.15.1)

The structural toManyAfterToOne rule rejects legitimate includes such as JuridicalIndividual → individual (to-one) → individualDatas (to-many). A root can instead declare the nested paths it allows; nested includes are then allowed only if declared:

const includablePaths = { JuridicalIndividual: ["individual.individualDatas", "juridical.juridicalDatas"] };
new PrismaAdapter(schema, { limits: { mode: "enforce", includablePaths } });
// or in the policy: buildGenQueryPolicy({ datamodel, includablePaths }) / EntityPolicy.includablePaths
  • a first-level include (one hop from the root) stays governed by the policy (includable), as before;
  • a nested path (rel.rel[.rel…], relative to the root) is allowed if declared or a prefix of a declared path;
  • for a root that declares includablePaths, any other nested path is a violation (includablePath event: wouldReject in log, QueryValidationError in enforce), whatever its cardinality; the structural rule no longer applies to that root;
  • roots that declare nothing keep the structural rule (compatibility);
  • maxDepth always applies, and sharedRoots still forbids any to-many, declared or not;
  • the adapter option wins over the schema/policy per root; invalid paths (unknown root or relation) make the PrismaAdapter constructor throw SchemaError. validateIncludablePaths(schema, paths) returns the problems, for a backend test.

Server-side includes: includeMerge (0.16)

baseArgs.include (trusted, set by the service) used to apply only when the client sent neither include nor select; otherwise it was silently dropped. PrismaAdapterOptions.includeMerge / GENQUERY_INCLUDE_MERGE:

| Value | Behaviour | |---|---| | log (default) | as before 0.16, plus a baseIncludeDropped event (entity, key, reason: clientInclude\|clientSelect) per dropped service key | | replace | as before 0.16, no event | | merge | recursive merge, the service is never widened |

merge rules: (1) where the service has a select, or arguments (where, orderBy, take, …) on a key, the service wins: a client true never widens it nor removes its arguments (a client nested include is added under the service arguments; a service omit is kept and unioned); (2) client-only keys are added (already validated by parser and limits); (3) a client select at the root drops the service include as before (event), and select + include are never emitted at the same level (on a key where they would clash the service include wins, event includeMergeConflict). mergeIncludes(service, client) is exported and pure.

genquery-check (0.16)

CLI that replays the REAL parseQuery + checkQueryShape (and your limits) on the include / searchBy literals found in source code, so a limit change or a schema change breaks CI instead of production.

# 1. manifest: entities, relations with cardinality, policy flags, limits
genquery-check manifest --schema prisma/schema.prisma [--limits genquery.limits.js] --out genquery.manifest.json
# 2. TypeScript: literals passed to engine.run/parse, .send(...), rpcCall(...)
genquery-check --lang ts   --manifest genquery.manifest.json [--routes routes.json] [--fail-on error|warn|never] src/
# 3. Dart: `include: {…}`, `searchBy:`/`searchParams:` maps and table filter keys `a:b:c`
genquery-check --lang dart --manifest genquery.manifest.json --routes routes.json lib/
  • --schema: schema.prisma or a DMMF JSON ({datamodel} / {models, enums}). --limits: JSON or JS module with maxDepth, maxFilterDepth, toManyAfterToOne, sharedRoots, includablePaths, policy (EntityPolicy per entity), extraSecretFields, allowSecretFields. --manifest may be a file or an http(s) URL.
  • --routes: JSON { "<route or cmd>": "<RootEntity>" } (values may be { "root": … }, the shape the gateway will export from OpenAPI x-genquery-root). Needed to know the root of a Dart literal (linked through the nearest apiUrl:) and of .send({cmd}, …) / rpcCall payloads; engine.run(q, "Root", …) carries its own root.
  • Table keys a:b:c are checked as the search {a:{b:{c:…}}} against the roots of --routes (every entity when absent); they pass if any candidate root accepts them.
  • Output file:line: error|warn: message. Errors: unknown field or relation, not includable, depth, to-many rule, undeclared includablePaths. Warnings: dynamic parts (spread, computed keys, expressions in an include), unresolved root. Dynamic values inside searchBy are not warnings (the value type is irrelevant to the shape).
  • --fail-on error|warn|never (default error): exit code 1 on errors only / also on warnings / never (annotations only). Usage error: 2.
  • The TypeScript mode needs the typescript package (optional peer dependency). The Dart extractor is a tolerant scanner written in TypeScript (no Dart analyzer): it reads map literals, not Dart semantics.

baseArgs (trusted, server-side) are not checked. DEFAULT_SECRET_FIELDS + DEFAULT_SECRET_PATTERNS (otp*, secret*, *Encrypted, *Enc) are never selectable, filterable or sortable and applyPolicy cannot reopen them; schemaFromPrisma takes extraSecretFields and a per-model allowSecretFields exception.

Client for browsers and Next.js: @generazioneai/genquery/client (0.17)

For whoever calls a genquery endpoint. Zero dependencies, no Node APIs, ESM + CJS + types. Nothing from the engine is pulled in. The wire encoding is the one of the Dart client of skillera_ui (GenQuery.build + ApiManager.asQueryParams), which is what createGenQueryDecorator({ from: 'query' }) of genquery-nestjs reads back: pagination raw for all/first and JSON otherwise, searchBy, orderBy ({field, direction, order}), include, select as JSON, empty ones omitted.

import { gq, toQueryParam, type GenQueryInput } from "@generazioneai/genquery/client";

const q = gq<Presence>()
  .where({ structureId: "s1" })
  .or({ attendanceStatus: "PENDING" }, { attendanceStatus: "APPROVED" })   // OR in AND with the rest
  .include({ structure: "all", individual: { individualDatas: true } })
  .orderBy("createdAt", "desc")
  .page(1, 20);                                  // 1-based; on the wire it is {"page":0,"perPage":20}

q.toQueryParam();      // { pagination: '{"page":0,"perPage":20}', searchBy: '{...}', orderBy: '...', include: '...' }
q.toQueryString();     // "pagination=%7B...&searchBy=..." (encodeURIComponent, like Dart)
q.toSearchParams();    // URLSearchParams
q.url("/api/hr/presences");   // "/api/hr/presences?pagination=..."

The builder is immutable (every method returns a new one: safe in React state and as a cache key). build() gives the plain GenQueryInput; toQueryParam(input) encodes one you already have. gq<T>() types where/include/select against an entity type, gq() stays loose.

Next.js

// app/presenze/page.tsx (Server Component)
import { gq } from "@generazioneai/genquery/client";

export default async function Page({ searchParams }: { searchParams: Promise<{ q?: string; p?: string }> }) {
  const { q, p } = await searchParams;
  const query = gq<Presence>()
    .filters({ "individual:individualDatas:firstName|individual:individualDatas:lastName": q })   // empty → ignored
    .orderBy("checkInAt", "desc")
    .page(Number(p) || 1, 25);
  const res = await fetch(query.url(`${process.env.API_URL}/hr/presences`), { cache: "no-store" });
  return <List data={await res.json()} />;
}

In a Client Component with SWR/React Query the key is query.toQueryString(); with fetch use query.url(base).

Table filters (filters, searchByFromFilters)

The flat keys of the skillera_ui tables become the nested searchBy: 'a:b:c' nests, 'x|y' is an OR across columns ('a:x|a:y' extracts the common prefix), values are wrapped by key (text → {value, contained: true}, id/…Id… as-is, …Ids and arrays → {in}, fullName without spaces; null and "" are skipped). buildQueryParams is the literal port of GenQuery.build/buildFromRaw/findOne (1-based page, pivot, orderByParams with {columnId, mode}).

Known divergence kept on purpose: a Dart DateTimeRange encodes as {gte, lte}, which the engine rejects for dates. searchByFromFilters reproduces it ({ start, end } → {gte, lte}) so the two clients stay byte-identical; in new code filter dates with { after, before } through where().

Equivalence with Dart

src/tests/fixtures/dart-equivalence.json holds ~35 cases (taken from skillera_hr and skillera_ops) with the output produced by the real Dart code; the test compares string by string, then feeds the result to the real parser. To regenerate after a change to skillera_ui: scripts/dart-equivalence/run.sh <skillera_ui dir> [dart]. (JSON numbers like 1.0 print as 1 in JS and 1.0 in Dart: avoid them in filters.)

Architecture

Three layers, strictly separated:

Wire types (types.ts)
  └─ Parser validates + normalizes → Parsed types (parsed.ts)
       └─ Adapter consumes ParsedQuery + Schema → ORM output

Adapters never see wire types. New adapters implement Adapter<TTarget, TResult> (re-exported from the package root).

See docs/custom-adapter.md for instructions.

Documentation

| File | Contents | |------|----------| | docs/getting-started.md | Installation, setup, first query | | docs/query-reference.md | Full query language reference | | docs/prisma-adapter.md | Prisma adapter options and internals | | docs/custom-adapter.md | Building a custom adapter | | docs/examples.md | End-to-end examples | | spec.md | Source-of-truth wire format spec |

License

BSD 3-Clause