Skip to content

Commit

Permalink
Merge 89633ea into a4c2086
Browse files Browse the repository at this point in the history
  • Loading branch information
clayrisser authored Jan 31, 2019
2 parents a4c2086 + 89633ea commit eef909d
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -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<Target>;

/**
* 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<EntityCrudRepository<Target, TargetID>>,
): HasAndBelongsToManyRepositoryFactory<Target, ForeignKeyType> {
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<Target, TargetID>
>(targetRepositoryGetter, constraint as DataObject<Target>);
};
}

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});
}
Original file line number Diff line number Diff line change
@@ -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<T extends Entity>(
targetResolver: EntityResolver<T>,
definition?: Partial<HasAndBelongsToManyDefinition>,
) {
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);
};
}
Original file line number Diff line number Diff line change
@@ -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<Target extends Entity> {
/**
* 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<Target>,
options?: Options,
): Promise<Target>;
/**
* 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<Target>, options?: Options): Promise<Target[]>;
/**
* 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<Target>, options?: Options): Promise<Count>;
/**
* 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<Target>,
where?: Where<Target>,
options?: Options,
): Promise<Count>;
}

export class DefaultHasAndBelongsToManyRepository<
TargetEntity extends Entity,
TargetID,
TargetRepository extends EntityCrudRepository<TargetEntity, TargetID>
> implements HasAndBelongsToManyRepository<TargetEntity> {
/**
* 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<TargetRepository>,
public constraint: DataObject<TargetEntity>,
) {}

async create(
targetModelData: DataObject<TargetEntity>,
options?: Options,
): Promise<TargetEntity> {
const targetRepository = await this.getTargetRepository();
return targetRepository.create(
constrainDataObject(targetModelData, this.constraint),
options,
);
}

async find(
filter?: Filter<TargetEntity>,
options?: Options,
): Promise<TargetEntity[]> {
const targetRepository = await this.getTargetRepository();
return targetRepository.find(
constrainFilter(filter, this.constraint),
options,
);
}

async delete(where?: Where<TargetEntity>, options?: Options): Promise<Count> {
const targetRepository = await this.getTargetRepository();
return targetRepository.deleteAll(
constrainWhere(where, this.constraint as Where<TargetEntity>),
options,
);
}

async patch(
dataObject: DataObject<TargetEntity>,
where?: Where<TargetEntity>,
options?: Options,
): Promise<Count> {
const targetRepository = await this.getTargetRepository();
return targetRepository.updateAll(
constrainDataObject(dataObject, this.constraint),
constrainWhere(where, this.constraint as Where<TargetEntity>),
options,
);
}
}
Original file line number Diff line number Diff line change
@@ -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';
1 change: 1 addition & 0 deletions packages/repository/src/relations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
23 changes: 19 additions & 4 deletions packages/repository/src/relations/relation.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit eef909d

Please sign in to comment.