@trapi/query 🌈
Important NOTE
This package has been replaced by another package, called: rapiq.
This is a library to build efficient and JSON:API
like REST-APIs.
It extends the specification format between request- & response-handling based on the following parameters:
fields
- Description: Return only specific fields or extend the default selection.
- URL-Parameter: fields
filters
- Description: Filter the data set, according to specific criteria.
- URL-Parameter: filter
relations
- Description: Include related resources of the primary data.
- 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
Table of Contents
Installation
npm install @trapi/query --save
Usage
Build 🏗
The general idea is to construct a BuildInput object and convert it to a string
representation with the buildQuery method on the frontend side.
The result should then be provided as a URL query string to the backend application.
The backend application is then able to process the request, by parsing and interpreting the query string.
The following example should give an insight on how to use this library on the frontend side.
Therefore, a type which will represent a User
and a method getAPIUsers
are defined.
The method should perform a request to the resource API to receive a collection of entities.
import axios from "axios";
import {
buildQuery,
BuildInput
} from "@trapi/query";
type Profile = {
id: number;
avatar: string;
cover: string;
}
type User = {
id: number;
name: string;
age?: number;
profile: Profile;
}
type ResponsePayload = {
data: User[],
meta: {
limit: number,
offset: number,
total: number
}
}
export async function getAPIUsers(
record: BuildInput<User>
): Promise<ResponsePayload> {
const response = await axios.get('users' + buildQuery(record));
return response.data;
}
(async () => {
const record: BuildInput<User> = {
pagination: {
limit: 20,
offset: 10
},
filters: {
id: 1
},
fields: ['id', 'name'],
sort: '-id',
relations: {
profile: true
}
};
const query = buildQuery(record);
let response = await getAPIUsers(record);
})();
The next section will describe, on how to parse and interpret the query string on the backend side.
Parsing 🔎
For explanation purposes, two simple entities with a basic relation between them are declared to demonstrate the usage on the backend side:
entities.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
JoinColumn
} 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;
@OneToOne(() => Profile)
profile: Profile;
}
@Entity()
export class Profile {
@PrimaryGeneratedColumn({unsigned: true})
id: number;
@Column({type: 'varchar', length: 255, default: null, nullable: true})
avatar: string;
@Column({type: 'varchar', length: 255, default: null, nullable: true})
cover: string;
@OneToOne(() => User)
@JoinColumn()
user: User;
}
Parse - Detailed
In the following code snippet, all query parameter parse functions (parseQueryFields
, parseQueryFilters
, ...)
will be imported separately, to show the usage in detail.
import {Request, Response} from 'express';
import {
parseQueryFields,
parseQueryFilters,
parseQueryRelations,
parseQueryPagination,
parseQuerySort,
Parameter
} from "@trapi/query";
import {
applyQueryParseOutput,
useDataSource
} from 'typeorm-extension';
import { User } from './entities';
export async function getUsers(req: Request, res: Response) {
const {fields, filter, include, page, sort} = req.query;
const dataSource = await useDataSource();
const repository = dataSource.getRepository(User);
const query = repository.createQueryBuilder('user');
const relationsParsed = parseQueryRelations(include, {
allowed: 'profile'
});
const fieldsParsed = parseQueryFields(fields, {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id', 'profile.avatar'],
relations: relationsParsed
});
const filterParsed = parseQueryFilters(filter, {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id'],
relations: relationsParsed
});
const pageParsed = parseQueryPagination(page, {
maxLimit: 20
});
const sortParsed = parseQuerySort(sort, {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id'],
relations: relationsParsed
});
const parsed = applyQueryParseOutput(query, {
fields: fieldsParsed,
filters: filterParsed,
relations: relationsParsed,
pagination: pageParsed,
sort: sortParsed
});
const [entities, total] = await query.getManyAndCount();
return res.json({
data: {
data: entities,
meta: {
total,
...parsed.pagination
}
}
});
}
Parse - Short
Another way is to directly import the parseQuery method, which will handle a group of query parameter values & options.
The ParseInput argument of the parseQuery
method accepts multiple (alias-) property keys.
import { Request, Response } from 'express';
import {
parseQuery,
Parameter,
ParseOutput
} from '@trapi/query';
import {
applyQueryParseOutput,
useDataSource
} from 'typeorm-extension';
export async function getUsers(req: Request, res: Response) {
const output: ParseOutput = parseQuery(req.query, {
fields: {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id', 'profile.avatar']
},
filters: {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id']
},
relations: {
allowed: ['profile']
},
pagination: {
maxLimit: 20
},
sort: {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id']
}
});
const dataSource = await useDataSource();
const repository = dataSource.getRepository(User);
const query = repository.createQueryBuilder('user');
const parsed = applyQueryParseOutput(query, output);
const [entities, total] = await query.getManyAndCount();
return res.json({
data: {
data: entities,
meta: {
total,
...output.pagination
}
}
});
}
Parse - Third Party Library
It can even be much simpler to parse the query key-value pairs with the typeorm-extension
library, because it
uses this library under the hood ⚡.
This is much shorter than the previous example and has less explicit dependencies 😁.
read more
Functions
buildQuery
▸ function
buildQuery<T
>(record: BuildInput<T>
, options?: BuildOptions
): string
Build a query string from a provided BuildInput.
Example
Simple
import {
buildQuery,
Parameter
} from "@trapi/query";
type User = {
id: number,
name: string,
age?: number
}
const query: string = buildQuery<User>({
fields: ['+age'],
relations: {
name: '~pe'
}
});
console.log(query);
Type parameters
Name | Description |
---|
T | A type, interface, or class which represent the data structure. |
Parameters
Name | Type | Description |
---|
input | BuildInput <T > | Input specification more. |
options | BuildOptions | Options for building fields, filter, include, ... |
Returns
string
The function returns a string, which can be parsed with the parseQuery function.
I.e. /users?page[limit]=10&page[offset]=0&include=profile&filter[id]=1&fields[user]=id,name
parseQuery
▸ function
parseQuery(input: ParseInput
, options?: ParseOptions
): ParseOutput
Parse a query string to an efficient data structure ⚡. The output will
be an object with each possible value of the Parameter enum as property key and the
parsed data as value.
Example
Simple
import {
FieldOperator,
FilterOperator,
parseQuery,
ParseOutput,
URLParameter
} from "@trapi/query";
const output: ParseOutput = parseQuery({
fields: ['+age'],
filters: {
name: '~pe'
}
});
console.log(output);
Type parameters
Parameters
Name | Type | Description |
---|
input | ParseInput | Query input data passed e.g. via URL more. |
options | ParseOptions | Options for parsing fields, filter, include, ... more |
Returns
ParseOutput
The function returns an object.
parseQueryParameter
▸ function
parseQueryParameter<T extends Parameter
>(
key: T
,
input: unknown
,
options?: ParseParameterOptions<T>
): ParseParameterOutput<T>
Parse a specific query Parameter value to an efficient data structure ⚡.
Example
fields
import {
FieldOperator,
FieldsParseOutput,
Parameter,
parseQueryParameter,
URLParameter
} from "@trapi/query";
const output: FieldsParseOutput = parseQueryParameter(
'fields',
['+name'],
{
allowed: ['id', 'name'],
defaultAlias: 'user'
}
);
console.log(output);
filters
import {
FiltersParseOutput,
Parameter,
parseQueryParameter,
URLParameter
} from "@trapi/query";
const output: FiltersParseOutput = parseQueryParameter(
'filters',
{id: 1},
{
allowed: ['id', 'name'],
defaultAlias: 'user'
}
);
console.log(output);
pagination
import {
PaginationParseOutput,
Parameter,
parseQueryParameter,
URLParameter
} from "@trapi/query";
const output: PaginationParseOutput = parseQueryParameter(
'pagination',
{limit: 100},
{
maxLimit: 50
}
);
console.log(output);
relations
import {
RelationsParseOutput,
Parameter,
parseQueryParameter,
URLParameter
} from "@trapi/query";
const output: RelationsParseOutput = parseQueryParameter(
'relations',
['roles'],
{
allowed: ['roles', 'photos'],
defaultAlias: 'user'
}
);
console.log(output);
sort
import {
SortParseOutput,
Parameter,
parseQueryParameter,
URLParameter
} from "@trapi/query";
const output: SortParseOutput = parseQueryParameter(
'sort',
['-name'],
{
allowed: ['id', 'name'],
defaultAlias: 'user'
}
);
console.log(output);
Type parameters
Parameters
Name | Type | Description |
---|
input | unknown | Query input data passed e.g. via URL more. |
options | ParseParameterOptions<Parameter> | Options for parsing fields, filter, include, ... more |
Returns
ParseOutput
The function returns an object.
Types
Build
BuildOptions
export type BuildOptions = {
}
BuildInput
export type BuildInput<
V extends Record<string, any>
> = {
[T in Parameter | URLParameter]?: BuildParameterInput<T, V>
}
Parse
ParseOptions
export type ParseOptions = {
[K in Parameter]?: ParseParameterOptions<K> | boolean
}
ParseParameterOptions
ParseInput
export type ParseInput = {
[K in Parameter | URLParameter]?: any
}
Parameter/URLParameter
ParseOutput
export type ParseOutput = {
[K in Parameter]?: ParseParameterOutput<K>
}
ParseParameterOutput
ParseParameter
ParseParameterOptions<T extends ParameterType | URLParameterType>
is a generic type and returns the available options for a given parameter type, e.g.
ParseParameterOutput<T extends ParameterType | URLParameterType>
is a generic type and returns the parsed output data for a given parameter type, e.g.
Parameter
Parameter
export enum Parameter {
FILTERS = 'filters',
FIELDS = 'fields',
PAGINATION = 'pagination',
RELATIONS = 'relations',
SORT = 'sort'
}
export type ParameterType = `${Parameter}`;
URLParameter
export enum URLParameter {
FILTERS = 'filter',
FIELDS = 'fields',
PAGINATION = 'page',
RELATIONS = 'include',
SORT = 'sort'
}
export type URLParameterType = `${Parameter}`;
Fields
FieldsParseOptions
export type FieldsParseOptions =
ParseOptionsBase<Parameter.FIELDS, Record<string, string[]> | string[]>;
The type structure looks like this:
{
aliasMapping?: Record<string, string>,
allowed?: Record<string, string[]> | string[],
relations?: RelationsParseOutput,
defaultAlias?: string
}
FieldsParseOutput
export enum FieldOperator {
INCLUDE = '+',
EXCLUDE = '-'
}
export type FieldsParseOutputElement =
ParseOutputElementBase<Parameter.FIELDS, FieldOperator>;
export type FieldsParseOutput =
FieldsParseOutputElement[];
The type structure looks like this:
{
// relation/resource alias
alias?: string,
// field name
key: string,
// '+' | '-'
value?: FieldOperator
}
Filters
FiltersParseOptions
export type FiltersParseOptions =
ParseOptionsBase<Parameter.FILTERS>
The type structure looks like this:
{
aliasMapping?: Record<string, string>,
allowed?: string[],
relations?: RelationsParseOutput,
defaultAlias?: string
}
FiltersParseOutput
export enum FilterOperatorLabel {
NEGATION = 'negation',
LIKE = 'like',
IN = 'in'
}
export type FiltersParseOutputElement =
ParseOutputElementBase<
Parameter.FILTERS,
FilterValue<string | number | boolean | null>
> & {
operator?: {
[K in FilterOperatorLabel]?: boolean
}
};
export type FiltersParseOutput = FiltersParseOutputElement[];
{
// relation/resource alias
alias?: string,
// filter name
key: string,
// {in: ..., ...}
operator?: {
[K in FilterOperatorLabel]?: boolean
},
value: FilterValue<string | number | boolean | null>
}
PaginationParseOptions
export type PaginationParseOptions =
ParseOptionsBase<Parameter.PAGINATION> & {
maxLimit?: number
};
The type structure looks like this:
{
maxLimit?: number
}
PaginationParseOutput
export type PaginationParseOutput = {
limit?: number,
offset?: number
};
Relations
RelationsParseOptions
export type RelationsParseOptions =
ParseOptionsBase<Parameter.SORT, string[] | string[][]>;
The type structure looks like this:
{
aliasMapping?: Record<string, string>,
allowed?: string[],
defaultAlias?: string,
includeParents?: boolean | string[] | string
}
RelationsParseOutput
export type RelationsParseOutputElement =
ParseOutputElementBase<Parameter.RELATIONS, string>;
export type RelationsParseOutput = RelationsParseOutputElement[];
The type structure looks like this:
{
// relation relative depth path
key: string,
// relation alias
value: string
}
Sort
SortParseOptions
export type SortParseOptions = ParseOptionsBase<Parameter.SORT, string[] | string[][]>;
The type structure looks like this:
{
aliasMapping?: Record<string, string>,
allowed?: string[] | string[][],
defaultAlias?: string
relations?: RelationsParseOutput
}
SortParseOutput
export enum SortDirection {
ASC = 'ASC',
DESC = 'DESC'
}
export type SortParseOutputElement =
ParseOutputElementBase<Parameter.SORT, SortDirection>;
export type SortParseOutput = SortParseOutputElement[];
The type structure looks like this:
{
// resource/relation alias
alias?: string,
// field name
key: string,
// 'ASC' | 'DESC'
value: SortDirection
}