diff --git a/docs/site/BelongsTo-relation.md b/docs/site/BelongsTo-relation.md index 42d32706948a..3a075bba0fdf 100644 --- a/docs/site/BelongsTo-relation.md +++ b/docs/site/BelongsTo-relation.md @@ -130,7 +130,7 @@ export class OrderRepository extends DefaultCrudRepository< ) { super(Order, db); this.customer = this._createBelongsToAccessorFor( - 'customerId', + 'customer', customerRepositoryGetter, ); } diff --git a/examples/todo-list/src/repositories/todo.repository.ts b/examples/todo-list/src/repositories/todo.repository.ts index 9eb7e1125d37..3c09cd83ff71 100644 --- a/examples/todo-list/src/repositories/todo.repository.ts +++ b/examples/todo-list/src/repositories/todo.repository.ts @@ -30,7 +30,7 @@ export class TodoRepository extends DefaultCrudRepository< super(Todo, dataSource); this.todoList = this._createBelongsToAccessorFor( - 'todoListId', + 'todoList', todoListRepositoryGetter, ); } diff --git a/packages/repository/src/decorators/model.decorator.ts b/packages/repository/src/decorators/model.decorator.ts index 0caeb1404823..4c3934e8d981 100644 --- a/packages/repository/src/decorators/model.decorator.ts +++ b/packages/repository/src/decorators/model.decorator.ts @@ -55,35 +55,46 @@ export function model(definition?: Partial) { decorator(target); // Build "ModelDefinition" and store it on model constructor - const modelDef = new ModelDefinition(def); - - const propertyMap: PropertyMap = - MetadataInspector.getAllPropertyMetadata( - MODEL_PROPERTIES_KEY, - target.prototype, - ) || {}; + buildModelDefinition(target, def); + }; +} - for (const p in propertyMap) { - const propertyDef = propertyMap[p]; - const designType = MetadataInspector.getDesignTypeForProperty( - target.prototype, - p, - ); - if (!propertyDef.type) { - propertyDef.type = designType; - } - modelDef.addProperty(p, propertyDef); +/** + * Build model definition from decorations + * @param target Target model class + * @param def Model definition spec + */ +export function buildModelDefinition( + target: Function & {definition?: ModelDefinition | undefined}, + def?: ModelDefinitionSyntax, +) { + // Check if the definition for this class has been built + if (!def && target.definition && target.definition.name === target.name) { + return target.definition; + } + const modelDef = new ModelDefinition(def || {name: target.name}); + const prototype = target.prototype; + const propertyMap: PropertyMap = + MetadataInspector.getAllPropertyMetadata(MODEL_PROPERTIES_KEY, prototype) || + {}; + for (const p in propertyMap) { + const propertyDef = propertyMap[p]; + const designType = MetadataInspector.getDesignTypeForProperty(prototype, p); + if (!propertyDef.type) { + propertyDef.type = designType; } - - target.definition = modelDef; - - const relationMap: RelationDefinitionMap = - MetadataInspector.getAllPropertyMetadata( - RELATIONS_KEY, - target.prototype, - ) || {}; - target.definition.relations = relationMap; - }; + modelDef.addProperty(p, propertyDef); + } + target.definition = modelDef; + const relationMeta: RelationDefinitionMap = + MetadataInspector.getAllPropertyMetadata(RELATIONS_KEY, prototype) || {}; + const relations: RelationDefinitionMap = {}; + // Build an object keyed by relation names + Object.values(relationMeta).forEach(r => { + relations[r.name] = r; + }); + target.definition.relations = relations; + return modelDef; } /** diff --git a/packages/repository/src/relations/belongs-to/belongs-to.decorator.ts b/packages/repository/src/relations/belongs-to/belongs-to.decorator.ts index dadffdf6efae..c48b57e193bf 100644 --- a/packages/repository/src/relations/belongs-to/belongs-to.decorator.ts +++ b/packages/repository/src/relations/belongs-to/belongs-to.decorator.ts @@ -39,13 +39,13 @@ export function belongsTo( // default values, can be customized by the caller { keyFrom: decoratedKey, + name: relationName, }, // properties provided by the caller definition, // properties enforced by the decorator { type: RelationType.belongsTo, - name: relationName, source: decoratedTarget.constructor, target: targetResolver, }, diff --git a/packages/repository/src/relations/has-many/has-many.decorator.ts b/packages/repository/src/relations/has-many/has-many.decorator.ts index 5019ce6769a7..1fc3f26d5a0a 100644 --- a/packages/repository/src/relations/has-many/has-many.decorator.ts +++ b/packages/repository/src/relations/has-many/has-many.decorator.ts @@ -25,13 +25,12 @@ export function hasMany( const meta: HasManyDefinition = Object.assign( // default values, can be customized by the caller - {}, + {name: key}, // properties provided by the caller definition, // properties enforced by the decorator { type: RelationType.hasMany, - name: key, source: decoratedTarget.constructor, target: targetResolver, }, diff --git a/packages/repository/src/relations/relation.decorator.ts b/packages/repository/src/relations/relation.decorator.ts index e30346587920..72a09d7f0c56 100644 --- a/packages/repository/src/relations/relation.decorator.ts +++ b/packages/repository/src/relations/relation.decorator.ts @@ -6,6 +6,7 @@ import {PropertyDecoratorFactory} from '@loopback/context'; import {Model, RelationDefinitionMap} from '../model'; import {RelationType} from './relation.types'; +import {buildModelDefinition} from '../decorators'; export const RELATIONS_KEY = 'loopback:relations'; @@ -28,7 +29,9 @@ export function relation(definition?: Object) { export function getModelRelations( modelCtor: typeof Model, ): RelationDefinitionMap { - return (modelCtor.definition && modelCtor.definition.relations) || {}; + // Build model definitions if `@model` is missing + const modelDef = buildModelDefinition(modelCtor); + return (modelDef && modelDef.relations) || {}; } // diff --git a/packages/repository/test/fixtures/repositories/order.repository.ts b/packages/repository/test/fixtures/repositories/order.repository.ts index 6a21c755c9f2..b8e9e2bd8fc2 100644 --- a/packages/repository/test/fixtures/repositories/order.repository.ts +++ b/packages/repository/test/fixtures/repositories/order.repository.ts @@ -29,7 +29,7 @@ export class OrderRepository extends DefaultCrudRepository< ) { super(Order, db); this.customer = this._createBelongsToAccessorFor( - 'customerId', + 'customer', customerRepositoryGetter, ); } diff --git a/packages/repository/test/unit/decorator/relation.decorator.unit.ts b/packages/repository/test/unit/decorator/relation.decorator.unit.ts index cee01aba0dc4..e30f19e5fd2d 100644 --- a/packages/repository/test/unit/decorator/relation.decorator.unit.ts +++ b/packages/repository/test/unit/decorator/relation.decorator.unit.ts @@ -29,6 +29,7 @@ describe('relation decorator', () => { addressBookId: number; } + @model() class AddressBook extends Entity { id: number; @@ -57,6 +58,15 @@ describe('relation decorator', () => { type: Array, itemType: () => Address, }); + + expect(AddressBook.definition.relations).to.eql({ + addresses: { + type: RelationType.hasMany, + name: 'addresses', + source: AddressBook, + target: () => Address, + }, + }); }); it('takes in both complex property type and hasMany metadata', () => { @@ -126,6 +136,7 @@ describe('relation decorator', () => { @property({id: true}) id: number; } + @model() class Address extends Entity { @belongsTo(() => AddressBook) addressBookId: number; @@ -139,6 +150,13 @@ describe('relation decorator', () => { type: Number, }, }); + expect(Address.definition.relations).to.containDeep({ + addressBook: { + keyFrom: 'addressBookId', + name: 'addressBook', + type: 'belongsTo', + }, + }); }); it('assigns it to target key', () => { @@ -170,6 +188,7 @@ describe('relation decorator', () => { }); it('accepts explicit keyFrom and keyTo', () => { + @model() class Address extends Entity { addressId: number; street: string; @@ -177,6 +196,7 @@ describe('relation decorator', () => { @belongsTo(() => AddressBook, { keyFrom: 'aForeignKey', keyTo: 'aPrimaryKey', + name: 'address-book', }) addressBookId: number; } @@ -194,6 +214,13 @@ describe('relation decorator', () => { keyFrom: 'aForeignKey', keyTo: 'aPrimaryKey', }); + expect(Address.definition.relations).to.containDeep({ + 'address-book': { + type: 'belongsTo', + keyFrom: 'aForeignKey', + keyTo: 'aPrimaryKey', + }, + }); }); }); });