@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/lib/ebja.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Heavily borrowed from express
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
var debug = require('ghost-ignition').debug('main');
|
|
7
|
+
var API = require('./api');
|
|
8
|
+
var mixin = require('merge-descriptors');
|
|
9
|
+
|
|
10
|
+
function createAPI(options) {
|
|
11
|
+
var api = function () {};
|
|
12
|
+
|
|
13
|
+
debug('createAPI');
|
|
14
|
+
|
|
15
|
+
mixin(api, API, false);
|
|
16
|
+
|
|
17
|
+
api.init(options);
|
|
18
|
+
|
|
19
|
+
return api;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
exports = module.exports = createAPI;
|
|
23
|
+
|
|
24
|
+
exports.plugin = require('./plugin');
|
package/lib/endpoint.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var debug = require('ghost-ignition').debug('endpoint');
|
|
3
|
+
var _ = require('lodash');
|
|
4
|
+
var http = require('./http');
|
|
5
|
+
var IncomingMessage = require('http').IncomingMessage;
|
|
6
|
+
var stack = require('./stack');
|
|
7
|
+
var apiWare = require('./apiware');
|
|
8
|
+
|
|
9
|
+
// @TODO come up with a better way to do this
|
|
10
|
+
// Inspect the request inside the HTTP layer? SO if it is a real-world GET request
|
|
11
|
+
// Or maybe use a different way to define which "type", like manually set the queue type
|
|
12
|
+
var types = {
|
|
13
|
+
GET: 'query',
|
|
14
|
+
POST: 'action',
|
|
15
|
+
PUT: 'action',
|
|
16
|
+
PATCH: 'action',
|
|
17
|
+
DELETE: 'destroy'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function prepareStack(options) {
|
|
21
|
+
var type = types[options.method || 'GET'];
|
|
22
|
+
debug('STACK TYPE', type);
|
|
23
|
+
var stack = _.clone(apiWare[type]);
|
|
24
|
+
|
|
25
|
+
// Override the queue functions with any matching functions passed in as options
|
|
26
|
+
// Matches are done between queue keys and option keys
|
|
27
|
+
_.intersection(Object.keys(stack), Object.keys(options)).forEach(function handleMatch(optionsStackMatch) {
|
|
28
|
+
stack[optionsStackMatch] = options[optionsStackMatch];
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return stack;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// @TODO how to keep track of the resource that an endpoint belongs to?
|
|
35
|
+
var Endpoint = function Endpoint(api, options) {
|
|
36
|
+
var self = this;
|
|
37
|
+
this.api = api;
|
|
38
|
+
this.options = _.merge({}, this.api.options, options);
|
|
39
|
+
|
|
40
|
+
this.stack = prepareStack(options);
|
|
41
|
+
|
|
42
|
+
function doEndpoint(apiReq, apiRes, cb) {
|
|
43
|
+
debug('Executing apiware stack');
|
|
44
|
+
stack.handle(_.values(self.stack), apiReq, apiRes, cb);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// @TODO move this somewhere else?
|
|
48
|
+
var handler = function handler() {
|
|
49
|
+
debug('handling endpoint');
|
|
50
|
+
var args = Array.from(arguments);
|
|
51
|
+
|
|
52
|
+
// @TODO make this adapter logic a bit nicer
|
|
53
|
+
if (args.length === 3 && args[0] instanceof IncomingMessage) {
|
|
54
|
+
args.unshift(self.options);
|
|
55
|
+
args.unshift(self.api);
|
|
56
|
+
debug('endpoint apply with adapter');
|
|
57
|
+
|
|
58
|
+
return doEndpoint.apply(self, http.adapter.apply(self, args));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new Error('Unimplemented!');
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return handler;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
module.exports = Endpoint;
|
package/lib/format.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var _ = require('lodash');
|
|
3
|
+
var debug = require('ghost-ignition').debug('format');
|
|
4
|
+
var Mapper = require('./vendor/jsonapi-mapper');
|
|
5
|
+
|
|
6
|
+
var defaultSerializerOptions = {};
|
|
7
|
+
var defaultMapperOptions = {enableLinks: true};
|
|
8
|
+
|
|
9
|
+
module.exports = function format(api, apiReq, apiRes) {
|
|
10
|
+
if (!apiRes || _.isEmpty(apiRes.model)) {
|
|
11
|
+
debug('Nothing to format :(');
|
|
12
|
+
// We don't throw an error, but return empty
|
|
13
|
+
// This allows us to support, empty delete responses etc
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
debug('mapping to JSONAPI');
|
|
18
|
+
// Apply any defaults that weren't overridden
|
|
19
|
+
_.defaults(apiRes.serializerOptions, defaultSerializerOptions);
|
|
20
|
+
_.defaults(apiRes.mapperOptions, defaultMapperOptions);
|
|
21
|
+
var mapper = Mapper(api.baseUrl, apiRes.serializerOptions);
|
|
22
|
+
var json = mapper.map(apiRes.model, apiRes.type, apiRes.mapperOptions);
|
|
23
|
+
var includes = apiReq.query.options.include;
|
|
24
|
+
|
|
25
|
+
// Omit data attribute from related models that have not been included
|
|
26
|
+
// This is a workaround for jsonapi-mapper bug - https://github.com/scoutforpets/jsonapi-mapper/issues/69
|
|
27
|
+
_.each(json.data.relationships, function (v, k) {
|
|
28
|
+
json.data.relationships[k] = !_.includes(includes, k) ? _.omit(v, 'data') : v;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
debug('returning');
|
|
32
|
+
return json;
|
|
33
|
+
};
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var http = exports;
|
|
4
|
+
var _private = {};
|
|
5
|
+
var debug = require('ghost-ignition').debug('http');
|
|
6
|
+
var _ = require('lodash');
|
|
7
|
+
var queryParser = require('./vendor/jsonapi-query-parser');
|
|
8
|
+
var formatter = require('./format');
|
|
9
|
+
var APIRequest = require('./request');
|
|
10
|
+
var APIResponse = require('./response');
|
|
11
|
+
|
|
12
|
+
// TODO: do this based on req.method instead of apiReq.options.method?
|
|
13
|
+
var successCodes = {
|
|
14
|
+
GET: 200,
|
|
15
|
+
POST: 201,
|
|
16
|
+
DELETE: 204,
|
|
17
|
+
PATCH: 200
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
_private.toAPI = function toAPI(api, req, options) {
|
|
21
|
+
var params = queryParser.parseRequest(req.url);
|
|
22
|
+
var payload = req.body;
|
|
23
|
+
var source = options.request.sourceProperty ? req[options.request.sourceProperty] : req.user;
|
|
24
|
+
source.ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
25
|
+
var custom = {};
|
|
26
|
+
|
|
27
|
+
_.each(options.request.customProperties, function (propName) {
|
|
28
|
+
custom[propName] = req[propName];
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
debug('CONVERT to API');
|
|
32
|
+
var apiRequest = new APIRequest(api.models[options.model], options, params, payload, source, custom);
|
|
33
|
+
var apiResponse = new APIResponse(apiRequest);
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
apiRequest,
|
|
37
|
+
apiResponse
|
|
38
|
+
];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
_private.fromAPI = function fromAPI(api, apiReq, apiRes) {
|
|
42
|
+
debug('CONVERT to HTTP');
|
|
43
|
+
return {
|
|
44
|
+
data: formatter(api, apiReq, apiRes),
|
|
45
|
+
status: apiReq.options.response.successCode || successCodes[apiReq.options.method] || 200
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
http.adapter = function adapter(api, options, req, res, next) {
|
|
50
|
+
var args = _private.toAPI(api, req, options);
|
|
51
|
+
var callback = function callback(err, apiReq, apiRes) {
|
|
52
|
+
debug('HTTP callback');
|
|
53
|
+
if (err) {
|
|
54
|
+
debug('Got an error');
|
|
55
|
+
return next(err);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var json = _private.fromAPI(api, apiReq, apiRes);
|
|
59
|
+
|
|
60
|
+
if (json) {
|
|
61
|
+
debug('Returning JSON');
|
|
62
|
+
return res.status(json.status).json(json.data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return next();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
args.push(callback);
|
|
69
|
+
return args;
|
|
70
|
+
};
|
|
71
|
+
|
package/lib/plugin.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var debug = require('ghost-ignition').debug('plugin');
|
|
3
|
+
var jsonapiParams = require('bookshelf-jsonapi-params');
|
|
4
|
+
|
|
5
|
+
var jsonapi = function jsonapi(Bookshelf) {
|
|
6
|
+
if (!Bookshelf.Model.hasOwnProperty('fetchJsonApi')) {
|
|
7
|
+
Bookshelf.plugin(jsonapiParams);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Load the pagination plugin if it doesn't already exist
|
|
11
|
+
if (!Bookshelf.Model.hasOwnProperty('fetchPage')) {
|
|
12
|
+
Bookshelf.plugin('pagination');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var Model = Bookshelf.Model.extend(
|
|
16
|
+
{
|
|
17
|
+
jsonapi: true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
getOne: function getOne(query) {
|
|
21
|
+
debug('getOne', query);
|
|
22
|
+
var options = query.options || {};
|
|
23
|
+
// enforce require true for single resource fetches
|
|
24
|
+
options.require = options.require || true;
|
|
25
|
+
|
|
26
|
+
return this
|
|
27
|
+
// forge won't work here!
|
|
28
|
+
.where(query.data)
|
|
29
|
+
.fetchJsonApi(options, false);
|
|
30
|
+
},
|
|
31
|
+
getPage: function getPage(query) {
|
|
32
|
+
debug('getPage');
|
|
33
|
+
return this.fetchJsonApi(query.options);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
Bookshelf.Model = Model;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
module.exports = jsonapi;
|
package/lib/request.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var debug = require('ghost-ignition').debug('request');
|
|
3
|
+
var _ = require('lodash');
|
|
4
|
+
|
|
5
|
+
// @TODO resolve this into a sensible signature
|
|
6
|
+
class APIRequest {
|
|
7
|
+
constructor(model, options, params, payload, source, custom) {
|
|
8
|
+
debug('APIRequest');
|
|
9
|
+
|
|
10
|
+
// Ensure we have response options
|
|
11
|
+
options.response = options.response || {};
|
|
12
|
+
|
|
13
|
+
// The bookshelf model for which this request is being made
|
|
14
|
+
this.model = model;
|
|
15
|
+
// The model method that will be called to do a query
|
|
16
|
+
this.modelMethod = options.modelMethod;
|
|
17
|
+
// Any other options passed in
|
|
18
|
+
this.options = options;
|
|
19
|
+
// Parameters
|
|
20
|
+
this.params = params;
|
|
21
|
+
// Payload
|
|
22
|
+
this.payload = payload;
|
|
23
|
+
// The query that is going to be passed to the model method
|
|
24
|
+
this.query = {
|
|
25
|
+
data: {},
|
|
26
|
+
options: {}
|
|
27
|
+
};
|
|
28
|
+
// Who or what is making the request (usually some sort of user)
|
|
29
|
+
this.source = source;
|
|
30
|
+
|
|
31
|
+
_.extend(this, custom);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = APIRequest;
|
package/lib/resource.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var debug = require('ghost-ignition').debug('resource');
|
|
3
|
+
var _ = require('lodash');
|
|
4
|
+
var Endpoint = require('./endpoint');
|
|
5
|
+
|
|
6
|
+
// @TODO fixup the way this is created vs api and endpoint
|
|
7
|
+
var Resource = function Resource(api, resourceOpts) {
|
|
8
|
+
debug('Registering resource: ' + resourceOpts.model);
|
|
9
|
+
|
|
10
|
+
this.Endpoint = function registerEndpoint(options) {
|
|
11
|
+
debug('Registering resource endpoint: ' + options.queryMethod);
|
|
12
|
+
return new Endpoint(api, _.merge({}, resourceOpts, options))
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
module.exports = Resource;
|
package/lib/response.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var debug = require('ghost-ignition').debug('response');
|
|
3
|
+
var _ = require('lodash');
|
|
4
|
+
var Mapper = require('./vendor/jsonapi-mapper');
|
|
5
|
+
|
|
6
|
+
function getType(request) {
|
|
7
|
+
if (request.options.response && request.options.response.type) {
|
|
8
|
+
return request.options.response.type;
|
|
9
|
+
}
|
|
10
|
+
return request.params.relationshipType || request.params.resourceType;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// API Response
|
|
14
|
+
// @TODO resolve this into a sensible signature
|
|
15
|
+
// This object is initially constructed from just the request object
|
|
16
|
+
class APIResponse {
|
|
17
|
+
constructor(request) {
|
|
18
|
+
debug('APIResponse');
|
|
19
|
+
// A reference to the request object
|
|
20
|
+
this.request = request;
|
|
21
|
+
// The result of the query (bookshelf model)
|
|
22
|
+
this.model = {};
|
|
23
|
+
// The type of object that we are returning
|
|
24
|
+
this.type = getType(request);
|
|
25
|
+
// Various convoluted settings/options that get passed to the jsonapi mapper when formatting
|
|
26
|
+
this.serializerOptions = _.pick(request.options.response, Mapper.knownSerializerOptions.concat(request.options.relations));
|
|
27
|
+
this.mapperOptions = _.pick(request.options.response, Mapper.knownMapperOptions);
|
|
28
|
+
// An array of executed model methods
|
|
29
|
+
this.exec = [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = APIResponse;
|
package/lib/stack.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
module.exports.handle = function handleStack(stack, apiReq, apiRes, cb) {
|
|
3
|
+
var idx = 0;
|
|
4
|
+
|
|
5
|
+
if (stack.length === 0) {
|
|
6
|
+
return cb(null, apiReq, apiRes);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
apiNext();
|
|
10
|
+
|
|
11
|
+
function apiNext(err) {
|
|
12
|
+
var nextFunc = stack[idx];
|
|
13
|
+
idx += 1;
|
|
14
|
+
|
|
15
|
+
if (!nextFunc) {
|
|
16
|
+
return cb(err, apiReq, apiRes);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (err) {
|
|
20
|
+
apiRes.err = err;
|
|
21
|
+
return cb(err, apiReq, apiRes);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
nextFunc(apiReq, apiRes, apiNext);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
cb(err, apiReq, apiRes);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
var Mapper = require('@tryghost/jsonapi-mapper');
|
|
2
|
+
|
|
3
|
+
module.exports = function mapper(url, options) {
|
|
4
|
+
return new Mapper.Bookshelf(url, options);
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
module.exports.knownSerializerOptions = [
|
|
8
|
+
'attributes', 'ref', 'included', 'id', 'topLevelLinks', 'dataLinks', 'relationshipLinks', 'relationshipMeta',
|
|
9
|
+
'ignoreRelationshipData', 'keyForAttribute', 'nullIfMissing', 'pluralizeType', 'typeForAttribute', 'meta'
|
|
10
|
+
];
|
|
11
|
+
module.exports.knownMapperOptions = [
|
|
12
|
+
'omitAttrs', 'idAttribute', 'keyForAttr', 'relations', 'typeForModel', 'enableLinks', 'query', 'pagination'
|
|
13
|
+
];
|
|
14
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tryghost/express-bookshelf-jsonapi",
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"description": "Create JSON API endpoints from bookshelf",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"alias": "ebja",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "ssh://git@github.com/TryGhost/Pro-Packages",
|
|
10
|
+
"directory": "packages/express-bookshelf-jsonapi"
|
|
11
|
+
},
|
|
12
|
+
"author": "Hannah Wolfe",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/TryGhost/Pro-Packages/issues"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/TryGhost/Pro-Packages/tree/main/packages/express-bookshelf-jsonapi",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"bookshelf-jsonapi-params": "1.5.3",
|
|
20
|
+
"debug": "2.2.0",
|
|
21
|
+
"express": "4.14.0",
|
|
22
|
+
"ghost-ignition": "2.3.0",
|
|
23
|
+
"jsonapi-query-parser": "1.3.1",
|
|
24
|
+
"lodash": "4.16.4",
|
|
25
|
+
"merge-descriptors": "1.0.1",
|
|
26
|
+
"@tryghost/jsonapi-mapper": "^1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
30
|
+
"create-error": "0.3.1",
|
|
31
|
+
"sinon": "21.0.3",
|
|
32
|
+
"vitest": "4.1.6"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"dev": "echo \"Implement me!\"",
|
|
36
|
+
"test": "NODE_ENV=testing vitest run --coverage",
|
|
37
|
+
"lint": "oxlint -c ../../.oxlintrc.json .",
|
|
38
|
+
"posttest": "pnpm lint"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
var sinon = require('sinon');
|
|
2
|
+
var utils = require('../utils');
|
|
3
|
+
|
|
4
|
+
var payload = require('../../lib/apiware/payload');
|
|
5
|
+
|
|
6
|
+
var sandbox = sinon.createSandbox();
|
|
7
|
+
|
|
8
|
+
describe('apiware: payload', function () {
|
|
9
|
+
var req, res;
|
|
10
|
+
|
|
11
|
+
afterEach(function () {
|
|
12
|
+
sandbox.restore();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
beforeEach(function () {
|
|
16
|
+
req = {
|
|
17
|
+
options: {},
|
|
18
|
+
payload: {},
|
|
19
|
+
query: {
|
|
20
|
+
data: {},
|
|
21
|
+
options: {}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
res = {
|
|
26
|
+
exec: []
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does nothing for empty payloads', function () {
|
|
31
|
+
return new Promise(function (resolve, reject) {
|
|
32
|
+
payload(req, res, function next(err) {
|
|
33
|
+
if (err) {
|
|
34
|
+
return reject(err);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
expect(typeof req.query.data).toBe('object');
|
|
38
|
+
expect(Object.keys(req.query.data)).toHaveLength(0);
|
|
39
|
+
resolve();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('data validation', function () {
|
|
45
|
+
it('errors for payloads without a data param', function () {
|
|
46
|
+
return new Promise(function (resolve, reject) {
|
|
47
|
+
req.payload = {id: 'a1b'};
|
|
48
|
+
|
|
49
|
+
payload(req, res, function next(err) {
|
|
50
|
+
if (err) {
|
|
51
|
+
utils.expectIgnitionError(err, 'BadRequestError', /Malformed payload/);
|
|
52
|
+
|
|
53
|
+
return resolve();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
reject(new Error('This should have thrown an error'));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('errors for payloads with a string data param', function () {
|
|
62
|
+
return new Promise(function (resolve, reject) {
|
|
63
|
+
req.payload = {data: 'a1b'};
|
|
64
|
+
|
|
65
|
+
payload(req, res, function next(err) {
|
|
66
|
+
if (err) {
|
|
67
|
+
utils.expectIgnitionError(err, 'BadRequestError', /Malformed payload/);
|
|
68
|
+
|
|
69
|
+
return resolve();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
reject(new Error('This should have thrown an error'));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('errors for payloads with an empty array data param', function () {
|
|
78
|
+
return new Promise(function (resolve, reject) {
|
|
79
|
+
req.payload = {data: []};
|
|
80
|
+
|
|
81
|
+
payload(req, res, function next(err) {
|
|
82
|
+
if (err) {
|
|
83
|
+
utils.expectIgnitionError(err, 'BadRequestError', /Malformed payload/);
|
|
84
|
+
|
|
85
|
+
return resolve();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
reject(new Error('This should have thrown an error'));
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('errors for payloads with an empty object data param', function () {
|
|
94
|
+
return new Promise(function (resolve, reject) {
|
|
95
|
+
req.payload = {data: {}};
|
|
96
|
+
|
|
97
|
+
payload(req, res, function next(err) {
|
|
98
|
+
if (err) {
|
|
99
|
+
utils.expectIgnitionError(err, 'BadRequestError', /Malformed payload/);
|
|
100
|
+
|
|
101
|
+
return resolve();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
reject(new Error('This should have thrown an error'));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('errors for payloads with object and no type or attributes', function () {
|
|
110
|
+
return new Promise(function (resolve, reject) {
|
|
111
|
+
req.payload = {data: {id: 'a1b'}};
|
|
112
|
+
|
|
113
|
+
payload(req, res, function next(err) {
|
|
114
|
+
if (err) {
|
|
115
|
+
utils.expectIgnitionError(err, 'BadRequestError', /Malformed payload/);
|
|
116
|
+
|
|
117
|
+
return resolve();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
reject(new Error('This should have thrown an error'));
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('errors for payloads with object, type, but no attributes', function () {
|
|
126
|
+
return new Promise(function (resolve, reject) {
|
|
127
|
+
req.payload = {data: {
|
|
128
|
+
id: 'a1b',
|
|
129
|
+
type: 'thing'
|
|
130
|
+
}};
|
|
131
|
+
|
|
132
|
+
payload(req, res, function next(err) {
|
|
133
|
+
if (err) {
|
|
134
|
+
utils.expectIgnitionError(err, 'BadRequestError', /Malformed payload/);
|
|
135
|
+
|
|
136
|
+
return resolve();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
reject(new Error('This should have thrown an error'));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('errors for payloads with object, attributes, but no type', function () {
|
|
145
|
+
return new Promise(function (resolve, reject) {
|
|
146
|
+
req.payload = {data: {
|
|
147
|
+
id: 'a1b',
|
|
148
|
+
attributes: {cat: 'hat'}
|
|
149
|
+
}};
|
|
150
|
+
|
|
151
|
+
payload(req, res, function next(err) {
|
|
152
|
+
if (err) {
|
|
153
|
+
utils.expectIgnitionError(err, 'BadRequestError', /Malformed payload/);
|
|
154
|
+
|
|
155
|
+
return resolve();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
reject(new Error('This should have thrown an error'));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('does not error for payload with data, attributes and type', function () {
|
|
164
|
+
return new Promise(function (resolve, reject) {
|
|
165
|
+
req.payload = {data: {
|
|
166
|
+
id: 'a1b',
|
|
167
|
+
type: 'thing',
|
|
168
|
+
attributes: {cat: 'hat'}
|
|
169
|
+
}};
|
|
170
|
+
|
|
171
|
+
payload(req, res, function next(err) {
|
|
172
|
+
if (err) {
|
|
173
|
+
return reject(err);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
expect(typeof req.query.data).toBe('object');
|
|
177
|
+
expect(req.query.data).toHaveProperty('cat', 'hat');
|
|
178
|
+
resolve();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('can handle a data array with length 1', function () {
|
|
184
|
+
return new Promise(function (resolve, reject) {
|
|
185
|
+
req.payload = {data: [{
|
|
186
|
+
id: 'a1b',
|
|
187
|
+
type: 'thing',
|
|
188
|
+
attributes: {cat: 'hat'}
|
|
189
|
+
}]};
|
|
190
|
+
|
|
191
|
+
payload(req, res, function next(err) {
|
|
192
|
+
if (err) {
|
|
193
|
+
return reject(err);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
expect(typeof req.query.data).toBe('object');
|
|
197
|
+
expect(req.query.data).toHaveProperty('cat', 'hat');
|
|
198
|
+
resolve();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('does not error for data array with multiple items, but also does nothing with it :(', function () {
|
|
204
|
+
return new Promise(function (resolve, reject) {
|
|
205
|
+
req.payload = {data: [
|
|
206
|
+
{
|
|
207
|
+
id: 'a1b',
|
|
208
|
+
type: 'thing1',
|
|
209
|
+
attributes: {cat: 'hat'}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'a2z',
|
|
213
|
+
type: 'thing2',
|
|
214
|
+
attributes: {mad: 'hat'}
|
|
215
|
+
}
|
|
216
|
+
]};
|
|
217
|
+
|
|
218
|
+
payload(req, res, function next(err) {
|
|
219
|
+
if (err) {
|
|
220
|
+
return reject(err);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
expect(typeof req.query.data).toBe('object');
|
|
224
|
+
expect(Object.keys(req.query.data)).toHaveLength(0);
|
|
225
|
+
resolve();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|