ti2 1.0.94 → 1.0.95
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/tests.yml +7 -3
- package/api.yml +159 -0
- package/cli.js +106 -0
- package/controllers/__tests__/admin.js +1 -1
- package/controllers/__tests__/allotments.js +1 -2
- package/controllers/__tests__/app.js +1 -1
- package/controllers/__tests__/bookings.js +13 -3
- package/controllers/__tests__/cronjobs.js +394 -0
- package/controllers/__tests__/ping.js +1 -1
- package/controllers/__tests__/productsSearch-background.js +1 -1
- package/controllers/__tests__/user.js +1 -1
- package/controllers/app.js +269 -6
- package/index.js +2 -3
- package/migrations/20240130000000-add-token-to-cronjobs.js +12 -0
- package/migrations/20250528_create_apiCronjobs.js +61 -0
- package/models/apiCronJobs.js +96 -0
- package/package.json +2 -2
- package/test/utils.js +37 -10
- package/worker/__tests__/job-callback.js +0 -22
- package/worker/index.js +41 -30
- package/worker/queue.js +69 -5
|
@@ -7,7 +7,7 @@ jobs:
|
|
|
7
7
|
timeout-minutes: 10
|
|
8
8
|
runs-on: ubuntu-latest
|
|
9
9
|
container:
|
|
10
|
-
image: node:16.
|
|
10
|
+
image: node:16.20.0
|
|
11
11
|
services:
|
|
12
12
|
mysql:
|
|
13
13
|
image: mysql
|
|
@@ -31,13 +31,17 @@ jobs:
|
|
|
31
31
|
- uses: actions/checkout@v2
|
|
32
32
|
- uses: actions/setup-node@v1
|
|
33
33
|
with:
|
|
34
|
-
node-version:
|
|
34
|
+
node-version: 16
|
|
35
35
|
- name: install dependencies
|
|
36
36
|
run: npm ci
|
|
37
37
|
- name: create/migrate the database
|
|
38
38
|
run: npx sequelize db:create && npx sequelize db:migrate
|
|
39
39
|
- name: seed the database
|
|
40
40
|
run: npx sequelize db:seed:all
|
|
41
|
-
-
|
|
41
|
+
- name: start worker process
|
|
42
|
+
run: RUNNER_TRACKING_ID="" node ./test/worker.js > worker.log 2>&1 &
|
|
42
43
|
- name: run the tests
|
|
43
44
|
run: npx jest -i --coverage --forceExit
|
|
45
|
+
- name: display worker logs
|
|
46
|
+
if: ${{ failure() }}
|
|
47
|
+
run: cat worker.log
|
package/api.yml
CHANGED
|
@@ -13,6 +13,78 @@ components:
|
|
|
13
13
|
scheme: bearer
|
|
14
14
|
bearerFormat: JWT
|
|
15
15
|
schemas:
|
|
16
|
+
CronJob:
|
|
17
|
+
type: object
|
|
18
|
+
properties:
|
|
19
|
+
id:
|
|
20
|
+
type: string
|
|
21
|
+
description: uuid of the crojob to remove
|
|
22
|
+
example: '550e8400-e29b-41d4-a716-446655440000'
|
|
23
|
+
method:
|
|
24
|
+
type: string
|
|
25
|
+
description: HTTP method for the API call
|
|
26
|
+
example: 'POST'
|
|
27
|
+
enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
|
|
28
|
+
url:
|
|
29
|
+
type: string
|
|
30
|
+
description: URL path for the API call
|
|
31
|
+
example: '/products/travelgate-hotelx/123/search'
|
|
32
|
+
cron:
|
|
33
|
+
type: string
|
|
34
|
+
description: Cron expression for job scheduling
|
|
35
|
+
example: '0 0 * * *'
|
|
36
|
+
userId:
|
|
37
|
+
type: string
|
|
38
|
+
description: User ID associated with the job
|
|
39
|
+
body:
|
|
40
|
+
type: object
|
|
41
|
+
description: body for the scheduled rquest
|
|
42
|
+
removeOnComplete:
|
|
43
|
+
type: boolean
|
|
44
|
+
description: Whether the job should be removed from the queue after successful completion (for one-off executions).
|
|
45
|
+
default: false
|
|
46
|
+
CronJobList:
|
|
47
|
+
type: object
|
|
48
|
+
properties:
|
|
49
|
+
jobs:
|
|
50
|
+
type: array
|
|
51
|
+
items:
|
|
52
|
+
$ref: '#/components/schemas/CronJob'
|
|
53
|
+
CronJobCreate:
|
|
54
|
+
type: object
|
|
55
|
+
required:
|
|
56
|
+
- method
|
|
57
|
+
- url
|
|
58
|
+
- cron
|
|
59
|
+
properties:
|
|
60
|
+
method:
|
|
61
|
+
type: string
|
|
62
|
+
description: HTTP method for the API call
|
|
63
|
+
example: 'POST'
|
|
64
|
+
enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
|
|
65
|
+
url:
|
|
66
|
+
type: string
|
|
67
|
+
description: URL path for the API call
|
|
68
|
+
example: '/products/travelgate-hotelx/123/search'
|
|
69
|
+
cron:
|
|
70
|
+
type: string
|
|
71
|
+
description: Cron expression for job scheduling
|
|
72
|
+
example: '0 0 * * *'
|
|
73
|
+
callbackUrl:
|
|
74
|
+
type: string
|
|
75
|
+
description: Optional URL to call after job completion
|
|
76
|
+
example: 'https://example.com/callback'
|
|
77
|
+
payload:
|
|
78
|
+
type: object
|
|
79
|
+
description: Optional payload for the API call
|
|
80
|
+
example:
|
|
81
|
+
startDate: '01/01/2023'
|
|
82
|
+
endDate: '03/04/2023'
|
|
83
|
+
keyPath: 'MAGLUX|7CQLDACMAGLUXDELSUI'
|
|
84
|
+
removeOnComplete:
|
|
85
|
+
type: boolean
|
|
86
|
+
description: Whether the job should be removed from the queue after successful completion (for one-off executions).
|
|
87
|
+
default: false
|
|
16
88
|
ServerInfo:
|
|
17
89
|
type: object
|
|
18
90
|
properties:
|
|
@@ -404,6 +476,93 @@ components:
|
|
|
404
476
|
type: boolean
|
|
405
477
|
default: false
|
|
406
478
|
paths:
|
|
479
|
+
/cronjobs/{userId}:
|
|
480
|
+
get:
|
|
481
|
+
security:
|
|
482
|
+
- bearerAuth: ['admin', 'user']
|
|
483
|
+
tags:
|
|
484
|
+
- cronjobs
|
|
485
|
+
summary: List cronjobs for a user
|
|
486
|
+
operationId: listCronjobs
|
|
487
|
+
parameters:
|
|
488
|
+
- name: userId
|
|
489
|
+
in: path
|
|
490
|
+
description: User ID to list cronjobs for
|
|
491
|
+
required: true
|
|
492
|
+
schema:
|
|
493
|
+
type: string
|
|
494
|
+
responses:
|
|
495
|
+
'200':
|
|
496
|
+
description: List of cronjobs
|
|
497
|
+
content:
|
|
498
|
+
application/json:
|
|
499
|
+
schema:
|
|
500
|
+
$ref: '#/components/schemas/CronJobList'
|
|
501
|
+
post:
|
|
502
|
+
security:
|
|
503
|
+
- bearerAuth: ['admin', 'user']
|
|
504
|
+
tags:
|
|
505
|
+
- cronjobs
|
|
506
|
+
summary: Create a new cronjob
|
|
507
|
+
operationId: createCronjob
|
|
508
|
+
parameters:
|
|
509
|
+
- name: userId
|
|
510
|
+
in: path
|
|
511
|
+
description: User ID to create cronjob for
|
|
512
|
+
required: true
|
|
513
|
+
schema:
|
|
514
|
+
type: string
|
|
515
|
+
requestBody:
|
|
516
|
+
required: true
|
|
517
|
+
content:
|
|
518
|
+
application/json:
|
|
519
|
+
schema:
|
|
520
|
+
$ref: '#/components/schemas/CronJobCreate'
|
|
521
|
+
responses:
|
|
522
|
+
'200':
|
|
523
|
+
description: Created cronjob
|
|
524
|
+
content:
|
|
525
|
+
application/json:
|
|
526
|
+
schema:
|
|
527
|
+
$ref: '#/components/schemas/CronJob'
|
|
528
|
+
/cronjobs/{userId}/{id}:
|
|
529
|
+
delete:
|
|
530
|
+
security:
|
|
531
|
+
- bearerAuth: ['admin', 'user']
|
|
532
|
+
tags:
|
|
533
|
+
- cronjobs
|
|
534
|
+
summary: Delete a cronjob
|
|
535
|
+
operationId: deleteCronjob
|
|
536
|
+
parameters:
|
|
537
|
+
- name: userId
|
|
538
|
+
in: path
|
|
539
|
+
description: User ID the cronjob belongs to
|
|
540
|
+
required: true
|
|
541
|
+
schema:
|
|
542
|
+
type: string
|
|
543
|
+
- name: id
|
|
544
|
+
in: path
|
|
545
|
+
description: Cron job ID (UUID) to delete
|
|
546
|
+
required: true
|
|
547
|
+
schema:
|
|
548
|
+
type: string
|
|
549
|
+
responses:
|
|
550
|
+
'404':
|
|
551
|
+
description: Cronjob not found
|
|
552
|
+
content:
|
|
553
|
+
application/json:
|
|
554
|
+
schema:
|
|
555
|
+
type: object
|
|
556
|
+
properties:
|
|
557
|
+
message:
|
|
558
|
+
type: string
|
|
559
|
+
example: 'Cronjob not found'
|
|
560
|
+
'200':
|
|
561
|
+
description: Success status
|
|
562
|
+
content:
|
|
563
|
+
application/json:
|
|
564
|
+
schema:
|
|
565
|
+
$ref: '#/components/schemas/Status'
|
|
407
566
|
/ping:
|
|
408
567
|
get:
|
|
409
568
|
tags:
|
package/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ const { Command, Argument } = require('commander');
|
|
|
5
5
|
const { migrateApp } = require('./controllers/app')();
|
|
6
6
|
const { migrate } = require('./controllers/admin');
|
|
7
7
|
const { decrypt, encrypt } = require('./lib/security');
|
|
8
|
+
const { queue, redisResults } = require('./worker/queue');
|
|
8
9
|
|
|
9
10
|
const program = new Command();
|
|
10
11
|
|
|
@@ -55,4 +56,109 @@ program.command('encrypt')
|
|
|
55
56
|
process.exit(0);
|
|
56
57
|
});
|
|
57
58
|
|
|
59
|
+
program.command('queue:list [pattern]')
|
|
60
|
+
.description('List jobs in the queue, optionally filtering by ID pattern (e.g., "jobId*", "*suffix", "prefix*suffix")')
|
|
61
|
+
.action(async (pattern) => {
|
|
62
|
+
try {
|
|
63
|
+
await queue.isReady(); // Ensure queue is ready
|
|
64
|
+
const jobTypes = ['waiting', 'active', 'completed', 'failed', 'delayed', 'paused'];
|
|
65
|
+
let message = `Fetching jobs of types: ${jobTypes.join(', ')}...`;
|
|
66
|
+
if (pattern) {
|
|
67
|
+
message += ` matching pattern "${pattern}"`;
|
|
68
|
+
}
|
|
69
|
+
console.log(message);
|
|
70
|
+
let jobs = await queue.getJobs(jobTypes);
|
|
71
|
+
|
|
72
|
+
if (pattern) {
|
|
73
|
+
const regexPatternText = '^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$';
|
|
74
|
+
const regexPattern = new RegExp(regexPatternText);
|
|
75
|
+
console.log(`Using filter regex: ${regexPattern}`);
|
|
76
|
+
jobs = jobs.filter(job => regexPattern.test(job.id.toString()));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (jobs.length === 0) {
|
|
80
|
+
if (pattern) {
|
|
81
|
+
console.log(`No jobs found matching pattern "${pattern}".`);
|
|
82
|
+
} else {
|
|
83
|
+
console.log('No jobs found in the queue.');
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
console.log(`Found ${jobs.length} jobs:`);
|
|
87
|
+
for (const job of jobs) {
|
|
88
|
+
const state = await job.getState();
|
|
89
|
+
const progress = job.progress();
|
|
90
|
+
const timestamp = new Date(job.timestamp).toISOString();
|
|
91
|
+
console.log(`\n- ID: ${job.id}`);
|
|
92
|
+
console.log(` State: ${state}`);
|
|
93
|
+
console.log(` Progress: ${progress}`);
|
|
94
|
+
console.log(` Added: ${timestamp}`);
|
|
95
|
+
console.log(` Name (Queue): ${job.name}`);
|
|
96
|
+
console.log(` Data: ${JSON.stringify(job.data, null, 2)}`);
|
|
97
|
+
if (job.opts.repeat) {
|
|
98
|
+
console.log(` Repeat Options: ${JSON.stringify(job.opts.repeat)}`);
|
|
99
|
+
}
|
|
100
|
+
if (state === 'failed' && job.failedReason) {
|
|
101
|
+
console.log(` Failed Reason: ${job.failedReason}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Error listing queue jobs:', error);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
process.exit(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
program.command('queue:remove')
|
|
113
|
+
.description('Remove job instances from the queue by ID or pattern (e.g., "jobId*", "*suffix", "prefix*suffix")')
|
|
114
|
+
.argument('<pattern>', 'Job ID or glob-like pattern for job IDs to remove')
|
|
115
|
+
.action(async (pattern) => {
|
|
116
|
+
try {
|
|
117
|
+
await queue.isReady(); // Ensure queue is ready
|
|
118
|
+
console.log(`Attempting to remove job instances matching pattern: "${pattern}"`);
|
|
119
|
+
|
|
120
|
+
const jobTypes = ['waiting', 'active', 'completed', 'failed', 'delayed', 'paused'];
|
|
121
|
+
const jobs = await queue.getJobs(jobTypes);
|
|
122
|
+
let removedCount = 0;
|
|
123
|
+
const removedJobIds = [];
|
|
124
|
+
|
|
125
|
+
const regexPatternText = '^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$';
|
|
126
|
+
const regexPattern = new RegExp(regexPatternText);
|
|
127
|
+
|
|
128
|
+
console.log(`Using regex: ${regexPattern}`);
|
|
129
|
+
|
|
130
|
+
for (const job of jobs) {
|
|
131
|
+
if (regexPattern.test(job.id.toString())) {
|
|
132
|
+
try {
|
|
133
|
+
await job.remove();
|
|
134
|
+
removedJobIds.push(job.id.toString());
|
|
135
|
+
removedCount++;
|
|
136
|
+
console.log(`Removed job ${job.id} from Bull queue.`);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(`Failed to remove job ${job.id} from Bull queue:`, e.message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (removedCount > 0) {
|
|
144
|
+
console.log(`Successfully removed ${removedCount} job instance(s) from Bull queue.`);
|
|
145
|
+
if (removedJobIds.length > 0) {
|
|
146
|
+
console.log(`Attempting to remove ${removedJobIds.length} result(s) from Redis for the removed jobs...`);
|
|
147
|
+
try {
|
|
148
|
+
const delCount = await redisResults.del(removedJobIds);
|
|
149
|
+
console.log(`Removed ${delCount} result(s) from Redis.`);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.error('Failed to remove results from Redis:', e.message);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
console.log(`No job instances found matching pattern "${pattern}".`);
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('Error removing queue jobs:', error);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
process.exit(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
58
164
|
program.parse();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* globals beforeAll describe it expect */
|
|
2
2
|
|
|
3
3
|
const chance = require('chance').Chance();
|
|
4
|
-
const testUtils = require('../../test/utils');
|
|
5
4
|
const slugify = require('../../test/slugify');
|
|
6
5
|
|
|
7
6
|
const { env: { adminKey } } = process;
|
|
8
7
|
|
|
9
8
|
describe('admin', () => {
|
|
9
|
+
const testUtils = require('../../test/utils');
|
|
10
10
|
const appName = slugify(
|
|
11
11
|
chance.company(),
|
|
12
12
|
);
|
|
@@ -3,11 +3,10 @@ const axios = require('axios');
|
|
|
3
3
|
const MockAdapter = require('axios-mock-adapter');
|
|
4
4
|
const chance = require('chance').Chance();
|
|
5
5
|
|
|
6
|
-
const testUtils = require('../../test/utils');
|
|
7
|
-
|
|
8
6
|
const { env: { adminKey } } = process;
|
|
9
7
|
|
|
10
8
|
describe('allotment', () => {
|
|
9
|
+
const testUtils = require('../../test/utils');
|
|
11
10
|
const newApp = {
|
|
12
11
|
name: 'tourplantest',
|
|
13
12
|
packageName: 'ti2-tourplanTest',
|
|
@@ -3,13 +3,13 @@ const chance = require('chance').Chance();
|
|
|
3
3
|
const jwt = require('jwt-promise');
|
|
4
4
|
const R = require('ramda');
|
|
5
5
|
|
|
6
|
-
const testUtils = require('../../test/utils');
|
|
7
6
|
const slugify = require('../../test/slugify');
|
|
8
7
|
let appController = require('../app');
|
|
9
8
|
|
|
10
9
|
const { env: { adminKey, jwtSecret } } = process;
|
|
11
10
|
|
|
12
11
|
describe('app', () => {
|
|
12
|
+
const testUtils = require('../../test/utils');
|
|
13
13
|
const appName = slugify(
|
|
14
14
|
chance.company(),
|
|
15
15
|
);
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const chance = require('chance').Chance();
|
|
4
4
|
const hash = require('object-hash');
|
|
5
|
-
const testUtils = require('../../test/utils');
|
|
6
5
|
const cache = require('../../cache');
|
|
7
6
|
|
|
8
7
|
const { env: { adminKey } } = process;
|
|
9
8
|
|
|
10
9
|
describe('user: bookings controller', () => {
|
|
10
|
+
const testUtils = require('../../test/utils');
|
|
11
11
|
const newApp = {
|
|
12
12
|
name: 'travelgate',
|
|
13
13
|
packageName: 'ti2-travelgate',
|
|
@@ -344,8 +344,18 @@ describe('user: bookings controller', () => {
|
|
|
344
344
|
expect(products.length).toBe(2); // Based on existing test expectations
|
|
345
345
|
});
|
|
346
346
|
|
|
347
|
-
// Plugin should only be called once due to lock mechanism
|
|
348
|
-
expect(plugins[0].searchProducts).
|
|
347
|
+
// Plugin should only be called once due to lock mechanism, and with correct context
|
|
348
|
+
expect(plugins[0].searchProducts).toHaveBeenCalledWith(
|
|
349
|
+
expect.objectContaining({
|
|
350
|
+
token: expect.objectContaining({
|
|
351
|
+
client: 'tourconnect',
|
|
352
|
+
endpoint: expect.stringContaining('https://api.travelgatex.com'),
|
|
353
|
+
}),
|
|
354
|
+
userId: expect.stringContaining(userId),
|
|
355
|
+
payload: {},
|
|
356
|
+
typeDefsAndQueries: expect.any(Object)
|
|
357
|
+
})
|
|
358
|
+
);
|
|
349
359
|
});
|
|
350
360
|
});
|
|
351
361
|
});
|