@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.
- package/.github/workflows/npm-publish.yml +28 -0
- package/README.md +0 -1
- package/index.js +2 -0
- package/lib/active_record/baseRecord.js +167 -5
- package/lib/redis/index.js +5 -0
- package/lib/redis/redisClient.js +45 -0
- package/package.json +6 -4
- package/test/active_record/baseRecord.test.js +366 -15
- package/lib/release/README.md +0 -66
- package/lib/release/develop.js +0 -27
- package/lib/release/git.js +0 -86
- package/lib/release/increment.js +0 -56
- package/lib/release/index.js +0 -10
- package/lib/release/release.js +0 -30
- package/lib/release/utils.js +0 -44
- package/scripts/release.js +0 -75
- package/test/release/develop.test.js +0 -57
- package/test/release/git.test.js +0 -189
- package/test/release/increment.test.js +0 -145
- package/test/release/release.test.js +0 -72
- package/test/release/utils.test.js +0 -148
|
@@ -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
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(
|
|
30
|
-
|
|
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
|
-
|
|
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,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
|
|
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
|
|
11
|
-
"develop": "node
|
|
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",
|