diff --git a/package.json b/package.json index 2374c5f3..52774067 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,11 @@ "private": true, "dependencies": { "@koa/cors": "^3.3.0", - "chokidar": "^3.5.3", - "bluebird": "^3.7.2", "bcryptjs": "^2.4.3", + "bluebird": "^3.7.2", "bytes": "^3.1.2", + "chokidar": "^3.5.3", + "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "commander": "^9.4.0", "dayjs": "^1.11.2", diff --git a/packages/cli/test/init.spec.ts b/packages/cli/test/init.spec.ts index c46f51bd..0035c27b 100644 --- a/packages/cli/test/init.spec.ts +++ b/packages/cli/test/init.spec.ts @@ -40,4 +40,4 @@ it('Init command with folder path should create default config in target folder' ); // Assert expect(config.name).toBe(projectName); -}, 30000); +}, 60000); diff --git a/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts b/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts index 9149d7c2..47faf159 100644 --- a/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts +++ b/packages/core/src/lib/artifact-builder/vulcanArtifactBuilder.ts @@ -1,9 +1,10 @@ import { ArtifactBuilder } from './artifactBuilder'; -import { PersistentStore } from '@vulcan-sql/core/models'; +import { BuiltArtifact, PersistentStore } from '@vulcan-sql/core/models'; import { Serializer } from '@vulcan-sql/core/models'; import { inject, injectable } from 'inversify'; import { TYPES } from '@vulcan-sql/core/types'; import { InternalError } from '../utils'; +import { plainToInstance, instanceToPlain } from 'class-transformer'; @injectable() export class VulcanArtifactBuilder implements ArtifactBuilder { @@ -22,13 +23,16 @@ export class VulcanArtifactBuilder implements ArtifactBuilder { } public async build(): Promise { - const serializedArtifact = this.serializer.serialize(this.artifact); + const artifactInPureObject = instanceToPlain(this.artifact); + const serializedArtifact = this.serializer.serialize(artifactInPureObject); await this.persistentStore.save(serializedArtifact); } public async load(): Promise { const serializedArtifact = await this.persistentStore.load(); - this.artifact = this.serializer.deserialize(serializedArtifact); + const artifactInPureObject = + this.serializer.deserialize(serializedArtifact); + this.artifact = plainToInstance(BuiltArtifact, artifactInPureObject); } public getArtifact(key: string): T { diff --git a/packages/core/src/lib/validators/constraints.ts b/packages/core/src/lib/validators/constraints.ts index ac27971d..bb02a167 100644 --- a/packages/core/src/lib/validators/constraints.ts +++ b/packages/core/src/lib/validators/constraints.ts @@ -1,7 +1,10 @@ import { intersection } from 'lodash'; import { InternalError } from '../utils'; +import { DiscriminatorDescriptor } from 'class-transformer'; export abstract class Constraint { + abstract __type: string; + static Required() { return new RequiredConstraint(); } @@ -38,6 +41,8 @@ export abstract class Constraint { } export class RequiredConstraint extends Constraint { + __type = 'Required'; + public compose() { // No matter what other required constraint is, we always return a required constraint return new RequiredConstraint(); @@ -45,6 +50,8 @@ export class RequiredConstraint extends Constraint { } export class MinValueConstraint extends Constraint { + __type = 'MinValue'; + constructor(private minValue: number, private exclusive = false) { super(); } @@ -72,6 +79,8 @@ export class MinValueConstraint extends Constraint { } export class MaxValueConstraint extends Constraint { + __type = 'MaxValue'; + constructor(private maxValue: number, private exclusive = false) { super(); } @@ -99,6 +108,8 @@ export class MaxValueConstraint extends Constraint { } export class MinLengthConstraint extends Constraint { + __type = 'MinLength'; + constructor(private minLength: number) { super(); } @@ -115,6 +126,8 @@ export class MinLengthConstraint extends Constraint { } export class MaxLengthConstraint extends Constraint { + __type = 'MaxLength'; + constructor(private maxLength: number) { super(); } @@ -131,6 +144,8 @@ export class MaxLengthConstraint extends Constraint { } export class RegexConstraint extends Constraint { + __type = 'Regex'; + constructor(private regex: string) { super(); } @@ -147,6 +162,8 @@ export class RegexConstraint extends Constraint { } export class EnumConstraint extends Constraint { + __type = 'Enum'; + constructor(private list: Array) { super(); } @@ -171,6 +188,8 @@ export type TypeConstraintType = | 'object'; export class TypeConstraint extends Constraint { + __type = 'Type'; + constructor(private type: TypeConstraintType) { super(); } @@ -185,3 +204,18 @@ export class TypeConstraint extends Constraint { ); } } + +// https://github.com/typestack/class-transformer/tree/master#providing-more-than-one-type-option +export const ConstraintDiscriminator: DiscriminatorDescriptor = { + property: '__type', + subTypes: [ + { value: RequiredConstraint, name: 'Required' }, + { value: MinValueConstraint, name: 'MinValue' }, + { value: MaxValueConstraint, name: 'MaxValue' }, + { value: MinLengthConstraint, name: 'MinLength' }, + { value: MaxLengthConstraint, name: 'MaxLength' }, + { value: RegexConstraint, name: 'Regex' }, + { value: EnumConstraint, name: 'Enum' }, + { value: TypeConstraint, name: 'Type' }, + ], +}; diff --git a/packages/core/src/models/artifact.ts b/packages/core/src/models/artifact.ts index ce9675f9..b4977d3e 100644 --- a/packages/core/src/models/artifact.ts +++ b/packages/core/src/models/artifact.ts @@ -18,7 +18,17 @@ error: message: 'You are not allowed to access this resource' */ -import { Constraint } from '../lib/validators/constraints'; +// This is the model of our built result +// It will be serialized and deserialized by class-transformer +// https://github.com/typestack/class-transformer/tree/master +// So we should use classes instead of interfaces. + +import { + Constraint, + ConstraintDiscriminator, +} from '../lib/validators/constraints'; +import { Type } from 'class-transformer'; +import 'reflect-metadata'; // Pagination mode should always be UPPERCASE because schema parser will transform the user inputs. export enum PaginationMode { @@ -39,62 +49,69 @@ export enum FieldDataType { STRING = 'STRING', } -export interface ValidatorDefinition { - name: string; - args: T; +export class ValidatorDefinition { + name!: string; + args!: T; } -export interface RequestSchema { - fieldName: string; +export class RequestSchema { + fieldName!: string; // the field put in query parameter or headers - fieldIn: FieldInType; - description: string; - type: FieldDataType; - validators: Array; - constraints: Array; + fieldIn!: FieldInType; + description!: string; + type!: FieldDataType; + validators!: Array; + @Type(() => Constraint, { + discriminator: ConstraintDiscriminator, + }) + constraints!: Array; } -export interface ResponseProperty { - name: string; +export class ResponseProperty { + name!: string; description?: string; - type: FieldDataType | Array; + type!: FieldDataType | Array; required?: boolean; } -export interface PaginationSchema { - mode: PaginationMode; +export class PaginationSchema { + mode!: PaginationMode; // The key name used for do filtering by key for keyset pagination. keyName?: string; } -export interface ErrorInfo { - code: string; - message: string; +export class ErrorInfo { + code!: string; + message!: string; } -export interface Sample { - profile: string; - parameters: Record; +export class Sample { + profile!: string; + parameters!: Record; } -export interface APISchema { +export class APISchema { // graphql operation name - operationName: string; + operationName!: string; // restful url path - urlPath: string; + urlPath!: string; // template, could be name or path - templateSource: string; - request: Array; - errors: Array; - response: Array; + templateSource!: string; + @Type(() => RequestSchema) + request!: Array; + @Type(() => ErrorInfo) + errors!: Array; + @Type(() => ResponseProperty) + response!: Array; description?: string; // The pagination strategy that do paginate when querying // If not set pagination, then API request not provide the field to do it pagination?: PaginationSchema; sample?: Sample; - profiles: Array; + profiles!: Array; } -export interface BuiltArtifact { - apiSchemas: Array; +export class BuiltArtifact { + @Type(() => APISchema) + schemas!: Array; } diff --git a/packages/core/test/validators/constraint-serialize.spec.ts b/packages/core/test/validators/constraint-serialize.spec.ts new file mode 100644 index 00000000..3b8d5503 --- /dev/null +++ b/packages/core/test/validators/constraint-serialize.spec.ts @@ -0,0 +1,30 @@ +import { plainToInstance, instanceToPlain } from 'class-transformer'; +import { RequestSchema } from '@vulcan-sql/core/models'; +import { Constraint, FieldDataType, FieldInType } from '@vulcan-sql/core'; + +it('Every constraint can be serialize and deserialize', async () => { + // Arrange + const constraints = [ + Constraint.Required(), + Constraint.MinValue(10), + Constraint.MaxValue(100, true), + Constraint.MinLength(10), + Constraint.MaxLength(100), + Constraint.Enum([1, 2, 3, 4]), + Constraint.Regex('.+'), + Constraint.Type('array'), + ]; + const request: RequestSchema = { + fieldName: 'test', + fieldIn: FieldInType.QUERY, + description: '', + type: FieldDataType.BOOLEAN, + validators: [], + constraints, + }; + // Act + const plainObject = instanceToPlain(request); + const instance = plainToInstance(RequestSchema, plainObject); + // Assert + expect(instance).toEqual(request); +}); diff --git a/yarn.lock b/yarn.lock index 15027aec..4443571d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2247,6 +2247,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"