From e7cf15b01331b1551e1c9a28d8286c9670b166ee Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 7 Jul 2026 19:49:16 +0200 Subject: [PATCH 1/6] docs: align root README usage section with the v2 packages Rewrites the root README for the v2 monorepo (plan 009, M4): package inventory table, per-package client/server installs (no rapiq umbrella package), and a parse example using URLDecoder + QueryVisitor + TypeormAdapter instead of the v1-era typeorm-extension applyQuery. Docs pages touched by the same review: URLDecoder (not SimpleParser) as the req.query entry point in quick-start and the typeorm integration, corrected expression-dialect examples to the function-call grammar, and added codec-url-expression / codec-url to the package tables. --- README.MD | 193 ++++++++---------- packages/docs/getting-started/index.md | 4 +- packages/docs/getting-started/installation.md | 11 +- packages/docs/getting-started/quick-start.md | 17 +- packages/docs/integrations/simple.md | 2 +- packages/docs/integrations/typeorm.md | 7 +- 6 files changed, 106 insertions(+), 128 deletions(-) diff --git a/README.MD b/README.MD index 54fbe7aee..d266a70a5 100644 --- a/README.MD +++ b/README.MD @@ -16,17 +16,44 @@ It defines a scheme for the request, but **not** for the response. **Table of Contents** +- [Packages](#packages) - [Installation](#installation) - [Documentation](#documentation) - [Parameters](#parameters) - [Usage](#usage) - [License](#license) +## Packages + +Version 2 splits the former single `rapiq` package into focused, composable `@rapiq/*` packages β€” +there is **no** `rapiq` umbrella package for v2, install only what you need +(`@rapiq/core` is a peer dependency of every other package): + +| Package | Purpose | +|---|---| +| [@rapiq/core](packages/core) | Query AST, typed build layer (`defineQuery`, condition helpers, `mergeQueries`), schema system & registry | +| [@rapiq/parser-simple](packages/parser-simple) | Parses plain object/array input (the "simple" dialect) into a `Query` | +| [@rapiq/parser-expression](packages/parser-expression) | Parses filter expressions like `and(eq(name, 'John'), gte(age, '18'))` | +| [@rapiq/codec-url-simple](packages/codec-url-simple) | URL query-string encoder & decoder for the simple dialect | +| [@rapiq/codec-url-expression](packages/codec-url-expression) | URL codec carrying nested filter compounds in a single `filter=and(...)` parameter | +| [@rapiq/codec-url](packages/codec-url) | Registry dispatching between URL codec dialects via the reserved `codec` parameter | +| [@rapiq/sql](packages/sql) | Dialect-agnostic SQL adapter (pg, mysql, sqlite, mssql & oracle presets) | +| [@rapiq/typeorm](packages/typeorm) | Applies a parsed `Query` to a TypeORM `SelectQueryBuilder` | + ## Installation +Client side β€” build queries and encode them as URL query strings: + +```bash +npm install @rapiq/core @rapiq/codec-url-simple +``` + +Server side β€” decode & validate incoming query input and apply it to the database: + ```bash -npm install rapiq --save +npm install @rapiq/core @rapiq/codec-url-simple @rapiq/sql @rapiq/typeorm ``` + ## Documentation To read the docs, visit [https://rapiq.tada5hi.net](https://rapiq.tada5hi.net) @@ -139,93 +166,48 @@ The next [section](#parse-) will describe, how to parse the query string on the ### Parse πŸ”Ž -The last step of the whole process is to parse the transpiled query string, to an efficient data structure. -The result object (`ParseOutput`) can contain an output for each -[Parameter](https://rapiq.tada5hi.net/guide/parameter-api-reference.md#parameter)/ -[URLParameter](https://rapiq.tada5hi.net/guide/parameter-api-reference.md#urlparameter). -- [Fields](https://rapiq.tada5hi.net/guide/fields-api-reference.md#fieldsparseoutput): `FieldsParseOutput` -- [Filter(s)](https://rapiq.tada5hi.net/guide/filters-api-reference.md#filtersparseoutput): `FiltersParseOutput` -- [Pagination](https://rapiq.tada5hi.net/guide/pagination-api-reference.md#paginationparseoutput): `PaginationParseOutput` -- [Relations](https://rapiq.tada5hi.net/guide/relations-api-reference.md#relationsparseoutput): `RelationsParseOutput` -- [Sort](https://rapiq.tada5hi.net/guide/sort-api-reference.md#sortparseoutput): `SortParseOutput` - -> **NOTE**: Check out the API-Reference of each parameter for output formats and examples. +On the server side the incoming query is decoded back into the same +[Query](https://rapiq.tada5hi.net/guide/query) AST. A +[Schema](https://rapiq.tada5hi.net/guide/schema) declares what a client may request per parameter +(allow-lists, defaults, mappings) β€” anything outside it is silently dropped +(set `throwOnFailure: true` on the schema to get a `ParseError` instead). +The decoded query is then applied to the database by an adapter +([@rapiq/typeorm](https://rapiq.tada5hi.net/integrations/typeorm) below; +[@rapiq/sql](https://rapiq.tada5hi.net/integrations/sql) renders parameterized SQL fragments for any other driver). #### Example The following example is based on the assumption, that the following packages are installed: - [express](https://www.npmjs.com/package/express) - [typeorm](https://www.npmjs.com/package/typeorm) -- [typeorm-extension](https://www.npmjs.com/package/typeorm-extension) - -For explanation purposes, three simple entities with relations between them are declared to demonstrate -the usage on the backend side. - -**`entities.ts`** -```typescript -import { - Entity, - PrimaryGeneratedColumn, - Column, - OneToMany, - JoinColumn, - ManyToOne -} from "typeorm"; - -@Entity() -export class User { - @PrimaryGeneratedColumn({unsigned: true}) - id: number; - - @Column({type: 'varchar', length: 30}) - @Index({unique: true}) - name: string; - - @Column({type: 'varchar', length: 255, default: null, nullable: true}) - email: string; - - @Column({type: 'int', nullable: true}) - age: number - - @ManyToOne(() => Realm, { onDelete: 'CASCADE' }) - realm: Realm; - - @OneToMany(() => User, { onDelete: 'CASCADE' }) - items: Item[]; -} -@Entity() -export class Realm { - @PrimaryColumn({ type: 'varchar', length: 36 }) - id: string; - - @Column({ type: 'varchar', length: 128, unique: true }) - name: string; - - @Column({ type: 'text', nullable: true, default: null }) - description: string | null; -} - -@Entity() -export class Item { - @PrimaryGeneratedColumn({unsigned: true}) - id: number; - - @ManyToOne(() => Realm, { onDelete: 'CASCADE' }) - realm: Realm; - - @ManyToOne(() => User, { onDelete: 'CASCADE' }) - user: User; -} -``` +It uses the same `User` & `Realm` types as the build example, declared as TypeORM entities. ```typescript import { Request, Response } from 'express'; - -import { - applyQuery, - useDataSource -} from 'typeorm-extension'; +import { SchemaRegistry, defineSchema } from '@rapiq/core'; +import { URLDecoder } from '@rapiq/codec-url-simple'; +import { QueryVisitor } from '@rapiq/sql'; +import { TypeormAdapter } from '@rapiq/typeorm'; + +const registry = new SchemaRegistry(); + +registry.add(defineSchema({ + name: 'realm', + fields: { allowed: ['id', 'name'] }, +})); + +registry.add(defineSchema({ + name: 'user', + fields: { allowed: ['id', 'name', 'email', 'age'] }, + filters: { allowed: ['id', 'name', 'age'] }, + relations: { allowed: ['items', 'realm'] }, + pagination: { maxLimit: 20 }, + sort: { allowed: ['id', 'name'] }, + schemaMapping: { realm: 'realm' }, +})); + +const decoder = new URLDecoder(registry); /** * Get many users. @@ -237,43 +219,32 @@ import { * @param res */ export async function getUsers(req: Request, res: Response) { - const dataSource = await useDataSource(); - const repository = dataSource.getRepository(User); - const query = repository.createQueryBuilder('user'); - - // ----------------------------------------------------- - - // parse and apply data on the db query. - const { pagination } = applyQuery(query, req.query, { - defaultPath: 'user', - fields: { - allowed: ['id', 'name', 'realm.id', 'realm.name'], - }, - filters: { - allowed: ['id', 'name', 'realm.id'], - }, - relations: { - allowed: ['items', 'realm'] - }, - pagination: { - maxLimit: 20 - }, - sort: { - allowed: ['id', 'name', 'realm.id'], - } - }); + // map the URL wire names (filter, page, include, ...) to their canonical + // parameters and validate against the schema allow-lists. + const query = decoder.decode(req.query, { schema: 'user' }); + if (!query) { + return res.status(400).end(); + } + + const queryBuilder = dataSource + .getRepository(User) + .createQueryBuilder('user'); - // ----------------------------------------------------- + const adapter = new TypeormAdapter({ + relations: { joinAndSelect: true } + }); + adapter.withQuery(queryBuilder); + query.accept(new QueryVisitor(adapter)); + const { pagination } = adapter.execute(); - const [entities, total] = await query.getManyAndCount(); + const [entities, total] = await queryBuilder.getManyAndCount(); return res.json({ - data: { - data: entities, - meta: { - total, - ...pagination - } + data: entities, + meta: { + total, + limit: pagination.limit, + offset: pagination.offset } }); } diff --git a/packages/docs/getting-started/index.md b/packages/docs/getting-started/index.md index b164888a0..628942626 100644 --- a/packages/docs/getting-started/index.md +++ b/packages/docs/getting-started/index.md @@ -48,8 +48,10 @@ URLEncoder (@rapiq/codec-url-simple) |---|---| | [@rapiq/core](https://github.com/tada5hi/rapiq/tree/master/packages/core) | Query AST, visitor interfaces, schema system & registry, parser base classes, errors | | [@rapiq/parser-simple](/integrations/simple) | Parses plain object/array input (URL-query-like "simple" dialect) into a `Query` | -| [@rapiq/parser-expression](/integrations/expression) | Parses an infix expression language (e.g. `age gte 18 and name eq 'John'`) into a `Query` | +| [@rapiq/parser-expression](/integrations/expression) | Parses a function-call expression language (e.g. `and(eq(name, 'John'), gte(age, '18'))`) into a `Query` | | [@rapiq/codec-url-simple](/integrations/url) | URL query-string encoder & decoder for the simple dialect | +| [@rapiq/codec-url-expression](/integrations/url#expression-dialect) | URL codec for the expression dialect β€” a nested filter compound in a single `filter=and(...)` parameter | +| [@rapiq/codec-url](/integrations/url#codec-registry) | Registry dispatching between URL codec dialects via the reserved `codec` parameter | | [@rapiq/sql](/integrations/sql) | Dialect-agnostic SQL adapter; ships presets for Postgres, MySQL, SQLite, MSSQL & Oracle | | [@rapiq/typeorm](/integrations/typeorm) | Applies a parsed `Query` to a TypeORM `SelectQueryBuilder` | diff --git a/packages/docs/getting-started/installation.md b/packages/docs/getting-started/installation.md index fad218fd4..b691e2efa 100644 --- a/packages/docs/getting-started/installation.md +++ b/packages/docs/getting-started/installation.md @@ -13,10 +13,11 @@ npm install @rapiq/core @rapiq/codec-url-simple ## Server side -To parse and validate incoming query input: +To decode and validate incoming URL query input (a raw query string or a pre-parsed +object like express' `req.query`): ```sh -npm install @rapiq/core @rapiq/parser-simple +npm install @rapiq/core @rapiq/codec-url-simple ``` Add the backend adapter that matches your stack: @@ -37,8 +38,10 @@ npm install @rapiq/sql @rapiq/typeorm | Package | Install when… | |---|---| -| `@rapiq/parser-expression` | you want to accept infix filter expressions like `age gte 18 and name eq 'John'` | -| `@rapiq/codec-url-simple` | you want to decode full URL query strings on the server (it builds on `@rapiq/parser-simple`) | +| `@rapiq/parser-simple` | your input already uses the canonical parameter keys (`filters`, `pagination`, …) instead of URL wire names β€” the layer `@rapiq/codec-url-simple` builds on | +| `@rapiq/parser-expression` | you want to accept function-call filter expressions like `and(eq(name, 'John'), gte(age, '18'))` | +| `@rapiq/codec-url-expression` | you want the expression dialect on the wire β€” a nested filter compound in a single `filter=and(...)` parameter | +| `@rapiq/codec-url` | you accept more than one URL codec dialect and want dispatch via the reserved `codec` parameter | ## Requirements diff --git a/packages/docs/getting-started/quick-start.md b/packages/docs/getting-started/quick-start.md index 3702f27bf..f22af7204 100644 --- a/packages/docs/getting-started/quick-start.md +++ b/packages/docs/getting-started/quick-start.md @@ -44,13 +44,13 @@ const response = await fetch(`/users?${queryString}`); The record generic types every field path against `User`. Filters take scalars (`{ name: 'John' }`), arrays (`{ realm_id: [1, null] }`), `$`-operator objects and [condition helpers](/guide/build#condition-helpers) like `or(gte('age', 18), eq('email', null))` β€” see [Building Queries](/guide/build) for the full grammar. -## 2. Parse & validate (server) +## 2. Decode & validate (server) -Declare a `Schema` β€” the allow-list of what a client may request β€” and parse the incoming input against it: +Declare a `Schema` β€” the allow-list of what a client may request β€” and decode the incoming query against it: ```typescript import { SchemaRegistry, defineSchema } from '@rapiq/core'; -import { SimpleParser } from '@rapiq/parser-simple'; +import { URLDecoder } from '@rapiq/codec-url-simple'; const registry = new SchemaRegistry(); @@ -69,16 +69,17 @@ registry.add(defineSchema({ schemaMapping: { realm: 'realm' }, })); -const parser = new SimpleParser(registry); +const decoder = new URLDecoder(registry); -// express parses the query string into an object for you (req.query) -const query = parser.parse(req.query, { schema: 'user' }); +// accepts the raw query string as well as a pre-parsed object (express req.query); +// URL wire names (filter, page, include, ...) map to their canonical parameters. +const query = decoder.decode(req.query, { schema: 'user' }); ``` Anything outside the allow-lists is silently dropped; set `throwOnFailure: true` on the schema to get a `ParseError` instead. See [Schemas](/guide/schema). -::: tip Raw query strings -If you only have the raw string, parse it with [qs](https://www.npmjs.com/package/qs) and feed the result to `SimpleParser` β€” or use the schema-less [`URLDecoder`](/integrations/url) when validation isn't needed. +::: tip Canonical object input +If your input isn't URL-shaped β€” it already uses the canonical parameter keys (`filters`, `pagination`, `relations`, …) β€” feed it to [`SimpleParser`](/integrations/simple) from `@rapiq/parser-simple` directly: `parser.parse(input, { schema: 'user' })`. The `URLDecoder` builds on it. ::: ## 3. Apply to the database (server) diff --git a/packages/docs/integrations/simple.md b/packages/docs/integrations/simple.md index b3c4b9c9e..91f2c9a1e 100644 --- a/packages/docs/integrations/simple.md +++ b/packages/docs/integrations/simple.md @@ -1,6 +1,6 @@ # Simple Parser -`@rapiq/parser-simple` parses plain object/array input β€” the URL-query-like "simple" dialect. It is the workhorse parser: express-style `req.query` objects feed straight into it, and the [URL codec](/integrations/url) builds on it. +`@rapiq/parser-simple` parses plain object/array input β€” the URL-query-like "simple" dialect. It is the workhorse parser: the [URL codec](/integrations/url) builds on it, mapping URL wire names to the canonical parameter keys this parser reads. ```sh npm install @rapiq/core @rapiq/parser-simple diff --git a/packages/docs/integrations/typeorm.md b/packages/docs/integrations/typeorm.md index 94f047787..f09595e62 100644 --- a/packages/docs/integrations/typeorm.md +++ b/packages/docs/integrations/typeorm.md @@ -85,7 +85,7 @@ The same pattern works with `FieldsVisitor` (`adapter.fields`), `SortsVisitor` ( ```typescript import { SchemaRegistry, defineSchema } from '@rapiq/core'; -import { SimpleParser } from '@rapiq/parser-simple'; +import { URLDecoder } from '@rapiq/codec-url-simple'; import { QueryVisitor } from '@rapiq/sql'; import { TypeormAdapter } from '@rapiq/typeorm'; @@ -100,10 +100,11 @@ registry.add(defineSchema({ schemaMapping: { realm: 'realm' }, })); -const parser = new SimpleParser(registry); +const decoder = new URLDecoder(registry); export async function getUsers(req: Request, res: Response) { - const query = parser.parse(req.query, { schema: 'user' }); + // wire names (filter, page, include, ...) map to canonical parameters + const query = decoder.decode(req.query, { schema: 'user' }); const queryBuilder = dataSource.getRepository(User).createQueryBuilder('user'); From 798165a415a9b74f2040661256fe2e418b4c9021 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 7 Jul 2026 19:56:53 +0200 Subject: [PATCH 2/6] docs: modernize root README with hero banner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds .github/assets/banner.svg β€” a self-contained dark hero card (brand gradient mark, wordmark, tagline and a decorative wire-format query string) β€” and restructures the README around it: centered intro, badge row (stale semantic-release and v1 npm badges replaced by Conventional Commits and MIT license shields), doc quick links, a 'Why rapiq?' pipeline overview, and tabular Parameters. Usage examples are unchanged apart from swapping the axios boilerplate for fetch. Renames README.MD to the conventional README.md casing and updates the agent-doc references. --- .agents/conventions.md | 4 +- .github/assets/banner.svg | 41 +++++++++ AGENTS.md | 2 +- README.MD => README.md | 173 ++++++++++++++++++-------------------- 4 files changed, 126 insertions(+), 94 deletions(-) create mode 100644 .github/assets/banner.svg rename README.MD => README.md (56%) diff --git a/.agents/conventions.md b/.agents/conventions.md index 21d072f54..919f54d87 100644 --- a/.agents/conventions.md +++ b/.agents/conventions.md @@ -17,7 +17,7 @@ - After making changes, **build the affected package** (`npx nx run @rapiq/:build`) and **run the linter** on changed files. Remember Nx builds dependents from `dist/`, so a stale `@rapiq/core` build breaks downstream type-checking. - When changing `@rapiq/core` public API, check all downstream packages (parser-simple, parser-expression, sql, typeorm, codec-url-simple) β€” they peer-depend on it. -- User-facing behavior changes should be reflected in `packages/docs/guide/` and, if relevant, the root `README.MD`. +- User-facing behavior changes should be reflected in `packages/docs/guide/` and, if relevant, the root `README.md`. ## Code Style @@ -93,4 +93,4 @@ This builds a cumulative mapping over time so future work can quickly find corre | Change | Docs to update | |--------|----------------| | Parameter syntax/semantics (fields, filters, sort, …) | `packages/docs/guide/` API reference pages | -| New package or export | `packages/docs/guide/` + root `README.MD` | +| New package or export | `packages/docs/guide/` + root `README.md` | diff --git a/.github/assets/banner.svg b/.github/assets/banner.svg new file mode 100644 index 000000000..aa729dd79 --- /dev/null +++ b/.github/assets/banner.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rapiq + One typed query language β€” from client to database. + ?fields=id,name&filter[age]=>=18&include=realm&page[limit]=20&sort=-name + + + diff --git a/AGENTS.md b/AGENTS.md index 04e6553a4..3c81d6a82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ npm run dev --workspace=packages/docs # run the docs site locally npm run build --workspace=packages/docs # build the docs site ``` -Note: the root `README.MD` documents the upcoming v2; v1 lives on the `v1` branch. +Note: the root `README.md` documents the upcoming v2; v1 lives on the `v1` branch. ## Detailed Guides diff --git a/README.MD b/README.md similarity index 56% rename from README.MD rename to README.md index d266a70a5..aba77a3ca 100644 --- a/README.MD +++ b/README.md @@ -1,46 +1,65 @@ -# rapiq 🌈 - -[![npm version](https://badge.fury.io/js/rapiq.svg)](https://badge.fury.io/js/rapiq) -[![main](https://github.com/Tada5hi/rapiq/actions/workflows/main.yml/badge.svg)](https://github.com/Tada5hi/rapiq/actions/workflows/main.yml) -[![codecov](https://codecov.io/gh/tada5hi/rapiq/branch/master/graph/badge.svg?token=QFGCsHRUax)](https://codecov.io/gh/tada5hi/rapiq) -[![Known Vulnerabilities](https://snyk.io/test/github/Tada5hi/rapiq/badge.svg)](https://snyk.io/test/github/Tada5hi/rapiq) -[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) +

+ rapiq β€” REST API Query +

+ +

+ Rapiq (Rest Api Query) builds an efficient, typed interface between client- & server-side applications.
+ It defines a scheme for the request β€” not for the response. +

+ +

+ CI + codecov + Known Vulnerabilities + Conventional Commits + License: MIT +

+ +

+ Documentation + Β· + Getting Started + Β· + Guide + Β· + Migration from v1 +

+ +--- > [!WARNING] > This README includes the documentation for the upcoming version 2. > > This is the [link](https://github.com/tada5hi/rapiq/tree/v1) for the v1 (and prior). -Rapiq (**R**est **Api** **Q**uery) is a library to build an efficient interface between client- & server-side applications. -It defines a scheme for the request, but **not** for the response. - -**Table of Contents** +## Why rapiq? + +Every REST list endpoint answers the same five questions: which **fields**, which **filters**, which **relations**, which **page**, which **order**. rapiq turns them into one typed pipeline instead of ad-hoc string parsing: + +```text +defineQuery({ filters: { age: gte(18) }, sort: '-name' }) client (typed) + β”‚ encode @rapiq/codec-url-simple + β–Ό +?filter[age]=>=18&sort=-name the wire (JSON-API style) + β”‚ decode + validate Schema allow-lists, defaults, mappings + β–Ό +Query β€” fields Β· filters Β· pagination Β· relations Β· sorts the AST + β”‚ accept(visitor) + β–Ό +parameterized SQL (@rapiq/sql) Β· SelectQueryBuilder (@rapiq/typeorm) your database +``` -- [Packages](#packages) -- [Installation](#installation) -- [Documentation](#documentation) -- [Parameters](#parameters) -- [Usage](#usage) -- [License](#license) +- 🧭 **Typed end to end** β€” every field path in `defineQuery` is checked against the record type; condition helpers (`eq`, `gte`, `and`, `or`, …) replace magic value strings. +- πŸ›‘οΈ **The server has the last word** β€” a `Schema` declares what a client may request per parameter (allow-lists, defaults, mappings). Anything outside it is dropped β€” or throws, opt-in β€” and server-injected conditions (`query.filters.and(...)`) can't be displaced by client input. +- πŸ” **Loss-free transport** β€” within each codec dialect, `decode(encode(query))` restores the same query; outside its subset, encoding fails loudly with a typed error instead of silently changing semantics. +- πŸ”Œ **Any backend** β€” the AST is consumed through visitors: parameterized SQL fragments with presets for Postgres, MySQL, SQLite, MSSQL & Oracle, or applied straight to a TypeORM `SelectQueryBuilder`. +- πŸ“¦ **Composable packages** β€” no monolith: install only what each side needs; `@rapiq/core` is the single shared foundation. -## Packages +## Installation Version 2 splits the former single `rapiq` package into focused, composable `@rapiq/*` packages β€” there is **no** `rapiq` umbrella package for v2, install only what you need -(`@rapiq/core` is a peer dependency of every other package): - -| Package | Purpose | -|---|---| -| [@rapiq/core](packages/core) | Query AST, typed build layer (`defineQuery`, condition helpers, `mergeQueries`), schema system & registry | -| [@rapiq/parser-simple](packages/parser-simple) | Parses plain object/array input (the "simple" dialect) into a `Query` | -| [@rapiq/parser-expression](packages/parser-expression) | Parses filter expressions like `and(eq(name, 'John'), gte(age, '18'))` | -| [@rapiq/codec-url-simple](packages/codec-url-simple) | URL query-string encoder & decoder for the simple dialect | -| [@rapiq/codec-url-expression](packages/codec-url-expression) | URL codec carrying nested filter compounds in a single `filter=and(...)` parameter | -| [@rapiq/codec-url](packages/codec-url) | Registry dispatching between URL codec dialects via the reserved `codec` parameter | -| [@rapiq/sql](packages/sql) | Dialect-agnostic SQL adapter (pg, mysql, sqlite, mssql & oracle presets) | -| [@rapiq/typeorm](packages/typeorm) | Applies a parsed `Query` to a TypeORM `SelectQueryBuilder` | - -## Installation +(see [Packages](#packages) below; `@rapiq/core` is a peer dependency of every other package). Client side β€” build queries and encode them as URL query strings: @@ -54,33 +73,9 @@ Server side β€” decode & validate incoming query input and apply it to the datab npm install @rapiq/core @rapiq/codec-url-simple @rapiq/sql @rapiq/typeorm ``` -## Documentation - -To read the docs, visit [https://rapiq.tada5hi.net](https://rapiq.tada5hi.net) - -## Parameters - -- `fields` - - Description: Return only specific resource fields or extend the default selection. - - URL-Parameter: **fields** -- `filters` - - Description: Filter the resources, according to specific criteria. - - URL-Parameter: **filter** -- `relations` - - Description: Include related resources of the primary resource. - - URL-Parameter: **include** -- `pagination` - - Description: Limit the number of resources returned from the entire collection. - - URL-Parameter: **page** -- `sort` - - Description: Sort the resources according to one or more keys in asc/desc direction. - - URL-Parameter: **sort** - -It is based on the [JSON-API](https://jsonapi.org/format/) specification. - ## Usage -This is a small outlook on how to use the library. For detailed explanations and extended examples, +A small outlook on how to use the library. For detailed explanations and extended examples, read the [docs](https://rapiq.tada5hi.net). ### Build πŸ”§ @@ -94,10 +89,7 @@ queries compose with [mergeQueries](https://rapiq.tada5hi.net/guide/merge). The query is serialized for transport by the URL codec (`@rapiq/codec-url-simple`) and parsed back into the same AST on the server side. -#### Example - ```typescript -import axios from 'axios'; import { defineQuery } from '@rapiq/core'; import { URLEncoder } from '@rapiq/codec-url-simple'; @@ -122,15 +114,6 @@ export type User = { items: Item[] } -type ResponsePayload = { - data: User[], - meta: { - limit: number, - offset: number, - total: number - } -} - const query = defineQuery({ pagination: { limit: 20, @@ -145,25 +128,12 @@ const query = defineQuery({ }); const encoder = new URLEncoder(); - -// console.log(encoder.encode(query)); +const queryString = encoder.encode(query); // fields=id,name&filter[id]=1&page[limit]=20&page[offset]=10&include=realm&sort=-id -async function getAPIUsers(): Promise { - const response = await axios.get(`users?${encoder.encode(query)}`); - - return response.data; -} - -(async () => { - let response = await getAPIUsers(); - - // do something with the response :) -})(); +const response = await fetch(`/users?${queryString}`); ``` -The next [section](#parse-) will describe, how to parse the query string on the backend side. - ### Parse πŸ”Ž On the server side the incoming query is decoded back into the same @@ -175,13 +145,9 @@ The decoded query is then applied to the database by an adapter ([@rapiq/typeorm](https://rapiq.tada5hi.net/integrations/typeorm) below; [@rapiq/sql](https://rapiq.tada5hi.net/integrations/sql) renders parameterized SQL fragments for any other driver). -#### Example - -The following example is based on the assumption, that the following packages are installed: -- [express](https://www.npmjs.com/package/express) -- [typeorm](https://www.npmjs.com/package/typeorm) - -It uses the same `User` & `Realm` types as the build example, declared as TypeORM entities. +The following example assumes [express](https://www.npmjs.com/package/express) and +[typeorm](https://www.npmjs.com/package/typeorm) are installed, and uses the same +`User` & `Realm` types as the build example, declared as TypeORM entities. ```typescript import { Request, Response } from 'express'; @@ -250,6 +216,31 @@ export async function getUsers(req: Request, res: Response) { } ``` +## Packages + +| Package | Purpose | +|---|---| +| [@rapiq/core](packages/core) | Query AST, typed build layer (`defineQuery`, condition helpers, `mergeQueries`), schema system & registry | +| [@rapiq/parser-simple](packages/parser-simple) | Parses plain object/array input (the "simple" dialect) into a `Query` | +| [@rapiq/parser-expression](packages/parser-expression) | Parses filter expressions like `and(eq(name, 'John'), gte(age, '18'))` | +| [@rapiq/codec-url-simple](packages/codec-url-simple) | URL query-string encoder & decoder for the simple dialect | +| [@rapiq/codec-url-expression](packages/codec-url-expression) | URL codec carrying nested filter compounds in a single `filter=and(...)` parameter | +| [@rapiq/codec-url](packages/codec-url) | Registry dispatching between URL codec dialects via the reserved `codec` parameter | +| [@rapiq/sql](packages/sql) | Dialect-agnostic SQL adapter (pg, mysql, sqlite, mssql & oracle presets) | +| [@rapiq/typeorm](packages/typeorm) | Applies a parsed `Query` to a TypeORM `SelectQueryBuilder` | + +## Parameters + +The query scheme is based on the [JSON-API](https://jsonapi.org/format/) specification: + +| Parameter | URL name | Description | +|---|---|---| +| `fields` | `fields` | Return only specific resource fields or extend the default selection. | +| `filters` | `filter` | Filter the resources, according to specific criteria. | +| `relations` | `include` | Include related resources of the primary resource. | +| `pagination` | `page` | Limit the number of resources returned from the entire collection. | +| `sort` | `sort` | Sort the resources according to one or more keys in asc/desc direction. | + ## License Made with πŸ’š From 0bc4387b45f3325ef0469ecdba6901229edd0389 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 7 Jul 2026 20:02:03 +0200 Subject: [PATCH 3/6] docs: replace README banner with logo mark & tabular pipeline The hero banner card is swapped for a standalone gradient logo mark plus a plain h1/tagline header, the tagline now claims 'typed REST queries' rather than 'query language' (the typing lives at the endpoints, not on the wire), and the ASCII pipeline diagram becomes a four-stage table (build/transport/validate/execute). --- .github/assets/banner.svg | 41 --------------------------------------- .github/assets/logo.svg | 19 ++++++++++++++++++ README.md | 27 ++++++++++++-------------- 3 files changed, 31 insertions(+), 56 deletions(-) delete mode 100644 .github/assets/banner.svg create mode 100644 .github/assets/logo.svg diff --git a/.github/assets/banner.svg b/.github/assets/banner.svg deleted file mode 100644 index aa729dd79..000000000 --- a/.github/assets/banner.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - rapiq - One typed query language β€” from client to database. - ?fields=id,name&filter[age]=>=18&include=realm&page[limit]=20&sort=-name - - - diff --git a/.github/assets/logo.svg b/.github/assets/logo.svg new file mode 100644 index 000000000..45357c23b --- /dev/null +++ b/.github/assets/logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index aba77a3ca..969695745 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@

- rapiq β€” REST API Query + rapiq

+

rapiq

+

- Rapiq (Rest Api Query) builds an efficient, typed interface between client- & server-side applications.
- It defines a scheme for the request β€” not for the response. + Typed REST queries β€” from client to database.
+ Rapiq (Rest Api Query) builds an efficient interface between client- & server-side applications β€”
+ it defines a scheme for the request, but not for the response.

@@ -36,18 +39,12 @@ Every REST list endpoint answers the same five questions: which **fields**, which **filters**, which **relations**, which **page**, which **order**. rapiq turns them into one typed pipeline instead of ad-hoc string parsing: -```text -defineQuery({ filters: { age: gte(18) }, sort: '-name' }) client (typed) - β”‚ encode @rapiq/codec-url-simple - β–Ό -?filter[age]=>=18&sort=-name the wire (JSON-API style) - β”‚ decode + validate Schema allow-lists, defaults, mappings - β–Ό -Query β€” fields Β· filters Β· pagination Β· relations Β· sorts the AST - β”‚ accept(visitor) - β–Ό -parameterized SQL (@rapiq/sql) Β· SelectQueryBuilder (@rapiq/typeorm) your database -``` +| Stage | What happens | +|---|---| +| **Build** client | `defineQuery({ filters: { age: gte(18) }, sort: '-name' })` β€” typed input in, query AST out | +| **Transport** wire | encoded as a JSON-API-style query string: `?filter[age]=>=18&sort=-name` | +| **Validate** server | decoded back into the same AST, checked against a `Schema` β€” allow-lists, defaults, mappings | +| **Execute** database | applied as parameterized SQL (`@rapiq/sql`) or to a TypeORM `SelectQueryBuilder` (`@rapiq/typeorm`) | - 🧭 **Typed end to end** β€” every field path in `defineQuery` is checked against the record type; condition helpers (`eq`, `gte`, `and`, `or`, …) replace magic value strings. - πŸ›‘οΈ **The server has the last word** β€” a `Schema` declares what a client may request per parameter (allow-lists, defaults, mappings). Anything outside it is dropped β€” or throws, opt-in β€” and server-injected conditions (`query.filters.and(...)`) can't be displaced by client input. From 929053f20b61d3e2b485dc99d604aa42d50a9d78 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 7 Jul 2026 20:11:33 +0200 Subject: [PATCH 4/6] docs: frame README topology-neutral (service-to-service, not just client) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rapiq connects any two applications β€” browser to API or service to service (gateway forwarding) β€” so the tagline, pipeline table, installation groups and schema copy now say calling/receiving application and caller instead of client/server, and a gateway composition note follows the pipeline table. --- README.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 969695745..1178b9090 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@

rapiq

- Typed REST queries β€” from client to database.
- Rapiq (Rest Api Query) builds an efficient interface between client- & server-side applications β€”
- it defines a scheme for the request, but not for the response. + Typed REST queries β€” build, transport, validate, execute.
+ Rapiq (Rest Api Query) builds an efficient interface between applications β€”
+ browser β†” API just as well as service β†” service. It defines a scheme for the request, but not for the response.

@@ -41,13 +41,17 @@ Every REST list endpoint answers the same five questions: which **fields**, whic | Stage | What happens | |---|---| -| **Build** client | `defineQuery({ filters: { age: gte(18) }, sort: '-name' })` β€” typed input in, query AST out | +| **Build** calling application | `defineQuery({ filters: { age: gte(18) }, sort: '-name' })` β€” typed input in, query AST out | | **Transport** wire | encoded as a JSON-API-style query string: `?filter[age]=>=18&sort=-name` | -| **Validate** server | decoded back into the same AST, checked against a `Schema` β€” allow-lists, defaults, mappings | +| **Validate** receiving application | decoded back into the same AST, checked against a `Schema` β€” allow-lists, defaults, mappings | | **Execute** database | applied as parameterized SQL (`@rapiq/sql`) or to a TypeORM `SelectQueryBuilder` (`@rapiq/typeorm`) | +The two ends are just applications. A browser querying an API is the common case, but services compose the same way β€” +an API gateway, for instance, validates an incoming query against its own schema, scopes it +(`query.filters.and(...)`) and re-encodes it for the upstream service. + - 🧭 **Typed end to end** β€” every field path in `defineQuery` is checked against the record type; condition helpers (`eq`, `gte`, `and`, `or`, …) replace magic value strings. -- πŸ›‘οΈ **The server has the last word** β€” a `Schema` declares what a client may request per parameter (allow-lists, defaults, mappings). Anything outside it is dropped β€” or throws, opt-in β€” and server-injected conditions (`query.filters.and(...)`) can't be displaced by client input. +- πŸ›‘οΈ **The receiving side has the last word** β€” a `Schema` declares what a caller may request per parameter (allow-lists, defaults, mappings). Anything outside it is dropped β€” or throws, opt-in β€” and injected conditions (`query.filters.and(...)`) can't be displaced by caller input. - πŸ” **Loss-free transport** β€” within each codec dialect, `decode(encode(query))` restores the same query; outside its subset, encoding fails loudly with a typed error instead of silently changing semantics. - πŸ”Œ **Any backend** β€” the AST is consumed through visitors: parameterized SQL fragments with presets for Postgres, MySQL, SQLite, MSSQL & Oracle, or applied straight to a TypeORM `SelectQueryBuilder`. - πŸ“¦ **Composable packages** β€” no monolith: install only what each side needs; `@rapiq/core` is the single shared foundation. @@ -58,13 +62,13 @@ Version 2 splits the former single `rapiq` package into focused, composable `@ra there is **no** `rapiq` umbrella package for v2, install only what you need (see [Packages](#packages) below; `@rapiq/core` is a peer dependency of every other package). -Client side β€” build queries and encode them as URL query strings: +Querying application β€” build queries and encode them as URL query strings: ```bash npm install @rapiq/core @rapiq/codec-url-simple ``` -Server side β€” decode & validate incoming query input and apply it to the database: +Queried application β€” decode & validate incoming query input and apply it to the database: ```bash npm install @rapiq/core @rapiq/codec-url-simple @rapiq/sql @rapiq/typeorm @@ -83,8 +87,8 @@ Filters accept scalars, arrays (`null` is a legal element), `$`-operator objects [condition helpers](https://rapiq.tada5hi.net/guide/build#condition-helpers) (`eq`, `gte`, `and`, `or`, …); queries compose with [mergeQueries](https://rapiq.tada5hi.net/guide/merge). -The query is serialized for transport by the URL codec (`@rapiq/codec-url-simple`) and parsed back -into the same AST on the server side. +The query is serialized for transport by the URL codec (`@rapiq/codec-url-simple`) and decoded back +into the same AST on the receiving side. ```typescript import { defineQuery } from '@rapiq/core'; @@ -133,9 +137,9 @@ const response = await fetch(`/users?${queryString}`); ### Parse πŸ”Ž -On the server side the incoming query is decoded back into the same +On the receiving side the incoming query is decoded back into the same [Query](https://rapiq.tada5hi.net/guide/query) AST. A -[Schema](https://rapiq.tada5hi.net/guide/schema) declares what a client may request per parameter +[Schema](https://rapiq.tada5hi.net/guide/schema) declares what a caller may request per parameter (allow-lists, defaults, mappings) β€” anything outside it is silently dropped (set `throwOnFailure: true` on the schema to get a `ParseError` instead). The decoded query is then applied to the database by an adapter From d83a594e4be611cd79177ee6269f457c5b890d5d Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 7 Jul 2026 20:16:55 +0200 Subject: [PATCH 5/6] docs: add per-package READMEs Every workspace package gets an npm-facing README (title, install, usage sourced from the corresponding VitePress integration/guide page, docs link, license) so the packages are documented on npm and in directory views even though the main docs live on the VitePress site. Links use absolute URLs so they survive npm publishing. --- packages/codec-url-expression/README.md | 42 +++++++++++++++++ packages/codec-url-simple/README.md | 56 ++++++++++++++++++++++ packages/codec-url/README.md | 43 +++++++++++++++++ packages/core/README.md | 63 +++++++++++++++++++++++++ packages/docs/README.md | 13 +++++ packages/parser-expression/README.md | 45 ++++++++++++++++++ packages/parser-simple/README.md | 52 ++++++++++++++++++++ packages/sql/README.md | 58 +++++++++++++++++++++++ packages/typeorm/README.md | 50 ++++++++++++++++++++ 9 files changed, 422 insertions(+) create mode 100644 packages/codec-url-expression/README.md create mode 100644 packages/codec-url-simple/README.md create mode 100644 packages/codec-url/README.md create mode 100644 packages/core/README.md create mode 100644 packages/docs/README.md create mode 100644 packages/parser-expression/README.md create mode 100644 packages/parser-simple/README.md create mode 100644 packages/sql/README.md create mode 100644 packages/typeorm/README.md diff --git a/packages/codec-url-expression/README.md b/packages/codec-url-expression/README.md new file mode 100644 index 000000000..873d34f57 --- /dev/null +++ b/packages/codec-url-expression/README.md @@ -0,0 +1,42 @@ +# @rapiq/codec-url-expression + +Part of [rapiq](https://github.com/tada5hi/rapiq) β€” typed REST queries: build, transport, validate, execute. + +URL codec for the [expression dialect](https://rapiq.tada5hi.net/integrations/expression): the filters parameter crosses the URL boundary as a single expression β€” nested `and`/`or` compounds first-class β€” while the other four parameters share the [simple codec](https://www.npmjs.com/package/@rapiq/codec-url-simple)'s wire format. + +## Installation + +```sh +npm install @rapiq/core @rapiq/parser-expression @rapiq/codec-url-simple @rapiq/codec-url-expression +``` + +## Usage + +```typescript +import { URLDecoder, URLEncoder } from '@rapiq/codec-url-expression'; +import { and, defineQuery, eq, gte, or } from '@rapiq/core'; + +const encoder = new URLEncoder(); + +const query = defineQuery({ + filters: and(eq('name', 'John'), or(gte('age', 18), eq('email', null))), + pagination: { limit: 20 }, +}); + +encoder.encode(query); +// filter=and(eq(name,'John'),or(gte(age,'18'),eq(email,null)))&page[limit]=20 +``` + +`URLDecoder` reverses it, delegating filters to the [`ExpressionParser`](https://www.npmjs.com/package/@rapiq/parser-expression) β€” pass a `SchemaRegistry` and `{ schema }` to validate untrusted input. + +The expressible subset is wider than the simple dialect's: nested compounds, several conditions on the same field and comma-containing strings all round-trip. Still outside it β€” and loudly rejected on encode with typed errors β€” are the `regex`/`mod`/`exists`/`elemMatch` operators, match text coercing to a non-string, and field segments colliding with grammar keywords. + +The package exports its codec identifier (`URL_EXPRESSION_CODEC`) for out-of-band negotiation; for in-band dispatch between dialects see [@rapiq/codec-url](https://www.npmjs.com/package/@rapiq/codec-url). + +## Documentation + +Full guide: [rapiq.tada5hi.net/integrations/url#expression-dialect](https://rapiq.tada5hi.net/integrations/url#expression-dialect) + +## License + +Published under the [MIT License](https://github.com/tada5hi/rapiq/blob/master/LICENSE). diff --git a/packages/codec-url-simple/README.md b/packages/codec-url-simple/README.md new file mode 100644 index 000000000..621b4dc74 --- /dev/null +++ b/packages/codec-url-simple/README.md @@ -0,0 +1,56 @@ +# @rapiq/codec-url-simple + +Part of [rapiq](https://github.com/tada5hi/rapiq) β€” typed REST queries: build, transport, validate, execute. + +Moves a [`Query`](https://rapiq.tada5hi.net/guide/query) over the wire: `URLEncoder` turns the AST into a JSON:API-style URL query string, `URLDecoder` turns a query string (or an express-style `req.query` object) back into the AST. + +## Installation + +```sh +npm install @rapiq/core @rapiq/parser-simple @rapiq/codec-url-simple +``` + +## Usage + +### Encoding + +```typescript +import { URLEncoder } from '@rapiq/codec-url-simple'; + +const encoder = new URLEncoder(); +const queryString = encoder.encode(query); +// fields=id,name&filter[age]=>=18&page[limit]=25&include=realm&sort=-age +``` + +Pass a `SchemaRegistry` to the constructor and `{ schema }` to `encode` for schema-aware encoding β€” disallowed keys are dropped (or throw with `throwOnFailure`), aliases resolve, `maxLimit` clamps, exactly mirroring the receiving side's decoder. + +### Decoding + +```typescript +import { URLDecoder } from '@rapiq/codec-url-simple'; + +const decoder = new URLDecoder(registry); + +// from a raw query string ... +const query = decoder.decode('filter[age]=>=18&sort=-age&page[limit]=25', { schema: 'user' }); + +// ... or from an already parsed query object (express req.query) +app.get('/users', (req, res) => { + const query = decoder.decode(req.query, { schema: 'user' }); + // ... +}); +``` + +The decoder maps the wire names (`filter`, `page`, `include`, …) to the canonical parameters and delegates to a schema-aware [`SimpleParser`](https://www.npmjs.com/package/@rapiq/parser-simple). Per-parameter helpers (`encodeFields`/`decodeFields`, …) are exported too. + +### The round-trip guarantee + +Within the dialect's expressible subset, `decode(encode(query)) ≍ query` β€” equal up to scalar type normalization (the wire is untyped: `'5'` β†’ `5`, `'true'` β†’ `true`). Outside the subset β€” `or(...)` compounds, multiple conditions per field, `regex`/`mod`/`exists`/`elemMatch`, values colliding with operator markers β€” `encode` throws a typed error instead of silently changing the query's meaning. For a wider wire subset (nested compounds), use [@rapiq/codec-url-expression](https://www.npmjs.com/package/@rapiq/codec-url-expression). + +## Documentation + +Full guide (wire format, operator support matrix): [rapiq.tada5hi.net/integrations/url](https://rapiq.tada5hi.net/integrations/url) + +## License + +Published under the [MIT License](https://github.com/tada5hi/rapiq/blob/master/LICENSE). diff --git a/packages/codec-url/README.md b/packages/codec-url/README.md new file mode 100644 index 000000000..0f2aceb59 --- /dev/null +++ b/packages/codec-url/README.md @@ -0,0 +1,43 @@ +# @rapiq/codec-url + +Part of [rapiq](https://github.com/tada5hi/rapiq) β€” typed REST queries: build, transport, validate, execute. + +Makes URL payloads self-describing across codec dialects: encoding through the `URLCodecRegistry` stamps a reserved `codec` parameter, decoding dispatches on it. Useful wherever the receiving side cannot know out-of-band which dialect produced a payload β€” a gateway forwarding queries it did not author, for instance. + +## Installation + +```sh +npm install @rapiq/core @rapiq/codec-url-simple @rapiq/codec-url-expression @rapiq/codec-url +``` + +## Usage + +```typescript +import { createURLCodecRegistry } from '@rapiq/codec-url'; + +const codecs = createURLCodecRegistry(schemaRegistry); + +codecs.encode(query); +// codec=url-simple&filter[name]=John + +codecs.encode(query, { codec: 'url-expression' }); +// codec=url-expression&filter=or(eq(name,'John'),gte(age,'18')) + +codecs.decode('codec=url-expression&filter=or(...)', { schema: 'user' }); +codecs.decode('filter[name]=John'); // no stamp β†’ default codec (simple) +``` + +Dispatch rules: + +- An **unstamped** payload falls back to the registry default β€” plain callers and hand-written URLs keep working. +- A payload naming an **unregistered** codec throws a typed `CodecError` (`ErrorCode.CODEC_UNRESOLVABLE`) β€” it is never silently mis-decoded under another dialect. + +`createURLCodecRegistry()` bundles the [simple](https://www.npmjs.com/package/@rapiq/codec-url-simple) and [expression](https://www.npmjs.com/package/@rapiq/codec-url-expression) dialects with `url-simple` as the default. Custom codecs implement the `URLCodec` shape (`{ name, encoder, decoder }`) and register on a plain `URLCodecRegistry`. + +## Documentation + +Full guide: [rapiq.tada5hi.net/integrations/url#codec-registry](https://rapiq.tada5hi.net/integrations/url#codec-registry) + +## License + +Published under the [MIT License](https://github.com/tada5hi/rapiq/blob/master/LICENSE). diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 000000000..afeaf73ad --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,63 @@ +# @rapiq/core + +The foundation of [rapiq](https://github.com/tada5hi/rapiq) β€” typed REST queries: build, transport, validate, execute. + +This package owns the query AST (`Query` with fields, filters, pagination, relations & sorts), the typed build layer, the schema system and the visitor interfaces every other `@rapiq/*` package builds on. + +## Installation + +```sh +npm install @rapiq/core +``` + +## Usage + +### Build a query + +`defineQuery` builds the AST directly from typed input β€” every field path is checked against the record generic: + +```typescript +import { defineQuery, gte, or, eq } from '@rapiq/core'; + +const query = defineQuery({ + fields: ['id', 'name'], + filters: or(gte('age', 18), eq('deleted_at', null)), + relations: ['realm'], + sort: '-created_at', + pagination: { limit: 10 }, +}); +``` + +Filters accept scalars (`{ name: 'John' }`), bare arrays (`in`, `null` is a legal element), `$`-operator objects (`{ age: { $gte: 18 } }`) and condition helpers (`eq`, `gte`, `inArray`, `and`, `or`, …). Queries compose immutably with `mergeQueries` (left priority) and the `Filters` combinators (`merge`, `and`, `or`). + +### Declare what a caller may request + +A `Schema` is the receiving side's allow-list β€” parsers and decoders validate incoming input against it: + +```typescript +import { SchemaRegistry, defineSchema } from '@rapiq/core'; + +const registry = new SchemaRegistry(); + +registry.add(defineSchema({ + name: 'user', + fields: { allowed: ['id', 'name', 'age'] }, + filters: { allowed: ['id', 'name', 'age'] }, + relations: { allowed: ['realm'] }, + sort: { allowed: ['id', 'age'] }, + pagination: { maxLimit: 50 }, + schemaMapping: { realm: 'realm' }, +})); +``` + +### Consume the AST + +Every node implements `accept(visitor)` β€” backends implement the visitor interfaces (`IQueryVisitor`, `IFiltersVisitor`, …) to walk a query into whatever they target. Ready-made adapters exist for [SQL](https://www.npmjs.com/package/@rapiq/sql) and [TypeORM](https://www.npmjs.com/package/@rapiq/typeorm); parsers and URL codecs live in their own packages as well. + +## Documentation + +Full guide: [rapiq.tada5hi.net](https://rapiq.tada5hi.net) β€” see [Building Queries](https://rapiq.tada5hi.net/guide/build), [Schemas](https://rapiq.tada5hi.net/guide/schema) and [Merging Queries](https://rapiq.tada5hi.net/guide/merge). + +## License + +Published under the [MIT License](https://github.com/tada5hi/rapiq/blob/master/LICENSE). diff --git a/packages/docs/README.md b/packages/docs/README.md new file mode 100644 index 000000000..2f0a0d4e4 --- /dev/null +++ b/packages/docs/README.md @@ -0,0 +1,13 @@ +# @rapiq/docs + +The [VitePress](https://vitepress.dev) documentation site for [rapiq](https://github.com/tada5hi/rapiq), published at [rapiq.tada5hi.net](https://rapiq.tada5hi.net). Private β€” not published to npm. + +## Development + +```sh +# from the repo root +npm run dev --workspace=packages/docs # local dev server +npm run build --workspace=packages/docs # production build (runs in CI) +``` + +Content lives in `getting-started/`, `guide/` and `integrations/`; navigation and sidebar are configured in `.vitepress/config.mjs`. diff --git a/packages/parser-expression/README.md b/packages/parser-expression/README.md new file mode 100644 index 000000000..a0b631983 --- /dev/null +++ b/packages/parser-expression/README.md @@ -0,0 +1,45 @@ +# @rapiq/parser-expression + +Part of [rapiq](https://github.com/tada5hi/rapiq) β€” typed REST queries: build, transport, validate, execute. + +Parses a function-call expression language for filters β€” useful when a single string must carry a complex condition tree (search boxes, saved filters, CLI flags): + +```txt +and(eq(name, 'John'), gte(age, '18')) +or(in(status, 'active', 'pending'), gt(age, '65')) +not(contains(user.name, 'Bob')) +``` + +## Installation + +```sh +npm install @rapiq/core @rapiq/parser-simple @rapiq/parser-expression +``` + +## Usage + +```typescript +import { ExpressionParser } from '@rapiq/parser-expression'; + +const parser = new ExpressionParser(registry); + +const query = parser.parse({ + filters: "and(eq(name, 'John'), gte(age, '18'))", + sort: '-age', + pagination: { limit: 25 }, +}, { schema: 'user' }); +``` + +Only the `filters` parameter uses the expression language β€” fields, relations, pagination and sort accept the same input as [@rapiq/parser-simple](https://www.npmjs.com/package/@rapiq/parser-simple), and the whole thing returns the same [`Query`](https://rapiq.tada5hi.net/guide/query) AST. A standalone `parseFilters(input, options)` returns just the `Filters` node. + +The grammar: `eq`, `ne`, `lt`, `lte`, `gt`, `gte`, `in`, `nin`, `contains`, `startsWith`, `endsWith` (and negations) as leaf conditions, composed with `and(…)` / `or(…)` / `not(…)`. Values are always single-quoted (`gte(age, '18')`); quoted numerals coerce to numbers, `'true'`/`'false'` to booleans, `'null'` to `null`. + +Syntax errors and schema violations throw `FiltersParseError` immediately β€” the expression parser has no silent-drop mode for malformed expressions. + +## Documentation + +Full guide (grammar & operator table): [rapiq.tada5hi.net/integrations/expression](https://rapiq.tada5hi.net/integrations/expression) + +## License + +Published under the [MIT License](https://github.com/tada5hi/rapiq/blob/master/LICENSE). diff --git a/packages/parser-simple/README.md b/packages/parser-simple/README.md new file mode 100644 index 000000000..5c1825bac --- /dev/null +++ b/packages/parser-simple/README.md @@ -0,0 +1,52 @@ +# @rapiq/parser-simple + +Part of [rapiq](https://github.com/tada5hi/rapiq) β€” typed REST queries: build, transport, validate, execute. + +Parses plain object/array input β€” the URL-query-like "simple" dialect β€” into a [`Query`](https://rapiq.tada5hi.net/guide/query) AST, validated against a schema. It is the workhorse parser the [URL codec](https://www.npmjs.com/package/@rapiq/codec-url-simple) builds on. + +## Installation + +```sh +npm install @rapiq/core @rapiq/parser-simple +``` + +## Usage + +```typescript +import { SchemaRegistry, defineSchema } from '@rapiq/core'; +import { SimpleParser } from '@rapiq/parser-simple'; + +const registry = new SchemaRegistry(); +registry.add(defineSchema({ + name: 'user', + fields: { allowed: ['id', 'name', 'age'] }, + filters: { allowed: ['id', 'name', 'age'] }, + relations: { allowed: ['realm'] }, + sort: { allowed: ['id', 'age'] }, + pagination: { maxLimit: 50 }, +})); + +const parser = new SimpleParser(registry); + +const query = parser.parse({ + fields: ['id', 'name'], + filters: { name: '~jo~', age: '>=18' }, + relations: ['realm'], + sort: '-age', + pagination: { limit: 25 }, +}, { schema: 'user' }); +``` + +Anything outside the schema's allow-lists is silently dropped; set `throwOnFailure: true` on the schema to get a `ParseError` instead. Parameters absent from the input still receive schema defaults. + +The parser is transport-agnostic: it reads the canonical parameter keys (`fields`, `filters`, `pagination`, `relations`, `sort`) only. To consume a raw URL query string or an express-style `req.query` object (JSON:API wire names like `filter`, `page`, `include`), use the [URL codec](https://www.npmjs.com/package/@rapiq/codec-url-simple) β€” its decoder maps the wire names and delegates to this parser. + +Per-parameter parser classes (`SimpleFieldsParser`, `SimpleFiltersParser`, `SimplePaginationParser`, `SimpleRelationsParser`, `SimpleSortParser`) are exported for parsing a single parameter. + +## Documentation + +Full guide: [rapiq.tada5hi.net/integrations/simple](https://rapiq.tada5hi.net/integrations/simple) β€” per-parameter input shapes and operator syntax are on the [parameter pages](https://rapiq.tada5hi.net/guide/filters). + +## License + +Published under the [MIT License](https://github.com/tada5hi/rapiq/blob/master/LICENSE). diff --git a/packages/sql/README.md b/packages/sql/README.md new file mode 100644 index 000000000..2c7acaeb0 --- /dev/null +++ b/packages/sql/README.md @@ -0,0 +1,58 @@ +# @rapiq/sql + +Part of [rapiq](https://github.com/tada5hi/rapiq) β€” typed REST queries: build, transport, validate, execute. + +Turns query AST nodes into **parameterized SQL fragments**. Database-agnostic: per-database behavior is injected as a small dialect option object (presets for `pg`, `mysql`, `sqlite`, `mssql`, `oracle`), and values are always bound as parameters β€” never interpolated. It is also the foundation the [TypeORM adapter](https://www.npmjs.com/package/@rapiq/typeorm) builds on. + +## Installation + +```sh +npm install @rapiq/core @rapiq/sql +``` + +## Usage + +A root `Adapter` bundles one sub-adapter per parameter, `QueryVisitor` walks a whole `Query` into it, and `build()` returns the accumulated clause fragments: + +```typescript +import { Adapter, QueryVisitor, pg } from '@rapiq/sql'; + +const adapter = new Adapter({ ...pg, rootAlias: 'user' }); +query.accept(new QueryVisitor(adapter)); + +const fragments = adapter.build(); +// { +// columns: ['"user"."id"', '"user"."name"', '"realm"."name"'], +// where: '("user"."age" >= $1 and ...)', +// params: [18, ...], +// orderBy: ['"user"."age" DESC'], +// limit: 25, +// offset: 50, +// relations: ['realm'], +// } +``` + +Per-parameter adapter/visitor pairs work standalone β€” e.g. rendering just the filters: + +```typescript +import { FiltersAdapter, FiltersVisitor, RelationsAdapter, pg } from '@rapiq/sql'; + +const filters = new FiltersAdapter(new RelationsAdapter(), pg); +query.filters.accept(new FiltersVisitor(filters)); + +const [sql, params] = filters.getQueryAndParameters(); +// sql: ("name" ~* $1 and "age" >= $2) +// params: ['jo', 18] +``` + +The package deliberately stops at fragments: composing the final `SELECT` β€” in particular `FROM`/`JOIN` conditions β€” is the caller's job, or a backend adapter's (that's exactly what [@rapiq/typeorm](https://www.npmjs.com/package/@rapiq/typeorm) does). + +Notable semantics: `null` filter values render as `IS NULL` / `IS NOT NULL` predicates, empty `IN` lists render as `1 = 0` (never invalid SQL), string-matching operators match literally on every dialect, and `resolveDialect(name)` maps driver/connection type names (`postgres`, `mariadb`, `better-sqlite3`, …) to the matching preset. On dialects without a regexp operator (SQL Server, stock SQLite), `contains`/`startsWith`/`endsWith` fall back to escaped `LIKE`; only the `regex` operator throws a typed `AdapterError`. + +## Documentation + +Full guide (dialects, null semantics, fragment API): [rapiq.tada5hi.net/integrations/sql](https://rapiq.tada5hi.net/integrations/sql) + +## License + +Published under the [MIT License](https://github.com/tada5hi/rapiq/blob/master/LICENSE). diff --git a/packages/typeorm/README.md b/packages/typeorm/README.md new file mode 100644 index 000000000..8825a7c00 --- /dev/null +++ b/packages/typeorm/README.md @@ -0,0 +1,50 @@ +# @rapiq/typeorm + +Part of [rapiq](https://github.com/tada5hi/rapiq) β€” typed REST queries: build, transport, validate, execute. + +Applies a parsed [`Query`](https://rapiq.tada5hi.net/guide/query) directly to a TypeORM `SelectQueryBuilder` β€” filters become parameterized `WHERE` conditions, relations become joins, fields/sort/pagination map to `select`/`orderBy`/`take`+`skip`. + +## Installation + +```sh +npm install @rapiq/core @rapiq/sql @rapiq/typeorm +``` + +## Usage + +```typescript +import { QueryVisitor } from '@rapiq/sql'; +import { TypeormAdapter } from '@rapiq/typeorm'; + +const queryBuilder = dataSource.getRepository(User).createQueryBuilder('user'); + +const adapter = new TypeormAdapter({ + relations: { joinAndSelect: true }, +}); +adapter.withQuery(queryBuilder); + +query.accept(new QueryVisitor(adapter)); +const { pagination } = adapter.execute(); + +const [entities, total] = await queryBuilder.getManyAndCount(); +``` + +The flow is always the same: + +1. `withQuery(queryBuilder)` β€” attach the builder. +2. `query.accept(new QueryVisitor(adapter))` β€” walk the AST; the adapter collects state. +3. `adapter.execute()` β€” apply everything to the builder in one go; it returns the applied pagination (e.g. for the response `meta` block). + +The SQL dialect is resolved from the attached builder's connection type; joins are applied idempotently and validated against the entity metadata. Options: `relations.joinAndSelect` (hydrate related entities), `relations.joinType` (`'left'` default / `'inner'`), and an `onJoin(path, alias, queryBuilder)` hook per applied join. Per-parameter visitors from [@rapiq/sql](https://www.npmjs.com/package/@rapiq/sql) work against the adapter's sub-adapters (`adapter.filters`, `adapter.sort`, …) when only part of a query applies. + +Typically the query comes from a [URL decoder](https://www.npmjs.com/package/@rapiq/codec-url-simple) validating `req.query` against a schema β€” see the [end-to-end example](https://rapiq.tada5hi.net/integrations/typeorm#end-to-end-example) in the docs. + +Migrating from typeorm-extension's `applyQuery`? The defaults mirror its contract (`leftJoinAndSelect`, returned pagination) β€” see the [migration guide](https://rapiq.tada5hi.net/guide/migration). + +## Documentation + +Full guide (options, dialect detection, alias convention): [rapiq.tada5hi.net/integrations/typeorm](https://rapiq.tada5hi.net/integrations/typeorm) + +## License + +Published under the [MIT License](https://github.com/tada5hi/rapiq/blob/master/LICENSE). From d5d41b333a492ab64786ca0921ec2b76fe8e762c Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 7 Jul 2026 20:20:21 +0200 Subject: [PATCH 6/6] docs: make example snippets self-contained & null-safe Addresses PR #750 review comments: the README parse example now imports its dataSource, the quick-start and typeorm end-to-end examples guard the nullable URLDecoder.decode result before use, the typeorm end-to-end example gains its missing express/dataSource imports, and the quick-start tip constructs the SimpleParser it references instead of using an undeclared variable. --- README.md | 2 ++ packages/docs/getting-started/quick-start.md | 6 +++++- packages/docs/integrations/typeorm.md | 7 +++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1178b9090..0f6fcb580 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,8 @@ import { SchemaRegistry, defineSchema } from '@rapiq/core'; import { URLDecoder } from '@rapiq/codec-url-simple'; import { QueryVisitor } from '@rapiq/sql'; import { TypeormAdapter } from '@rapiq/typeorm'; +// your app's TypeORM DataSource instance +import { dataSource } from './data-source'; const registry = new SchemaRegistry(); diff --git a/packages/docs/getting-started/quick-start.md b/packages/docs/getting-started/quick-start.md index f22af7204..68bde6c71 100644 --- a/packages/docs/getting-started/quick-start.md +++ b/packages/docs/getting-started/quick-start.md @@ -74,12 +74,16 @@ const decoder = new URLDecoder(registry); // accepts the raw query string as well as a pre-parsed object (express req.query); // URL wire names (filter, page, include, ...) map to their canonical parameters. const query = decoder.decode(req.query, { schema: 'user' }); +if (!query) { + // decode returns null for non-object input β€” e.g. reply 400 Bad Request + throw new Error('Invalid query input.'); +} ``` Anything outside the allow-lists is silently dropped; set `throwOnFailure: true` on the schema to get a `ParseError` instead. See [Schemas](/guide/schema). ::: tip Canonical object input -If your input isn't URL-shaped β€” it already uses the canonical parameter keys (`filters`, `pagination`, `relations`, …) β€” feed it to [`SimpleParser`](/integrations/simple) from `@rapiq/parser-simple` directly: `parser.parse(input, { schema: 'user' })`. The `URLDecoder` builds on it. +If your input isn't URL-shaped β€” it already uses the canonical parameter keys (`filters`, `pagination`, `relations`, …) β€” feed it to [`SimpleParser`](/integrations/simple) from `@rapiq/parser-simple` directly: `new SimpleParser(registry).parse(input, { schema: 'user' })`. The `URLDecoder` builds on it. ::: ## 3. Apply to the database (server) diff --git a/packages/docs/integrations/typeorm.md b/packages/docs/integrations/typeorm.md index f09595e62..cc0831d04 100644 --- a/packages/docs/integrations/typeorm.md +++ b/packages/docs/integrations/typeorm.md @@ -84,10 +84,13 @@ The same pattern works with `FieldsVisitor` (`adapter.fields`), `SortsVisitor` ( ## End-to-end example ```typescript +import { Request, Response } from 'express'; import { SchemaRegistry, defineSchema } from '@rapiq/core'; import { URLDecoder } from '@rapiq/codec-url-simple'; import { QueryVisitor } from '@rapiq/sql'; import { TypeormAdapter } from '@rapiq/typeorm'; +// your app's TypeORM DataSource instance +import { dataSource } from './data-source'; const registry = new SchemaRegistry(); registry.add(defineSchema({ @@ -105,6 +108,10 @@ const decoder = new URLDecoder(registry); export async function getUsers(req: Request, res: Response) { // wire names (filter, page, include, ...) map to canonical parameters const query = decoder.decode(req.query, { schema: 'user' }); + if (!query) { + // decode returns null for non-object input + return res.status(400).end(); + } const queryBuilder = dataSource.getRepository(User).createQueryBuilder('user');