-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] [RFC] feat: Add REST mapping for repository interfaces #740
Conversation
0c0cc27
to
96203db
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this pull request, I think it's a good illustration of how to expose repositories via REST 👍
In my (very strong) opinion, we should not be packaging these controllers for consumption via inheritance. Instead, we should write CLI tool to generate these controllers.
I have already explained this in my older comment #485 (comment), which I am cross-posting below
You may ask why to code-generate all those controller files? In my experience, one of the most frequently sought feature of LB 3.x is the ability to control and customize the public API. Users start with a wide built-in CRUD API provided by the LoopBack, but soon enough then need to lock the API down, expose only subset of endpoints, and customize how exactly these endpoints work.
In LoopBack 3.x, this was a complex process that required hooks at many different levels (middleware/strong-remoting phases, remoting hooks, operation hooks) and unintuitive API like disableRemoteMethodByName.
In my proposal, customization of the REST API is as easy as editing the generated code.
- Need to hide a built-in (generated) endpoint? Just delete the controller method!
- Need to modify the request arguments before executing the underlying CRUD method? Add that code into the controller method.
- Need to add more parameters to a built-in (generated) endpoint? Just modify the controller method.
- Need to post-process the value returned from the model? Add the transformation after you awaited the result of the model method.
+1 to @bajtos ' review |
96203db
to
d63bf88
Compare
Using the base controllers and having CLI generate sub controllers do not have to be exclusive. @bajtos All of your requirements can be achieved with the base controllers too. See: There are a few possible cases:
|
b23c8e9
to
8b0fe3d
Compare
@bajtos @kjdelisle PTAL |
e8c32d9
to
3c67397
Compare
Can you confirm that this is removing the endpoint from the OpenAPI spec too?
What if users want to add additional endpoint parameters? How can they do that? Do they need to copy all existing decorators from the base class method? I find that too difficult compared to the alternative approach where the controller source file in user's project already contains all decorators and is ready to be modified. Sorry @raymondfeng, I think you did not understand my point. I agree that using the base controllers and having CLI generate sub controllers do not have to be exclusive. What I am saying is that "base controllers" approach require too much effort to develop, maintain and support. It also has a steeper learning curve, because developers must understand how the base controllers are built internally, what are the mechanisms for controlling the API (e.g. hiding methods) and customizing the behavior (e.g. via remoting hooks or sequence action(s) or overriding base controller methods). Compare this with the alternative approach where the full source code of controllers is generated. Any JS developer should be able to immediately understand what each controller method does under the hood and how it can be tweaked (by editing its body) or disabled (by removing the method). For me, just the steeper learning curve is enough justification for rejecting this approach. If that's not enough, then please consider how much difficulties we already have in supporting all existing features in LB 3.x and all database connectors, with a team of only about five people. Let's not repeat the mistakes we made in LB 1.x-3.x that led to this struggle, and make the new codebase less demanding for maintainers time. Let me quote Antoine de Saint Exupéry:
and Edsger Dijkstra:
My point here is that by avoiding base controllers, we can avoid a lot of extra code that we will have to maintain (API for hiding methods inherited from base controllers, pre/after/onError hooks). |
(In case it's not clear from my previous comment, I am proposing to close this pull request as rejected.) |
@bajtos Thank you for the comments. I have a different view here: In summary, there are two styles to promote a convention/pattern to map repository interfaces to REST endpoints:
What are in common is that we need come up a The difference is the mechanism. It's quite similar as C++ templates vs inheritance. IMO, there are pros and cons for each approach. For example:
I see three styles from the user experience perspective:
While I'm fine with the idea to allow some application developers to choose 1, I think the other styles should be supported too. I would also like to hear opinions from LoopBack 3.x users. It will be useful to get comments from IBM app connect team who uses LoopBack as the platform and requires dynamic activation/deactivation of certain artifacts. |
I object to the concept of the Repository interfaces in general, since it implies that there's inherent value not only in the concept, but in implementing this concept. My original attempt to create an implementation of the EntityCrudRepository and CrudConnector led me to the idea that even though I could build implementations of those functions, it would be easier to simply avoid them entirely. This is why I began the spike for a TypeORM mixin. Allowing the conventions of the target ORM to dictate the shape of some of the artifacts feels clean and tangible, IMO, and directing our efforts there feels like a better way to get people what they want with popular offerings, rather than attempting to create a "universal" interface that can't possibly accommodate every edge case. Tangling the REST API with the data layer feels like an anti-pattern. While I understand that some may want a magic generation of everything, we need to be careful about what we offer here since it leaves us with the responsibility of supporting all sorts of advanced use cases that won't easily fit into these patterns. I also think we should close this PR in favour of code generation via the CLI. |
Repository is common pattern implemented by a lot of frameworks. See:
We cannot afford losing out-of-box data access capabilities in LoopBack 4. Being able to CRUD typical DBs and expose REST APIs by convention is one of the key features of LoopBack and it differentiate LoopBack from other frameworks such as Express, Hapi or Resify. If the integration is removed from LoopBack 4, I don't see why LoopBack is better than https://github.com/thiagobustamante/typescript-rest.
IMO, it's contradictory to your statement that you object to Repository interfaces. TypeORM indeed supports repository pattern. I'm open to integrate with other ORMs. That's why I see there are different flavors of Repository interfaces. Having a consistent CRUD operations and unified query and mutation in JSON format is a key factor behind the success of LoopBack.
I strongly disagree. First of all, what's proposed in this PR is creating a bridge between controllers and repositories and it's not tangling at all. Model oriented resources fit REST paradigm pretty naturally. The mapping from repository to REST by convention finds its way in many popular frameworks. See:
BTW, no matter how we support controllers with CRUD (code gen or base class), the REST mapping for Repository (-like) operations is always required, either in the template or base class. The debate is about how we serve the pattern for developers. |
@emckean @cfjedimaster @svennam92 Would you like to chime in? |
To clarify: I meant that I don't feel comfortable with our current set of interfaces like EntityCrudRepository and the like, because there's no way to utilize them with other ORMs that isn't more easily replaced by using those ORMs directly (hence the explanation about how I already tried to use those interfaces to implement an adapter). Now, it might be that there's missing code that would bridge the gap for actually constructing the datasource instances for another ORM, but even so, I can't see a compelling reason for an extension developer to implement those interfaces for their ORM of choice other than to avoid changing controller logic and calls to our CRUD functions ( (EDIT: I should also point out that when I attempted to adapt TypeORM to our interfaces, there was no equivalent to replaceById, which meant I had to make multiple separate calls to TypeORM's repository, which creates a race condition!) I certainly do not object to the general principle of a Repository as a design pattern. I just don't think there's a way to make a universal Repository interface that will function well with all known data sources.
This was meant within the context of our "universal" interface. There's definitely some immediate value in being able to mount a REST API to your database, as long as everyone's aware of the limitations and potential risks that come with this approach, and how to mitigate them. The interfaces we've already made make me feel uneasy because they provide a false sense of equivalence; being able to mount any ORM to them when all of the necessary pieces are made doesn't mean that the quality of that experience will be equivalent; in fact, it almost guarantees that only the ORM we build in-house will provide the best experience with the rest of the framework. This is why I'm pushing to avoid this sort of lock-in. If we can treat our ORM (if we even make one!) the exact same way as all the others, it lets the community gravitate to whichever implementations they find most satisfying.
I'm not suggesting that we surrender out-of-the-box data capabilities in LoopBack, but rather we change the approach we take to get there. I think that there's a middle ground somewhere between frameworks like Express/Hapi/typescript-rest and something like LoopBack 3. By providing generated code and mixins that work with that code, we can still give people a nicely sewn up experience, but one that's unique to which path they choose at the beginning (be it the ORM they select at the prompt, or something else). Rather than having to provide end-to-end support for an ORM and a variety of connectors, we would only need to ensure that our templates and mixins are compatible with the latest implementations they're made to work with. In exchange, this leaves us with more free time to write the documentation, tutorials, blog posts and other materials that can help those devs find their way to the goal. We currently support 32 connectors for the Juggler, and it was taxing in the days before we shifted to loopback-next. It pains me to see how many things we've had to let slip (or just plain missed) when it comes to those connectors and I don't want to be stuck in that position indefinitely.
I want to touch on this point specifically. We had this issue (strongloop/loopback#683) open for years with no satisfactory resolution, and tons of feedback from developers asking us to add JOIN support. The trouble is that there's no easy way to add "simple" support for things like join, which is why ORMs like TypeORM and knex.js provide query builders to give developers the ability to construct SQL queries programmatically to suit their needs. Having a quick way to reach your data over REST is a satisfying first step, but most users that decide to use the framework want to go much further, and they expect us to not only provide these sorts of advanced features, but to do it efficiently. If we're going to provide our own ORM, then I would expect us to take on the burden of building these advanced features into it, for every single database we claim to support, SQL or otherwise. I'm only going to get on board with the idea of building our own ORM if it can perform at least as well as some of its competitors in these more advanced examples, and to be blunt, I doubt that we're going to get that far in the timeframe we originally intended with as many developers as we have. |
Yes, I agree!
In my experience, every team has different non-functional concerns they need to enforce in all controllers. It's very difficult (if possible at all) to provide a single universal base class with extension points that will accommodate all possible non-functional concerns. Instead, I am proposing to advise teams to build their own base controller classes with whatever non-functional requirement they have, and share these base controller classes in their project/team/company - for example as an npm package. In that light, it makes sense to me to require our CLI tooling to support controllers inheriting from a base class template, but asking the CLI users to provide those base classes! In other words, we need to make the following easy:
|
I'm only chiming in as I was asked to. ;) I have no idea what a repository is in terms of LoopBack. I've read the first few comments a few times, and frankly, I just can't connect it to my knowledge of LB3. :) |
@cfjedimaster In LoopBack 3, we use
To help LB 3.x users to have one class to connect CRUD with REST, this proposal defines a base controller class with decorators to map various CRUD methods to REST (similar as LB 3.x PersistedModel) and depends on the repository for the CRUD implementation. Without this, a developer will have to define a controller class, add CRUD methods with decorators for REST, and implement each method by delegating a repository instance (can be injected into the controller as a dependency). |
So being able to customize at the level you describe sounds nice, but I'd not use it most of the time as I've been more than satisfied with how LB3 handles things out of the box. (Or I add a few custom remote properties.) So yes - I'd say LB4 has to have this or I'd have to start asking why I'm using LB if I have to do more work to get simple stuff done. :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we've settled on the Juggler approach for our new version, this PR seems like a no-brainer to me, but I would like to rename it so that we can have a more generic CrudController beneath this layer that provides the repository mappings without the protocol-specific decorators.
/** | ||
* Base controller class to expose CrudRepository operations to REST | ||
*/ | ||
export abstract class CrudController<T extends ValueObject | Entity> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rename this to RestController; that way we can keep the idea of a CrudController open as a base class for a RestController, a GrpcController, etc.
/** | ||
* Base controller class to expose CrudRepository operations to REST | ||
*/ | ||
export abstract class EntityCrudController< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, I'd call it EntityRestController
3c67397
to
de36c65
Compare
de36c65
to
c5ad28a
Compare
dac8c34
to
f82167c
Compare
f82167c
to
952d23e
Compare
@raymondfeng is this pull request still relevant? |
I'll keep it open as an option till the LB3 functional parity is settled. |
Sounds good. I think we will need something similar to what is proposed here for the following user story: From model definition to REST API with no repository/controller classes #2036. Besides a built-in controller for CRUD APIs, we will also need a controller for KeyValue style. |
Created #2736 to keep track of this finishing this work. I am proposing to call the extension package Similarly, I am proposing to call the controller class |
Closing in favor of #3582 |
Description
This PR illustrates how we can use base controller classes to expose Repository interfaces over REST so that we can provision REST APIs for persistent models.
Related issues
Checklist