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

fix: allow using ES imports syntax in the controllers #15

Merged
merged 7 commits into from
Oct 26, 2021
Merged
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
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "standard",
"root": true,
"parser": "babel-eslint",
rchl marked this conversation as resolved.
Show resolved Hide resolved
"parserOptions": {
"sourceType": "module"
},
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ jobs:
run: yarn test
- name: Coverage
if: ${{ matrix.node-version == '14.x' }}
run: yarn nyc:ava && yarn nyc report --reporter=lcov > coverage.lcov && yarn codecov -t ${{ secrets.CODECOV_TOKEN }}
run: yarn coverage && yarn codecov -t ${{ secrets.CODECOV_TOKEN }}
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ node_modules
*.log
.nuxt
dist
.nyc_output
coverage
36 changes: 18 additions & 18 deletions docs/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,25 @@ Create a new folder on your project, lets assume ```~/api```.
```
Then lets create a new file, for example, ```~/api/todos.js``` and export a new class:
```js
const Todos = require('<your_todos_data_provider>');
import Todos from '<your_todos_data_provider>'

class TodosController {
// Required: This will be the action to route mapper.
static ROUTES = {
allAction: {
path: '/', // your route will be /api/todos'/'
verb: 'GET'
},
getAction: {
path: '/:id', // your route will be /api/todos/:id - route paths are express-like
verb: 'GET'
},
createAction: {
path: '/', // your route will be /api/todos'/'
verb: 'POST'
},
};

// Everytime this class instantiates, request object will be injected into the construtor params.
constructor(request) {
this.request = request;
Expand Down Expand Up @@ -46,23 +62,7 @@ class TodosController {
}
}

// Required: This will be the action to route mapper.
TodosController.ROUTES = {
allAction: {
path: '/', // your route will be /api/todos'/'
verb: 'GET'
},
getAction: {
path: '/:id', // your route will be /api/todos/:id - route paths are express-like
verb: 'GET'
},
createAction: {
path: '/', // your route will be /api/todos'/'
verb: 'POST'
},
};

module.exports = TodosController;
export default TodosController;
```

Now if you call your server at: ```GET http://<your_local_host>:<your_local_port>/api/todos```,
Expand Down
6 changes: 3 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The default options are:
// Universal error handler (will run both on client side and server side $api calls) -- optional
errorHandler: null,

// Success Response handler - will decide how the data will be send (as json, xml, etc..)
// Success Response handler - will decide how the data will be sent (as json, xml, etc..)
serverSuccessResponse: function (req, res, options) {
if (!res.result && options.noContentStatusOnEmpty) {
return res.status(204).send();
Expand All @@ -47,7 +47,7 @@ The default options are:
return res.status(200).json(res.result);
},

// Error Response handler - will decide how the error will be send to the client
// Error Response handler - will decide how the error will be sent to the client
serverErrorResponse: function (err, req, res, options) {
if (err && err.statusCode) {
return res.status(err.statusCode).json({
Expand All @@ -71,7 +71,7 @@ The default options are:

// In case the route wasn't found, will decide what to send to the client
serverNotFoundRouteResponse: function (req, res) {
return res.status(404).json({message: 'Route not found'});
return res.status(404).json({message: 'Route not found'});
}
}
```
54 changes: 26 additions & 28 deletions docs/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,42 +29,40 @@ It's possible to add middleware into 3 parts of your API:
// file: ~/api/todos.js

class TodoController {
// ...
}

TodoController.ROUTES = {
//...
};
static ROUTES = {
//...
};

TodoController.MIDDLEWARE = [
function (req) {
console.log('first');
}
]
static MIDDLEWARE = [
function (req) {
console.log('first');
}
]
}
```

- Middleware for a specific action in a controller.

```js
//file: ~/api/todos.js

class TodoController {
// ...
static ROUTES = {
allAction: {
verb: "GET",
path: "/",
middleware: [
function (req) {
console.log('second');
},
function (req) {
// supports promises
return Promise.resolve().then(() => console.log('third'));
}
]
}
};
}

TodoController.ROUTES = {
allAction: {
verb: "GET",
path: "/",
middleware: [
function (req) {
console.log('second');
},
function (req) {
// supports promises
return Promise.resolve().then(() => console.log('third'));
}
]
}
};
```

*NOTE:* The middleware is mostly used to block access and enrich request object with data.
Expand Down
21 changes: 10 additions & 11 deletions lib/module.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const path = require('path');
import path from 'path';
import { injectAPI, injectRouteControllerMapping } from './server_middleware/api';
import { controllerMappingClientSide } from './utility/controllers';

const DEFAULT_MODULE_OPTIONS = {
directory: '~/api',
Expand Down Expand Up @@ -45,23 +47,20 @@ const DEFAULT_MODULE_OPTIONS = {
}
};

module.exports = function NeoModule(moduleOptions) {
export default async function NeoModule(moduleOptions) {
moduleOptions = Object.assign({}, DEFAULT_MODULE_OPTIONS, moduleOptions);
moduleOptions.aliasKey = /^[~|@]/g;
moduleOptions.srcDir = this.options.srcDir;
moduleOptions.srcDir = this.options.srcDir;
moduleOptions.directory = moduleOptions.directory.replace(moduleOptions.aliasKey, moduleOptions.srcDir);

// Globalize server-side http errors
moduleOptions.httpErrors && require('./utility/http_errors');

const { injectAPI, injectRouteControllerMapping } = require('./server_middleware/api');
const { controllerMappingClientSide } = require('./utility/controllers');
moduleOptions.httpErrors && await import('./utility/http_errors');

// Inject API server middleware
this.addServerMiddleware(injectAPI(moduleOptions));
this.addServerMiddleware(await injectAPI(moduleOptions));

// Inject Route Controller mapping
this.addServerMiddleware(injectRouteControllerMapping(moduleOptions));
this.addServerMiddleware(await injectRouteControllerMapping(moduleOptions));

// Inject client-side http global errors
if (moduleOptions.httpErrors) {
Expand All @@ -78,8 +77,8 @@ module.exports = function NeoModule(moduleOptions) {
apiHandlerFile: moduleOptions.clientSideApiHandler,
successHandlerFile: moduleOptions.successHandler,
errorHandlerFile: moduleOptions.errorHandler,
controllers: JSON.stringify(controllerMappingClientSide(moduleOptions.directory)),
controllers: JSON.stringify(await controllerMappingClientSide(moduleOptions.directory)),
apiConfig: JSON.stringify(moduleOptions)
}
});
};
};
17 changes: 8 additions & 9 deletions lib/plugins/api.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ function runErrorHandler(error) {
*/
function injectParamsIntoPath(path, params) {
if (params) {
Object.keys(params).forEach(function (key) {
for (const key of Object.keys(params)) {
path = path.replace(new RegExp(':' + key, 'g'), params[key]);
});
}
}

return path;
Expand All @@ -58,7 +58,7 @@ function injectParamsIntoPath(path, params) {
*/
function generateAPI(controllerMapping, ctx) {
const api = {};
Object.keys(controllerMapping).forEach(function (key) {
for (const key of Object.keys(controllerMapping)) {
const context = controllerMapping[key];
if (context && context.path && context.verb) {
api[key] = function ({params, ...values} = {}) {
Expand All @@ -68,7 +68,7 @@ function generateAPI(controllerMapping, ctx) {
context.path.slice(0, context.path.length - 1) :
context.path,
params
),
),
context.verb,
values || {},
<%= options.apiConfig %>,
Expand All @@ -84,14 +84,13 @@ function generateAPI(controllerMapping, ctx) {
} else if (typeof controllerMapping[key] === 'object') {
api[key] = generateAPI(controllerMapping[key], ctx);
}
});
}

return api;
}

export default (ctx) => {
export default async (ctx, inject) => {
const {app, req} = ctx;
const $api = process.server ? req._controllersTree(ctx) : generateAPI(<%= options.controllers %>, ctx);
app.$api = $api;
Vue.prototype.$api = $api;
const $api = process.server ? await req._controllersTree(ctx) : generateAPI(<%= options.controllers %>, ctx);
inject('api', $api);
};
56 changes: 26 additions & 30 deletions lib/server_middleware/api.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
const express = require('express');
const url = require('url');
const expressBodyParser = require('body-parser');
const { DEFAULT_PARSER_OPTIONS } = require('../utility/body_parser');
const {
import express from 'express';
import url from 'url';
import expressBodyParser from 'body-parser';
import { DEFAULT_PARSER_OPTIONS } from '../utility/body_parser';
import {
camelcaseToDashcase,
getControllerFiles,
getControllerMiddleware,
middlewareHandler,
controllerMappingServerSide
} = require('../utility/controllers');
} from '../utility/controllers';

/**
* Action handler middleware.
Expand Down Expand Up @@ -42,12 +42,13 @@ function actionHandler(ControllerClass, actionName, options) {
* @param options
* @returns {Router}
*/
function createControllerRouter(controllerFile, options) {
const router = express.Router();
const ControllerClass = require(controllerFile);
const routes = ControllerClass.ROUTES || {};
async function createControllerRouter(controllerFile, options) {
const router = express.Router();
let ControllerClass = await import(controllerFile);
ControllerClass = ControllerClass.default || ControllerClass
const routes = ControllerClass.ROUTES || {};

Object.keys(routes).forEach(function (actionName) {
for (const actionName of Object.keys(routes)) {
if (ControllerClass.prototype[actionName]) {
const { path, verb } = routes[actionName];
const controllerMiddleware = getControllerMiddleware(ControllerClass);
Expand All @@ -63,7 +64,7 @@ function createControllerRouter(controllerFile, options) {
actionHandler(ControllerClass, actionName, options) // Handle controller action method.
);
}
});
}

return router;
}
Expand All @@ -76,7 +77,7 @@ function createControllerRouter(controllerFile, options) {
function injectBodyParsers(options) {
const parsers = [];
if (options.bodyParsers) {
(Array.isArray(options.bodyParsers) ? options.bodyParsers : [options.bodyParsers]).forEach(function (parser) {
for (const parser of (Array.isArray(options.bodyParsers) ? options.bodyParsers : [options.bodyParsers])) {
if (typeof parser === 'string' && expressBodyParser[parser]) {
parsers.push(expressBodyParser[parser](DEFAULT_PARSER_OPTIONS[parser] || {}));
} else if (typeof parser === 'object' && parser.adapter && expressBodyParser[parser.adapter]) {
Expand All @@ -88,7 +89,7 @@ function injectBodyParsers(options) {
} else if (typeof parser === 'function') {
parsers.push(parser(options));
}
});
}
}

return parsers;
Expand All @@ -100,23 +101,23 @@ function injectBodyParsers(options) {
* @param router
* @param options
*/
function injectControllers(router, options) {
getControllerFiles(options.directory).forEach(function (path) {
async function injectControllers(router, options) {
for (const path of getControllerFiles(options.directory)) {
const relativePath = path.replace(options.directory, '');
const routePrefix = relativePath.replace(/\/index|\.js/g, '').split('/').map(camelcaseToDashcase).join('/');

router.use(routePrefix, createControllerRouter(path, options));
});
router.use(routePrefix, await createControllerRouter(path, options));
};
}

/**
* Injects the API.
*
* @param options
* @returns {{path: *, handler: *}}
* @returns {Promise<{path: *, handler: *}>}
*/
function injectAPI(options) {
const _app = express();
export async function injectAPI(options) {
const _app = express();
const router = express.Router();

// Express-like req and res objects
Expand Down Expand Up @@ -149,7 +150,7 @@ function injectAPI(options) {
}

// Inject Controller routes
injectControllers(router, options);
await injectControllers(router, options);

// Success Response handler
router.use(function (req, res, next) {
Expand Down Expand Up @@ -186,17 +187,12 @@ function injectAPI(options) {
* @param options
* @returns {Function}
*/
function injectRouteControllerMapping(options) {
export function injectRouteControllerMapping(options) {
return function (req, res, next) {
req._controllersTree = function (ctx) {
return controllerMappingServerSide(options.directory, req, options, ctx);
req._controllersTree = async function (ctx) {
return await controllerMappingServerSide(options.directory, req, options, ctx);
};

next();
}
}

module.exports = {
injectAPI,
injectRouteControllerMapping
};
Loading