ti2 1.0.94 → 1.0.96
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/README.md +70 -1
- 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-searchProducts.js +533 -0
- package/controllers/__tests__/bookings.js +29 -267
- 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 +280 -14
- package/controllers/bookings.js +188 -108
- 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 +54 -26
- package/worker/__tests__/job-callback.js +0 -22
- package/worker/__tests__/job-plugin.js +207 -0
- package/worker/index.js +191 -108
- package/worker/queue.js +70 -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 => {
|
|
@@ -127,30 +142,34 @@ const createAppToken = async (req, res, next) => {
|
|
|
127
142
|
if (existing) {
|
|
128
143
|
const bullJob = await queue.getJob(job.bullJobId);
|
|
129
144
|
if (!bullJob) {
|
|
130
|
-
|
|
131
|
-
existing.bullJobId =
|
|
145
|
+
let rawBullJobId = await addJob(jobPayload, jobParams);
|
|
146
|
+
existing.bullJobId = (rawBullJobId && typeof rawBullJobId === 'object' && rawBullJobId.id) ? rawBullJobId.id : rawBullJobId;
|
|
132
147
|
await existing.save();
|
|
133
148
|
} else {
|
|
134
149
|
// make sure the cron is the same
|
|
150
|
+
// bullJobId here is from existing.bullJobId, which should be a string.
|
|
151
|
+
// The queue.getJob expects a string ID.
|
|
135
152
|
const bullCron = R.path(
|
|
136
153
|
['opts', 'repeat', 'cron'],
|
|
137
|
-
await queue.getJob(bullJobId),
|
|
154
|
+
await queue.getJob(existing.bullJobId), // Use existing.bullJobId
|
|
138
155
|
);
|
|
139
156
|
if (bullCron !== job.cron) {
|
|
140
|
-
await queue.removeJobs(bullJobId);
|
|
141
|
-
|
|
142
|
-
existing.bullJobId =
|
|
157
|
+
await queue.removeJobs(existing.bullJobId); // Use existing.bullJobId
|
|
158
|
+
let rawBullJobId = await addJob(jobPayload, jobParams);
|
|
159
|
+
existing.bullJobId = (rawBullJobId && typeof rawBullJobId === 'object' && rawBullJobId.id) ? rawBullJobId.id : rawBullJobId;
|
|
143
160
|
await existing.save();
|
|
144
161
|
}
|
|
145
162
|
}
|
|
146
163
|
} else {
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
let rawBullJobId = await addJob(jobPayload, jobParams);
|
|
165
|
+
const actualBullJobId = (rawBullJobId && typeof rawBullJobId === 'object' && rawBullJobId.id) ? rawBullJobId.id : rawBullJobId;
|
|
166
|
+
await sqldb.CronJobs.create({ ...where, bullJobId: actualBullJobId });
|
|
149
167
|
}
|
|
150
168
|
});
|
|
151
169
|
}
|
|
152
170
|
});
|
|
153
171
|
|
|
172
|
+
|
|
154
173
|
return res.json({ value: newAppKey.get('id').toString() });
|
|
155
174
|
} catch (err) {
|
|
156
175
|
return next(err);
|
|
@@ -297,7 +316,7 @@ const migrateApp = async ({ integrationId, action }) => {
|
|
|
297
316
|
'migrations',
|
|
298
317
|
);
|
|
299
318
|
try {
|
|
300
|
-
await fs.access(migrationsPath);
|
|
319
|
+
await fs.promises.access(migrationsPath);
|
|
301
320
|
} catch (err) {
|
|
302
321
|
throw Error(`Could not find any migrations for ${integrationId} on ${migrationsPath}`);
|
|
303
322
|
}
|
|
@@ -332,12 +351,10 @@ const migrateApp = async ({ integrationId, action }) => {
|
|
|
332
351
|
const getAppScheduledJobs = async ({
|
|
333
352
|
integrationId,
|
|
334
353
|
userId,
|
|
335
|
-
hint,
|
|
336
354
|
}) => {
|
|
337
355
|
const where = R.reject(R.isNil)({
|
|
338
356
|
pluginName: integrationId,
|
|
339
357
|
userId,
|
|
340
|
-
hint,
|
|
341
358
|
});
|
|
342
359
|
const jobs = await sqldb.CronJobs.findAll({
|
|
343
360
|
where,
|
|
@@ -487,6 +504,252 @@ const getAppToken = async (req, res, next) => {
|
|
|
487
504
|
return res.json({ token: await userAppKey.token });
|
|
488
505
|
};
|
|
489
506
|
|
|
507
|
+
const createCronjob = async (req, res, next) => {
|
|
508
|
+
const {
|
|
509
|
+
params: {
|
|
510
|
+
userId,
|
|
511
|
+
},
|
|
512
|
+
body: {
|
|
513
|
+
method,
|
|
514
|
+
url,
|
|
515
|
+
cron,
|
|
516
|
+
callbackUrl,
|
|
517
|
+
body,
|
|
518
|
+
removeOnComplete,
|
|
519
|
+
},
|
|
520
|
+
} = req;
|
|
521
|
+
|
|
522
|
+
let transaction;
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
// Validate required fields
|
|
526
|
+
if (!method || !url || !cron) {
|
|
527
|
+
const error = new Error('method, url, and cron are required fields');
|
|
528
|
+
error.status = 400;
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Validate URL path
|
|
533
|
+
const validUrlPattern = /^\/[\w\-\/\.]+$/;
|
|
534
|
+
if (!validUrlPattern.test(url)) {
|
|
535
|
+
const error = new Error('Invalid URL path');
|
|
536
|
+
error.status = 400;
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Helper function to find a matching path in the OpenAPI schema
|
|
541
|
+
const findMatchingOpenApiPath = (requestUrl, schemaPaths) => {
|
|
542
|
+
if (!schemaPaths) return null;
|
|
543
|
+
for (const schemaPathKey in schemaPaths) {
|
|
544
|
+
// Convert schemaPathKey to a regex: e.g., /products/{id} -> ^/products/[^/]+$
|
|
545
|
+
const regexPattern = '^' + schemaPathKey.replace(/{[^}]+}/g, '[^/]+') + '$';
|
|
546
|
+
const regex = new RegExp(regexPattern);
|
|
547
|
+
if (regex.test(requestUrl)) {
|
|
548
|
+
return schemaPathKey; // Return the original schema path key
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const matchedSchemaPathKey = findMatchingOpenApiPath(url, openApiSchema ? openApiSchema.paths : null);
|
|
555
|
+
|
|
556
|
+
// Validate URL and method against OpenAPI schema
|
|
557
|
+
if (!openApiSchema || !matchedSchemaPathKey || !openApiSchema.paths[matchedSchemaPathKey][method.toLowerCase()]) {
|
|
558
|
+
const errorMessage = openApiSchema && openApiSchema.paths ?
|
|
559
|
+
`Invalid URL ('${url}') or method ('${method}') combination not found in API schema, or schema path not matched.` :
|
|
560
|
+
'API schema not loaded, cannot validate URL and method.';
|
|
561
|
+
const error = new Error(errorMessage);
|
|
562
|
+
error.status = 400;
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Create a user token for the job test
|
|
567
|
+
const jobToken = jwt.sign({ userId }, process.env.jwtSecret);
|
|
568
|
+
|
|
569
|
+
// Start transaction
|
|
570
|
+
transaction = await sqldb.sequelize.transaction();
|
|
571
|
+
|
|
572
|
+
// Create job in database
|
|
573
|
+
const cronJobData = {
|
|
574
|
+
userId,
|
|
575
|
+
// bullJobId will be set by the hook
|
|
576
|
+
cron,
|
|
577
|
+
method,
|
|
578
|
+
url,
|
|
579
|
+
token: jobToken,
|
|
580
|
+
body: { // Ensure body includes callbackUrl if present, or is an empty object if body is null/undefined
|
|
581
|
+
...(body || {}),
|
|
582
|
+
...(callbackUrl ? { callbackUrl } : {}),
|
|
583
|
+
},
|
|
584
|
+
removeOnComplete: removeOnComplete || false, // Ensure it defaults to false if not provided
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const cronJob = await sqldb.ApiCronJobs.create(cronJobData, { transaction });
|
|
588
|
+
|
|
589
|
+
await transaction.commit();
|
|
590
|
+
// The cronJob instance returned here should have bullJobId populated by the hook
|
|
591
|
+
return res.json(R.omit(['token'], cronJob.get({ plain:true })));
|
|
592
|
+
} catch (err) {
|
|
593
|
+
if (transaction) await transaction.rollback();
|
|
594
|
+
// No need to manually cleanup bullJobId here.
|
|
595
|
+
// If hook failed, transaction is rolled back by the hook re-throwing,
|
|
596
|
+
// or by create itself failing.
|
|
597
|
+
// If create itself failed before hook, no bull job was created.
|
|
598
|
+
return next(err);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const listCronjobs = async (req, res, next) => {
|
|
603
|
+
const {
|
|
604
|
+
params: {
|
|
605
|
+
userId,
|
|
606
|
+
},
|
|
607
|
+
} = req;
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
// Get all jobs from Bull queue
|
|
611
|
+
const bullJobs = await queue.getJobs(['active', 'wait', 'delayed']);
|
|
612
|
+
const bullJobIds = new Set(bullJobs.map(job => job.id));
|
|
613
|
+
|
|
614
|
+
// Get repeatable jobs
|
|
615
|
+
const repeatableJobs = await queue.getRepeatableJobs();
|
|
616
|
+
const repeatablePatterns = new Set(repeatableJobs.map(job => job.cron));
|
|
617
|
+
|
|
618
|
+
// Get jobs from database
|
|
619
|
+
const jobs = await sqldb.ApiCronJobs.findAll({
|
|
620
|
+
where: {
|
|
621
|
+
userId,
|
|
622
|
+
},
|
|
623
|
+
attributes: { exclude: ['token'] },
|
|
624
|
+
raw: true,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Add inQueue status to jobs
|
|
628
|
+
const jobsWithStatus = jobs.map(job => {
|
|
629
|
+
const jobIdParts = job.bullJobId.split(':');
|
|
630
|
+
const isRepeatJob = jobIdParts[0] === 'repeat';
|
|
631
|
+
let isInQueue = false;
|
|
632
|
+
|
|
633
|
+
if (isRepeatJob) {
|
|
634
|
+
isInQueue = repeatablePatterns.has(job.cron);
|
|
635
|
+
} else {
|
|
636
|
+
isInQueue = bullJobIds.has(job.bullJobId);
|
|
637
|
+
}
|
|
638
|
+
return { ...job, inQueue: isInQueue };
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
return res.json({ jobs: jobsWithStatus });
|
|
642
|
+
} catch (err) {
|
|
643
|
+
return next(err);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const deleteCronjob = async (req, res, next) => {
|
|
648
|
+
const {
|
|
649
|
+
params: {
|
|
650
|
+
userId,
|
|
651
|
+
id, // Changed from jobId to id
|
|
652
|
+
},
|
|
653
|
+
} = req;
|
|
654
|
+
|
|
655
|
+
// Extract token from Authorization header
|
|
656
|
+
const getToken = (req) => {
|
|
657
|
+
if (
|
|
658
|
+
!req.header('Authorization') ||
|
|
659
|
+
req.header('Authorization').substring(0, 7) !== 'Bearer '
|
|
660
|
+
) return null;
|
|
661
|
+
return req.header('Authorization').split(' ')[1];
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const token = getToken(req);
|
|
665
|
+
let transaction; // Add transaction variable for deleteCronjob
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
// Check if user is admin
|
|
669
|
+
const isAdmin = token === process.env.adminKey;
|
|
670
|
+
|
|
671
|
+
// For non-admin users, verify token
|
|
672
|
+
if (!isAdmin) {
|
|
673
|
+
try {
|
|
674
|
+
const decoded = jwt.verify(token, process.env.jwtSecret);
|
|
675
|
+
// Verify userId in token matches requested userId
|
|
676
|
+
if (!decoded || !decoded.userId || decoded.userId !== userId) {
|
|
677
|
+
throw new Error('User ID mismatch');
|
|
678
|
+
}
|
|
679
|
+
} catch (err) {
|
|
680
|
+
return res.status(403).json({
|
|
681
|
+
status: 403,
|
|
682
|
+
message: 'Forbidden'
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Find the cronjob in the database
|
|
688
|
+
const cronJob = await sqldb.ApiCronJobs.findOne({
|
|
689
|
+
where: {
|
|
690
|
+
userId,
|
|
691
|
+
id, // Changed from bullJobId to id
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// If cronjob doesn't exist, return 404
|
|
696
|
+
if (!cronJob) {
|
|
697
|
+
const error = new Error('Cronjob not found');
|
|
698
|
+
error.status = 404;
|
|
699
|
+
return next(error);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// For non-admin users, verify ownership of the cronjob
|
|
703
|
+
if (!isAdmin && userId !== cronJob.userId) {
|
|
704
|
+
return res.status(403).json({
|
|
705
|
+
status: 403,
|
|
706
|
+
message: 'Forbidden'
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Start transaction
|
|
711
|
+
transaction = await sqldb.sequelize.transaction();
|
|
712
|
+
|
|
713
|
+
// Remove from database. The beforeDestroy hook in ApiCronJobs model
|
|
714
|
+
// will handle removing the job from the Bull queue.
|
|
715
|
+
// Pass the transaction to ensure atomicity.
|
|
716
|
+
// The destroy operation will only succeed if the beforeDestroy hook (including removeJob) succeeds.
|
|
717
|
+
const destroyedRows = await sqldb.ApiCronJobs.destroy({
|
|
718
|
+
where: {
|
|
719
|
+
// id, // cronJob instance is already fetched and verified
|
|
720
|
+
id: cronJob.id, // Use the id from the fetched cronJob instance
|
|
721
|
+
userId: cronJob.userId, // Ensure we are deleting the correct user's job
|
|
722
|
+
},
|
|
723
|
+
transaction, // Pass transaction to destroy
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// It's good practice to check if any rows were actually deleted,
|
|
727
|
+
// though in this flow, cronJob existence is checked prior.
|
|
728
|
+
if (destroyedRows === 0) {
|
|
729
|
+
// This case should ideally not be reached if cronJob was found
|
|
730
|
+
if (transaction) await transaction.rollback();
|
|
731
|
+
const error = new Error('Cronjob not found for deletion, though it was fetched prior.');
|
|
732
|
+
error.status = 404; // Or 500 for internal inconsistency
|
|
733
|
+
return next(error);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
await transaction.commit();
|
|
737
|
+
// Return success response
|
|
738
|
+
return res.json({ success: true });
|
|
739
|
+
} catch (err) {
|
|
740
|
+
if (transaction) await transaction.rollback(); // Rollback transaction on error
|
|
741
|
+
// Pass along errors with status
|
|
742
|
+
if (err.status) {
|
|
743
|
+
return next(err);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Otherwise, create a generic 500 error
|
|
747
|
+
const error = new Error(err.message || 'Internal Server Error');
|
|
748
|
+
error.status = 500;
|
|
749
|
+
return next(error); // Changed from next(err) to next(the new error object) for consistency
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
490
753
|
module.exports = plugins => ({
|
|
491
754
|
createAppToken,
|
|
492
755
|
getAppToken,
|
|
@@ -504,4 +767,7 @@ module.exports = plugins => ({
|
|
|
504
767
|
tokenTemplate,
|
|
505
768
|
validateAppToken: validateAppToken(plugins),
|
|
506
769
|
getAffiliates: getAffiliates(plugins),
|
|
770
|
+
createCronjob,
|
|
771
|
+
deleteCronjob,
|
|
772
|
+
listCronjobs,
|
|
507
773
|
});
|