diff --git a/packages/openapi-v3/src/__tests__/unit/json-to-schema.unit.ts b/packages/openapi-v3/src/__tests__/unit/json-to-schema.unit.ts index 94b2e8be6734..5c2b34aac143 100644 --- a/packages/openapi-v3/src/__tests__/unit/json-to-schema.unit.ts +++ b/packages/openapi-v3/src/__tests__/unit/json-to-schema.unit.ts @@ -5,6 +5,8 @@ import {JsonSchema} from '@loopback/repository-json-schema'; import {expect} from '@loopback/testlab'; +import {PropertyDefinition} from '@loopback/repository'; + import {jsonOrBooleanToJSON, jsonToSchemaObject, SchemaObject} from '../..'; describe('jsonToSchemaObject', () => { @@ -308,6 +310,29 @@ describe('jsonToSchemaObject', () => { propertyConversionTest(itemsDef, expectedItems); }); + it('add extra properties as extension properties', () => { + const properties: PropertyDefinition = { + type: 'string', + defaultFn: 'guid', + index: false, + precision: 10, + scale: 10, + generated: true, + hidden: false, + }; + + const expectedItems: SchemaObject = { + type: 'string', + 'x-defaultFn': 'guid', + 'x-generated': true, + 'x-hidden': false, + 'x-index': false, + 'x-precision': 10, + 'x-scale': 10, + }; + propertyConversionTest(properties, expectedItems); + }); + // Helper function to check conversion of JSON Schema properties // to Swagger versions function propertyConversionTest(property: object, expected: object) { diff --git a/packages/openapi-v3/src/json-to-schema.ts b/packages/openapi-v3/src/json-to-schema.ts index d4eb6da9dd7f..65308656d518 100644 --- a/packages/openapi-v3/src/json-to-schema.ts +++ b/packages/openapi-v3/src/json-to-schema.ts @@ -140,6 +140,21 @@ export function jsonToSchemaObject( delete result[converted]; // Check if the description contains information about TypeScript type const matched = result.description?.match(/^\(tsType: (.+), schemaOptions:/); + const extensionProperties = [ + 'defaultFn', + 'index', + 'length', + 'precision', + 'scale', + 'generated', + 'hidden', + ]; + Object.keys(result).forEach(key => { + if (extensionProperties.includes(key)) { + result[`x-${key}`] = result[key]; + delete result[key]; + } + }); if (matched) { result['x-typescript-type'] = matched[1]; } diff --git a/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts b/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts index 1fbac51d0107..ce40d03897e7 100644 --- a/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts +++ b/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts @@ -772,6 +772,9 @@ describe('build-schema', () => { email: { type: 'string', format: 'email', + index: { + unique: true, + }, maxLength: 50, minLength: 5, }, diff --git a/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts b/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts index 29493866baf2..31646d9cdb3a 100644 --- a/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts +++ b/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts @@ -203,6 +203,30 @@ describe('build-schema', () => { }); }); + it('keeps extensions on property', () => { + expect( + metaToJsonProperty({ + type: String, + defaultFn: 'guid', + index: false, + length: 50, + precision: 10, + scale: 0, + generated: true, + hidden: true, + }), + ).to.eql({ + type: 'string', + defaultFn: 'guid', + index: false, + length: 50, + precision: 10, + scale: 0, + generated: true, + hidden: true, + }); + }); + it('keeps AJV keywords', () => { const schema = metaToJsonProperty({ type: String, diff --git a/packages/repository-json-schema/src/build-schema.ts b/packages/repository-json-schema/src/build-schema.ts index ce7e106e1f0b..a2e28afae12f 100644 --- a/packages/repository-json-schema/src/build-schema.ts +++ b/packages/repository-json-schema/src/build-schema.ts @@ -16,7 +16,7 @@ import { } from '@loopback/repository'; import debugFactory from 'debug'; import {inspect} from 'util'; -import {JsonSchema} from './index'; +import {JsonSchema, ExtensionProperties} from './index'; import {JSON_SCHEMA_KEY} from './keys'; const debug = debugFactory('loopback:repository-json-schema:build-schema'); @@ -255,9 +255,25 @@ export function isArrayType(type: string | Function | PropertyType) { * @param meta */ export function metaToJsonProperty(meta: PropertyDefinition): JsonSchema { - const propDef: JsonSchema = {}; + const propDef: JsonSchema & ExtensionProperties = {}; let result: JsonSchema; let propertyType = meta.type as string | Function; + const propertiesToCopy = [ + 'default', + 'defaultFn', + 'index', + 'length', + 'precision', + 'scale', + 'generated', + 'hidden', + ]; + + propertiesToCopy.forEach(prop => { + if (meta[prop] !== undefined) { + propDef[prop] = meta[prop]; + } + }); if (isArrayType(propertyType) && meta.itemType) { if (isArrayType(meta.itemType) && !meta.jsonSchema) { diff --git a/packages/repository-json-schema/src/index.ts b/packages/repository-json-schema/src/index.ts index b18911c30c47..dcecf17d06ed 100644 --- a/packages/repository-json-schema/src/index.ts +++ b/packages/repository-json-schema/src/index.ts @@ -45,3 +45,5 @@ export type Optional = Omit< K > & Partial>; + +export type ExtensionProperties = {[x: string]: number | boolean | string}; diff --git a/packages/rest/src/__tests__/unit/ajv-factory.provider.unit.ts b/packages/rest/src/__tests__/unit/ajv-factory.provider.unit.ts index 23fabf3c7892..51a2d89967fb 100644 --- a/packages/rest/src/__tests__/unit/ajv-factory.provider.unit.ts +++ b/packages/rest/src/__tests__/unit/ajv-factory.provider.unit.ts @@ -290,4 +290,56 @@ describe('Ajv factory', () => { ctx = new Context(); ctx.bind(RestBindings.AJV_FACTORY).toProvider(AjvFactoryProvider); } + + it('validate extension properties', async () => { + const ajvFactory = await ctx.get(RestBindings.AJV_FACTORY); + const validator = ajvFactory().compile({ + type: 'object', + properties: { + validation: { + type: 'object', + properties: { + 'x-default': { + type: 'string', + }, + 'x-defaultFn': { + type: 'string', + enum: ['guid', 'uuid', 'uuidv4', 'now'], + }, + 'x-index': { + type: 'boolean', + }, + 'x-length': { + type: 'number', + }, + 'x-precision': { + type: 'number', + }, + 'x-scale': { + type: 'number', + }, + 'x-generated': { + type: 'boolean', + }, + 'x-hidden': { + type: 'boolean', + }, + }, + }, + }, + }); + const result = validator({ + validation: { + 'x-default': 'default value', + 'x-defaultFn': 'guid', + 'x-index': false, + 'x-length': 54, + 'x-precision': 10, + 'x-scale': 0, + 'x-generated': true, + 'x-hidden': false, + }, + }); + expect(result).to.be.true(); + }); }); diff --git a/packages/rest/src/validation/ajv-factory.provider.ts b/packages/rest/src/validation/ajv-factory.provider.ts index 6850680e1c9d..1020fd77ffdb 100644 --- a/packages/rest/src/validation/ajv-factory.provider.ts +++ b/packages/rest/src/validation/ajv-factory.provider.ts @@ -64,6 +64,28 @@ export class AjvFactoryProvider implements Provider { ajvInst.addKeyword('components'); ajvInst.addKeyword('x-typescript-type'); ajvInst.addKeyword('x-index-info'); + ajvInst.addKeyword({ + keyword: 'x-default', + schemaType: [ + 'string', + 'number', + 'integer', + 'boolean', + 'null', + 'object', + 'array', + ], + }); + ajvInst.addKeyword({keyword: 'x-defaultFn', schemaType: ['string']}); + ajvInst.addKeyword({ + keyword: 'x-index', + schemaType: ['boolean', 'object'], + }); + ajvInst.addKeyword({keyword: 'x-length', schemaType: ['number']}); + ajvInst.addKeyword({keyword: 'x-precision', schemaType: ['number']}); + ajvInst.addKeyword({keyword: 'x-scale', schemaType: ['number']}); + ajvInst.addKeyword({keyword: 'x-generated', schemaType: 'boolean'}); + ajvInst.addKeyword({keyword: 'x-hidden', schemaType: ['boolean']}); ajvKeywords(ajvInst, validationOptions.ajvKeywords);