@crowdin/app-project-module 0.68.0 → 0.69.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.
package/out/index.js CHANGED
@@ -79,6 +79,7 @@ const projectMenuApp = __importStar(require("./modules/project-menu"));
79
79
  const projectMenuCrowdsourceApp = __importStar(require("./modules/project-menu-crowdsource"));
80
80
  const projectReportsApp = __importStar(require("./modules/project-reports"));
81
81
  const projectToolsApp = __importStar(require("./modules/project-tools"));
82
+ const webhooks = __importStar(require("./modules/webhooks"));
82
83
  const subscription_1 = require("./util/subscription");
83
84
  var types_2 = require("./types");
84
85
  Object.defineProperty(exports, "ProjectPermissions", { enumerable: true, get: function () { return types_2.ProjectPermissions; } });
@@ -175,6 +176,7 @@ function addCrowdinEndpoints(app, clientConfig) {
175
176
  aiTools.registerAiTools({ config, app });
176
177
  aiTools.registerAiToolWidgets({ config, app });
177
178
  externalQaCheck.register({ config, app });
179
+ webhooks.register({ config, app });
178
180
  addFormSchema({ config, app });
179
181
  return Object.assign(Object.assign({}, exports.metadataStore), { establishCrowdinConnection: (authRequest, moduleKey) => {
180
182
  let jwtToken = '';
@@ -240,6 +242,9 @@ function convertClientConfig(clientConfig) {
240
242
  throw new Error('Missing [clientId, clientSecret] parameters');
241
243
  }
242
244
  }
245
+ if (clientConfig.projectIntegration) {
246
+ clientConfig.api = Object.assign({ default: true }, clientConfig.api);
247
+ }
243
248
  return Object.assign(Object.assign({}, clientConfig), { baseUrl: baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl, clientId,
244
249
  clientSecret, awsConfig: {
245
250
  tmpBucketName,
@@ -462,7 +462,7 @@ function addDefaultApiEndpoints(app, config) {
462
462
  }
463
463
  exports.addDefaultApiEndpoints = addDefaultApiEndpoints;
464
464
  function addSwagerApiDocumentation(app, config) {
465
- var _a, _b;
465
+ var _a, _b, _c;
466
466
  const options = {
467
467
  swaggerDefinition: {
468
468
  openapi: '3.0.0',
@@ -492,16 +492,16 @@ function addSwagerApiDocumentation(app, config) {
492
492
  },
493
493
  ],
494
494
  },
495
- apis: config.projectIntegration
495
+ apis: config.projectIntegration && ((_a = config.api) === null || _a === void 0 ? void 0 : _a.default)
496
496
  ? [path_1.default.resolve(__dirname, './base.js'), path_1.default.resolve(__dirname, './components.js'), __filename]
497
497
  : [],
498
498
  };
499
- if ((_a = config.api) === null || _a === void 0 ? void 0 : _a.docFile) {
499
+ if ((_b = config.api) === null || _b === void 0 ? void 0 : _b.docFile) {
500
500
  options.apis.push(config.api.docFile);
501
501
  }
502
502
  const swaggerSpec = (0, swagger_jsdoc_1.default)(options);
503
503
  // remove Login info from doc
504
- if (config.projectIntegration && !((_b = config.projectIntegration) === null || _b === void 0 ? void 0 : _b.loginForm)) {
504
+ if (config.projectIntegration && !((_c = config.projectIntegration) === null || _c === void 0 ? void 0 : _c.loginForm)) {
505
505
  delete swaggerSpec.paths['/login'];
506
506
  delete swaggerSpec.paths['/login-fields'];
507
507
  delete swaggerSpec.components.schemas['Login'];
@@ -43,6 +43,7 @@ function handle(config, integration) {
43
43
  projectId: projectId,
44
44
  client: req.crowdinApiClient,
45
45
  jobType: types_1.JobClientType.MANUAL,
46
+ jobStoreType: integration.jobStoreType,
46
47
  jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
47
48
  var _c;
48
49
  if (req.body && ((_c = req.body) === null || _c === void 0 ? void 0 : _c.length)) {
@@ -37,6 +37,7 @@ function handle(config, integration) {
37
37
  projectId: req.crowdinContext.jwtPayload.context.project_id,
38
38
  client: req.crowdinApiClient,
39
39
  jobType: types_1.JobClientType.MANUAL,
40
+ jobStoreType: integration.jobStoreType,
40
41
  jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
41
42
  const result = yield integration.updateIntegration({
42
43
  projectId: req.crowdinContext.jwtPayload.context.project_id,
@@ -36,6 +36,7 @@ function handle(config, integration) {
36
36
  projectId: req.crowdinContext.jwtPayload.context.project_id,
37
37
  client: req.crowdinApiClient,
38
38
  jobType: types_1.JobClientType.MANUAL,
39
+ jobStoreType: integration.jobStoreType,
39
40
  jobCallback: () => __awaiter(this, void 0, void 0, function* () {
40
41
  if (Array.isArray(expandIntegrationFolders) && expandIntegrationFolders.length) {
41
42
  const allFiles = (yield (0, files_1.expandFilesTree)(expandIntegrationFolders, req, integration)).map((node) => ({
@@ -1,7 +1,7 @@
1
1
  import Crowdin, { SourceFilesModel, TranslationStatusModel } from '@crowdin/crowdin-api-client';
2
2
  import { Request } from 'express';
3
3
  import { CrowdinClientRequest, ModuleKey } from '../../types';
4
- import { JobClient } from './util/types';
4
+ import { JobClient, JobStoreType } from './util/types';
5
5
  export interface IntegrationLogic extends ModuleKey {
6
6
  /**
7
7
  * Customize your app login form
@@ -56,6 +56,10 @@ export interface IntegrationLogic extends ModuleKey {
56
56
  appSettings?: any;
57
57
  job: JobClient;
58
58
  }) => Promise<void | ExtendedResult<void>>;
59
+ /**
60
+ * Store to use for memorizing job data
61
+ */
62
+ jobStoreType?: JobStoreType;
59
63
  /**
60
64
  * function to define configuration(settings) modal for you app (by default app will not have any custom settings)
61
65
  */
@@ -93,6 +93,7 @@ function runUpdateProviderJob({ integrationId, crowdinId, type, title, payload,
93
93
  projectId,
94
94
  client,
95
95
  reRunJobId,
96
+ jobStoreType: integration.jobStoreType,
96
97
  jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
97
98
  const updateParams = {
98
99
  projectId,
@@ -174,6 +175,8 @@ function processSyncSettings({ config, integration, period, syncSettings, }) {
174
175
  user_id: crowdinCredentials.userId,
175
176
  },
176
177
  },
178
+ crowdinId: crowdinCredentials.id,
179
+ clientId: integrationCredentials.id,
177
180
  };
178
181
  try {
179
182
  const preparedCrowdinClient = yield (0, connection_1.prepareCrowdinClient)({
@@ -186,7 +189,7 @@ function processSyncSettings({ config, integration, period, syncSettings, }) {
186
189
  crowdinClient = preparedCrowdinClient.client;
187
190
  }
188
191
  catch (e) {
189
- (0, logger_1.logError)(e);
192
+ (0, logger_1.logError)(e, context);
190
193
  return;
191
194
  }
192
195
  const { expired } = yield (0, subscription_1.checkSubscription)({
@@ -204,7 +207,7 @@ function processSyncSettings({ config, integration, period, syncSettings, }) {
204
207
  .data;
205
208
  }
206
209
  catch (e) {
207
- (0, logger_1.logError)(e);
210
+ (0, logger_1.logError)(e, context);
208
211
  return;
209
212
  }
210
213
  // eslint-disable-next-line @typescript-eslint/camelcase
@@ -228,7 +231,7 @@ function processSyncSettings({ config, integration, period, syncSettings, }) {
228
231
  });
229
232
  }
230
233
  catch (e) {
231
- (0, logger_1.logError)(e);
234
+ (0, logger_1.logError)(e, context);
232
235
  return;
233
236
  }
234
237
  }
@@ -267,6 +270,7 @@ function processSyncSettings({ config, integration, period, syncSettings, }) {
267
270
  crowdinClient,
268
271
  onlyApproved,
269
272
  onlyTranslated,
273
+ context,
270
274
  });
271
275
  if (Object.keys(filesToProcess).length <= 0) {
272
276
  return;
@@ -409,7 +413,7 @@ function getNewFoldersFile(folders, snapshotFiles) {
409
413
  files = files.filter((file) => 'type' in file);
410
414
  return files;
411
415
  }
412
- function getOnlyTranslatedOrApprovedFiles({ projectId, crowdinFiles, crowdinClient, onlyApproved, onlyTranslated, }) {
416
+ function getOnlyTranslatedOrApprovedFiles({ projectId, crowdinFiles, crowdinClient, onlyApproved, onlyTranslated, context, }) {
413
417
  return __awaiter(this, void 0, void 0, function* () {
414
418
  (0, logger_1.log)(`Filtering files to process only ${onlyApproved ? 'approved' : 'translated'} files`);
415
419
  const filesInfo = yield Promise.all(Object.keys(crowdinFiles).map((fileId) => __awaiter(this, void 0, void 0, function* () {
@@ -424,7 +428,7 @@ function getOnlyTranslatedOrApprovedFiles({ projectId, crowdinFiles, crowdinClie
424
428
  }
425
429
  catch (e) {
426
430
  delete crowdinFiles[fileId];
427
- (0, logger_1.logError)(e);
431
+ (0, logger_1.logError)(e, context);
428
432
  }
429
433
  })));
430
434
  const filteredFiles = {};
@@ -258,6 +258,7 @@ function applyIntegrationModuleDefaults(config, integration) {
258
258
  if (!integration.userErrorLifetimeDays) {
259
259
  integration.userErrorLifetimeDays = 30;
260
260
  }
261
+ integration.jobStoreType = integration.jobStoreType || 'db';
261
262
  }
262
263
  exports.applyIntegrationModuleDefaults = applyIntegrationModuleDefaults;
263
264
  function constructOauthUrl({ config, integration, clientId, loginForm, }) {
@@ -1,8 +1,8 @@
1
- import { JobClient, JobClientType, JobType } from './types';
1
+ import { JobClient, JobClientType, JobStoreType, JobType } from './types';
2
2
  import { Response } from 'express';
3
3
  import Crowdin from '@crowdin/crowdin-api-client';
4
4
  import { Config } from '../../../types';
5
- export declare function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobCallback, onError, reRunJobId, }: {
5
+ export declare function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobStoreType, jobCallback, onError, reRunJobId, }: {
6
6
  integrationId: string;
7
7
  crowdinId: string;
8
8
  type: JobType;
@@ -12,6 +12,7 @@ export declare function runAsJob({ integrationId, crowdinId, type, title, payloa
12
12
  projectId: number;
13
13
  client: Crowdin;
14
14
  jobType: JobClientType;
15
+ jobStoreType: JobStoreType;
15
16
  jobCallback: (arg1: JobClient) => Promise<any>;
16
17
  onError?: (e: any) => Promise<void>;
17
18
  reRunJobId?: string;
@@ -47,7 +47,8 @@ const blockingJobs = {
47
47
  [types_1.JobType.INTEGRATION_SYNC_SETTINGS_SAVE]: [types_1.JobType.INTEGRATION_SYNC_SETTINGS_SAVE],
48
48
  };
49
49
  const maxAttempts = 3;
50
- function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobCallback, onError, reRunJobId, }) {
50
+ const store = {};
51
+ function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobStoreType, jobCallback, onError, reRunJobId, }) {
51
52
  return __awaiter(this, void 0, void 0, function* () {
52
53
  let jobId;
53
54
  const storage = (0, storage_1.getStorage)();
@@ -77,6 +78,7 @@ function runAsJob({ integrationId, crowdinId, type, title, payload, res, project
77
78
  if (res) {
78
79
  res.status(202).send({ jobId });
79
80
  }
81
+ const isDbStore = jobStoreType === 'db';
80
82
  const job = {
81
83
  get: function getJob() {
82
84
  return __awaiter(this, void 0, void 0, function* () {
@@ -102,14 +104,37 @@ function runAsJob({ integrationId, crowdinId, type, title, payload, res, project
102
104
  },
103
105
  type: jobType,
104
106
  fetchTranslation: ({ fileId, languageId }) => __awaiter(this, void 0, void 0, function* () {
105
- const translationCache = yield storage.getFileTranslationCacheByLanguage({
106
- integrationId,
107
- crowdinId,
108
- fileId,
109
- languageId,
110
- });
107
+ var _a, _b, _c;
108
+ const translationCache = isDbStore
109
+ ? yield storage.getFileTranslationCacheByLanguage({
110
+ integrationId,
111
+ crowdinId,
112
+ fileId,
113
+ languageId,
114
+ })
115
+ : (_c = (_b = (_a = store[integrationId]) === null || _a === void 0 ? void 0 : _a[crowdinId]) === null || _b === void 0 ? void 0 : _b[fileId]) === null || _c === void 0 ? void 0 : _c[languageId];
111
116
  if (!translationCache) {
112
- yield storage.saveTranslationCache({ integrationId, crowdinId, fileId, languageId });
117
+ if (isDbStore) {
118
+ yield storage.saveTranslationCache({ integrationId, crowdinId, fileId, languageId });
119
+ }
120
+ else {
121
+ if (!store[integrationId]) {
122
+ store[integrationId] = {};
123
+ }
124
+ if (!store[integrationId][crowdinId]) {
125
+ store[integrationId] = {
126
+ [crowdinId]: {},
127
+ };
128
+ }
129
+ if (!store[integrationId][crowdinId][fileId]) {
130
+ store[integrationId] = {
131
+ [crowdinId]: {
132
+ [fileId]: {},
133
+ },
134
+ };
135
+ }
136
+ store[integrationId][crowdinId][fileId][languageId] = {};
137
+ }
113
138
  }
114
139
  let translation = null;
115
140
  try {
@@ -126,6 +151,10 @@ function runAsJob({ integrationId, crowdinId, type, title, payload, res, project
126
151
  }),
127
152
  // translationUploaded: async ({ fileId, languageId, etag }) => {
128
153
  translationUploaded: ({ fileId, translationParams }) => __awaiter(this, void 0, void 0, function* () {
154
+ if (!isDbStore) {
155
+ translationParams.forEach(({ languageId, etag }) => (store[integrationId][crowdinId][fileId][languageId] = { etag }));
156
+ return;
157
+ }
129
158
  const translationCache = (yield storage.getFileTranslationCache({
130
159
  integrationId,
131
160
  crowdinId,
@@ -21,6 +21,7 @@ export declare enum JobClientType {
21
21
  MANUAL = "manual",
22
22
  RERUN = "rerun"
23
23
  }
24
+ export type JobStoreType = 'db' | 'in-memory';
24
25
  export interface Job {
25
26
  id: string;
26
27
  integrationId: string;
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.listenQueueMessage = exports.updateCrowdinFromWebhookRequest = exports.prepareWebhookData = exports.unregisterAllCrowdinWebhooks = exports.unregisterWebhooks = exports.registerWebhooks = exports.HookEvents = void 0;
39
+ const logsFormatter = __importStar(require("@crowdin/logs-formatter"));
39
40
  const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-functions"));
40
41
  const amqplib_1 = __importDefault(require("amqplib"));
41
42
  const types_1 = require("../types");
@@ -265,8 +266,7 @@ function prepareWebhookData({ config, integration, provider, webhookUrlParam, })
265
266
  if (!crowdinCredentials) {
266
267
  return { projectId, crowdinClient, rootFolder, appSettings, syncSettings, preparedIntegrationCredentials };
267
268
  }
268
- const context = {
269
- jwtPayload: {
269
+ const context = Object.assign({ jwtPayload: {
270
270
  context: {
271
271
  // eslint-disable-next-line @typescript-eslint/camelcase
272
272
  project_id: projectId,
@@ -275,8 +275,9 @@ function prepareWebhookData({ config, integration, provider, webhookUrlParam, })
275
275
  // eslint-disable-next-line @typescript-eslint/camelcase
276
276
  user_id: crowdinCredentials === null || crowdinCredentials === void 0 ? void 0 : crowdinCredentials.userId,
277
277
  },
278
- },
279
- };
278
+ }, crowdinId: crowdinCredentials.id }, ((integrationCredentials === null || integrationCredentials === void 0 ? void 0 : integrationCredentials.id) && { clientId: integrationCredentials.id }));
279
+ logsFormatter.resetContext();
280
+ logsFormatter.setContext(context);
280
281
  crowdinClient = yield (0, connection_1.prepareCrowdinClient)({
281
282
  config,
282
283
  credentials: crowdinCredentials,
@@ -225,6 +225,18 @@ function handle(config) {
225
225
  })));
226
226
  }
227
227
  }
228
+ if (config.webhooks) {
229
+ const webhooks = Array.isArray(config.webhooks) ? config.webhooks : [config.webhooks];
230
+ modules['webhook'] = [];
231
+ for (let i = 0; i < webhooks.length; i++) {
232
+ webhooks[i].key = config.identifier + '-' + 'webhook-' + i;
233
+ modules['webhook'].push({
234
+ key: webhooks[i].key,
235
+ url: '/webhooks',
236
+ events: webhooks[i].events,
237
+ });
238
+ }
239
+ }
228
240
  if (config.externalQaCheck) {
229
241
  config.externalQaCheck.key = config.identifier + '-qa-check';
230
242
  const uiModule = config.externalQaCheck.settingsUiModule;
@@ -0,0 +1,5 @@
1
+ /// <reference types="qs" />
2
+ import { CrowdinClientRequest } from '../../../types';
3
+ import { Response } from 'express';
4
+ import { Webhook } from '../types';
5
+ export declare function webhookHandler(webhooks: Webhook[]): (req: 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;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.webhookHandler = void 0;
39
+ const util_1 = require("../../../util");
40
+ const lodash_isstring_1 = __importDefault(require("lodash.isstring"));
41
+ const crypto = __importStar(require("node:crypto"));
42
+ const storage = __importStar(require("../../../storage"));
43
+ function webhookHandler(webhooks) {
44
+ return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
45
+ const domain = req.headers['x-crowdin-domain'];
46
+ const organizationId = req.headers['x-crowdin-organization-id'];
47
+ const signature = req.headers['x-crowdin-signature'];
48
+ const moduleKey = req.headers['x-application-webhook-key'];
49
+ if (!(0, lodash_isstring_1.default)(domain) || !(0, lodash_isstring_1.default)(organizationId) || !(0, lodash_isstring_1.default)(signature) || !(0, lodash_isstring_1.default)(moduleKey)) {
50
+ res.status(400).send({ error: 'Invalid request' });
51
+ return;
52
+ }
53
+ const crowdinId = domain || organizationId;
54
+ const credentials = yield storage.getStorage().getCrowdinCredentials(crowdinId);
55
+ if (!credentials) {
56
+ throw new Error('Failed to find Crowdin credentials');
57
+ }
58
+ const hmac = crypto.createHmac('sha256', credentials.appSecret);
59
+ hmac.update(JSON.stringify(req.body).replace(/\//g, '\\/'));
60
+ const generatedSignature = hmac.digest('hex');
61
+ if (generatedSignature !== signature.replace('sha256=', '')) {
62
+ res.status(403).send({ error: 'Invalid signature' });
63
+ return;
64
+ }
65
+ res.status(200).send();
66
+ for (const webhook of webhooks) {
67
+ if (webhook.key === moduleKey) {
68
+ for (const event of req.body.payload.events) {
69
+ yield webhook.callback({ credentials, event });
70
+ }
71
+ }
72
+ }
73
+ }));
74
+ }
75
+ exports.webhookHandler = webhookHandler;
@@ -0,0 +1,6 @@
1
+ import { Config } from '../../types';
2
+ import { Express } from 'express';
3
+ export declare function register({ config, app }: {
4
+ config: Config;
5
+ app: Express;
6
+ }): void;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.register = void 0;
7
+ const json_response_1 = __importDefault(require("../../middlewares/json-response"));
8
+ const webhook_handler_1 = require("./handlers/webhook-handler");
9
+ function register({ config, app }) {
10
+ if (!config.webhooks) {
11
+ return;
12
+ }
13
+ const webhooks = Array.isArray(config.webhooks) ? config.webhooks : [config.webhooks];
14
+ if (webhooks.length) {
15
+ app.post('/webhooks', json_response_1.default, (0, webhook_handler_1.webhookHandler)(webhooks));
16
+ }
17
+ }
18
+ exports.register = register;