ti2 1.0.14 → 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 CHANGED
@@ -42,4 +42,5 @@ module.exports = {
42
42
  },
43
43
  ],
44
44
  },
45
+ ignorePatterns: ['**/docs/*'],
45
46
  };
@@ -1,15 +1,13 @@
1
1
  name: Tests Workflow
2
2
 
3
- on:
4
- workflow_dispatch:
5
- push:
3
+ on: [workflow_dispatch, push, pull_request]
6
4
 
7
5
  jobs:
8
6
  test:
9
7
  timeout-minutes: 10
10
8
  runs-on: ubuntu-latest
11
9
  container:
12
- image: node:erbium
10
+ image: node:16.15.0
13
11
  services:
14
12
  mysql:
15
13
  image: mysql
@@ -19,9 +17,12 @@ jobs:
19
17
  MYSQL_ROOT_PASSWORD: root
20
18
  ports:
21
19
  - 3306:3306
20
+ redis:
21
+ image: redis
22
22
  env:
23
23
  CI: true
24
24
  DB_URL: 'mysql://root:root@mysql/ti2development'
25
+ REDIS_URL: 'redis://redis:6379'
25
26
  dbCryptoKey: 'YmYzYzE5OWMyNDcwY2I0NzdkOTA3YjFlMDkxN2MxN2I='
26
27
  jwtSecret: '9rifawckwztuKehocJOtGM6deTqICGWzIyc5CqgNyi'
27
28
  frontendKey: 'lYjEJHzb7lbQ1hk3ypjX'
@@ -32,10 +33,11 @@ jobs:
32
33
  with:
33
34
  node-version: 12
34
35
  - name: install dependencies
35
- run: npm i
36
+ run: npm ci
36
37
  - name: create/migrate the database
37
38
  run: npx sequelize db:create && npx sequelize db:migrate
38
39
  - name: seed the database
39
40
  run: npx sequelize db:seed:all
41
+ - run: RUNNER_TRACKING_ID="" && (nohup node ./test/worker.js&)
40
42
  - name: run the tests
41
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:
@@ -816,6 +966,97 @@ paths:
816
966
  application/json:
817
967
  schema:
818
968
  $ref: '#/components/schemas/bookingsProductSearchReturn'
969
+ /bookings/{appKey}/{userId}/availabilitycalendar:
970
+ post:
971
+ security:
972
+ - bearerAuth: ['admin', 'user']
973
+ tags:
974
+ - bookings
975
+ summary: Availability Search
976
+ operationId: bookingsAvailabilityCalendar
977
+ parameters:
978
+ - name: appKey
979
+ in: path
980
+ description: app to search availability for, defaults to first token
981
+ required: true
982
+ example: 'travelgate-hotelx'
983
+ schema:
984
+ type: string
985
+ - name: userId
986
+ in: path
987
+ description: the user/company that the appKey belongs to
988
+ required: true
989
+ schema:
990
+ type: string
991
+ requestBody:
992
+ description: both app details
993
+ required: true
994
+ content:
995
+ application/json:
996
+ schema:
997
+ $ref: '#/components/schemas/AnyValue'
998
+ responses:
999
+ '200':
1000
+ description: search results
1001
+ content:
1002
+ application/json:
1003
+ schema:
1004
+ type: object
1005
+ properties:
1006
+ availability:
1007
+ type: array
1008
+ required: true
1009
+ items:
1010
+ $ref: '#/components/schemas/availabilityItem'
1011
+ /bookings/{appKey}/{userId}/{hint}/availabilitycalendar:
1012
+ post:
1013
+ security:
1014
+ - bearerAuth: ['admin', 'user']
1015
+ tags:
1016
+ - bookings
1017
+ summary: Availability Search for an specific hint
1018
+ operationId: bookingsAvailabilityCalendar
1019
+ parameters:
1020
+ - name: appKey
1021
+ in: path
1022
+ description: app to search availability for, defaults to first token
1023
+ required: true
1024
+ example: 'travelgate-hotelx'
1025
+ schema:
1026
+ type: string
1027
+ - name: userId
1028
+ in: path
1029
+ description: the user/company that the appKey belongs to
1030
+ required: true
1031
+ schema:
1032
+ type: string
1033
+ - name: hint
1034
+ in: path
1035
+ description: a specific token hint to be used
1036
+ required: true
1037
+ example: 'travelgate-australia'
1038
+ schema:
1039
+ type: string
1040
+ requestBody:
1041
+ description: both app details
1042
+ required: true
1043
+ content:
1044
+ application/json:
1045
+ schema:
1046
+ $ref: '#/components/schemas/AnyValue'
1047
+ responses:
1048
+ '200':
1049
+ description: search results
1050
+ content:
1051
+ application/json:
1052
+ schema:
1053
+ type: object
1054
+ properties:
1055
+ availability:
1056
+ type: array
1057
+ required: true
1058
+ items:
1059
+ $ref: '#/components/schemas/availabilityItem'
819
1060
  /bookings/{appKey}/{userId}/availability:
820
1061
  post:
821
1062
  security:
@@ -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 => (!Boolean(val)))) {
74
+ if (result.some(val => (!val))) {
75
75
  return next();
76
76
  }
77
77
  return next(result[0]);
package/cli.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command, Argument } = require('commander');
4
+
5
+ const { migrateApp } = require('./controllers/app');
6
+ const { migrate } = require('./controllers/admin');
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('ti2-cli')
12
+ .description('TI2 Command line interface utilities')
13
+ .version((require('./package.json')).version);
14
+
15
+ program.command('dbapp')
16
+ .description('Handle database migrations for apps')
17
+ .argument('<string>', 'integration to operate on')
18
+ .addArgument(
19
+ new Argument('<action>', 'action to take', 'migrate')
20
+ .choices(['migrate', 'revert']),
21
+ )
22
+ .action(async (integration, action) => {
23
+ await migrateApp({ integrationId: integration, action });
24
+ });
25
+
26
+ program.command('db')
27
+ .description('Handle database migrations for integrations')
28
+ .addArgument(
29
+ new Argument('<action>', 'action to take', 'migrate')
30
+ .choices(['migrate', 'revert']),
31
+ )
32
+ .action(async action => {
33
+ await migrate({ action });
34
+ });
35
+
36
+ program.parse();
package/closeAll.js ADDED
@@ -0,0 +1,6 @@
1
+ const { redisResults, queue } = require('./worker/queue');
2
+
3
+ module.exports = async () => {
4
+ await queue.close();
5
+ await redisResults.quit();
6
+ };
@@ -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).toEqual(
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
- const { doApiPost, doApiGet, doApiPut } = testUtils({
20
- plugins: [appName],
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
- const {
26
- doApiDelete,
27
- doApiGet,
28
- doApiPost,
29
- plugins,
30
- } = testUtils({
31
- plugins: [newApp.name],
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
- const { doApiPost, doApiGet, doApiDelete } = testUtils({
19
- plugins: [appName],
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',
@@ -1,6 +1,11 @@
1
1
  const jwt = require('jsonwebtoken');
2
2
  const assert = require('assert');
3
3
  const { omit } = require('ramda');
4
+ const { Umzug, SequelizeStorage } = require('umzug');
5
+ const Sequelize = require('sequelize');
6
+ const fs = require('fs').promises;
7
+ const path = require('path');
8
+
4
9
  const sqldb = require('../models');
5
10
 
6
11
  const { env: { jwtSecret } } = process;
@@ -91,10 +96,50 @@ const resetIntegrationToken = async (req, res, next) => {
91
96
  }
92
97
  };
93
98
 
99
+ const migrate = async ({ action }) => {
100
+ const { sequelize } = sqldb;
101
+ const migrationsPath = path.join(
102
+ __dirname,
103
+ '../',
104
+ 'migrations',
105
+ );
106
+ try {
107
+ await fs.access(migrationsPath);
108
+ } catch (err) {
109
+ throw Error(`Could not find any migrations on ${migrationsPath}`);
110
+ }
111
+ const umzug = new Umzug({
112
+ migrations: {
113
+ glob: `${migrationsPath}/*.js`,
114
+ resolve: ({ name, path: migPath, context }) => {
115
+ const migration = require(migPath);
116
+ return {
117
+ name,
118
+ up: async () => migration.up(context, Sequelize),
119
+ down: async () => migration.down(context, Sequelize),
120
+ };
121
+ },
122
+ },
123
+ context: sequelize.getQueryInterface(),
124
+ storage: new SequelizeStorage({
125
+ sequelize,
126
+ }),
127
+ logger: console,
128
+ });
129
+ if (action === 'migrate') {
130
+ return umzug.up();
131
+ }
132
+ if (action === 'revert') {
133
+ return umzug.down({ to: 0 });
134
+ }
135
+ throw Error('No recognized action');
136
+ };
137
+
94
138
  module.exports = {
95
139
  createApp,
96
- deleteApp,
97
140
  createUserToken,
141
+ deleteApp,
98
142
  listApps,
143
+ migrate,
99
144
  resetIntegrationToken,
100
145
  };