Nest + TypeOrm + Postgres + GraphQL(DataLoader) + Restful + Redis + MQ + Jest
Bullshit: It’s full of dry stuff. It’s a ready-made pie. As hungry you, meet food; Thirsty you, meet dew; Flat poor you, meet money!!
Required environment: Node(V12.19.0), Redis, Postgres
Blind comparison to this, this partial address
The use of graphql
- For those of you who have written graphQL in JS, you need to maintain the field twice. For TS, you don’t need to.
- Use TS to generate graphQL type code. This can be generated as a database table annotated.
- @field ({nullable: true}) can be omitted and nullable is marked as true by default (not recommended).
// NoIdBase is one of my base classes: It contains createTime, deleteTime, and so on and it's also an ObjectType annotation; Class @objectType () @Entity('user') export class user extends NoIdBase {@field ({nullable: true, description: 'id' }) @PrimaryColumn() id: string; @Index({}) @Field({ nullable: true }) @Column({ comment: 'name', nullable: true }) name: string; @field ({nullable: true}) @column ({comment: 'id', nullable: true}) roleNo: string; @field ({nullable: true}) @index ({unique: true}) @column ({comment: 'email ', nullable: true}) email: string; }Copy the code
Here are two random examples, one for Query and one for Mutation, and QueryParams is a type I encapsulated for parameter passing, sorting, and paging.
@query (() => [User], {description: 'Query User'}) async users(@args ('queryParams') {filter, order, pagination}: QueryParams) { return await this.userService.getUserList({ pagination, filter, order }); } @ Mutation (() = > User, {description: 'I am note}) async createUser () {return enclosing userService. GetUserList (null); }Copy the code
// app.module.ts registers a imports: [GraphqlModule. forRoot({autoSchemaFile: true, resolvers: {JSON: GraphQLJSON},}),],Copy the code
After registering a handful in app.module.ts and running it, you should see the following image. You can see that the inherited arguments are also in there.
Graphql, restful wrapped paging query
Here is about the idea of encapsulation of paging and about the code. For many admin tables, there will be conditional queries for fields, sorting, and paging. The basic query table is consistent except for the table name.
@body('queryParams') {filter, order, pagination}: @args (' QueryParams ') {filter, order, pagination}: QueryParamsCopy the code
The QueryParams class details are as follows, and of course, graphQL and restful could be the same. Some are default values for annotations and some are default values for classes.
@ObjectType() @InputType({}) export class QueryParams implements IQueryParams { @Field(() => graphqlTypeJson, { nullable: true }) filter? : JSON; @Field(() => graphqlTypeJson, { nullable: true }) order? : JSON; @Field(() => PageInput, { nullable: true, defaultValue: { page: 0, limit: 10 } }) pagination? : PageInput; }Copy the code
This is the generic function that encapsulates the basic table query, simply putting the table class as an argument, see the code for details. / / This is a package made by postgres database based on Typeorm. If it is monogo, the parameters will be passed by the front end. Many processing is not needed. ‘ ‘
@param table class * @param tableName tableName * @param queryParams front pass parameter * @param customCondition customCondition */ export const generalList = async <T>(T: any, talbeName: string, queryParams: IQueryParams, customCondition: FindConditions<T>): Promise<Pagination<T>> => {// timeParamsHandle(customCondition, queryParams.filter); // orderByCondition = orderParamsHandle(talbeName, queryParams.order) const orderByCondition = orderParamsHandle(talbeName, queryParams.order); const [data, total] = await createQueryBuilder<T>(T, talbeName) .skip(queryParams.pagination.page) .take(queryParams.pagination.limit) .where(customCondition) .orderBy(orderByCondition) .getManyAndCount(); return new Pagination<T>({ data, total }); };Copy the code
Encapsulating roles: Global guard, which is generally used as follows (every request goes through here)
- If you don’t comment @roles (), don’t do anything, just skip it;
- The restful request and graphQL request are merged together for validation;
- Returns true if the user has the role, otherwise returns no privileges;
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Observable } from 'rxjs'; import { Reflector } from '@nestjs/core'; import { User } from '.. /.. /entity/user/user.entity'; import { GqlExecutionContext } from '@nestjs/graphql'; @Injectable() export class RolesGuard implements CanActivate { constructor(private readonly reflector: Reflector) {} canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> { const roles = this.reflector.get<string[]>('roles', context.getHandler()); // With no role annotations, API any role can access if (! roles) { return true; } let user: User; const request = context.switchToHttp().getRequest(); if (request) { user = request.user; } else { const ctx = GqlExecutionContext.create(context); const graphqlRequest = ctx.getContext().req; user = graphqlRequest.user; } // Return true const hasRole = () => user.rolesList.some((role: string) => roles.indexof (role) > -1); return user && user.rolesList && hasRole(); }}Copy the code
Use: Import and use in AppModule. The GlobalAuthGuard comes first because Roles requires the user in context.
// AppModule // XXX: [{// provide: APP_GUARD, useClass: GlobalAuthGuard,}, {provide: APP_GUARD, useClass: RolesGuard,},], controllers: [], }) export class AppModule {}Copy the code
Encapsulation guard: global guard (every request goes through here, JWT and local login verification is done here, and if authentication is not required, it is skipped)
- Most interfaces require authentication (JWT by default if no annotations are written);
- The login interface requires local authentication (I have encapsulated a annotation: @loginauth (), if it is login, directly local authentication);
- An interface that does not require validation (I have encapsulated an annotation: @noauth (), which returns true without validation if it exists);
// GlobalAuthGuard writes globally, @Injectable() export class GlobalAuthGuard implements CanActivate {constructor(private readonly reflector: Reflector) {} canActivate(context: ExecutionContext): Boolean | Promise < Boolean > | observables < Boolean > {/ / login for annotations const loginAuth = this.reflector.get<boolean>('login-auth', context.getHandler()); Const noAuth = this.reflector. Get < Boolean >('no-auth', context.gethandler ()); if (noAuth) { return true; } const guard = GlobalAuthGuard.getAuthGuard(loginAuth); // Execute the canActivate method of the selected Guard. CanActivate (context); } private static getAuthGuard(loginAuth: Boolean): private static getAuthGuard(loginAuth: Boolean) IAuthGuard { if (loginAuth) { return new LocalAuthGuard(); } else { return new JwtAuthGuard(); }}}Copy the code
Use: Import and use in AppModule.
// AppModule // XXX: provide: APP_GUARD, useClass: GlobalAuthGuard,}], controllers: [], }) export class AppModule {}Copy the code
In fact, UseGuards are also sequential. If you want to use UseGuards alone in each controller or resolver, if there are two Guards, one is JWT authentication and one is role authentication. @useGuards (JwtAuthGuard, RolesGuard) @useGuards (JwtAuthGuard, RolesGuard)
JwtAuthGuard graphQL problem
* “message”: “Unknown Authentication strategy “JWT “”,* so here’s the code:
import { ExecutionContext, Injectable } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { getRequest(context: ExecutionContext) { const restfulRequest = context.switchToHttp().getRequest(); const ctx = GqlExecutionContext.create(context); const graphqlRequest = ctx.getContext().req; if (restfulRequest) { // restful return restfulRequest; } else if (graphqlRequest) { // graphql return graphqlRequest; }}}Copy the code
Graphql login problem using @nestjs/ Passport
In the official documentation of Nest.js, @nestjs/ Passport is recommended. In the process of using it, I have encountered some pits in the previous use.
- As for restful interfaces, I was using Postman to try the interface. Because I did not write “Content-Type: application/json”, I kept reporting errors. When debugging the library code, I found that there was no error at all.
- @nestjs/ Passport: @nestjs/passport: @nestjs/passport: @nestjs/passport: @nestjs/passport: @nestjs/passport: @nestjs/passport: Context. SwitchToHttp (.) getRequest (). Null is found when graphQL is used. So, you can’t log in with GraphQL.
Graphql data-loader problem
- Graphql uses the mount problem itself, so there is an N +1 problem. If there is a user configuration table under one user, the configuration table needs to be checked 10 times, so the user table is checked once and the configuration table is checked 10 times. If you hang more subdomains, the query will be very slow. After the data-loader parses, the id is in the form of [XXX, XXX, XXX], which is checked only once.
- Nestjs-dataloader is a library that is packaged with the nestjs-Dataloader. An example of nestjs-Dataloader looks like this:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import {DataLoaderInterceptor} from 'nestjs-dataloader'
...
@Module({
providers: [
AccountResolver,
AccountLoader,
{
provide: APP_INTERCEPTOR,
useClass: DataLoaderInterceptor,
},
],
})
export class ResolversModule { }
Copy the code
When you write another one, that’s when it looks like this:
AccountResolver,
UserResolver,
Copy the code
This will report: Nest could not find XXX element (this provider does not exist in the current context). DataLoaderModule is a global module. When he designed it, it was registered once. The back of the exchange is probably written like this (specific exchange details point here issues) :
@Module({
imports: [TypeOrmModule.forFeature([XXX1, XXX2, XX3])],
providers: [
xxx1DataLoader,
xxx2DataLoader,
xxx3DataLoader,
xxx4DataLoader,
xxx5DataLoader,
xxx6DataLoader,
{
provide: APP_INTERCEPTOR,
useClass: DataLoaderInterceptor,
},
],
})
export class DataLoaderModule {}
Copy the code
The DataLoader also needs to be aware of this: keys.map cannot be omitted. If 10 keys are checked, 10 must be returned, even if it is null. An error will be reported. Import * as DataLoader from ‘DataLoader ‘; Remember to write this, as if not to write this sometimes error (specific exchange details point here issues).
import * as DataLoader from 'dataloader'; import { NestDataLoader } from 'nestjs-dataloader'; @Injectable() export class UserConfigDataLoader implements NestDataLoader<string, UserConfig> { constructor(@InjectRepository(UserConfig) private userConfigRepository: Repository<UserConfig>) {} generateDataLoader(): DataLoader<string, UserConfig> { return new DataLoader<string, UserConfig>(async (keys: string[]) => { const loadedEntities = await this.userConfigRepository.find({ userId: In(keys) }); return keys.map(key => loadedEntities.find(entity => entity.userId === key)); }); }}Copy the code
The usage is as follows: Users Query
@query (() => [User], {description: 'Query User list'}) async Users () {return await userrebo.find (); }Copy the code
It hangs a userConfig object at the bottom. ResolveField() means that it is a subdomain; @parent () says its Parent is User
Retrieve the user configuration / * * * * / @ ResolveField () is async userConfig (@ Parent () the user: the user, @ Loader (UserConfigDataLoader. Name) dataLoader: DataLoader<string, UserConfig>) { return await dataLoader.load(user.id); }Copy the code
Always define variables in the parent class when a subdomain is mounted. * Error: Undefined type error. Make sure you are providing an explicit type for the “userConfig” of the “UserResolver” class.
Graphql has too many query data layers and nested problems back and forth.
We’ve had this problem before, when we got a get request, it returned 200. Net ::ERR_CONTENT_LENGTH_MISMATCH 200 (OK) net::ERR_CONTENT_LENGTH_MISMATCH 200 (OK) net::ERR_CONTENT_LENGTH_MISMATCH 200 (OK) net::ERR_CONTENT_LENGTH_MISMATCH 200 (OK) To avoid this nesting back and forth, there are too many query fields, causing server performance to degrade. Limit the size of the request body, limit the level (i.e., the depth). Later, IT was found that Nest had a complex library, which solved the problem as shown in the picture below.
With the GraphQL-Query-Complexity library, each field has one complexity (the default is 1), and then you limit the complexity to the maximum, no matter how many layers you nest. Prevent you from having too many query fields nested back and forth.
query {
author(id: "abc") { # complexity: 1
title # complexity: 1
}
}
Copy the code
Error TS2420: Error TS2420: Class ‘ComplexityPlugin’ incorrectly implements interface ‘ApolloServerPlugin
>’. :
import { HttpStatus } from '@nestjs/common'; import { GraphQLSchemaHost, Plugin } from '@nestjs/graphql'; import { ApolloServerPlugin, GraphQLRequestListener } from 'apollo-server-plugin-base'; import { fieldExtensionsEstimator, getComplexity, simpleEstimator } from 'graphql-query-complexity'; import { CustomException } from '.. /http-handle/custom-exception'; @Plugin() export class ComplexityPlugin implements ApolloServerPlugin { constructor(private gqlSchemaHost: GraphQLSchemaHost) {} requestDidStart(): GraphQLRequestListener { const { schema } = this.gqlSchemaHost; return { didResolveOperation({ request, document }) { const complexity = getComplexity({ schema, operationName: request.operationName, query: document, variables: request.variables, estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })], }); If (complexity > 50) {throw new CustomException(' graphQL query is too complex: ${complexity}. Maximum allowed complexity: 50`, HttpStatus.BAD_REQUEST); }}}; }} // App. Module. Ts providers: [ComplexityPlugin,],Copy the code
If graphQL uses a transform.interceptor, beware!
// This package encapsulates only Resetful, but not graphQL. Apollo’s return requires a data. Expected Iterable, but did not find one for field “query.xxx “.”,
/** * encapsulate the correct return format * {* data, * code: 200, * message: 'success' * } */ @Injectable() export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler<T>): Observable<Response<T>> { return next.handle().pipe( map(data => { return { data, code: 200, message: 'success', }; })); }}Copy the code
The following additional actions should be done:
interface Response<T> { data: T; } /** * encapsulates the correct return format * {* data, * code: 200, * message: 'success' * } */ @Injectable() export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> { const ctx = GqlExecutionContext.create(context); const graphqlRequest = ctx.getContext().req; const restfulRequest = context.switchToHttp().getRequest(); if (restfulRequest) { return next.handle().pipe( map(data => { return { data, code: 200, message: 'success', }; })); } else if (graphqlRequest) { return next.handle().pipe(tap()); }}}Copy the code
If you want to run locally, please change the directory below the config/dev to know the database connection can be. Synchronize: true A table is created automatically when it is synchronized with a table. Never turn this on during proD.
export default {
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'liang',
timezone: 'UTC',
charset: 'utf8mb4',
synchronize: true,
logging: false,
autoLoadEntities: true,
};
Copy the code
Send Buddha to west, and send a database script.
// userConfig INSERT INTO "public"."userConfig"("id", "createTime", "updateTime", "deleteTime", "version", "userId", "Fee ", "feeType") VALUES (' C7E35F7C-6bd2-429E-A65C-194405e321 ', '2020-11-22 10:42:57', '2020-11-22 10:43:000.07196 ', NULL, 1, 'b5d57AF1-7118-48C4-AC75-7bb282d5a5B2 ', '10',' I'm enumeration String'); // user INSERT INTO "public"."user"("createTime", "updateTime", "deleteTime", "version", "name", "phone", "roleNo", "Locked" VALUES ('2020-11-21 20:09:42', '2020-11-21 20:10:42.834375', NULL, 1, '220-11-21 20:10:42.834375', '100', 'f', '[email protected]', 'b5d57af1-7118-48c4-ac75-7bb282d5a5b2');Copy the code
I have been busy recently. I hope the next one will not take so long to write like this one.
Purely original and handwritten, Github wants you to hit a star. thank you