@crowdin/app-project-module 0.88.0 → 0.89.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 (31) hide show
  1. package/out/middlewares/api-call.d.ts +2 -0
  2. package/out/middlewares/api-call.js +7 -0
  3. package/out/middlewares/crowdin-client.js +8 -1
  4. package/out/middlewares/integration-credentials.js +19 -1
  5. package/out/modules/api/api.js +19 -26
  6. package/out/modules/api/components.d.ts +0 -3
  7. package/out/modules/api/components.js +0 -3
  8. package/out/modules/integration/handlers/crowdin-file-progress.js +7 -0
  9. package/out/modules/integration/handlers/crowdin-update.js +7 -0
  10. package/out/modules/integration/handlers/integration-login.js +27 -2
  11. package/out/modules/integration/handlers/integration-update.js +11 -2
  12. package/out/modules/integration/handlers/job-cancel.d.ts +1 -2
  13. package/out/modules/integration/handlers/job-cancel.js +12 -10
  14. package/out/modules/integration/handlers/job-info.js +50 -32
  15. package/out/modules/integration/handlers/main.js +1 -0
  16. package/out/modules/integration/handlers/settings-save.js +9 -2
  17. package/out/modules/integration/handlers/sync-settings-save.js +17 -0
  18. package/out/modules/integration/handlers/sync-settings.js +7 -0
  19. package/out/modules/integration/index.js +1 -1
  20. package/out/modules/integration/types.d.ts +4 -0
  21. package/out/modules/integration/util/defaults.js +1 -1
  22. package/out/modules/integration/util/job.d.ts +2 -1
  23. package/out/modules/integration/util/job.js +2 -2
  24. package/out/modules/webhooks/handlers/webhook-handler.js +1 -1
  25. package/out/modules/webhooks/types.d.ts +1 -2
  26. package/out/storage/postgre.js +1 -1
  27. package/out/types.d.ts +1 -0
  28. package/out/util/index.d.ts +0 -1
  29. package/out/util/index.js +1 -6
  30. package/out/views/main.handlebars +18 -6
  31. package/package.json +1 -1
@@ -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
  */
@@ -293,7 +293,7 @@ function applyIntegrationModuleDefaults(config, integration) {
293
293
  if (!((_b = integration.filtering) === null || _b === void 0 ? void 0 : _b.hasOwnProperty('crowdinLanguages'))) {
294
294
  integration.filtering = Object.assign(Object.assign({}, (integration.filtering || {})), { crowdinLanguages: true });
295
295
  }
296
- integration.filtering.integrationFileStatus = Object.assign({ notSynced: true }, integration.filtering.integrationFileStatus);
296
+ integration.filtering.integrationFileStatus = Object.assign(Object.assign({}, (integration.integrationOneLevelFetching ? {} : { notSynced: true })), integration.filtering.integrationFileStatus);
297
297
  if (!((_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.hasOwnProperty('integrationFilterConfig'))) {
298
298
  const filterItems = [
299
299
  {
@@ -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,7 @@ 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({ credentials, events: json.events, client });
72
+ yield webhook.callback({ events: json.events, client });
73
73
  }
74
74
  }
75
75
  }));
@@ -1,4 +1,4 @@
1
- import { CrowdinCredentials, ModuleKey } from '../../types';
1
+ import { ModuleKey } from '../../types';
2
2
  import Crowdin from '@crowdin/crowdin-api-client';
3
3
  export interface Webhook extends ModuleKey {
4
4
  /**
@@ -9,7 +9,6 @@ export interface Webhook extends ModuleKey {
9
9
  * handle function
10
10
  */
11
11
  callback: (data: {
12
- credentials: CrowdinCredentials;
13
12
  events: Event[];
14
13
  client: Crowdin;
15
14
  }) => 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));
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;
@@ -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}}
@@ -249,7 +252,6 @@
249
252
  <crowdin-modal
250
253
  style="display: none;"
251
254
  id="settings-modal"
252
- body-overflow-unset="{{#checkLength configurationFields 3}}false{{else}}true{{/checkLength}}"
253
255
  modal-width="65"
254
256
  modal-title="Settings"
255
257
  close-button-title="Close"
@@ -863,18 +865,28 @@
863
865
  }
864
866
  }
865
867
 
866
- function uploadFilesToIntegration(e) {
867
- 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) {
868
879
  showToast('Select files which will be uploaded to {{name}}');
869
880
  return;
870
881
  }
882
+
871
883
  appComponent.setAttribute('is-to-integration-process', true);
872
884
  const req = {};
873
- Object.keys(e.detail)
885
+ Object.keys(files)
874
886
  .filter(id => crowdinData.find(c => c.id === id).node_type === fileType)
875
- .forEach(id => (req[id] = e.detail[id]));
887
+ .forEach(id => (req[id] = files[id]));
876
888
  checkOrigin()
877
- .then(restParams => fetch('api/integration/update' + restParams, {
889
+ .then(restParams => fetch(`api/integration/update${restParams}&forcePushTranslations=${forcePushTranslations}`, {
878
890
  method: 'POST',
879
891
  headers: { 'Content-Type': 'application/json' },
880
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.0",
3
+ "version": "0.89.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",