adapt-authoring-api 0.0.1
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/.eslintignore +1 -0
- package/.eslintrc +14 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
- package/.github/dependabot.yml +11 -0
- package/.github/pull_request_template.md +25 -0
- package/.github/workflows/labelled_prs.yml +16 -0
- package/.github/workflows/new.yml +19 -0
- package/adapt-authoring.json +9 -0
- package/conf/config.schema.json +22 -0
- package/docs/writing-an-api.md +135 -0
- package/errors/errors.json +32 -0
- package/index.js +7 -0
- package/lib/AbstractApiModule.js +668 -0
- package/lib/AbstractApiUtils.js +145 -0
- package/lib/DataCache.js +46 -0
- package/lib/typedefs.js +67 -0
- package/package.json +23 -0
- package/tests/abstractApiModule.spec.js +84 -0
- package/tests/abstractApiUtils.spec.js +49 -0
- package/tests/data/testApiModule.js +50 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for APIs
|
|
3
|
+
* @memberof api
|
|
4
|
+
*/
|
|
5
|
+
class AbstractApiUtils {
|
|
6
|
+
/**
|
|
7
|
+
* Converts HTTP methods to a corresponding 'action' for use in auth
|
|
8
|
+
* @param {String} method The HTTP method
|
|
9
|
+
* @return {String}
|
|
10
|
+
*/
|
|
11
|
+
static httpMethodToAction (method) {
|
|
12
|
+
switch (method.toLowerCase()) {
|
|
13
|
+
case 'get':
|
|
14
|
+
return 'read'
|
|
15
|
+
case 'post':
|
|
16
|
+
case 'put':
|
|
17
|
+
case 'patch':
|
|
18
|
+
case 'delete':
|
|
19
|
+
return 'write'
|
|
20
|
+
default:
|
|
21
|
+
return ''
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Converts HTTP methods to a corresponding database function
|
|
27
|
+
* @param {String} method The HTTP method
|
|
28
|
+
* @return {String}
|
|
29
|
+
*/
|
|
30
|
+
static httpMethodToDBFunction (method) {
|
|
31
|
+
switch (method.toLowerCase()) {
|
|
32
|
+
case 'post': return 'insert'
|
|
33
|
+
case 'get': return 'find'
|
|
34
|
+
case 'put': case 'patch': return 'update'
|
|
35
|
+
case 'delete': return 'delete'
|
|
36
|
+
default: return ''
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generates a list of arguments to be passed to the MongoDBModule from a request object
|
|
42
|
+
* @param {external:ExpressRequest} req
|
|
43
|
+
* @return {Array<*>}
|
|
44
|
+
*/
|
|
45
|
+
static argsFromReq (req) {
|
|
46
|
+
const opts = { schemaName: req.apiData.schemaName, collectionName: req.apiData.collectionName }
|
|
47
|
+
switch (req.method) {
|
|
48
|
+
case 'GET': case 'DELETE':
|
|
49
|
+
return [req.apiData.query, opts]
|
|
50
|
+
case 'POST':
|
|
51
|
+
return [req.apiData.data, opts]
|
|
52
|
+
case 'PUT': case 'PATCH':
|
|
53
|
+
return [req.apiData.query, req.apiData.data, opts]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generates REST API metadata and stores on route config
|
|
59
|
+
* @param {AbstractApiModule} instance The current AbstractApiModule instance
|
|
60
|
+
*/
|
|
61
|
+
static generateApiMetadata (instance) {
|
|
62
|
+
const getData = isList => {
|
|
63
|
+
const $ref = { $ref: `#/components/schemas/${instance.schemaName}` }
|
|
64
|
+
return {
|
|
65
|
+
description: `The ${instance.schemaName} data`,
|
|
66
|
+
content: { 'application/json': { schema: isList ? { type: 'array', items: $ref } : $ref } }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const queryParams = [
|
|
70
|
+
{
|
|
71
|
+
name: 'limit',
|
|
72
|
+
in: 'query',
|
|
73
|
+
description: `How many results should be returned Default value is ${instance.app.config.get('adapt-authoring-api.defaultPageSize')} (max value is ${instance.app.config.get('adapt-authoring-api.maxPageSize')})`
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'page',
|
|
77
|
+
in: 'query',
|
|
78
|
+
description: 'The page of results to return (determined from the limit value)'
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
const verbMap = {
|
|
82
|
+
put: 'Replace',
|
|
83
|
+
get: 'Retrieve',
|
|
84
|
+
patch: 'Update',
|
|
85
|
+
delete: 'Delete',
|
|
86
|
+
post: 'Insert'
|
|
87
|
+
}
|
|
88
|
+
instance.routes.forEach(r => {
|
|
89
|
+
r.meta = {}
|
|
90
|
+
Object.keys(r.handlers).forEach(method => {
|
|
91
|
+
let summary, parameters, requestBody, responses
|
|
92
|
+
switch (r.route) {
|
|
93
|
+
case '/':
|
|
94
|
+
if (method === 'post') {
|
|
95
|
+
summary = `${verbMap.post} a new ${instance.schemaName} document`
|
|
96
|
+
requestBody = getData()
|
|
97
|
+
responses = { 201: getData() }
|
|
98
|
+
} else {
|
|
99
|
+
summary = `${verbMap.get} all ${instance.collectionName} documents`
|
|
100
|
+
parameters = queryParams
|
|
101
|
+
responses = { 200: getData(true) }
|
|
102
|
+
}
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
case '/:_id':
|
|
106
|
+
summary = `${verbMap[method]} an existing ${instance.schemaName} document`
|
|
107
|
+
requestBody = method === 'put' || method === 'patch' ? getData() : method === 'delete' ? undefined : {}
|
|
108
|
+
responses = { [method === 'delete' ? 204 : 200]: getData() }
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
case '/query':
|
|
112
|
+
summary = `Query all ${instance.collectionName}`
|
|
113
|
+
parameters = queryParams
|
|
114
|
+
responses = { 200: getData(true) }
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
case '/schema':
|
|
118
|
+
summary = `Retrieve ${instance.schemaName} schema`
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
r.meta[method] = { summary, parameters, requestBody, responses }
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Clones an object and converts any Dates and ObjectIds to Strings
|
|
128
|
+
* @param {Object} data
|
|
129
|
+
* @returns A clone object with stringified ObjectIds
|
|
130
|
+
*/
|
|
131
|
+
static stringifyValues (data) {
|
|
132
|
+
return Object.entries(data).reduce((cloned, [key, val]) => {
|
|
133
|
+
const type = val?.constructor?.name
|
|
134
|
+
cloned[key] =
|
|
135
|
+
type === 'Date' || type === 'ObjectId'
|
|
136
|
+
? val.toString()
|
|
137
|
+
: type === 'Array' || type === 'Object'
|
|
138
|
+
? this.stringifyValues(val)
|
|
139
|
+
: val
|
|
140
|
+
return cloned
|
|
141
|
+
}, Array.isArray(data) ? [] : {})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default AbstractApiUtils
|
package/lib/DataCache.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { App } from 'adapt-authoring-core'
|
|
2
|
+
/**
|
|
3
|
+
* Time-limited data cache
|
|
4
|
+
* @memberof api
|
|
5
|
+
*/
|
|
6
|
+
class DataCache {
|
|
7
|
+
/** @override */
|
|
8
|
+
constructor ({ enable, lifespan }) {
|
|
9
|
+
this.isEnabled = enable !== false
|
|
10
|
+
this.lifespan = lifespan ?? App.instance.config.get('adapt-authoring-api.defaultCacheLifespan')
|
|
11
|
+
this.cache = {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Retrieve cached data, or run fresh query if no cache exists or cache is invalid
|
|
16
|
+
* @param {Object} query
|
|
17
|
+
* @param Object} options
|
|
18
|
+
* @param {Object} mongoOptions
|
|
19
|
+
* @returns {*} The cached data
|
|
20
|
+
*/
|
|
21
|
+
async get (query, options, mongoOptions) {
|
|
22
|
+
const key = JSON.stringify(query) + JSON.stringify(options) + JSON.stringify(mongoOptions)
|
|
23
|
+
this.prune()
|
|
24
|
+
if (this.cache[key]) {
|
|
25
|
+
return this.cache[key].data
|
|
26
|
+
}
|
|
27
|
+
const mongodb = await App.instance.waitForModule('mongodb')
|
|
28
|
+
const data = await mongodb.find(options.collectionName, query, mongoOptions)
|
|
29
|
+
this.cache[key] = { data, timestamp: Date.now() }
|
|
30
|
+
return data
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Removes invalid cache data
|
|
35
|
+
*/
|
|
36
|
+
prune () {
|
|
37
|
+
Object.keys(this.cache).forEach(k => {
|
|
38
|
+
const cache = this.cache[k]
|
|
39
|
+
if (Date.now() > (cache.timestamp + this.lifespan)) {
|
|
40
|
+
delete this.cache[k]
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default DataCache
|
package/lib/typedefs.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file exists to define the below types for documentation purposes.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* For AbstractApiModule subclasses the Express ClientRequest object is given an extra apiData property which contains useful data related to the incoming request.
|
|
6
|
+
* Extends Route definition with additional API-specific attributes
|
|
7
|
+
* @memberof api
|
|
8
|
+
* @typedef {Object} ApiRequestData
|
|
9
|
+
* @property {Object} config The API route's config data. Set when the route is initialised.
|
|
10
|
+
* @property {String} collectionName The DB collection name
|
|
11
|
+
* @property {Object} data The request body data
|
|
12
|
+
* @property {Object} query The request query data
|
|
13
|
+
* @property {String} schemaName The schema name for data validation
|
|
14
|
+
* @property {Boolean} modifying Whether the request modifies data
|
|
15
|
+
* @see {ApiRoute}
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Extends the existing Route definition with additional API-specific attributes
|
|
19
|
+
* @memberof api
|
|
20
|
+
* @typedef {Route} ApiRoute
|
|
21
|
+
* @extends {Route}
|
|
22
|
+
* @property {Array<string>} modifiers Defines which HTTP methods can modify data (verbs must be lower-case). This only needs do be defined for routes are non standard. By default, it is assumed that 'get' requests are non-modifying, and 'post', 'put', 'patch' and 'delete' are modifying.
|
|
23
|
+
* @example
|
|
24
|
+
* modifiers: ['post']
|
|
25
|
+
* @property {Boolean} validate Whether the request data should be validated
|
|
26
|
+
* @property {Object} permissions Definition of permissions allowed required to access each handler
|
|
27
|
+
* @property {Array<string>} [permissions.post] Permissions scopes required to access this route
|
|
28
|
+
* @property {Array<string>} [permissions.get] Permissions scopes required to access this route
|
|
29
|
+
* @property {Array<string>} [permissions.put] Permissions scopes required to access this route
|
|
30
|
+
* @property {Array<string>} [permissions.delete] Permissions scopes required to access this route
|
|
31
|
+
* @example
|
|
32
|
+
* {
|
|
33
|
+
* route: '/',
|
|
34
|
+
* handlers: { post: postHandler },
|
|
35
|
+
* permissions: { post: ['write:scope'] }
|
|
36
|
+
* modifiers: ['post'],
|
|
37
|
+
* }
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* @memberof api
|
|
41
|
+
* @typedef {Object} InsertOptions
|
|
42
|
+
* @property {String} schemaName Name of the schema to validate against
|
|
43
|
+
* @property {String} collectionName DB collection to insert document into
|
|
44
|
+
* @property {String} validate Whether the incoming data should be validated
|
|
45
|
+
* @property {String} invokePostHook Whether the function should invoke the 'post' action hook on success
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* @memberof api
|
|
49
|
+
* @typedef {Object} FindOptions
|
|
50
|
+
* @property {String} schemaName Name of the schema to validate against
|
|
51
|
+
* @property {String} collectionName DB collection to insert document into
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* @memberof api
|
|
55
|
+
* @typedef {Object} UpdateOptions
|
|
56
|
+
* @property {String} schemaName Name of the schema to validate against
|
|
57
|
+
* @property {String} collectionName DB collection to insert document into
|
|
58
|
+
* @property {Boolean} validate Whether the incoming data should be validated
|
|
59
|
+
* @property {String} invokePostHook Whether the function should invoke the 'post' action hook on success
|
|
60
|
+
* @property {Boolean} rawUpdate Whether the provided data should be considered 'raw' (i.e. not format and apply $set MongoDB keyword)
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* @memberof api
|
|
64
|
+
* @typedef {Object} DeleteOptions
|
|
65
|
+
* @property {String} collectionName DB collection to remove document from
|
|
66
|
+
* @property {String} invokePostHook Whether the function should invoke the 'post' action hook on success
|
|
67
|
+
*/
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adapt-authoring-api",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Abstract module for creating APIs",
|
|
5
|
+
"homepage": "https://github.com/adapt-security/adapt-authoring-api",
|
|
6
|
+
"license": "GPL-3.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"repository": "github:adapt-security/adapt-authoring-api",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"lodash": "^4.17.21"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"adapt-authoring-auth": "github:adapt-security/adapt-authoring-auth",
|
|
15
|
+
"adapt-authoring-core": "github:adapt-security/adapt-authoring-core",
|
|
16
|
+
"adapt-authoring-jsonschema": "github:adapt-security/adapt-authoring-jsonschema",
|
|
17
|
+
"adapt-authoring-mongodb": "github:adapt-security/adapt-authoring-mongodb"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"eslint": "^9.14.0",
|
|
21
|
+
"standard": "^17.1.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const { App } = require('adapt-authoring-core');
|
|
2
|
+
const should = require('should');
|
|
3
|
+
const TestApiModule = require('./data/testApiModule');
|
|
4
|
+
|
|
5
|
+
describe('Abstract API module', function() {
|
|
6
|
+
before(function(done) {
|
|
7
|
+
const loadModule = (mod, done) => {
|
|
8
|
+
const m = this.app.getModule(mod);
|
|
9
|
+
m.preload (this.app, () => m.boot(this.app, done, done), done);
|
|
10
|
+
};
|
|
11
|
+
this.app = App.instance;
|
|
12
|
+
|
|
13
|
+
loadModule('server', () => loadModule('mongodb', done));
|
|
14
|
+
|
|
15
|
+
this.tmi = new TestApiModule(this.app, {});
|
|
16
|
+
this.tmi.router.should.not.be.undefined();
|
|
17
|
+
});
|
|
18
|
+
describe('#requestHandler()', function() {
|
|
19
|
+
it('should customise the request object', function() {
|
|
20
|
+
const req = { method: 'GET' };
|
|
21
|
+
TestApiModule.requestHandler()(req, {}, () => {});
|
|
22
|
+
should.exist(req.type);
|
|
23
|
+
should.exist(req.dsquery);
|
|
24
|
+
});
|
|
25
|
+
it('should correctly map HTTP methods to MongoDBModule functions', function() {
|
|
26
|
+
const m1 = this.app.getModule('mongodb');
|
|
27
|
+
const m2 = {
|
|
28
|
+
retrieve: () => {
|
|
29
|
+
return new Promise((resolve, reject) => { correctlyMapped = true; })
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
let correctlyMapped = false;
|
|
33
|
+
this.app.dependencyloader.modules['adapt-authoring-mongodb'] = m2;
|
|
34
|
+
TestApiModule.requestHandler()({ method: 'GET' }, {}, () => {});
|
|
35
|
+
this.app.dependencyloader.modules['adapt-authoring-mongodb'] = m1;
|
|
36
|
+
correctlyMapped.should.be.true();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('#preload()', function() {
|
|
40
|
+
it('should create a child Router with the correct route', function(done) {
|
|
41
|
+
this.tmi.preload(this.app, () => {
|
|
42
|
+
this.tmi.router.constructor.name.should.equal('Router');
|
|
43
|
+
done();
|
|
44
|
+
}, done);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('#initSchemas()', function() {
|
|
48
|
+
it('should add specified schemas to the DB', function(done) {
|
|
49
|
+
this.tmi.boot(this.app, () => {
|
|
50
|
+
should.exist(this.app.getModule('mongodb').connection.models.test);
|
|
51
|
+
done();
|
|
52
|
+
}, done);
|
|
53
|
+
});
|
|
54
|
+
it('should ignore badly configured data', function() {
|
|
55
|
+
const mongoModels = this.app.getModule('mongodb').connection.models;
|
|
56
|
+
Object.keys(mongoModels).length.should.equal(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('#initMiddleware()', function() {
|
|
60
|
+
it('should add middleware to the API router', function() {
|
|
61
|
+
const middleware = this.tmi.router.middleware;
|
|
62
|
+
middleware.length.should.equal(1);
|
|
63
|
+
middleware[0].name.should.equal('testMiddleware');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('#initRoutes()', function() {
|
|
67
|
+
it('should add routes defined as an object', function() {
|
|
68
|
+
const routes = this.tmi.router.routes.map(r => r.route);
|
|
69
|
+
routes.should.containEql('/objectroute');
|
|
70
|
+
});
|
|
71
|
+
it('should add routes defined as an array', function() {
|
|
72
|
+
const routes = this.tmi.router.routes.map(r => r.route);
|
|
73
|
+
routes.should.containEql('/arrayroute');
|
|
74
|
+
});
|
|
75
|
+
it('should set custom permissions scopes if specified', function() {
|
|
76
|
+
const postScopes = this.app.auth.routes.secure['/api/arrayroute'].post;
|
|
77
|
+
postScopes.should.containEql('testscope');
|
|
78
|
+
});
|
|
79
|
+
it('should set generic permissions scopes if not specified', function() {
|
|
80
|
+
const scopes = this.app.auth.routes.secure['/api/objectroute'];
|
|
81
|
+
scopes.should.not.be.undefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const should = require('should');
|
|
2
|
+
|
|
3
|
+
describe('Abstract API utilities', function() {
|
|
4
|
+
describe('#callDbFunction()', function() {
|
|
5
|
+
it('should fail if request parameter doesn\'t specify a type', function() {
|
|
6
|
+
false.should.be.true();
|
|
7
|
+
});
|
|
8
|
+
it('should fail if attempting to call an unknown DB function', function() {
|
|
9
|
+
false.should.be.true();
|
|
10
|
+
});
|
|
11
|
+
it('should return data in response', function() {
|
|
12
|
+
false.should.be.true();
|
|
13
|
+
});
|
|
14
|
+
it('should set response HTTP status', function() {
|
|
15
|
+
false.should.be.true();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('#httpMethodToAction()', function() {
|
|
19
|
+
it('should return a string', function() {
|
|
20
|
+
false.should.be.true();
|
|
21
|
+
});
|
|
22
|
+
it('should return action string for known action', function() {
|
|
23
|
+
false.should.be.true();
|
|
24
|
+
});
|
|
25
|
+
it('should return empty string for unknown action', function() {
|
|
26
|
+
false.should.be.true();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('#validateSchemaDef()', function() {
|
|
30
|
+
it('should fail if def isn\'t an object', function() {
|
|
31
|
+
false.should.be.true();
|
|
32
|
+
});
|
|
33
|
+
it('should fail if def has no name', function() {
|
|
34
|
+
false.should.be.true();
|
|
35
|
+
});
|
|
36
|
+
it('should fail if def has no model', function() {
|
|
37
|
+
false.should.be.true();
|
|
38
|
+
});
|
|
39
|
+
it('should fail if schemas isn\'t an array', function() {
|
|
40
|
+
false.should.be.true();
|
|
41
|
+
});
|
|
42
|
+
it('should fail if def has no routes', function() {
|
|
43
|
+
false.should.be.true();
|
|
44
|
+
});
|
|
45
|
+
it('should validate routes', function() {
|
|
46
|
+
false.should.be.true();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const AbstractAPIModule = require('../../lib/AbstractApiModule');
|
|
2
|
+
|
|
3
|
+
const TestSchema = {
|
|
4
|
+
name: 'test',
|
|
5
|
+
definition: {
|
|
6
|
+
isTest: {
|
|
7
|
+
type: "boolean", default: true
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
class TestApiModule extends AbstractAPIModule {
|
|
13
|
+
static get def() {
|
|
14
|
+
return {
|
|
15
|
+
name: 'test',
|
|
16
|
+
model: 'test',
|
|
17
|
+
schemas: [
|
|
18
|
+
TestSchema,
|
|
19
|
+
{ name: 't2' },
|
|
20
|
+
{ definition: {} }
|
|
21
|
+
],
|
|
22
|
+
middleware: [testMiddleware],
|
|
23
|
+
routes: [
|
|
24
|
+
{
|
|
25
|
+
route: '/arrayroute',
|
|
26
|
+
handlers: ['post','get','put','delete'],
|
|
27
|
+
scopes: { post: 'testscope' }
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
route: '/objectroute',
|
|
31
|
+
handlers: {
|
|
32
|
+
post: testRouteHandler,
|
|
33
|
+
get: testRouteHandler,
|
|
34
|
+
put: testRouteHandler,
|
|
35
|
+
delete: testRouteHandler
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function testMiddleware(req, res, next) {
|
|
44
|
+
console.log('Test middleware called');
|
|
45
|
+
}
|
|
46
|
+
function testRouteHandler(req, res, next) {
|
|
47
|
+
console.log('Test handler called');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = TestApiModule;
|