-
Notifications
You must be signed in to change notification settings - Fork 0
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
Sugar APIs for creating controllers, models, and repositories #3
Conversation
@bajtos @raymondfeng here is the new LB4 sugar API for JS. Good thing, it preserves the underlying metadata info without exposing them to the user. The following files are created by the user: What makes them possible: https://github.com/strongloop/loopback4-example-javascript/tree/hack/lib The codebase can be optimized some more. Before that, I want your feedback on whether this is the right direction or not. Do you see any potential issues, how does it look? |
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.
@hacksparrow Great effort of creating the suger generator functions!
I cloned your branch and run on local, verified that the application starts perfectly and shows the explorer 👍
I also tried some endpoints, like GET /color/count
, which works fine, while got 500 internal server error when try to create a color instance by POST /color
with payload
{
"id": 200,
"value": "string"
}
This is not a complain of some endpoints not working, I totally understand the effort needed to generate a perfect working app :)
My point is, based my trial, the next thing I would like to do is taking a look of the generated controller file like the typescript version TodoController.ts, the generator approach is more like LB3 style that we hide the implementation point for users. I am wondering are we going to create CLIs to generate the js files of artifacts in the future so that users can see and modify those files directly?
Other than ^ I like this Js example, the file structures are pretty straightforward and well organized.
'x-operation-name': 'pang', | ||
'responses': { | ||
'200': { | ||
'description': 'POST Ping Response', |
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.
nitpick: POST Pong Response?
@hacksparrow I like the idea of |
Thanks for the review, @jannyHou. It's working on my machine. Let me add some acceptance tests for everyone else to confirm. |
@raymondfeng good suggestion. |
The files under the |
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.
My first reaction to the latest proposal: 😱
Besides the comments below, I am concerned that this proposal is focused on supporting very narrow subset of possible usage of LB4 APIs that will not get us very far. For example, how can users configure additional model and repository methods or configure their controllers and repositories to receive additional dependencies via DI?
Let me cross-post your loopbackio/loopback-next#1978 (comment):
Technical implementations to figure out:
- How to define models, additional metadata for model properties?
- How to define controllers, how to provide OpenAPI spec for controller methods?
- How to do dependency injection, and how to provide arguments?
As I see it, the proposal provides:
- A solution for 1 with possible limitations - see my comments below.
- A partial solution for 2 that comes with UX issues (see my comment below) and as a result you are proposing a CRUD Controller base class instead of scaffolding the CRUD controller in target project as it the case in TypeScript.
- No easy-to-use solution for 3, at least AFAICT.
I believe that if we can find a decent solution for 2 and especially for 3, then no class factories will be needed.
Let's do few more iterations to find a better solution.
this.controllerRepository = controllerRepository; | ||
} | ||
async create(entity) { | ||
return await this.controllerRepository.create(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.
We have intentionally designed LB4 in a way that gives full control over the REST API shape to app developers, our lb4 controller
command is intentionally scaffolding entire CRUD API implementation.
Your proposal here is similar to what Raymond proposed in loopbackio/loopback-next#740. It's a valid feature, but not what we are looking for in this spike.
As I see it, the spike should show how JavaScript developers can implement CRUD controllers while preserving full control over the shape of the REST API and the underlying implementation. Most importantly, the following task must be easy to achieve:
- add or remove request parameters
- modify the type (and OpenAPI spec) of a request parameter
- implement a custom pre-processing step executed before the repository method is invoked
- implement a custom post-processing step executed after the repository method returns
}, | ||
}), | ||
param(0, rest.param.path.number('id')), | ||
param(1, rest.requestBody()), |
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.
Is it possible to implement this built-in controller class via the custom-controller-factory?
], ColorController); | ||
|
||
exports.ColorController = ColorController; | ||
exports.ColorController = crudControllerFactory('color'); |
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.
As commented above, we are looking for a nice way how to keep the actual actual controller implementation & API spec inside this file.
rest.get('/ping', { | ||
responses: { | ||
'200': PING_RESPONSE, | ||
const specifications = { |
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 have to problems with this approach:
-
We don't have a controller class, therefore it's difficult to share state between method calls, e.g. the public API method is calling out to a private helper shared with other API methods and wants to keep the state in a member property of the controller instance.
-
How do you envision injecting dependencies? Let's say that
ping
wants to receiveRestBindings.URL
and send it back in the response. Or perhapsAuthenticationBindings.CURRENT_USER
to make this more realistic -
You have not addressed my earlier comment JavaScript abstraction layer for LB4 #1 (comment)
The method implementation is far away from its metadata. In my experience, it's an anti-pattern that makes the code difficult to read and maintain. We should look for ways how to allow developers to keep both the implementation and the metadata together in one place.
|
||
exports.MemoryDataSource = MemoryDataSource; | ||
const config = require('./memory.datasource.json'); | ||
exports.MemoryDataSource = datasourceFactory('memory', config); |
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.
In TypeScript, we are introducing per-datasource classes for the following reasons:
- To simplify bindings setup and conventional boot, which is relying on unique DataSource class names.
- To allow datasources to receive their configuration via Dependency Injection.
- To make it easier for developers to add additional custom APIs or modify behavior of existing DataSource methods.
IIUC your proposal, users cannot customize the class because it's created by the factory - it's taking the third item away. At which point I'd prefer to find a way how we can use base juggler.DataSource
class and allow the javascript file to export such metadata that 1) booter creates the right datasource binding 2) the configuration is provided to the datasource constructor.
This is a problem that's not specific to JavaScript only, it's a legitimate requirement to bind multiple instances of the same class but with a different config. See e.g. loopbackio/loopback-next#2259 support binding config and @inject.config
I'd much rather find a clean solution that works both for TS and JS codebases, rather than trying to implement a datasource-specific hack like what is proposed here.
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'd much rather find a clean solution that works both for TS and JS codebases, rather than trying to implement a datasource-specific hack like what is proposed here.
Or even better, find out an easy-to-use solution for configuring Dependency Injection from JavaScript (because we need it anyway) and then we can keep the existing code structure.
} | ||
}, | ||
// Supporting these additional features would be a lot of additional work | ||
// and some features are still not available in LB4, yet |
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.
Let's forget about LB3 model definition please. In LB4, we have slightly different interface - see ModelDefinitionSyntax.
People building models from JavaScript can (and IMO should) use ModelDefinition class.
As a general rule I'd like us to follow, our TS APIs should not rely on decorators only, they should always provide a reasonable non-decorator path too. (A side note: decorators are an experimental API, the standardization in TC39 was put on hold. If decorators ever make it into official JavaScript language, their shape may be very different from what we have now.)
LB4 already contains examples on how to create models without decorators, see https://github.com/strongloop/loopback-next/blob/91a37dca4214bb8d0d3cdfbbcf3b64fbd7e7cae2/packages/repository/test/unit/model/model.unit.ts#L41-L58
The example I pointed to can be easily converted to JavaScript as follows:
const addressDef = new ModelDefinition('Address');
addressDef
.addProperty('street', 'string')
.addProperty('city', 'string')
.addProperty('state', String)
.addProperty('zipCode', STRING);
class Address extends Entity {
static definition = addressDef;
constructor(data?: Partial<Address>) {
super(data);
}
}
Personally, I would use the following version:
class Address extends Entity {
static definition = new ModelDefinition('Address')
.addProperty('street', 'string')
.addProperty('city', 'string')
.addProperty('state', String)
.addProperty('zipCode', STRING);
constructor(data?: Partial<Address>) {
super(data);
}
}
IMPORTANT
LB2/LB3 users were asking for the ability to define models as regular ES6 classes for a long time. The new LB4 design makes that possible in TypeScript, we should look for ways how to preserve that in JavaScript too.
A typical scenario to consider: how to add a new helper method to a model class.
class Address extends Entity {
static definition = new ModelDefinition('Address')
.addProperty('street', 'string')
.addProperty('city', 'string')
.addProperty('state', String)
.addProperty('zipCode', STRING);
constructor(data?: Partial<Address>) {
super(data);
}
// Return the address as a multi-line string, e.g.
// 1 New Orchard RdArmonk
// NY 10504
getFormattedAddress()
return [this.street, this.city, `${this.state} ${this.zipCode}`].join('\n');
}
}
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.
Uh oh, JavaScript does not support property initialization syntax I used above :(
Here are two option how to work around that.
Option 1
const addressDef = new ModelDefinition('Address');
// add properties, etc.
class Address extends Entity {
static get definition() { return addressDef; }
}
Option 2
class Address extends Entity {
}
Address.definition = new ModelDefinition('Address')
// add properties, etc.
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.
FWIW, static property initializers are at Stage 3 in TC39, see https://github.com/tc39/proposal-static-class-features
Slightly related: https://github.com/tc39/proposal-class-fields
} | ||
decorate([ | ||
repository.property(repositoryProperties), | ||
metadata('design:type', typeMap[property.type]) |
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 like LB4 to work without design:type
metadata too. Where are we relying on design:type
of model properties? I'd like us to rework those places to use the real model definition instead. IMO, the only place accessing design-type metadata of model properties is the @property
and @model
decorators that are building the actual model definition.
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.
design:type
is only used for inference/inspection of TS types and they can/should be always overridable.
|
||
Model = decorate([ | ||
repository.model(), | ||
metadata('design:paramtypes', [Object]) |
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.
This design:paramtypes
entry shouldn't be needed at all, right? Can we remove it please?
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.
Not need in all the cases, but in some places it is a breaker not to have them. Eg: https://github.com/strongloop/loopback-next/blob/master/packages/openapi-v3/src/controller-spec.ts#L203.
This file is not supposed to be exposed to the users, having additional consistent code should not hurt, IMO.
I am proposing the following next steps:
For DI configuration, I'd like to consider approach based on what we have for model definition, where the additional metadata is stored in static property. For example: class MyController {
/** property dependencies **/
get $propertyDependencies() {
return {
// inject dependencies of our parent classes
...this.propertyDependencies,
// configure the property `myController.url`
url: RestBindings.URL
};
}
/** constructor dependencies **/
constructor(currentUser) {
this.currentUser = curentUser;
}
static get constructorDependencies() {
return [AuthenticationBindings.CURRENT_USER];
}
/** method-level dependencies **/
getProfile(currentUser) {
return currentUser;
}
get getProfileDependencies() {
return [AuthenticationBindings.CURRENT_USER];
}
/** see how it can be used for OpenAPI spec too! **/
get getProfileSpec() {
return {
responses: {
200: {
'application/json': {
schema: getUserProfileSchema();
}
}
}
} I expect my proposal to be controversial and I am happy to consider different approaches and options. For example, we can use OpenAPI spec (OperationObject) to define method parameters to be injected. The caveat: these injected parameters must be removed from OpenAPI spec before it's returned by the app (e.g. at |
Great discussion! Here is how I see it:
Most of TypeScript classes are generated/predefined. They are less magic/open than LB3 artifacts.
|
@jannyHou, the app should work now, btw. It was caused by the hard-coded local path of the database file. |
Sugar APIs for creating controllers, models, and repositories.