Skip to content

Commit b267f3c

Browse files
biniamshimks
authored andcommitted
feat(repository): introduce hasmany relation decorator inference
- Implement `hasMany` decorator which takes the target model class and infers the relation metadata and return type as an array of the target model instances - store relation metadata on model definition - modify the function returned by `hasManyRepositoryFactory` function to only take in value of the PK/FK constraint instead of a key value pair for the PK (`{id: 5}` -> `5`} - create a protected function in DefaultEntityCrudRepository which calls `hasManyRepositoryFactory` using the metadata stored by `hasMany` decorator on the source model definition and returns a constrained version of the target repository Co-authored-by: Kyu Shim <kyu.shim@ibm.com>
1 parent fb97f01 commit b267f3c

13 files changed

+741
-139
lines changed

packages/repository/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@
2525
"@loopback/build": "^0.6.11",
2626
"@loopback/testlab": "^0.10.10",
2727
"@types/lodash": "^4.14.108",
28-
"@types/node": "^10.1.1",
29-
"lodash": "^4.17.10"
28+
"@types/node": "^10.1.1"
3029
},
3130
"dependencies": {
3231
"@loopback/context": "^0.11.9",
3332
"@loopback/core": "^0.10.1",
3433
"@loopback/dist-util": "^0.3.3",
34+
"lodash": "^4.17.10",
3535
"loopback-datasource-juggler": "^3.20.2"
3636
},
3737
"files": [

packages/repository/src/decorators/model.decorator.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ModelDefinitionSyntax,
1616
PropertyDefinition,
1717
} from '../model';
18+
import {RELATIONS_KEY, RelationDefinitionBase} from './relation.decorator';
1819

1920
export const MODEL_KEY = MetadataAccessor.create<
2021
Partial<ModelDefinitionSyntax>,
@@ -30,6 +31,7 @@ export const MODEL_WITH_PROPERTIES_KEY = MetadataAccessor.create<
3031
>('loopback:model-and-properties');
3132

3233
export type PropertyMap = MetadataMap<PropertyDefinition>;
34+
export type RelationMap = MetadataMap<RelationDefinitionBase>;
3335

3436
// tslint:disable:no-any
3537

@@ -73,6 +75,13 @@ export function model(definition?: Partial<ModelDefinitionSyntax>) {
7375
}
7476

7577
target.definition = modelDef;
78+
79+
const relationMap: RelationMap =
80+
MetadataInspector.getAllPropertyMetadata(
81+
RELATIONS_KEY,
82+
target.prototype,
83+
) || {};
84+
target.definition.relations = relationMap;
7685
};
7786
}
7887

packages/repository/src/decorators/relation.decorator.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55

66
import {Class} from '../common-types';
77
import {Entity} from '../model';
8-
98
import {PropertyDecoratorFactory} from '@loopback/context';
9+
import {property} from './model.decorator';
10+
import {camelCase} from 'lodash';
1011

1112
// tslint:disable:no-any
1213

@@ -28,6 +29,15 @@ export class RelationMetadata {
2829
as: string;
2930
}
3031

32+
export interface RelationDefinitionBase {
33+
type: RelationType;
34+
}
35+
36+
export interface HasManyDefinition extends RelationDefinitionBase {
37+
type: RelationType.hasMany;
38+
keyTo: string;
39+
}
40+
3141
/**
3242
* Decorator for relations
3343
* @param definition
@@ -61,12 +71,44 @@ export function hasOne(definition?: Object) {
6171

6272
/**
6373
* Decorator for hasMany
64-
* @param definition
74+
* Calls property.array decorator underneath the hood and infers foreign key
75+
* name from target model name unless explicitly specified
76+
* @param targetModel Target model for hasMany relation
77+
* @param definition Optional metadata for setting up hasMany relation
6578
* @returns {(target:any, key:string)}
6679
*/
67-
export function hasMany(definition?: Object) {
68-
const rel = Object.assign({type: RelationType.hasMany}, definition);
69-
return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel);
80+
export function hasMany<T extends typeof Entity>(
81+
targetModel: T,
82+
definition?: Partial<HasManyDefinition>,
83+
) {
84+
// todo(shimks): extract out common logic (such as @property.array) to
85+
// @relation
86+
return function(target: Object, key: string) {
87+
property.array(targetModel)(target, key);
88+
89+
const defaultFkName = camelCase(target.constructor.name + '_id');
90+
const hasKeyTo = definition && definition.keyTo;
91+
const hasDefaultFkProperty =
92+
targetModel.definition &&
93+
targetModel.definition.properties &&
94+
targetModel.definition.properties[defaultFkName];
95+
if (!(hasKeyTo || hasDefaultFkProperty)) {
96+
// note(shimks): should we also check for the existence of explicitly
97+
// given foreign key name on the juggler definition?
98+
throw new Error(
99+
`foreign key ${defaultFkName} not found on ${
100+
targetModel.name
101+
} model's juggler definition`,
102+
);
103+
}
104+
const meta = {keyTo: defaultFkName};
105+
Object.assign(meta, definition, {type: RelationType.hasMany});
106+
107+
PropertyDecoratorFactory.createDecorator(
108+
RELATIONS_KEY,
109+
meta as HasManyDefinition,
110+
)(target, key);
111+
};
70112
}
71113

72114
/**

packages/repository/src/model.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import {Options, AnyObject, DataObject} from './common-types';
77
import {Type} from './types';
8+
import {RelationDefinitionBase} from './decorators/relation.decorator';
89

910
/**
1011
* This module defines the key classes representing building blocks for Domain
@@ -45,6 +46,7 @@ export interface ModelDefinitionSyntax {
4546
name: string;
4647
properties?: {[name: string]: PropertyDefinition | PropertyType};
4748
settings?: {[name: string]: any};
49+
relations?: {[name: string]: RelationDefinitionBase};
4850
[attribute: string]: any;
4951
}
5052

@@ -55,14 +57,15 @@ export class ModelDefinition {
5557
readonly name: string;
5658
properties: {[name: string]: PropertyDefinition};
5759
settings: {[name: string]: any};
60+
relations: {[name: string]: RelationDefinitionBase};
5861
// indexes: Map<string, any>;
5962
[attribute: string]: any; // Other attributes
6063

6164
constructor(nameOrDef: string | ModelDefinitionSyntax) {
6265
if (typeof nameOrDef === 'string') {
6366
nameOrDef = {name: nameOrDef};
6467
}
65-
const {name, properties, settings} = nameOrDef;
68+
const {name, properties, settings, relations} = nameOrDef;
6669

6770
this.name = name;
6871

@@ -74,6 +77,7 @@ export class ModelDefinition {
7477
}
7578

7679
this.settings = settings || new Map();
80+
this.relations = relations || {};
7781
}
7882

7983
/**

packages/repository/src/repositories/legacy-juggler-bridge.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import {
1818
import {Entity, ModelDefinition} from '../model';
1919
import {Filter, Where} from '../query';
2020
import {EntityCrudRepository} from './repository';
21+
import {createHasManyRepositoryFactory} from './relation.factory';
22+
import {HasManyDefinition} from '../decorators/relation.decorator';
23+
// need the import for exporting of a return type
24+
// tslint:disable-next-line:no-unused-variable
25+
import {HasManyRepository} from './relation.repository';
2126

2227
export namespace juggler {
2328
export import DataSource = legacy.DataSource;
@@ -117,6 +122,43 @@ export class DefaultCrudRepository<T extends Entity, ID>
117122
this.modelClass.attachTo(dataSource);
118123
}
119124

125+
/**
126+
* Function to create a constrained relation repository factory
127+
*
128+
* ```ts
129+
* class CustomerRepository extends DefaultCrudRepository<
130+
* Customer,
131+
* typeof Customer.prototype.id
132+
* > {
133+
* public orders: HasManyRepositoryFactory<Order, typeof Order.prototype.id>;
134+
*
135+
* constructor(
136+
* protected db: juggler.DataSource,
137+
* orderRepository: EntityCrudRepository<Order, typeof Order.prototype.id>,
138+
* ) {
139+
* super(Customer, db);
140+
* this.orders = this._createHasManyRepositoryFactoryFor(
141+
* 'orders',
142+
* orderRepository,
143+
* );
144+
* }
145+
* }
146+
* ```
147+
*
148+
* @param relationName Name of the relation defined on the source model
149+
* @param targetRepo Target repository instance
150+
*/
151+
protected _createHasManyRepositoryFactoryFor<Target extends Entity>(
152+
relationName: string,
153+
targetRepo: EntityCrudRepository<Target, typeof Entity.prototype.id>,
154+
) {
155+
const meta = this.entityClass.definition.relations[relationName];
156+
return createHasManyRepositoryFactory(
157+
meta as HasManyDefinition,
158+
targetRepo,
159+
);
160+
}
161+
120162
async create(entity: Partial<T>, options?: Options): Promise<T> {
121163
const model = await ensurePromise(this.modelClass.create(entity, options));
122164
return this.toEntity(model);

packages/repository/src/repositories/relation.factory.ts

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,45 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {EntityCrudRepository} from './repository';
7-
import {RelationType} from '../decorators/relation.decorator';
7+
import {HasManyDefinition} from '../decorators/relation.decorator';
88
import {Entity} from '../model';
99
import {
10-
HasManyEntityCrudRepository,
10+
HasManyRepository,
1111
DefaultHasManyEntityCrudRepository,
1212
} from './relation.repository';
1313

14-
export interface RelationDefinitionBase {
15-
type: RelationType;
16-
keyTo: string;
17-
}
14+
export type HasManyRepositoryFactory<T extends Entity, ID> = (
15+
fkValue: ID,
16+
) => HasManyRepository<T>;
1817

19-
export interface HasManyDefinition extends RelationDefinitionBase {
20-
type: RelationType.hasMany;
21-
}
2218
/**
2319
* Enforces a constraint on a repository based on a relationship contract
24-
* between models. Returns a relational repository that exposes applicable CRUD
25-
* method APIs for the related target repository. For example, if a Customer model is
26-
* related to an Order model via a HasMany relation, then, the relational
27-
* repository returned by this method would be constrained by a Customer model
28-
* instance's id(s).
20+
* between models. For example, if a Customer model is related to an Order model
21+
* via a HasMany relation, then, the relational repository returned by the
22+
* factory function would be constrained by a Customer model instance's id(s).
2923
*
30-
* @param constraint The constraint to apply to the target repository. For
31-
* example, {id: '5'}.
32-
* @param relationMetadata The relation metadata used to used to describe the
24+
* @param relationMeta The relation metadata used to describe the
3325
* relationship and determine how to apply the constraint.
34-
* @param targetRepository The repository which represents the target model of a
26+
* @param targetRepo The repository which represents the target model of a
3527
* relation attached to a datasource.
36-
*
28+
* @returns The factory function which accepts a foreign key value to constrain
29+
* the given target repository
3730
*/
38-
export function hasManyRepositoryFactory<SourceID, T extends Entity, ID>(
39-
sourceModelId: SourceID,
40-
relationMetadata: HasManyDefinition,
41-
targetRepository: EntityCrudRepository<T, ID>,
42-
): HasManyEntityCrudRepository<T> {
43-
switch (relationMetadata.type) {
44-
case RelationType.hasMany:
45-
const fkConstraint = {[relationMetadata.keyTo]: sourceModelId};
46-
47-
return new DefaultHasManyEntityCrudRepository<
48-
T,
49-
EntityCrudRepository<T, ID>
50-
>(targetRepository, fkConstraint);
51-
}
31+
export function createHasManyRepositoryFactory<T extends Entity, ID>(
32+
relationMeta: HasManyDefinition,
33+
targetRepo: EntityCrudRepository<T, ID>,
34+
): HasManyRepositoryFactory<T, ID> {
35+
return function(fkValue: ID) {
36+
const fkName = relationMeta.keyTo;
37+
if (!fkName) {
38+
throw new Error(
39+
'The foreign key property name (keyTo) must be specified',
40+
);
41+
}
42+
return new DefaultHasManyEntityCrudRepository<
43+
T,
44+
ID,
45+
EntityCrudRepository<T, ID>
46+
>(targetRepo, {[fkName]: fkValue});
47+
};
5248
}

packages/repository/src/repositories/relation.repository.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {Filter, Where} from '../query';
1616
/**
1717
* CRUD operations for a target repository of a HasMany relation
1818
*/
19-
export interface HasManyEntityCrudRepository<T extends Entity> {
19+
export interface HasManyRepository<T extends Entity> {
2020
/**
2121
* Create a target model instance
2222
* @param targetModelData The target model data
@@ -54,8 +54,9 @@ export interface HasManyEntityCrudRepository<T extends Entity> {
5454

5555
export class DefaultHasManyEntityCrudRepository<
5656
T extends Entity,
57-
TargetRepository extends EntityCrudRepository<T, typeof Entity.prototype.id>
58-
> implements HasManyEntityCrudRepository<T> {
57+
ID,
58+
TargetRepository extends EntityCrudRepository<T, ID>
59+
> implements HasManyRepository<T> {
5960
/**
6061
* Constructor of DefaultHasManyEntityCrudRepository
6162
* @param targetRepository the related target model repository instance

0 commit comments

Comments
 (0)