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
5 changes: 3 additions & 2 deletions examples/todo-list/src/models/todo.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Entity, property, model} from '@loopback/repository';
import {Entity, property, model, belongsTo} from '@loopback/repository';
import {TodoList} from './todo-list.model';

@model()
export class Todo extends Entity {
Expand All @@ -29,7 +30,7 @@ export class Todo extends Entity {
})
isComplete: boolean;

@property()
@belongsTo(() => TodoList)
todoListId: number;

getId() {
Expand Down
25 changes: 21 additions & 4 deletions examples/todo-list/src/repositories/todo.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,32 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {DefaultCrudRepository, juggler} from '@loopback/repository';
import {Todo} from '../models';
import {inject} from '@loopback/core';
import {Getter, inject} from '@loopback/core';
import {
BelongsToAccessor,
DefaultCrudRepository,
juggler,
repository,
} from '@loopback/repository';
import {Todo, TodoList} from '../models';
import {TodoListRepository} from './todo-list.repository';

export class TodoRepository extends DefaultCrudRepository<
Todo,
typeof Todo.prototype.id
> {
constructor(@inject('datasources.db') dataSource: juggler.DataSource) {
public todoList: BelongsToAccessor<TodoList, typeof Todo.prototype.id>;

constructor(
@inject('datasources.db') dataSource: juggler.DataSource,
@repository.getter('TodoListRepository')
protected todoListRepositoryGetter: Getter<TodoListRepository>,
) {
super(Todo, dataSource);

this.todoList = this._createBelongsToAccessorFor(
'todoListId',
todoListRepositoryGetter,
);
}
}
13 changes: 10 additions & 3 deletions examples/todo-list/test/acceptance/todo-list-todo.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createRestAppClient,
expect,
givenHttpServerConfig,
toJSON,
} from '@loopback/testlab';
import {TodoListApplication} from '../../src/application';
import {Todo, TodoList} from '../../src/models/';
Expand Down Expand Up @@ -41,13 +42,17 @@ describe('TodoListApplication', () => {
});

it('creates todo for a todoList', async () => {
const todo = givenTodo();
const todo = givenTodo({todoListId: undefined});
const response = await client
.post(`/todo-lists/${persistedTodoList.id}/todos`)
.send(todo)
.expect(200);

expect(response.body).to.containDeep(todo);
const expected = {...todo, todoListId: persistedTodoList.id};
expect(response.body).to.containEql(expected);

const created = await todoRepo.findById(response.body.id);
expect(toJSON(created)).to.deepEqual({id: response.body.id, ...expected});
});

context('when dealing with multiple persisted Todos', () => {
Expand Down Expand Up @@ -213,7 +218,9 @@ describe('TodoListApplication', () => {
id: typeof Todo.prototype.id,
todo?: Partial<Todo>,
) {
return await todoListRepo.todos(id).create(givenTodo(todo));
const data = givenTodo(todo);
delete data.todoListId;
return await todoListRepo.todos(id).create(data);
}

async function givenTodoListInstance(todoList?: Partial<TodoList>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe('TodoListApplication', () => {

it('returns 404 when updating a todo-list that does not exist', () => {
return client
.patch('/todos/99999')
.patch('/todo-lists/99999')
.send(givenTodoList())
.expect(404);
});
Expand Down
1 change: 1 addition & 0 deletions examples/todo-list/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function givenTodo(todo?: Partial<Todo>) {
title: 'do a thing',
desc: 'There are some things that need doing',
isComplete: false,
todoListId: 999,
},
todo,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {model, property, Entity, hasMany} from '@loopback/repository';
import {MetadataInspector} from '@loopback/context';
import {
belongsTo,
Entity,
hasMany,
model,
property,
} from '@loopback/repository';
import {expect} from '@loopback/testlab';
import * as Ajv from 'ajv';
import {
modelToJsonSchema,
JSON_SCHEMA_KEY,
getJsonSchema,
JsonSchema,
JSON_SCHEMA_KEY,
modelToJsonSchema,
} from '../..';
import {expect} from '@loopback/testlab';
import {MetadataInspector} from '@loopback/context';
import * as Ajv from 'ajv';

describe('build-schema', () => {
describe('modelToJsonSchema', () => {
Expand Down Expand Up @@ -420,13 +426,13 @@ describe('build-schema', () => {
expectValidJsonSchema(jsonSchema);
});

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

@property()
@belongsTo(() => Customer)
customerId: number;
}

Expand All @@ -439,10 +445,16 @@ describe('build-schema', () => {
orders: Order[];
}

const orderSchema = modelToJsonSchema(Order);
const customerSchema = modelToJsonSchema(Customer);

expectValidJsonSchema(customerSchema);
expectValidJsonSchema(orderSchema);

expect(orderSchema.properties).to.deepEqual({
id: {type: 'number'},
customerId: {type: 'number'},
});
expect(customerSchema.properties).to.deepEqual({
id: {type: 'number'},
orders: {
Expand Down
5 changes: 2 additions & 3 deletions packages/repository/examples/models/order.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ class Order extends Entity {
// as simple types string, number, boolean can be inferred
@property({type: 'string', id: true, generated: true})
id: string;
customerId: string;

@belongsTo()
customer: Customer;
@belongsTo(() => Customer)
customerId: string;
}
108 changes: 96 additions & 12 deletions packages/repository/src/decorators/relation.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

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

Expand All @@ -21,19 +27,65 @@ export enum RelationType {
export const RELATIONS_KEY = 'loopback:relations';

export interface RelationDefinitionBase {
/**
* The type of the relation, must be one of RelationType values.
*/
type: RelationType;

/**
* The relation name, typically matching the name of the accessor property
* defined on the source model. For example "orders" or "customer".
*/
name: string;

/**
* The source model of this relation.
*
* E.g. when a Customer has many Order instances, then Customer is the source.
*/
source: typeof Entity;

/**
* The target model of this relation.
*
* E.g. when a Customer has many Order instances, then Order is the target.
*/
target: TypeResolver<Entity, typeof Entity>;
}

export interface HasManyDefinition extends RelationDefinitionBase {
type: RelationType.hasMany;

/**
* 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;

/*
* The foreign key in the source model, e.g. Order#customerId.
*/
keyFrom: string;

/*
* The primary key of the target model, e.g Customer#id.
*/
keyTo?: string;
}

// TODO(bajtos) add other relation types, e.g. BelongsToDefinition
export type RelationMetadata = HasManyDefinition | RelationDefinitionBase;
export type RelationMetadata =
| HasManyDefinition
| BelongsToDefinition
// TODO(bajtos) add other relation types and remove RelationDefinitionBase once
// all relation types are covered.
| RelationDefinitionBase;

/**
* Decorator for relations
Expand All @@ -48,12 +100,45 @@ export function relation(definition?: Object) {
/**
* Decorator for belongsTo
* @param definition
* @returns {(target:any, key:string)}
* @returns {(target: Object, key:string)}
*/
export function belongsTo(definition?: Object) {
// Apply model definition to the model class
const rel = Object.assign({type: RelationType.belongsTo}, definition);
return PropertyDecoratorFactory.createDecorator(RELATIONS_KEY, rel);
export function belongsTo<T extends Entity>(
targetResolver: EntityResolver<T>,
definition?: Partial<BelongsToDefinition>,
) {
return function(decoratedTarget: Entity, decoratedKey: string) {
const propMeta: PropertyDefinition = {
type: MetadataInspector.getDesignTypeForProperty(
decoratedTarget,
decoratedKey,
),
// TODO(bajtos) Make the foreign key required once our REST API layer
// allows controller methods to exclude required properties
// required: true,
};
property(propMeta)(decoratedTarget, decoratedKey);

// @belongsTo() is typically decorating the foreign key property,
// e.g. customerId. We need to strip the trailing "Id" suffix from the name.
const relationName = decoratedKey.replace(/Id$/, '');

const meta: BelongsToDefinition = Object.assign(
// default values, can be customized by the caller
{
keyFrom: decoratedKey,
},
// properties provided by the caller
definition,
// properties enforced by the decorator
{
type: RelationType.belongsTo,
name: relationName,
source: decoratedTarget.constructor,
target: targetResolver,
},
);
relation(meta)(decoratedTarget, decoratedKey);
};
}

/**
Expand All @@ -78,14 +163,13 @@ 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(decoratedTarget: Object, key: string) {
property.array(targetResolver)(decoratedTarget, key);

const meta: HasManyDefinition = Object.assign(
// default values, can be customized by the caller
{},
// properties customizable by users
// properties provided by the caller
definition,
// properties enforced by the decorator
{
Expand Down
19 changes: 18 additions & 1 deletion packages/repository/src/repositories/legacy-juggler-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ import {
Options,
PositionalParameters,
} from '../common-types';
import {HasManyDefinition} from '../decorators/relation.decorator';
import {
BelongsToDefinition,
HasManyDefinition,
} from '../decorators/relation.decorator';
import {EntityNotFoundError} from '../errors';
import {Entity, ModelDefinition} from '../model';
import {Filter, Where} from '../query';
import {
BelongsToAccessor,
createBelongsToAccessor,
createHasManyRepositoryFactory,
HasManyRepositoryFactory,
} from './relation.factory';
Expand Down Expand Up @@ -179,6 +184,18 @@ export class DefaultCrudRepository<T extends Entity, ID>
);
}

protected _createBelongsToAccessorFor<Target extends Entity, TargetId>(
relationName: string,
targetRepoGetter: Getter<EntityCrudRepository<Target, TargetId>>,
): BelongsToAccessor<Target, ID> {
const meta = this.entityClass.definition.relations[relationName];
return createBelongsToAccessor<Target, TargetId, T, ID>(
meta as BelongsToDefinition,
targetRepoGetter,
this,
);
}

async create(entity: DataObject<T>, options?: Options): Promise<T> {
const model = await ensurePromise(this.modelClass.create(entity, options));
return this.toEntity(model);
Expand Down
Loading