@vida-global/core 2.4.3 → 2.4.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.
@@ -401,9 +401,9 @@ class BaseRecord extends Model {
401
401
  }
402
402
 
403
403
 
404
- static async cachedIds(key, where, { clear=false }={}) {
404
+ static async cachedIds(key, where, { clear=false, expireSeconds=null }={}) {
405
405
  const pk = this.primaryKeyAttribute;
406
- const result = await this.cachedFetch([key], { clear }, async () => {
406
+ const result = await this.cachedFetch([key], { clear, expireSeconds }, async () => {
407
407
  const rows = await this.findAll({ where, attributes: [pk], raw: true});
408
408
  return {[key]: rows.map(row => row[pk])};
409
409
  });
@@ -412,7 +412,7 @@ class BaseRecord extends Model {
412
412
  }
413
413
 
414
414
 
415
- static async cachedFetch(keys, { clear=false }, fetcher) {
415
+ static async cachedFetch(keys, { clear=false, expireSeconds=null }, fetcher) {
416
416
  const client = await this._getRedisClient();
417
417
  const cachedValues = clear ? [] : await client.mGet(keys);
418
418
  const kvPairs = keys.map((key,idx) => {
@@ -429,14 +429,14 @@ class BaseRecord extends Model {
429
429
  if (missedKeys.length) {
430
430
  this.debugLog('Cache Miss', missedKeys.join(', '));
431
431
  fetchedData = await fetcher(missedKeys);
432
- await this._cacheFetchedData(fetchedData)
432
+ await this._cacheFetchedData(fetchedData, { expireSeconds })
433
433
  }
434
434
 
435
435
  return {...cachedData, ...fetchedData};
436
436
  }
437
437
 
438
438
 
439
- static async _cacheFetchedData(fetchedData) {
439
+ static async _cacheFetchedData(fetchedData, { expireSeconds=null }={}) {
440
440
  const toCache = {};
441
441
 
442
442
  for (const [key, value] of Object.entries(fetchedData)) {
@@ -445,7 +445,14 @@ class BaseRecord extends Model {
445
445
 
446
446
  const client = await this._getRedisClient();
447
447
  if (Object.keys(toCache).length) {
448
- await client.mSet(toCache);
448
+ if (expireSeconds) {
449
+ // mSet cannot carry an expiry, so expiring entries are set one by one.
450
+ await Promise.all(Object.entries(toCache).map(([key, value]) => {
451
+ return client.set(key, value, { EX: expireSeconds });
452
+ }));
453
+ } else {
454
+ await client.mSet(toCache);
455
+ }
449
456
  }
450
457
  this.debugLog('Cache Set', Object.keys(fetchedData).join(', '));
451
458
  }
@@ -1,4 +1,5 @@
1
- const winston = require('winston');
1
+ const util = require('node:util');
2
+ const winston = require('winston');
2
3
  const { colorize, combine, printf, timestamp } = winston.format;
3
4
 
4
5
 
@@ -122,12 +123,25 @@ class Logger {
122
123
  }
123
124
 
124
125
 
125
- debug(msg) { this.#logger.debug(msg) }
126
- error(msg) { this.#logger.error(msg) }
127
- info(msg) { this.#logger.info(msg) }
128
- silly(msg) { this.#logger.silly(msg) }
129
- verbose(msg) { this.#logger.verbose(msg) }
130
- warn(msg) { this.#logger.warn(msg) }
126
+ debug(...args) { this.#logger.debug(this.formatArguments(args)) }
127
+ error(...args) { this.#logger.error(this.formatArguments(args)) }
128
+ info(...args) { this.#logger.info(this.formatArguments(args)) }
129
+ silly(...args) { this.#logger.silly(this.formatArguments(args)) }
130
+ verbose(...args) { this.#logger.verbose(this.formatArguments(args)) }
131
+ warn(...args) { this.#logger.warn(this.formatArguments(args)) }
132
+
133
+
134
+ // The final winston formatter emits `message` as-is, so an object argument would land as
135
+ // "[object Object]". `util.formatWithOptions` is what console.log itself uses, so callers get
136
+ // the same handling of objects, format specifiers, and Errors.
137
+ formatArguments(args) {
138
+ return util.formatWithOptions(this.inspectOptions, ...args);
139
+ }
140
+
141
+
142
+ get inspectOptions() {
143
+ return { depth: 4, colors: false };
144
+ }
131
145
 
132
146
  get _winstonLogger() {
133
147
  this.warn('Directly accessing the underlying winston logger should be avoided unless necessary');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vida-global/core",
3
- "version": "2.4.3",
3
+ "version": "2.4.4",
4
4
  "description": "Core libraries for supporting Vida development",
5
5
  "author": "",
6
6
  "license": "ISC",
@@ -730,6 +730,14 @@ describe('BaseRecord', () => {
730
730
  expect(Person.findAll).toHaveBeenCalledTimes(1);
731
731
  expect(Person.findAll).toHaveBeenCalledWith({ where, attributes: [pk], raw: true });
732
732
  });
733
+
734
+ it ('sets the cache entry with an expiry when expireSeconds is passed', async () => {
735
+ Person.findAll.mockImplementation(() => rows);
736
+ await Person.cachedIds(key, where, {expireSeconds: 300})
737
+ expect(redisClient.mSet).not.toHaveBeenCalled();
738
+ expect(redisClient.set).toHaveBeenCalledTimes(1);
739
+ expect(redisClient.set).toHaveBeenCalledWith(key, JSON.stringify(ids), {EX: 300});
740
+ });
733
741
  });
734
742
 
735
743
 
@@ -779,7 +787,7 @@ describe('BaseRecord', () => {
779
787
  await Person.cachedFetch([key1, key2], {}, fetcher);
780
788
 
781
789
  expect(cacheSpy).toHaveBeenCalledTimes(1);
782
- expect(cacheSpy).toHaveBeenCalledWith(data);
790
+ expect(cacheSpy).toHaveBeenCalledWith(data, {expireSeconds: null});
783
791
  });
784
792
 
785
793
  it ('returns data from cache hits and misses', async () => {
@@ -841,6 +849,19 @@ describe('BaseRecord', () => {
841
849
  expect(redisClient.mSet).toHaveBeenCalledTimes(1);
842
850
  expect(redisClient.mSet).toHaveBeenCalledWith(cachedData);
843
851
  });
852
+
853
+ it ('sets each entry with an expiry instead of a multi set when expireSeconds is passed', async () => {
854
+ const objKey = TestHelpers.Faker.Text.randomString();
855
+ const objVal = TestHelpers.Faker.Text.randomString();
856
+ const obj = {[objKey]: objVal};
857
+
858
+ await Person._cacheFetchedData({ [key1]: obj, [key2]: obj }, { expireSeconds: 60 });
859
+
860
+ expect(redisClient.mSet).not.toHaveBeenCalled();
861
+ expect(redisClient.set).toHaveBeenCalledTimes(2);
862
+ expect(redisClient.set).toHaveBeenCalledWith(key1, JSON.stringify(obj), {EX: 60});
863
+ expect(redisClient.set).toHaveBeenCalledWith(key2, JSON.stringify(obj), {EX: 60});
864
+ });
844
865
  });
845
866
 
846
867
 
@@ -60,6 +60,7 @@ const redisClient = {
60
60
  del: jest.fn(),
61
61
  mGet: jest.fn(),
62
62
  mSet: jest.fn(),
63
+ set: jest.fn(),
63
64
  connect: jest.fn(),
64
65
  scanIterator: jest.fn(),
65
66
  }
@@ -197,4 +197,81 @@ describe('Logger', () => {
197
197
  expect(result[marker]).toEqual('info');
198
198
  });
199
199
  });
200
+
201
+
202
+ describe('#formatArguments', () => {
203
+ it ('returns a lone string unchanged', () => {
204
+ const logger = new Logger(randomScope());
205
+ const message = TestHelpers.Faker.Text.randomString();
206
+
207
+ expect(logger.formatArguments([message])).toEqual(message);
208
+ });
209
+
210
+ it ('inspects an object instead of stringifying it', () => {
211
+ const logger = new Logger(randomScope());
212
+ const value = TestHelpers.Faker.Math.randomNumber();
213
+
214
+ const result = logger.formatArguments([{ count: value }]);
215
+
216
+ expect(result).toEqual(`{ count: ${value} }`);
217
+ expect(result).not.toContain('[object Object]');
218
+ });
219
+
220
+ it ('joins a message and an object the way console.log does', () => {
221
+ const logger = new Logger(randomScope());
222
+ const value = TestHelpers.Faker.Math.randomNumber();
223
+
224
+ const result = logger.formatArguments(['totals', { count: value }]);
225
+
226
+ expect(result).toEqual(`totals { count: ${value} }`);
227
+ });
228
+
229
+ it ('applies printf style format specifiers', () => {
230
+ const logger = new Logger(randomScope());
231
+ const value = TestHelpers.Faker.Math.randomNumber();
232
+
233
+ expect(logger.formatArguments(['count %d', value])).toEqual(`count ${value}`);
234
+ });
235
+
236
+ it ('renders an error with its stack', () => {
237
+ const logger = new Logger(randomScope());
238
+ const message = TestHelpers.Faker.Text.randomString();
239
+
240
+ const result = logger.formatArguments([new Error(message)]);
241
+
242
+ expect(result).toContain(message);
243
+ expect(result).toContain('at ');
244
+ });
245
+
246
+ it ('nests objects to the configured depth', () => {
247
+ const logger = new Logger(randomScope());
248
+
249
+ const result = logger.formatArguments([{ a: { b: { c: { d: { e: { f: 1 } } } } } }]);
250
+
251
+ expect(result).toContain('e:');
252
+ expect(result).toContain('[Object]');
253
+ });
254
+ });
255
+
256
+
257
+ describe.each([
258
+ ['debug'],
259
+ ['error'],
260
+ ['info'],
261
+ ['silly'],
262
+ ['verbose'],
263
+ ['warn'],
264
+ ])('#%s', (level) => {
265
+ it ('formats its arguments before handing them to the transport', () => {
266
+ const logger = new Logger(randomScope());
267
+ const spy = jest.spyOn(logger, 'formatArguments');
268
+ const value = TestHelpers.Faker.Math.randomNumber();
269
+
270
+ logger[level]('totals', { count: value });
271
+
272
+ expect(spy).toHaveBeenCalledTimes(1);
273
+ expect(spy).toHaveBeenCalledWith(['totals', { count: value }]);
274
+ spy.mockRestore();
275
+ });
276
+ });
200
277
  });