@crowdin/app-project-module 0.88.1 → 0.90.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 (37) hide show
  1. package/out/index.js +4 -6
  2. package/out/middlewares/api-call.d.ts +2 -0
  3. package/out/middlewares/api-call.js +7 -0
  4. package/out/middlewares/crowdin-client.js +8 -1
  5. package/out/middlewares/integration-credentials.js +19 -1
  6. package/out/modules/api/api.js +19 -26
  7. package/out/modules/api/components.d.ts +0 -3
  8. package/out/modules/api/components.js +0 -3
  9. package/out/modules/integration/handlers/crowdin-file-progress.js +7 -0
  10. package/out/modules/integration/handlers/crowdin-update.js +7 -0
  11. package/out/modules/integration/handlers/integration-login.js +27 -2
  12. package/out/modules/integration/handlers/integration-update.js +11 -2
  13. package/out/modules/integration/handlers/job-cancel.d.ts +1 -2
  14. package/out/modules/integration/handlers/job-cancel.js +12 -10
  15. package/out/modules/integration/handlers/job-info.js +50 -32
  16. package/out/modules/integration/handlers/main.js +1 -0
  17. package/out/modules/integration/handlers/settings-save.js +9 -2
  18. package/out/modules/integration/handlers/sync-settings-save.js +17 -0
  19. package/out/modules/integration/handlers/sync-settings.js +7 -0
  20. package/out/modules/integration/index.js +1 -1
  21. package/out/modules/integration/types.d.ts +4 -0
  22. package/out/modules/integration/util/defaults.js +46 -39
  23. package/out/modules/integration/util/job.d.ts +2 -1
  24. package/out/modules/integration/util/job.js +2 -2
  25. package/out/modules/webhooks/handlers/webhook-handler.js +10 -1
  26. package/out/modules/webhooks/types.d.ts +7 -0
  27. package/out/storage/index.d.ts +2 -2
  28. package/out/storage/mysql.d.ts +6 -1
  29. package/out/storage/mysql.js +48 -0
  30. package/out/storage/postgre.d.ts +1 -1
  31. package/out/storage/postgre.js +1 -1
  32. package/out/storage/sqlite.d.ts +2 -2
  33. package/out/types.d.ts +2 -1
  34. package/out/util/index.d.ts +0 -1
  35. package/out/util/index.js +1 -6
  36. package/out/views/main.handlebars +18 -5
  37. package/package.json +1 -1
package/out/index.js CHANGED
@@ -98,13 +98,11 @@ exports.metadataStore = {
98
98
  },
99
99
  saveMetadata: (id, metadata, crowdinId) => __awaiter(void 0, void 0, void 0, function* () {
100
100
  if (!crowdinId) {
101
- console.warn('Warning: The crowdinId parameter in saveMetadata will be required. Please update your code to provide this parameter.');
101
+ throw new Error('The crowdinId parameter is required.');
102
102
  }
103
- else {
104
- const crowdinCredentials = yield storage.getStorage().getCrowdinCredentials(crowdinId);
105
- if (!crowdinCredentials) {
106
- console.error('Invalid crowdinId parameter: You can get your crowdinId from the JWT payload');
107
- }
103
+ const crowdinCredentials = yield storage.getStorage().getCrowdinCredentials(crowdinId);
104
+ if (!crowdinCredentials) {
105
+ throw new Error('Invalid crowdinId parameter: You can get your crowdinId from the JWT payload.');
108
106
  }
109
107
  const existing = yield storage.getStorage().getMetadata(id);
110
108
  if (existing) {
@@ -0,0 +1,2 @@
1
+ import { Request, Response } from 'express';
2
+ export default function handle(req: Request, _res: Response, next: Function): void;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ function handle(req, _res, next) {
4
+ req.isApiCall = true;
5
+ next();
6
+ }
7
+ exports.default = handle;
@@ -65,7 +65,7 @@ function prepareCrowdinRequest({ jwtToken, config, optional = false, checkSubscr
65
65
  exports.prepareCrowdinRequest = prepareCrowdinRequest;
66
66
  function handle({ config, optional = false, checkSubscriptionExpiration = true, moduleKey, }) {
67
67
  return (0, util_1.runAsyncWrapper)((req, res, next) => __awaiter(this, void 0, void 0, function* () {
68
- var _a;
68
+ var _a, _b;
69
69
  const jwtToken = getToken(req);
70
70
  if (!jwtToken) {
71
71
  (0, logger_1.temporaryErrorDebug)('Access denied: crowdin-client', req);
@@ -80,6 +80,13 @@ function handle({ config, optional = false, checkSubscriptionExpiration = true,
80
80
  moduleKey,
81
81
  });
82
82
  if ((_a = config.api) === null || _a === void 0 ? void 0 : _a.default) {
83
+ if (req.isApiCall && !((_b = req.body) === null || _b === void 0 ? void 0 : _b.projectId)) {
84
+ return res.status(400).json({
85
+ error: {
86
+ message: 'Missing required parameter: projectId',
87
+ },
88
+ });
89
+ }
83
90
  data.context = (0, api_1.updateCrowdinContext)(req, data.context);
84
91
  }
85
92
  req.crowdinContext = data.context;
@@ -40,6 +40,7 @@ const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-function
40
40
  function handle(config, integration, optional = false) {
41
41
  return (0, util_1.runAsyncWrapper)((req, res, next) => __awaiter(this, void 0, void 0, function* () {
42
42
  let clientId = req.crowdinContext.clientId;
43
+ const isApiCall = req === null || req === void 0 ? void 0 : req.isApiCall;
43
44
  const { organization, projectId, userId } = crowdinAppFunctions.parseCrowdinId(clientId);
44
45
  req.logInfo(`Loading integration credentials for client ${clientId}`);
45
46
  let integrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(clientId);
@@ -73,6 +74,13 @@ function handle(config, integration, optional = false) {
73
74
  owners: null,
74
75
  hideActions: false,
75
76
  };
77
+ if (isApiCall) {
78
+ return res.status(errorOptions.code).json({
79
+ error: {
80
+ message: errorOptions.message,
81
+ },
82
+ });
83
+ }
76
84
  if (owners) {
77
85
  errorOptions.message = 'Looks like you don’t have access';
78
86
  errorOptions.hideActions = true;
@@ -88,7 +96,17 @@ function handle(config, integration, optional = false) {
88
96
  }
89
97
  catch (e) {
90
98
  console.error(e);
91
- throw new util_1.CodeError('Credentials to integration either exprired or invalid', 401);
99
+ const message = 'Credentials to integration either expired or invalid';
100
+ if (isApiCall) {
101
+ return res.status(401).json({
102
+ error: {
103
+ message,
104
+ },
105
+ });
106
+ }
107
+ else {
108
+ throw new util_1.CodeError(message, 401);
109
+ }
92
110
  }
93
111
  const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(clientId);
94
112
  if (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config) {
@@ -10,6 +10,7 @@ const swagger_jsdoc_1 = __importDefault(require("swagger-jsdoc"));
10
10
  const crowdin_client_1 = __importDefault(require("../../middlewares/crowdin-client"));
11
11
  const integration_credentials_1 = __importDefault(require("../../middlewares/integration-credentials"));
12
12
  const json_response_1 = __importDefault(require("../../middlewares/json-response"));
13
+ const api_call_1 = __importDefault(require("../../middlewares/api-call"));
13
14
  const crowdin_file_progress_1 = __importDefault(require("../integration/handlers/crowdin-file-progress"));
14
15
  const crowdin_files_1 = __importDefault(require("../integration/handlers/crowdin-files"));
15
16
  const crowdin_update_1 = __importDefault(require("../integration/handlers/crowdin-update"));
@@ -157,10 +158,7 @@ function getFormFields(fields) {
157
158
  const formFields = [];
158
159
  for (const field of fields) {
159
160
  if (field === null || field === void 0 ? void 0 : field.key) {
160
- formFields.push({
161
- name: field.label,
162
- key: field.key,
163
- });
161
+ formFields.push(Object.assign({ name: field.label, key: field.key, type: field.type }, (field.type === 'select' && field.options ? { options: field.options } : {})));
164
162
  }
165
163
  }
166
164
  return formFields;
@@ -187,7 +185,7 @@ function addDefaultApiEndpoints(app, config) {
187
185
  * data:
188
186
  * $ref: '#/components/schemas/CrowdinFiles'
189
187
  */
190
- app.get('/crowdin-files', json_response_1.default, (0, crowdin_client_1.default)({
188
+ app.get('/crowdin-files', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
191
189
  config,
192
190
  optional: false,
193
191
  checkSubscriptionExpiration: true,
@@ -221,12 +219,12 @@ function addDefaultApiEndpoints(app, config) {
221
219
  * data:
222
220
  * $ref: '#/components/schemas/FileProgress'
223
221
  */
224
- app.get('/file-progress', (0, crowdin_client_1.default)({
222
+ app.get('/file-progress', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
225
223
  config,
226
224
  optional: false,
227
225
  checkSubscriptionExpiration: true,
228
226
  moduleKey: 'file-translation-progress-api',
229
- }), (0, crowdin_file_progress_1.default)(config.projectIntegration));
227
+ }), (0, integration_credentials_1.default)(config, config.projectIntegration), (0, crowdin_file_progress_1.default)(config.projectIntegration));
230
228
  /**
231
229
  * @openapi
232
230
  * /integration-files:
@@ -248,7 +246,7 @@ function addDefaultApiEndpoints(app, config) {
248
246
  * $ref: '#/components/schemas/IntegrationFiles'
249
247
  *
250
248
  */
251
- app.get('/integration-files', json_response_1.default, (0, crowdin_client_1.default)({
249
+ app.get('/integration-files', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
252
250
  config,
253
251
  optional: false,
254
252
  checkSubscriptionExpiration: true,
@@ -276,7 +274,7 @@ function addDefaultApiEndpoints(app, config) {
276
274
  * data:
277
275
  * $ref: '#/components/schemas/UpdateResponse'
278
276
  */
279
- app.post('/crowdin-update', json_response_1.default, (0, crowdin_client_1.default)({
277
+ app.post('/crowdin-update', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
280
278
  config,
281
279
  optional: false,
282
280
  checkSubscriptionExpiration: true,
@@ -304,7 +302,7 @@ function addDefaultApiEndpoints(app, config) {
304
302
  * data:
305
303
  * $ref: '#/components/schemas/UpdateResponse'
306
304
  */
307
- app.post('/integration-update', json_response_1.default, (0, crowdin_client_1.default)({
305
+ app.post('/integration-update', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
308
306
  config,
309
307
  optional: false,
310
308
  checkSubscriptionExpiration: true,
@@ -334,12 +332,12 @@ function addDefaultApiEndpoints(app, config) {
334
332
  * schema:
335
333
  * $ref: '#/components/schemas/JobResponse'
336
334
  */
337
- app.get('/jobs', json_response_1.default, (0, crowdin_client_1.default)({
335
+ app.get('/jobs', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
338
336
  config,
339
337
  optional: false,
340
338
  checkSubscriptionExpiration: true,
341
339
  moduleKey: 'job-get-api',
342
- }), (0, job_info_1.default)(config));
340
+ }), (0, integration_credentials_1.default)(config, config.projectIntegration), (0, job_info_1.default)(config));
343
341
  /**
344
342
  * @openapi
345
343
  * /jobs:
@@ -352,7 +350,7 @@ function addDefaultApiEndpoints(app, config) {
352
350
  * - $ref: '#/components/parameters/ProjectId'
353
351
  * - name: jobId
354
352
  * in: query
355
- * required: false
353
+ * required: true
356
354
  * schema:
357
355
  * type: string
358
356
  * example: 067da473-fc0b-43e3-b0a2-09d26af130c1
@@ -360,12 +358,12 @@ function addDefaultApiEndpoints(app, config) {
360
358
  * 204:
361
359
  * description: 'Job canceled successfully'
362
360
  */
363
- app.delete('/jobs', json_response_1.default, (0, crowdin_client_1.default)({
361
+ app.delete('/jobs', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
364
362
  config,
365
363
  optional: false,
366
364
  checkSubscriptionExpiration: true,
367
365
  moduleKey: 'job-cancel-api',
368
- }), (0, job_cancel_1.default)(config));
366
+ }), (0, integration_credentials_1.default)(config, config.projectIntegration), (0, job_cancel_1.default)());
369
367
  /**
370
368
  * @openapi
371
369
  * /settings:
@@ -386,7 +384,7 @@ function addDefaultApiEndpoints(app, config) {
386
384
  * data:
387
385
  * $ref: '#/components/schemas/SettingsResponse'
388
386
  */
389
- app.get('/settings', (0, crowdin_client_1.default)({
387
+ app.get('/settings', api_call_1.default, (0, crowdin_client_1.default)({
390
388
  config,
391
389
  optional: false,
392
390
  checkSubscriptionExpiration: true,
@@ -409,7 +407,7 @@ function addDefaultApiEndpoints(app, config) {
409
407
  * 204:
410
408
  * description: 'Application Settings was successfully update'
411
409
  */
412
- app.post('/settings', (0, crowdin_client_1.default)({
410
+ app.post('/settings', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
413
411
  config,
414
412
  optional: false,
415
413
  checkSubscriptionExpiration: true,
@@ -445,7 +443,7 @@ function addDefaultApiEndpoints(app, config) {
445
443
  * - { $ref: '#/components/schemas/CrowdinSyncSettingsResponse' }
446
444
  * - { $ref: '#/components/schemas/IntegrationSyncSettingsResponse' }
447
445
  */
448
- app.get('/sync-settings', json_response_1.default, (0, crowdin_client_1.default)({
446
+ app.get('/sync-settings', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
449
447
  config,
450
448
  optional: false,
451
449
  checkSubscriptionExpiration: true,
@@ -468,7 +466,7 @@ function addDefaultApiEndpoints(app, config) {
468
466
  * 204:
469
467
  * description: 'Application Sync Settings was successfully update'
470
468
  */
471
- app.post('/sync-settings', json_response_1.default, (0, crowdin_client_1.default)({
469
+ app.post('/sync-settings', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
472
470
  config,
473
471
  optional: false,
474
472
  checkSubscriptionExpiration: true,
@@ -493,12 +491,7 @@ function addDefaultApiEndpoints(app, config) {
493
491
  * data:
494
492
  * $ref: '#/components/schemas/LoginFieldsResponse'
495
493
  */
496
- app.get('/login-fields', json_response_1.default, (0, crowdin_client_1.default)({
497
- config,
498
- optional: false,
499
- checkSubscriptionExpiration: true,
500
- moduleKey: 'login-data',
501
- }), (req, res) => {
494
+ app.get('/login-fields', api_call_1.default, json_response_1.default, (req, res) => {
502
495
  var _a, _b;
503
496
  let fields = [];
504
497
  if ((_b = (_a = config.projectIntegration) === null || _a === void 0 ? void 0 : _a.loginForm) === null || _b === void 0 ? void 0 : _b.fields) {
@@ -523,7 +516,7 @@ function addDefaultApiEndpoints(app, config) {
523
516
  * 204:
524
517
  * description: 'Login successful'
525
518
  */
526
- app.post('/login', (0, crowdin_client_1.default)({
519
+ app.post('/login', api_call_1.default, json_response_1.default, (0, crowdin_client_1.default)({
527
520
  config,
528
521
  optional: false,
529
522
  checkSubscriptionExpiration: false,
@@ -93,9 +93,6 @@
93
93
  * jobId:
94
94
  * type: string
95
95
  * example: '067da473-fc0b-43e3-b0a2-09d26af130c1'
96
- * message:
97
- * type: string
98
- * example: 'Another sync is running'
99
96
  * SettingsData:
100
97
  * type: object
101
98
  * example: {schedule: 0, condition: 0}
@@ -94,9 +94,6 @@
94
94
  * jobId:
95
95
  * type: string
96
96
  * example: '067da473-fc0b-43e3-b0a2-09d26af130c1'
97
- * message:
98
- * type: string
99
- * example: 'Another sync is running'
100
97
  * SettingsData:
101
98
  * type: object
102
99
  * example: {schedule: 0, condition: 0}
@@ -13,6 +13,13 @@ const util_1 = require("../../../util");
13
13
  const logger_1 = require("../../../util/logger");
14
14
  function handle(integration) {
15
15
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
16
+ if (req.isApiCall && !req.params.fileId && !req.body.fileId) {
17
+ return res.status(400).json({
18
+ error: {
19
+ message: 'Missing required parameter: fileId',
20
+ },
21
+ });
22
+ }
16
23
  const fileId = Number(req.params.fileId || req.body.fileId);
17
24
  req.logInfo(`Loading translation progress for file ${fileId}`);
18
25
  if (integration.getFileProgress) {
@@ -30,6 +30,13 @@ function handle(config, integration) {
30
30
  if (rootFolder) {
31
31
  req.logInfo(`Updating crowdin files under folder ${rootFolder.id}`);
32
32
  }
33
+ if (req.isApiCall && !req.body.files) {
34
+ return res.status(400).json({
35
+ error: {
36
+ message: 'Missing required parameter: files',
37
+ },
38
+ });
39
+ }
33
40
  // A request via API has a different structure
34
41
  if (((_b = config.api) === null || _b === void 0 ? void 0 : _b.default) && req.body.files) {
35
42
  req.body = req.body.files;
@@ -14,10 +14,35 @@ const util_1 = require("../../../util");
14
14
  const logger_1 = require("../../../util/logger");
15
15
  function handle(config, integration) {
16
16
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
17
- var _a;
17
+ var _a, _b;
18
18
  req.logInfo('Received integration login request');
19
19
  let credentials = req.body.credentials;
20
- if ((_a = integration.loginForm) === null || _a === void 0 ? void 0 : _a.performGetTokenRequest) {
20
+ if (req.isApiCall) {
21
+ if (!credentials) {
22
+ return res.status(400).json({
23
+ error: {
24
+ message: 'Missing required parameter: credentials',
25
+ },
26
+ });
27
+ }
28
+ if ((_a = integration.loginForm) === null || _a === void 0 ? void 0 : _a.fields) {
29
+ const missingFields = integration.loginForm.fields
30
+ .filter((field) => 'key' in field)
31
+ .filter((field) => {
32
+ const formField = field;
33
+ return formField.type !== 'checkbox' && !credentials[formField.key];
34
+ })
35
+ .map((field) => field.key);
36
+ if (missingFields.length > 0) {
37
+ return res.status(400).json({
38
+ error: {
39
+ message: `Missing required credential fields: ${missingFields.join(', ')}`,
40
+ },
41
+ });
42
+ }
43
+ }
44
+ }
45
+ if ((_b = integration.loginForm) === null || _b === void 0 ? void 0 : _b.performGetTokenRequest) {
21
46
  req.logInfo('Performing custom get bearer token request');
22
47
  const tokenCredentials = yield integration.loginForm.performGetTokenRequest(credentials);
23
48
  credentials = Object.assign(Object.assign({}, credentials), { expireIn: Number(tokenCredentials.expires_in) + Date.now() / 1000, accessToken: tokenCredentials.access_token, refreshToken: tokenCredentials.refresh_token });
@@ -17,16 +17,24 @@ const logger_1 = require("../../../util/logger");
17
17
  const files_1 = require("../util/files");
18
18
  function handle(config, integration) {
19
19
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
20
- var _a;
20
+ var _a, _b, _c;
21
21
  req.logInfo('Updating integration data');
22
22
  const client = req.crowdinApiClient;
23
23
  const projectId = req.crowdinContext.jwtPayload.context.project_id;
24
+ const forcePushTranslations = ((_a = req.query) === null || _a === void 0 ? void 0 : _a.forcePushTranslations) === 'true' || !!((_b = req.body) === null || _b === void 0 ? void 0 : _b.forcePushTranslations);
24
25
  const rootFolder = yield (0, defaults_1.getRootFolder)(config, integration, req.crowdinApiClient, projectId);
25
26
  if (rootFolder) {
26
27
  req.logInfo(`Updating integration data for crowding root folder ${rootFolder.id}`);
27
28
  }
29
+ if (req.isApiCall && !req.body.files) {
30
+ return res.status(400).json({
31
+ error: {
32
+ message: 'Missing required parameter: files',
33
+ },
34
+ });
35
+ }
28
36
  // A request via API has a different structure
29
- if (((_a = config.api) === null || _a === void 0 ? void 0 : _a.default) && req.body.files) {
37
+ if (((_c = config.api) === null || _c === void 0 ? void 0 : _c.default) && req.body.files) {
30
38
  req.body = req.body.files;
31
39
  }
32
40
  let payload = req.body;
@@ -77,6 +85,7 @@ function handle(config, integration) {
77
85
  });
78
86
  throw e;
79
87
  }),
88
+ forcePushTranslations,
80
89
  });
81
90
  }));
82
91
  }
@@ -1,4 +1,3 @@
1
1
  /// <reference types="qs" />
2
2
  import { Response } from 'express';
3
- import { Config } from '../../../types';
4
- export default function handle(config: Config): (req: import("../../../types").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;
3
+ export default function handle(): (req: import("../../../types").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;
@@ -12,22 +12,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const types_1 = require("../util/types");
13
13
  const util_1 = require("../../../util");
14
14
  const storage_1 = require("../../../storage");
15
- function handle(config) {
15
+ function handle() {
16
16
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
17
- var _a;
18
17
  const id = req.query.jobId || req.body.jobId;
19
- const isApi = (0, util_1.isApiRequest)(req, config);
20
- if (isApi && !((_a = req.body) === null || _a === void 0 ? void 0 : _a.projectId)) {
21
- res.send({
22
- error: 'Project id is require',
23
- });
24
- return;
25
- }
26
18
  if (!id) {
27
- req.logInfo('Job id is absent');
19
+ req.logInfo('Job id is missing');
28
20
  res.status(400).send('Job id is required');
29
21
  return;
30
22
  }
23
+ if (req.isApiCall) {
24
+ const job = yield (0, storage_1.getStorage)().getJob({ id });
25
+ if (!job) {
26
+ return res.status(404).json({
27
+ error: {
28
+ message: `Job with ID ${id} not found`,
29
+ },
30
+ });
31
+ }
32
+ }
31
33
  req.logInfo(`User has been canceled the job id: ${id}`);
32
34
  yield (0, storage_1.getStorage)().updateJob({ id, status: types_1.JobStatus.CANCELED });
33
35
  res.sendStatus(204);
@@ -15,6 +15,7 @@ const storage_1 = require("../../../storage");
15
15
  const cron_1 = require("../util/cron");
16
16
  const defaults_1 = require("../util/defaults");
17
17
  const connection_1 = require("../../../util/connection");
18
+ const logger_1 = require("../../../util/logger");
18
19
  const MINUTES = 60;
19
20
  function getHumanETA(ms) {
20
21
  const seconds = Math.floor(ms / 1000);
@@ -35,15 +36,8 @@ function getHumanETA(ms) {
35
36
  }
36
37
  function handle(config) {
37
38
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
38
- var _a;
39
+ const isApi = req.isApiCall;
39
40
  const id = req.query.jobId || req.body.jobId;
40
- const isApi = (0, util_1.isApiRequest)(req, config);
41
- if (isApi && !((_a = req.body) === null || _a === void 0 ? void 0 : _a.projectId)) {
42
- res.send({
43
- error: 'Project id is require',
44
- });
45
- return;
46
- }
47
41
  if (!id) {
48
42
  req.logInfo('Get active jobs');
49
43
  const jobs = yield (0, storage_1.getStorage)().getActiveJobs({
@@ -67,6 +61,13 @@ function handle(config) {
67
61
  }
68
62
  req.logInfo(`Get job info for id ${id}`);
69
63
  const job = yield (0, storage_1.getStorage)().getJob({ id });
64
+ if (isApi && !job) {
65
+ return res.status(404).json({
66
+ error: {
67
+ message: `Job with ID ${id} not found`,
68
+ },
69
+ });
70
+ }
70
71
  if (job && job.status === types_1.JobStatus.IN_PROGRESS && job.progress > 5 && job.updatedAt) {
71
72
  job.eta = ((Date.now() - job.createdAt) / job.progress) * (100 - job.progress);
72
73
  job.info = getHumanETA(job.eta) + (job.info ? `\n${job.info}` : '');
@@ -88,30 +89,47 @@ function handle(config) {
88
89
  const integration = config.projectIntegration;
89
90
  const crowdinId = req.crowdinContext.crowdinId;
90
91
  const integrationId = req.crowdinContext.clientId;
91
- const integrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(integrationId);
92
- const credentials = yield (0, connection_1.prepareIntegrationCredentials)(config, integration, integrationCredentials);
93
- const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(integrationId);
94
- const intConfig = (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config)
95
- ? JSON.parse(integrationConfig.config)
96
- : { schedule: '0', condition: '0' };
97
- const rootFolder = yield (0, defaults_1.getRootFolder)(config, integration, req.crowdinApiClient, projectId);
98
- req.logInfo(`Restarting the job after no updates for more than ${MINUTES} minutes.`);
99
- return (0, cron_1.runUpdateProviderJob)({
100
- integrationId,
101
- crowdinId,
102
- type: job.type,
103
- title: job.title,
104
- payload: JSON.parse(job.payload),
105
- jobType: types_1.JobClientType.RERUN,
106
- projectId,
107
- client: req.crowdinApiClient,
108
- integration,
109
- context,
110
- credentials,
111
- rootFolder,
112
- appSettings: intConfig,
113
- reRunJobId: id,
114
- });
92
+ try {
93
+ const integrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(integrationId);
94
+ const credentials = yield (0, connection_1.prepareIntegrationCredentials)(config, integration, integrationCredentials);
95
+ const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(integrationId);
96
+ const intConfig = (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config)
97
+ ? JSON.parse(integrationConfig.config)
98
+ : { schedule: '0', condition: '0' };
99
+ const rootFolder = yield (0, defaults_1.getRootFolder)(config, integration, req.crowdinApiClient, projectId);
100
+ req.logInfo(`Restarting the job after no updates for more than ${MINUTES} minutes.`);
101
+ return (0, cron_1.runUpdateProviderJob)({
102
+ integrationId,
103
+ crowdinId,
104
+ type: job.type,
105
+ title: job.title,
106
+ payload: JSON.parse(job.payload),
107
+ jobType: types_1.JobClientType.RERUN,
108
+ projectId,
109
+ client: req.crowdinApiClient,
110
+ integration,
111
+ context,
112
+ credentials,
113
+ rootFolder,
114
+ appSettings: intConfig,
115
+ reRunJobId: id,
116
+ });
117
+ }
118
+ catch (e) {
119
+ (0, logger_1.logError)(e);
120
+ yield (0, storage_1.getStorage)().updateJob({
121
+ id: job.id,
122
+ status: types_1.JobStatus.FAILED,
123
+ info: (0, logger_1.getErrorMessage)(e),
124
+ });
125
+ return res.status(500).json({
126
+ error: {
127
+ message: `Job was failed after attempt to restart it due to inactivity (no updates for ${MINUTES} minutes). Please try to restart the job manually.`,
128
+ jobId: job.id,
129
+ status: types_1.JobStatus.FAILED,
130
+ },
131
+ });
132
+ }
115
133
  }
116
134
  req.logInfo(`Returning job info ${JSON.stringify(job, null, 2)}`);
117
135
  res.send(job);
@@ -94,6 +94,7 @@ function handle(config, integration) {
94
94
  options.integrationSearchListener = integration.integrationSearchListener;
95
95
  options.checkSubscription = !(0, subscription_1.isAppFree)(config);
96
96
  options.uploadTranslations = integration.uploadTranslations;
97
+ options.forcePushTranslations = integration.forcePushTranslations;
97
98
  options.excludedTargetLanguages = integration.excludedTargetLanguages;
98
99
  options.sentryData = process.env.SENTRY_DSN
99
100
  ? {
@@ -18,6 +18,13 @@ const cron_1 = require("../util/cron");
18
18
  function handle(config, integration) {
19
19
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
20
20
  const appSettings = req.body.config;
21
+ if (req.isApiCall && !req.body.config) {
22
+ return res.status(400).json({
23
+ error: {
24
+ message: 'Missing required parameter: config',
25
+ },
26
+ });
27
+ }
21
28
  const clientId = req.crowdinContext.clientId;
22
29
  req.logInfo(`Saving settings ${JSON.stringify(appSettings, null, 2)}`);
23
30
  const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(clientId);
@@ -38,10 +45,10 @@ function handle(config, integration) {
38
45
  });
39
46
  }
40
47
  else {
41
- if (appSettings['new-crowdin-files']) {
48
+ if (appSettings === null || appSettings === void 0 ? void 0 : appSettings['new-crowdin-files']) {
42
49
  (0, snapshot_1.createOrUpdateFileSnapshot)(config, integration, req, types_1.Provider.CROWDIN);
43
50
  }
44
- if (appSettings['new-integration-files']) {
51
+ if (appSettings === null || appSettings === void 0 ? void 0 : appSettings['new-integration-files']) {
45
52
  (0, snapshot_1.createOrUpdateFileSnapshot)(config, integration, req, types_1.Provider.INTEGRATION);
46
53
  }
47
54
  }
@@ -19,6 +19,7 @@ const snapshot_1 = require("../util/snapshot");
19
19
  const files_1 = require("../util/files");
20
20
  const job_1 = require("../util/job");
21
21
  const types_1 = require("../util/types");
22
+ const types_2 = require("../types");
22
23
  function checkAutoSyncSettings(integration, appSettings, provider) {
23
24
  var _a;
24
25
  return !!(!integration.webhooks && ((_a = integration.syncNewElements) === null || _a === void 0 ? void 0 : _a[provider]) && appSettings[`new-${provider}-files`]);
@@ -26,6 +27,22 @@ function checkAutoSyncSettings(integration, appSettings, provider) {
26
27
  function handle(config, integration) {
27
28
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
28
29
  const { files, provider, expandIntegrationFolders } = req.body;
30
+ if (req.isApiCall) {
31
+ if (!files || !provider) {
32
+ return res.status(400).json({
33
+ error: {
34
+ message: `Missing required parameter: ${!files ? 'files' : 'provider'}`,
35
+ },
36
+ });
37
+ }
38
+ if (!Object.values(types_2.Provider).includes(provider)) {
39
+ return res.status(400).json({
40
+ error: {
41
+ message: `Invalid provider. Must be one of: ${Object.values(types_2.Provider).join(', ')}`,
42
+ },
43
+ });
44
+ }
45
+ }
29
46
  yield (0, job_1.runAsJob)({
30
47
  integrationId: req.crowdinContext.clientId,
31
48
  crowdinId: req.crowdinContext.crowdinId,
@@ -15,6 +15,13 @@ function handle() {
15
15
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
16
16
  let files = {};
17
17
  const provider = req.params.provider || req.body.provider;
18
+ if (req.isApiCall && !provider) {
19
+ return res.status(400).json({
20
+ error: {
21
+ message: 'Missing required parameter: provider',
22
+ },
23
+ });
24
+ }
18
25
  req.logInfo(`Loading sync settings for provider ${provider}`);
19
26
  const syncSettings = yield (0, storage_1.getStorage)().getSyncSettingsByProvider(req.crowdinContext.clientId, provider);
20
27
  if (syncSettings) {
@@ -99,7 +99,7 @@ function register({ config, app }) {
99
99
  optional: false,
100
100
  checkSubscriptionExpiration: true,
101
101
  moduleKey: integrationLogic.key,
102
- }), (0, job_cancel_1.default)(config));
102
+ }), (0, job_cancel_1.default)());
103
103
  app.post('/api/settings', (0, crowdin_client_1.default)({
104
104
  config,
105
105
  optional: false,
@@ -154,6 +154,10 @@ export interface IntegrationLogic extends ModuleKey {
154
154
  * Enable the option to upload translations to crowdin that are already present in the integration.
155
155
  */
156
156
  uploadTranslations?: boolean;
157
+ /**
158
+ * Force sync translations from Crowdin to integration regardless of etag.
159
+ */
160
+ forcePushTranslations?: boolean;
157
161
  /**
158
162
  * Enable the option to upload file for translation into selected languages.
159
163
  */
@@ -146,7 +146,7 @@ function applyIntegrationModuleDefaults(config, integration) {
146
146
  }
147
147
  const getUserSettings = integration.getConfiguration;
148
148
  integration.getConfiguration = (projectId, crowdinClient, integrationCredentials) => __awaiter(this, void 0, void 0, function* () {
149
- var _m, _o;
149
+ var _m, _o, _p;
150
150
  let fields = [];
151
151
  const project = (yield crowdinClient.projectsGroupsApi.getProject(projectId));
152
152
  if (getUserSettings) {
@@ -163,42 +163,49 @@ function applyIntegrationModuleDefaults(config, integration) {
163
163
  });
164
164
  }
165
165
  if (integration.withCronSync || integration.webhooks) {
166
- defaultSettings.push({
167
- key: 'schedule',
168
- label: 'Sync schedule',
169
- helpText: `Defines how often content is synced between ${config.name} and Crowdin. Make sure Auto Sync is enabled for selected directories and files in the dual pane view.`,
170
- type: 'select',
171
- defaultValue: '0',
172
- category: types_1.DefaultCategory.SYNC,
173
- position: 0,
174
- options: [
175
- {
176
- value: '0',
177
- label: 'Disabled',
178
- },
179
- {
180
- value: '1',
181
- label: '1 hour',
182
- },
183
- {
184
- value: '3',
185
- label: '3 hours',
186
- },
187
- {
188
- value: '6',
189
- label: '6 hours',
190
- },
191
- {
192
- value: '12',
193
- label: '12 hours',
194
- },
195
- {
196
- value: '24',
197
- label: '24 hours',
198
- },
199
- ],
200
- });
201
- if ((_m = integration.syncNewElements) === null || _m === void 0 ? void 0 : _m.crowdin) {
166
+ const userSchedule = fields.find((field) => 'key' in field && field.key === 'schedule');
167
+ if (userSchedule) {
168
+ userSchedule.position = (_m = userSchedule.position) !== null && _m !== void 0 ? _m : 0;
169
+ userSchedule.category = types_1.DefaultCategory.SYNC;
170
+ }
171
+ else {
172
+ defaultSettings.push({
173
+ key: 'schedule',
174
+ label: 'Sync schedule',
175
+ helpText: `Defines how often content is synced between ${config.name} and Crowdin. Make sure Auto Sync is enabled for selected directories and files in the dual pane view.`,
176
+ type: 'select',
177
+ defaultValue: '0',
178
+ category: types_1.DefaultCategory.SYNC,
179
+ position: 0,
180
+ options: [
181
+ {
182
+ value: '0',
183
+ label: 'Disabled',
184
+ },
185
+ {
186
+ value: '1',
187
+ label: '1 hour',
188
+ },
189
+ {
190
+ value: '3',
191
+ label: '3 hours',
192
+ },
193
+ {
194
+ value: '6',
195
+ label: '6 hours',
196
+ },
197
+ {
198
+ value: '12',
199
+ label: '12 hours',
200
+ },
201
+ {
202
+ value: '24',
203
+ label: '24 hours',
204
+ },
205
+ ],
206
+ });
207
+ }
208
+ if ((_o = integration.syncNewElements) === null || _o === void 0 ? void 0 : _o.crowdin) {
202
209
  defaultSettings.push({
203
210
  key: 'new-crowdin-files',
204
211
  label: 'Automatically sync new translations from Crowdin',
@@ -208,7 +215,7 @@ function applyIntegrationModuleDefaults(config, integration) {
208
215
  position: 1,
209
216
  });
210
217
  }
211
- if ((_o = integration.syncNewElements) === null || _o === void 0 ? void 0 : _o.integration) {
218
+ if ((_p = integration.syncNewElements) === null || _p === void 0 ? void 0 : _p.integration) {
212
219
  defaultSettings.push({
213
220
  key: 'new-integration-files',
214
221
  label: `Automatically sync new content from ${config.name}`,
@@ -293,7 +300,7 @@ function applyIntegrationModuleDefaults(config, integration) {
293
300
  if (!((_b = integration.filtering) === null || _b === void 0 ? void 0 : _b.hasOwnProperty('crowdinLanguages'))) {
294
301
  integration.filtering = Object.assign(Object.assign({}, (integration.filtering || {})), { crowdinLanguages: true });
295
302
  }
296
- integration.filtering.integrationFileStatus = Object.assign({ notSynced: true }, integration.filtering.integrationFileStatus);
303
+ integration.filtering.integrationFileStatus = Object.assign(Object.assign({}, (integration.integrationOneLevelFetching ? {} : { notSynced: true })), integration.filtering.integrationFileStatus);
297
304
  if (!((_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.hasOwnProperty('integrationFilterConfig'))) {
298
305
  const filterItems = [
299
306
  {
@@ -2,7 +2,7 @@ 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, jobStoreType, jobCallback, onError, reRunJobId, }: {
5
+ export declare function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobStoreType, jobCallback, onError, reRunJobId, forcePushTranslations, }: {
6
6
  integrationId: string;
7
7
  crowdinId: string;
8
8
  type: JobType;
@@ -16,5 +16,6 @@ export declare function runAsJob({ integrationId, crowdinId, type, title, payloa
16
16
  jobCallback: (arg1: JobClient) => Promise<any>;
17
17
  onError?: (e: any) => Promise<void>;
18
18
  reRunJobId?: string;
19
+ forcePushTranslations?: boolean;
19
20
  }): Promise<void>;
20
21
  export declare function reRunInProgressJobs(config: Config): Promise<void>;
@@ -48,7 +48,7 @@ const blockingJobs = {
48
48
  };
49
49
  const maxAttempts = 3;
50
50
  const store = {};
51
- function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobStoreType, jobCallback, onError, reRunJobId, }) {
51
+ function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobStoreType, jobCallback, onError, reRunJobId, forcePushTranslations, }) {
52
52
  return __awaiter(this, void 0, void 0, function* () {
53
53
  let jobId;
54
54
  const storage = (0, storage_1.getStorage)();
@@ -141,7 +141,7 @@ function runAsJob({ integrationId, crowdinId, type, title, payload, res, project
141
141
  (0, logger_1.log)(`Receiving translation for file ${fileId} in language ${languageId}`);
142
142
  translation = yield client.translationsApi.buildProjectFileTranslation(projectId, fileId, {
143
143
  targetLanguageId: languageId,
144
- }, (translationCache === null || translationCache === void 0 ? void 0 : translationCache.etag) ? translationCache === null || translationCache === void 0 ? void 0 : translationCache.etag : undefined);
144
+ }, (translationCache === null || translationCache === void 0 ? void 0 : translationCache.etag) && !forcePushTranslations ? translationCache === null || translationCache === void 0 ? void 0 : translationCache.etag : undefined);
145
145
  return translation;
146
146
  }
147
147
  catch (e) {
@@ -69,7 +69,16 @@ function webhookHandler(config, webhooks) {
69
69
  const json = JSON.parse(req.body.toString());
70
70
  for (const webhook of webhooks) {
71
71
  if (webhook.key === moduleKey) {
72
- yield webhook.callback({ events: json.events, client });
72
+ yield webhook.callback({
73
+ events: json.events,
74
+ client,
75
+ webhookContext: {
76
+ domain: credentials.domain,
77
+ organizationId: credentials.organizationId,
78
+ userId: credentials.userId,
79
+ agentId: credentials.agentId,
80
+ },
81
+ });
73
82
  }
74
83
  }
75
84
  }));
@@ -9,10 +9,17 @@ export interface Webhook extends ModuleKey {
9
9
  * handle function
10
10
  */
11
11
  callback: (data: {
12
+ webhookContext: WebhookContext;
12
13
  events: Event[];
13
14
  client: Crowdin;
14
15
  }) => Promise<void>;
15
16
  }
17
+ interface WebhookContext {
18
+ domain?: string;
19
+ organizationId?: number;
20
+ userId?: number;
21
+ agentId?: number;
22
+ }
16
23
  interface EventPayload {
17
24
  event: string;
18
25
  }
@@ -30,8 +30,8 @@ export interface Storage {
30
30
  getAllIntegrationCredentials(crowdinId: string): Promise<IntegrationCredentials[]>;
31
31
  deleteIntegrationCredentials(id: string): Promise<void>;
32
32
  deleteAllIntegrationCredentials(crowdinId: string): Promise<void>;
33
- saveMetadata(id: string, metadata: any, crowdinId?: string): Promise<void>;
34
- updateMetadata(id: string, metadata: any, crowdinId?: string): Promise<void>;
33
+ saveMetadata(id: string, metadata: any, crowdinId: string): Promise<void>;
34
+ updateMetadata(id: string, metadata: any, crowdinId: string): Promise<void>;
35
35
  getMetadata(id: string): Promise<any | undefined>;
36
36
  getAllMetadata(): Promise<any[] | undefined>;
37
37
  deleteMetadata(id: string): Promise<void>;
@@ -17,6 +17,11 @@ export declare class MySQLStorage implements Storage {
17
17
  private _rej?;
18
18
  private dbPromise;
19
19
  private config;
20
+ tableIndexes: {
21
+ [key: string]: {
22
+ [key: string]: string;
23
+ };
24
+ };
20
25
  tables: {
21
26
  crowdin_credentials: string;
22
27
  integration_credentials: string;
@@ -48,7 +53,7 @@ export declare class MySQLStorage implements Storage {
48
53
  deleteIntegrationCredentials(id: string): Promise<void>;
49
54
  deleteAllIntegrationCredentials(crowdinId: string): Promise<void>;
50
55
  saveMetadata(id: string, metadata: any, crowdinId: string): Promise<void>;
51
- updateMetadata(id: string, metadata: any, crowdinId?: string): Promise<void>;
56
+ updateMetadata(id: string, metadata: any, crowdinId: string): Promise<void>;
52
57
  getMetadata(id: string): Promise<any>;
53
58
  getAllMetadata(): Promise<any[]>;
54
59
  deleteMetadata(id: string): Promise<void>;
@@ -24,6 +24,46 @@ class MySQLStorage {
24
24
  this._res = res;
25
25
  this._rej = rej;
26
26
  });
27
+ this.tableIndexes = {
28
+ integration_credentials: {
29
+ idx_crowdin: '(crowdin_id)',
30
+ },
31
+ sync_settings: {
32
+ idx_integration_provider: '(integration_id, provider)',
33
+ idx_crowdin: '(crowdin_id)',
34
+ idx_type: '(type)',
35
+ },
36
+ files_snapshot: {
37
+ idx_integration: '(integration_id)',
38
+ idx_crowdin: '(crowdin_id)',
39
+ },
40
+ webhooks: {
41
+ idx_integration_crowdin: '(integration_id, crowdin_id)',
42
+ idx_file_provider: '(file_id, provider)',
43
+ },
44
+ user_errors: {
45
+ idx_integration_crowdin: '(integration_id, crowdin_id)',
46
+ idx_crowdin: '(crowdin_id)',
47
+ idx_created_at: '(created_at)',
48
+ },
49
+ integration_settings: {
50
+ idx_integration: '(integration_id)',
51
+ idx_crowdin: '(crowdin_id)',
52
+ },
53
+ job: {
54
+ idx_integration_crowdin: '(integration_id, crowdin_id)',
55
+ idx_finished_at: '(finished_at)',
56
+ idx_status: '(status)',
57
+ },
58
+ translation_file_cache: {
59
+ idx_integration_crowdin_file_language: '(integration_id, crowdin_id, file_id, language_id)',
60
+ idx_crowdin: '(crowdin_id)',
61
+ },
62
+ unsynced_files: {
63
+ idx_integration_crowdin: '(integration_id, crowdin_id)',
64
+ idx_crowdin: '(crowdin_id)',
65
+ },
66
+ };
27
67
  this.tables = {
28
68
  crowdin_credentials: `(
29
69
  id varchar(255) primary key,
@@ -168,6 +208,14 @@ class MySQLStorage {
168
208
  return __awaiter(this, void 0, void 0, function* () {
169
209
  for (const [tableName, tableSchema] of Object.entries(this.tables)) {
170
210
  yield connection.execute(`create table if not exists ${tableName} ${tableSchema}`);
211
+ if (this.tableIndexes[tableName]) {
212
+ for (const [indexName, indexSchema] of Object.entries(this.tableIndexes[tableName])) {
213
+ // For MySQL, we need to handle partial indexes differently since MySQL doesn't support WHERE clauses in indexes
214
+ // We'll create the basic index without the WHERE clause
215
+ const indexColumns = indexSchema.replace(/WHERE.*$/, '').trim();
216
+ yield connection.execute(`create index if not exists ${indexName} on ${tableName}${indexColumns}`);
217
+ }
218
+ }
171
219
  }
172
220
  });
173
221
  }
@@ -62,7 +62,7 @@ export declare class PostgreStorage implements Storage {
62
62
  deleteIntegrationCredentials(id: string): Promise<void>;
63
63
  deleteAllIntegrationCredentials(crowdinId: string): Promise<void>;
64
64
  saveMetadata(id: string, metadata: any, crowdinId: string): Promise<void>;
65
- updateMetadata(id: string, metadata: any, crowdinId?: string): Promise<void>;
65
+ updateMetadata(id: string, metadata: any, crowdinId: string): Promise<void>;
66
66
  getMetadata(id: string): Promise<any>;
67
67
  getAllMetadata(): Promise<any[]>;
68
68
  deleteMetadata(id: string): Promise<void>;
@@ -197,7 +197,7 @@ class PostgreStorage {
197
197
  migrate() {
198
198
  return __awaiter(this, void 0, void 0, function* () {
199
199
  try {
200
- if (this.directoryPath && fs_1.default.existsSync(this.directoryPath)) {
200
+ if (this.directoryPath && fs_1.default.existsSync(this.directoryPath + '/' + types_1.storageFiles.SQLITE)) {
201
201
  yield this.migrateFromSqlite(this.directoryPath);
202
202
  }
203
203
  yield this.executeQuery((client) => this.addTables(client));
@@ -50,8 +50,8 @@ export declare class SQLiteStorage implements Storage {
50
50
  getAllIntegrationCredentials(crowdinId: string): Promise<IntegrationCredentials[]>;
51
51
  deleteIntegrationCredentials(id: string): Promise<void>;
52
52
  deleteAllIntegrationCredentials(crowdinId: string): Promise<void>;
53
- saveMetadata(id: string, metadata: any, crowdinId?: string): Promise<void>;
54
- updateMetadata(id: string, metadata: any, crowdinId?: string): Promise<void>;
53
+ saveMetadata(id: string, metadata: any, crowdinId: string): Promise<void>;
54
+ updateMetadata(id: string, metadata: any, crowdinId: string): Promise<void>;
55
55
  getMetadata(id: string): Promise<any>;
56
56
  getAllMetadata(): Promise<any[]>;
57
57
  deleteMetadata(id: string): Promise<void>;
package/out/types.d.ts CHANGED
@@ -328,6 +328,7 @@ export interface CrowdinClientRequest extends Request {
328
328
  subscriptionInfo?: SubscriptionInfo;
329
329
  logInfo: LogFunction;
330
330
  logError: LogErrorFunction;
331
+ isApiCall?: boolean;
331
332
  }
332
333
  export interface CrowdinCredentials {
333
334
  id: string;
@@ -393,7 +394,7 @@ export interface CrowdinAppUtilities extends CrowdinMetadataStore {
393
394
  storage: Storage;
394
395
  }
395
396
  export interface CrowdinMetadataStore {
396
- saveMetadata: (id: string, metadata: any, crowdinId?: string) => Promise<void>;
397
+ saveMetadata: (id: string, metadata: any, crowdinId: string) => Promise<void>;
397
398
  getMetadata: (id: string) => Promise<any | undefined>;
398
399
  deleteMetadata: (id: string) => Promise<void>;
399
400
  /**
@@ -14,7 +14,6 @@ export declare function isJson(string: string): boolean;
14
14
  export declare function getPreviousDate(days: number): Date;
15
15
  export declare function prepareFormDataMetadataId(req: CrowdinClientRequest, config: Config): Promise<string>;
16
16
  export declare function validateEmail(email: string | number): boolean;
17
- export declare function isApiRequest(req: Request, config: Config): boolean;
18
17
  export declare function getFormattedDate({ date, userTimezone }: {
19
18
  date: Date | number;
20
19
  userTimezone: string | undefined;
package/out/util/index.js CHANGED
@@ -32,7 +32,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
32
32
  });
33
33
  };
34
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.getFormattedDate = exports.isApiRequest = exports.validateEmail = exports.prepareFormDataMetadataId = exports.getPreviousDate = exports.isJson = exports.isAuthorizedConfig = exports.getLogoUrl = exports.executeWithRetry = exports.decryptData = exports.encryptData = exports.runAsyncWrapper = exports.CodeError = void 0;
35
+ exports.getFormattedDate = exports.validateEmail = exports.prepareFormDataMetadataId = exports.getPreviousDate = exports.isJson = exports.isAuthorizedConfig = exports.getLogoUrl = exports.executeWithRetry = exports.decryptData = exports.encryptData = exports.runAsyncWrapper = exports.CodeError = void 0;
36
36
  const crypto = __importStar(require("crypto-js"));
37
37
  const storage_1 = require("../storage");
38
38
  const types_1 = require("../types");
@@ -162,11 +162,6 @@ function validateEmail(email) {
162
162
  return emailRegExp.test(String(email).toLowerCase());
163
163
  }
164
164
  exports.validateEmail = validateEmail;
165
- function isApiRequest(req, config) {
166
- const origin = req.headers['origin'] || req.headers['referer'];
167
- return !origin || !origin.includes(config.baseUrl);
168
- }
169
- exports.isApiRequest = isApiRequest;
170
165
  // Format the date as 'MMM DD, YYYY HH:mm'
171
166
  function getFormattedDate({ date, userTimezone }) {
172
167
  if (!userTimezone) {
@@ -89,6 +89,9 @@
89
89
  integration-button-menu-items='[{"label":"Select target languages", "title":"Upload file for translation into selected languages", "action":"excludedTargetLanguages"}]'
90
90
  {{/if}}
91
91
  {{/if}}
92
+ {{#if forcePushTranslations}}
93
+ crowdin-button-menu-items='[{"label":"Force Sync Translations", "action":"forcePushTranslations"}]'
94
+ {{/if}}
92
95
  {{#if filtering.crowdinLanguages}}
93
96
  crowdin-filter
94
97
  {{/if}}
@@ -862,18 +865,28 @@
862
865
  }
863
866
  }
864
867
 
865
- function uploadFilesToIntegration(e) {
866
- if (Object.keys(e.detail).length === 0) {
868
+ function uploadFilesToIntegration(event) {
869
+ let files = {};
870
+ let forcePushTranslations = false;
871
+ if (event.detail?.action === 'forcePushTranslations') {
872
+ files = event.detail.files;
873
+ forcePushTranslations = true;
874
+ } else {
875
+ files = event.detail;
876
+ }
877
+
878
+ if (Object.keys(event.detail).length === 0 || !event.detail) {
867
879
  showToast('Select files which will be uploaded to {{name}}');
868
880
  return;
869
881
  }
882
+
870
883
  appComponent.setAttribute('is-to-integration-process', true);
871
884
  const req = {};
872
- Object.keys(e.detail)
885
+ Object.keys(files)
873
886
  .filter(id => crowdinData.find(c => c.id === id).node_type === fileType)
874
- .forEach(id => (req[id] = e.detail[id]));
887
+ .forEach(id => (req[id] = files[id]));
875
888
  checkOrigin()
876
- .then(restParams => fetch('api/integration/update' + restParams, {
889
+ .then(restParams => fetch(`api/integration/update${restParams}&forcePushTranslations=${forcePushTranslations}`, {
877
890
  method: 'POST',
878
891
  headers: { 'Content-Type': 'application/json' },
879
892
  body: JSON.stringify(req)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.88.1",
3
+ "version": "0.90.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",