@vida-global/core 2.0.11 → 2.1.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.
- package/lib/activeRecord/README.md +1 -1
- package/lib/activeRecord/baseRecord.js +70 -4
- package/lib/server/README.md +8 -219
- package/lib/server/controllerImporter.js +0 -1
- package/lib/server/controllerMixins/callbacks.js +16 -61
- package/lib/server/controllerMixins/classScopedRegistry.js +48 -0
- package/lib/server/controllerMixins/documentation.js +41 -9
- package/lib/server/controllerMixins/renderer.js +140 -99
- package/lib/server/controllerMixins/validations.js +57 -0
- package/lib/server/doc/callbacks.md +23 -0
- package/lib/server/doc/documentation.md +135 -0
- package/lib/server/doc/renderer.md +115 -0
- package/lib/server/doc/requestDetails.md +52 -0
- package/lib/server/doc/validations.md +40 -0
- package/lib/server/index.js +1 -1
- package/lib/server/openApi/apiDocGenerator.js +426 -0
- package/lib/server/openApi/apiDocsGenerator.js +147 -0
- package/lib/server/openApi/schemaImporter.js +41 -0
- package/lib/server/openApi/schemaRegistry.js +43 -0
- package/lib/server/openApi/schemas.js +97 -0
- package/lib/server/openApi/tagRegistry.js +27 -0
- package/lib/server/server.js +14 -0
- package/lib/server/serverController.js +4 -9
- package/lib/server/statusTexts.js +38 -0
- package/lib/utils/yamlLoader.js +17 -0
- package/package.json +4 -2
- package/test/activeRecord/baseRecord.test.js +101 -0
- package/test/server/apiDocGenerator.test.js +743 -0
- package/test/server/controllerMixins/callbacks.test.js +326 -0
- package/test/server/controllerMixins/classScopedRegistry.test.js +95 -0
- package/test/server/controllerMixins/documentation.test.js +168 -0
- package/test/server/controllerMixins/renderer.test.js +734 -0
- package/test/server/controllerMixins/requestDetails.test.js +225 -0
- package/test/server/controllerMixins/routing.test.js +82 -0
- package/test/server/controllerMixins/validations.test.js +509 -0
- package/test/server/openApi/apiDocsGenerator.test.js +306 -0
- package/test/server/openApi/helpers/apiDocsPackageFixture/package.json +5 -0
- package/test/server/openApi/schemaImporter.test.js +54 -0
- package/test/server/openApi/schemaRegistry.test.js +93 -0
- package/test/server/openApi/schemas.test.js +109 -0
- package/test/server/serverController.test.js +8 -867
- package/test/server/statusTexts.test.js +30 -0
- package/test/utils/yamlLoader.test.js +43 -0
- package/lib/server/apiDocsGenerator.js +0 -86
- package/test/server/apiDocsGenerator.test.js +0 -38
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { AbstractAutoImporter } = require('../../utils/abstractAutoImporter');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SchemaImporter extends AbstractAutoImporter {
|
|
5
|
+
#schemaClasses;
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
get schemaClasses() {
|
|
9
|
+
if (!this.#schemaClasses) this.#buildMap();
|
|
10
|
+
return this.#schemaClasses;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
shouldImport(obj) {
|
|
15
|
+
return typeof obj?.documentationSchema === 'function';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
#buildMap() {
|
|
20
|
+
this.#schemaClasses = new Map();
|
|
21
|
+
for (const cls of this.imports) {
|
|
22
|
+
this.#addClass(cls);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
#addClass(cls) {
|
|
28
|
+
const name = cls.documentationSchemaName || cls.name;
|
|
29
|
+
const existing = this.#schemaClasses.get(name);
|
|
30
|
+
|
|
31
|
+
if (existing == cls) return;
|
|
32
|
+
if (this.#schemaClasses.has(name)) {
|
|
33
|
+
throw new Error(`Duplicate documentationSchema name: ${name}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.#schemaClasses.set(name, cls);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
module.exports = { SchemaImporter };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { SchemaImporter } = require('./schemaImporter');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SchemaRegistry {
|
|
5
|
+
#schemas = {};
|
|
6
|
+
#classes;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
constructor(server = {}) {
|
|
10
|
+
const importer = new SchemaImporter(server.schemaDirectories || []);
|
|
11
|
+
this.#classes = importer.schemaClasses;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
resolve(name) {
|
|
16
|
+
const cls = this.#classes.get(name);
|
|
17
|
+
if (!cls) throw new Error(`Unknown schema ref: ${name}`);
|
|
18
|
+
return cls;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
has(name) {
|
|
23
|
+
return Object.prototype.hasOwnProperty.call(this.#schemas, name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
reserve(name) {
|
|
28
|
+
this.#schemas[name] = {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
set(name, schema) {
|
|
33
|
+
this.#schemas[name] = schema;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
get schemas() {
|
|
38
|
+
return this.#schemas;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
module.exports = { SchemaRegistry };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
class Schemas {
|
|
2
|
+
static for(details, resolveRef) {
|
|
3
|
+
if (resolveRef && details.ref !== undefined) return resolveRef(details.ref);
|
|
4
|
+
if (details.isInteger !== undefined) return this.integerSchema(details.isInteger);
|
|
5
|
+
if (details.isNumber !== undefined) return this.numberSchema(details.isNumber);
|
|
6
|
+
if (details.isString !== undefined) return this.stringSchema(details.isString);
|
|
7
|
+
if (details.isBoolean !== undefined) return this.booleanSchema();
|
|
8
|
+
if (details.isDateTime !== undefined) return this.dateTimeSchema();
|
|
9
|
+
if (details.isEnum !== undefined) return this.enumSchema(details.isEnum);
|
|
10
|
+
if (details.isArray !== undefined) return this.arraySchema(details.isArray, resolveRef);
|
|
11
|
+
if (details.isObject !== undefined) return this.objectSchema(details.isObject, resolveRef);
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
static schemaFor(details, resolveRef) {
|
|
17
|
+
return this.for(details, resolveRef);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
static isRequired(details) {
|
|
22
|
+
return details.optional !== true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
static integerSchema(options) {
|
|
27
|
+
const schema = { type: 'integer' };
|
|
28
|
+
if (options?.gte !== undefined) schema.minimum = options.gte;
|
|
29
|
+
if (options?.lte !== undefined) schema.maximum = options.lte;
|
|
30
|
+
return schema;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
static numberSchema(options) {
|
|
35
|
+
const schema = { type: 'number' };
|
|
36
|
+
if (options?.gte !== undefined) schema.minimum = options.gte;
|
|
37
|
+
if (options?.lte !== undefined) schema.maximum = options.lte;
|
|
38
|
+
return schema;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
static booleanSchema() {
|
|
43
|
+
return { type: 'boolean' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
static dateTimeSchema() {
|
|
48
|
+
return { type: 'string', format: 'date-time' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
static arraySchema(options, resolveRef) {
|
|
53
|
+
const schema = { type: 'array', items: options?.of ? this.schemaFor(options.of, resolveRef) : {} };
|
|
54
|
+
if (options?.length?.gte !== undefined) schema.minItems = options.length.gte;
|
|
55
|
+
if (options?.length?.lte !== undefined) schema.maxItems = options.length.lte;
|
|
56
|
+
return schema;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
static objectSchema(options, resolveRef) {
|
|
61
|
+
const properties = {};
|
|
62
|
+
const required = [];
|
|
63
|
+
for (const [name, details] of Object.entries(options?.properties || {})) {
|
|
64
|
+
properties[name] = this.schemaFor(details, resolveRef);
|
|
65
|
+
if (this.isRequired(details)) required.push(name);
|
|
66
|
+
}
|
|
67
|
+
const schema = { type: 'object', properties };
|
|
68
|
+
if (required.length) schema.required = required;
|
|
69
|
+
return schema;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
static stringSchema(options) {
|
|
74
|
+
const schema = { type: 'string' };
|
|
75
|
+
if (options?.length?.gte !== undefined) schema.minLength = options.length.gte;
|
|
76
|
+
if (options?.length?.lte !== undefined) schema.maxLength = options.length.lte;
|
|
77
|
+
if (options?.regex) schema.pattern = options.regex.source;
|
|
78
|
+
return schema;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
static enumSchema(options) {
|
|
83
|
+
return { type: this.enumType(options.enums), enum: [...options.enums] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
static enumType(enums) {
|
|
88
|
+
if (!enums.every(value => typeof value === 'number')) return 'string';
|
|
89
|
+
if (enums.every(value => Number.isInteger(value))) return 'integer';
|
|
90
|
+
return 'number';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
Schemas
|
|
97
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class TagRegistry {
|
|
2
|
+
#tags = new Map();
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
register(name, description = '') {
|
|
6
|
+
if (!this.#tags.has(name)) {
|
|
7
|
+
this.#tags.set(name, description);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (!this.#tags.get(name) && description) this.#tags.set(name, description);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
get tags() {
|
|
15
|
+
return [...this.#tags].map(([name, description]) => this.#tag(name, description));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
#tag(name, description) {
|
|
20
|
+
const tag = { name };
|
|
21
|
+
if (description) tag.description = description;
|
|
22
|
+
return tag;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
module.exports = { TagRegistry };
|
package/lib/server/server.js
CHANGED
|
@@ -42,6 +42,7 @@ class VidaServer {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
get origin() { return `https://${this.host}` }
|
|
45
46
|
get host() { return this.#host; }
|
|
46
47
|
get port() { return this.#port; }
|
|
47
48
|
get logger() { return logger.server; }
|
|
@@ -240,6 +241,11 @@ class VidaServer {
|
|
|
240
241
|
}
|
|
241
242
|
|
|
242
243
|
|
|
244
|
+
get schemaDirectories() {
|
|
245
|
+
return [`${process.cwd()}/models`];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
243
249
|
get serverEngineFactory() {
|
|
244
250
|
return express;
|
|
245
251
|
}
|
|
@@ -270,6 +276,14 @@ class VidaServer {
|
|
|
270
276
|
}
|
|
271
277
|
|
|
272
278
|
|
|
279
|
+
/***********************************************************************************************
|
|
280
|
+
* DOCUMENTATION
|
|
281
|
+
***********************************************************************************************/
|
|
282
|
+
get securitySchemes() {
|
|
283
|
+
return { apiKeyAuth: { type: 'apiKey', in: 'header', name: 'Authorization' } };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
273
287
|
/***********************************************************************************************
|
|
274
288
|
* DEPRECATED PASS THROUGH METHODS
|
|
275
289
|
***********************************************************************************************/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const APM = require('../apm');
|
|
2
2
|
const { camelize } = require('inflection');
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
|
-
const Errors
|
|
5
|
-
const { logger }
|
|
4
|
+
const Errors = require('./errors');
|
|
5
|
+
const { logger } = require('../logger');
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class VidaServerController {
|
|
@@ -22,6 +22,7 @@ class VidaServerController {
|
|
|
22
22
|
this.#logger = (serverLogger || logger).child(this.requestId);
|
|
23
23
|
|
|
24
24
|
this._applyCallbacks();
|
|
25
|
+
this._applyDocumentation();
|
|
25
26
|
this._setupStatusRenderers();
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -140,12 +141,6 @@ class VidaServerController {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
|
|
143
|
-
static autoLoadDocumentation() {
|
|
144
|
-
const docs = this._autoLoadModule(this.documentationDirectory);
|
|
145
|
-
Object.assign(this, docs);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
144
|
static _autoLoadModule(path) {
|
|
150
145
|
try {
|
|
151
146
|
return require(path);
|
|
@@ -164,7 +159,7 @@ class VidaServerController {
|
|
|
164
159
|
|
|
165
160
|
|
|
166
161
|
static get documentationDirectory() {
|
|
167
|
-
return `${process.cwd()}/
|
|
162
|
+
return `${process.cwd()}/docs/controllers${this.routePrefix}Controller`;
|
|
168
163
|
}
|
|
169
164
|
|
|
170
165
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const STATUS_TEXTS = {
|
|
2
|
+
200: 'ok',
|
|
3
|
+
201: 'created',
|
|
4
|
+
202: 'accepted',
|
|
5
|
+
204: 'no content',
|
|
6
|
+
301: 'moved permanently',
|
|
7
|
+
302: 'found',
|
|
8
|
+
304: 'not modified',
|
|
9
|
+
307: 'temporary redirect',
|
|
10
|
+
308: 'permanent redirect',
|
|
11
|
+
400: 'bad request',
|
|
12
|
+
401: 'unauthorized',
|
|
13
|
+
402: 'payment required',
|
|
14
|
+
403: 'forbidden',
|
|
15
|
+
404: 'not found',
|
|
16
|
+
405: 'method not allowed',
|
|
17
|
+
408: 'request timeout',
|
|
18
|
+
409: 'conflict',
|
|
19
|
+
410: 'gone',
|
|
20
|
+
413: 'content too large',
|
|
21
|
+
415: 'unsupported media type',
|
|
22
|
+
422: 'unprocessable content',
|
|
23
|
+
423: 'locked',
|
|
24
|
+
429: 'too many requests',
|
|
25
|
+
500: 'server error',
|
|
26
|
+
501: 'not implemented',
|
|
27
|
+
502: 'bad gateway',
|
|
28
|
+
503: 'service unavailable',
|
|
29
|
+
504: 'gateway timeout',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
function statusTextFor(code) {
|
|
34
|
+
return STATUS_TEXTS[code];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
module.exports = { STATUS_TEXTS, statusTextFor };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const yaml = require('js-yaml');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class YamlLoader {
|
|
6
|
+
static loadFile(path) {
|
|
7
|
+
return yaml.load(fs.readFileSync(path, 'utf8')) || {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
static fileExists(path) {
|
|
12
|
+
return fs.existsSync(path);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
module.exports = { YamlLoader };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vida-global/core",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Core libraries for supporting Vida development",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "ISC",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"express": "^4.21.2",
|
|
28
28
|
"express-request-id": "1.4.1",
|
|
29
29
|
"express-winston": "^4.0.0",
|
|
30
|
+
"js-yaml": "^3.14.2",
|
|
30
31
|
"mustache-express": "^1.2.8",
|
|
31
32
|
"newrelic": "^13.0.0",
|
|
32
33
|
"pg": "^8.16.3",
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
"sequelize": "^6.37.7",
|
|
36
37
|
"sequelize-cli": "^6.6.3",
|
|
37
38
|
"socket.io": "^4.4.0",
|
|
38
|
-
"winston": "^3.0.0"
|
|
39
|
+
"winston": "^3.0.0",
|
|
40
|
+
"xmlbuilder2": "^4.0.0"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@vida-global/test-helpers": "^1.0.1",
|
|
@@ -742,4 +742,105 @@ describe('BaseRecord', () => {
|
|
|
742
742
|
});
|
|
743
743
|
});
|
|
744
744
|
});
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
describe('.documentationSchema', () => {
|
|
748
|
+
const buildModel = (attributes) => {
|
|
749
|
+
class Thing extends BaseRecord {}
|
|
750
|
+
jest.spyOn(Thing, 'initialize').mockImplementation(() => {});
|
|
751
|
+
Thing.getAttributes = () => attributes;
|
|
752
|
+
return Thing;
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
it ('builds a properties map from the table columns', () => {
|
|
757
|
+
const Thing = buildModel({
|
|
758
|
+
id: { type: { key: 'INTEGER' } },
|
|
759
|
+
name: { type: { key: 'STRING' } },
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
expect(Thing.documentationSchema()).toEqual({
|
|
763
|
+
id: { type: 'integer' },
|
|
764
|
+
name: { type: 'string' },
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
it ('excludes created_at, updated_at and deleted_at', () => {
|
|
770
|
+
const Thing = buildModel({
|
|
771
|
+
id: { type: { key: 'INTEGER' } },
|
|
772
|
+
created_at: { type: { key: 'DATE' } },
|
|
773
|
+
updated_at: { type: { key: 'DATE' } },
|
|
774
|
+
deleted_at: { type: { key: 'DATE' } },
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
expect(Object.keys(Thing.documentationSchema())).toEqual(['id']);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
it.each([
|
|
782
|
+
['INTEGER', { type: 'integer' }],
|
|
783
|
+
['BIGINT', { type: 'integer' }],
|
|
784
|
+
['DECIMAL', { type: 'number' }],
|
|
785
|
+
['FLOAT', { type: 'number' }],
|
|
786
|
+
['BOOLEAN', { type: 'boolean' }],
|
|
787
|
+
['DATE', { type: 'string', format: 'date-time' }],
|
|
788
|
+
['JSONB', { type: 'object', properties: {} }],
|
|
789
|
+
['ARRAY', { type: 'array', items: {} }],
|
|
790
|
+
['STRING', { type: 'string' }],
|
|
791
|
+
['UUID', { type: 'string' }],
|
|
792
|
+
['GEOMETRY', { type: 'string' }],
|
|
793
|
+
])('maps a %s column to the expected schema', (key, expected) => {
|
|
794
|
+
const Thing = buildModel({ col: { type: { key } } });
|
|
795
|
+
|
|
796
|
+
expect(Thing.documentationSchema().col).toEqual(expected);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
it ('maps an ENUM column to its values', () => {
|
|
801
|
+
const Thing = buildModel({ status: { type: { key: 'ENUM', values: ['on', 'off'] } } });
|
|
802
|
+
|
|
803
|
+
expect(Thing.documentationSchema().status).toEqual({ type: 'string', enum: ['on', 'off'] });
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
describe('#apiResponseColumns', () => {
|
|
809
|
+
it ('returns the column names exposed by documentationSchema', () => {
|
|
810
|
+
class Thing extends BaseRecord {}
|
|
811
|
+
Thing.documentationSchema = () => ({ id: { type: 'integer' }, name: { type: 'string' } });
|
|
812
|
+
const record = Object.create(Thing.prototype);
|
|
813
|
+
|
|
814
|
+
expect(record.apiResponseColumns).toEqual(['id', 'name']);
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
describe('#toApiResponse', () => {
|
|
820
|
+
const buildRecord = (schema, values) => {
|
|
821
|
+
class Thing extends BaseRecord {}
|
|
822
|
+
Thing.documentationSchema = () => schema;
|
|
823
|
+
const record = Object.create(Thing.prototype);
|
|
824
|
+
Object.assign(record, values);
|
|
825
|
+
return record;
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
it ('returns only the columns exposed by documentationSchema', () => {
|
|
830
|
+
const record = buildRecord(
|
|
831
|
+
{ id: { type: 'integer' }, name: { type: 'string' } },
|
|
832
|
+
{ id: 7, name: 'widget', deleted_at: 'now' },
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
expect(record.toApiResponse()).toEqual({ id: 7, name: 'widget' });
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
it ('applies overridden getters when building the response', () => {
|
|
840
|
+
const record = buildRecord({ name: { type: 'string' } }, {});
|
|
841
|
+
Object.defineProperty(record, 'name', { get: () => 'OVERRIDDEN', configurable: true });
|
|
842
|
+
|
|
843
|
+
expect(record.toApiResponse()).toEqual({ name: 'OVERRIDDEN' });
|
|
844
|
+
});
|
|
845
|
+
});
|
|
745
846
|
});
|