diff --git a/packages/cli/generators/model/index.js b/packages/cli/generators/model/index.js index f68918de675b..125470a12dbd 100644 --- a/packages/cli/generators/model/index.js +++ b/packages/cli/generators/model/index.js @@ -121,7 +121,7 @@ module.exports = class ModelGenerator extends ArtifactGenerator { choices: this.typeChoices, }, { - name: 'arrayType', + name: 'itemType', message: 'Type of array items:', type: 'list', choices: this.typeChoices.filter(choice => { @@ -204,18 +204,18 @@ module.exports = class ModelGenerator extends ArtifactGenerator { // Set up types for Templating const TS_TYPES = ['string', 'number', 'object', 'boolean', 'any']; const NON_TS_TYPES = ['geopoint', 'date']; - Object.entries(this.artifactInfo.properties).forEach(([key, val]) => { + Object.values(this.artifactInfo.properties).forEach(val => { // Default tsType is the type property val.tsType = val.type; // Override tsType based on certain type values if (val.type === 'array') { - if (TS_TYPES.includes(val.arrayType)) { - val.tsType = `${val.arrayType}[]`; + if (TS_TYPES.includes(val.itemType)) { + val.tsType = `${val.itemType}[]`; } else if (val.type === 'buffer') { - val.tsType = `Buffer[]`; + val.tsType = 'Buffer[]'; } else { - val.tsType = `string[]`; + val.tsType = 'string[]'; } } else if (val.type === 'buffer') { val.tsType = 'Buffer'; @@ -234,8 +234,8 @@ module.exports = class ModelGenerator extends ArtifactGenerator { // Convert Type to include '' for template val.type = `'${val.type}'`; - if (val.arrayType) { - val.arrayType = `'${val.arrayType}'`; + if (val.itemType) { + val.itemType = `'${val.itemType}'`; } // If required is false, we can delete it as that's the default assumption diff --git a/packages/cli/generators/model/templates/model.ts.ejs b/packages/cli/generators/model/templates/model.ts.ejs index a0f7835006ed..e7d8a70014a1 100644 --- a/packages/cli/generators/model/templates/model.ts.ejs +++ b/packages/cli/generators/model/templates/model.ts.ejs @@ -5,7 +5,7 @@ export class <%= className %> extends Entity { <% Object.entries(properties).forEach(([key, val]) => { -%> @property({ <%_ Object.entries(val).forEach(([propKey, propVal]) => { -%> - <%_ if (propKey !== 'tsType') { -%> + <%_ if (!['tsType'].includes(propKey)) { -%> <%= propKey %>: <%- propVal %>, <%_ } -%> <%_ }) -%> diff --git a/packages/repository-json-schema/src/build-schema.ts b/packages/repository-json-schema/src/build-schema.ts index 31ea12cee2ce..2ce6fa7b8006 100644 --- a/packages/repository-json-schema/src/build-schema.ts +++ b/packages/repository-json-schema/src/build-schema.ts @@ -36,7 +36,10 @@ export function getJsonSchema(ctor: Function): JSONSchema { * Gets the wrapper function of primitives string, number, and boolean * @param type Name of type */ -export function stringTypeToWrapper(type: string): Function { +export function stringTypeToWrapper(type: string | Function): Function { + if (typeof type === 'function') { + return type; + } type = type.toLowerCase(); let wrapper; switch (type) { @@ -52,6 +55,22 @@ export function stringTypeToWrapper(type: string): Function { wrapper = Boolean; break; } + case 'array': { + wrapper = Array; + break; + } + case 'object': { + wrapper = Object; + break; + } + case 'date': { + wrapper = Date; + break; + } + case 'buffer': { + wrapper = Buffer; + break; + } default: { throw new Error('Unsupported type: ' + type); } @@ -64,9 +83,22 @@ export function stringTypeToWrapper(type: string): Function { * @param ctor Constructor */ export function isComplexType(ctor: Function) { - return !([String, Number, Boolean, Object, Function] as Function[]).includes( - ctor, - ); + return !([ + String, + Number, + Boolean, + Object, + Function, + Array, + ] as Function[]).includes(ctor); +} + +/** + * Determines whether a given string or constructor is array type or not + * @param type Type as string or wrapper + */ +export function isArrayType(type: string | Function) { + return type === Array || type === 'array'; } /** @@ -74,30 +106,32 @@ export function isComplexType(ctor: Function) { * @param meta */ export function metaToJsonProperty(meta: PropertyDefinition): JSONSchema { - let ctor = meta.type as string | Function; - let def: JSONSchema = {}; - - // 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); + // tslint:disable-next-line:no-any + const propDef: JSONSchema = {}; + let result: JSONSchema; + let propertyType = meta.type as string | Function; + + if (isArrayType(propertyType) && meta.itemType) { + if (Array.isArray(meta.itemType)) { + throw new Error('itemType as an array is not supported'); + } + result = {type: 'array', items: propDef}; + propertyType = meta.itemType as string | Function; + } else { + result = propDef; } - const propDef = isComplexType(ctor) - ? {$ref: `#/definitions/${ctor.name}`} - : {type: ctor.name.toLowerCase()}; + propertyType = stringTypeToWrapper(propertyType); - if (meta.array) { - def.type = 'array'; - def.items = propDef; + if (isComplexType(propertyType)) { + Object.assign(propDef, {$ref: `#/definitions/${propertyType.name}`}); } else { - Object.assign(def, propDef); + Object.assign(propDef, { + type: propertyType.name.toLowerCase(), + }); } - return def; + return result; } // NOTE(shimks) no metadata for: union, optional, nested array, any, enum, @@ -131,15 +165,19 @@ export function modelToJsonSchema(ctor: Function): JSONSchema { result.properties = result.properties || {}; result.properties[p] = result.properties[p] || {}; - const metaProperty = meta.properties[p]; - const metaType = metaProperty.type; + const metaProperty = Object.assign({}, meta.properties[p]); // populating "properties" key result.properties[p] = metaToJsonProperty(metaProperty); // populating JSON Schema 'definitions' - if (typeof metaType === 'function' && isComplexType(metaType)) { - const propSchema = getJsonSchema(metaType); + const referenceType = isArrayType(metaProperty.type as string | Function) + ? // shimks: ugly type casting; this should be replaced by logic to throw + // error if itemType/type is not a string or a function + (metaProperty.itemType as string | Function) + : (metaProperty.type as string | Function); + if (typeof referenceType === 'function' && isComplexType(referenceType)) { + const propSchema = getJsonSchema(referenceType); if (propSchema && Object.keys(propSchema).length > 0) { result.definitions = result.definitions || {}; @@ -152,7 +190,7 @@ export function modelToJsonSchema(ctor: Function): JSONSchema { delete propSchema.definitions; } - result.definitions[metaType.name] = propSchema; + result.definitions[referenceType.name] = propSchema; } } diff --git a/packages/repository-json-schema/test/integration/build-schema.integration.ts b/packages/repository-json-schema/test/integration/build-schema.integration.ts index 4d225e2ee2d9..f4d35bf45721 100644 --- a/packages/repository-json-schema/test/integration/build-schema.integration.ts +++ b/packages/repository-json-schema/test/integration/build-schema.integration.ts @@ -153,6 +153,113 @@ describe('build-schema', () => { expectValidJsonSchema(jsonSchema); }); + it('properly converts primitive array properties', () => { + @model() + class TestModel { + @property.array(Number) + numArr: number[]; + } + + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.properties).to.deepEqual({ + numArr: { + type: 'array', + items: { + type: 'number', + }, + }, + }); + expectValidJsonSchema(jsonSchema); + }); + + it('properly converts properties with recursive arrays', () => { + @model() + class RecursiveArray { + @property.array(Array) + recArr: string[][]; + } + + const jsonSchema = modelToJsonSchema(RecursiveArray); + expect(jsonSchema.properties).to.eql({ + recArr: { + type: 'array', + items: { + type: 'array', + }, + }, + }); + }); + + 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({ + hardStr: { + type: 'string', + }, + hardBool: { + type: 'boolean', + }, + hardNum: { + type: 'number', + }, + }); + expectValidJsonSchema(jsonSchema); + }); + + 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.required).to.deepEqual(['propTwo']); + expectValidJsonSchema(jsonSchema); + }); + + 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('properly converts array of types defined by strings', () => { + @model() + class TestModel { + @property({type: 'array', itemType: 'number'}) + num: number[]; + } + + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.properties).to.eql({ + num: { + type: 'array', + items: { + type: 'number', + }, + }, + }); + }); + context('with custom type properties', () => { it('properly converts undecorated custom type properties', () => { class CustomType { @@ -207,6 +314,62 @@ describe('build-schema', () => { expectValidJsonSchema(jsonSchema); }); + it('properly converts undecorated custom array type properties', () => { + class CustomType { + prop: string; + } + + @model() + class TestModel { + @property.array(CustomType) + cusArr: CustomType[]; + } + + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.properties).to.deepEqual({ + cusArr: { + type: 'array', + items: { + $ref: '#/definitions/CustomType', + }, + }, + }); + expectValidJsonSchema(jsonSchema); + }); + + it('properly converts decorated custom array type properties', () => { + @model() + class CustomType { + @property() + prop: string; + } + + @model() + class TestModel { + @property.array(CustomType) + cusType: CustomType[]; + } + + const jsonSchema = modelToJsonSchema(TestModel); + expect(jsonSchema.properties).to.deepEqual({ + cusType: { + type: 'array', + items: {$ref: '#/definitions/CustomType'}, + }, + }); + expect(jsonSchema.definitions).to.deepEqual({ + CustomType: { + title: 'CustomType', + properties: { + prop: { + type: 'string', + }, + }, + }, + }); + expectValidJsonSchema(jsonSchema); + }); + it('creates definitions only at the root level of the schema', () => { @model() class CustomTypeFoo { @@ -258,124 +421,6 @@ describe('build-schema', () => { expectValidJsonSchema(jsonSchema); }); }); - - it('properly converts primitive arrays properties', () => { - @model() - class TestModel { - @property.array(Number) - numArr: number[]; - } - - const jsonSchema = modelToJsonSchema(TestModel); - expect(jsonSchema.properties).to.deepEqual({ - numArr: { - type: 'array', - items: { - type: 'number', - }, - }, - }); - expectValidJsonSchema(jsonSchema); - }); - - it('properly converts custom type arrays properties', () => { - class CustomType { - prop: string; - } - - @model() - class TestModel { - @property.array(CustomType) - cusArr: CustomType[]; - } - - const jsonSchema = modelToJsonSchema(TestModel); - expect(jsonSchema.properties).to.deepEqual({ - cusArr: { - type: 'array', - items: { - $ref: '#/definitions/CustomType', - }, - }, - }); - expectValidJsonSchema(jsonSchema); - }); - - 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({ - hardStr: { - type: 'string', - }, - hardBool: { - type: 'boolean', - }, - hardNum: { - type: 'number', - }, - }); - expectValidJsonSchema(jsonSchema); - }); - - 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.required).to.deepEqual(['propTwo']); - expectValidJsonSchema(jsonSchema); - }); - - 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[]; - } - - 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[][]; - } - - expect(() => { - modelToJsonSchema(BadArray); - }).to.throw(/type is defined as an array/); - }); }); function expectValidJsonSchema(schema: JsonSchema) { diff --git a/packages/repository-json-schema/test/unit/build-schema.unit.ts b/packages/repository-json-schema/test/unit/build-schema.unit.ts index bfa187b7b1a5..5d3ecf898264 100644 --- a/packages/repository-json-schema/test/unit/build-schema.unit.ts +++ b/packages/repository-json-schema/test/unit/build-schema.unit.ts @@ -8,10 +8,34 @@ import {isComplexType, stringTypeToWrapper, metaToJsonProperty} from '../..'; 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); + context('when given primitive types in string', () => { + it('returns String for "string"', () => { + expect(stringTypeToWrapper('string')).to.eql(String); + }); + + it('returns Number for "number"', () => { + expect(stringTypeToWrapper('number')).to.eql(Number); + }); + + it('returns Boolean for "boolean"', () => { + expect(stringTypeToWrapper('boolean')).to.eql(Boolean); + }); + + it('returns Array for "array"', () => { + expect(stringTypeToWrapper('array')).to.eql(Array); + }); + + it('returns Buffer for "buffer"', () => { + expect(stringTypeToWrapper('buffer')).to.eql(Buffer); + }); + + it('returns Date for "date"', () => { + expect(stringTypeToWrapper('date')).to.eql(Date); + }); + + it('returns Object for "object"', () => { + expect(stringTypeToWrapper('object')).to.eql(Object); + }); }); it('errors out if other types are given', () => { @@ -21,19 +45,34 @@ describe('build-schema', () => { 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); + context('when given primitive wrappers', () => { + it('returns false for Number', () => { + expect(isComplexType(Number)).to.eql(false); + }); + + it('returns false for String', () => { + expect(isComplexType(String)).to.eql(false); + }); + + it('returns false for Boolean', () => { + expect(isComplexType(Boolean)).to.eql(false); + }); + + it('returns false for Object', () => { + expect(isComplexType(Object)).to.eql(false); + }); + + it('returns false for Function', () => { + expect(isComplexType(Function)).to.eql(false); + }); + + it('returns false for Array', () => { + expect(isComplexType(Array)).to.eql(false); + }); }); it('returns true if any other wrappers are passed in', () => { @@ -43,9 +82,9 @@ describe('build-schema', () => { }); describe('metaToJsonSchema', () => { - it('errors out if "type" property is Array', () => { - expect(() => metaToJsonProperty({type: Array})).to.throw( - /type is defined as an array/, + it('errors out if "itemType" is an array', () => { + expect(() => metaToJsonProperty({type: Array, itemType: []})).to.throw( + /itemType as an array is not supported/, ); }); @@ -61,6 +100,12 @@ describe('build-schema', () => { }); }); + it('converts arrays', () => { + expect(metaToJsonProperty({type: Array})).to.eql({ + type: 'array', + }); + }); + it('converts complex types', () => { class CustomType {} expect(metaToJsonProperty({type: CustomType})).to.eql({ @@ -69,7 +114,7 @@ describe('build-schema', () => { }); it('converts primitive arrays', () => { - expect(metaToJsonProperty({array: true, type: Number})).to.eql({ + expect(metaToJsonProperty({type: Array, itemType: Number})).to.eql({ type: 'array', items: {type: 'number'}, }); @@ -77,7 +122,7 @@ describe('build-schema', () => { it('converts arrays of custom types', () => { class CustomType {} - expect(metaToJsonProperty({array: true, type: CustomType})).to.eql({ + expect(metaToJsonProperty({type: Array, itemType: CustomType})).to.eql({ type: 'array', items: {$ref: '#/definitions/CustomType'}, }); diff --git a/packages/repository/src/decorators/model.decorator.ts b/packages/repository/src/decorators/model.decorator.ts index 8ec1bc148b47..21be80f0c937 100644 --- a/packages/repository/src/decorators/model.decorator.ts +++ b/packages/repository/src/decorators/model.decorator.ts @@ -121,9 +121,10 @@ export namespace property { throw new Error(ERR_PROP_NOT_ARRAY); } else { property( - Object.assign({array: true}, definition, { - type: itemType, - }), + Object.assign( + {type: Array, itemType} as Partial, + definition, + ), )(target, propertyName); } }; diff --git a/packages/repository/src/model.ts b/packages/repository/src/model.ts index 71ebc6559676..89ec260d9c6c 100644 --- a/packages/repository/src/model.ts +++ b/packages/repository/src/model.ts @@ -25,6 +25,7 @@ export interface PropertyDefinition { id?: boolean; json?: PropertyForm; store?: PropertyForm; + itemType?: PropertyType; // type of array [attribute: string]: any; // Other attributes } diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index f68d6e55e956..2aa37af20bc1 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -112,9 +112,16 @@ export class DefaultCrudRepository // We need to convert property definitions from PropertyDefinition // to plain data object because of a juggler limitation const properties: {[name: string]: object} = {}; - for (const p in definition.properties) { - properties[p] = Object.assign({}, definition.properties[p]); - } + + // We need to convert PropertyDefinition into the definition that + // the juggler understands + Object.entries(definition.properties).forEach(([key, value]) => { + if (value.type === 'array' || value.type === Array) { + value = Object.assign({}, value, {type: [value.itemType]}); + delete value.itemType; + } + properties[key] = Object.assign({}, value); + }); this.modelClass = dataSource.createModel( definition.name, diff --git a/packages/repository/test/unit/decorator/metadata.unit.ts b/packages/repository/test/unit/decorator/metadata.unit.ts index 1ee75897ab4f..78e9e3d070f6 100644 --- a/packages/repository/test/unit/decorator/metadata.unit.ts +++ b/packages/repository/test/unit/decorator/metadata.unit.ts @@ -76,8 +76,8 @@ describe('Repository', () => { type: Number, }, colours: { - array: true, - type: Colour, + type: Array, + itemType: Colour, }, }, settings: new Map(), diff --git a/packages/repository/test/unit/decorator/model-and-relation.decorator.unit.ts b/packages/repository/test/unit/decorator/model-and-relation.decorator.unit.ts index 566637e8b9b4..17f8c10c127d 100644 --- a/packages/repository/test/unit/decorator/model-and-relation.decorator.unit.ts +++ b/packages/repository/test/unit/decorator/model-and-relation.decorator.unit.ts @@ -192,6 +192,21 @@ describe('model decorator', () => { expect(meta.isShipped).to.eql({type: Boolean}); }); + it('adds explicitly declared array property metadata', () => { + @model() + class ArrayModel { + @property({type: Array}) + strArr: string[]; + } + + const meta = + MetadataInspector.getAllPropertyMetadata( + MODEL_PROPERTIES_KEY, + ArrayModel.prototype, + ) || /* istanbul ignore next */ {}; + expect(meta.strArr).to.eql({type: Array}); + }); + it('adds embedsOne metadata', () => { const meta = MetadataInspector.getAllPropertyMetadata( @@ -316,7 +331,7 @@ describe('model decorator', () => { MODEL_PROPERTIES_KEY, TestModel.prototype, ) || /* istanbul ignore next */ {}; - expect(meta.items).to.eql({type: Product, array: true}); + expect(meta.items).to.eql({type: Array, itemType: Product}); }); it('throws when @property.array is used on a non-array property', () => { diff --git a/packages/repository/test/unit/decorator/relation.decorator.unit.ts b/packages/repository/test/unit/decorator/relation.decorator.unit.ts index f8396f8c24e9..d186edf0d314 100644 --- a/packages/repository/test/unit/decorator/relation.decorator.unit.ts +++ b/packages/repository/test/unit/decorator/relation.decorator.unit.ts @@ -61,12 +61,12 @@ describe('relation decorator', () => { keyTo: 'addressBookId', }); expect(jugglerMeta).to.eql({ - type: Address, - array: true, + type: Array, + itemType: Address, }); }); - it('takes in both complex property type and asMany metadata', () => { + it('takes in both complex property type and hasMany metadata', () => { class Address extends Entity { addressId: number; street: string; @@ -95,8 +95,8 @@ describe('relation decorator', () => { keyTo: 'someForeignKey', }); expect(jugglerMeta).to.eql({ - type: Address, - array: true, + type: Array, + itemType: Address, }); }); diff --git a/packages/repository/test/unit/repositories/legacy-juggler-bridge.unit.ts b/packages/repository/test/unit/repositories/legacy-juggler-bridge.unit.ts index 8f4eaef5cc7f..eeb1561c303f 100644 --- a/packages/repository/test/unit/repositories/legacy-juggler-bridge.unit.ts +++ b/packages/repository/test/unit/repositories/legacy-juggler-bridge.unit.ts @@ -75,6 +75,55 @@ describe('DefaultCrudRepository', () => { await model.deleteAll(); }); + context('constructor', () => { + class ShoppingList extends Entity { + static definition = new ModelDefinition({ + name: 'ShoppingList', + properties: { + id: { + type: 'number', + id: true, + }, + toBuy: { + type: 'array', + itemType: 'string', + }, + toVisit: { + type: Array, + itemType: String, + }, + }, + }); + + toBuy: String[]; + toVisit: String[]; + } + + it('converts PropertyDefinition with array type', () => { + const originalPropertyDefinition = Object.assign( + {}, + ShoppingList.definition.properties, + ); + const listDefinition = new DefaultCrudRepository(ShoppingList, ds) + .modelClass.definition; + const jugglerPropertyDefinition = { + toBuy: { + type: [String], + }, + toVisit: { + type: [String], + }, + }; + + expect(listDefinition.properties).to.containDeep( + jugglerPropertyDefinition, + ); + expect(ShoppingList.definition.properties).to.containDeep( + originalPropertyDefinition, + ); + }); + }); + it('shares the backing PersistedModel across repo instances', () => { const model1 = new DefaultCrudRepository(Note, ds).modelClass; const model2 = new DefaultCrudRepository(Note, ds).modelClass;