@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 +21 -0
- package/README.md +164 -0
- package/index.js +3 -0
- package/lib/api.js +41 -0
- package/lib/apiware/action.js +47 -0
- package/lib/apiware/destroy.js +35 -0
- package/lib/apiware/format.js +54 -0
- package/lib/apiware/index.js +76 -0
- package/lib/apiware/noop.js +5 -0
- package/lib/apiware/params-data.js +12 -0
- package/lib/apiware/params-fields.js +14 -0
- package/lib/apiware/params-filter.js +23 -0
- package/lib/apiware/params-include.js +22 -0
- package/lib/apiware/params-page.js +36 -0
- package/lib/apiware/params-sort.js +16 -0
- package/lib/apiware/payload.js +52 -0
- package/lib/apiware/query.js +28 -0
- package/lib/ebja.js +24 -0
- package/lib/endpoint.js +67 -0
- package/lib/format.js +33 -0
- package/lib/http.js +71 -0
- package/lib/plugin.js +40 -0
- package/lib/request.js +35 -0
- package/lib/resource.js +16 -0
- package/lib/response.js +33 -0
- package/lib/stack.js +30 -0
- package/lib/vendor/jsonapi-mapper.js +14 -0
- package/lib/vendor/jsonapi-query-parser.js +2 -0
- package/package.json +40 -0
- package/test/apiware/payload.test.js +230 -0
- package/test/stack.test.js +126 -0
- package/test/stacks/action.test.js +199 -0
- package/test/stacks/destroy.test.js +171 -0
- package/test/stacks/query.test.js +198 -0
- package/test/structure.test.js +352 -0
- package/test/utils.js +22 -0
- package/vitest.config.ts +20 -0
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
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,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
|
+
};
|