adapt-authoring-api 1.6.1 → 2.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/index.js CHANGED
@@ -3,5 +3,6 @@
3
3
  * @namespace api
4
4
  */
5
5
  export { default as AbstractApiModule } from './lib/AbstractApiModule.js'
6
- export { default as AbstractApiUtils } from './lib/AbstractApiUtils.js'
7
6
  export { default } from './lib/AbstractApiModule.js'
7
+ /** @deprecated Use named import { stringifyValues } from 'adapt-authoring-core' instead */
8
+ export { stringifyValues } from 'adapt-authoring-core'
@@ -1,6 +1,6 @@
1
1
  import _ from 'lodash'
2
- import { AbstractModule, Hook } from 'adapt-authoring-core'
3
- import ApiUtils from './AbstractApiUtils.js'
2
+ import { AbstractModule, Hook, stringifyValues } from 'adapt-authoring-core'
3
+ import { argsFromReq, generateApiMetadata, httpMethodToDBFunction } from './utils.js'
4
4
  import DataCache from './DataCache.js'
5
5
  /**
6
6
  * Abstract module for creating APIs
@@ -177,7 +177,14 @@ class AbstractApiModule extends AbstractModule {
177
177
  return this.log('error', 'Must set API root before calling useDefaultConfig function')
178
178
  }
179
179
  /** @ignore */ this.routes = this.DEFAULT_ROUTES
180
- ApiUtils.generateApiMetadata(this)
180
+ this.generateApiMetadata()
181
+ }
182
+
183
+ /**
184
+ * Generates REST API metadata and stores on route config
185
+ */
186
+ generateApiMetadata () {
187
+ generateApiMetadata(this)
181
188
  }
182
189
 
183
190
  /**
@@ -349,7 +356,7 @@ class AbstractApiModule extends AbstractModule {
349
356
  requestHandler () {
350
357
  const requestHandler = async (req, res, next) => {
351
358
  const method = req.method.toLowerCase()
352
- const func = this[ApiUtils.httpMethodToDBFunction(method)]
359
+ const func = this[httpMethodToDBFunction(method)]
353
360
  if (!func) {
354
361
  return next(this.app.errors.HTTP_METHOD_NOT_SUPPORTED.setData({ method }))
355
362
  }
@@ -361,7 +368,7 @@ class AbstractApiModule extends AbstractModule {
361
368
  if (preCheck) {
362
369
  await this.checkAccess(req, req.apiData.query)
363
370
  }
364
- data = await func.apply(this, ApiUtils.argsFromReq(req))
371
+ data = await func.apply(this, argsFromReq(req))
365
372
  if (postCheck) {
366
373
  data = await this.checkAccess(req, data)
367
374
  }
@@ -654,7 +661,7 @@ class AbstractApiModule extends AbstractModule {
654
661
 
655
662
  if (options.invokePreHook !== false) await this.preUpdateHook.invoke(originalDoc, formattedData.$set, options, mongoOptions)
656
663
  formattedData.$set = await this.validate(options.schemaName, {
657
- ...ApiUtils.stringifyValues(originalDoc),
664
+ ...stringifyValues(originalDoc),
658
665
  ...formattedData.$set
659
666
  }, options)
660
667
 
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Generates a list of arguments to be passed to the MongoDBModule from a request object
3
+ * @param {external:ExpressRequest} req
4
+ * @return {Array<*>}
5
+ * @memberof api
6
+ */
7
+ export function argsFromReq (req) {
8
+ const opts = { schemaName: req.apiData.schemaName, collectionName: req.apiData.collectionName }
9
+ switch (req.method) {
10
+ case 'GET': case 'DELETE':
11
+ return [req.apiData.query, opts]
12
+ case 'POST':
13
+ return [req.apiData.data, opts]
14
+ case 'PUT': case 'PATCH':
15
+ return [req.apiData.query, req.apiData.data, opts]
16
+ }
17
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Generates REST API metadata and stores on route config
3
+ * @param {AbstractApiModule} instance The current AbstractApiModule instance
4
+ * @memberof api
5
+ */
6
+ export function generateApiMetadata (instance) {
7
+ const getData = isList => {
8
+ const $ref = { $ref: `#/components/schemas/${instance.schemaName}` }
9
+ return {
10
+ description: `The ${instance.schemaName} data`,
11
+ content: { 'application/json': { schema: isList ? { type: 'array', items: $ref } : $ref } }
12
+ }
13
+ }
14
+ const queryParams = [
15
+ {
16
+ name: 'limit',
17
+ in: 'query',
18
+ 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')})`
19
+ },
20
+ {
21
+ name: 'page',
22
+ in: 'query',
23
+ description: 'The page of results to return (determined from the limit value)'
24
+ }
25
+ ]
26
+ const verbMap = {
27
+ put: 'Replace',
28
+ get: 'Retrieve',
29
+ patch: 'Update',
30
+ delete: 'Delete',
31
+ post: 'Insert'
32
+ }
33
+ instance.routes.forEach(r => {
34
+ r.meta = {}
35
+ Object.keys(r.handlers).forEach(method => {
36
+ let summary, parameters, requestBody, responses
37
+ switch (r.route) {
38
+ case '/':
39
+ if (method === 'post') {
40
+ summary = `${verbMap.post} a new ${instance.schemaName} document`
41
+ requestBody = getData()
42
+ responses = { 201: getData() }
43
+ } else {
44
+ summary = `${verbMap.get} all ${instance.collectionName} documents`
45
+ parameters = queryParams
46
+ responses = { 200: getData(true) }
47
+ }
48
+ break
49
+
50
+ case '/:_id':
51
+ summary = `${verbMap[method]} an existing ${instance.schemaName} document`
52
+ requestBody = method === 'put' || method === 'patch' ? getData() : method === 'delete' ? undefined : {}
53
+ responses = { [method === 'delete' ? 204 : 200]: getData() }
54
+ break
55
+
56
+ case '/query':
57
+ summary = `Query all ${instance.collectionName}`
58
+ parameters = queryParams
59
+ responses = { 200: getData(true) }
60
+ break
61
+
62
+ case '/schema':
63
+ summary = `Retrieve ${instance.schemaName} schema`
64
+ break
65
+ }
66
+ r.meta[method] = { summary, parameters, requestBody, responses }
67
+ })
68
+ })
69
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Converts HTTP methods to a corresponding 'action' for use in auth
3
+ * @param {String} method The HTTP method
4
+ * @return {String}
5
+ * @memberof api
6
+ */
7
+ export function httpMethodToAction (method) {
8
+ switch (method.toLowerCase()) {
9
+ case 'get':
10
+ return 'read'
11
+ case 'post':
12
+ case 'put':
13
+ case 'patch':
14
+ case 'delete':
15
+ return 'write'
16
+ default:
17
+ return ''
18
+ }
19
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Converts HTTP methods to a corresponding database function
3
+ * @param {String} method The HTTP method
4
+ * @return {String}
5
+ * @memberof api
6
+ */
7
+ export function httpMethodToDBFunction (method) {
8
+ switch (method.toLowerCase()) {
9
+ case 'post': return 'insert'
10
+ case 'get': return 'find'
11
+ case 'put': case 'patch': return 'update'
12
+ case 'delete': return 'delete'
13
+ default: return ''
14
+ }
15
+ }
package/lib/utils.js ADDED
@@ -0,0 +1,4 @@
1
+ export { argsFromReq } from './utils/argsFromReq.js'
2
+ export { generateApiMetadata } from './utils/generateApiMetadata.js'
3
+ export { httpMethodToAction } from './utils/httpMethodToAction.js'
4
+ export { httpMethodToDBFunction } from './utils/httpMethodToDBFunction.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-api",
3
- "version": "1.6.1",
3
+ "version": "2.0.1",
4
4
  "description": "Abstract module for creating APIs",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-api",
6
6
  "license": "GPL-3.0",
@@ -11,7 +11,7 @@
11
11
  "test": "node --test 'tests/**/*.spec.js'"
12
12
  },
13
13
  "dependencies": {
14
- "adapt-authoring-core": "^1.7.0",
14
+ "adapt-authoring-core": "^2.0.0",
15
15
  "lodash": "^4.17.21"
16
16
  },
17
17
  "peerDependencies": {
@@ -0,0 +1,42 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { argsFromReq } from '../lib/utils/argsFromReq.js'
4
+
5
+ describe('argsFromReq()', () => {
6
+ const baseApiData = {
7
+ query: { _id: '123' },
8
+ data: { name: 'test' },
9
+ schemaName: 'testSchema',
10
+ collectionName: 'testCollection'
11
+ }
12
+ const expectedOpts = { schemaName: 'testSchema', collectionName: 'testCollection' }
13
+
14
+ it('should return [query, opts] for GET', () => {
15
+ const result = argsFromReq({ method: 'GET', apiData: baseApiData })
16
+ assert.deepEqual(result, [baseApiData.query, expectedOpts])
17
+ })
18
+
19
+ it('should return [query, opts] for DELETE', () => {
20
+ const result = argsFromReq({ method: 'DELETE', apiData: baseApiData })
21
+ assert.deepEqual(result, [baseApiData.query, expectedOpts])
22
+ })
23
+
24
+ it('should return [data, opts] for POST', () => {
25
+ const result = argsFromReq({ method: 'POST', apiData: baseApiData })
26
+ assert.deepEqual(result, [baseApiData.data, expectedOpts])
27
+ })
28
+
29
+ it('should return [query, data, opts] for PUT', () => {
30
+ const result = argsFromReq({ method: 'PUT', apiData: baseApiData })
31
+ assert.deepEqual(result, [baseApiData.query, baseApiData.data, expectedOpts])
32
+ })
33
+
34
+ it('should return [query, data, opts] for PATCH', () => {
35
+ const result = argsFromReq({ method: 'PATCH', apiData: baseApiData })
36
+ assert.deepEqual(result, [baseApiData.query, baseApiData.data, expectedOpts])
37
+ })
38
+
39
+ it('should return undefined for unknown methods', () => {
40
+ assert.equal(argsFromReq({ method: 'OPTIONS', apiData: baseApiData }), undefined)
41
+ })
42
+ })
@@ -0,0 +1,113 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { generateApiMetadata } from '../lib/utils/generateApiMetadata.js'
4
+
5
+ describe('generateApiMetadata()', () => {
6
+ function createInstance (routes) {
7
+ return {
8
+ schemaName: 'TestSchema',
9
+ collectionName: 'testcollection',
10
+ app: {
11
+ config: {
12
+ get: (key) => {
13
+ const map = {
14
+ 'adapt-authoring-api.defaultPageSize': 50,
15
+ 'adapt-authoring-api.maxPageSize': 200
16
+ }
17
+ return map[key]
18
+ }
19
+ }
20
+ },
21
+ routes: routes || []
22
+ }
23
+ }
24
+
25
+ it('should set meta on each route', () => {
26
+ const instance = createInstance([
27
+ { route: '/', handlers: { post: () => {}, get: () => {} } }
28
+ ])
29
+ generateApiMetadata(instance)
30
+ assert.ok(instance.routes[0].meta)
31
+ assert.ok(instance.routes[0].meta.post)
32
+ assert.ok(instance.routes[0].meta.get)
33
+ })
34
+
35
+ it('should generate correct summary for POST /', () => {
36
+ const instance = createInstance([
37
+ { route: '/', handlers: { post: () => {} } }
38
+ ])
39
+ generateApiMetadata(instance)
40
+ assert.equal(instance.routes[0].meta.post.summary, 'Insert a new TestSchema document')
41
+ })
42
+
43
+ it('should generate correct summary for GET /', () => {
44
+ const instance = createInstance([
45
+ { route: '/', handlers: { get: () => {} } }
46
+ ])
47
+ generateApiMetadata(instance)
48
+ assert.equal(instance.routes[0].meta.get.summary, 'Retrieve all testcollection documents')
49
+ })
50
+
51
+ it('should include query parameters for GET /', () => {
52
+ const instance = createInstance([
53
+ { route: '/', handlers: { get: () => {} } }
54
+ ])
55
+ generateApiMetadata(instance)
56
+ const params = instance.routes[0].meta.get.parameters
57
+ assert.ok(Array.isArray(params))
58
+ assert.equal(params.length, 2)
59
+ assert.equal(params[0].name, 'limit')
60
+ assert.equal(params[1].name, 'page')
61
+ })
62
+
63
+ it('should generate correct summary for /:_id routes', () => {
64
+ const instance = createInstance([
65
+ { route: '/:_id', handlers: { get: () => {}, put: () => {}, patch: () => {}, delete: () => {} } }
66
+ ])
67
+ generateApiMetadata(instance)
68
+ assert.equal(instance.routes[0].meta.get.summary, 'Retrieve an existing TestSchema document')
69
+ assert.equal(instance.routes[0].meta.put.summary, 'Replace an existing TestSchema document')
70
+ assert.equal(instance.routes[0].meta.patch.summary, 'Update an existing TestSchema document')
71
+ assert.equal(instance.routes[0].meta.delete.summary, 'Delete an existing TestSchema document')
72
+ })
73
+
74
+ it('should set 201 response for POST and 204 for DELETE', () => {
75
+ const instance = createInstance([
76
+ { route: '/', handlers: { post: () => {} } },
77
+ { route: '/:_id', handlers: { delete: () => {} } }
78
+ ])
79
+ generateApiMetadata(instance)
80
+ assert.ok(instance.routes[0].meta.post.responses[201])
81
+ assert.ok(instance.routes[1].meta.delete.responses[204])
82
+ })
83
+
84
+ it('should generate correct summary for /query route', () => {
85
+ const instance = createInstance([
86
+ { route: '/query', handlers: { post: () => {} } }
87
+ ])
88
+ generateApiMetadata(instance)
89
+ assert.equal(instance.routes[0].meta.post.summary, 'Query all testcollection')
90
+ })
91
+
92
+ it('should generate correct summary for /schema route', () => {
93
+ const instance = createInstance([
94
+ { route: '/schema', handlers: { get: () => {} } }
95
+ ])
96
+ generateApiMetadata(instance)
97
+ assert.equal(instance.routes[0].meta.get.summary, 'Retrieve TestSchema schema')
98
+ })
99
+
100
+ it('should handle multiple routes', () => {
101
+ const instance = createInstance([
102
+ { route: '/', handlers: { post: () => {}, get: () => {} } },
103
+ { route: '/:_id', handlers: { get: () => {}, delete: () => {} } },
104
+ { route: '/query', handlers: { post: () => {} } },
105
+ { route: '/schema', handlers: { get: () => {} } }
106
+ ])
107
+ generateApiMetadata(instance)
108
+ assert.equal(Object.keys(instance.routes[0].meta).length, 2)
109
+ assert.equal(Object.keys(instance.routes[1].meta).length, 2)
110
+ assert.equal(Object.keys(instance.routes[2].meta).length, 1)
111
+ assert.equal(Object.keys(instance.routes[3].meta).length, 1)
112
+ })
113
+ })
@@ -0,0 +1,27 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { httpMethodToAction } from '../lib/utils/httpMethodToAction.js'
4
+
5
+ describe('httpMethodToAction()', () => {
6
+ it('should return "read" for GET', () => {
7
+ assert.equal(httpMethodToAction('get'), 'read')
8
+ })
9
+
10
+ it('should be case-insensitive', () => {
11
+ assert.equal(httpMethodToAction('GET'), 'read')
12
+ assert.equal(httpMethodToAction('Get'), 'read')
13
+ })
14
+
15
+ const writeMethods = ['post', 'put', 'patch', 'delete']
16
+ writeMethods.forEach(method => {
17
+ it(`should return "write" for ${method.toUpperCase()}`, () => {
18
+ assert.equal(httpMethodToAction(method), 'write')
19
+ assert.equal(httpMethodToAction(method.toUpperCase()), 'write')
20
+ })
21
+ })
22
+
23
+ it('should return empty string for unknown methods', () => {
24
+ assert.equal(httpMethodToAction('options'), '')
25
+ assert.equal(httpMethodToAction('head'), '')
26
+ })
27
+ })
@@ -0,0 +1,30 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { httpMethodToDBFunction } from '../lib/utils/httpMethodToDBFunction.js'
4
+
5
+ describe('httpMethodToDBFunction()', () => {
6
+ const cases = [
7
+ { method: 'post', expected: 'insert' },
8
+ { method: 'get', expected: 'find' },
9
+ { method: 'put', expected: 'update' },
10
+ { method: 'patch', expected: 'update' },
11
+ { method: 'delete', expected: 'delete' }
12
+ ]
13
+
14
+ cases.forEach(({ method, expected }) => {
15
+ it(`should return "${expected}" for ${method.toUpperCase()}`, () => {
16
+ assert.equal(httpMethodToDBFunction(method), expected)
17
+ })
18
+ })
19
+
20
+ it('should be case-insensitive', () => {
21
+ assert.equal(httpMethodToDBFunction('POST'), 'insert')
22
+ assert.equal(httpMethodToDBFunction('Get'), 'find')
23
+ assert.equal(httpMethodToDBFunction('DELETE'), 'delete')
24
+ })
25
+
26
+ it('should return empty string for unknown methods', () => {
27
+ assert.equal(httpMethodToDBFunction('options'), '')
28
+ assert.equal(httpMethodToDBFunction('head'), '')
29
+ })
30
+ })
@@ -1,145 +0,0 @@
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,139 +0,0 @@
1
- import { describe, it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import AbstractApiUtils from '../lib/AbstractApiUtils.js'
4
-
5
- describe('AbstractApiUtils', () => {
6
- describe('.httpMethodToAction()', () => {
7
- it('should return "read" for GET', () => {
8
- assert.equal(AbstractApiUtils.httpMethodToAction('get'), 'read')
9
- assert.equal(AbstractApiUtils.httpMethodToAction('GET'), 'read')
10
- })
11
-
12
- const writeMethods = ['post', 'put', 'patch', 'delete']
13
- writeMethods.forEach(method => {
14
- it(`should return "write" for ${method.toUpperCase()}`, () => {
15
- assert.equal(AbstractApiUtils.httpMethodToAction(method), 'write')
16
- assert.equal(AbstractApiUtils.httpMethodToAction(method.toUpperCase()), 'write')
17
- })
18
- })
19
-
20
- it('should return empty string for unknown methods', () => {
21
- assert.equal(AbstractApiUtils.httpMethodToAction('options'), '')
22
- assert.equal(AbstractApiUtils.httpMethodToAction('head'), '')
23
- })
24
- })
25
-
26
- describe('.httpMethodToDBFunction()', () => {
27
- const cases = [
28
- { method: 'post', expected: 'insert' },
29
- { method: 'get', expected: 'find' },
30
- { method: 'put', expected: 'update' },
31
- { method: 'patch', expected: 'update' },
32
- { method: 'delete', expected: 'delete' }
33
- ]
34
- cases.forEach(({ method, expected }) => {
35
- it(`should return "${expected}" for ${method.toUpperCase()}`, () => {
36
- assert.equal(AbstractApiUtils.httpMethodToDBFunction(method), expected)
37
- })
38
- })
39
-
40
- it('should be case-insensitive', () => {
41
- assert.equal(AbstractApiUtils.httpMethodToDBFunction('POST'), 'insert')
42
- assert.equal(AbstractApiUtils.httpMethodToDBFunction('Get'), 'find')
43
- })
44
-
45
- it('should return empty string for unknown methods', () => {
46
- assert.equal(AbstractApiUtils.httpMethodToDBFunction('options'), '')
47
- })
48
- })
49
-
50
- describe('.argsFromReq()', () => {
51
- const baseApiData = {
52
- query: { _id: '123' },
53
- data: { name: 'test' },
54
- schemaName: 'testSchema',
55
- collectionName: 'testCollection'
56
- }
57
-
58
- it('should return [query, opts] for GET', () => {
59
- const req = { method: 'GET', apiData: baseApiData }
60
- const result = AbstractApiUtils.argsFromReq(req)
61
- assert.deepEqual(result[0], baseApiData.query)
62
- assert.deepEqual(result[1], { schemaName: 'testSchema', collectionName: 'testCollection' })
63
- assert.equal(result.length, 2)
64
- })
65
-
66
- it('should return [query, opts] for DELETE', () => {
67
- const req = { method: 'DELETE', apiData: baseApiData }
68
- const result = AbstractApiUtils.argsFromReq(req)
69
- assert.deepEqual(result[0], baseApiData.query)
70
- assert.equal(result.length, 2)
71
- })
72
-
73
- it('should return [data, opts] for POST', () => {
74
- const req = { method: 'POST', apiData: baseApiData }
75
- const result = AbstractApiUtils.argsFromReq(req)
76
- assert.deepEqual(result[0], baseApiData.data)
77
- assert.equal(result.length, 2)
78
- })
79
-
80
- it('should return [query, data, opts] for PUT', () => {
81
- const req = { method: 'PUT', apiData: baseApiData }
82
- const result = AbstractApiUtils.argsFromReq(req)
83
- assert.deepEqual(result[0], baseApiData.query)
84
- assert.deepEqual(result[1], baseApiData.data)
85
- assert.equal(result.length, 3)
86
- })
87
-
88
- it('should return [query, data, opts] for PATCH', () => {
89
- const req = { method: 'PATCH', apiData: baseApiData }
90
- const result = AbstractApiUtils.argsFromReq(req)
91
- assert.deepEqual(result[0], baseApiData.query)
92
- assert.deepEqual(result[1], baseApiData.data)
93
- assert.equal(result.length, 3)
94
- })
95
-
96
- it('should return undefined for unknown methods', () => {
97
- const req = { method: 'OPTIONS', apiData: baseApiData }
98
- assert.equal(AbstractApiUtils.argsFromReq(req), undefined)
99
- })
100
- })
101
-
102
- describe('.stringifyValues()', () => {
103
- it('should pass through plain values unchanged', () => {
104
- const data = { a: 'hello', b: 42, c: true, d: null }
105
- const result = AbstractApiUtils.stringifyValues(data)
106
- assert.deepEqual(result, data)
107
- })
108
-
109
- it('should convert Date values to strings', () => {
110
- const date = new Date('2025-01-01T00:00:00.000Z')
111
- const result = AbstractApiUtils.stringifyValues({ date })
112
- assert.equal(typeof result.date, 'string')
113
- assert.equal(result.date, date.toString())
114
- })
115
-
116
- it('should recursively process nested objects', () => {
117
- const data = { nested: { value: 'test', date: new Date('2025-01-01') } }
118
- const result = AbstractApiUtils.stringifyValues(data)
119
- assert.equal(typeof result.nested, 'object')
120
- assert.equal(typeof result.nested.date, 'string')
121
- })
122
-
123
- it('should recursively process arrays', () => {
124
- const date = new Date('2025-01-01')
125
- const data = { items: [date, 'text', 42] }
126
- const result = AbstractApiUtils.stringifyValues(data)
127
- assert.ok(Array.isArray(result.items))
128
- assert.equal(typeof result.items[0], 'string')
129
- assert.equal(result.items[1], 'text')
130
- assert.equal(result.items[2], 42)
131
- })
132
-
133
- it('should return an array when input is an array', () => {
134
- const result = AbstractApiUtils.stringifyValues([{ a: 1 }, { b: 2 }])
135
- assert.ok(Array.isArray(result))
136
- assert.equal(result.length, 2)
137
- })
138
- })
139
- })