@vida-global/core 2.1.1 → 2.2.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 +2 -6
- package/lib/activeRecord/baseRecord.js +151 -41
- package/lib/activeRecord/db/migrator.js +14 -0
- package/lib/activeRecord/db/queryInterface.js +1 -1
- package/lib/cache/memoryCache.js +31 -7
- package/lib/server/controllerMixins/renderer.js +51 -33
- package/lib/server/doc/renderer.md +1 -1
- package/lib/server/serverController.js +2 -1
- package/package.json +1 -1
- package/test/activeRecord/baseRecord.test.js +250 -9
- package/test/activeRecord/db/migrator.test.js +21 -0
- package/test/activeRecord/db/queryInterface.test.js +4 -2
- package/test/activeRecord/helpers/baseRecordMocks.js +23 -4
- package/test/cache/memoryCache.test.js +76 -0
- package/test/server/controllerMixins/callbacks.test.js +6 -5
- package/test/server/controllerMixins/renderer.test.js +74 -14
- package/test/server/helpers/response.js +19 -0
- package/test/server/serverController.test.js +8 -7
|
@@ -98,7 +98,7 @@ module.exports = {
|
|
|
98
98
|
Available migration helpers:
|
|
99
99
|
|
|
100
100
|
```js
|
|
101
|
-
createTable(tableName, details, { timestamps })
|
|
101
|
+
createTable(tableName, details, { timestamps, paranoid })
|
|
102
102
|
dropTable(tableName)
|
|
103
103
|
addColumn(tableName, columnName, columnDetails)
|
|
104
104
|
removeColumn(tableName, columnName)
|
|
@@ -111,6 +111,7 @@ changeColumn(tableName, columnName, dataTypeOrOptions)
|
|
|
111
111
|
**Conventions:**
|
|
112
112
|
|
|
113
113
|
- `createTable` adds an auto-increment `id` primary key and `created_at` / `updated_at` columns by default. Pass `{ timestamps: false }` to skip the timestamps.
|
|
114
|
+
- Pass `{ paranoid: true }` to add a nullable `deleted_at` column. Models over that table soft delete: `initializationOptions` turns on `paranoid` for any table whose schema has `deleted_at`, so `destroy()` sets the column and ordinary queries skip the row. Unique indexes on a paranoid table should be partial — `where: { deleted_at: null }` — or a deleted row will still block re-creating the same one.
|
|
114
115
|
- Column names are automatically converted to snake_case, so `teamId` in the details object becomes `team_id` in the database.
|
|
115
116
|
- Each migration runs inside a transaction.
|
|
116
117
|
|
|
@@ -307,8 +308,3 @@ class User extends ActiveRecord.BaseRecord {
|
|
|
307
308
|
```
|
|
308
309
|
|
|
309
310
|
See [Sequelize validators](https://sequelize.org/docs/v6/core-concepts/validations-and-constraints/).
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
## TODO
|
|
313
|
-
|
|
314
|
-
- **Paranoid (soft delete).** The infrastructure for a `deleted_at` column is wired in `initializationOptions`, but `paranoid: true` is not set, so soft deletes are not yet usable out of the box.
|
|
@@ -33,21 +33,74 @@ class BaseRecord extends Model {
|
|
|
33
33
|
/***********************************************************************************************
|
|
34
34
|
* QUERIES
|
|
35
35
|
***********************************************************************************************/
|
|
36
|
-
static async find(ids) {
|
|
36
|
+
static async find(ids, { useWriteDB=false, ...options }={}) {
|
|
37
37
|
if (this.isCacheable) {
|
|
38
|
-
return await this._cachedFind(
|
|
38
|
+
return await this._cachedFind(ids, options);
|
|
39
39
|
} else if (Array.isArray(ids)) {
|
|
40
40
|
const pk = this.primaryKeyAttribute
|
|
41
|
-
return await this.where({[pk]: ids});
|
|
41
|
+
return await this.where({[pk]: ids}, { useMaster: useWriteDB });
|
|
42
42
|
} else {
|
|
43
|
-
return await this.findByPk(ids);
|
|
43
|
+
return await this.findByPk(ids, { useMaster: useWriteDB });
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
static async where(where, options={}) {
|
|
49
|
-
const
|
|
50
|
-
|
|
49
|
+
const { page, pageSize, ...queryOptions } = options;
|
|
50
|
+
|
|
51
|
+
if (!page && !pageSize) {
|
|
52
|
+
const parameters = {...queryOptions, where };
|
|
53
|
+
return await this.findAll(parameters);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const offset = pageSize * (page - 1);
|
|
57
|
+
const rows = await this.findAll({
|
|
58
|
+
...queryOptions,
|
|
59
|
+
where,
|
|
60
|
+
order: queryOptions.order || [this.defaultSort()],
|
|
61
|
+
limit: pageSize,
|
|
62
|
+
offset,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const totalCount = await this.paginatedTotalCount(rows, where,
|
|
66
|
+
{
|
|
67
|
+
offset,
|
|
68
|
+
pageSize,
|
|
69
|
+
queryOptions,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return { rows, totalCount }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
static async transaction(callback) {
|
|
77
|
+
return await this.connection.queryInterface.transaction(callback);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
static defaultSort() {
|
|
82
|
+
return ['created_at', 'DESC'];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
static async paginatedTotalCount(rows, where, { offset, pageSize, queryOptions }) {
|
|
87
|
+
const isFullPage = rows.length === pageSize
|
|
88
|
+
const isEmptyPageAfterFirst = rows.length === 0 && offset > 0
|
|
89
|
+
|
|
90
|
+
if (isFullPage || isEmptyPageAfterFirst) {
|
|
91
|
+
return await this.count({...queryOptions, where })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return offset + rows.length
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
static async execute(query, { raw=false, ...options }={}) {
|
|
99
|
+
return await this.sequelize.query(query, {
|
|
100
|
+
type: Sequelize.QueryTypes.SELECT,
|
|
101
|
+
...(raw ? {} : { model: this, mapToModel: true }),
|
|
102
|
+
...options
|
|
103
|
+
});
|
|
51
104
|
}
|
|
52
105
|
|
|
53
106
|
|
|
@@ -78,10 +131,16 @@ class BaseRecord extends Model {
|
|
|
78
131
|
updatedAt: 'updated_at',
|
|
79
132
|
}
|
|
80
133
|
|
|
81
|
-
|
|
134
|
+
const hasTimestamps = schema.created_at && schema.updated_at;
|
|
135
|
+
|
|
136
|
+
if (!hasTimestamps) {
|
|
82
137
|
options.timestamps = false;
|
|
83
138
|
}
|
|
84
139
|
|
|
140
|
+
if (hasTimestamps && schema.deleted_at) {
|
|
141
|
+
options.paranoid = true;
|
|
142
|
+
}
|
|
143
|
+
|
|
85
144
|
this.configureOverridenAccessors(schema);
|
|
86
145
|
this.configureVirtualAccessors(schema);
|
|
87
146
|
|
|
@@ -278,30 +337,39 @@ class BaseRecord extends Model {
|
|
|
278
337
|
}
|
|
279
338
|
|
|
280
339
|
|
|
281
|
-
static
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
marshalledValue = JSON.stringify(value.toJSON());
|
|
285
|
-
marshalledValue = `${this.recordCachingPrefix}:${marshalledValue}`;
|
|
286
|
-
} else {
|
|
287
|
-
marshalledValue = JSON.stringify(value);
|
|
288
|
-
}
|
|
289
|
-
return marshalledValue;
|
|
340
|
+
static async clearCacheKey(key) {
|
|
341
|
+
const client = await this._getRedisClient();
|
|
342
|
+
await client.del(key);
|
|
290
343
|
}
|
|
291
344
|
|
|
292
345
|
|
|
293
|
-
static
|
|
294
|
-
|
|
295
|
-
|
|
346
|
+
static async forEachCacheKeyByPattern(pattern, callback, count=100) {
|
|
347
|
+
const client = await this._getRedisClient();
|
|
348
|
+
let batch = [];
|
|
296
349
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return JSON.parse(value);
|
|
350
|
+
for await (const scanned of client.scanIterator({ MATCH: pattern, COUNT: count })) {
|
|
351
|
+
batch.push(...this._scannedKeysAsArray(scanned));
|
|
352
|
+
if (batch.length < count) continue;
|
|
353
|
+
|
|
354
|
+
await callback(batch, client);
|
|
355
|
+
batch = [];
|
|
304
356
|
}
|
|
357
|
+
|
|
358
|
+
if (batch.length) await callback(batch, client);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
// redis 4 yields one key at a time; redis 5 yields an array of keys per scan.
|
|
363
|
+
static _scannedKeysAsArray(scanned) {
|
|
364
|
+
if (Array.isArray(scanned)) return scanned;
|
|
365
|
+
return [scanned];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
static async clearCacheKeysByPattern(pattern) {
|
|
370
|
+
await this.forEachCacheKeyByPattern(pattern, async (keys, client) => {
|
|
371
|
+
await client.del(keys);
|
|
372
|
+
});
|
|
305
373
|
}
|
|
306
374
|
|
|
307
375
|
|
|
@@ -310,9 +378,8 @@ class BaseRecord extends Model {
|
|
|
310
378
|
|
|
311
379
|
const pk = this.constructor.primaryKeyAttribute;
|
|
312
380
|
const key = this.constructor._recordCacheKey(this[pk]);
|
|
313
|
-
|
|
381
|
+
await this.constructor.clearCacheKey(key);
|
|
314
382
|
|
|
315
|
-
await client.del(key);
|
|
316
383
|
this.constructor.debugLog('Cache Del', key);
|
|
317
384
|
}
|
|
318
385
|
|
|
@@ -330,18 +397,6 @@ class BaseRecord extends Model {
|
|
|
330
397
|
}
|
|
331
398
|
|
|
332
399
|
|
|
333
|
-
static get redisClientFactory() {
|
|
334
|
-
return redisClientFactory;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
static async _getRedisClient() {
|
|
339
|
-
const client = this.redisClientFactory();
|
|
340
|
-
await client.connect();
|
|
341
|
-
return client;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
400
|
static get isCacheable() { return false; }
|
|
346
401
|
|
|
347
402
|
|
|
@@ -357,6 +412,45 @@ class BaseRecord extends Model {
|
|
|
357
412
|
}
|
|
358
413
|
|
|
359
414
|
|
|
415
|
+
static _marshallDataForCaching(value) {
|
|
416
|
+
let marshalledValue;
|
|
417
|
+
if (value instanceof this) {
|
|
418
|
+
marshalledValue = JSON.stringify(value.toJSON());
|
|
419
|
+
marshalledValue = `${this.recordCachingPrefix}:${marshalledValue}`;
|
|
420
|
+
} else {
|
|
421
|
+
marshalledValue = JSON.stringify(value);
|
|
422
|
+
}
|
|
423
|
+
return marshalledValue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
static _unmarshallCachedData(value) {
|
|
428
|
+
if (value == null) return null;
|
|
429
|
+
if (typeof value != 'string') return value;
|
|
430
|
+
|
|
431
|
+
if (value.startsWith(this.recordCachingPrefix)) {
|
|
432
|
+
const regExp = new RegExp(`^${this.recordCachingPrefix}:`);
|
|
433
|
+
value = value.replace(regExp, '');
|
|
434
|
+
const data = JSON.parse(value);
|
|
435
|
+
return new this(data, {isNewRecord: false});
|
|
436
|
+
} else {
|
|
437
|
+
return JSON.parse(value);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
static get _redisClientFactory() {
|
|
443
|
+
return redisClientFactory;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
static async _getRedisClient() {
|
|
448
|
+
const client = this._redisClientFactory();
|
|
449
|
+
await client.connect();
|
|
450
|
+
return client;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
360
454
|
/***********************************************************************************************
|
|
361
455
|
* MISC
|
|
362
456
|
***********************************************************************************************/
|
|
@@ -376,17 +470,33 @@ class BaseRecord extends Model {
|
|
|
376
470
|
toApiResponse() {
|
|
377
471
|
const response = {};
|
|
378
472
|
for (const column of this.apiResponseColumns) {
|
|
379
|
-
response[column] = this[column]; // Use overridden getters when available
|
|
473
|
+
response[this.apiResponseKey(column)] = this[column]; // Use overridden getters when available
|
|
380
474
|
}
|
|
381
475
|
return response;
|
|
382
476
|
}
|
|
383
477
|
|
|
384
478
|
|
|
479
|
+
toObject() {
|
|
480
|
+
return this.toApiResponse();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
385
484
|
get apiResponseColumns() {
|
|
386
485
|
return Object.keys(this.constructor.documentationSchema());
|
|
387
486
|
}
|
|
388
487
|
|
|
389
488
|
|
|
489
|
+
apiResponseKey(column) {
|
|
490
|
+
if (!this.camelizeFieldsForApiResponse) return column;
|
|
491
|
+
return camelize(column, true);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
get camelizeFieldsForApiResponse() {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
|
|
390
500
|
/***********************************************************************************************
|
|
391
501
|
* DOCUMENTATION
|
|
392
502
|
***********************************************************************************************/
|
|
@@ -72,6 +72,9 @@ class Migrator {
|
|
|
72
72
|
if (options.timestamps !== false) {
|
|
73
73
|
details = {...details, ...this.defaultTimestampColumns};
|
|
74
74
|
}
|
|
75
|
+
if (options.paranoid) {
|
|
76
|
+
details = {...details, ...this.deletedAtColumn};
|
|
77
|
+
}
|
|
75
78
|
|
|
76
79
|
details = this.normalizeTableDetails(details);
|
|
77
80
|
await this.#sequelizeQueryInterface.createTable(tableName, details);
|
|
@@ -113,6 +116,17 @@ class Migrator {
|
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
|
|
119
|
+
// Records use soft deletes when the table has this column: initializationOptions turns paranoid
|
|
120
|
+
// on for any model whose schema includes it.
|
|
121
|
+
get deletedAtColumn() {
|
|
122
|
+
return {
|
|
123
|
+
deleted_at: {
|
|
124
|
+
allowNull: true,
|
|
125
|
+
type: this.DataTypes.DATE }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
116
130
|
async dropTable(tableName) {
|
|
117
131
|
await this.#sequelizeQueryInterface.dropTable(tableName);
|
|
118
132
|
}
|
package/lib/cache/memoryCache.js
CHANGED
|
@@ -14,6 +14,16 @@ class MemoryCache {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
async delete({ namespace, key } = {}) {
|
|
18
|
+
this.#validateNamespace(namespace);
|
|
19
|
+
|
|
20
|
+
const cacheKey = this.#createCacheKey({ namespace, key });
|
|
21
|
+
const entryDeleted = this.#entries.delete(cacheKey);
|
|
22
|
+
const fetchInvalidated = this.#removePendingFetch(cacheKey);
|
|
23
|
+
return entryDeleted || fetchInvalidated;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
17
27
|
async fetch(options = {}, fetcher) {
|
|
18
28
|
this.#validateNamespace(options.namespace);
|
|
19
29
|
this.#validateTtl(options.ttlSeconds);
|
|
@@ -26,13 +36,15 @@ class MemoryCache {
|
|
|
26
36
|
const pending = this.#pendingFetches.get(cacheKey);
|
|
27
37
|
if (pending) return this.#buildFetchResult(await pending, false);
|
|
28
38
|
|
|
29
|
-
const fetch = this.#
|
|
39
|
+
const fetch = this.#runFetcher(fetcher);
|
|
30
40
|
this.#pendingFetches.set(cacheKey, fetch);
|
|
31
41
|
|
|
32
42
|
try {
|
|
33
|
-
|
|
43
|
+
const value = await fetch;
|
|
44
|
+
this.#cacheFetchedValue(cacheKey, options.ttlSeconds, fetch, value);
|
|
45
|
+
return this.#buildFetchResult(value, false);
|
|
34
46
|
} finally {
|
|
35
|
-
this.#
|
|
47
|
+
this.#removePendingFetch(cacheKey, fetch);
|
|
36
48
|
}
|
|
37
49
|
}
|
|
38
50
|
|
|
@@ -46,10 +58,22 @@ class MemoryCache {
|
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
|
|
49
|
-
async #
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
async #runFetcher(fetcher) {
|
|
62
|
+
return fetcher();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
#cacheFetchedValue(cacheKey, ttlSeconds, fetch, value) {
|
|
67
|
+
if (this.#pendingFetches.get(cacheKey) !== fetch) return;
|
|
68
|
+
this.#set(cacheKey, ttlSeconds, value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
#removePendingFetch(cacheKey, expectedFetch) {
|
|
73
|
+
const currentFetch = this.#pendingFetches.get(cacheKey);
|
|
74
|
+
if (expectedFetch !== undefined && currentFetch !== expectedFetch) return false;
|
|
75
|
+
|
|
76
|
+
return this.#pendingFetches.delete(cacheKey);
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
|
|
@@ -51,6 +51,9 @@ function setSseHeaders(controller) {
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
const InstanceMethods = {
|
|
54
|
+
MIME_TYPES: CONTENT_TYPE_BY_EXTENSION,
|
|
55
|
+
|
|
56
|
+
|
|
54
57
|
async render(body, options={}) {
|
|
55
58
|
if (this.rendered) return;
|
|
56
59
|
if (this.isStreaming) return;
|
|
@@ -76,21 +79,21 @@ const InstanceMethods = {
|
|
|
76
79
|
|
|
77
80
|
|
|
78
81
|
renderJSONResponse(body) {
|
|
79
|
-
this.
|
|
82
|
+
if (!this.responseContentType) this.responseContentType = CONTENT_TYPE_BY_EXTENSION.json;
|
|
80
83
|
this._response.json(body);
|
|
81
84
|
},
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
renderXMLResponse(body) {
|
|
85
|
-
this.
|
|
86
|
-
const doc = create({
|
|
88
|
+
if (!this.responseContentType) this.responseContentType = CONTENT_TYPE_BY_EXTENSION.xml;
|
|
89
|
+
const doc = create({ [this.xmlRootElement]: body });
|
|
87
90
|
const xml = doc.end();
|
|
88
91
|
this._response.send(xml);
|
|
89
92
|
},
|
|
90
93
|
|
|
91
94
|
|
|
92
95
|
renderTextResponse(body) {
|
|
93
|
-
this.
|
|
96
|
+
if (!this.responseContentType) this.responseContentType = CONTENT_TYPE_BY_EXTENSION.txt;
|
|
94
97
|
this._response.send(body);
|
|
95
98
|
},
|
|
96
99
|
|
|
@@ -163,7 +166,7 @@ const InstanceMethods = {
|
|
|
163
166
|
|
|
164
167
|
|
|
165
168
|
_serializeDateForResponseBody(date, options) {
|
|
166
|
-
return date.getTime();
|
|
169
|
+
return Math.floor(date.getTime() / 1000);
|
|
167
170
|
},
|
|
168
171
|
|
|
169
172
|
|
|
@@ -186,33 +189,6 @@ const InstanceMethods = {
|
|
|
186
189
|
},
|
|
187
190
|
|
|
188
191
|
|
|
189
|
-
_setupStatusRenderers() {
|
|
190
|
-
const successStatuses = {
|
|
191
|
-
CreationSuccessful: 201,
|
|
192
|
-
Accepted: 202,
|
|
193
|
-
NoConent: 204,
|
|
194
|
-
MovedPermanently: 301,
|
|
195
|
-
Found: 302,
|
|
196
|
-
NotModified: 304,
|
|
197
|
-
TemporaryRedirect: 307,
|
|
198
|
-
PermanentRedirect: 308,
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
for (let [type, statusCode] of Object.entries(Errors.Statuses)) {
|
|
202
|
-
if (type == 'Server') type = 'ServerError';
|
|
203
|
-
this[`render${type}`] = async function(message=null, body=null) {
|
|
204
|
-
return await this._renderError(statusCode, message, body);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
for (let [type, statusCode] of Object.entries(successStatuses)) {
|
|
209
|
-
this[`render${type}`] = async function(body=null) {
|
|
210
|
-
return await this._renderSuccess(statusCode, body);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
|
|
216
192
|
async _renderError(statusCode, message, body) {
|
|
217
193
|
this.statusCode = statusCode;
|
|
218
194
|
body = structuredClone(body || {});
|
|
@@ -305,7 +281,37 @@ const InstanceMethods = {
|
|
|
305
281
|
}
|
|
306
282
|
|
|
307
283
|
|
|
284
|
+
const StatusRenderSuccessStatuses = {
|
|
285
|
+
CreationSuccessful: 201,
|
|
286
|
+
Accepted: 202,
|
|
287
|
+
NoContent: 204,
|
|
288
|
+
MovedPermanently: 301,
|
|
289
|
+
Found: 302,
|
|
290
|
+
NotModified: 304,
|
|
291
|
+
TemporaryRedirect: 307,
|
|
292
|
+
PermanentRedirect: 308,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
308
296
|
const StaticMethods = {
|
|
297
|
+
// Define render<Status> helpers on the prototype (not per instance) so subclasses and mixins
|
|
298
|
+
// can override them with a normal method of the same name.
|
|
299
|
+
setupStatusRenderers() {
|
|
300
|
+
for (let [type, statusCode] of Object.entries(Errors.Statuses)) {
|
|
301
|
+
if (type == 'Server') type = 'ServerError';
|
|
302
|
+
this.prototype[`render${type}`] = async function(message=null, body=null) {
|
|
303
|
+
return await this._renderError(statusCode, message, body);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const [type, statusCode] of Object.entries(StatusRenderSuccessStatuses)) {
|
|
308
|
+
this.prototype[`render${type}`] = async function(body=null) {
|
|
309
|
+
return await this._renderSuccess(statusCode, body);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
|
|
309
315
|
formatResponseBody(body, errors, { statusCode, statusText }) {
|
|
310
316
|
const response = {data: body, status: statusText};
|
|
311
317
|
if (errors) response.errors = errors;
|
|
@@ -325,7 +331,6 @@ const Accessors = {
|
|
|
325
331
|
get() {
|
|
326
332
|
const override = `${this.params._format || ''}`.toLowerCase();
|
|
327
333
|
if (override === 'xml' || override === 'json') return override;
|
|
328
|
-
if (/\bxml\b/i.test(this.requestHeaders.accept || '')) return 'xml';
|
|
329
334
|
return 'json';
|
|
330
335
|
}
|
|
331
336
|
},
|
|
@@ -341,6 +346,19 @@ const Accessors = {
|
|
|
341
346
|
},
|
|
342
347
|
|
|
343
348
|
|
|
349
|
+
responseContentType: {
|
|
350
|
+
get() { return this._response.getHeader('content-type') },
|
|
351
|
+
set(type) { return this._response.setHeader('content-type', type) }
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
// Root element wrapped around XML response bodies. Override in a subclass to emit a custom
|
|
356
|
+
// root (e.g. a TwiML `<Response>` for a telecom webhook).
|
|
357
|
+
xmlRootElement: {
|
|
358
|
+
get() { return 'response' }
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
|
|
344
362
|
statusText: {
|
|
345
363
|
get() {
|
|
346
364
|
return STATUS_TEXTS[this.statusCode];
|
|
@@ -47,7 +47,7 @@ Don't set `this.statusCode` directly — use the helper that pairs the status wi
|
|
|
47
47
|
|---|---|
|
|
48
48
|
| 201 Created | `this.renderCreationSuccessful(body)` |
|
|
49
49
|
| 202 Accepted | `this.renderAccepted(body)` |
|
|
50
|
-
| 204 No Content | `this.
|
|
50
|
+
| 204 No Content | `this.renderNoContent(body)` |
|
|
51
51
|
| 301 Moved Permanently | `this.renderMovedPermanently(body)` |
|
|
52
52
|
| 302 Found | `this.renderFound(body)` |
|
|
53
53
|
| 304 Not Modified | `this.renderNotModified(body)` |
|
|
@@ -23,7 +23,6 @@ class VidaServerController {
|
|
|
23
23
|
|
|
24
24
|
this._applyCallbacks();
|
|
25
25
|
this._applyDocumentation();
|
|
26
|
-
this._setupStatusRenderers();
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
|
|
@@ -260,6 +259,8 @@ VidaServerController.loadMixin(RendererMixin);
|
|
|
260
259
|
VidaServerController.loadMixin(RoutingMixin);
|
|
261
260
|
VidaServerController.loadMixin(ValidationMixin);
|
|
262
261
|
|
|
262
|
+
VidaServerController.setupStatusRenderers();
|
|
263
|
+
|
|
263
264
|
|
|
264
265
|
/***************************************************************************************************
|
|
265
266
|
* ERRORS
|