Skip to content
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

Closed
wants to merge 11 commits into from
Closed
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
10 changes: 10 additions & 0 deletions db.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"ids": {
"Color": 2
},
"models": {
"Color": {
"1": "{\"value\":\"Red\",\"id\":1}"
}
}
}
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"use strict";
'use strict';

const application = require('./server');

Expand Down
185 changes: 185 additions & 0 deletions lib/crud-controller-factory.js
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);
Copy link
Member

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

}
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()),
Copy link
Member

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?

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;
}
66 changes: 66 additions & 0 deletions lib/custom-controller-factory.js
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;
};
27 changes: 27 additions & 0 deletions lib/datasource-factory.js
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;
}
4 changes: 2 additions & 2 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ var decorate = function (decorators, target, key, desc) {
var c = arguments.length;
var r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc;
var d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') {
r = Reflect.decorate(decorators, target, key, desc);
}
else {
Expand All @@ -18,7 +18,7 @@ var decorate = function (decorators, target, key, desc) {
};

var metadata = function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
if (typeof Reflect === 'object' && typeof Reflect.metadata === 'function') return Reflect.metadata(k, v);
};

var param = function (paramIndex, decorator) {
Expand Down
10 changes: 10 additions & 0 deletions lib/lb4.js
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');
48 changes: 48 additions & 0 deletions lib/model-factory.js
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])
Copy link
Member

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.

Copy link
Member

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.prototype, key, void 0);

});

Model = decorate([
repository.model(),
metadata('design:paramtypes', [Object])
Copy link
Member

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?

Copy link
Member Author

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.

], Model);

return Model;
}
Loading