From 4cc8eba853232213f9f82408568587d81103142e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 7 May 2020 09:57:49 +0200 Subject: [PATCH] feat(repository): extract helper `rejectNavigationalPropertiesInData` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the code for rejecting navigational properites into a standalone helper function that can be used from other repository implementations too. Signed-off-by: Miroslav Bajtoš --- .../src/__tests__/unit/model/model.unit.ts | 39 +++++++++++++++++++ packages/repository/src/model.ts | 36 ++++++++++++++++- .../src/repositories/legacy-juggler-bridge.ts | 35 ++++++----------- 3 files changed, 86 insertions(+), 24 deletions(-) diff --git a/packages/repository/src/__tests__/unit/model/model.unit.ts b/packages/repository/src/__tests__/unit/model/model.unit.ts index a692db18f3d3..f924d51ed920 100644 --- a/packages/repository/src/__tests__/unit/model/model.unit.ts +++ b/packages/repository/src/__tests__/unit/model/model.unit.ts @@ -5,9 +5,11 @@ import {expect} from '@loopback/testlab'; import { + BelongsToDefinition, Entity, HasManyDefinition, ModelDefinition, + rejectNavigationalPropertiesInData, RelationType, STRING, } from '../../../'; @@ -457,4 +459,41 @@ describe('model', () => { firstName: 'Test User', }); }); + + describe('rejectNavigationalPropertiesInData', () => { + class Order extends Entity { + static definition = new ModelDefinition('Order') + .addProperty('id', {type: 'string', id: true}) + .addRelation({ + name: 'customer', + type: RelationType.belongsTo, + targetsMany: false, + source: Order, + target: () => User, + keyFrom: 'customerId', + }); + + id: string; + customerId: string; + + customer?: User; + } + + it('accepts data with no navigational properties', () => { + rejectNavigationalPropertiesInData(Order, {id: '1'}); + }); + + it('rejects data with a navigational property', () => { + expect(() => + rejectNavigationalPropertiesInData(Order, { + id: '1', + customer: { + id: '2', + email: 'test@example.com', + firstName: 'a customer', + }, + }), + ).to.throw(/Navigational properties are not allowed in model data/); + }); + }); }); diff --git a/packages/repository/src/model.ts b/packages/repository/src/model.ts index df161ab9a4eb..d0be36a56b23 100644 --- a/packages/repository/src/model.ts +++ b/packages/repository/src/model.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {AnyObject, DataObject, Options} from './common-types'; +import {AnyObject, DataObject, Options, PrototypeOf} from './common-types'; import {JsonSchema} from './index'; import {RelationMetadata} from './relations'; import {TypeResolver} from './type-resolver'; @@ -393,3 +393,37 @@ export class Event { export type EntityData = DataObject; export type EntityResolver = TypeResolver; + +/** + * Check model data for navigational properties linking to related models. + * Throw a descriptive error if any such property is found. + * + * @param modelClass Model constructor, e.g. `Product`. + * @param entityData Model instance or a plain-data object, + * e.g. `{name: 'pen'}`. + */ +export function rejectNavigationalPropertiesInData( + modelClass: M, + data: DataObject>, +) { + const def = modelClass.definition; + const props = def.properties; + + for (const r in def.relations) { + const relName = def.relations[r].name; + if (!(relName in data)) continue; + + let msg = + 'Navigational properties are not allowed in model data ' + + `(model "${modelClass.modelName}" property "${relName}"), ` + + 'please remove it.'; + + if (relName in props) { + msg += + ' The error might be invoked by belongsTo relations, please make' + + ' sure the relation name is not the same as the property name.'; + } + + throw new Error(msg); + } +} diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 6cc74bb53fa0..dec68f72d511 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -11,12 +11,18 @@ import { Command, Count, DataObject, + DeepPartial, NamedParameters, Options, PositionalParameters, } from '../common-types'; import {EntityNotFoundError} from '../errors'; -import {Entity, Model, PropertyType} from '../model'; +import { + Entity, + Model, + PropertyType, + rejectNavigationalPropertiesInData, +} from '../model'; import {Filter, FilterExcludingWhere, Inclusion, Where} from '../query'; import { BelongsToAccessor, @@ -109,8 +115,8 @@ export class DefaultCrudRepository< /** * Constructor of DefaultCrudRepository - * @param entityClass - Legacy entity class - * @param dataSource - Legacy data source + * @param entityClass - LoopBack 4 entity class + * @param dataSource - Legacy juggler data source */ constructor( // entityClass should have type "typeof T", but that's not supported by TSC @@ -590,27 +596,10 @@ export class DefaultCrudRepository< const data: AnyObject = typeof entity.toJSON === 'function' ? entity.toJSON() : {...entity}; */ + const data: DeepPartial = new this.entityClass(entity); + + rejectNavigationalPropertiesInData(this.entityClass, data); - const data: AnyObject = new this.entityClass(entity); - - const def = this.entityClass.definition; - const props = def.properties; - for (const r in def.relations) { - const relName = def.relations[r].name; - if (relName in data) { - let invalidNameMsg = ''; - if (relName in props) { - invalidNameMsg = - ` The error might be invoked by belongsTo relations, please make sure the relation name is not the same as` + - ` the property name.`; - } - throw new Error( - `Navigational properties are not allowed in model data (model "${this.entityClass.modelName}"` + - ` property "${relName}"), please remove it.` + - invalidNameMsg, - ); - } - } return data; }