ti2 1.0.16 → 1.0.17
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/.eslintrc.js +1 -0
- package/.github/workflows/tests.yml +6 -2
- package/.nvmrc +1 -0
- package/api.yml +150 -0
- package/auth/authHandler.js +2 -2
- package/closeAll.js +6 -0
- package/controllers/__tests__/admin.js +12 -9
- package/controllers/__tests__/app.js +57 -5
- package/controllers/__tests__/bookings.js +20 -10
- package/controllers/__tests__/ping.js +8 -3
- package/controllers/__tests__/user.js +9 -3
- package/controllers/app.js +122 -0
- package/controllers/ping.js +2 -1
- package/controllers/user.js +2 -6
- package/index.js +44 -8
- package/jest.setup.js +11 -0
- package/migrations/20200128162519-deletedAtForIntegrations.js +0 -1
- package/migrations/20210408024232-expand-JWT-field-size.js +2 -2
- package/migrations/20220426153807-cronJobs.js +41 -0
- package/models/cronjobs.js +31 -0
- package/models/index.js +1 -0
- package/package.json +11 -3
- package/test/plugin.js +40 -13
- package/test/utils.js +3 -5
- package/test/worker.js +5 -0
- package/tutorials/setup-your-instance.md +69 -17
- package/worker/index.js +53 -0
- package/worker/queue.js +88 -0
package/.eslintrc.js
CHANGED
|
@@ -7,7 +7,7 @@ jobs:
|
|
|
7
7
|
timeout-minutes: 10
|
|
8
8
|
runs-on: ubuntu-latest
|
|
9
9
|
container:
|
|
10
|
-
image: node:
|
|
10
|
+
image: node:16.15.0
|
|
11
11
|
services:
|
|
12
12
|
mysql:
|
|
13
13
|
image: mysql
|
|
@@ -17,9 +17,12 @@ jobs:
|
|
|
17
17
|
MYSQL_ROOT_PASSWORD: root
|
|
18
18
|
ports:
|
|
19
19
|
- 3306:3306
|
|
20
|
+
redis:
|
|
21
|
+
image: redis
|
|
20
22
|
env:
|
|
21
23
|
CI: true
|
|
22
24
|
DB_URL: 'mysql://root:root@mysql/ti2development'
|
|
25
|
+
REDIS_URL: 'redis://redis:6379'
|
|
23
26
|
dbCryptoKey: 'YmYzYzE5OWMyNDcwY2I0NzdkOTA3YjFlMDkxN2MxN2I='
|
|
24
27
|
jwtSecret: '9rifawckwztuKehocJOtGM6deTqICGWzIyc5CqgNyi'
|
|
25
28
|
frontendKey: 'lYjEJHzb7lbQ1hk3ypjX'
|
|
@@ -30,10 +33,11 @@ jobs:
|
|
|
30
33
|
with:
|
|
31
34
|
node-version: 12
|
|
32
35
|
- name: install dependencies
|
|
33
|
-
run: npm
|
|
36
|
+
run: npm ci
|
|
34
37
|
- name: create/migrate the database
|
|
35
38
|
run: npx sequelize db:create && npx sequelize db:migrate
|
|
36
39
|
- name: seed the database
|
|
37
40
|
run: npx sequelize db:seed:all
|
|
41
|
+
- run: RUNNER_TRACKING_ID="" && (nohup node ./test/worker.js&)
|
|
38
42
|
- name: run the tests
|
|
39
43
|
run: npx jest -i --coverage --forceExit
|
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v16.15.0
|
package/api.yml
CHANGED
|
@@ -29,6 +29,34 @@ components:
|
|
|
29
29
|
properties:
|
|
30
30
|
value:
|
|
31
31
|
type: string
|
|
32
|
+
JobSpec:
|
|
33
|
+
type: object
|
|
34
|
+
properties:
|
|
35
|
+
payload:
|
|
36
|
+
type: object
|
|
37
|
+
description: The job app parameters
|
|
38
|
+
required: true
|
|
39
|
+
example: '{
|
|
40
|
+
"method": "dailyReport"
|
|
41
|
+
}'
|
|
42
|
+
jobParams:
|
|
43
|
+
type: object
|
|
44
|
+
description: Additional bull job params
|
|
45
|
+
example: '{
|
|
46
|
+
"delay": "5000"
|
|
47
|
+
}'
|
|
48
|
+
JobStatus:
|
|
49
|
+
type: object
|
|
50
|
+
properties:
|
|
51
|
+
jobId:
|
|
52
|
+
type: string
|
|
53
|
+
status:
|
|
54
|
+
type: string
|
|
55
|
+
example: 'active'
|
|
56
|
+
result:
|
|
57
|
+
type: object
|
|
58
|
+
description: The job return values
|
|
59
|
+
example: '{ "result": "value" }'
|
|
32
60
|
App:
|
|
33
61
|
type: object
|
|
34
62
|
properties:
|
|
@@ -534,6 +562,128 @@ paths:
|
|
|
534
562
|
content:
|
|
535
563
|
application/json:
|
|
536
564
|
schema: '#/components/schemas/Message'
|
|
565
|
+
|
|
566
|
+
/{app}/{userId}/jobRun:
|
|
567
|
+
security:
|
|
568
|
+
- bearerAuth: ['app', 'user', 'admin']
|
|
569
|
+
post:
|
|
570
|
+
tags:
|
|
571
|
+
- jobs
|
|
572
|
+
summary: Run a Job
|
|
573
|
+
security:
|
|
574
|
+
- bearerAuth: ['app', 'user', 'admin']
|
|
575
|
+
operationId: runAppJob
|
|
576
|
+
parameters:
|
|
577
|
+
- name: app
|
|
578
|
+
in: path
|
|
579
|
+
description: app that defines the job
|
|
580
|
+
example: 'ventrata'
|
|
581
|
+
required: true
|
|
582
|
+
schema:
|
|
583
|
+
type: string
|
|
584
|
+
- name: userId
|
|
585
|
+
in: path
|
|
586
|
+
description: user to run the schedule app against
|
|
587
|
+
example: '536830b6ed19afa44a000002'
|
|
588
|
+
required: true
|
|
589
|
+
schema:
|
|
590
|
+
type: string
|
|
591
|
+
requestBody:
|
|
592
|
+
description: The job parameters
|
|
593
|
+
required: true
|
|
594
|
+
content:
|
|
595
|
+
application/json:
|
|
596
|
+
schema:
|
|
597
|
+
$ref: '#/components/schemas/JobSpec'
|
|
598
|
+
responses:
|
|
599
|
+
200:
|
|
600
|
+
description: success
|
|
601
|
+
content:
|
|
602
|
+
application/json:
|
|
603
|
+
schema: '#/components/schemas/JobStatus'
|
|
604
|
+
/{app}/{userId}/{hint}/jobRun:
|
|
605
|
+
security:
|
|
606
|
+
- bearerAuth: ['app', 'user', 'admin']
|
|
607
|
+
post:
|
|
608
|
+
tags:
|
|
609
|
+
- jobs
|
|
610
|
+
summary: Run a schedule Job
|
|
611
|
+
security:
|
|
612
|
+
- bearerAuth: ['app', 'user', 'admin']
|
|
613
|
+
operationId: runAppJob
|
|
614
|
+
parameters:
|
|
615
|
+
- name: app
|
|
616
|
+
in: path
|
|
617
|
+
description: app that defines the job
|
|
618
|
+
example: 'ventrata'
|
|
619
|
+
required: true
|
|
620
|
+
schema:
|
|
621
|
+
type: string
|
|
622
|
+
- name: userId
|
|
623
|
+
in: path
|
|
624
|
+
description: user to run the schedule app against
|
|
625
|
+
example: '536830b6ed19afa44a000002'
|
|
626
|
+
required: true
|
|
627
|
+
schema:
|
|
628
|
+
type: string
|
|
629
|
+
- name: hint
|
|
630
|
+
in: path
|
|
631
|
+
description: a specific token hint to be used
|
|
632
|
+
required: true
|
|
633
|
+
example: 'travelgate-australia'
|
|
634
|
+
schema:
|
|
635
|
+
type: string
|
|
636
|
+
requestBody:
|
|
637
|
+
description: The job parameters
|
|
638
|
+
required: true
|
|
639
|
+
content:
|
|
640
|
+
application/json:
|
|
641
|
+
schema:
|
|
642
|
+
$ref: '#/components/schemas/JobSpec'
|
|
643
|
+
responses:
|
|
644
|
+
200:
|
|
645
|
+
description: success
|
|
646
|
+
content:
|
|
647
|
+
application/json:
|
|
648
|
+
schema: '#/components/schemas/JobStatus'
|
|
649
|
+
/{app}/{userId}/{jobId}/jobStatus:
|
|
650
|
+
security:
|
|
651
|
+
- bearerAuth: ['app', 'user', 'admin']
|
|
652
|
+
get:
|
|
653
|
+
tags:
|
|
654
|
+
- jobs
|
|
655
|
+
summary: Get the status and/or result from a job
|
|
656
|
+
security:
|
|
657
|
+
- bearerAuth: ['app', 'user', 'admin']
|
|
658
|
+
operationId: getJobStatus
|
|
659
|
+
parameters:
|
|
660
|
+
- name: app
|
|
661
|
+
in: path
|
|
662
|
+
description: app that defines the job
|
|
663
|
+
example: 'ventrata'
|
|
664
|
+
required: true
|
|
665
|
+
schema:
|
|
666
|
+
type: string
|
|
667
|
+
- name: userId
|
|
668
|
+
in: path
|
|
669
|
+
description: user that owns the job
|
|
670
|
+
example: '536830b6ed19afa44a000002'
|
|
671
|
+
required: true
|
|
672
|
+
schema:
|
|
673
|
+
type: string
|
|
674
|
+
- name: jobId
|
|
675
|
+
in: path
|
|
676
|
+
description: jobId to get the status for
|
|
677
|
+
example: '536830b6ed19afa44a000002'
|
|
678
|
+
required: true
|
|
679
|
+
schema:
|
|
680
|
+
type: string
|
|
681
|
+
responses:
|
|
682
|
+
200:
|
|
683
|
+
description: success
|
|
684
|
+
content:
|
|
685
|
+
application/json:
|
|
686
|
+
schema: '#/components/schemas/JobStatus'
|
|
537
687
|
/user/{userId}/apps:
|
|
538
688
|
get:
|
|
539
689
|
security:
|
package/auth/authHandler.js
CHANGED
|
@@ -68,10 +68,10 @@ const frontend = async (req, res, next) => {
|
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
const adminOrUser = levels => async (req, res, next) => {
|
|
71
|
-
result = await Promise.all(levels.map(
|
|
71
|
+
const result = await Promise.all(levels.map(
|
|
72
72
|
level => level(req, res, err => err),
|
|
73
73
|
));
|
|
74
|
-
if (result.some(val => (!
|
|
74
|
+
if (result.some(val => (!val))) {
|
|
75
75
|
return next();
|
|
76
76
|
}
|
|
77
77
|
return next(result[0]);
|
package/closeAll.js
ADDED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* globals describe it expect */
|
|
1
|
+
/* globals beforeAll describe it expect */
|
|
2
2
|
|
|
3
3
|
const chance = require('chance').Chance();
|
|
4
4
|
const testUtils = require('../../test/utils');
|
|
@@ -15,11 +15,18 @@ describe('admin', () => {
|
|
|
15
15
|
packageName: `ti2-${appName}`,
|
|
16
16
|
adminEmail: chance.email(),
|
|
17
17
|
};
|
|
18
|
-
const { doApiPost, doApiGet } = testUtils({
|
|
19
|
-
plugins: [appName],
|
|
20
|
-
});
|
|
21
18
|
let appKey;
|
|
22
19
|
const userId = '536830b6ed19afa44a000002';
|
|
20
|
+
let doApiPost; let
|
|
21
|
+
doApiGet;
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
({
|
|
24
|
+
doApiGet,
|
|
25
|
+
doApiPost,
|
|
26
|
+
} = await testUtils({
|
|
27
|
+
plugins: [appName],
|
|
28
|
+
}));
|
|
29
|
+
});
|
|
23
30
|
it('should be able to create an app', async () => {
|
|
24
31
|
({ value: appKey } = await doApiPost({
|
|
25
32
|
url: '/app',
|
|
@@ -33,11 +40,7 @@ describe('admin', () => {
|
|
|
33
40
|
url: '/apps',
|
|
34
41
|
token: adminKey,
|
|
35
42
|
});
|
|
36
|
-
expect(integrations).
|
|
37
|
-
expect.arrayContaining([
|
|
38
|
-
expect.objectContaining(newApp),
|
|
39
|
-
]),
|
|
40
|
-
);
|
|
43
|
+
expect(integrations.find(({ name }) => name === newApp.name)).toBeTruthy();
|
|
41
44
|
});
|
|
42
45
|
it('should be able to reset an app\'s key', async () => {
|
|
43
46
|
const url = `/app/resetAppKey/${newApp.name}`;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
/* globals describe it expect */
|
|
2
|
-
|
|
1
|
+
/* globals beforeAll describe it expect */
|
|
3
2
|
const chance = require('chance').Chance();
|
|
4
3
|
const jwt = require('jwt-promise');
|
|
4
|
+
const R = require('ramda');
|
|
5
|
+
|
|
5
6
|
const testUtils = require('../../test/utils');
|
|
6
7
|
const slugify = require('../../test/slugify');
|
|
8
|
+
const appController = require('../app');
|
|
7
9
|
|
|
8
10
|
const { env: { adminKey, jwtSecret } } = process;
|
|
9
11
|
|
|
@@ -16,9 +18,9 @@ describe('app', () => {
|
|
|
16
18
|
packageName: `ti2-${appName}`,
|
|
17
19
|
adminEmail: chance.email(),
|
|
18
20
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
let doApiPost;
|
|
22
|
+
let doApiGet;
|
|
23
|
+
let doApiPut;
|
|
22
24
|
let appKey;
|
|
23
25
|
const userId = chance.guid();
|
|
24
26
|
const apiKey = chance.guid();
|
|
@@ -31,6 +33,11 @@ describe('app', () => {
|
|
|
31
33
|
payload: { lorem: chance.paragraph() },
|
|
32
34
|
};
|
|
33
35
|
let encodedKey;
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
({ doApiPost, doApiGet, doApiPut } = await testUtils({
|
|
38
|
+
plugins: [appName],
|
|
39
|
+
}));
|
|
40
|
+
});
|
|
34
41
|
it('should create a new app', async () => {
|
|
35
42
|
({ value: appKey } = await doApiPost({
|
|
36
43
|
url: '/app',
|
|
@@ -62,6 +69,51 @@ describe('app', () => {
|
|
|
62
69
|
});
|
|
63
70
|
expect(parseInt(value, 10)).toBeGreaterThan(0);
|
|
64
71
|
});
|
|
72
|
+
describe('jobs', () => {
|
|
73
|
+
it('the scheduled job should have been created for the user', async () => {
|
|
74
|
+
const jobs = R.path(
|
|
75
|
+
['jobs'],
|
|
76
|
+
await appController.getAppScheduledJobs({
|
|
77
|
+
integrationId: appName,
|
|
78
|
+
userId,
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
expect(Array.isArray(jobs)).toBeTruthy();
|
|
82
|
+
expect(R.head(R.project(['pluginJobId', 'cron'], jobs)))
|
|
83
|
+
.toEqual({
|
|
84
|
+
pluginJobId: 'dailyReport',
|
|
85
|
+
cron: '0 9 * * *',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
it.todo('if a new version of the plugin has a different set of scheduled tasks, they shoul dbe re-syncjed');
|
|
89
|
+
let jobId;
|
|
90
|
+
it('should be able to run a job', async () => {
|
|
91
|
+
let status;
|
|
92
|
+
({ jobId, status } = await doApiPost({
|
|
93
|
+
url: `/${appName}/${userId}/jobRun`,
|
|
94
|
+
token: appKey,
|
|
95
|
+
payload: {
|
|
96
|
+
payload: {
|
|
97
|
+
method: 'dailyReport',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
expect(jobId).toBeTruthy();
|
|
102
|
+
expect(status).toBeTruthy();
|
|
103
|
+
});
|
|
104
|
+
it('should be able to get the status of a ran job', async () => {
|
|
105
|
+
await global.sleep(5e3);
|
|
106
|
+
const url = `/${appName}/${userId}/${jobId}/jobStatus`;
|
|
107
|
+
const jobStatus = await doApiGet({
|
|
108
|
+
url,
|
|
109
|
+
token: appKey,
|
|
110
|
+
payload: {},
|
|
111
|
+
});
|
|
112
|
+
expect(jobStatus.jobId).toBeTruthy();
|
|
113
|
+
expect(jobStatus.status).toBe('success');
|
|
114
|
+
expect(jobStatus.result).toEqual({ someVal: true });
|
|
115
|
+
}, 6e3);
|
|
116
|
+
});
|
|
65
117
|
it('should be able to get a list of all tokens related to the app', async () => {
|
|
66
118
|
const { userAppKeys } = await doApiGet({
|
|
67
119
|
url: `/app/tokens/${appName}`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* globals describe it expect */
|
|
1
|
+
/* globals beforeAll describe it expect */
|
|
2
2
|
|
|
3
3
|
const chance = require('chance').Chance();
|
|
4
4
|
const testUtils = require('../../test/utils');
|
|
@@ -22,13 +22,19 @@ describe('user: booking search', () => {
|
|
|
22
22
|
tokenHint: 'testingToken',
|
|
23
23
|
token,
|
|
24
24
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
let doApiDelete;
|
|
26
|
+
let doApiGet;
|
|
27
|
+
let doApiPost;
|
|
28
|
+
let plugins;
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
({
|
|
31
|
+
doApiDelete,
|
|
32
|
+
doApiGet,
|
|
33
|
+
doApiPost,
|
|
34
|
+
plugins,
|
|
35
|
+
} = await testUtils({
|
|
36
|
+
plugins: [newApp.name],
|
|
37
|
+
}));
|
|
32
38
|
});
|
|
33
39
|
|
|
34
40
|
let userToken;
|
|
@@ -39,13 +45,17 @@ describe('user: booking search', () => {
|
|
|
39
45
|
token: adminKey,
|
|
40
46
|
payload: { tokenHint: 'testingToken' },
|
|
41
47
|
});
|
|
42
|
-
} catch {
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.debug(err);
|
|
50
|
+
}
|
|
43
51
|
try {
|
|
44
52
|
await doApiDelete({
|
|
45
53
|
url: '/app/travelgate',
|
|
46
54
|
token: adminKey,
|
|
47
55
|
});
|
|
48
|
-
} catch {
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.debug(err);
|
|
58
|
+
}
|
|
49
59
|
});
|
|
50
60
|
it('create the travelgate app', async () => {
|
|
51
61
|
const { value: apiKey } = await doApiPost({
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
/* global describe, it, expect */
|
|
2
|
-
|
|
3
|
-
const { doApiGet } = require('../../test/utils')();
|
|
1
|
+
/* global beforeAll, describe, it, expect */
|
|
2
|
+
const testUtils = require('../../test/utils');
|
|
4
3
|
|
|
5
4
|
describe('ping', () => {
|
|
5
|
+
let doApiGet;
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
({
|
|
8
|
+
doApiGet,
|
|
9
|
+
} = await testUtils());
|
|
10
|
+
});
|
|
6
11
|
it('should get a server pong', async () => {
|
|
7
12
|
const response = await doApiGet({
|
|
8
13
|
url: '/ping',
|
|
@@ -15,9 +15,8 @@ describe('user', () => {
|
|
|
15
15
|
packageName: `ti2-${appName}`,
|
|
16
16
|
adminEmail: chance.email(),
|
|
17
17
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
18
|
+
let doApiGet; let doApiPost; let
|
|
19
|
+
doApiDelete;
|
|
21
20
|
let appKey;
|
|
22
21
|
const userId = chance.guid();
|
|
23
22
|
let apiKey = chance.guid();
|
|
@@ -27,6 +26,13 @@ describe('user', () => {
|
|
|
27
26
|
apiKey,
|
|
28
27
|
};
|
|
29
28
|
beforeAll(async () => {
|
|
29
|
+
({
|
|
30
|
+
doApiDelete,
|
|
31
|
+
doApiGet,
|
|
32
|
+
doApiPost,
|
|
33
|
+
} = await testUtils({
|
|
34
|
+
plugins: [appName],
|
|
35
|
+
}));
|
|
30
36
|
// create an App
|
|
31
37
|
({ value: appKey } = await doApiPost({
|
|
32
38
|
url: '/app',
|
package/controllers/app.js
CHANGED
|
@@ -5,8 +5,15 @@ const { Umzug, SequelizeStorage } = require('umzug');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const Sequelize = require('sequelize');
|
|
7
7
|
const fs = require('fs').promises;
|
|
8
|
+
const bb = require('bluebird');
|
|
9
|
+
const R = require('ramda');
|
|
8
10
|
|
|
9
11
|
const sqldb = require('../models');
|
|
12
|
+
const {
|
|
13
|
+
queue,
|
|
14
|
+
addJob,
|
|
15
|
+
jobStatus,
|
|
16
|
+
} = require('../worker/queue');
|
|
10
17
|
|
|
11
18
|
const { env: { jwtSecret } } = process;
|
|
12
19
|
|
|
@@ -48,6 +55,61 @@ const createAppToken = async (req, res, next) => {
|
|
|
48
55
|
});
|
|
49
56
|
if (userAppKeyDup) await userAppKeyDup.destroy();
|
|
50
57
|
const newAppKey = await sqldb.UserAppKey.create(payload);
|
|
58
|
+
// create any cronjobs related to the app
|
|
59
|
+
await bb.each(req.app.plugins, async plugin => {
|
|
60
|
+
if (Array.isArray(plugin.jobs)) {
|
|
61
|
+
await bb.each(plugin.jobs.filter(job => Boolean(job.cron)), async job => {
|
|
62
|
+
const where = {
|
|
63
|
+
pluginName: plugin.name,
|
|
64
|
+
pluginJobId: job.method,
|
|
65
|
+
userId,
|
|
66
|
+
hint,
|
|
67
|
+
cron: job.cron,
|
|
68
|
+
};
|
|
69
|
+
const existing = await sqldb.CronJobs.findOne({
|
|
70
|
+
where: R.omit(['cron'], where),
|
|
71
|
+
});
|
|
72
|
+
const jobPayload = {
|
|
73
|
+
...where,
|
|
74
|
+
...job.payload,
|
|
75
|
+
};
|
|
76
|
+
const jobParams = {
|
|
77
|
+
...(job.cron ? {
|
|
78
|
+
repeat: {
|
|
79
|
+
cron: job.cron,
|
|
80
|
+
},
|
|
81
|
+
} : {}),
|
|
82
|
+
...(job.params || {}),
|
|
83
|
+
removeOnComplete: false,
|
|
84
|
+
};
|
|
85
|
+
let bullJobId;
|
|
86
|
+
if (existing) {
|
|
87
|
+
const bullJob = await queue.getJob(job.bullJobId);
|
|
88
|
+
if (!bullJob) {
|
|
89
|
+
bullJobId = await addJob(jobPayload, jobParams);
|
|
90
|
+
existing.bullJobId = bullJobId;
|
|
91
|
+
await existing.save();
|
|
92
|
+
} else {
|
|
93
|
+
// make sure the cron is the same
|
|
94
|
+
const bullCron = R.path(
|
|
95
|
+
['opts', 'repeat', 'cron'],
|
|
96
|
+
await queue.getJob(bullJobId),
|
|
97
|
+
);
|
|
98
|
+
if (bullCron !== job.cron) {
|
|
99
|
+
await queue.removeJobs(bullJobId);
|
|
100
|
+
bullJobId = await addJob(jobPayload, jobParams);
|
|
101
|
+
existing.bullJobId = bullJobId;
|
|
102
|
+
await existing.save();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
bullJobId = await addJob(jobPayload, jobParams);
|
|
107
|
+
await sqldb.CronJobs.create({ ...where, bullJobId });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
51
113
|
return res.json({ value: newAppKey.get('id').toString() });
|
|
52
114
|
} catch (err) {
|
|
53
115
|
return next(err);
|
|
@@ -133,10 +195,70 @@ const migrateApp = async ({ integrationId, action }) => {
|
|
|
133
195
|
throw Error('No recognized action');
|
|
134
196
|
};
|
|
135
197
|
|
|
198
|
+
const getAppScheduledJobs = async ({
|
|
199
|
+
integrationId,
|
|
200
|
+
userId,
|
|
201
|
+
hint,
|
|
202
|
+
}) => {
|
|
203
|
+
const where = R.reject(R.isNil)({
|
|
204
|
+
pluginName: integrationId,
|
|
205
|
+
userId,
|
|
206
|
+
hint,
|
|
207
|
+
});
|
|
208
|
+
const jobs = await sqldb.CronJobs.findAll({
|
|
209
|
+
where,
|
|
210
|
+
raw: true,
|
|
211
|
+
});
|
|
212
|
+
return { jobs };
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const runAppJob = async (req, res, next) => {
|
|
216
|
+
const {
|
|
217
|
+
body: {
|
|
218
|
+
payload,
|
|
219
|
+
jobParams,
|
|
220
|
+
},
|
|
221
|
+
params: {
|
|
222
|
+
app: pluginName,
|
|
223
|
+
hint,
|
|
224
|
+
userId,
|
|
225
|
+
},
|
|
226
|
+
} = req;
|
|
227
|
+
try {
|
|
228
|
+
const bullJobId = await addJob({
|
|
229
|
+
...payload,
|
|
230
|
+
pluginName,
|
|
231
|
+
hint,
|
|
232
|
+
userId,
|
|
233
|
+
}, jobParams);
|
|
234
|
+
assert(bullJobId);
|
|
235
|
+
return res.json(await jobStatus({ jobId: bullJobId }));
|
|
236
|
+
} catch (err) {
|
|
237
|
+
return next(err);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const getJobStatus = async (req, res, next) => {
|
|
242
|
+
const {
|
|
243
|
+
params: {
|
|
244
|
+
jobId,
|
|
245
|
+
},
|
|
246
|
+
} = req;
|
|
247
|
+
try {
|
|
248
|
+
const returnValue = await jobStatus({ jobId });
|
|
249
|
+
return res.json(returnValue);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return next(err);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
136
255
|
module.exports = {
|
|
137
256
|
createAppToken,
|
|
138
257
|
deleteAppToken,
|
|
258
|
+
getAppScheduledJobs,
|
|
259
|
+
getJobStatus,
|
|
139
260
|
jwtEncode,
|
|
140
261
|
listAppTokens,
|
|
141
262
|
migrateApp,
|
|
263
|
+
runAppJob,
|
|
142
264
|
};
|
package/controllers/ping.js
CHANGED
package/controllers/user.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
// controllers/user.js
|
|
2
|
-
const { omit
|
|
3
|
-
const assert = require('assert');
|
|
4
|
-
const Promise = require('bluebird');
|
|
5
|
-
const moment = require('moment');
|
|
2
|
+
const { omit } = require('ramda');
|
|
6
3
|
|
|
7
|
-
const { UserAppKey
|
|
8
|
-
// const { load } = require('../../plugins/index.js');
|
|
4
|
+
const { UserAppKey } = require('../models');
|
|
9
5
|
|
|
10
6
|
const userAppList = async (req, res, next) => {
|
|
11
7
|
const { params: { userId } } = req;
|
package/index.js
CHANGED
|
@@ -5,6 +5,9 @@ const swaggerUi = require('swagger-ui-express');
|
|
|
5
5
|
const yaml = require('js-yaml');
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const { pickBy } = require('ramda');
|
|
8
|
+
const bb = require('bluebird');
|
|
9
|
+
const { Op } = require('sequelize');
|
|
10
|
+
const R = require('ramda');
|
|
8
11
|
|
|
9
12
|
const schema = yaml.load(fs.readFileSync(`${__dirname}/api.yml`));
|
|
10
13
|
|
|
@@ -14,18 +17,20 @@ const adminController = require('./controllers/admin');
|
|
|
14
17
|
const appController = require('./controllers/app');
|
|
15
18
|
const userController = require('./controllers/user');
|
|
16
19
|
const bookingsController = require('./controllers/bookings');
|
|
20
|
+
const { Integration } = require('./models');
|
|
17
21
|
|
|
18
|
-
module.exports = ({
|
|
22
|
+
module.exports = async ({
|
|
19
23
|
apiDocs = true,
|
|
20
24
|
elasticLogIndex = 'apilog_ti2',
|
|
21
25
|
elasticLogsClient,
|
|
22
26
|
plugins: pluginsParam = {},
|
|
23
27
|
port: portParam,
|
|
24
28
|
startServer = true,
|
|
29
|
+
worker = false,
|
|
25
30
|
}) => {
|
|
26
31
|
const port = portParam || process.env.PORT || 10010;
|
|
27
|
-
|
|
28
|
-
const plugins = Object.entries(pluginsParam)
|
|
32
|
+
// create the plugin instances
|
|
33
|
+
const plugins = await bb.map(Object.entries(pluginsParam), async ([pluginName, Plugin]) => {
|
|
29
34
|
// pass all env variables
|
|
30
35
|
const pluginEnv = pickBy(
|
|
31
36
|
(_val, key) => key.substring(0, `ti2_${pluginName}`.length)
|
|
@@ -37,9 +42,34 @@ module.exports = ({
|
|
|
37
42
|
const nuName = attr.replace(/_/g, '-').replace(`ti2-${pluginName}-`, '');
|
|
38
43
|
params[nuName] = value;
|
|
39
44
|
});
|
|
40
|
-
|
|
45
|
+
const pluginInstance = await new Plugin({ name: pluginName, ...params });
|
|
46
|
+
return pluginInstance;
|
|
47
|
+
});
|
|
48
|
+
if (worker) {
|
|
49
|
+
return require('./worker/index')({ plugins });
|
|
50
|
+
}
|
|
51
|
+
// make sure all plugins have a DB entry
|
|
52
|
+
const pluginNames = plugins.map(R.prop('name'));
|
|
53
|
+
const matchedIntegrations = await Integration.findAll({
|
|
54
|
+
attributes: ['name'],
|
|
55
|
+
where: {
|
|
56
|
+
name: {
|
|
57
|
+
[Op.in]: pluginNames,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
raw: true,
|
|
41
61
|
});
|
|
42
|
-
|
|
62
|
+
const missingIntegrations = R.difference(pluginNames, matchedIntegrations.map(R.prop('name')));
|
|
63
|
+
if (missingIntegrations.length > 0) {
|
|
64
|
+
// need to crete the missing integrations
|
|
65
|
+
await Integration.bulkCreate(missingIntegrations.map(name => ({
|
|
66
|
+
name,
|
|
67
|
+
packageName: `ti2-${name}`,
|
|
68
|
+
adminEmail: `ti2+${name}@localhost.local`,
|
|
69
|
+
})));
|
|
70
|
+
}
|
|
71
|
+
// create the API Web server
|
|
72
|
+
const app = express();
|
|
43
73
|
const api = {
|
|
44
74
|
...pingController,
|
|
45
75
|
...adminController,
|
|
@@ -141,9 +171,12 @@ module.exports = ({
|
|
|
141
171
|
connect(app);
|
|
142
172
|
app.use(middleware.mock());
|
|
143
173
|
// global error Handling
|
|
144
|
-
app.use((err, req, res
|
|
174
|
+
app.use((err, req, res) => {
|
|
145
175
|
// console.log(req.headers.['X-Request-Id'], err);
|
|
146
176
|
res.status(err.status || 500);
|
|
177
|
+
if ((process.env.JEST_WORKER_ID)) {
|
|
178
|
+
console.debug(err);
|
|
179
|
+
}
|
|
147
180
|
return res.json({
|
|
148
181
|
message: err.message || 'Internal Error',
|
|
149
182
|
});
|
|
@@ -156,8 +189,11 @@ module.exports = ({
|
|
|
156
189
|
// first create a generic "terminator"
|
|
157
190
|
const terminator = sig => {
|
|
158
191
|
if (typeof sig === 'string') {
|
|
159
|
-
console.log(
|
|
160
|
-
|
|
192
|
+
console.log(
|
|
193
|
+
'%s: Received %s - terminating ti2 ...',
|
|
194
|
+
Date(Date.now()),
|
|
195
|
+
sig,
|
|
196
|
+
);
|
|
161
197
|
process.exit(1);
|
|
162
198
|
}
|
|
163
199
|
console.log('%s: Node server stopped.', Date(Date.now()));
|
package/jest.setup.js
ADDED
|
@@ -3,8 +3,8 @@ module.exports = {
|
|
|
3
3
|
type: Sequelize.TEXT,
|
|
4
4
|
allowNull: false,
|
|
5
5
|
}),
|
|
6
|
-
down: (queryInterface, Sequelize) =>
|
|
6
|
+
down: (queryInterface, Sequelize) => queryInterface.changeColumn('UserAppKeys', 'appKey', {
|
|
7
7
|
type: Sequelize.STRING,
|
|
8
8
|
allowNull: false,
|
|
9
|
-
})
|
|
9
|
+
}),
|
|
10
10
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
up: (queryInterface, Sequelize) => queryInterface
|
|
3
|
+
.createTable('CronJobs', {
|
|
4
|
+
pluginName: {
|
|
5
|
+
allowNull: false,
|
|
6
|
+
primaryKey: true,
|
|
7
|
+
type: Sequelize.STRING,
|
|
8
|
+
},
|
|
9
|
+
userId: {
|
|
10
|
+
allowNull: false,
|
|
11
|
+
primaryKey: true,
|
|
12
|
+
type: Sequelize.STRING,
|
|
13
|
+
},
|
|
14
|
+
hint: {
|
|
15
|
+
allowNull: false,
|
|
16
|
+
primaryKey: true,
|
|
17
|
+
type: Sequelize.STRING,
|
|
18
|
+
},
|
|
19
|
+
pluginJobId: {
|
|
20
|
+
allowNull: false,
|
|
21
|
+
type: Sequelize.STRING,
|
|
22
|
+
},
|
|
23
|
+
bullJobId: {
|
|
24
|
+
allowNull: false,
|
|
25
|
+
type: Sequelize.STRING,
|
|
26
|
+
},
|
|
27
|
+
cron: {
|
|
28
|
+
allowNull: false,
|
|
29
|
+
type: Sequelize.STRING,
|
|
30
|
+
},
|
|
31
|
+
createdAt: {
|
|
32
|
+
allowNull: false,
|
|
33
|
+
type: Sequelize.DATE,
|
|
34
|
+
},
|
|
35
|
+
updatedAt: {
|
|
36
|
+
allowNull: false,
|
|
37
|
+
type: Sequelize.DATE,
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
down: queryInterface => queryInterface.dropTable('CronJobs'),
|
|
41
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const Sequelize = require('sequelize');
|
|
2
|
+
const db = require('./db');
|
|
3
|
+
|
|
4
|
+
const CronJobs = db.define('CronJobs', {
|
|
5
|
+
pluginName: {
|
|
6
|
+
type: Sequelize.STRING,
|
|
7
|
+
primaryKey: true,
|
|
8
|
+
},
|
|
9
|
+
userId: {
|
|
10
|
+
type: Sequelize.STRING,
|
|
11
|
+
primaryKey: true,
|
|
12
|
+
},
|
|
13
|
+
hint: {
|
|
14
|
+
type: Sequelize.STRING,
|
|
15
|
+
primaryKey: true,
|
|
16
|
+
},
|
|
17
|
+
pluginJobId: {
|
|
18
|
+
type: Sequelize.STRING,
|
|
19
|
+
primaryKey: true,
|
|
20
|
+
},
|
|
21
|
+
bullJobId: {
|
|
22
|
+
type: Sequelize.STRING,
|
|
23
|
+
allowNull: false,
|
|
24
|
+
},
|
|
25
|
+
cron: {
|
|
26
|
+
type: Sequelize.STRING,
|
|
27
|
+
allowNull: false,
|
|
28
|
+
},
|
|
29
|
+
}, {});
|
|
30
|
+
|
|
31
|
+
module.exports = CronJobs;
|
package/models/index.js
CHANGED
|
@@ -10,6 +10,7 @@ fs
|
|
|
10
10
|
.readdirSync(__dirname)
|
|
11
11
|
.filter(file => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'))
|
|
12
12
|
.forEach(file => {
|
|
13
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
13
14
|
const model = require(path.join(__dirname, file));
|
|
14
15
|
models[model.name] = model;
|
|
15
16
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ti2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "Tourist Industry Exchange (TI2)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -8,21 +8,28 @@
|
|
|
8
8
|
"sequelize": "sequelize-cli",
|
|
9
9
|
"doc": "jsdoc -c jsdoc.conf.json -u tutorials"
|
|
10
10
|
},
|
|
11
|
+
"jest": {
|
|
12
|
+
"setupFilesAfterEnv": [
|
|
13
|
+
"./jest.setup.js"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
11
16
|
"repository": {
|
|
12
17
|
"type": "git",
|
|
13
18
|
"url": "git+https://github.com/ti2travel/ti2.git"
|
|
14
19
|
},
|
|
15
20
|
"bin": "./cli.js",
|
|
16
21
|
"author": "",
|
|
17
|
-
"license": "GPL-3",
|
|
22
|
+
"license": "GPL-3.0",
|
|
18
23
|
"bugs": {
|
|
19
24
|
"url": "https://github.com/ti2travel/ti2/issues"
|
|
20
25
|
},
|
|
21
26
|
"homepage": "https://github.com/ti2travel/ti2#readme",
|
|
22
27
|
"dependencies": {
|
|
23
28
|
"bluebird": "^3.7.2",
|
|
29
|
+
"bull": "^4.8.2",
|
|
24
30
|
"commander": "^9.2.0",
|
|
25
31
|
"express": "^4.17.2",
|
|
32
|
+
"ioredis": "^5.0.4",
|
|
26
33
|
"js-yaml": "^4.1.0",
|
|
27
34
|
"jsonwebtoken": "^8.5.1",
|
|
28
35
|
"jwt-promise": "^1.0.0",
|
|
@@ -34,17 +41,18 @@
|
|
|
34
41
|
"swagger-express-middleware": "3.0.0-alpha.5",
|
|
35
42
|
"swagger-routes-express": "3.3.1",
|
|
36
43
|
"swagger-ui-express": "4.3.0",
|
|
44
|
+
"throng": "^5.0.0",
|
|
37
45
|
"umzug": "^3.1.1",
|
|
38
46
|
"uninstall": "^0.0.0"
|
|
39
47
|
},
|
|
40
48
|
"devDependencies": {
|
|
49
|
+
"@salvador/minami": "^1.2.3",
|
|
41
50
|
"chance": "^1.1.8",
|
|
42
51
|
"eslint": "^8.7.0",
|
|
43
52
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
44
53
|
"eslint-plugin-import": "^2.25.4",
|
|
45
54
|
"jest": "^27.4.7",
|
|
46
55
|
"jsdoc": "^3.6.10",
|
|
47
|
-
"minami": "git+https://github.com/MyNameReallySux/minami.git#sort-data",
|
|
48
56
|
"supertest": "^6.2.2"
|
|
49
57
|
}
|
|
50
58
|
}
|
package/test/plugin.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
/* global jest, expect */
|
|
2
|
+
/* eslint class-methods-use-this: "off", max-len: "off" */
|
|
3
|
+
|
|
4
|
+
const jestPlugin = (() => {
|
|
5
|
+
if (typeof jest === 'undefined') {
|
|
6
|
+
return {
|
|
7
|
+
fn: fn => fn,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return jest;
|
|
11
|
+
})();
|
|
12
|
+
|
|
2
13
|
const chance = require('chance').Chance();
|
|
3
14
|
|
|
4
15
|
class Plugin {
|
|
@@ -15,16 +26,16 @@ class Plugin {
|
|
|
15
26
|
this[attr] = value;
|
|
16
27
|
});
|
|
17
28
|
// mock implementations
|
|
18
|
-
this.validateToken =
|
|
19
|
-
this.getProfile =
|
|
20
|
-
this.updateProfile =
|
|
21
|
-
this.getProduct =
|
|
22
|
-
this.getProducts =
|
|
23
|
-
this.createLocation =
|
|
24
|
-
this.updateLocation =
|
|
25
|
-
this.searchBooking =
|
|
26
|
-
this.searchProducts =
|
|
27
|
-
this.searchAvailability =
|
|
29
|
+
this.validateToken = jestPlugin.fn(() => true);
|
|
30
|
+
this.getProfile = jestPlugin.fn(() => {});
|
|
31
|
+
this.updateProfile = jestPlugin.fn(() => {});
|
|
32
|
+
this.getProduct = jestPlugin.fn(() => ({ products: [] }));
|
|
33
|
+
this.getProducts = jestPlugin.fn(() => true);
|
|
34
|
+
this.createLocation = jestPlugin.fn(() => ({ locationId: chance.guid() }));
|
|
35
|
+
this.updateLocation = jestPlugin.fn(() => true);
|
|
36
|
+
this.searchBooking = jestPlugin.fn(() => ({ bookings: [] }));
|
|
37
|
+
this.searchProducts = jestPlugin.fn(() => ({ bookings: [], products: [] }));
|
|
38
|
+
this.searchAvailability = jestPlugin.fn(({
|
|
28
39
|
token,
|
|
29
40
|
payload: {
|
|
30
41
|
travelDateStart,
|
|
@@ -40,9 +51,23 @@ class Plugin {
|
|
|
40
51
|
expect(Array.isArray(occupancies)).toBeTruthy();
|
|
41
52
|
return { availability: [{ id: chance.guid() }] };
|
|
42
53
|
});
|
|
54
|
+
this.searchQuote = jestPlugin.fn(() => ({ quote: [{ id: chance.guid() }] }));
|
|
55
|
+
this.createBooking = jestPlugin.fn(() => {});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Background and schedule Jos
|
|
59
|
+
*/
|
|
60
|
+
this.jobs = [{
|
|
61
|
+
method: 'dailyReport',
|
|
62
|
+
payload: {
|
|
63
|
+
action: 'report',
|
|
64
|
+
},
|
|
65
|
+
cron: '0 9 * * *',
|
|
66
|
+
}];
|
|
43
67
|
|
|
44
|
-
this.
|
|
45
|
-
|
|
68
|
+
this.dailyReport = jestPlugin.fn(() => ({
|
|
69
|
+
someVal: true,
|
|
70
|
+
}));
|
|
46
71
|
}
|
|
47
72
|
|
|
48
73
|
/**
|
|
@@ -258,7 +283,7 @@ class Plugin {
|
|
|
258
283
|
return true;
|
|
259
284
|
}
|
|
260
285
|
|
|
261
|
-
searchHotelBooking =
|
|
286
|
+
searchHotelBooking = jestPlugin.fn(() => ({ bookings: [] }));
|
|
262
287
|
|
|
263
288
|
/**
|
|
264
289
|
* Booking Object
|
|
@@ -363,6 +388,7 @@ class Plugin {
|
|
|
363
388
|
searchAvailability() {
|
|
364
389
|
// return (args);
|
|
365
390
|
}
|
|
391
|
+
|
|
366
392
|
/**
|
|
367
393
|
* Retrieve an availability calendar over a date range
|
|
368
394
|
* @async
|
|
@@ -379,6 +405,7 @@ class Plugin {
|
|
|
379
405
|
availabilityCalndar() {
|
|
380
406
|
// return (args);
|
|
381
407
|
}
|
|
408
|
+
|
|
382
409
|
/**
|
|
383
410
|
* A quote return spec
|
|
384
411
|
* @typedef {Object} Quote
|
package/test/utils.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const chance = require('chance').Chance();
|
|
4
4
|
const request = require('supertest');
|
|
5
5
|
const assert = require('assert');
|
|
6
|
+
|
|
6
7
|
const appReq = require('../index');
|
|
7
8
|
const Plugin = require('./plugin');
|
|
8
9
|
const slugify = require('./slugify');
|
|
@@ -10,12 +11,10 @@ const sqldb = require('../models/db');
|
|
|
10
11
|
|
|
11
12
|
const { env: { adminKey } } = process;
|
|
12
13
|
|
|
13
|
-
const timeout = ms => (new Promise(resolve => setTimeout(resolve, ms)));
|
|
14
|
-
|
|
15
14
|
afterAll(async () => {
|
|
16
15
|
await sqldb.connectionManager.close();
|
|
17
16
|
});
|
|
18
|
-
module.exports = (appParams = {}) => {
|
|
17
|
+
module.exports = async (appParams = {}) => {
|
|
19
18
|
const plugins = (() => {
|
|
20
19
|
if (Array.isArray(appParams.plugins)) {
|
|
21
20
|
const retVal = {};
|
|
@@ -28,7 +27,7 @@ module.exports = (appParams = {}) => {
|
|
|
28
27
|
mockName: Plugin,
|
|
29
28
|
};
|
|
30
29
|
})();
|
|
31
|
-
const app = appReq({
|
|
30
|
+
const app = await appReq({
|
|
32
31
|
startServer: false,
|
|
33
32
|
...appParams,
|
|
34
33
|
plugins,
|
|
@@ -104,7 +103,6 @@ module.exports = (appParams = {}) => {
|
|
|
104
103
|
doApiPost,
|
|
105
104
|
doApiPut,
|
|
106
105
|
slugify,
|
|
107
|
-
timeout,
|
|
108
106
|
plugins: app.plugins,
|
|
109
107
|
};
|
|
110
108
|
};
|
package/test/worker.js
ADDED
|
@@ -23,6 +23,7 @@ User + app integration credentials can be added after the app has been added to
|
|
|
23
23
|
A MySQl instance is required and the following environment variables are required:
|
|
24
24
|
|
|
25
25
|
- DB_URL (mysql connection in url format)
|
|
26
|
+
- REDIS_URL (a redis database url to store the background queue)
|
|
26
27
|
- dbCryptoKey (integration details encription key, should be a 32 chars base64 encoded random string)
|
|
27
28
|
- adminKey (a key for admin related requests)
|
|
28
29
|
- jwtKey (a key to encrypt user sessions request)
|
|
@@ -31,6 +32,7 @@ A MySQl instance is required and the following environment variables are require
|
|
|
31
32
|
|
|
32
33
|
```
|
|
33
34
|
DB_URL=mysql://root:@mysqlserver/ti2development
|
|
35
|
+
REDIS_URL=redis://redis:6379
|
|
34
36
|
dbCryptoKey=cG9VYmJyQ2tlSDVHeXQ4RUN4VlNieEJRejNYWDlZU0g=
|
|
35
37
|
jwtSecret=C8k0mrHVfWVTP7pIoZEHRrvFgTULhw3E4swDq1aoDH4P
|
|
36
38
|
frontendKey=IpOiLahrGQxsQimwoK7Z
|
|
@@ -61,21 +63,57 @@ then just create an entry file like this :
|
|
|
61
63
|
|
|
62
64
|
```javascript
|
|
63
65
|
// index.js
|
|
64
|
-
const ti2 = require('ti2')({
|
|
65
|
-
plugins: {
|
|
66
|
-
travelgate: require('ti2-travelgate'),
|
|
67
|
-
ventrata: require('ti2-ventrata'),
|
|
68
|
-
tourconnect: require('ti2-tourconnect'),
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
```
|
|
72
66
|
|
|
73
|
-
|
|
67
|
+
module.exports = (async () => {
|
|
68
|
+
const ti2 = await require('ti2')({
|
|
69
|
+
plugins: {
|
|
70
|
+
ventrata: require('ti2-ventrata'),
|
|
71
|
+
travelgate: require('ti2-travelgate'),
|
|
72
|
+
tourconnect: require('ti2-tourconnect'),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return ti2;
|
|
76
|
+
})();
|
|
74
77
|
|
|
75
78
|
```
|
|
79
|
+
|
|
80
|
+
and start the server:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
76
83
|
$ node index.js
|
|
77
84
|
```
|
|
78
85
|
|
|
86
|
+
in order to support the background job queue (required by some plugins) you should consider the following example:
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const plugins = {
|
|
90
|
+
ventrata: require('ti2-ventrata'),
|
|
91
|
+
travelgate: require('ti2-travelgate'),
|
|
92
|
+
tourconnect: require('ti2-tourconnect'),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
module.exports = (async () => {
|
|
96
|
+
if (process.argv.indexOf('worker') > 0) {
|
|
97
|
+
const worker = await require('ti2')({
|
|
98
|
+
plugins,
|
|
99
|
+
worker: true,
|
|
100
|
+
});
|
|
101
|
+
return worker;
|
|
102
|
+
} else {
|
|
103
|
+
const ti2 = await require('ti2')({
|
|
104
|
+
plugins,
|
|
105
|
+
});
|
|
106
|
+
return ti2;
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
and start a background worker (on a second terminal), required for some plugins:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
$ node index.js worker
|
|
115
|
+
```
|
|
116
|
+
|
|
79
117
|
## API logging
|
|
80
118
|
|
|
81
119
|
Currently logging is supported via elastic search, an elasticLogClient value can ben passed down, example:
|
|
@@ -87,12 +125,26 @@ const elasticLogsClient = (() => {
|
|
|
87
125
|
return new Client({ node: elasticLogs });
|
|
88
126
|
})();
|
|
89
127
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
128
|
+
const plugins = {
|
|
129
|
+
ventrata: require('ti2-ventrata'),
|
|
130
|
+
travelgate: require('ti2-travelgate'),
|
|
131
|
+
tourconnect: require('ti2-tourconnect'),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
module.exports = (async () => {
|
|
135
|
+
if (process.argv.indexOf('worker') > 0) {
|
|
136
|
+
const worker = await require('ti2')({
|
|
137
|
+
elasticLogsClient,
|
|
138
|
+
plugins,
|
|
139
|
+
worker: true,
|
|
140
|
+
});
|
|
141
|
+
return worker;
|
|
142
|
+
} else {
|
|
143
|
+
const ti2 = await require('ti2')({
|
|
144
|
+
elasticLogsClient,
|
|
145
|
+
plugins,
|
|
146
|
+
});
|
|
147
|
+
return ti2;
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
98
150
|
```
|
package/worker/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const throng = require('throng');
|
|
2
|
+
require('util').inspect.defaultOptions.depth = null;
|
|
3
|
+
const fakePlugin = require('../test/plugin');
|
|
4
|
+
|
|
5
|
+
const { queue, saveResult } = require('./queue');
|
|
6
|
+
|
|
7
|
+
const workers = process.env.WEB_CONCURRENCY || 2;
|
|
8
|
+
|
|
9
|
+
const maxJobsPerWorker = 1;
|
|
10
|
+
|
|
11
|
+
const worker = ({ plugins: pluginsParam }) => (id, disconnect) => {
|
|
12
|
+
console.log(`Started worker ${id}`);
|
|
13
|
+
|
|
14
|
+
const bye = () => {
|
|
15
|
+
console.log(`Worker ${id} exiting`);
|
|
16
|
+
disconnect();
|
|
17
|
+
process.exit(0);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
process.on('SIGTERM', bye);
|
|
21
|
+
process.on('SIGUSR2', bye);
|
|
22
|
+
process.on('SIGINT', bye);
|
|
23
|
+
|
|
24
|
+
queue.process(maxJobsPerWorker, async job => {
|
|
25
|
+
const {
|
|
26
|
+
data: {
|
|
27
|
+
pluginName,
|
|
28
|
+
method,
|
|
29
|
+
userId,
|
|
30
|
+
inTesting,
|
|
31
|
+
},
|
|
32
|
+
id: jobId,
|
|
33
|
+
data: params,
|
|
34
|
+
} = job;
|
|
35
|
+
console.log(`job ${jobId} > running ${pluginName}:${method} for ${userId}`);
|
|
36
|
+
const plugins = await (async () => {
|
|
37
|
+
if (inTesting) {
|
|
38
|
+
return [...pluginsParam, await new fakePlugin({ name: pluginName })];
|
|
39
|
+
}
|
|
40
|
+
return pluginsParam;
|
|
41
|
+
})();
|
|
42
|
+
const thePlugin = plugins.find(({ name }) => name === pluginName);
|
|
43
|
+
const resultValue = await thePlugin[method](params);
|
|
44
|
+
console.log(`job ${jobId} > completed`);
|
|
45
|
+
await saveResult({ id: jobId, resultValue });
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
module.exports = async args => throng({
|
|
50
|
+
workers,
|
|
51
|
+
worker: worker(args),
|
|
52
|
+
signals: ['SIGUSR2', 'SIGTERM', 'SIGINT'],
|
|
53
|
+
});
|
package/worker/queue.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const Queue = require('bull');
|
|
2
|
+
const Redis = require('ioredis');
|
|
3
|
+
const R = require('ramda');
|
|
4
|
+
|
|
5
|
+
const REDIS_URL = process.env.REDIS_URL || 'redis://redis:6379';
|
|
6
|
+
const itemsTTL = 3 * 60 * 60; // 3 hours
|
|
7
|
+
|
|
8
|
+
const queue = new Queue('work', `${REDIS_URL}/0`);
|
|
9
|
+
const redisResults = new Redis(`${REDIS_URL}/1`);
|
|
10
|
+
|
|
11
|
+
const getPending = async () => {
|
|
12
|
+
const pending = await queue.count();
|
|
13
|
+
const running = await queue.getActiveCount();
|
|
14
|
+
return pending + running;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const allDone = async () => {
|
|
18
|
+
const left1 = await getPending();
|
|
19
|
+
if (left1 === 0) return true;
|
|
20
|
+
return (new Promise(resolve => {
|
|
21
|
+
queue.on('global:completed', async () => {
|
|
22
|
+
const left = await getPending();
|
|
23
|
+
if (left === 0) resolve();
|
|
24
|
+
});
|
|
25
|
+
}));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
queue.on('failed', (job, err) => {
|
|
29
|
+
console.log(`job ${job.id} failed`, err);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const addJob = async (payload, paramsParam) => {
|
|
33
|
+
const params = paramsParam || {};
|
|
34
|
+
const inTesting = Boolean(process.env.JEST_WORKER_ID);
|
|
35
|
+
const id = R.path(['id'], await queue.add({
|
|
36
|
+
...payload,
|
|
37
|
+
inTesting,
|
|
38
|
+
}, {
|
|
39
|
+
removeOnComplete: true,
|
|
40
|
+
...params,
|
|
41
|
+
}));
|
|
42
|
+
return id;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const saveResult = async ({ id, resultValue }) => {
|
|
46
|
+
await redisResults.set(id, JSON.stringify(resultValue), 'EX', itemsTTL);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const jobStatus = async ({ jobId }) => {
|
|
50
|
+
try {
|
|
51
|
+
const job = await queue.getJob(jobId);
|
|
52
|
+
if (!job) {
|
|
53
|
+
// check if it is done
|
|
54
|
+
const result = JSON.parse(await redisResults.get(jobId));
|
|
55
|
+
// await global.sleep();
|
|
56
|
+
if (result) {
|
|
57
|
+
return {
|
|
58
|
+
jobId,
|
|
59
|
+
status: 'success',
|
|
60
|
+
result,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { // 404
|
|
64
|
+
jobId,
|
|
65
|
+
status: 'failed',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const state = await job.getState();
|
|
69
|
+
return {
|
|
70
|
+
jobId,
|
|
71
|
+
status: state.toLowerCase(),
|
|
72
|
+
};
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.log(`could not get job status for ${jobId}`, err);
|
|
75
|
+
return { // 500
|
|
76
|
+
jobId,
|
|
77
|
+
status: 'failed',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
module.exports = {
|
|
82
|
+
addJob,
|
|
83
|
+
allDone,
|
|
84
|
+
jobStatus,
|
|
85
|
+
queue,
|
|
86
|
+
saveResult,
|
|
87
|
+
redisResults,
|
|
88
|
+
};
|