@tryghost/express-bookshelf-jsonapi 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 Hannah Wolfe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # express-bookshelf-jsonapi
2
+ _ebja for short - yep this module needs a better name, that's why it's still under my username_
3
+
4
+ **WARNING: This is still very much a work in progress**
5
+
6
+ ebja can be used to create API endpoints which:
7
+
8
+ 1. respond on express routes (act as middleware)
9
+ 2. serialise a bookshelf model as a response (even if it also does other work)
10
+ 3. use the JSONAPI format for request / responses
11
+
12
+ ebja assumes that endpoints need to be extremely customisable. It has lots of sensible defaults but tries to make everything overridable, whilst still making it very easy to return full JSONAPI-compliant responses.
13
+
14
+ ## Example Usage
15
+
16
+ ```js
17
+ var app = express();
18
+
19
+ var jsonapi = ebja({
20
+ baseUrl: 'http://localhost:80/api',
21
+ models: require('./my-bookshelf-models')
22
+ });
23
+ var blogs = jsonapi.Resource({
24
+ model: 'Blog'
25
+ });
26
+
27
+ blogs.read = blogs.Endpoint({});
28
+
29
+ app.get('/blogs/:id', blogs.read)
30
+ ```
31
+
32
+ ## ebja()
33
+
34
+ The intial usage of ebja is to setup the initial config for your API.
35
+
36
+ ```js
37
+ var jsonapi = ebja({
38
+ baseUrl: 'http://localhost:80/api',
39
+ models: require('./my-bookshelf-models')
40
+ });
41
+ ```
42
+
43
+ `ebja()` takes an options object with two parameters:
44
+ * `baseUrl` - the url to prefix each endpoint with, for outputting in JSONAPI links
45
+ * `models` - an object with model names as keys, and bookshelf models as values
46
+
47
+ Once setup, the resulting object has two methods:
48
+ * `Resource({})` - define a resource for which endpoints will be created, e.g. a Blogs resource for creating various CRUD endpoints (returns a resource)
49
+ * `Endpoint({})` - define an endpoint without first creating a resource (returns a middleware function)
50
+
51
+ `Resource` & `Endpoint` both take all the same options.
52
+ A `Resource` has an `Endpoint` method which has all options passed to the resource as overridable defaults.
53
+
54
+ Resources are a convenience for packaging up options for a group of endpoints, E.g. setting the model, permissions and configuring the response across a full suite of CRUD endpoints.
55
+
56
+ ## Principles
57
+
58
+ #### Bookshelf plugin
59
+
60
+ ebja assumes you'll wire up its bookshelf plugin. This provides access to two special model methods:
61
+
62
+ * `getOne` - return a single resource
63
+ * `getPage` - return a collection
64
+
65
+ Both of these methods are used by ebja by default, to ensure that the JSONAPI params `include`, `page`, `sort`, `field` and `filter` are passed through to the model correctly and applied.\*
66
+
67
+ ```js
68
+ var knex = require('knex')(dbConfig)
69
+ var Bookshelf = require('bookshelf')(knex);
70
+
71
+ Bookshelf.plugin(require('ebja').plugin);
72
+ ```
73
+
74
+ \* Note: the params `field` and `filter` are currently disabled by this module. The params are only utilised in GET / query type endpoints.
75
+
76
+
77
+ #### Endpoint "types"
78
+
79
+ ebja works on the idea that there are 3 main types of endpoint:
80
+ - query (method: GET) - default
81
+ - action (methods: POST, PUT or PATCH)
82
+ - destroy (method: DELETE)
83
+
84
+ **Query** endpoints perform a fetch for a single object or collection with support for the JSONAPI params `include`, `page`, `sort`, `field` and `filter`.
85
+
86
+ **Action** endpoints assume there is some operation to perform, e.g. to add or update a model. These endpoints usually perform a `getOne` query after the operation in order to return a serialised model in JSONAPI format.
87
+
88
+ **Destroy** endpoints assume they are deleting a resource. They return a 204 and empty response by default on success.
89
+
90
+ By setting the `method` option for an endpoint, the type of endpoint can be changed.
91
+
92
+ #### Function "stacks" (apiware)
93
+
94
+ For each type of endpoint (query, action, destroy) ebja has a stack of functions, much like middleware, that it will execute (see the Stacks listing below for details). Some stack functions have default behaviour, some are noops intended to be overridden with custom behaviour, e.g. `permissions`. These functions are referred to as apiware.
95
+
96
+ Calling Endpoint() results in a function which can be called as express middleware. When called as middleware, the http `req` and `res` parameters are converted into transport-agnostic `APIRequest` and `APIResponse` objects. The apiware functions operate the same as express middleware, with the `APIRequest` and `APIResponse` object being past to each in turn, and each function calling `next()` to continue.
97
+
98
+ For example, for a destroy endpoint there are just 2 apiware functions in the stack: `permissions()` (a noop) and `destroy()`. All 3 types of endpoint have a `permissions()` function just before the main operation. This function can be overridden by passing a function as a `permission` option to the Endpoint or Resource. All stack functions can be overridden in the same way.
99
+
100
+ Example:
101
+
102
+ ```js
103
+ var usersRead = jsonapi.Endpoint({
104
+ method: 'GET', // this is the default
105
+ queryMethod: 'getOne', // this is the default
106
+ model: 'User',
107
+ permissions: function (apiReq, apiRes, next) {
108
+ // If the id parameter in the url (query.data.id) matches the authenticated user (source.id)
109
+ if (apiReq.query.data.id === apiReq.source.id) {
110
+ // We're safe to continue because we are allowed access to this resource
111
+ return next();
112
+ }
113
+
114
+ // Else, we have no permission to continue
115
+ return next(new Error('NoPermissionError');
116
+ }
117
+ });
118
+
119
+ ```
120
+
121
+ ## Options
122
+
123
+ * `method` (default: `GET`) the HTTP method of the endpoint
124
+ * `queryMethod` (default: `getOne`) the method to call on the model in the query apiware (query & action endpoints)
125
+ * `actionMethod` (default: `noop`) the method to call on the model in the action apiware (action endpoints only)
126
+ * `destroyMethod` (default: `destroy`) the method to call on the model in the destroy apiware (destroy endpoints only)
127
+ * `relations` array of relations that the endpoint can `include`
128
+ * `defaultSort` the default sort order
129
+ * `<apiwarename>` all apiware functions can be overridden by providing a function with the same name to the options
130
+ * `response` an object containing options for configuring the response
131
+ * `type` a string type to return in the JSONAPI response
132
+ * `attributes` whitelist
133
+ * any other option supported by [jsonapi-serializer](https://github.com/SeyZ/jsonapi-serializer#serialization) or [jsonapi-mapper](https://github.com/scoutforpets/jsonapi-mapper#api)
134
+
135
+
136
+ ## Stacks
137
+
138
+ #### Query (default)
139
+
140
+ * validate (noop)
141
+ * paramsData
142
+ * paramsInclude
143
+ * paramsPage
144
+ * paramsFilter
145
+ * paramsFields
146
+ * paramsSort
147
+ * permissions (noop)
148
+ * query (main operation)
149
+ * process (noop)
150
+ * format
151
+
152
+ #### Action
153
+
154
+ * validate (noop)
155
+ * payload
156
+ * permissions (noop)
157
+ * action (main operation)
158
+ * query (secondary operation)
159
+ * process (noop)
160
+ * format
161
+
162
+ #### Destroy
163
+ * permissions (noop)
164
+ * destroy (main operation)
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./lib/ebja');
package/lib/api.js ADDED
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ var debug = require('ghost-ignition').debug('api');
4
+ var _ = require('lodash');
5
+ var Endpoint = require('./endpoint');
6
+ var Resource = require('./resource');
7
+
8
+ // Declare our API top-level object
9
+ var api = exports = module.exports = {};
10
+
11
+ api.init = function init(options) {
12
+ debug('initing api');
13
+
14
+ if (!options) {
15
+ throw new Error('EBJA requires configuration');
16
+ }
17
+
18
+ if (!options.baseUrl) {
19
+ throw new Error('EBJA requires a baseUrl');
20
+ }
21
+
22
+ if (!options.models) {
23
+ throw new Error('EBJA requires bookshelf models');
24
+ }
25
+
26
+ this.baseUrl = options.baseUrl;
27
+ this.models = options.models;
28
+
29
+ delete options.baseUrl;
30
+ delete options.models;
31
+
32
+ this.options = _.defaults({}, options, {request: {}, response: {}});
33
+ };
34
+
35
+ api.Resource = function registerResource(options) {
36
+ return new Resource(this, options);
37
+ };
38
+
39
+ api.Endpoint = function registerEndpoint(options) {
40
+ return new Endpoint(this, options);
41
+ };
@@ -0,0 +1,47 @@
1
+ var debug = require('ghost-ignition').debug('apiware:action');
2
+ var errors = require('ghost-ignition').errors;
3
+ var _ = require('lodash');
4
+
5
+ /**
6
+ * Action
7
+ *
8
+ * Default behaviour: takes a method & sends the payload to it
9
+ */
10
+ module.exports = function action(apiReq, apiRes, next) {
11
+ // If there's no actionMethod defined, then this is a noop
12
+ if (!apiReq.options.actionMethod) {
13
+ debug('doing noop');
14
+ return next();
15
+ }
16
+
17
+ var methodToCall = apiReq.options.actionMethod;
18
+ debug('doing model actionMethod', methodToCall);
19
+
20
+ var actionPayload = _.clone(apiReq.query.data);
21
+ // We've used data, so we clear it from apiReq.query
22
+ apiReq.query.data = {};
23
+
24
+ if (apiReq.params.identifier) {
25
+ actionPayload.id = apiReq.params.identifier;
26
+ }
27
+
28
+ apiRes.exec.push({
29
+ method: methodToCall,
30
+ payload: actionPayload
31
+ });
32
+
33
+ return apiReq.model[methodToCall](actionPayload)
34
+ .then(function actionSuccess(result) {
35
+ debug('Success!', result.id);
36
+ apiReq.query.data.id = result.id;
37
+
38
+ return next();
39
+ }, function actionFailure(err) {
40
+ if (err instanceof apiReq.model.NotFoundError || err instanceof apiReq.model.NoRowsUpdatedError) {
41
+ return next(new errors.NotFoundError({message: 'Resource Not Found'}));
42
+ }
43
+
44
+ debug('model action failed');
45
+ return next(err);
46
+ });
47
+ };
@@ -0,0 +1,35 @@
1
+ var debug = require('ghost-ignition').debug('apiware:destroy');
2
+ var errors = require('ghost-ignition').errors;
3
+
4
+ module.exports = function destroy(apiReq, apiRes, next) {
5
+ if (!apiReq.params.identifier) {
6
+ return next(new errors.BadRequestError({message: 'Attempting to destroy a resource without an identifier'}));
7
+ }
8
+
9
+ // Default to looking for a method called destroy
10
+ var methodToCall = apiReq.options.destroyMethod || 'destroy';
11
+
12
+ var destroyPayload = {
13
+ id: apiReq.params.identifier,
14
+ require: true
15
+ // @TODO make it possible to pass through through a transaction
16
+ };
17
+
18
+ apiRes.exec.push({
19
+ method: methodToCall,
20
+ payload: destroyPayload
21
+ });
22
+
23
+ return apiReq.model[methodToCall](destroyPayload)
24
+ .then(function destroySuccess() {
25
+ debug('Success!');
26
+ return next();
27
+ }, function destroyFailure(err) {
28
+ if (err instanceof apiReq.model.NoRowsDeletedError) {
29
+ return next(new errors.NotFoundError({message: 'Resource Not Found'}));
30
+ }
31
+
32
+ debug('model destroy failed');
33
+ return next(err);
34
+ });
35
+ };
@@ -0,0 +1,54 @@
1
+ var debug = require('ghost-ignition').debug('apiware:format');
2
+
3
+ function updateRelations(model, relation) {
4
+ if (model.relations && !model.relations[relation]) {
5
+ // Simulating an empty collection in json-mapper
6
+ model.relations[relation] = {toJSON: function toJSON() {
7
+ return relation.match(/s$/) ? [] : {};
8
+ }};
9
+ }
10
+ }
11
+
12
+ function ensureRelationLinksArePresent(relations, apiRes) {
13
+ if (relations) {
14
+ // Update mapper options so that these are output
15
+ apiRes.mapperOptions.relations = {
16
+ fields: relations, included: true
17
+ };
18
+
19
+ // Update model, this is a hack to work around https://github.com/scoutforpets/jsonapi-mapper/issues/69
20
+ // That is, there's no other way to tell the mapper to include relations
21
+ relations.forEach(function addRelationToModel(relation) {
22
+ if (apiRes.model.length) {
23
+ // This is a collection
24
+ apiRes.model.forEach(function (model) {
25
+ updateRelations(model, relation)
26
+ });
27
+ } else {
28
+ updateRelations(apiRes.model, relation);
29
+ }
30
+ });
31
+
32
+ // If serializerOptions.attributes is set, we need to merge the relations into the list to ensure
33
+ // That these are also output, else we have to duplicate the definition of them
34
+ if (apiRes.serializerOptions.attributes) {
35
+ apiRes.serializerOptions.attributes = apiRes.serializerOptions.attributes.concat(relations);
36
+ }
37
+ }
38
+ }
39
+
40
+ // @TODO think about how to restructure this
41
+ // So that there is default behaviour, which is overridable / extensible
42
+ // Rather than having to wholesale replace this function
43
+ module.exports = function format(apiReq, apiRes, next) {
44
+ debug('compiling mapper options');
45
+ var relations = apiReq.options.relations;
46
+ ensureRelationLinksArePresent(relations, apiRes);
47
+
48
+ // Ensure we get pagination from the model
49
+ if (apiRes.model && apiRes.model.pagination) {
50
+ apiRes.mapperOptions.pagination = apiRes.model.pagination;
51
+ }
52
+
53
+ next();
54
+ };
@@ -0,0 +1,76 @@
1
+ var queue = exports;
2
+
3
+ /**
4
+ * Query GET "queue"
5
+ *
6
+ * - Validate = run any validations defined (how to define these?!)
7
+ * - Params = run through all the params items, each with some config?
8
+ * - Permissions = defined by the calling module / default concept of ownership
9
+ * - Query = execute the modelMethod to collect the data to return
10
+ * - Process = clean up the model data, post permissions, etc
11
+ * - Format = prepare for mapper/formatter
12
+ */
13
+ queue.query = {
14
+ // Validate: no-op until we have a standard definition
15
+ validate: require('./noop'),
16
+ // Params: there's a bunch of these!
17
+ // TODO: we need an efficient way to run them all
18
+ // TODO: and a simple way to hook into each one to override behaviour / define if it is enabled or not
19
+ paramsData: require('./params-data'), // this should probably have a different name?
20
+ paramsInclude: require('./params-include'),
21
+ paramsPage: require('./params-page'),
22
+ paramsFilter: require('./params-filter'),
23
+ paramsFields: require('./params-fields'),
24
+ paramsSort: require('./params-sort'),
25
+ // Permissions: noop until we have a default concept of ownership
26
+ permissions: require('./noop'),
27
+ // Query: execute a modelMethod to get data to return
28
+ query: require('./query'),
29
+ // Process: noop
30
+ process: require('./noop'),
31
+ // Format: prepare options to be passed to the formatter
32
+ format: require('./format')
33
+ };
34
+
35
+ /**
36
+ * Action POST/PUT/PATCH "queue"
37
+ *
38
+ * - Validate = run any validations defined (how to define these?!)
39
+ * - Permissions = defined by the calling module / default concept of ownership
40
+ * - Action = defined by the calling module
41
+ * - Query = execute the modelMethod to collect the data to return
42
+ * - Process = clean up the model data, post permissions, etc
43
+ * - Format = prepare for mapper/formatter
44
+ */
45
+ queue.action = {
46
+ // Validate: no-op until we have a standard definition
47
+ validate: require('./noop'),
48
+ // Payload: process req.body
49
+ payload: require('./payload'),
50
+ // Permissions: noop until we have a default concept of ownership
51
+ permissions: require('./noop'),
52
+ // Action: noop unless defined by the calling module
53
+ action: require('./action'),
54
+ // Query: execute a modelMethod to get data to return
55
+ query: require('./query'),
56
+ // Process: noop
57
+ process: require('./noop'),
58
+ // Format: prepare options to be passed to the formatter
59
+ format: require('./format')
60
+ };
61
+
62
+
63
+ /**
64
+ * DELETE "queue"
65
+ *
66
+ * - Permissions = defined by the calling module / default concept of ownership
67
+ * - Query = execute the modelMethod to delete
68
+ * END -> return error or 204 No Content
69
+ */
70
+
71
+ queue.destroy = {
72
+ // Permissions: noop until we have a default concept of ownership
73
+ permissions: require('./noop'),
74
+ // Destroy: execute destroy or destroyMethod if set to remove data
75
+ destroy: require('./destroy')
76
+ };
@@ -0,0 +1,5 @@
1
+ var debug = require('ghost-ignition').debug('apiware:noop');
2
+ module.exports = function noop(apiReq, apiRes, next) {
3
+ debug('doing no op');
4
+ next();
5
+ };
@@ -0,0 +1,12 @@
1
+ var debug = require('ghost-ignition').debug('apiware:paramsData');
2
+
3
+ module.exports = function paramsData(apiReq, apiRes, next) {
4
+ debug('Checking for identifier');
5
+ if (apiReq.params.identifier) {
6
+ // Handle having an ID
7
+ debug('Handling identifier');
8
+ apiReq.query.data.id = apiReq.params.identifier;
9
+ }
10
+
11
+ next();
12
+ };
@@ -0,0 +1,14 @@
1
+ var debug = require('ghost-ignition').debug('apiware:paramsFields');
2
+ var errors = require('ghost-ignition').errors;
3
+ var _ = require('lodash');
4
+
5
+ module.exports = function paramsFields(apiReq, apiRes, next) {
6
+ debug('Fields is not yet supported');
7
+
8
+ if (!_.isEmpty(apiReq.params.queryData.fields)) {
9
+ return next(new errors.BadRequestError({message: 'Fields is not yet supported'}));
10
+ }
11
+
12
+ apiReq.query.options.fields = {};
13
+ return next();
14
+ };
@@ -0,0 +1,23 @@
1
+ var debug = require('ghost-ignition').debug('apiware:paramsFilter');
2
+ var errors = require('ghost-ignition').errors;
3
+ var _ = require('lodash');
4
+
5
+ module.exports = function paramsFilter(apiReq, apiRes, next) {
6
+ debug('Filter is not yet supported');
7
+ var filterIsEmpty = true;
8
+
9
+ // @TODO, yep this is dumb, waiting for jsonapi-mapper & bookshelf-jsonapi-params to come out of beta
10
+ // @TODO, figure out what we should support here
11
+ _.each(apiReq.params.queryData.filter, function (filter) {
12
+ if (!_.isEmpty(filter)) {
13
+ filterIsEmpty = false;
14
+ }
15
+ });
16
+
17
+ if (!filterIsEmpty) {
18
+ return next(new errors.BadRequestError({message: 'Filter is not yet supported'}));
19
+ }
20
+
21
+ apiReq.query.options.filter = {};
22
+ return next();
23
+ };
@@ -0,0 +1,22 @@
1
+ var debug = require('ghost-ignition').debug('apiware:paramsInclude');
2
+ var errors = require('ghost-ignition').errors;
3
+ var _ = require('lodash');
4
+
5
+ module.exports = function paramsInclude(apiReq, apiRes, next) {
6
+ // Handle Includes
7
+ var requestedIncludes = apiReq.params.queryData.include;
8
+ var allowedIncludes = apiReq.options.relations;
9
+ var diff = _.difference(requestedIncludes, allowedIncludes);
10
+ debug('permitting includes', allowedIncludes);
11
+
12
+ if (diff.length > 0) {
13
+ return next(new errors.ValidationError({
14
+ message: 'Unsupported include value',
15
+ source: diff.join(', ')
16
+ }));
17
+ }
18
+
19
+ apiReq.query.options.include = apiReq.params.queryData.include;
20
+
21
+ return next();
22
+ };
@@ -0,0 +1,36 @@
1
+ var _ = require('lodash'),
2
+ debug = require('ghost-ignition').debug('apiware:paramsPage'),
3
+ defaultPageOptions = {
4
+ limit: 10,
5
+ offset: 0
6
+ };
7
+
8
+ module.exports = function paramsPage(apiReq, apiRes, next) {
9
+ debug('Ensuring limit and offset');
10
+
11
+ if (apiReq.options.queryMethod === 'getOne') {
12
+ return next();
13
+ }
14
+
15
+ var pageOptions = apiReq.options.page || {};
16
+
17
+ if (pageOptions.pageSize) {
18
+ pageOptions.limit = pageOptions.pageSize;
19
+ delete pageOptions.pageSize;
20
+ }
21
+
22
+ if (pageOptions.page) {
23
+ pageOptions.offset = pageOptions.page;
24
+ delete pageOptions.page;
25
+ }
26
+
27
+ pageOptions = _.defaults({}, apiReq.params.queryData.page, pageOptions, defaultPageOptions);
28
+
29
+ if (pageOptions.limit === 'all') {
30
+ pageOptions = false;
31
+ }
32
+
33
+ apiReq.query.options.page = pageOptions;
34
+
35
+ return next();
36
+ };
@@ -0,0 +1,16 @@
1
+ var debug = require('ghost-ignition').debug('apiware:paramsSort');
2
+ var _ = require('lodash');
3
+
4
+ module.exports = function paramsSort(apiReq, apiRes, next) {
5
+ debug('Sorting');
6
+
7
+ // Apply the default
8
+ apiReq.query.options.sort = apiReq.options.defaultSort || [];
9
+
10
+ if (!_.isEmpty(apiReq.params.queryData.sort)) {
11
+ // If a sort was set for this request, apply that instead
12
+ apiReq.query.options.sort = apiReq.params.queryData.sort;
13
+ }
14
+
15
+ return next();
16
+ };
@@ -0,0 +1,52 @@
1
+ var errors = require('ghost-ignition').errors;
2
+ var _ = require('lodash');
3
+
4
+ function isValidData(data) {
5
+ if (_.isEmpty(data)) {
6
+ return false;
7
+ }
8
+
9
+ if (!_.isArray(data) && !_.isObject(data)) {
10
+ return false;
11
+ }
12
+
13
+ if (_.isArray(data)) {
14
+ // TODO: validate collections / arrays
15
+ return true;
16
+ }
17
+
18
+ if (!_.has(data, 'type')) {
19
+ return false;
20
+ }
21
+
22
+ if (!_.has(data, 'attributes')) {
23
+ return false;
24
+ }
25
+
26
+ return true;
27
+ }
28
+
29
+ module.exports = function payload(apiReq, apiRes, next) {
30
+ if (!_.isEmpty(apiReq.payload)) {
31
+ var data = apiReq.payload.data;
32
+ // Handle having a payload
33
+ if (!isValidData(data)) {
34
+ return next(new errors.BadRequestError({
35
+ message: 'Malformed payload: data object is invalid'
36
+ }));
37
+ }
38
+
39
+ if (_.isArray(data) && data.length === 1) {
40
+ apiReq.query.data = data[0].attributes;
41
+ }
42
+
43
+ // TODO handle array payloads with more than 1 item?
44
+
45
+ // Arrays return true for isObject :(
46
+ if (!_.isArray(data) && _.isObject(data)) {
47
+ apiReq.query.data = data.attributes;
48
+ }
49
+ }
50
+
51
+ next();
52
+ };
@@ -0,0 +1,28 @@
1
+ var debug = require('ghost-ignition').debug('apiware:query');
2
+ var errors = require('ghost-ignition').errors;
3
+
4
+ module.exports = function query(apiReq, apiRes, next) {
5
+ // Default to using the getOne method
6
+ var methodToCall = apiReq.options.queryMethod || 'getOne';
7
+
8
+ debug('doing model queryMethod', methodToCall);
9
+
10
+ apiRes.exec.push({
11
+ method: methodToCall,
12
+ payload: apiReq.query
13
+ });
14
+
15
+ apiReq.model[methodToCall](apiReq.query)
16
+ .then(function querySuccess(result) {
17
+ debug('model query succeeded');
18
+ apiRes.model = result;
19
+ return next();
20
+ }, function queryFailure(err) {
21
+ if (err instanceof apiReq.model.NotFoundError) {
22
+ return next(new errors.NotFoundError({message: 'No result found'}));
23
+ }
24
+
25
+ debug('model query failed');
26
+ return next(err);
27
+ });
28
+ };