├── .gitignore ├── Makefile ├── README.md ├── index.js ├── lib ├── actionUtil.js ├── actions │ ├── add.js │ ├── create.js │ ├── destroy.js │ ├── find.js │ ├── findOne.js │ ├── populate.js │ ├── remove.js │ └── update.js ├── generalUtil.js └── index.js ├── package.json └── test ├── .boilerplate ├── actions ├── add.js ├── create.js ├── destroy.js ├── find.js ├── findOne.js ├── populate.js ├── remove.js └── update.js ├── auth.scheme.js ├── models.definition.js ├── models.fixtures.json ├── options ├── _private.count.js ├── actAsUser.js ├── createdLocation.js ├── deletedFlag.js ├── omit.js ├── pkAttr.js └── prefix.js ├── server.js └── server.setup.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @node node_modules/lab/bin/lab 3 | test-cov: 4 | @node node_modules/lab/bin/lab -t 100 5 | test-cov-html: 6 | @node node_modules/lab/bin/lab -r html -o coverage.html 7 | 8 | .PHONY: test test-cov test-cov-html 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![bedwetter](http://i.imgur.com/Emheg8o.png) 2 | 3 | #### Auto-generated, RESTful, CRUDdy route handlers 4 | to be used with [hapi 8](https://github.com/hapijs/hapi) (and 7) and its [Waterline](https://github.com/balderdashy/waterline) plugin, [dogwater](https://github.com/devinivy/dogwater). 5 | 6 | --- 7 | 8 | ## What it does 9 | Bedwetter registers route handlers based upon the `method` and `path` of your route. It turns them into RESTful API endpoints that automatically interact with the model defined using dogwater. The route handler is based on one of eight bedwetters: 10 | 11 | - `POST` is used for `create`, `add` when `add` is used to create a record then add it to a relation, and for `update` 12 | - `PATCH` is also used for `update` 13 | - `PUT` is used for `add` when it's used to simply add a record to a relation 14 | - `GET` is used for `find`, `findOne`, and `populate` (get related records or check an association) 15 | - `DELETE` is used for `destroy` and `remove` (remove a record from a relation) 16 | 17 | For now, see SailsJs's [documentation on Blueprints](http://sailsjs.org/#!/documentation/reference/blueprint-api) for info about parameters for the bedwetters. A portion of the code is adapted from this SailsJs hook. 18 | 19 | Bedwetter also allows you to manage resources/records with owners. There are options to act on behalf of a user via hapi authentication. You can set owners automatically on new records, only display records when owned by the authenticated user, and make bedwetters behave like the primary record is the authenticated user. 20 | 21 | 22 | ## Bedwetting Patterns 23 | Suppose users are associated with comments via dogwater/Waterline. The user model associates comments in an attribute named `comments`. Here are some examples as to how the plugin will deduce which of the eight bedwetters to use, based upon route method and path definition. 24 | 25 | * `GET /users` ↦ `find` 26 | 27 | Returns an array of users with an `HTTP 200 OK` response. 28 | 29 | * `GET /users/count` ↦ `find` with `/count` 30 | 31 | Returns the integer number of users matched with an `HTTP 200 OK` response. 32 | 33 | * `GET /users/{id}` ↦ `findOne` 34 | 35 | Returns user `id` with an `HTTP 200 OK` response. Responds with an `HTTP 404 Not Found` response if the user is not found. 36 | 37 | * `GET /users/{id}/comments` ↦ `populate` 38 | 39 | Returns an array of comments associated with user `id`. Returns `HTTP 200 OK` if that user is found. Returns an `HTTP 404 Not Found` response if that user is not found. 40 | 41 | * `GET /users/{id}/comments/count` ↦ `populate` with `/count` 42 | 43 | Returns the integer number of comments associated with user `id`. Returns `HTTP 200 OK` if that user is found. Returns an `HTTP 404 Not Found` response if that user is not found. 44 | 45 | * `GET /users/{id}/comments/{childId}` ↦ `populate` 46 | 47 | Returns `HTTP 204 No Content` if comment `childId` is associated with user `id`. Returns an `HTTP 404 Not Found` response if that user is not found or that comment is not associated with the user. 48 | 49 | * `POST /users` ↦ `create` 50 | 51 | Creates a new user using the request payload and returns it with an `HTTP 201 Created` response. 52 | 53 | * `POST /users/{id}/comments` ↦ `add` 54 | 55 | Creates a new comment using the request payload and associates that comment with user `id`. Returns that comment with an `HTTP 201 Created response`. If that user is not found, returns an `HTTP 404 Not Found` response. 56 | 57 | * `PUT /users/{id}/comments/{childId}` ↦ `add` 58 | 59 | Associates comment `childId` with user `id`. Returns an `HTTP 204 No Content` response on success. If the user or comment are not found, returns an `HTTP 404 Not Found` response. 60 | 61 | * `DELETE /users/{id}` ↦ `destroy` 62 | 63 | Destroys user `id`. Returns an `HTTP 204 No Content` response on success. If the user doesn't exist, returns an `HTTP 404 Not Found` response. 64 | 65 | * `DELETE /users/{id}/comment/{childId}` ↦ `remove` 66 | 67 | Removes association between user `id` and comment `childId`. Returns an `HTTP 204 No Content` response on success. If the user or comment doesn't exist, returns an `HTTP 404 Not Found` response. 68 | 69 | * `PATCH /users/{id}` or `POST /user/{id}` ↦ `update` 70 | 71 | Updates user `id` using the request payload (which will typically only contain the attributes to update) and responds with the updated user. Returns an `HTTP 200 OK` response on success. If the user doesn't exist, returns an `HTTP 404 Not Found` response. 72 | 73 | 74 | ## Options 75 | Options can be passed to the plugin when registered or defined directly on the route handler. Those defined on the route handler override those passed to the plugin on a per-route basis. 76 | 77 | ### Acting as a User 78 | These options allow you to act on behalf of the authenticated user. Typically the user info is taken directly off the credentials object without checking the `Request.auth.isAuthenticated` flag. This allows you to use authentication modes however you wish. For examples, for now please see tests at `test/options/actAsUser.js`. 79 | 80 | * `actAsUser` (boolean, defaults `false`). Applies to `findOne`, `find`, `create`, `update`, `destroy`, `add`, `remove`, and `populate`. 81 | 82 | This must be set to `true` for the following options in the section to take effect. The acting user is defined by hapi authentication credentials and the `userIdProperty` option. 83 | 84 | * `userIdProperty` (string, defaults `"id"`). Applies to `findOne`, `find`, `create`, `update`, `destroy`, `add`, `remove`, and `populate`. 85 | 86 | When `actAsUser` is `true` this option takes effect. It defines a path into `Request.auth.credentials` to determine the acting user's id. For example, if the credentials object equals `{user: {info: {id: 17}}}` then `"user.info.id"` would grab user id `17`. See [`Hoek.reach`](https://github.com/hapijs/hoek#reachobj-chain-options), which is used to convert the string to a deep property in the hapi credentials object. 87 | 88 | * `userUrlPrefix` (string, defaults `"/user"`). Applies to `findOne`, `update`, `destroy`, `add`, `remove`, and `populate`. 89 | 90 | When `actAsUser` is `true` this option takes effect. This option works in tandem with `userModel`. When a route path begins with `userUrlPrefix` (after any other inert prefix has been stripped via the `prefix` option), the URL is transformed to begin `/:userModel/:actingUserId` before matching for a bedwetter; it essentially sets the primary record to the acting user. 91 | 92 | * `userModel` (string, defaults `"users"`). Applies to `findOne`, `update`, `destroy`, `add`, `remove`, and `populate`. 93 | 94 | When `actAsUser` is `true` this option takes effect. This option works in tandem with `userUrlPrefix`. When a route path begins with `userUrlPrefix` (after any other inert prefix has been stripped via the `prefix` option), the URL is transformed to begin `/:userModel/:actingUserId` before matching for a bedwetter; it essentially sets the primary record to the acting user. E.g., by default when `actAsUser` is enabled, route path `PUT /user/following/10` would internally be considered as `PUT /users/17/following/10`, which corresponds to the `add` bedwetter applied to the authenticated user. 95 | 96 | * `requireOwner` (boolean, defaults `false`). Applies to `findOne`, `find`, `create`, `update`, `destroy`, `add`, `remove`, and `populate`. 97 | 98 | When `actAsUser` is `true` this option takes effect. The option forces any record to that's being viewed or modified (including associations) to be owned by the user. Ownership is determined by matching the acting user's id against the attribute of the record determined by `ownerAttr` or `childOwnerAttr`. 99 | 100 | * `setOwner` (boolean, defaults `false`). Applies to `create`, `update`, `add`. 101 | 102 | When `actAsUser` is `true` this option takes effect. The option forces any record to that's being created or updated (including associated records) to be owned by the acting user. The owner is set on the record's attribute determined by `ownerAttr` or `childOwnerAttr`. 103 | 104 | * `ownerAttr` (string or `false`, defaults `"owner"`). Applies to `findOne`, `find`, `update`, `destroy`, `add`, `remove`, and `populate`. 105 | 106 | When `actAsUser` is `true` this option takes effect. If `false`, `requireOwner` and `setOwner` are disabled on the primary record. Otherwise, `requireOwner` and `setOwner` options act using the primary record's attribute with name specified by `ownerAttr`. 107 | 108 | * `childOwnerAttr` (string or `false`, defaults `"owner"`). Applies to `add`, `remove`, and `populate`. 109 | 110 | When `actAsUser` is `true` this option takes effect. If `false`, `requireOwner` and `setOwner` are disabled on the child record. Otherwise, `requireOwner` and `setOwner` options act using the child record's attribute with name specified by `childOwnerAttr`. 111 | 112 | ### Other Options 113 | 114 | * `prefix` (string). Applies to `findOne`, `find`, `create`, `update`, `destroy`, `add`, `remove`, and `populate`. 115 | 116 | Allows one to specify a prefix to the route path that will be ignored when determining which bedwetter to apply. 117 | 118 | * `createdLocation` (string). Applies to `create` and sometimes to `add`. 119 | 120 | When this set (typically as a route-level option rather than a plugin-level option), a `Location` header will be added to responses with a URL pointing to the created record. This option will act as the first argument to [`util.format`](http://nodejs.org/api/util.html#util_util_format_format) when set, and there should be a single placeholder for the created record's id. 121 | 122 | * `model` (string). Applies to `findOne`, `find`, `create`, `update`, `destroy`, `add`, `remove`, and `populate`. 123 | 124 | Name of the model's Waterline identity. If not provided as an option, it is deduced from the route path. 125 | 126 | Ex: `/user/1/files/3` has the model `user`. 127 | 128 | * `associationAttr` (string). Applies to `add`, `remove`, and `populate` 129 | 130 | Name of the association's Waterline attribute. If not provided as an option, it is deduced from the route path. 131 | 132 | Ex: `/user/1/files/3` has the association attribute `files` (i.e., the Waterline model `user` has an attribute, `files` containing records in a one-to-many relationship). 133 | 134 | * `criteria` (object). Applies to `find` and `populate`. 135 | * `blacklist` (array) 136 | 137 | An array of attribute names. The criteria blacklist disallows searching by certain attribute criteria. 138 | 139 | * `where` (object). Applies to `find` and `populate`. When `where.id` is specified, also applies to `findOne`, `update`, `destroy`, `add`, and `remove`. 140 | 141 | Typically sets default criteria for the records in a list. Keys represent are attribute names and values represent values for those attributes. This can be overridden by query parameters. When `where.id` is set, this is is used instead of the primary key path parameter (similarly to the `id` option), but does not override the `id` option. 142 | 143 | * `id` (string or integer). Applies to `findOne`, `update`, `destroy`, `add`, `remove`, and `populate`. 144 | 145 | Fixes a supplied primary key to a certain value. Typically this primary key would be pulled from the route parameter. In most cases this will cause a confusing implementation, but may be worth keeping to interact with future features. 146 | 147 | * `limit` (positive integer). Applies to `find` and `populate`. 148 | 149 | Set default limit of records returned in a list. If not provided, this defaults to 30. 150 | 151 | * `maxLimit` (positive integer). Applies to `find` and `populate`. 152 | 153 | If a user requests a certain number of records to be returned in a list (using the `limit` query parameter), it cannot exceed this maximum limit. 154 | 155 | * `populate` (boolean). Applies to `find` and `findOne`. 156 | 157 | Determines if all association attributes are by default populated (overridden by `populate` query parameter, which contains a comma-separated list of attributes). Defaults to false. 158 | 159 | * `skip` (positive integer). Applies to `find` and `populate`. 160 | 161 | Sets default number of records to skip in a list (overridden by `skip` query parameter). Defaults to 0. 162 | 163 | * `sort` (string). Applies to `find` and `populate`. 164 | 165 | Sets default sorting criteria (i.e. `createdDate ASC`) (overridden by `sort` query parameter). Defaults to no sort applied. 166 | 167 | * `values` (object). Applies to `create`, `update`, and sometimes to `add`. Sets default attribute values in key-value pairs for records to be created or updated. Also includes a `blacklist` parameter: 168 | * `blacklist` (array) 169 | 170 | An array of attribute names to be omitted when creating or updating a record. 171 | 172 | * `deletedFlag` (boolean, defaults `false`). Applies to `destroy`. 173 | 174 | Rather than destroying the object, this will simply set a flag on the object using the `deletedAttr` and `deletedValue` options. 175 | 176 | * `deletedAttr` (string, defaults `"deleted"`). Applies to `destroy`. 177 | 178 | Model attribute to be updated with the `deletedValue`. 179 | 180 | * `deletedValue` (string|int, defaults `1`). Applies to `destroy`. 181 | 182 | Value to be updated on the model attribute specified `deletedAttr` when the `deletedFlag` option is enabled. 183 | 184 | * `omit` (string|array, defaults `[]`). Applies to `add`, `create`, `find`, `findOne`, `populate`, `update`. 185 | 186 | When returning a record or array of records, the list of fields will not be included in the response per record. When populating a record association, you may use [`Hoek.reach`](https://github.com/hapijs/hoek#reachobj-chain-options style key identifiers to omit deep properties. If the property holds an array, deep omissions will omit the property from each record in the array. 187 | 188 | * `pkAttr` (string or `false`, defaults `false`). Applies to `add`, `destroy`, `findOne`, `populate`, `remove`, `update`. 189 | 190 | This overrides which attribute used for looking-up the primary/parent record. By default bedwetter uses the model's primary key. This option can be used to look-up records by a unique identifier other than the primary key. 191 | 192 | Ex: To look users up by their `username` attribute rather than their numeric primary key `id`, set `pkAttr` to `"username"`. Then `GET /users/john-doe` will return the user with username `"john-doe"`. 193 | 194 | * `childPkAttr` (string or `false`, defaults `false`). Applies to `add`, `populate`, `remove`. 195 | 196 | This overrides which attribute used for looking-up the secondary/child record. By default bedwetter uses the model's primary key. This option can be used to look-up records by a unique identifier other than the primary key. 197 | 198 | 199 | ## Request State 200 | The bedwetter request state can be accessed on `Request.plugins.bedwetter`. It it an object containing the following properties: 201 | 202 | * `action` (string). Indicates which one of the eight bedwetter actions was used. It is one of `find`, `findone`, `update`, `create`, `destroy`, `populate`, `add`, or `remove`. 203 | * `options` (object). These are active bedwetter options used for the request. If any hooks modified the options, that will be reflected here. 204 | * `primaryRecord` (Waterline model). This provides access to any primary record associated with this request. This will not be set if there is no primary record. 205 | * `secondaryRecord` (Waterline model). This provides access to any secondary record associated with this request. This will not be set if there is no secondary record. 206 | 207 | 208 | ## Usage 209 | Here's a sort of crappy example. 210 | 211 | ```javascript 212 | // Assume `server` is a hapi server with the bedwetter plugin registered. 213 | // Models with identities "zoo" and "treat" exist via dogwater. 214 | // zoos and treats are in a many-to-many correspondence with each other. 215 | // I suggest checking out ./test 216 | 217 | server.route([ 218 | { // findOne 219 | method: 'GET', 220 | path: '/zoo/{id}', 221 | handler: { 222 | bedwetter: options 223 | } 224 | }, 225 | { // find 226 | method: 'GET', 227 | path: '/treat', 228 | handler: { 229 | bedwetter: options 230 | } 231 | }, 232 | { // find with prefix 233 | method: 'GET', 234 | path: '/v1/treat', 235 | handler: { 236 | bedwetter: { 237 | prefix: '/v1' 238 | } 239 | } 240 | }, 241 | { // destroy 242 | method: 'DELETE', 243 | path: '/treat/{id}', 244 | handler: { 245 | bedwetter: options 246 | } 247 | }, 248 | { // create 249 | method: 'POST', 250 | path: '/zoo', 251 | handler: { 252 | bedwetter: options 253 | } 254 | }, 255 | { // update 256 | method: ['PATCH', 'POST'], 257 | path: '/treat/{id}', 258 | handler: { 259 | bedwetter: options 260 | } 261 | }, 262 | { // remove 263 | method: 'DELETE', 264 | path: '/zoo/{id}/treats/{childId}', 265 | handler: { 266 | bedwetter: options 267 | } 268 | }, 269 | { // create then add 270 | method: 'POST', 271 | path: '/zoo/{id}/treats', 272 | handler: { 273 | bedwetter: options 274 | } 275 | }, 276 | { // add 277 | method: 'PUT', 278 | path: '/zoo/{id}/treats/{childId}', 279 | handler: { 280 | bedwetter: options 281 | } 282 | }, 283 | { // populate 284 | method: 'GET', 285 | path: '/zoo/{id}/treats/{childId?}', 286 | handler: { 287 | bedwetter: options 288 | } 289 | }]); 290 | ``` 291 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); -------------------------------------------------------------------------------- /lib/actionUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var _ = require('lodash'); 5 | var mergeDefaults = require('merge-defaults'); 6 | var Boom = require('boom'); 7 | var Hoek = require('hoek'); 8 | var util = require('util'); 9 | 10 | 11 | /** 12 | * Utility methods used in built-in blueprint actions. 13 | * 14 | * @type {Object} 15 | */ 16 | module.exports = function(request, options) { 17 | 18 | return { 19 | 20 | /** 21 | * Given a Waterline query, populate the appropriate/specified 22 | * association attributes and return it so it can be chained 23 | * further ( i.e. so you can .exec() it ) 24 | * 25 | * @param {Query} query [waterline query object] 26 | * @param {Request} request 27 | * @param {Object} options 28 | * @return {Query} 29 | */ 30 | populateEach: function (query) { 31 | var DEFAULT_POPULATE_LIMIT = 30; 32 | var _options = options; 33 | var aliasFilter = request.query.populate; 34 | var shouldPopulate = _options.populate; 35 | 36 | // Convert the string representation of the filter list to an Array. We 37 | // need this to provide flexibility in the request param. This way both 38 | // list string representations are supported: 39 | // /model?populate=alias1,alias2,alias3 40 | // /model?populate=[alias1,alias2,alias3] 41 | if (typeof aliasFilter === 'string') { 42 | aliasFilter = aliasFilter.replace(/\[|\]/g, ''); 43 | aliasFilter = (aliasFilter) ? aliasFilter.split(',') : []; 44 | } 45 | 46 | return _(_options.associations).reduce(function populateEachAssociation (query, association) { 47 | 48 | // If an alias filter was provided, override the blueprint config. 49 | if (aliasFilter) { 50 | shouldPopulate = _.contains(aliasFilter, association.alias); 51 | } 52 | 53 | // TODO: use max limit! 54 | // Only populate associations if a population filter has been supplied 55 | // with the request or if `populate` is set within the blueprint config. 56 | // Population filters will override any value stored in the config. 57 | // 58 | // Additionally, allow an object to be specified, where the key is the 59 | // name of the association attribute, and value is true/false 60 | // (true to populate, false to not) 61 | if (shouldPopulate) { 62 | 63 | var populationLimit = 64 | _options['populate_'+association.alias+'_limit'] || 65 | _options.populateLimit || 66 | _options.limit || 67 | DEFAULT_POPULATE_LIMIT; 68 | 69 | var opts = {limit: populationLimit}; 70 | // If the deleted flag is set, make sure those results aren't being included 71 | if(_options.deletedFlag) { 72 | var values = {}; 73 | values[_options.deletedAttr] = { '!': _options.deletedValue }; 74 | _.merge(opts, {where: values}); 75 | } 76 | 77 | return query.populate(association.alias, opts); 78 | 79 | } else { 80 | 81 | return query; 82 | 83 | } 84 | 85 | }, query); 86 | }, 87 | 88 | 89 | /** 90 | * Parse primary key value for use in a Waterline criteria 91 | * (e.g. for `find`, `update`, or `destroy`) 92 | * 93 | * @param {Request} request 94 | * @param {Object} options 95 | * @param {Boolean} child 96 | * @return {Integer|String} 97 | */ 98 | parsePk: function (child) { 99 | 100 | var primaryKeyParam; 101 | 102 | child = child || false; 103 | 104 | if (child) { 105 | primaryKeyParam = options.associatedPkName; 106 | } else { 107 | primaryKeyParam = options.pkName; 108 | } 109 | 110 | //TODO: support these options..id for parent and child 111 | var pk; 112 | 113 | // If actAsUser modified the path, grab the user's pk from auth credentials. 114 | if (!child && 115 | options._private.actAsUserModifiedPath) { 116 | 117 | pk = this.getUserId(); 118 | 119 | } else { // Otherwise, grab it as per usual. 120 | 121 | pk = options.id || (options.where && options.where.id) || request.params[primaryKeyParam]; 122 | } 123 | 124 | // TODO: make this smarter... 125 | // (e.g. look for actual primary key of model and look for it 126 | // in the absence of `id`.) 127 | // See coercePK for reference (although be aware it is not currently in use) 128 | 129 | // exclude criteria on id field 130 | pk = _.isPlainObject(pk) ? undefined : pk; 131 | 132 | // If the primary key field for the record is specified, use it as criteria. 133 | var tmpPk; 134 | var pkAttr = child ? options.childPkAttr : options.pkAttr; 135 | 136 | if (typeof pk !== "undefined" && pkAttr) { 137 | 138 | tmpPk = {}; 139 | tmpPk[pkAttr] = pk; 140 | pk = tmpPk; 141 | } 142 | 143 | return pk; 144 | }, 145 | 146 | 147 | /** 148 | * Parse primary key value from parameters. 149 | * Throw an error if it cannot be retrieved. 150 | * 151 | * @param {Request} request 152 | * @param {Object} options 153 | * @param {Boolean} child 154 | * @return {Integer|String} 155 | */ 156 | requirePk: function (child) { 157 | 158 | child = child || false; 159 | 160 | var pk = this.parsePk(child); 161 | 162 | // Validate the required `id` parameter 163 | if ( !pk ) { 164 | 165 | var err; 166 | 167 | // If path was modified for acting user, pk came from credentials. 168 | if (!child && 169 | options._private.actAsUserModifiedPath) { 170 | 171 | err = Boom.unauthorized(); 172 | 173 | } else { 174 | 175 | // TODO: error message here. 176 | err = Boom.notFound(); 177 | } 178 | 179 | throw err; 180 | } 181 | 182 | return pk; 183 | }, 184 | 185 | 186 | /** 187 | * Parse `criteria` for a Waterline `find` or `populate` from query parameters. 188 | * 189 | * @param {Request} request 190 | * @param {Object} options 191 | * @return {Object} the WHERE criteria object 192 | */ 193 | parseCriteria: function (child) { 194 | 195 | // Allow customizable blacklist for params NOT to include as criteria. 196 | options.criteria = options.criteria || {}; 197 | options.criteria.blacklist = options.criteria.blacklist || ['limit', 'skip', 'sort', 'populate', 'omit']; 198 | 199 | // Validate blacklist to provide a more helpful error msg. 200 | var blacklist = options.criteria && options.criteria.blacklist; 201 | 202 | Hoek.assert(_.isArray(blacklist), 'Invalid `options.criteria.blacklist`. Should be an array of strings (parameter names.)'); 203 | 204 | // Look for explicitly specified `where` parameter. 205 | var where = request.query.where; 206 | 207 | // If `where` parameter is a string, try to interpret it as JSON 208 | if (_.isString(where)) { 209 | where = tryToParseJSON(where); 210 | } 211 | 212 | // If `where` has not been specified, but other unbound parameter variables 213 | // **ARE** specified, build the `where` option using them. 214 | if (!where) { 215 | 216 | // Prune params which aren't fit to be used as `where` criteria 217 | // to build a proper where query 218 | where = request.query; 219 | 220 | // Omit built-in runtime config (like query modifiers) 221 | where = _.omit(where, blacklist || ['limit', 'skip', 'sort', 'omit']); 222 | 223 | // Omit any params w/ undefined values 224 | where = _.omit(where, function (p){ return _.isUndefined(p); }); 225 | 226 | // Omit jsonp callback param (but only if jsonp is enabled) 227 | if (request.route.jsonp) { 228 | delete where[request.route.jsonp] 229 | } 230 | } 231 | 232 | // Merge w/ options.where and return 233 | where = _.merge({}, options.where || {}, where) || undefined; 234 | 235 | where = _.merge(where, this.checkDeletedFlag()); 236 | 237 | // Deal with ownership 238 | if (options.actAsUser && options.requireOwner) { 239 | 240 | // Grab appropriate transformation object 241 | var ownerAttrTransformation = child ? options.childOwnerAttrs : options.ownerAttrs; 242 | Hoek.assert(_.isObject(ownerAttrTransformation), 'Owner attribute options (`options.childOwnerAttr`/`options.ownerAttr`/`options.childOwnerAttrs`/`options.ownerAttrs`) should be set for use with `options.requireOwner`.'); 243 | 244 | // Get new values with transformation 245 | var requireWhere = Hoek.transform(request.auth.credentials, ownerAttrTransformation); 246 | 247 | _.merge(where, requireWhere); 248 | 249 | } 250 | 251 | return where; 252 | }, 253 | 254 | 255 | /** TODO 256 | * Parse `values` for a Waterline `create` or `update` from all 257 | * request parameters. 258 | * 259 | * @param {Request} req 260 | * @return {Object} 261 | */ 262 | parseValues: function (child) { 263 | 264 | // Allow customizable blacklist for params NOT to include as values. 265 | options.values = options.values || {}; 266 | 267 | // Validate blacklist to provide a more helpful error msg. 268 | var blacklist = options.values.blacklist || []; 269 | Hoek.assert(_.isArray(blacklist), 'Invalid `options.values.blacklist`. Should be an array of strings (parameter names.)'); 270 | 271 | // Merge payload into req.options.values, omitting the blacklist. 272 | var values = mergeDefaults(_.cloneDeep(request.payload), _.omit(options.values, 'blacklist')); 273 | 274 | // Omit values that are in the blacklist (like query modifiers) 275 | values = _.omit(values, blacklist); 276 | 277 | // Omit any values w/ undefined values 278 | values = _.omit(values, function (p){ return _.isUndefined(p); }); 279 | 280 | // Set owner value if we ought 281 | if (options.actAsUser && 282 | options.setOwner) { 283 | 284 | // Grab appropriate transformation object 285 | var ownerAttrTransformation = child ? options.childOwnerAttrs : options.ownerAttrs; 286 | Hoek.assert(_.isObject(ownerAttrTransformation), 'Owner attribute options (`options.childOwnerAttr`/`options.ownerAttr`/`options.childOwnerAttrs`/`options.ownerAttrs`) should be set for use with `options.setOwner`.'); 287 | 288 | // Get new values with transformation 289 | var newValues = Hoek.transform(request.auth.credentials, ownerAttrTransformation); 290 | 291 | // Set new values 292 | _.merge(values, newValues); 293 | 294 | } 295 | 296 | return values; 297 | }, 298 | 299 | 300 | /** 301 | * Determine the model class to use w/ this blueprint action. 302 | * @param {Request} request 303 | * @param {Object} options 304 | * @return {WLCollection} 305 | */ 306 | parseModel: function () { 307 | 308 | // Ensure a model can be deduced from the request options. 309 | 310 | var model = options.model; 311 | if (!model) throw new Error(util.format('No `model` specified in route options.')); 312 | 313 | var Model = request.model[model]; 314 | if (!Model) throw new Error(util.format('Invalid route option, `model`.\nI don\'t know about any models named: `%s`',model)); 315 | 316 | return Model; 317 | }, 318 | 319 | 320 | /** 321 | * @param {Request} request 322 | * @param {Object} options 323 | */ 324 | parseSort: function () { 325 | return request.query.sort || options.sort || undefined; 326 | }, 327 | 328 | 329 | /** 330 | * @param {Request} request 331 | * @param {Object} options 332 | */ 333 | parseLimit: function () { 334 | 335 | var DEFAULT_LIMIT = 30; 336 | var DEFAULT_MAX_LIMIT = 30; 337 | 338 | var requestedLimit = request.query.limit ? 339 | Math.abs(request.query.limit) || false : 340 | false; 341 | 342 | var maxLimit = (typeof options.maxLimit !== 'undefined') ? 343 | options.maxLimit : 344 | DEFAULT_MAX_LIMIT; 345 | 346 | var limit; 347 | 348 | if (requestedLimit) { 349 | 350 | if (requestedLimit <= maxLimit) { 351 | limit = requestedLimit; 352 | } else { 353 | limit = DEFAULT_LIMIT; 354 | } 355 | 356 | } else if (typeof options.limit !== 'undefined') { 357 | 358 | limit = options.limit; 359 | } else { 360 | 361 | limit = DEFAULT_LIMIT; 362 | } 363 | 364 | // This is from sails. What does it do? 365 | // I suppose it would cast skip to something falsy if it were not a number. 366 | if (limit) { limit = +limit; } 367 | 368 | return limit; 369 | }, 370 | 371 | 372 | /** 373 | * @param {Request} request 374 | * @param {Object} options 375 | */ 376 | parseSkip: function () { 377 | var DEFAULT_SKIP = 0; 378 | var skip = request.query.skip || (typeof options.skip !== 'undefined' ? options.skip : DEFAULT_SKIP); 379 | 380 | // This is from sails. What does it do? 381 | // I suppose it would cast skip to something falsy if it were not a number. 382 | if (skip) { skip = +skip; } 383 | 384 | return skip; 385 | }, 386 | 387 | 388 | /** 389 | * Determine whether or not the deletedFlag should be included in where clause. 390 | * @return {Object} 391 | */ 392 | checkDeletedFlag: function() { 393 | // If the deleted flag is set, make sure those results aren't being included 394 | if(options.deletedFlag) { 395 | var values = {}; 396 | values[options.deletedAttr] = { '!': options.deletedValue }; 397 | return values||{}; 398 | } else { 399 | return {}; 400 | } 401 | }, 402 | 403 | 404 | getUserId: function() { 405 | 406 | Hoek.assert(options.actAsUser, 'Not currently acting as user, per `options.actAsUser`.'); 407 | Hoek.assert(_.isString(options.userIdProperty), '`options.userIdProperty` must be a string.'); 408 | 409 | // No creds, no user id. 410 | if (!_.isObject(request.auth.credentials)) { 411 | return false; 412 | } 413 | 414 | var userId = Hoek.reach(request.auth.credentials, options.userIdProperty); 415 | 416 | return userId; 417 | }, 418 | 419 | 420 | validOwnership: function(record, child) { 421 | 422 | if (!options.actAsUser || !options.requireOwner) { 423 | return true; 424 | } 425 | 426 | // Grab appropriate transformation object 427 | var ownerAttrTransformation = child ? options.childOwnerAttrs : options.ownerAttrs; 428 | Hoek.assert(_.isObject(ownerAttrTransformation), 'Owner attribute options (`options.childOwnerAttr`/`options.ownerAttr`/`options.childOwnerAttrs`/`options.ownerAttrs`) should be set for use with `options.requireOwner`.'); 429 | 430 | // There're no owner attrs set for this record 431 | if (_.isEmpty(ownerAttrTransformation)) { 432 | return true; 433 | } 434 | 435 | // Get new values with transformation 436 | var requiredValues = Hoek.transform(request.auth.credentials, ownerAttrTransformation); 437 | 438 | // Check for a non-match 439 | var requiredValue, recordOwnerValue; 440 | for (var attributeName in requiredValues) { 441 | requiredValue = requiredValues[attributeName]; 442 | 443 | recordOwnerValue = record[attributeName]; 444 | Hoek.assert(!_.isUndefined(recordOwnerValue), 'Record does not have provided owner attribute `' + attributeName + '`.') 445 | 446 | if (recordOwnerValue != requiredValue) { 447 | return false; 448 | } 449 | 450 | } 451 | 452 | // Passed all the tests. 453 | return true; 454 | 455 | }, 456 | 457 | 458 | getCreatedLocation: function(id) { 459 | 460 | if (options.createdLocation && id) { 461 | return util.format(options.createdLocation, id); 462 | } else { 463 | return null; 464 | } 465 | 466 | }, 467 | 468 | // Apply options.omit and request.query.omit 469 | omitFields: function(records) { 470 | 471 | // Grab omissions from options 472 | var optionsOmissions = options.omit || []; 473 | 474 | if (!_.isArray(optionsOmissions)) { 475 | 476 | optionsOmissions = [optionsOmissions]; 477 | } 478 | 479 | // Grab omissions from query 480 | var queryOmissions = request.query.omit || []; 481 | 482 | if (!_.isArray(queryOmissions)) { 483 | 484 | queryOmissions = [queryOmissions]; 485 | } 486 | 487 | // All omissions 488 | var omissions = optionsOmissions.concat(queryOmissions) 489 | 490 | // If there are no omissions, peace 491 | if (!omissions.length) { 492 | 493 | return records; 494 | } 495 | 496 | // Make records array 497 | var wasntArray = false; 498 | if (!_.isArray(records)) { 499 | 500 | records = [records]; 501 | wasntArray = true; 502 | } 503 | 504 | // Do the deed, omit keys on each record 505 | records.forEach(function(record) { 506 | 507 | omitDeep(record, omissions); 508 | }); 509 | 510 | return wasntArray ? records[0] : records; 511 | } 512 | 513 | }; 514 | 515 | } 516 | 517 | // Attempt to parse JSON 518 | // If the parse fails, return the error object 519 | // If JSON is falsey, return null 520 | // (this is so that it will be ignored if not specified) 521 | function tryToParseJSON(json) { 522 | if (!_.isString(json)) return null; 523 | try { 524 | return JSON.parse(json); 525 | } 526 | catch (e) { return e; } 527 | } 528 | 529 | // keys, an array of Hoek style deep keys, to omit from obj 530 | function omitDeep(obj, keys) { 531 | 532 | Hoek.assert(_.isArray(keys), 'Internal omitDeep function requires keys parameter to be an array.'); 533 | 534 | var path, ref; 535 | keys.forEach(function(key) { 536 | 537 | path = key.split('.'); 538 | ref = obj; 539 | 540 | for (var i = 0, il = path.length; i < il; ++i) { 541 | var segment = path[i]; 542 | if (i + 1 === il) { 543 | 544 | if (_.isArray(ref)) { 545 | 546 | for (var j = 0, jl = ref.length; j < jl; ++j) { 547 | 548 | delete ref[j][segment]; 549 | } 550 | 551 | } else { 552 | 553 | delete ref[segment]; 554 | } 555 | 556 | } 557 | 558 | ref = ref[segment]; 559 | } 560 | }); 561 | 562 | } 563 | -------------------------------------------------------------------------------- /lib/actions/add.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var _ = require('lodash'); 5 | var Boom = require('boom'); 6 | var Hoek = require('hoek'); 7 | var Async = require('async'); 8 | var WL2Boom = require('waterline-to-boom'); 9 | var GeneralUtil = require('../generalUtil'); 10 | 11 | /** 12 | * Add Record To Collection 13 | * 14 | * post /:modelIdentity/:id/:collectionAttr/:childid 15 | * 16 | * Associate one record with the collection attribute of another. 17 | * e.g. add a Horse named "Jimmy" to a Farm's "animals". 18 | * If the record being added has a primary key value already, it will 19 | * just be linked. If it doesn't, a new record will be created, then 20 | * linked appropriately. In either case, the association is bidirectional. 21 | * 22 | */ 23 | 24 | module.exports = function addToCollection (route, origOptions) { 25 | 26 | return function (request, reply) { 27 | 28 | // Transform the original options using options hook 29 | var options = GeneralUtil.applyOptionsHook(request, origOptions); 30 | 31 | var RequestState = request.plugins.bedwetter = { 32 | action: 'add', 33 | options: options 34 | }; 35 | 36 | var actionUtil = require('../actionUtil')(request, options); 37 | 38 | // Ensure a model and alias can be deduced from the request. 39 | var Model = actionUtil.parseModel(); 40 | var relation = options.associationAttr; 41 | 42 | if (!relation) { 43 | return reply(Boom.wrap(new Error('Missing required route option, `options.associationAttr`.'))); 44 | } 45 | 46 | // The primary key of the parent record 47 | var parentPk; 48 | 49 | try { 50 | parentPk = actionUtil.requirePk(); 51 | } catch(e) { 52 | return reply(Boom.wrap(e)); 53 | } 54 | 55 | // Get the model class of the child in order to figure out the name of 56 | // the primary key attribute. 57 | 58 | var associationAttr = _.findWhere(options.associations, { alias: relation }); 59 | Hoek.assert(_.isObject(associationAttr), 'Bad association.'); 60 | 61 | var ChildModel = request.model[associationAttr.collection]; 62 | var childPkAttr = ChildModel.primaryKey; 63 | 64 | 65 | // The child record to associate is defined by either... 66 | var child; 67 | 68 | // ...a primary key: 69 | var supposedChildPk = actionUtil.parsePk(true); 70 | 71 | // ...or an object of values: 72 | if (!supposedChildPk) { 73 | 74 | options.values = options.values || {}; 75 | options.values.blacklist = options.values.blacklist || []; 76 | // Make sure nobody can specify the id of the child. 77 | 78 | // You either link a record with the id in the URL or create an enitrely new record without specifying the id! 79 | options.values.blacklist.push(childPkAttr); 80 | child = actionUtil.parseValues(true); 81 | 82 | } 83 | 84 | if (!child && !supposedChildPk) { 85 | return reply(Boom.badRequest('You must specify the record to add (either the primary key of an existing record to link, or a new object without a primary key which will be used to create a record then link it.)')); 86 | } 87 | 88 | var createdChild = false; 89 | 90 | Async.auto({ 91 | 92 | // Look up the parent record 93 | parent: function (cb) { 94 | 95 | Model.findOne(parentPk).exec(function foundParent(err, parentRecord) { 96 | 97 | if (err) return cb(err); 98 | if (!parentRecord) return cb(Boom.notFound()); 99 | if (!actionUtil.validOwnership(parentRecord, false)) return cb(Boom.unauthorized()); 100 | if (!parentRecord[relation] || !parentRecord[relation].add) return cb(Boom.notFound()); 101 | 102 | cb(null, parentRecord); 103 | 104 | }); 105 | }, 106 | 107 | // If a primary key was specified in the `child` object we parsed 108 | // from the request, look it up to make sure it exists. Send back its primary key value. 109 | // This is here because, although you can do this with `.save()`, you can't actually 110 | // get ahold of the created child record data, unless you create it first. 111 | actualChild: ['parent', function(cb) { 112 | 113 | // Below, we use the primary key attribute to pull out the primary key value 114 | // (which might not have existed until now, if the .add() resulted in a `create()`) 115 | 116 | // If the primary key was specified for the child record, we should try to find it 117 | if (supposedChildPk) { 118 | 119 | ChildModel.findOne(supposedChildPk).exec(function foundChild(err, childRecord) { 120 | 121 | if (err) return cb(err); 122 | 123 | // Trying to associate a record that does not exist 124 | if (!childRecord) 125 | return cb(Boom.notFound()); 126 | 127 | if (!actionUtil.validOwnership(childRecord, true)) 128 | return cb(Boom.unauthorized()); 129 | 130 | // Otherwise use the one we found. 131 | return cb(null, childRecord); 132 | }); 133 | 134 | } else { // Otherwise, it must be referring to a new thing, so create it. 135 | 136 | ChildModel.create(child).exec(function createdNewChild (err, newChildRecord) { 137 | 138 | if (err) return cb(err); 139 | createdChild = true; 140 | 141 | return cb(null, newChildRecord); 142 | }); 143 | 144 | } 145 | 146 | }], 147 | 148 | // Add the child record to the parent's collection 149 | add: ['parent', 'actualChild', function(cb, asyncData) { 150 | 151 | try { 152 | 153 | // `collection` is the parent record's collection we 154 | // want to add the child to. 155 | var collection = asyncData.parent[relation]; 156 | collection.add(asyncData.actualChild[childPkAttr]); 157 | 158 | return cb(); 159 | 160 | } catch (err) { 161 | 162 | // TODO: could all this be simplified? do we need try/catch for record.add? 163 | // I think not. It's just an Array.push: https://github.com/balderdashy/waterline/blob/4653f8a18016d2bcde9a70c90dd63a7c69381935/lib/waterline/model/lib/association.js 164 | // On the flipside, what if this relation doesn't exist? Test!! Should this err be turned into a notFound, similarly to in the parent function? 165 | // Okay now this relation is properly tested for in the parent function 166 | if (err) { 167 | return cb(err); 168 | } 169 | 170 | return cb(); 171 | } 172 | 173 | }] 174 | }, 175 | 176 | // Finally, save the parent record 177 | function readyToSave (err, asyncResults) { 178 | 179 | if (err) return reply(WL2Boom(err)); 180 | 181 | asyncResults.parent.save(function saved(err, savedParent) { 182 | 183 | // Ignore `insert` errors for duplicate adds, as add is idempotent. 184 | var isDuplicateInsertError = (err && typeof err === 'object' && err.length && err[0] && err[0].type === 'insert'); 185 | if (err && !isDuplicateInsertError) return reply(WL2Boom(err)); 186 | 187 | // Share primary record 188 | RequestState.primaryRecord = asyncResults.parent; 189 | 190 | if (createdChild) { 191 | 192 | var location = actionUtil.getCreatedLocation(asyncResults.actualChild[childPkAttr]); 193 | 194 | // Omit fields if necessary 195 | actionUtil.omitFields(asyncResults.actualChild); 196 | 197 | return reply(asyncResults.actualChild).created(location); 198 | 199 | } else { 200 | 201 | // Share secondary record 202 | RequestState.secondaryRecord = asyncResults.actualChild; 203 | 204 | // "HTTP 204 / No Content" means success 205 | return reply().code(204); 206 | } 207 | 208 | }); 209 | 210 | }); // end async.auto 211 | 212 | } 213 | 214 | }; 215 | -------------------------------------------------------------------------------- /lib/actions/create.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var Boom = require('boom'); 5 | var WL2Boom = require('waterline-to-boom'); 6 | var GeneralUtil = require('../generalUtil'); 7 | 8 | /** 9 | * Create Record 10 | * 11 | * post /:modelIdentity 12 | * 13 | * An API call to find and return a single model instance from the data adapter 14 | * using the specified criteria. If an id was specified, just the instance with 15 | * that unique id will be returned. 16 | * 17 | */ 18 | 19 | module.exports = function createRecord (route, origOptions) { 20 | 21 | return function(request, reply) { 22 | 23 | // Transform the original options using options hook 24 | var options = GeneralUtil.applyOptionsHook(request, origOptions); 25 | 26 | var RequestState = request.plugins.bedwetter = { 27 | action: 'create', 28 | options: options 29 | }; 30 | 31 | var actionUtil = require('../actionUtil')(request, options); 32 | 33 | var Model = actionUtil.parseModel(); 34 | 35 | // Create data object (monolithic combination of all parameters) 36 | // Omit the blacklisted params (like JSONP callback param, etc.) 37 | var data = actionUtil.parseValues(); 38 | 39 | // Create new instance of model using data from params 40 | Model.create(data).exec(function created (err, newInstance) { 41 | 42 | // Differentiate between waterline-originated validation errors 43 | // and serious underlying issues. Respond with badRequest if a 44 | // validation error is encountered, w/ validation info. 45 | if (err) return reply(WL2Boom(err)); 46 | 47 | var location = actionUtil.getCreatedLocation(newInstance.id); 48 | 49 | // Omit fields if necessary 50 | actionUtil.omitFields(newInstance); 51 | 52 | // "HTTP 201 / Created" 53 | reply(newInstance).created(location); 54 | }); 55 | } 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /lib/actions/destroy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var Boom = require('boom'); 5 | var WL2Boom = require('waterline-to-boom'); 6 | var GeneralUtil = require('../generalUtil'); 7 | 8 | /** 9 | * Destroy One Record 10 | * 11 | * delete /:modelIdentity/:id 12 | * 13 | * Destroys the single model instance with the specified `id` from 14 | * the data adapter for the given model if it exists. 15 | * 16 | */ 17 | module.exports = function destroyOneRecord (route, origOptions) { 18 | 19 | return function(request, reply) { 20 | 21 | // Transform the original options using options hook 22 | var options = GeneralUtil.applyOptionsHook(request, origOptions); 23 | 24 | var RequestState = request.plugins.bedwetter = { 25 | action: 'destroy', 26 | options: options 27 | }; 28 | 29 | var actionUtil = require('../actionUtil')(request, options); 30 | 31 | var Model = actionUtil.parseModel(); 32 | 33 | // The primary key of the record 34 | var pk; 35 | 36 | try { 37 | pk = actionUtil.requirePk(); 38 | } catch(e) { 39 | return reply(Boom.wrap(e)); 40 | } 41 | 42 | Model.findOne(pk).exec(function foundRecord (err, record) { 43 | 44 | if (err) 45 | return reply(WL2Boom(err)); 46 | 47 | if(!record) 48 | return reply(Boom.notFound('No record found with the specified `id`.')); 49 | 50 | if(!actionUtil.validOwnership(record, false)) 51 | return reply(Boom.unauthorized()); 52 | 53 | // Check for setting of deleted flag rather than destroying 54 | if(options.deletedFlag) { 55 | 56 | var values = {}; 57 | values[options.deletedAttr] = options.deletedValue; 58 | 59 | Model.update(record[Model.primaryKey], values).exec(function updated(err, records) { 60 | 61 | if (err) return reply(WL2Boom(err)); 62 | 63 | // Share primary record 64 | RequestState.primaryRecord = records[0]; 65 | 66 | // "HTTP 204 No Content" means success 67 | return reply().code(204); 68 | 69 | }); 70 | 71 | } else { 72 | 73 | Model.destroy(record[Model.primaryKey]).exec(function destroyedRecord (err) { 74 | 75 | if (err) return reply(WL2Boom(err)); 76 | 77 | // Share primary record 78 | RequestState.primaryRecord = record; 79 | 80 | // "HTTP 204 / No Content" means success 81 | return reply().code(204); 82 | 83 | }); 84 | 85 | } 86 | 87 | }); 88 | 89 | } 90 | 91 | }; 92 | -------------------------------------------------------------------------------- /lib/actions/find.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var Boom = require('boom'); 5 | var WL2Boom = require('waterline-to-boom'); 6 | var _ = require('lodash'); 7 | var GeneralUtil = require('../generalUtil'); 8 | 9 | /** 10 | * Find Records 11 | * 12 | * get /:modelIdentity 13 | * 14 | * An API call to find and return model instances from the data adapter 15 | * using the specified criteria. If an id was specified, just the instance 16 | * with that unique id will be returned. 17 | * 18 | * Optional: 19 | * @param {Object} where - the find criteria (passed directly to the ORM) 20 | * @param {Integer} limit - the maximum number of records to send back (useful for pagination) 21 | * @param {Integer} skip - the number of records to skip (useful for pagination) 22 | * @param {String} sort - the order of returned records, e.g. `name ASC` or `age DESC` 23 | * 24 | */ 25 | 26 | module.exports = function findRecords (route, origOptions) { 27 | 28 | return function(request, reply) { 29 | 30 | // Transform the original options using options hook 31 | var options = GeneralUtil.applyOptionsHook(request, origOptions); 32 | 33 | var RequestState = request.plugins.bedwetter = { 34 | action: 'find', 35 | options: options 36 | }; 37 | 38 | var actionUtil = require('../actionUtil')(request, options); 39 | 40 | // Look up the model 41 | var Model = actionUtil.parseModel(); 42 | 43 | // Lookup for records that match the specified criteria. Are we just counting? 44 | var query; 45 | if (options._private.count) { 46 | 47 | query = Model.count() 48 | .where( actionUtil.parseCriteria() ); 49 | 50 | } else { 51 | 52 | query = Model.find() 53 | .where( actionUtil.parseCriteria() ) 54 | .limit( actionUtil.parseLimit() ) 55 | .skip( actionUtil.parseSkip() ) 56 | .sort( actionUtil.parseSort() ); 57 | 58 | // TODO: .populateEach(req.options); 59 | query = actionUtil.populateEach(query); 60 | 61 | } 62 | 63 | query.exec(function found(err, matchingRecords) { 64 | 65 | if (err) return reply(WL2Boom(err)); 66 | 67 | // Omit fields if necessary 68 | if (!options._private.count) { 69 | 70 | actionUtil.omitFields(matchingRecords); 71 | } 72 | 73 | // If count is set, this this an integer. Otherwise, it's an array of matching records. 74 | return reply(matchingRecords); 75 | 76 | }); 77 | 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /lib/actions/findOne.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var Boom = require('boom'); 5 | var WL2Boom = require('waterline-to-boom'); 6 | var GeneralUtil = require('../generalUtil'); 7 | 8 | /** 9 | * Find One Record 10 | * 11 | * get /:modelIdentity/:id 12 | * 13 | * An API call to find and return a single model instance from the data adapter 14 | * using the specified id. 15 | * 16 | */ 17 | 18 | module.exports = function findOneRecord (route, origOptions) { 19 | 20 | return function(request, reply) { 21 | 22 | // Transform the original options using options hook 23 | var options = GeneralUtil.applyOptionsHook(request, origOptions); 24 | 25 | var RequestState = request.plugins.bedwetter = { 26 | action: 'findone', 27 | options: options 28 | }; 29 | 30 | var actionUtil = require('../actionUtil')(request, options); 31 | 32 | var Model = actionUtil.parseModel(); 33 | 34 | // The primary key of the record 35 | var pk; 36 | 37 | try { 38 | pk = actionUtil.requirePk(); 39 | } catch(e) { 40 | return reply(Boom.wrap(e)); 41 | } 42 | 43 | var query = Model.findOne(pk).where(actionUtil.checkDeletedFlag()); 44 | 45 | query = actionUtil.populateEach(query); 46 | 47 | query.exec(function found(err, matchingRecord) { 48 | 49 | if (err) 50 | return reply(WL2Boom(err)); 51 | 52 | if (!matchingRecord) 53 | return reply(Boom.notFound('No record found with the specified `id`.')); 54 | 55 | if (!actionUtil.validOwnership(matchingRecord, false)) 56 | return reply(Boom.unauthorized()); 57 | 58 | // Share primary record 59 | RequestState.primaryRecord = matchingRecord; 60 | 61 | // Omit fields if necessary 62 | actionUtil.omitFields(matchingRecord); 63 | 64 | reply(matchingRecord); 65 | 66 | }); 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /lib/actions/populate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var Boom = require('boom'); 5 | var WL2Boom = require('waterline-to-boom'); 6 | var _ = require('lodash'); 7 | var GeneralUtil = require('../generalUtil'); 8 | 9 | /** 10 | * Populate an association 11 | * 12 | * get /model/:parentid/relation 13 | * get /model/:parentid/relation/:id 14 | * 15 | * @param {Object} where - the find criteria (passed directly to the ORM) 16 | * @param {Integer} limit - the maximum number of records to send back (useful for pagination) 17 | * @param {Integer} skip - the number of records to skip (useful for pagination) 18 | * @param {String} sort - the order of returned records, e.g. `name ASC` or `age DESC` 19 | * 20 | */ 21 | 22 | module.exports = function expand(route, origOptions) { 23 | 24 | return function(request, reply) { 25 | 26 | // Transform the original options using options hook 27 | var options = GeneralUtil.applyOptionsHook(request, origOptions); 28 | 29 | var RequestState = request.plugins.bedwetter = { 30 | action: 'populate', 31 | options: options 32 | }; 33 | 34 | var actionUtil = require('../actionUtil')(request, options); 35 | 36 | var Model = actionUtil.parseModel(); 37 | var relation = options.associationAttr; 38 | 39 | if (!relation || !Model) return reply(Boom.notFound()); 40 | 41 | var parentPk; 42 | 43 | try { 44 | parentPk = actionUtil.requirePk(); 45 | } catch(e) { 46 | return reply(Boom.wrap(e)); 47 | } 48 | 49 | // Determine whether to populate using a criteria, or the 50 | // specified primary key of the child record, or with no 51 | // filter at all. 52 | var childPk = actionUtil.parsePk(true); 53 | var criteria; 54 | 55 | if (childPk) { 56 | 57 | criteria = childPk; 58 | 59 | } else { 60 | 61 | if (options._private.count) { 62 | 63 | criteria = { 64 | where: actionUtil.parseCriteria(true) 65 | }; 66 | 67 | } else { 68 | 69 | criteria = { 70 | where: actionUtil.parseCriteria(true), 71 | skip: actionUtil.parseSkip(), 72 | limit: actionUtil.parseLimit(), 73 | sort: actionUtil.parseSort() 74 | }; 75 | 76 | } 77 | } 78 | 79 | Model 80 | .findOne(parentPk) 81 | .populate(relation, criteria) 82 | .exec(function found(err, matchingRecord) { 83 | 84 | if (err) 85 | return reply(WL2Boom(err)); 86 | 87 | if (!matchingRecord) 88 | return reply(Boom.notFound('No record found with the specified id.')); 89 | 90 | if (!actionUtil.validOwnership(matchingRecord, false)) 91 | return reply(Boom.unauthorized()); 92 | 93 | if (!matchingRecord[relation]) 94 | return reply(Boom.notFound(util.format('Specified record (%s) is missing relation `%s`', GeneralUtil.pkToString(parentPk), relation))); 95 | 96 | // Share primary record 97 | RequestState.primaryRecord = matchingRecord; 98 | 99 | // If looking for a particular relation, return that it exists or that it does not. 100 | // Otherwise, just return the results. 101 | if (childPk) { 102 | 103 | if (matchingRecord[relation].length) { 104 | 105 | if (actionUtil.validOwnership(matchingRecord[relation][0], true)) { 106 | 107 | // Share secondary record 108 | RequestState.secondaryRecord = matchingRecord[relation][0]; 109 | 110 | // The relation exists. Acknowledge with "204 No Content" 111 | return reply().code(204); 112 | 113 | } else { 114 | 115 | // Not authorized to check, didn't own child record 116 | return reply(Boom.unauthorized()); 117 | 118 | } 119 | 120 | } else { 121 | 122 | // The relation does not exist. That's a 404! 123 | return reply(Boom.notFound()); 124 | } 125 | 126 | } else { 127 | 128 | if (options._private.count) { 129 | 130 | // Here's your count! 131 | return reply(matchingRecord[relation].length); 132 | 133 | } else { 134 | 135 | // Omit fields if necessary 136 | actionUtil.omitFields(matchingRecord[relation]); 137 | 138 | // Here are your results 139 | return reply(matchingRecord[relation]); 140 | 141 | } 142 | 143 | } 144 | 145 | 146 | }); 147 | 148 | } 149 | 150 | }; 151 | -------------------------------------------------------------------------------- /lib/actions/remove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var Boom = require('boom'); 5 | var Hoek = require('hoek'); 6 | var WL2Boom = require('waterline-to-boom'); 7 | var _ = require('lodash'); 8 | var GeneralUtil = require('../generalUtil'); 9 | 10 | /** 11 | * delete /model/:parentid/relation/:id 12 | * 13 | * Remove a member from an association 14 | * 15 | */ 16 | 17 | module.exports = function remove(route, origOptions) { 18 | 19 | return function(request, reply) { 20 | 21 | // Transform the original options using options hook 22 | var options = GeneralUtil.applyOptionsHook(request, origOptions); 23 | 24 | var RequestState = request.plugins.bedwetter = { 25 | action: 'remove', 26 | options: options 27 | }; 28 | 29 | var actionUtil = require('../actionUtil')(request, options); 30 | 31 | // Ensure a model and alias can be deduced from the request. 32 | var Model = actionUtil.parseModel(); 33 | var relation = options.associationAttr; 34 | 35 | if (!relation) { 36 | return reply(Boom.wrap(new Error('Missing required route option, `options.associationAttr`.'))); 37 | } 38 | 39 | var associationAttr = _.findWhere(options.associations, { alias: relation }); 40 | Hoek.assert(_.isObject(associationAttr), 'Bad association.'); 41 | 42 | var ChildModel = request.model[associationAttr.collection]; 43 | var childPkAttr = ChildModel.primaryKey; 44 | 45 | // The primary key of the parent record 46 | var parentPk; 47 | 48 | try { 49 | parentPk = actionUtil.requirePk(); 50 | } catch(e) { 51 | return reply(Boom.wrap(e)); 52 | } 53 | 54 | // The primary key of the child record to remove 55 | // from the aliased collection 56 | var childPk; 57 | 58 | try { 59 | childPk = actionUtil.requirePk(true); 60 | } catch(e) { 61 | return reply(Boom.wrap(e)); 62 | } 63 | 64 | Model 65 | .findOne(parentPk).populate(relation, childPk).exec(function found(err, parentRecord) { 66 | 67 | if (err) 68 | return reply(WL2Boom(err)); 69 | 70 | // That parent record wasn't found 71 | if (!parentRecord) 72 | return reply(Boom.notFound()); 73 | 74 | // Check parent record owner 75 | if (!actionUtil.validOwnership(parentRecord, false)) 76 | return reply(Boom.unauthorized()); 77 | 78 | // That child record wasn't found. 79 | if (!parentRecord[relation] || !parentRecord[relation][0]) 80 | return reply(Boom.notFound()); 81 | 82 | // Check child record owner 83 | if (!actionUtil.validOwnership(parentRecord[relation][0], true)) 84 | return reply(Boom.unauthorized()); 85 | 86 | parentRecord[relation].remove(parentRecord[relation][0][childPkAttr]); 87 | 88 | parentRecord.save(function(err, parentRecordSaved) { 89 | 90 | if (err) return reply(WL2Boom(err)); 91 | 92 | // Share primary and secondary records 93 | RequestState.primaryRecord = parentRecordSaved; 94 | RequestState.secondaryRecord = parentRecord[relation][0]; 95 | 96 | // "HTTP 204 / No Content" means success 97 | return reply().code(204); 98 | 99 | }); 100 | }); 101 | 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /lib/actions/update.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var Boom = require('boom'); 5 | var WL2Boom = require('waterline-to-boom'); 6 | var Async = require('async'); 7 | var GeneralUtil = require('../generalUtil'); 8 | 9 | 10 | /** 11 | * Update One Record 12 | * 13 | * An API call to update a model instance with the specified `id`, 14 | * treating the other unbound parameters as attributes. 15 | * 16 | */ 17 | 18 | module.exports = function updateOneRecord (route, origOptions) { 19 | 20 | return function(request, reply) { 21 | 22 | // Transform the original options using options hook 23 | var options = GeneralUtil.applyOptionsHook(request, origOptions); 24 | 25 | var RequestState = request.plugins.bedwetter = { 26 | action: 'update', 27 | options: options 28 | }; 29 | 30 | var actionUtil = require('../actionUtil')(request, options); 31 | 32 | // Look up the model 33 | var Model = actionUtil.parseModel(); 34 | 35 | // Locate and validate the required `id` parameter. 36 | var pk; 37 | 38 | try { 39 | pk = actionUtil.requirePk(); 40 | } catch(e) { 41 | return reply(Boom.wrap(e)); 42 | } 43 | 44 | // Create `values` object (monolithic combination of all parameters) 45 | // But omit the blacklisted params (like JSONP callback param, etc.) 46 | var values = actionUtil.parseValues(); 47 | 48 | // Find, update, then reply 49 | Async.waterfall([ 50 | 51 | // Find 52 | function(cb) { 53 | 54 | // TODO: DRY this up with findOne? 55 | Model.findOne(pk).exec(function found(err, matchingRecord) { 56 | 57 | if (err) 58 | return cb(err); 59 | 60 | if (!matchingRecord) 61 | return cb(Boom.notFound('No record found with the specified `id`.')); 62 | 63 | if (!actionUtil.validOwnership(matchingRecord, false)) 64 | return cb(Boom.unauthorized()); 65 | 66 | return cb(null, matchingRecord); 67 | 68 | }); 69 | }, 70 | 71 | // Update 72 | function(matchingRecord, cb) { 73 | 74 | Model.update(matchingRecord[Model.primaryKey], values).exec(function updated(err, records) { 75 | 76 | // Differentiate between waterline-originated validation errors 77 | // and serious underlying issues. Respond with badRequest if a 78 | // validation error is encountered, w/ validation info. 79 | if (err) return cb(err); 80 | 81 | // Because this should only update a single record and update 82 | // returns an array, just use the first item. 83 | if (!records || !records.length) { 84 | return cb(Boom.notFound()); 85 | } 86 | 87 | var updatedRecord = records[0]; 88 | 89 | //If more than one record was returned, something is amiss. 90 | if (records.length > 1) { 91 | // TODO: Log it 92 | } 93 | 94 | return cb(null, updatedRecord); 95 | 96 | }); 97 | 98 | }], 99 | 100 | // Reply 101 | function(err, updatedRecord) { 102 | 103 | if (err) return reply(WL2Boom(err)); 104 | 105 | // Share primary record 106 | RequestState.primaryRecord = updatedRecord; 107 | 108 | // Omit fields if necessary 109 | actionUtil.omitFields(updatedRecord); 110 | 111 | return reply(updatedRecord); 112 | 113 | }); 114 | 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /lib/generalUtil.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var internals = {}; 4 | 5 | module.exports = { 6 | 7 | applyOptionsHook: function(request, origOptions) { 8 | 9 | if (origOptions.hooks && typeof origOptions.hooks.options === "function") { 10 | 11 | return origOptions.hooks.options(_.cloneDeep(origOptions), request); 12 | } else { 13 | 14 | return _.cloneDeep(origOptions); 15 | } 16 | 17 | }, 18 | 19 | // This pk comes in the form of waterline criteria: object, string, or int 20 | pkToString: function(pk) { 21 | 22 | if (_.isObject(pk) && Object.keys(pk).length) { 23 | 24 | return String(pk[Object.keys(pk)[0]]); 25 | } else { 26 | 27 | return String(pk); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('lodash'); 6 | var Call = require('call'); 7 | var Hoek = require('hoek'); 8 | // var pluralize = require('pluralize'); 9 | var BedWetters = { 10 | create : require('./actions/create'), 11 | find : require('./actions/find'), 12 | findone : require('./actions/findOne'), 13 | update : require('./actions/update'), 14 | destroy : require('./actions/destroy'), 15 | populate: require('./actions/populate'), 16 | add : require('./actions/add'), 17 | remove : require('./actions/remove'), 18 | } 19 | 20 | 21 | var internals = {}; 22 | 23 | internals.defaults = { 24 | 25 | populate: false, 26 | prefix: '', 27 | createdLocation: false, 28 | //pluralize: false, TODO: support on opts.associationAttr and opts.model 29 | 30 | actAsUser: false, 31 | userUrlPrefix: 'user', // this is in the url in lieu of /users/{id} 32 | userModel: 'users', // since it's not in the url 33 | userIdProperty: 'id', // on auth credentials 34 | 35 | setOwner: false, 36 | requireOwner: false, 37 | ownerAttr: 'owner', 38 | ownerAttrs: {}, 39 | childOwnerAttr: 'owner', 40 | childOwnerAttrs: {}, 41 | 42 | // Setting a deleted flag rather than destroying 43 | deletedFlag: false, // destroy by default 44 | deletedAttr: 'deleted', 45 | deletedValue: 1, 46 | 47 | // Change pk field 48 | pkAttr: false, 49 | childPkAttr: false, 50 | 51 | // Omit, later also pick, etc. 52 | omit: [], 53 | 54 | // A place for hooks 55 | hooks: { 56 | options: _.identity 57 | }, 58 | 59 | // A place for private info to get passed around 60 | _private: { 61 | actAsUserModifiedPath: false, 62 | count: false 63 | } 64 | 65 | } 66 | 67 | /** 68 | * BedWet 69 | * 70 | * (see http://nodejs.org/api/documentation.html#documentation_stability_index) 71 | */ 72 | 73 | exports.register = function(server, options, next) { 74 | 75 | server.dependency('dogwater'); 76 | 77 | server.handler('bedwetter', function(route, handlerOptions) { 78 | 79 | // handlerOptions come user-defined in route definition 80 | // nothing should override these! 81 | 82 | var Dogwater = server.plugins.dogwater; 83 | 84 | var thisRouteOpts = _.cloneDeep(internals.defaults); 85 | 86 | // Plugin-level user-defined options 87 | _.merge(thisRouteOpts, options) 88 | 89 | // Route-level user-defined options 90 | _.merge(thisRouteOpts, handlerOptions); 91 | 92 | // Route-level info (should not override plugin options & handler options) 93 | internals.setOptionsFromRouteInfo(route, thisRouteOpts); 94 | 95 | // Set associations now that the model is locked-down 96 | // TODO: figure out why these don't stick when model grabbed from parseModel 97 | var Model = Dogwater[thisRouteOpts.model]; 98 | 99 | Hoek.assert(Model, 'Model `' + thisRouteOpts.model + '` must exist to build route.'); 100 | 101 | // Don't overwrite associations if they've been set as an option for some reason 102 | _.defaults(thisRouteOpts, {associations: internals.getAssociationsFromModel(Model)}); 103 | 104 | thisRouteOpts = internals.normalizeOptions(thisRouteOpts); 105 | 106 | // Here's our little bed wetter! 107 | var bedWetter = internals.determineBedWetter(route, thisRouteOpts); 108 | 109 | return bedWetter(route, thisRouteOpts); 110 | 111 | }); 112 | 113 | next(); 114 | }; 115 | 116 | exports.register.attributes = { 117 | pkg: require('../package.json') 118 | }; 119 | 120 | 121 | internals.Router = new Call.Router({}); 122 | 123 | internals.determineBedWetter = function(route, thisRouteOpts) { 124 | 125 | var method = route.method; 126 | var path = route.path; 127 | 128 | path = internals.normalizePath(path, thisRouteOpts); 129 | 130 | var pathInfo = internals.Router.analyze(path); 131 | var pathSegments = pathInfo.segments.length; 132 | var err; 133 | 134 | // Account for `update` allowing POST or PATCH 135 | if (_.isArray(method) && 136 | method.length == 2 && 137 | _.intersection(method, ['post', 'patch']).length == 2) { 138 | 139 | method = 'patch'; 140 | } 141 | 142 | var countIsOkay = false; 143 | var bedwetter; 144 | switch (method) { 145 | 146 | case 'post': 147 | 148 | if (pathSegments == 1 && 149 | pathInfo.segments[0].literal) { // model 150 | 151 | // Create 152 | bedwetter = BedWetters.create; 153 | 154 | } else if (pathSegments == 2 && 155 | pathInfo.segments[0].literal && // model 156 | pathInfo.segments[1].name) { // record 157 | 158 | // Patched update 159 | bedwetter = BedWetters.update; 160 | 161 | } else if (pathSegments == 3 && 162 | pathInfo.segments[0].literal && // model 163 | pathInfo.segments[1].name && // record 164 | pathInfo.segments[2].literal) { // association 165 | 166 | // Create and add to relation 167 | bedwetter = BedWetters.add; 168 | 169 | } else { 170 | err = new Error('This ' + method + ' route does not match a BedWetting pattern.'); 171 | } 172 | 173 | break; 174 | 175 | case 'patch': 176 | 177 | if (pathSegments == 2 && 178 | pathInfo.segments[0].literal && // model 179 | pathInfo.segments[1].name) { // record 180 | 181 | // Patched update 182 | bedwetter = BedWetters.update; 183 | 184 | } else { 185 | err = new Error('This ' + method + ' route does not match a BedWetting pattern.'); 186 | } 187 | 188 | break; 189 | 190 | case 'put': 191 | 192 | if (pathSegments == 4 && 193 | pathInfo.segments[0].literal && // model 194 | pathInfo.segments[1].name && // record 195 | pathInfo.segments[2].literal && // association 196 | pathInfo.segments[3].name) { // record_to_add 197 | 198 | // Add to a relation 199 | bedwetter = BedWetters.add; 200 | 201 | } else { 202 | err = new Error('This ' + method + ' route does not match a BedWetting pattern.'); 203 | } 204 | 205 | break; 206 | 207 | case 'get': 208 | 209 | if (pathSegments == 1 && 210 | pathInfo.segments[0].literal) { // model 211 | 212 | countIsOkay = true; 213 | 214 | // Find with criteria 215 | bedwetter = BedWetters.find; 216 | 217 | } else if (pathSegments == 2 && 218 | pathInfo.segments[0].literal && // model 219 | pathInfo.segments[1].name) { // record 220 | 221 | // Find one by id 222 | bedwetter = BedWetters.findone; 223 | 224 | } else if (pathSegments == 3 && 225 | pathInfo.segments[0].literal && // model 226 | pathInfo.segments[1].name && // record 227 | pathInfo.segments[2].literal) { // association 228 | 229 | countIsOkay = true; 230 | 231 | // Get associated records 232 | bedwetter = BedWetters.populate; 233 | 234 | } else if (pathSegments == 4 && 235 | pathInfo.segments[0].literal && // model 236 | pathInfo.segments[1].name && // record 237 | pathInfo.segments[2].literal && // association 238 | pathInfo.segments[3].name) { // record_to_check 239 | 240 | // Check for an association between records 241 | bedwetter = BedWetters.populate; 242 | 243 | } else { 244 | err = new Error('This ' + method + ' route does not match a BedWetting pattern.'); 245 | } 246 | 247 | break; 248 | 249 | case 'delete': 250 | 251 | if (pathSegments == 2 && 252 | pathInfo.segments[0].literal && // model 253 | pathInfo.segments[1].name) { // record 254 | 255 | bedwetter = BedWetters.destroy; 256 | 257 | } else if (pathSegments == 4 && 258 | pathInfo.segments[0].literal && // model 259 | pathInfo.segments[1].name && // record 260 | pathInfo.segments[2].literal && // association 261 | pathInfo.segments[3].name) { // record_to_remove 262 | 263 | bedwetter = BedWetters.remove; 264 | 265 | } else { 266 | err = new Error('This ' + method + ' route does not match a BedWetting pattern.'); 267 | } 268 | 269 | break; 270 | 271 | default: 272 | err = new Error('Method isn\'t a BedWetter. Must be POST, GET, DELETE, PUT, or PATCH.'); 273 | break; 274 | } 275 | 276 | // Only allow counting on find and array populate 277 | if (thisRouteOpts._private.count && !countIsOkay) { 278 | err = new Error('This bedwetter can\'t count!'); 279 | } 280 | 281 | if (err) { 282 | throw err; 283 | } else { 284 | return bedwetter; 285 | } 286 | 287 | } 288 | 289 | internals.setOptionsFromRouteInfo = function(route, thisRouteOpts) { 290 | 291 | var routeInfo = {}; 292 | 293 | var path = internals.normalizePath(route.path, thisRouteOpts); 294 | var pathInfo = internals.Router.analyze(path); 295 | var pathSegments = pathInfo.segments.length; 296 | 297 | Hoek.assert(1 <= pathSegments && pathSegments <= 4, 'Number of path segments should be between 1 and 4.'); 298 | 299 | switch (pathSegments) { 300 | case 4: 301 | routeInfo.associatedPkName = pathInfo.segments[3].name; 302 | case 3: 303 | routeInfo.associationAttr = pathInfo.segments[2].literal; 304 | case 2: 305 | routeInfo.pkName = pathInfo.segments[1].name; 306 | case 1: 307 | routeInfo.model = pathInfo.segments[0].literal; 308 | } 309 | 310 | _.defaults(thisRouteOpts, routeInfo); 311 | 312 | } 313 | 314 | // See https://github.com/balderdashy/sails/blob/master/lib/hooks/orm/build-orm.js#L65 [waterline v0.10.11] 315 | internals.getAssociationsFromModel = function(thisModel) { 316 | 317 | return _.reduce(thisModel.attributes, function (associatedWith, attrDef, attrName) { 318 | 319 | if (typeof attrDef === 'object' && (attrDef.model || attrDef.collection)) { 320 | var assoc = { 321 | alias: attrName, 322 | type: attrDef.model ? 'model' : 'collection' 323 | }; 324 | if (attrDef.model) { 325 | assoc.model = attrDef.model; 326 | } 327 | if (attrDef.collection) { 328 | assoc.collection = attrDef.collection; 329 | } 330 | if (attrDef.via) { 331 | assoc.via = attrDef.via; 332 | } 333 | 334 | associatedWith.push(assoc); 335 | } 336 | 337 | return associatedWith; 338 | 339 | }, []); 340 | 341 | } 342 | 343 | internals.normalizePath = function(path, thisRouteOpts) { 344 | 345 | Hoek.assert(_.isString(thisRouteOpts.userUrlPrefix) || !thisRouteOpts.userUrlPrefix, 'Option userUrlPrefix should only have a string or a falsy value.'); 346 | Hoek.assert(_.isString(thisRouteOpts.userModel) || !thisRouteOpts.userModel, 'Option userModel should only have a string or a falsy value.'); 347 | 348 | // Deal with prefix option 349 | path = internals.removePrefixFromPath(path, thisRouteOpts.prefix); 350 | 351 | // TODO: adjust bedwetters! 352 | if (internals.pathEndsWith(path, '/count')) { 353 | thisRouteOpts._private.count = true; 354 | path = internals.removeSuffixFromPath(path, '/count'); 355 | } 356 | 357 | // Deal with user creds options. 358 | if (thisRouteOpts.actAsUser && 359 | thisRouteOpts.userUrlPrefix && 360 | thisRouteOpts.userModel && 361 | internals.pathBeginsWith(path, thisRouteOpts.userUrlPrefix)) { 362 | 363 | thisRouteOpts._private.actAsUserModifiedPath = true; 364 | 365 | // Transform path to seem like it's of the form /users/{userId}... 366 | path = internals.removePrefixFromPath(path, thisRouteOpts.userUrlPrefix); 367 | path = '/' + thisRouteOpts.userModel + '/{userId}' + path; 368 | } 369 | 370 | return path; 371 | } 372 | 373 | internals.pathEndsWith = function(path, needle) { 374 | 375 | if (path.indexOf(needle) !== -1 && 376 | path.indexOf(needle) === path.length-needle.length) { 377 | return true; 378 | } else { 379 | return false; 380 | } 381 | 382 | } 383 | 384 | internals.removeSuffixFromPath = function(path, suffix) { 385 | 386 | if (internals.pathEndsWith(path, suffix)) { 387 | return path.slice(0, path.length-suffix.length); 388 | } else { 389 | return path; 390 | } 391 | 392 | } 393 | 394 | internals.pathBeginsWith = function(path, needle) { 395 | 396 | // Remove trailing slashes from needle 397 | needle = needle.replace(/\/+$/, ''); 398 | 399 | // path begins with needle 400 | var softBegins = (path.indexOf(needle) === 0); 401 | 402 | if (!softBegins) return false; 403 | 404 | // Assuming path begins with needle, 405 | // make sure it takes up enitre query parts. 406 | // We check this by seeing if removing needle would leave an empty string (they have equal lengths) 407 | // or if removing needle would leave a '/' as the first character in the newly constructed path. 408 | var hardBegins = (path.length == needle.length) || path[needle.length] === '/'; 409 | 410 | if (!hardBegins) return false; 411 | 412 | // Passed the tests 413 | return true; 414 | 415 | } 416 | 417 | internals.removePrefixFromPath = function(path, prefix) { 418 | 419 | Hoek.assert(_.isString(path), 'Path parameter should be a string') 420 | Hoek.assert(!prefix || _.isString(prefix), 'Prefix parameter should be a string or falsy.') 421 | 422 | if (!prefix) { 423 | return path; 424 | } 425 | 426 | // Remove trailing slashes from prefix 427 | prefix = prefix.replace(/\/+$/, ''); 428 | 429 | // If the path begins with the prefix, chop it off! 430 | if (path.indexOf(prefix) === 0) { 431 | path = path.slice(prefix.length); 432 | } 433 | 434 | return path; 435 | 436 | } 437 | 438 | internals.normalizeOptions = function(options) { 439 | 440 | var partial = { 441 | ownerAttrs: {}, 442 | childOwnerAttrs: {} 443 | }; 444 | 445 | // Currently just map *ownerAttr strings to *ownerAttrs objects, if not already set. 446 | if (options.ownerAttr && options.userIdProperty && !options.ownerAttrs[options.ownerAttr]) { 447 | partial.ownerAttrs[options.ownerAttr] = options.userIdProperty; 448 | } 449 | 450 | delete options.ownerAttr; 451 | 452 | if (options.childOwnerAttr && options.userIdProperty && !options.childOwnerAttrs[options.childOwnerAttr]) { 453 | partial.childOwnerAttrs[options.childOwnerAttr] = options.userIdProperty; 454 | } 455 | 456 | delete options.childOwnerAttr; 457 | 458 | _.merge(options, partial); 459 | 460 | return options; 461 | 462 | } 463 | 464 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bedwetter", 3 | "version": "1.8.2", 4 | "description": "Auto-generated, RESTful, CRUDdy route handlers for Waterline models in hapi", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test-cov" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/devinivy/bedwetter" 12 | }, 13 | "keywords": [ 14 | "hapi", 15 | "plugin", 16 | "model", 17 | "waterline", 18 | "dogwater", 19 | "rest", 20 | "crud" 21 | ], 22 | "author": "Devin Ivy ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/devinivy/bedwetter/issues" 26 | }, 27 | "dependencies": { 28 | "lodash": "~2.4.1", 29 | "async": "~0.2.9", 30 | "merge-defaults": "~0.1.0", 31 | "boom": "~2.5.1", 32 | "waterline-to-boom": "0.1.x", 33 | "hoek": "2.x.x", 34 | "call": "1.x.x", 35 | "pluralize": "~0.0.5" 36 | }, 37 | "peerDependencies": { 38 | "hapi": ">=6.x.x", 39 | "dogwater": ">=0.3 <0.5" 40 | }, 41 | "devDependencies": { 42 | "hapi": "~7.x.x", 43 | "dogwater": "^0.4.x", 44 | "sails-memory": "^0.10.x", 45 | "lab": "4.x.x", 46 | "async": "0.9.0" 47 | }, 48 | "homepage": "https://github.com/devinivy/bedwetter" 49 | } 50 | -------------------------------------------------------------------------------- /test/.boilerplate: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hapi = require('hapi') 4 | var Async = require('async') 5 | var ServerSetup = require('../server.setup.js'); /* Adjust this path as necessary */ 6 | 7 | // Test shortcuts 8 | var lab = exports.lab = Lab.script(); 9 | var expect = Lab.expect; 10 | var before = lab.before; 11 | var after = lab.after; 12 | var experiment = lab.experiment; 13 | var test = lab.test; 14 | 15 | /* ^^^ Get rid of aliases you don't use ^^^ */ 16 | 17 | experiment('Something', function () { 18 | 19 | // This will be a Hapi server for each test. 20 | var server = new Hapi.Server(); 21 | 22 | // Setup Hapi server to register the plugin 23 | before(function(done){ 24 | 25 | ServerSetup(server, {/* Plugin options */}, function(err) { 26 | 27 | if (err) done(err); 28 | 29 | server.route([ 30 | { /* Create some routes */ 31 | method: 'GET', 32 | path: '/zoo', 33 | handler: { 34 | bedwetter: {} 35 | } 36 | }]); 37 | 38 | done(); 39 | }); 40 | }); 41 | 42 | test('does something.', function (done) { 43 | 44 | server.inject({ /* Test a route! */ 45 | method: 'GET', 46 | url: '/zoo', 47 | }, function(res) { 48 | 49 | /* See if the result is what it should be */ 50 | 51 | done(); 52 | }); 53 | 54 | }); 55 | 56 | after(function(done) { 57 | 58 | var orm = server.plugins.dogwater.zoo.waterline; 59 | 60 | /* Take each connection used by the orm... */ 61 | Async.each(Object.keys(orm.connections), function(key, cbDone) { 62 | 63 | var adapter = orm.connections[key]._adapter; 64 | 65 | /* ... and use the relevant adapter to kill it. */ 66 | if (typeof adapter.teardown === "function") { 67 | 68 | adapter.teardown(function(err) { 69 | cbDone(err); 70 | }); 71 | 72 | } else { 73 | cbDone(); 74 | } 75 | 76 | }, 77 | function (err) { 78 | done(err); 79 | }); 80 | 81 | }); 82 | 83 | }); 84 | 85 | 86 | -------------------------------------------------------------------------------- /test/actions/add.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi'); 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | experiment('Add bedwetter', function () { 20 | 21 | // This will be a Hapi server for each test. 22 | var server = new Hapi.Server(); 23 | server.connection(); 24 | 25 | // Setup Hapi server to register the plugin 26 | before(function(done){ 27 | 28 | ServerSetup(server, { 29 | userModel: 'animals', 30 | userIdProperty: 'animal.id', 31 | userUrlPrefix: '/animal' 32 | }, function(err) { 33 | 34 | if (err) done(err); 35 | 36 | server.route([ 37 | { // adds a relation 38 | method: 'PUT', 39 | path: '/zoo/{id}/treats/{child_id}', 40 | handler: { 41 | bedwetter: {} 42 | } 43 | }, 44 | { // adds and creates 45 | method: 'POST', 46 | path: '/zoo/{id}/treats', 47 | handler: { 48 | bedwetter: { 49 | createdLocation: false 50 | } 51 | } 52 | }]); 53 | 54 | done(); 55 | }); 56 | }); 57 | 58 | test('adds and creates.', function (done) { 59 | 60 | server.inject({ 61 | method: 'POST', 62 | url: '/zoo/1/treats', 63 | payload: { 64 | name: "Fig Newtons" 65 | } 66 | }, function(res) { 67 | 68 | expect(res.statusCode).to.equal(201); 69 | expect(res.result).to.be.an.object; 70 | expect(res.result.name).to.equal("Fig Newtons"); 71 | expect(res.headers.location).to.not.exist; 72 | 73 | // Make sure the bedwetter sets request state 74 | var RequestState = res.request.plugins.bedwetter; 75 | expect(RequestState).to.be.an.object; 76 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord']); 77 | expect(RequestState.action).to.equal('add'); 78 | expect(RequestState.options).to.be.an.object; 79 | expect(RequestState.primaryRecord).to.be.an.object; 80 | //console.log(res.statusCode, res.result); 81 | 82 | done(); 83 | }) 84 | 85 | }); 86 | 87 | test('adds.', function (done) { 88 | 89 | server.inject({ 90 | method: 'PUT', 91 | url: '/zoo/2/treats/1', 92 | }, function(res) { 93 | 94 | expect(res.statusCode).to.equal(204); 95 | expect(res.result).to.be.null; 96 | 97 | // Make sure the bedwetter sets request state 98 | var RequestState = res.request.plugins.bedwetter; 99 | expect(RequestState).to.be.an.object; 100 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord', 'secondaryRecord']); 101 | expect(RequestState.action).to.equal('add'); 102 | expect(RequestState.options).to.be.an.object; 103 | expect(RequestState.primaryRecord).to.be.an.object; 104 | expect(RequestState.secondaryRecord).to.be.an.object; 105 | //console.log(res.statusCode, res.result); 106 | 107 | done(); 108 | }) 109 | 110 | }); 111 | 112 | after(function(done) { 113 | Memory.teardown(done); 114 | }); 115 | 116 | }); 117 | 118 | 119 | -------------------------------------------------------------------------------- /test/actions/create.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi') 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | experiment('Create bedwetter', function () { 20 | 21 | // This will be a Hapi server for each test. 22 | var server = new Hapi.Server(); 23 | server.connection(); 24 | 25 | 26 | // Setup Hapi server to register the plugin 27 | before(function(done){ 28 | 29 | ServerSetup(server, { 30 | userModel: 'animals', 31 | userIdProperty: 'animal.id', 32 | userUrlPrefix: '/animal' 33 | }, function(err) { 34 | 35 | if (err) done(err); 36 | 37 | server.route([ 38 | { // create 39 | method: 'POST', 40 | path: '/zoo', 41 | handler: { 42 | bedwetter: { 43 | createdLocation: false 44 | } 45 | } 46 | }]); 47 | 48 | done(); 49 | }); 50 | }); 51 | 52 | test('creates.', function (done) { 53 | 54 | server.inject({ 55 | method: 'POST', 56 | url: '/zoo', 57 | payload: { 58 | name: "Big Room Studios" 59 | } 60 | }, function(res) { 61 | 62 | expect(res.statusCode).to.equal(201); 63 | expect(res.result).to.be.an.object; 64 | expect(res.result.name).to.equal("Big Room Studios"); 65 | expect(res.result.location).to.not.exist; 66 | 67 | // Make sure the bedwetter sets request state 68 | var RequestState = res.request.plugins.bedwetter; 69 | expect(RequestState).to.be.an.object; 70 | expect(RequestState).to.have.keys(['action', 'options']); 71 | expect(RequestState.action).to.equal('create'); 72 | expect(RequestState.options).to.be.an.object; 73 | 74 | //console.log(res.statusCode, res.result); 75 | 76 | done(); 77 | }) 78 | 79 | }); 80 | 81 | after(function(done) { 82 | Memory.teardown(done); 83 | }); 84 | 85 | }); 86 | 87 | 88 | -------------------------------------------------------------------------------- /test/actions/destroy.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi') 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | 20 | experiment('Destroy bedwetter', function () { 21 | 22 | // This will be a Hapi server for each test. 23 | var server = new Hapi.Server(); 24 | server.connection(); 25 | 26 | // Setup Hapi server to register the plugin 27 | before(function(done){ 28 | 29 | ServerSetup(server, { 30 | userModel: 'animals', 31 | userIdProperty: 'animal.id', 32 | userUrlPrefix: '/animal' 33 | }, function(err) { 34 | 35 | if (err) done(err); 36 | 37 | server.route([ 38 | { // destroy 39 | method: 'DELETE', 40 | path: '/treat/{id}', 41 | handler: { 42 | bedwetter: {} 43 | } 44 | }]); 45 | 46 | done(); 47 | }); 48 | }); 49 | 50 | test('destroys.', function (done) { 51 | 52 | server.inject({ 53 | method: 'DELETE', 54 | url: '/treat/1' 55 | }, function(res) { 56 | 57 | expect(res.statusCode).to.equal(204); 58 | expect(res.result).to.be.null; 59 | 60 | // Make sure the bedwetter sets request state 61 | var RequestState = res.request.plugins.bedwetter; 62 | expect(RequestState).to.be.an.object; 63 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord']); 64 | expect(RequestState.action).to.equal('destroy'); 65 | expect(RequestState.options).to.be.an.object; 66 | expect(RequestState.primaryRecord).to.be.an.object; 67 | //console.log(res.statusCode, res.result); 68 | 69 | done(); 70 | }) 71 | 72 | }); 73 | 74 | after(function(done) { 75 | Memory.teardown(done); 76 | }); 77 | 78 | }); 79 | 80 | -------------------------------------------------------------------------------- /test/actions/find.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi') 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | experiment('Find bedwetter', function () { 20 | 21 | // This will be a Hapi server for each test. 22 | var server = new Hapi.Server(); 23 | server.connection(); 24 | 25 | // Setup Hapi server to register the plugin 26 | before(function(done){ 27 | 28 | ServerSetup(server, { 29 | userModel: 'animals', 30 | userIdProperty: 'animal.id', 31 | userUrlPrefix: '/animal' 32 | }, function(err) { 33 | 34 | if (err) done(err); 35 | 36 | server.route([ 37 | { // find 38 | method: 'GET', 39 | path: '/treat', 40 | handler: { 41 | bedwetter: {} 42 | } 43 | }]); 44 | 45 | done(); 46 | }); 47 | }); 48 | 49 | test('finds.', function (done) { 50 | 51 | server.inject({ 52 | method: 'GET', 53 | url: '/treat?sort=id ASC&skip=1&limit=3&where={"id":[1,2]}' 54 | }, function(res) { 55 | 56 | expect(res.statusCode).to.equal(200); 57 | expect(res.result).to.be.an.array; 58 | 59 | // Make sure the bedwetter sets request state 60 | var RequestState = res.request.plugins.bedwetter; 61 | expect(RequestState).to.be.an.object; 62 | expect(RequestState).to.have.keys(['action', 'options']); 63 | expect(RequestState.action).to.equal('find'); 64 | expect(RequestState.options).to.be.an.object; 65 | //console.log(res.statusCode, res.result); 66 | 67 | done(); 68 | }) 69 | 70 | }); 71 | 72 | after(function(done) { 73 | Memory.teardown(done); 74 | }); 75 | 76 | }); 77 | 78 | 79 | -------------------------------------------------------------------------------- /test/actions/findOne.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi') 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | experiment('FindOne bedwetter', function () { 20 | 21 | // This will be a Hapi server for each test. 22 | var server = new Hapi.Server();; 23 | server.connection(); 24 | 25 | // Setup Hapi server to register the plugin 26 | before(function(done){ 27 | 28 | ServerSetup(server, { 29 | userModel: 'animals', 30 | userIdProperty: 'animal.id', 31 | userUrlPrefix: '/animal' 32 | }, function(err) { 33 | 34 | if (err) done(err); 35 | 36 | server.route([ 37 | { // findOne acting as user 38 | method: 'GET', 39 | path: '/animal', 40 | config: {auth: 'default'}, 41 | handler: { 42 | bedwetter: { 43 | actAsUser: true 44 | } 45 | } 46 | }, 47 | { // findOne 48 | method: 'GET', 49 | path: '/zoo/{id}', 50 | handler: { 51 | bedwetter: {} 52 | } 53 | }]); 54 | 55 | done(); 56 | }); 57 | }); 58 | 59 | test('finds one.', function (done) { 60 | 61 | server.inject({ 62 | method: 'GET', 63 | url: '/zoo/1' 64 | }, function(res) { 65 | 66 | expect(res.statusCode).to.equal(200); 67 | expect(res.result).to.be.an.object; 68 | expect(res.result.treats).to.not.be.an.array; 69 | 70 | // Make sure the bedwetter sets request state 71 | var RequestState = res.request.plugins.bedwetter; 72 | expect(RequestState).to.be.an.object; 73 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord']); 74 | expect(RequestState.action).to.equal('findone'); 75 | expect(RequestState.options).to.be.an.object; 76 | expect(RequestState.primaryRecord).to.be.an.object; 77 | //console.log(res.statusCode, res.result); 78 | 79 | done(); 80 | }) 81 | 82 | }); 83 | 84 | test('finds one and populates.', function (done) { 85 | 86 | server.inject({ 87 | method: 'GET', 88 | url: '/zoo/1?populate=treats' 89 | }, function(res) { 90 | 91 | expect(res.statusCode).to.equal(200); 92 | expect(res.result).to.be.an.object; 93 | expect(res.result.treats).to.be.an.array; 94 | 95 | // Make sure the bedwetter sets request state 96 | var RequestState = res.request.plugins.bedwetter; 97 | expect(RequestState).to.be.an.object; 98 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord']); 99 | expect(RequestState.action).to.equal('findone'); 100 | expect(RequestState.options).to.be.an.object; 101 | expect(RequestState.primaryRecord).to.be.an.object; 102 | //console.log(res.statusCode, res.result); 103 | 104 | done(); 105 | }) 106 | 107 | }); 108 | 109 | after(function(done) { 110 | Memory.teardown(done); 111 | }); 112 | 113 | }); 114 | 115 | 116 | -------------------------------------------------------------------------------- /test/actions/populate.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi') 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | 20 | experiment('Populate bedwetter', function () { 21 | 22 | // This will be a Hapi server for each test. 23 | var server = new Hapi.Server(); 24 | server.connection(); 25 | 26 | // Setup Hapi server to register the plugin 27 | before(function(done){ 28 | 29 | ServerSetup(server, { 30 | userModel: 'animals', 31 | userIdProperty: 'animal.id', 32 | userUrlPrefix: '/animal' 33 | }, function(err) { 34 | 35 | if (err) done(err); 36 | 37 | server.route([ 38 | { // populates 39 | method: 'GET', 40 | path: '/zoo/{id}/treats/{childId?}', 41 | handler: { 42 | bedwetter: {} 43 | } 44 | }]); 45 | 46 | done(); 47 | }); 48 | }); 49 | 50 | test('populates.', function (done) { 51 | 52 | server.inject({ 53 | method: 'GET', 54 | url: '/zoo/1/treats', 55 | }, function(res) { 56 | 57 | //console.log(res.statusCode, res.result); 58 | expect(res.statusCode).to.equal(200); 59 | expect(res.result).to.be.an.array; 60 | expect(res.result).to.have.length(2); 61 | 62 | // Make sure the bedwetter sets request state 63 | var RequestState = res.request.plugins.bedwetter; 64 | expect(RequestState).to.be.an.object; 65 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord']); 66 | expect(RequestState.action).to.equal('populate'); 67 | expect(RequestState.options).to.be.an.object; 68 | expect(RequestState.primaryRecord).to.be.an.object; 69 | 70 | done(); 71 | }) 72 | 73 | }); 74 | 75 | test('acknowledges an association.', function (done) { 76 | 77 | server.inject({ 78 | method: 'GET', 79 | url: '/zoo/1/treats/2', 80 | }, function(res) { 81 | 82 | expect(res.statusCode).to.equal(204); 83 | expect(res.result).to.be.null; 84 | 85 | // Make sure the bedwetter sets request state 86 | var RequestState = res.request.plugins.bedwetter; 87 | expect(RequestState).to.be.an.object; 88 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord', 'secondaryRecord']); 89 | expect(RequestState.action).to.equal('populate'); 90 | expect(RequestState.options).to.be.an.object; 91 | expect(RequestState.primaryRecord).to.be.an.object; 92 | expect(RequestState.secondaryRecord).to.be.an.object; 93 | //console.log(res.statusCode, res.result); 94 | 95 | done(); 96 | }) 97 | 98 | }); 99 | 100 | test('acknowledges a non-association.', function (done) { 101 | 102 | server.inject({ 103 | method: 'GET', 104 | url: '/zoo/1/treats/666', 105 | }, function(res) { 106 | 107 | expect(res.statusCode).to.equal(404); 108 | 109 | // Make sure the bedwetter sets request state 110 | var RequestState = res.request.plugins.bedwetter; 111 | expect(RequestState).to.be.an.object; 112 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord']); 113 | expect(RequestState.action).to.equal('populate'); 114 | expect(RequestState.options).to.be.an.object; 115 | expect(RequestState.primaryRecord).to.be.an.object; 116 | //console.log(res.statusCode, res.result); 117 | 118 | done(); 119 | }) 120 | 121 | }); 122 | 123 | after(function(done) { 124 | Memory.teardown(done); 125 | }); 126 | 127 | }); 128 | 129 | -------------------------------------------------------------------------------- /test/actions/remove.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi') 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | 20 | experiment('Remove bedwetter', function () { 21 | 22 | // This will be a Hapi server for each test. 23 | var server = new Hapi.Server(); 24 | server.connection(); 25 | 26 | // Setup Hapi server to register the plugin 27 | before(function(done){ 28 | 29 | ServerSetup(server, { 30 | userModel: 'animals', 31 | userIdProperty: 'animal.id', 32 | userUrlPrefix: '/animal' 33 | }, function(err) { 34 | 35 | if (err) done(err); 36 | 37 | server.route([ 38 | { // remove 39 | method: 'DELETE', 40 | path: '/zoo/{id}/treats/{child_id}', 41 | handler: { 42 | bedwetter: {} 43 | } 44 | }]); 45 | 46 | done(); 47 | }); 48 | }); 49 | 50 | test('removes.', function (done) { 51 | 52 | server.inject({ 53 | method: 'DELETE', 54 | url: '/zoo/2/treats/2', 55 | }, function(res) { 56 | 57 | expect(res.statusCode).to.equal(204); 58 | expect(res.result).to.be.null; 59 | 60 | // Make sure the bedwetter sets request state 61 | var RequestState = res.request.plugins.bedwetter; 62 | expect(RequestState).to.be.an.object; 63 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord', 'secondaryRecord']); 64 | expect(RequestState.action).to.equal('remove'); 65 | expect(RequestState.options).to.be.an.object; 66 | expect(RequestState.primaryRecord).to.be.an.object; 67 | expect(RequestState.secondaryRecord).to.be.an.object; 68 | //console.log(res.statusCode, res.result); 69 | 70 | done(); 71 | }) 72 | 73 | }); 74 | 75 | after(function(done) { 76 | Memory.teardown(done); 77 | }); 78 | 79 | }); 80 | 81 | -------------------------------------------------------------------------------- /test/actions/update.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi') 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | 20 | experiment('Update bedwetter', function () { 21 | 22 | // This will be a Hapi server for each test. 23 | var server = new Hapi.Server(); 24 | server.connection(); 25 | 26 | // Setup Hapi server to register the plugin 27 | before(function(done){ 28 | 29 | ServerSetup(server, { 30 | userModel: 'animals', 31 | userIdProperty: 'animal.id', 32 | userUrlPrefix: '/animal' 33 | }, function(err) { 34 | 35 | if (err) done(err); 36 | 37 | server.route([ 38 | { // update 39 | method: ['PATCH', 'POST'], 40 | path: '/treat/{id}', 41 | handler: { 42 | bedwetter: {} 43 | } 44 | }]); 45 | 46 | done(); 47 | }); 48 | }); 49 | 50 | test('updates with post.', function (done) { 51 | 52 | server.inject({ 53 | method: 'PATCH', 54 | url: '/treat/2', 55 | payload: { 56 | name: "Fried BOreos" 57 | } 58 | }, function(res) { 59 | 60 | expect(res.statusCode).to.equal(200); 61 | expect(res.result).to.be.an.object; 62 | expect(res.result.name).to.equal("Fried BOreos"); 63 | 64 | // Make sure the bedwetter sets request state 65 | var RequestState = res.request.plugins.bedwetter; 66 | expect(RequestState).to.be.an.object; 67 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord']); 68 | expect(RequestState.action).to.equal('update'); 69 | expect(RequestState.options).to.be.an.object; 70 | expect(RequestState.primaryRecord).to.be.an.object; 71 | //console.log(res.statusCode, res.result); 72 | 73 | done(); 74 | }) 75 | 76 | }); 77 | 78 | test('updates with patch.', function (done) { 79 | 80 | server.inject({ 81 | method: 'PATCH', 82 | url: '/treat/2', 83 | payload: { 84 | name: "Fried Oreos" 85 | } 86 | }, function(res) { 87 | 88 | expect(res.statusCode).to.equal(200); 89 | expect(res.result).to.be.an.object; 90 | expect(res.result.name).to.equal("Fried Oreos"); 91 | 92 | // Make sure the bedwetter sets request state 93 | var RequestState = res.request.plugins.bedwetter; 94 | expect(RequestState).to.be.an.object; 95 | expect(RequestState).to.have.keys(['action', 'options', 'primaryRecord']); 96 | expect(RequestState.action).to.equal('update'); 97 | expect(RequestState.options).to.be.an.object; 98 | expect(RequestState.primaryRecord).to.be.an.object; 99 | //console.log(res.statusCode, res.result); 100 | 101 | done(); 102 | }) 103 | 104 | }); 105 | 106 | after(function(done) { 107 | Memory.teardown(done); 108 | }); 109 | 110 | }); 111 | 112 | -------------------------------------------------------------------------------- /test/auth.scheme.js: -------------------------------------------------------------------------------- 1 | var Hoek = require('hoek'); 2 | var Boom = require('boom'); 3 | 4 | var internals = {}; 5 | 6 | module.exports = function (server, options) { 7 | 8 | var settings = Hoek.clone(options); 9 | 10 | var scheme = { 11 | authenticate: function (request, reply) { 12 | 13 | var req = request.raw.req; 14 | var authorization = req.headers.authorization; 15 | if (!authorization) { 16 | return reply(Boom.unauthorized(null, 'Custom')); 17 | } 18 | 19 | var parts = authorization.split(/\s+/); 20 | if (parts.length !== 2) { 21 | return reply(true); 22 | } 23 | 24 | var username = parts[1]; 25 | var credentials = {}; 26 | 27 | credentials.animal = settings.animals[username]; 28 | 29 | if (!credentials) { 30 | return reply(Boom.unauthorized('Missing credentials', 'Custom')); 31 | } 32 | 33 | if (typeof credentials === 'string') { 34 | return reply(credentials); 35 | } 36 | 37 | return reply.continue({ credentials: credentials }); 38 | } 39 | }; 40 | 41 | return scheme; 42 | }; -------------------------------------------------------------------------------- /test/models.definition.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | identity: 'treat', 4 | connection: 'testing', 5 | schema: true, 6 | 7 | attributes: { 8 | name: 'string', 9 | deleted: { 10 | type: 'integer', 11 | defaultsTo: 0 12 | }, 13 | calories: 'integer', 14 | place: { 15 | collection: 'zoo', 16 | via: 'treats' 17 | }, 18 | animalOwner: { 19 | model: 'animals' 20 | } 21 | } 22 | }, 23 | { 24 | identity: 'zoo', 25 | connection: 'testing', 26 | schema: true, 27 | 28 | attributes: { 29 | name: 'string', 30 | treats: { 31 | collection: 'treat', 32 | via: 'place', 33 | dominant: true 34 | } 35 | } 36 | }, 37 | { 38 | identity: 'animals', // (users) 39 | connection: 'testing', 40 | schema: true, 41 | 42 | attributes: { 43 | species: 'string', 44 | treats: { 45 | collection: 'treat', 46 | via: 'animalOwner' 47 | } 48 | } 49 | } 50 | ]; -------------------------------------------------------------------------------- /test/models.fixtures.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "treat", 4 | "items": [ 5 | { 6 | "id": 1, 7 | "name": "French Fries", 8 | "animalOwner": 2 9 | }, 10 | { 11 | "id": 2, 12 | "name": "Burgers", 13 | "animalOwner": 1 14 | }, 15 | { 16 | "id": 3, 17 | "name": "Popcorn Chicken", 18 | "animalOwner": 1 19 | } 20 | ] 21 | }, 22 | { 23 | "model": "zoo", 24 | "items": [ 25 | { 26 | "id": 1, 27 | "name": "Portland, ME Zoo" 28 | }, 29 | { 30 | "id": 2, 31 | "name": "Portland, OR Zoo" 32 | } 33 | ] 34 | }, 35 | { 36 | "model": "animals", 37 | "items": [ 38 | { 39 | "id": 1, 40 | "species": "Doggie" 41 | }, 42 | { 43 | "id": 2, 44 | "species": "Kitty" 45 | } 46 | ] 47 | }, 48 | { 49 | "model": "treat_place__zoo_treats", 50 | "items": [ 51 | { 52 | "treat_place": 1, 53 | "zoo_treats": 1 54 | }, 55 | { 56 | "treat_place": 2, 57 | "zoo_treats": 1 58 | }, 59 | { 60 | "treat_place": 2, 61 | "zoo_treats": 2 62 | }, 63 | { 64 | "treat_place": 3, 65 | "zoo_treats": 2 66 | } 67 | ] 68 | } 69 | ] -------------------------------------------------------------------------------- /test/options/_private.count.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hapi = require('hapi') 4 | var Memory = require('sails-memory'); 5 | var Async = require('async') 6 | var ServerSetup = require('../server.setup.js'); 7 | 8 | // Test shortcuts 9 | var lab = exports.lab = Lab.script(); 10 | var expect = Lab.expect; 11 | var before = lab.before; 12 | var after = lab.after; 13 | var experiment = lab.experiment; 14 | var test = lab.test; 15 | 16 | 17 | experiment('Count postfix', function () { 18 | 19 | // This will be a Hapi server for each test. 20 | var server = new Hapi.Server(); 21 | server.connection(); 22 | 23 | // Setup Hapi server to register the plugin 24 | before(function(done){ 25 | 26 | ServerSetup(server, {/* Plugin options */}, function(err) { 27 | 28 | if (err) done(err); 29 | 30 | server.route([ 31 | { 32 | method: 'GET', 33 | path: '/zoo/count', 34 | handler: { 35 | bedwetter: {} 36 | } 37 | }, 38 | { 39 | method: 'GET', 40 | path: '/zoo/{id}/treats/count', 41 | handler: { 42 | bedwetter: {} 43 | } 44 | }]); 45 | 46 | done(); 47 | }); 48 | }); 49 | 50 | test('(populate) counts populated items.', function (done) { 51 | 52 | server.inject({ 53 | method: 'GET', 54 | url: '/zoo/count', 55 | }, function(res) { 56 | 57 | //console.log(res.statusCode, res.result); 58 | expect(res.statusCode).to.equal(200); 59 | expect(res.result).to.equal(2); 60 | 61 | done(); 62 | }); 63 | 64 | }); 65 | 66 | test('(find) counts find items.', function (done) { 67 | 68 | server.inject({ 69 | method: 'GET', 70 | url: '/zoo/2/treats/count', 71 | }, function(res) { 72 | 73 | //console.log(res.statusCode, res.result); 74 | expect(res.statusCode).to.equal(200); 75 | expect(res.result).to.equal(2); 76 | 77 | done(); 78 | }); 79 | 80 | }); 81 | 82 | after(function(done) { 83 | Memory.teardown(done); 84 | }); 85 | 86 | }); 87 | 88 | 89 | -------------------------------------------------------------------------------- /test/options/actAsUser.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hapi = require('hapi') 4 | var Memory = require('sails-memory') 5 | var Async = require('async') 6 | var ServerSetup = require('../server.setup.js'); 7 | 8 | // Test shortcuts 9 | var lab = exports.lab = Lab.script(); 10 | var expect = Lab.expect; 11 | var before = lab.before; 12 | var after = lab.after; 13 | var experiment = lab.experiment; 14 | var test = lab.test; 15 | 16 | experiment('actAsUser option', function () { 17 | 18 | // This will be a Hapi server for each test. 19 | var server = new Hapi.Server(); 20 | server.connection(); 21 | 22 | // Setup Hapi server to register the plugin 23 | before(function(done){ 24 | 25 | ServerSetup(server, { 26 | actAsUser: true, 27 | userModel: 'animals', 28 | userIdProperty: 'animal.id', 29 | userUrlPrefix: '/animal' 30 | }, function(err) { 31 | 32 | if (err) done(err); 33 | 34 | server.route([ 35 | { // Get a treat if it's owned by the user 36 | method: 'GET', 37 | path: '/treat/{id}', 38 | config: { 39 | auth: { 40 | strategy: 'default', 41 | mode: 'try' 42 | } 43 | }, 44 | handler: { 45 | bedwetter: { 46 | requireOwner: true, 47 | ownerAttr: 'animalOwner' 48 | } 49 | } 50 | }, 51 | { // Get treats owned by user 52 | method: 'GET', 53 | path: '/treat', 54 | config: { 55 | auth: { 56 | strategy: 'default' 57 | } 58 | }, 59 | handler: { 60 | bedwetter: { 61 | requireOwner: true, 62 | ownerAttr: 'animalOwner' 63 | } 64 | } 65 | }, 66 | { // Check record association 67 | method: 'GET', 68 | path: '/zoo/{id}/treats/{childId}', 69 | config: { 70 | auth: { 71 | strategy: 'default' 72 | } 73 | }, 74 | handler: { 75 | bedwetter: { 76 | requireOwner: true, 77 | childOwnerAttr: 'animalOwner', 78 | ownerAttr: false 79 | } 80 | } 81 | }, 82 | { // Add record association 83 | method: 'PUT', 84 | path: '/zoo/{id}/treats/{childId}', 85 | config: { 86 | auth: { 87 | strategy: 'default' 88 | } 89 | }, 90 | handler: { 91 | bedwetter: { 92 | requireOwner: true, 93 | childOwnerAttr: 'animalOwner', 94 | ownerAttr: false 95 | } 96 | } 97 | }, 98 | { // Remove record association 99 | method: 'DELETE', 100 | path: '/zoo/{id}/treats/{childId}', 101 | config: { 102 | auth: { 103 | strategy: 'default' 104 | } 105 | }, 106 | handler: { 107 | bedwetter: { 108 | requireOwner: true, 109 | childOwnerAttr: 'animalOwner', 110 | ownerAttr: false 111 | } 112 | } 113 | }, 114 | { // Update a record 115 | method: 'PATCH', 116 | path: '/treat/{id}', 117 | config: { 118 | auth: { 119 | strategy: 'default' 120 | } 121 | }, 122 | handler: { 123 | bedwetter: { 124 | requireOwner: true, 125 | ownerAttr: 'animalOwner' 126 | } 127 | } 128 | }, 129 | { // Remove a record 130 | method: 'DELETE', 131 | path: '/treat/{id}', 132 | config: { 133 | auth: { 134 | strategy: 'default' 135 | } 136 | }, 137 | handler: { 138 | bedwetter: { 139 | requireOwner: true, 140 | ownerAttr: 'animalOwner' 141 | } 142 | } 143 | }, 144 | { // Create a record 145 | method: 'POST', 146 | path: '/treat', 147 | config: { 148 | auth: { 149 | strategy: 'default' 150 | } 151 | }, 152 | handler: { 153 | bedwetter: { 154 | setOwner: true, 155 | ownerAttr: 'animalOwner' 156 | } 157 | } 158 | }, 159 | { // Create/add a record 160 | method: 'POST', 161 | path: '/zoo/{id}/treats', 162 | config: { 163 | auth: { 164 | strategy: 'default' 165 | } 166 | }, 167 | handler: { 168 | bedwetter: { 169 | setOwner: true, 170 | childOwnerAttr: 'animalOwner' 171 | } 172 | } 173 | }, 174 | { // Get this user's treats 175 | method: 'GET', 176 | path: '/animal/treats', 177 | config: { 178 | auth: { 179 | strategy: 'default', 180 | mode: 'try' 181 | } 182 | }, 183 | handler: { 184 | bedwetter: {} 185 | } 186 | }, 187 | { // Get a particular animal (user) 188 | method: 'GET', 189 | path: '/animals/{id}', 190 | config: { 191 | auth: { 192 | strategy: 'default' 193 | } 194 | }, 195 | handler: { 196 | bedwetter: {} 197 | } 198 | }]); 199 | 200 | done(); 201 | }); 202 | }); 203 | 204 | test('(populate) allows a prefix signifying the the primary model is user and and primary model id is that of the logged-in user.', function (done) { 205 | 206 | server.inject({ /* Test a route! */ 207 | method: 'GET', 208 | url: '/animal/treats', 209 | headers: { authorization: 'Custom Doggie' } 210 | }, function(res) { 211 | 212 | expect(res.statusCode).to.equal(200); 213 | expect(res.result).to.be.an.array; 214 | expect(res.result).to.have.length(2); 215 | //console.log(res.statusCode, res.result); 216 | 217 | done(); 218 | }); 219 | 220 | }); 221 | 222 | // TODO: reconcile when creds are set but user is unauthorized, i.e. with try mode. 223 | test('(populate) returns Unauthorized if there are no credentials when using prefix.', function (done) { 224 | 225 | server.inject({ /* Test a route! */ 226 | method: 'GET', 227 | url: '/animal/treats', 228 | }, function(res) { 229 | 230 | expect(res.statusCode).to.equal(401); 231 | expect(res.result).to.be.an.object; 232 | //console.log(res.statusCode, res.result); 233 | 234 | done(); 235 | }); 236 | 237 | }); 238 | 239 | test('(findOne) with requireOwner and an ownerAttr, allows viewing a record that is owned by the user.', function (done) { 240 | 241 | server.inject({ 242 | method: 'GET', 243 | url: '/treat/2', 244 | headers: { authorization: 'Custom Doggie' } 245 | }, function(res) { 246 | 247 | expect(res.statusCode).to.equal(200); 248 | expect(res.result).to.be.an.object; 249 | expect(res.result.id).to.equal(2); 250 | //console.log(res.statusCode, res.result); 251 | 252 | done(); 253 | }); 254 | 255 | }); 256 | 257 | test('(findOne) with requireOwner and an ownerAttr, rejects viewing a record that isn\'t owned by the user.', function (done) { 258 | 259 | server.inject({ 260 | method: 'GET', 261 | url: '/treat/1', 262 | headers: { authorization: 'Custom Doggie' } 263 | }, function(res) { 264 | 265 | expect(res.statusCode).to.equal(401); 266 | //console.log(res.statusCode, res.result); 267 | 268 | done(); 269 | }); 270 | 271 | }); 272 | 273 | test('(find) with requireOwner and an ownerAttr, only returns a list of records owned by the user.', function (done) { 274 | 275 | server.inject({ 276 | method: 'GET', 277 | url: '/treat', 278 | headers: { authorization: 'Custom Doggie' } 279 | }, function(res) { 280 | 281 | expect(res.statusCode).to.equal(200); 282 | expect(res.result).to.be.an.array; 283 | expect(res.result).to.have.length(2); 284 | //console.log(res.statusCode, res.result); 285 | 286 | done(); 287 | }); 288 | 289 | }); 290 | 291 | test('(populate) with requireOwner and childOwnerAttr, will not display info about record association if bad credentials.', function (done) { 292 | 293 | server.inject({ 294 | method: 'GET', 295 | url: '/zoo/1/treats/1', 296 | headers: { authorization: 'Custom Doggie' } 297 | }, function(res) { 298 | 299 | expect(res.statusCode).to.equal(401); 300 | expect(res.result).to.be.an.object; 301 | //console.log(res.statusCode, res.result); 302 | 303 | done(); 304 | }); 305 | 306 | }); 307 | 308 | test('(populate) with requireOwner, childOwnerAttr, and false ownerAttr, will display info about record association if good credentials for only associated record.', function (done) { 309 | 310 | server.inject({ 311 | method: 'GET', 312 | url: '/zoo/1/treats/2', 313 | headers: { authorization: 'Custom Doggie' } 314 | }, function(res) { 315 | 316 | expect(res.statusCode).to.equal(204); 317 | expect(res.result).to.be.null; 318 | //console.log(res.statusCode, res.result); 319 | 320 | done(); 321 | }); 322 | 323 | }); 324 | 325 | test('(update) with requireOwner and ownerAttr, succeeds if good credentials.', function (done) { 326 | 327 | server.inject({ 328 | method: 'PATCH', 329 | url: '/treat/2', 330 | payload: { 331 | name: 'Hot Dogs' 332 | }, 333 | headers: { authorization: 'Custom Doggie' } 334 | }, function(res) { 335 | 336 | expect(res.statusCode).to.equal(200); 337 | expect(res.result).to.be.an.object; 338 | expect(res.result.name).to.equal('Hot Dogs'); 339 | //console.log(res.statusCode, res.result); 340 | 341 | done(); 342 | }); 343 | 344 | }); 345 | 346 | test('(update) with requireOwner and ownerAttr, fails if bad credentials.', function (done) { 347 | 348 | server.inject({ 349 | method: 'PATCH', 350 | url: '/treat/2', 351 | payload: { 352 | name: 'Hot Dogs' 353 | }, 354 | headers: { authorization: 'Custom Kitty' } 355 | }, function(res) { 356 | 357 | expect(res.statusCode).to.equal(401); 358 | //console.log(res.statusCode, res.result); 359 | 360 | done(); 361 | }); 362 | 363 | }); 364 | 365 | test('(add) with requireOwner, childOwnerAttr, and false ownerAttr, succeeds if good credentials.', function (done) { 366 | 367 | server.inject({ 368 | method: 'PUT', 369 | url: '/zoo/1/treats/1', 370 | payload: { 371 | name: 'Hot Dogs' 372 | }, 373 | headers: { authorization: 'Custom Kitty' } 374 | }, function(res) { 375 | 376 | expect(res.statusCode).to.equal(204); 377 | expect(res.result).to.be.null; 378 | //console.log(res.statusCode, res.result); 379 | 380 | done(); 381 | }); 382 | 383 | }); 384 | 385 | test('(add) with requireOwner, childOwnerAttr, and false ownerAttr, fails if bad credentials.', function (done) { 386 | 387 | server.inject({ 388 | method: 'PUT', 389 | url: '/zoo/1/treats/1', 390 | payload: { 391 | name: 'Hot Dogs' 392 | }, 393 | headers: { authorization: 'Custom Doggy' } 394 | }, function(res) { 395 | 396 | expect(res.statusCode).to.equal(401); 397 | //console.log(res.statusCode, res.result); 398 | 399 | done(); 400 | }); 401 | 402 | }); 403 | 404 | // TODO: decide whether 401s should be 404s. the order of these next two tests affects the outcome of this test. 405 | // The outcome of the second test verifies this one is correct. 406 | test('(remove) with requireOwner, childOwnerAttr, and false ownerAttr, fails if bad credentials.', function (done) { 407 | 408 | server.inject({ 409 | method: 'DELETE', 410 | url: '/zoo/1/treats/1', 411 | payload: { 412 | name: 'Hot Dogs' 413 | }, 414 | headers: { authorization: 'Custom Doggy' } 415 | }, function(res) { 416 | 417 | expect(res.statusCode).to.equal(401); 418 | //console.log(res.statusCode, res.result); 419 | 420 | done(); 421 | }); 422 | 423 | }); 424 | 425 | test('(remove) with requireOwner, childOwnerAttr, and false ownerAttr, succeeds if good credentials.', function (done) { 426 | 427 | server.inject({ 428 | method: 'DELETE', 429 | url: '/zoo/1/treats/1', 430 | payload: { 431 | name: 'Hot Dogs' 432 | }, 433 | headers: { authorization: 'Custom Kitty' } 434 | }, function(res) { 435 | 436 | expect(res.statusCode).to.equal(204); 437 | expect(res.result).to.be.null; 438 | //console.log(res.statusCode, res.result); 439 | 440 | done(); 441 | }); 442 | 443 | }); 444 | 445 | test('(destroy) with requireOwner and ownerAttr, fails if bad credentials.', function (done) { 446 | 447 | server.inject({ 448 | method: 'DELETE', 449 | url: '/treat/2', 450 | headers: { authorization: 'Custom Kitty' } 451 | }, function(res) { 452 | 453 | expect(res.statusCode).to.equal(401); 454 | //console.log(res.statusCode, res.result); 455 | 456 | done(); 457 | }); 458 | 459 | }); 460 | 461 | test('(destroy) with requireOwner and ownerAttr, succeeds if good credentials.', function (done) { 462 | 463 | server.inject({ 464 | method: 'DELETE', 465 | url: '/treat/2', 466 | headers: { authorization: 'Custom Doggie' } 467 | }, function(res) { 468 | 469 | expect(res.statusCode).to.equal(204); 470 | expect(res.result).to.be.null; 471 | //console.log(res.statusCode, res.result); 472 | 473 | done(); 474 | }); 475 | 476 | }); 477 | 478 | test('(create) with setOwner and ownerAttr sets owner on record.', function (done) { 479 | 480 | server.inject({ 481 | method: 'POST', 482 | url: '/treat', 483 | payload: { 484 | name: "Wings" 485 | }, 486 | headers: { authorization: 'Custom Doggie' } 487 | }, function(res) { 488 | 489 | expect(res.statusCode).to.equal(201); 490 | expect(res.result).to.be.an.object; 491 | expect(res.result.animalOwner).to.equal(1); 492 | //console.log(res.statusCode, res.result); 493 | 494 | done(); 495 | }); 496 | 497 | }); 498 | 499 | // TODO: do this with requireOwner too 500 | test('(add) with setOwner and childOwnerAttr sets owner on a to-be-associated record.', function (done) { 501 | 502 | server.inject({ 503 | method: 'POST', 504 | url: '/zoo/1/treats', 505 | payload: { 506 | name: "Fried Pickles" 507 | }, 508 | headers: { authorization: 'Custom Doggie' } 509 | }, function(res) { 510 | 511 | expect(res.statusCode).to.equal(201); 512 | expect(res.result).to.be.an.object; 513 | expect(res.result.animalOwner).to.equal(1); 514 | expect(res.result.name).to.equal("Fried Pickles"); 515 | //console.log(res.statusCode, res.result); 516 | 517 | done(); 518 | }); 519 | 520 | }); 521 | 522 | test('(findOne) does not automatically set primary record on a normal users path.', function (done) { 523 | 524 | server.inject({ 525 | method: 'GET', 526 | url: '/animals/1', 527 | headers: { authorization: 'Custom Kitty' } // id: 2 528 | }, function(res) { 529 | 530 | expect(res.statusCode).to.equal(200); 531 | expect(res.result).to.be.an.object; 532 | expect(res.result.id).to.equal(1); 533 | expect(res.result.species).to.equal("Doggie"); 534 | //console.log(res.statusCode, res.result); 535 | 536 | done(); 537 | }); 538 | 539 | }); 540 | 541 | after(function(done) { 542 | Memory.teardown(done); 543 | }); 544 | 545 | }); 546 | 547 | 548 | -------------------------------------------------------------------------------- /test/options/createdLocation.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi') 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | experiment('createdLocation option', function () { 20 | 21 | // This will be a Hapi server for each test. 22 | var server = new Hapi.Server(); 23 | server.connection(); 24 | 25 | // Setup Hapi server to register the plugin 26 | before(function(done){ 27 | 28 | ServerSetup(server, { 29 | userModel: 'animals', 30 | userIdProperty: 'animal.id', 31 | userUrlPrefix: '/animal' 32 | }, function(err) { 33 | 34 | if (err) done(err); 35 | 36 | server.route([ 37 | { // create 38 | method: 'POST', 39 | path: '/zoo', 40 | handler: { 41 | bedwetter: { 42 | createdLocation: '/zoo/%s' 43 | } 44 | } 45 | }, 46 | { // adds and creates 47 | method: 'POST', 48 | path: '/zoo/{id}/treats', 49 | handler: { 50 | bedwetter: { 51 | createdLocation: '/treats/%s' 52 | } 53 | } 54 | }]); 55 | 56 | done(); 57 | }); 58 | }); 59 | 60 | test('sets the location header on create.', function (done) { 61 | 62 | server.inject({ 63 | method: 'POST', 64 | url: '/zoo', 65 | payload: { 66 | name: "Big Room Studios" 67 | } 68 | }, function(res) { 69 | 70 | expect(res.statusCode).to.equal(201); 71 | expect(res.result).to.be.an.object; 72 | expect(res.result.name).to.equal("Big Room Studios"); 73 | expect(res.headers.location).to.exist; 74 | expect(res.headers.location).to.match(new RegExp('/zoo/'+res.result.id+'$')); 75 | //console.log(res.statusCode, res.result); 76 | 77 | done(); 78 | }) 79 | 80 | }); 81 | 82 | test('sets the location header on add+create.', function (done) { 83 | 84 | server.inject({ 85 | method: 'POST', 86 | url: '/zoo/1/treats', 87 | payload: { 88 | name: "Fig Newtons" 89 | } 90 | }, function(res) { 91 | 92 | expect(res.statusCode).to.equal(201); 93 | expect(res.result).to.be.an.object; 94 | expect(res.result.name).to.equal("Fig Newtons"); 95 | expect(res.headers.location).to.exist; 96 | expect(res.headers.location).to.match(new RegExp('/treats/'+res.result.id+'$')); 97 | //console.log(res.statusCode, res.result); 98 | 99 | done(); 100 | }) 101 | 102 | }); 103 | 104 | after(function(done) { 105 | Memory.teardown(done); 106 | }); 107 | 108 | }); 109 | 110 | 111 | -------------------------------------------------------------------------------- /test/options/deletedFlag.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi') 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | 20 | experiment('deletedFlag option', function () { 21 | 22 | // This will be a Hapi server for each test. 23 | var server = new Hapi.Server(); 24 | server.connection(); 25 | 26 | // Setup Hapi server to register the plugin 27 | before(function(done){ 28 | 29 | ServerSetup(server, { 30 | userModel: 'animals', 31 | userIdProperty: 'animal.id', 32 | userUrlPrefix: '/animal' 33 | }, function(err) { 34 | 35 | if (err) done(err); 36 | 37 | server.route([ 38 | { // destroy 39 | method: 'DELETE', 40 | path: '/treat/{id}', 41 | handler: { 42 | bedwetter: { 43 | deletedFlag: true 44 | } 45 | } 46 | }, 47 | { // findone 48 | method: 'GET', 49 | path: '/treat/{id}', 50 | handler: { 51 | bedwetter: {} 52 | } 53 | }]); 54 | 55 | done(); 56 | }); 57 | }); 58 | 59 | test('destroys with deleted flag.', function (done) { 60 | 61 | // Call the soft delete route 62 | server.inject({ 63 | method: 'DELETE', 64 | url: '/treat/1' 65 | }, function(res) { 66 | 67 | expect(res.statusCode).to.equal(204); 68 | expect(res.result).to.be.null; 69 | 70 | // Ensure that the flag was set and the object was NOT removed 71 | server.inject({ 72 | method: 'GET', 73 | url: '/treat/1' 74 | }, function(res) { 75 | 76 | expect(res.statusCode).to.equal(200); 77 | expect(res.result.deleted).to.equal(1); 78 | 79 | done(); 80 | }); 81 | 82 | }); 83 | 84 | }); 85 | 86 | after(function(done) { 87 | Memory.teardown(done); 88 | }); 89 | 90 | }); 91 | 92 | -------------------------------------------------------------------------------- /test/options/omit.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hapi = require('hapi') 4 | var Memory = require('sails-memory') 5 | var Async = require('async') 6 | var ServerSetup = require('../server.setup.js'); 7 | 8 | // Test shortcuts 9 | var lab = exports.lab = Lab.script(); 10 | var expect = Lab.expect; 11 | var before = lab.before; 12 | var after = lab.after; 13 | var experiment = lab.experiment; 14 | var test = lab.test; 15 | 16 | experiment('omit option', function () { 17 | 18 | // This will be a Hapi server for each test. 19 | var server = new Hapi.Server(); 20 | server.connection(); 21 | 22 | // Setup Hapi server to register the plugin 23 | before(function(done){ 24 | 25 | ServerSetup(server, { 26 | omit: ['name'] 27 | }, function(err) { 28 | 29 | if (err) done(err); 30 | 31 | server.route([ 32 | { // Get a treat if it's owned by the user 33 | method: 'GET', 34 | path: '/treat/{id}', 35 | handler: { 36 | bedwetter: {} 37 | } 38 | }, 39 | { // Get treats owned by user 40 | method: 'GET', 41 | path: '/treat', 42 | handler: { 43 | bedwetter: {} 44 | } 45 | }, 46 | { // Get associated records 47 | method: 'GET', 48 | path: '/zoo/{id}/treats', 49 | handler: { 50 | bedwetter: {} 51 | } 52 | }, 53 | { // Update a record 54 | method: 'PATCH', 55 | path: '/treat/{id}', 56 | handler: { 57 | bedwetter: {} 58 | } 59 | }, 60 | { // Create a record 61 | method: 'POST', 62 | path: '/treat', 63 | handler: { 64 | bedwetter: {} 65 | } 66 | }, 67 | { // Create/add a record 68 | method: 'POST', 69 | path: '/zoo/{id}/treats', 70 | handler: { 71 | bedwetter: {} 72 | } 73 | }, 74 | { // Get a zoo 75 | method: 'GET', 76 | path: '/zoo/{id}', 77 | handler: { 78 | bedwetter: { 79 | omit: 'id' 80 | } 81 | } 82 | }, 83 | { // Get zoos 84 | method: 'GET', 85 | path: '/zoo', 86 | handler: { 87 | bedwetter: { 88 | omit: ['treats.dd'] 89 | } 90 | } 91 | }]); 92 | 93 | done(); 94 | }); 95 | }); 96 | 97 | test('omits on findOne.', function (done) { 98 | 99 | server.inject({ 100 | method: 'GET', 101 | url: '/treat/1' 102 | }, function(res) { 103 | 104 | expect(res.statusCode).to.equal(200); 105 | expect(res.result).to.be.an.object; 106 | expect(res.result).to.contain.keys(['id', 'animalOwner']); 107 | expect(res.result).to.not.contain.keys(['name']); 108 | //console.log(res.statusCode, res.result); 109 | 110 | done(); 111 | }); 112 | 113 | }); 114 | 115 | test('omits on find.', function (done) { 116 | 117 | server.inject({ 118 | method: 'GET', 119 | url: '/treat' 120 | }, function(res) { 121 | 122 | expect(res.statusCode).to.equal(200); 123 | expect(res.result).to.be.an.array; 124 | expect(res.result).to.have.length(3); 125 | 126 | res.result.forEach(function(item) { 127 | 128 | expect(item).to.contain.keys(['id', 'animalOwner']); 129 | expect(item).to.not.contain.keys(['name']); 130 | }); 131 | //console.log(res.statusCode, res.result); 132 | 133 | done(); 134 | }); 135 | 136 | }); 137 | 138 | test('omits on populated array.', function (done) { 139 | 140 | server.inject({ 141 | method: 'GET', 142 | url: '/zoo/1/treats' 143 | }, function(res) { 144 | 145 | expect(res.statusCode).to.equal(200); 146 | expect(res.result).to.be.an.array; 147 | expect(res.result).to.have.length(2); 148 | 149 | res.result.forEach(function(item) { 150 | 151 | expect(item).to.contain.keys(['id', 'animalOwner']); 152 | expect(item).to.not.contain.keys(['name']); 153 | }); 154 | //console.log(res.statusCode, res.result); 155 | 156 | done(); 157 | }); 158 | 159 | }); 160 | 161 | test('omits on update.', function (done) { 162 | 163 | server.inject({ 164 | method: 'PATCH', 165 | url: '/treat/2', 166 | payload: { 167 | animalOwner: 2 168 | } 169 | }, function(res) { 170 | 171 | expect(res.statusCode).to.equal(200); 172 | expect(res.result).to.be.an.object; 173 | expect(res.result).to.contain.keys(['id', 'animalOwner']); 174 | expect(res.result.animalOwner).to.equal(2); 175 | expect(res.result).to.not.contain.keys(['name']); 176 | //console.log(res.statusCode, res.result); 177 | 178 | done(); 179 | }); 180 | 181 | }); 182 | 183 | test('omits on create.', function (done) { 184 | 185 | server.inject({ 186 | method: 'POST', 187 | url: '/treat', 188 | payload: { 189 | name: "Wings", 190 | animalOwner: 1 191 | } 192 | }, function(res) { 193 | 194 | expect(res.statusCode).to.equal(201); 195 | expect(res.result).to.be.an.object; 196 | expect(res.result).to.contain.keys(['id', 'animalOwner']); 197 | expect(res.result.animalOwner).to.equal(1); 198 | expect(res.result).to.not.contain.keys(['name']); 199 | //console.log(res.statusCode, res.result); 200 | 201 | done(); 202 | }); 203 | 204 | }); 205 | 206 | test('omits on adding a created record.', function (done) { 207 | 208 | server.inject({ 209 | method: 'POST', 210 | url: '/zoo/1/treats', 211 | payload: { 212 | name: "Brisket", 213 | animalOwner: 2 214 | } 215 | }, function(res) { 216 | 217 | expect(res.statusCode).to.equal(201); 218 | expect(res.result).to.be.an.object; 219 | expect(res.result).to.contain.keys(['id', 'animalOwner']); 220 | expect(res.result.animalOwner).to.equal(2); 221 | expect(res.result).to.not.contain.keys(['name']); 222 | //console.log(res.statusCode, res.result); 223 | 224 | done(); 225 | }); 226 | 227 | }); 228 | 229 | test('omits using route options, allows string as option.', function (done) { 230 | 231 | server.inject({ 232 | method: 'GET', 233 | url: '/zoo/1' 234 | }, function(res) { 235 | 236 | expect(res.statusCode).to.equal(200); 237 | expect(res.result).to.be.an.object; 238 | expect(res.result).to.contain.keys(['name']); 239 | expect(res.result.name).to.equal("Portland, ME Zoo"); 240 | expect(res.result).to.not.contain.keys(['id']); 241 | //console.log(res.statusCode, res.result); 242 | 243 | done(); 244 | }); 245 | 246 | }); 247 | 248 | test('omits deeply when populating a field.', function (done) { 249 | 250 | server.inject({ 251 | method: 'GET', 252 | url: '/zoo?populate=treats' 253 | }, function(res) { 254 | 255 | expect(res.statusCode).to.equal(200); 256 | expect(res.result).to.be.an.array; 257 | expect(res.result).to.have.length(2); 258 | 259 | res.result.forEach(function(item) { 260 | 261 | expect(item).to.contain.keys(['id', 'treats']); 262 | expect(item.treats).to.not.contain.keys(['name']); 263 | }); 264 | 265 | done(); 266 | }); 267 | 268 | }); 269 | 270 | test('does not fail when omitting deeply, but not populating a field.', function (done) { 271 | 272 | server.inject({ 273 | method: 'GET', 274 | url: '/zoo' 275 | }, function(res) { 276 | 277 | expect(res.statusCode).to.equal(200); 278 | expect(res.result).to.be.an.array; 279 | expect(res.result).to.have.length(2); 280 | 281 | res.result.forEach(function(item) { 282 | 283 | expect(item.treats).to.not.contain.keys(['treats']); 284 | }); 285 | 286 | done(); 287 | }); 288 | 289 | }); 290 | 291 | test('omits using query and options.', function (done) { 292 | 293 | server.inject({ 294 | method: 'GET', 295 | url: '/treat/1?omit[]=id' 296 | }, function(res) { 297 | 298 | expect(res.result).to.be.an.object; 299 | expect(res.result).to.contain.keys(['animalOwner']); 300 | expect(res.result.animalOwner).to.equal(2); 301 | expect(res.result).to.not.contain.keys(['id', 'name']); 302 | 303 | done(); 304 | }); 305 | 306 | }); 307 | 308 | after(function(done) { 309 | 310 | Memory.teardown(done); 311 | }); 312 | 313 | }); 314 | 315 | 316 | -------------------------------------------------------------------------------- /test/options/pkAttr.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hapi = require('hapi') 4 | var Memory = require('sails-memory') 5 | var Async = require('async') 6 | var ServerSetup = require('../server.setup.js'); 7 | 8 | // Test shortcuts 9 | var lab = exports.lab = Lab.script(); 10 | var expect = Lab.expect; 11 | var before = lab.before; 12 | var after = lab.after; 13 | var experiment = lab.experiment; 14 | var test = lab.test; 15 | 16 | experiment('pkField option', function () { 17 | 18 | // This will be a Hapi server for each test. 19 | var server = new Hapi.Server(); 20 | server.connection(); 21 | 22 | // Setup Hapi server to register the plugin 23 | before(function(done){ 24 | 25 | ServerSetup(server, { 26 | pkAttr: 'name', 27 | childPkAttr: 'name' 28 | }, function(err) { 29 | 30 | if (err) done(err); 31 | 32 | server.route([ 33 | { // Get a treat 34 | method: 'GET', 35 | path: '/treat/{id}', 36 | handler: { 37 | bedwetter: {} 38 | } 39 | }, 40 | { 41 | method: 'GET', 42 | path: '/treat/{id}/place/{childId}', 43 | handler: { 44 | bedwetter: {} 45 | } 46 | }, 47 | { 48 | method: 'PATCH', 49 | path: '/treat/{id}', 50 | handler: { 51 | bedwetter: {} 52 | } 53 | }, 54 | { 55 | method: 'DELETE', 56 | path: '/treat/{id}/place/{childId}', 57 | handler: { 58 | bedwetter: {} 59 | } 60 | }, 61 | { 62 | method: 'PUT', 63 | path: '/treat/{id}/place/{childId}', 64 | handler: { 65 | bedwetter: {} 66 | } 67 | }, 68 | { 69 | method: 'DELETE', 70 | path: '/treat/{id}', 71 | handler: { 72 | bedwetter: {} 73 | } 74 | }]); 75 | 76 | done(); 77 | }); 78 | }); 79 | 80 | test('(findOne).', function (done) { 81 | 82 | server.inject({ 83 | method: 'GET', 84 | url: '/treat/French%20Fries', 85 | }, function(res) { 86 | 87 | expect(res.statusCode).to.equal(200); 88 | expect(res.result).to.be.an.object; 89 | expect(res.result.name).to.equal("French Fries"); 90 | expect(res.result.id).to.equal(1); 91 | //console.log(res.statusCode, res.result); 92 | 93 | done(); 94 | }); 95 | 96 | }); 97 | 98 | test('(populate).', function (done) { 99 | 100 | server.inject({ 101 | method: 'GET', 102 | url: '/treat/French%20Fries/place/Portland,%20ME%20Zoo', 103 | }, function(res) { 104 | 105 | expect(res.statusCode).to.equal(204); 106 | expect(res.result).to.be.null; 107 | //console.log(res.statusCode, res.result); 108 | 109 | done(); 110 | }); 111 | 112 | }); 113 | 114 | test('(update).', function (done) { 115 | 116 | server.inject({ 117 | method: 'PATCH', 118 | url: '/treat/French%20Fries', 119 | payload: { 120 | name: "Onion Rings" 121 | } 122 | }, function(res) { 123 | 124 | expect(res.statusCode).to.equal(200); 125 | expect(res.result).to.be.an.object; 126 | expect(res.result.name).to.equal("Onion Rings"); 127 | expect(res.result.id).to.equal(1); 128 | //console.log(res.statusCode, res.result); 129 | 130 | done(); 131 | }); 132 | 133 | }); 134 | 135 | 136 | test('(remove).', function (done) { 137 | 138 | server.inject({ 139 | method: 'DELETE', 140 | url: '/treat/Onion%20Rings/place/Portland,%20ME%20Zoo', 141 | }, function(res) { 142 | 143 | expect(res.statusCode).to.equal(204); 144 | expect(res.result).to.be.null; 145 | //console.log(res.statusCode, res.result); 146 | 147 | done(); 148 | }); 149 | 150 | }); 151 | 152 | test('(add).', function (done) { 153 | 154 | server.inject({ 155 | method: 'PUT', 156 | url: '/treat/Onion%20Rings/place/Portland,%20ME%20Zoo', 157 | }, function(res) { 158 | 159 | expect(res.statusCode).to.equal(204); 160 | expect(res.result).to.be.null; 161 | //console.log(res.statusCode, res.result); 162 | 163 | done(); 164 | }); 165 | 166 | }); 167 | 168 | test('(destroy).', function (done) { 169 | 170 | server.inject({ 171 | method: 'DELETE', 172 | url: '/treat/Onion%20Rings', 173 | }, function(res) { 174 | 175 | expect(res.statusCode).to.equal(204); 176 | expect(res.result).to.be.null; 177 | //console.log(res.statusCode, res.result); 178 | 179 | server.inject({ 180 | method: 'GET', 181 | url: '/treat/Onion%20Rings', 182 | }, function(res) { 183 | 184 | expect(res.statusCode).to.equal(404); 185 | 186 | done(); 187 | }); 188 | 189 | }); 190 | 191 | }); 192 | 193 | after(function(done) { 194 | Memory.teardown(done); 195 | }); 196 | 197 | }); 198 | 199 | 200 | -------------------------------------------------------------------------------- /test/options/prefix.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | var Hapi = require('hapi') 6 | var Memory = require('sails-memory'); 7 | var Async = require('async') 8 | var ServerSetup = require('../server.setup.js'); 9 | 10 | // Test shortcuts 11 | var lab = exports.lab = Lab.script(); 12 | var expect = Lab.expect; 13 | var before = lab.before; 14 | var after = lab.after; 15 | var experiment = lab.experiment; 16 | var test = lab.test; 17 | 18 | 19 | experiment('Prefix option', function () { 20 | 21 | // This will be a Hapi server for each test. 22 | var server = new Hapi.Server(); 23 | server.connection(); 24 | 25 | // Setup Hapi server to register the plugin 26 | before(function(done){ 27 | 28 | ServerSetup(server, { 29 | userModel: 'animals', 30 | userIdProperty: 'animal.id', 31 | userUrlPrefix: '/animal' 32 | }, function(err) { 33 | 34 | if (err) done(err); 35 | 36 | server.route([ 37 | { // find with prefix 38 | method: 'GET', 39 | path: '/v1/treat', 40 | handler: { 41 | bedwetter: { 42 | prefix: '/v1' 43 | } 44 | } 45 | }]); 46 | 47 | done(); 48 | }); 49 | }); 50 | 51 | test('finds with prefix.', function (done) { 52 | 53 | server.inject({ 54 | method: 'GET', 55 | url: '/v1/treat?limit=1' 56 | }, function(res) { 57 | 58 | expect(res.statusCode).to.equal(200); 59 | expect(res.result).to.be.an.array; 60 | // console.log(res.statusCode, res.result); 61 | 62 | done(); 63 | }) 64 | 65 | }); 66 | 67 | after(function(done) { 68 | Memory.teardown(done); 69 | }); 70 | 71 | }); 72 | 73 | 74 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | // Load modules 2 | var Lab = require('lab'); 3 | var Hapi = require('hapi') 4 | var Async = require('async') 5 | var ServerSetup = require('./server.setup.js'); /* Adjust this path as necessary */ 6 | 7 | // Test shortcuts 8 | var lab = exports.lab = Lab.script(); 9 | var expect = Lab.expect; 10 | var before = lab.before; 11 | var after = lab.after; 12 | var experiment = lab.experiment; 13 | var test = lab.test; 14 | 15 | /* ^^^ Get rid of aliases you don't use ^^^ */ 16 | 17 | experiment('Server', function () { 18 | 19 | // This will be a Hapi server for each test. 20 | var server = new Hapi.Server(); 21 | server.connection(); 22 | 23 | // Setup Hapi server to register the plugin 24 | before(function(done){ 25 | 26 | ServerSetup(server, {/* Plugin options */}, function(err) { 27 | 28 | if (err) done(err); 29 | 30 | done(); 31 | }); 32 | }); 33 | 34 | test('errors when bedwetter has a /count postfix inappropriately.', function (done) { 35 | 36 | expect(function(){ 37 | server.route({ 38 | method: 'GET', 39 | path: '/zoo/{id1}/treats/{id2}/count', 40 | handler: { 41 | bedwetter: {} 42 | } 43 | }); 44 | }).to.throw(/count/); 45 | done(); 46 | }); 47 | 48 | test('errors when bedwetter doesn\'t match GET verb/path pattern requirement.', function (done) { 49 | 50 | expect(function(){ 51 | server.route({ 52 | method: 'GET', 53 | path: '/zoo/{id1}/{id2}', 54 | handler: { 55 | bedwetter: {} 56 | } 57 | }); 58 | }).to.throw(/get/i); 59 | 60 | done(); 61 | }); 62 | 63 | test('errors when bedwetter doesn\'t match POST verb/path pattern requirement.', function (done) { 64 | 65 | expect(function(){ 66 | server.route({ 67 | method: 'POST', 68 | path: '/zoo/{id1}/{id2}', 69 | handler: { 70 | bedwetter: {} 71 | } 72 | }); 73 | }).to.throw(/post/i); 74 | 75 | done(); 76 | }); 77 | 78 | test('errors when bedwetter doesn\'t match PUT verb/path pattern requirement.', function (done) { 79 | 80 | expect(function(){ 81 | server.route({ 82 | method: 'PUT', 83 | path: '/zoo/{id1}/{id2}', 84 | handler: { 85 | bedwetter: {} 86 | } 87 | }); 88 | }).to.throw(/put/i); 89 | 90 | done(); 91 | }); 92 | 93 | test('errors when bedwetter doesn\'t match PATCH verb/path pattern requirement.', function (done) { 94 | 95 | expect(function(){ 96 | server.route({ 97 | method: 'PATCH', 98 | path: '/zoo/{id1}/{id2}', 99 | handler: { 100 | bedwetter: {} 101 | } 102 | }); 103 | }).to.throw(/patch/i); 104 | 105 | done(); 106 | }); 107 | 108 | test('errors when bedwetter doesn\'t match DELETE verb/path pattern requirement.', function (done) { 109 | 110 | expect(function(){ 111 | server.route({ 112 | method: 'DELETE', 113 | path: '/zoo/{id1}/{id2}', 114 | handler: { 115 | bedwetter: {} 116 | } 117 | }); 118 | }).to.throw(/delete/i); 119 | 120 | done(); 121 | }); 122 | 123 | after(function(done) { 124 | 125 | var orm = server.plugins.dogwater.zoo.waterline; 126 | 127 | /* Take each connection used by the orm... */ 128 | Async.each(Object.keys(orm.connections), function(key, cbDone) { 129 | 130 | var adapter = orm.connections[key]._adapter; 131 | 132 | /* ... and use the relevant adapter to kill it. */ 133 | if (typeof adapter.teardown === "function") { 134 | 135 | adapter.teardown(function(err) { 136 | cbDone(err); 137 | }); 138 | 139 | } else { 140 | cbDone(); 141 | } 142 | 143 | }, 144 | function (err) { 145 | done(err); 146 | }); 147 | 148 | }); 149 | 150 | }); 151 | 152 | 153 | -------------------------------------------------------------------------------- /test/server.setup.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi'); 2 | var Memory = require('sails-memory'); 3 | var Hoek = require('hoek'); 4 | var Path = require('path'); 5 | 6 | module.exports = function(server, pluginOpts, cb) { 7 | 8 | // Connection is for late hapi v7 9 | Hoek.assert(server instanceof Hapi.Server || server instanceof require('hapi/lib/connection'), 'You\'re setting up something that is not a hapi server.'); 10 | 11 | if (typeof pluginOpts == 'function') { 12 | cb = pluginOpts; 13 | pluginOpts = {}; 14 | } 15 | 16 | // Setup dummy connections/adapters. 17 | var connections = { 18 | 'testing': { 19 | adapter: 'memory' 20 | } 21 | }; 22 | 23 | // Setup adapters for testing fixtures. 24 | var adapters = { memory: Memory }; 25 | var modelsFile = './models.definition.js'; 26 | var fixturesFile = './models.fixtures.json'; 27 | 28 | // Setup 29 | server.auth.scheme('custom', require('./auth.scheme.js')); 30 | server.auth.strategy('default', 'custom', false, { animals: { Doggie: {id:1}, Kitty: {id:2} } }); 31 | 32 | var plugins = [ 33 | { 34 | register: require('..'), 35 | options: pluginOpts 36 | }, 37 | { 38 | register: require('dogwater'), 39 | options: { 40 | connections: connections, 41 | adapters: adapters, 42 | models: Path.normalize(__dirname + '/' + modelsFile), 43 | data: { 44 | fixtures: require('./models.fixtures.json') 45 | } 46 | } 47 | }]; 48 | 49 | server.register(plugins, function (err) { 50 | 51 | cb(err); 52 | }); 53 | 54 | } --------------------------------------------------------------------------------