# AsterQL

```sh
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.

```asterql
*.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

```sh
pnpm add asterql
```

```sh
npm install asterql
```

## Quick Start

```ts
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

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

### Join Across Collections

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

### Group And Aggregate

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

### Flatten Nested JSON

```asterql
*.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`. `@` remains accepted as a legacy current-value spelling.
Missing fields evaluate to `null`.

### Traversal

```asterql
*.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

```asterql
*.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:

```asterql
*.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.

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

### Pipes

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

```asterql
*.posts[status == "published"] | sort(-date)[:5] {title, date}
*.posts | [status == "published"] | sort(-date) | slice($, 0, 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:

- `[condition]` or `filter(expr?)` for filter stages.
- `select(expr?)` as a jq-friendly filter alias.
- `map(expr?)` for item mapping.
- `sort(expr, ...)` for stable sorting.
- `sort_by(expr, ...)` as a jq-friendly sort alias.
- `group(expr)` for `{key, items}` groups.
- `slice(value, start?, end?)` for slicing arrays, sequences, or strings.
- `length(value?)` as a jq-friendly alias for `len(value?)`.
- `reverse(value?)` for reversing arrays, sequences, or strings.
- `add(value?)` for adding numbers or concatenating strings/arrays.
- `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?)`
- `length(value?)`
- `sum(expr?)`
- `avg(expr?)`
- `min(expr?)`
- `max(expr?)`
- `any(expr?)`
- `all(expr?)`
- `unique(expr?)`
- `filter(expr?)`
- `select(expr?)`
- `map(expr?)`
- `sort(expr, ...)`
- `sort_by(expr, ...)`
- `group(expr?)`
- `reverse(value?)`
- `add(value?)`
- `keys(value?)`
- `type(value?)`
- `when(condition, then, else?)`

`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.

`when(condition, then, else?)` is the expression-level conditional. It evaluates
and returns `then` when the condition is truthy (the same truthiness as filters)
and `else` otherwise, returning `null` when the untaken branch is omitted. Only
the taken branch is evaluated.

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

## API

```ts
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

- [SPEC.md](./SPEC.md): language semantics and examples.
- [GRAMMAR.md](./GRAMMAR.md): implementation-driven grammar reference.
- [CONTRIBUTING.md](./CONTRIBUTING.md): local development and release checks.

## Development

```sh
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.
