@trapi/query 🌈
This is a library for building JSON:API
like REST-APIs.
It extends the specification format between request- & response-handling for querying and fetching data according the following query parameters:
fields
Return only specific fields or extend the default selection.filter
: Filter the data set, according specific criteria.include
Include related resources of the primary data.page
Limit the number of resources returned of the whole set.sort
Sort the resources according one or more keys in asc/desc direction.
Important NOTE
The examples in the Parsing section, are not available with current release of the typeorm-extension@0.3.0
library.
Table of Contents
Installation
npm install @trapi/query --save
Usage
Build 🏗
The general idea is to construct a QueryRecord
at the frontend side, which will be formatted to a string and passed to the backend application as URL query string.
The backend application is than always fully capable of processing the request, as far the provided query was not malformed.
Therefore, two components of this module are required in the frontend application:
- generic type:
QueryRecord<T>
- function:
buildQuery
.
The method will generate the query string, which was addressed in the previous section.
In the following example a Class which will represent the structure of a User
.
The getAPIUsers
will handle the resource request to the resource API.
import axios from "axios";
import {QueryRecord, buildQuery} 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: QueryRecord<User>): Promise<ResponsePayload> {
const response = await axios.get('users' + buildQuery(record));
return response.data;
}
(async () => {
const record: QueryRecord<User> = {
page: {
limit: 20,
offset: 10
},
filter: {
id: 1
},
fields: ['id', 'name'],
sort: '-id',
include: {
profile: true
}
};
const query = buildQuery(record);
let response = await getAPIUsers(record);
})();
The next examples will be about how to parse and validate the transformed QueryRecord<T>
on the backend side.
Parsing 🔎
For explanation proposes how to use the query utils,
two simple entities with a simple 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 as the object-relational mapping (ORM) and typeorm-extension is used
to apply the parsed request query parameters on the db query.
When you use express or another library, you can use the API utils accordingly to the
following code snippet:
Parse - In Detail
import {getRepository} from "typeorm";
import {Request, Response} from 'express';
import {
parseFields,
parseFilters,
parseIncludes,
parsePagination,
parseSort,
QueryKey
} from "@trapi/query";
import {
applyParsed
} 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 includesParsed = parseIncludes(include, {
allowed: 'profile'
});
const fieldsParsed = parseFields(fields, {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id', 'profile.avatar'],
includes: includesParsed
});
const filterParsed = parseFilters(filter, {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id'],
includes: includesParsed
});
const pageParsed = parsePagination(page, {
maxLimit: 20
});
const sortParsed = parseSort(sort, {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id'],
includes: includesParsed
});
const parsed = applyParsed(query, {
[QueryKey.FIELDS]: fieldsParsed,
[QueryKey.FILTER]: filterParsed,
[QueryKey.INCLUDE]: includesParsed,
[QueryKey.PAGE]: pageParsed,
[QueryKey.SORT]: sortParsed
});
const [entities, total] = await query.getManyAndCount();
return res.json({
data: {
data: entities,
meta: {
total,
...parsed[QueryKey.PAGE]
}
}
});
}
import {getRepository} from "typeorm";
import {Request, Response} from 'express';
import {
parseQuery,
QueryKey,
QueryParseOutput
} from "@trapi/query";
import {
applyParsed
} from "typeorm-extension";
export async function getUsers(req: Request, res: Response) {
const {fields, filter, include, page, sort} = req.query;
const output: QueryParseOutput = parseQuery({
[QueryKey.FIELDS]: {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id', 'profile.avatar']
},
[QueryKey.FILTER]: {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id']
},
[QueryKey.INCLUDE]: {
allowed: ['profile']
},
[QueryKey.PAGE]: {
maxLimit: 20
},
[QueryKey.SORT]: {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id']
}
});
const repository = getRepository(User);
const query = repository.createQueryBuilder('user');
const parsed = applyParsed(query, output);
const [entities, total] = await query.getManyAndCount();
return res.json({
data: {
data: entities,
meta: {
total,
...parsed[QueryKey.PAGE]
}
}
});
}
Parse - Third Party Library
It can even be much easier 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 😁.
import {getRepository} from "typeorm";
import {Request, Response} from 'express';
import {
applyFields,
applyFilters,
applyIncludes,
applyPagination,
applySort
} 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 includes = applyIncludes(query, include, {
defaultAlias: 'user',
allowed: ['profile']
});
applySort(query, sort, {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id'],
includes: includes
});
applyFields(query, fields, {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id', 'profile.avatar'],
includes: includes
})
applyFilters(query, filter, {
defaultAlias: 'user',
allowed: ['id', 'name', 'profile.id'],
includes: includes
});
const pagination = applyPagination(query, page, {maxLimit: 20});
const [entities, total] = await query.getManyAndCount();
return res.json({
data: {
data: entities,
meta: {
total,
...pagination
}
}
});
}
Functions
buildQuery
▸ function
buildQuery<T
>(record: QueryRecord<T>
, options?: QueryBuildOptions
): string
Build a query string from a provided QueryRecord.
Example
Simple
import {buildQuery, QueryKey} from "@trapi/query";
type User = {
id: number,
name: string,
age?: number
}
const query: string = buildQuery<T>({
[QueryKey.FIELDS]: ['+age'],
[QueryKey.FILTER]: {
name: '~pe'
}
});
console.log(query);
Type parameters
Name | Description |
---|
T | A type, interface, or class which represent the data structure. |
Parameters
Name | Type | Description |
---|
record | QueryRecord <T > | Query specification more. |
options | QueryBuildOptions | Options for building fields, filter, include, ... query more |
Returns
string
The function return 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<T
>(input: QueryParseInput
, options?: QueryParseOptions
): QueryParseOutput
Parse a query string to an efficient data structure ⚡. The output will
be an object with each possible value of the QueryKey enum as property key and the
parsed data as value.
Example
Simple
import {
FieldOperator,
FilterOperator,
parseQuery,
QueryParseOutput
} from "@trapi/query";
const output: QueryParseOutput = parseQuery({
[QueryKey.FIELDS]: ['+age'],
[QueryKey.FILTER]: {
name: '~pe'
}
});
console.log(output);
Type parameters
Parameters
Name | Type | Description |
---|
input | QueryParseInput | Query input data passed i.e. via URL more. |
options | QueryParseOptions | Options for parsing fields, filter, include, ... more |
Returns
QueryParseOutput
The function return an object.
Types
Key
QueryKey
export enum QueryKey {
FILTER = 'filter',
FIELDS = 'fields',
SORT = 'sort',
INCLUDE = 'include',
PAGE = 'page'
}
QueryKeyOption
export type QueryKeyOption<T extends QueryKey> = T extends QueryKey.FIELDS ?
FieldsOptions :
T extends QueryKey.FILTER ?
FiltersOptions :
T extends QueryKey.INCLUDE ?
IncludesOptions :
T extends QueryKey.PAGE ?
PaginationOptions :
T extends QueryKey.SORT ?
SortOptions :
never;
Record
QueryRecord
export type QueryRecord<R extends Record<string, any>> = {
[K in QueryKey]?: QueryRecordType<K,R>
}
export type QueryRecordType<
T extends QueryKey,
R extends Record<string, any>
> = T extends QueryKey.FIELDS ?
FilterRecord<R> :
T extends QueryKey.FILTER ?
FieldRecord<R> :
T extends QueryKey.INCLUDE ?
IncludeRecord<R> :
T extends QueryKey.PAGE ?
PaginationRecord<R> :
T extends QueryKey.SORT ?
SortRecord<R> :
never;
QueryRecordParsed
export type QueryRecordParsed<T extends QueryKey> =
T extends QueryKey.FIELDS ?
FieldsParsed :
T extends QueryKey.FILTER ?
FiltersParsed :
T extends QueryKey.INCLUDE ?
IncludesParsed :
T extends QueryKey.PAGE ?
PaginationParsed :
T extends QueryKey.SORT ?
SortParsed :
never;
Parse
QueryParseOptions
export type QueryParseOptions = {
[K in QueryKey]?: QueryKeyOption<K> | boolean
}
QueryParseInput
export type QueryParseOutput = {
[K in QueryKey]?: any
}
QueryParseOutput
export type QueryParseOutput = {
[K in QueryKey]?: QueryRecordParsed<K>
}
Parsed
FieldsParsed
export enum FieldOperator {
INCLUDE = '+',
EXCLUDE = '-'
}
export type FieldParsed = {
key: string,
alias?: string,
operator?: FieldOperator
};
export type FieldsParsed = FieldParsed[];
FiltersParsed
export enum FilterOperatorLabel {
NEGATION = 'negation',
LIKE = 'like',
IN = 'in'
}
export type FilterParsed = {
key: string,
alias?: string,
operator?: {
[K in FilterOperatorLabel]?: boolean
},
value: FilterValue<string | number | boolean | null>
};
export type FiltersParsed = FilterParsed[];
IncludesParsed
export type IncludeParsed = {
property: string,
alias: string
};
export type IncludesParsed = IncludeParsed[];
export type PaginationParsed = {
limit?: number,
offset?: number
};
SortParsed
export enum SortDirection {
ASC = 'ASC',
DESC = 'DESC'
}
export type SortElementParsed = {
alias?: string,
key: string,
value: SortDirection
}
export type SortParsed = SortElementParsed[];
Options
FieldsOptions
export type FieldsOptions = {
aliasMapping?: Record<string, string>,
allowed?: Record<string, string[]> | string[],
includes?: IncludesParsed,
defaultAlias?: string
};
FiltersOptions
export type FiltersOptions = {
aliasMapping?: Record<string, string>,
allowed?: string[],
includes?: IncludesParsed,
defaultAlias?: string,
queryBindingKeyFn?: (key: string) => string
};
IncludesOptions
export type IncludesOptions = {
aliasMapping?: Record<string, string>,
allowed?: string[],
includeParents?: boolean | string[] | string
defaultAlias?: string,
};
export type PaginationOptions = {
maxLimit?: number
};
SortOptions
export type SortOptions = {
aliasMapping?: Record<string, string>,
allowed?: string[] | string[][],
includes?: IncludesParsed,
defaultAlias?: string
};