Skip to content

Commit d6d3c4e

Browse files
authored
Merge pull request nestjsx#811 from xTCry/class-transform-options
Added class transform options
2 parents 44f2478 + a516a62 commit d6d3c4e

File tree

9 files changed

+127
-40
lines changed

9 files changed

+127
-40
lines changed

packages/crud-request/src/interfaces/parsed-request.interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { ObjectLiteral } from '@nestjsx/util';
2+
import { ClassTransformOptions } from 'class-transformer';
23
import { QueryFields, QueryFilter, QueryJoin, QuerySort, SCondition } from '../types';
34

45
export interface ParsedRequestParams {
56
fields: QueryFields;
67
paramsFilter: QueryFilter[];
78
authPersist: ObjectLiteral;
9+
classTransformOptions: ClassTransformOptions;
810
search: SCondition;
911
filter: QueryFilter[];
1012
or: QueryFilter[];

packages/crud-request/src/request-query.parser.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
isNil,
1212
ObjectLiteral,
1313
} from '@nestjsx/util';
14+
import { ClassTransformOptions } from 'class-transformer';
1415

1516
import { RequestQueryException } from './exceptions';
1617
import { ParamsOptions, ParsedRequestParams, RequestQueryBuilderOptions } from './interfaces';
@@ -42,6 +43,8 @@ export class RequestQueryParser implements ParsedRequestParams {
4243

4344
public authPersist: ObjectLiteral = undefined;
4445

46+
public classTransformOptions: ClassTransformOptions = undefined;
47+
4548
public search: SCondition;
4649

4750
public filter: QueryFilter[] = [];
@@ -83,6 +86,7 @@ export class RequestQueryParser implements ParsedRequestParams {
8386
fields: this.fields,
8487
paramsFilter: this.paramsFilter,
8588
authPersist: this.authPersist,
89+
classTransformOptions: this.classTransformOptions,
8690
search: this.search,
8791
filter: this.filter,
8892
or: this.or,
@@ -144,6 +148,10 @@ export class RequestQueryParser implements ParsedRequestParams {
144148
this.authPersist = persist || /* istanbul ignore next */ {};
145149
}
146150

151+
setClassTransformOptions(options: ClassTransformOptions = {}) {
152+
this.classTransformOptions = options || /* istanbul ignore next */ {};
153+
}
154+
147155
convertFilterToSearch(filter: QueryFilter): SFields | SConditionAND {
148156
const isEmptyValue = {
149157
isnull: true,

packages/crud-request/test/request-query.parser.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,13 +457,27 @@ describe('#request-query', () => {
457457
});
458458
});
459459

460+
describe('#setClassTransformOptions', () => {
461+
it('it should set classTransformOptions, 1', () => {
462+
qp.setClassTransformOptions();
463+
expect(qp.classTransformOptions).toMatchObject({});
464+
});
465+
it('it should set classTransformOptions, 2', () => {
466+
const testOptions = { groups: ['TEST'] };
467+
qp.setClassTransformOptions(testOptions);
468+
const parsed = qp.getParsed();
469+
expect(parsed.classTransformOptions).toMatchObject(testOptions);
470+
});
471+
});
472+
460473
describe('#getParsed', () => {
461474
it('should return parsed params', () => {
462475
const expected: ParsedRequestParams = {
463476
fields: [],
464477
paramsFilter: [],
465478
search: undefined,
466479
authPersist: undefined,
480+
classTransformOptions: undefined,
467481
filter: [],
468482
or: [],
469483
join: [],

packages/crud-typeorm/src/typeorm-crud.service.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ export class TypeOrmCrudService<T> extends CrudService<T> {
169169
const toSave = !allowParamsOverride
170170
? { ...found, ...dto, ...paramsFilters, ...req.parsed.authPersist }
171171
: { ...found, ...dto, ...req.parsed.authPersist };
172-
const updated = await this.repo.save(plainToClass(this.entityType, toSave) as unknown as DeepPartial<T>);
172+
const updated = await this.repo.save(
173+
plainToClass(this.entityType, toSave, req.parsed.classTransformOptions) as unknown as DeepPartial<T>,
174+
);
173175

174176
if (returnShallow) {
175177
return updated;
@@ -209,7 +211,9 @@ export class TypeOrmCrudService<T> extends CrudService<T> {
209211
...dto,
210212
...req.parsed.authPersist,
211213
};
212-
const replaced = await this.repo.save(plainToClass(this.entityType, toSave) as unknown as DeepPartial<T>);
214+
const replaced = await this.repo.save(
215+
plainToClass(this.entityType, toSave, req.parsed.classTransformOptions) as unknown as DeepPartial<T>,
216+
);
213217

214218
if (returnShallow) {
215219
return replaced;
@@ -233,7 +237,9 @@ export class TypeOrmCrudService<T> extends CrudService<T> {
233237
public async deleteOne(req: CrudRequest): Promise<void | T> {
234238
const { returnDeleted } = req.options.routes.deleteOneBase;
235239
const found = await this.getOneOrFail(req, returnDeleted);
236-
const toReturn = returnDeleted ? plainToClass(this.entityType, { ...found }) : undefined;
240+
const toReturn = returnDeleted
241+
? plainToClass(this.entityType, { ...found }, req.parsed.classTransformOptions)
242+
: undefined;
237243
const deleted =
238244
req.options.query.softDelete === true
239245
? await this.repo.softRemove(found as unknown as DeepPartial<T>)
@@ -421,7 +427,7 @@ export class TypeOrmCrudService<T> extends CrudService<T> {
421427

422428
return dto instanceof this.entityType
423429
? Object.assign(dto, parsed.authPersist)
424-
: plainToClass(this.entityType, { ...dto, ...parsed.authPersist });
430+
: plainToClass(this.entityType, { ...dto, ...parsed.authPersist }, parsed.classTransformOptions);
425431
}
426432

427433
protected getAllowedColumns(columns: string[], options: QueryOptions): string[] {

packages/crud/src/crud/crud-routes.factory.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ export class CrudRoutesFactory {
8080
if (isUndefined(this.options.auth.property)) {
8181
this.options.auth.property = CrudConfigService.config.auth.property;
8282
}
83+
if (isUndefined(this.options.auth.groups)) {
84+
this.options.auth.groups = CrudConfigService.config.auth.groups;
85+
}
86+
if (isUndefined(this.options.auth.classTransformOptions)) {
87+
this.options.auth.classTransformOptions = CrudConfigService.config.auth.classTransformOptions;
88+
}
8389

8490
// merge query config
8591
const query = isObjectFull(this.options.query) ? this.options.query : {};

packages/crud/src/interceptors/crud-request.interceptor.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BadRequestException, CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
22
import { RequestQueryException, RequestQueryParser, SCondition, QueryFilter } from '@nestjsx/crud-request';
33
import { isNil, isFunction, isArrayFull, hasLength } from '@nestjsx/util';
4+
import { ClassTransformOptions } from 'class-transformer';
45

56
import { PARSED_CRUD_REQUEST_KEY } from '../constants';
67
import { CrudActions } from '../enums';
@@ -142,6 +143,16 @@ export class CrudRequestInterceptor extends CrudBaseInterceptor implements NestI
142143
if (isFunction(crudOptions.auth.persist)) {
143144
parser.setAuthPersist(crudOptions.auth.persist(userOrRequest));
144145
}
146+
147+
const options: ClassTransformOptions = {};
148+
if (isFunction(crudOptions.auth.classTransformOptions)) {
149+
Object.assign(options, crudOptions.auth.classTransformOptions(userOrRequest));
150+
}
151+
152+
if (isFunction(crudOptions.auth.groups)) {
153+
options.groups = crudOptions.auth.groups(userOrRequest);
154+
}
155+
parser.setClassTransformOptions(options);
145156
}
146157

147158
return auth;

packages/crud/src/interceptors/crud-response.interceptor.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
22
import { isFalse, isObject, isFunction } from '@nestjsx/util';
3-
import { classToPlain, classToPlainFromExist } from 'class-transformer';
3+
import { classToPlain, classToPlainFromExist, ClassTransformOptions } from 'class-transformer';
44
import { Observable } from 'rxjs';
55
import { map } from 'rxjs/operators';
66
import { CrudActions } from '../enums';
@@ -27,31 +27,51 @@ export class CrudResponseInterceptor extends CrudBaseInterceptor implements Nest
2727
return next.handle().pipe(map((data) => this.serialize(context, data)));
2828
}
2929

30-
protected transform(dto: any, data: any) {
30+
protected transform(dto: any, data: any, options: ClassTransformOptions) {
3131
if (!isObject(data) || isFalse(dto)) {
3232
return data;
3333
}
3434

3535
if (!isFunction(dto)) {
36-
return data.constructor !== Object ? classToPlain(data) : data;
36+
return data.constructor !== Object ? classToPlain(data, options) : data;
3737
}
3838

39-
return data instanceof dto ? classToPlain(data) : classToPlain(classToPlainFromExist(data, new dto()));
39+
return data instanceof dto
40+
? classToPlain(data, options)
41+
: classToPlain(classToPlainFromExist(data, new dto()), options);
4042
}
4143

4244
protected serialize(context: ExecutionContext, data: any): any {
45+
const req = context.switchToHttp().getRequest();
4346
const { crudOptions, action } = this.getCrudInfo(context);
4447
const { serialize } = crudOptions;
4548
const dto = serialize[actionToDtoNameMap[action]];
4649
const isArray = Array.isArray(data);
4750

51+
const options: ClassTransformOptions = {};
52+
/* istanbul ignore else */
53+
if (isFunction(crudOptions.auth?.classTransformOptions)) {
54+
const userOrRequest = crudOptions.auth.property ? req[crudOptions.auth.property] : req;
55+
Object.assign(options, crudOptions.auth.classTransformOptions(userOrRequest));
56+
}
57+
58+
/* istanbul ignore else */
59+
if (isFunction(crudOptions.auth?.groups)) {
60+
const userOrRequest = crudOptions.auth.property ? req[crudOptions.auth.property] : req;
61+
options.groups = crudOptions.auth.groups(userOrRequest);
62+
}
63+
4864
switch (action) {
4965
case CrudActions.ReadAll:
50-
return isArray ? (data as any[]).map((item) => this.transform(serialize.get, item)) : this.transform(dto, data);
66+
return isArray
67+
? (data as any[]).map((item) => this.transform(serialize.get, item, options))
68+
: this.transform(dto, data, options);
5169
case CrudActions.CreateMany:
52-
return isArray ? (data as any[]).map((item) => this.transform(dto, item)) : this.transform(dto, data);
70+
return isArray
71+
? (data as any[]).map((item) => this.transform(dto, item, options))
72+
: this.transform(dto, data, options);
5373
default:
54-
return this.transform(dto, data);
74+
return this.transform(dto, data, options);
5575
}
5676
}
5777
}

packages/crud/src/interfaces/auth-options.interface.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import { SCondition } from '@nestjsx/crud-request/lib/types/request-query.types';
22
import { ObjectLiteral } from '@nestjsx/util';
3+
import { ClassTransformOptions } from 'class-transformer';
34

45
export interface AuthGlobalOptions {
56
property?: string;
7+
/** Get options for the `classToPlain` function (response) */
8+
classTransformOptions?: (req: any) => ClassTransformOptions;
9+
/** Get `groups` value for the `classToPlain` function options (response) */
10+
groups?: (req: any) => string[];
611
}
712

813
export interface AuthOptions {
914
property?: string;
15+
/** Get options for the `classToPlain` function (response) */
16+
classTransformOptions?: (req: any) => ClassTransformOptions;
17+
/** Get `groups` value for the `classToPlain` function options (response) */
18+
groups?: (req: any) => string[];
1019
filter?: (req: any) => SCondition | void;
1120
or?: (req: any) => SCondition | void;
1221
persist?: (req: any) => ObjectLiteral;

packages/crud/test/crud-request.interceptor.spec.ts

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import {
2-
Controller,
3-
Get,
4-
Param,
5-
ParseIntPipe,
6-
Query,
7-
UseInterceptors,
8-
} from '@nestjs/common';
1+
import { Controller, Get, Param, ParseIntPipe, Query, UseInterceptors } from '@nestjs/common';
92
import { NestApplication } from '@nestjs/core';
103
import { Test } from '@nestjs/testing';
114
import { RequestQueryBuilder } from '@nestjsx/crud-request';
@@ -61,10 +54,7 @@ describe('#crud', () => {
6154

6255
@UseInterceptors(CrudRequestInterceptor)
6356
@Get('other2/:id/twoParams/:someParam')
64-
async twoParams(
65-
@ParsedRequest() req: CrudRequest,
66-
@Param('someParam', ParseIntPipe) p: number,
67-
) {
57+
async twoParams(@ParsedRequest() req: CrudRequest, @Param('someParam', ParseIntPipe) p: number) {
6858
return { filter: req.parsed.paramsFilter };
6959
}
7060
}
@@ -123,6 +113,23 @@ describe('#crud', () => {
123113
constructor(public service: TestService<TestModel>) {}
124114
}
125115

116+
@Crud({
117+
model: { type: TestModel },
118+
})
119+
@CrudAuth({
120+
groups: () => ['TEST_2'],
121+
classTransformOptions: () => ({ groups: ['TEST_1'] }),
122+
})
123+
@Controller('test6')
124+
class Test6Controller {
125+
constructor(public service: TestService<TestModel>) {}
126+
127+
@Override('getManyBase')
128+
get(@ParsedRequest() req: CrudRequest) {
129+
return req;
130+
}
131+
}
132+
126133
let $: supertest.SuperTest<supertest.Test>;
127134
let app: NestApplication;
128135

@@ -135,6 +142,7 @@ describe('#crud', () => {
135142
Test3Controller,
136143
Test4Controller,
137144
Test5Controller,
145+
Test6Controller,
138146
],
139147
}).compile();
140148
app = module.createNestApplication();
@@ -158,8 +166,15 @@ describe('#crud', () => {
158166
const page = 2;
159167
const limit = 10;
160168
const fields = ['a', 'b', 'c'];
161-
const sorts: any[][] = [['a', 'ASC'], ['b', 'DESC']];
162-
const filters: any[][] = [['a', 'eq', 1], ['c', 'in', [1, 2, 3]], ['d', 'notnull']];
169+
const sorts: any[][] = [
170+
['a', 'ASC'],
171+
['b', 'DESC'],
172+
];
173+
const filters: any[][] = [
174+
['a', 'eq', 1],
175+
['c', 'in', [1, 2, 3]],
176+
['d', 'notnull'],
177+
];
163178

164179
qb.setPage(page).setLimit(limit);
165180
qb.select(fields);
@@ -170,9 +185,7 @@ describe('#crud', () => {
170185
qb.setFilter({ field: f[0], operator: f[1], value: f[2] });
171186
}
172187

173-
const res = await $.get('/test/query')
174-
.query(qb.query())
175-
.expect(200);
188+
const res = await $.get('/test/query').query(qb.query()).expect(200);
176189
expect(res.body.parsed).toHaveProperty('page', page);
177190
expect(res.body.parsed).toHaveProperty('limit', limit);
178191
expect(res.body.parsed).toHaveProperty('fields', fields);
@@ -190,9 +203,7 @@ describe('#crud', () => {
190203
});
191204

192205
it('should others working', async () => {
193-
const res = await $.get('/test/other')
194-
.query({ page: 2, per_page: 11 })
195-
.expect(200);
206+
const res = await $.get('/test/other').query({ page: 2, per_page: 11 }).expect(200);
196207
expect(res.body.page).toBe(2);
197208
});
198209

@@ -225,9 +236,7 @@ describe('#crud', () => {
225236
});
226237

227238
it('should handle authorized request, 1', async () => {
228-
const res = await $.post('/test3')
229-
.send({})
230-
.expect(201);
239+
const res = await $.post('/test3').send({}).expect(201);
231240
const authPersist = { bar: false };
232241
const { parsed } = res.body;
233242
expect(parsed.authPersist).toMatchObject(authPersist);
@@ -241,19 +250,21 @@ describe('#crud', () => {
241250

242251
it('should handle authorized request, 3', async () => {
243252
const query = qb.search({ name: 'test' }).query();
244-
const res = await $.get('/test4')
245-
.query(query)
246-
.expect(200);
253+
const res = await $.get('/test4').query(query).expect(200);
247254
const search = { $or: [{ id: 1 }, { $and: [{}, { name: 'test' }] }] };
248255
expect(res.body.parsed.search).toMatchObject(search);
249256
});
250257
it('should handle authorized request, 4', async () => {
251258
const query = qb.search({ name: 'test' }).query();
252-
const res = await $.get('/test3')
253-
.query(query)
254-
.expect(200);
259+
const res = await $.get('/test3').query(query).expect(200);
255260
const search = { $and: [{ user: 'test', buz: 1 }, { name: 'persist' }] };
256261
expect(res.body.parsed.search).toMatchObject(search);
257262
});
263+
264+
it('should handle classTransformOptions, 1', async () => {
265+
const res = await $.get('/test6').expect(200);
266+
const groups = ['TEST_2'];
267+
expect(res.body.parsed.classTransformOptions.groups).toMatchObject(groups);
268+
});
258269
});
259270
});

0 commit comments

Comments
 (0)