@trapi/query 🌈
This is a library to build efficient and optimized 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
Important NOTE
The examples in the Parsing section, are not available with releases <1.0.0
of the typeorm-extension
library.
Table of Contents
Installation
npm install @trapi/query --save
Usage
Build 🏗
The general idea is to construct a BuildInput at the frontend side,
which will be formatted to a string
and passed to the backend application as a URL query string.
As a result the backend application will always able to process the request.
Therefore, two components of this module are required in the frontend application:
- generic type:
BuildInput<T>
- function:
buildQuery
.
The method will generate the query string.
In the following example a Class which will represent the structure of a User
and a function called
getAPIUsers
, which will handle the resource request to the resource API, will be defined.
import axios from "axios";
import {
buildQuery,
BuildInput
} from "@trapi/query";
class Profile {
id: number;
avatar: string;
cover: string;
}
class 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 examples will demonstrate how to parse and validate the transformed BuildInput<T>
on the backend side.
Parsing 🔎
For explanation purposes,
two simple entities with a basic relation between them are declared to demonstrate their usage:
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;
}
In this example typeorm is used for object-relational mapping (ORM)
and typeorm-extension is used
to apply the parsed request query parameters on the db query and express to handle requests.
Parse - Extended
In the following example, all query parameter parse functions (parseQueryFields
, parseQueryFilters
, ...)
will be imported separately.
import {getRepository} from "typeorm";
import {Request, Response} from 'express';
import {
parseQueryFields,
parseQueryFilters,
parseQueryRelations,
parseQueryPagination,
parseQuerySort,
Parameter
} from "@trapi/query";
import {
applyQueryParseOutput
} from "typeorm-extension";
export async function getUsers(req: Request, res: Response) {
const {fields, filter, include, page, sort} = req.query;
const repository = 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
function, which will handle a group of query parameter values & options.
The ParseInput data of the parseQuery
function can have the following (alias-) property keys, which will be formatted to Parameter
keys after the parse process:
fields
| Parameter.FIELDS
include
| relations
| Parameter.RELATIONS
- ... more
import {getRepository} from "typeorm";
import {Request, Response} from 'express';
import {
parseQuery,
Parameter,
ParseOutput
} from "@trapi/query";
import {
applyQueryParseOutput
} 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 repository = 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 shorter to parse the query key values, because typeorm-extension
uses @trapi/query
under the hood ⚡.
This is much shorter than the previous example and has less direct 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";
import {URLParameter} from "@trapi/query/src";
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, i.e:
ParseParameterOutput<T extends ParameterType | URLParameterType>
is a generic type and returns the parsed output data for a given parameter type, i.e:
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
}