Skip to content

Commit

Permalink
Merge pull request #811 from xTCry/class-transform-options
Browse files Browse the repository at this point in the history
Added class transform options
  • Loading branch information
michaelyali authored Dec 7, 2022
2 parents 44f2478 + a516a62 commit d6d3c4e
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -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[];
Expand Down
8 changes: 8 additions & 0 deletions packages/crud-request/src/request-query.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -42,6 +43,8 @@ export class RequestQueryParser implements ParsedRequestParams {

public authPersist: ObjectLiteral = undefined;

public classTransformOptions: ClassTransformOptions = undefined;

public search: SCondition;

public filter: QueryFilter[] = [];
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions packages/crud-request/test/request-query.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,13 +457,27 @@ 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 = {
fields: [],
paramsFilter: [],
search: undefined,
authPersist: undefined,
classTransformOptions: undefined,
filter: [],
or: [],
join: [],
Expand Down
14 changes: 10 additions & 4 deletions packages/crud-typeorm/src/typeorm-crud.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ export class TypeOrmCrudService<T> extends CrudService<T> {
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<T>);
const updated = await this.repo.save(
plainToClass(this.entityType, toSave, req.parsed.classTransformOptions) as unknown as DeepPartial<T>,
);

if (returnShallow) {
return updated;
Expand Down Expand Up @@ -209,7 +211,9 @@ export class TypeOrmCrudService<T> extends CrudService<T> {
...dto,
...req.parsed.authPersist,
};
const replaced = await this.repo.save(plainToClass(this.entityType, toSave) as unknown as DeepPartial<T>);
const replaced = await this.repo.save(
plainToClass(this.entityType, toSave, req.parsed.classTransformOptions) as unknown as DeepPartial<T>,
);

if (returnShallow) {
return replaced;
Expand All @@ -233,7 +237,9 @@ export class TypeOrmCrudService<T> extends CrudService<T> {
public async deleteOne(req: CrudRequest): Promise<void | T> {
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<T>)
Expand Down Expand Up @@ -421,7 +427,7 @@ export class TypeOrmCrudService<T> extends CrudService<T> {

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[] {
Expand Down
6 changes: 6 additions & 0 deletions packages/crud/src/crud/crud-routes.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 : {};
Expand Down
11 changes: 11 additions & 0 deletions packages/crud/src/interceptors/crud-request.interceptor.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down
34 changes: 27 additions & 7 deletions packages/crud/src/interceptors/crud-response.interceptor.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
}
}
}
9 changes: 9 additions & 0 deletions packages/crud/src/interfaces/auth-options.interface.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
69 changes: 40 additions & 29 deletions packages/crud/test/crud-request.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 };
}
}
Expand Down Expand Up @@ -123,6 +113,23 @@ describe('#crud', () => {
constructor(public service: TestService<TestModel>) {}
}

@Crud({
model: { type: TestModel },
})
@CrudAuth({
groups: () => ['TEST_2'],
classTransformOptions: () => ({ groups: ['TEST_1'] }),
})
@Controller('test6')
class Test6Controller {
constructor(public service: TestService<TestModel>) {}

@Override('getManyBase')
get(@ParsedRequest() req: CrudRequest) {
return req;
}
}

let $: supertest.SuperTest<supertest.Test>;
let app: NestApplication;

Expand All @@ -135,6 +142,7 @@ describe('#crud', () => {
Test3Controller,
Test4Controller,
Test5Controller,
Test6Controller,
],
}).compile();
app = module.createNestApplication();
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
});

Expand Down Expand Up @@ -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);
Expand All @@ -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);
});
});
});

0 comments on commit d6d3c4e

Please sign in to comment.