@trapi/query
Advanced tools
Comparing version 0.0.2 to 1.0.0
@@ -1,2 +0,8 @@ | ||
export * from './request'; | ||
export * from './utils'; | ||
export * from './build'; | ||
export * from './fields'; | ||
export * from './filters'; | ||
export * from './includes'; | ||
export * from './pagination'; | ||
export * from './sort'; | ||
export * from './type'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -19,4 +19,9 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__exportStar(require("./request"), exports); | ||
__exportStar(require("./utils"), exports); | ||
__exportStar(require("./build"), exports); | ||
__exportStar(require("./fields"), exports); | ||
__exportStar(require("./filters"), exports); | ||
__exportStar(require("./includes"), exports); | ||
__exportStar(require("./pagination"), exports); | ||
__exportStar(require("./sort"), exports); | ||
__exportStar(require("./type"), exports); | ||
//# sourceMappingURL=index.js.map |
@@ -0,1 +1,7 @@ | ||
export * from './field'; | ||
export * from './flatten'; | ||
export * from './include'; | ||
export * from './object'; | ||
export * from './type'; | ||
export * from './url'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -19,3 +19,8 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__exportStar(require("./field"), exports); | ||
__exportStar(require("./flatten"), exports); | ||
__exportStar(require("./include"), exports); | ||
__exportStar(require("./object"), exports); | ||
__exportStar(require("./type"), exports); | ||
__exportStar(require("./url"), exports); | ||
//# sourceMappingURL=index.js.map |
export declare function hasOwnProperty<X extends {}, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record<Y, unknown>; | ||
/** | ||
* Build alias mapping from strings in array or object representation to object representation. | ||
* | ||
* {field1: 'field1', ...} => {field1: 'field1', ...} | ||
* ['field1', 'field2'] => {field1: 'field1', field2: 'field2'} | ||
* | ||
* @param rawFields | ||
*/ | ||
export declare function buildObjectFromStringArray(rawFields: string[] | Record<string, string>): Record<string, string>; | ||
//# sourceMappingURL=object.d.ts.map |
@@ -9,3 +9,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.hasOwnProperty = void 0; | ||
exports.buildObjectFromStringArray = exports.hasOwnProperty = void 0; | ||
function hasOwnProperty(obj, prop) { | ||
@@ -15,2 +15,23 @@ return obj.hasOwnProperty(prop); | ||
exports.hasOwnProperty = hasOwnProperty; | ||
/** | ||
* Build alias mapping from strings in array or object representation to object representation. | ||
* | ||
* {field1: 'field1', ...} => {field1: 'field1', ...} | ||
* ['field1', 'field2'] => {field1: 'field1', field2: 'field2'} | ||
* | ||
* @param rawFields | ||
*/ | ||
function buildObjectFromStringArray(rawFields) { | ||
if (Array.isArray(rawFields)) { | ||
const record = {}; | ||
rawFields | ||
.filter(field => typeof field === 'string') | ||
.map(field => { | ||
record[field] = field; | ||
}); | ||
return record; | ||
} | ||
return rawFields; | ||
} | ||
exports.buildObjectFromStringArray = buildObjectFromStringArray; | ||
//# sourceMappingURL=object.js.map |
{ | ||
"name": "@trapi/query", | ||
"version": "0.0.2", | ||
"version": "1.0.0", | ||
"description": "An tiny library which provides utility types/functions for request and response query handling.", | ||
@@ -8,3 +8,3 @@ "main": "./dist/index.js", | ||
"scripts": { | ||
"build": "rm -rf ./dist && tsc", | ||
"build": "rm -rf ./dist && tsc -p tsconfig.build.json", | ||
"test": "cross-env NODE_ENV=test jest --config ./test/jest.config.js", | ||
@@ -53,3 +53,3 @@ "test:coverage": "cross-env NODE_ENV=test jest --config ./test/jest.config.js --coverage" | ||
}, | ||
"gitHead": "f931516f93b31fa0fc3c04926713a6cc91680f39" | ||
"gitHead": "4355a7f0d19c3df26929981011a621e7f70590e6" | ||
} |
432
README.MD
@@ -1,2 +0,2 @@ | ||
# @trapi/query 🏗 | ||
# @trapi/query 🌈 | ||
@@ -9,3 +9,4 @@ [![main](https://github.com/Tada5hi/typescript-rest-api/actions/workflows/main.yml/badge.svg)](https://github.com/Tada5hi/typescript-rest-api/actions/workflows/main.yml) | ||
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 parameters: | ||
It extends the specification format between request- & response-handling for querying and fetching data according the following query parameters: | ||
- `filter`: Filter the data set, according specific criteria. | ||
@@ -19,3 +20,18 @@ - `include` Include related resources of the primary data. | ||
- [Installation](#installation) | ||
- [Usage](#usage) | ||
- [Build](#build-) | ||
- [Parsing](#parsing-) | ||
- Types | ||
- Options | ||
- [FieldsOptions](#fieldsoptions) | ||
- [FiltersOptions](#filtersoptions) | ||
- [IncludeOptions](#includesoptions) | ||
- [PaginationOptions](#paginationoptions) | ||
- [SortOptions](#sortoptions) | ||
- Transformed | ||
- [FieldsTransformed](#fieldstransformed) | ||
- [FiltersTransformed](#filterstransformed) | ||
- [IncludeTransformed](#includestransformed) | ||
- [PaginationTransformed](#paginationtransformed) | ||
- [SortTransformed](#sorttransformed) | ||
## Installation | ||
@@ -28,1 +44,411 @@ | ||
## 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. | ||
```typescript | ||
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 // some possible values: | ||
// 1 | [1,2,3] | '!1' | '~1' | ['!1',2,3] | {profile: {avatar: 'xxx.jpg'}} | ||
}, | ||
fields: ['id', 'name'], // some possible values: | ||
// 'id' | ['id', 'name'] | '+id' | {user: ['id', 'name'], profile: ['avatar']} | ||
sort: '-id', // some possible values: | ||
// 'id' | ['id', 'name'] | '-id' | {id: 'DESC', profile: {avatar: 'ASC'}} | ||
include: { | ||
profile: true | ||
} | ||
}; | ||
// console.log(tranformQueryRecord); | ||
// ?filter[id]=1&fields=id,name&page[limit]=20&page[offset]=10&sort=-id&include=profile | ||
let response = await getAPIUsers(record); | ||
// do somethin with the response :) | ||
})(); | ||
``` | ||
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: | ||
```typescript | ||
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 transformed 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: | ||
#### Parsing - Extended | ||
```typescript | ||
import {getRepository} from "typeorm"; | ||
import {Request, Response} from 'express'; | ||
import { | ||
parseFields, | ||
parseFilters, | ||
parseIncludes, | ||
parsePagination, | ||
parseSort | ||
} from "typeorm-extension"; | ||
import { | ||
applyFieldsTransformed, | ||
applyFiltersTransformed, | ||
applyIncludesTransformed, | ||
applyPaginationTransformed, | ||
applySortTransformed | ||
} from "typeorm-extension"; | ||
/** | ||
* Get many users. | ||
* | ||
* Request example | ||
* - url: /users?page[limit]=10&page[offset]=0&include=profile&filter[id]=1&fields[user]=id,name | ||
* | ||
* Return Example: | ||
* { | ||
* data: [ | ||
* {id: 1, name: 'tada5hi', profile: {avatar: 'avatar.jpg', cover: 'cover.jpg'}} | ||
* ], | ||
* meta: { | ||
* total: 1, | ||
* limit: 20, | ||
* offset: 0 | ||
* } | ||
* } | ||
* @param req | ||
* @param res | ||
*/ | ||
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 = applyIncludesTransformed(query, parseIncludes(include)); | ||
applySortTransformed(query, parseSort(sort, { | ||
queryAlias: 'user', | ||
allowed: ['id', 'name', 'profile.id'], | ||
// profile.id can only be used as sorting key, if the relation 'profile' is included. | ||
includes: includes | ||
})); | ||
applyFieldsTransformed(query, parseFields(fields, { | ||
queryAlias: 'user', | ||
allowed: ['id', 'name', 'profile.id', 'profile.avatar'], | ||
// porfile fields can only be included, if the relation 'profile' is included. | ||
includes: includes | ||
})); | ||
// only allow filtering users by id & name | ||
applyFiltersTransformed(query, parseFilters(filter, { | ||
queryAlias: 'user', | ||
allowed: ['id', 'name', 'profile.id'], | ||
// porfile.id can only be used as a filter, if the relation 'profile' is included. | ||
includes: includes | ||
})); | ||
// only allow to select 20 items at maximum. | ||
const pagination = applyPaginationTransformed(query, parsePagination(page, {maxLimit: 20})); | ||
// ----------------------------------------------------- | ||
const [entities, total] = await query.getManyAndCount(); | ||
return res.json({ | ||
data: { | ||
data: entities, | ||
meta: { | ||
total, | ||
...pagination | ||
} | ||
} | ||
}); | ||
} | ||
``` | ||
This can even be much easier, because `typeorm-extension` uses `@trapi/query` under the hood ⚡. | ||
#### Transform - Simple | ||
This is much shorter than the previous example and has less direct dependencies 😁. | ||
```typescript | ||
import {getRepository} from "typeorm"; | ||
import {Request, Response} from 'express'; | ||
import { | ||
applyFields, | ||
applyFilters, | ||
applyIncludes, | ||
applyPagination, | ||
applySort | ||
} from "typeorm-extension"; | ||
/** | ||
* Get many users. | ||
* | ||
* Request example | ||
* - url: /users?page[limit]=10&page[offset]=0&include=profile&filter[id]=1&fields[user]=id,name | ||
* | ||
* Return Example: | ||
* { | ||
* data: [ | ||
* {id: 1, name: 'tada5hi', profile: {avatar: 'avatar.jpg', cover: 'cover.jpg'}} | ||
* ], | ||
* meta: { | ||
* total: 1, | ||
* limit: 20, | ||
* offset: 0 | ||
* } | ||
* } | ||
* @param req | ||
* @param res | ||
*/ | ||
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, { | ||
queryAlias: 'user', | ||
allowed: ['profile'] | ||
}); | ||
applySort(query, sort, { | ||
queryAlias: 'user', | ||
allowed: ['id', 'name', 'profile.id'], | ||
// profile.id can only be used as sorting key, if the relation 'profile' is included. | ||
includes: includes | ||
}); | ||
applyFields(query, fields, { | ||
queryAlias: 'user', | ||
allowed: ['id', 'name', 'profile.id', 'profile.avatar'], | ||
// porfile fields can only be included, if the relation 'profile' is included. | ||
includes: includes | ||
}) | ||
// only allow filtering users by id & name | ||
applyFilters(query, filter, { | ||
queryAlias: 'user', | ||
allowed: ['id', 'name', 'profile.id'], | ||
// porfile.id can only be used as a filter, if the relation 'profile' is included. | ||
includes: includes | ||
}); | ||
// only allow to select 20 items at maximum. | ||
const pagination = applyPagination(query, page, {maxLimit: 20}); | ||
// ----------------------------------------------------- | ||
const [entities, total] = await query.getManyAndCount(); | ||
return res.json({ | ||
data: { | ||
data: entities, | ||
meta: { | ||
total, | ||
...pagination | ||
} | ||
} | ||
}); | ||
} | ||
``` | ||
### Options | ||
#### FieldsOptions | ||
```typescript | ||
type FieldsOptions = { | ||
aliasMapping?: Record<string, string>, | ||
allowed?: Record<string, string[]> | string[], | ||
includes?: IncludesTransformed, | ||
queryAlias?: string | ||
}; | ||
``` | ||
#### FiltersOptions | ||
```typescript | ||
export type FiltersOptions = { | ||
aliasMapping?: Record<string, string>, | ||
allowed?: string[], | ||
includes?: IncludesTransformed, | ||
queryAlias?: string, | ||
queryBindingKeyFn?: (key: string) => string | ||
}; | ||
``` | ||
#### IncludesOptions | ||
```typescript | ||
type IncludesOptions = { | ||
aliasMapping?: Record<string, string>, | ||
allowed?: string[], | ||
includeParents?: boolean | string[] | string | ||
queryAlias?: string, | ||
}; | ||
``` | ||
#### PaginationOptions | ||
```typescript | ||
type PaginationOptions = { | ||
maxLimit?: number | ||
}; | ||
``` | ||
#### SortOptions | ||
```typescript | ||
type SortOptions = { | ||
aliasMapping?: Record<string, string>, | ||
allowed?: string[] | string[][], | ||
includes?: IncludesTransformed, | ||
queryAlias?: string | ||
}; | ||
``` | ||
### Transformed | ||
#### FieldsTransformed | ||
```typescript | ||
export type AliasFields = { | ||
addFields?: boolean, | ||
alias?: string, | ||
fields: string[] | ||
}; | ||
export type FieldsTransformed = AliasFields[]; | ||
``` | ||
#### FiltersTransformed | ||
```typescript | ||
export type FilterTransformed = { | ||
statement: string, | ||
binding: Record<string, any> | ||
}; | ||
export type FiltersTransformed = FilterTransformed[]; | ||
``` | ||
#### IncludesTransformed | ||
```typescript | ||
export type IncludeTransformed = { | ||
property: string, | ||
alias: string | ||
}; | ||
export type IncludesTransformed = IncludeTransformed[]; | ||
``` | ||
#### PaginationTransformed | ||
```typescript | ||
export type PaginationTransformed = { | ||
limit?: number, | ||
offset?: number | ||
}; | ||
``` | ||
#### SortTransformed | ||
```typescript | ||
export type SortDirection = 'ASC' | 'DESC'; | ||
export type SortTransformed = Record<string, SortDirection>; | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
100831
118
1197
1
452
1