Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 78 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,70 @@ minor release may break compatibility, in which case the break is spelled out be

## [Unreleased]

No change to the grammar or to the semantics of evaluation. The only edit to
`query-language-schema.json` is its root `description`, and its `$id` still names `v0.4.0`.
## [0.5.0] — 2026-09-13

**A tooling release.** No change to the grammar or to the semantics of evaluation:
`query-language-schema.json` is byte-identical to 0.4.0 apart from its root `description`, and its
`$id` still names `v0.4.0`, because the `$id` version tracks the grammar and the grammar did not
move. Consumers pinning that `$id` have nothing to do.

What did move is the generator, which is now the thing an API provider uses when implementing
search: point it at the resource schema, state the slice of the language you can actually serve,
and get back a filter schema that permits exactly that slice plus a capability document that
describes it honestly. It also travels with the package for the first time, as a `bin` named
`jql-generate`, rather than being a file inside a repository nobody installs.

[SPEC.md](./SPEC.md) gains two clarifications in service of that, both about what an
implementation may *claim* rather than about what a filter means:
[§2.1](./SPEC.md#21-profiles) says out loud that accepting part of a profile is permitted and
advertising it is not, and [§2.2](./SPEC.md#22-capability-discovery) documents the capability
document's top-level members. No filter valid under 0.4.0 becomes invalid.

### Added

- **The generator selects capabilities, not just profiles**
([#11](https://github.com/christosgkoros/json-query-language/issues/11)). Profiles are the unit
a server advertises, but three shapes do not fit inside one: a backend with `LIKE` and no
`POSITION` supports `$like` and not `$contains`; a key-value store cannot implement `$exists` at
all; a provider compiling to a flat conjunctive index wants one AND level and no shorthand. Six
new knobs, each available as a flag and as a JS API option — `--operators`, `--drop-operators`,
`--no-shorthand`, `--max-filter-depth`, `--limits`, and `--config` to hold the combination. What
is declined is absent from the generated schema, so a client learns it from validation rather
than from an `unsupported-operator` at runtime. Defaults are unchanged: with none of them given
the output is byte-identical to before.
- **`--config <file>`, and `examples/pet.jql.config.json`.** The capability selection is a
decision about the endpoint, not a shell invocation, so it goes in a JSON file checked in beside
the resource schema and regenerated from. Its keys are the JS API's option names plus `resource`,
`out` and `capabilities`; relative paths in it resolve against its own directory, an explicit
flag beats it, and an unrecognised key is refused rather than ignored — a misspelled key is a
capability that silently did not apply. `npm run generate:example` now runs through one.
- **`--max-filter-depth <n>`** caps how deep `$and`/`$or`/`$nor`/`$not` may nest: `1` is a flat
filter offering no logical operators at all, `2` permits one level of them. JSON Schema
cannot count how deep an instance already is, so the filter is emitted as a chain of levels:
level *i* offers the logical operators over level *i+1* and the last level does not offer them
at all. Every level shares the operand `$defs`, so the cost is *n* copies of a map of `$ref`s.
Field-level `$not` is bounded to a single application by the same flag — under Kleene logic
`¬¬X ≡ X` even for UNKNOWN, so a negated negation says nothing the plain constraint does not.
- **`--limits <json|@file>`** puts the SPEC §7 numbers a provider actually enforces into the
capability document. They were emitted unconditionally, so every document generated from the CLI
claimed `maxDepth: 10, maxClauses: 100, maxSetLength: 1000` whether or not that was true. Where
`--max-filter-depth` is given and `maxDepth` is not, the enforced bound is published.
- **The generator is part of the package.** `tools/` was not in `package.json` `files` and there
was no `bin` entry, so the tool the README points readers at could not travel with the package
at all. It is now a `bin` named `jql-generate`, with `json-query-language/generate` exporting
`generateFilterSchema` for programmatic use. This repository still publishes no artifacts
([RELEASING.md](./RELEASING.md)), so the command is reachable from a clone or a git install and
not from npmjs; what changed is that it is ready to be, and `npm pack` now contains it.
- **[SPEC.md §2.2](./SPEC.md#22-capability-discovery) documents the capability document's
top-level members** — `queryLanguage`, `profiles`, `fields`, `limits` and `filterSchema` — in a
table beside the existing per-field one. `limits` appeared in the example and in no table, and
`filterSchema`, `itemValues` and `nullable` were emitted by the generator and described nowhere.
No normative change to what the members mean.
- **[SPEC.md §2.1](./SPEC.md#21-profiles) says what a partial profile may and may not do.** The
rule was already there — a profile other than `core` is implemented in full or not at all — but
it read as a prohibition on the implementation rather than on the advertisement. An endpoint
accepting part of a profile is not prohibited from existing; it states what it accepts per path
and omits the incomplete profile from `profiles`.
- **`examples/mcp-server/`** — a runnable MCP server whose one tool, `search_pets`, takes a
filter as its `filter` argument and nothing else. The tool's `inputSchema` is
`examples/pet.filter.json` inlined verbatim; validation is ajv against that same file, and
Expand All @@ -27,6 +86,14 @@ No change to the grammar or to the semantics of evaluation. The only edit to

### Changed

- **The capability document's `profiles` reports coverage rather than the request.** It echoed
whatever `--profiles` was given; it now lists only the profiles the final operator set covers in
full, because [SPEC.md §2.1](./SPEC.md#21-profiles) makes a partial profile one an
implementation may not advertise. `--drop-operators '$contains'` therefore costs the `strings`
claim, and the per-field `operators` lists carry what is on offer instead — with a warning on
stderr naming the operator responsible. Declining a `core` operator drops `core` too, and warns
that the result is not a conforming implementation. Nothing changes for a selection that is
whole profiles, which is every invocation before this release.
- **Positioned as one JSON-Schema-described query language with two integration points**, rather
than as an agent interface. An earlier revision in this same unreleased window led with the MCP
tool definition and moved §*Exposing search to an agent* ahead of the OpenAPI and generator
Expand All @@ -51,6 +118,14 @@ No change to the grammar or to the semantics of evaluation. The only edit to

### Fixed

- **`--max-depth` accepted a value that was not a number.** It was coerced with `Number()` and
never checked, so `--max-depth deep` became `NaN` and silently stopped the walk at the first
nested object. It and `--max-filter-depth` are both validated now.
- **An operator whose dependency was dropped is dropped with it.** `$flags` carries
`dependentRequired: ["$regex"]` out of the grammar, so `--drop-operators '$regex'` would have
left `$flags` in `properties` with a rule naming a member `additionalProperties: false` forbids
— present in the schema and impossible to use. The closure is read off
`$defs/ConstraintObject`, so a dependency added later is handled by construction.
- **Generated filter schemas were not a narrowing** ([#8](https://github.com/christosgkoros/json-query-language/issues/8)).
`tools/generate-filter-schema.mjs` carried the published constraint object's
`dependentRequired` rule but not its `dependentSchemas` one, so `{"microchip": {"$unknownAs":
Expand Down Expand Up @@ -359,6 +434,7 @@ Initial research draft: `$and`, `$or`, `$not` over eight leaf condition types
(`$eq`, `$ne`, `$in`, `$nin`, `$like`, `$nlike`, `$gt`/`$gte`/`$lt`/`$lte`/`$between`, `$isnull`),
laid out as an OpenAPI `components.schemas` fragment.

[0.5.0]: https://github.com/christosgkoros/json-query-language/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/christosgkoros/json-query-language/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/christosgkoros/json-query-language/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/christosgkoros/json-query-language/compare/v0.2.0...v0.3.0
Expand Down
49 changes: 44 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ One schema, two integration points, because JSON Schema is what both already spe
- **Semantics** — [`SPEC.md`](./SPEC.md) — nulls, paths, coercion, errors, limits
- **MCP server** — [`examples/mcp-server/`](./examples/mcp-server) — a search tool with the language as its `inputSchema`, runnable
- **OpenAPI documents** — [`examples/`](./examples) — working 3.1 (`POST /…/search`) and 3.2 (`QUERY`) integrations
- **Generator** — [`tools/generate-filter-schema.mjs`](./tools/generate-filter-schema.mjs) — turns a resource's JSON Schema into a per-field filter schema
- **Generator** — [`tools/generate-filter-schema.mjs`](./tools/generate-filter-schema.mjs) — turns a resource's JSON Schema, plus the slice of the language you can serve, into a per-field filter schema
- **Compared with GraphQL** — [`COMPARISON.md`](./COMPARISON.md) — what this overlaps with, what it does not, and what a JSON-Schema-native alternative would still need
- **Version** — `0.4.0`, and the schema's `$id` names `v0.4.0`. This release is **breaking**: it replaces two array mechanisms with one quantifier family and adds `$unknownAs`. See [`CHANGELOG.md`](./CHANGELOG.md) for the migration, and [`decisions/0001`](./decisions/0001-array-quantifiers-and-unknown-handling.md) for why.
- **Version** — `0.5.0`. The schema's `$id` still names `v0.4.0`: the `$id` tracks the grammar, and 0.5.0 changed only the tooling and what it may claim. See [`CHANGELOG.md`](./CHANGELOG.md) for this release, and [`decisions/0001`](./decisions/0001-array-quantifiers-and-unknown-handling.md) for the v0.4.0 migration.

> **Work in progress — including the name.** This is a design published for review, not a distribution you can depend on yet. The artifact's own name is a working title, and every identifier that follows from it — the package names, the schema `$id`, the URLs in the integration examples — is a placeholder. Several do not currently resolve, and getting them right is deliberately not a goal until the name is settled. The grammar and its semantics are the part worth reviewing. See [Status](#status) before you try to install or `$ref` anything.

Expand Down Expand Up @@ -323,7 +323,43 @@ What it decides, and why:
- **Prose comes from the grammar**, not from the generator, so operator descriptions stay in one place. `--descriptions brief` (the default) keeps them for the operators people get wrong and drops them for `$eq` and `$gt`, which matters when the output goes into an MCP tool definition.
- **Narrowing only.** Every filter the generated schema accepts is also valid against the published grammar, so a server implementing the published semantics evaluates it unchanged. The rule that makes this hold: the constraint object's own keywords are copied from the grammar, and only its *operator set* and *operand schemas* are narrowed. `tests/generator.test.mjs` asserts the property by sampling filters out of each generated schema's vocabulary — a list of examples can only re-check the leaks someone already thought of.

Opt a property out, or override its operators, from the resource schema itself:
### Selecting what you support

Everything above narrows the *value* side of a filter. The other half is the *feature* side: which operators, and which parts of the grammar, your backend can actually honour. Whatever you decline here the generated schema refuses, so a client finds out from validation rather than from a runtime error — or worse, from an empty result set.

Profiles are the coarse unit, and three shapes do not fit inside one:

| Your situation | Say |
| --- | --- |
| `LIKE` but no `POSITION`, so `$like` works and `$contains` does not | `--drop-operators '$contains'` |
| A key-value store with no notion of key absence, so `$exists` is unimplementable | `--drop-operators '$exists'` |
| A flat conjunctive index: one AND level and no shorthand | `--max-filter-depth 2 --no-shorthand` |
| A single-clause lookup: no logical operators at all | `--max-filter-depth 1` |

`--operators` takes the other direction — exactly these, intersected with `--profiles` — and `--limits` replaces SPEC §7's defaults with your real bounds in the capability document.

The combinations get long, and they are not a thing to retype, so the same selection goes in a file you check in beside the resource schema and regenerate from:

```json
{
"resource": "pet.schema.json",
"id": "https://api.example.com/schemas/pet.filter.json",
"out": "pet.filter.json",
"capabilities": "pet.capabilities.json",
"profiles": ["core", "strings"],
"dropOperators": ["$contains", "$exists"],
"shorthand": false,
"limits": { "maxDepth": 4, "maxClauses": 40, "maxSetLength": 100 }
}
```

```bash
node tools/generate-filter-schema.mjs --config jql.config.json # paths resolve against the config file
```

One consequence worth knowing before you use it. [SPEC §2.1](./SPEC.md#21-profiles) says a profile other than `core` is implemented in full or not at all, so a profile you have narrowed is no longer one you can advertise: it drops out of the capability document's `profiles`, the per-field `operators` lists carry what you do offer, and the generator says on stderr which operator cost you the claim. Declining a `core` operator costs conformance outright, and it says that too. The *schema* stays legal either way — it accepts strictly fewer filters than the published grammar, which is the only rule generation has.

Opt a single property out, or override its operators, from the resource schema itself:

```json
{ "internalNotes": { "type": "string", "x-jql": false } }
Expand All @@ -332,6 +368,8 @@ Opt a property out, or override its operators, from the resource schema itself:

`--include`, `--exclude`, `--max-depth` and `--pointer` do the rest. Run `--help` for the full list.

The tool is part of the package and is installed as `jql-generate`, so once the package is published it is `npx jql-generate` rather than a path. It is not published yet — see [Status](#status) — so today it is either a clone or `npm install github:christosgkoros/json-query-language`.

## Exposing search to an agent

The other integration path, and a working server for it lives in [`examples/mcp-server/`](./examples/mcp-server) — one tool, real records, `node examples/mcp-server/demo.mjs` to watch it answer and reject.
Expand Down Expand Up @@ -389,10 +427,10 @@ query-language-schema.json the schema — the only file you need to consume
SPEC.md normative semantics
RELEASING.md how a release is cut (no artifacts are published)
COMPARISON.md how this relates to GraphQL, OData and JSON:API
tools/generate-filter-schema.mjs resource schema -> per-field filter schema + capabilities
tools/generate-filter-schema.mjs resource schema + capability selection -> filter schema + capabilities
examples/mcp-server/ a runnable MCP server; the language as a tool's inputSchema
examples/ working OpenAPI 3.1 and 3.2 documents
examples/pet.schema.json the generator's input, and its committed output beside it
examples/pet.schema.json the generator's input, its config, and its committed output beside them
tests/validate.test.mjs meta-validation + fixture runner
tests/generator.test.mjs the generator: narrowing, soundness, recursion
tests/fixtures/valid/ one per operator; also the docs' example set
Expand All @@ -417,6 +455,7 @@ experiments/filter-to-sql/ an exercise: compile a filter to SQL, then judge
| `$id` / `$ref` — `https://christosgkoros.com/json/query-language/v0.4.0/query-language-schema.json` | Does not resolve. Used throughout [Using it from OpenAPI](#using-it-from-openapi) and in the capability document examples. |
| The package names `json-query-language` and `@christosgkoros/json-query-language` | Not published, to npmjs or to GitHub Packages, and the release pipeline no longer tries to. Claiming a name under a working title would burn it. |
| The version line at the top, and the version inside the `$id` | May lag the latest tag. `CHANGELOG.md` is authoritative. |
| `npx jql-generate` | Not reachable from a registry, for the reason in the row above. The `bin` entry is real and the tool ships inside the package, so this works from a clone or a git install; it does not work from npmjs. |

These will be fixed in one pass once the name is fixed, because fixing them before then means doing it twice. Until then the only fetchable copy of the schema is raw GitHub:

Expand Down
Loading
Loading