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

@uniqu/core

v0.1.2

Published

Canonical query format types, tree walker, and utilities for Uniqu

Readme

@uniqu/core

Canonical query format types and transport-agnostic utilities for the Uniqu query representation.

Install

pnpm add @uniqu/core

Query Format

A Uniquery consists of a filter (recursive expression tree) and controls (pagination, projection, sorting):

import type { Uniquery, FilterExpr } from '@uniqu/core'

const query: Uniquery = {
  filter: {
    age: { $gte: 18, $lte: 30 },
    status: { $ne: 'DELETED' },
    role: { $in: ['Admin', 'Editor'] },
  },
  controls: {
    $sort: { createdAt: -1 },
    $limit: 20,
    $select: ['name', 'email'],
  },
}

Filter Expressions

A FilterExpr is either a comparison node (leaf) or a logical node (branch):

// Comparison — one or more field conditions
{ age: { $gte: 18 }, status: 'active' }

// Bare primitive is implicit $eq
{ name: 'John' }  // equivalent to { name: { $eq: 'John' } }

// Logical — $and / $or wrapping child expressions
{ $or: [
  { age: { $gt: 25 } },
  { status: 'VIP' },
]}

// Negation — $not wrapping a single child
{ $not: { status: 'DELETED' } }

Comparison Operators

| Operator | Description | Value Type | |----------|-------------|------------| | $eq | Equal (implicit when bare value) | Primitive | | $ne | Not equal | Primitive | | $gt | Greater than | Primitive | | $gte | Greater than or equal | Primitive | | $lt | Less than | Primitive | | $lte | Less than or equal | Primitive | | $in | In list | Primitive[] | | $nin | Not in list | Primitive[] | | $regex | Regular expression match | RegExp \| string | | $exists | Field existence check | boolean |

Primitive = string | number | boolean | null | RegExp | Date

Note on Date: Date is included for direct code usage (e.g. { createdAt: { $gt: new Date() } }). The URL parser produces ISO strings, not Date instances. Adapters are responsible for converting Date to their native format (.toISOString() for SQL, native Date for MongoDB).

Controls

| Field | Type | Description | |-------|------|-------------| | $sort | Record<string, 1 \| -1> | Sort fields (1 = asc, -1 = desc) | | $skip | number | Skip N results | | $limit | number | Limit to N results | | $count | boolean | Request total count | | $select | SelectExpr<T> | Field projection — array of strings/aggregates for inclusion, object for exclusion/mixed | | $groupBy | (keyof T & string)[] | Fields to group by for aggregate queries | | $having | FilterExpr | Post-aggregation filter on aliases and dimension fields | | $with | (WithRelation \| string)[] | Relations to populate alongside the primary query | | $<custom> | unknown | Arbitrary pass-through keywords |

Relation Loading ($with)

$with declares which relations to populate alongside the primary query. Each entry can be a string (relation name only) or a full object (a WithRelation sub-query with its own filter, controls, and insights):

import type { Uniquery, WithRelation } from '@uniqu/core'

const query: Uniquery = {
  filter: { status: 'active' },
  controls: {
    $with: [
      // String shorthand — just the relation name
      'profile',
      // Object form — full sub-query
      {
        name: 'posts',
        filter: { status: 'published' },
        controls: {
          $sort: { createdAt: -1 },
          $limit: 5,
          $select: ['title', 'body'],
          $with: [
            { name: 'comments', filter: {}, controls: { $limit: 10 } },
            'author',
          ],
        },
      },
    ],
  },
}

WithRelation is a Uniquery with a required name:

type WithRelation = Uniquery & { name: string }

The Uniquery type itself has an optional name — when present it is a nested relation, when absent it is the root query. This means every $with object entry is a self-contained query with its own filter, controls (including $sort, $skip, $limit, $select, nested $with, and pass-through keywords), and optional insights. The structure is recursive to any depth.

When a Nav generic is provided, string entries and name fields are constrained to keyof Nav & string. Without a generic, any string is accepted.

Uniqu is a query parser, not an ORM. It records what was requested — the consumer (e.g. a database adapter) decides how to execute it (JOINs, subqueries, separate queries), validates relation names against its schema, and enforces depth/security limits.

Aggregation ($groupBy + $select)

$groupBy declares grouping fields. Aggregate functions appear as AggregateExpr objects in the $select array alongside plain field names:

import type { Uniquery, AggregateExpr } from '@uniqu/core'

const query: Uniquery = {
  filter: { status: 'active' },
  controls: {
    $select: [
      'currency',
      { $fn: 'sum', $field: 'amount', $as: 'total' },
      { $fn: 'count', $field: '*', $as: 'count' },
    ],
    $groupBy: ['currency'],
    $sort: { total: -1 },
    $limit: 10,
  },
}

AggregateExpr has three fields:

interface AggregateExpr {
  $fn: AggregateFn | (string & {})  // 'sum' | 'count' | 'avg' | 'min' | 'max' | custom
  $field: string                     // field name, or '*' for count(*)
  $as?: string                       // optional alias for the result
}

Known functions are sum, count, avg, min, max (AggregateFn), but $fn accepts any string for extensibility — consumers validate and execute supported functions.

Post-Aggregation Filter ($having)

$having filters groups after aggregation — the equivalent of SQL HAVING. It operates on aggregate result aliases and dimension fields:

const query: Uniquery = {
  filter: { status: 'active' },
  controls: {
    $select: [
      'currency',
      { $fn: 'sum', $field: 'amount', $as: 'total' },
    ],
    $groupBy: ['currency'],
    $having: { total: { $gt: 1000 } },
    $sort: { total: -1 },
  },
}

$having accepts a full FilterExpr — logical operators ($and, $or, $not) and all comparison operators are supported. It is untyped (FilterExpr without a generic) because its fields are aggregate aliases that don't exist on the entity type T.

Insights track $having fields with the '$having' op:

// insights for the query above:
// 'total'    => Set { '$having', '$order' }
// 'currency' => Set { '$select', '$groupBy' }
// 'amount'   => Set { 'sum' }

Insights track aggregate usage with bare function names (not $-prefixed), making it easy to distinguish controls from aggregates:

// insights for the query above:
// 'currency' => Set { '$select', '$groupBy' }
// 'amount'   => Set { 'sum' }
// '*'        => Set { 'count' }
// 'total'    => Set { '$order' }

Type-Safe Filters

FilterExpr<T> accepts a generic entity type for compile-time field and value checking:

interface User {
  name: string
  age: number
  active: boolean
}

const filter: FilterExpr<User> = {
  name: 'John',              // string — ok
  age: { $gte: 18 },         // number — ok
  active: true,              // boolean — ok
  // age: { $gte: 'old' },   // type error: string not assignable to number
  // foo: 'bar',             // type error: 'foo' is not a key of User
}

When typed, only keys of T are allowed — no arbitrary string keys. Without a generic argument, FilterExpr accepts any string keys with any values (untyped mode).

Type-Safe Controls

UniqueryControls<T> constrains $select and $sort field names when a type parameter is provided:

const query: Uniquery<User> = {
  filter: { name: 'John' },
  controls: {
    $select: ['name', 'email'],     // ✅ autocomplete, catches typos
    $sort: { name: 1 },             // ✅ only known fields
    // $select: ['foo'],            // type error: 'foo' is not keyof User
  },
}

Tree Walker

walkFilter traverses a filter tree and calls a visitor at each node. The generic return type R is controlled by the visitor — string for SQL rendering, boolean for validation, void for side-effect traversals:

import { walkFilter, type FilterVisitor } from '@uniqu/core'

// Example: render to a SQL WHERE clause
const sqlVisitor: FilterVisitor<string> = {
  comparison(field, op, value) {
    const ops: Record<string, string> = {
      $eq: '=', $ne: '!=', $gt: '>', $gte: '>=', $lt: '<', $lte: '<=',
    }
    if (ops[op]) return `${field} ${ops[op]} ${JSON.stringify(value)}`
    if (op === '$in') return `${field} IN (${(value as unknown[]).map(v => JSON.stringify(v)).join(', ')})`
    if (op === '$regex') return `${field} ~ ${value}`
    if (op === '$exists') return value ? `${field} IS NOT NULL` : `${field} IS NULL`
    return `${field} ${op} ${JSON.stringify(value)}`
  },
  and: (children) => children.join(' AND '),
  or: (children) => `(${children.join(' OR ')})`,
  not: (child) => `NOT (${child})`,
}

const where = walkFilter(query.filter, sqlVisitor)
// "age >= 18 AND age <= 30 AND status != \"DELETED\" AND role IN (\"Admin\", \"Editor\")"

Visitor Interface

interface FilterVisitor<R> {
  /** Called for each field comparison (bare values normalized to $eq). */
  comparison(field: string, op: ComparisonOp, value: Primitive | Primitive[]): R

  /** Combine children with AND logic. */
  and(children: R[]): R

  /** Combine children with OR logic. */
  or(children: R[]): R

  /** Negate a child expression. */
  not(child: R): R
}

Walker Behavior

  • Bare primitive values ({ name: 'John' }) are normalized to comparison(field, '$eq', value) calls
  • Multi-field comparison nodes ({ age: ..., status: ... }) are expanded into individual comparison calls wrapped in visitor.and(...)
  • $and / $or nodes recurse into children and call the corresponding visitor method
  • $not nodes recurse into the single child and call visitor.not(...)
  • Empty nodes call visitor.and([])

Lazy Insights

computeInsights walks an already-built query to produce a map of field names to the set of operators used on each field. This is the lazy counterpart to the eager insights computed during URL parsing:

import { computeInsights } from '@uniqu/core'

const insights = computeInsights(query.filter, query.controls)
// Map {
//   'age'       => Set { '$gte', '$lte' },
//   'status'    => Set { '$ne' },
//   'role'      => Set { '$in' },
//   'createdAt' => Set { '$order' },
//   'name'      => Set { '$select' },
//   'email'     => Set { '$select' },
//   'posts'     => Set { '$with' },
// }

Relation names from $with are captured with the $with insight operator. Nested $with insights bubble up to the parent with dot-notation prefixed field names, and each relation also carries its own scoped insights:

const controls: UniqueryControls = {
  $with: [
    {
      name: 'tasks',
      filter: {},
      controls: {
        $with: [
          { name: 'comments', filter: { body: { $regex: 'Great' } }, controls: {} },
        ],
      },
    },
  ],
}

const insights = computeInsights({}, controls)
// Map {
//   'tasks'               => Set { '$with' },
//   'tasks.comments'      => Set { '$with' },
//   'tasks.comments.body' => Set { '$regex' },
// }

// Each relation also has its own scoped insights:
// tasks.insights        => Map { 'comments' => Set { '$with' }, 'comments.body' => Set { '$regex' } }
// comments.insights     => Map { 'body' => Set { '$regex' } }

Use cases: field whitelisting, operator auditing, index planning, relation validation.

getInsights

getInsights returns pre-computed insights when present on the query, or computes them lazily:

import { getInsights } from '@uniqu/core'

const insights = getInsights(query)
// Uses query.insights if present (e.g. from parseUrl), otherwise calls computeInsights

API Reference

Types

| Export | Description | |--------|-------------| | Primitive | string \| number \| boolean \| null \| RegExp \| Date | | ComparisonOp | Union of all $-prefixed operator names | | FieldOpsFor<V> | Per-field typed operator map | | FieldOps | Untyped operator map (FieldOpsFor<Primitive>) | | FieldValue | Primitive \| FieldOps | | FilterExpr<T> | ComparisonNode<T> \| LogicalNode<T> | | ComparisonNode<T> | Leaf node — keys constrained to keyof T when typed | | LogicalNode<T> | { $and: ... } \| { $or: ... } \| { $not: ... } — variants are mutually exclusive via never | | AggregateFn | 'sum' \| 'count' \| 'avg' \| 'min' \| 'max' | | AggregateExpr<Fn, Field, Alias> | { $fn, $field, $as? } — aggregate function call in $select. Generic params preserve literal types for result inference | | SelectExpr<T> | ((keyof T & string) \| AggregateExpr)[] \| Record<keyof T & string, 0 \| 1> | | UniqueryControls<T> | Pagination, sorting, projection, grouping, $having$select/$sort/$groupBy constrained to keyof T when typed | | Uniquery<T> | { name?, filter, controls, insights? } — root query (no name) or nested relation (with name) | | TypedWithRelation<Nav> | Typed $with entry — keyof Nav & string or object with typed filter/controls | | WithRelation | Untyped $with relation with { name: string, filter?, controls?, insights? } | | AggregateControls<T, D, M> | Typed aggregate controls — $groupBy required, $with forbidden, $select constrained to dimensions + aggregates | | AggregateQuery<T, D, M> | Typed aggregate query — { filter?, controls, insights? } with dimension/measure constraints | | AggregateResult<T, Select> | Infer result row type from $select — dimensions preserve original types, aggregates → number (min/max preserve field type) | | ResolveAlias<A> | Resolve the output alias of an AggregateExpr — uses $as if provided, otherwise {fn}_{field} | | InsightOp | ComparisonOp \| '$select' \| '$order' \| '$with' \| '$groupBy' \| '$having' \| AggregateFn \| string | | UniqueryInsights | Map<string, Set<InsightOp>> |

Functions

| Export | Signature | Description | |--------|-----------|-------------| | walkFilter | <R>(expr: FilterExpr, visitor: FilterVisitor<R>) => R | Traverse filter tree with visitor callbacks | | computeInsights | (filter: FilterExpr, controls?: UniqueryControls) => UniqueryInsights | Lazily compute field/operator usage map | | getInsights | (query: Uniquery) => UniqueryInsights | Return pre-computed or lazily computed insights | | isPrimitive | (x: unknown) => x is Primitive | Type guard for primitive values |

License

MIT