@crowdin/app-project-module 0.39.1 → 0.41.0

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.
Files changed (46) hide show
  1. package/out/handlers/integration/crowdin-update.js +36 -17
  2. package/out/handlers/integration/integration-data.js +2 -0
  3. package/out/handlers/integration/integration-update.js +35 -17
  4. package/out/handlers/integration/integration-webhook.js +1 -1
  5. package/out/handlers/integration/job-cancel.d.ts +3 -0
  6. package/out/handlers/integration/job-cancel.js +28 -0
  7. package/out/handlers/integration/job-info.d.ts +3 -0
  8. package/out/handlers/integration/job-info.js +54 -0
  9. package/out/handlers/integration/main.js +6 -3
  10. package/out/handlers/integration/settings-save.js +8 -1
  11. package/out/handlers/integration/user-errors.d.ts +3 -0
  12. package/out/handlers/{user-errors.js → integration/user-errors.js} +2 -2
  13. package/out/handlers/uninstall.js +4 -2
  14. package/out/index.d.ts +2 -1
  15. package/out/index.js +35 -27
  16. package/out/middlewares/crowdin-client.js +1 -0
  17. package/out/middlewares/integration-credentials.js +3 -2
  18. package/out/middlewares/ui-module.js +1 -0
  19. package/out/models/index.d.ts +54 -18
  20. package/out/models/index.js +1 -0
  21. package/out/models/job.d.ts +44 -0
  22. package/out/models/job.js +16 -0
  23. package/out/static/js/form.js +13 -13
  24. package/out/static/js/main.js +1 -1
  25. package/out/storage/index.d.ts +11 -2
  26. package/out/storage/index.js +3 -0
  27. package/out/storage/mysql.d.ts +11 -2
  28. package/out/storage/mysql.js +155 -10
  29. package/out/storage/postgre.d.ts +11 -2
  30. package/out/storage/postgre.js +153 -9
  31. package/out/storage/sqlite.d.ts +14 -3
  32. package/out/storage/sqlite.js +160 -14
  33. package/out/util/cron.d.ts +1 -0
  34. package/out/util/cron.js +53 -6
  35. package/out/util/defaults.js +4 -11
  36. package/out/util/file-snapshot.js +2 -0
  37. package/out/util/files.d.ts +2 -1
  38. package/out/util/files.js +19 -1
  39. package/out/util/index.js +3 -1
  40. package/out/util/job.d.ts +12 -0
  41. package/out/util/job.js +88 -0
  42. package/out/util/logger.js +4 -0
  43. package/out/util/webhooks.js +53 -6
  44. package/out/views/main.handlebars +153 -5
  45. package/package.json +16 -15
  46. package/out/handlers/user-errors.d.ts +0 -3
@@ -44,6 +44,8 @@ const connection_1 = require("./connection");
44
44
  const defaults_1 = require("./defaults");
45
45
  const index_1 = require("./index");
46
46
  const logger_1 = require("./logger");
47
+ const prefetchCount = 20;
48
+ const forceProcessDelay = 5000;
47
49
  exports.HookEvents = {
48
50
  fileAdded: 'file.added',
49
51
  fileDeleted: 'file.deleted',
@@ -227,6 +229,7 @@ function prepareWebhookData(config, integration, webhookUrlParam, provider) {
227
229
  const { projectId, crowdinId, clientId } = decodedUrlParam(config, webhookUrlParam);
228
230
  const crowdinCredentials = yield (0, storage_1.getStorage)().getCrowdinCredentials(crowdinId);
229
231
  const integrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(clientId);
232
+ const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(clientId);
230
233
  const context = {
231
234
  jwtPayload: {
232
235
  context: {
@@ -245,8 +248,8 @@ function prepareWebhookData(config, integration, webhookUrlParam, provider) {
245
248
  context,
246
249
  });
247
250
  const preparedIntegrationCredentials = yield (0, connection_1.prepareIntegrationCredentials)(config, integration, integrationCredentials);
248
- if (integrationCredentials === null || integrationCredentials === void 0 ? void 0 : integrationCredentials.config) {
249
- appSettings = JSON.parse(integrationCredentials.config);
251
+ if (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config) {
252
+ appSettings = JSON.parse(integrationConfig.config);
250
253
  const isWebhookSync = Number(appSettings.schedule) !== models_1.SyncSchedule.DISABLED;
251
254
  if (isWebhookSync) {
252
255
  syncSettings = (yield (0, storage_1.getStorage)().getSyncSettings(clientId, crowdinId, 'schedule', provider));
@@ -308,7 +311,7 @@ function listenQueueMessage(config, integration, queueUrl, queueName) {
308
311
  const channel = yield connection.createChannel();
309
312
  if (channel) {
310
313
  yield channel.assertQueue(queueName, { durable: true });
311
- yield channel.prefetch(1);
314
+ yield channel.prefetch(prefetchCount);
312
315
  yield channel.consume(queueName, consumer(channel, config, integration), { noAck: false });
313
316
  }
314
317
  }
@@ -321,23 +324,67 @@ function listenQueueMessage(config, integration, queueUrl, queueName) {
321
324
  }
322
325
  exports.listenQueueMessage = listenQueueMessage;
323
326
  function consumer(channel, config, integration) {
327
+ let messagesCounter = 0;
328
+ let webhooksInfo = {};
329
+ let webhooksData = [];
330
+ let timeoutId;
331
+ const resetStateVariables = () => {
332
+ messagesCounter = 0;
333
+ webhooksInfo = {};
334
+ webhooksData = [];
335
+ };
324
336
  return function (msg) {
325
337
  var _a;
326
338
  return __awaiter(this, void 0, void 0, function* () {
339
+ messagesCounter++;
327
340
  if (!msg) {
328
341
  return;
329
342
  }
343
+ clearTimeout(timeoutId);
330
344
  try {
331
345
  const data = JSON.parse(msg.content.toString());
332
346
  const urlParam = (_a = integration.webhooks) === null || _a === void 0 ? void 0 : _a.urlParam;
333
347
  const webhookUrlParam = data.query[urlParam];
334
- const webhookData = yield prepareWebhookData(config, integration, webhookUrlParam, models_1.Provider.INTEGRATION);
335
- yield updateCrowdinFromWebhookRequest({ integration, webhookData, req: data });
348
+ const { clientId } = decodedUrlParam(config, webhookUrlParam);
349
+ if (!webhooksInfo[clientId]) {
350
+ webhooksInfo[clientId] = {};
351
+ webhooksInfo[clientId].data = [data];
352
+ webhooksInfo[clientId].integration = integration;
353
+ webhooksData.push(prepareWebhookData(config, integration, webhookUrlParam, models_1.Provider.INTEGRATION).then((res) => {
354
+ webhooksInfo[clientId].webhookData = res;
355
+ }));
356
+ }
357
+ else {
358
+ webhooksInfo[clientId].data.push(data);
359
+ }
360
+ if (messagesCounter < prefetchCount) {
361
+ // if all messages are not received, wait 5 seconds to force process messages
362
+ timeoutId = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
363
+ yield processMessages(webhooksData, webhooksInfo, channel, msg);
364
+ resetStateVariables();
365
+ }), forceProcessDelay);
366
+ return;
367
+ }
368
+ clearTimeout(timeoutId);
369
+ yield processMessages(webhooksData, webhooksInfo, channel, msg);
370
+ resetStateVariables();
336
371
  }
337
372
  catch (e) {
338
373
  (0, logger_1.logError)(e);
339
374
  }
340
- channel.ack(msg);
341
375
  });
342
376
  };
343
377
  }
378
+ function processMessages(webhooksData, webhooksInfo, channel, msg) {
379
+ return __awaiter(this, void 0, void 0, function* () {
380
+ yield Promise.all(webhooksData);
381
+ for (const { data, integration, webhookData } of Object.values(webhooksInfo)) {
382
+ yield updateCrowdinFromWebhookRequest({
383
+ integration: integration,
384
+ webhookData: webhookData,
385
+ req: data,
386
+ });
387
+ }
388
+ channel.ack(msg, true);
389
+ });
390
+ }
@@ -58,6 +58,7 @@
58
58
  {{/if}}
59
59
  </div>
60
60
  <crowdin-simple-integration
61
+ async-progress
61
62
  {{#if syncNewElements.crowdin}}
62
63
  skip-crowdin-auto-schedule
63
64
  {{/if}}
@@ -101,6 +102,9 @@
101
102
  </div>
102
103
  </div>
103
104
  <crowdin-toasts></crowdin-toasts>
105
+ <crowdin-async-progress
106
+ cancelAsyncAction=""
107
+ ></crowdin-async-progress>
104
108
  <crowdin-modal id="subscription-modal" modal-width="50" close-button="false">
105
109
  <div>
106
110
  <crowdin-alert type="warning">Subscribe to continue using the {{name}} app.</crowdin-alert>
@@ -296,6 +300,9 @@
296
300
  });
297
301
  document.body.addEventListener('uploadFilesToCrowdin', uploadFilesToCrowdin);
298
302
  document.body.addEventListener('uploadFilesToIntegration', uploadFilesToIntegration);
303
+ document.body.addEventListener('cancelAsyncAction', (e) => {
304
+ cancelJob(e.detail);
305
+ });
299
306
  {{#if integrationSearchListener}}
300
307
  document.body.addEventListener("integrationFilterChange", (event) => {
301
308
  getIntegrationData(false, 0, event.detail);
@@ -308,11 +315,27 @@
308
315
  const fileType = '1';
309
316
  const branchType = '2';
310
317
 
318
+ const JOB_TYPE = {
319
+ updateCrowdin: 'updateCrowdin',
320
+ updateIntegration: 'updateIntegration',
321
+ };
322
+
323
+ const JOB_STATUS = {
324
+ created: 'created',
325
+ inProgress: 'inProgress',
326
+ failed: 'failed',
327
+ canceled: 'canceled',
328
+ finished: 'finished',
329
+ };
330
+
331
+ const asyncJobs = {};
332
+
311
333
  let project = {};
312
334
  let crowdinData = [];
313
335
 
314
336
  getCrowdinData();
315
337
  getIntegrationData();
338
+ getActiveJobs();
316
339
 
317
340
  function integrationLogout() {
318
341
  checkOrigin()
@@ -481,10 +504,131 @@
481
504
  body: JSON.stringify(req)
482
505
  }))
483
506
  .then(checkResponse)
484
- .then(() => showToast(`File${event.detail.length > 1 ? 's' : ''} imported successfully`))
485
- .then(getCrowdinData)
507
+ .then((res) => {
508
+ checkJob({
509
+ jobId: res?.jobId,
510
+ jobType: JOB_TYPE.updateCrowdin,
511
+ })
512
+ })
486
513
  .catch(e => catchRejection(e, 'Can\'t upload templates to Crowdin'))
487
- .finally(() => (appComponent.setAttribute('is-to-crowdin-process', false)));
514
+ }
515
+
516
+ function getActiveJobs() {
517
+ checkOrigin()
518
+ .then(restParams => fetch(`api/jobs${restParams}`))
519
+ .then(checkResponse)
520
+ .then((jobs) => {
521
+ if (!Array.isArray(jobs)) {
522
+ return;
523
+ }
524
+
525
+ jobs.forEach((job) => {
526
+ switch (job.type) {
527
+ case JOB_TYPE.updateCrowdin:
528
+ appComponent.setAttribute('is-to-crowdin-process', true);
529
+ break;
530
+ case JOB_TYPE.updateIntegration:
531
+ appComponent.setAttribute('is-to-integration-process', true);
532
+ break;
533
+ default:
534
+ }
535
+
536
+ checkJob({
537
+ jobId: job.id,
538
+ jobType: job.type,
539
+ })
540
+ })
541
+ })
542
+ .catch(e => catchRejection(e, 'Sync status check failed'))
543
+ }
544
+
545
+ function cancelJob(jobId) {
546
+ if (asyncJobs[jobId]?.isFailed) {
547
+ return;
548
+ }
549
+
550
+ checkOrigin()
551
+ .then(restParams => fetch('api/jobs' + restParams + '&job_id=' + jobId, {
552
+ method: 'DELETE',
553
+ headers: { 'Content-Type': 'application/json' },
554
+ }))
555
+ .then(checkResponse)
556
+ .then(() => showToast('Cancellation…'))
557
+ .catch(e => catchRejection(e, 'Sync cancellation failed'));
558
+ }
559
+
560
+ function checkJob({jobId, jobType, onSuccess, onError, onFinally}) {
561
+ switch (jobType) {
562
+ case JOB_TYPE.updateCrowdin:
563
+ if (!onSuccess) {
564
+ onSuccess = ((job) => {
565
+ getCrowdinData();
566
+ });
567
+ }
568
+
569
+ if (!onFinally) {
570
+ onFinally = (() => appComponent.setAttribute('is-to-crowdin-process', false));
571
+ }
572
+ break;
573
+ case JOB_TYPE.updateIntegration:
574
+ if (!onFinally) {
575
+ onFinally = (() => appComponent.setAttribute('is-to-integration-process', false));
576
+ }
577
+ break;
578
+ default:
579
+ }
580
+
581
+ checkOrigin()
582
+ .then(restParams => fetch('api/jobs' + restParams + '&job_id=' + jobId))
583
+ .then(checkResponse)
584
+ .then((job) => {
585
+ const isFailed = [JOB_STATUS.failed, JOB_STATUS.canceled].includes(job.status);
586
+ const progress = isFailed || JOB_STATUS.finished === job.status ? 100 : job.progress;
587
+ const info = JOB_STATUS.canceled === job.status ? `Cancelled\n${job.info || ''}` : job.info;
588
+
589
+ pushJobs([ {
590
+ id: job.id,
591
+ title: job.title,
592
+ progress,
593
+ info,
594
+ isFailed,
595
+ } ]);
596
+
597
+ if (isFailed) {
598
+ onError && onError();
599
+ } else if ([JOB_STATUS.created, JOB_STATUS.inProgress].includes(job.status) && job.progress < 100) {
600
+ setTimeout(() => {
601
+ checkJob({jobId, jobType, onSuccess, onError, onFinally});
602
+ }, {{asyncProgress.checkInterval}});
603
+ return;
604
+ } else {
605
+ onSuccess && onSuccess(job);
606
+ }
607
+
608
+ onFinally && onFinally(job);
609
+ })
610
+ .catch((e) => {
611
+ onFinally && onFinally();
612
+
613
+ pushJobs([ {
614
+ id: jobId,
615
+ isFailed: true,
616
+ } ]);
617
+
618
+ showToast('Sync status check failed');
619
+ });
620
+ }
621
+
622
+ function pushJobs(jobs) {
623
+ const el = document.querySelector('crowdin-async-progress');
624
+ if (el && el.pushJobs) {
625
+ el.pushJobs(jobs);
626
+ jobs.forEach((job) => (asyncJobs[job.id] = job));
627
+ } else {
628
+ setTimeout(() => {
629
+ pushJobs(jobs);
630
+ }, 300);
631
+ }
488
632
  }
489
633
 
490
634
  function uploadFilesToIntegration(e) {
@@ -504,9 +648,13 @@
504
648
  body: JSON.stringify(req)
505
649
  }))
506
650
  .then(checkResponse)
507
- .then(() => showToast(`File${Object.keys(e.detail).length > 1 ? 's' : ''} exported successfully`))
651
+ .then((res) => {
652
+ checkJob({
653
+ jobId: res?.jobId,
654
+ jobType: JOB_TYPE.updateIntegration,
655
+ })
656
+ })
508
657
  .catch(e => catchRejection(e, 'Can\'t upload files to {{name}}'))
509
- .finally(() => (appComponent.setAttribute('is-to-integration-process', false)));
510
658
  }
511
659
 
512
660
  {{#if configurationFields}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.39.1",
3
+ "version": "0.41.0",
4
4
  "description": "Module that generates for you all common endpoints for serving standalone Crowdin App",
5
5
  "main": "out/index.js",
6
6
  "types": "out/index.d.ts",
@@ -15,11 +15,10 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@aws-sdk/client-s3": "^3.423.0",
18
- "@aws-sdk/s3-request-presigner": "^3.423.0",
18
+ "@aws-sdk/s3-request-presigner": "^3.484.0",
19
19
  "@crowdin/crowdin-apps-functions": "0.6.0",
20
- "@crowdin/logs-formatter": "^2.0.6",
20
+ "@crowdin/logs-formatter": "^2.1.2",
21
21
  "@godaddy/terminus": "^4.12.1",
22
- "@types/pg": "^8.10.3",
23
22
  "amqplib": "^0.10.3",
24
23
  "crypto-js": "^4.2.0",
25
24
  "express": "4.18.2",
@@ -33,29 +32,31 @@
33
32
  "uuid": "^8.3.2"
34
33
  },
35
34
  "devDependencies": {
36
- "@babel/preset-react": "^7.22.15",
35
+ "@babel/preset-react": "^7.23.3",
37
36
  "@emotion/react": "^11.11.1",
38
37
  "@emotion/styled": "^11.11.0",
39
- "@mui/icons-material": "^5.14.19",
40
- "@mui/material": "^5.14.19",
41
- "@rjsf/core": "^5.15.0",
42
- "@rjsf/mui": "^5.15.0",
43
- "@rjsf/utils": "^5.15.0",
44
- "@rjsf/validator-ajv8": "^5.15.0",
38
+ "@mui/icons-material": "^5.15.7",
39
+ "@mui/material": "^5.15.7",
40
+ "@rjsf/core": "^5.16.1",
41
+ "@rjsf/mui": "^5.16.1",
42
+ "@rjsf/utils": "^5.15.1",
43
+ "@rjsf/validator-ajv8": "^5.16.1",
45
44
  "@rollup/plugin-babel": "^6.0.4",
46
45
  "@rollup/plugin-commonjs": "^24.1.0",
47
46
  "@rollup/plugin-json": "^6.0.0",
48
- "@rollup/plugin-node-resolve": "^15.2.1",
47
+ "@rollup/plugin-node-resolve": "^15.2.3",
49
48
  "@rollup/plugin-replace": "^5.0.5",
50
49
  "@rollup/plugin-terser": "^0.4.3",
51
50
  "@types/amqplib": "^0.10.4",
52
51
  "@types/crypto-js": "^4.1.3",
53
- "@types/express": "4.17.18",
52
+ "@types/express": "4.17.21",
54
53
  "@types/express-handlebars": "^5.3.1",
55
54
  "@types/jest": "^29.5.5",
56
- "@types/node": "^16.18.60",
55
+ "@types/node": "^16.18.69",
57
56
  "@types/node-cron": "^3.0.9",
58
- "@types/swagger-jsdoc": "^6.0.1",
57
+ "@types/pg": "^8.10.3",
58
+ "@types/swagger-jsdoc": "^6.0.4",
59
+ "@types/uuid": "^9.0.7",
59
60
  "@typescript-eslint/eslint-plugin": "^2.3.1",
60
61
  "@typescript-eslint/parser": "^2.3.1",
61
62
  "eslint": "^6.4.0",
@@ -1,3 +0,0 @@
1
- /// <reference types="qs" />
2
- import { Response } from 'express';
3
- export default function handle(): (req: import("../models").CrowdinClientRequest | import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: Function) => void;