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

asterql

v0.3.0

Published

A JavaScript-native language for shaping and analyzing JSON.

Downloads

553

Readme

AsterQL

npm install asterql

A small language for shaping and analyzing JSON.

AsterQL is a JSON query and projection language for TypeScript. It is terse, JSON-shaped, synchronous, and built around the kind of data work application code does all the time: filter this API response, join that lookup table, group these nested records, and return exactly the shape a UI wants.

*.posts[status == "published"]
| sort(-date)[:10]{
  id,
  title,
  author: authorId -> *.users[id == @][0]{id, name},
  tags: tagIds -> *.tags[id in @]{name}
}

Why “AsterQL”? AsterQL starts at *, the full input value. The name points at the asterisk without making the language feel like a utility function. It is a small star for JSON data: a terse way to navigate, reshape, and summarize the structure in front of you.

AsterQL takes inspiration from Sanity GROQ's *[filter]{projection} shape, Sanity JSONMatch's tiny cursor-driven implementation style, and jq's compositional feel. It is not a clone of any of them. The goal is a JavaScript-native language for JSON data shaping, UI projections, and lightweight analytics.

Why AsterQL?

  • 🧭 Starts from JSON: query plain data, API responses, config, fixtures, or generated objects.
  • ✂️ Shapes for UI: return the exact nested structure your component wants.
  • 📊 Does analytical work: filter, sort, group, join, and aggregate without moving into SQL-shaped tables first.
  • 🧩 Composes deliberately: traversal and projection stay JSON-like; pipes mark collection-level transformations.
  • 🔒 Runs locally and synchronously: no hidden I/O, mutation, modules, network calls, or resolver magic.

Install

pnpm add asterql
npm install asterql

Quick Start

import { evaluate, query, compile } from "asterql";

const data = {
  posts: [
    { id: "p1", status: "published", title: "Alpha", authorId: "u1" },
    { id: "p2", status: "draft", title: "Beta", authorId: "u2" },
  ],
  users: [
    { id: "u1", name: "Ada" },
    { id: "u2", name: "Grace" },
  ],
};

const result = evaluate(
  data,
  '*.posts[status == "published"]{title, author: authorId -> *.users[id == @][0].name}',
);

console.log(result);
// [{ title: "Alpha", author: "Ada" }]

const compiled = compile("*.posts[$status = status]{id, title}");
compiled.evaluate(data, { params: { status: "draft" } });
// [{ id: "p2", title: "Beta" }]

for (const entry of query(data, '*.posts[status == "published"]')) {
  console.log(entry.value, entry.path);
}

A Few Shapes

Project UI Data

*.products[active?]{
  id,
  label: name,
  price,
  badge: category -> *.categories[id == @][0].name
}

Join Across Collections

*.posts{
  title,
  author: authorId -> *.users[id == @][0]{id, name}
}

Group And Aggregate

*.orders
| group(status){
  status: @key,
  count: len(@items),
  revenue: @items | sum(total)
}

Flatten Nested JSON

*.orders[]{
  orderId: id,
  lines: items[]{sku, quantity, subtotal: quantity * price}
}

Language Shape

Scopes

  • * is the root input value.
  • @ is the current value.
  • ^ is the parent current value.
  • $name reads a parameter from the host.

Bare identifiers read attributes from the current value, so title is the same idea as @.title. Missing fields evaluate to null.

Traversal

*.posts[0]
*.posts[-1]
*.posts[:10]
*.posts[].tags[]
*.settings["theme-color"]
*.posts..slug

[] is the explicit fan-out operator. Ordinary field traversal does not implicitly flatten nested arrays.

Filtering

*.products[price > 10 && active?]
*.posts[id in $ids]

Filters keep truthy values. The postfix ? operator tests whether a value exists.

Projection

Projections are intentionally JSON-like:

*.cart.items{
  id,
  label: name,
  subtotal: price * quantity,
  ...metadata
}

Projection maps arrays and lazy sequences item-by-item. On a single object it returns one shaped object.

Joins

left -> right evaluates the right side with @ bound to the result of the left side and ^ bound to the previous current value.

*.posts{
  title,
  author: authorId -> *.users[id == @][0]{id, name}
}

Pipes

Pipes are deliberate collection boundaries. The left value becomes @ for the right expression.

*.posts[status == "published"] | sort(-date)[:5]{title, date}
*.posts | group(status){status: @key, count: len(@items)}
*.orders | fold{count: len(@), revenue: sum(total), statuses: unique(status)}

The initial collection tools are:

  • sort(expr, ...) for stable sorting.
  • group(expr) for {key, items} groups.
  • fold{...} for aggregating a collection into one projected object.

Sorting, grouping, negative slices, and aggregation buffer because they require the whole collection. Traversal, filters, projections, joins, and positive slices remain lazy.

Built-ins

AsterQL keeps the default built-in set intentionally compact:

  • len(value?)
  • sum(expr?)
  • avg(expr?)
  • min(expr?)
  • max(expr?)
  • any(expr?)
  • all(expr?)
  • unique(expr?)
  • keys(value?)
  • type(value?)

sum(total) and avg(total) are the preferred “sum by” / “average by” shape: the first argument is evaluated once per item in the current collection. == and === are aliases for equality with the same deep JSON semantics as =; there is no JavaScript coercion.

Custom scalar functions can be passed through EvaluationOptions.functions. Custom collection reducers can be passed through EvaluationOptions.reducers.

API

parse(query: string): ProgramNode;
scan(query: string): Token[];
query(
  data: unknown,
  queryOrAst: string | ProgramNode,
  options?,
): Iterable<QueryEntry>;
evaluate(
  data: unknown,
  queryOrAst: string | ProgramNode,
  options?,
): JsonValue;
compile(query: string): CompiledQuery;

evaluate returns a materialized JSON result. query yields {value, path} entries; paths are preserved for real traversals and set to null for computed values.

Docs

Development

pnpm install
pnpm lint
pnpm typecheck
pnpm test
pnpm build

Status

AsterQL is early. The parser and evaluator are small enough to audit, and the current package is ready to consume as an ESM TypeScript library. The language surface should still be considered pre-1.0 while the analytics and codegen story settles.