Skip to content

Commit

Permalink
feat(crud): added dto options
Browse files Browse the repository at this point in the history
fix #132

Co-authored-by: bashleigh <[email protected]>
  • Loading branch information
michaelyali and bashleigh committed Oct 19, 2019
1 parent 883cef9 commit c189bab
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 26 deletions.
1 change: 0 additions & 1 deletion packages/crud-request/src/request-query.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export class RequestQueryParser implements ParsedRequestParams {
public authFilter: ObjectLiteral = undefined;
public authPersist: ObjectLiteral = undefined;
public search: SCondition;
public searchJson: SCondition;
public filter: QueryFilter[] = [];
public or: QueryFilter[] = [];
public join: QueryJoin[] = [];
Expand Down
35 changes: 22 additions & 13 deletions packages/crud/src/crud/crud-routes.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ export class CrudRoutesFactory {
};
}

// set dto
if (!isObjectFull(this.options.dto)) {
this.options.dto = {};
}

R.setCrudOptions(this.options, this.target);
}

Expand Down Expand Up @@ -348,18 +353,20 @@ export class CrudRoutesFactory {

private setRouteArgs(name: BaseRouteName) {
let rest = {};
const toValidate: BaseRouteName[] = [
const routes: BaseRouteName[] = [
'createManyBase',
'createOneBase',
'updateOneBase',
'replaceOneBase',
];

if (isIn(name, toValidate)) {
const group =
isEqual(name, 'updateOneBase') || isEqual(name, 'replaceOneBase')
? CrudValidationGroups.UPDATE
: CrudValidationGroups.CREATE;
if (isIn(name, routes)) {
const action = this.routeNameAction(name);
const hasDto = !isNil(this.options.dto[action]);
const { UPDATE, CREATE } = CrudValidationGroups;
const groupEnum = isIn(name, ['updateOneBase', 'replaceOneBase']) ? UPDATE : CREATE;
const group = !hasDto ? groupEnum : undefined;

rest = R.setBodyArg(1, [Validation.getValidationPipe(this.options, group)]);
}

Expand All @@ -370,12 +377,10 @@ export class CrudRoutesFactory {
if (isEqual(name, 'createManyBase')) {
const bulkDto = Validation.createBulkDto(this.options);
R.setRouteArgsTypes([Object, bulkDto], this.targetProto, name);
} else if (
isEqual(name, 'createOneBase') ||
isEqual(name, 'updateOneBase') ||
isEqual(name, 'replaceOneBase')
) {
R.setRouteArgsTypes([Object, this.modelType], this.targetProto, name);
} else if (isIn(name, ['createOneBase', 'updateOneBase', 'replaceOneBase'])) {
const action = this.routeNameAction(name);
const dto = this.options.dto[action] || this.modelType;
R.setRouteArgsTypes([Object, dto], this.targetProto, name);
} else {
R.setRouteArgsTypes([Object], this.targetProto, name);
}
Expand Down Expand Up @@ -441,7 +446,11 @@ export class CrudRoutesFactory {
: HttpStatus.OK;
const isArray = isEqual(name, 'createManyBase') || isEqual(name, 'getManyBase');
const metadata = Swagger.getResponseOk(this.targetProto[name]);
const responseOkMeta = Swagger.createReponseOkMeta(status, isArray, this.modelType);
const responseOkMeta = Swagger.createResponseOkMeta(status, isArray, this.modelType);
Swagger.setResponseOk({ ...metadata, ...responseOkMeta }, this.targetProto[name]);
}

private routeNameAction(name: BaseRouteName): string {
return name.split('OneBase')[0];
}
}
2 changes: 1 addition & 1 deletion packages/crud/src/crud/swagger.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class Swagger {
return swaggerPkg ? R.get(swaggerPkg.DECORATORS.API_RESPONSE, func) || {} : {};
}

static createReponseOkMeta(status: HttpStatus, isArray: boolean, dto: any): any {
static createResponseOkMeta(status: HttpStatus, isArray: boolean, dto: any): any {
return swaggerPkg
? {
[status]: {
Expand Down
26 changes: 15 additions & 11 deletions packages/crud/src/crud/validation.helper.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ValidationPipe } from '@nestjs/common';
import { isFalse } from '@nestjsx/util';
import { isFalse, isNil } from '@nestjsx/util';

import { CrudValidationGroups } from '../enums';
import { CreateManyDto, CrudOptions } from '../interfaces';
import { CreateManyDto, CrudOptions, MergedCrudOptions } from '../interfaces';
import { safeRequire } from '../util';

const validator = safeRequire('class-validator');
Expand All @@ -28,28 +28,32 @@ class BulkDto<T> implements CreateManyDto<T> {
export class Validation {
static getValidationPipe(
options: CrudOptions,
group: CrudValidationGroups,
group?: CrudValidationGroups,
): ValidationPipe {
return validator && !isFalse(options.validation)
? new ValidationPipe({ ...(options.validation || {}), groups: [group] })
? new ValidationPipe({
...(options.validation || {}),
groups: group ? [group] : undefined,
})
: /* istanbul ignore next */ undefined;
}

static createBulkDto<T = any>(options: CrudOptions): any {
static createBulkDto<T = any>(options: MergedCrudOptions): any {
/* istanbul ignore else */
if (validator && transformer && !isFalse(options.validation)) {
const { IsArray, ArrayNotEmpty, ValidateNested } = validator;
const { Type } = transformer;
const groups = [CrudValidationGroups.CREATE];

const Model = options.model.type;
const hasDto = !isNil(options.dto.create);
const groups = !hasDto ? [CrudValidationGroups.CREATE] : undefined;
const always = hasDto ? true : undefined;
const Model = hasDto ? options.dto.create : options.model.type;

// tslint:disable-next-line:max-classes-per-file
class BulkDtoImpl implements CreateManyDto<T> {
@ApiModelProperty({ type: Model, isArray: true })
@IsArray({ groups })
@ArrayNotEmpty({ groups })
@ValidateNested({ each: true, groups })
@IsArray({ groups, always })
@ArrayNotEmpty({ groups, always })
@ValidateNested({ each: true, groups, always })
@Type(() => Model)
bulk: T[];
}
Expand Down
2 changes: 2 additions & 0 deletions packages/crud/src/interfaces/crud-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ParamsOptions } from './params-options.interface';
import { QueryOptions } from './query-options.interface';
import { RoutesOptions } from './routes-options.interface';
import { AuthOptions } from './auth-options.interface';
import { DtoOptions } from './dto-options.interface';

export interface CrudRequestOptions {
query?: QueryOptions;
Expand All @@ -14,6 +15,7 @@ export interface CrudRequestOptions {

export interface CrudOptions {
model: ModelOptions;
dto?: DtoOptions;
query?: QueryOptions;
routes?: RoutesOptions;
params?: ParamsOptions;
Expand Down
5 changes: 5 additions & 0 deletions packages/crud/src/interfaces/dto-options.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface DtoOptions {
create?: any;
update?: any;
replace?: any;
}
1 change: 1 addition & 0 deletions packages/crud/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './model-options.interface';
export * from './create-many-dto.interface';
export * from './get-many-default-response.interface';
export * from './crud-global-config.interface';
export * from './dto-options.interface';
2 changes: 2 additions & 0 deletions packages/crud/test/__fixture__/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './test-create.dto';
export * from './test-update.dto';
22 changes: 22 additions & 0 deletions packages/crud/test/__fixture__/dto/test-create.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
IsString,
IsEmail,
IsNumber,
IsOptional,
IsNotEmpty,
IsEmpty,
} from 'class-validator';

export class TestCreateDto {
@IsString()
firstName: string;

@IsString()
lastName: string;

@IsEmail({ require_tld: false })
email: string;

@IsNumber()
age: number;
}
26 changes: 26 additions & 0 deletions packages/crud/test/__fixture__/dto/test-update.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
IsString,
IsEmail,
IsNumber,
IsOptional,
IsNotEmpty,
IsEmpty,
} from 'class-validator';

export class TestUpdateDto {
@IsOptional()
@IsString()
firstName?: string;

@IsOptional()
@IsString()
lastName?: string;

@IsOptional()
@IsEmail({ require_tld: false })
email?: string;

@IsOptional()
@IsNumber()
age?: number;
}
108 changes: 108 additions & 0 deletions packages/crud/test/crud.dto.options.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { Controller, INestApplication } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

import { Crud } from '../src/decorators/crud.decorator';
import { HttpExceptionFilter } from './__fixture__/exception.filter';
import { TestModel } from './__fixture__/test.model';
import { TestCreateDto, TestUpdateDto } from './__fixture__/dto';
import { TestService } from './__fixture__/test.service';

describe('#crud', () => {
describe('#dto options', () => {
let app: INestApplication;
let server: any;

@Crud({
model: {
type: TestModel,
},
dto: {
create: TestCreateDto,
update: TestUpdateDto,
},
})
@Controller('test')
class TestController {
constructor(public service: TestService<TestModel>) {}
}

beforeAll(async () => {
const fixture = await Test.createTestingModule({
controllers: [TestController],
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }, TestService],
}).compile();

app = fixture.createNestApplication();

await app.init();
server = app.getHttpServer();
});

afterAll(async () => {
await app.close();
});

describe('#createOneBase', () => {
it('should return status 201', () => {
const send: TestCreateDto = {
firstName: 'firstName',
lastName: 'lastName',
email: '[email protected]',
age: 15,
};
return request(server)
.post('/test')
.send(send)
.expect(201);
});
it('should return status 400', (done) => {
const send: TestModel = {
firstName: 'firstName',
lastName: 'lastName',
email: '[email protected]',
};
return request(server)
.post('/test')
.send(send)
.expect(400)
.end((_, res) => {
expect(res.body.message[0].property).toBe('age');
done();
});
});
});

describe('#updateOneBase', () => {
it('should return status 200', () => {
const send: TestModel = {
id: 1,
firstName: 'firstName',
lastName: 'lastName',
email: '[email protected]',
age: 15,
};
return request(server)
.patch('/test/1')
.send(send)
.expect(200);
});
it('should return status 400', (done) => {
const send: TestModel = {
firstName: 'firstName',
lastName: 'lastName',
email: 'foo',
};
return request(server)
.patch('/test/1')
.send(send)
.expect(400)
.end((_, res) => {
expect(res.body.message[0].property).toBe('email');
done();
});
});
});
});
});

0 comments on commit c189bab

Please sign in to comment.