Schemas
A Schema<RECORD> is the server-side allow-list: it declares what a client may request, per parameter. Parsers consult it during parsing — anything outside the allow-lists is silently dropped, or throws when throwOnFailure is set.
Defining a schema
import { SchemaRegistry, defineSchema } from '@rapiq/core';
type User = {
id: number,
name: string,
email: string,
age: number,
realm: Realm,
items: Item[],
};
const userSchema = defineSchema<User>({
name: 'user',
fields: {
allowed: ['id', 'name', 'email', 'age'],
default: ['id', 'name'],
},
filters: {
allowed: ['id', 'name', 'age'],
},
relations: {
allowed: ['realm', 'items'],
},
sort: {
allowed: ['id', 'name', 'age'],
default: { id: 'DESC' },
},
pagination: {
maxLimit: 50,
},
schemaMapping: {
realm: 'realm',
items: 'item',
},
});Field keys are typed against RECORD via recursive key paths — allowed and default autocomplete and type-check.
Top-level options
| Option | Type | Description |
|---|---|---|
name | string | Registry key; also used to resolve nested schemas. |
throwOnFailure | boolean | Throw on disallowed input instead of dropping it. Inherited by every sub-schema that doesn't set its own value. |
schemaMapping | Record<string, string> | Maps a relation name to a registered schema name, so nested input (realm.name) validates against the related record's schema. |
Per-parameter options
Every sub-schema also accepts its own throwOnFailure.
| Parameter | Options |
|---|---|
fields | allowed, default, mapping (alias → field) |
filters | allowed, default (a default condition), mapping, validate (per-filter validation hook) |
relations | allowed, mapping |
sort | allowed (flat list, or list of lists to enforce exact multi-key combinations), default, mapping |
pagination | maxLimit |
Standalone factories exist for each parameter — defineFieldsSchema, defineFiltersSchema, defineRelationsSchema, defineSortSchema, definePaginationSchema — useful when calling a single parameter parser directly.
TIP
allowed: [] blocks the parameter entirely; omitting allowed permits everything. Be deliberate about which one you mean.
The registry
The SchemaRegistry stores schemas by name and resolves relation paths through schemaMapping:
const registry = new SchemaRegistry();
registry.add(realmSchema);
registry.add(userSchema);
registry.get('user'); // Schema<User> | undefined
registry.getOrFail('user'); // throws if missing
registry.resolve('user', 'items'); // → 'item' schema, via schemaMappingHand the registry to a parser and reference schemas by name:
import { SimpleParser } from '@rapiq/parser-simple';
const parser = new SimpleParser(registry);
const query = parser.parse(input, { schema: 'user' });With the mapping above, input like fields: { realm: ['name'] } or filters: { 'realm.name': 'master' } is validated against the realm schema's allow-lists.
Failure behavior
By default, parsers drop what the schema doesn't allow — the query still parses, minus the offending parts. With throwOnFailure: true (top-level or per parameter), parsers throw instead:
import { FiltersParseError } from '@rapiq/core';
try {
parser.parse({ filters: { secret: 'x' } }, { schema: 'user' });
} catch (e) {
if (e instanceof FiltersParseError) {
// e.code from ErrorCode, e.message names the offending key
}
}Each parameter has its own error class: FieldsParseError, FiltersParseError, PaginationParseError, RelationsParseError, SortParseError — all extend ParseError → BaseError.