Skip to content

Commit

Permalink
Merge 86f8a09 into 79fd2fd
Browse files Browse the repository at this point in the history
  • Loading branch information
its-dibo committed Jun 14, 2024
2 parents 79fd2fd + 86f8a09 commit 4996eaa
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 3 deletions.
4 changes: 4 additions & 0 deletions packages/crud-request/src/request-query.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export class RequestQueryBuilder {
};
}

/**
* get the default options of the query builder
* @returns
*/
static getOptions(): RequestQueryBuilderOptions {
return RequestQueryBuilder._options;
}
Expand Down
1 change: 1 addition & 0 deletions packages/crud-util/src/checks.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const isArrayFull = (val: any): boolean => Array.isArray(val) && hasLengt
export const isArrayStrings = (val: any): boolean =>
isArrayFull(val) && (val as string[]).every((v) => isStringFull(v));
export const isObject = (val: any): boolean => typeof val === 'object' && !isNull(val);
/** if the value is an object that contains at least one element */
export const isObjectFull = (val: any) => isObject(val) && hasLength(objKeys(val));
export const isNumber = (val: any): boolean =>
typeof val === 'number' && !Number.isNaN(val) && Number.isFinite(val);
Expand Down
129 changes: 127 additions & 2 deletions packages/crud/src/crud/crud-routes.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,19 @@ import { SerializeHelper } from './serialize.helper';
import { Swagger } from './swagger.helper';
import { Validation } from './validation.helper';

export interface SwaggerModels {
get?: any;
getMany?: any;
create?: any;
update?: any;
replace?: any;
delete?: any;
recover?: any;
}

export class CrudRoutesFactory {
protected options: MergedCrudOptions;
protected swaggerModels: any = {};
protected swaggerModels: SwaggerModels = {};

constructor(protected target: any, options: CrudOptions) {
this.options = options;
Expand Down Expand Up @@ -72,6 +82,9 @@ export class CrudRoutesFactory {
this.enableRoutes(routesSchema);
}

/**
* merge options with global options that provided via CrudConfigService.load()
*/
protected mergeOptions() {
// merge auth config
const authOptions = R.getCrudAuthOptions(this.target);
Expand All @@ -94,7 +107,7 @@ export class CrudRoutesFactory {
// merge routes config
const routes = isObjectFull(this.options.routes) ? this.options.routes : {};
this.options.routes = deepmerge(CrudConfigService.config.routes, routes, {
arrayMerge: (a, b, c) => b,
arrayMerge: (target, source, opts) => source,
});

// merge operators config
Expand Down Expand Up @@ -152,6 +165,11 @@ export class CrudRoutesFactory {
R.setCrudOptions(this.options, this.target);
}

/**
* an array of routes schema for methods (routes) to be generated for the target controller
* such as getManyBase, ...
* @returns
*/
protected getRoutesSchema(): BaseRoute[] {
return [
{
Expand Down Expand Up @@ -221,6 +239,19 @@ export class CrudRoutesFactory {
];
}

/**
* generate `getManyBase()` method for the controller
* note that `this` here will refer to the target i.e. the controller
*
* the generated method is similar to:
* ```
* class UserController{
* getManyBase(req){
* return this.service.getMany(req);
* }
* }
* ```
*/
protected getManyBase(name: BaseRouteName) {
this.targetProto[name] = function getManyBase(req: CrudRequest) {
return this.service.getMany(req);
Expand Down Expand Up @@ -269,6 +300,13 @@ export class CrudRoutesFactory {
};
}

/**
* check if the route can be created
* i.e. included in only[] or doesn't included in exclude[] options
* if only[] has values, exclude[] is ignored
* @param name
* @returns
*/
protected canCreateRoute(name: BaseRouteName) {
const only = this.options.routes.only;
const exclude = this.options.routes.exclude;
Expand All @@ -289,10 +327,30 @@ export class CrudRoutesFactory {
return true;
}

/**
* generate Swagger response DTO for each operation
* it uses options.serialize for each operation, or the model type (i.e. the entity)
* @returns an object contains DTO for each operation
* @example
* ```
* {
* get: UserEntity,
* getMany: createGetManyDto(UserEntity),
* create: UserEntity,
* update: UserEntity,
* replace: UserEntity,
* delete: UserEntity,
* recover: UserEntity,
* }
* ```
*/
protected setResponseModels() {
// if model.type is a function or class, use it
// otherwise generate an empty DTO class
const modelType = isFunction(this.modelType)
? this.modelType
: SerializeHelper.createGetOneResponseDto(this.modelName);

this.swaggerModels.get = isFunction(this.options.serialize.get)
? this.options.serialize.get
: modelType;
Expand All @@ -314,17 +372,24 @@ export class CrudRoutesFactory {
this.swaggerModels.recover = isFunction(this.options.serialize.recover)
? this.options.serialize.recover
: modelType;

Swagger.setExtraModels(this.swaggerModels);
}

/**
* generate the controller's methods (routers), such as getManyBase(), ...
* @param routesSchema
*/
protected createRoutes(routesSchema: BaseRoute[]) {
// primary keys that are not disabled, i.e. doesn't have `{ disabled: true }`
const primaryParams = this.getPrimaryParams().filter(
(param) => !this.options.params[param].disabled,
);

routesSchema.forEach((route) => {
if (this.canCreateRoute(route.name)) {
// create base method
// call this.getManyBase("getManyBase"), and so on ...
this[route.name](route.name);
route.enable = true;
// set metadata
Expand Down Expand Up @@ -431,6 +496,10 @@ export class CrudRoutesFactory {
}
}

/**
* get the params that has `primary: true`
* @returns
*/
protected getPrimaryParams(): string[] {
return objKeys(this.options.params).filter(
(param) => this.options.params[param] && this.options.params[param].primary,
Expand All @@ -450,6 +519,23 @@ export class CrudRoutesFactory {
this.setDecorators(name);
}

/**
* generate body DTO
* and add the NestJs ValidationPipe to create, update and replace operations
* ValidationPipe.group option is used to distinguish between the DTO for creating and the DTO for updating
* for example each prop in body is optional in "update", but required in "create" operation
* https://gid-oss.github.io/dataui-nestjs-crud/controllers/#request-validation
*
* ```
* class UserEntity{
* @IsOptional({ groups: [UPDATE] })
* @IsNotEmpty({ groups: [CREATE] })
* @Column({ ...})
* name: string;
* }
* ```
* @param name
*/
protected setRouteArgs(name: BaseRouteName) {
let rest = {};
const routes: BaseRouteName[] = [
Expand All @@ -459,6 +545,7 @@ export class CrudRoutesFactory {
'replaceOneBase',
];

// add ValidationPipe to create, update and replace operations
if (isIn(name, routes)) {
const action = this.routeNameAction(name);
const hasDto = !isNil(this.options.dto[action]);
Expand All @@ -485,6 +572,12 @@ export class CrudRoutesFactory {
}
}

/**
* add metadata for interceptors to the routes
* including the built-in CrudRequestInterceptor, CrudResponseInterceptor
* and the interceptors provided by the user via options
* @param name
*/
protected setInterceptors(name: BaseRouteName) {
const interceptors = this.options.routes[name].interceptors;
R.setInterceptors(
Expand All @@ -506,35 +599,60 @@ export class CrudRoutesFactory {
);
}

/**
* add metadata for the action's name of the operation to the route method
* @param name
*/
protected setAction(name: BaseRouteName) {
R.setAction(this.actionsMap[name], this.targetProto[name]);
}

/**
* add metadata for Swagger's \@ApiOperation() to the method
* @param name
*/
protected setSwaggerOperation(name: BaseRouteName) {
const summary = Swagger.operationsMap(this.modelName)[name];
// example: getManyBaseUsersControllerUserEntity
const operationId = name + this.targetProto.constructor.name + this.modelName;
Swagger.setOperation({ summary, operationId }, this.targetProto[name]);
}

/**
* add metadata for Swagger's path params to the method
* i.e. @ApiParam()
* @param name
*/
protected setSwaggerPathParams(name: BaseRouteName) {
const metadata = Swagger.getParams(this.targetProto[name]);
// operations that don't need the primary key
const withoutPrimary: BaseRouteName[] = [
'createManyBase',
'createOneBase',
'getManyBase',
];

// true if withoutPrimary[] includes the operation
const removePrimary = isIn(name, withoutPrimary);

// get path params that are not disabled, and not primary key (if removePrimary is true)
// for this path `users/:id` the params are ['id']
const params = objKeys(this.options.params)
.filter((key) => !this.options.params[key].disabled)
.filter((key) => !(removePrimary && this.options.params[key].primary))
// example: { id: opts.params.id, ... }
.reduce((a, c) => ({ ...a, [c]: this.options.params[c] }), {});

const pathParamsMeta = Swagger.createPathParamsMeta(params);
Swagger.setParams([...metadata, ...pathParamsMeta], this.targetProto[name]);
}

/**
* add metadata for Swagger's \@ApiQuery()
* @param name
*/
protected setSwaggerQueryParams(name: BaseRouteName) {
// existing metadata
const metadata = Swagger.getParams(this.targetProto[name]);
const queryParamsMeta = Swagger.createQueryParamsMeta(name, this.options);
Swagger.setParams([...metadata, ...queryParamsMeta], this.targetProto[name]);
Expand All @@ -548,6 +666,13 @@ export class CrudRoutesFactory {
Swagger.setResponseOk({ ...metadata, ...metadataToAdd }, this.targetProto[name]);
}

/**
* get the Action's name
* @param name the route's name, such as getManyBase, createOneBase, ...
* @returns
* @example getOneBase -> get
* @example createOneBase -> create
*/
protected routeNameAction(name: BaseRouteName): string {
return (
name.split('OneBase')[0] || /* istanbul ignore next */ name.split('ManyBase')[0]
Expand Down
6 changes: 6 additions & 0 deletions packages/crud/src/crud/reflection.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ export class R {
return R.createCustomRouteArg(PARSED_CRUD_REQUEST_KEY, index);
}

/**
* add pipes for the request's body
* @param index
* @param pipes
* @returns
*/
static setBodyArg(index: number, /* istanbul ignore next */ pipes: any[] = []) {
return R.createRouteArg(RouteParamtypes.BODY, index, pipes);
}
Expand Down
21 changes: 21 additions & 0 deletions packages/crud/src/crud/serialize.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { GetManyDefaultResponse } from '../interfaces';
import { ApiProperty } from './swagger.helper';

export class SerializeHelper {
/**
* generate DTO for getMany
* add additional fields for pagination such as count, total, ...
*
* @param dto the DTO or entity that used for getOne
* @param resourceName
* @returns
*/
static createGetManyDto(dto: any, resourceName: string): any {
class GetManyResponseDto implements GetManyDefaultResponse<any> {
@ApiProperty({ type: dto, isArray: true })
Expand Down Expand Up @@ -30,6 +38,19 @@ export class SerializeHelper {
return GetManyResponseDto;
}

/**
* generate a DTO from an entity name
* @param resourceName
* @returns a class that has a `name` property
* @example
* ```
* let UserDTO = createGetOneResponseDto("user")
* console.log(UserDTO);
* // Class UserDTO{
* // name: "userResponseDto";
* // }
* ```
*/
static createGetOneResponseDto(resourceName: string): any {
class GetOneResponseDto {}

Expand Down
Loading

0 comments on commit 4996eaa

Please sign in to comment.