├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── lib ├── Accessor.js ├── Field.js ├── Link.js ├── Registry.js ├── Resource.js ├── ResourceView.js ├── Response.js ├── Runtime.js ├── Transaction.js ├── accessors │ ├── Const.js │ ├── Property.js │ ├── Ref.js │ ├── Refs.js │ ├── Template.js │ └── index.js ├── errors │ ├── ApiError.js │ ├── HttpError.js │ ├── InvalidFieldValue.js │ ├── ResourceNotFound.js │ ├── UnknownError.js │ └── index.js ├── filters │ ├── common.js │ ├── filter.js │ ├── index.js │ ├── paginate.js │ ├── paginateOffset.js │ ├── select.js │ └── sort.js ├── index.js ├── middleware │ ├── assign.js │ ├── common.js │ ├── create.js │ ├── enumerate.js │ ├── errorHandler.js │ ├── index.js │ ├── modify.js │ ├── read.js │ ├── remove.js │ └── update.js └── selectors.js ├── package.json └── test ├── Field.js ├── Link.js ├── Resource.js ├── ResourceView.js ├── Response.js ├── Transaction.js ├── accessors ├── Const.js ├── Property.js ├── Ref.js ├── Refs.js └── Template.js ├── common.js ├── filters ├── filter.js ├── paginate.js ├── paginateOffset.js ├── select.js └── sort.js ├── middleware ├── assign.js ├── create.js ├── enumerate.js ├── errorHandler.js ├── modify.js ├── read.js ├── remove.js └── update.js └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | # Backup files 2 | *~ 3 | 4 | # KDE directory folders 5 | .directory 6 | 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 33 | node_modules 34 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | stable 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "iojs" 5 | services: mongodb 6 | script: "npm run test-coverage" 7 | after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonapify 2 | 3 | [![NPM](https://nodei.co/npm/jsonapify.png?downloads=true)](https://nodei.co/npm/jsonapify/) 4 | 5 | [![Build Status](https://travis-ci.org/alex94puchades/jsonapify.svg?branch=master)](https://travis-ci.org/alex94puchades/jsonapify) 6 | [![Dependencies](https://david-dm.org/alex94puchades/jsonapify.svg)](https://david-dm.org/alex94puchades/jsonapify) 7 | [![Coverage Status](https://coveralls.io/repos/alex94puchades/jsonapify/badge.svg?branch=master&service=github)](https://coveralls.io/github/alex94puchades/jsonapify?branch=master) 8 | [![Gratipay Tips](https://img.shields.io/gratipay/AlexPuchades.svg)](https://gratipay.com/~AlexPuchades/) 9 | 10 | jsonapify is a library to assist the development of JSON-API compatible APIs with NodeJS. 11 | 12 | You can see jsonapify in action in this [repo](https://github.com/avem-ifmsa/avem-rest-api). 13 | 14 | ## Why jsonapify? 15 | 16 | - __Simple__: jsonapify is designed around simplicity. *Easy things are easy to do, hard things are possible*. If you feel something can be made simpler, by all means [file an issue](https://github.com/alex94puchades/jsonapify/issues)! 17 | - __Unintrusive__: ExpressJS, Restify, Connect,... No matter, jsonapify integrates nicely. 18 | - __Interoperable__: By offering a common-interface across your APIs, jsonapify lets your users build great things on top of them. If you don't know yet about the JSON-API specification, you should [read about it](http://jsonapi.org/) and all the oportunities it has to offer. 19 | - __Well tested__: jsonapify is designed from the start with unit testing in mind. Reliability is at the core of what we do. 20 | 21 | ## Declaring resources 22 | 23 | jsonapify detaches mongoose models from the actual representation of the resources. This allows for a lot of flexibility: as a matter of fact, declaring a non-readable field is this elegant: 24 | 25 | ```js 26 | var User = require('../models/User'); 27 | 28 | var userResource = new jsonapify.Resource(User, { 29 | type: 'users', 30 | id: new jsonapify.Property('_id'), 31 | attributes: { 32 | email: new jsonapify.Property('email'), 33 | password: { 34 | value: new jsonapify.Property('password'), 35 | readable: false, 36 | }, 37 | }, 38 | }); 39 | 40 | jsonapify.Runtime.addResource('User', userResource); 41 | ``` 42 | 43 | ### ES6 in action 44 | 45 | This is how the previous example would look in ES6: 46 | 47 | ```js 48 | import {Property, Resource, Runtime} from 'jsonapify'; 49 | import User from '../models'; 50 | 51 | const userResource = new Resource(User, { 52 | type: 'users', 53 | id: new Property('_id'), 54 | attributes: { 55 | email: new Property('email'), 56 | password: { 57 | value: new Property('password'), 58 | readable: false, 59 | }, 60 | }, 61 | }); 62 | 63 | Runtime.addResource('User', userResource); 64 | ``` 65 | 66 | ## Navigating resources 67 | 68 | [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) is one of the most important principles of the [REST](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) phylosophy. jsonapify makes interconnecting your resources a piece of cake: 69 | 70 | ```js 71 | var User = require('../models/User'); 72 | 73 | var userResource = new jsonapify.Resource(User, { 74 | type: 'users', 75 | id: new jsonapify.Property('_id'), 76 | links: { 77 | self: { 78 | value: new jsonapify.Template('/users/${_id}'), 79 | writable: false, 80 | }, 81 | }, 82 | }); 83 | 84 | jsonapify.Runtime.addResource('User', userResource); 85 | ``` 86 | 87 | ## Linking resources 88 | 89 | As someone said, "nobody is an island". Resources are not islands either. Linking resources in jsonapify is as easy as you'd expect: 90 | 91 | ```js 92 | var User = require('../models/User'); 93 | var roleResource = require('./roles').resource; 94 | 95 | var userResource = new jsonapify.Resource(User, { 96 | type: 'users', 97 | id: new jsonapify.Property('_id'), 98 | relationships: { 99 | role: new jsonapify.Ref('Role', 'role'), 100 | }, 101 | }); 102 | 103 | jsonapify.Runtime.addResource('User', userResource); 104 | ``` 105 | 106 | **Note**: **_related resources_ are not _subresources_**. Subresources are resource-like objects so tightly linked to their parent resource that they can't exist on their own. jsonapify does not support access of related resources as subresources. This is by-design. 107 | 108 | ## Exposing resources 109 | 110 | We all know about [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). But then, why do we keep writing the same endpoint boilerplate again and again? jsonapify offers all [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) as connect-compatible middleware. That means plugging a new endpoint is as simple as it gets: 111 | 112 | ```js 113 | app.get('/users/', [ 114 | jsonapify.enumerate('User'), 115 | jsonapify.errorHandler() 116 | ]); 117 | ``` 118 | 119 | ## Middleware and resource addressing 120 | 121 | Everything in REST is a resource. Resources can have subresources, too. That means that you can apply a READ operation (GET verb in REST terms) to a subresource. Let's see how resource addressing works in jsonapify. 122 | 123 | * Resource chains come in the form of **\[\(typename, \[selector\]\)+\]**. 124 | * Resource chain selectors are applied at request-time, and are used to select a subset of objects of the preceeding resource type. 125 | * At this moment, selectors can get info from: 126 | - Request params: `jsonapify.param(...)` 127 | - Request query params: `jsonapify.query(...)` 128 | - Resource parent object: `jsonapify.parent(...)` 129 | * There are **partial** and **full** resource chains. A *full resource chain* maps to a single resource object, whereas a *partial resource chain* maps to a subset of resource objects. The same chain can be considered *full* or *partial* depending on the middleware (*partial* for enumerate, and *full* for create, read, update and the like). 130 | * Some jsonapify operations require full resource chains (ie: *READ*, *UPDATE*,...), while others require partial resource chains (only *CREATE* at this moment). Therefore, the same resource chain may be interpreted as a full or a partial one depending on the context. 131 | 132 | For example, a *READ* operation with the following resource chain, directed to the URI '/groups/{group}/users/{user}', would retrieve a resource object of type 'User', with `group == parent._id and name == user`, where `parent` is the group the user logically belongs to: 133 | 134 | ```js 135 | /* full chain */ [ 136 | 'UserGroup', { 137 | name: jsonapify.param('group'), 138 | }, 139 | 'User', { 140 | group: jsonapify.parent('_id'), 141 | name: jsonapify.param('user'), 142 | }, 143 | ] 144 | ``` 145 | 146 | On the other hand, a *CREATE* operation with the following resource chain, directed to the same URI, would store a resource object of type 'User', with `group == parent._id` and the rest of the properties from the request body: 147 | 148 | ```js 149 | /* partial chain */ [ 150 | 'UserGroup', { 151 | name: jsonapify.param('group'), 152 | }, 153 | 'User', { 154 | group: jsonapify.parent('_id'), 155 | }, 156 | ] 157 | ``` 158 | 159 | **Note**: While jsonapify subresource addressing is already functional, it is not polished enough to be considered production-ready (think of error reporting, usability...) If you ever encounter a bug, please [file an issue](https://github.com/alex94puchades/jsonapify/issues) and it will get assigned a high priority. 160 | 161 | ## Transaction filters 162 | 163 | In addition to all of the above, jsonapify also offers **transaction filters**. These filters enable per-request functionality, such as [pagination](http://jsonapi.org/format/#fetching-pagination), [sparse-fields](http://jsonapi.org/format/#fetching-sparse-fieldsets), [sorting](http://jsonapi.org/format/#fetching-sorting)... The most common transaction filters are enabled by default, so you don't have to worry. 164 | 165 | ## Credits 166 | 167 | This library wouldn't have been possible without all the great work by the people of the [JSON-API specification](http://jsonapi.org/). Thank you guys, you're awesome! 168 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); 2 | -------------------------------------------------------------------------------- /lib/Accessor.js: -------------------------------------------------------------------------------- 1 | function Accessor() {} 2 | 3 | Accessor.prototype.visitProperties = function(callback) {}; 4 | 5 | Accessor.prototype.serialize = function(field, transaction, object, done) { 6 | done(null, undefined); 7 | }; 8 | 9 | Accessor.prototype.deserialize = function(field, transaction, resdata, object, done) { 10 | done(null, object); 11 | }; 12 | 13 | module.exports = Accessor; 14 | -------------------------------------------------------------------------------- /lib/Field.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var Accessor = require('./Accessor'); 4 | var Const = require('./accessors/Const'); 5 | var InvalidFieldValue = require('./errors/InvalidFieldValue'); 6 | 7 | function Field(resource, name, value, opts) { 8 | opts = _.defaults({}, opts, { 9 | readable: true, 10 | writable: true, 11 | nullable: false, 12 | }); 13 | this._name = name; 14 | this._resource = resource; 15 | this._accessor = toAccessor(value); 16 | this._readable = opts.readable; 17 | this._writable = opts.writable; 18 | this._nullable = opts.nullable; 19 | } 20 | 21 | Object.defineProperties(Field.prototype, { 22 | name: { get: function() { return this._name }}, 23 | resource: { get: function() { return this._resource }}, 24 | readable: { get: function() { return this._readable }}, 25 | writable: { get: function() { return this._writable }}, 26 | nullable: { get: function() { return this._nullable }}, 27 | }); 28 | 29 | Field.prototype.visitProperties = function(callback) { 30 | this._accessor.visitProperties(callback); 31 | }; 32 | 33 | Field.prototype.serialize = function(transaction, object, callback) { 34 | var self = this; 35 | if (!this._readable) return callback(null); 36 | this._accessor.serialize(this, transaction, object, function(err, value) { 37 | if (err) return callback(err); 38 | if (_.isUndefined(value) && !self._nullable) 39 | return callback(new InvalidFieldValue(self, undefined)); 40 | return callback(null, value); 41 | }); 42 | }; 43 | 44 | Field.prototype.deserialize = function(transaction, resdata, object, callback) { 45 | if (!this._writable) return callback(null, object); 46 | if (_.isUndefined(resdata)) { 47 | return this._nullable 48 | ? callback(null, object) 49 | : callback(new InvalidFieldValue(this, undefined)); 50 | } 51 | this._accessor.deserialize(this, transaction, resdata, object, callback); 52 | }; 53 | 54 | function toAccessor(object) { 55 | return (object instanceof Accessor) ? object : new Const(object); 56 | } 57 | 58 | module.exports = Field; 59 | -------------------------------------------------------------------------------- /lib/Link.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | function Link(href, meta) { 4 | if (_.isPlainObject(href) && !meta) { 5 | var link = href; 6 | href = link.href; 7 | meta = link.meta; 8 | } 9 | this._href = href || ''; 10 | this._meta = meta || {}; 11 | } 12 | 13 | Object.defineProperty(Link.prototype, 'href', { 14 | get: function() { return this._href; }, 15 | set: function(href) { this._href = href; }, 16 | }); 17 | 18 | Object.defineProperty(Link.prototype, 'meta', { 19 | get: function() { return this._meta; }, 20 | }); 21 | 22 | Link.prototype.toJSON = function() { 23 | if (_.isEmpty(this._meta)) 24 | return this._href; 25 | return { 26 | href: this._href, 27 | meta: this._meta, 28 | }; 29 | }; 30 | 31 | module.exports = Link; 32 | -------------------------------------------------------------------------------- /lib/Registry.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var Runtime = require('./Runtime'); 4 | 5 | function Registry() {} 6 | 7 | Registry.prototype.add = util.deprecate(function(name, resource) { 8 | return Runtime.addResource(name, resource); 9 | }, [ 10 | 'Registry#add has been deprecated and will soon be removed. ', 11 | 'Consider using Runtime#addResource instead.', 12 | ].join('')); 13 | 14 | Registry.prototype.get = util.deprecate(function(name) { 15 | return Runtime.getResource(name); 16 | }, [ 17 | 'Registry#get has been deprecated and will soon be removed. ', 18 | 'Consider using Runtime#getResource instead.', 19 | ].join('')); 20 | 21 | Registry.prototype.remove = util.deprecate(function(name) { 22 | Runtime.removeResource(name); 23 | }, [ 24 | 'Registry#remove has been deprecated and will soon be removed. ', 25 | 'Consider using Runtime#removeResource instead.', 26 | ].join('')); 27 | 28 | module.exports = new Registry; 29 | -------------------------------------------------------------------------------- /lib/Resource.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var Field = require('./Field'); 4 | var Accessor = require('./Accessor'); 5 | var ResourceView = require('./ResourceView'); 6 | 7 | function Resource(model, descriptor, opts) { 8 | if (!descriptor && _.isPlainObject(model)) { 9 | descriptor = model; 10 | model = undefined; 11 | } 12 | this._model = model; 13 | this._type = descriptor.type; 14 | this._fields = extractFields(this, descriptor); 15 | } 16 | 17 | Object.defineProperties(Resource.prototype, { 18 | type: { get: function() { return this._type }}, 19 | model: { get: function() { return this._model }}, 20 | }); 21 | 22 | Resource.prototype.view = function(transaction) { 23 | var resview = new ResourceView(transaction, this, this._fields); 24 | return transaction.transform(this, 'view', resview); 25 | }; 26 | 27 | function extractFields(self, object, fields) { 28 | return (function iterate(object, scope, fields) { 29 | _.each(object, function(value, key) { 30 | var newScope = scope.concat(key); 31 | var name = newScope.join('.'); 32 | if (_.isPlainObject(value) && !_.isEmpty(value)) { 33 | if (value instanceof Accessor) { 34 | var field = new Field(self, name, value); 35 | fields.push(field); 36 | } else if (isFieldDescriptor(value)) { 37 | var opts = _.omit(value, 'value'); 38 | var field = new Field(self, name, value.value, opts); 39 | fields.push(field); 40 | } else { 41 | iterate(value, newScope, fields); 42 | } 43 | } else { 44 | var field = new Field(self, name, value); 45 | fields.push(field); 46 | } 47 | }); 48 | return fields; 49 | })(object, [], fields || []); 50 | } 51 | 52 | function isFieldDescriptor(object) { 53 | if (!_.isPlainObject(object)) return false; 54 | if (!_.has(object, 'value')) return false; 55 | var allowedKeys = ['value','readable','writable','nullable']; 56 | return _.all(object, function(value, key) { 57 | return _.includes(allowedKeys, key); 58 | }); 59 | } 60 | 61 | module.exports = Resource; 62 | -------------------------------------------------------------------------------- /lib/ResourceView.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var _ = require('lodash'); 4 | var async = require('async'); 5 | 6 | function ResourceView(transaction, resource, fields) { 7 | if (transaction instanceof ResourceView) { 8 | var resview = transaction; 9 | copyFrom(this, resview); 10 | if (_.isPlainObject(resource)) { 11 | var opts = resource; 12 | applyOptions(this, opts); 13 | } 14 | } else { 15 | this._root = this; 16 | this._transaction = transaction; 17 | this._resource = resource; 18 | setFields(this, fields); 19 | } 20 | } 21 | 22 | function copyFrom(self, resview) { 23 | self._root = resview._root; 24 | self._transaction = resview._transaction; 25 | self._resource = resview._resource; 26 | self._fields = resview._fields; 27 | self._readableFields = resview._readableFields; 28 | self._writableFields = resview._writableFields; 29 | } 30 | 31 | function applyOptions(self, opts) { 32 | if (opts.fields) setFields(self, opts.fields); 33 | } 34 | 35 | function setFields(self, fields) { 36 | self._fields = {}; 37 | self._readableFields = []; 38 | self._writableFields = []; 39 | _.each(fields, function(field) { 40 | self._fields[field.name] = field; 41 | if (field.readable) self._readableFields.push(field); 42 | if (field.writable) self._writableFields.push(field); 43 | }); 44 | } 45 | 46 | Object.defineProperties(ResourceView.prototype, { 47 | type: { get: function() { return this._resource.type }}, 48 | model: { get: function() { return this._resource.model }}, 49 | }); 50 | 51 | ResourceView.prototype.field = function(name) { 52 | var self = this; 53 | var field = this._fields[name]; 54 | if (field) return field; 55 | var knownFields = ['attributes', 'relationships']; 56 | _.each(knownFields, function(prefix) { 57 | var fullName = util.format('%s.%s', prefix, name); 58 | field = self._fields[fullName]; 59 | if (field) return false; 60 | }) 61 | return field; 62 | }; 63 | 64 | ResourceView.prototype.visitProperties = function(name, callback) { 65 | var field = this.field(name); 66 | if (field) field.visitProperties(callback); 67 | }; 68 | 69 | ResourceView.prototype.select = function(names) { 70 | var self = this; 71 | if (!_.isArray(names)) names = [names]; 72 | var selected = _(names).map(function(name) { 73 | return self.field(name); 74 | }).compact().uniq().value(); 75 | return new ResourceView(this, { fields: selected }); 76 | }; 77 | 78 | ResourceView.prototype.findOne = function(filter, callback) { 79 | var transaction = this._transaction; 80 | var query = this.model.findOne(filter); 81 | querySelectFields(query, this._fields); 82 | transaction.notify(this, 'query', query); 83 | if (!callback) return query; 84 | query.exec(callback); 85 | }; 86 | 87 | ResourceView.prototype.findMany = function(filter, callback) { 88 | var transaction = this._transaction; 89 | var query = this.model.find(filter); 90 | querySelectFields(query, this._fields); 91 | transaction.notify(this, 'query', query); 92 | if (!callback) return query; 93 | query.exec(callback); 94 | }; 95 | 96 | function querySelectFields(query, fields) { 97 | _.each(fields, function(field) { 98 | field.visitProperties(function(property) { 99 | query.select(property); 100 | }); 101 | }); 102 | } 103 | 104 | ResourceView.prototype.serialize = function(object, done) { 105 | var self = this; 106 | var resdata = {}; 107 | async.each(this._readableFields, function(field, next) { 108 | field.serialize(self._transaction, object, function(err, value) { 109 | if (err) return next(err); 110 | _.set(resdata, field.name, value); 111 | next(); 112 | }); 113 | }, function(err) { 114 | err ? done(err) : done(null, resdata); 115 | }); 116 | }; 117 | 118 | ResourceView.prototype.deserialize = function(resdata, object, done) { 119 | var self = this; 120 | async.each(this._writableFields, function(field, next) { 121 | var value = _.get(resdata, field.name); 122 | field.deserialize(self._transaction, value, object, next); 123 | }, function(err) { 124 | err ? done(err) : done(null, object); 125 | }); 126 | }; 127 | 128 | module.exports = ResourceView; 129 | -------------------------------------------------------------------------------- /lib/Response.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var mongoose = require('mongoose'); 3 | var ObjectId = mongoose.Types.ObjectId; 4 | 5 | function Response(res) { 6 | this._res = res; 7 | this._meta = {}; 8 | this._links = {}; 9 | this._errors = []; 10 | this._included = []; 11 | } 12 | 13 | Object.defineProperties(Response.prototype, { 14 | raw: { get: function() { return this._res } }, 15 | meta: { get: function() { return this._meta } }, 16 | links: { get: function() { return this._links } }, 17 | errors: { get: function() { return this._errors } }, 18 | }); 19 | 20 | Response.prototype.error = function(err) { 21 | this._errors.push(err); 22 | return this; 23 | }; 24 | 25 | Object.defineProperty(Response.prototype, 'included', { 26 | get: function() { return this._included; }, 27 | }); 28 | 29 | Response.prototype.include = function(type, id, data) { 30 | var include = _.find(this._included, function(include) { 31 | return include.type === type && include.id.equals(id); 32 | }); 33 | if (!data) return include; 34 | if (!include) { 35 | include = {}; 36 | this._included.push(include); 37 | } 38 | _.assign(include, data, { type: type, id: new ObjectId(id) }); 39 | return this; 40 | }; 41 | 42 | Object.defineProperty(Response.prototype, 'data', { 43 | get: function() { return this._data; }, 44 | set: function(data) { this._data = data; }, 45 | }); 46 | 47 | Response.prototype.toJSON = function() { 48 | var object = {}; 49 | object.meta = this._meta; 50 | object.links = this._links; 51 | object.errors = this._errors; 52 | object.included = this._included; 53 | object.jsonapi = { version: '1.0' }; 54 | object = _.omit(object, _.isEmpty); 55 | if (!_.isUndefined(this._data) && _.isEmpty(this._errors)) 56 | object.data = this._data; 57 | return object; 58 | }; 59 | 60 | 61 | Response.prototype.send = function(data) { 62 | var res = this._res; 63 | res.contentType('application/vnd.api+json'); 64 | if (!_.isEmpty(this._errors)) 65 | res.statusCode = aproximateErrorCode(this._errors); 66 | if (!_.isUndefined(data)) this._data = data; 67 | res.json(this); 68 | }; 69 | 70 | function aproximateErrorCode(errors) { 71 | return _(errors).pluck('status').reduce(function(status, errCode) { 72 | if (errCode === status) return status; 73 | var prev = genericErrorCode(status); 74 | var next = genericErrorCode(errCode); 75 | return Math.max(prev, next); 76 | }); 77 | } 78 | 79 | function genericErrorCode(status) { 80 | return status - status % 100; 81 | } 82 | 83 | module.exports = Response; 84 | -------------------------------------------------------------------------------- /lib/Runtime.js: -------------------------------------------------------------------------------- 1 | var Resource = require('./Resource'); 2 | 3 | function Runtime() { 4 | this._resources = {}; 5 | }; 6 | 7 | Runtime.prototype.addResource = function(name, resource) { 8 | if (!(resource instanceof Resource)) 9 | throw new TypeError('resource must be of Resource type'); 10 | if (!this._resources[name]) 11 | this._resources[name] = resource; 12 | }; 13 | 14 | Runtime.prototype.getResource = function(name) { 15 | return this._resources[name]; 16 | }; 17 | 18 | Runtime.prototype.removeResource = function(name) { 19 | if (this._resources[name]) 20 | this._resources[name] = undefined; 21 | }; 22 | 23 | module.exports = new Runtime; 24 | -------------------------------------------------------------------------------- /lib/Transaction.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var _ = require('lodash'); 4 | 5 | function Transaction(resource, response) { 6 | this._resource = resource; 7 | this._response = response; 8 | this._handlers = {}; 9 | } 10 | 11 | Object.defineProperty(Transaction.prototype, 'resource', { 12 | get: function() { return this._resource; }, 13 | }); 14 | 15 | Object.defineProperty(Transaction.prototype, 'response', { 16 | get: function() { return this._response; }, 17 | }); 18 | 19 | Transaction.prototype.subscribe = function(type, what, handler) { 20 | var key = util.format('%s.%s', type, what); 21 | var handlers = this._handlers[key]; 22 | if (handlers) { 23 | handlers.push(handler); 24 | } else { 25 | handlers = [handler]; 26 | this._handlers[key] = handlers; 27 | } 28 | }; 29 | 30 | Transaction.prototype.unsubscribe = function(type, what, handler) { 31 | var key = util.format('%s.%s', type, what); 32 | var handlers = this._handlers[key]; 33 | if (!handlers) return; 34 | _.remove(handlers, _.partial(_.eq, handler)); 35 | if (_.isEmpty(handlers)) this._handlers[key] = undefined; 36 | }; 37 | 38 | Transaction.prototype.notify = function(resource, what) { 39 | var params = _.slice(arguments, 2); 40 | var key = util.format('%s.%s', resource.type, what); 41 | var handlers = this._handlers[key]; 42 | if (!handlers) return false; 43 | var args = [resource].concat(params); 44 | _.each(handlers, function(handler) { 45 | handler.apply(null, args); 46 | }); 47 | return true; 48 | }; 49 | 50 | Transaction.prototype.transform = function(resource, what) { 51 | var params = _.slice(arguments, 2); 52 | var value = params.pop(); 53 | var key = util.format('%s.%s', resource.type, what); 54 | var handlers = this._handlers[key]; 55 | if (!handlers) return value; 56 | var index = 1 + params.length; 57 | var args = [resource].concat(params, value); 58 | return _.reduce(handlers, function(value, handler) { 59 | var result = handler.apply(null, args); 60 | args.splice(index, 1, result); 61 | return result; 62 | }, value); 63 | }; 64 | 65 | module.exports = Transaction; 66 | -------------------------------------------------------------------------------- /lib/accessors/Const.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | 3 | var Accessor = require('../Accessor'); 4 | var InvalidFieldValue = require('../errors/InvalidFieldValue'); 5 | 6 | function Const(value) { 7 | this._value = value; 8 | } 9 | 10 | inherits(Const, Accessor); 11 | 12 | Const.prototype.serialize = function(field, transaction, object, done) { 13 | done(null, this._value); 14 | }; 15 | 16 | Const.prototype.deserialize = function(field, transaction, resdata, object, done) { 17 | if (resdata !== this._value) { 18 | var opts = { meta: { expected: this._value }}; 19 | var err = new InvalidFieldValue(field, resdata, opts); 20 | return done(err); 21 | } 22 | done(null, object); 23 | }; 24 | 25 | module.exports = Const; 26 | -------------------------------------------------------------------------------- /lib/accessors/Property.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | 3 | var _ = require('lodash'); 4 | 5 | var Accessor = require('../Accessor'); 6 | var InvalidFieldValue = require('../errors/InvalidFieldValue'); 7 | 8 | function Property(path) { 9 | this._path = path; 10 | } 11 | 12 | inherits(Property, Accessor); 13 | 14 | Property.prototype.visitProperties = function(callback) { 15 | callback(this._path); 16 | }; 17 | 18 | Property.prototype.serialize = function(field, transaction, object, done) { 19 | var value = _.get(object, this._path); 20 | done(null, value); 21 | }; 22 | 23 | Property.prototype.deserialize = function(field, transaction, resdata, object, done) { 24 | _.set(object, this._path, resdata); 25 | done(null, object); 26 | }; 27 | 28 | module.exports = Property; 29 | -------------------------------------------------------------------------------- /lib/accessors/Ref.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | 3 | var _ = require('lodash'); 4 | var async = require('async'); 5 | var mongoose = require('mongoose'); 6 | var ObjectId = mongoose.Types.ObjectId; 7 | 8 | var Runtime = require('../Runtime'); 9 | var Accessor = require('../Accessor'); 10 | var ResourceNotFound = require('../errors/ResourceNotFound'); 11 | var InvalidFieldValue = require('../errors/InvalidFieldValue'); 12 | 13 | function Ref(resourceName, thisPath, opts) { 14 | opts = opts || {}; 15 | this._resourceName = resourceName; 16 | this._thisPath = thisPath; 17 | this._links = opts.links; 18 | this._meta = opts.meta; 19 | } 20 | 21 | inherits(Ref, Accessor); 22 | 23 | Ref.prototype.visitProperties = function(callback) { 24 | callback(this._thisPath); 25 | }; 26 | 27 | Ref.prototype.serialize = function(field, transaction, object, done) { 28 | var self = this; 29 | var id = _.get(object, this._thisPath); 30 | if (_.isUndefined(id)) return done(null); 31 | var resource = Runtime.getResource(this._resourceName); 32 | var response = transaction.response; 33 | var resview = resource.view(transaction); 34 | resview.findOne({ _id: id }, function(err, linked) { 35 | if (err) return done(err); 36 | if (!linked) return done(new ResourceNotFound(resource, { _id: id })); 37 | resview.serialize(linked, function(err, linkedData) { 38 | if (err) return done(err); 39 | var resdata = _.pick(linkedData, 'type', 'id'); 40 | var isIncluded = transaction.transform(field.resource, 'include', field.name, true); 41 | if (isIncluded) 42 | response.include(resdata.type, resdata.id, linkedData); 43 | if (self._meta) resdata.meta = self._meta; 44 | if (self._links) resdata.links = self._links; 45 | done(null, resdata); 46 | }); 47 | }); 48 | }; 49 | 50 | Ref.prototype.deserialize = function(field, transaction, resdata, object, done) { 51 | if (!_.isPlainObject(resdata)) return done(new InvalidFieldValue(field, resdata)); 52 | var target = Runtime.getResource(this._resourceName); 53 | if (resdata.type !== target.type || !ObjectId.isValid(resdata.id)) 54 | return done(new InvalidFieldValue(field, resdata)); 55 | _.set(object, this._thisPath, new ObjectId(resdata.id)); 56 | done(null, object); 57 | }; 58 | 59 | module.exports = Ref; 60 | -------------------------------------------------------------------------------- /lib/accessors/Refs.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | 3 | var _ = require('lodash'); 4 | var async = require('async'); 5 | var mongoose = require('mongoose'); 6 | var ObjectId = mongoose.Types.ObjectId; 7 | 8 | var Runtime = require('../Runtime'); 9 | var Accessor = require('../Accessor'); 10 | var ResourceNotFound = require('../errors/ResourceNotFound'); 11 | var InvalidFieldValue = require('../errors/InvalidFieldValue'); 12 | 13 | function Refs(resourceName, thisPath, opts) { 14 | opts = opts || {}; 15 | this._resourceName = resourceName; 16 | this._thisPath = thisPath; 17 | } 18 | 19 | inherits(Refs, Accessor); 20 | 21 | Refs.prototype.accessProperties = function(callback) { 22 | callback(this._thisPath); 23 | }; 24 | 25 | Refs.prototype.serialize = function(field, transaction, object, done) { 26 | var self = this; 27 | var ids = _.get(object, this._thisPath); 28 | if (_.isUndefined(ids)) return done(null); 29 | var resource = Runtime.getResource(this._resourceName); 30 | var response = transaction.response; 31 | var resview = resource.view(transaction); 32 | var isIncluded = transaction.transform(field.resource, 'include', field.name, true); 33 | async.map(ids, function(id, next) { 34 | resview.findOne({ _id: id }, function(err, linked) { 35 | if (err) return next(err); 36 | if (!linked) return new ResourceNotFound(resource, { _id: id }); 37 | resview.serialize(linked, function(err, linkedData) { 38 | if (err) return next(err); 39 | var link = _.pick(linkedData, 'type', 'id'); 40 | if (isIncluded) 41 | response.include(link.type, link.id, linkedData); 42 | next(null, link); 43 | }); 44 | }); 45 | }, function(err, links) { 46 | err ? done(err) : done(null, links); 47 | }); 48 | }; 49 | 50 | Refs.prototype.deserialize = function(field, transaction, resdata, object, done) { 51 | if (!_.isArray(resdata)) 52 | return done(new InvalidFieldValue(field, resdata)); 53 | var self = this; 54 | var links = []; 55 | var target = Runtime.getResource(self._resourceName); 56 | _.set(object, this._thisPath, links); 57 | _.each(resdata, function(link, index) { 58 | if (link.type !== target.type || !ObjectId.isValid(link.id)) { 59 | var opts = { meta: { index: index }}; 60 | var err = new InvalidFieldValue(field, resdata, opts); 61 | return done(err); 62 | } 63 | var id = new ObjectId(link.id); 64 | links.push(id); 65 | }); 66 | done(null, object); 67 | }; 68 | 69 | module.exports = Refs; 70 | -------------------------------------------------------------------------------- /lib/accessors/Template.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | 3 | var _ = require('lodash'); 4 | 5 | var Accessor = require('../Accessor'); 6 | var InvalidFieldValue = require('../errors/InvalidFieldValue'); 7 | 8 | function Template(format) { 9 | var self = this; 10 | this._strings = []; 11 | this._values = []; 12 | var chunks = format.split(/\$\{([^\}]*)\}/); 13 | _(chunks).chunk(2).forEach(function(stringValue) { 14 | self._strings.push(stringValue[0]); 15 | if (stringValue.length > 1) 16 | self._values.push(stringValue[1]); 17 | }).run(); 18 | } 19 | 20 | inherits(Template, Accessor); 21 | 22 | Template.prototype.visitProperties = function(callback) { 23 | _.each(this._values, callback); 24 | }; 25 | 26 | Template.prototype.serialize = function(field, transaction, object, done) { 27 | var values = _.map(this._values, _.partial(_.get, object)); 28 | if (_.any(values, _.isUndefined)) return done(null); 29 | var result = _(this._strings).zip(values).flatten().value().join(''); 30 | done(null, result); 31 | }; 32 | 33 | Template.prototype.deserialize = function(field, transaction, resdata, object, done) { 34 | if (!_.isString(resdata)) 35 | return done(new InvalidFieldValue(field, resdata)); 36 | done(null, object); 37 | }; 38 | 39 | module.exports = Template; 40 | -------------------------------------------------------------------------------- /lib/accessors/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Ref: require('./Ref'), 3 | Refs: require('./Refs'), 4 | Const: require('./Const'), 5 | Property: require('./Property'), 6 | Template: require('./Template'), 7 | }; 8 | -------------------------------------------------------------------------------- /lib/errors/ApiError.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var _ = require('lodash'); 4 | 5 | function ApiError(opts) { 6 | opts = _.defaults({}, opts, { 7 | meta: {}, 8 | links: {}, 9 | status: 500, 10 | }); 11 | this._id = opts.id; 12 | this._code = opts.code; 13 | this._meta = opts.meta; 14 | this._links = opts.links; 15 | this._title = opts.title; 16 | this._status = opts.status; 17 | this._detail = opts.detail; 18 | this._source = opts.source; 19 | Error.call(this, opts.detail); 20 | } 21 | 22 | util.inherits(ApiError, Error); 23 | 24 | Object.defineProperties(ApiError.prototype, { 25 | meta: { get: function() { return this._meta; } }, 26 | links: { get: function() { return this._links; } }, 27 | status: { get: function() { return this._status; } }, 28 | }); 29 | 30 | ApiError.prototype.toJSON = function() { 31 | var object = {}; 32 | object.id = this._id; 33 | object.code = this._code; 34 | object.meta = this._meta; 35 | object.links = this._links; 36 | object.title = this._title; 37 | object.status = this._status; 38 | object.detail = this._detail; 39 | object.source = this._source; 40 | return _.omit(object, _.isEmpty); 41 | }; 42 | 43 | module.exports = ApiError; 44 | -------------------------------------------------------------------------------- /lib/errors/HttpError.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var http = require('http'); 3 | 4 | var _ = require('lodash'); 5 | 6 | var ApiError = require('./ApiError'); 7 | 8 | function HttpError(status, opts) { 9 | opts = opts || {}; 10 | opts.status = status; 11 | opts.detail = http.STATUS_CODES[status]; 12 | ApiError.call(this, opts); 13 | } 14 | 15 | util.inherits(HttpError, ApiError); 16 | 17 | module.exports = HttpError; 18 | -------------------------------------------------------------------------------- /lib/errors/InvalidFieldValue.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var ApiError = require('./ApiError'); 4 | 5 | function InvalidFieldValue(field, value, opts) { 6 | opts = opts || {}; 7 | opts.status = 422; 8 | opts.detail = 'Invalid Field Value'; 9 | ApiError.call(this, opts); 10 | this._field = field; 11 | this._value = value; 12 | } 13 | 14 | util.inherits(InvalidFieldValue, ApiError); 15 | 16 | Object.defineProperty(InvalidFieldValue.prototype, 'field', { 17 | get: function() { return this._field; }, 18 | }); 19 | 20 | Object.defineProperty(InvalidFieldValue.prototype, 'value', { 21 | get: function() { return this._value; }, 22 | }); 23 | 24 | module.exports = InvalidFieldValue; 25 | -------------------------------------------------------------------------------- /lib/errors/ResourceNotFound.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var ApiError = require('./ApiError'); 4 | 5 | function ResourceNotFound(resource, anchor, opts) { 6 | opts = opts || {}; 7 | opts.status = 404; 8 | opts.detail = 'Resource Not Found'; 9 | ApiError.call(this, opts); 10 | this._resource = resource; 11 | this._anchor = anchor; 12 | }; 13 | 14 | util.inherits(ResourceNotFound, ApiError); 15 | 16 | Object.defineProperty(ResourceNotFound.prototype, 'resource', { 17 | get: function() { return this._resource; }, 18 | }); 19 | 20 | Object.defineProperty(ResourceNotFound.prototype, 'anchor', { 21 | get: function() { return this._anchor; }, 22 | }); 23 | 24 | module.exports = ResourceNotFound; 25 | -------------------------------------------------------------------------------- /lib/errors/UnknownError.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var ApiError = require('./ApiError'); 4 | 5 | function UnknownError(err, opts) { 6 | opts = opts || {}; 7 | opts.status = 500; 8 | opts.detail = err.message; 9 | ApiError.call(this, opts); 10 | } 11 | 12 | util.inherits(UnknownError, ApiError); 13 | 14 | module.exports = UnknownError; 15 | -------------------------------------------------------------------------------- /lib/errors/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ApiError: require('./ApiError'), 3 | HttpError: require('./HttpError'), 4 | UnknownError: require('./UnknownError'), 5 | ResourceNotFound: require('./ResourceNotFound'), 6 | InvalidFieldValue: require('./InvalidFieldValue'), 7 | }; 8 | -------------------------------------------------------------------------------- /lib/filters/common.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | var qs = require('qs'); 3 | 4 | var _ = require('lodash'); 5 | 6 | function createFilter(queryParse, handler) { 7 | return function(transaction) { 8 | var restype = transaction.resource.type; 9 | transaction.subscribe(restype, 'start', function(resource, req) { 10 | queryParse(req, function(type, params) { 11 | if (_.isUndefined(params)) { 12 | params = type; 13 | type = restype; 14 | } 15 | handler(transaction, req, type, params); 16 | }); 17 | }); 18 | }; 19 | } 20 | 21 | function modifyQuery(uri, query) { 22 | var info = url.parse(uri); 23 | var queryObj = qs.parse(info.query); 24 | queryObj = _.merge(queryObj, query); 25 | info.search = '?' + qs.stringify(queryObj, { encode: false }); 26 | return url.format(info); 27 | } 28 | 29 | exports.createFilter = createFilter; 30 | exports.modifyQuery = modifyQuery; 31 | -------------------------------------------------------------------------------- /lib/filters/filter.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var glob2re = require('glob2re'); 3 | 4 | var common = require('./common'); 5 | 6 | function filter() { 7 | return common.createFilter(queryParser, function(transaction, req, type, param) { 8 | var filters = parseParam(param); 9 | transaction.subscribe(type, 'query', function(resview, query) { 10 | _.each(filters, function(filter, field) { 11 | resview.visitProperties(field, _.partialRight(filter, query)); 12 | }); 13 | }); 14 | }); 15 | } 16 | 17 | function queryParser(req, callback) { 18 | var params = req.query['filter']; 19 | if (!_.isArray(params)) params = [params]; 20 | _.each(params, function(param) { 21 | if (_.isPlainObject(param)) 22 | callback(param); 23 | }); 24 | } 25 | 26 | function parseParam(param) { 27 | return _(param).pick(_.isString).mapValues(function(filter) { 28 | if (isReFilter(filter)) return reFilter(filter); 29 | if (isEqFilter(filter)) return eqFilter(filter); 30 | if (isGeFilter(filter)) return geFilter(filter); 31 | if (isGtFilter(filter)) return gtFilter(filter); 32 | if (isLeFilter(filter)) return leFilter(filter); 33 | if (isLtFilter(filter)) return ltFilter(filter); 34 | if (isNeFilter(filter)) return neFilter(filter); 35 | return strMatchFilter(filter); 36 | }).value(); 37 | } 38 | 39 | function isReFilter(expr) { return _.startsWith(expr, '=~'); } 40 | function isEqFilter(expr) { return _.startsWith(expr, '='); } 41 | function isNeFilter(expr) { return _.startsWith(expr, '!='); } 42 | function isGeFilter(expr) { return _.startsWith(expr, '>='); } 43 | function isLeFilter(expr) { return _.startsWith(expr, '<='); } 44 | function isGtFilter(expr) { return _.startsWith(expr, '>'); } 45 | function isLtFilter(expr) { return _.startsWith(expr, '<'); } 46 | 47 | function eqFilter(filter) { 48 | var value = filter.slice(1); 49 | return function(property, query) { 50 | query.where(property).equals(value); 51 | }; 52 | } 53 | 54 | function geFilter(filter) { 55 | var value = filter.slice(2); 56 | return function(property, query) { 57 | query.where(property).gte(value); 58 | }; 59 | } 60 | 61 | function gtFilter(filter) { 62 | var value = filter.slice(1); 63 | return function(property, query) { 64 | query.where(property).gt(value); 65 | }; 66 | } 67 | 68 | function leFilter(filter) { 69 | var value = filter.slice(2); 70 | return function(property, query) { 71 | query.where(property).lte(value); 72 | }; 73 | } 74 | 75 | function ltFilter(filter) { 76 | var value = filter.slice(1); 77 | return function(property, query) { 78 | query.where(property).lt(value); 79 | }; 80 | } 81 | 82 | function neFilter(filter) { 83 | var value = filter.slice(2); 84 | return function(property, query) { 85 | query.where(property).ne(value); 86 | }; 87 | } 88 | 89 | function reFilter(filter) { 90 | var beg = filter.indexOf('/', 2); 91 | var end = filter.lastIndexOf('/'); 92 | var pattern = filter.slice(beg + 1, end); 93 | var flags = filter.slice(end + 1); 94 | var re = new RegExp(pattern, flags); 95 | return function(property, query) { 96 | var condition = _.set({}, property, re); 97 | query.where(condition); 98 | }; 99 | } 100 | 101 | function strMatchFilter(filter) { 102 | var re = glob2re(filter); 103 | return function(property, query) { 104 | var condition = _.set({}, property, re); 105 | query.where(condition); 106 | }; 107 | } 108 | 109 | module.exports = filter; 110 | -------------------------------------------------------------------------------- /lib/filters/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sort: require('./sort'), 3 | select: require('./select'), 4 | filter: require('./filter'), 5 | paginate: require('./paginate'), 6 | paginateOffset: require('./paginateOffset'), 7 | }; 8 | -------------------------------------------------------------------------------- /lib/filters/paginate.js: -------------------------------------------------------------------------------- 1 | var common = require('./common'); 2 | 3 | var _ = require('lodash'); 4 | 5 | function paginate() { 6 | return common.createFilter(queryParser, function(transaction, req, type, params) { 7 | var pageNumber = params.number, pageSize = params.size; 8 | 9 | transaction.subscribe(type, 'query', function(resource, query) { 10 | query.skip((pageNumber - 1) * pageSize).limit(pageSize); 11 | }); 12 | 13 | transaction.subscribe(type, 'end', function(resource) { 14 | var selfLink = req.originalUrl; 15 | var response = transaction.response; 16 | var count = response.meta['count']; 17 | var pageCount = Math.ceil(count / pageSize); 18 | response.links['first'] = common.modifyQuery(selfLink, { page: { number: 1 }}); 19 | response.links['last'] = common.modifyQuery(selfLink, { page: { number: pageCount }}); 20 | if (pageNumber > 1) 21 | response.links['prev'] = common.modifyQuery(selfLink, { page: { number: pageNumber - 1 }}); 22 | if (pageNumber < pageCount) 23 | response.links['next'] = common.modifyQuery(selfLink, { page: { number: pageNumber + 1 }}); 24 | }); 25 | }); 26 | } 27 | 28 | function queryParser(req, callback) { 29 | var param = req.query['page']; 30 | var pageInfo = _.pick(param, 'size', 'number'); 31 | if (pageInfo.size !== undefined && pageInfo.number !== undefined) 32 | callback(pageInfo); 33 | } 34 | 35 | module.exports = paginate; 36 | -------------------------------------------------------------------------------- /lib/filters/paginateOffset.js: -------------------------------------------------------------------------------- 1 | var common = require('./common'); 2 | 3 | var _ = require('lodash'); 4 | 5 | function paginate() { 6 | return common.createFilter(queryParser, function(transaction, req, type, params) { 7 | var pageOffset = params.offset, pageLimit = params.limit; 8 | 9 | transaction.subscribe(type, 'query', function(resource, query) { 10 | query.skip(pageOffset).limit(pageLimit); 11 | }); 12 | 13 | transaction.subscribe(type, 'end', function(resource) { 14 | var selfLink = req.originalUrl; 15 | var response = transaction.response; 16 | var count = response.meta['count']; 17 | response.links['first'] = common.modifyQuery(selfLink, { page: { 18 | offset: 0 19 | }}); 20 | response.links['last'] = common.modifyQuery(selfLink, { page: { 21 | offset: count - pageLimit 22 | }}); 23 | if (pageOffset > 0) { 24 | response.links['prev'] = common.modifyQuery(selfLink, { page: { 25 | offset: (pageOffset - pageLimit < 0) ? 0 : pageOffset - pageLimit 26 | }}); 27 | } 28 | if ((pageOffset + pageLimit) < count) { 29 | response.links['next'] = common.modifyQuery(selfLink, { page: { 30 | offset: pageOffset + pageLimit 31 | }}); 32 | } 33 | }); 34 | }); 35 | } 36 | 37 | function queryParser(req, callback) { 38 | var param = req.query['page']; 39 | var pageInfo = _.pick(param, 'offset', 'limit'); 40 | if (pageInfo.offset !== undefined && pageInfo.limit !== undefined) 41 | callback(pageInfo); 42 | } 43 | 44 | module.exports = paginate; 45 | -------------------------------------------------------------------------------- /lib/filters/select.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var common = require('./common'); 4 | 5 | function select() { 6 | return common.createFilter(queryParser, function(transaction, req, type, fields) { 7 | transaction.subscribe(type, 'view', function(resource, resview) { 8 | fields = ['type', 'id'].concat(fields); 9 | return resview.select(fields); 10 | }); 11 | }); 12 | } 13 | 14 | function queryParser(req, callback) { 15 | var params = req.query['fields']; 16 | if (!_.isArray(params)) params = [params]; 17 | _.each(params, function(param) { 18 | if (_.isString(param)) { 19 | callback(param.split(',')); 20 | } else if (_.isPlainObject(param)) { 21 | _.each(param, function(fields, restype) { 22 | callback(restype, fields.split(',')); 23 | }); 24 | } 25 | }); 26 | } 27 | 28 | module.exports = select; 29 | -------------------------------------------------------------------------------- /lib/filters/sort.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var common = require('./common'); 4 | 5 | function sort() { 6 | return common.createFilter(queryParser, function(transaction, req, type, fields) { 7 | transaction.subscribe(type, 'query', function(resview, query) { 8 | _.each(fields, function(field) { 9 | resview.visitProperties(field.name, function(property) { 10 | var param = _.set({}, property, field.order); 11 | query.sort(param); 12 | }); 13 | }); 14 | }); 15 | }); 16 | } 17 | 18 | function queryParser(req, callback) { 19 | var params = req.query['sort']; 20 | if (!_.isArray(params)) params = [params]; 21 | _.each(params, function(param) { 22 | if (_.isString(param)) { 23 | callback(parseParam(param)); 24 | } else if (_.isPlainObject(param)) { 25 | _.each(param, function(param, restype) { 26 | callback(restype, parseParam(param)); 27 | }); 28 | } 29 | }); 30 | } 31 | 32 | function parseParam(param) { 33 | return _.map(param.split(','), function(string) { 34 | if (_.startsWith(string, '+')) { 35 | return { name: string.slice(1), order: 1 }; 36 | } else if (_.startsWith(string, '-')) { 37 | return { name: string.slice(1), order: -1 }; 38 | } else { 39 | return { name: string, order: 1 }; 40 | } 41 | }); 42 | } 43 | 44 | module.exports = sort; 45 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = exports = { 4 | Link: require('./Link'), 5 | Field: require('./Field'), 6 | Runtime: require('./Runtime'), 7 | Registry: require('./Registry'), 8 | Accessor: require('./Accessor'), 9 | Response: require('./Response'), 10 | Resource: require('./Resource'), 11 | Transaction: require('./Transaction'), 12 | ResourceView: require('./ResourceView'), 13 | middleware: require('./middleware'), 14 | accessors: require('./accessors'), 15 | selectors: require('./selectors'), 16 | filters: require('./filters'), 17 | errors: require('./errors'), 18 | }; 19 | 20 | _.merge(exports, exports.errors); 21 | _.merge(exports, exports.filters); 22 | _.merge(exports, exports.accessors); 23 | _.merge(exports, exports.selectors); 24 | _.merge(exports, exports.middleware); 25 | -------------------------------------------------------------------------------- /lib/middleware/assign.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | 4 | var common = require('./common'); 5 | 6 | var defaultOpts = { 7 | strict: false, 8 | noWait: false, 9 | filters: null, 10 | }; 11 | 12 | function assign(chain, opts) { 13 | opts = _.defaults({}, opts, defaultOpts); 14 | chain = common.parseChain(chain); 15 | var lastLink = chain.pop(); 16 | return middleware; 17 | 18 | function middleware(req, res, next) { 19 | var resource = lastLink.resource; 20 | var transaction = common.initTransaction(resource, res); 21 | _.each(opts.filters, function(filter) { filter(transaction) }); 22 | transaction.notify(resource, 'start', req); 23 | async.waterfall([ 24 | function(next) { 25 | common.applyChain(transaction, chain, req, next); 26 | }, 27 | function(parent, next) { 28 | var selector = lastLink.selector; 29 | var resview = resource.view(transaction); 30 | var filter = common.resolveSelector(selector, req, parent); 31 | resview.findOne(filter, function(err, object) { 32 | if (err) return next(err); 33 | object = object || new resource.model(filter); 34 | next(null, object, resview); 35 | }); 36 | }, 37 | function(object, resview, next) { 38 | resview.deserialize(req.body.data, object, function(err, data) { 39 | err ? next(err) : next(null, data, resview); 40 | }); 41 | }, 42 | ], function(err, object, resview) { 43 | if (err) return next(err); 44 | var response = transaction.response; 45 | if (opts.noWait) { 46 | response.raw.statusCode = 202; 47 | transaction.notify(resource, 'end'); 48 | response.send(null); 49 | object.save(next); 50 | } else { 51 | async.parallel({ 52 | save: function(next) { 53 | object.save(next); 54 | }, 55 | data: function(next) { 56 | resview.serialize(object, next); 57 | }, 58 | }, function(err, results) { 59 | if (err) return next(err); 60 | response.data = results.data; 61 | transaction.notify(resource, 'end'); 62 | response.send(); 63 | next(); 64 | }); 65 | } 66 | }); 67 | } 68 | } 69 | 70 | module.exports = exports = assign; 71 | exports.defaultOptions = defaultOpts; 72 | -------------------------------------------------------------------------------- /lib/middleware/common.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | 4 | var Runtime = require('../Runtime'); 5 | var Response = require('../Response'); 6 | var Transaction = require('../Transaction'); 7 | var ResourceNotFound = require('../errors/ResourceNotFound'); 8 | 9 | function initTransaction(resource, res) { 10 | var assignProperty = 'jsonapify.transaction'; 11 | var transaction = _.get(res, assignProperty); 12 | if (!transaction) { 13 | var response = new Response(res); 14 | transaction = new Transaction(resource, response); 15 | _.set(res, assignProperty, transaction); 16 | } 17 | return transaction; 18 | } 19 | 20 | function parseChain(chain) { 21 | if (!_.isArray(chain)) chain = [chain]; 22 | var chunks = _.chunk(chain, 2); 23 | return _.map(chunks, function(chunk) { 24 | return { 25 | resource: Runtime.getResource(chunk[0]), 26 | selector: chunk[1], 27 | }; 28 | }); 29 | } 30 | 31 | function resolveSelector(selector, req, parent) { 32 | if (_.isFunction(selector)) 33 | selector = { _id: selector }; 34 | return (function iterate(object, result) { 35 | _.each(object, function(value, key) { 36 | if (_.isPlainObject(value) && !_.isEmpty(value)) { 37 | var subobj = {}; 38 | iterate(value, subobj); 39 | _.set(result, key, subobj); 40 | } else if (_.isFunction(value)) { 41 | _.set(result, key, value(req, parent)); 42 | } else { 43 | _.set(result, key, value); 44 | } 45 | }); 46 | return result; 47 | })(selector, {}); 48 | } 49 | 50 | function applyChain(transaction, chain, req, callback) { 51 | return async.reduce(chain, null, function(parent, link, next) { 52 | var resource = link.resource; 53 | var resview = resource.view(transaction); 54 | var filter = resolveSelector(link.selector, req, parent); 55 | resview.findOne(filter, function(err, object) { 56 | if (err) return next(err); 57 | if (!object) return next(new ResourceNotFound(resource, filter)); 58 | next(null, object); 59 | }); 60 | }, callback); 61 | } 62 | 63 | exports.parseChain = parseChain; 64 | exports.initTransaction = initTransaction; 65 | exports.resolveSelector = resolveSelector; 66 | exports.applyChain = applyChain; 67 | -------------------------------------------------------------------------------- /lib/middleware/create.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | 4 | var common = require('./common'); 5 | 6 | var defaultOpts = { 7 | strict: false, 8 | noWait: false, 9 | filters: null, 10 | }; 11 | 12 | function create(chain, opts) { 13 | opts = _.defaults({}, opts, defaultOpts); 14 | chain = common.parseChain(chain); 15 | var lastLink = chain.pop(); 16 | return middleware; 17 | 18 | function middleware(req, res, next) { 19 | var resource = lastLink.resource; 20 | var transaction = common.initTransaction(resource, res); 21 | _.each(opts.filters, function(filter) { filter(transaction) }); 22 | transaction.notify(resource, 'start', req); 23 | async.waterfall([ 24 | function(next) { 25 | common.applyChain(transaction, chain, req, next); 26 | }, 27 | function(parent, next) { 28 | var selector = lastLink.selector; 29 | var filter = common.resolveSelector(selector, req, parent); 30 | var object = new resource.model(filter); 31 | var resview = resource.view(transaction); 32 | resview.deserialize(req.body.data, object, function(err) { 33 | err ? next(err) : next(null, object, resview); 34 | }); 35 | }, 36 | ], function(err, object, resview) { 37 | if (err) return next(err); 38 | var response = transaction.response; 39 | response.links['self'] = req.originalUrl; 40 | if (opts.noWait) { 41 | response.raw.statusCode = 202; 42 | transaction.notify(resource, 'end'); 43 | response.send(null); 44 | object.save(next); 45 | } else { 46 | async.parallel({ 47 | save: function(next) { 48 | object.save(next); 49 | }, 50 | data: function(next) { 51 | resview.serialize(object, next); 52 | }, 53 | }, function(err, results) { 54 | if (err) return next(err); 55 | var data = results.data; 56 | response.data = data; 57 | response.raw.statusCode = 201; 58 | var location = _.get(data, 'links.self'); 59 | if (location) response.raw.set('Location', location); 60 | transaction.notify(resource, 'end'); 61 | response.send(); 62 | next(); 63 | }); 64 | } 65 | }); 66 | } 67 | } 68 | 69 | module.exports = exports = create; 70 | exports.defaultOptions = defaultOpts; 71 | -------------------------------------------------------------------------------- /lib/middleware/enumerate.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | 4 | var common = require('./common'); 5 | var filters = require('../filters'); 6 | 7 | var defaultOpts = { 8 | strict: false, 9 | filters: [ 10 | filters.sort(), 11 | filters.filter(), 12 | filters.select(), 13 | filters.paginateOffset(), 14 | ], 15 | }; 16 | 17 | function enumerate(chain, opts) { 18 | opts = _.defaults({}, opts, defaultOpts); 19 | chain = common.parseChain(chain); 20 | var lastLink = chain.pop(); 21 | return middleware; 22 | 23 | function middleware(req, res, next) { 24 | var resource = lastLink.resource; 25 | var transaction = common.initTransaction(resource, res); 26 | _.each(opts.filters, function(filter) { filter(transaction) }); 27 | transaction.notify(resource, 'start', req); 28 | async.waterfall([ 29 | function(next) { 30 | common.applyChain(transaction, chain, req, next); 31 | }, 32 | function(parent, next) { 33 | var selector = lastLink.selector; 34 | var filter = common.resolveSelector(selector, req, parent); 35 | var resview = resource.view(transaction); 36 | async.parallel({ 37 | data: function(next) { 38 | resview.findMany(filter, function(err, objects) { 39 | if (err) return next(err); 40 | async.map(objects, function(object, next) { 41 | resview.serialize(object, next); 42 | }, next); 43 | }); 44 | }, 45 | count: function(next) { 46 | resview.model.where(filter).count(next); 47 | }, 48 | }, next); 49 | }, 50 | ], function(err, results) { 51 | if (err) return next(err); 52 | var response = transaction.response; 53 | response.meta.count = results.count; 54 | response.links.self = req.originalUrl; 55 | response.data = results.data; 56 | transaction.notify(resource, 'end'); 57 | response.send(); 58 | next(); 59 | }); 60 | } 61 | } 62 | 63 | module.exports = exports = enumerate; 64 | exports.defaultOptions = defaultOpts; 65 | -------------------------------------------------------------------------------- /lib/middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | var common = require('./common'); 2 | var ApiError = require('../errors/ApiError'); 3 | var UnknownError = require('../errors/UnknownError'); 4 | 5 | function errorHandler() { 6 | return middleware; 7 | 8 | function middleware(err, req, res, next) { 9 | var transaction = common.initTransaction(null, res); 10 | var response = transaction.response; 11 | if (!(err instanceof ApiError)) 12 | err = new UnknownError(err); 13 | response.error(err); 14 | response.send(); 15 | } 16 | } 17 | 18 | module.exports = errorHandler; 19 | -------------------------------------------------------------------------------- /lib/middleware/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | read: require('./read'), 3 | create: require('./create'), 4 | assign: require('./assign'), 5 | update: require('./update'), 6 | modify: require('./modify'), 7 | remove: require('./remove'), 8 | enumerate: require('./enumerate'), 9 | errorHandler: require('./errorHandler'), 10 | }; 11 | -------------------------------------------------------------------------------- /lib/middleware/modify.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | var jsonpatch = require('jsonpatch'); 4 | 5 | var common = require('./common'); 6 | 7 | var defaultOpts = { 8 | strict: false, 9 | filters: null, 10 | }; 11 | 12 | function modify(chain, opts) { 13 | opts = _.defaults({}, opts, defaultOpts); 14 | chain = common.parseChain(chain); 15 | return middleware; 16 | 17 | function middleware(req, res, next) { 18 | var resource = _.last(chain).resource; 19 | var transaction = common.initTransaction(resource, res); 20 | _.each(opts.filters, function(filter) { filter(transaction) }); 21 | transaction.notify(resource, 'start', req); 22 | async.waterfall([ 23 | function(next) { 24 | common.applyChain(transaction, chain, req, next); 25 | }, 26 | function(object, next) { 27 | var resview = resource.view(transaction); 28 | resview.serialize(object, function(err, data) { 29 | err ? next(err) : next(null, data, resview, object); 30 | }); 31 | }, 32 | function(data, resview, object, next) { 33 | try { 34 | var result = jsonpatch.apply_patch(data, req.body.data); 35 | resview.deserialize(result, object, function(err) { 36 | err ? next(err) : next(null, object, result); 37 | }); 38 | } catch (err) { return next(err) } 39 | }, 40 | function(object, result, next) { 41 | object.save(function(err) { 42 | err ? next(err) : next(null, result); 43 | }); 44 | }, 45 | ], function(err, resdata) { 46 | if (err) return next(err); 47 | var response = transaction.response; 48 | response.data = resdata; 49 | transaction.notify(resource, 'end'); 50 | response.send(); 51 | next(); 52 | }); 53 | } 54 | } 55 | 56 | module.exports = exports = modify; 57 | exports.defaultOptions = defaultOpts; 58 | -------------------------------------------------------------------------------- /lib/middleware/read.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | 4 | var common = require('./common'); 5 | var filters = require('../filters'); 6 | var ResourceNotFound = require('../errors/ResourceNotFound'); 7 | 8 | var defaultOpts = { 9 | strict: false, 10 | filters: [ 11 | filters.select(), 12 | filters.sort(), 13 | ], 14 | }; 15 | 16 | function read(chain, opts) { 17 | opts = _.defaults({}, opts, defaultOpts); 18 | chain = common.parseChain(chain); 19 | return middleware; 20 | 21 | function middleware(req, res, next) { 22 | var lastLink = _.last(chain); 23 | var resource = lastLink.resource; 24 | var transaction = common.initTransaction(resource, res); 25 | _.each(opts.filters, function(filter) { filter(transaction) }); 26 | transaction.notify(resource, 'start', req); 27 | async.waterfall([ 28 | function(next) { 29 | common.applyChain(transaction, chain, req, next); 30 | }, 31 | function(object, next) { 32 | var resview = resource.view(transaction); 33 | resview.serialize(object, next); 34 | }, 35 | ], function(err, resdata) { 36 | if (err) return next(err); 37 | var response = transaction.response; 38 | response.links.self = req.originalUrl; 39 | response.data = resdata; 40 | transaction.notify(resource, 'end'); 41 | response.send(); 42 | next(); 43 | }); 44 | } 45 | } 46 | 47 | module.exports = exports = read; 48 | exports.defaultOptions = defaultOpts; 49 | -------------------------------------------------------------------------------- /lib/middleware/remove.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | 4 | var common = require('./common'); 5 | 6 | var defaultOpts = { 7 | strict: false, 8 | filters: null, 9 | }; 10 | 11 | function remove(chain, opts) { 12 | opts = _.defaults({}, opts, defaultOpts); 13 | chain = common.parseChain(chain); 14 | return middleware; 15 | 16 | function middleware(req, res, next) { 17 | var resource = _.last(chain).resource; 18 | var transaction = common.initTransaction(resource, res); 19 | _.each(opts.filters, function(filter) { filter(transaction) }); 20 | transaction.notify(resource, 'start', req); 21 | common.applyChain(transaction, chain, req, function(err, object) { 22 | if (err) return next(err); 23 | var response = transaction.response; 24 | if (opts.noWait) { 25 | response.raw.statusCode = 202; 26 | transaction.notify(resource, 'end'); 27 | response.send(null); 28 | object.remove(next); 29 | } else { 30 | object.remove(function(err) { 31 | if (err) return next(err); 32 | response.raw.statusCode = 204; 33 | transaction.notify(resource, 'end'); 34 | response.send(null); 35 | next(); 36 | }); 37 | } 38 | }); 39 | } 40 | } 41 | 42 | module.exports = exports = remove; 43 | exports.defaultOptions = defaultOpts; 44 | -------------------------------------------------------------------------------- /lib/middleware/update.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var async = require('async'); 3 | 4 | var common = require('./common'); 5 | 6 | var defaultOpts = { 7 | strict: false, 8 | noWait: false, 9 | filters: null, 10 | }; 11 | 12 | function update(chain, opts) { 13 | opts = _.defaults({}, opts, defaultOpts); 14 | chain = common.parseChain(chain); 15 | return middleware; 16 | 17 | function middleware(req, res, next) { 18 | var resource = _.last(chain).resource; 19 | var transaction = common.initTransaction(resource, res); 20 | _.each(opts.filters, function(filter) { filter(transaction) }); 21 | transaction.notify(resource, 'start', req); 22 | common.applyChain(transaction, chain, req, function(err, object) { 23 | if (err) return next(err); 24 | var resview = resource.view(transaction); 25 | resview.deserialize(req.body.data, object, function(err) { 26 | if (err) return next(err); 27 | var response = transaction.response; 28 | if (opts.noWait) { 29 | response.raw.statusCode = 202; 30 | transaction.notify(resource, 'end'); 31 | response.send(null); 32 | object.save(next); 33 | } else { 34 | async.parallel({ 35 | save: function(next) { 36 | object.save(next); 37 | }, 38 | data: function(next) { 39 | resview.serialize(object, next); 40 | }, 41 | }, function(err, results) { 42 | if (err) return next(err); 43 | response.data = results.data; 44 | transaction.notify(resource, 'end'); 45 | response.send(); 46 | next(); 47 | }); 48 | } 49 | }); 50 | }); 51 | } 52 | } 53 | 54 | module.exports = exports = update; 55 | exports.defaultOptions = defaultOpts; 56 | -------------------------------------------------------------------------------- /lib/selectors.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | function query(name) { 4 | return function(req, parent) { 5 | return req.query[name]; 6 | }; 7 | } 8 | 9 | function param(name) { 10 | return function(req, parent) { 11 | return req.params[name]; 12 | }; 13 | } 14 | 15 | function parent(name) { 16 | return function(req, parent) { 17 | return _.get(parent, name); 18 | }; 19 | } 20 | 21 | exports.query = query; 22 | exports.param = param; 23 | exports.parent = parent; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonapify", 3 | "version": "1.6.9", 4 | "description": "A library for the development of JSON APIs", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "test-coverage": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/alex94puchades/jsonapify.git" 13 | }, 14 | "keywords": [ 15 | "jsonapi", 16 | "json-api", 17 | "rest", 18 | "api", 19 | "express", 20 | "connect", 21 | "middleware", 22 | "mongoose" 23 | ], 24 | "author": "Alejandro Caravaca Puchades ", 25 | "license": "Apache-2.0", 26 | "bugs": { 27 | "url": "https://github.com/alex94puchades/jsonapify/issues" 28 | }, 29 | "homepage": "https://github.com/alex94puchades/jsonapify#readme", 30 | "devDependencies": { 31 | "chai": "^3.4.0", 32 | "coveralls": "^2.11.4", 33 | "istanbul": "^0.4.0", 34 | "mocha": "^3.2.0", 35 | "node-mocks-http": "^1.5.0", 36 | "qs": "^6.1.0", 37 | "sinon": "^1.17.2", 38 | "sinon-chai": "^2.8.0" 39 | }, 40 | "dependencies": { 41 | "async": "^2.1.4", 42 | "glob2re": "^1.0.1", 43 | "jsonpatch": "^3.0.1", 44 | "lodash": "^3.10.1", 45 | "mongoose": "^4.2.3", 46 | "qs": "^6.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/Field.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | chai.use(require('sinon-chai')); 4 | var expect = chai.expect; 5 | 6 | var common = require('./common'); 7 | var Field = require('../lib/Field'); 8 | var Resource = require('../lib/Resource'); 9 | 10 | describe('Field', function() { 11 | var resource; 12 | before(function() { 13 | resource = new Resource({ type: 'test' }); 14 | }); 15 | 16 | describe('#name', function() { 17 | it('returns the field name', function() { 18 | var expected = 'name'; 19 | var field = new Field(resource, expected, null); 20 | expect(field).to.have.property('name', expected); 21 | }); 22 | }); 23 | 24 | describe('#resource', function() { 25 | it('returns the resource the field is associated with', function() { 26 | var field = new Field(resource, 'name', null); 27 | expect(field).to.have.property('resource', resource); 28 | }); 29 | }); 30 | 31 | describe('#readable', function() { 32 | it('returns true if field is readable', function() { 33 | var field = new Field(resource, 'name', null, { readable: true }); 34 | expect(field).to.have.property('readable', true); 35 | }); 36 | 37 | it('returns false if field is not readable', function() { 38 | var field = new Field(resource, 'name', null, { readable: false }); 39 | expect(field).to.have.property('readable', false); 40 | }); 41 | }); 42 | 43 | describe('#writable', function() { 44 | it('returns true if field is writable', function() { 45 | var field = new Field(resource, 'name', null, { writable: true }); 46 | expect(field).to.have.property('writable', true); 47 | }); 48 | 49 | it('returns false if field is not writable', function() { 50 | var field = new Field(resource, 'name', null, { writable: false }); 51 | expect(field).to.have.property('writable', false); 52 | }); 53 | }); 54 | 55 | describe('#nullable', function() { 56 | it('returns true if field is nullable', function() { 57 | var field = new Field(resource, 'name', null, { nullable: true }); 58 | expect(field).to.have.property('nullable', true); 59 | }); 60 | 61 | it('returns false if field is not nullable', function() { 62 | var field = new Field(resource, 'name', null, { nullable: false }); 63 | expect(field).to.have.property('nullable', false); 64 | }); 65 | }); 66 | 67 | describe('#visitProperties', function() { 68 | it('invokes visitProperties method on accessor', function() { 69 | var callback = sinon.spy(); 70 | var accessor = common.createAccessor(); 71 | accessor.visitProperties.callsArgWith(0, 'property'); 72 | var field = new Field(resource, 'name', accessor); 73 | field.visitProperties(callback); 74 | expect(accessor.visitProperties).to.have.been.calledWith(callback); 75 | expect(callback).to.have.been.calledWith('property'); 76 | }); 77 | }); 78 | 79 | describe('#serialize', function() { 80 | var transaction, object; 81 | beforeEach(function() { 82 | object = {}; 83 | transaction = common.createTransaction(resource); 84 | }); 85 | 86 | it('gives constant value in callback', function(done) { 87 | var expected = 'value'; 88 | var field = new Field(resource, 'name', expected); 89 | field.serialize(transaction, object, function(err, value) { 90 | if (err) return done(err); 91 | expect(value).to.equal(expected); 92 | done(); 93 | }); 94 | }); 95 | 96 | it('invokes serialize method on accessor', function(done) { 97 | var expected = 'value'; 98 | var accessor = common.createAccessor(); 99 | accessor.serialize.callsArgWithAsync(3, null, expected); 100 | var field = new Field(resource, 'name', accessor); 101 | field.serialize(transaction, object, function(err, value) { 102 | if (err) return done(err); 103 | expect(value).to.equal(expected); 104 | expect(accessor.serialize).to.have.been.calledWith(field, transaction, object); 105 | done(); 106 | }); 107 | }); 108 | 109 | it('gives undefined if field is not readable', function(done) { 110 | var field = new Field(resource, 'name', 'value', { readable: false }); 111 | field.serialize(transaction, object, function(err, value) { 112 | if (err) return done(err); 113 | expect(value).to.not.exist; 114 | done(); 115 | }); 116 | }); 117 | }); 118 | 119 | describe('#deserialize', function() { 120 | var transaction, object; 121 | beforeEach(function() { 122 | transaction = common.createTransaction(resource); 123 | object = {}; 124 | }); 125 | 126 | it('invokes callback with output object', function(done) { 127 | var expected = 'value'; 128 | var field = new Field(resource, 'name', expected); 129 | field.deserialize(transaction, expected, object, function(err, output) { 130 | if (err) return done(err); 131 | expect(output).to.equal(object); 132 | expect(object).to.be.empty; 133 | done(); 134 | }); 135 | }); 136 | 137 | it('does not change object if field is not writable', function(done) { 138 | var expected = 'value'; 139 | var field = new Field(resource, 'name', expected, { writable: false }); 140 | field.deserialize(transaction, expected, object, function(err, output) { 141 | if (err) return done(err); 142 | expect(output).to.equal(object); 143 | expect(object).to.be.empty; 144 | done(); 145 | }); 146 | }); 147 | 148 | 149 | it('invokes deserialize method on accessor', function(done) { 150 | var expected = 'value'; 151 | var accessor = common.createAccessor(); 152 | accessor.deserialize.callsArgWithAsync(4, null, object); 153 | var field = new Field(resource, 'name', accessor); 154 | field.deserialize(transaction, expected, object, function(err, output) { 155 | if (err) return done(err); 156 | expect(output).to.equal(object); 157 | expect(accessor.deserialize).to.have.been.calledWith(field, transaction, expected, object); 158 | done(); 159 | }); 160 | }); 161 | 162 | it('gives an error if not expected field value', function(done) { 163 | var field = new Field(resource, 'name', 'value'); 164 | field.deserialize(transaction, 'invalid', object, function(err, output) { 165 | expect(err).to.exist; 166 | expect(object).to.be.empty; 167 | done(); 168 | }); 169 | }); 170 | 171 | it('gives an error if value is undefined for not nullable field', function(done) { 172 | var field = new Field(resource, 'name', 'value'); 173 | field.deserialize(transaction, undefined, object, function(err, output) { 174 | expect(err).to.exist; 175 | expect(object).to.be.empty; 176 | done(); 177 | }); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /test/Link.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var jsonapify = require('../'); 4 | var Link = jsonapify.Link; 5 | 6 | describe('Link', function() { 7 | describe('#href', function() { 8 | it('returns href from link object', function() { 9 | var expected = 'https://jsonapify.js'; 10 | var link = new Link(expected); 11 | expect(link).to.have.property('href', expected); 12 | }); 13 | 14 | it('sets href in link object', function() { 15 | var link = new Link; 16 | var expected = 'https://jsonapify.js'; 17 | link.href = expected; 18 | expect(link).to.have.property('href', expected); 19 | }); 20 | }); 21 | 22 | describe('#meta', function() { 23 | it('returns link meta object', function() { 24 | var link = new Link; 25 | link.meta['name'] = 'value'; 26 | expect(link).to.have.deep.property('meta.name', 'value'); 27 | }); 28 | }); 29 | 30 | describe('#toJSON', function() { 31 | it('returns href directly if no meta object in link', function() { 32 | var expected = 'https://jsonapify.js'; 33 | var link = new Link(expected); 34 | var object = link.toJSON(); 35 | expect(object).to.equal(expected); 36 | }); 37 | 38 | it('returns link object if meta object in link', function() { 39 | var expected = { 40 | href: 'https://jsonapify.org', 41 | meta: { name: 'value' } 42 | }; 43 | var link = new Link(expected.href, expected.meta); 44 | var object = link.toJSON(); 45 | expect(object).to.deep.equal(expected); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/Resource.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | chai.use(require('sinon-chai')); 4 | var mongoose = require('mongoose'); 5 | var httpMocks = require('node-mocks-http'); 6 | var expect = chai.expect; 7 | 8 | var jsonapify = require('../'); 9 | var Resource = jsonapify.Resource; 10 | var ResourceView = jsonapify.ResourceView; 11 | var Transaction = jsonapify.Transaction; 12 | var Response = jsonapify.Response; 13 | 14 | describe('Resource', function() { 15 | var model; 16 | before(function() { 17 | model = mongoose.model('ResourceTest', new mongoose.Schema); 18 | }); 19 | 20 | describe('#model', function() { 21 | it('gives associated model', function() { 22 | var resource = new Resource(model, { type: 'test' }); 23 | expect(resource).to.have.property('model', model); 24 | }); 25 | }); 26 | 27 | describe('#type', function() { 28 | it('gives resource type', function() { 29 | var expected = 'test'; 30 | var resource = new Resource(model, { type: expected }); 31 | expect(resource).to.have.property('type', expected); 32 | }); 33 | }); 34 | 35 | describe('#view', function() { 36 | var resource, transaction; 37 | beforeEach(function() { 38 | var res = httpMocks.createResponse(); 39 | var response = new Response(res); 40 | resource = new Resource(model, { type: 'test' }); 41 | transaction = new Transaction(resource, response); 42 | }); 43 | 44 | it('gives expected resource view', function() { 45 | var resview = resource.view(transaction); 46 | expect(resview).to.be.an.instanceof(ResourceView); 47 | expect(resview).to.have.property('type', resource.type); 48 | expect(resview).to.have.property('model', resource.model); 49 | }); 50 | 51 | it('notifies transaction observers', function() { 52 | var handler = sinon.stub().returnsArg(1); 53 | transaction.subscribe(resource.type, 'view', handler); 54 | var resview = resource.view(transaction); 55 | expect(handler).to.have.been.calledWith(resource, resview); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/ResourceView.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var chai = require('chai'); 3 | var async = require('async'); 4 | var sinon = require('sinon'); 5 | chai.use(require('sinon-chai')); 6 | var mongoose = require('mongoose'); 7 | var httpMocks = require('node-mocks-http'); 8 | var expect = chai.expect; 9 | 10 | var common = require('./common'); 11 | var jsonapify = require('../'); 12 | var Resource = jsonapify.Resource; 13 | var Response = jsonapify.Response; 14 | var Transaction = jsonapify.Transaction; 15 | var ResourceView = jsonapify.ResourceView; 16 | 17 | describe('ResourceView', function() { 18 | var model, response; 19 | before(function(done) { 20 | mongoose.connect('mongodb://localhost/test', function(err) { 21 | if (err) return done(err); 22 | model = mongoose.model('ResourceViewTest', new mongoose.Schema); 23 | var res = httpMocks.createResponse(); 24 | response = new Response(res); 25 | done(); 26 | }); 27 | }); 28 | 29 | after(function(done) { 30 | mongoose.disconnect(done); 31 | }); 32 | 33 | describe('#model', function() { 34 | it('gives resource model', function() { 35 | var resource = new Resource(model, { type: 'test' }); 36 | var transaction = new Transaction(resource, response); 37 | var resview = resource.view(transaction); 38 | expect(resview).to.have.property('model', resource.model); 39 | }); 40 | }); 41 | 42 | describe('#type', function() { 43 | it('gives resource type', function() { 44 | var resource = new Resource({ type: 'test' }); 45 | var transaction = new Transaction(resource, response); 46 | var resview = resource.view(transaction); 47 | expect(resview).to.have.property('type', resource.type); 48 | }); 49 | }); 50 | 51 | describe('#field', function() { 52 | it('gives selected field', function() { 53 | var resource = new Resource({ type: 'test', name: 'value' }); 54 | var transaction = new Transaction(resource, response); 55 | var resview = resource.view(transaction); 56 | var field = resview.field('name'); 57 | expect(field).to.have.property('name', 'name'); 58 | expect(field).to.have.property('resource', resource); 59 | }); 60 | 61 | it('gives attribute field', function() { 62 | var resource = new Resource({ 63 | type: 'test', 64 | attributes: { 65 | name: 'value', 66 | }, 67 | }); 68 | var transaction = new Transaction(resource, response); 69 | var resview = resource.view(transaction); 70 | var field = resview.field('name'); 71 | expect(field).to.have.property('name', 'attributes.name'); 72 | expect(field).to.have.property('resource', resource); 73 | }); 74 | 75 | it('gives relationship field', function() { 76 | var resource = new Resource({ 77 | type: 'test', 78 | relationships: { 79 | name: 'value', 80 | }, 81 | }); 82 | var transaction = new Transaction(resource, response); 83 | var resview = resource.view(transaction); 84 | var field = resview.field('name'); 85 | expect(field).to.have.property('name', 'relationships.name'); 86 | expect(field).to.have.property('resource', resource); 87 | }); 88 | 89 | it('gives null if invalid field', function() { 90 | var resource = new Resource({ type: 'test' }); 91 | var transaction = new Transaction(resource, response); 92 | var resview = resource.view(transaction); 93 | var field = resview.field('invalid'); 94 | expect(field).to.not.exist; 95 | }); 96 | }); 97 | 98 | describe('#select', function() { 99 | it('includes only specified fields in view', function() { 100 | var resource = new Resource({ 101 | type: 'test', 102 | attributes: { 103 | selected: 'value', 104 | 'not-selected': 'value', 105 | }, 106 | }); 107 | var transaction = new Transaction(resource, response); 108 | var resview = resource.view(transaction).select('selected'); 109 | expect(resview.field('selected')).to.exist; 110 | expect(resview.field('not-selected')).to.not.exist; 111 | }); 112 | }); 113 | 114 | describe('#findOne', function() { 115 | var resource, transaction, object; 116 | before(function() { 117 | resource = new Resource(model, { type: 'test' }); 118 | transaction = new Transaction(resource, response); 119 | }); 120 | 121 | beforeEach(function(done) { 122 | model.create({}, function(err, result) { 123 | if (err) return done(err); 124 | object = result; 125 | done(); 126 | }); 127 | }); 128 | 129 | afterEach(function(done) { 130 | mongoose.connection.db.dropDatabase(done); 131 | }); 132 | 133 | it('retrieves mongoose document from the database', function(done) { 134 | var resview = resource.view(transaction); 135 | resview.findOne({ _id: object._id }, function(err, data) { 136 | if (err) return done(err); 137 | expect(data).to.have.property('id'); 138 | expect(data._id).to.satisfy(function(id) { 139 | return id.equals(object._id); 140 | }); 141 | done(); 142 | }); 143 | }); 144 | 145 | it('runs transaction query handlers', function() { 146 | var handler = sinon.stub().returnsArg(1); 147 | transaction.subscribe(resource.type, 'query', handler); 148 | var resview = resource.view(transaction); 149 | var query = resview.findOne({ _id: object._id }); 150 | expect(handler).to.have.been.calledWith(resview, query); 151 | }); 152 | }); 153 | 154 | describe('#findMany', function() { 155 | var resource, transaction, objects; 156 | before(function() { 157 | resource = new Resource(model, { type: 'test' }); 158 | transaction = new Transaction(resource, response); 159 | }); 160 | 161 | beforeEach(function(done) { 162 | async.parallel([ 163 | function(next) { model.create({}, next); }, 164 | function(next) { model.create({}, next); }, 165 | function(next) { model.create({}, next); }, 166 | ], function(err, results) { 167 | if (err) return done(err); 168 | objects = results; 169 | done(); 170 | }); 171 | }); 172 | 173 | afterEach(function(done) { 174 | mongoose.connection.db.dropDatabase(done); 175 | }); 176 | 177 | it('retrieves mongoose documents from the database', function(done) { 178 | var resview = resource.view(transaction); 179 | resview.findMany({}, function(err, results) { 180 | if (err) return done(err); 181 | expect(results).to.have.length(objects.length); 182 | _.each(results, function(result) { 183 | var obj = _.find(objects, function(obj) { 184 | return obj._id.equals(result._id); 185 | }); 186 | expect(obj).to.exist; 187 | }); 188 | done(); 189 | }); 190 | }); 191 | 192 | it('runs transaction query handlers', function() { 193 | var handler = sinon.stub().returnsArg(1); 194 | transaction.subscribe(resource.type, 'query', handler); 195 | var resview = resource.view(transaction); 196 | var query = resview.findMany({}); 197 | expect(handler).to.have.been.calledWith(resview, query); 198 | }); 199 | }); 200 | 201 | describe('#serialize', function() { 202 | it('invokes serialize method on field', function(done) { 203 | var object = {}; 204 | var accessor = common.createAccessor(); 205 | common.initAccessor(accessor, 'value', object); 206 | var resource = new Resource({ type: 'test', field: accessor }); 207 | var transaction = new Transaction(resource, response); 208 | var resview = resource.view(transaction); 209 | resview.serialize(object, function(err, resdata) { 210 | if (err) return done(err); 211 | expect(accessor.serialize).to.have.been.called.once; 212 | done(); 213 | }); 214 | }); 215 | }); 216 | 217 | describe('#deserialize', function() { 218 | it('invokes deserialize method on field', function(done) { 219 | var object = {}; 220 | var accessor = common.createAccessor(); 221 | var resdata = { type: 'test', field: 'value' }; 222 | common.initAccessor(accessor, undefined, object); 223 | var resource = new Resource({ type: 'test', field: accessor }); 224 | var transaction = new Transaction(resource, response); 225 | var resview = resource.view(transaction); 226 | resview.deserialize(resdata, object, function(err, resdata) { 227 | if (err) return done(err); 228 | expect(accessor.deserialize).to.have.been.called.once; 229 | done(); 230 | }); 231 | }); 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /test/Response.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var mongoose = require('mongoose'); 3 | var ObjectId = mongoose.Types.ObjectId; 4 | var httpMocks = require('node-mocks-http'); 5 | 6 | var jsonapify = require('../'); 7 | var Response = jsonapify.Response; 8 | var HttpError = jsonapify.errors.HttpError; 9 | 10 | describe('Response', function() { 11 | var res, response; 12 | beforeEach(function() { 13 | res = httpMocks.createResponse(); 14 | response = new Response(res); 15 | }) 16 | 17 | describe('#raw', function() { 18 | it('returns http response object', function() { 19 | expect(response).to.have.property('raw', res); 20 | }); 21 | }); 22 | 23 | describe('#data', function() { 24 | it('sets response data', function() { 25 | var expected = 'data'; 26 | response.data = expected; 27 | response.send(); 28 | var resdata = JSON.parse(res._getData()); 29 | expect(resdata).to.have.property('data', expected); 30 | }); 31 | 32 | it('returns response data', function() { 33 | var expected = 'data'; 34 | response.data = expected; 35 | expect(response).to.have.property('data', expected); 36 | }); 37 | }); 38 | 39 | describe('#meta', function() { 40 | it('returns response meta object', function() { 41 | response.meta['name'] = 'value'; 42 | expect(response).to.have.deep.property('meta.name', 'value'); 43 | }); 44 | }); 45 | 46 | describe('#links', function() { 47 | it('returns response links object', function() { 48 | var value = 'https://jsonapify.js'; 49 | response.links['name'] = value; 50 | expect(response).to.have.deep.property('links.name', value); 51 | }); 52 | }); 53 | 54 | describe('#errors', function() { 55 | it('returns response errors object', function() { 56 | var httpError = new HttpError(500); 57 | response.errors.push(httpError); 58 | response.send(); 59 | var resdata = JSON.parse(res._getData()); 60 | expect(resdata).to.have.property('errors').with.length(1); 61 | expect(resdata.errors[0]).to.deep.equal(httpError.toJSON()); 62 | }); 63 | }); 64 | 65 | describe('#error', function() { 66 | it('adds error to response errors', function() { 67 | var httpError = new HttpError(500); 68 | response.error(httpError); 69 | expect(response).to.have.property('errors').with.length(1); 70 | expect(response.errors[0]).to.equal(httpError); 71 | }); 72 | }); 73 | 74 | describe('#include', function() { 75 | it('adds object to response included objects', function() { 76 | var expected = { type: 'test', id: new ObjectId, data: 'value' }; 77 | response.include(expected.type, expected.id, expected); 78 | response.send(null); 79 | var resdata = JSON.parse(res._getData()); 80 | expect(resdata).to.have.property('included').with.length(1); 81 | var included = resdata.included[0]; 82 | expect(included).to.have.property('id'); 83 | expect(included.id).to.satisfy(function(id) { 84 | return expected.id.equals(id); 85 | }); 86 | expect(included).to.have.property('type', expected.type); 87 | expect(included).to.have.property('data', expected.data); 88 | }); 89 | 90 | it('retrieves object from response included objects', function() { 91 | var expected = { type: 'test', id: new ObjectId, data: 'value' }; 92 | response.include(expected.type, expected.id, expected); 93 | var include = response.include(expected.type, expected.id); 94 | expect(include).to.have.property('id'); 95 | expect(include.id).to.satisfy(function(id) { 96 | return expected.id.equals(id); 97 | }); 98 | expect(include).to.have.property('type', expected.type); 99 | expect(include).to.have.property('data', expected.data); 100 | }); 101 | }); 102 | 103 | describe('#included', function() { 104 | it('returns response included objects', function() { 105 | var expected = { type: 'test', id: new ObjectId, data: 'value' }; 106 | response.include(expected.type, expected.id, expected); 107 | expect(response).to.have.property('included').with.length(1); 108 | var include = response.included[0]; 109 | expect(include).to.have.property('id'); 110 | expect(include.id).to.satisfy(function(id) { 111 | return expected.id.equals(id); 112 | }); 113 | expect(include).to.have.property('type', expected.type); 114 | expect(include).to.have.property('data', expected.data); 115 | }); 116 | }); 117 | 118 | describe('#send', function() { 119 | it('sets response data object', function() { 120 | var expected = 'data'; 121 | response.send(expected); 122 | var resdata = JSON.parse(res._getData()); 123 | expect(resdata).to.have.property('data', expected); 124 | }); 125 | 126 | it('sets error code as http status only if all errors are equal', function() { 127 | var httpError = new HttpError(404); 128 | response.error(httpError).send(); 129 | expect(res.statusCode).to.equal(httpError.status); 130 | }); 131 | 132 | it('sets appropiate status if there are different errors', function() { 133 | response.error(new HttpError(401)); 134 | response.error(new HttpError(404)); 135 | response.error(new HttpError(415)); 136 | response.send(); 137 | expect(res.statusCode).to.not.equal(200); 138 | }); 139 | 140 | it('omits meta object if empty', function() { 141 | response.send(null); 142 | var resdata = JSON.parse(res._getData()); 143 | expect(resdata).to.not.have.property('meta'); 144 | }); 145 | 146 | it('omits links object if empty', function() { 147 | response.send(null); 148 | var resdata = JSON.parse(res._getData()); 149 | expect(resdata).to.not.have.property('links'); 150 | }); 151 | 152 | it('omits errors object if empty', function() { 153 | response.send(null); 154 | var resdata = JSON.parse(res._getData()); 155 | expect(resdata).to.not.have.property('errors'); 156 | }); 157 | 158 | it('omits data if there is any error', function() { 159 | response.error(new HttpError(500)).send(null); 160 | var resdata = JSON.parse(res._getData()); 161 | expect(resdata).to.not.have.property('data'); 162 | expect(resdata).to.have.property('errors').with.length(1); 163 | }); 164 | 165 | it('appends jsonapi version info', function() { 166 | response.send(null); 167 | var resdata = JSON.parse(res._getData()); 168 | expect(resdata).to.have.property('jsonapi'); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/Transaction.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var httpMocks = require('node-mocks-http'); 4 | chai.use(require('sinon-chai')); 5 | var expect = chai.expect; 6 | 7 | var jsonapify = require('../'); 8 | var Resource = jsonapify.Resource; 9 | var Response = jsonapify.Response; 10 | var Transaction = jsonapify.Transaction; 11 | 12 | describe('Transaction', function() { 13 | var resource, response, transaction; 14 | before(function() { 15 | resource = new Resource({ type: 'test' }); 16 | }); 17 | 18 | beforeEach(function() { 19 | var res = httpMocks.createResponse(); 20 | response = new Response(res); 21 | transaction = new Transaction(resource, response); 22 | }); 23 | 24 | describe('#subscribe', function() { 25 | it('handler is called for subscribed events', function() { 26 | var handler = sinon.stub().returns(true); 27 | transaction.subscribe('test', 'event', handler); 28 | var expected = 'value'; 29 | var handled = transaction.notify(resource, 'event', expected); 30 | expect(handler).to.have.been.calledWith(resource, expected); 31 | expect(handled).to.be.true; 32 | }); 33 | 34 | it('handler is not called for events not subscribed to', function() { 35 | var handler = sinon.stub().returns(true); 36 | transaction.subscribe('test', 'event', handler); 37 | var handled = transaction.notify(resource, 'other'); 38 | expect(handler).to.not.have.been.called; 39 | expect(handled).to.be.false; 40 | }); 41 | }); 42 | 43 | describe('#unsubscribe', function() { 44 | it('handler is not called for unsubscribed event anymore', function() { 45 | var handler = sinon.stub().returns(true); 46 | transaction.subscribe('test', 'event', handler); 47 | transaction.unsubscribe('test', 'event', handler); 48 | var handled = transaction.notify(resource, 'event'); 49 | expect(handler).to.not.have.been.called; 50 | expect(handled).to.be.false; 51 | }); 52 | }); 53 | 54 | describe('#notify', function() { 55 | it('returns true if event was handled', function() { 56 | var handler = sinon.stub().returns(true); 57 | transaction.subscribe('test', 'event', handler); 58 | var expected = 'value'; 59 | var handled = transaction.notify(resource, 'event', expected); 60 | expect(handler).to.have.been.calledWith(resource, expected); 61 | expect(handled).to.be.true; 62 | }); 63 | 64 | it('returns false if event was not handled', function() { 65 | var handled = transaction.notify(resource, 'event'); 66 | expect(handled).to.be.false; 67 | }); 68 | }); 69 | 70 | describe('#transform', function() { 71 | it('returns value transformed by subscribed handlers', function() { 72 | var expected = 'result'; 73 | var handler = sinon.stub().returns(expected); 74 | transaction.subscribe('test', 'transform', handler); 75 | var result = transaction.transform(resource, 'transform', 'value'); 76 | expect(handler).to.have.been.calledWith(resource, 'value'); 77 | expect(result).to.equal(expected); 78 | }); 79 | 80 | it('leading params are preserved', function() { 81 | var expected = 'result'; 82 | var handler = sinon.stub().returns(expected); 83 | transaction.subscribe('test', 'transform', handler); 84 | var result = transaction.transform(resource, 'transform', 'param', 'value'); 85 | expect(handler).to.have.been.calledWith(resource, 'param', 'value'); 86 | expect(result).to.equal(expected); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/accessors/Const.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var httpMocks = require('node-mocks-http'); 3 | 4 | var jsonapify = require('../../'); 5 | var Field = jsonapify.Field; 6 | var Resource = jsonapify.Resource; 7 | var Response = jsonapify.Response; 8 | var Const = jsonapify.accessors.Const; 9 | var Transaction = jsonapify.Transaction; 10 | var InvalidFieldValue = jsonapify.errors.InvalidFieldValue; 11 | 12 | describe('Const', function() { 13 | var resource, transaction; 14 | beforeEach(function() { 15 | var res = httpMocks.createResponse(); 16 | resource = new Resource({ type: 'test' }); 17 | var response = new Response(resource, res); 18 | transaction = new Transaction(resource, response); 19 | }); 20 | 21 | describe('#serialize', function() { 22 | var object; 23 | beforeEach(function() { 24 | object = {}; 25 | }); 26 | 27 | it('invokes callback with given value', function(done) { 28 | var expected = 'value'; 29 | var accessor = new Const(expected); 30 | var field = new Field(resource, 'name', accessor); 31 | accessor.serialize(field, transaction, object, function(err, value) { 32 | if (err) return done(err); 33 | expect(value).to.equal(value); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | describe('#deserialize', function() { 40 | var object; 41 | beforeEach(function() { 42 | object = {}; 43 | }); 44 | 45 | it('does not change resource object', function(done) { 46 | var value = 'value'; 47 | var accessor = new Const(value); 48 | var field = new Field(resource, 'name', accessor); 49 | accessor.deserialize(field, transaction, value, object, function(err, output) { 50 | if (err) return done(err); 51 | expect(output).to.equal(object); 52 | expect(object).to.be.empty 53 | done(); 54 | }); 55 | }); 56 | 57 | it('gives an error if invalid value given', function(done) { 58 | var accessor = new Const('expected'); 59 | var field = new Field(resource, 'name', accessor); 60 | accessor.deserialize(field, transaction, 'invalid', object, function(err, output) { 61 | expect(err).to.be.an.instanceof(InvalidFieldValue); 62 | expect(object).to.be.empty; 63 | done(); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/accessors/Property.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var expect = require('chai').expect; 3 | var httpMocks = require('node-mocks-http'); 4 | 5 | var jsonapify = require('../../'); 6 | var Field = jsonapify.Field; 7 | var Resource = jsonapify.Resource; 8 | var Response = jsonapify.Response; 9 | var Transaction = jsonapify.Transaction; 10 | var Property = jsonapify.accessors.Property; 11 | 12 | describe('Property', function() { 13 | var resource, transaction; 14 | beforeEach(function() { 15 | var res = httpMocks.createResponse(); 16 | var response = new Response(resource, res); 17 | resource = new Resource({ type: 'test' }); 18 | transaction = new Transaction(resource, response); 19 | }); 20 | 21 | describe('#serialize', function() { 22 | it('returns property from object', function(done) { 23 | var expected = 'value'; 24 | var property = 'this.is.a.property'; 25 | var object = _.set({}, property, expected); 26 | var accessor = new Property(property); 27 | var field = new Field(resource, 'name', accessor); 28 | accessor.serialize(field, transaction, object, function(err, value) { 29 | if (err) return done(err); 30 | expect(value).to.equal(expected); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | describe('#deserialize', function() { 37 | var object; 38 | beforeEach(function() { 39 | object = {}; 40 | }); 41 | 42 | it('sets value as object property', function(done) { 43 | var expected = 'value'; 44 | var property = 'this.is.a.property'; 45 | var accessor = new Property(property); 46 | var field = new Field(resource, 'name', accessor); 47 | accessor.deserialize(field, transaction, expected, object, function(err, output) { 48 | if (err) return done(err); 49 | expect(output).to.equal(object); 50 | expect(object).to.have.deep.property(property, expected); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/accessors/Ref.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var mongoose = require('mongoose'); 3 | var ObjectId = mongoose.Types.ObjectId; 4 | var httpMocks = require('node-mocks-http'); 5 | 6 | var jsonapify = require('../../'); 7 | var Field = jsonapify.Field; 8 | var Runtime = jsonapify.Runtime; 9 | var Resource = jsonapify.Resource; 10 | var Response = jsonapify.Response; 11 | var Transaction = jsonapify.Transaction; 12 | var Ref = jsonapify.accessors.Ref; 13 | var Property = jsonapify.accessors.Property; 14 | var InvalidFieldValue = jsonapify.errors.InvalidFieldValue; 15 | 16 | describe('Ref', function() { 17 | var linkedModel, linkedResource, resource, transaction; 18 | before(function(done) { 19 | mongoose.connect('mongodb://localhost/test', function(err) { 20 | if (err) return done(err); 21 | linkedModel = mongoose.model('RefTest', new mongoose.Schema); 22 | done(); 23 | }); 24 | }); 25 | 26 | beforeEach(function() { 27 | var res = httpMocks.createResponse(); 28 | resource = new Resource({ type: 'test' }); 29 | Runtime.addResource('Test', resource); 30 | var response = new Response(resource, res); 31 | transaction = new Transaction(resource, response); 32 | linkedResource = new Resource(linkedModel, { 33 | type: 'linked', 34 | id: new Property('_id'), 35 | }); 36 | Runtime.addResource('Linked', linkedResource); 37 | }); 38 | 39 | afterEach(function(done) { 40 | mongoose.connection.db.dropDatabase(done); 41 | Runtime.removeResource('Test'); 42 | Runtime.removeResource('Linked'); 43 | }); 44 | 45 | after(function(done) { 46 | mongoose.disconnect(done); 47 | }); 48 | 49 | describe('#serialize', function() { 50 | it('invokes callback with resource link', function(done) { 51 | linkedModel.create({}, function(err, linked) { 52 | if (err) return done(err); 53 | var object = { link: linked._id }; 54 | var accessor = new Ref('Linked', 'link'); 55 | var field = new Field(resource, 'name', accessor); 56 | accessor.serialize(field, transaction, object, function(err, resdata) { 57 | if (err) return done(err); 58 | expect(resdata).to.have.property('id'); 59 | expect(resdata.id).to.satisfy(function(id) { 60 | return linked._id.equals(id); 61 | }); 62 | expect(resdata).to.have.property('type', 'linked'); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | 68 | it('includes linked resource in response', function(done) { 69 | linkedModel.create({}, function(err, linked) { 70 | if (err) return done(err); 71 | var object = { link: linked._id }; 72 | var accessor = new Ref('Linked', 'link'); 73 | var field = new Field(resource, 'name', accessor); 74 | accessor.serialize(field, transaction, object, function(err, resdata) { 75 | if (err) return done(err); 76 | var response = transaction.response; 77 | expect(response.included).to.have.length(1); 78 | expect(response.included[0]).to.have.property('id'); 79 | expect(response.included[0].id).to.satisfy(function(id) { 80 | return linked._id.equals(id); 81 | }); 82 | expect(response.included[0]).to.have.property('type', 'linked'); 83 | done(); 84 | }); 85 | }); 86 | }); 87 | }); 88 | 89 | describe('#deserialize', function() { 90 | var object; 91 | beforeEach(function() { 92 | object = {}; 93 | }); 94 | 95 | it('sets document property from resource field', function(done) { 96 | var id = new ObjectId; 97 | var accessor = new Ref('Linked', 'link'); 98 | var field = new Field(resource, 'name', accessor); 99 | var resdata = { type: 'linked', id: id.toString() }; 100 | accessor.deserialize(field, transaction, resdata, object, function(err, output) { 101 | if (err) return done(err); 102 | expect(output).to.equal(object); 103 | expect(object).to.have.property('link'); 104 | expect(object.link).to.satisfy(function(id) { 105 | return id.equals(id); 106 | }); 107 | done(); 108 | }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/accessors/Refs.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var expect = require('chai').expect; 3 | var mongoose = require('mongoose'); 4 | var ObjectId = mongoose.Types.ObjectId; 5 | var httpMocks = require('node-mocks-http'); 6 | 7 | var jsonapify = require('../../'); 8 | var Field = jsonapify.Field; 9 | var Runtime = jsonapify.Runtime; 10 | var Resource = jsonapify.Resource; 11 | var Response = jsonapify.Response; 12 | var Transaction = jsonapify.Transaction; 13 | var Refs = jsonapify.accessors.Refs; 14 | var Property = jsonapify.accessors.Property; 15 | var InvalidFieldValue = jsonapify.errors.InvalidFieldValue; 16 | 17 | describe('Refs', function() { 18 | var linkedModel, linkedResource, resource, transaction; 19 | before(function(done) { 20 | mongoose.connect('mongodb://localhost/test', function(err) { 21 | if (err) return done(err); 22 | linkedModel = mongoose.model('RefsTest', new mongoose.Schema); 23 | done(); 24 | }); 25 | }); 26 | 27 | beforeEach(function() { 28 | var res = httpMocks.createResponse(); 29 | resource = new Resource({ type: 'test' }); 30 | Runtime.addResource('Test', resource); 31 | var response = new Response(resource, res); 32 | transaction = new Transaction(resource, response); 33 | linkedResource = new Resource(linkedModel, { 34 | type: 'linked', 35 | id: new Property('_id'), 36 | }); 37 | Runtime.addResource('Linked', linkedResource); 38 | }); 39 | 40 | afterEach(function(done) { 41 | mongoose.connection.db.dropDatabase(done); 42 | Runtime.removeResource('Test'); 43 | Runtime.removeResource('Linked'); 44 | }); 45 | 46 | after(function(done) { 47 | mongoose.disconnect(done); 48 | }); 49 | 50 | describe('#serialize', function() { 51 | it('invokes callback with resource link', function(done) { 52 | linkedModel.create({}, function(err, linked) { 53 | if (err) return done(err); 54 | var object = { links: [linked._id] }; 55 | var accessor = new Refs('Linked', 'links'); 56 | var field = new Field(resource, 'name', accessor); 57 | accessor.serialize(field, transaction, object, function(err, resdata) { 58 | if (err) return done(err); 59 | _.each(resdata, function(link) { 60 | expect(link).to.have.property('id'); 61 | expect(link.id).to.satisfy(function(id) { 62 | return linked._id.equals(id); 63 | }); 64 | expect(link).to.have.property('type', 'linked'); 65 | }); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | 71 | it('includes linked resource in response', function(done) { 72 | linkedModel.create({}, function(err, linked) { 73 | if (err) return done(err); 74 | var object = { links: [linked._id] }; 75 | var accessor = new Refs('Linked', 'links'); 76 | var field = new Field(resource, 'name', accessor); 77 | accessor.serialize(field, transaction, object, function(err, resdata) { 78 | if (err) return done(err); 79 | var response = transaction.response; 80 | expect(response.included).to.have.length(1); 81 | expect(response.included[0]).to.have.property('id'); 82 | expect(response.included[0].id).to.satisfy(function(id) { 83 | return linked._id.equals(id); 84 | }); 85 | expect(response.included[0]).to.have.property('type', 'linked'); 86 | done(); 87 | }); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('#deserialize', function() { 93 | var object; 94 | beforeEach(function() { 95 | object = {}; 96 | }); 97 | 98 | it('sets document property from resource field', function(done) { 99 | var linkedId = new ObjectId; 100 | var accessor = new Refs('Linked', 'links'); 101 | var field = new Field(resource, 'name', accessor); 102 | var resdata = [{ type: 'linked', id: linkedId.toString() }]; 103 | accessor.deserialize(field, transaction, resdata, object, function(err, output) { 104 | if (err) return done(err); 105 | expect(output).to.equal(object); 106 | expect(object).to.have.property('links'); 107 | _.each(object.links, function(id) { 108 | expect(id).to.satisfy(function(id) { 109 | return id.equals(linkedId); 110 | }); 111 | }); 112 | done(); 113 | }); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/accessors/Template.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var httpMocks = require('node-mocks-http'); 3 | 4 | var jsonapify = require('../../'); 5 | var Field = jsonapify.Field; 6 | var Resource = jsonapify.Resource; 7 | var Response = jsonapify.Response; 8 | var Transaction = jsonapify.Transaction; 9 | var Template = jsonapify.accessors.Template; 10 | var InvalidFieldValue = jsonapify.errors.InvalidFieldValue; 11 | 12 | describe('Template', function() { 13 | var resource, transaction; 14 | beforeEach(function() { 15 | var res = httpMocks.createResponse(); 16 | var response = new Response(res); 17 | resource = new Resource({ type: 'test' }); 18 | transaction = new Transaction(resource, response); 19 | }); 20 | 21 | describe('#serialize', function() { 22 | it('applies template with object fields', function(done) { 23 | var object = { expected: 'expected', value: 'value' }; 24 | var accessor = new Template('${expected} ${value}'); 25 | var field = new Field(resource, accessor); 26 | accessor.serialize(field, transaction, object, function(err, resdata) { 27 | if (err) return done(err); 28 | expect(resdata).to.equal('expected value'); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('#deserialize', function() { 35 | var object; 36 | beforeEach(function() { 37 | object = {}; 38 | }); 39 | 40 | it('does not change object', function(done) { 41 | var accessor = new Template('${name}'); 42 | var field = new Field(resource, accessor); 43 | accessor.deserialize(field, transaction, 'value', object, function(err, output) { 44 | if (err) return done(err); 45 | expect(output).to.equal(object); 46 | expect(object).to.be.empty; 47 | done(); 48 | }); 49 | }); 50 | 51 | it('gives an error if value is not string', function(done) { 52 | var accessor = new Template('${name}'); 53 | var field = new Field(resource, accessor); 54 | accessor.deserialize(field, transaction, ['invalid'], object, function(err, output) { 55 | expect(err).to.be.an.instanceof(InvalidFieldValue); 56 | expect(output).to.not.exist; 57 | done(); 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'); 2 | var httpMocks = require('node-mocks-http'); 3 | 4 | var Accessor = require('../lib/Accessor'); 5 | var Response = require('../lib/Response'); 6 | var Transaction = require('../lib/Transaction'); 7 | 8 | function createAccessor() { 9 | var accessor = new Accessor; 10 | sinon.stub(accessor, 'serialize'); 11 | sinon.stub(accessor, 'deserialize'); 12 | sinon.stub(accessor, 'visitProperties'); 13 | return accessor; 14 | } 15 | 16 | function initAccessor(accessor, value, object, property) { 17 | accessor.serialize.callsArgWithAsync(3, null, value); 18 | accessor.deserialize.callsArgWithAsync(4, null, object); 19 | accessor.visitProperties.callsArgWithAsync(0, property); 20 | } 21 | 22 | function createTransaction(resource) { 23 | var res = httpMocks.createResponse(); 24 | var response = new Response(res); 25 | return new Transaction(resource, response); 26 | } 27 | 28 | exports.initAccessor = initAccessor; 29 | exports.createAccessor = createAccessor; 30 | exports.createTransaction = createTransaction; 31 | -------------------------------------------------------------------------------- /test/filters/filter.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var chai = require('chai'); 3 | var async = require('async'); 4 | var mongoose = require('mongoose'); 5 | var httpMocks = require('node-mocks-http'); 6 | var expect = chai.expect; 7 | 8 | var common = require('../common'); 9 | var jsonapify = require('../../'); 10 | var Resource = jsonapify.Resource; 11 | var Response = jsonapify.Response; 12 | var Transaction = jsonapify.Transaction; 13 | var Property = jsonapify.accessors.Property; 14 | 15 | describe('filter', function() { 16 | var resource, transaction; 17 | before(function(done) { 18 | mongoose.connect('mongodb://localhost/test', function(err) { 19 | if (err) return done(err); 20 | model = mongoose.model('FilterTest', new mongoose.Schema({ 21 | number: Number, 22 | string: String, 23 | })); 24 | done(); 25 | }); 26 | }); 27 | 28 | beforeEach(function() { 29 | resource = new Resource(model, { 30 | number: { 31 | value: new Property('number'), 32 | nullable: true, 33 | }, 34 | string: { 35 | value: new Property('string'), 36 | nullable: true, 37 | }, 38 | }); 39 | var res = httpMocks.createResponse(); 40 | var response = new Response(res); 41 | transaction = new Transaction(resource, response); 42 | jsonapify.filters.filter()(transaction); 43 | }); 44 | 45 | afterEach(function(done) { 46 | mongoose.connection.db.dropDatabase(done); 47 | }); 48 | 49 | after(function(done) { 50 | mongoose.disconnect(done); 51 | }); 52 | 53 | describe('eqFilter', function() { 54 | it('gives only results whose fields equal value', function(done) { 55 | async.parallel([ 56 | function(next) { model.create({ number: 0 }, next) }, 57 | function(next) { model.create({ number: 1 }, next) }, 58 | ], function(err, objects) { 59 | if (err) return done(err); 60 | var req = httpMocks.createRequest({ 61 | query: { filter: { number: '= 0' }}, 62 | }); 63 | transaction.notify(resource, 'start', req); 64 | var resview = resource.view(transaction); 65 | resview.findMany({}, function(err, results) { 66 | if (err) return done(err); 67 | _.each(results, function(object) { 68 | expect(object).to.have.property('number', 0); 69 | }); 70 | var response = transaction.response; 71 | transaction.notify(resource, 'end'); 72 | done(); 73 | }); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('neFilter', function() { 79 | it('gives only results whose fields do not equal value', function(done) { 80 | async.parallel([ 81 | function(next) { model.create({ number: 0 }, next) }, 82 | function(next) { model.create({ number: 1 }, next) }, 83 | ], function(err, objects) { 84 | if (err) return done(err); 85 | var req = httpMocks.createRequest({ 86 | query: { filter: { number: '!= 0' }}, 87 | }); 88 | transaction.notify(resource, 'start', req); 89 | var resview = resource.view(transaction); 90 | resview.findMany({}, function(err, results) { 91 | if (err) return done(err); 92 | _.each(results, function(object) { 93 | expect(object).to.have.property('number').not.equal(0); 94 | }); 95 | var response = transaction.response; 96 | transaction.notify(resource, 'end'); 97 | done(); 98 | }); 99 | }); 100 | }); 101 | }); 102 | 103 | describe('gtFilter', function() { 104 | it('gives only results whose fields are greater than value', function(done) { 105 | async.parallel([ 106 | function(next) { model.create({ number: -1 }, next) }, 107 | function(next) { model.create({ number: 0 }, next) }, 108 | function(next) { model.create({ number: 1 }, next) }, 109 | ], function(err, objects) { 110 | if (err) return done(err); 111 | var req = httpMocks.createRequest({ 112 | query: { filter: { number: '> 0' }}, 113 | }); 114 | transaction.notify(resource, 'start', req); 115 | var resview = resource.view(transaction); 116 | resview.findMany({}, function(err, results) { 117 | if (err) return done(err); 118 | _.each(results, function(object) { 119 | expect(object).to.have.property('number').gt(0); 120 | }); 121 | var response = transaction.response; 122 | transaction.notify(resource, 'end'); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | }); 128 | 129 | describe('geFilter', function() { 130 | it('gives only results whose fields are greater or equal than value', function(done) { 131 | async.parallel([ 132 | function(next) { model.create({ number: -1 }, next) }, 133 | function(next) { model.create({ number: 0 }, next) }, 134 | function(next) { model.create({ number: 1 }, next) }, 135 | ], function(err, objects) { 136 | if (err) return done(err); 137 | var req = httpMocks.createRequest({ 138 | query: { filter: { number: '>= 0' }}, 139 | }); 140 | transaction.notify(resource, 'start', req); 141 | var resview = resource.view(transaction); 142 | resview.findMany({}, function(err, results) { 143 | if (err) return done(err); 144 | _.each(results, function(object) { 145 | expect(object).to.have.property('number').gte(0); 146 | }); 147 | var response = transaction.response; 148 | transaction.notify(resource, 'end'); 149 | done(); 150 | }); 151 | }); 152 | }); 153 | }); 154 | 155 | describe('ltFilter', function() { 156 | it('gives only results whose fields are lesser than value', function(done) { 157 | async.parallel([ 158 | function(next) { model.create({ number: -1 }, next) }, 159 | function(next) { model.create({ number: 0 }, next) }, 160 | function(next) { model.create({ number: 1 }, next) }, 161 | ], function(err, objects) { 162 | if (err) return done(err); 163 | var req = httpMocks.createRequest({ 164 | query: { filter: { number: '< 0' }}, 165 | }); 166 | transaction.notify(resource, 'start', req); 167 | var resview = resource.view(transaction); 168 | resview.findMany({}, function(err, results) { 169 | if (err) return done(err); 170 | _.each(results, function(object) { 171 | expect(object).to.have.property('number').lt(0); 172 | }); 173 | var response = transaction.response; 174 | transaction.notify(resource, 'end'); 175 | done(); 176 | }); 177 | }); 178 | }); 179 | }); 180 | 181 | describe('leFilter', function() { 182 | it('gives only results whose fields are greater or equal than value', function(done) { 183 | async.parallel([ 184 | function(next) { model.create({ number: -1 }, next) }, 185 | function(next) { model.create({ number: 0 }, next) }, 186 | function(next) { model.create({ number: 1 }, next) }, 187 | ], function(err, objects) { 188 | if (err) return done(err); 189 | var req = httpMocks.createRequest({ 190 | query: { filter: { number: '<= 0' }}, 191 | }); 192 | transaction.notify(resource, 'start', req); 193 | var resview = resource.view(transaction); 194 | resview.findMany({}, function(err, results) { 195 | if (err) return done(err); 196 | _.each(results, function(object) { 197 | expect(object).to.have.property('number').lte(0); 198 | }); 199 | var response = transaction.response; 200 | transaction.notify(resource, 'end'); 201 | done(); 202 | }); 203 | }); 204 | }); 205 | }); 206 | 207 | describe('reFilter', function() { 208 | it('gives only results whose fields match regexp', function(done) { 209 | async.parallel([ 210 | function(next) { model.create({ string: 'alice' }, next) }, 211 | function(next) { model.create({ string: 'bob' }, next) }, 212 | ], function(err, objects) { 213 | if (err) return done(err); 214 | var req = httpMocks.createRequest({ 215 | query: { filter: { string: '=~ /^a/' }}, 216 | }); 217 | transaction.notify(resource, 'start', req); 218 | var resview = resource.view(transaction); 219 | resview.findMany({}, function(err, results) { 220 | if (err) return done(err); 221 | _.each(results, function(object) { 222 | expect(object).to.have.property('string').match(/^a/); 223 | }); 224 | var response = transaction.response; 225 | transaction.notify(resource, 'end'); 226 | done(); 227 | }); 228 | }); 229 | }); 230 | }); 231 | 232 | describe('strMatchFilter', function() { 233 | it('gives only results whose fields match expression', function(done) { 234 | async.parallel([ 235 | function(next) { model.create({ string: 'aac12' }, next) }, 236 | function(next) { model.create({ string: 'abc34' }, next) }, 237 | function(next) { model.create({ string: 'acc56' }, next) }, 238 | ], function(err, objects) { 239 | if (err) return done(err); 240 | var req = httpMocks.createRequest({ 241 | query: { filter: { string: 'a?c*' }}, 242 | }); 243 | transaction.notify(resource, 'start', req); 244 | var resview = resource.view(transaction); 245 | resview.findMany({}, function(err, results) { 246 | if (err) return done(err); 247 | _.each(results, function(object) { 248 | expect(object).to.have.property('string'); 249 | expect(object.string).to.match(/^a.c/); 250 | }); 251 | var response = transaction.response; 252 | transaction.notify(resource, 'end'); 253 | done(); 254 | }); 255 | }); 256 | }); 257 | }); 258 | }); 259 | -------------------------------------------------------------------------------- /test/filters/paginate.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var chai = require('chai'); 3 | var async = require('async'); 4 | chai.use(require('sinon-chai')); 5 | var mongoose = require('mongoose'); 6 | var httpMocks = require('node-mocks-http'); 7 | var qs = require('qs'); 8 | var expect = chai.expect; 9 | 10 | var common = require('../common'); 11 | var jsonapify = require('../../'); 12 | var Resource = jsonapify.Resource; 13 | var Response = jsonapify.Response; 14 | var Transaction = jsonapify.Transaction; 15 | 16 | describe('paginate', function() { 17 | var model, resource, transaction, objects; 18 | before(function(done) { 19 | mongoose.connect('mongodb://localhost/test', function(err) { 20 | if (err) return done(err); 21 | model = mongoose.model('PaginateTest', new mongoose.Schema); 22 | done(); 23 | }); 24 | }); 25 | 26 | beforeEach(function(done) { 27 | resource = new Resource(model, { type: 'test' }); 28 | async.parallel([ 29 | function(next) { model.create({}, next); }, 30 | function(next) { model.create({}, next); }, 31 | function(next) { model.create({}, next); }, 32 | function(next) { model.create({}, next); }, 33 | function(next) { model.create({}, next); }, 34 | ], function(err, results) { 35 | if (err) return done(err); 36 | objects = results; 37 | var res = httpMocks.createResponse(); 38 | var response = new Response(res); 39 | transaction = new Transaction(resource, response); 40 | done(); 41 | }); 42 | }); 43 | 44 | afterEach(function(done) { 45 | mongoose.connection.db.dropDatabase(done); 46 | }); 47 | 48 | after(function(done) { 49 | mongoose.disconnect(done); 50 | }); 51 | 52 | it('paginates resources and adds pagination links', function(done) { 53 | var query = { page: { number: 2, size: 2 }}; 54 | var req = httpMocks.createRequest({ 55 | url: '/test?' + qs.stringify(query), 56 | query: query 57 | }); 58 | jsonapify.filters.paginate()(transaction, req); 59 | transaction.notify(resource, 'start', req); 60 | var resview = resource.view(transaction); 61 | resview.findMany({}, function(err, results) { 62 | if (err) return done(err); 63 | expect(results).to.have.length(2); 64 | var response = transaction.response; 65 | response.meta['count'] = objects.length; 66 | transaction.notify(resource, 'end'); 67 | expect(response).to.have.deep.property('links.first', '/test?page[number]=1&page[size]=2'); 68 | expect(response).to.have.deep.property('links.last', '/test?page[number]=3&page[size]=2'); 69 | expect(response).to.have.deep.property('links.prev', '/test?page[number]=1&page[size]=2'); 70 | expect(response).to.have.deep.property('links.next', '/test?page[number]=3&page[size]=2'); 71 | done(); 72 | }); 73 | }); 74 | 75 | it('omits prev link if first page selected', function(done) { 76 | var query = { page: { number: 1, size: 2 }}; 77 | var req = httpMocks.createRequest({ 78 | url: '/test?' + qs.stringify(query), 79 | query: query 80 | }); 81 | jsonapify.filters.paginate()(transaction, req); 82 | transaction.notify(resource, 'start', req); 83 | var resview = resource.view(transaction); 84 | resview.findMany({}, function(err, results) { 85 | if (err) return done(err); 86 | expect(results).to.have.length(2); 87 | var response = transaction.response; 88 | response.meta['count'] = objects.length; 89 | transaction.notify(resource, 'end'); 90 | expect(response).to.have.deep.property('links.first', '/test?page[number]=1&page[size]=2'); 91 | expect(response).to.have.deep.property('links.last', '/test?page[number]=3&page[size]=2'); 92 | expect(response).to.not.have.deep.property('links.prev'); 93 | expect(response).to.have.deep.property('links.next', '/test?page[number]=2&page[size]=2'); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('omits next link if last page selected', function(done) { 99 | var query = { page: { number: objects.length - 2, size: 2 }}; 100 | var req = httpMocks.createRequest({ 101 | url: '/test?' + qs.stringify(query), 102 | query: query 103 | }); 104 | jsonapify.filters.paginate()(transaction, req); 105 | transaction.notify(resource, 'start', req); 106 | var resview = resource.view(transaction); 107 | resview.findMany({}, function(err, results) { 108 | if (err) return done(err); 109 | expect(results).to.have.length.of.most(2); 110 | var response = transaction.response; 111 | response.meta['count'] = objects.length; 112 | transaction.notify(resource, 'end'); 113 | expect(response).to.have.deep.property('links.first', '/test?page[number]=1&page[size]=2'); 114 | expect(response).to.have.deep.property('links.last', '/test?page[number]=3&page[size]=2'); 115 | expect(response).to.have.deep.property('links.prev', '/test?page[number]=2&page[size]=2'); 116 | expect(response).to.not.have.deep.property('links.next'); 117 | done(); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/filters/paginateOffset.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var chai = require('chai'); 3 | var async = require('async'); 4 | chai.use(require('sinon-chai')); 5 | var mongoose = require('mongoose'); 6 | var httpMocks = require('node-mocks-http'); 7 | var qs = require('qs'); 8 | var expect = chai.expect; 9 | 10 | var common = require('../common'); 11 | var jsonapify = require('../../'); 12 | var Resource = jsonapify.Resource; 13 | var Response = jsonapify.Response; 14 | var Transaction = jsonapify.Transaction; 15 | 16 | describe('paginateOffset', function() { 17 | var model, resource, transaction, objects; 18 | before(function(done) { 19 | mongoose.connect('mongodb://localhost/test', function(err) { 20 | if (err) return done(err); 21 | model = mongoose.model('PaginateOffsetTest', new mongoose.Schema); 22 | done(); 23 | }); 24 | }); 25 | 26 | beforeEach(function(done) { 27 | resource = new Resource(model, { type: 'test' }); 28 | async.parallel([ 29 | function(next) { model.create({}, next); }, 30 | function(next) { model.create({}, next); }, 31 | function(next) { model.create({}, next); }, 32 | function(next) { model.create({}, next); }, 33 | function(next) { model.create({}, next); }, 34 | ], function(err, results) { 35 | if (err) return done(err); 36 | objects = results; 37 | var res = httpMocks.createResponse(); 38 | var response = new Response(res); 39 | transaction = new Transaction(resource, response); 40 | done(); 41 | }); 42 | }); 43 | 44 | afterEach(function(done) { 45 | mongoose.connection.db.dropDatabase(done); 46 | }); 47 | 48 | after(function(done) { 49 | mongoose.disconnect(done); 50 | }); 51 | 52 | it('paginates resources and adds pagination links', function(done) { 53 | var query = { page: { offset: 1, limit: 2 }}; 54 | var req = httpMocks.createRequest({ 55 | url: '/test?' + qs.stringify(query), 56 | query: query 57 | }); 58 | jsonapify.filters.paginateOffset()(transaction, req); 59 | transaction.notify(resource, 'start', req); 60 | var resview = resource.view(transaction); 61 | resview.findMany({}, function(err, results) { 62 | if (err) return done(err); 63 | expect(results).to.have.length(2); 64 | var response = transaction.response; 65 | response.meta['count'] = objects.length; 66 | transaction.notify(resource, 'end'); 67 | expect(response).to.have.deep.property('links.first', '/test?page[offset]=0&page[limit]=2'); 68 | expect(response).to.have.deep.property('links.last', '/test?page[offset]=3&page[limit]=2'); 69 | expect(response).to.have.deep.property('links.prev', '/test?page[offset]=0&page[limit]=2'); 70 | expect(response).to.have.deep.property('links.next', '/test?page[offset]=3&page[limit]=2'); 71 | done(); 72 | }); 73 | }); 74 | 75 | it('omits prev link if first page selected', function(done) { 76 | var query = { page: { offset: 0, limit: 2 }}; 77 | var req = httpMocks.createRequest({ 78 | url: '/test?' + qs.stringify(query), 79 | query: query 80 | }); 81 | jsonapify.filters.paginateOffset()(transaction, req); 82 | transaction.notify(resource, 'start', req); 83 | var resview = resource.view(transaction); 84 | resview.findMany({}, function(err, results) { 85 | if (err) return done(err); 86 | expect(results).to.have.length(2); 87 | var response = transaction.response; 88 | response.meta['count'] = objects.length; 89 | transaction.notify(resource, 'end'); 90 | expect(response).to.have.deep.property('links.first', '/test?page[offset]=0&page[limit]=2'); 91 | expect(response).to.have.deep.property('links.last', '/test?page[offset]=3&page[limit]=2'); 92 | expect(response).to.not.have.deep.property('links.prev'); 93 | expect(response).to.have.deep.property('links.next', '/test?page[offset]=2&page[limit]=2'); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('omits next link if last page selected', function(done) { 99 | var query = { page: { offset: objects.length - 2, limit: 2 }}; 100 | var req = httpMocks.createRequest({ 101 | url: '/test?' + qs.stringify(query), 102 | query: query 103 | }); 104 | jsonapify.filters.paginateOffset()(transaction, req); 105 | transaction.notify(resource, 'start', req); 106 | var resview = resource.view(transaction); 107 | resview.findMany({}, function(err, results) { 108 | if (err) return done(err); 109 | expect(results).to.have.length.of.most(2); 110 | var response = transaction.response; 111 | response.meta['count'] = objects.length; 112 | transaction.notify(resource, 'end'); 113 | expect(response).to.have.deep.property('links.first', '/test?page[offset]=0&page[limit]=2'); 114 | expect(response).to.have.deep.property('links.last', '/test?page[offset]=3&page[limit]=2'); 115 | expect(response).to.have.deep.property('links.prev', '/test?page[offset]=1&page[limit]=2'); 116 | expect(response).to.not.have.deep.property('links.next'); 117 | done(); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/filters/select.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var chai = require('chai'); 3 | chai.use(require('sinon-chai')); 4 | var mongoose = require('mongoose'); 5 | var ObjectId = mongoose.Types.ObjectId; 6 | var httpMocks = require('node-mocks-http'); 7 | var expect = chai.expect; 8 | 9 | var common = require('../common'); 10 | var jsonapify = require('../../'); 11 | var Resource = jsonapify.Resource; 12 | var Response = jsonapify.Response; 13 | var Transaction = jsonapify.Transaction; 14 | 15 | describe('select', function() { 16 | var resource, accessors, transaction; 17 | beforeEach(function() { 18 | accessors = { 19 | id: common.createAccessor(), 20 | selected: common.createAccessor(), 21 | notSelected: common.createAccessor(), 22 | }; 23 | resource = new Resource({ 24 | type: 'test', 25 | id: accessors.id, 26 | selected: accessors.selected, 27 | 'not-selected': accessors.notSelected, 28 | }); 29 | var res = httpMocks.createResponse(); 30 | var response = new Response(res); 31 | transaction = new Transaction(resource, response); 32 | common.initAccessor(accessors.id, new ObjectId); 33 | common.initAccessor(accessors.selected, 'value'); 34 | common.initAccessor(accessors.notSelected, 'value'); 35 | jsonapify.filters.select()(transaction); 36 | }); 37 | 38 | it('makes resource views contain only selected fields', function(done) { 39 | var req = httpMocks.createRequest({ query: { fields: ['selected'] }}); 40 | transaction.notify(resource, 'start', req); 41 | var resview = resource.view(transaction); 42 | resview.serialize({}, function(err, resdata) { 43 | if (err) return done(err); 44 | expect(accessors.selected.serialize).to.have.been.called.once; 45 | expect(accessors.notSelected.serialize).to.not.have.been.called; 46 | done(); 47 | }); 48 | }); 49 | 50 | it('type and id fields are included implicitly', function(done) { 51 | var req = httpMocks.createRequest({ query: { fields: [] }}); 52 | transaction.notify(resource, 'start', req); 53 | var resview = resource.view(transaction); 54 | resview.serialize({}, function(err, resdata) { 55 | if (err) return done(err); 56 | expect(resdata).to.have.property('id'); 57 | expect(resdata).to.have.property('type', 'test'); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('selects a given resource type fields', function(done) { 63 | var req = httpMocks.createRequest({ 64 | query: { fields: { test: 'selected' }}, 65 | }); 66 | transaction.notify(resource, 'start', req); 67 | var resview = resource.view(transaction); 68 | resview.serialize({}, function(err, resdata) { 69 | if (err) return done(err); 70 | expect(accessors.selected.serialize).to.have.been.called.once; 71 | expect(accessors.notSelected.serialize).to.not.have.been.called; 72 | done(); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/filters/sort.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var chai = require('chai'); 3 | var async = require('async'); 4 | chai.use(require('sinon-chai')); 5 | var mongoose = require('mongoose'); 6 | var httpMocks = require('node-mocks-http'); 7 | var expect = chai.expect; 8 | 9 | var common = require('../common'); 10 | var jsonapify = require('../../'); 11 | var Resource = jsonapify.Resource; 12 | var Response = jsonapify.Response; 13 | var Transaction = jsonapify.Transaction; 14 | var Property = jsonapify.accessors.Property; 15 | 16 | describe('sort', function() { 17 | var model, resource, transaction, objects; 18 | before(function(done) { 19 | mongoose.connect('mongodb://localhost/test', function(err) { 20 | if (err) return done(err); 21 | model = mongoose.model('SortTest', new mongoose.Schema({ 22 | orderBy: Number, 23 | })); 24 | done(); 25 | }); 26 | }); 27 | 28 | beforeEach(function(done) { 29 | resource = new Resource(model, { 30 | type: 'test', 31 | 'order-by': new Property('orderBy'), 32 | }); 33 | async.parallel([ 34 | function(next) { model.create({ orderBy: 1 }, next); }, 35 | function(next) { model.create({ orderBy: 0 }, next); }, 36 | function(next) { model.create({ orderBy: 2 }, next); }, 37 | ], function(err, results) { 38 | if (err) return done(err); 39 | objects = results; 40 | var res = httpMocks.createResponse(); 41 | var response = new Response(res); 42 | transaction = new Transaction(resource, response); 43 | jsonapify.filters.sort()(transaction); 44 | done(); 45 | }); 46 | }); 47 | 48 | afterEach(function(done) { 49 | mongoose.connection.db.dropDatabase(done); 50 | }); 51 | 52 | after(function(done) { 53 | mongoose.disconnect(done); 54 | }); 55 | 56 | it('sorts resources by ascending selected field', function(done) { 57 | var req = httpMocks.createRequest({ query: { sort: 'order-by' }}); 58 | transaction.notify(resource, 'start', req); 59 | var resview = resource.view(transaction); 60 | resview.findMany({}, function(err, results) { 61 | if (err) return done(err); 62 | expect(results).to.have.length(objects.length); 63 | expect(results[0]).to.have.property('orderBy', 0); 64 | expect(results[1]).to.have.property('orderBy', 1); 65 | expect(results[2]).to.have.property('orderBy', 2); 66 | done(); 67 | }); 68 | }); 69 | 70 | it('sorts resources by descending selected field', function(done) { 71 | var req = httpMocks.createRequest({ query: { sort: '-order-by' }}); 72 | transaction.notify(resource, 'start', req); 73 | var resview = resource.view(transaction); 74 | resview.findMany({}, function(err, results) { 75 | if (err) return done(err); 76 | expect(results).to.have.length(objects.length); 77 | expect(results[0]).to.have.property('orderBy', 2); 78 | expect(results[1]).to.have.property('orderBy', 1); 79 | expect(results[2]).to.have.property('orderBy', 0); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('sorts a given resource type by ascending selected field', function(done) { 85 | var req = httpMocks.createRequest({ 86 | query: { sort: { test: 'order-by' }}, 87 | }); 88 | transaction.notify(resource, 'start', req); 89 | var resview = resource.view(transaction); 90 | resview.findMany({}, function(err, results) { 91 | if (err) return done(err); 92 | expect(results).to.have.length(objects.length); 93 | expect(results[0]).to.have.property('orderBy', 0); 94 | expect(results[1]).to.have.property('orderBy', 1); 95 | expect(results[2]).to.have.property('orderBy', 2); 96 | done(); 97 | }); 98 | }); 99 | 100 | it('sorts a given resource type by descending selected field', function(done) { 101 | var req = httpMocks.createRequest({ 102 | query: { sort: { test: '-order-by' }}, 103 | }); 104 | transaction.notify(resource, 'start', req); 105 | var resview = resource.view(transaction); 106 | resview.findMany({}, function(err, results) { 107 | if (err) return done(err); 108 | expect(results).to.have.length(objects.length); 109 | expect(results[0]).to.have.property('orderBy', 2); 110 | expect(results[1]).to.have.property('orderBy', 1); 111 | expect(results[2]).to.have.property('orderBy', 0); 112 | done(); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/middleware/assign.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | chai.use(require('sinon-chai')); 4 | var mongoose = require('mongoose'); 5 | var httpMocks = require('node-mocks-http'); 6 | var expect = chai.expect; 7 | 8 | var common = require('../common'); 9 | var jsonapify = require('../../'); 10 | 11 | var Runtime = jsonapify.Runtime; 12 | var Resource = jsonapify.Resource; 13 | var assign = jsonapify.middleware.assign; 14 | var InvalidFieldValue = jsonapify.errors.InvalidFieldValue; 15 | 16 | describe('assign', function() { 17 | var model, resource, accessor, res; 18 | before(function(done) { 19 | mongoose.connect('mongodb://localhost/test', function(err) { 20 | if (err) return done(err); 21 | model = mongoose.model('AssignTest', new mongoose.Schema({ 22 | num: Number, 23 | field: String, 24 | })); 25 | done(); 26 | }); 27 | }); 28 | 29 | beforeEach(function() { 30 | accessor = common.createAccessor(); 31 | resource = new Resource(model, { type: 'test', field: accessor }); 32 | Runtime.addResource('AssignResource', resource); 33 | res = httpMocks.createResponse(); 34 | }); 35 | 36 | afterEach(function(done) { 37 | Runtime.removeResource('AssignResource'); 38 | mongoose.connection.db.dropDatabase(done); 39 | }); 40 | 41 | after(function(done) { 42 | mongoose.disconnect(done); 43 | }); 44 | 45 | it('creates resource if it does not exist and sends back resource data', function(done) { 46 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 47 | accessor.deserialize.callsArgWithAsync(4, null); 48 | var req = httpMocks.createRequest({ 49 | params: { num: 12345 }, 50 | body: { data: { type: 'test', field: 'value' }}, 51 | }); 52 | assign(['AssignResource', { num: jsonapify.param('num') }])(req, res, function(err) { 53 | if (err) return done(err); 54 | expect(accessor.serialize).to.have.been.called.once; 55 | expect(accessor.deserialize).to.have.been.called.once; 56 | model.findOne({ num: 12345 }, function(err, object) { 57 | if (err) return done(err); 58 | expect(object).to.exist; 59 | done(); 60 | }); 61 | }); 62 | }); 63 | 64 | it('updates resource if it already exists and sends back resource data', function(done) { 65 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 66 | accessor.deserialize.callsArgWithAsync(4, null); 67 | model.create({ num: 12345, field: 'before' }, function(err, object) { 68 | if (err) return done(err); 69 | var req = httpMocks.createRequest({ 70 | params: { num: object.num }, 71 | body: { data: { type: 'test', field: 'after' }}, 72 | }); 73 | assign(['AssignResource', { num: jsonapify.param('num') }])(req, res, function(err) { 74 | if (err) return done(err); 75 | expect(accessor.serialize).to.have.been.called.once; 76 | expect(accessor.deserialize).to.have.been.called.once; 77 | model.findById(object._id, function(err, object) { 78 | if (err) return done(err); 79 | expect(object).to.have.property('num', req.params.num); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | }); 85 | 86 | it('invokes transaction filters', function(done) { 87 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 88 | accessor.deserialize.callsArgWithAsync(4, null); 89 | var req = httpMocks.createRequest({ 90 | params: { num: 12345 }, 91 | body: { data: { type: 'test', field: 'value' }}, 92 | }); 93 | var filter = sinon.spy(); 94 | var chain = ['AssignResource', { num: jsonapify.param('num') }]; 95 | assign(chain, { filters: [filter] })(req, res, function(err) { 96 | if (err) return done(err); 97 | expect(filter).to.have.been.called.once; 98 | done(); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/middleware/create.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | chai.use(require('sinon-chai')); 4 | var mongoose = require('mongoose'); 5 | var httpMocks = require('node-mocks-http'); 6 | var expect = chai.expect; 7 | 8 | var common = require('../common'); 9 | var jsonapify = require('../../'); 10 | 11 | var Runtime = jsonapify.Runtime; 12 | var Resource = jsonapify.Resource; 13 | var create = jsonapify.middleware.create; 14 | var InvalidFieldValue = jsonapify.errors.InvalidFieldValue; 15 | 16 | describe('create', function() { 17 | var model, resource, accessor, res; 18 | before(function(done) { 19 | mongoose.connect('mongodb://localhost/test', function(err) { 20 | if (err) return done(err); 21 | model = mongoose.model('CreateTest', new mongoose.Schema); 22 | done(); 23 | }); 24 | }); 25 | 26 | beforeEach(function() { 27 | accessor = common.createAccessor(); 28 | resource = new Resource(model, { type: 'test', field: accessor }); 29 | Runtime.addResource('CreateResource', resource); 30 | res = httpMocks.createResponse(); 31 | }); 32 | 33 | afterEach(function(done) { 34 | Runtime.removeResource('CreateResource'); 35 | mongoose.connection.db.dropDatabase(done); 36 | }); 37 | 38 | after(function(done) { 39 | mongoose.disconnect(done); 40 | }); 41 | 42 | it('creates resource and sends back resource data', function(done) { 43 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 44 | accessor.deserialize.callsArgWithAsync(4, null); 45 | var req = httpMocks.createRequest({ 46 | body: { data: { type: 'test', field: 'value' }} 47 | }); 48 | create('CreateResource')(req, res, function(err) { 49 | if (err) return done(err); 50 | expect(accessor.serialize).to.have.been.called.once; 51 | expect(accessor.deserialize).to.have.been.called.once; 52 | model.count(function(err, count) { 53 | if (err) return done(err); 54 | expect(count).to.equal(1); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | 60 | it('sends an error if trying to create resource with wrong type', function(done) { 61 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 62 | accessor.deserialize.callsArgWithAsync(4, null); 63 | var req = httpMocks.createRequest({ 64 | body: { data: { type: 'invalid', field: 'value' }} 65 | }); 66 | create('CreateResource')(req, res, function(err) { 67 | expect(err).to.be.an.instanceof(InvalidFieldValue); 68 | model.find(function(err, results) { 69 | if (err) return done(err); 70 | expect(results).to.be.empty; 71 | done(); 72 | }); 73 | }); 74 | }); 75 | 76 | it('invokes transaction filters', function(done) { 77 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 78 | accessor.deserialize.callsArgWithAsync(4, null); 79 | var req = httpMocks.createRequest({ 80 | body: { data: { type: 'test', field: 'value' }}, 81 | }); 82 | var filter = sinon.spy(); 83 | create('CreateResource', { filters: [filter] })(req, res, function(err) { 84 | if (err) return done(err); 85 | expect(filter).to.have.been.called.once; 86 | done(); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/middleware/enumerate.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var chai = require('chai'); 3 | var sinon = require('sinon'); 4 | var async = require('async'); 5 | chai.use(require('sinon-chai')); 6 | var mongoose = require('mongoose'); 7 | var httpMocks = require('node-mocks-http'); 8 | var expect = chai.expect; 9 | 10 | var common = require('../common'); 11 | var jsonapify = require('../../'); 12 | 13 | var Runtime = jsonapify.Runtime; 14 | var Resource = jsonapify.Resource; 15 | var enumerate = jsonapify.middleware.enumerate; 16 | 17 | describe('enumerate', function() { 18 | var model, resource, accessor, req, res, objects; 19 | before(function(done) { 20 | mongoose.connect('mongodb://localhost/test', function(err) { 21 | if (err) return done(err); 22 | model = mongoose.model('EnumerateTest', new mongoose.Schema); 23 | done(); 24 | }); 25 | }); 26 | 27 | beforeEach(function(done) { 28 | req = httpMocks.createRequest(); 29 | res = httpMocks.createResponse(); 30 | accessor = common.createAccessor(); 31 | resource = new Resource(model, { type: 'test', field: accessor }); 32 | Runtime.addResource('EnumResource', resource); 33 | async.parallel([ 34 | function(next) { model.create({}, next); }, 35 | function(next) { model.create({}, next); }, 36 | function(next) { model.create({}, next); }, 37 | ], function(err, results) { 38 | if (err) return done(err); 39 | objects = results; 40 | done(); 41 | }); 42 | }); 43 | 44 | afterEach(function(done) { 45 | Runtime.removeResource('EnumResource'); 46 | mongoose.connection.db.dropDatabase(done); 47 | }); 48 | 49 | after(function(done) { 50 | mongoose.disconnect(done); 51 | }); 52 | 53 | it('responds with an array of resources', function(done) { 54 | common.initAccessor(accessor, 'value', null); 55 | enumerate('EnumResource')(req, res, function(err) { 56 | if (err) return done(err); 57 | expect(accessor.serialize).to.have.been.called.thrice; 58 | expect(accessor.deserialize).to.not.have.been.called; 59 | done(); 60 | }); 61 | }); 62 | 63 | it('invokes transaction filters', function(done) { 64 | var filter = sinon.spy(); 65 | common.initAccessor(accessor, 'value', null); 66 | enumerate('EnumResource', { filters: [filter] })(req, res, function(err) { 67 | if (err) return done(err); 68 | expect(filter).to.have.been.called.once; 69 | done(); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | chai.use(require('sinon-chai')); 4 | var httpMocks = require('node-mocks-http'); 5 | var expect = chai.expect; 6 | 7 | var jsonapify = require('../../'); 8 | var ApiError = jsonapify.errors.HttpError; 9 | var UnknownError = jsonapify.errors.UnknownError; 10 | var errorHandler = jsonapify.middleware.errorHandler; 11 | 12 | describe('errorHandler', function() { 13 | var req, res, next; 14 | beforeEach(function() { 15 | req = httpMocks.createRequest(); 16 | res = httpMocks.createResponse(); 17 | next = sinon.spy(); 18 | }); 19 | 20 | it('sends errors in response', function() { 21 | var err = new Error; 22 | errorHandler()(err, req, res, next); 23 | var resdata = res._getData(); 24 | resdata = JSON.parse(resdata); 25 | expect(resdata).to.have.property('errors').with.length(1); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/middleware/modify.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | chai.use(require('sinon-chai')); 4 | var mongoose = require('mongoose'); 5 | var httpMocks = require('node-mocks-http'); 6 | var ObjectId = mongoose.Types.ObjectId; 7 | var expect = chai.expect; 8 | 9 | var common = require('../common'); 10 | var jsonapify = require('../../'); 11 | 12 | var Runtime = jsonapify.Runtime; 13 | var Resource = jsonapify.Resource; 14 | var modify = jsonapify.middleware.modify; 15 | 16 | describe('modify', function() { 17 | var model, resource, accessors, res; 18 | before(function(done) { 19 | mongoose.connect('mongodb://localhost/test', function(err) { 20 | if (err) return done(err); 21 | model = mongoose.model('ModifyTest', new mongoose.Schema); 22 | done(); 23 | }); 24 | }); 25 | 26 | beforeEach(function() { 27 | accessors = { 28 | foo: new jsonapify.Accessor, 29 | field: common.createAccessor(), 30 | output: common.createAccessor(), 31 | }; 32 | resource = new Resource(model, { 33 | type: 'test', 34 | field: { 35 | value: accessors.field, 36 | nullable: true, 37 | }, 38 | output: { 39 | value: accessors.output, 40 | nullable: true, 41 | }, 42 | }); 43 | Runtime.addResource('ModifyResource', resource); 44 | res = httpMocks.createResponse(); 45 | }); 46 | 47 | afterEach(function(done) { 48 | Runtime.removeResource('ModifyResource'); 49 | mongoose.connection.db.dropDatabase(done); 50 | }); 51 | 52 | after(function(done) { 53 | mongoose.disconnect(done); 54 | }); 55 | 56 | describe('add', function() { 57 | it('inserts element in array at index', function(done) { 58 | model.create({}, function(err, object) { 59 | if (err) return done(err); 60 | common.initAccessor(accessors.field, ['a', 'c']); 61 | common.initAccessor(accessors.output, undefined, object); 62 | var req = httpMocks.createRequest({ 63 | params: { id: object._id.toString() }, 64 | body: { 65 | data: [{ 66 | op: 'add', 67 | path: '/field/1', 68 | value: 'b', 69 | }], 70 | }, 71 | }); 72 | var chain = ['ModifyResource', jsonapify.param('id')]; 73 | modify(chain)(req, res, function(err) { 74 | if (err) return done(err); 75 | var resdata = JSON.parse(res._getData()); 76 | expect(resdata).to.have.deep.property('data.field'); 77 | expect(resdata.data.field).to.deep.equal(['a','b','c']); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | 83 | it('adds new property to object', function(done) { 84 | model.create({}, function(err, object) { 85 | if (err) return done(err); 86 | common.initAccessor(accessors.field); 87 | common.initAccessor(accessors.output); 88 | var req = httpMocks.createRequest({ 89 | params: { id: object._id.toString() }, 90 | body: { 91 | data: [{ 92 | op: 'add', 93 | path: '/field', 94 | value: 'value', 95 | }], 96 | }, 97 | }); 98 | var chain = ['ModifyResource', jsonapify.param('id')]; 99 | modify(chain)(req, res, function(err) { 100 | if (err) return done(err); 101 | var resdata = JSON.parse(res._getData()); 102 | expect(resdata).to.have.deep.property('data.field'); 103 | expect(resdata.data.field).to.equal('value'); 104 | done(); 105 | }); 106 | }); 107 | }); 108 | 109 | it('replaces existing object property', function(done) { 110 | model.create({}, function(err, object) { 111 | if (err) return done(err); 112 | common.initAccessor(accessors.field, 'prev'); 113 | common.initAccessor(accessors.output); 114 | var req = httpMocks.createRequest({ 115 | params: { id: object._id.toString() }, 116 | body: { 117 | data: [{ 118 | op: 'add', 119 | path: '/field', 120 | value: 'current', 121 | }], 122 | }, 123 | }); 124 | var chain = ['ModifyResource', jsonapify.param('id')]; 125 | modify(chain)(req, res, function(err) { 126 | if (err) return done(err); 127 | var resdata = JSON.parse(res._getData()); 128 | expect(resdata).to.have.deep.property('data.field'); 129 | expect(resdata.data.field).to.equal('current'); 130 | done(); 131 | }); 132 | }); 133 | }); 134 | }); 135 | 136 | describe('remove', function() { 137 | it('removes the value at the target location', function(done) { 138 | model.create({}, function(err, object) { 139 | if (err) return done(err); 140 | common.initAccessor(accessors.field, 'value'); 141 | common.initAccessor(accessors.output); 142 | var req = httpMocks.createRequest({ 143 | params: { id: object._id.toString() }, 144 | body: { 145 | data: [{ 146 | op: 'remove', 147 | path: '/field', 148 | }], 149 | }, 150 | }); 151 | var chain = ['ModifyResource', jsonapify.param('id')]; 152 | modify(chain)(req, res, function(err) { 153 | if (err) return done(err); 154 | var resdata = JSON.parse(res._getData()); 155 | expect(resdata).to.not.have.deep.property('data.field'); 156 | done(); 157 | }); 158 | }); 159 | }); 160 | 161 | it('gives an error if the value does not exist', function(done) { 162 | model.create({}, function(err, object) { 163 | if (err) return done(err); 164 | common.initAccessor(accessors.field); 165 | common.initAccessor(accessors.output); 166 | var req = httpMocks.createRequest({ 167 | params: { id: object._id.toString() }, 168 | body: { 169 | data: [{ 170 | op: 'remove', 171 | path: '/invalid', 172 | }], 173 | }, 174 | }); 175 | var chain = ['ModifyResource', jsonapify.param('id')]; 176 | modify(chain)(req, res, function(err) { 177 | expect(err).to.exist; 178 | done(); 179 | }); 180 | }); 181 | }); 182 | }); 183 | 184 | describe('replace', function() { 185 | it('replaces the value at the target location', function(done) { 186 | model.create({}, function(err, object) { 187 | if (err) return done(err); 188 | common.initAccessor(accessors.field, 'prev'); 189 | common.initAccessor(accessors.output); 190 | var req = httpMocks.createRequest({ 191 | params: { id: object._id.toString() }, 192 | body: { 193 | data: [{ 194 | op: 'replace', 195 | path: '/field', 196 | value: 'current', 197 | }], 198 | }, 199 | }); 200 | var chain = ['ModifyResource', jsonapify.param('id')]; 201 | modify(chain)(req, res, function(err) { 202 | if (err) return done(err); 203 | var resdata = JSON.parse(res._getData()); 204 | expect(resdata).to.have.deep.property('data.field'); 205 | expect(resdata.data.field).to.equal('current'); 206 | done(); 207 | }); 208 | }); 209 | }); 210 | 211 | it('gives an error if the value does not exist', function(done) { 212 | model.create({}, function(err, object) { 213 | if (err) return done(err); 214 | common.initAccessor(accessors.field); 215 | common.initAccessor(accessors.output); 216 | var req = httpMocks.createRequest({ 217 | params: { id: object._id.toString() }, 218 | body: { 219 | data: [{ 220 | op: 'replace', 221 | path: '/invalid', 222 | }], 223 | }, 224 | }); 225 | var chain = ['ModifyResource', jsonapify.param('id')]; 226 | modify(chain)(req, res, function(err) { 227 | expect(err).to.exist; 228 | done(); 229 | }); 230 | }); 231 | }); 232 | }); 233 | 234 | describe('move', function() { 235 | it('removes the value from path and adds it to the target location', function(done) { 236 | model.create({}, function(err, object) { 237 | if (err) return done(err); 238 | common.initAccessor(accessors.field, 'value'); 239 | common.initAccessor(accessors.output); 240 | var req = httpMocks.createRequest({ 241 | params: { id: object._id.toString() }, 242 | body: { 243 | data: [{ 244 | op: 'move', 245 | from: '/field', 246 | path: '/output', 247 | }], 248 | }, 249 | }); 250 | var chain = ['ModifyResource', jsonapify.param('id')]; 251 | modify(chain)(req, res, function(err) { 252 | if (err) return done(err); 253 | var resdata = JSON.parse(res._getData()); 254 | expect(resdata).to.not.have.deep.property('data.field'); 255 | expect(resdata).to.have.deep.property('data.output'); 256 | expect(resdata.data.output).to.equal('value'); 257 | done(); 258 | }); 259 | }); 260 | }); 261 | 262 | it('gives an error if the value does not exist', function(done) { 263 | model.create({}, function(err, object) { 264 | if (err) return done(err); 265 | common.initAccessor(accessors.field); 266 | common.initAccessor(accessors.output); 267 | var req = httpMocks.createRequest({ 268 | params: { id: object._id.toString() }, 269 | body: { 270 | data: [{ 271 | op: 'move', 272 | from: '/invalid', 273 | path: '/output', 274 | }], 275 | }, 276 | }); 277 | var chain = ['ModifyResource', jsonapify.param('id')]; 278 | modify(chain)(req, res, function(err) { 279 | expect(err).to.exist; 280 | done(); 281 | }); 282 | }); 283 | }); 284 | }); 285 | 286 | describe('copy', function() { 287 | it('copies the value from path to the target location', function(done) { 288 | model.create({}, function(err, object) { 289 | if (err) return done(err); 290 | common.initAccessor(accessors.field, 'value'); 291 | common.initAccessor(accessors.output); 292 | var req = httpMocks.createRequest({ 293 | params: { id: object._id.toString() }, 294 | body: { 295 | data: [{ 296 | op: 'copy', 297 | from: '/field', 298 | path: '/output', 299 | }], 300 | }, 301 | }); 302 | var chain = ['ModifyResource', jsonapify.param('id')]; 303 | modify(chain)(req, res, function(err) { 304 | if (err) return done(err); 305 | var resdata = JSON.parse(res._getData()); 306 | expect(resdata).to.have.deep.property('data.field'); 307 | expect(resdata).to.have.deep.property('data.output'); 308 | expect(resdata.data.field).to.equal('value'); 309 | expect(resdata.data.output).to.equal('value'); 310 | done(); 311 | }); 312 | }); 313 | }); 314 | 315 | it('gives an error if the value does not exist', function(done) { 316 | model.create({}, function(err, object) { 317 | if (err) return done(err); 318 | common.initAccessor(accessors.field); 319 | common.initAccessor(accessors.output); 320 | var req = httpMocks.createRequest({ 321 | params: { id: object._id.toString() }, 322 | body: { 323 | data: [{ 324 | op: 'copy', 325 | from: '/invalid', 326 | path: '/output', 327 | }], 328 | }, 329 | }); 330 | var chain = ['ModifyResource', jsonapify.param('id')]; 331 | modify(chain)(req, res, function(err) { 332 | expect(err).to.exist; 333 | done(); 334 | }); 335 | }); 336 | }); 337 | }); 338 | 339 | describe('test', function() { 340 | it('tests that value at path is equal to value', function(done) { 341 | model.create({}, function(err, object) { 342 | if (err) return done(err); 343 | common.initAccessor(accessors.field, 'expected'); 344 | common.initAccessor(accessors.output); 345 | var req = httpMocks.createRequest({ 346 | params: { id: object._id.toString() }, 347 | body: { 348 | data: [{ 349 | op: 'test', 350 | path: '/field', 351 | value: 'expected', 352 | }], 353 | }, 354 | }); 355 | var chain = ['ModifyResource', jsonapify.param('id')]; 356 | modify(chain)(req, res, function(err) { 357 | if (err) return done(err); 358 | var resdata = JSON.parse(res._getData()); 359 | expect(resdata).to.have.deep.property('data.field'); 360 | expect(resdata.data.field).to.equal('expected'); 361 | done(); 362 | }); 363 | }); 364 | }); 365 | 366 | it('gives an error if values do not match', function(done) { 367 | model.create({}, function(err, object) { 368 | if (err) return done(err); 369 | common.initAccessor(accessors.field, 'value'); 370 | common.initAccessor(accessors.output); 371 | var req = httpMocks.createRequest({ 372 | params: { id: object._id.toString() }, 373 | body: { 374 | data: [{ 375 | op: 'test', 376 | path: '/field', 377 | value: 'invalid', 378 | }], 379 | }, 380 | }); 381 | var chain = ['ModifyResource', jsonapify.param('id')]; 382 | modify(chain)(req, res, function(err) { 383 | expect(err).to.exist; 384 | done(); 385 | }); 386 | }); 387 | }); 388 | 389 | it('gives an error if the value does not exist', function(done) { 390 | model.create({}, function(err, object) { 391 | if (err) return done(err); 392 | common.initAccessor(accessors.field); 393 | common.initAccessor(accessors.output); 394 | var req = httpMocks.createRequest({ 395 | params: { id: object._id.toString() }, 396 | body: { 397 | data: [{ 398 | op: 'test', 399 | path: '/invalid', 400 | value: 'expected', 401 | }], 402 | }, 403 | }); 404 | var chain = ['ModifyResource', jsonapify.param('id')]; 405 | modify(chain)(req, res, function(err) { 406 | expect(err).to.exist; 407 | done(); 408 | }); 409 | }); 410 | }); 411 | }); 412 | }); 413 | -------------------------------------------------------------------------------- /test/middleware/read.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | chai.use(require('sinon-chai')); 4 | var mongoose = require('mongoose'); 5 | var httpMocks = require('node-mocks-http'); 6 | var ObjectId = mongoose.Types.ObjectId; 7 | var expect = chai.expect; 8 | 9 | var common = require('../common'); 10 | var jsonapify = require('../../'); 11 | 12 | var Runtime = jsonapify.Runtime; 13 | var Resource = jsonapify.Resource; 14 | var read = jsonapify.middleware.read; 15 | var Property = jsonapify.accessors.Property; 16 | var ResourceNotFound = jsonapify.errors.ResourceNotFound; 17 | 18 | describe('read', function() { 19 | var model, resource, accessor, res; 20 | before(function(done) { 21 | mongoose.connect('mongodb://localhost/test', function(err) { 22 | if (err) return done(err); 23 | model = mongoose.model('ReadTest', new mongoose.Schema); 24 | done(); 25 | }); 26 | }); 27 | 28 | beforeEach(function() { 29 | accessor = common.createAccessor(); 30 | resource = new Resource(model, { type: 'test', field: accessor }); 31 | Runtime.addResource('ReadResource', resource); 32 | res = httpMocks.createResponse(); 33 | }); 34 | 35 | afterEach(function(done) { 36 | Runtime.removeResource('ReadResource'); 37 | mongoose.connection.db.dropDatabase(done); 38 | }); 39 | 40 | after(function(done) { 41 | mongoose.disconnect(done); 42 | }); 43 | 44 | it('retrieves existing resource and sends back resource data', function(done) { 45 | model.create({ field: 'value' }, function(err, object) { 46 | if (err) return done(err); 47 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 48 | accessor.deserialize.callsArgWithAsync(4, null); 49 | var req = httpMocks.createRequest({ params: { id: object._id }}); 50 | read(['ReadResource', jsonapify.param('id')])(req, res, function(err) { 51 | if (err) return done(err); 52 | expect(accessor.serialize).to.have.been.called.once; 53 | expect(accessor.deserialize).to.not.have.been.called; 54 | done(); 55 | }); 56 | }); 57 | }); 58 | 59 | it('sends an error if resource not found', function(done) { 60 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 61 | accessor.deserialize.callsArgWithAsync(4, null); 62 | var req = httpMocks.createRequest({ params: { id: ObjectId() }}); 63 | read(['ReadResource', jsonapify.param('id')])(req, res, function(err) { 64 | expect(err).to.be.an.instanceof(ResourceNotFound); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('invokes transaction filters', function(done) { 70 | model.create({ field: 'value' }, function(err, object) { 71 | if (err) return done(err); 72 | var filter = sinon.spy(); 73 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 74 | accessor.deserialize.callsArgWithAsync(4, null); 75 | var req = httpMocks.createRequest({ params: { id: object._id }}); 76 | var chain = ['ReadResource', jsonapify.param('id')]; 77 | read(chain, { filters: [filter] })(req, res, function(err) { 78 | if (err) return done(err); 79 | expect(filter).to.have.been.called.once; 80 | done(); 81 | }); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/middleware/remove.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | chai.use(require('sinon-chai')); 4 | var mongoose = require('mongoose'); 5 | var httpMocks = require('node-mocks-http'); 6 | var ObjectId = mongoose.Types.ObjectId; 7 | var expect = chai.expect; 8 | 9 | var jsonapify = require('../../'); 10 | 11 | var Runtime = jsonapify.Runtime; 12 | var Resource = jsonapify.Resource; 13 | var remove = jsonapify.middleware.remove; 14 | var ResourceNotFound = jsonapify.errors.ResourceNotFound; 15 | 16 | describe('remove', function() { 17 | var model, resource, accessor, res; 18 | before(function(done) { 19 | mongoose.connect('mongodb://localhost/test', function(err) { 20 | if (err) return done(err); 21 | model = mongoose.model('RemoveTest', new mongoose.Schema); 22 | done(); 23 | }); 24 | }); 25 | 26 | beforeEach(function() { 27 | resource = new Resource(model, { type: 'test' }); 28 | Runtime.addResource('RemoveResource', resource); 29 | res = httpMocks.createResponse(); 30 | }); 31 | 32 | afterEach(function(done) { 33 | Runtime.removeResource('RemoveResource'); 34 | mongoose.connection.db.dropDatabase(done); 35 | }); 36 | 37 | after(function(done) { 38 | mongoose.disconnect(done); 39 | }); 40 | 41 | it('removes existing resource', function(done) { 42 | model.create({}, function(err, object) { 43 | if (err) return done(err); 44 | var req = httpMocks.createRequest({ params: { id: object._id }}); 45 | remove(['RemoveResource', jsonapify.param('id')])(req, res, function(err) { 46 | if (err) return done(err); 47 | model.findById(object._id, function(err, object) { 48 | if (err) return done(err); 49 | expect(object).to.not.exist; 50 | done(); 51 | }); 52 | }); 53 | }); 54 | }); 55 | 56 | it('sends an error if resource does not exist', function(done) { 57 | var req = httpMocks.createRequest({ params: { id: ObjectId() }}); 58 | remove(['RemoveResource', jsonapify.param('id')])(req, res, function(err) { 59 | expect(err).to.be.an.instanceof(ResourceNotFound); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('invokes transaction filters', function(done) { 65 | model.create({}, function(err, object) { 66 | if (err) return done(err); 67 | var req = httpMocks.createRequest({ 68 | params: { id: object._id }, 69 | body: { data: { type: 'test', field: 'value' }}, 70 | }); 71 | var filter = sinon.spy(); 72 | var chain = ['RemoveResource', jsonapify.param('id')]; 73 | remove(chain, { filters: [filter] })(req, res, function(err) { 74 | if (err) return done(err); 75 | expect(filter).to.have.been.called.once; 76 | done(); 77 | }); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/middleware/update.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | chai.use(require('sinon-chai')); 4 | var mongoose = require('mongoose'); 5 | var httpMocks = require('node-mocks-http'); 6 | var ObjectId = mongoose.Types.ObjectId; 7 | var expect = chai.expect; 8 | 9 | var common = require('../common'); 10 | var jsonapify = require('../../'); 11 | 12 | var Runtime = jsonapify.Runtime; 13 | var Resource = jsonapify.Resource; 14 | var update = jsonapify.middleware.update; 15 | var ResourceNotFound = jsonapify.errors.ResourceNotFound; 16 | var InvalidFieldValue = jsonapify.errors.InvalidFieldValue; 17 | 18 | describe('update', function() { 19 | var model, resource, accessor, res; 20 | before(function(done) { 21 | mongoose.connect('mongodb://localhost/test', function(err) { 22 | if (err) return done(err); 23 | model = mongoose.model('UpdateTest', new mongoose.Schema); 24 | done(); 25 | }); 26 | }); 27 | 28 | beforeEach(function() { 29 | accessor = common.createAccessor(); 30 | resource = new Resource(model, { 31 | type: 'test', 32 | field: accessor, 33 | }); 34 | Runtime.addResource('UpdateResource', resource); 35 | res = httpMocks.createResponse(); 36 | }); 37 | 38 | afterEach(function(done) { 39 | Runtime.removeResource('UpdateResource'); 40 | mongoose.connection.db.dropDatabase(done); 41 | }); 42 | 43 | after(function(done) { 44 | mongoose.disconnect(done); 45 | }); 46 | 47 | it('updates existing resource and returns resource data', function(done) { 48 | model.create({ field: 'before' }, function(err, object) { 49 | if (err) return done(err); 50 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 51 | accessor.deserialize.callsArgWithAsync(4, null); 52 | var req = httpMocks.createRequest({ 53 | params: { id: object._id }, 54 | body: { data: { type: 'test', field: 'after' }}, 55 | }); 56 | update(['UpdateResource', jsonapify.param('id')])(req, res, function(err) { 57 | if (err) return done(err); 58 | expect(accessor.serialize).to.have.been.called.once; 59 | expect(accessor.deserialize).to.have.been.called.once; 60 | done(); 61 | }); 62 | }); 63 | }); 64 | 65 | it('sends an error if trying to update resource with wrong type', function(done) { 66 | model.create({ field: 'before' }, function(err, object) { 67 | if (err) return done(err); 68 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 69 | accessor.deserialize.callsArgWithAsync(4, null); 70 | var req = httpMocks.createRequest({ 71 | params: { id: object._id }, 72 | body: { data: { type: 'invalid', field: 'after' }}, 73 | }); 74 | update(['UpdateResource', jsonapify.param('id')])(req, res, function(err) { 75 | expect(err).to.be.an.instanceof(InvalidFieldValue); 76 | done(); 77 | }); 78 | }); 79 | }); 80 | 81 | it('sends an error if resource does not exist', function(done) { 82 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 83 | accessor.deserialize.callsArgWithAsync(4, null); 84 | var req = httpMocks.createRequest({ 85 | params: { id: ObjectId() }, 86 | body: { data: { type: 'test', field: 'after' }}, 87 | }); 88 | update(['UpdateResource', jsonapify.param('id')])(req, res, function(err) { 89 | expect(err).to.be.an.instanceof(ResourceNotFound); 90 | done(); 91 | }); 92 | }); 93 | 94 | it('invokes transaction filters', function(done) { 95 | model.create({ field: 'before' }, function(err, object) { 96 | if (err) return done(err); 97 | var filter = sinon.spy(); 98 | accessor.serialize.callsArgWithAsync(3, null, 'value'); 99 | accessor.deserialize.callsArgWithAsync(4, null); 100 | var req = httpMocks.createRequest({ 101 | params: { id: object._id }, 102 | body: { data: { type: 'test', field: 'after' }}, 103 | }); 104 | var chain = ['UpdateResource', jsonapify.param('id')]; 105 | update(chain, { filters: [filter] })(req, res, function(err) { 106 | if (err) return done(err); 107 | expect(filter).to.have.been.called.once; 108 | done(); 109 | }); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --------------------------------------------------------------------------------