diff --git a/packages/crud-request/src/interfaces/parsed-request.interface.ts b/packages/crud-request/src/interfaces/parsed-request.interface.ts index 130fe9ed..7b4bbf55 100644 --- a/packages/crud-request/src/interfaces/parsed-request.interface.ts +++ b/packages/crud-request/src/interfaces/parsed-request.interface.ts @@ -1,10 +1,12 @@ import { ObjectLiteral } from '@nestjsx/util'; +import { ClassTransformOptions } from 'class-transformer'; import { QueryFields, QueryFilter, QueryJoin, QuerySort, SCondition } from '../types'; export interface ParsedRequestParams { fields: QueryFields; paramsFilter: QueryFilter[]; authPersist: ObjectLiteral; + classTransformOptions: ClassTransformOptions; search: SCondition; filter: QueryFilter[]; or: QueryFilter[]; diff --git a/packages/crud-request/src/request-query.parser.ts b/packages/crud-request/src/request-query.parser.ts index 4bf79f96..b4a42563 100644 --- a/packages/crud-request/src/request-query.parser.ts +++ b/packages/crud-request/src/request-query.parser.ts @@ -11,6 +11,7 @@ import { isNil, ObjectLiteral, } from '@nestjsx/util'; +import { ClassTransformOptions } from 'class-transformer'; import { RequestQueryException } from './exceptions'; import { ParamsOptions, ParsedRequestParams, RequestQueryBuilderOptions } from './interfaces'; @@ -42,6 +43,8 @@ export class RequestQueryParser implements ParsedRequestParams { public authPersist: ObjectLiteral = undefined; + public classTransformOptions: ClassTransformOptions = undefined; + public search: SCondition; public filter: QueryFilter[] = []; @@ -83,6 +86,7 @@ export class RequestQueryParser implements ParsedRequestParams { fields: this.fields, paramsFilter: this.paramsFilter, authPersist: this.authPersist, + classTransformOptions: this.classTransformOptions, search: this.search, filter: this.filter, or: this.or, @@ -144,6 +148,10 @@ export class RequestQueryParser implements ParsedRequestParams { this.authPersist = persist || /* istanbul ignore next */ {}; } + setClassTransformOptions(options: ClassTransformOptions = {}) { + this.classTransformOptions = options || /* istanbul ignore next */ {}; + } + convertFilterToSearch(filter: QueryFilter): SFields | SConditionAND { const isEmptyValue = { isnull: true, diff --git a/packages/crud-request/test/request-query.parser.spec.ts b/packages/crud-request/test/request-query.parser.spec.ts index f0a2fb74..f908bcdd 100644 --- a/packages/crud-request/test/request-query.parser.spec.ts +++ b/packages/crud-request/test/request-query.parser.spec.ts @@ -457,6 +457,19 @@ describe('#request-query', () => { }); }); + describe('#setClassTransformOptions', () => { + it('it should set classTransformOptions, 1', () => { + qp.setClassTransformOptions(); + expect(qp.classTransformOptions).toMatchObject({}); + }); + it('it should set classTransformOptions, 2', () => { + const testOptions = { groups: ['TEST'] }; + qp.setClassTransformOptions(testOptions); + const parsed = qp.getParsed(); + expect(parsed.classTransformOptions).toMatchObject(testOptions); + }); + }); + describe('#getParsed', () => { it('should return parsed params', () => { const expected: ParsedRequestParams = { @@ -464,6 +477,7 @@ describe('#request-query', () => { paramsFilter: [], search: undefined, authPersist: undefined, + classTransformOptions: undefined, filter: [], or: [], join: [], diff --git a/packages/crud-typeorm/src/typeorm-crud.service.ts b/packages/crud-typeorm/src/typeorm-crud.service.ts index 9e701127..60f36856 100644 --- a/packages/crud-typeorm/src/typeorm-crud.service.ts +++ b/packages/crud-typeorm/src/typeorm-crud.service.ts @@ -169,7 +169,9 @@ export class TypeOrmCrudService extends CrudService { const toSave = !allowParamsOverride ? { ...found, ...dto, ...paramsFilters, ...req.parsed.authPersist } : { ...found, ...dto, ...req.parsed.authPersist }; - const updated = await this.repo.save(plainToClass(this.entityType, toSave) as unknown as DeepPartial); + const updated = await this.repo.save( + plainToClass(this.entityType, toSave, req.parsed.classTransformOptions) as unknown as DeepPartial, + ); if (returnShallow) { return updated; @@ -209,7 +211,9 @@ export class TypeOrmCrudService extends CrudService { ...dto, ...req.parsed.authPersist, }; - const replaced = await this.repo.save(plainToClass(this.entityType, toSave) as unknown as DeepPartial); + const replaced = await this.repo.save( + plainToClass(this.entityType, toSave, req.parsed.classTransformOptions) as unknown as DeepPartial, + ); if (returnShallow) { return replaced; @@ -233,7 +237,9 @@ export class TypeOrmCrudService extends CrudService { public async deleteOne(req: CrudRequest): Promise { const { returnDeleted } = req.options.routes.deleteOneBase; const found = await this.getOneOrFail(req, returnDeleted); - const toReturn = returnDeleted ? plainToClass(this.entityType, { ...found }) : undefined; + const toReturn = returnDeleted + ? plainToClass(this.entityType, { ...found }, req.parsed.classTransformOptions) + : undefined; const deleted = req.options.query.softDelete === true ? await this.repo.softRemove(found as unknown as DeepPartial) @@ -421,7 +427,7 @@ export class TypeOrmCrudService extends CrudService { return dto instanceof this.entityType ? Object.assign(dto, parsed.authPersist) - : plainToClass(this.entityType, { ...dto, ...parsed.authPersist }); + : plainToClass(this.entityType, { ...dto, ...parsed.authPersist }, parsed.classTransformOptions); } protected getAllowedColumns(columns: string[], options: QueryOptions): string[] { diff --git a/packages/crud/src/crud/crud-routes.factory.ts b/packages/crud/src/crud/crud-routes.factory.ts index ccdf9603..de347c99 100644 --- a/packages/crud/src/crud/crud-routes.factory.ts +++ b/packages/crud/src/crud/crud-routes.factory.ts @@ -80,6 +80,12 @@ export class CrudRoutesFactory { if (isUndefined(this.options.auth.property)) { this.options.auth.property = CrudConfigService.config.auth.property; } + if (isUndefined(this.options.auth.groups)) { + this.options.auth.groups = CrudConfigService.config.auth.groups; + } + if (isUndefined(this.options.auth.classTransformOptions)) { + this.options.auth.classTransformOptions = CrudConfigService.config.auth.classTransformOptions; + } // merge query config const query = isObjectFull(this.options.query) ? this.options.query : {}; diff --git a/packages/crud/src/interceptors/crud-request.interceptor.ts b/packages/crud/src/interceptors/crud-request.interceptor.ts index d326269f..d7d53c66 100644 --- a/packages/crud/src/interceptors/crud-request.interceptor.ts +++ b/packages/crud/src/interceptors/crud-request.interceptor.ts @@ -1,6 +1,7 @@ import { BadRequestException, CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { RequestQueryException, RequestQueryParser, SCondition, QueryFilter } from '@nestjsx/crud-request'; import { isNil, isFunction, isArrayFull, hasLength } from '@nestjsx/util'; +import { ClassTransformOptions } from 'class-transformer'; import { PARSED_CRUD_REQUEST_KEY } from '../constants'; import { CrudActions } from '../enums'; @@ -142,6 +143,16 @@ export class CrudRequestInterceptor extends CrudBaseInterceptor implements NestI if (isFunction(crudOptions.auth.persist)) { parser.setAuthPersist(crudOptions.auth.persist(userOrRequest)); } + + const options: ClassTransformOptions = {}; + if (isFunction(crudOptions.auth.classTransformOptions)) { + Object.assign(options, crudOptions.auth.classTransformOptions(userOrRequest)); + } + + if (isFunction(crudOptions.auth.groups)) { + options.groups = crudOptions.auth.groups(userOrRequest); + } + parser.setClassTransformOptions(options); } return auth; diff --git a/packages/crud/src/interceptors/crud-response.interceptor.ts b/packages/crud/src/interceptors/crud-response.interceptor.ts index b584585f..862ed383 100644 --- a/packages/crud/src/interceptors/crud-response.interceptor.ts +++ b/packages/crud/src/interceptors/crud-response.interceptor.ts @@ -1,6 +1,6 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { isFalse, isObject, isFunction } from '@nestjsx/util'; -import { classToPlain, classToPlainFromExist } from 'class-transformer'; +import { classToPlain, classToPlainFromExist, ClassTransformOptions } from 'class-transformer'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { CrudActions } from '../enums'; @@ -27,31 +27,51 @@ export class CrudResponseInterceptor extends CrudBaseInterceptor implements Nest return next.handle().pipe(map((data) => this.serialize(context, data))); } - protected transform(dto: any, data: any) { + protected transform(dto: any, data: any, options: ClassTransformOptions) { if (!isObject(data) || isFalse(dto)) { return data; } if (!isFunction(dto)) { - return data.constructor !== Object ? classToPlain(data) : data; + return data.constructor !== Object ? classToPlain(data, options) : data; } - return data instanceof dto ? classToPlain(data) : classToPlain(classToPlainFromExist(data, new dto())); + return data instanceof dto + ? classToPlain(data, options) + : classToPlain(classToPlainFromExist(data, new dto()), options); } protected serialize(context: ExecutionContext, data: any): any { + const req = context.switchToHttp().getRequest(); const { crudOptions, action } = this.getCrudInfo(context); const { serialize } = crudOptions; const dto = serialize[actionToDtoNameMap[action]]; const isArray = Array.isArray(data); + const options: ClassTransformOptions = {}; + /* istanbul ignore else */ + if (isFunction(crudOptions.auth?.classTransformOptions)) { + const userOrRequest = crudOptions.auth.property ? req[crudOptions.auth.property] : req; + Object.assign(options, crudOptions.auth.classTransformOptions(userOrRequest)); + } + + /* istanbul ignore else */ + if (isFunction(crudOptions.auth?.groups)) { + const userOrRequest = crudOptions.auth.property ? req[crudOptions.auth.property] : req; + options.groups = crudOptions.auth.groups(userOrRequest); + } + switch (action) { case CrudActions.ReadAll: - return isArray ? (data as any[]).map((item) => this.transform(serialize.get, item)) : this.transform(dto, data); + return isArray + ? (data as any[]).map((item) => this.transform(serialize.get, item, options)) + : this.transform(dto, data, options); case CrudActions.CreateMany: - return isArray ? (data as any[]).map((item) => this.transform(dto, item)) : this.transform(dto, data); + return isArray + ? (data as any[]).map((item) => this.transform(dto, item, options)) + : this.transform(dto, data, options); default: - return this.transform(dto, data); + return this.transform(dto, data, options); } } } diff --git a/packages/crud/src/interfaces/auth-options.interface.ts b/packages/crud/src/interfaces/auth-options.interface.ts index 84d8bdf8..cf933693 100644 --- a/packages/crud/src/interfaces/auth-options.interface.ts +++ b/packages/crud/src/interfaces/auth-options.interface.ts @@ -1,12 +1,21 @@ import { SCondition } from '@nestjsx/crud-request/lib/types/request-query.types'; import { ObjectLiteral } from '@nestjsx/util'; +import { ClassTransformOptions } from 'class-transformer'; export interface AuthGlobalOptions { property?: string; + /** Get options for the `classToPlain` function (response) */ + classTransformOptions?: (req: any) => ClassTransformOptions; + /** Get `groups` value for the `classToPlain` function options (response) */ + groups?: (req: any) => string[]; } export interface AuthOptions { property?: string; + /** Get options for the `classToPlain` function (response) */ + classTransformOptions?: (req: any) => ClassTransformOptions; + /** Get `groups` value for the `classToPlain` function options (response) */ + groups?: (req: any) => string[]; filter?: (req: any) => SCondition | void; or?: (req: any) => SCondition | void; persist?: (req: any) => ObjectLiteral; diff --git a/packages/crud/test/crud-request.interceptor.spec.ts b/packages/crud/test/crud-request.interceptor.spec.ts index 73aa6ac8..4cd8bc75 100644 --- a/packages/crud/test/crud-request.interceptor.spec.ts +++ b/packages/crud/test/crud-request.interceptor.spec.ts @@ -1,11 +1,4 @@ -import { - Controller, - Get, - Param, - ParseIntPipe, - Query, - UseInterceptors, -} from '@nestjs/common'; +import { Controller, Get, Param, ParseIntPipe, Query, UseInterceptors } from '@nestjs/common'; import { NestApplication } from '@nestjs/core'; import { Test } from '@nestjs/testing'; import { RequestQueryBuilder } from '@nestjsx/crud-request'; @@ -61,10 +54,7 @@ describe('#crud', () => { @UseInterceptors(CrudRequestInterceptor) @Get('other2/:id/twoParams/:someParam') - async twoParams( - @ParsedRequest() req: CrudRequest, - @Param('someParam', ParseIntPipe) p: number, - ) { + async twoParams(@ParsedRequest() req: CrudRequest, @Param('someParam', ParseIntPipe) p: number) { return { filter: req.parsed.paramsFilter }; } } @@ -123,6 +113,23 @@ describe('#crud', () => { constructor(public service: TestService) {} } + @Crud({ + model: { type: TestModel }, + }) + @CrudAuth({ + groups: () => ['TEST_2'], + classTransformOptions: () => ({ groups: ['TEST_1'] }), + }) + @Controller('test6') + class Test6Controller { + constructor(public service: TestService) {} + + @Override('getManyBase') + get(@ParsedRequest() req: CrudRequest) { + return req; + } + } + let $: supertest.SuperTest; let app: NestApplication; @@ -135,6 +142,7 @@ describe('#crud', () => { Test3Controller, Test4Controller, Test5Controller, + Test6Controller, ], }).compile(); app = module.createNestApplication(); @@ -158,8 +166,15 @@ describe('#crud', () => { const page = 2; const limit = 10; const fields = ['a', 'b', 'c']; - const sorts: any[][] = [['a', 'ASC'], ['b', 'DESC']]; - const filters: any[][] = [['a', 'eq', 1], ['c', 'in', [1, 2, 3]], ['d', 'notnull']]; + const sorts: any[][] = [ + ['a', 'ASC'], + ['b', 'DESC'], + ]; + const filters: any[][] = [ + ['a', 'eq', 1], + ['c', 'in', [1, 2, 3]], + ['d', 'notnull'], + ]; qb.setPage(page).setLimit(limit); qb.select(fields); @@ -170,9 +185,7 @@ describe('#crud', () => { qb.setFilter({ field: f[0], operator: f[1], value: f[2] }); } - const res = await $.get('/test/query') - .query(qb.query()) - .expect(200); + const res = await $.get('/test/query').query(qb.query()).expect(200); expect(res.body.parsed).toHaveProperty('page', page); expect(res.body.parsed).toHaveProperty('limit', limit); expect(res.body.parsed).toHaveProperty('fields', fields); @@ -190,9 +203,7 @@ describe('#crud', () => { }); it('should others working', async () => { - const res = await $.get('/test/other') - .query({ page: 2, per_page: 11 }) - .expect(200); + const res = await $.get('/test/other').query({ page: 2, per_page: 11 }).expect(200); expect(res.body.page).toBe(2); }); @@ -225,9 +236,7 @@ describe('#crud', () => { }); it('should handle authorized request, 1', async () => { - const res = await $.post('/test3') - .send({}) - .expect(201); + const res = await $.post('/test3').send({}).expect(201); const authPersist = { bar: false }; const { parsed } = res.body; expect(parsed.authPersist).toMatchObject(authPersist); @@ -241,19 +250,21 @@ describe('#crud', () => { it('should handle authorized request, 3', async () => { const query = qb.search({ name: 'test' }).query(); - const res = await $.get('/test4') - .query(query) - .expect(200); + const res = await $.get('/test4').query(query).expect(200); const search = { $or: [{ id: 1 }, { $and: [{}, { name: 'test' }] }] }; expect(res.body.parsed.search).toMatchObject(search); }); it('should handle authorized request, 4', async () => { const query = qb.search({ name: 'test' }).query(); - const res = await $.get('/test3') - .query(query) - .expect(200); + const res = await $.get('/test3').query(query).expect(200); const search = { $and: [{ user: 'test', buz: 1 }, { name: 'persist' }] }; expect(res.body.parsed.search).toMatchObject(search); }); + + it('should handle classTransformOptions, 1', async () => { + const res = await $.get('/test6').expect(200); + const groups = ['TEST_2']; + expect(res.body.parsed.classTransformOptions.groups).toMatchObject(groups); + }); }); });