From 89633ea6627e67997a0a83a8b8abaa2f8764fd77 Mon Sep 17 00:00:00 2001 From: Jam Risser Date: Thu, 31 Jan 2019 00:51:02 -0600 Subject: [PATCH] feat(repository): created hasAndBelongsToMany following convention pattern --- ...-and-belongs-to-many-repository.factory.ts | 109 ++++++++++++++++ .../has-and-belongs-to-many.decorator.ts | 37 ++++++ .../has-and-belongs-to-many.repository.ts | 117 ++++++++++++++++++ .../has-and-belongs-to-many/index.ts | 8 ++ packages/repository/src/relations/index.ts | 1 + .../src/relations/relation.types.ts | 23 +++- .../src/repositories/legacy-juggler-bridge.ts | 76 +++++++++++- 7 files changed, 363 insertions(+), 8 deletions(-) create mode 100644 packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many-repository.factory.ts create mode 100644 packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many.decorator.ts create mode 100644 packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many.repository.ts create mode 100644 packages/repository/src/relations/has-and-belongs-to-many/index.ts diff --git a/packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many-repository.factory.ts b/packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many-repository.factory.ts new file mode 100644 index 000000000000..1460f5920425 --- /dev/null +++ b/packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many-repository.factory.ts @@ -0,0 +1,109 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-todo +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as debugFactory from 'debug'; +import {camelCase} from 'lodash'; +import {DataObject} from '../../common-types'; +import {InvalidRelationError} from '../../errors'; +import {Entity} from '../../model'; +import {EntityCrudRepository} from '../../repositories/repository'; +import {isTypeResolver} from '../../type-resolver'; +import {Getter, HasAndBelongsToManyDefinition} from '../relation.types'; +import { + DefaultHasAndBelongsToManyRepository, + HasAndBelongsToManyRepository, +} from './has-and-belongs-to-many.repository'; + +const debug = debugFactory('loopback:repository:has-many-repository-factory'); + +export type HasAndBelongsToManyRepositoryFactory< + Target extends Entity, + ForeignKeyType +> = (fkValue: ForeignKeyType) => HasAndBelongsToManyRepository; + +/** + * Enforces a constraint on a repository based on a relationship contract + * between models. For example, if a Customer model is related to an Order model + * via a HasAndBelongsToMany relation, then, the relational repository returned by the + * factory function would be constrained by a Customer model instance's id(s). + * + * @param relationMetadata The relation metadata used to describe the + * relationship and determine how to apply the constraint. + * @param targetRepositoryGetter The repository which represents the target model of a + * relation attached to a datasource. + * @returns The factory function which accepts a foreign key value to constrain + * the given target repository + */ +export function createHasAndBelongsToManyRepositoryFactory< + Target extends Entity, + TargetID, + ForeignKeyType +>( + relationMetadata: HasAndBelongsToManyDefinition, + targetRepositoryGetter: Getter>, +): HasAndBelongsToManyRepositoryFactory { + const meta = resolveHasAndBelongsToManyMetadata(relationMetadata); + debug('Resolved HasAndBelongsToMany relation metadata: %o', meta); + return function(fkValue: ForeignKeyType) { + // tslint:disable-next-line:no-any + const constraint: any = {[meta.keyTo]: fkValue}; + return new DefaultHasAndBelongsToManyRepository< + Target, + TargetID, + EntityCrudRepository + >(targetRepositoryGetter, constraint as DataObject); + }; +} + +type HasAndBelongsToManyResolvedDefinition = HasAndBelongsToManyDefinition & { + keyTo: string; +}; + +/** + * Resolves given hasMany metadata if target is specified to be a resolver. + * Mainly used to infer what the `keyTo` property should be from the target's + * belongsTo metadata + * @param relationMeta hasMany metadata to resolve + */ +function resolveHasAndBelongsToManyMetadata( + relationMeta: HasAndBelongsToManyDefinition, +): HasAndBelongsToManyResolvedDefinition { + if (!isTypeResolver(relationMeta.target)) { + const reason = 'target must be a type resolver'; + throw new InvalidRelationError(reason, relationMeta); + } + + if (relationMeta.keyTo) { + // The explict cast is needed because of a limitation of type inference + return relationMeta as HasAndBelongsToManyResolvedDefinition; + } + + const sourceModel = relationMeta.source; + if (!sourceModel || !sourceModel.modelName) { + const reason = 'source model must be defined'; + throw new InvalidRelationError(reason, relationMeta); + } + + const targetModel = relationMeta.target(); + debug( + 'Resolved model %s from given metadata: %o', + targetModel.modelName, + targetModel, + ); + const defaultFkName = camelCase(sourceModel.modelName + '_id'); + const hasDefaultFkProperty = + targetModel.definition && + targetModel.definition.properties && + targetModel.definition.properties[defaultFkName]; + + if (!hasDefaultFkProperty) { + const reason = `target model ${ + targetModel.name + } is missing definition of foreign key ${defaultFkName}`; + throw new InvalidRelationError(reason, relationMeta); + } + + return Object.assign(relationMeta, {keyTo: defaultFkName}); +} diff --git a/packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many.decorator.ts b/packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many.decorator.ts new file mode 100644 index 000000000000..f7930f925289 --- /dev/null +++ b/packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many.decorator.ts @@ -0,0 +1,37 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, EntityResolver} from '../../model'; +import {relation} from '../relation.decorator'; +import {HasAndBelongsToManyDefinition, RelationType} from '../relation.types'; + +/** + * Decorator for hasAndBelongsToMany + * Calls property.array decorator underneath the hood and infers foreign key + * name from target model name unless explicitly specified + * @param targetResolver Target model for hasAndBelongsToMany relation + * @param definition Optional metadata for setting up hasAndBelongsToMany relation + * @returns {(target:any, key:string)} + */ +export function hasAndBelongsToMany( + targetResolver: EntityResolver, + definition?: Partial, +) { + return function(decoratedTarget: Object, key: string) { + const meta: HasAndBelongsToManyDefinition = 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.hasAndBelongsToMany, + source: decoratedTarget.constructor, + target: targetResolver, + }, + ); + relation(meta)(decoratedTarget, key); + }; +} diff --git a/packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many.repository.ts b/packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many.repository.ts new file mode 100644 index 000000000000..f6e4fbf2e2a4 --- /dev/null +++ b/packages/repository/src/relations/has-and-belongs-to-many/has-and-belongs-to-many.repository.ts @@ -0,0 +1,117 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-todo +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter} from '@loopback/context'; +import {Count, DataObject, Options} from '../../common-types'; +import {Entity} from '../../model'; +import {Filter, Where} from '../../query'; +import { + constrainDataObject, + constrainFilter, + constrainWhere, +} from '../../repositories/constraint-utils'; +import {EntityCrudRepository} from '../../repositories/repository'; + +/** + * CRUD operations for a target repository of a HasAndBelongsToMany relation + */ +export interface HasAndBelongsToManyRepository { + /** + * Create a target model instance + * @param targetModelData The target model data + * @param options Options for the operation + * @returns A promise which resolves to the newly created target model instance + */ + create( + targetModelData: DataObject, + options?: Options, + ): Promise; + /** + * Find target model instance(s) + * @param filter A filter object for where, order, limit, etc. + * @param options Options for the operation + * @returns A promise which resolves with the found target instance(s) + */ + find(filter?: Filter, options?: Options): Promise; + /** + * Delete multiple target model instances + * @param where Instances within the where scope are deleted + * @param options + * @returns A promise which resolves the deleted target model instances + */ + delete(where?: Where, options?: Options): Promise; + /** + * Patch multiple target model instances + * @param dataObject The fields and their new values to patch + * @param where Instances within the where scope are patched + * @param options + * @returns A promise which resolves the patched target model instances + */ + patch( + dataObject: DataObject, + where?: Where, + options?: Options, + ): Promise; +} + +export class DefaultHasAndBelongsToManyRepository< + TargetEntity extends Entity, + TargetID, + TargetRepository extends EntityCrudRepository +> implements HasAndBelongsToManyRepository { + /** + * Constructor of DefaultHasAndBelongsToManyEntityCrudRepository + * @param getTargetRepository the getter of the related target model repository instance + * @param constraint the key value pair representing foreign key name to constrain + * the target repository instance + */ + constructor( + public getTargetRepository: Getter, + public constraint: DataObject, + ) {} + + async create( + targetModelData: DataObject, + options?: Options, + ): Promise { + const targetRepository = await this.getTargetRepository(); + return targetRepository.create( + constrainDataObject(targetModelData, this.constraint), + options, + ); + } + + async find( + filter?: Filter, + options?: Options, + ): Promise { + const targetRepository = await this.getTargetRepository(); + return targetRepository.find( + constrainFilter(filter, this.constraint), + options, + ); + } + + async delete(where?: Where, options?: Options): Promise { + const targetRepository = await this.getTargetRepository(); + return targetRepository.deleteAll( + constrainWhere(where, this.constraint as Where), + options, + ); + } + + async patch( + dataObject: DataObject, + where?: Where, + options?: Options, + ): Promise { + const targetRepository = await this.getTargetRepository(); + return targetRepository.updateAll( + constrainDataObject(dataObject, this.constraint), + constrainWhere(where, this.constraint as Where), + options, + ); + } +} diff --git a/packages/repository/src/relations/has-and-belongs-to-many/index.ts b/packages/repository/src/relations/has-and-belongs-to-many/index.ts new file mode 100644 index 000000000000..c36b4040bbc9 --- /dev/null +++ b/packages/repository/src/relations/has-and-belongs-to-many/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './has-and-belongs-to-many.decorator'; +export * from './has-and-belongs-to-many.repository'; +export * from './has-and-belongs-to-many-repository.factory'; diff --git a/packages/repository/src/relations/index.ts b/packages/repository/src/relations/index.ts index b048ad61acb3..4e0ab0332d4f 100644 --- a/packages/repository/src/relations/index.ts +++ b/packages/repository/src/relations/index.ts @@ -8,3 +8,4 @@ export * from './relation.decorator'; export * from './belongs-to'; export * from './has-many'; export * from './has-one'; +export * from './has-and-belongs-to-many'; diff --git a/packages/repository/src/relations/relation.types.ts b/packages/repository/src/relations/relation.types.ts index fb15b5be4f88..5077c39792c1 100644 --- a/packages/repository/src/relations/relation.types.ts +++ b/packages/repository/src/relations/relation.types.ts @@ -8,12 +8,13 @@ import {TypeResolver} from '../type-resolver'; export enum RelationType { belongsTo = 'belongsTo', - hasOne = 'hasOne', - hasMany = 'hasMany', - embedsOne = 'embedsOne', embedsMany = 'embedsMany', - referencesOne = 'referencesOne', + embedsOne = 'embedsOne', + hasAndBelongsToMany = 'hasAndBelongsToMany', + hasMany = 'hasMany', + hasOne = 'hasOne', referencesMany = 'referencesMany', + referencesOne = 'referencesOne', } export interface RelationDefinitionBase { @@ -56,6 +57,19 @@ export interface HasManyDefinition extends RelationDefinitionBase { keyTo?: string; } +export interface HasAndBelongsToManyDefinition extends RelationDefinitionBase { + type: RelationType.hasAndBelongsToMany; + + /** + * The foreign key used by the target model. + * + * E.g. when a Customer has many Order instances, then keyTo is "customerId". + * Note that "customerId" is the default FK assumed by the framework, users + * can provide a custom FK name by setting "keyTo". + */ + keyTo?: string; +} + export interface BelongsToDefinition extends RelationDefinitionBase { type: RelationType.belongsTo; @@ -90,6 +104,7 @@ export type RelationMetadata = | HasManyDefinition | BelongsToDefinition | HasOneDefinition + | HasAndBelongsToManyDefinition // TODO(bajtos) add other relation types and remove RelationDefinitionBase once // all relation types are covered. | RelationDefinitionBase; diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 26e6a5eaf9b8..fd163bffdf86 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -19,15 +19,18 @@ import {EntityNotFoundError} from '../errors'; import {Entity, ModelDefinition} from '../model'; import {Filter, Where} from '../query'; import { + BelongsToAccessor, BelongsToDefinition, + HasAndBelongsToManyDefinition, + HasAndBelongsToManyRepositoryFactory, HasManyDefinition, HasManyRepositoryFactory, - createHasManyRepositoryFactory, - BelongsToAccessor, - createBelongsToAccessor, - createHasOneRepositoryFactory, HasOneDefinition, HasOneRepositoryFactory, + createBelongsToAccessor, + createHasAndBelongsToManyRepositoryFactory, + createHasManyRepositoryFactory, + createHasOneRepositoryFactory, } from '../relations'; import {resolveType} from '../type-resolver'; import {EntityCrudRepository} from './repository'; @@ -210,6 +213,71 @@ export class DefaultCrudRepository ); } + /** + * @deprecated + * Function to create a constrained relation repository factory + * + * Use `this.createHasAndBelongsToManyRepositoryFactoryFor()` instaed + * + * @param relationName Name of the relation defined on the source model + * @param targetRepo Target repository instance + */ + protected _createHasAndBelongsToManyRepositoryFactoryFor< + Target extends Entity, + TargetID, + ForeignKeyType + >( + relationName: string, + targetRepoGetter: Getter>, + ): HasAndBelongsToManyRepositoryFactory { + return this.createHasAndBelongsToManyRepositoryFactoryFor( + relationName, + targetRepoGetter, + ); + } + + /** + * Function to create a constrained relation repository factory + * + * ```ts + * class CustomerRepository extends DefaultCrudRepository< + * Customer, + * typeof Customer.prototype.id + * > { + * public readonly orders: HasAndBelongsToManyRepositoryFactory; + * + * constructor( + * protected db: juggler.DataSource, + * orderRepository: EntityCrudRepository, + * ) { + * super(Customer, db); + * this.orders = this._createHasAndBelongsToManyRepositoryFactoryFor( + * 'orders', + * orderRepository, + * ); + * } + * } + * ``` + * + * @param relationName Name of the relation defined on the source model + * @param targetRepo Target repository instance + */ + protected createHasAndBelongsToManyRepositoryFactoryFor< + Target extends Entity, + TargetID, + ForeignKeyType + >( + relationName: string, + targetRepoGetter: Getter>, + ): HasAndBelongsToManyRepositoryFactory { + const meta = this.entityClass.definition.relations[relationName]; + return createHasAndBelongsToManyRepositoryFactory< + Target, + TargetID, + ForeignKeyType + >(meta as HasAndBelongsToManyDefinition, targetRepoGetter); + } + /** * @deprecated * Function to create a belongs to accessor