@nest-excalibur/common-api v0.1.0
common-api
@nest-excalibur/common-api is a set of functions and classes for Nest.js .
Index
3.1 Guards
3.2 Interceptors
3.3 Headers
3.4 SetMetadata
3.5 CrudApi
Install:
npm i @nest-excalibur/common-apiREST API
One of the strongest features of this library is to implement an API-REST quickly. To do this, you must first consider implementing the following classes:
- Entity
- Service
- DTO
- Controller
Create entity class with extends from AbstractEntity
If you want the entity has an auntoincremental id column, createdAt, updatedAt columns.
import {AbstractEntity} from '@nest-excalibur/common-api/lib';
@Entity('product')
export class ProductEntity extends AbstractEntity {
}Create a service class which extends from AbstractService
import {AbstractService} from '@nest-excalibur/common-api/lib';
@Injectable()
export class ProductService extends AbstractService<ProductEntity> {
constructor(
@InjectRepository(ProductEntity)
private readonly _productRepository: Repository<ProductEntity>,
) {
super(_productRepository);
}
}Create a DTO class for update and create:
It is optional to extend from BaseDTO , This class allows to validate that the fields: id , createdAt and updatedAt should not be empty
import {BaseDTO} from '@nest-excalibur/common-api/lib';
export class ProductCreateDto extends BaseDTO{
@IsAlpha()
@IsNotEmpty()
name: string;
@IsNotEmpty()
description: string;
@IsNumber()
@IsNotEmpty()
price: number;
}Puting it all together
import {CrudController, CrudOptions} from '@nest-excalibur/common-api/lib';
const options: CrudOptions = {
dtoConfig: {
createDtoType: ProductCreateDto,
updateDtoType: ProductUpdateDto,
},
}
@Controller('product')
export class ProductController extends CrudController<ProductEntity>(options) {
constructor(private readonly _productService: ProductService) {
super(
_productService,
);
}
}API-REST ENPOINTS:
For a controllerPrefix given on the Controller decorator. The following
set of routes will be generated.
| HTTP METHOD | PATH | Controller and Service method |
|---|---|---|
| POST | <controllerPrefix> | createOne |
| POST | <controllerPrefix> /create-many | createMany |
| PUT | /<controllerPrefix>/<id:number> | updateOne |
| GET | /<controllerPrefix>/<id:number> | findOne |
| GET | /<controllerPrefix>?query=<find-query> | findAll |
| DELETE | /<controllerPrefix>/<id:number> | deleteOne |
Find Query
SQL Data Bases
For SQL DB you can make a search criteria, that complies with the following scheme:
{
"where": {
// Entity attributes and relations
},
"skip": 0, // Pagination
"take": 10 // Pagination
} For example:
The product entity has a relation many to one with category entity , so lets make
the following search:
Products that have a price greater than or equal to 10 or less than 2 and that the name of the product category
can be snacks , drinks or that the same name of the category includes "sna" .
Find-Query :
{
"where": {
"price": [
{
"$gte": 10.00
},
{
"$lt": 2.00
}
],
"category": {
"$join": "inner",
"name": [
{
"$like": "%25sna%25"
},
{
"$in": ["snacks", "drinks"]
}
]
}
}
} On
likeoperator with the wildcar%, you should use%25instead of%cause some problems with browsers andhttp clientsasPostman.
Examples
Browser or client side
http://localhost:3000/product?query={"where":{"name":{"$like":"%25choco%25"},"category":{}}}Results:
{
"nextQuery": null,
"data": [
{
"price": "18",
"id": 22,
"createdAt": "2020-07-23T23:51:56.898Z",
"updatedAt": "2020-07-23T23:51:56.898Z",
"name": "chocobreak",
"description": "Voluptate irure eu dolor sit et id nisi dolore ex aliquip.",
"category": {
"id": 8,
"name": "candies"
}
},
{
"price": "16",
"id": 21,
"createdAt": "2020-07-23T23:51:56.897Z",
"updatedAt": "2020-07-23T23:51:56.897Z",
"name": "great chocolate",
"description": "Commodo sit duis id consectetur minim nisi nostrud ex sit ad aute cillum eiusmod.",
"category": {
"id": 8,
"name": "candies"
}
},
{
"price": "2",
"id": 17,
"createdAt": "2020-07-23T23:51:56.891Z",
"updatedAt": "2020-07-23T23:51:56.891Z",
"name": "happy chocolate",
"description": "Duis magna exercitation aute pariatur voluptate velit magna ut.",
"category": {
"id": 8,
"name": "candies"
}
}
],
"total": 3
}If you are working on backend side you could use the widlcard
%without problems. Also you could use any wildcard onlikeoperator according your data base.
const query = {
where: {
id: { $like: '%chocho%' },
category: {
name: 'candy',
},
supermaket: { // inner join with `supermarket` entity.
id: 25,
address: '',
city: { // inner join with `city` entity.
name: {$like: 'c[^u]'},
state: { // inner join with `state` entity.
id: {$in: [4, 5, 6, 7]},
},
},
},
},
skip: 0,
take: 30, // Pagination
}
const searchResponse: [ProductEntity[], number] = await this.productService.findAll(query);
const filterProducts = searchResponse[0];
const totalFecthed = searchResponse[1]; // All filtered records in the Data BaseFor scape characters on
likeoperator: use\\
percentCode: {"$like": "%25\\%25%25"}} // Client side
percentCode: {"$like": "%\\%%"}} // Backend sideOr operator
You can make a query with OR operator using the keyword "$or" as "true"
For example: Get products with a price of 7 or name includes "choco"
{
"where": {
"price": {"$eq": 7, "$or": true},
"name": {"$like": "%25choco%25", "$or": true}
}
} Putting it all together
GET /product?query={"where":{.......}}
Find Query Object
Operators
| Operator | keyword | Example |
|---|---|---|
| Like | $like | "$like": "%sns%" |
| iLike | $ilike (PostgreSQL) | "$ilike": "%sns%" |
> | $gt | "$gt": 20 |
>= | $gte | "$gte": 20 |
< | $lt | "$lt": 20 |
<= | $lte | "$lte": 20 |
= | $eq | "$eq": 20 |
!= | $ne | "$ne": 20 |
| Between | $btw | "$btw": A, B |
| In | $in | "$in": A, B, ... |
| Not In | $nin | "$nin": A, B, ..." |
| Not Between | $nbtw | "$nbtw": A, B, ..." |
if your are using MongoDB, you must use the query operators for mongo, check the documentation
Join Relations
The join relations could be many levels as you want, you need to write the
ManyToOne , OneToMany , OneToOne , relationship name in your Find Query Object like the previous example.
If the join is of the inner type it is not necessary to put the keyword "$join": "inner" , only if you want to use a join of the type "left" ( " $join ":" left" )
Pagination
The pagination by default is skip: 0 and take: 10 .
Order By
The order by criteria by default with respect the entity id is DESC :
{
"where": {
},
"orderBy": {
// Order by criteria
}
} Select columns
In order to get records with an specific set of columns, you could make use of $sel operator:
For example: Get products with a bigger than 7 and only retrieves the name of the filtered products.
{
"where": {
"$sel": ["name"],
"price": {"$gt": 7}
}
} Also, you could use the $sel operator on queries with joins.
All columns that are retrieved will always include the id column
For example: the following query retrieves products with name and its supermarket with only name and address.
const query = {
where: {
$sel: ["name"],
category: {
name: 'candy',
},
supermaket: { // Select address and name
$sel: ["name", "address"],
},
},
skip: 0,
take: 30,
}Working with transactions
The AbstractService class has the following methods in order to perform transactions:
findAllWithTransaction
findOneWithTransaction
createOneWithTransaction
createManyWithTransaction
updateOneWithTransaction
deleteOneWithTransaction
deleteManyByIdsWithTransaction
Example:
import {EntityManager, getManager, Repository} from 'typeorm';
import {
AbstractService,
TransactionResponse,
FindFullQuerym
} from '@nest-excalibur/common-api/lib';
import {getManager} from 'typeorm';
@Injectable()
export class ProductService extends AbstractService<ProductEntity> {
constructor(
@InjectRepository(ProductEntity)
private readonly _productRepository: Repository<ProductEntity>,
) {
super(_productRepository);
}
async deleteByCategory(categoryId: number): Promise<ProductEntity[]> {
return await getManager()
.transaction(
'SERIALIZABLE',
async (entityManager: EntityManager) => {
// Define the find condition
const finQuery: FindFullQuery = {
where: {
category: {
id: categoryId,
}
}
};
const findResponse = await this.findAllWithTransaction(entityManager, finQuery);
// Update the entityManager for the next operation
entityManager = findResponse.entityManager;
const [productsToDelete, totalFetched] = findResponse.response;
// Get only the ids
const ids = productsToDelete.map(product => product.id);
// Get only the deleted rows
const {response} = await this
.deleteManyByIdsWithTransaction(entityManager, ids);
return response;
}
);
}
}MongoDb (TypeOrm)
Entity (Optional)
If you want the entity has an ObjectId, updatedAt columns, you need to extends from AbstractMongoEntity
import {AbstractMongoEntity} from '@nest-excalibur/common-api/lib';
@Entity('post')
export class PostEntity extends AbstractMongoEntity{
}DTO
It is optional to extend from BaseMongoDTO . This class allows to validate that the fields: id , createdAt and updatedAt should not be empty
import {BaseMongoDTO} from '@nest-excalibur/common-api/lib';
export class Post extends BaseMongoDTO{
}Service
The service class must extends from AbstractMongoService
import {AbstractMongoService} from '@nest-excalibur/common-api/lib';
@Injectable()
export class PostService extends AbstractMongoService<PostEntity> {
constructor(
@InjectRepository(PostEntity, 'mongo_conn')
private postRepository: MongoRepository<PostEntity>,
) {
super(
localizacionRepository,
{ // MongoIndexConfigInterface
fieldOrSpec: { localization: '2dsphere' },
options: {
min: -180,
max: 180,
},
},
);
}
}Controller
import {CrudController, CrudOptions} from '@nest-excalibur/common-api/lib';
const options: CrudOptions = {
useMongo: true,
dtoConfig: {
createDtoType: PostCreateDto,
updateDtoType: PostCreateDto,
},
}
@Controller('post')
export class PostController extends CrudController<PostEntity>(options) {
constructor(private readonly _postService: PostService) {
super(
_postService,
);
}
}Decorators
GUARDS
For Guards for every Crud Method you need to make use of CrudGuards or CrudApi decorator.
Example:
import {CrudGuards} from '@nest-excalibur/common-api/lib';
@CrudGuards(
{
findAll: [ProductoFindAllGuard,]
updateOne: [ProductUpdaeOneGuard],
...othersCrudMethod
}
)
@Controller('product')
export class ProductController extends CrudController<PostEntity>(options) {
}Interceptors
For Interceptors for every Crud Method you need to make use of CrudInterceptors or CrudApi decorator.
Example:
import {CrudInterceptors} from '@nest-excalibur/common-api/lib';
@CrudInterceptors(
{
findAll: [ProductFindallInterceptor,]
...othersCrudMethod
}
)
@Controller('product')
export class ProductController extends CrudController<PostEntity>(options) {
}Headers
For Headers on Crud Methods you need to make use of CrudHeaders or CrudApi decorator.
Example:
import {CrudHeaders} from '@nest-excalibur/common-api/lib';
@CrudHeaders(
{
findAll: {
name: 'Custom Header',
value: ''
},
...othersCrudMethod
}
)
@Controller('product')
export class ProductController extends CrudController<PostEntity>(options) {
}SetMetadata
For set metadata on Crud Methods you need to make use of CrudMetadata or CrudApi decorator.
Example:
import {CrudMetadata} from '@nest-excalibur/common-api/lib';
@CrudMetadata(
{
findAll: [
{
key: 'permissions',
data: ['READ'],
}
],
}
)
@CrudGuards(
{
findAll: [
FindAllGuard,
],
},
)
@Controller('product')
export class ProductController extends CrudController<PostEntity>(options) {
}You can retrieve the metadata using Reflector from @nestjs/core;
@Injectable()
export class FindAllGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
async canActivate(
context: ExecutionContext,
): Promise<boolean> {
const data = this.reflector.get<string[]>(
'permissions',
context.getHandler(),
);
return true;
}
}CrudApi
The CrudApi is a general decorator to put the configuration of swagger, guards, interceptors and headers for every
Crud Method.
Example:
import {CrudApi} from '@nest-excalibur/common-api/lib';
@CrudApi(
{
findAll: {
guards: [ProductFindAllGuard,],
interceptors: [ProductFindallInterceptor],
header: {
name: 'Custom Header',
value: ''
},
},
},
)
@Controller('product')
export class ProductController extends CrudController<PostEntity>(options) {
}