From 3d852dbacc2b4f6edff2f0ca689b9e20fe23a1d8 Mon Sep 17 00:00:00 2001 From: Pedro Pereira Date: Fri, 25 May 2018 12:28:51 +0100 Subject: [PATCH 1/3] Updated documentation --- README.md | 21 ------ docs/.vuepress/config.js | 4 +- docs/README.md | 22 +----- docs/api-module.md | 52 +++++++++++++- .../{service-module.md => services-module.md} | 0 docs/work-flow-usage.md | 69 ------------------- 6 files changed, 54 insertions(+), 114 deletions(-) rename docs/{service-module.md => services-module.md} (100%) diff --git a/README.md b/README.md index 5ba582c..1cfb3ac 100644 --- a/README.md +++ b/README.md @@ -19,27 +19,6 @@ I created this module so you can create your API inside nuxt.js easily, based on - Vue/Nuxt helpers to ease-up data flow access. - Lazy-loaded services layer per request, where your controllers can access. - Global HTTP Errors for better error handling - -### How it works ### -- You create a folder and assign it to be your api root folder (e.g: ~/api). -- Based on that folder/file tree, it will auto generate your routes: - - Imagine with this folder structure: - - ```~/api/users/index.js ``` - - ```~/api/users/categories/index.js ``` - - ```~/api/users/categories/types.js ``` - - ```~/api/products.js ``` - - The following route prefixes will be generated: - - ```/users ``` - - ```/users/categories ``` - - ```/users/categories/types ``` - - ```/products ``` - - Each .js file will be your controller, which is a class. - - A controller has a list of routes (its pretty much a resource). - - A controller can have middleware for all the nested routes or per action. -- Optional: You create a folder and assign it to be your services root folder (e.g: ~/services). - - Each file inside the services folder is a service class, that receives the request method. - - a getService method is injected into request object, and you can simply call - the service as ```getService('users')``` ### License ### MIT diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index d864716..7e1dc81 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -24,8 +24,8 @@ module.exports = { ['/getting-started', 'Getting Started'], ['/work-flow-usage', 'Work flow Usage'], ['/api-module', 'API Module'], - ['/service-module', 'Service Module'] - ] + ['/services-module', 'Services Module'] + ], } } } diff --git a/docs/README.md b/docs/README.md index bb664b9..471798b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,26 +16,6 @@ I created this module so you can create your API inside nuxt.js easily, based on - Vue/Nuxt helpers to ease-up data flow access. - Lazy-loaded services layer per request, where your controllers can access. - Global HTTP Errors for better error handling - -### How it works ### -- You create a folder and assign it to be your api root folder (e.g: ```~/api```). -- Based on that folder/file tree, it will auto generate your routes: - - Imagine with this folder structure: - - ```~/api/users/index.js ``` - - ```~/api/users/categories/index.js ``` - - ```~/api/users/categories/types.js ``` - - ```~/api/products.js ``` - - The following route prefixes will be generated: - - ```/users ``` - - ```/users/categories ``` - - ```/users/categories/types ``` - - ```/products ``` - - Each .js file will be your controller, which is a class. - - A controller has a list of routes (its pretty much a resource). - - A controller can have middleware for all the nested routes or per action. -- Optional: You create a folder and assign it to be your services root folder (e.g: ```~/services```). - - Each file inside the services folder is a service class, that receives the request method. - - a getService method is injected into request object, and you can simply call - the service as ```getService('users')``` + ### License ### MIT diff --git a/docs/api-module.md b/docs/api-module.md index d80846a..d029d22 100644 --- a/docs/api-module.md +++ b/docs/api-module.md @@ -74,7 +74,7 @@ api: { } ``` -### Middleware ### +## Middleware ## It's possible to add middleware into 3 parts of your API: - Middleware for all your api (this means all routes from ```/api/**/*```) ```js @@ -148,4 +148,54 @@ TodoController.MIDDLEWARE = [ } ] ] +``` + +## Error Handling ## +Imagine that you have some kind of validation you want to perform before creating a new todo: +```js + // An object is passed to all actions, with the route params, query string and body. + async createAction({params, query, body}) { + if (!body.title || !body.content) { + throw new BadRequestError('Invalid body payload', [ + !body.title && 'Title is required', + !body.content && 'Content is required' + ].filter(Boolean)); + } + + return await Todos.create(body.title, body.content); + } +``` + +Or in case you fetch a todo by id and it doesn't exist: +```js + // An object is passed to all actions, with the route params, query string and body. + async getAction({params}) { + const todo = await Todos.fetchById(params.id); + if (!todo) { + throw new NotFoundError(`Todo "${params.id}" doesn't exist.`) + } + + return todo; + } +``` + +- By default, ```nuxt-neo``` globalizes what we call ```http errors/exceptions```. Those exceptions, +are a kind of error that is a request flow error (route not found, bad request payload, reserved/private areas). +If an ```http errors/exceptions``` is throwed, a special kind of response will be sent +(e.g bad request will send a list of errors, not found will represent that a given id todo doesn't exist, ...). + +**NOTE**: You are not required to use this, you can simply throw a normal error (```Error native class```), its simply a +nice-to-have way to organize your error handling. If you don't want this classes you can simply disable it doing: +```js +{ + modules: [ + ['nuxt-neo', { + api: { + // ... + httpErrors: false // disable http global error classes + // ... + }, + }] + ] +} ``` \ No newline at end of file diff --git a/docs/service-module.md b/docs/services-module.md similarity index 100% rename from docs/service-module.md rename to docs/services-module.md diff --git a/docs/work-flow-usage.md b/docs/work-flow-usage.md index 8a3cb5c..362fc68 100644 --- a/docs/work-flow-usage.md +++ b/docs/work-flow-usage.md @@ -36,7 +36,6 @@ If you want to disable, for example, ```services```, you should pass the value a } ``` - ### API Module ### To make the api module to work, you should first create a new folder on your project, lets assume ```~/api```. ```js @@ -117,74 +116,6 @@ it will return a response with ```200``` status code and the given structure: } ``` -There are a few magical things here: -- By default, your api will have the prefix ```/api```, this can be changed editing api options ```prefix```. -```js -{ - modules: [ - ['nuxt-neo', { - api: { - directory: __dirname + '/api', - prefix: '/api/' - }, - }] - ] -} -``` -- By default, it will return a json response (with the content being what you return in the controller action), -with ```200``` status code. If the result returned by the controller action is ```null/undefined/false/empty-array```, -it will return ```204``` status code, with no content (which is what the http code standard definition represents). -You can change this by changing the api options default handlers (more above). - -Now Imagine that you have some kind of validation you want to perform before creating a new todo: -```js - // An object is passed to all actions, with the route params, query string and body. - async createAction({params, query, body}) { - if (!body.title || !body.content) { - throw new BadRequestError('Invalid body payload', [ - !body.title && 'Title is required', - !body.content && 'Content is required' - ].filter(Boolean)); - } - - return await Todos.create(body.title, body.content); - } -``` - -Or in case you fetch a todo by id and it doesn't exist: -```js - // An object is passed to all actions, with the route params, query string and body. - async getAction({params}) { - const todo = await Todos.fetchById(params.id); - if (!todo) { - throw new NotFoundError(`Todo "${params.id}" doesn't exist.`) - } - - return todo; - } -``` - -- By default, ```nuxt-neo``` globalizes what we call ```http errors/exceptions```. Those exceptions, -are a kind of error that is a request flow error (route not found, bad request payload, reserved/private areas). -If an ```http errors/exceptions``` is throwed, a special kind of response will be sent -(e.g bad request will send a list of errors, not found will represent that a given id todo doesn't exist, ...). - -**NOTE**: You are not required to use this, you can simply throw a normal error (```Error native class```), its simply a -nice-to-have way to organize your error handling. If you don't want this classes you can simply disable it doing: -```js -{ - modules: [ - ['nuxt-neo', { - api: { - directory: __dirname + '/api', - prefix: '/api/', - httpErrors: false // disable http global error classes - }, - }] - ] -} -``` - ### Services Module ### Services are pretty much your data providers (it's sometimes called repositories), but it can be whatever your want, even a bag of utility functions. Services are request dependent. We provide a simple, lazy-loaded, From 9f703ea9aafc647d4851627a64263b4b0ce93418 Mon Sep 17 00:00:00 2001 From: Pedro Pereira Date: Sat, 26 May 2018 14:50:30 +0100 Subject: [PATCH 2/3] By default, services are disabled. Removed asyncData and fetch helpers. Injected object tree in vue js root instance (app) and in Vue prototype. Updated documentation --- .editorconfig | 8 ++ docs/api-module.md | 1 + docs/services-module.md | 108 ++++++++++++++++++++++- docs/work-flow-usage.md | 148 ++------------------------------ lib/module.js | 7 +- lib/plugins/api.template.js | 6 +- lib/plugins/helpers.template.js | 69 --------------- lib/server_middleware/api.js | 38 ++++---- package.json | 3 + tests/fixtures/pages/index.vue | 4 +- 10 files changed, 150 insertions(+), 242 deletions(-) delete mode 100644 lib/plugins/helpers.template.js diff --git a/.editorconfig b/.editorconfig index 852ad5f..50eb444 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,5 +9,13 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = false +[package.json] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false + [*.md] trim_trailing_whitespace = false diff --git a/docs/api-module.md b/docs/api-module.md index d029d22..d25725f 100644 --- a/docs/api-module.md +++ b/docs/api-module.md @@ -1,5 +1,6 @@ # API Module # +## Configuration ## The default options of the api module are: ```js diff --git a/docs/services-module.md b/docs/services-module.md index 50a54c5..3422bf2 100644 --- a/docs/services-module.md +++ b/docs/services-module.md @@ -1,5 +1,6 @@ # Services Module # +## Configuration ## The default options of the service module are: ```js @@ -7,4 +8,109 @@ services: { // services folder -- required directory: __dirname + '/api' } -``` \ No newline at end of file +``` + +## Creating a service ## +Services are pretty much your data providers (it's sometimes called repositories), but it can be whatever your want, +even a bag of utility functions. Services are request dependent. We provide a simple, lazy-loaded, +way to abstract your controllers from, for example, database connectors. + +To make the services module to work, you should first create a new folder on your project, lets assume ```~/services```. +```js +{ + modules: [ + ['nuxt-neo', { + services: { + directory: __dirname + '/services' + } + }] + ] +} +``` + +Now lets create our ```Todo Service``` class (```~/services/todos.js```): +```js +class TodoService { + constructor(request) { + this.request = request; + } + + async fetchAll() { + // your database/cache/private api todos fetching logic + + return todos; + } + + async fetchById(id) { + // your database/cache/private api todos fetching logic + + return todo; + } + + async create(title, content) { + // your database/cache/private api todos fetching logic + + return todo; + } +} + +module.exports = TodoService; +``` + +Now lets change our ```TodoController``` class (```~/api/todos.js```): +```js +class TodosController { + // Everytime this class instantiates, request object will be injected into the construtor params. + constructor(request) { + this.request = request; + } + + async allAction() { + const todos = await this.getService('todos').fetchAll(); + + return { + total: todos.length, + items: todos + } + } + + // An object is passed to all actions, with the route params, query string and body. + async getAction({params}) { + const todo = await this.getService('todos').fetchById(params.id); + + return todo; + } + + // An object is passed to all actions, with the route params, query string and body. + async createAction({params, query, body}) { + + return await this.getService('todos').create(body.title, body.content); + } + + // Shortcut to access to getService (you can always create a controller class and extend it from there). + getService(name) { + return this.request.getService(name); + } +} + +// 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; +``` + +As you can see, now we can simply access to the todo service, calling ```getService('todos')```. +```'todos'``` is the name of the service file (without the ```.js``` part). diff --git a/docs/work-flow-usage.md b/docs/work-flow-usage.md index 362fc68..162025a 100644 --- a/docs/work-flow-usage.md +++ b/docs/work-flow-usage.md @@ -1,4 +1,4 @@ -# Work Flow Usage +# Work Flow Usage # After you installed ```nuxt-neo``` package and added there are some required options you must set. @@ -36,8 +36,8 @@ If you want to disable, for example, ```services```, you should pass the value a } ``` -### API Module ### -To make the api module to work, you should first create a new folder on your project, lets assume ```~/api```. +## Creating your API ## +Create a new folder on your project, lets assume ```~/api```. ```js { modules: [ @@ -116,112 +116,7 @@ it will return a response with ```200``` status code and the given structure: } ``` -### Services Module ### -Services are pretty much your data providers (it's sometimes called repositories), but it can be whatever your want, -even a bag of utility functions. Services are request dependent. We provide a simple, lazy-loaded, -way to abstract your controllers from, for example, database connectors. - -To make the services module to work, you should first create a new folder on your project, lets assume ```~/services```. -```js -{ - modules: [ - ['nuxt-neo', { - services: { - directory: __dirname + '/services' - } - }] - ] -} -``` - -Now lets create our ```Todo Service``` class (```~/services/todos.js```): -```js -class TodoService { - constructor(request) { - this.request = request; - } - - async fetchAll() { - // your database/cache/private api todos fetching logic - - return todos; - } - - async fetchById(id) { - // your database/cache/private api todos fetching logic - - return todo; - } - - async create(title, content) { - // your database/cache/private api todos fetching logic - - return todo; - } -} - -module.exports = TodoService; -``` - -Now lets change our ```TodoController``` class (```~/api/todos.js```): -```js -class TodosController { - // Everytime this class instantiates, request object will be injected into the construtor params. - constructor(request) { - this.request = request; - } - - async allAction() { - const todos = await this.getService('todos').fetchAll(); - - return { - total: todos.length, - items: todos - } - } - - // An object is passed to all actions, with the route params, query string and body. - async getAction({params}) { - const todo = await this.getService('todos').fetchById(params.id); - - return todo; - } - - // An object is passed to all actions, with the route params, query string and body. - async createAction({params, query, body}) { - - return await this.getService('todos').create(body.title, body.content); - } - - // Shortcut to access to getService (you can always create a controller class and extend it from there). - getService(name) { - return this.request.getService(name); - } -} - -// 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; -``` - -As you can see, now we can simply access to the todo service, calling ```getService('todos')```. -```'todos'``` is the name of the service file (without the ```.js``` part). - -``` Client Side - Vue Pages ``` +## Accessing your API in your Vue Pages ## First you need to create your client-side http request handler: ```js { @@ -262,7 +157,7 @@ export default () { } ``` -**NOTE**: Your should return exactly what the controller action + ```successHandler``` return, +**NOTE**: You must return exactly what the controller action + ```successHandler``` return, to keep the data uniform. Now lets connect our api with our page. Using the power of ```asyncData``` and/or ```fetch``` special @@ -284,15 +179,7 @@ properties for server-side fetching on vue.js pages, we can simply do this: - ``` -- ```asyncData``` global function is an helper to declare initial data, whatever if it's server or client side -(because client routing changes are client side only) - ```$api``` is injected into all Vue instances (including root), since ```asyncData``` doesn't have ```this``` -property since the component is not yet instantiated. -- You can always not use the helpers: -```js - asyncData: function ({req, app}) { - if (process.server) { - return { - todos: req.generateControllersTree().todos.allAction(); - } - } - - return { - todos: app.$api.todos.allAction() - } - } -``` -- ```req.generateControllersTree``` will create the controllers tree, so you can access directly the code, -when you are on server side, instead of having to make a new request. It must be executed, since it may not be needed -everytime, on every vue page. -- On the other hand, ```app.$api``` tree will always be generated/executed on runtime, since it's browser work. \ No newline at end of file +property. \ No newline at end of file diff --git a/lib/module.js b/lib/module.js index 9bd845e..7e74fd8 100755 --- a/lib/module.js +++ b/lib/module.js @@ -46,9 +46,7 @@ const DEFAULT_MODULE_OPTIONS = { return res.status(404).json({message: 'Route not found'}); } }, - services: { - directory: path.resolve(__dirname, '/services') - } + services: false }; module.exports = function NeoModule(moduleOptions) { @@ -81,8 +79,5 @@ module.exports = function NeoModule(moduleOptions) { }) } }); - - // Inject vue js helpers - this.addPlugin(path.resolve(__dirname, 'plugins', 'helpers.template.js')); } }; \ No newline at end of file diff --git a/lib/plugins/api.template.js b/lib/plugins/api.template.js index d41a753..0ecc123 100644 --- a/lib/plugins/api.template.js +++ b/lib/plugins/api.template.js @@ -49,6 +49,8 @@ function generateAPI(controllerMapping) { return api; } -export default () => { - Vue.prototype.$api = generateAPI(<%= options.controllers %>); +export default ({app, req}) => { + const $api = process.server ? req._controllersTree : generateAPI(<%= options.controllers %>); + app.$api = $api; + Vue.prototype.$api = $api; }; diff --git a/lib/plugins/helpers.template.js b/lib/plugins/helpers.template.js deleted file mode 100644 index acaecb8..0000000 --- a/lib/plugins/helpers.template.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Async Data helper to fetch data. - * - * @param flow - * @returns {Function} - */ -function asyncData(flow) { - return function (context) { - context.app.$api = process.server ? context.req.generateControllersTree() : context.app.$api; - - switch (typeof flow) { - case 'function': - return Promise.resolve(flow(context)); - case 'object': - const result = {}; - return Promise.all( - Object.keys(flow).map(function (key) { - return Promise.resolve(flow[key] && flow[key](context)).then(function (value) { - result[key] = value; - }); - }) - ).then(() => result); - default: - throw new Error('First parameters must be either a function or an object'); - } - } -} - -/** - * Fetch helper to fetch data. - * - * @param flow - * @returns {Function} - */ -function fetch(flow) { - return function (context) { - context.app.$api = context.req ? context.req.generateControllersTree() : context.app.$api; - - switch (typeof flow) { - case 'function': - return Promise.resolve(flow(context)) - .then(function (result) { - Object.keys(result || {}).forEach(function (key) { - context.store.commit(key, result[key]); - }); - }); - case 'object': - return Promise.all( - Object.keys(flow).map(function (key) { - Promise.resolve(flow[key] && flow[key](context)).then(function (result) { - context.store.commit(key, result); - }); - }) - ); - default: - throw new Error('First parameters must be either a function or an object'); - } - } -} - -if (process.server) { - global.asyncData = asyncData; - global.fetch = fetch; -} - -if (process.browser) { - window.asyncData = asyncData; - window.fetch = fetch; -} \ No newline at end of file diff --git a/lib/server_middleware/api.js b/lib/server_middleware/api.js index b19123f..20e791c 100755 --- a/lib/server_middleware/api.js +++ b/lib/server_middleware/api.js @@ -135,27 +135,25 @@ function injectAPI(options) { */ function injectRouteControllerMapping(options) { return function (req, res, next) { - req.generateControllersTree = function () { - return controllerMapping(options.directory, function (ControllerClass, actionName) { - return function (params, body, query) { - try { - return Promise.resolve(new ControllerClass(req)[actionName]({ - params: params || {}, - body: body || {}, - query: query || {} - })) - .then(function (result) { - return options.successHandler(result); - }) - .catch(function (err) { - return options.errorHandler(err, req); - }); - } catch (err) { - return options.errorHandler(err, req); - } + req._controllersTree = controllerMapping(options.directory, function (ControllerClass, actionName) { + return function (params, body, query) { + try { + return Promise.resolve(new ControllerClass(req)[actionName]({ + params: params || {}, + body: body || {}, + query: query || {} + })) + .then(function (result) { + return options.successHandler(result); + }) + .catch(function (err) { + return options.errorHandler(err, req); + }); + } catch (err) { + return options.errorHandler(err, req); } - }); - }; + } + }); next(); } diff --git a/package.json b/package.json index 5c8b154..b42a4be 100755 --- a/package.json +++ b/package.json @@ -23,6 +23,9 @@ "type": "git", "url": "https://github.com/ezypeeze/nuxt-neo" }, + "bugs": { + "url": "https://github.com/ezypeeze/nuxt-neo/issues" + }, "main": "lib/module.js", "scripts": { "docs:dev": "vuepress dev docs", diff --git a/tests/fixtures/pages/index.vue b/tests/fixtures/pages/index.vue index 1876038..f523108 100644 --- a/tests/fixtures/pages/index.vue +++ b/tests/fixtures/pages/index.vue @@ -11,9 +11,7 @@