-
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
Changes from all commits
3814823
631349c
2d8053c
b2a2532
7367492
668928b
c033759
74edf8b
50aec28
b22a376
2e1c4d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"ids": { | ||
"Color": 2 | ||
}, | ||
"models": { | ||
"Color": { | ||
"1": "{\"value\":\"Red\",\"id\":1}" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
"use strict"; | ||
'use strict'; | ||
|
||
const application = require('./server'); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
'use strict'; | ||
|
||
const { decorate, metadata, param } = require('./helpers.js'); | ||
const repository = require('@loopback/repository'); | ||
const rest = require('@loopback/rest'); | ||
const models = require('../server/models'); | ||
const repositories = require('../server/repositories'); | ||
|
||
module.exports = function (controllerName) { | ||
|
||
controllerName = controllerName.toLowerCase(); | ||
const controllerClassName = controllerName.charAt(0).toUpperCase() + controllerName.slice(1); | ||
const modelName = controllerClassName; | ||
|
||
let classes = {}; | ||
|
||
classes[controllerName] = class { | ||
constructor(controllerRepository) { | ||
this.controllerRepository = controllerRepository; | ||
} | ||
async create(entity) { | ||
return await this.controllerRepository.create(entity); | ||
} | ||
async count(where) { | ||
return await this.controllerRepository.count(where); | ||
} | ||
async find(filter) { | ||
return await this.controllerRepository.find(filter); | ||
} | ||
async updateAll(entity, where) { | ||
return await this.controllerRepository.updateAll(entity, where); | ||
} | ||
async findById(id) { | ||
return await this.controllerRepository.findById(id); | ||
} | ||
async updateById(id, entity) { | ||
await this.controllerRepository.updateById(id, entity); | ||
} | ||
async replaceById(id, entity) { | ||
await this.controllerRepository.replaceById(id, entity); | ||
} | ||
async deleteById(id) { | ||
await this.controllerRepository.deleteById(id); | ||
} | ||
}; | ||
|
||
let ControllerClass = classes[controllerName]; | ||
Object.defineProperty(ControllerClass, 'name', { writable: true }); | ||
ControllerClass.name = controllerClassName + 'Controller'; | ||
Object.defineProperty(ControllerClass, 'name', { writable: false }); | ||
|
||
decorate([ | ||
rest.post(`/${controllerName}`, { | ||
responses: { | ||
'200': { | ||
description: `${modelName} model instance`, | ||
content: { 'application/json': { schema: { 'x-ts-type': models[modelName] } } }, | ||
}, | ||
}, | ||
}), | ||
param(0, rest.requestBody()), | ||
metadata('design:type', Function), | ||
metadata('design:paramtypes', [models[modelName]]), | ||
metadata('design:returntype', Promise) | ||
], ControllerClass.prototype, 'create', null); | ||
|
||
decorate([ | ||
rest.get(`/${controllerName}/count`, { | ||
responses: { | ||
'200': { | ||
description: `${modelName} model count`, | ||
content: { 'application/json': { schema: repository.CountSchema } }, | ||
}, | ||
}, | ||
}), | ||
param(0, rest.param.query.object('where', rest.getWhereSchemaFor(models[modelName]))), | ||
metadata('design:type', Function), | ||
metadata('design:paramtypes', [Object]), | ||
metadata('design:returntype', Promise) | ||
], ControllerClass.prototype, 'count', null); | ||
|
||
decorate([ | ||
rest.get(`/${controllerName}`, { | ||
responses: { | ||
'200': { | ||
description: 'Array of ${modelName} model instances', | ||
content: { | ||
'application/json': { | ||
schema: { type: 'array', items: { 'x-ts-type': models[modelName] } }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}), | ||
param(0, rest.param.query.object('filter', rest.getFilterSchemaFor(models[modelName]))), | ||
metadata('design:type', Function), | ||
metadata('design:paramtypes', [Object]), | ||
metadata('design:returntype', Promise) | ||
], ControllerClass.prototype, 'find', null); | ||
|
||
decorate([ | ||
rest.patch(`/${controllerName}`, { | ||
responses: { | ||
'200': { | ||
description: `${modelName} PATCH success count`, | ||
content: { 'application/json': { schema: repository.CountSchema } }, | ||
}, | ||
}, | ||
}), | ||
param(0, rest.requestBody()), | ||
param(1, rest.param.query.object('where', rest.getWhereSchemaFor(models[modelName]))), | ||
metadata('design:type', Function), | ||
metadata('design:paramtypes', [models[modelName], Object]), | ||
metadata('design:returntype', Promise) | ||
], ControllerClass.prototype, 'updateAll', null); | ||
|
||
decorate([ | ||
rest.get(`/${controllerName}/{id}`, { | ||
responses: { | ||
'200': { | ||
description: `${modelName} model instance`, | ||
content: { 'application/json': { schema: { 'x-ts-type': models[modelName] } } }, | ||
}, | ||
}, | ||
}), | ||
param(0, rest.param.path.number('id')), | ||
metadata('design:type', Function), | ||
metadata('design:paramtypes', [Number]), | ||
metadata('design:returntype', Promise) | ||
], ControllerClass.prototype, 'findById', null); | ||
|
||
decorate([ | ||
rest.patch(`/${controllerName}/{id}`, { | ||
responses: { | ||
'204': { | ||
description: `${modelName} PATCH success`, | ||
}, | ||
}, | ||
}), | ||
param(0, rest.param.path.number('id')), | ||
param(1, rest.requestBody()), | ||
metadata('design:type', Function), | ||
metadata('design:paramtypes', [Number, models[modelName]]), | ||
metadata('design:returntype', Promise) | ||
], ControllerClass.prototype, 'updateById', null); | ||
|
||
decorate([ | ||
rest.put(`/${controllerName}/{id}`, { | ||
responses: { | ||
'204': { | ||
description: `${modelName} PUT success`, | ||
}, | ||
}, | ||
}), | ||
param(0, rest.param.path.number('id')), | ||
param(1, rest.requestBody()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
metadata('design:type', Function), | ||
metadata('design:paramtypes', [Number, models[modelName]]), | ||
metadata('design:returntype', Promise) | ||
], ControllerClass.prototype, 'replaceById', null); | ||
|
||
decorate([ | ||
rest.del(`/${controllerName}/{id}`, { | ||
responses: { | ||
'204': { | ||
description: `${modelName} DELETE success`, | ||
}, | ||
}, | ||
}), | ||
param(0, rest.param.path.number('id')), | ||
metadata('design:type', Function), | ||
metadata('design:paramtypes', [Number]), | ||
metadata('design:returntype', Promise) | ||
], ControllerClass.prototype, 'deleteById', null); | ||
|
||
const repositoryKey = modelName + 'Repository'; | ||
const modelRepository = repositories[repositoryKey]; | ||
|
||
ControllerClass = decorate([ | ||
param(0, repository.repository(modelRepository)), | ||
metadata('design:paramtypes', [modelRepository]) | ||
], ControllerClass); | ||
|
||
return ControllerClass; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
'use strict'; | ||
|
||
const { decorate, metadata, param } = require('./helpers.js'); | ||
const rest = require('@loopback/rest'); | ||
const context = require('@loopback/context'); | ||
|
||
module.exports = function (specifications, operations) { | ||
|
||
const paths = Object.keys(specifications); | ||
const controllerClasses = {}; | ||
const decorationProperties = []; | ||
|
||
paths.forEach(path => { | ||
const methods = Object.keys(specifications[path]); | ||
methods.forEach(method => { | ||
const decorationProperty = [path]; | ||
const specs = specifications[path][method]; | ||
const controllerName = specs['x-controller-name']; | ||
const operationName = specs['x-operation-name']; | ||
|
||
if (!(controllerName in controllerClasses)) { | ||
// Create the basic class | ||
controllerClasses[controllerName] = class { | ||
constructor(req) { | ||
this.req = req; | ||
} | ||
} | ||
} | ||
|
||
// Add the class methods dynamically | ||
controllerClasses[controllerName].prototype[operationName] = operations[operationName]; | ||
decorationProperty.push(method); | ||
decorationProperty.push(operationName); | ||
decorationProperties.push(decorationProperty); | ||
}); | ||
}); | ||
|
||
const controllerName = Object.keys(controllerClasses)[0]; | ||
let ControllerClass = controllerClasses[controllerName]; | ||
|
||
Object.defineProperty(ControllerClass, 'name', { writable: true }); | ||
ControllerClass.name = controllerName; | ||
Object.defineProperty(ControllerClass, 'name', { writable: false }); | ||
|
||
decorationProperties.forEach(decorationProperty => { | ||
const path = decorationProperty[0]; | ||
const method = decorationProperty[1]; | ||
const operation = decorationProperty[2]; | ||
|
||
decorate([ | ||
rest[method](path, { | ||
responses: specifications[path][method].responses | ||
}), | ||
metadata('design:type', Function), | ||
metadata('design:paramtypes', []), | ||
metadata('design:returntype', Object) | ||
], ControllerClass.prototype, operation, null); | ||
}); | ||
|
||
ControllerClass = decorate([ | ||
param(0, context.inject(rest.RestBindings.Http.REQUEST)), | ||
metadata('design:paramtypes', [Object]) | ||
], ControllerClass); | ||
|
||
return ControllerClass; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
'use strict'; | ||
|
||
const { decorate, metadata, param } = require('./helpers.js'); | ||
const core = require('@loopback/core'); | ||
const repository = require('@loopback/repository'); | ||
|
||
module.exports = function (dataSourceName, config) { | ||
let classes = {}; | ||
classes[dataSourceName] = class extends repository.juggler.DataSource { | ||
constructor(dsConfig = config) { | ||
super(dsConfig); | ||
} | ||
}; | ||
|
||
let DataSourceClass = classes[dataSourceName]; | ||
DataSourceClass.dataSourceName = dataSourceName; | ||
Object.defineProperty(DataSourceClass, 'name', { writable: true }); | ||
DataSourceClass.name = dataSourceName.charAt(0).toUpperCase() + dataSourceName.slice(1) + 'DataSource'; | ||
Object.defineProperty(DataSourceClass, 'name', { writable: false }); | ||
|
||
DataSourceClass = decorate([ | ||
param(0, core.inject('datasources.config.' + dataSourceName, { optional: true })), | ||
metadata("design:paramtypes", [Object]) | ||
], DataSourceClass); | ||
|
||
return DataSourceClass; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
'use strict'; | ||
|
||
// This package loading order is important | ||
|
||
global.modelFactory = require('./model-factory'); | ||
global.datasourceFactory = require('./datasource-factory'); | ||
global.repositoryFactory = require('./repository-factory'); | ||
global.crudControllerFactory = require('./crud-controller-factory'); | ||
global.customControllerFactory = require('./custom-controller-factory'); | ||
global.sequenceFactory = require('./sequence-factory'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
'use strict'; | ||
|
||
const { decorate, metadata, param } = require('./helpers.js'); | ||
const repository = require('@loopback/repository'); | ||
|
||
module.exports = function (modelName, modelDefinition) { | ||
let classes = {}; | ||
|
||
classes[modelName] = class extends repository.Entity { | ||
constructor(data) { | ||
super(data); | ||
} | ||
}; | ||
|
||
let Model = classes[modelName]; | ||
Object.defineProperty(Model, 'name', { writable: true }); | ||
Model.name = modelName; | ||
Object.defineProperty(Model, 'name', { writable: false }); | ||
|
||
// More to be added later | ||
const typeMap = { | ||
'string': String, | ||
'number': Number | ||
}; | ||
|
||
const modelProperties = modelDefinition.properties; | ||
Object.keys(modelProperties).forEach(key => { | ||
const property = modelProperties[key]; | ||
let repositoryProperties = { | ||
type: property.type | ||
}; | ||
if (key === 'id') { | ||
repositoryProperties.id = true; | ||
} | ||
decorate([ | ||
repository.property(repositoryProperties), | ||
metadata('design:type', typeMap[property.type]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like LB4 to work without There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
], Model.prototype, key, void 0); | ||
|
||
}); | ||
|
||
Model = decorate([ | ||
repository.model(), | ||
metadata('design:paramtypes', [Object]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
], Model); | ||
|
||
return Model; | ||
} |
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: