Skip to content

Commit

Permalink
feat: excludes valus for specific key from response
Browse files Browse the repository at this point in the history
  • Loading branch information
jiho-kr committed Sep 18, 2023
1 parent 101a58a commit e03e44a
Show file tree
Hide file tree
Showing 16 changed files with 219 additions and 15 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ interface RouteBaseOption {
hide?: boolean;
response?: Type<unknown>;
};
exclude?: string[];
}
```

Expand Down
149 changes: 149 additions & 0 deletions spec/exclude/exclude.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* eslint-disable max-classes-per-file */
import { HttpStatus, INestApplication } from '@nestjs/common';
import { Controller, Injectable, Module } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm';
import { IsOptional } from 'class-validator';
import request from 'supertest';
import { Entity, BaseEntity, Repository, PrimaryColumn, Column, DeleteDateColumn } from 'typeorm';

import { Crud } from '../../src/lib/crud.decorator';
import { CrudService } from '../../src/lib/crud.service';
import { CrudController } from '../../src/lib/interface';
import { TestHelper } from '../test.helper';

@Entity('exclude-test')
class TestEntity extends BaseEntity {
@PrimaryColumn()
@IsOptional({ always: true })
col1: number;

@Column({ nullable: true })
@IsOptional({ always: true })
col2: string;

@Column({ nullable: true })
@IsOptional({ always: true })
col3: string;

@Column({ nullable: true })
@IsOptional({ always: true })
col4: string;

@DeleteDateColumn()
deletedAt?: Date;
}

@Injectable()
class TestService extends CrudService<TestEntity> {
constructor(@InjectRepository(TestEntity) repository: Repository<TestEntity>) {
super(repository);
}
}

@Crud({
entity: TestEntity,
routes: {
readOne: { exclude: ['col1'] },
readMany: { exclude: ['col2'] },
search: { exclude: ['col3'] },
create: { exclude: ['col4'] },
update: { exclude: ['col1', 'col2'] },
delete: { exclude: ['col1', 'col3'] },
upsert: { exclude: ['col1', 'col4'] },
recover: { exclude: ['col1', 'col2', 'col3'] },
},
})
@Controller('base')
class TestController implements CrudController<TestEntity> {
constructor(public readonly crudService: TestService) {}
}

@Module({
imports: [TypeOrmModule.forFeature([TestEntity])],
controllers: [TestController],
providers: [TestService],
})
class TestModule {}

describe('Exclude key of entity', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [TestModule, TestHelper.getTypeOrmPgsqlModule([TestEntity])],
}).compile();
app = moduleFixture.createNestApplication();
await TestEntity.delete({});
await app.init();
});

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

it('should be excluded from the response', async () => {
// exclude col4
const { body: createdBody } = await request(app.getHttpServer())
.post('/base')
.send({
col1: 1,
col2: 'col2',
col3: 'col3',
col4: 'col4',
})
.expect(HttpStatus.CREATED);
expect(createdBody).toEqual({
col1: 1,
col2: 'col2',
col3: 'col3',
deletedAt: null,
});
expect(createdBody.col4).not.toBeDefined();

// exclude col1
const { body: readOneBody } = await request(app.getHttpServer()).get(`/base/${createdBody.col1}`).expect(HttpStatus.OK);
expect(readOneBody).toEqual({ col2: 'col2', col3: 'col3', col4: 'col4', deletedAt: null });
expect(readOneBody.col1).not.toBeDefined();

// exclude col2
const { body: readManyBody } = await request(app.getHttpServer()).get('/base').expect(HttpStatus.OK);
expect(readManyBody.data[0]).toEqual({ col1: 1, col3: 'col3', col4: 'col4', deletedAt: null });
expect(readManyBody.data[0].col2).not.toBeDefined();

// exclude col3
const { body: searchBody } = await request(app.getHttpServer()).post('/base/search').expect(HttpStatus.OK);
expect(searchBody.data[0]).toEqual({ col1: 1, col2: 'col2', col4: 'col4', deletedAt: null });
expect(searchBody.data[0].col3).not.toBeDefined();

// exclude col1, col2
const { body: updatedBody } = await request(app.getHttpServer())
.patch(`/base/${createdBody.col1}`)
.send({ col2: 'test' })
.expect(HttpStatus.OK);
expect(updatedBody).toEqual({ col3: 'col3', col4: 'col4', deletedAt: null });
expect(updatedBody.col1).not.toBeDefined();
expect(updatedBody.col2).not.toBeDefined();

// exclude col1, col3
const { body: deletedBody } = await request(app.getHttpServer()).delete(`/base/${createdBody.col1}`).expect(HttpStatus.OK);
expect(deletedBody).toEqual({ col2: 'test', col4: 'col4', deletedAt: expect.any(String) });
expect(deletedBody.col1).not.toBeDefined();
expect(deletedBody.col3).not.toBeDefined();

// exclude col1, col2, col3
const { body: recoverBody } = await request(app.getHttpServer())
.post(`/base/${createdBody.col1}/recover`)
.expect(HttpStatus.CREATED);
expect(recoverBody).toEqual({ col4: 'col4', deletedAt: null });
expect(recoverBody.col1).not.toBeDefined();
expect(recoverBody.col2).not.toBeDefined();
expect(recoverBody.col3).not.toBeDefined();

// exclude col1, col4
const { body: upsertBody } = await request(app.getHttpServer()).put('/base/100').send({ col2: 'test' }).expect(HttpStatus.OK);
expect(upsertBody).toEqual({ col2: 'test', col3: null, deletedAt: null });
expect(upsertBody.col1).not.toBeDefined();
expect(upsertBody.col4).not.toBeDefined();
});
});
8 changes: 7 additions & 1 deletion spec/logging/logging.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('Logging', () => {
await request(app.getHttpServer()).post('/base').send({
col1: 1,
});
expect(loggerSpy).toHaveBeenNthCalledWith(1, { body: { col1: 1 } }, 'CRUD POST /base');
expect(loggerSpy).toHaveBeenNthCalledWith(1, { body: { col1: 1 }, exclude: new Set() }, 'CRUD POST /base');

await request(app.getHttpServer()).get('/base');
expect(loggerSpy).toHaveBeenNthCalledWith(
Expand All @@ -87,6 +87,7 @@ describe('Logging', () => {
withDeleted: false,
relations: [],
},
_exclude: new Set(),
_pagination: { _isNext: false, type: 'cursor', _where: btoa('{}') },
_sort: 'DESC',
}),
Expand All @@ -102,6 +103,7 @@ describe('Logging', () => {
},
fields: [],
relations: [],
exclude: new Set(),
softDeleted: expect.any(Boolean),
},
'CRUD GET /base/1',
Expand All @@ -117,6 +119,7 @@ describe('Logging', () => {
body: {
col2: 'test',
},
exclude: new Set(),
},
'CRUD PATCH /base/1',
);
Expand All @@ -129,6 +132,7 @@ describe('Logging', () => {
col1: '2',
},
body: {},
exclude: new Set(),
},
'CRUD PUT /base/2',
);
Expand All @@ -141,6 +145,7 @@ describe('Logging', () => {
col1: '1',
},
softDeleted: true,
exclude: new Set(),
},
'CRUD DELETE /base/1',
);
Expand All @@ -152,6 +157,7 @@ describe('Logging', () => {
params: {
col1: '1',
},
exclude: new Set(),
},
'CRUD POST /base/1/recover',
);
Expand Down
7 changes: 6 additions & 1 deletion src/lib/crud.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ describe('CrudService', () => {

it('should return entity', async () => {
await expect(
crudService.reservedReadOne({ params: { id: mockEntity.id } as Partial<BaseEntity>, relations: [] }),
crudService.reservedReadOne({
params: { id: mockEntity.id } as Partial<BaseEntity>,
relations: [],
exclude: new Set(),
}),
).resolves.toEqual(mockEntity);
});
});
Expand All @@ -46,6 +50,7 @@ describe('CrudService', () => {
crudService.reservedDelete({
params: {},
softDeleted: false,
exclude: new Set(),
}),
).rejects.toThrow(ConflictException);
});
Expand Down
34 changes: 27 additions & 7 deletions src/lib/crud.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export class CrudService<T extends BaseEntity> {
readonly reservedReadMany = async (crudReadManyRequest: CrudReadManyRequest<T>): Promise<PaginationResponse<T>> => {
try {
const { entities, total } = await (async () => {
const findEntities = this.repository.find({ ...crudReadManyRequest.findOptions });
const findEntities = this.repository.find({ ...crudReadManyRequest.findOptions }).then((entities) => {
return crudReadManyRequest.exclude.size === 0
? entities
: entities.map((entity) => this.excludeEntity(entity, crudReadManyRequest.exclude));
});

if (crudReadManyRequest.pagination.isNext) {
const entities = await findEntities;
Expand Down Expand Up @@ -64,7 +68,7 @@ export class CrudService<T extends BaseEntity> {
if (_.isNil(entity)) {
throw new NotFoundException();
}
return entity;
return this.excludeEntity(entity, crudReadOneRequest.exclude);
});
};

Expand All @@ -82,7 +86,9 @@ export class CrudService<T extends BaseEntity> {
return this.repository
.save(entities)
.then((result) => {
return isCrudCreateManyRequest<T>(crudCreateRequest) ? result : result[0];
return isCrudCreateManyRequest<T>(crudCreateRequest)
? result.map((entity) => this.excludeEntity(entity, crudCreateRequest.exclude))
: this.excludeEntity(result[0], crudCreateRequest.exclude);
})
.catch((error) => {
throw new ConflictException(error);
Expand All @@ -100,7 +106,9 @@ export class CrudService<T extends BaseEntity> {
_.merge(upsertEntity, { [crudUpsertRequest.author.property]: crudUpsertRequest.author.value });
}

return this.repository.save(_.assign(upsertEntity, crudUpsertRequest.body));
return this.repository
.save(_.assign(upsertEntity, crudUpsertRequest.body))
.then((entity) => this.excludeEntity(entity, crudUpsertRequest.exclude));
});
};

Expand All @@ -114,7 +122,9 @@ export class CrudService<T extends BaseEntity> {
_.merge(entity, { [crudUpdateOneRequest.author.property]: crudUpdateOneRequest.author.value });
}

return this.repository.save(_.assign(entity, crudUpdateOneRequest.body));
return this.repository
.save(_.assign(entity, crudUpdateOneRequest.body))
.then((entity) => this.excludeEntity(entity, crudUpdateOneRequest.exclude));
});
};

Expand All @@ -133,7 +143,7 @@ export class CrudService<T extends BaseEntity> {
}

await (crudDeleteOneRequest.softDeleted ? entity.softRemove() : entity.remove());
return entity;
return this.excludeEntity(entity, crudDeleteOneRequest.exclude);
});
};

Expand All @@ -143,7 +153,7 @@ export class CrudService<T extends BaseEntity> {
throw new NotFoundException();
}
await this.repository.recover(entity);
return entity;
return this.excludeEntity(entity, crudRecoverRequest.exclude);
});
};

Expand All @@ -162,4 +172,14 @@ export class CrudService<T extends BaseEntity> {
await runner.release();
}
}

private excludeEntity(entity: T, exclude: Set<string>): T {
if (exclude.size === 0) {
return entity;
}
for (const excludeKey of exclude.values()) {
delete entity[excludeKey as unknown as keyof T];
}
return entity;
}
}
1 change: 1 addition & 0 deletions src/lib/interceptor/create-request.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function CreateRequestInterceptor(crudOptions: CrudOptions, factoryOption
const crudCreateRequest: CrudCreateRequest<typeof crudOptions.entity> = {
body,
author: this.getAuthor(req, crudOptions, Method.CREATE),
exclude: new Set(crudOptions.routes?.[Method.CREATE]?.exclude ?? []),
};

this.crudLogger.logRequest(req, crudCreateRequest);
Expand Down
1 change: 1 addition & 0 deletions src/lib/interceptor/delete-request.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function DeleteRequestInterceptor(crudOptions: CrudOptions, factoryOption
params,
softDeleted,
author: this.getAuthor(req, crudOptions, method),
exclude: new Set(deleteOptions.exclude ?? []),
};

this.crudLogger.logRequest(req, crudDeleteOneRequest);
Expand Down
1 change: 1 addition & 0 deletions src/lib/interceptor/read-many-request.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function ReadManyRequestInterceptor(crudOptions: CrudOptions, factoryOpti
.setSort(readManyOptions.sort ? Sort[readManyOptions.sort] : CRUD_POLICY[method].default.sort)
.setRelations(this.getRelations(customReadManyRequestOptions))
.setDeserialize(this.deserialize)
.setExclude(readManyOptions.exclude ?? [])
.generate();

this.crudLogger.logRequest(req, crudReadManyRequest.toString());
Expand Down
5 changes: 3 additions & 2 deletions src/lib/interceptor/read-one-request.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ export function ReadOneRequestInterceptor(crudOptions: CrudOptions, factoryOptio

async intercept(context: ExecutionContext, next: CallHandler<unknown>): Promise<Observable<unknown>> {
const req: Record<string, any> = context.switchToHttp().getRequest<Request>();

const readOneOptions = crudOptions.routes?.[method] ?? {};
const customReadOneRequestOptions: CustomReadOneRequestOptions = req[CUSTOM_REQUEST_OPTIONS];

const fieldsByRequest = this.checkFields(req.query?.fields);

const softDeleted = _.isBoolean(customReadOneRequestOptions?.softDeleted)
? customReadOneRequestOptions.softDeleted
: crudOptions.routes?.[method]?.softDelete ?? (CRUD_POLICY[method].default.softDeleted as boolean);
: readOneOptions.softDelete ?? (CRUD_POLICY[method].default.softDeleted as boolean);

const params = await this.checkParams(crudOptions.entity, req.params, factoryOption.columns);

Expand All @@ -38,6 +38,7 @@ export function ReadOneRequestInterceptor(crudOptions: CrudOptions, factoryOptio
fields: this.getFields(customReadOneRequestOptions?.fields, fieldsByRequest),
softDeleted,
relations: this.getRelations(customReadOneRequestOptions),
exclude: new Set(readOneOptions.exclude ?? []),
};

this.crudLogger.logRequest(req, crudReadOneRequest);
Expand Down
1 change: 1 addition & 0 deletions src/lib/interceptor/recover-request.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function RecoverRequestInterceptor(crudOptions: CrudOptions, factoryOptio
const crudRecoverRequest: CrudRecoverRequest<typeof crudOptions.entity> = {
params,
author: this.getAuthor(req, crudOptions, Method.RECOVER),
exclude: new Set(crudOptions.routes?.[Method.RECOVER]?.exclude ?? []),
};

this.crudLogger.logRequest(req, crudRecoverRequest);
Expand Down
1 change: 1 addition & 0 deletions src/lib/interceptor/search-request.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption
)
.setRelations(customSearchRequestOptions?.relations ?? factoryOption.relations)
.setDeserialize(this.deserialize)
.setExclude(searchOptions.exclude ?? [])
.generate();

this.crudLogger.logRequest(req, crudReadManyRequest.toString());
Expand Down
1 change: 1 addition & 0 deletions src/lib/interceptor/update-request.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function UpdateRequestInterceptor(crudOptions: CrudOptions, factoryOption
params,
body,
author: this.getAuthor(req, crudOptions, Method.UPDATE),
exclude: new Set(crudOptions.routes?.[Method.UPDATE]?.exclude ?? []),
};

this.crudLogger.logRequest(req, crudUpdateOneRequest);
Expand Down
Loading

0 comments on commit e03e44a

Please sign in to comment.