adapt-authoring-api 0.0.1 → 1.0.0

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.
@@ -1,145 +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
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 CHANGED
@@ -1,46 +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
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 CHANGED
@@ -1,67 +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
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
67
  */
package/package.json CHANGED
@@ -1,23 +1,58 @@
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
- }
1
+ {
2
+ "name": "adapt-authoring-api",
3
+ "version": "1.0.0",
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
+ "@semantic-release/commit-analyzer": "^9.0.2",
23
+ "@semantic-release/git": "^10.0.1",
24
+ "@semantic-release/github": "^8.0.5",
25
+ "@semantic-release/npm": "^9.0.1",
26
+ "@semantic-release/release-notes-generator": "^10.0.3",
27
+ "conventional-changelog-eslint": "^3.0.9",
28
+ "semantic-release": "^21.0.1",
29
+ "semantic-release-replace-plugin": "^1.2.7"
30
+ },
31
+ "release": {
32
+ "plugins": [
33
+ [
34
+ "@semantic-release/commit-analyzer",
35
+ {
36
+ "preset": "eslint"
37
+ }
38
+ ],
39
+ [
40
+ "@semantic-release/release-notes-generator",
41
+ {
42
+ "preset": "eslint"
43
+ }
44
+ ],
45
+ "@semantic-release/npm",
46
+ "@semantic-release/github",
47
+ [
48
+ "@semantic-release/git",
49
+ {
50
+ "assets": [
51
+ "package.json"
52
+ ],
53
+ "message": "Chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
54
+ }
55
+ ]
56
+ ]
57
+ }
58
+ }