Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions docs/site/HasMany-relation.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class Customer extends Entity {
})
name: string;

@hasMany(Order)
@hasMany(() => Order)
orders?: Order[];

constructor(data: Partial<Customer>) {
Expand All @@ -66,13 +66,13 @@ export class Customer extends Entity {
```

The definition of the `hasMany` relation is inferred by using the `@hasMany`
decorator. The decorator takes in the target model class constructor and
optionally a custom foreign key to store the relation metadata. The decorator
logic also designates the relation type and tries to infer the foreign key on
the target model (`keyTo` in the relation metadata) to a default value (source
model name appended with `id` in camel case, same as LoopBack 3). It also calls
`property.array()` to ensure that the type of the property is inferred properly
as an array of the target model instances.
decorator. The decorator takes in a function resolving the target model class
constructor and optionally a custom foreign key to store the relation metadata.
The decorator logic also designates the relation type and tries to infer the
foreign key on the target model (`keyTo` in the relation metadata) to a default
value (source model name appended with `id` in camel case, same as LoopBack 3).
It also calls `property.array()` to ensure that the type of the property is
inferred properly as an array of the target model instances.

The decorated property name is used as the relation name and stored as part of
the source model definition's relation metadata. The property type metadata is
Expand All @@ -85,7 +85,7 @@ as follows:
// import statements
class Customer extends Entity {
// constructor, properties, etc.
@hasMany(Order, {keyTo: 'custId'})
@hasMany(() => Order, {keyTo: 'custId'})
orders?: Order[];
}
```
Expand All @@ -97,8 +97,12 @@ repository level. Once `hasMany` relation is defined on the source model, then
there are a couple of steps involved to configure it and use it. On the source
repository, the following are required:

- Use [Dependency Injection](Dependency-injection.md) to inject an instance of
the target repository in the constructor of your source repository class.
- In the constructor of your source repository class, use
[Dependency Injection](Dependency-injection.md) to receive a getter function
for obtaining an instance of the target repository. _Note: We need a getter
function instead of a repository instance in order to break a cyclic
dependency between a repository with a hasMany relation and a repository with
the matching belongsTo relation._
- Declare a property with the factory function type
`HasManyRepositoryFactory<targetModel, typeof sourceModel.prototype.id>` on
the source repository class.
Expand All @@ -121,7 +125,7 @@ import {
HasManyRepositoryFactory,
repository,
} from '@loopback/repository';
import {inject} from '@loopback/core';
import {inject, Getter} from '@loopback/core';

class CustomerRepository extends DefaultCrudRepository<
Customer,
Expand All @@ -130,12 +134,13 @@ class CustomerRepository extends DefaultCrudRepository<
public orders: HasManyRepositoryFactory<Order, typeof Customer.prototype.id>;
constructor(
@inject('datasources.db') protected db: juggler.DataSource,
@repository(OrderRepository) orderRepository: OrderRepository,
@repository.getter(OrderRepository)
getOrderRepository: Getter<OrderRepository>,
) {
super(Customer, db);
this.orders = this._createHasManyRepositoryFactoryFor(
'orders',
orderRepository,
getOrderRepository,
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion docs/site/todo-list-tutorial-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ model. Add the following property to the `TodoList` model:
export class TodoList extends Entity {
// ...properties defined by the CLI...

@hasMany(Todo)
@hasMany(() => Todo)
todos?: Todo[];

// ...constructor def...
Expand Down
2 changes: 1 addition & 1 deletion examples/todo-list/src/models/todo-list.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class TodoList extends Entity {
})
color?: string;

@hasMany(Todo)
@hasMany(() => Todo)
todos: Todo[];

constructor(data?: Partial<TodoList>) {
Expand Down
11 changes: 6 additions & 5 deletions examples/todo-list/src/repositories/todo-list.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Getter, inject} from '@loopback/core';
import {
DefaultCrudRepository,
juggler,
HasManyRepositoryFactory,
juggler,
repository,
} from '@loopback/repository';
import {TodoList, Todo} from '../models';
import {inject} from '@loopback/core';
import {Todo, TodoList} from '../models';
import {TodoRepository} from './todo.repository';

export class TodoListRepository extends DefaultCrudRepository<
Expand All @@ -21,12 +21,13 @@ export class TodoListRepository extends DefaultCrudRepository<

constructor(
@inject('datasources.db') protected datasource: juggler.DataSource,
@repository(TodoRepository) protected todoRepository: TodoRepository,
@repository.getter(TodoRepository)
protected todoRepositoryGetter: Getter<TodoRepository>,
) {
super(TodoList, datasource);
this.todos = this._createHasManyRepositoryFactoryFor(
'todos',
todoRepository,
todoRepositoryGetter,
);
}

Expand Down
10 changes: 10 additions & 0 deletions packages/context/src/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,16 @@ export function inject(
*/
export type Getter<T> = () => Promise<T>;

export namespace Getter {
/**
* Convert a value into a Getter returning that value.
* @param value
*/
export function fromValue<T>(value: T): Getter<T> {
return () => Promise.resolve(value);
}
}

/**
* The function injected by `@inject.setter(key)`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ describe('Context bindings - Injecting dependencies of classes', () => {
expect(ctx.getSync('key')).to.equal('a-value');
});

it('creates getter from a value', () => {
const getter = Getter.fromValue('data');
expect(getter).to.be.a.Function();
return expect(getter()).to.be.fulfilledWith('data');
});

it('injects a nested property', async () => {
class TestComponent {
constructor(@inject('config#test') public config: string) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {model, property} from '@loopback/repository';
import {model, property, Entity, hasMany} from '@loopback/repository';
import {
modelToJsonSchema,
JSON_SCHEMA_KEY,
Expand Down Expand Up @@ -420,6 +420,49 @@ describe('build-schema', () => {
expectValidJsonSchema(jsonSchema);
});

it('properly converts models with hasMany properties', () => {
@model()
class Order extends Entity {
@property({id: true})
id: number;

@property()
customerId: number;
}

@model()
class Customer extends Entity {
@property({id: true})
id: number;

@hasMany(() => Order)
orders: Order[];
}

const customerSchema = modelToJsonSchema(Customer);

expectValidJsonSchema(customerSchema);

expect(customerSchema.properties).to.deepEqual({
id: {type: 'number'},
orders: {
type: 'array',
items: {$ref: '#/definitions/Order'},
},
});
expect(customerSchema.definitions).to.deepEqual({
Order: {
title: 'Order',
properties: {
id: {
type: 'number',
},
customerId: {type: 'number'},
},
},
});
});

it('creates definitions only at the root level of the schema', () => {
@model()
class CustomTypeFoo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,6 @@ class Customer extends Entity {
@property()
name: string;

@hasMany(Order)
@hasMany(() => Order)
orders?: Order[];
}
2 changes: 2 additions & 0 deletions packages/repository/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@loopback/context": "^0.12.13",
"@loopback/core": "^0.11.14",
"@loopback/dist-util": "^0.3.7",
"@types/debug": "0.0.30",
"debug": "^4.0.1",
"lodash": "^4.17.10",
"loopback-datasource-juggler": "^3.23.0"
},
Expand Down
12 changes: 7 additions & 5 deletions packages/repository/src/decorators/model.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
// License text available at https://opensource.org/licenses/MIT

import {
MetadataInspector,
ClassDecoratorFactory,
PropertyDecoratorFactory,
MetadataMap,
MetadataAccessor,
MetadataInspector,
MetadataMap,
PropertyDecoratorFactory,
} from '@loopback/context';
import {
ModelDefinition,
ModelDefinitionSyntax,
PropertyDefinition,
PropertyType,
RelationDefinitionMap,
} from '../model';
import {RELATIONS_KEY} from './relation.decorator';
Expand Down Expand Up @@ -104,12 +105,13 @@ export namespace property {

/**
*
* @param itemType The class of the array to decorate
* @param itemType The type of array items.
* Examples: `number`, `Product`, `() => Order`.
* @param definition Optional PropertyDefinition object for additional
* metadata
*/
export function array(
itemType: Function,
itemType: PropertyType,
definition?: Partial<PropertyDefinition>,
) {
return function(target: Object, propertyName: string) {
Expand Down
67 changes: 28 additions & 39 deletions packages/repository/src/decorators/relation.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Entity, Model, RelationDefinitionMap} from '../model';
import {PropertyDecoratorFactory} from '@loopback/context';
import {Entity, EntityResolver, Model, RelationDefinitionMap} from '../model';
import {TypeResolver} from '../type-resolver';
import {property} from './model.decorator';
import {camelCase} from 'lodash';

// tslint:disable:no-any

export enum RelationType {
belongsTo = 'belongsTo',
Expand All @@ -22,21 +20,21 @@ export enum RelationType {

export const RELATIONS_KEY = 'loopback:relations';

export class RelationMetadata {
type: RelationType;
target: string | typeof Entity;
as: string;
}

export interface RelationDefinitionBase {
type: RelationType;
name: string;
source: typeof Entity;
target: TypeResolver<Entity, typeof Entity>;
}

export interface HasManyDefinition extends RelationDefinitionBase {
type: RelationType.hasMany;
keyTo: string;
keyTo?: string;
}

// TODO(bajtos) add other relation types, e.g. BelongsToDefinition
export type RelationMetadata = HasManyDefinition | RelationDefinitionBase;

/**
* Decorator for relations
* @param definition
Expand Down Expand Up @@ -72,41 +70,32 @@ export function hasOne(definition?: Object) {
* Decorator for hasMany
* Calls property.array decorator underneath the hood and infers foreign key
* name from target model name unless explicitly specified
* @param targetModel Target model for hasMany relation
* @param targetResolver Target model for hasMany relation
* @param definition Optional metadata for setting up hasMany relation
* @returns {(target:any, key:string)}
*/
export function hasMany<T extends typeof Entity>(
targetModel: T,
export function hasMany<T extends Entity>(
targetResolver: EntityResolver<T>,
definition?: Partial<HasManyDefinition>,
) {
// todo(shimks): extract out common logic (such as @property.array) to
// @relation
return function(target: Object, key: string) {
property.array(targetModel)(target, key);

const defaultFkName = camelCase(target.constructor.name + '_id');
const hasKeyTo = definition && definition.keyTo;
const hasDefaultFkProperty =
targetModel.definition &&
targetModel.definition.properties &&
targetModel.definition.properties[defaultFkName];
if (!(hasKeyTo || hasDefaultFkProperty)) {
// note(shimks): should we also check for the existence of explicitly
// given foreign key name on the juggler definition?
throw new Error(
`foreign key ${defaultFkName} not found on ${
targetModel.name
} model's juggler definition`,
);
}
const meta = {keyTo: defaultFkName};
Object.assign(meta, definition, {type: RelationType.hasMany});

PropertyDecoratorFactory.createDecorator(
RELATIONS_KEY,
meta as HasManyDefinition,
)(target, key);
return function(decoratedTarget: Object, key: string) {
property.array(targetResolver)(decoratedTarget, key);

const meta: HasManyDefinition = Object.assign(
{},
// properties customizable by users
definition,
// properties enforced by the decorator
{
type: RelationType.hasMany,
name: key,
source: decoratedTarget.constructor,
target: targetResolver,
},
);
relation(meta)(decoratedTarget, key);
};
}

Expand Down
Loading