@vida-global/core 1.4.5 → 2.0.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/AGENTS.md +13 -0
- package/README.md +1 -0
- package/config/newrelic-config.js +118 -0
- package/index.js +9 -5
- package/lib/activeRecord/baseRecord.js +49 -3
- package/lib/apm/agent.js +22 -0
- package/lib/apm/index.js +54 -0
- package/lib/apm/utils.js +43 -0
- package/lib/http/client.js +16 -2
- package/lib/jobQueue/README.md +63 -0
- package/lib/jobQueue/abstractJob.js +103 -0
- package/lib/jobQueue/abstractJobComponent.js +37 -0
- package/lib/jobQueue/index.js +10 -0
- package/lib/jobQueue/jobImporter.js +19 -0
- package/lib/jobQueue/queue.js +161 -0
- package/lib/jobQueue/worker.js +273 -0
- package/lib/logger/README.md +33 -2
- package/lib/logger/index.js +117 -20
- package/lib/logger/serverMiddleware.js +41 -0
- package/lib/redis/redisClient.js +32 -24
- package/lib/server/controllerImporter.js +9 -47
- package/lib/server/server.js +34 -71
- package/lib/server/serverController.js +215 -23
- package/lib/utils/abstractAutoImporter.js +64 -0
- package/package.json +8 -6
- package/test/activeRecord/baseRecord.test.js +129 -0
- package/test/activeRecord/db/connection.test.js +4 -1
- package/test/activeRecord/db/connectionConfiguration.test.js +9 -3
- package/test/activeRecord/db/schema.test.js +217 -0
- package/test/activeRecord/helpers/baseRecordMocks.js +6 -4
- package/test/activeRecord/helpers/connection.js +0 -3
- package/test/activeRecord/helpers/connectionConfiguration.js +0 -8
- package/test/activeRecord/utils.test.js +72 -0
- package/test/apm/agent.test.js +56 -0
- package/test/apm/utils.test.js +121 -0
- package/test/helpers/env.js +33 -0
- package/test/http/client.test.js +3 -3
- package/test/jobQueue/abstractJob.test.js +307 -0
- package/test/jobQueue/abstractJobComponent.test.js +110 -0
- package/test/jobQueue/helpers/abstractJob.js +26 -0
- package/test/jobQueue/helpers/apmMock.js +20 -0
- package/test/jobQueue/helpers/bullmqMock.js +67 -0
- package/test/jobQueue/helpers/env.js +29 -0
- package/test/jobQueue/helpers/fixtureJobs/notAJob.js +8 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobA.js +14 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobB.js +14 -0
- package/test/jobQueue/helpers/loggerMock.js +43 -0
- package/test/jobQueue/helpers/worker.js +38 -0
- package/test/jobQueue/queue.test.js +320 -0
- package/test/jobQueue/worker.test.js +526 -0
- package/test/logger/index.test.js +61 -0
- package/test/logger/serverMiddleware.test.js +72 -0
- package/test/server/apiDocsGenerator.test.js +38 -0
- package/test/server/server.test.js +186 -4
- package/test/server/serverController.test.js +407 -5
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const TestHelpers = require('@vida-global/test-helpers');
|
|
2
|
+
const { setupJobQueueEnv } = require('./helpers/env');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
jest.mock('bullmq', () => {
|
|
6
|
+
const mock = require('./helpers/bullmqMock');
|
|
7
|
+
return { Queue: mock.Queue, Worker: mock.Worker };
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
jest.mock('../../lib/logger', () => {
|
|
11
|
+
const { buildLoggerMock } = require('./helpers/loggerMock');
|
|
12
|
+
return { logger: buildLoggerMock() };
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
jest.mock('crypto', () => {
|
|
16
|
+
const actual = jest.requireActual('crypto');
|
|
17
|
+
return { ...actual, randomUUID: jest.fn() };
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
let bullmqMock;
|
|
22
|
+
let getQueue;
|
|
23
|
+
let logger;
|
|
24
|
+
let randomUUID;
|
|
25
|
+
let restoreEnv;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.resetModules();
|
|
29
|
+
restoreEnv = setupJobQueueEnv();
|
|
30
|
+
|
|
31
|
+
bullmqMock = require('./helpers/bullmqMock');
|
|
32
|
+
({ getQueue } = require('../../lib/jobQueue/queue'));
|
|
33
|
+
({ logger } = require('../../lib/logger'));
|
|
34
|
+
({ randomUUID } = require('crypto'));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
restoreEnv();
|
|
40
|
+
jest.clearAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
function expectedConnection() {
|
|
45
|
+
return {
|
|
46
|
+
host: process.env.REDIS_BULL_HOST,
|
|
47
|
+
password: process.env.REDIS_BULL_PASSWORD,
|
|
48
|
+
port: process.env.REDIS_BULL_PORT,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
describe('getQueue', () => {
|
|
54
|
+
it ('returns a Queue with the given queueName', () => {
|
|
55
|
+
const name = TestHelpers.Faker.Text.randomString();
|
|
56
|
+
|
|
57
|
+
const queue = getQueue(name);
|
|
58
|
+
|
|
59
|
+
expect(queue.queueName).toEqual(name);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it ('memoizes by full queue name', () => {
|
|
63
|
+
const name = TestHelpers.Faker.Text.randomString();
|
|
64
|
+
|
|
65
|
+
const first = getQueue(name);
|
|
66
|
+
const second = getQueue(name);
|
|
67
|
+
|
|
68
|
+
expect(second).toBe(first);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it ('returns different instances for different names', () => {
|
|
72
|
+
const queueA = getQueue(TestHelpers.Faker.Text.randomString());
|
|
73
|
+
const queueB = getQueue(TestHelpers.Faker.Text.randomString());
|
|
74
|
+
|
|
75
|
+
expect(queueB).not.toBe(queueA);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it ('treats the same raw name under different NODE_ENVs as different queues', () => {
|
|
79
|
+
const name = TestHelpers.Faker.Text.randomString();
|
|
80
|
+
|
|
81
|
+
process.env.NODE_ENV = 'test';
|
|
82
|
+
const inTest = getQueue(name);
|
|
83
|
+
|
|
84
|
+
process.env.NODE_ENV = 'staging';
|
|
85
|
+
const inStaging = getQueue(name);
|
|
86
|
+
|
|
87
|
+
expect(inStaging).not.toBe(inTest);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
describe('Queue', () => {
|
|
93
|
+
let queueName;
|
|
94
|
+
let queue;
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
queueName = TestHelpers.Faker.Text.randomString();
|
|
98
|
+
queue = getQueue(queueName);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
describe('#queueName', () => {
|
|
103
|
+
it ('returns the raw queue name', () => {
|
|
104
|
+
expect(queue.queueName).toEqual(queueName);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
describe('#fullQueueName', () => {
|
|
110
|
+
it ('appends the current NODE_ENV', () => {
|
|
111
|
+
expect(queue.fullQueueName).toEqual(`${queueName}-test`);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
describe('#logger', () => {
|
|
117
|
+
it ('uses the queue scope keyed by fullQueueName', () => {
|
|
118
|
+
const child = queue.logger;
|
|
119
|
+
|
|
120
|
+
expect(logger.queue.child).toHaveBeenCalledWith(`${queueName}-test`);
|
|
121
|
+
expect(child).toBe(logger.queue.child.mock.results[0].value);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
describe('lazy BullMQ.Queue construction', () => {
|
|
127
|
+
it ('does not construct the BullMQ queue until a method needs it', () => {
|
|
128
|
+
expect(bullmqMock.instances.queues).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it ('constructs the BullMQ queue with fullQueueName and connection options on first use', async () => {
|
|
132
|
+
await queue.numQueuedJobs();
|
|
133
|
+
|
|
134
|
+
expect(bullmqMock.instances.queues).toHaveLength(1);
|
|
135
|
+
const instance = bullmqMock.instances.queues[0];
|
|
136
|
+
expect(instance.name).toEqual(`${queueName}-test`);
|
|
137
|
+
expect(instance.options).toEqual({ connection: expectedConnection() });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it ('reuses the same BullMQ queue across multiple method calls', async () => {
|
|
141
|
+
await queue.numQueuedJobs();
|
|
142
|
+
await queue.numActiveJobs();
|
|
143
|
+
await queue.close();
|
|
144
|
+
|
|
145
|
+
expect(bullmqMock.instances.queues).toHaveLength(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it ('registers an error handler on the BullMQ queue', async () => {
|
|
149
|
+
await queue.numQueuedJobs();
|
|
150
|
+
|
|
151
|
+
const instance = bullmqMock.instances.queues[0];
|
|
152
|
+
expect(instance.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
describe('#queueJob', () => {
|
|
158
|
+
it ('adds the job to the BullMQ queue with a generated UUID, args, and settings', async () => {
|
|
159
|
+
const id = TestHelpers.Faker.Text.randomString();
|
|
160
|
+
randomUUID.mockReturnValue(id);
|
|
161
|
+
const args = [TestHelpers.Faker.Text.randomString(), TestHelpers.Faker.Math.randomNumber()];
|
|
162
|
+
const settings = { attempts: TestHelpers.Faker.Math.randomNumber(10) };
|
|
163
|
+
const jobClass = { name: TestHelpers.Faker.Text.randomString() };
|
|
164
|
+
|
|
165
|
+
await queue.queueJob(jobClass, args, settings);
|
|
166
|
+
|
|
167
|
+
const instance = bullmqMock.instances.queues[0];
|
|
168
|
+
expect(instance.add).toHaveBeenCalledTimes(1);
|
|
169
|
+
expect(instance.add).toHaveBeenCalledWith(jobClass.name, { id, args }, settings);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
describe.each([
|
|
175
|
+
['close', 'close', []],
|
|
176
|
+
['pause', 'pause', []],
|
|
177
|
+
['resume', 'resume', []],
|
|
178
|
+
])('#%s', (method, bullMethod, expectedArgs) => {
|
|
179
|
+
it (`calls bullmq queue.${bullMethod}`, async () => {
|
|
180
|
+
await queue[method]();
|
|
181
|
+
|
|
182
|
+
const instance = bullmqMock.instances.queues[0];
|
|
183
|
+
expect(instance[bullMethod]).toHaveBeenCalledWith(...expectedArgs);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
describe('count-style queries', () => {
|
|
189
|
+
it.each([
|
|
190
|
+
['numQueuedJobs', 'count'],
|
|
191
|
+
['numActiveJobs', 'getActiveCount'],
|
|
192
|
+
['numFailedJobs', 'getFailedCount'],
|
|
193
|
+
])('#%s delegates to bullmq queue.%s and returns its value', async (method, bullMethod) => {
|
|
194
|
+
const expectedCount = TestHelpers.Faker.Math.randomNumber();
|
|
195
|
+
const queueInstance = (() => { queue[method](); return bullmqMock.instances.queues[0]; })();
|
|
196
|
+
queueInstance[bullMethod].mockResolvedValue(expectedCount);
|
|
197
|
+
|
|
198
|
+
const result = await queue[method]();
|
|
199
|
+
|
|
200
|
+
expect(result).toEqual(expectedCount);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
describe('#numWorkers', () => {
|
|
206
|
+
it ('returns the length of bullmq queue.getWorkers()', async () => {
|
|
207
|
+
await queue.numQueuedJobs();
|
|
208
|
+
const instance = bullmqMock.instances.queues[0];
|
|
209
|
+
const workers = Array.from({ length: TestHelpers.Faker.Math.randomNumber(10) }, () => ({}));
|
|
210
|
+
instance.getWorkers.mockResolvedValue(workers);
|
|
211
|
+
|
|
212
|
+
await expect(queue.numWorkers()).resolves.toEqual(workers.length);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
describe('clear methods', () => {
|
|
218
|
+
it ('#clearFailedJobs calls bullmq clean(0, null, "failed")', async () => {
|
|
219
|
+
await queue.clearFailedJobs();
|
|
220
|
+
|
|
221
|
+
const instance = bullmqMock.instances.queues[0];
|
|
222
|
+
expect(instance.clean).toHaveBeenCalledWith(0, null, 'failed');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it ('#clearCompletedJobs calls bullmq clean(0, null, "completed")', async () => {
|
|
226
|
+
await queue.clearCompletedJobs();
|
|
227
|
+
|
|
228
|
+
const instance = bullmqMock.instances.queues[0];
|
|
229
|
+
expect(instance.clean).toHaveBeenCalledWith(0, null, 'completed');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it ('#clearQueuedJobs calls bullmq drain()', async () => {
|
|
233
|
+
await queue.clearQueuedJobs();
|
|
234
|
+
|
|
235
|
+
const instance = bullmqMock.instances.queues[0];
|
|
236
|
+
expect(instance.drain).toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
describe('job-list queries', () => {
|
|
242
|
+
function buildBullJob(overrides = {}) {
|
|
243
|
+
return {
|
|
244
|
+
queue: { name: TestHelpers.Faker.Text.randomString() },
|
|
245
|
+
name: TestHelpers.Faker.Text.randomString(),
|
|
246
|
+
data: { args: [TestHelpers.Faker.Text.randomString()] },
|
|
247
|
+
id: TestHelpers.Faker.Text.randomString(),
|
|
248
|
+
attemptsMade: TestHelpers.Faker.Math.randomNumber(10),
|
|
249
|
+
attemptsStarted: TestHelpers.Faker.Math.randomNumber(10),
|
|
250
|
+
progress: TestHelpers.Faker.Math.randomNumber(100),
|
|
251
|
+
failedReason: TestHelpers.Faker.Text.randomString(),
|
|
252
|
+
...overrides,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function expectedShape(bullJob) {
|
|
257
|
+
return {
|
|
258
|
+
queueName: bullJob.queue.name,
|
|
259
|
+
name: bullJob.name,
|
|
260
|
+
args: bullJob.data,
|
|
261
|
+
id: bullJob.id,
|
|
262
|
+
attemptsMade: bullJob.attemptsMade,
|
|
263
|
+
attemptsStarted: bullJob.attemptsStarted,
|
|
264
|
+
progress: bullJob.progress,
|
|
265
|
+
failedReason: bullJob.failedReason,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
it ('#getQueuedJobs maps getJobs(["paused","wait","prioritized","delayed"]) to formatted shape', async () => {
|
|
271
|
+
const bullJob1 = buildBullJob();
|
|
272
|
+
const bullJob2 = buildBullJob();
|
|
273
|
+
await queue.numQueuedJobs();
|
|
274
|
+
const instance = bullmqMock.instances.queues[0];
|
|
275
|
+
instance.getJobs.mockResolvedValue([bullJob1, bullJob2]);
|
|
276
|
+
|
|
277
|
+
const result = await queue.getQueuedJobs();
|
|
278
|
+
|
|
279
|
+
expect(instance.getJobs).toHaveBeenCalledWith(['paused', 'wait', 'prioritized', 'delayed']);
|
|
280
|
+
expect(result).toEqual([expectedShape(bullJob1), expectedShape(bullJob2)]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it ('#getActiveJobs maps getActive() to formatted shape', async () => {
|
|
284
|
+
const bullJob = buildBullJob();
|
|
285
|
+
await queue.numQueuedJobs();
|
|
286
|
+
const instance = bullmqMock.instances.queues[0];
|
|
287
|
+
instance.getActive.mockResolvedValue([bullJob]);
|
|
288
|
+
|
|
289
|
+
const result = await queue.getActiveJobs();
|
|
290
|
+
|
|
291
|
+
expect(result).toEqual([expectedShape(bullJob)]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it ('#getFailedJobs maps getFailed() to formatted shape', async () => {
|
|
295
|
+
const bullJob = buildBullJob();
|
|
296
|
+
await queue.numQueuedJobs();
|
|
297
|
+
const instance = bullmqMock.instances.queues[0];
|
|
298
|
+
instance.getFailed.mockResolvedValue([bullJob]);
|
|
299
|
+
|
|
300
|
+
const result = await queue.getFailedJobs();
|
|
301
|
+
|
|
302
|
+
expect(result).toEqual([expectedShape(bullJob)]);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
describe('error handler', () => {
|
|
308
|
+
it ('forwards arguments from the bullmq error event to logger.error', async () => {
|
|
309
|
+
await queue.numQueuedJobs();
|
|
310
|
+
const instance = bullmqMock.instances.queues[0];
|
|
311
|
+
const errorListener = bullmqMock.listenerFor(instance, 'error');
|
|
312
|
+
const err = new Error(TestHelpers.Faker.Text.randomString());
|
|
313
|
+
|
|
314
|
+
errorListener(err);
|
|
315
|
+
|
|
316
|
+
const child = logger.queue.child.mock.results[0].value;
|
|
317
|
+
expect(child.error).toHaveBeenCalledWith(err);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|