ti2 1.0.15 → 1.0.18

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.
@@ -1,6 +1,19 @@
1
1
  const jwt = require('jwt-promise');
2
2
  const { omit } = require('ramda');
3
+ const assert = require('assert');
4
+ const { Umzug, SequelizeStorage } = require('umzug');
5
+ const path = require('path');
6
+ const Sequelize = require('sequelize');
7
+ const fs = require('fs').promises;
8
+ const bb = require('bluebird');
9
+ const R = require('ramda');
10
+
3
11
  const sqldb = require('../models');
12
+ const {
13
+ queue,
14
+ addJob,
15
+ jobStatus,
16
+ } = require('../worker/queue');
4
17
 
5
18
  const { env: { jwtSecret } } = process;
6
19
 
@@ -42,6 +55,61 @@ const createAppToken = async (req, res, next) => {
42
55
  });
43
56
  if (userAppKeyDup) await userAppKeyDup.destroy();
44
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
+
45
113
  return res.json({ value: newAppKey.get('id').toString() });
46
114
  } catch (err) {
47
115
  return next(err);
@@ -83,9 +151,114 @@ const listAppTokens = async (req, res, next) => {
83
151
  }
84
152
  };
85
153
 
154
+ const migrateApp = async ({ integrationId, action }) => {
155
+ const { sequelize } = sqldb;
156
+ assert(integrationId);
157
+ assert(action);
158
+ const migrationsPath = path.join(
159
+ __dirname,
160
+ '../',
161
+ '../',
162
+ `ti2-${integrationId}`,
163
+ 'migrations',
164
+ );
165
+ try {
166
+ await fs.access(migrationsPath);
167
+ } catch (err) {
168
+ throw Error(`Could not find any migrations for ${integrationId} on ${migrationsPath}`);
169
+ }
170
+ const umzug = new Umzug({
171
+ migrations: {
172
+ glob: `${migrationsPath}/*.js`,
173
+ resolve: ({ name, path: migPath, context }) => {
174
+ const migration = require(migPath);
175
+ return {
176
+ name,
177
+ up: async () => migration.up(context, Sequelize),
178
+ down: async () => migration.down(context, Sequelize),
179
+ };
180
+ },
181
+ },
182
+ context: sequelize.getQueryInterface(),
183
+ storage: new SequelizeStorage({
184
+ sequelize,
185
+ tableName: `SequelizeMeta-${integrationId}`,
186
+ }),
187
+ logger: console,
188
+ });
189
+ if (action === 'migrate') {
190
+ return umzug.up();
191
+ }
192
+ if (action === 'revert') {
193
+ return umzug.down({ to: 0 });
194
+ }
195
+ throw Error('No recognized action');
196
+ };
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
+
86
255
  module.exports = {
87
- jwtEncode,
88
256
  createAppToken,
89
257
  deleteAppToken,
258
+ getAppScheduledJobs,
259
+ getJobStatus,
260
+ jwtEncode,
90
261
  listAppTokens,
262
+ migrateApp,
263
+ runAppJob,
91
264
  };
@@ -20,7 +20,8 @@ const ping = (_req, res, next) => {
20
20
  });
21
21
  } catch (err) {
22
22
  return next(err);
23
- } };
23
+ }
24
+ };
24
25
 
25
26
  module.exports = {
26
27
  ping,
@@ -1,11 +1,7 @@
1
1
  // controllers/user.js
2
- const { omit, flatten } = require('ramda');
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, Sequelize: { Op } } = require('../models');
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
- const app = express();
28
- const plugins = Object.entries(pluginsParam).map(([pluginName, Plugin]) => {
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
- return new Plugin({ name: pluginName, ...params });
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
- // console.log({ plugins });
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, next) => {
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('%s: Received %s - terminating ti2 ...',
160
- Date(Date.now()), sig);
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
@@ -0,0 +1,11 @@
1
+ /* global afterAll */
2
+
3
+ const closeAll = require('./closeAll');
4
+
5
+ afterAll(async () => {
6
+ await closeAll();
7
+ });
8
+
9
+ global.sleep = (ms = 300) => new Promise(r => {
10
+ setTimeout(r, ms);
11
+ });
package/jsdoc.conf.json CHANGED
@@ -23,6 +23,6 @@
23
23
  "recurse": true,
24
24
  "sort": false,
25
25
  "readme": "README.md",
26
- "template": "node_modules/minami"
26
+ "template": "node_modules/@salvador/minami"
27
27
  }
28
28
  }
@@ -1,4 +1,3 @@
1
-
2
1
  module.exports = {
3
2
  up: (queryInterface, Sequelize) => queryInterface.addColumn(
4
3
  'Integrations',
@@ -3,8 +3,8 @@ module.exports = {
3
3
  type: Sequelize.TEXT,
4
4
  allowNull: false,
5
5
  }),
6
- down: (queryInterface, Sequelize) => queryInterface.changeColumn('UserAppKeys', 'appKey', {
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/db.js CHANGED
@@ -7,7 +7,6 @@ const {
7
7
  } = process;
8
8
 
9
9
  const options = {
10
- operatorsAliases: false,
11
10
  logging: false,
12
11
  };
13
12
 
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.15",
3
+ "version": "1.0.18",
4
4
  "description": "Tourist Industry Exchange (TI2)",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -8,19 +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
  },
20
+ "bin": "./cli.js",
15
21
  "author": "",
16
- "license": "GPL-3",
22
+ "license": "GPL-3.0",
17
23
  "bugs": {
18
24
  "url": "https://github.com/ti2travel/ti2/issues"
19
25
  },
20
26
  "homepage": "https://github.com/ti2travel/ti2#readme",
21
27
  "dependencies": {
22
28
  "bluebird": "^3.7.2",
29
+ "bull": "^4.8.2",
30
+ "commander": "^9.2.0",
23
31
  "express": "^4.17.2",
32
+ "ioredis": "^5.0.4",
24
33
  "js-yaml": "^4.1.0",
25
34
  "jsonwebtoken": "^8.5.1",
26
35
  "jwt-promise": "^1.0.0",
@@ -32,16 +41,18 @@
32
41
  "swagger-express-middleware": "3.0.0-alpha.5",
33
42
  "swagger-routes-express": "3.3.1",
34
43
  "swagger-ui-express": "4.3.0",
44
+ "throng": "^5.0.0",
45
+ "umzug": "^3.1.1",
35
46
  "uninstall": "^0.0.0"
36
47
  },
37
48
  "devDependencies": {
49
+ "@salvador/minami": "^1.2.3",
38
50
  "chance": "^1.1.8",
39
51
  "eslint": "^8.7.0",
40
52
  "eslint-config-airbnb-base": "^15.0.0",
41
53
  "eslint-plugin-import": "^2.25.4",
42
54
  "jest": "^27.4.7",
43
55
  "jsdoc": "^3.6.10",
44
- "minami": "git+https://github.com/MyNameReallySux/minami.git#sort-data",
45
56
  "supertest": "^6.2.2"
46
57
  }
47
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 = jest.fn(() => true);
19
- this.getProfile = jest.fn(() => {});
20
- this.updateProfile = jest.fn(() => {});
21
- this.getProduct = jest.fn(() => ({ products: [] }));
22
- this.getProducts = jest.fn(() => true);
23
- this.createLocation = jest.fn(() => ({ locationId: chance.guid() }));
24
- this.updateLocation = jest.fn(() => true);
25
- this.searchBooking = jest.fn(() => ({ bookings: [] }));
26
- this.searchProducts = jest.fn(() => ({ bookings: [], products: [] }));
27
- this.searchAvailability = jest.fn(({
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.searchQuote = jest.fn(() => ({ quote: [{ id: chance.guid() }] }));
45
- this.createBooking = jest.fn(() => {});
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 = jest.fn(() => ({ bookings: [] }));
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
@@ -0,0 +1,5 @@
1
+ // tests only file, should not be execute by itself
2
+
3
+ const worker = require('../worker/index')({ plugins: [] });
4
+
5
+ worker.then(() => {});
@@ -216,14 +216,17 @@ Ti2 uses [Sequelize v6.13](https://sequelize.org/v6) which is the database ORM w
216
216
 
217
217
  Migrations should be run after the fact, from the root of ti2 instance like so:
218
218
 
219
-
220
219
  ```
221
- $ npm explore ti2<pluginName> -- npx sequelize db:migrate"
220
+ $ ti2 dbapp [plugin name] migrate
222
221
  ```
223
222
 
224
223
  We strongly encourage all the plugin's table names are prefixed with the plugin name (i.e. ti2-pluginName-cacheTable).
225
224
 
226
- This should be executed after the ti2 project migrations have been executed.
225
+ This should be executed after all ti2 host migrations had been executed.
226
+
227
+ ```
228
+ $ ti2 db migrate
229
+ ```
227
230
 
228
231
  ## Scheduled Task (to be released on v2)
229
232