diff --git a/.gitignore b/.gitignore index 034c8d8..55e2152 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage *.key *.secret yarn-error.log +.idea diff --git a/__tests__/classValidatorToJsonSchema.test.ts b/__tests__/classValidatorToJsonSchema.test.ts new file mode 100644 index 0000000..17a7ac8 --- /dev/null +++ b/__tests__/classValidatorToJsonSchema.test.ts @@ -0,0 +1,53 @@ +import { IsString, MinLength } from 'class-validator' +import { classValidatorToJsonSchema, validationMetadatasToSchemas } from '../src' + +export class User { + @MinLength(5) + @IsString() + name: string; +} + +describe('classValidatorToJsonSchema', () => { + it('handles default object', async () => { + const schema = classValidatorToJsonSchema(User) + + expect(schema).toStrictEqual({ + properties: { + name: { minLength: 5, type: 'string' } + }, + required: ['name'], + type: 'object' + }) + + // Import another User class. + const alternativeUserImport = await import('./classes/User') + + /** + * By importing another User class with the same name JSON schemas get merged. + * User JSON schema now contains properties from the classes/User.ts class as + * well (firstName) + */ + const schemas = validationMetadatasToSchemas() + expect(Boolean(schemas.User!.properties!.name)).toBeTruthy() + expect(Boolean(schemas.User!.properties!.firstName)).toBeTruthy() + + /** + * When we get JSON schema for a specific class, + * it returns properties specific for that class (without merging) + */ + const schema2 = classValidatorToJsonSchema(User) + // Schema stays the same + expect(schema).toStrictEqual(schema2) + + const alternativeUserSchema = classValidatorToJsonSchema(alternativeUserImport.User) + + expect(alternativeUserSchema).toStrictEqual({ + properties: { + firstName: { minLength: 5, type: 'string' } + }, + required: ['firstName'], + type: 'object' + }) + }) +}) + diff --git a/__tests__/classes/User.ts b/__tests__/classes/User.ts new file mode 100644 index 0000000..e987a3f --- /dev/null +++ b/__tests__/classes/User.ts @@ -0,0 +1,7 @@ +import { IsString, MinLength } from 'class-validator' + +export class User { + @MinLength(5) + @IsString() + firstName: string; +} \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index dcfcd58..17fa4e8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,7 @@ module.exports = { moduleDirectories: ['node_modules', 'src'], moduleFileExtensions: ['ts', 'js', 'json'], roots: ['/__tests__'], - testPathIgnorePatterns: ['/node_modules/'], + testPathIgnorePatterns: ['/node_modules/', '/__tests__/classes'], coverageDirectory: 'coverage', collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}', '!src/**/*.d.ts'] } diff --git a/src/index.ts b/src/index.ts index 7a59916..8d8bdb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,29 +30,58 @@ export function validationMetadatasToSchemas(userOptions?: Partial) { const target = ownMetas[0].target as Function const metas = ownMetas.concat(getInheritedMetadatas(target, metadatas)) - const properties = _(metas) - .groupBy('propertyName') - .mapValues((propMetas, propKey) => { - const schema = applyConverters(propMetas, options) - return applyDecorators(schema, target, options, propKey) - }) - .value() - - const definitionSchema: SchemaObject = { - properties, - type: 'object', - } + return buildJsonSchemaForSpecificTarget(metadatas, target, metas, options) + }) + .value() - const required = getRequiredPropNames(target, metas, options) - if (required.length > 0) { - definitionSchema.required = required - } + return schemas +} - return applyDecorators(definitionSchema, target, options, target.name) +/** + * Convert class-validator class into JSON Schema definition. + */ +export type ClassType = new (...args: any[]) => T; + +function buildJsonSchemaForSpecificTarget( metadatas: ValidationMetadata[], target: ClassType | Function, targetMetadatas: ValidationMetadata[], options: IOptions) { + const metas = targetMetadatas.concat(getInheritedMetadatas(target, metadatas)) + + const properties = _(metas) + .groupBy('propertyName') + .mapValues((propMetas, propKey) => { + const schema = applyConverters(propMetas, options) + return applyDecorators(schema, target, options, propKey) }) .value() - return schemas + const definitionSchema: SchemaObject = { + properties, + type: 'object' + } + + const required = getRequiredPropNames(target, metas, options) + if (required.length > 0) { + definitionSchema.required = required + } + + return applyDecorators(definitionSchema, target, options, target.name) +} + +export function classValidatorToJsonSchema( target: ClassType, userOptions?: Partial) { + const options: IOptions = { + ...defaultOptions, + ...userOptions, + } + + const metadatas = getMetadatasFromStorage( + options.classValidatorMetadataStorage + ) + + const targetMetadatas = _(metadatas) + .filter(metadata => { + return metadata.target === target + }).value(); + + return buildJsonSchemaForSpecificTarget(metadatas, target, targetMetadatas, options) } /**