diff --git a/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param-body.test.ts b/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param-body.test.ts index 08115ea957a8..15c0d6d4c0ac 100644 --- a/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param-body.test.ts +++ b/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param-body.test.ts @@ -61,6 +61,7 @@ describe('Routing metadata for parameters', () => { const actualSpec = getControllerSpec(MyController); expect(actualSpec.definitions).to.deepEqual({ MyData: { + title: 'MyData', properties: { name: { type: 'string', diff --git a/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts b/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts index e6a302d441b4..31af70ddc844 100644 --- a/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts +++ b/packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts @@ -311,6 +311,7 @@ describe('Routing metadata for parameters', () => { // tslint:disable-next-line:no-any expect(defs).to.have.keys('Foo', 'Bar'); expect(defs.Foo).to.deepEqual({ + title: 'Foo', properties: { price: { type: 'number', @@ -318,6 +319,7 @@ describe('Routing metadata for parameters', () => { }, }); expect(defs.Bar).to.deepEqual({ + title: 'Bar', properties: { name: { type: 'string', @@ -354,7 +356,7 @@ describe('Routing metadata for parameters', () => { expect(defs.MyBody).to.not.have.key('definitions'); }); - it('infers empty body parameter schema if no property metadata is present', () => { + it('infers no properties if no property metadata is present', () => { const paramSpec: ParameterObject = { name: 'foo', in: 'body', @@ -372,7 +374,7 @@ describe('Routing metadata for parameters', () => { .definitions as DefinitionsObject; expect(defs).to.have.key('MyBody'); - expect(defs.MyBody).to.deepEqual({}); + expect(defs.MyBody).to.not.have.key('properties'); }); it('does not infer definition if no class metadata is present', () => { diff --git a/packages/repository-json-schema/src/build-schema.ts b/packages/repository-json-schema/src/build-schema.ts index d9386b972416..3e3b6026c18c 100644 --- a/packages/repository-json-schema/src/build-schema.ts +++ b/packages/repository-json-schema/src/build-schema.ts @@ -48,6 +48,72 @@ export function getJsonSchema(ctor: Function): JsonDefinition { } } +/** + * Gets the wrapper function of primitives string, number, and boolean + * @param type Name of type + */ +export function stringTypeToWrapper(type: string): Function { + type = type.toLowerCase(); + let wrapper; + switch (type) { + case 'number': { + wrapper = Number; + break; + } + case 'string': { + wrapper = String; + break; + } + case 'boolean': { + wrapper = Boolean; + break; + } + default: { + throw new Error('Unsupported type'); + } + } + return wrapper; +} + +/** + * Determines whether the given constructor is a custom type or not + * @param ctor Constructor + */ +export function isComplexType(ctor: Function) { + return !includes([String, Number, Boolean, Object, Function], ctor); +} + +/** + * Converts property metadata into a JSON property definition + * @param meta + */ +export function metaToJsonProperty(meta: PropertyDefinition): JsonDefinition { + let ctor = meta.type as string | Function; + let def: JsonDefinition = {}; + + // errors out if @property.array() is not used on a property of array + if (ctor === Array) { + throw new Error('type is defined as an array'); + } + + if (typeof ctor === 'string') { + ctor = stringTypeToWrapper(ctor); + } + + const propDef = isComplexType(ctor) + ? {$ref: `#definitions/${ctor.name}`} + : {type: ctor.name.toLowerCase()}; + + if (meta.array) { + def.type = 'array'; + def.items = propDef; + } else { + Object.assign(def, propDef); + } + + return def; +} + // NOTE(shimks) no metadata for: union, optional, nested array, any, enum, // string literal, anonymous types, and inherited properties @@ -57,80 +123,59 @@ export function getJsonSchema(ctor: Function): JsonDefinition { * @param ctor Constructor of class to convert from */ export function modelToJsonSchema(ctor: Function): JsonDefinition { - const meta: ModelDefinition = ModelMetadataHelper.getModelMetadata(ctor); - const schema: JsonDefinition = {}; + const meta: ModelDefinition | {} = ModelMetadataHelper.getModelMetadata(ctor); + const result: JsonDefinition = {}; - const isComplexType = (constructor: Function) => - !includes([String, Number, Boolean, Object], constructor); + // returns an empty object if metadata is an empty object + if (!(meta instanceof ModelDefinition)) { + return {}; + } - const determinePropertyDef = (constructor: Function) => - isComplexType(constructor) - ? {$ref: `#definitions/${constructor.name}`} - : {type: constructor.name.toLowerCase()}; + result.title = meta.title || ctor.name; + + if (meta.description) { + result.description = meta.description; + } for (const p in meta.properties) { - const propMeta = meta.properties[p]; - let propCtor = propMeta.type; - if (typeof propCtor === 'string') { - const type = propCtor.toLowerCase(); - switch (type) { - case 'number': { - propCtor = Number; - break; - } - case 'string': { - propCtor = String; - break; - } - case 'boolean': { - propCtor = Boolean; - break; - } - default: { - throw new Error('Unsupported type'); - } - } + if (!meta.properties[p].type) { + continue; } - if (propCtor && typeof propCtor === 'function') { - // errors out if @property.array() is not used on a property of array - if (propCtor === Array) { - throw new Error('type is defined as an array'); - } - const propDef: JsonDefinition = determinePropertyDef(propCtor); + result.properties = result.properties || {}; + result.properties[p] = result.properties[p] || {}; - if (!schema.properties) { - schema.properties = {}; - } + const property = result.properties[p]; + const metaProperty = meta.properties[p]; + const metaType = metaProperty.type; - if (propMeta.array === true) { - schema.properties[p] = { - type: 'array', - items: propDef, - }; - } else { - schema.properties[p] = propDef; - } + // populating "properties" key + result.properties[p] = metaToJsonProperty(metaProperty); - if (isComplexType(propCtor)) { - const propSchema = getJsonSchema(propCtor); + // populating JSON Schema 'definitions' + if (typeof metaType === 'function' && isComplexType(metaType)) { + const propSchema = getJsonSchema(metaType); - if (propSchema && Object.keys(propSchema).length > 0) { - if (!schema.definitions) { - schema.definitions = {}; - } + if (propSchema && Object.keys(propSchema).length > 0) { + result.definitions = result.definitions || {}; - if (propSchema.definitions) { - for (const key in propSchema.definitions) { - schema.definitions[key] = propSchema.definitions[key]; - } - delete propSchema.definitions; + // delete nested definition + if (propSchema.definitions) { + for (const key in propSchema.definitions) { + result.definitions[key] = propSchema.definitions[key]; } - - schema.definitions[propCtor.name] = propSchema; + delete propSchema.definitions; } + + result.definitions[metaType.name] = propSchema; } } + + // handling 'required' metadata + if (metaProperty.required) { + result.required = result.required || []; + result.required.push(p); + } } - return schema; + return result; } diff --git a/packages/repository-json-schema/test/integration/build-schema.test.ts b/packages/repository-json-schema/test/integration/build-schema.test.ts index a616294b2628..ed9c80048c46 100644 --- a/packages/repository-json-schema/test/integration/build-schema.test.ts +++ b/packages/repository-json-schema/test/integration/build-schema.test.ts @@ -3,13 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - model, - property, - ModelMetadataHelper, - ModelDefinition, - PropertyMap, -} from '@loopback/repository'; +import {model, property} from '@loopback/repository'; import {modelToJsonSchema} from '../../src/build-schema'; import {expect} from '@loopback/testlab'; import {MetadataInspector} from '@loopback/context'; @@ -17,273 +11,325 @@ import {JSON_SCHEMA_KEY, getJsonSchema} from '../../index'; describe('build-schema', () => { describe('modelToJsonSchema', () => { - it('does not convert null or undefined property', () => { - @model() - class TestModel { - @property() nul: null; - @property() undef: undefined; - } + context('properties conversion', () => { + it('does not convert null or undefined property', () => { + @model() + class TestModel { + @property() nul: null; + @property() undef: undefined; + } - const jsonSchema = modelToJsonSchema(TestModel); - expect(jsonSchema.properties).to.not.have.keys(['nul', 'undef']); - }); + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.properties).to.not.have.keys(['nul', 'undef']); + }); - it('does not convert properties that have not been decorated', () => { - @model() - class NoPropertyMeta { - prop: string; - } - @model() - class OnePropertyDecorated { - @property() foo: string; - bar: boolean; - baz: number; - } + it('does not convert properties that have not been decorated', () => { + @model() + class NoPropertyMeta { + prop: string; + } + @model() + class OnePropertyDecorated { + @property() foo: string; + bar: boolean; + baz: number; + } - expect(modelToJsonSchema(NoPropertyMeta)).to.eql({}); - expect(modelToJsonSchema(OnePropertyDecorated)).to.deepEqual({ - properties: { + const noPropJson = modelToJsonSchema(NoPropertyMeta); + const onePropJson = modelToJsonSchema(OnePropertyDecorated); + expect(noPropJson).to.not.have.key('properties'); + expect(onePropJson.properties).to.deepEqual({ foo: { type: 'string', }, - }, + }); }); - }); - it('does not convert models that have not been decorated with @model()', () => { - class Empty {} - class NoModelMeta { - @property() foo: string; - bar: number; - } + it('does not convert models that have not been decorated with @model()', () => { + class Empty {} + class NoModelMeta { + @property() foo: string; + bar: number; + } - expect(modelToJsonSchema(Empty)).to.eql({}); - expect(modelToJsonSchema(NoModelMeta)).to.eql({}); - }); + expect(modelToJsonSchema(Empty)).to.eql({}); + expect(modelToJsonSchema(NoModelMeta)).to.eql({}); + }); - it('properly converts string, number, and boolean properties', () => { - @model() - class TestModel { - @property() str: string; - @property() num: number; - @property() bool: boolean; - } + it('infers "title" property from constructor name', () => { + @model() + class TestModel { + @property() foo: string; + } - const jsonSchema = modelToJsonSchema(TestModel); - expect(jsonSchema.properties).to.deepEqual({ - str: { - type: 'string', - }, - num: { - type: 'number', - }, - bool: { - type: 'boolean', - }, + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.title).to.eql('TestModel'); }); - }); - it('properly converts object properties', () => { - @model() - class TestModel { - @property() obj: object; - } + it('overrides "title" property if explicitly given', () => { + @model({title: 'NewName'}) + class TestModel { + @property() foo: string; + } - const jsonSchema = modelToJsonSchema(TestModel); - expect(jsonSchema.properties).to.deepEqual({ - obj: { - type: 'object', - }, + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.title).to.eql('NewName'); }); - }); - context('with custom type properties', () => { - it('properly converts undecorated custom type properties', () => { - class CustomType { - prop: string; + it('retains "description" properties from top-level metadata', () => { + const topMeta = { + description: 'Test description', + }; + @model(topMeta) + class TestModel { + @property() foo: string; } + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.description).to.eql(topMeta.description); + }); + + it('properly converts string, number, and boolean properties', () => { @model() class TestModel { - @property() cusType: CustomType; + @property() str: string; + @property() num: number; + @property() bool: boolean; } const jsonSchema = modelToJsonSchema(TestModel); expect(jsonSchema.properties).to.deepEqual({ - cusType: { - $ref: '#definitions/CustomType', + str: { + type: 'string', + }, + num: { + type: 'number', + }, + bool: { + type: 'boolean', }, }); - expect(jsonSchema).to.not.have.key('definitions'); }); - it('properly converts decorated custom type properties', () => { - @model() - class CustomType { - @property() prop: string; - } - + it('properly converts object properties', () => { @model() class TestModel { - @property() cusType: CustomType; + @property() obj: object; } const jsonSchema = modelToJsonSchema(TestModel); expect(jsonSchema.properties).to.deepEqual({ - cusType: { - $ref: '#definitions/CustomType', + obj: { + type: 'object', }, }); - expect(jsonSchema.definitions).to.deepEqual({ - CustomType: { - properties: { - prop: { - type: 'string', + }); + + context('with custom type properties', () => { + it('properly converts undecorated custom type properties', () => { + class CustomType { + prop: string; + } + + @model() + class TestModel { + @property() cusType: CustomType; + } + + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.properties).to.deepEqual({ + cusType: { + $ref: '#definitions/CustomType', + }, + }); + expect(jsonSchema).to.not.have.key('definitions'); + }); + + it('properly converts decorated custom type properties', () => { + @model() + class CustomType { + @property() prop: string; + } + + @model() + class TestModel { + @property() cusType: CustomType; + } + + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.properties).to.deepEqual({ + cusType: { + $ref: '#definitions/CustomType', + }, + }); + expect(jsonSchema.definitions).to.deepEqual({ + CustomType: { + title: 'CustomType', + properties: { + prop: { + type: 'string', + }, }, }, - }, + }); + }); + + it('creates definitions only at the root level of the schema', () => { + @model() + class CustomTypeFoo { + @property() prop: string; + } + + @model() + class CustomTypeBar { + @property.array(CustomTypeFoo) prop: CustomTypeFoo[]; + } + + @model() + class TestModel { + @property() cusBar: CustomTypeBar; + } + + const jsonSchema = modelToJsonSchema(TestModel); + const schemaProps = jsonSchema.properties; + const schemaDefs = jsonSchema.definitions; + expect(schemaProps).to.deepEqual({ + cusBar: { + $ref: '#definitions/CustomTypeBar', + }, + }); + expect(schemaDefs).to.deepEqual({ + CustomTypeFoo: { + title: 'CustomTypeFoo', + properties: { + prop: { + type: 'string', + }, + }, + }, + CustomTypeBar: { + title: 'CustomTypeBar', + properties: { + prop: { + type: 'array', + items: { + $ref: '#definitions/CustomTypeFoo', + }, + }, + }, + }, + }); }); }); - it('creates definitions only at the root level of the schema', () => { + it('properly converts primitive arrays properties', () => { @model() - class CustomTypeFoo { - @property() prop: string; + class TestModel { + @property.array(Number) numArr: number[]; } - @model() - class CustomTypeBar { - @property.array(CustomTypeFoo) prop: CustomTypeFoo[]; + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.properties).to.deepEqual({ + numArr: { + type: 'array', + items: { + type: 'number', + }, + }, + }); + }); + + it('properly converts custom type arrays properties', () => { + class CustomType { + prop: string; } @model() class TestModel { - @property() cusBar: CustomTypeBar; + @property.array(CustomType) cusArr: CustomType[]; } const jsonSchema = modelToJsonSchema(TestModel); - const schemaProps = jsonSchema.properties; - const schemaDefs = jsonSchema.definitions; - expect(schemaProps).to.deepEqual({ - cusBar: { - $ref: '#definitions/CustomTypeBar', - }, - }); - expect(schemaDefs).to.deepEqual({ - CustomTypeFoo: { - properties: { - prop: { - type: 'string', - }, - }, - }, - CustomTypeBar: { - properties: { - prop: { - type: 'array', - items: { - $ref: '#definitions/CustomTypeFoo', - }, - }, + expect(jsonSchema.properties).to.deepEqual({ + cusArr: { + type: 'array', + items: { + $ref: '#definitions/CustomType', }, }, }); }); - }); - it('properly converts primitive arrays properties', () => { - @model() - class TestModel { - @property.array(Number) numArr: number[]; - } + it('supports explicit primitive type decoration via strings', () => { + @model() + class TestModel { + @property({type: 'string'}) + hardStr: Number; + @property({type: 'boolean'}) + hardBool: String; + @property({type: 'number'}) + hardNum: Boolean; + } - const jsonSchema = modelToJsonSchema(TestModel); - expect(jsonSchema.properties).to.deepEqual({ - numArr: { - type: 'array', - items: { + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.properties).to.deepEqual({ + hardStr: { + type: 'string', + }, + hardBool: { + type: 'boolean', + }, + hardNum: { type: 'number', }, - }, + }); }); - }); - - it('properly converts custom type arrays properties', () => { - class CustomType { - prop: string; - } - @model() - class TestModel { - @property.array(CustomType) cusArr: CustomType[]; - } + it('maps "required" keyword to the schema appropriately', () => { + @model() + class TestModel { + @property({required: false}) + propOne: string; + @property({required: true}) + propTwo: string; + @property() propThree: number; + } - const jsonSchema = modelToJsonSchema(TestModel); - expect(jsonSchema.properties).to.deepEqual({ - cusArr: { - type: 'array', - items: { - $ref: '#definitions/CustomType', - }, - }, + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.required).to.deepEqual(['propTwo']); }); - }); - it('supports explicit primitive type decoration via strings', () => { - @model() - class TestModel { - @property({type: 'string'}) - hardStr: Number; - @property({type: 'boolean'}) - hardBool: String; - @property({type: 'number'}) - hardNum: Boolean; - } + it('errors out when explicit type decoration is not primitive', () => { + @model() + class TestModel { + @property({type: 'NotPrimitive'}) + bad: String; + } - const jsonSchema = modelToJsonSchema(TestModel); - expect(jsonSchema.properties).to.deepEqual({ - hardStr: { - type: 'string', - }, - hardBool: { - type: 'boolean', - }, - hardNum: { - type: 'number', - }, + expect(() => modelToJsonSchema(TestModel)).to.throw(/Unsupported type/); }); - }); - - it('errors out when explicit type decoration is not primitive', () => { - @model() - class TestModel { - @property({type: 'NotPrimitive'}) - bad: String; - } - - expect(() => modelToJsonSchema(TestModel)).to.throw(/Unsupported type/); - }); - it('errors out when "@property.array" is not used on an array', () => { - @model() - class BadArray { - @property() badArr: string[]; - } + it('errors out when "@property.array" is not used on an array', () => { + @model() + class BadArray { + @property() badArr: string[]; + } - expect(() => { - modelToJsonSchema(BadArray); - }).to.throw(/type is defined as an array/); - }); + expect(() => { + modelToJsonSchema(BadArray); + }).to.throw(/type is defined as an array/); + }); - it('errors out if "@property.array" is given "Array" as parameter', () => { - @model() - class BadArray { - @property.array(Array) badArr: string[][]; - } + it('errors out if "@property.array" is given "Array" as parameter', () => { + @model() + class BadArray { + @property.array(Array) badArr: string[][]; + } - expect(() => { - modelToJsonSchema(BadArray); - }).to.throw(/type is defined as an array/); + expect(() => { + modelToJsonSchema(BadArray); + }).to.throw(/type is defined as an array/); + }); }); }); diff --git a/packages/repository-json-schema/test/unit/build-schema.test.ts b/packages/repository-json-schema/test/unit/build-schema.test.ts new file mode 100644 index 000000000000..678f58dcb582 --- /dev/null +++ b/packages/repository-json-schema/test/unit/build-schema.test.ts @@ -0,0 +1,85 @@ +import {expect} from '@loopback/testlab'; +import { + isComplexType, + stringTypeToWrapper, + metaToJsonProperty, +} from '../../index'; + +describe('build-schema', () => { + describe('stringTypeToWrapper', () => { + it('returns respective wrapper of number, string and boolean', () => { + expect(stringTypeToWrapper('string')).to.eql(String); + expect(stringTypeToWrapper('number')).to.eql(Number); + expect(stringTypeToWrapper('boolean')).to.eql(Boolean); + }); + + it('errors out if other types are given', () => { + expect(() => { + stringTypeToWrapper('arbitraryType'); + }).to.throw(/Unsupported type/); + expect(() => { + stringTypeToWrapper('function'); + }).to.throw(/Unsupported type/); + expect(() => { + stringTypeToWrapper('object'); + }).to.throw(/Unsupported type/); + }); + }); + + describe('isComplextype', () => { + it('returns false if primitive or object wrappers are passed in', () => { + expect(isComplexType(Number)).to.eql(false); + expect(isComplexType(String)).to.eql(false); + expect(isComplexType(Boolean)).to.eql(false); + expect(isComplexType(Object)).to.eql(false); + expect(isComplexType(Function)).to.eql(false); + }); + + it('returns true if any other wrappers are passed in', () => { + class CustomType {} + expect(isComplexType(CustomType)).to.eql(true); + }); + }); + + describe('metaToJsonSchema', () => { + it('errors out if "type" property is Array', () => { + expect(() => metaToJsonProperty({type: Array})).to.throw( + /type is defined as an array/, + ); + }); + + it('converts types in strings', () => { + expect(metaToJsonProperty({type: 'number'})).to.eql({ + type: 'number', + }); + }); + + it('converts primitives', () => { + expect(metaToJsonProperty({type: Number})).to.eql({ + type: 'number', + }); + }); + + it('converts complex types', () => { + class CustomType {} + expect(metaToJsonProperty({type: CustomType})).to.eql({ + $ref: '#definitions/CustomType', + }); + }); + + it('converts primitive arrays', () => { + expect(metaToJsonProperty({array: true, type: Number})).to.eql({ + type: 'array', + items: {type: 'number'}, + }); + }); + + it('converts arrays of custom types', () => { + class CustomType {} + expect(metaToJsonProperty({array: true, type: CustomType})).to.eql({ + type: 'array', + items: {$ref: '#definitions/CustomType'}, + }); + }); + }); +}); diff --git a/packages/repository/src/decorators/metadata.ts b/packages/repository/src/decorators/metadata.ts index 9b599f162345..9bc107beb3f9 100644 --- a/packages/repository/src/decorators/metadata.ts +++ b/packages/repository/src/decorators/metadata.ts @@ -5,6 +5,7 @@ import {InspectionOptions, MetadataInspector} from '@loopback/context'; import { + MODEL_KEY, MODEL_PROPERTIES_KEY, MODEL_WITH_PROPERTIES_KEY, PropertyMap, @@ -22,7 +23,7 @@ export class ModelMetadataHelper { static getModelMetadata( target: Function, options?: InspectionOptions, - ): ModelDefinition { + ): ModelDefinition | {} { let classDef: ModelDefinition | undefined; classDef = MetadataInspector.getClassMetadata( MODEL_WITH_PROPERTIES_KEY, @@ -35,20 +36,37 @@ export class ModelMetadataHelper { if (classDef) { return classDef; } else { - // sets the metadata to a dedicated key if cached value does not exist - const meta = new ModelDefinition( - Object.assign({name: target.name}, classDef), + const modelMeta = MetadataInspector.getClassMetadata( + MODEL_KEY, + target, + options, ); - meta.properties = Object.assign( - {}, - MetadataInspector.getAllPropertyMetadata( - MODEL_PROPERTIES_KEY, - target.prototype, - options, - ), - ); - MetadataInspector.defineMetadata(MODEL_WITH_PROPERTIES_KEY, meta, target); - return meta; + if (!modelMeta) { + return {}; + } else { + // sets the metadata to a dedicated key if cached value does not exist + + // set ModelDefinition properties if they don't already exist + const meta = new ModelDefinition(Object.assign({}, modelMeta)); + + // set properies lost from creating instance of ModelDefinition + Object.assign(meta, modelMeta); + + meta.properties = Object.assign( + {}, + MetadataInspector.getAllPropertyMetadata( + MODEL_PROPERTIES_KEY, + target.prototype, + options, + ), + ); + MetadataInspector.defineMetadata( + MODEL_WITH_PROPERTIES_KEY, + meta, + target, + ); + return meta; + } } } } diff --git a/packages/repository/src/decorators/model.ts b/packages/repository/src/decorators/model.ts index 920bdc455641..5bba8538cda7 100644 --- a/packages/repository/src/decorators/model.ts +++ b/packages/repository/src/decorators/model.ts @@ -81,6 +81,7 @@ export function property(definition?: Partial) { export namespace property { export const ERR_PROP_NOT_ARRAY = '@property.array can only decorate array properties!'; + export const ERR_NO_ARGS = 'decorator received less than two parameters'; /** * @@ -88,7 +89,7 @@ export namespace property { * @param definition Optional PropertyDefinition object for additional * metadata */ - export const array = function( + export function array( itemType: Function, definition?: Partial, ) { @@ -107,5 +108,5 @@ export namespace property { )(target, propertyName); } }; - }; + } } diff --git a/packages/repository/src/model.ts b/packages/repository/src/model.ts index 5fe1fa97fbf0..67e2c50401de 100644 --- a/packages/repository/src/model.ts +++ b/packages/repository/src/model.ts @@ -45,6 +45,7 @@ export interface ModelDefinitionSyntax { name: string; properties?: {[name: string]: PropertyDefinition | PropertyType}; settings?: {[name: string]: any}; + [attribute: string]: any; } /** diff --git a/packages/repository/test/unit/decorator/metadata.ts b/packages/repository/test/unit/decorator/metadata.ts index 4f72f19bdb62..71cc9a9f53c8 100644 --- a/packages/repository/test/unit/decorator/metadata.ts +++ b/packages/repository/test/unit/decorator/metadata.ts @@ -16,6 +16,10 @@ import {MetadataInspector} from '@loopback/context'; describe('Repository', () => { describe('getAllClassMetadata', () => { + class Oops { + @property() oopsie: string; + } + @model() class Colour { @property({}) @@ -41,6 +45,11 @@ describe('Repository', () => { @property.array(Colour) colours: Colour[]; } + it('returns empty object for classes without @model', () => { + const meta = ModelMetadataHelper.getModelMetadata(Oops); + expect(meta).to.deepEqual({}); + }); + it('retrieves metadata for classes with @model', () => { const meta = ModelMetadataHelper.getModelMetadata(Samoflange); expect(meta).to.deepEqual( diff --git a/packages/repository/test/unit/decorator/model-and-relation.ts b/packages/repository/test/unit/decorator/model-and-relation.ts index 183d04601fbb..425813cee66b 100644 --- a/packages/repository/test/unit/decorator/model-and-relation.ts +++ b/packages/repository/test/unit/decorator/model-and-relation.ts @@ -95,8 +95,6 @@ describe('model decorator', () => { // Validates that property no longer requires a parameter @property() isShipped: boolean; - - @property.array(Product) items: Product[]; } @model() @@ -152,6 +150,18 @@ describe('model decorator', () => { expect(meta).to.eql({name: 'foo'}); }); + it('adds model metadata with arbitrary properties', () => { + @model({arbitrary: 'property'}) + class Arbitrary { + name: string; + } + + const meta: {[props: string]: string} = + MetadataInspector.getClassMetadata(MODEL_KEY, Arbitrary) || + /* istanbul ignore next */ {}; + expect(meta.arbitrary).to.eql('property'); + }); + it('adds property metadata', () => { const meta = MetadataInspector.getAllPropertyMetadata( @@ -166,7 +176,6 @@ describe('model decorator', () => { }); expect(meta.id).to.eql({type: 'string', id: true, generated: true}); expect(meta.isShipped).to.eql({type: Boolean}); - expect(meta.items).to.eql({type: Product, array: true}); }); it('adds embedsOne metadata', () => { @@ -258,16 +267,34 @@ describe('model decorator', () => { }); }); - it('throws when @property.array is used on a non-array property', () => { - expect.throws( - () => { - // tslint:disable-next-line:no-unused-variable - class Oops { - @property.array(Product) product: Product; + describe('property namespace', () => { + describe('array', () => { + it('"@property.array" adds array metadata', () => { + @model() + class TestModel { + @property.array(Product) items: Product[]; } - }, - Error, - property.ERR_PROP_NOT_ARRAY, - ); + + const meta = + MetadataInspector.getAllPropertyMetadata( + MODEL_PROPERTIES_KEY, + TestModel.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.items).to.eql({type: Product, array: true}); + }); + + it('throws when @property.array is used on a non-array property', () => { + expect.throws( + () => { + // tslint:disable-next-line:no-unused-variable + class Oops { + @property.array(Product) product: Product; + } + }, + Error, + property.ERR_PROP_NOT_ARRAY, + ); + }); + }); }); }); diff --git a/packages/rest/test/unit/rest-server/rest-server.open-api-spec.test.ts b/packages/rest/test/unit/rest-server/rest-server.open-api-spec.test.ts index 8f03a9d15565..0453ed960c31 100644 --- a/packages/rest/test/unit/rest-server/rest-server.open-api-spec.test.ts +++ b/packages/rest/test/unit/rest-server/rest-server.open-api-spec.test.ts @@ -134,6 +134,7 @@ describe('RestServer.getApiSpec()', () => { const spec = server.getApiSpec(); expect(spec.definitions).to.deepEqual({ MyModel: { + title: 'MyModel', properties: { bar: { type: 'string',