Skip to content

Commit

Permalink
feat(repository): add keyFrom to resolved relation metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
nabdelgadir committed Sep 14, 2019
1 parent 7752b08 commit 62f803e
Show file tree
Hide file tree
Showing 4 changed files with 352 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright IBM Corp. 2019. 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 {expect} from '@loopback/testlab';
import {
Entity,
HasManyDefinition,
ModelDefinition,
RelationType,
} from '../../..';
import {resolveHasManyMetadata} from '../../../relations/has-many/has-many.helpers';

describe('keyTo and keyFrom with resolveHasManyMetadata', () => {
it('resolves metadata using keyTo and keyFrom', () => {
const meta = resolveHasManyMetadata(Category.definition.relations[
'products'
] as HasManyDefinition);

expect(meta).to.eql({
name: 'products',
type: 'hasMany',
targetsMany: true,
source: Category,
keyFrom: 'id',
target: () => Product,
keyTo: 'categoryId',
});
});

it('resolves metadata using keyTo, but not keyFrom', () => {
const meta = resolveHasManyMetadata(Category.definition.relations[
'items'
] as HasManyDefinition);

expect(meta).to.not.have.property('keyFrom');

expect(meta).to.eql({
name: 'items',
type: 'hasMany',
targetsMany: true,
source: Category,
target: () => Item,
keyTo: 'categoryId',
});
});

it('infers keyTo if is it not provided', () => {
const meta = resolveHasManyMetadata(Category.definition.relations[
'things'
] as HasManyDefinition);

expect(meta).to.eql({
name: 'things',
type: 'hasMany',
targetsMany: true,
source: Category,
keyFrom: 'id',
target: () => Thing,
keyTo: 'categoryId',
});
});

it('throws if both keyFrom and keyTo are not provided', async () => {
let error;

try {
resolveHasManyMetadata(Category.definition.relations[
'categories'
] as HasManyDefinition);
} catch (err) {
error = err;
}

expect(error.message).to.eql(
'Invalid hasMany definition for Category#categories: target model ' +
'Category is missing definition of foreign key categoryId',
);

expect(error.code).to.eql('INVALID_RELATION_DEFINITION');
});

/****** HELPERS *******/

class Category extends Entity {}

Category.definition = new ModelDefinition('Category')
.addProperty('id', {type: 'number', id: true, required: true})
.addRelation(<HasManyDefinition>{
name: 'products',
type: RelationType.hasMany,
targetsMany: true,

source: Category,
keyFrom: 'id',

target: () => Product,
keyTo: 'categoryId',
})
.addRelation(<HasManyDefinition>{
name: 'items',
type: RelationType.hasMany,
targetsMany: true,

source: Category,
// no keyFrom

target: () => Item,
keyTo: 'categoryId',
})
.addRelation(<HasManyDefinition>{
name: 'things',
type: RelationType.hasMany,
targetsMany: true,

source: Category,
keyFrom: 'id',

target: () => Thing,
// no keyTo
})
.addRelation(<HasManyDefinition>{
name: 'categories',
type: RelationType.hasMany,
targetsMany: true,

source: Category,
// no keyFrom

target: () => Category,
// no keyTo
});

class Product extends Entity {}

Product.definition = new ModelDefinition('Product')
.addProperty('id', {
type: 'number',
id: true,
required: true,
})
.addProperty('categoryId', {type: 'number'});

class Item extends Entity {}

Item.definition = new ModelDefinition('Item')
.addProperty('id', {
type: 'number',
id: true,
required: true,
})
.addProperty('categoryId', {type: 'number'});

class Thing extends Entity {}

Thing.definition = new ModelDefinition('Thing')
.addProperty('id', {
type: 'number',
id: true,
required: true,
})
.addProperty('categoryId', {type: 'number'});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright IBM Corp. 2019. 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 {expect} from '@loopback/testlab';
import {
Entity,
HasOneDefinition,
ModelDefinition,
RelationType,
} from '../../..';
import {resolveHasOneMetadata} from '../../../relations/has-one/has-one.helpers';

describe('keyTo and keyFrom with resolveHasOneMetadata', () => {
it('resolves metadata using keyTo and keyFrom', () => {
const meta = resolveHasOneMetadata(Category.definition.relations[
'product'
] as HasOneDefinition);

expect(meta).to.eql({
name: 'product',
type: 'hasOne',
targetsMany: false,
source: Category,
keyFrom: 'id',
target: () => Product,
keyTo: 'categoryId',
});
});

it('resolves metadata using keyTo, but not keyFrom', () => {
const meta = resolveHasOneMetadata(Category.definition.relations[
'item'
] as HasOneDefinition);

expect(meta).to.not.have.property('keyFrom');

expect(meta).to.eql({
name: 'item',
type: 'hasOne',
targetsMany: false,
source: Category,
target: () => Item,
keyTo: 'categoryId',
});
});

it('infers keyTo if is it not provided', () => {
const meta = resolveHasOneMetadata(Category.definition.relations[
'thing'
] as HasOneDefinition);

expect(meta).to.eql({
name: 'thing',
type: 'hasOne',
targetsMany: false,
source: Category,
keyFrom: 'id',
target: () => Thing,
keyTo: 'categoryId',
});
});

it('throws if both keyFrom and keyTo are not provided', async () => {
let error;

try {
resolveHasOneMetadata(Category.definition.relations[
'category'
] as HasOneDefinition);
} catch (err) {
error = err;
}

expect(error.message).to.eql(
'Invalid hasOne definition for Category#category: target model Category' +
' is missing definition of foreign key categoryId',
);

expect(error.code).to.eql('INVALID_RELATION_DEFINITION');
});

/****** HELPERS *******/

class Category extends Entity {}

Category.definition = new ModelDefinition('Category')
.addProperty('id', {type: 'number', id: true, required: true})
.addRelation(<HasOneDefinition>{
name: 'product',
type: RelationType.hasOne,
targetsMany: false,

source: Category,
keyFrom: 'id',

target: () => Product,
keyTo: 'categoryId',
})
.addRelation(<HasOneDefinition>{
name: 'item',
type: RelationType.hasOne,
targetsMany: false,

source: Category,
// no keyFrom

target: () => Item,
keyTo: 'categoryId',
})
.addRelation(<HasOneDefinition>{
name: 'thing',
type: RelationType.hasOne,
targetsMany: false,

source: Category,
keyFrom: 'id',

target: () => Thing,
// no keyTo
})
.addRelation(<HasOneDefinition>{
name: 'category',
type: RelationType.hasOne,
targetsMany: false,

source: Category,
// no keyFrom

target: () => Category,
// no keyTo
});

class Product extends Entity {}

Product.definition = new ModelDefinition('Product')
.addProperty('id', {
type: 'number',
id: true,
required: true,
})
.addProperty('categoryId', {type: 'number'});

class Item extends Entity {}

Item.definition = new ModelDefinition('Item')
.addProperty('id', {
type: 'number',
id: true,
required: true,
})
.addProperty('categoryId', {type: 'number'});

class Thing extends Entity {}

Thing.definition = new ModelDefinition('Thing')
.addProperty('id', {
type: 'number',
id: true,
required: true,
})
.addProperty('categoryId', {type: 'number'});
});
14 changes: 12 additions & 2 deletions packages/repository/src/relations/has-many/has-many.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ const debug = debugFactory('loopback:repository:has-many-helpers');
* Relation definition with optional metadata (e.g. `keyTo`) filled in.
* @internal
*/
export type HasManyResolvedDefinition = HasManyDefinition & {keyTo: string};
export type HasManyResolvedDefinition = HasManyDefinition & {
keyFrom: string;
keyTo: string;
};

/**
* Resolves given hasMany metadata if target is specified to be a resolver.
Expand Down Expand Up @@ -49,6 +52,13 @@ export function resolveHasManyMetadata(
throw new InvalidRelationError(reason, relationMeta);
}

const keyFrom = sourceModel.getIdProperties()[0];

if (relationMeta.keyTo) {
// The explict cast is needed because of a limitation of type inference
return Object.assign(relationMeta, {keyFrom}) as HasManyResolvedDefinition;
}

debug(
'Resolved model %s from given metadata: %o',
targetModel.modelName,
Expand All @@ -62,5 +72,5 @@ export function resolveHasManyMetadata(
throw new InvalidRelationError(reason, relationMeta);
}

return Object.assign(relationMeta, {keyTo: defaultFkName});
return Object.assign(relationMeta, {keyFrom, keyTo: defaultFkName});
}
14 changes: 12 additions & 2 deletions packages/repository/src/relations/has-one/has-one.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ const debug = debugFactory('loopback:repository:has-one-helpers');
* Relation definition with optional metadata (e.g. `keyTo`) filled in.
* @internal
*/
export type HasOneResolvedDefinition = HasOneDefinition & {keyTo: string};
export type HasOneResolvedDefinition = HasOneDefinition & {
keyFrom: string;
keyTo: string;
};

/**
* Resolves given hasOne metadata if target is specified to be a resolver.
Expand Down Expand Up @@ -49,6 +52,13 @@ export function resolveHasOneMetadata(
throw new InvalidRelationError(reason, relationMeta);
}

const keyFrom = sourceModel.getIdProperties()[0];

if (relationMeta.keyTo) {
// The explict cast is needed because of a limitation of type inference
return Object.assign(relationMeta, {keyFrom}) as HasOneResolvedDefinition;
}

debug(
'Resolved model %s from given metadata: %o',
targetModel.modelName,
Expand All @@ -62,5 +72,5 @@ export function resolveHasOneMetadata(
throw new InvalidRelationError(reason, relationMeta);
}

return Object.assign(relationMeta, {keyTo: defaultFkName});
return Object.assign(relationMeta, {keyFrom, keyTo: defaultFkName});
}

0 comments on commit 62f803e

Please sign in to comment.