Timeboxed to 2 weeks from start
We need to build support in the legacy juggler bridge to allow usage of the current juggler's relation engine. To do this, we need to answer some questions about what form and format it will take.
In this scenario, we want to retrieve the relationship that connects a Customer to their Orders so that we can return a Customer with its Orders embedded in it as a part of the call to getCustomerWithOrders.
Example pseudo-TS:
// customer.repository.ts
export class CustomerRepository extends EntityCrudRepository<Customer, number> {
// ctor goes here
getRelations(relationName: string, customer: Customer, ctx?: Context)
: OrderRepository {
// find the constructor of the item under relation
const rel = Customer.definition.relations[relationName];
// use rel.modelTo and find (in the metadata) the reference to the correct
// repository class
// somehow, we need to find context!
// Get the Binding<OrderRepository> using one of these approaches:
const orderRepoBinding = ctx.find('repositories.OrderRepository');
// OR
const orderRepoBinding = ctx.findByTag('repositories:Order').first();
// List the pros v. cons of each as a part of the spike
// Next, we either grab the typical instance of the OrderRepository via
// injection and return that directly...
return orderRepoBinding.getValue(ctx);
// ...OR we create a constrained instance of the repository using the
// initial instance, by wrapping it.
const orderRepo = orderRepoBinding.getValue(ctx);
return ConstrainedRepositoryFactory.build(
orderRepoBinding, rel.constraints);
}
}
// order.repository.ts
export class OrderRepository extends EntityCrudRepository<Order, number> {
// this would be the typical implementation
}
// customer.controller.ts
export class CustomerController {
constructor(
@repository('CustomerRepository') protected customerRepository: CustomerRepository,
)
async getCustomerWithOrders(id: number) {
const customer = await this.customerRepository.findById(id);
const orderRepo = customer.getRelations('order', customer);
const orders = orderRepo.find({
where: {
createdDate: {
$gt: new Date('2012-01-01'),
},
}
});
customer.orders = orders;
return customer;
}
}
@model()
export class Customer extends Entity {
orders: Order
}
OrderRepository using Order)?update function of the wrapped repo doesn't allow editing of the foreign key customerId on orders since that breaks the relationship constraint)Can't wait to see this feature in loopback-next..
Cross posting from #419
In LB3, we support the following types of model relation:
- BelongsTo
- HasOne
- HasMany
- HasManyThrough
- HasAndBelongsToMany
- Polymorphic
- EmbedsOne
- EmbedsMany
- ReferencesMany
@dhmlau I think this spike can focus on building the skeleton for the navigation among models(repositories), we can consider it as a factory, any specific relation's details can be discussed in separate stories.
The pseudo code is a great start, a few questions based on it:
At run time, we resolve the related repository(orderRepo in this case) by ctx, does the ctx equal to application by default? And it allows to be overridden?
IIUC the relation related files in legacy juggler relations.js, relation-definition.js, scope.js are not directly related to this spike, so we can put any logic in those files aside until we implement a certain relation(and if some logic are reusable).
In LB3 we dynamically build a relation property for model instance, is it still a good UX we want to keep? From the code above, it seems the new "relation" is essentially a set of controller functions.
How to integrate the legacy juggler's DSL(query)? Especially include, which implies a navigation to query related model under the hood. From the pseudo code, seems the only thing we leverage in LB4 is the legacy juggler's CRUD implementation(dao.js).
Given the pseudo code, we'd better clarify:
repository@raymondfeng I've been trying to create a test app that demonstrates the use case here. I would greatly appreciate some guidance based on @jannyHou's concerns and the following:
What sort of metadata do we need to create and/or retrieve so that we can find the repository based on the known model type (ex. finding
OrderRepositoryusingOrder)?
Can we leverage RepositoryMetadata to reverse lookup the repo based on the model?
Assuming a Customer model hasMany relation with Order model, would users be expected to write out customerId property for Order or should that be inferred by juggler bridge?
is it possible to rename model relation to
@OneToOne
@OneToMany
@ManyToOne
@ManyToMany
Like in Hibernate, Doctrine or TypeOrm
@sbacem thank you for your comment. I think it is possible to use that naming convention and I feel it makes it more intuitive and show the relationship from both sides of the field. It might actually make it easier to describe relationships in one shot as opposed to say having two complimentary decorators (for e.g. @hasMany and @belongsTo). On the other hand, I'm sure there are points to be made to continue to use it as the we have it in LoopBack 3.
Sorry for the late update, everyone. You can find some of the prototype code in the spike #1194. We have discussed about the findings and here is my proposed list of tasks to come out of this:
execute function out of Repository interface and into its own interface for arbitrary SQL commands.Repository interface into different interfaces based on the persistence function type i.e. LookupRepository interface to have all the Retrieval methods, WriteRepository (I'm sure there is a better name), would have the create methods, MutationRepository might have the update and related methods (this might fall under the previous one), and DestroyRepository for deletes.lb4 relation or the likes to wire up related models (think of LB3)There are notes at https://github.com/strongloop/loopback-next/blob/b4da30ed38b897f8804a655a0a64240cbb0962f5/docs/site/Model-relations.md as well.
From @bajtos,
We need a vision of how to iteratively deliver small increments of functionality, where each increment is something that our users (LB4 app developers) can start using. In our call, we were discussing the first smallest step. IIRC, it was
HasManywith a single operationfind, covering both repository/persistence and REST API in a controller method. The next step as I remember it: addcreatemethod to repository/persistence layer and expose it in REST API as a new controller method. After that, I think we can go wider - one task can be adding all remaining HasMany methods, another task can beBelongsTo. Many of the tasks you listed make sense to do after those two initial tasks are done. Some of them should be probably done as part of those two initial tasks. For example, "remove execute" and "split Repository interface" - I think that should be done in the extend needed by those baby steps.
This sentiment is shared by myself and others on the team, and I'm working on the first increments described above in a new PR.
spike is complete.
Most helpful comment
is it possible to rename model relation to
Like in Hibernate, Doctrine or TypeOrm