Skip to content

Commit

Permalink
Merge c7bed9e into 596a143
Browse files Browse the repository at this point in the history
  • Loading branch information
clayrisser committed Feb 9, 2019
2 parents 596a143 + c7bed9e commit 93fd33b
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ describe('relation repository', () => {

context('DefaultHasManyEntityCrudRepository', () => {
it('can create related model instance', async () => {
const constraint: Partial<Customer> = {age: 25};
const hasManyCrudInstance = givenDefaultHasManyInstance(constraint);
const getConstraint: () => Promise<Partial<Customer>> = async () => ({
age: 25,
});
const hasManyCrudInstance = givenDefaultHasManyInstance(getConstraint);
await hasManyCrudInstance.create({id: 1, name: 'Joe'});
sinon.assert.calledWithMatch(customerRepo.stubs.create, {
id: 1,
Expand All @@ -80,8 +82,10 @@ describe('relation repository', () => {
});

it('can find related model instance', async () => {
const constraint: Partial<Customer> = {name: 'Jane'};
const hasManyCrudInstance = givenDefaultHasManyInstance(constraint);
const getConstraint: () => Promise<Partial<Customer>> = async () => ({
name: 'Jane',
});
const hasManyCrudInstance = givenDefaultHasManyInstance(getConstraint);
await hasManyCrudInstance.find({where: {id: 3}});
sinon.assert.calledWithMatch(customerRepo.stubs.find, {
where: {id: 3, name: 'Jane'},
Expand All @@ -90,8 +94,10 @@ describe('relation repository', () => {

context('patch', () => {
it('can patch related model instance', async () => {
const constraint: Partial<Customer> = {name: 'Jane'};
const hasManyCrudInstance = givenDefaultHasManyInstance(constraint);
const getConstraint: () => Promise<Partial<Customer>> = async () => ({
name: 'Jane',
});
const hasManyCrudInstance = givenDefaultHasManyInstance(getConstraint);
await hasManyCrudInstance.patch({country: 'US'}, {id: 3});
sinon.assert.calledWith(
customerRepo.stubs.updateAll,
Expand All @@ -101,17 +107,21 @@ describe('relation repository', () => {
});

it('cannot override the constrain data', async () => {
const constraint: Partial<Customer> = {name: 'Jane'};
const hasManyCrudInstance = givenDefaultHasManyInstance(constraint);
const getConstraint: () => Promise<Partial<Customer>> = async () => ({
name: 'Jane',
});
const hasManyCrudInstance = givenDefaultHasManyInstance(getConstraint);
await expect(
hasManyCrudInstance.patch({name: 'Joe'}),
).to.be.rejectedWith(/Property "name" cannot be changed!/);
});
});

it('can delete related model instance', async () => {
const constraint: Partial<Customer> = {name: 'Jane'};
const hasManyCrudInstance = givenDefaultHasManyInstance(constraint);
const getConstraint: () => Promise<Partial<Customer>> = async () => ({
name: 'Jane',
});
const hasManyCrudInstance = givenDefaultHasManyInstance(getConstraint);
await hasManyCrudInstance.delete({id: 3});
sinon.assert.calledWith(customerRepo.stubs.deleteAll, {
id: 3,
Expand Down Expand Up @@ -142,11 +152,13 @@ describe('relation repository', () => {
customerRepo = createStubInstance(CustomerRepository);
}

function givenDefaultHasManyInstance(constraint: DataObject<Customer>) {
function givenDefaultHasManyInstance(
getConstraint: () => Promise<DataObject<Customer>>,
) {
return new DefaultHasManyRepository<
Customer,
typeof Customer.prototype.id,
CustomerRepository
>(Getter.fromValue(customerRepo), constraint);
>(Getter.fromValue(customerRepo), getConstraint);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
import * as debugFactory from 'debug';
import {camelCase} from 'lodash';
import {DataObject} from '../../common-types';
import {InvalidRelationError} from '../../errors';
import {EntityNotFoundError, InvalidRelationError} from '../../errors';
import {Entity} from '../../model';
import {EntityCrudRepository} from '../../repositories/repository';
import {constrainFilter} from '../../repositories/constraint-utils';
import {isTypeResolver} from '../../type-resolver';
import {Getter, HasManyDefinition} from '../relation.types';
import {
Expand All @@ -22,6 +23,8 @@ export type HasManyRepositoryFactory<Target extends Entity, ForeignKeyType> = (
fkValue: ForeignKeyType,
) => HasManyRepository<Target>;

export interface IThrough extends Entity {}

/**
* 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
Expand All @@ -38,21 +41,42 @@ export type HasManyRepositoryFactory<Target extends Entity, ForeignKeyType> = (
export function createHasManyRepositoryFactory<
Target extends Entity,
TargetID,
ForeignKeyType
ForeignKeyType,
Through = IThrough,
ThroughID = string
>(
relationMetadata: HasManyDefinition,
targetRepositoryGetter: Getter<EntityCrudRepository<Target, TargetID>>,
throughRepositoryGetter?: Getter<EntityCrudRepository<IThrough, ThroughID>>,
): HasManyRepositoryFactory<Target, ForeignKeyType> {
const meta = resolveHasManyMetadata(relationMetadata);
debug('Resolved HasMany relation metadata: %o', meta);
return function(fkValue: ForeignKeyType) {
// tslint:disable-next-line:no-any
const constraint: any = {[meta.keyTo]: fkValue};
async function getConstraint(): Promise<DataObject<Target>> {
// tslint:disable-next-line:no-any
let constraint: any = {[meta.keyTo]: fkValue};
if (meta.targetFkName && throughRepositoryGetter) {
const throughRepo = await throughRepositoryGetter();
const throughInstances = await throughRepo.find(
constrainFilter(undefined, constraint),
);
if (!throughInstances.length) {
const id = 'through constraint ' + JSON.stringify(constraint);
throw new EntityNotFoundError(throughRepo.entityClass, id);
}
constraint = {
or: throughInstances.map((throughInstance: IThrough) => {
return {id: throughInstance[meta.targetFkName as keyof IThrough]};
}),
};
}
return constraint as DataObject<Target>;
}
return new DefaultHasManyRepository<
Target,
TargetID,
EntityCrudRepository<Target, TargetID>
>(targetRepositoryGetter, constraint as DataObject<Target>);
>(targetRepositoryGetter, getConstraint);
};
}

Expand Down Expand Up @@ -89,14 +113,42 @@ function resolveHasManyMetadata(
targetModel.modelName,
targetModel,
);
let throughModel = null;

if (relationMeta.through) {
if (!isTypeResolver(relationMeta.through)) {
const reason = 'through must be a type resolver';
throw new InvalidRelationError(reason, relationMeta);
}
throughModel = relationMeta.through();
debug(
'Resolved model %s from given metadata: %o',
throughModel.modelName,
throughModel,
);
const targetFkName = camelCase(targetModel.modelName + '_id');
const hasTargetFkName =
throughModel.definition &&
throughModel.definition.properties &&
throughModel.definition.properties[targetFkName];
if (!hasTargetFkName) {
const reason = `target model ${
throughModel.name
} is missing definition of target foreign key ${targetFkName}`;
throw new InvalidRelationError(reason, relationMeta);
}
Object.assign(relationMeta, {targetFkName});
}

const defaultFkName = camelCase(sourceModel.modelName + '_id');
const modelWithFkProperty = throughModel || targetModel;
const hasDefaultFkProperty =
targetModel.definition &&
targetModel.definition.properties &&
targetModel.definition.properties[defaultFkName];
modelWithFkProperty.definition &&
modelWithFkProperty.definition.properties &&
modelWithFkProperty.definition.properties[defaultFkName];

if (!hasDefaultFkProperty) {
const reason = `target model ${
const reason = ` ${throughModel ? 'through' : 'target'} model ${
targetModel.name
} is missing definition of foreign key ${defaultFkName}`;
throw new InvalidRelationError(reason, relationMeta);
Expand Down
16 changes: 10 additions & 6 deletions packages/repository/src/relations/has-many/has-many.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class DefaultHasManyRepository<
*/
constructor(
public getTargetRepository: Getter<TargetRepository>,
public constraint: DataObject<TargetEntity>,
public getConstraint: () => Promise<DataObject<TargetEntity>>,
) {}

async create(
Expand All @@ -78,7 +78,7 @@ export class DefaultHasManyRepository<
): Promise<TargetEntity> {
const targetRepository = await this.getTargetRepository();
return targetRepository.create(
constrainDataObject(targetModelData, this.constraint),
constrainDataObject(targetModelData, await this.getConstraint()),
options,
);
}
Expand All @@ -89,15 +89,17 @@ export class DefaultHasManyRepository<
): Promise<TargetEntity[]> {
const targetRepository = await this.getTargetRepository();
return targetRepository.find(
constrainFilter(filter, this.constraint),
constrainFilter(filter, await this.getConstraint()),
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>),
constrainWhere(where, (await this.getConstraint()) as Where<
TargetEntity
>),
options,
);
}
Expand All @@ -109,8 +111,10 @@ export class DefaultHasManyRepository<
): Promise<Count> {
const targetRepository = await this.getTargetRepository();
return targetRepository.updateAll(
constrainDataObject(dataObject, this.constraint),
constrainWhere(where, this.constraint as Where<TargetEntity>),
constrainDataObject(dataObject, await this.getConstraint()),
constrainWhere(where, (await this.getConstraint()) as Where<
TargetEntity
>),
options,
);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/repository/src/relations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './relation.types';
export * from './relation.decorator';
export * from './belongs-to';
export * from './has-many';
export * from './has-one';
export * from './relation.decorator';
export * from './relation.types';
24 changes: 18 additions & 6 deletions packages/repository/src/relations/relation.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import {TypeResolver} from '../type-resolver';

export enum RelationType {
belongsTo = 'belongsTo',
hasOne = 'hasOne',
hasMany = 'hasMany',
embedsOne = 'embedsOne',
embedsMany = 'embedsMany',
referencesOne = 'referencesOne',
embedsOne = 'embedsOne',
hasMany = 'hasMany',
hasOne = 'hasOne',
referencesMany = 'referencesMany',
referencesOne = 'referencesOne',
}

export interface RelationDefinitionBase {
Expand Down Expand Up @@ -47,13 +47,25 @@ export interface HasManyDefinition extends RelationDefinitionBase {
type: RelationType.hasMany;

/**
* The foreign key used by the target model.
* The through model of this relation.
*
* E.g. when a Customer has many Order instances and a Seller has many Order instances, then Order is through.
*/
through?: TypeResolver<Entity, typeof Entity>;

/**
* The foreign key used by the target or through 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;

/**
* The foreign key used by the target model when using a through model.
*/
targetFkName?: string;
}

export interface BelongsToDefinition extends RelationDefinitionBase {
Expand Down Expand Up @@ -87,8 +99,8 @@ export interface HasOneDefinition extends RelationDefinitionBase {
* A union type describing all possible Relation metadata objects.
*/
export type RelationMetadata =
| HasManyDefinition
| BelongsToDefinition
| HasManyDefinition
| HasOneDefinition
// TODO(bajtos) add other relation types and remove RelationDefinitionBase once
// all relation types are covered.
Expand Down
9 changes: 5 additions & 4 deletions packages/repository/src/repositories/legacy-juggler-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ import {EntityNotFoundError} from '../errors';
import {Entity, ModelDefinition} from '../model';
import {Filter, Where} from '../query';
import {
BelongsToAccessor,
BelongsToDefinition,
HasManyDefinition,
HasManyRepositoryFactory,
createHasManyRepositoryFactory,
BelongsToAccessor,
createBelongsToAccessor,
createHasOneRepositoryFactory,
HasOneDefinition,
HasOneRepositoryFactory,
createBelongsToAccessor,
createHasManyRepositoryFactory,
createHasOneRepositoryFactory,
} from '../relations';

import {resolveType} from '../type-resolver';
import {EntityCrudRepository} from './repository';

Expand Down

0 comments on commit 93fd33b

Please sign in to comment.