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
9 changes: 8 additions & 1 deletion .agents/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ The `Query` AST is an **intermediate representation (IR)**. Every package plays
1. **Define & interact** — client-side construction (plan 012): `defineQuery<RECORD>(QueryBuildInput)` + per-parameter `define*` fragment factories desugar typed input (scalars → `eq`, bare arrays → `in` with `null` legal, `$`-operator objects, condition-helper trees) straight to the AST — schema-free, no parsing. Condition helpers (`parameter/filters/helpers/`, one per `FilterFieldOperator`; `in` → `inArray` since `in` is reserved) build `Filter`/`Filters` nodes directly. Queries compose immutably via `mergeQueries` (left-priority; fields/relations/sorts keyed by name, pagination per-property) and the `Filters` combinators: `merge()` = per-field replace, flat root-AND only (typed `MergeError`, `ErrorCode.FILTERS_NOT_FLAT`); `and()`/`or()` = wrap & inject (server scoping — injected conditions can't be displaced by later merges). `$and`/`$or` object keys stay reserved for a future mongo parser dialect. `QueryBuilder` was removed — `defineQuery` replaces it.
2. **Parse to IR** — parsers transform *dialect* input (a spec for how parameters are written: "simple" object shapes, "expression" strings) into the IR, validated against a `Schema`. Parsers are **transport-agnostic**: they read only the canonical `Parameter` keys (`fields`, `filters`, `pagination`, `relations`, `sort`) and know nothing about how the input crossed a process boundary.
3. **Consume the IR** — either interpret/walk it directly (`@rapiq/sql`, `@rapiq/typeorm` via visitors), or…
4. **Transport the IR between application boundaries via a codec** — `@rapiq/codec-url-simple` is *one* such codec (HTTP URI scheme). The codec owns the complete wire format: the parameter wire names (`URLParameter`: `filter`, `page`, `include`, …) live **only** there, and `URLDecoder` is the boundary adapter — it accepts a raw query string *or* a pre-parsed query object (express `req.query`), maps wire names to canonical parameters and delegates to a schema-aware `SimpleParser`. App2 then works with the same IR.
4. **Transport the IR between application boundaries via a codec** — `@rapiq/codec-url-simple` is *one* such codec (HTTP URI scheme). The codec owns the complete wire format: the parameter wire names (`URLParameter`: `filter`, `page`, `include`, …) live **only** there, and `URLDecoder` is the boundary adapter — it accepts a raw query string *or* a pre-parsed query object (express `req.query`), maps wire names to canonical parameters and delegates to a schema-aware `SimpleParser`. App2 then works with the same IR. `@rapiq/codec-url-expression` is the sibling codec for the expression dialect (nested filter compounds in a single `filter=and(...)` param; the other four parameters share the simple wire machinery).

Codec rules settled during plan 007 (2026-07):

- **Subset law**: each dialect expresses only a subset of the IR — within it `decode(encode(q)) ≍ q` *modulo scalar type normalization* (the wire is untyped: `'5'` → `5`, `'true'` → `true`); outside it `encode` throws typed `FEATURE_UNSUPPORTED`/`OPERATOR_UNSUPPORTED` instead of silently changing semantics. The simple encoder enforces this pointwise: every emitted wire token is re-parsed and must decode back to the operator it came from.
- **Codec identity is in-band** (reverses the earlier out-of-band-only stance): `@rapiq/codec-url` ships `URLCodecRegistry` — encoding through it stamps a reserved `codec` parameter; decoding dispatches on it (absent → default simple, so plain clients keep working; unregistered name → typed `CodecError`, never a silent mis-decode). Each codec package also exports its identifier constant for out-of-band negotiation.
- **Schema-aware encode** validates by piping the plain-encoded output through the schema-bound decoder and re-encoding — parser-exact semantics by construction (drop by default, schema `throwOnFailure` opts into throwing); parameters absent from the input query are masked so schema defaults don't materialize onto the wire.
- The shared filter-value wire grammar (`parseFilterScalar`/`parseFilterValue`/`parseFilterWireValue`/`serializeFilterValue`) lives in `@rapiq/parser-simple` (`parameter/filters/value.ts`) — the single source for scalar coercion and operator-marker parsing used by both parsers and the simple codec.

Placement rules that follow (settled during plan 006, don't re-litigate):

Expand Down
20 changes: 18 additions & 2 deletions .agents/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ npm-workspaces monorepo (`packages/*`) orchestrated by Nx. Every publishable pac
| [@rapiq/parser-simple](../packages/parser-simple) | Library | Parses plain object/array input (URL-query-like "simple" dialect) into a `Query` |
| [@rapiq/parser-expression](../packages/parser-expression) | Library | Parses a function-call expression language (e.g. `and(eq(name, 'John'), gte(age, '18'))`) into a `Query` |
| [@rapiq/codec-url-simple](../packages/codec-url-simple) | Library | URL query-string encoder (`URLEncoder`) & decoder (`URLDecoder`) for the simple dialect; uses `qs` |
| [@rapiq/codec-url-expression](../packages/codec-url-expression) | Library | URL codec for the expression dialect: nested filter compounds in a single `filter=and(...)` param; other parameters shared with codec-url-simple |
| [@rapiq/codec-url](../packages/codec-url) | Library | `URLCodecRegistry` dispatching between URL codec dialects via the in-band reserved `codec` parameter (default: simple) |
| [@rapiq/sql](../packages/sql) | Library | Dialect-agnostic SQL adapter + visitor; ships dialect presets (pg, mysql, sqlite, mssql, oracle) |
| [@rapiq/typeorm](../packages/typeorm) | Library | Adapter applying a parsed `Query` to a TypeORM `SelectQueryBuilder` |
| [@rapiq/docs](../packages/docs) | Docs app | VitePress documentation site (rapiq.tada5hi.net); private, not published |
Expand All @@ -30,6 +32,12 @@ Layer 2:
@rapiq/parser-expression (core + parser-simple)
@rapiq/codec-url-simple (core + parser-simple)
@rapiq/typeorm (core + sql + typeorm)

Layer 3:
@rapiq/codec-url-expression (core + parser-expression + codec-url-simple)

Layer 4:
@rapiq/codec-url (core + codec-url-simple + codec-url-expression)
```

Changes to `@rapiq/core` affect every other package.
Expand Down Expand Up @@ -84,6 +92,14 @@ packages/codec-url-simple/src/
├── encoder/ # URLEncoder + serializer/ + visitors/
├── decoder/ # URLDecoder (qs-based, reuses parser-simple parsers)
└── utils/

packages/codec-url-expression/src/
├── encoder/ # URLEncoder (filters → expression string; other params via codec-url-simple)
└── decoder/ # URLDecoder (qs-based, delegates to ExpressionParser)

packages/codec-url/src/
├── module.ts # URLCodecRegistry (in-band `codec` param dispatch)
└── factory.ts # createURLCodecRegistry (bundles simple + expression)
```

## Package Exports
Expand All @@ -109,6 +125,6 @@ Public API is controlled via the barrel `src/index.ts` of each package; anything

- **AST & type definitions** → `@rapiq/core` (`parameter/`)
- **What a client may request (allow-lists, defaults, mappings)** → `@rapiq/core` (`schema/`)
- **Turning raw input into the AST** → `@rapiq/parser-simple`, `@rapiq/parser-expression`, `@rapiq/codec-url-simple` (decode)
- **Turning the AST into transport format** → `@rapiq/codec-url-simple` (encode)
- **Turning raw input into the AST** → `@rapiq/parser-simple`, `@rapiq/parser-expression`, `@rapiq/codec-url-{simple,expression}` (decode)
- **Turning the AST into transport format** → `@rapiq/codec-url-{simple,expression}` (encode), `@rapiq/codec-url` (dialect dispatch via in-band `codec` param)
- **Turning the AST into backend queries** → `@rapiq/sql`, `@rapiq/typeorm`
48 changes: 48 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions packages/codec-url-expression/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "@rapiq/codec-url-expression",
"version": "1.0.0",
"description": "A package containing an url encoder & decoder for the expression dialect.",
"type": "module",
"main": "dist/index.mjs",
"types": "dist/index.d.mts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
}
},
"files": [
"dist/"
],
"dependencies": {
"qs": "^6.15.3"
},
"devDependencies": {
"@rapiq/codec-url-simple": "^1.0.0",
"@rapiq/core": "^1.0.0",
"@rapiq/parser-expression": "^1.0.0",
"@rapiq/parser-simple": "^1.0.0",
"@types/qs": "^6.14.0"
},
"peerDependencies": {
"@rapiq/codec-url-simple": "^1.0.0",
"@rapiq/core": "^1.0.0",
"@rapiq/parser-expression": "^1.0.0",
"@rapiq/parser-simple": "^1.0.0"
},
Comment thread
tada5hi marked this conversation as resolved.
"scripts": {
"build:types": "tsc --noEmit -p tsconfig.build.json",
"build:js": "tsdown",
"build": "npm run build:types && npm run build:js",
"test": "vitest --config test/vitest.config.ts --run",
"test:coverage": "vitest --config test/vitest.config.ts --run --coverage",
"prepublishOnly": "npm run build"
},
"author": {
"name": "Peter Placzek",
"email": "contact@tada5hi.net",
"url": "https://github.com/tada5hi"
},
"license": "MIT",
"keywords": [
"query",
"json",
"json-api",
"api",
"rest",
"api-utils",
"include",
"pagination",
"sort",
"fields",
"filter",
"relations",
"typescript"
],
"repository": {
"type": "git",
"url": "git+https://github.com/Tada5hi/rapiq.git",
"directory": "packages/codec-url-expression"
},
"bugs": {
"url": "https://github.com/Tada5hi/rapiq/issues"
},
"homepage": "https://github.com/Tada5hi/rapiq#readme"
}
13 changes: 13 additions & 0 deletions packages/codec-url-expression/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2026.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/

/**
* Stable identifier of this codec (wire dialect), e.g. for the
* in-band `codec` parameter dispatched by a codec registry or an
* out-of-band content-negotiation header.
*/
export const URL_EXPRESSION_CODEC = 'url-expression';
8 changes: 8 additions & 0 deletions packages/codec-url-expression/src/decoder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright (c) 2026.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/

export * from './module';
Loading
Loading