@vida-global/core 2.3.2 → 2.3.4

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/AGENTS.md CHANGED
@@ -2,12 +2,53 @@
2
2
 
3
3
  Use this guide to understand and write code for the vida-core repo.
4
4
 
5
+ `@vida-global/core` is the shared library behind all Vida apps. It also ships to
6
+ customers inside `vida-apps-tools`, so the exported surface is a public API.
7
+ Treat a changed or removed export as a breaking change, not a refactor.
8
+
9
+ CommonJS throughout (`require` / `module.exports`). There is no linter in this
10
+ repo — the style guide below is the only enforcement, so follow it closely.
11
+
12
+
5
13
  # Style and conventions
14
+
15
+ Always in force:
16
+
6
17
  @agents/style.md
7
- @agents/server.md
8
- @agents/db.md
9
- @agents/apis.md
10
18
  @agents/testing.md
11
19
 
20
+ Read only when the work touches that area:
21
+
22
+ - `agents/server.md` — Express endpoints, controllers, validation, status codes
23
+ - `agents/db.md` — ActiveRecord, Sequelize, migrations, caching
24
+ - `agents/apis.md` — outbound HTTP clients to third parties
25
+ - `agents/git.md` — branches, commits, GitHub
26
+ - `agents/frontend.md` — Next.js/React only. Not used by this package.
27
+
28
+
29
+ # Commands
30
+
31
+ npm test # jest, coverage on by default
32
+ npm test -- test/server # narrow to a directory
33
+ npm test -- -t 'name' # narrow by test name
34
+ npm run develop # prerelease via @vida-global/release
35
+ npm run release # release via @vida-global/release
36
+
37
+ `npm test` carries required flags (`LOG_LEVEL=test`, `NODE_NO_WARNINGS=1`,
38
+ `node --experimental-vm-modules`). Always go through the script. Calling `jest`
39
+ directly drops them and the run misbehaves.
40
+
41
+ Coverage is collected on every run. `/helpers/` is excluded by design.
42
+
43
+
44
+ # Repository map
45
+
46
+ `index.js` is the whole public surface. `lib/` holds the modules; `test/` mirrors
47
+ `lib/` one directory per module. Per-module guides live next to the code and are
48
+ listed in `README.md`: `activeRecord`, `http`, `logger`, `server`, `jobQueue`,
49
+ plus `cache`, `redis`, `apm`, `observability`, `utils`.
12
50
 
13
- # Helpers for common use cases
51
+ One non-obvious mechanic in `index.js`: server error classes are exported by
52
+ filtering `lib/server` exports on `prototype instanceof AbstractServerError`. A
53
+ new error class that extends it is exported automatically — do not add it to
54
+ `module.exports` by hand. One that does not extend it is silently absent.
@@ -249,7 +249,7 @@ user.fullName; // "Ada Lovelace"
249
249
  Opt a model in to Redis-backed caching by overriding `isCacheable`. With caching on:
250
250
 
251
251
  - `Model.find(id)` and `Model.find([id1, id2])` consult Redis first and fall back to the database for misses, repopulating the cache for next time.
252
- - `afterSave` and `afterDestroy` hooks automatically invalidate the cached record.
252
+ - An `afterCommit` hook automatically invalidates the cached record, so a write that is rolled back never disturbs the cache.
253
253
  - Cache keys are namespaced as `AR:<ClassName>` (`<ClassName>#find:<id>`).
254
254
 
255
255
  ```js
@@ -294,7 +294,29 @@ User.addHook('afterSave', async (user, options) => {
294
294
  });
295
295
  ```
296
296
 
297
- See [Sequelize hooks](https://sequelize.org/docs/v6/other-topics/hooks/) for the full list of hook points. The caching layer above uses `afterSave` and `afterDestroy` under the hood when `isCacheable` is true.
297
+ See [Sequelize hooks](https://sequelize.org/docs/v6/other-topics/hooks/) for the full list of hook points.
298
+
299
+
300
+ ### afterCommit
301
+
302
+ Every hook above runs *inside* the transaction the write belongs to, so its work is undone if that
303
+ transaction rolls back. Use `afterCommit` for anything that must not happen until the data is
304
+ durable — cache invalidation, broadcasts, enqueuing jobs:
305
+
306
+ ```js
307
+ User.addHook('afterCommit', async (user, options) => {
308
+ await broadcast('users.changed', { id: user.id }); // never fires if the transaction rolls back
309
+ });
310
+ ```
311
+
312
+ - Fires once per record written, after the transaction commits. A write outside a transaction fires
313
+ it immediately.
314
+ - Fires on create, update, destroy, restore and upsert. Bulk statements such as
315
+ `Model.destroy({ where })` only fire it when `individualHooks: true` is set, the same as
316
+ `afterDestroy`.
317
+ - With nested transactions, only the outermost commit fires it. A savepoint that commits inside a
318
+ transaction that later rolls back fires nothing.
319
+ - A record written more than once in the same transaction still fires each callback once.
298
320
 
299
321
 
300
322
  ## Associations
@@ -0,0 +1,167 @@
1
+ // Sequelize has no model level afterCommit hook, only Transaction#afterCommit. This module holds
2
+ // the callbacks registered for one and defers them onto the transaction a write belongs to, so
3
+ // BaseRecord can offer afterCommit alongside the hooks sequelize does provide.
4
+
5
+ // Keyed by model class so callbacks stay scoped to the model they were registered on. Module scope
6
+ // is what keeps them unreachable from outside: a static # field is unreadable through a subclass,
7
+ // and a plain static field on BaseRecord would be one collection shared by every model.
8
+ const modelCallbacks = new WeakMap();
9
+ const claimedRegistrations = new WeakMap();
10
+
11
+
12
+ /***************************************************************************************************
13
+ * REGISTRATION
14
+ ***************************************************************************************************/
15
+ function addCallback(model, name, fn) {
16
+ const callback = buildCallback(name, fn);
17
+ ownCallbacks(model).push(callback);
18
+ }
19
+
20
+
21
+ // Sequelize's addHook takes either (name, fn) or (fn), and stores the named form as an object so it
22
+ // can be removed by name later. Match both.
23
+ function buildCallback(name, fn) {
24
+ if (typeof name === 'function') return name;
25
+
26
+ return { name, fn };
27
+ }
28
+
29
+
30
+ function removeCallback(model, name) {
31
+ const remaining = ownCallbacks(model).filter(callback => !callbackMatches(callback, name));
32
+ modelCallbacks.set(model, remaining);
33
+ }
34
+
35
+
36
+ // Only named callbacks are removable by name. An unnamed one carries its function name, which would
37
+ // otherwise match a string by accident.
38
+ function callbackMatches(callback, name) {
39
+ if (typeof name === 'function') return callback === name;
40
+ if (typeof callback === 'object') return callback.name === name;
41
+
42
+ return false;
43
+ }
44
+
45
+
46
+ function ownCallbacks(model) {
47
+ if (!modelCallbacks.has(model)) modelCallbacks.set(model, []);
48
+
49
+ return modelCallbacks.get(model);
50
+ }
51
+
52
+
53
+ /***************************************************************************************************
54
+ * LOOKUP
55
+ ***************************************************************************************************/
56
+ // Ancestors first, so callbacks run in the order they were registered as you read down a hierarchy.
57
+ function callbacksFor(model) {
58
+ const callbacks = [];
59
+ for (const ancestor of modelChain(model)) {
60
+ callbacks.unshift(...inheritableCallbacks(ancestor));
61
+ }
62
+ return callbacks;
63
+ }
64
+
65
+
66
+ function inheritableCallbacks(model) {
67
+ const callbacks = modelCallbacks.get(model) || [];
68
+ return callbacks.map(callback => callbackFunction(callback));
69
+ }
70
+
71
+
72
+ function callbackFunction(callback) {
73
+ if (typeof callback === 'function') return callback;
74
+
75
+ return callback.fn;
76
+ }
77
+
78
+
79
+ function* modelChain(model) {
80
+ let ancestor = model;
81
+ while (typeof ancestor === 'function') {
82
+ yield ancestor;
83
+ ancestor = Object.getPrototypeOf(ancestor);
84
+ }
85
+ }
86
+
87
+
88
+ /***************************************************************************************************
89
+ * DISPATCH
90
+ ***************************************************************************************************/
91
+ async function runCallbacks(model, record, options) {
92
+ const transaction = rootTransaction(options.transaction);
93
+
94
+ if (!transaction) return await callImmediately(model, record, options);
95
+
96
+ deferToTransaction(transaction, model, record, options);
97
+ }
98
+
99
+
100
+ // A savepoint commits while its parent can still roll back, so the callbacks belong to the
101
+ // outermost transaction.
102
+ function rootTransaction(transaction) {
103
+ let root = transaction;
104
+ while (root?.parent) {
105
+ root = root.parent;
106
+ }
107
+ return root;
108
+ }
109
+
110
+
111
+ async function callImmediately(model, record, options) {
112
+ for (const callback of callbacksFor(model)) {
113
+ await callback.call(model, record, options);
114
+ }
115
+ }
116
+
117
+
118
+ function deferToTransaction(transaction, model, record, options) {
119
+ for (const callback of callbacksFor(model)) {
120
+ deferCallback(transaction, callback, model, record, options);
121
+ }
122
+ }
123
+
124
+
125
+ function deferCallback(transaction, callback, model, record, options) {
126
+ if (!claimRegistration(transaction, callback, record)) return;
127
+
128
+ transaction.afterCommit(async () => await callback.call(model, record, options));
129
+ }
130
+
131
+
132
+ /***************************************************************************************************
133
+ * DEDUPLICATION
134
+ ***************************************************************************************************/
135
+ // Overlapping hooks can report the same write twice. Each callback runs once per record per
136
+ // transaction; returns false when this one is already spoken for.
137
+ function claimRegistration(transaction, callback, record) {
138
+ const records = claimedRecords(transaction, callback);
139
+ if (records.has(record)) return false;
140
+
141
+ records.add(record);
142
+ return true;
143
+ }
144
+
145
+
146
+ // A WeakSet so claiming a record doesn't hold it in memory for the life of the transaction.
147
+ function claimedRecords(transaction, callback) {
148
+ const callbacks = transactionClaims(transaction);
149
+ if (!callbacks.has(callback)) callbacks.set(callback, new WeakSet());
150
+
151
+ return callbacks.get(callback);
152
+ }
153
+
154
+
155
+ function transactionClaims(transaction) {
156
+ if (!claimedRegistrations.has(transaction)) claimedRegistrations.set(transaction, new Map());
157
+
158
+ return claimedRegistrations.get(transaction);
159
+ }
160
+
161
+
162
+ module.exports = {
163
+ addCallback,
164
+ callbacksFor,
165
+ removeCallback,
166
+ runCallbacks,
167
+ }
@@ -6,10 +6,14 @@ const { Model, Op, Sequelize } = require('sequelize');
6
6
  const nodeUtil = require('util');
7
7
  const { redisClientFactory } = require('../redis');
8
8
  const { camelize, tableize } = require('inflection');
9
+ const afterCommitHooks = require('./afterCommitHooks');
9
10
  const utils = require('./utils');
10
11
  const validations = require('./validations');
11
12
 
12
13
 
14
+ const AFTER_COMMIT = 'afterCommit';
15
+
16
+
13
17
  class BaseRecord extends Model {
14
18
  static _connection;
15
19
  static _initialized = false;
@@ -197,14 +201,76 @@ class BaseRecord extends Model {
197
201
  }
198
202
 
199
203
 
204
+ /***********************************************************************************************
205
+ * HOOKS
206
+ ***********************************************************************************************/
200
207
  static initializeHooks() {
208
+ this._initializeAfterCommitHooks();
209
+
201
210
  if (this.isCacheable) {
202
- this.addHook('afterSave', this._afterSaveCacheHook);
203
- this.addHook('afterDestroy', this._afterDestroyCacheHook);
211
+ this.addHook(AFTER_COMMIT, this._afterCommitCacheHook);
212
+ }
213
+ }
214
+
215
+
216
+ static _initializeAfterCommitHooks() {
217
+ // Every hook that reports a write to a single record. afterSave is deliberately the only save hook
218
+ // listed: sequelize proxies it into afterCreate and afterUpdate, so adding those would double fire.
219
+ const AFTER_COMMIT_SOURCE_HOOKS = ['afterSave', 'afterDestroy', 'afterRestore', 'afterUpsert'];
220
+ for (const hookType of AFTER_COMMIT_SOURCE_HOOKS) {
221
+ this.addHook(hookType, this._afterCommitHandler);
204
222
  }
205
223
  }
206
224
 
207
225
 
226
+ static addHook(hookType, name, fn) {
227
+ if (hookType != AFTER_COMMIT) return super.addHook(hookType, name, fn);
228
+
229
+ afterCommitHooks.addCallback(this, name, fn);
230
+ return this;
231
+ }
232
+
233
+
234
+ static removeHook(hookType, name) {
235
+ if (hookType != AFTER_COMMIT) return super.removeHook(hookType, name);
236
+
237
+ afterCommitHooks.removeCallback(this, name);
238
+ return this;
239
+ }
240
+
241
+
242
+ static hasHook(hookType) {
243
+ if (hookType != AFTER_COMMIT) return super.hasHook(hookType);
244
+
245
+ return Boolean(afterCommitHooks.callbacksFor(this).length);
246
+ }
247
+
248
+
249
+ // Sequelize aliases hasHooks onto its own hasHook, so overriding hasHook alone leaves it behind.
250
+ static hasHooks(hookType) {
251
+ return this.hasHook(hookType);
252
+ }
253
+
254
+
255
+ // Sequelize generates a shorthand registration method for each of its hook types. Match it.
256
+ static afterCommit(name, fn) {
257
+ return this.addHook(AFTER_COMMIT, name, fn);
258
+ }
259
+
260
+
261
+ static async _afterCommitHandler(recordOrResult, options) {
262
+ const record = this._afterCommitRecord(recordOrResult);
263
+ await afterCommitHooks.runCallbacks(this, record, options);
264
+ }
265
+
266
+
267
+ static _afterCommitRecord(recordOrResult) {
268
+ if (Array.isArray(recordOrResult)) return recordOrResult[0]; // afterUpsert: [record, created]
269
+
270
+ return recordOrResult;
271
+ }
272
+
273
+
208
274
  /***********************************************************************************************
209
275
  * VALIDATIONS
210
276
  ***********************************************************************************************/
@@ -350,7 +416,7 @@ class BaseRecord extends Model {
350
416
  const client = await this._getRedisClient();
351
417
  const cachedValues = clear ? [] : await client.mGet(keys);
352
418
  const kvPairs = keys.map((key,idx) => {
353
- const val = this._unmarshallCachedData(cachedValues[idx]);
419
+ const val = this._decodeCachedValue(cachedValues[idx]);
354
420
  return [key, val]
355
421
  });
356
422
  const cachedData = Object.fromEntries(kvPairs);
@@ -374,7 +440,7 @@ class BaseRecord extends Model {
374
440
  const toCache = {};
375
441
 
376
442
  for (const [key, value] of Object.entries(fetchedData)) {
377
- toCache[key] = this._marshallDataForCaching(value);
443
+ toCache[key] = await this._encodeForCache(value);
378
444
  }
379
445
 
380
446
  const client = await this._getRedisClient();
@@ -448,42 +514,41 @@ class BaseRecord extends Model {
448
514
  static get isCacheable() { return false; }
449
515
 
450
516
 
451
- static async _afterSaveCacheHook(record, options) {
517
+ static async _afterCommitCacheHook(record, options) {
452
518
  await record.clearSelfCache();
453
519
  await record.updateCache(options);
454
520
  }
455
521
 
456
522
 
457
- static async _afterDestroyCacheHook(record, options) {
458
- await record.clearSelfCache();
459
- await record.updateCache(options);
523
+ // The data a record contributes to its cache entry. Override to cache associated data
524
+ // alongside the record so a single cache read returns both.
525
+ async toCachePayload() {
526
+ return this.toJSON();
460
527
  }
461
528
 
462
529
 
463
- static _marshallDataForCaching(value) {
464
- let marshalledValue;
465
- if (value instanceof this) {
466
- marshalledValue = JSON.stringify(value.toJSON());
467
- marshalledValue = `${this.recordCachingPrefix}:${marshalledValue}`;
468
- } else {
469
- marshalledValue = JSON.stringify(value);
470
- }
471
- return marshalledValue;
530
+ // The inverse of toCachePayload: rebuilds a record from its cached data.
531
+ static fromCachePayload(data) {
532
+ return new this(data, {isNewRecord: false});
472
533
  }
473
534
 
474
535
 
475
- static _unmarshallCachedData(value) {
536
+ static async _encodeForCache(value) {
537
+ if (!(value instanceof this)) return JSON.stringify(value);
538
+
539
+ const payload = await value.toCachePayload();
540
+ return `${this.recordCachingPrefix}:${JSON.stringify(payload)}`;
541
+ }
542
+
543
+
544
+ static _decodeCachedValue(value) {
476
545
  if (value == null) return null;
477
546
  if (typeof value != 'string') return value;
547
+ if (!value.startsWith(this.recordCachingPrefix)) return JSON.parse(value);
478
548
 
479
- if (value.startsWith(this.recordCachingPrefix)) {
480
- const regExp = new RegExp(`^${this.recordCachingPrefix}:`);
481
- value = value.replace(regExp, '');
482
- const data = JSON.parse(value);
483
- return new this(data, {isNewRecord: false});
484
- } else {
485
- return JSON.parse(value);
486
- }
549
+ const regExp = new RegExp(`^${this.recordCachingPrefix}:`);
550
+ const data = JSON.parse(value.replace(regExp, ''));
551
+ return this.fromCachePayload(data);
487
552
  }
488
553
 
489
554
 
@@ -119,8 +119,11 @@ class Migrator {
119
119
 
120
120
 
121
121
  idColumn(idType=DEFAULT_ID_TYPE) {
122
+ if (idType === false) return {};
123
+
122
124
  if (!ID_TYPES.includes(idType)) {
123
- throw new Error(`Unknown id type "${idType}". Use one of: ${ID_TYPES.join(', ')}`);
125
+ throw new Error(`Unknown id type "${idType}". Use one of: ${ID_TYPES.join(', ')}, or `
126
+ + `false for a table that declares its own primary key.`);
124
127
  }
125
128
 
126
129
  return this[`${idType}IdColumn`];
@@ -9,6 +9,13 @@ class StreamInProgressError extends Error {}
9
9
  class NoActiveStreamError extends Error {}
10
10
 
11
11
 
12
+ // xmlbuilder2's default object-notation control prefixes: @attribute, #text, $cdata,
13
+ // !comment, ?processing-instruction. A key using one of these must survive sanitization
14
+ // unmangled, or an intended attribute (e.g. {'@track': 'both'}) silently turns into a
15
+ // child element (`<_track>both</_track>`) instead of `track="both"`.
16
+ const XML_CONVERT_PREFIXES = ['@', '#', '$', '!', '?'];
17
+
18
+
12
19
  const CONTENT_TYPE_BY_EXTENSION = {
13
20
  css: 'text/css; charset=utf-8',
14
21
  gif: 'image/gif',
@@ -88,12 +95,17 @@ const InstanceMethods = {
88
95
  if (!this.responseContentType) this.responseContentType = CONTENT_TYPE_BY_EXTENSION.xml;
89
96
  const doc = create({ [this.xmlRootElement]: body });
90
97
  const xml = doc.end();
91
- this._response.send(xml);
98
+ this._send(xml);
92
99
  },
93
100
 
94
101
 
95
102
  renderTextResponse(body) {
96
103
  if (!this.responseContentType) this.responseContentType = CONTENT_TYPE_BY_EXTENSION.txt;
104
+ this._send(body);
105
+ },
106
+
107
+
108
+ _send(body) {
97
109
  this._response.send(body);
98
110
  },
99
111
 
@@ -182,8 +194,23 @@ const InstanceMethods = {
182
194
 
183
195
  // XML element names can't start with a digit or contain spaces/other invalid characters,
184
196
  // so sanitize keys that would otherwise make xmlbuilder2 throw on dynamic response data.
197
+ // A leading xmlbuilder2 control prefix (see XML_CONVERT_PREFIXES) is preserved as-is;
198
+ // only the remainder (the attribute/PI name, if any) is sanitized.
185
199
  _toXMLElementName(key) {
186
- let name = `${key}`.replace(/[^a-zA-Z0-9_.-]/g, '_');
200
+ key = `${key}`;
201
+
202
+ const prefix = XML_CONVERT_PREFIXES.find(p => key.startsWith(p));
203
+ if (prefix) {
204
+ const rest = key.slice(prefix.length);
205
+ return rest ? prefix + this._sanitizeXMLNamePart(rest) : prefix;
206
+ }
207
+
208
+ return this._sanitizeXMLNamePart(key);
209
+ },
210
+
211
+
212
+ _sanitizeXMLNamePart(name) {
213
+ name = name.replace(/[^a-zA-Z0-9_.-]/g, '_');
187
214
  if (!/^[a-zA-Z_]/.test(name)) name = `_${name}`;
188
215
  return name;
189
216
  },
@@ -47,8 +47,10 @@ const Accessors = {
47
47
 
48
48
  contentType: { get() { return this.requestHeaders['content-type'] }},
49
49
  cookies: { get() { return this._request.cookies || {} }},
50
+ isProxiedRequest:{ get() { return Boolean(this.requestHeaders['x-forwarded-for']) }},
50
51
  requestBody: { get() { return this._request.body }},
51
52
  requestHeaders: { get() { return structuredClone(this._request.headers || {}) }},
53
+ requestHost: { get() { return this.requestHeaders.host }},
52
54
  requestId: { get() { return this._request.id }},
53
55
  requestIp: { get() { return this._request.ip }},
54
56
  requestMethod: { get() { return this._request.method }},
@@ -73,6 +75,36 @@ const Accessors = {
73
75
  return match[1].trim();
74
76
  }
75
77
  },
78
+
79
+
80
+ // The absolute URL the caller addressed, rebuilt from the request. Webhook signature schemes
81
+ // sign this string, so it has to match what the sender used character for character.
82
+ requestUrl: {
83
+ get() { return `${this.requestProtocol}://${this.requestHost}${this.url}` }
84
+ },
85
+
86
+
87
+ // TLS usually terminates at a load balancer, and not every balancer forwards
88
+ // X-Forwarded-Proto, so `_request.protocol` can report http on a request the caller made over
89
+ // https. Prefer the forwarded protocol, assume https for anything else that arrived through a
90
+ // proxy, and fall back to the connection's own protocol for direct requests.
91
+ requestProtocol: {
92
+ get() {
93
+ if (this.forwardedProtocol) return this.forwardedProtocol;
94
+ if (this.isProxiedRequest) return 'https';
95
+ return this._request.protocol;
96
+ }
97
+ },
98
+
99
+
100
+ // A comma-joined chain lists the original client's protocol first.
101
+ forwardedProtocol: {
102
+ get() {
103
+ const header = this.requestHeaders['x-forwarded-proto'];
104
+ if (!header) return null;
105
+ return header.split(',')[0].trim();
106
+ }
107
+ },
76
108
  }
77
109
 
78
110
 
@@ -70,7 +70,7 @@ const InstanceMethods = {
70
70
 
71
71
  validateIsString(value, options) {
72
72
  if (typeof value !== 'string') return 'must be a string';
73
- if (options?.allowEmpty === false && value.length === 0) return 'must not be empty';
73
+ if (options?.allowEmpty === false && value.trim().length === 0) return 'must not be empty';
74
74
 
75
75
  if (options?.length?.gte !== undefined) {
76
76
  if (value.length < options.length.gte) return `must be greater than or equal to ${options.length.gte} characters`;
@@ -19,6 +19,11 @@ Within an action, the controller exposes:
19
19
  | `this.requestMethod` | HTTP method. |
20
20
  | `this.requestIp` | Client IP address. |
21
21
  | `this.url` | Original URL. |
22
+ | `this.requestHost` | Shortcut for `requestHeaders['host']`. |
23
+ | `this.requestUrl` | Absolute URL the caller addressed: `requestProtocol://requestHost + url`. Use this when verifying a webhook signature that covers the URL. |
24
+ | `this.requestProtocol` | `forwardedProtocol`, else `https` when `isProxiedRequest`, else the connection's own protocol. |
25
+ | `this.forwardedProtocol` | First entry of `X-Forwarded-Proto`, or `null`. |
26
+ | `this.isProxiedRequest` | `true` when `X-Forwarded-For` is present. |
22
27
  | `this.bearerToken` | Parsed `Authorization: Bearer <token>` value, or `null`. |
23
28
  | `this.statusCode` | Current outgoing status (settable, but prefer the render helpers below). |
24
29
  | `this.logger` | Per-request child logger. |
@@ -192,7 +192,7 @@ class VidaServer {
192
192
  const method = action.method.toLowerCase();
193
193
  const requestHandler = this.requestHandler(action.action, controllerCls)
194
194
  if (process.env.NODE_ENV != 'test') {
195
- this.logger.verbose(`ROUTE: ${method.toUpperCase().padEnd(6)} ${action.path}`);
195
+ this.logger.debug(`ROUTE: ${method.toUpperCase().padEnd(6)} ${action.path}`);
196
196
  }
197
197
  this['_'+method](action.path, requestHandler);
198
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vida-global/core",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "Core libraries for supporting Vida development",
5
5
  "author": "",
6
6
  "license": "ISC",