@vida-global/core 1.1.13 → 1.2.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.
@@ -0,0 +1,28 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ push:
8
+ branches:
9
+ - release/production
10
+
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+
15
+ jobs:
16
+ publish:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-node@v4
21
+ with:
22
+ node-version: '24'
23
+ registry-url: 'https://registry.npmjs.org'
24
+ - run: npm ci
25
+ env:
26
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_READ_TOKEN }}
27
+ - run: npm test
28
+ - run: npm publish
package/README.md CHANGED
@@ -7,4 +7,3 @@ This package is intended to contain core elements used across all Vida Apps. Kee
7
7
  - [HttpClient](lib/http)
8
8
  - [Logger](lib/logger)
9
9
  - [VidaServer](lib/server)
10
- - [Release Scripts](lib/release)
package/index.js CHANGED
@@ -4,6 +4,7 @@ const { AuthorizationError,
4
4
  VidaServerController } = require('./lib/server');
5
5
  const { logger } = require('./lib/logger');
6
6
  const ActiveRecord = require('./lib/active_record');
7
+ const { redisClientFactory } = require('./lib/redis');
7
8
 
8
9
 
9
10
  module.exports = {
@@ -12,6 +13,7 @@ module.exports = {
12
13
  HttpClient,
13
14
  HttpError,
14
15
  logger,
16
+ redisClientFactory,
15
17
  VidaServer,
16
18
  VidaServerController,
17
19
  };
@@ -1,10 +1,11 @@
1
1
  const { Connection, DEFAULT_DATABASE_ID } = require('./db/connection');
2
2
  const { getActiveRecordSchema } = require('./db/schema');
3
+ const { logger } = require('../logger');
3
4
  const { Model, Op } = require('sequelize');
5
+ const nodeUtil = require('util');
6
+ const { redisClientFactory } = require('../redis');
4
7
  const { tableize } = require('inflection');
5
8
  const utils = require('./utils');
6
- const nodeUtil = require('util');
7
-
8
9
 
9
10
 
10
11
  class BaseRecord extends Model {
@@ -26,8 +27,15 @@ class BaseRecord extends Model {
26
27
  /***********************************************************************************************
27
28
  * QUERIES
28
29
  ***********************************************************************************************/
29
- static async find(id) {
30
- return await this.findByPk(id);
30
+ static async find(ids) {
31
+ if (this.isCacheable) {
32
+ return await this._cachedFind(...arguments);
33
+ } else if (Array.isArray(ids)) {
34
+ const pk = this.primaryKeyAttribute
35
+ return await this.where({[pk]: ids});
36
+ } else {
37
+ return await this.findByPk(ids);
38
+ }
31
39
  }
32
40
 
33
41
 
@@ -61,10 +69,19 @@ class BaseRecord extends Model {
61
69
 
62
70
  this.init(schema, options);
63
71
 
72
+ this.initializeHooks();
73
+
64
74
  this._initialized = true;
65
75
  }
66
76
 
67
77
 
78
+ static initializeHooks() {
79
+ if (this.isCacheable) {
80
+ this.addHook('afterSave', this._afterSaveCacheHook);
81
+ }
82
+ }
83
+
84
+
68
85
  /***********************************************************************************************
69
86
  * DB CONNECTION
70
87
  ***********************************************************************************************/
@@ -96,13 +113,158 @@ class BaseRecord extends Model {
96
113
  }
97
114
 
98
115
 
116
+ /***********************************************************************************************
117
+ * CACHING
118
+ ***********************************************************************************************/
119
+ static async _cachedFind(ids, { clear=false }={}) {
120
+ const multiFind = Array.isArray(ids)
121
+ const idsToFind = multiFind ? ids : [ids];
122
+ const keyPairs = idsToFind.map(id => [this._recordCacheKey(id), id]);
123
+ const keysToIds = Object.fromEntries(keyPairs);
124
+ const idsToKeys = Object.fromEntries(keyPairs.map(p => p.reverse()));
125
+ const keys = Object.keys(keysToIds);
126
+
127
+ let records = await this.cachedFetch(keys, { clear }, async (missedKeys) => {
128
+ const idsToFind = missedKeys.map(key => keysToIds[key]);
129
+ const pk = this.primaryKeyAttribute
130
+ const records = await this.where({[pk]: idsToFind});
131
+ const keysToRecords = records.map(r => [idsToKeys[r[pk]], r])
132
+ return Object.fromEntries(keysToRecords);
133
+ });
134
+
135
+ records = Object.values(records);
136
+ if (!multiFind) return records[0] || null;
137
+ return records.filter(r => Boolean(r));
138
+ }
139
+
140
+
141
+ static async cachedIds(key, where, { clear=false }={}) {
142
+ const pk = this.primaryKeyAttribute;
143
+ const result = await this.cachedFetch([key], { clear }, async () => {
144
+ const rows = await this.findAll({ where, attributes: [pk], raw: true});
145
+ return {[key]: rows.map(row => row[pk])};
146
+ });
147
+
148
+ return result[key];
149
+ }
150
+
151
+
152
+ static async cachedFetch(keys, { clear=false }, fetcher) {
153
+ const client = await this._getRedisClient();
154
+ const cachedValues = clear ? [] : await client.mGet(keys);
155
+ const kvPairs = keys.map((key,idx) => {
156
+ const val = this._unmarshallCachedData(cachedValues[idx]);
157
+ return [key, val]
158
+ });
159
+ const cachedData = Object.fromEntries(kvPairs);
160
+ const missedKeys = keys.filter((key, idx) => cachedValues[idx] == null);
161
+ const hitKeys = keys.filter(key => !missedKeys.includes(key));
162
+
163
+ this.debugLog('Cache Hit', hitKeys.join(', ') || '-');
164
+
165
+ let fetchedData = {};
166
+ if (missedKeys.length) {
167
+ this.debugLog('Cache Miss', missedKeys.join(', '));
168
+ fetchedData = await fetcher(missedKeys);
169
+ await this._cacheFetchedData(fetchedData)
170
+ }
171
+
172
+ return {...cachedData, ...fetchedData};
173
+ }
174
+
175
+
176
+ static async _cacheFetchedData(fetchedData) {
177
+ const toCache = {};
178
+
179
+ for (const [key, value] of Object.entries(fetchedData)) {
180
+ toCache[key] = this._marshallDataForCaching(value);
181
+ }
182
+
183
+ const client = await this._getRedisClient();
184
+ await client.mSet(toCache);
185
+ this.debugLog('Cache Set', Object.keys(fetchedData).join(', '));
186
+ }
187
+
188
+
189
+ static _marshallDataForCaching(value) {
190
+ let marshalledValue;
191
+ if (value instanceof this) {
192
+ marshalledValue = JSON.stringify(value.toJSON());
193
+ marshalledValue = `${this.recordCachingPrefix}:${marshalledValue}`;
194
+ } else {
195
+ marshalledValue = JSON.stringify(value);
196
+ }
197
+ return marshalledValue;
198
+ }
199
+
200
+
201
+ static _unmarshallCachedData(value) {
202
+ if (value == null) return null;
203
+ if (typeof value != 'string') return value;
204
+
205
+ if (value.startsWith(this.recordCachingPrefix)) {
206
+ const regExp = new RegExp(`^${this.recordCachingPrefix}:`);
207
+ value = value.replace(regExp, '');
208
+ const data = JSON.parse(value);
209
+ return new this(data, {isNewRecord: false});
210
+ } else {
211
+ return JSON.parse(value);
212
+ }
213
+ }
214
+
215
+
216
+ async clearSelfCache() {
217
+ if (!this.constructor.isCacheable) return;
218
+
219
+ const pk = this.constructor.primaryKeyAttribute;
220
+ const key = this.constructor._recordCacheKey(this[pk]);
221
+ const client = await this.constructor._getRedisClient();
222
+
223
+ await client.del(key);
224
+ this.constructor.debugLog('Cache Del', key);
225
+ }
226
+
227
+
228
+ async updateCache() {}
229
+
230
+
231
+ static get recordCachingPrefix() {
232
+ return `AR:${this.name}`;
233
+ }
234
+
235
+
236
+ static _recordCacheKey(id) {
237
+ return `${this.name}#find:${id}`;
238
+ }
239
+
240
+
241
+ static async _getRedisClient() {
242
+ return await redisClientFactory();
243
+ }
244
+
245
+
246
+ static get isCacheable() { return false; }
247
+
248
+
249
+ static async _afterSaveCacheHook(record, options) {
250
+ await record.clearSelfCache();
251
+ await record.updateCache(options);
252
+ }
253
+
254
+
99
255
  /***********************************************************************************************
100
256
  * MISC
101
257
  ***********************************************************************************************/
102
258
  [nodeUtil.inspect.custom](opts) {
103
259
  const args = Array.from(arguments);
104
260
  args.shift();
105
- return nodeUtil.inspect(this.dataValues, ...args);
261
+ const str = `${this.constructor.name} ${nodeUtil.inspect(this.dataValues)}`;
262
+ return nodeUtil.inspect(str, ...args);
263
+ }
264
+
265
+
266
+ static debugLog(tag, log) {
267
+ logger.debug(`[AR:${this.name}][${tag}] ${log}`);
106
268
  }
107
269
  }
108
270
 
@@ -0,0 +1,5 @@
1
+ const { redisClientFactory } = require('./redisClient');
2
+
3
+ module.exports = {
4
+ redisClientFactory
5
+ }
@@ -0,0 +1,45 @@
1
+ const fs = require('fs');
2
+ const { logger } = require('../logger');
3
+ const redis = require('redis');
4
+
5
+
6
+ let redisSingleton;
7
+
8
+
9
+ async function initRedisClientSingleton() {
10
+ if (redisSingleton) return;
11
+
12
+ const certLocation = process.env.REDIS_CERT_LOCATION || `${process.cwd()}/redis_ca.pem`;
13
+ const cert = fs.readFileSync(certLocation)
14
+
15
+ redisSingleton = redis.createClient({
16
+ socket: {
17
+ host: process.env.REDIS_HOST,
18
+ port: parseInt(process.env.REDIS_PORT),
19
+ tls: true,
20
+ ca: [cert]
21
+ },
22
+ password: process.env.REDIS_PASSWORD
23
+ });
24
+
25
+ redisSingleton.on('ready', (err) => {
26
+ logger.info('Redis Client Connected');
27
+ });
28
+ redisSingleton .on('error', (err) => {
29
+ const errno = err.errno ? ` (${err.errno})` : '';
30
+ logger.error(`Redis: ${err.message}${errno}`);
31
+ });
32
+
33
+ await redisSingleton.connect();
34
+ }
35
+
36
+
37
+ async function redisClientFactory() {
38
+ await initRedisClientSingleton();
39
+ return redisSingleton;
40
+ }
41
+
42
+
43
+ module.exports = {
44
+ redisClientFactory
45
+ };
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@vida-global/core",
3
- "version": "1.1.13",
3
+ "version": "1.2.1",
4
4
  "description": "Core libraries for supporting Vida development",
5
5
  "author": "",
6
6
  "license": "ISC",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules node_modules/jest/bin/jest.js",
10
- "release": "node ./scripts/release.js",
11
- "develop": "node ./scripts/release.js develop"
10
+ "release": "node node_modules/@vida-global/release/scripts/release.js",
11
+ "develop": "node node_modules/@vida-global/release/scripts/release.js develop"
12
12
  },
13
13
  "jest": {
14
14
  "collectCoverage": true,
@@ -25,10 +25,12 @@
25
25
  "pino": "^9.6.0",
26
26
  "pino-http": "^10.4.0",
27
27
  "pino-pretty": "^13.0.0",
28
+ "redis": "^5.0.0",
28
29
  "response-time": "^2.3.3",
29
30
  "sequelize": "^6.37.7",
30
31
  "sequelize-cli": "^6.6.3",
31
- "socket.io": "^4.4.0"
32
+ "socket.io": "^4.4.0",
33
+ "@vida-global/release": "^1.0.0"
32
34
  },
33
35
  "devDependencies": {
34
36
  "jest": "^29.6.2",