ti2 1.0.93 → 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 +5 -6
- 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
package/controllers/app.js
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
const jwt = require('
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
2
|
const { omit } = require('ramda');
|
|
3
3
|
const assert = require('assert');
|
|
4
4
|
const { Umzug, SequelizeStorage } = require('umzug');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const Sequelize = require('sequelize');
|
|
7
|
-
const fs = require('fs')
|
|
7
|
+
const fs = require('fs'); // Changed to synchronous fs for initial load
|
|
8
|
+
const yaml = require('js-yaml');
|
|
8
9
|
const bb = require('bluebird');
|
|
9
10
|
const R = require('ramda');
|
|
10
11
|
|
|
12
|
+
// Load OpenAPI schema
|
|
13
|
+
let openApiSchema = null;
|
|
14
|
+
try {
|
|
15
|
+
const schemaPath = path.join(__dirname, '..', 'api.yml'); // Assuming api.yml is in the parent directory of controllers
|
|
16
|
+
const schemaFile = fs.readFileSync(schemaPath, 'utf8');
|
|
17
|
+
openApiSchema = yaml.load(schemaFile);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.error('Failed to load OpenAPI schema:', e);
|
|
20
|
+
// Handle error appropriately, perhaps by preventing the app from starting
|
|
21
|
+
// or by using a default empty schema to prevent crashes later.
|
|
22
|
+
// For now, we'll let it potentially crash if openApiSchema is null and accessed.
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
const sqldb = require('../models');
|
|
12
26
|
const {
|
|
13
27
|
queue,
|
|
14
28
|
addJob,
|
|
15
29
|
jobStatus,
|
|
30
|
+
removeJob,
|
|
16
31
|
} = require('../worker/queue');
|
|
17
32
|
|
|
18
33
|
const { env: { jwtSecret } } = process;
|
|
@@ -96,7 +111,7 @@ const createAppToken = async (req, res, next) => {
|
|
|
96
111
|
if (userAppKeyDup) await userAppKeyDup.destroy();
|
|
97
112
|
const newAppKey = await sqldb.UserAppKey.create(payload);
|
|
98
113
|
// create any cronjobs related to the app
|
|
99
|
-
|
|
114
|
+
await bb.each(req.app.plugins, async plugin => {
|
|
100
115
|
if (Array.isArray(plugin.jobs)) {
|
|
101
116
|
const validJobs = plugin.jobs.filter(job => Boolean(job.cron) && Boolean(job.method));
|
|
102
117
|
await bb.each(validJobs, async job => {
|
|
@@ -151,6 +166,7 @@ const createAppToken = async (req, res, next) => {
|
|
|
151
166
|
}
|
|
152
167
|
});
|
|
153
168
|
|
|
169
|
+
|
|
154
170
|
return res.json({ value: newAppKey.get('id').toString() });
|
|
155
171
|
} catch (err) {
|
|
156
172
|
return next(err);
|
|
@@ -297,7 +313,7 @@ const migrateApp = async ({ integrationId, action }) => {
|
|
|
297
313
|
'migrations',
|
|
298
314
|
);
|
|
299
315
|
try {
|
|
300
|
-
await fs.access(migrationsPath);
|
|
316
|
+
await fs.promises.access(migrationsPath);
|
|
301
317
|
} catch (err) {
|
|
302
318
|
throw Error(`Could not find any migrations for ${integrationId} on ${migrationsPath}`);
|
|
303
319
|
}
|
|
@@ -332,12 +348,10 @@ const migrateApp = async ({ integrationId, action }) => {
|
|
|
332
348
|
const getAppScheduledJobs = async ({
|
|
333
349
|
integrationId,
|
|
334
350
|
userId,
|
|
335
|
-
hint,
|
|
336
351
|
}) => {
|
|
337
352
|
const where = R.reject(R.isNil)({
|
|
338
353
|
pluginName: integrationId,
|
|
339
354
|
userId,
|
|
340
|
-
hint,
|
|
341
355
|
});
|
|
342
356
|
const jobs = await sqldb.CronJobs.findAll({
|
|
343
357
|
where,
|
|
@@ -487,6 +501,252 @@ const getAppToken = async (req, res, next) => {
|
|
|
487
501
|
return res.json({ token: await userAppKey.token });
|
|
488
502
|
};
|
|
489
503
|
|
|
504
|
+
const createCronjob = async (req, res, next) => {
|
|
505
|
+
const {
|
|
506
|
+
params: {
|
|
507
|
+
userId,
|
|
508
|
+
},
|
|
509
|
+
body: {
|
|
510
|
+
method,
|
|
511
|
+
url,
|
|
512
|
+
cron,
|
|
513
|
+
callbackUrl,
|
|
514
|
+
body,
|
|
515
|
+
removeOnComplete,
|
|
516
|
+
},
|
|
517
|
+
} = req;
|
|
518
|
+
|
|
519
|
+
let transaction;
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
// Validate required fields
|
|
523
|
+
if (!method || !url || !cron) {
|
|
524
|
+
const error = new Error('method, url, and cron are required fields');
|
|
525
|
+
error.status = 400;
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Validate URL path
|
|
530
|
+
const validUrlPattern = /^\/[\w\-\/\.]+$/;
|
|
531
|
+
if (!validUrlPattern.test(url)) {
|
|
532
|
+
const error = new Error('Invalid URL path');
|
|
533
|
+
error.status = 400;
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Helper function to find a matching path in the OpenAPI schema
|
|
538
|
+
const findMatchingOpenApiPath = (requestUrl, schemaPaths) => {
|
|
539
|
+
if (!schemaPaths) return null;
|
|
540
|
+
for (const schemaPathKey in schemaPaths) {
|
|
541
|
+
// Convert schemaPathKey to a regex: e.g., /products/{id} -> ^/products/[^/]+$
|
|
542
|
+
const regexPattern = '^' + schemaPathKey.replace(/{[^}]+}/g, '[^/]+') + '$';
|
|
543
|
+
const regex = new RegExp(regexPattern);
|
|
544
|
+
if (regex.test(requestUrl)) {
|
|
545
|
+
return schemaPathKey; // Return the original schema path key
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const matchedSchemaPathKey = findMatchingOpenApiPath(url, openApiSchema ? openApiSchema.paths : null);
|
|
552
|
+
|
|
553
|
+
// Validate URL and method against OpenAPI schema
|
|
554
|
+
if (!openApiSchema || !matchedSchemaPathKey || !openApiSchema.paths[matchedSchemaPathKey][method.toLowerCase()]) {
|
|
555
|
+
const errorMessage = openApiSchema && openApiSchema.paths ?
|
|
556
|
+
`Invalid URL ('${url}') or method ('${method}') combination not found in API schema, or schema path not matched.` :
|
|
557
|
+
'API schema not loaded, cannot validate URL and method.';
|
|
558
|
+
const error = new Error(errorMessage);
|
|
559
|
+
error.status = 400;
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Create a user token for the job test
|
|
564
|
+
const jobToken = jwt.sign({ userId }, process.env.jwtSecret);
|
|
565
|
+
|
|
566
|
+
// Start transaction
|
|
567
|
+
transaction = await sqldb.sequelize.transaction();
|
|
568
|
+
|
|
569
|
+
// Create job in database
|
|
570
|
+
const cronJobData = {
|
|
571
|
+
userId,
|
|
572
|
+
// bullJobId will be set by the hook
|
|
573
|
+
cron,
|
|
574
|
+
method,
|
|
575
|
+
url,
|
|
576
|
+
token: jobToken,
|
|
577
|
+
body: { // Ensure body includes callbackUrl if present, or is an empty object if body is null/undefined
|
|
578
|
+
...(body || {}),
|
|
579
|
+
...(callbackUrl ? { callbackUrl } : {}),
|
|
580
|
+
},
|
|
581
|
+
removeOnComplete: removeOnComplete || false, // Ensure it defaults to false if not provided
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const cronJob = await sqldb.ApiCronJobs.create(cronJobData, { transaction });
|
|
585
|
+
|
|
586
|
+
await transaction.commit();
|
|
587
|
+
// The cronJob instance returned here should have bullJobId populated by the hook
|
|
588
|
+
return res.json(R.omit(['token'], cronJob.get({ plain:true })));
|
|
589
|
+
} catch (err) {
|
|
590
|
+
if (transaction) await transaction.rollback();
|
|
591
|
+
// No need to manually cleanup bullJobId here.
|
|
592
|
+
// If hook failed, transaction is rolled back by the hook re-throwing,
|
|
593
|
+
// or by create itself failing.
|
|
594
|
+
// If create itself failed before hook, no bull job was created.
|
|
595
|
+
return next(err);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const listCronjobs = async (req, res, next) => {
|
|
600
|
+
const {
|
|
601
|
+
params: {
|
|
602
|
+
userId,
|
|
603
|
+
},
|
|
604
|
+
} = req;
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
// Get all jobs from Bull queue
|
|
608
|
+
const bullJobs = await queue.getJobs(['active', 'wait', 'delayed']);
|
|
609
|
+
const bullJobIds = new Set(bullJobs.map(job => job.id));
|
|
610
|
+
|
|
611
|
+
// Get repeatable jobs
|
|
612
|
+
const repeatableJobs = await queue.getRepeatableJobs();
|
|
613
|
+
const repeatablePatterns = new Set(repeatableJobs.map(job => job.cron));
|
|
614
|
+
|
|
615
|
+
// Get jobs from database
|
|
616
|
+
const jobs = await sqldb.ApiCronJobs.findAll({
|
|
617
|
+
where: {
|
|
618
|
+
userId,
|
|
619
|
+
},
|
|
620
|
+
attributes: { exclude: ['token'] },
|
|
621
|
+
raw: true,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// Add inQueue status to jobs
|
|
625
|
+
const jobsWithStatus = jobs.map(job => {
|
|
626
|
+
const jobIdParts = job.bullJobId.split(':');
|
|
627
|
+
const isRepeatJob = jobIdParts[0] === 'repeat';
|
|
628
|
+
let isInQueue = false;
|
|
629
|
+
|
|
630
|
+
if (isRepeatJob) {
|
|
631
|
+
isInQueue = repeatablePatterns.has(job.cron);
|
|
632
|
+
} else {
|
|
633
|
+
isInQueue = bullJobIds.has(job.bullJobId);
|
|
634
|
+
}
|
|
635
|
+
return { ...job, inQueue: isInQueue };
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
return res.json({ jobs: jobsWithStatus });
|
|
639
|
+
} catch (err) {
|
|
640
|
+
return next(err);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const deleteCronjob = async (req, res, next) => {
|
|
645
|
+
const {
|
|
646
|
+
params: {
|
|
647
|
+
userId,
|
|
648
|
+
id, // Changed from jobId to id
|
|
649
|
+
},
|
|
650
|
+
} = req;
|
|
651
|
+
|
|
652
|
+
// Extract token from Authorization header
|
|
653
|
+
const getToken = (req) => {
|
|
654
|
+
if (
|
|
655
|
+
!req.header('Authorization') ||
|
|
656
|
+
req.header('Authorization').substring(0, 7) !== 'Bearer '
|
|
657
|
+
) return null;
|
|
658
|
+
return req.header('Authorization').split(' ')[1];
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const token = getToken(req);
|
|
662
|
+
let transaction; // Add transaction variable for deleteCronjob
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
// Check if user is admin
|
|
666
|
+
const isAdmin = token === process.env.adminKey;
|
|
667
|
+
|
|
668
|
+
// For non-admin users, verify token
|
|
669
|
+
if (!isAdmin) {
|
|
670
|
+
try {
|
|
671
|
+
const decoded = jwt.verify(token, process.env.jwtSecret);
|
|
672
|
+
// Verify userId in token matches requested userId
|
|
673
|
+
if (!decoded || !decoded.userId || decoded.userId !== userId) {
|
|
674
|
+
throw new Error('User ID mismatch');
|
|
675
|
+
}
|
|
676
|
+
} catch (err) {
|
|
677
|
+
return res.status(403).json({
|
|
678
|
+
status: 403,
|
|
679
|
+
message: 'Forbidden'
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Find the cronjob in the database
|
|
685
|
+
const cronJob = await sqldb.ApiCronJobs.findOne({
|
|
686
|
+
where: {
|
|
687
|
+
userId,
|
|
688
|
+
id, // Changed from bullJobId to id
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// If cronjob doesn't exist, return 404
|
|
693
|
+
if (!cronJob) {
|
|
694
|
+
const error = new Error('Cronjob not found');
|
|
695
|
+
error.status = 404;
|
|
696
|
+
return next(error);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// For non-admin users, verify ownership of the cronjob
|
|
700
|
+
if (!isAdmin && userId !== cronJob.userId) {
|
|
701
|
+
return res.status(403).json({
|
|
702
|
+
status: 403,
|
|
703
|
+
message: 'Forbidden'
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Start transaction
|
|
708
|
+
transaction = await sqldb.sequelize.transaction();
|
|
709
|
+
|
|
710
|
+
// Remove from database. The beforeDestroy hook in ApiCronJobs model
|
|
711
|
+
// will handle removing the job from the Bull queue.
|
|
712
|
+
// Pass the transaction to ensure atomicity.
|
|
713
|
+
// The destroy operation will only succeed if the beforeDestroy hook (including removeJob) succeeds.
|
|
714
|
+
const destroyedRows = await sqldb.ApiCronJobs.destroy({
|
|
715
|
+
where: {
|
|
716
|
+
// id, // cronJob instance is already fetched and verified
|
|
717
|
+
id: cronJob.id, // Use the id from the fetched cronJob instance
|
|
718
|
+
userId: cronJob.userId, // Ensure we are deleting the correct user's job
|
|
719
|
+
},
|
|
720
|
+
transaction, // Pass transaction to destroy
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// It's good practice to check if any rows were actually deleted,
|
|
724
|
+
// though in this flow, cronJob existence is checked prior.
|
|
725
|
+
if (destroyedRows === 0) {
|
|
726
|
+
// This case should ideally not be reached if cronJob was found
|
|
727
|
+
if (transaction) await transaction.rollback();
|
|
728
|
+
const error = new Error('Cronjob not found for deletion, though it was fetched prior.');
|
|
729
|
+
error.status = 404; // Or 500 for internal inconsistency
|
|
730
|
+
return next(error);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
await transaction.commit();
|
|
734
|
+
// Return success response
|
|
735
|
+
return res.json({ success: true });
|
|
736
|
+
} catch (err) {
|
|
737
|
+
if (transaction) await transaction.rollback(); // Rollback transaction on error
|
|
738
|
+
// Pass along errors with status
|
|
739
|
+
if (err.status) {
|
|
740
|
+
return next(err);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Otherwise, create a generic 500 error
|
|
744
|
+
const error = new Error(err.message || 'Internal Server Error');
|
|
745
|
+
error.status = 500;
|
|
746
|
+
return next(error); // Changed from next(err) to next(the new error object) for consistency
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
|
|
490
750
|
module.exports = plugins => ({
|
|
491
751
|
createAppToken,
|
|
492
752
|
getAppToken,
|
|
@@ -504,4 +764,7 @@ module.exports = plugins => ({
|
|
|
504
764
|
tokenTemplate,
|
|
505
765
|
validateAppToken: validateAppToken(plugins),
|
|
506
766
|
getAffiliates: getAffiliates(plugins),
|
|
767
|
+
createCronjob,
|
|
768
|
+
deleteCronjob,
|
|
769
|
+
listCronjobs,
|
|
507
770
|
});
|
package/index.js
CHANGED
|
@@ -191,7 +191,7 @@ module.exports = async ({
|
|
|
191
191
|
app.use(async (req, res, next) => {
|
|
192
192
|
const startHrTime = process.hrtime();
|
|
193
193
|
const body = composeBodyFromReq(req);
|
|
194
|
-
req.
|
|
194
|
+
req.requestBody = R.clone(body);
|
|
195
195
|
ti2Events.emit('request.start', body);
|
|
196
196
|
req.requestId = body.requestId;
|
|
197
197
|
res.on('finish', async () => {
|
|
@@ -201,7 +201,7 @@ module.exports = async ({
|
|
|
201
201
|
10,
|
|
202
202
|
);
|
|
203
203
|
ti2Events.emit('request.end', {
|
|
204
|
-
...
|
|
204
|
+
...req.requestBody,
|
|
205
205
|
cacheKey: req.cacheKey,
|
|
206
206
|
responseTimeInMs,
|
|
207
207
|
responseStatusCode: res.statusCode,
|
|
@@ -312,9 +312,8 @@ module.exports = async ({
|
|
|
312
312
|
addJob({
|
|
313
313
|
type: 'callback',
|
|
314
314
|
payload: callbackBody,
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
});
|
|
315
|
+
removeOnComplete: true,
|
|
316
|
+
}).then();
|
|
318
317
|
return originalJson.call(this, data);
|
|
319
318
|
};
|
|
320
319
|
}
|
|
@@ -328,7 +327,7 @@ module.exports = async ({
|
|
|
328
327
|
...cacheSettings['*'],
|
|
329
328
|
...(currentPlugin ? R.keys(filteredPluginCacheSettings) : []),
|
|
330
329
|
];
|
|
331
|
-
const body = req.
|
|
330
|
+
const body = req.requestBody;
|
|
332
331
|
if (cachingOperations.indexOf(body.operationId) > -1) {
|
|
333
332
|
const cacheBody = R.omit(['requestId', 'date'], body);
|
|
334
333
|
const cacheKey = hash(cacheBody);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
up: async (queryInterface, Sequelize) => {
|
|
3
|
+
await queryInterface.addColumn('CronJobs', 'token', {
|
|
4
|
+
type: Sequelize.STRING,
|
|
5
|
+
allowNull: true
|
|
6
|
+
});
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
down: async (queryInterface) => {
|
|
10
|
+
await queryInterface.removeColumn('CronJobs', 'token');
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
up: async (queryInterface, Sequelize) => {
|
|
3
|
+
// Create a new table with the updated schema
|
|
4
|
+
await queryInterface.createTable('ApiCronJobs', {
|
|
5
|
+
id: {
|
|
6
|
+
type: Sequelize.UUID,
|
|
7
|
+
defaultValue: Sequelize.UUIDV4,
|
|
8
|
+
primaryKey: true,
|
|
9
|
+
},
|
|
10
|
+
userId: {
|
|
11
|
+
type: Sequelize.STRING,
|
|
12
|
+
allowNull: false,
|
|
13
|
+
},
|
|
14
|
+
bullJobId: {
|
|
15
|
+
type: Sequelize.STRING,
|
|
16
|
+
allowNull: true,
|
|
17
|
+
},
|
|
18
|
+
cron: {
|
|
19
|
+
type: Sequelize.STRING,
|
|
20
|
+
allowNull: false,
|
|
21
|
+
},
|
|
22
|
+
method: {
|
|
23
|
+
type: Sequelize.STRING,
|
|
24
|
+
allowNull: false,
|
|
25
|
+
},
|
|
26
|
+
url: {
|
|
27
|
+
type: Sequelize.STRING,
|
|
28
|
+
allowNull: false,
|
|
29
|
+
},
|
|
30
|
+
callbackUrl: {
|
|
31
|
+
type: Sequelize.STRING,
|
|
32
|
+
allowNull: true,
|
|
33
|
+
},
|
|
34
|
+
token: {
|
|
35
|
+
type: Sequelize.STRING,
|
|
36
|
+
allowNull: false,
|
|
37
|
+
},
|
|
38
|
+
body: {
|
|
39
|
+
type: Sequelize.JSON,
|
|
40
|
+
allowNull: true,
|
|
41
|
+
},
|
|
42
|
+
removeOnComplete: {
|
|
43
|
+
type: Sequelize.BOOLEAN,
|
|
44
|
+
allowNull: false,
|
|
45
|
+
defaultValue: false,
|
|
46
|
+
},
|
|
47
|
+
createdAt: {
|
|
48
|
+
type: Sequelize.DATE,
|
|
49
|
+
allowNull: false,
|
|
50
|
+
},
|
|
51
|
+
updatedAt: {
|
|
52
|
+
type: Sequelize.DATE,
|
|
53
|
+
allowNull: false,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
down: async (queryInterface, Sequelize) => {
|
|
59
|
+
await queryInterface.dropTable('ApiCronJobs');
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const Sequelize = require('sequelize');
|
|
2
|
+
const db = require('./db');
|
|
3
|
+
|
|
4
|
+
const ApiCronJobs = db.define('ApiCronJobs', {
|
|
5
|
+
id: {
|
|
6
|
+
type: Sequelize.UUID,
|
|
7
|
+
defaultValue: Sequelize.UUIDV4,
|
|
8
|
+
primaryKey: true,
|
|
9
|
+
},
|
|
10
|
+
userId: {
|
|
11
|
+
type: Sequelize.STRING,
|
|
12
|
+
allowNull: false,
|
|
13
|
+
},
|
|
14
|
+
bullJobId: {
|
|
15
|
+
type: Sequelize.STRING,
|
|
16
|
+
// It might be null initially until the afterCreate hook successfully adds the job
|
|
17
|
+
allowNull: true,
|
|
18
|
+
},
|
|
19
|
+
cron: {
|
|
20
|
+
type: Sequelize.STRING,
|
|
21
|
+
allowNull: false,
|
|
22
|
+
},
|
|
23
|
+
method: {
|
|
24
|
+
type: Sequelize.STRING,
|
|
25
|
+
allowNull: false,
|
|
26
|
+
},
|
|
27
|
+
url: {
|
|
28
|
+
type: Sequelize.STRING,
|
|
29
|
+
allowNull: false,
|
|
30
|
+
},
|
|
31
|
+
token: {
|
|
32
|
+
type: Sequelize.STRING,
|
|
33
|
+
allowNull: false,
|
|
34
|
+
},
|
|
35
|
+
body: {
|
|
36
|
+
type: Sequelize.JSON,
|
|
37
|
+
allowNull: true, // Assuming body can be optional
|
|
38
|
+
},
|
|
39
|
+
removeOnComplete: {
|
|
40
|
+
type: Sequelize.BOOLEAN,
|
|
41
|
+
allowNull: false,
|
|
42
|
+
defaultValue: false,
|
|
43
|
+
}
|
|
44
|
+
}, {
|
|
45
|
+
hooks: {
|
|
46
|
+
afterCreate: async (apiCronJob, options) => {
|
|
47
|
+
const { addJob } = require('../worker/queue');
|
|
48
|
+
const jobPayload = {
|
|
49
|
+
type: 'api',
|
|
50
|
+
method: apiCronJob.method,
|
|
51
|
+
url: apiCronJob.url,
|
|
52
|
+
headers: {
|
|
53
|
+
'content-type': 'application/json',
|
|
54
|
+
'Authorization': `Bearer ${apiCronJob.token}`,
|
|
55
|
+
},
|
|
56
|
+
payload: (apiCronJob.body || {}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const jobParams = {
|
|
60
|
+
repeat: {
|
|
61
|
+
cron: apiCronJob.cron,
|
|
62
|
+
},
|
|
63
|
+
removeOnComplete: apiCronJob.removeOnComplete,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// The addJob must be part of the same transaction that created apiCronJob
|
|
68
|
+
const bullJobId = await addJob(jobPayload, jobParams, options.transaction);
|
|
69
|
+
// Update the instance with the bullJobId and save it within the same transaction
|
|
70
|
+
// Note: This will trigger update hooks if any are defined, which is generally fine.
|
|
71
|
+
// apiCronJob.bullJobId = bullJobId;
|
|
72
|
+
await apiCronJob.update({ bullJobId }, { transaction: options.transaction, hooks: false });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// If addJob or the subsequent save fails, the transaction should be rolled back
|
|
75
|
+
// by the controller that initiated it.
|
|
76
|
+
// We re-throw the error to ensure the transaction is rolled back.
|
|
77
|
+
console.error('Failed to create Bull job or save bullJobId:', err);
|
|
78
|
+
throw err; // This will trigger the rollback in the controller
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
beforeDestroy: async (apiCronJob, options) => {
|
|
82
|
+
const { removeJob } = require('../worker/queue');
|
|
83
|
+
try {
|
|
84
|
+
// The removeJob must be part of the same transaction
|
|
85
|
+
await removeJob(apiCronJob.bullJobId, options.transaction);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// If removeJob fails, the transaction should be rolled back.
|
|
88
|
+
console.error('Failed to remove Bull job:', err);
|
|
89
|
+
throw err; // This will trigger the rollback in the controller
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
module.exports = ApiCronJobs;
|
|
96
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ti2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.95",
|
|
4
4
|
"description": "Tourist Industry Exchange (TI2)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"axios": "^0.27.2",
|
|
29
29
|
"axios-curlirize": "^1.3.7",
|
|
30
30
|
"bluebird": "^3.7.2",
|
|
31
|
-
"bull": "^4.
|
|
31
|
+
"bull": "^4.16.5",
|
|
32
32
|
"commander": "^9.2.0",
|
|
33
33
|
"compression": "^1.7.4",
|
|
34
34
|
"eventemitter2": "^6.4.9",
|
package/test/utils.js
CHANGED
|
@@ -9,7 +9,10 @@ const Plugin = require('./plugin');
|
|
|
9
9
|
const slugify = require('./slugify');
|
|
10
10
|
const sqldb = require('../models/db');
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const jwt = require('jsonwebtoken');
|
|
13
|
+
const { env: { adminKey, jwtSecret } } = process;
|
|
14
|
+
|
|
15
|
+
const createUserToken = (userId) => jwt.sign({ userId }, jwtSecret);
|
|
13
16
|
|
|
14
17
|
afterAll(async () => {
|
|
15
18
|
await sqldb.connectionManager.close();
|
|
@@ -51,8 +54,14 @@ module.exports = async (appParams = {}) => {
|
|
|
51
54
|
} else {
|
|
52
55
|
resp = await request(app)[verb](url).query(query).send(payload);
|
|
53
56
|
}
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
if (resp.statusCode !== expectStatusCode) {
|
|
58
|
+
const error = new Error();
|
|
59
|
+
error.response = {
|
|
60
|
+
status: resp.statusCode,
|
|
61
|
+
data: resp.body
|
|
62
|
+
};
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
56
65
|
if (rawResponse) return resp;
|
|
57
66
|
return resp.body;
|
|
58
67
|
};
|
|
@@ -62,9 +71,9 @@ module.exports = async (appParams = {}) => {
|
|
|
62
71
|
const doApiPut = params => doApi({ ...params, verb: 'put' });
|
|
63
72
|
const doApiDelete = params => doApi({ ...params, verb: 'delete' });
|
|
64
73
|
|
|
65
|
-
const appSetup = async ({ userId: userIdParam } = {}) => {
|
|
74
|
+
const appSetup = async ({ userId: userIdParam, appName: appNameParam } = {}) => {
|
|
66
75
|
const userId = userIdParam || chance.guid();
|
|
67
|
-
const appName1 = slugify(
|
|
76
|
+
const appName1 = appNameParam || slugify(
|
|
68
77
|
chance.company(),
|
|
69
78
|
).toLowerCase();
|
|
70
79
|
const newApp = {
|
|
@@ -77,31 +86,49 @@ module.exports = async (appParams = {}) => {
|
|
|
77
86
|
endpoint: chance.url(),
|
|
78
87
|
apiKey: apiKey1,
|
|
79
88
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
|
|
90
|
+
let appKey;
|
|
91
|
+
|
|
92
|
+
// Only create a new app if appNameParam is not provided
|
|
93
|
+
if (!appNameParam) {
|
|
94
|
+
const { body: { value: newAppKey } } = await request(app)
|
|
95
|
+
.post('/app')
|
|
96
|
+
.set('Authorization', `Bearer ${adminKey}`)
|
|
97
|
+
.send(newApp);
|
|
98
|
+
appKey = newAppKey;
|
|
99
|
+
} else {
|
|
100
|
+
// If appNameParam is provided, we'll use the adminKey directly
|
|
101
|
+
// This avoids the need for a GET /app/{appName} endpoint
|
|
102
|
+
appKey = adminKey;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create user token for the app
|
|
106
|
+
const hint = token.apiKey.split('-')[0];
|
|
84
107
|
await request(app)
|
|
85
108
|
.post(`/${newApp.name}/${userId}`)
|
|
86
109
|
.set('Authorization', `Bearer ${appKey}`)
|
|
87
110
|
.send({
|
|
88
|
-
tokenHint:
|
|
111
|
+
tokenHint: hint,
|
|
89
112
|
token,
|
|
90
113
|
});
|
|
114
|
+
|
|
91
115
|
return {
|
|
92
116
|
newApp,
|
|
93
117
|
token,
|
|
94
118
|
appKey,
|
|
95
119
|
userId,
|
|
120
|
+
hint,
|
|
96
121
|
};
|
|
97
122
|
};
|
|
98
123
|
|
|
99
124
|
return {
|
|
125
|
+
app,
|
|
100
126
|
appSetup,
|
|
101
127
|
doApiDelete,
|
|
102
128
|
doApiGet,
|
|
103
129
|
doApiPost,
|
|
104
130
|
doApiPut,
|
|
131
|
+
createUserToken,
|
|
105
132
|
slugify,
|
|
106
133
|
plugins: app.plugins,
|
|
107
134
|
};
|
|
@@ -45,28 +45,6 @@ describe('worker: Callback job handling', () => {
|
|
|
45
45
|
}));
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
it('should include the correct structure for callback jobs', () => {
|
|
49
|
-
// This test verifies the worker/index.js handles callback jobs correctly
|
|
50
|
-
// by checking the code structure without actually running it
|
|
51
|
-
|
|
52
|
-
// Read the worker code
|
|
53
|
-
const workerCode = require('fs').readFileSync(require('path').resolve(__dirname, '../index.js'), 'utf8');
|
|
54
|
-
|
|
55
|
-
// Verify the worker code includes handling for callback jobs
|
|
56
|
-
expect(workerCode).toContain('if (type === \'callback\')');
|
|
57
|
-
|
|
58
|
-
// Verify it extracts the necessary parameters
|
|
59
|
-
expect(workerCode).toContain('callbackUrl,');
|
|
60
|
-
expect(workerCode).toContain('request,');
|
|
61
|
-
expect(workerCode).toContain('result,');
|
|
62
|
-
|
|
63
|
-
// Verify it calls the sendCallback function
|
|
64
|
-
expect(workerCode).toContain('sendCallback');
|
|
65
|
-
|
|
66
|
-
// Verify it returns the expected result structure
|
|
67
|
-
expect(workerCode).toContain('return { success: true }');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
48
|
it('should use the callback module to send callbacks', async () => {
|
|
71
49
|
// Get a reference to the mocked axios.post function
|
|
72
50
|
const axios = require('axios');
|