ti2 1.0.91 → 1.0.93

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/api.yml CHANGED
@@ -41,15 +41,13 @@ components:
41
41
  type: object
42
42
  description: The job app parameters
43
43
  required: true
44
- example: '{
45
- "method": "dailyReport"
46
- }'
44
+ example:
45
+ method: dailyReport
47
46
  jobParams:
48
47
  type: object
49
48
  description: Additional bull job params
50
- example: '{
51
- "delay": "5000"
52
- }'
49
+ example:
50
+ delay: 5000
53
51
  JobStatus:
54
52
  type: object
55
53
  properties:
@@ -388,6 +386,23 @@ components:
388
386
  settings:
389
387
  type: object
390
388
  description: an object with the app+user seettings
389
+ parameters:
390
+ CallbackUrl:
391
+ name: callbackUrl
392
+ in: query
393
+ description: Callback URL for asynchronous operations
394
+ required: false
395
+ schema:
396
+ type: string
397
+ format: url
398
+ BackgroundJob:
399
+ name: backgroundJob
400
+ in: query
401
+ description: Whether to run the job in the background
402
+ required: false
403
+ schema:
404
+ type: boolean
405
+ default: false
391
406
  paths:
392
407
  /ping:
393
408
  get:
@@ -1640,6 +1655,8 @@ paths:
1640
1655
  required: true
1641
1656
  schema:
1642
1657
  type: string
1658
+ - $ref: '#/components/parameters/CallbackUrl'
1659
+ - $ref: '#/components/parameters/BackgroundJob'
1643
1660
  requestBody:
1644
1661
  description: Search query
1645
1662
  required: true
@@ -1683,6 +1700,8 @@ paths:
1683
1700
  example: 'travelgate-australia'
1684
1701
  schema:
1685
1702
  type: string
1703
+ - $ref: '#/components/parameters/CallbackUrl'
1704
+ - $ref: '#/components/parameters/BackgroundJob'
1686
1705
  requestBody:
1687
1706
  description: Search query
1688
1707
  required: true
package/closeAll.js CHANGED
@@ -1,8 +1,22 @@
1
1
  const { redisResults, queue } = require('./worker/queue');
2
2
  const { cache } = require('./cache');
3
+ const { sequelize } = require('./models');
3
4
 
4
5
  module.exports = async () => {
6
+ // Close queue and redis connections
5
7
  await queue.close();
6
8
  await redisResults.quit();
7
9
  await cache.quit();
10
+
11
+ // Close database connection if it exists and is open
12
+ if (sequelize && typeof sequelize.close === 'function') {
13
+ try {
14
+ await sequelize.close();
15
+ } catch (err) {
16
+ // If connection is already closed, ignore the error
17
+ if (!err.message.includes('Connection already closed')) {
18
+ console.error('Error closing sequelize connection:', err);
19
+ }
20
+ }
21
+ }
8
22
  };
@@ -1,9 +1,8 @@
1
- /* globals beforeAll describe it expect */
1
+ /* globals beforeAll describe it expect jest */
2
2
  const axios = require('axios');
3
3
  const MockAdapter = require('axios-mock-adapter');
4
4
  const chance = require('chance').Chance();
5
5
 
6
-
7
6
  const testUtils = require('../../test/utils');
8
7
 
9
8
  const { env: { adminKey } } = process;
@@ -15,7 +14,8 @@ describe('allotment', () => {
15
14
  adminEmail: 'engineering+tourplantest@tourconnect.com',
16
15
  };
17
16
  const appKey = newApp.name;
18
- const userId = '551394be5ac58e5c76000019';
17
+ // Generate a random userId for better test isolation
18
+ const userId = `test-user-${chance.guid()}`;
19
19
  const token = {
20
20
  endpoint: 'https://someendpoint.com',
21
21
  username: chance.guid(),
@@ -25,14 +25,12 @@ describe('allotment', () => {
25
25
  tokenHint: 'testingToken',
26
26
  token,
27
27
  };
28
- let doApiDelete;
29
28
  let doApiGet;
30
29
  let doApiPost;
31
30
  let plugins;
32
31
  let userToken;
33
32
  beforeAll(async () => {
34
33
  ({
35
- doApiDelete,
36
34
  doApiGet,
37
35
  doApiPost,
38
36
  plugins,
@@ -40,18 +38,8 @@ describe('allotment', () => {
40
38
  plugins: [appKey],
41
39
  }));
42
40
 
43
- // drop any existing user integrations
44
- try {
45
- await doApiDelete({
46
- url: `/${appKey}/${userId}`,
47
- token: adminKey,
48
- payload: { tokenHint: newIntegration.tokenHint },
49
- });
50
- } catch (err) {
51
- // console.debug(err);
52
- }
53
-
54
- // create the user
41
+ // Using random user IDs, so no need to drop existing integrations
42
+ // create the user - handle case where random user doesn't exist yet
55
43
  ({ value: userToken } = await doApiPost({
56
44
  url: '/user',
57
45
  token: adminKey,
@@ -59,20 +47,13 @@ describe('allotment', () => {
59
47
  }));
60
48
  expect(userToken).toBeTruthy();
61
49
 
62
- // create the App+User setup
63
- const { userAppKeys } = await doApiGet({
64
- url: `/user/${userId}/apps`,
50
+ // create the App+User relation
51
+ const { value } = await doApiPost({
52
+ url: `/${appKey}/${userId}`,
65
53
  token: userToken,
54
+ payload: newIntegration,
66
55
  });
67
- if (!userAppKeys.map(e => e.integrationId).includes(appKey)) {
68
- // relation does not exits
69
- const { value } = await doApiPost({
70
- url: `/${appKey}/${userId}`,
71
- token: userToken,
72
- payload: newIntegration,
73
- });
74
- expect(value).toBeTruthy();
75
- }
56
+ expect(value).toBeTruthy();
76
57
  });
77
58
  const payload = {
78
59
  dateFormat: 'DD-MM-YYYY',
@@ -106,10 +87,13 @@ describe('allotment', () => {
106
87
  expect(call.token).toEqual(token);
107
88
  });
108
89
  it('should be able to receive an axios error', async () => {
90
+ // Temporarily silence console.error for this test since we're expecting an error
91
+ const originalConsoleError = console.error;
92
+ console.error = jest.fn();
93
+
109
94
  const mock = new MockAdapter(axios);
110
95
  mock.onGet('http://www.example.com').reply(500, { what: 'nothing here' });
111
96
  let urlParamsError = new URLSearchParams({
112
-
113
97
  ...payload,
114
98
  keyPath: 'errorAxios',
115
99
  }).toString();
@@ -120,8 +104,14 @@ describe('allotment', () => {
120
104
  });
121
105
  expect(returnValue.allotments).toBeFalsy();
122
106
  expect(returnValue.message).toBe('nothing here');
107
+ // Restore console.error even if the test fails
108
+ console.error = originalConsoleError;
123
109
  });
124
110
  it('should be able to receive an error from a regular response', async () => {
111
+ // Temporarily silence console.error for this test since we're expecting an error
112
+ const originalConsoleError = console.error;
113
+ console.error = jest.fn();
114
+
125
115
  const mock = new MockAdapter(axios);
126
116
  mock.onGet('http://www.example.com').reply(200, { error: 'something went wrong' });
127
117
  let urlParamsError = new URLSearchParams({
@@ -135,5 +125,7 @@ describe('allotment', () => {
135
125
  });
136
126
  expect(returnValue.allotments).toBeFalsy();
137
127
  expect(returnValue.message).toBe('something went wrong');
128
+ // Restore console.error even if the test fails
129
+ console.error = originalConsoleError;
138
130
  });
139
131
  });
@@ -112,18 +112,20 @@ describe('app', () => {
112
112
  expect(jobId).toBeTruthy();
113
113
  expect(status).toBeTruthy();
114
114
  });
115
- it('should be able to get the status of a ran job', async () => {
116
- await global.sleep(5e3);
115
+ it('should be able to get the status of a job', async () => {
117
116
  const url = `/${appName}/${userId}/${jobId}/jobStatus`;
118
- const jobStatus = await doApiGet({
119
- url,
120
- token: appKey,
121
- payload: {},
122
- });
123
- expect(jobStatus.jobId).toBeTruthy();
117
+ let jobStatus;
118
+ do {
119
+ jobStatus = await doApiGet({
120
+ url,
121
+ token: appKey,
122
+ payload: {},
123
+ });
124
+ await global.sleep(1e3);
125
+ } while (['waiting', 'active'].includes(jobStatus.status));
124
126
  expect(jobStatus.status).toBe('success');
125
127
  expect(jobStatus.result).toEqual({ someVal: true });
126
- }, 6e3);
128
+ }, 30e3);
127
129
  });
128
130
  it('should be able to get a list of all tokens related to the app', async () => {
129
131
  const { userAppKeys } = await doApiGet({
@@ -14,7 +14,8 @@ describe('user: bookings controller', () => {
14
14
  adminEmail: 'engineering+travelgate@tourconnect.com',
15
15
  };
16
16
  const appKey = newApp.name;
17
- const userId = '551394be5ac58e5c76000019';
17
+ // Generate a random userId for better test isolation
18
+ const userId = `test-user-${chance.guid()}`;
18
19
  const token = {
19
20
  endpoint: 'https://api.travelgatex.com',
20
21
  apiKey: chance.guid(),
@@ -28,6 +29,7 @@ describe('user: bookings controller', () => {
28
29
  let doApiGet;
29
30
  let doApiPost;
30
31
  let plugins;
32
+ let userToken;
31
33
  beforeAll(async () => {
32
34
  ({
33
35
  doApiDelete,
@@ -38,67 +40,69 @@ describe('user: bookings controller', () => {
38
40
  plugins: [newApp.name],
39
41
  }));
40
42
 
41
- // drop all cache keys, clean slate
42
- const cacheKeys = await cache.keys();
43
- cacheKeys.forEach(key => cache.drop({
44
- pluginName: key.split(':')[0],
45
- key: key.split(':')[1],
46
- }));
47
- });
48
- beforeEach(async () => {
49
- jest.clearAllMocks();
50
- });
51
-
52
- let userToken;
53
- it('drop any existing travelgateapp integration', async () => {
54
- try {
55
- await doApiDelete({
56
- url: `/travelgate/${userId}`,
57
- token: adminKey,
58
- payload: { tokenHint: 'testingToken' },
59
- });
60
- } catch (err) {
61
- console.debug(err);
62
- }
63
- try {
64
- await doApiDelete({
65
- url: '/app/travelgate',
66
- token: adminKey,
67
- });
68
- } catch (err) {
69
- console.debug(err);
70
- }
71
- });
72
- it('create the travelgate app', async () => {
43
+ // drop all related cache keys, clean slate for this user
44
+ const cacheKey = hash({
45
+ appKey,
46
+ userId,
47
+ hint: 'testingToken',
48
+ operationId: 'bookingsProductSearch',
49
+ });
50
+ await cache.drop({
51
+ pluginName: appKey,
52
+ key: cacheKey,
53
+ });
54
+ await cache.drop({
55
+ pluginName: appKey,
56
+ key: `${cacheKey}:lock`,
57
+ });
58
+ // create the travelgate app and user
73
59
  const { value: apiKey } = await doApiPost({
74
60
  url: '/app',
75
61
  token: adminKey,
76
62
  payload: newApp,
77
63
  });
78
64
  expect(apiKey).toBeTruthy();
79
- // create the user token
65
+
66
+ // Create the user token
80
67
  ({ value: userToken } = await doApiPost({
81
68
  url: '/user',
82
69
  token: adminKey,
83
70
  payload: { userId },
84
71
  }));
85
72
  expect(userToken).toBeTruthy();
86
- });
87
- it('create the travelGage+User setup', async () => {
88
- const { userAppKeys } = await doApiGet({
73
+ // Create the user first, then get the token
74
+ await doApiPost({
75
+ url: '/user',
76
+ token: adminKey,
77
+ payload: { userId, email: `${userId}@example.com` },
78
+ });
79
+ // Now get the token
80
+ ({ value: userToken } = await doApiPost({
81
+ url: '/user',
82
+ token: adminKey,
83
+ payload: { userId },
84
+ }));
85
+ expect(userToken).toBeTruthy();
86
+ // create the travelGage+User setup
87
+ let userAppKeys = [];
88
+ ({ userAppKeys } = await doApiGet({
89
89
  url: `/user/${userId}/apps`,
90
90
  token: userToken,
91
- });
91
+ }));
92
+ // Check if we need to create the integration
92
93
  if (!userAppKeys.find(e => e.hint === newIntegration.tokenHint
93
- && e.integrationId === newApp.name)) {
94
- // relation does not exits
94
+ && e.integrationId === newApp.name)) {
95
+ // relation does not exist
95
96
  const { value } = await doApiPost({
96
97
  url: `/${appKey}/${userId}`,
97
98
  token: userToken,
98
- payload: newIntegration,
99
- });
100
- expect(value).toBeTruthy();
101
- }
99
+ payload: newIntegration,
100
+ });
101
+ expect(value).toBeTruthy();
102
+ }
103
+ });
104
+ beforeEach(async () => {
105
+ jest.clearAllMocks();
102
106
  });
103
107
  it('should be able to search a for a booking', async () => {
104
108
  const payload = {
@@ -173,6 +177,7 @@ describe('user: bookings controller', () => {
173
177
  expect(products[1].options.length).toBe(2);
174
178
  });
175
179
  it('should be able to get booking products with searchInput', async () => {
180
+
176
181
  const payload = {
177
182
  searchInput: 'Transfer from Sydney Harbor to Hilton Hotel',
178
183
  };
@@ -245,35 +250,21 @@ describe('user: bookings controller', () => {
245
250
  client: 'tourconnect',
246
251
  ttlForProducts: 2, // 2 seconds TTR
247
252
  };
248
-
249
- beforeEach(async () => {
250
- // Clear any existing integration
251
- try {
252
- await doApiDelete({
253
- url: `/${appKey}/${userId}`,
254
- token: adminKey,
255
- payload: { tokenHint: 'ttr-test' },
256
- });
257
- } catch (err) {
258
- console.debug(err);
259
- }
260
-
261
- // Create new integration with short TTR
253
+ beforeAll(async () => {
262
254
  await doApiPost({
263
255
  url: `/${appKey}/${userId}`,
264
- token: adminKey,
256
+ token: userToken,
265
257
  payload: {
266
- token: shortTTRToken,
267
258
  tokenHint: 'ttr-test',
259
+ token: shortTTRToken,
268
260
  },
269
261
  });
270
-
271
- // Clear mock calls
262
+ });
263
+ beforeEach(async () => {
272
264
  jest.clearAllMocks();
273
265
  });
274
266
 
275
267
  it('should use cache within TTR period', async () => {
276
- // First call should hit the plugin
277
268
  await doApiPost({
278
269
  url: `/products/${appKey}/${userId}/ttr-test/search`,
279
270
  token: userToken,
@@ -0,0 +1,111 @@
1
+ /* globals beforeAll describe it expect jest beforeEach */
2
+
3
+ const chance = require('chance').Chance();
4
+ const testUtils = require('../../test/utils');
5
+
6
+ describe('user: bookings controller - productSearch background job', () => {
7
+ const newApp = {
8
+ name: `travelgate-${chance.guid()}`,
9
+ packageName: 'ti2-travelgate',
10
+ adminEmail: 'engineering+travelgate@tourconnect.com',
11
+ };
12
+ const appKey = newApp.name;
13
+ const userId = `testUser-${chance.guid()}`;
14
+ const token = {
15
+ endpoint: 'https://api.travelgatex.com',
16
+ apiKey: chance.guid(),
17
+ client: 'tourconnect',
18
+ };
19
+ const tokenHint = 'testingToken';
20
+ const newIntegration = {
21
+ tokenHint,
22
+ token,
23
+ };
24
+ let doApiDelete;
25
+ let doApiGet;
26
+ let doApiPost;
27
+ let plugins;
28
+ let userToken;
29
+
30
+ beforeAll(async () => {
31
+ // Setup test utilities with mocked plugins
32
+ ({
33
+ doApiDelete,
34
+ doApiGet,
35
+ doApiPost,
36
+ plugins,
37
+ } = await testUtils({
38
+ plugins: [newApp.name],
39
+ }));
40
+
41
+ const { env: { adminKey } } = process;
42
+ // create the user
43
+ await doApiPost({
44
+ url: '/user',
45
+ token: adminKey,
46
+ payload: { userId, email: `${userId}@example.com` },
47
+ });
48
+ // Clean slate - Drop potential existing app/user integration
49
+ await doApiPost({
50
+ url: '/app',
51
+ token: adminKey,
52
+ payload: newApp,
53
+ });
54
+
55
+ // Create the test user and get token
56
+ ({ value: userToken } = await doApiPost({
57
+ url: '/user',
58
+ token: adminKey,
59
+ payload: { userId },
60
+ }));
61
+
62
+ // Create the user integration
63
+ await doApiPost({
64
+ url: `/${appKey}/${userId}`,
65
+ token: userToken,
66
+ payload: newIntegration,
67
+ });
68
+ });
69
+
70
+ beforeEach(async () => {
71
+ jest.clearAllMocks();
72
+ });
73
+
74
+ it('should create a background job when calling bookingsProductSearch with backgroundJob in payload', async () => {
75
+ // Payload with backgroundJob flag set to true
76
+ const payload = {
77
+ backgroundJob: true
78
+ };
79
+
80
+ // Call the endpoint with backgroundJob in the payload
81
+ const response = await doApiPost({
82
+ url: `/products/${appKey}/${userId}/${tokenHint}/search`,
83
+ token: userToken,
84
+ payload,
85
+ // Expect 200 OK with a jobId in the response
86
+ expectStatusCode: 200,
87
+ });
88
+
89
+ // Check if jobId is returned in the response body
90
+ expect(response.jobId).toBeTruthy();
91
+ expect(typeof response.jobId).toBe('string');
92
+ });
93
+
94
+ it('should return products directly when not using backgroundJob flag', async () => {
95
+ // Payload without backgroundJob flag
96
+ const payload = {};
97
+
98
+ // Call the endpoint without backgroundJob flag
99
+ const response = await doApiPost({
100
+ url: `/products/${appKey}/${userId}/${tokenHint}/search`,
101
+ token: userToken,
102
+ payload,
103
+ // Expect 200 OK, as the endpoint returns results directly
104
+ expectStatusCode: 200,
105
+ });
106
+
107
+ // Check if products array is returned in the response body
108
+ expect(Array.isArray(response.products)).toBeTruthy();
109
+ expect(response.products.length).toBeGreaterThan(0);
110
+ });
111
+ });
@@ -1,7 +1,6 @@
1
1
  const assert = require('assert');
2
2
  const hash = require('object-hash');
3
3
  const R = require('ramda');
4
- const cache = require('../cache');
5
4
  const { UserAppKey } = require('../models/index');
6
5
  const { typeDefs: productTypeDefs, query: productQuery } = require('./graphql-schemas/product');
7
6
  const { typeDefs: availTypeDefs, query: availQuery } = require('./graphql-schemas/availability');
@@ -151,19 +150,15 @@ const $bookingsProductSearch = plugins => async ({
151
150
  const func = (app.searchProducts || app.searchProductsForItinerary).bind(app);
152
151
  // NOTE: this is intend to cache the entire product list
153
152
  const cacheKey = hash({
154
- appKey,
155
153
  userId,
156
154
  hint,
157
155
  operationId: 'bookingsProductSearch',
158
156
  });
159
- // TODO: remove debugging console.logs after no related issues reported for a while
160
157
  // Check cache first if not forcing refresh
161
- const cacheValue = forceRefresh ? null : await cache.get({
162
- pluginName: appKey,
158
+ const cacheValue = forceRefresh ? null : await app.cache.get({
163
159
  key: cacheKey,
164
160
  });
165
- const lastUpdated = await cache.get({
166
- pluginName: appKey,
161
+ const lastUpdated = await app.cache.get({
167
162
  key: `${cacheKey}:lastUpdated`,
168
163
  });
169
164
  // refresh every 24 hours
@@ -176,16 +171,13 @@ const $bookingsProductSearch = plugins => async ({
176
171
  if (doNotCallPluginForProducts) {
177
172
  isStale = false;
178
173
  }
179
- const hasLock = await cache.get({
180
- pluginName: appKey,
174
+ const hasLock = await app.cache.get({
181
175
  key: `${cacheKey}:lock`,
182
176
  });
183
- console.log(`${appKey}/${userId}/${hint}: lastUpdated: ${lastUpdated}, ttr: ${ttr}, isStale: ${isStale}, hasLock: ${hasLock}, foundCache: ${!!cacheValue}`);
184
177
  // when there is a lock (meaning another request is already fetching the products)
185
178
  // return stale cache during this short window
186
179
  if (cacheValue && cacheValue.products && (hasLock || !isStale)) {
187
180
  const searchResults = $searchProductList(cacheValue.products, searchInput, optionId);
188
- console.log(`${appKey}/${userId}/${hint}: returning cached products: ${searchResults.length}`);
189
181
  return {
190
182
  ...cacheValue,
191
183
  products: searchResults,
@@ -194,13 +186,10 @@ const $bookingsProductSearch = plugins => async ({
194
186
  };
195
187
  }
196
188
  if (doNotCallPluginForProducts && !forceRefresh) {
197
- console.log(`${appKey}/${userId}/${hint}:not calling the plugin because doNotCallPluginForProducts is true and forceRefresh is false`);
198
189
  return { products: [] };
199
190
  }
200
- console.log(`${appKey}/${userId}/${hint}: (forceRefresh: ${forceRefresh}) so calling func to get fresh products`);
201
191
  // create a lock with 2 minute TTL
202
- await cache.save({
203
- pluginName: appKey,
192
+ await app.cache.save({
204
193
  key: `${cacheKey}:lock`,
205
194
  value: true,
206
195
  ttl: 120,
@@ -215,28 +204,32 @@ const $bookingsProductSearch = plugins => async ({
215
204
  });
216
205
  // save cache if products are found
217
206
  if (funcResults && funcResults.products && funcResults.products.length > 0) {
218
- console.log(`${appKey}/${userId}/${hint}: saving cache of ${funcResults.products.length} products`);
219
207
  // I initially wanted to let the cache live forever
220
208
  // but just in case user left TC or something, we don't want to keep the cache forever
221
209
  // so we set the TTL to 30 days, within 30 days,
222
210
  // we will refresh the cache every 24 hours(or specified by the user) without deleting the stale cache
223
211
  const monthInSeconds = 30 * 24 * 60 * 60;
224
- await cache.save({
225
- pluginName: appKey,
212
+ await app.cache.save({
226
213
  key: `${cacheKey}:lastUpdated`,
227
214
  value: Date.now(),
228
215
  ttl: monthInSeconds,
229
216
  });
230
- await cache.save({
231
- pluginName: appKey,
217
+ await app.cache.save({
232
218
  key: cacheKey,
233
219
  value: funcResults,
234
220
  ttl: monthInSeconds,
235
221
  });
222
+ app.events.emit('bookingsProductSearch:cache:save', {
223
+ cacheKey,
224
+ userId,
225
+ hint,
226
+ operationId: 'bookingsProductSearch',
227
+ requestId,
228
+ pluginName: app.name,
229
+ });
236
230
  }
237
231
  // release the lock
238
- await cache.drop({
239
- pluginName: appKey,
232
+ await app.cache.drop({
240
233
  key: `${cacheKey}:lock`,
241
234
  });
242
235
  const searchResults = $searchProductList(R.pathOr([], ['products'], funcResults), searchInput, optionId);
@@ -27,6 +27,7 @@ const typeDefs = `
27
27
  supplierId: String
28
28
  paxList: [Passenger]
29
29
  paxConfigs: [PaxConfig]
30
+ status: String
30
31
  }
31
32
 
32
33
  type Query {
@@ -65,6 +66,7 @@ const query = `{
65
66
  supplierName
66
67
  supplierId
67
68
  startDate
69
+ status
68
70
  paxConfigs {
69
71
  roomType
70
72
  adults
package/index.js CHANGED
@@ -14,6 +14,7 @@ const { v4: uuidv4 } = require('uuid');
14
14
  const EventEmitter = require('eventemitter2');
15
15
  const axios = require('axios');
16
16
  const curlirize = require('axios-curlirize');
17
+ const { addJob } = require('./worker/queue');
17
18
 
18
19
  const cacheSettings = {
19
20
  '*': [
@@ -58,13 +59,14 @@ const axiosSafeResponse = response => {
58
59
  module.exports = async ({
59
60
  apiDocs = true,
60
61
  plugins: pluginsParam = {},
62
+ pluginsInstantiated = false,
61
63
  port: portParam,
62
64
  startServer = true,
63
65
  worker = false,
64
66
  }) => {
65
67
  const port = portParam || process.env.PORT || 10010;
66
68
  // create the plugin instances
67
- const plugins = await bb.map(Object.entries(pluginsParam), async ([pluginName, Plugin]) => {
69
+ const plugins = pluginsInstantiated || await bb.map(Object.entries(pluginsParam), async ([pluginName, Plugin]) => {
68
70
  // pass all env variables
69
71
  const pluginEnv = pickBy(
70
72
  (_val, key) => key.substring(0, `ti2_${pluginName}`.length)
@@ -83,6 +85,7 @@ module.exports = async ({
83
85
  getOrExec: args => cache.getOrExec({ ...args, pluginName }),
84
86
  save: args => cache.save({ ...args, pluginName }),
85
87
  },
88
+ axios,
86
89
  events: ti2Events,
87
90
  name: pluginName,
88
91
  ...params,
@@ -277,6 +280,44 @@ module.exports = async ({
277
280
  },
278
281
  }
279
282
  */
283
+ // check if the operation is supposed to be ran in the background
284
+ const backgroundJob = R.pathOr(
285
+ false, ['backgroundJob'], req.body,
286
+ );
287
+ if (backgroundJob) {
288
+ const job = await addJob({
289
+ type: 'api',
290
+ method: req.method,
291
+ url: req.url,
292
+ headers: req.headers,
293
+ payload: {
294
+ ...req.body,
295
+ requestId: req.requestId,
296
+ },
297
+ });
298
+ return res.json({ jobId: job });
299
+ }
300
+ // intercept res.json
301
+ const callbackUrl = R.pathOr(
302
+ false, ['callbackUrl'], req.body);
303
+ if (callbackUrl) {
304
+ // send callback
305
+ const originalJson = res.json;
306
+ res.json = function (data) {
307
+ const callbackBody = {
308
+ callbackUrl,
309
+ request: body,
310
+ result: data,
311
+ };
312
+ addJob({
313
+ type: 'callback',
314
+ payload: callbackBody,
315
+ }).then(jobId => {
316
+ console.debug('created callback job', jobId);
317
+ });
318
+ return originalJson.call(this, data);
319
+ };
320
+ }
280
321
  const pluginCacheSettings = R.pathOr({}, ['cacheSettings'], currentPlugin);
281
322
  // Filter plugin cache settings to only include those with cacheInMiddleware: true
282
323
  const filteredPluginCacheSettings = R.pickBy(
package/jest.setup.js CHANGED
@@ -1,8 +1,22 @@
1
- /* global afterAll */
1
+ /* global beforeAll afterAll */
2
2
 
3
3
  const closeAll = require('./closeAll');
4
+ const models = require('./models');
4
5
 
6
+ // Setup global beforeAll and afterAll hooks
7
+ beforeAll(async () => {
8
+ // Ensure database connection is established before tests
9
+ if (models.sequelize && typeof models.sequelize.authenticate === 'function') {
10
+ await models.sequelize.authenticate();
11
+ }
12
+ });
13
+
14
+ // Make sure all connections are properly closed after tests
5
15
  afterAll(async () => {
16
+ // Wait a bit to ensure all pending operations are completed
17
+ await global.sleep(100);
18
+
19
+ // Then close all connections
6
20
  await closeAll();
7
21
  });
8
22
 
@@ -0,0 +1,131 @@
1
+ /* globals beforeAll describe it expect jest afterAll beforeEach */
2
+
3
+ const axios = require('axios');
4
+ const MockAdapter = require('axios-mock-adapter');
5
+ const { sendCallback } = require('../callback');
6
+
7
+ describe('callback', () => {
8
+ let mock;
9
+
10
+ beforeAll(() => {
11
+ mock = new MockAdapter(axios);
12
+ });
13
+
14
+ afterAll(() => {
15
+ mock.restore();
16
+ });
17
+
18
+ beforeEach(() => {
19
+ mock.reset();
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ describe('sendCallback', () => {
24
+ it('should not do anything if callbackUrl is not provided', async () => {
25
+ const consoleSpy = jest.spyOn(console, 'error');
26
+ await sendCallback({
27
+ request: {
28
+ operationId: 'test',
29
+ payload: { test: true },
30
+ userId: '123',
31
+ integrationId: 'test-integration',
32
+ hint: 'test-hint'
33
+ },
34
+ result: { success: true },
35
+ });
36
+ expect(consoleSpy).not.toHaveBeenCalled();
37
+ consoleSpy.mockRestore();
38
+ });
39
+
40
+ it('should send a POST request to the callback URL with correct payload', async () => {
41
+ const callbackUrl = 'https://api.example.com/callback';
42
+ const params = {
43
+ callbackUrl,
44
+ request: {
45
+ operationId: 'bookingsProductSearch',
46
+ payload: { searchInput: 'test' },
47
+ userId: '123',
48
+ integrationId: 'test-integration',
49
+ hint: 'test-hint'
50
+ },
51
+ result: { products: [] }
52
+ };
53
+
54
+ mock.onPost(callbackUrl).reply((config) => {
55
+ const data = JSON.parse(config.data);
56
+ expect(data).toMatchObject({
57
+ request: params.request,
58
+ result: params.result,
59
+ timestamp: expect.any(String)
60
+ });
61
+ return [200];
62
+ });
63
+
64
+ await sendCallback(params);
65
+ expect(mock.history.post.length).toBe(1);
66
+ expect(mock.history.post[0].url).toBe(callbackUrl);
67
+ });
68
+
69
+ it('should log error when callback request fails', async () => {
70
+ const callbackUrl = 'https://api.example.com/callback';
71
+ const params = {
72
+ callbackUrl,
73
+ request: {
74
+ operationId: 'bookingsProductSearch',
75
+ payload: { searchInput: 'test' },
76
+ userId: '123',
77
+ integrationId: 'test-integration',
78
+ hint: 'test-hint'
79
+ },
80
+ result: { products: [] }
81
+ };
82
+
83
+ mock.onPost(callbackUrl).reply(500);
84
+
85
+ // Mock console.error but prevent actual logging
86
+ const originalError = console.error;
87
+ console.error = jest.fn();
88
+
89
+ await sendCallback(params);
90
+
91
+ expect(mock.history.post.length).toBe(1);
92
+ expect(console.error).toHaveBeenCalledWith(
93
+ `Error sending callback to ${callbackUrl}: Request failed with status code 500`
94
+ );
95
+
96
+ // Restore original console.error
97
+ console.error = originalError;
98
+ });
99
+
100
+ it('should log error when network error occurs', async () => {
101
+ const callbackUrl = 'https://api.example.com/callback';
102
+ const params = {
103
+ callbackUrl,
104
+ request: {
105
+ operationId: 'bookingsProductSearch',
106
+ payload: { searchInput: 'test' },
107
+ userId: '123',
108
+ integrationId: 'test-integration',
109
+ hint: 'test-hint'
110
+ },
111
+ result: { products: [] }
112
+ };
113
+
114
+ mock.onPost(callbackUrl).networkError();
115
+
116
+ // Mock console.error but prevent actual logging
117
+ const originalError = console.error;
118
+ console.error = jest.fn();
119
+
120
+ await sendCallback(params);
121
+
122
+ expect(mock.history.post.length).toBe(1);
123
+ expect(console.error).toHaveBeenCalledWith(
124
+ `Error sending callback to ${callbackUrl}: Network Error`
125
+ );
126
+
127
+ // Restore original console.error
128
+ console.error = originalError;
129
+ });
130
+ });
131
+ });
@@ -0,0 +1,55 @@
1
+ const axios = require('axios');
2
+
3
+ /**
4
+ * Send operation results to a callback URL
5
+ * @param {Object} params - Parameters for the callback
6
+ * @param {string} params.callbackUrl - URL to send the callback to
7
+ * @param {string} params.operationId - ID of the operation that was performed
8
+ * @param {Object} params.payload - The payload that was received
9
+ * @param {Object} params.result - The result of the operation
10
+ * @param {string} params.userId - User ID associated with the request
11
+ * @param {string} params.integrationId - Integration ID associated with the request
12
+ * @param {string} params.hint - Hint used in the request
13
+ * @returns {Promise<void>}
14
+ */
15
+ async function sendCallback({
16
+ callbackUrl,
17
+ request,
18
+ result,
19
+ }) {
20
+ if (!callbackUrl) {
21
+ return;
22
+ }
23
+
24
+ const callbackPayload = {
25
+ request,
26
+ result,
27
+ timestamp: new Date().toISOString()
28
+ };
29
+
30
+ try {
31
+ await axios.post(callbackUrl, callbackPayload, {
32
+ headers: {
33
+ 'Content-Type': 'application/json'
34
+ },
35
+ maxBodyLength: Infinity,
36
+ maxContentLength: Infinity
37
+ });
38
+ } catch (error) {
39
+ let errorMessage = `Error sending callback to ${callbackUrl}: ${error.message}`;
40
+ if (error.code) {
41
+ errorMessage += ` (Code: ${error.code})`;
42
+ }
43
+ if (error.syscall) {
44
+ errorMessage += ` (Syscall: ${error.syscall})`;
45
+ }
46
+ if (error.address && error.port) {
47
+ errorMessage += ` (Address: ${error.address}:${error.port})`;
48
+ }
49
+ console.error(errorMessage);
50
+ }
51
+ }
52
+
53
+ module.exports = {
54
+ sendCallback
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2",
3
- "version": "1.0.91",
3
+ "version": "1.0.93",
4
4
  "description": "Tourist Industry Exchange (TI2)",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test/plugin.js CHANGED
@@ -631,6 +631,7 @@ class Plugin {
631
631
  * @property {string} [linePrice] - Line price
632
632
  * @property {string} [quantity] - Quantity
633
633
  * @property {string} startDate - Start date (YYYY-MM-DD)
634
+ * @property {string} status - Status
634
635
  * @property {Array<PaxConfig>} paxConfigs - List of pax configs for this service line
635
636
  * @property {Array<Passenger>} paxList - List of passengers for this service line
636
637
  */
@@ -384,6 +384,7 @@ No, we don't have a specific test scenarios, but we recommend you to cover the f
384
384
  ],
385
385
  "serviceLineId": "745684",
386
386
  "startDate": "2025-08-13",
387
+ "status": "OK"
387
388
  },
388
389
  ],
389
390
  "totalPrice": "187795",
@@ -0,0 +1,173 @@
1
+ const axios = require('axios');
2
+ require('../../test/utils');
3
+
4
+ // Mock throng to directly execute the worker function
5
+ jest.mock('throng', () => jest.fn(config => {
6
+ if (typeof config.worker === 'function') {
7
+ // Provide dummy id and disconnect function, similar to how throng would call it
8
+ config.worker('test-worker-id', jest.fn());
9
+ } else {
10
+ console.error('Mocked throng: config.worker is not a function or not provided.');
11
+ // Optionally, throw an error or handle as appropriate for your tests
12
+ // throw new Error('Throng mock expected a worker function in config.');
13
+ }
14
+ // throng can return a promise or be void, depending on usage. Here, we don't need to return.
15
+ }));
16
+
17
+ // Mock axios
18
+ jest.mock('axios');
19
+
20
+ // Mock the queue to avoid actual Redis operations
21
+ jest.mock('../queue', () => {
22
+ const original = jest.requireActual('../queue');
23
+ return {
24
+ ...original,
25
+ queue: {
26
+ ...original.queue,
27
+ process: jest.fn(),
28
+ getJob: jest.fn(),
29
+ },
30
+ };
31
+ });
32
+
33
+ // Mock the queue module
34
+ jest.mock('../queue', () => {
35
+ const original = jest.requireActual('../queue');
36
+ return {
37
+ ...original,
38
+ queue: {
39
+ ...original.queue,
40
+ process: jest.fn(),
41
+ getJob: jest.fn().mockResolvedValue({
42
+ id: 'test-job-id',
43
+ data: {
44
+ type: 'api',
45
+ method: 'POST',
46
+ url: '/products/testAppKey/testUserId/testTokenHint/search',
47
+ headers: { 'content-type': 'application/json' },
48
+ payload: { backgroundJob: true },
49
+ inTesting: true
50
+ },
51
+ remove: jest.fn().mockResolvedValue(true)
52
+ })
53
+ },
54
+ addJob: jest.fn().mockResolvedValue('test-job-id'),
55
+ allDone: jest.fn().mockResolvedValue(true)
56
+ };
57
+ });
58
+
59
+ // Import the mocked queue and the actual worker module
60
+ const { addJob, queue, allDone } = require('../queue');
61
+ const actualWorkerModule = require('../index'); // Actual worker module
62
+
63
+ describe('worker: API job handling', () => {
64
+ let serverUrl;
65
+ let plugins;
66
+ let doApiPost;
67
+
68
+ let actualJobHandler;
69
+
70
+ beforeAll(async () => {
71
+ // Silence console.log for tests
72
+ jest.spyOn(console, 'log').mockImplementation(() => {});
73
+
74
+ // Setting up test environment
75
+
76
+ const testUtilsResult = await require('../../test/utils')({
77
+ plugins: ['mockPlugin'],
78
+ startServer: false,
79
+ worker: false,
80
+ });
81
+
82
+ plugins = testUtilsResult.plugins;
83
+ // doApiPost might not be used directly if worker uses supertest
84
+ serverUrl = 'http://127.0.0.1:3000'; // This might be for constructing expected URLs
85
+
86
+ axios.mockImplementation(config => {
87
+ // This mock is for the internal axios call made by the worker
88
+ if (config.url && config.url.includes('/products/testAppKey/testUserId/testTokenHint/search')) {
89
+ return Promise.resolve({ status: 200, data: { success: true, message: 'Mocked internal success' } });
90
+ }
91
+ console.error('Unexpected axios call to mock:', config);
92
+ return Promise.reject(new Error(`Unexpected axios call in mock for URL: ${config.url}`));
93
+ });
94
+
95
+ // Mock queue.process to capture the handler passed by the actual worker
96
+ queue.process.mockImplementation((...args) => {
97
+ let handlerCallback;
98
+ if (typeof args[args.length - 1] === 'function') {
99
+ handlerCallback = args[args.length - 1];
100
+ } else {
101
+ console.error('queue.process mock called without a handler function', args);
102
+ throw new Error('queue.process mock called without a handler function');
103
+ }
104
+ const queueName = typeof args[0] === 'string' ? args[0] : 'default';
105
+ actualJobHandler = handlerCallback;
106
+ });
107
+
108
+ // Initializing actual worker module
109
+ actualWorkerModule({ plugins }); // Initialize the actual worker, it should call the mocked queue.process
110
+ // Actual worker module initialized
111
+
112
+ // Test environment setup complete
113
+ });
114
+
115
+ afterAll(() => {
116
+ jest.restoreAllMocks(); // Restores all mocks, including console.log spy
117
+ actualJobHandler = null; // Clear captured handler
118
+ });
119
+
120
+ it('should create a job of type "api" for the product search endpoint and send it to the API server', async () => {
121
+
122
+ // 1. Test that we can create a job
123
+ const jobData = {
124
+ type: 'api',
125
+ method: 'POST',
126
+ url: '/products/testAppKey/testUserId/testTokenHint/search',
127
+ headers: { 'content-type': 'application/json' },
128
+ payload: { backgroundJob: true },
129
+ };
130
+
131
+ // Adding job to queue...
132
+ const jobId = await addJob(jobData);
133
+
134
+ // Basic assertions
135
+ expect(jobId).toBeTruthy();
136
+ expect(typeof jobId).toBe('string');
137
+
138
+ // Verify addJob was called correctly
139
+ expect(addJob).toHaveBeenCalledWith(jobData);
140
+
141
+ // Simulate job processing by invoking the captured handler
142
+ // Simulating job processing with captured handler...
143
+ if (!actualJobHandler) {
144
+ throw new Error('Job handler was not captured from queue.process. Worker might not have initialized queue processing correctly.');
145
+ }
146
+ // Construct a mock Bull job object
147
+ const mockJob = {
148
+ data: jobData,
149
+ id: jobId,
150
+ log: jest.fn(),
151
+ progress: jest.fn(),
152
+ update: jest.fn(),
153
+ };
154
+ await actualJobHandler(mockJob);
155
+ // Job processing simulation complete.
156
+
157
+ // Verify queue.process was called (to capture the handler)
158
+ expect(queue.process).toHaveBeenCalled();
159
+
160
+ // Verify the internal API call was made by the worker correctly
161
+ expect(axios).toHaveBeenCalledWith(
162
+ expect.objectContaining({
163
+ method: jobData.method.toLowerCase(), // Ensure method matches job data
164
+ url: expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+\/products\/testAppKey\/testUserId\/testTokenHint\/search$/),
165
+ data: {}, // Payload after R.omit(['backgroundJob'], { backgroundJob: true }) is an empty object
166
+ headers: expect.objectContaining({
167
+ // Axios might add/remove/modify some headers, so be specific about what must be there
168
+ // 'content-type': 'application/json' // This might be set by axios default or omitted if data is empty
169
+ }),
170
+ })
171
+ );
172
+ });
173
+ });
@@ -0,0 +1,104 @@
1
+ /* globals describe it expect jest beforeAll afterAll */
2
+
3
+ // Import queue module directly to avoid reference issues
4
+ const queueModule = require('../queue');
5
+
6
+ // Create a mock for axios that will be used by the callback module
7
+ jest.mock('axios', () => ({
8
+ post: jest.fn().mockResolvedValue({ status: 200 })
9
+ }));
10
+
11
+ describe('worker: Callback job handling', () => {
12
+ it('should create a job of type "callback" with the correct structure', async () => {
13
+ // Create a job to send a callback
14
+ const callbackData = {
15
+ callbackUrl: 'https://example.com/callback',
16
+ request: { id: '123', method: 'GET' },
17
+ result: { status: 'success', data: { message: 'Operation completed' } }
18
+ };
19
+
20
+ const jobData = {
21
+ type: 'callback',
22
+ payload: callbackData
23
+ };
24
+
25
+ // Add the job to the queue
26
+ const jobId = await queueModule.addJob(jobData);
27
+
28
+ // Verify job was created with a valid ID
29
+ expect(jobId).toBeTruthy();
30
+ expect(typeof jobId).toBe('string');
31
+
32
+ // Get the job from the queue to verify its data
33
+ const job = await queueModule.queue.getJob(jobId);
34
+
35
+ // Verify the job exists and has the correct data
36
+ expect(job).toBeTruthy();
37
+ expect(job.data).toEqual(expect.objectContaining({
38
+ type: 'callback',
39
+ payload: expect.objectContaining({
40
+ callbackUrl: 'https://example.com/callback',
41
+ request: expect.any(Object),
42
+ result: expect.any(Object)
43
+ }),
44
+ inTesting: true // This is added by addJob function
45
+ }));
46
+ });
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
+ it('should use the callback module to send callbacks', async () => {
71
+ // Get a reference to the mocked axios.post function
72
+ const axios = require('axios');
73
+
74
+ // Reset mock call history
75
+ axios.post.mockClear();
76
+
77
+ // Create a job with callback data
78
+ const callbackData = {
79
+ callbackUrl: 'https://example.com/callback',
80
+ request: { id: '123', method: 'GET' },
81
+ result: { status: 'success', data: { message: 'Operation completed' } }
82
+ };
83
+
84
+ // Directly call the sendCallback function to verify it works as expected
85
+ const { sendCallback } = require('../../lib/callback');
86
+ await sendCallback(callbackData);
87
+
88
+ // Verify axios.post was called with the correct parameters
89
+ expect(axios.post).toHaveBeenCalledTimes(1);
90
+ expect(axios.post).toHaveBeenCalledWith(
91
+ callbackData.callbackUrl,
92
+ expect.objectContaining({
93
+ request: callbackData.request,
94
+ result: callbackData.result,
95
+ timestamp: expect.any(String)
96
+ }),
97
+ expect.objectContaining({
98
+ headers: expect.objectContaining({
99
+ 'Content-Type': 'application/json'
100
+ })
101
+ })
102
+ );
103
+ });
104
+ });
package/worker/index.js CHANGED
@@ -1,11 +1,10 @@
1
1
  const throng = require('throng');
2
2
  require('util').inspect.defaultOptions.depth = null;
3
-
3
+ const R = require('ramda');
4
4
  const { queue, saveResult } = require('./queue');
5
5
 
6
6
  const workers = process.env.WEB_CONCURRENCY || 2;
7
-
8
- const maxJobsPerWorker = 1;
7
+ const maxJobsPerWorker = process.env.MAX_JOBS_PER_WORKER || 1;
9
8
 
10
9
  const worker = ({ plugins: pluginsParam }) => (id, disconnect) => {
11
10
  console.log(`Started worker ${id}`);
@@ -23,14 +22,88 @@ const worker = ({ plugins: pluginsParam }) => (id, disconnect) => {
23
22
  queue.process(maxJobsPerWorker, async job => {
24
23
  const {
25
24
  data: {
25
+ type = 'plugin',
26
26
  pluginName,
27
27
  method,
28
28
  userId,
29
29
  inTesting,
30
+ payload = {}
30
31
  },
31
32
  id: jobId,
32
33
  data: params,
33
34
  } = job;
35
+ if (type === 'api') {
36
+ const {
37
+ method,
38
+ url,
39
+ payload,
40
+ headers,
41
+ } = params;
42
+
43
+ let code, result, server;
44
+ const http = require('http');
45
+ const axios = require('axios');
46
+ try {
47
+ const app = await require('../index')({
48
+ pluginsInstantiated: pluginsParam,
49
+ startServer: false,
50
+ worker: false,
51
+ });
52
+
53
+ // Manually start server on random port
54
+ server = http.createServer(app);
55
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
56
+ const { address, port } = server.address();
57
+ const baseURL = `http://${address}:${port}`;
58
+ console.log(`Worker internal server for job ${jobId} listening on: ${baseURL}`);
59
+
60
+ // Use axios to make the request
61
+ const response = await axios({
62
+ method: method.toLowerCase(),
63
+ url: `${baseURL}${url}`,
64
+ data: R.omit(['backgroundJob'], payload),
65
+ headers: R.omit(['content-length'], headers),
66
+ validateStatus: () => true, // Prevent axios from throwing on non-2xx status
67
+ });
68
+
69
+ code = response.status;
70
+ result = response.data;
71
+ console.log(`Worker internal request for job ${jobId} completed with code ${code}`);
72
+ } catch (error) {
73
+ // Handle errors during app init or axios request
74
+ console.error(`Worker internal request error for job ${jobId}:`, error.message);
75
+ // If axios error has response, use its status
76
+ code = R.pathOr(500, ['response', 'status'], error);
77
+ result = R.pathOr({ message: error.message }, ['response', 'data'], error);
78
+ } finally {
79
+ // Ensure server is closed if it was created
80
+ if (server) {
81
+ await new Promise(resolve => server.close(resolve));
82
+ console.log(`Worker internal server for job ${jobId} closed.`);
83
+ }
84
+ }
85
+
86
+ return {
87
+ code,
88
+ result,
89
+ success: code >= 200 && code < 300,
90
+ };
91
+ }
92
+ if (type === 'callback') {
93
+ const {
94
+ callbackUrl,
95
+ request,
96
+ result,
97
+ } = payload;
98
+
99
+ console.log(`job ${jobId} > sending callback to ${callbackUrl}`);
100
+ await require('../lib/callback').sendCallback({
101
+ callbackUrl,
102
+ request,
103
+ result,
104
+ });
105
+ return { success: true };
106
+ }
34
107
  console.log(`job ${jobId} > running ${pluginName}:${method} for ${userId}`);
35
108
  const plugins = await (async () => {
36
109
  if (inTesting) {
@@ -41,8 +114,10 @@ const worker = ({ plugins: pluginsParam }) => (id, disconnect) => {
41
114
  })();
42
115
  const thePlugin = plugins.find(({ name }) => name === pluginName);
43
116
  const resultValue = await thePlugin[method]({
44
- ...params,
117
+ ...params.payload,
118
+ token: params.token,
45
119
  plugins,
120
+ typeDefsAndQueries: require('../controllers/bookings').typeDefsAndQueries,
46
121
  });
47
122
  console.log(`job ${jobId} > completed`);
48
123
  await saveResult({ id: jobId, resultValue });