Skip to content

Commit

Permalink
feat: endpoint service support validation nested object
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewPattell committed Mar 16, 2023
1 parent fb119c8 commit e453a03
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 17 deletions.
6 changes: 5 additions & 1 deletion __helpers__/root-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import RemoteConfig from '@services/remote-config';
*/
export const mochaHooks = {
beforeAll(): void {
sinon.stub(console, 'info');
Log.configure({ silent: true });
Log.transports.find((transport) => Log.remove(transport));
RemoteConfig.init(Microservice.create(), { isOffline: true, msConfigName: '', msName: '' });
},
beforeEach(): void {
if (!console.info?.['resetHistory']) {
sinon.stub(console, 'info');
}
},
afterAll(): void {
sinon.restore();
},
Expand Down
16 changes: 15 additions & 1 deletion __mocks__/entities/test-entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { Allow, Length } from 'class-validator';
import { Type } from 'class-transformer';
import { Allow, IsObject, Length, ValidateNested } from 'class-validator';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import IsUndefinable from '@validators/is-undefinable';

class NestedEntity {
@Length(2, 5)
@IsUndefinable()
hello?: string;
}

@Entity()
class TestEntity {
Expand All @@ -10,6 +18,12 @@ class TestEntity {
@Column()
@Length(1, 50)
param: string;

@Type(() => NestedEntity)
@IsObject()
@ValidateNested()
@IsUndefinable()
nested?: NestedEntity;
}

export default TestEntity;
51 changes: 50 additions & 1 deletion __tests__/services/endpoint-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/unbound-method,sonarjs/no-duplicate-string */
import TypeormJsonQuery from '@lomray/typeorm-json-query';
import { expect } from 'chai';
import sinon from 'sinon';
Expand Down Expand Up @@ -339,6 +339,25 @@ describe('services/endpoint', () => {
}
});

it('handler - should throw error if nested entity not valid: validation failed', async () => {
try {
await Endpoint.defaultHandler.create({
fields: {
param: 'param',
nested: {
hello: '1',
},
},
repository,
});

expect(shouldNotCall).to.be.undefined;
} catch (e) {
expect(e.payload.length).to.equal(1);
expect(e.payload[0].property).to.equal('nested');
}
});

it('handler - should success create entity', async () => {
const fields = { param: 'test' };

Expand Down Expand Up @@ -690,6 +709,23 @@ describe('services/endpoint', () => {
}
});

it('handler - should throw error: nested validation failed', async () => {
TypeormMock.queryBuilder.getMany.resolves([repository.create(entity)]);

try {
await Endpoint.defaultHandler.update(
repository.createQueryBuilder().where('id = 1'),
{ param: 'param', nested: { hello: '1' } },
repository,
);

expect(shouldNotCall).to.be.undefined;
} catch (e) {
expect(e.payload.length).to.equal(1);
expect(e.payload[0].property).to.equal('nested');
}
});

it('handler - should throw error: unknown error', async () => {
TypeormMock.entityManager.save.rejects(new Error('Unknown'));
TypeormMock.queryBuilder.getMany.resolves([repository.create(entity)]);
Expand Down Expand Up @@ -1115,6 +1151,19 @@ describe('services/endpoint', () => {
expect(await waitResult(result)).to.throw('Invalid request params');
});

it('should throw error: nested invalid params', async () => {
const params = { id: 1, param: 'param', nested: { hello: '1' } };
const defaultHandler = sandbox.stub();
const customHandler = Endpoint.custom(
() => ({ output: {}, input: TestEntity }),
defaultHandler,
);

const result = customHandler(params, endpointOptions);

expect(await waitResult(result)).to.throw('Invalid request params');
});

it('should return custom handler metadata', () => {
const description = 'custom description for endpoint';
const customHandler = Endpoint.custom(
Expand Down
15 changes: 7 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@opentelemetry/resources": "^1.9.1",
"@opentelemetry/sdk-node": "^0.35.1",
"@opentelemetry/semantic-conventions": "^1.9.1",
"class-transformer": "0.4.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"class-validator-jsonschema": "^5.0.0",
"klona": "^2.0.6",
Expand Down
16 changes: 11 additions & 5 deletions src/services/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BaseException } from '@lomray/microservice-nodejs-lib';
import type { ObjectLiteral, IJsonQuery } from '@lomray/microservices-types';
import type { ITypeormJsonQueryOptions } from '@lomray/typeorm-json-query';
import TypeormJsonQuery from '@lomray/typeorm-json-query';
import { Type } from 'class-transformer';
import { Type, plainToInstance } from 'class-transformer';
import { IsArray, IsBoolean, IsNumber, IsObject, validate } from 'class-validator';
import { JSONSchema } from 'class-validator-jsonschema';
import { DeleteResult } from 'typeorm';
Expand Down Expand Up @@ -658,7 +658,7 @@ const createDefaultHandler = async <TEntity, TResult = never>({
}

const entities: (TEntity & Partial<TEntity>)[] = entitiesAttributes.map((attributes) =>
Object.assign(repository.create(), attributes),
plainToInstance(repository.target as Constructable<TEntity>, attributes),
);
const errors = await Promise.all(
entities.map((entity) =>
Expand Down Expand Up @@ -769,7 +769,13 @@ const updateDefaultHandler = async <TEntity>(
}

const { entity } = await viewDefaultHandler(query);
const result = Object.assign(entity, fields);
const result = plainToInstance(
(entity as ObjectLiteral).constructor as Constructable<Record<string, any>>,
{
...entity,
...fields,
},
);
const errors = await validate(result, {
whitelist: true,
forbidNonWhitelisted: true,
Expand Down Expand Up @@ -1495,7 +1501,7 @@ class Endpoint {

if (typeof input === 'function') {
const errors = await validate(
Object.assign(new (input as Constructable)() as Record<string, any>, params),
plainToInstance(input as Constructable<Record<string, any>>, params),
{
whitelist: true,
validationError: { target: false },
Expand Down Expand Up @@ -1533,7 +1539,7 @@ class Endpoint {

if (typeof input === 'function') {
const errors = await validate(
Object.assign(new (input as Constructable)() as Record<string, any>, params),
plainToInstance(input as Constructable<Record<string, any>>, params),
{
whitelist: true,
validationError: { target: false },
Expand Down

0 comments on commit e453a03

Please sign in to comment.