@crowdin/app-project-module 0.64.0 → 0.65.1

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.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Express } from 'express';
2
2
  import { ClientConfig, Config, CrowdinAppUtilities, CrowdinMetadataStore } from './types';
3
3
  import express from './util/terminus-express';
4
+ import { getRequestCredentialsMasker, postRequestCredentialsMasker } from './util/credentials-masker';
5
+ export { getRequestCredentialsMasker, postRequestCredentialsMasker };
4
6
  export { ProjectPermissions, Scope, UserPermissions } from './types';
5
7
  export { express };
6
8
  export declare const metadataStore: CrowdinMetadataStore;
package/out/index.js CHANGED
@@ -35,7 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.addCrowdinEndpoints = exports.createApp = exports.metadataStore = exports.express = exports.UserPermissions = exports.Scope = exports.ProjectPermissions = void 0;
38
+ exports.addCrowdinEndpoints = exports.createApp = exports.metadataStore = exports.express = exports.UserPermissions = exports.Scope = exports.ProjectPermissions = exports.postRequestCredentialsMasker = exports.getRequestCredentialsMasker = void 0;
39
39
  const logsFormatter = __importStar(require("@crowdin/logs-formatter"));
40
40
  const path_1 = require("path");
41
41
  const crowdin_client_1 = __importStar(require("./middlewares/crowdin-client"));
@@ -57,6 +57,8 @@ const logger_1 = require("./util/logger");
57
57
  const terminus_express_1 = __importDefault(require("./util/terminus-express"));
58
58
  exports.express = terminus_express_1.default;
59
59
  const credentials_masker_1 = require("./util/credentials-masker");
60
+ Object.defineProperty(exports, "getRequestCredentialsMasker", { enumerable: true, get: function () { return credentials_masker_1.getRequestCredentialsMasker; } });
61
+ Object.defineProperty(exports, "postRequestCredentialsMasker", { enumerable: true, get: function () { return credentials_masker_1.postRequestCredentialsMasker; } });
60
62
  //apps
61
63
  const apiApp = __importStar(require("./modules/api"));
62
64
  const contextMenuApp = __importStar(require("./modules/context-menu"));
@@ -109,7 +111,7 @@ function createApp(clientConfig) {
109
111
  const config = convertClientConfig(clientConfig);
110
112
  addCrowdinEndpoints(app, config);
111
113
  /* eslint no-console: "off" */
112
- app.listen(config.port, () => console.log(`App started on port ${config.port}`));
114
+ app.listen(config, () => console.log(`App started on port ${config.port}`));
113
115
  }
114
116
  exports.createApp = createApp;
115
117
  function addCrowdinEndpoints(app, clientConfig) {
@@ -15,8 +15,15 @@ function handle() {
15
15
  const projectId = req.crowdinContext.jwtPayload.context.project_id;
16
16
  req.logInfo(`Loading crowdin project ${projectId}`);
17
17
  const project = yield req.crowdinApiClient.projectsGroupsApi.getProject(projectId);
18
+ let projectEditorLink;
19
+ if (req.crowdinContext.jwtPayload.domain) {
20
+ projectEditorLink = `https://${req.crowdinContext.jwtPayload.domain}.crowdin.com/editor/${project.data.id}`;
21
+ }
22
+ else {
23
+ projectEditorLink = `https://crowdin.com/editor/${project.data.identifier}`;
24
+ }
18
25
  req.logInfo(`Loaded crowdin project ${projectId}`);
19
- res.send(project.data);
26
+ res.send(Object.assign(Object.assign({}, project.data), { projectEditorLink }));
20
27
  }));
21
28
  }
22
29
  exports.default = handle;
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const storage_1 = require("../../../storage");
13
13
  const util_1 = require("../../../util");
14
+ const logger_1 = require("../../../util/logger");
14
15
  function handle() {
15
16
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
16
17
  var _a;
@@ -20,6 +21,12 @@ function handle() {
20
21
  }
21
22
  req.logInfo(`Loading user errors for crowdinId ${req.crowdinContext.crowdinId} and integrationId ${req.crowdinContext.clientId}`);
22
23
  const userErrors = (_a = (yield (0, storage_1.getStorage)().getAllUserErrors(req.crowdinContext.crowdinId, req.crowdinContext.clientId))) === null || _a === void 0 ? void 0 : _a.map((userError) => {
24
+ if (!(0, util_1.isJson)(userError.data)) {
25
+ (0, logger_1.logError)(userError);
26
+ userError.data = JSON.stringify({
27
+ appData: { Warning: 'Invalid response data. Please contact the Crowdin support manager.' },
28
+ });
29
+ }
23
30
  const date = new Date(+userError.createdAt);
24
31
  // Format the date as 'MMM DD, YYYY HH:mm'
25
32
  const formattedDate = new Intl.DateTimeFormat('en-US', {
@@ -22,6 +22,15 @@ var __importStar = (this && this.__importStar) || function (mod) {
22
22
  __setModuleDefault(result, mod);
23
23
  return result;
24
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
+ };
25
34
  var __importDefault = (this && this.__importDefault) || function (mod) {
26
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
36
  };
@@ -56,6 +65,7 @@ const sync_settings_1 = __importDefault(require("./handlers/sync-settings"));
56
65
  const sync_settings_save_1 = __importDefault(require("./handlers/sync-settings-save"));
57
66
  const user_errors_1 = __importDefault(require("./handlers/user-errors"));
58
67
  const cron_1 = require("./util/cron");
68
+ const storage_1 = require("../../storage");
59
69
  function register({ config, app }) {
60
70
  var _a, _b, _c;
61
71
  const integrationLogic = config.projectIntegration;
@@ -96,8 +106,12 @@ function register({ config, app }) {
96
106
  cron.schedule('0 */3 * * *', () => (0, cron_1.filesCron)({ config, integration: integrationLogic, period: '3' }).catch(console.error));
97
107
  cron.schedule('0 */6 * * *', () => (0, cron_1.filesCron)({ config, integration: integrationLogic, period: '6' }).catch(console.error));
98
108
  cron.schedule('0 */12 * * *', () => (0, cron_1.filesCron)({ config, integration: integrationLogic, period: '12' }).catch(console.error));
99
- cron.schedule('0 0 * * *', () => (0, cron_1.filesCron)({ config, integration: integrationLogic, period: '24' }).catch(console.error));
100
109
  }
110
+ // remove user errors
111
+ cron.schedule('0 0 * * *', () => __awaiter(this, void 0, void 0, function* () {
112
+ const date = (0, util_1.getPreviousDate)(integrationLogic.userErrorLifetimeDays);
113
+ yield (0, storage_1.getStorage)().deleteAllUsersErrorsOlderThan(`${date.getTime()}`);
114
+ }));
101
115
  if (integrationLogic.webhooks) {
102
116
  app.post(`${integrationLogic.webhooks.crowdinWebhookUrl
103
117
  ? integrationLogic.webhooks.crowdinWebhookUrl
@@ -142,6 +142,10 @@ export interface IntegrationLogic {
142
142
  asyncProgress?: {
143
143
  checkInterval?: number;
144
144
  };
145
+ /**
146
+ * The duration for storing user errors, default is 30 days.
147
+ */
148
+ userErrorLifetimeDays: number;
145
149
  }
146
150
  export interface LoginForm {
147
151
  fields: FormEntity[];
@@ -1,11 +1,28 @@
1
- import Crowdin from '@crowdin/crowdin-api-client';
2
- import { Config } from '../../../types';
1
+ import Crowdin, { SourceFilesModel } from '@crowdin/crowdin-api-client';
2
+ import { Config, CrowdinContextInfo } from '../../../types';
3
3
  import { CronJob, IntegrationLogic, IntegrationRequest, Provider, UpdateIntegrationRequest } from '../types';
4
+ import { JobClientType, JobType } from './types';
4
5
  export declare function runJob({ config, integration, job, }: {
5
6
  config: Config;
6
7
  integration: IntegrationLogic;
7
8
  job: CronJob;
8
9
  }): Promise<void>;
10
+ export declare function runUpdateProviderJob({ integrationId, crowdinId, type, title, payload, jobType, projectId, client, integration, context, credentials, rootFolder, appSettings, reRunJobId, }: {
11
+ integrationId: string;
12
+ crowdinId: string;
13
+ type: JobType;
14
+ title?: string;
15
+ payload?: any;
16
+ jobType: JobClientType;
17
+ projectId: number;
18
+ client: Crowdin;
19
+ integration: IntegrationLogic;
20
+ context: CrowdinContextInfo;
21
+ credentials: any;
22
+ rootFolder?: SourceFilesModel.Directory;
23
+ appSettings?: any;
24
+ reRunJobId?: string;
25
+ }): Promise<void>;
9
26
  export declare function filesCron({ config, integration, period, }: {
10
27
  config: Config;
11
28
  integration: IntegrationLogic;
@@ -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.removeFinishedJobs = exports.createOrUpdateSyncSettings = exports.skipFoldersFromIntegrationRequest = exports.filesCron = exports.runJob = void 0;
35
+ exports.removeFinishedJobs = exports.createOrUpdateSyncSettings = exports.skipFoldersFromIntegrationRequest = exports.filesCron = exports.runUpdateProviderJob = exports.runJob = void 0;
36
36
  const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-functions"));
37
37
  const storage_1 = require("../../../storage");
38
38
  const connection_1 = require("../../../util/connection");
@@ -80,6 +80,52 @@ function runJob({ config, integration, job, }) {
80
80
  });
81
81
  }
82
82
  exports.runJob = runJob;
83
+ function runUpdateProviderJob({ integrationId, crowdinId, type, title, payload, jobType, projectId, client, integration, context, credentials, rootFolder, appSettings, reRunJobId, }) {
84
+ return __awaiter(this, void 0, void 0, function* () {
85
+ try {
86
+ yield (0, job_1.runAsJob)({
87
+ integrationId,
88
+ crowdinId,
89
+ type,
90
+ title,
91
+ payload,
92
+ jobType,
93
+ projectId,
94
+ client,
95
+ reRunJobId,
96
+ jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
97
+ const updateParams = {
98
+ projectId,
99
+ client,
100
+ credentials,
101
+ request: payload,
102
+ rootFolder,
103
+ appSettings,
104
+ job,
105
+ };
106
+ if (type === types_2.JobType.UPDATE_TO_CROWDIN) {
107
+ yield integration.updateCrowdin(updateParams);
108
+ }
109
+ else if (type === types_2.JobType.UPDATE_TO_INTEGRATION) {
110
+ yield integration.updateIntegration(updateParams);
111
+ }
112
+ }),
113
+ });
114
+ }
115
+ catch (e) {
116
+ const action = type === types_2.JobType.UPDATE_TO_CROWDIN ? 'Auto sync files to Crowdin' : 'Auto sync files to External Service';
117
+ yield (0, logger_1.handleUserError)({
118
+ action,
119
+ error: e,
120
+ crowdinId: crowdinId,
121
+ clientId: integrationId,
122
+ });
123
+ (0, logger_1.logError)(e, context);
124
+ throw e;
125
+ }
126
+ });
127
+ }
128
+ exports.runUpdateProviderJob = runUpdateProviderJob;
83
129
  function filesCron({ config, integration, period, }) {
84
130
  return __awaiter(this, void 0, void 0, function* () {
85
131
  (0, logger_1.log)(`Starting files cron job with period [${period}]`);
@@ -237,7 +283,7 @@ function processSyncSettings({ config, integration, period, syncSettings, }) {
237
283
  removeInContextLanguage(filesToProcess, projectData);
238
284
  }
239
285
  try {
240
- yield (0, job_1.runAsJob)({
286
+ yield runUpdateProviderJob({
241
287
  integrationId: syncSettings.integrationId,
242
288
  crowdinId: syncSettings.crowdinId,
243
289
  type: types_2.JobType.UPDATE_TO_INTEGRATION,
@@ -246,27 +292,14 @@ function processSyncSettings({ config, integration, period, syncSettings, }) {
246
292
  jobType: types_2.JobClientType.CRON,
247
293
  projectId: projectId,
248
294
  client: crowdinClient,
249
- jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
250
- yield integration.updateIntegration({
251
- projectId,
252
- client: crowdinClient,
253
- credentials: apiCredentials,
254
- request: filesToProcess,
255
- rootFolder,
256
- appSettings: intConfig,
257
- job,
258
- });
259
- }),
295
+ integration,
296
+ context,
297
+ credentials: apiCredentials,
298
+ rootFolder,
299
+ appSettings: intConfig,
260
300
  });
261
301
  }
262
302
  catch (e) {
263
- yield (0, logger_1.handleUserError)({
264
- action: 'Auto sync files to External Service',
265
- error: e,
266
- crowdinId: syncSettings.crowdinId,
267
- clientId: syncSettings.integrationId,
268
- });
269
- (0, logger_1.logError)(e, context);
270
303
  return;
271
304
  }
272
305
  if (Object.keys(newFiles).length) {
@@ -289,7 +322,7 @@ function processSyncSettings({ config, integration, period, syncSettings, }) {
289
322
  (0, logger_1.log)(`Executing updateCrowdin task for files cron job with period [${period}] for project ${projectId}. Files ${intFiles.length}`);
290
323
  const apiCredentials = yield (0, connection_1.prepareIntegrationCredentials)(config, integration, integrationCredentials);
291
324
  try {
292
- yield (0, job_1.runAsJob)({
325
+ yield runUpdateProviderJob({
293
326
  integrationId: syncSettings.integrationId,
294
327
  crowdinId: syncSettings.crowdinId,
295
328
  type: types_2.JobType.UPDATE_TO_CROWDIN,
@@ -298,27 +331,14 @@ function processSyncSettings({ config, integration, period, syncSettings, }) {
298
331
  jobType: types_2.JobClientType.CRON,
299
332
  projectId: projectId,
300
333
  client: crowdinClient,
301
- jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
302
- yield integration.updateCrowdin({
303
- projectId,
304
- client: crowdinClient,
305
- credentials: apiCredentials,
306
- request: intFiles,
307
- rootFolder,
308
- appSettings: intConfig,
309
- job,
310
- });
311
- }),
334
+ integration,
335
+ context,
336
+ credentials: apiCredentials,
337
+ rootFolder,
338
+ appSettings: intConfig,
312
339
  });
313
340
  }
314
341
  catch (e) {
315
- yield (0, logger_1.handleUserError)({
316
- action: 'Auto sync files to Crowdin',
317
- error: e,
318
- crowdinId: syncSettings.crowdinId,
319
- clientId: syncSettings.integrationId,
320
- });
321
- (0, logger_1.logError)(e, context);
322
342
  return;
323
343
  }
324
344
  if (Object.keys(newFiles).length) {
@@ -255,6 +255,9 @@ function applyIntegrationModuleDefaults(config, integration) {
255
255
  if (!((_b = integration.filtering) === null || _b === void 0 ? void 0 : _b.hasOwnProperty('crowdinLanguages'))) {
256
256
  integration.filtering = Object.assign(Object.assign({}, (integration.filtering || {})), { crowdinLanguages: true });
257
257
  }
258
+ if (!integration.userErrorLifetimeDays) {
259
+ integration.userErrorLifetimeDays = 30;
260
+ }
258
261
  }
259
262
  exports.applyIntegrationModuleDefaults = applyIntegrationModuleDefaults;
260
263
  function constructOauthUrl({ config, integration, clientId, loginForm, }) {
@@ -1,7 +1,8 @@
1
1
  import { JobClient, JobClientType, JobType } from './types';
2
2
  import { Response } from 'express';
3
3
  import Crowdin from '@crowdin/crowdin-api-client';
4
- export declare function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobCallback, onError, }: {
4
+ import { Config } from '../../../types';
5
+ export declare function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobCallback, onError, reRunJobId, }: {
5
6
  integrationId: string;
6
7
  crowdinId: string;
7
8
  type: JobType;
@@ -13,4 +14,6 @@ export declare function runAsJob({ integrationId, crowdinId, type, title, payloa
13
14
  jobType: JobClientType;
14
15
  jobCallback: (arg1: JobClient) => Promise<any>;
15
16
  onError?: (e: any) => Promise<void>;
17
+ reRunJobId?: string;
16
18
  }): Promise<void>;
19
+ export declare function reRunInProgressJobs(config: Config): Promise<void>;
@@ -1,4 +1,27 @@
1
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
+ };
2
25
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
26
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
27
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -9,37 +32,48 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
32
  });
10
33
  };
11
34
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.runAsJob = void 0;
35
+ exports.reRunInProgressJobs = exports.runAsJob = void 0;
13
36
  const types_1 = require("./types");
14
37
  const storage_1 = require("../../../storage");
15
38
  const logger_1 = require("../../../util/logger");
39
+ const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-functions"));
40
+ const connection_1 = require("../../../util/connection");
41
+ const defaults_1 = require("./defaults");
42
+ const cron_1 = require("./cron");
16
43
  const blockingJobs = {
17
44
  [types_1.JobType.UPDATE_TO_CROWDIN]: [types_1.JobType.UPDATE_TO_CROWDIN, types_1.JobType.UPDATE_TO_INTEGRATION],
18
45
  [types_1.JobType.UPDATE_TO_INTEGRATION]: [types_1.JobType.UPDATE_TO_CROWDIN, types_1.JobType.UPDATE_TO_INTEGRATION],
19
46
  [types_1.JobType.CROWDIN_SYNC_SETTINGS_SAVE]: [types_1.JobType.CROWDIN_SYNC_SETTINGS_SAVE],
20
47
  [types_1.JobType.INTEGRATION_SYNC_SETTINGS_SAVE]: [types_1.JobType.INTEGRATION_SYNC_SETTINGS_SAVE],
21
48
  };
22
- function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobCallback, onError, }) {
49
+ const maxAttempts = 3;
50
+ function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobCallback, onError, reRunJobId, }) {
23
51
  return __awaiter(this, void 0, void 0, function* () {
52
+ let jobId;
24
53
  const storage = (0, storage_1.getStorage)();
25
- const activeJobs = yield storage.getActiveJobs({ integrationId, crowdinId });
26
- if (activeJobs === null || activeJobs === void 0 ? void 0 : activeJobs.length) {
27
- const existingJob = activeJobs.find((job) => blockingJobs[type].includes(job.type));
28
- if (existingJob === null || existingJob === void 0 ? void 0 : existingJob.id) {
29
- if (res) {
30
- res.status(202).send({ jobId: existingJob.id, message: 'Another sync is running' });
54
+ if (!reRunJobId) {
55
+ const activeJobs = yield storage.getActiveJobs({ integrationId, crowdinId });
56
+ if (activeJobs === null || activeJobs === void 0 ? void 0 : activeJobs.length) {
57
+ const existingJob = activeJobs.find((job) => blockingJobs[type].includes(job.type));
58
+ if (existingJob === null || existingJob === void 0 ? void 0 : existingJob.id) {
59
+ if (res) {
60
+ res.status(202).send({ jobId: existingJob.id, message: 'Another sync is running' });
61
+ }
62
+ (0, logger_1.log)(`Unable to run new job '${type}', with title '${title}', already running jobId ${existingJob.id}.`);
63
+ return;
31
64
  }
32
- (0, logger_1.log)(`Unable to run new job '${type}', with title '${title}', already running jobId ${existingJob.id}.`);
33
- return;
34
65
  }
66
+ jobId = yield storage.createJob({
67
+ integrationId,
68
+ crowdinId,
69
+ type,
70
+ title: title || '',
71
+ payload: JSON.stringify(payload),
72
+ });
73
+ }
74
+ else {
75
+ jobId = reRunJobId;
35
76
  }
36
- const jobId = yield storage.createJob({
37
- integrationId,
38
- crowdinId,
39
- type,
40
- title: title || '',
41
- payload: JSON.stringify(payload),
42
- });
43
77
  if (res) {
44
78
  res.status(202).send({ jobId });
45
79
  }
@@ -49,7 +83,7 @@ function runAsJob({ integrationId, crowdinId, type, title, payload, res, project
49
83
  return yield storage.getJob({ id: jobId });
50
84
  });
51
85
  },
52
- update: function updateProgress({ progress, status, info, data }) {
86
+ update: function updateProgress({ progress, status, info, data, attempt }) {
53
87
  return __awaiter(this, void 0, void 0, function* () {
54
88
  const prevData = yield this.get();
55
89
  if ((prevData === null || prevData === void 0 ? void 0 : prevData.status) === types_1.JobStatus.CANCELED) {
@@ -61,6 +95,7 @@ function runAsJob({ integrationId, crowdinId, type, title, payload, res, project
61
95
  status: status || types_1.JobStatus.IN_PROGRESS,
62
96
  info,
63
97
  data: JSON.stringify(data),
98
+ attempt,
64
99
  });
65
100
  return { isCanceled: false };
66
101
  });
@@ -136,3 +171,86 @@ function runAsJob({ integrationId, crowdinId, type, title, payload, res, project
136
171
  });
137
172
  }
138
173
  exports.runAsJob = runAsJob;
174
+ function reRunInProgressJobs(config) {
175
+ return __awaiter(this, void 0, void 0, function* () {
176
+ const storage = (0, storage_1.getStorage)();
177
+ const inProgressJobs = yield storage.getAllInProgressJobs();
178
+ if (!(inProgressJobs === null || inProgressJobs === void 0 ? void 0 : inProgressJobs.length)) {
179
+ return;
180
+ }
181
+ yield Promise.all(inProgressJobs.map((activeJob) => __awaiter(this, void 0, void 0, function* () {
182
+ if (activeJob && activeJob.status !== types_1.JobStatus.IN_PROGRESS) {
183
+ return;
184
+ }
185
+ if (activeJob.attempt && activeJob.attempt >= maxAttempts) {
186
+ yield storage.updateJob({ id: activeJob.id, status: types_1.JobStatus.FAILED });
187
+ return;
188
+ }
189
+ const { integrationId, crowdinId, type, title, payload } = activeJob;
190
+ const integration = config.projectIntegration;
191
+ let crowdinClient;
192
+ const integrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(integrationId);
193
+ const crowdinCredentials = yield (0, storage_1.getStorage)().getCrowdinCredentials(crowdinId);
194
+ const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(integrationId);
195
+ if (!integrationCredentials || !crowdinCredentials) {
196
+ return;
197
+ }
198
+ const projectId = crowdinAppFunctions.getProjectId(integrationCredentials.id);
199
+ const context = {
200
+ jwtPayload: {
201
+ context: {
202
+ // eslint-disable-next-line @typescript-eslint/camelcase
203
+ project_id: projectId,
204
+ // eslint-disable-next-line @typescript-eslint/camelcase
205
+ organization_id: crowdinCredentials.organizationId,
206
+ // eslint-disable-next-line @typescript-eslint/camelcase
207
+ organization_domain: crowdinCredentials.domain,
208
+ // eslint-disable-next-line @typescript-eslint/camelcase
209
+ user_id: crowdinCredentials.userId,
210
+ },
211
+ },
212
+ };
213
+ try {
214
+ const preparedCrowdinClient = yield (0, connection_1.prepareCrowdinClient)({
215
+ config,
216
+ credentials: crowdinCredentials,
217
+ autoRenew: true,
218
+ context,
219
+ });
220
+ crowdinClient = preparedCrowdinClient.client;
221
+ }
222
+ catch (e) {
223
+ (0, logger_1.logError)(e);
224
+ return;
225
+ }
226
+ const rootFolder = yield (0, defaults_1.getRootFolder)(config, integration, crowdinClient, projectId);
227
+ const apiCredentials = yield (0, connection_1.prepareIntegrationCredentials)(config, integration, integrationCredentials);
228
+ const intConfig = (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config)
229
+ ? JSON.parse(integrationConfig.config)
230
+ : { schedule: '0', condition: '0' };
231
+ yield storage.updateJob({ attempt: +(activeJob.attempt || 0) + 1, id: activeJob.id });
232
+ if ([types_1.JobType.UPDATE_TO_CROWDIN, types_1.JobType.UPDATE_TO_INTEGRATION].includes(type)) {
233
+ yield (0, cron_1.runUpdateProviderJob)({
234
+ integrationId,
235
+ crowdinId,
236
+ type,
237
+ title,
238
+ payload: JSON.parse(payload),
239
+ jobType: types_1.JobClientType.RERUN,
240
+ projectId,
241
+ client: crowdinClient,
242
+ integration,
243
+ context,
244
+ credentials: apiCredentials,
245
+ rootFolder,
246
+ appSettings: intConfig,
247
+ reRunJobId: activeJob.id,
248
+ });
249
+ }
250
+ else if ([types_1.JobType.CROWDIN_SYNC_SETTINGS_SAVE, types_1.JobType.INTEGRATION_SYNC_SETTINGS_SAVE].includes(type)) {
251
+ yield storage.updateJob({ status: types_1.JobStatus.CANCELED, id: activeJob.id });
252
+ }
253
+ })));
254
+ });
255
+ }
256
+ exports.reRunInProgressJobs = reRunInProgressJobs;
@@ -18,7 +18,8 @@ export declare enum JobStatus {
18
18
  }
19
19
  export declare enum JobClientType {
20
20
  CRON = "cron",
21
- MANUAL = "manual"
21
+ MANUAL = "manual",
22
+ RERUN = "rerun"
22
23
  }
23
24
  export interface Job {
24
25
  id: string;
@@ -35,6 +36,7 @@ export interface Job {
35
36
  finishedAt?: number;
36
37
  eta?: number;
37
38
  info?: string;
39
+ attempt?: number;
38
40
  }
39
41
  export type GetJobParams = Pick<Job, 'id'>;
40
42
  export type GetActiveJobsParams = Pick<Job, 'integrationId' | 'crowdinId'>;
@@ -45,6 +47,7 @@ export type UpdateJobParams = {
45
47
  status?: JobStatus;
46
48
  info?: string;
47
49
  data?: string;
50
+ attempt?: number;
48
51
  };
49
52
  export type JobClient = {
50
53
  get: () => Promise<Job | undefined>;
@@ -53,7 +56,7 @@ export type JobClient = {
53
56
  translationUploaded: SaveUploadedFileTranslation;
54
57
  fetchTranslation: FetchTranslation;
55
58
  };
56
- export type UpdateJobProgress = ({ progress, status, info, data, }: Omit<UpdateJobParams, 'id'>) => Promise<{
59
+ export type UpdateJobProgress = ({ progress, status, info, data, attempt, }: Omit<UpdateJobParams, 'id'>) => Promise<{
57
60
  isCanceled: boolean;
58
61
  }>;
59
62
  export interface GetAllNewFilesArgs {
@@ -20,4 +20,5 @@ var JobClientType;
20
20
  (function (JobClientType) {
21
21
  JobClientType["CRON"] = "cron";
22
22
  JobClientType["MANUAL"] = "manual";
23
+ JobClientType["RERUN"] = "rerun";
23
24
  })(JobClientType = exports.JobClientType || (exports.JobClientType = {}));
@@ -34,6 +34,7 @@ export interface Storage {
34
34
  getAllUserErrors(crowdinId: string, integrationId?: string): Promise<UserErrors[] | undefined>;
35
35
  saveUserError(action: string, message: string, data: any, createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
36
36
  deleteUserErrors(date: string, crowdinId: string, integrationId?: string): Promise<void>;
37
+ deleteAllUsersErrorsOlderThan(date: string): Promise<void>;
37
38
  saveIntegrationConfig(integrationId: string, crowdinId: string, config: any): Promise<void>;
38
39
  getAllIntegrationConfigs(crowdinId: string): Promise<IntegrationConfig[]>;
39
40
  getIntegrationConfig(integrationId: string): Promise<IntegrationConfig | undefined>;
@@ -43,6 +44,7 @@ export interface Storage {
43
44
  getJob(params: GetJobParams): Promise<Job | undefined>;
44
45
  getActiveJobs(params: GetActiveJobsParams): Promise<Job[] | undefined>;
45
46
  deleteFinishedJobs(): Promise<void>;
47
+ getAllInProgressJobs(): Promise<Job[] | undefined>;
46
48
  saveTranslationCache(params: TranslationCache): Promise<void>;
47
49
  getFileTranslationCache(params: GetFileTranslationCache): Promise<TranslationCache[] | undefined>;
48
50
  getFileTranslationCacheByLanguage(params: GetFileTranslationCacheByLanguageParams): Promise<TranslationCache | undefined>;
@@ -51,15 +51,17 @@ export declare class MySQLStorage implements Storage {
51
51
  getAllUserErrors(crowdinId: string, integrationId?: string): Promise<UserErrors[] | undefined>;
52
52
  saveUserError(action: string, message: string, data: any, createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
53
53
  deleteUserErrors(createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
54
+ deleteAllUsersErrorsOlderThan(createdAt: string): Promise<void>;
54
55
  saveIntegrationConfig(integrationId: string, crowdinId: string, config: any): Promise<void>;
55
56
  getAllIntegrationConfigs(crowdinId: string): Promise<IntegrationConfig[]>;
56
57
  getIntegrationConfig(integrationId: string): Promise<IntegrationConfig | undefined>;
57
58
  updateIntegrationConfig(integrationId: string, config: any): Promise<void>;
58
59
  createJob({ integrationId, crowdinId, type, title, payload }: CreateJobParams): Promise<string>;
59
- updateJob({ id, progress, status, info, data }: UpdateJobParams): Promise<void>;
60
+ updateJob({ id, progress, status, info, data, attempt }: UpdateJobParams): Promise<void>;
60
61
  getJob({ id }: GetJobParams): Promise<Job | undefined>;
61
62
  getActiveJobs({ integrationId, crowdinId }: GetActiveJobsParams): Promise<Job[] | undefined>;
62
63
  deleteFinishedJobs(): Promise<void>;
64
+ getAllInProgressJobs(): Promise<Job[] | undefined>;
63
65
  saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }: TranslationCache): Promise<void>;
64
66
  getFileTranslationCache({ integrationId, crowdinId, fileId, }: GetFileTranslationCache): Promise<TranslationCache[] | undefined>;
65
67
  getFileTranslationCacheByLanguage({ integrationId, crowdinId, fileId, languageId, }: GetFileTranslationCacheByLanguageParams): Promise<TranslationCache | undefined>;
@@ -161,6 +161,7 @@ class MySQLStorage {
161
161
  payload text,
162
162
  info text,
163
163
  data text,
164
+ attempt int 0,
164
165
  created_at varchar(255) not null,
165
166
  updated_at varchar(255),
166
167
  finished_at varchar(255)
@@ -471,6 +472,12 @@ class MySQLStorage {
471
472
  });
472
473
  });
473
474
  }
475
+ deleteAllUsersErrorsOlderThan(createdAt) {
476
+ return __awaiter(this, void 0, void 0, function* () {
477
+ yield this.dbPromise;
478
+ yield this.executeQuery((connection) => connection.execute('DELETE FROM user_errors WHERE created_at < ?', [createdAt]));
479
+ });
480
+ }
474
481
  saveIntegrationConfig(integrationId, crowdinId, config) {
475
482
  return __awaiter(this, void 0, void 0, function* () {
476
483
  yield this.dbPromise;
@@ -518,7 +525,7 @@ class MySQLStorage {
518
525
  return id;
519
526
  });
520
527
  }
521
- updateJob({ id, progress, status, info, data }) {
528
+ updateJob({ id, progress, status, info, data, attempt }) {
522
529
  return __awaiter(this, void 0, void 0, function* () {
523
530
  const updateFields = ['updated_at'];
524
531
  const updateParams = [Date.now().toString()];
@@ -546,6 +553,10 @@ class MySQLStorage {
546
553
  updateFields.push('info = ?');
547
554
  updateParams.push(info);
548
555
  }
556
+ if (attempt) {
557
+ updateFields.push('attempt = ?');
558
+ updateParams.push(attempt);
559
+ }
549
560
  updateParams.push(id);
550
561
  yield this.dbPromise;
551
562
  yield this.executeQuery((connection) => connection.execute(`
@@ -561,7 +572,7 @@ class MySQLStorage {
561
572
  return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
562
573
  const [rows] = yield connection.execute(`
563
574
  SELECT id, integration_id as integrationId, crowdin_id as crowdinId, type, payload, progress, status,
564
- title, info, payload, data, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
575
+ title, info, data, attempt, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
565
576
  FROM job
566
577
  WHERE id = ?
567
578
  `, [id]);
@@ -575,7 +586,7 @@ class MySQLStorage {
575
586
  return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
576
587
  const [rows] = yield connection.execute(`
577
588
  SELECT id, integration_id as integrationId, crowdin_id as crowdinId, type, payload, progress, status,
578
- title, info, payload, data, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
589
+ title, info, data, attempt, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
579
590
  FROM job
580
591
  WHERE integration_id = ? AND crowdin_id = ? AND finished_at is NULL
581
592
  `, [integrationId, crowdinId]);
@@ -589,6 +600,20 @@ class MySQLStorage {
589
600
  yield this.executeQuery((connection) => connection.execute('DELETE FROM job WHERE finished_at is not NULL', []));
590
601
  });
591
602
  }
603
+ getAllInProgressJobs() {
604
+ return __awaiter(this, void 0, void 0, function* () {
605
+ yield this.dbPromise;
606
+ return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
607
+ const [rows] = yield connection.execute(`
608
+ SELECT id, integration_id as integrationId, crowdin_id as crowdinId, type, payload, progress, status,
609
+ title, info, data, attempt, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
610
+ FROM job
611
+ WHERE status = ? AND finished_at is NULL
612
+ `, [types_1.JobStatus.IN_PROGRESS]);
613
+ return rows || [];
614
+ }));
615
+ });
616
+ }
592
617
  saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }) {
593
618
  return __awaiter(this, void 0, void 0, function* () {
594
619
  yield this.dbPromise;
@@ -26,6 +26,7 @@ export declare class PostgreStorage implements Storage {
26
26
  migrate(): Promise<void>;
27
27
  alterTables(client: Client): Promise<void>;
28
28
  addColumns(client: Client, newColumns: string[], tableName: string): Promise<void>;
29
+ addColumn(client: Client, columnName: string, tableName: string, columnType: string): Promise<void>;
29
30
  addTables(client: Client): Promise<void>;
30
31
  saveCrowdinCredentials(credentials: CrowdinCredentials): Promise<void>;
31
32
  updateCrowdinCredentials(credentials: CrowdinCredentials): Promise<void>;
@@ -57,15 +58,17 @@ export declare class PostgreStorage implements Storage {
57
58
  getAllUserErrors(crowdinId: string, integrationId?: string): Promise<UserErrors[] | undefined>;
58
59
  saveUserError(action: string, message: string, data: any, createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
59
60
  deleteUserErrors(createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
61
+ deleteAllUsersErrorsOlderThan(createdAt: string): Promise<void>;
60
62
  saveIntegrationConfig(integrationId: string, crowdinId: string, config: any): Promise<void>;
61
63
  getAllIntegrationConfigs(crowdinId: string): Promise<IntegrationConfig[]>;
62
64
  getIntegrationConfig(integrationId: string): Promise<IntegrationConfig | undefined>;
63
65
  updateIntegrationConfig(integrationId: string, config: any): Promise<void>;
64
66
  createJob({ integrationId, crowdinId, type, payload, title }: CreateJobParams): Promise<string>;
65
- updateJob({ id, progress, status, info, data }: UpdateJobParams): Promise<void>;
67
+ updateJob({ id, progress, status, info, data, attempt }: UpdateJobParams): Promise<void>;
66
68
  getJob({ id }: GetJobParams): Promise<Job | undefined>;
67
69
  getActiveJobs({ integrationId, crowdinId }: GetActiveJobsParams): Promise<Job[] | undefined>;
68
70
  deleteFinishedJobs(): Promise<void>;
71
+ getAllInProgressJobs(): Promise<Job[] | undefined>;
69
72
  saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }: TranslationCache): Promise<void>;
70
73
  getFileTranslationCache({ integrationId, crowdinId, fileId, }: GetFileTranslationCache): Promise<TranslationCache[] | undefined>;
71
74
  getFileTranslationCacheByLanguage({ integrationId, crowdinId, fileId, languageId, }: GetFileTranslationCacheByLanguageParams): Promise<TranslationCache | undefined>;
@@ -63,6 +63,7 @@ class PostgreStorage {
63
63
  return __awaiter(this, void 0, void 0, function* () {
64
64
  yield this.addColumns(client, ['crowdin_id'], 'app_metadata');
65
65
  yield this.addColumns(client, ['agent_id'], 'crowdin_credentials');
66
+ yield this.addColumn(client, 'attempt', 'job', 'int default 0');
66
67
  });
67
68
  }
68
69
  addColumns(client, newColumns, tableName) {
@@ -79,6 +80,15 @@ class PostgreStorage {
79
80
  }
80
81
  });
81
82
  }
83
+ addColumn(client, columnName, tableName, columnType) {
84
+ return __awaiter(this, void 0, void 0, function* () {
85
+ const tableInfo = yield client.query('SELECT column_name FROM information_schema.columns WHERE table_name = $1', [tableName]);
86
+ const exists = tableInfo.rows.some((columnInfo) => columnInfo.column_name === columnName);
87
+ if (!exists) {
88
+ yield client.query(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType};`);
89
+ }
90
+ });
91
+ }
82
92
  addTables(client) {
83
93
  return __awaiter(this, void 0, void 0, function* () {
84
94
  yield client.query(`
@@ -178,6 +188,7 @@ class PostgreStorage {
178
188
  payload varchar null,
179
189
  info varchar null,
180
190
  data varchar null,
191
+ attempt int default 0,
181
192
  created_at varchar not null,
182
193
  updated_at varchar null,
183
194
  finished_at varchar null
@@ -489,6 +500,14 @@ class PostgreStorage {
489
500
  });
490
501
  });
491
502
  }
503
+ deleteAllUsersErrorsOlderThan(createdAt) {
504
+ return __awaiter(this, void 0, void 0, function* () {
505
+ yield this.dbPromise;
506
+ yield this.executeQuery((client) => {
507
+ return client.query('DELETE FROM user_errors WHERE created_at < $1', [createdAt]);
508
+ });
509
+ });
510
+ }
492
511
  saveIntegrationConfig(integrationId, crowdinId, config) {
493
512
  return __awaiter(this, void 0, void 0, function* () {
494
513
  yield this.dbPromise;
@@ -538,7 +557,7 @@ class PostgreStorage {
538
557
  return id;
539
558
  });
540
559
  }
541
- updateJob({ id, progress, status, info, data }) {
560
+ updateJob({ id, progress, status, info, data, attempt }) {
542
561
  return __awaiter(this, void 0, void 0, function* () {
543
562
  const updateFields = ['updated_at'];
544
563
  const updateParams = [Date.now().toString()];
@@ -566,6 +585,10 @@ class PostgreStorage {
566
585
  updateFields.push('info = $' + updateParams.length.toString());
567
586
  updateParams.push(info);
568
587
  }
588
+ if (attempt) {
589
+ updateFields.push('attempt = $' + updateParams.length.toString());
590
+ updateParams.push(attempt);
591
+ }
569
592
  updateParams.push(id);
570
593
  yield this.dbPromise;
571
594
  yield this.executeQuery((client) => client.query(`
@@ -581,7 +604,7 @@ class PostgreStorage {
581
604
  return this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
582
605
  const res = yield client.query(`
583
606
  SELECT id, integration_id as integrationId, crowdin_id as crowdinId, type, payload, progress, status,
584
- title, info, payload, data, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
607
+ title, info, data, attempt, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
585
608
  FROM job
586
609
  WHERE id = $1
587
610
  `, [id]);
@@ -595,7 +618,7 @@ class PostgreStorage {
595
618
  return this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
596
619
  const res = yield client.query(`
597
620
  SELECT id, integration_id as integrationId, crowdin_id as crowdinId, type, payload, progress, status,
598
- title, info, payload, data, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
621
+ title, info, data, attempt, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
599
622
  FROM job
600
623
  WHERE integration_id = $1 AND crowdin_id = $2 AND finished_at is NULL
601
624
  `, [integrationId, crowdinId]);
@@ -609,6 +632,20 @@ class PostgreStorage {
609
632
  yield this.executeQuery((client) => client.query(' DELETE FROM job WHERE finished_at is not NULL', []));
610
633
  });
611
634
  }
635
+ getAllInProgressJobs() {
636
+ return __awaiter(this, void 0, void 0, function* () {
637
+ yield this.dbPromise;
638
+ return this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
639
+ const res = yield client.query(`
640
+ SELECT id, integration_id as integrationId, crowdin_id as crowdinId, type, payload, progress, status,
641
+ title, info, data, attempt, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
642
+ FROM job
643
+ WHERE status = $1 AND finished_at is NULL
644
+ `, [types_1.JobStatus.IN_PROGRESS]);
645
+ return (res === null || res === void 0 ? void 0 : res.rows) || [];
646
+ }));
647
+ });
648
+ }
612
649
  saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }) {
613
650
  return __awaiter(this, void 0, void 0, function* () {
614
651
  yield this.dbPromise;
@@ -19,6 +19,7 @@ export declare class SQLiteStorage implements Storage {
19
19
  private each;
20
20
  private removeColumns;
21
21
  private addColumns;
22
+ private addColumn;
22
23
  private updateTables;
23
24
  private moveIntegrationSettings;
24
25
  migrate(): Promise<void>;
@@ -52,15 +53,17 @@ export declare class SQLiteStorage implements Storage {
52
53
  getAllUserErrors(crowdinId: string, integrationId?: string): Promise<UserErrors[]>;
53
54
  saveUserError(action: string, message: string, data: any, createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
54
55
  deleteUserErrors(createAt: string, crowdinId: string, integrationId?: string): Promise<void>;
56
+ deleteAllUsersErrorsOlderThan(createAt: string): Promise<void>;
55
57
  saveIntegrationConfig(integrationId: string, crowdinId: string, config: any): Promise<void>;
56
58
  getAllIntegrationConfigs(crowdinId: string): Promise<IntegrationConfig[]>;
57
59
  getIntegrationConfig(integrationId: string): Promise<IntegrationConfig | undefined>;
58
60
  updateIntegrationConfig(integrationId: string, config: any): Promise<void>;
59
61
  createJob({ integrationId, crowdinId, type, title, payload }: CreateJobParams): Promise<string>;
60
- updateJob({ id, progress, status, info, data }: UpdateJobParams): Promise<void>;
62
+ updateJob({ id, progress, status, info, data, attempt }: UpdateJobParams): Promise<void>;
61
63
  getJob({ id }: GetJobParams): Promise<Job | undefined>;
62
64
  getActiveJobs({ integrationId, crowdinId }: GetActiveJobsParams): Promise<Job[] | undefined>;
63
65
  deleteFinishedJobs(): Promise<void>;
66
+ getAllInProgressJobs(): Promise<Job[] | undefined>;
64
67
  saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag }: TranslationCache): Promise<void>;
65
68
  getFileTranslationCache({ integrationId, crowdinId, fileId, }: GetFileTranslationCache): Promise<TranslationCache[] | undefined>;
66
69
  getFileTranslationCacheByLanguage({ integrationId, crowdinId, fileId, languageId, }: GetFileTranslationCacheByLanguageParams): Promise<TranslationCache | undefined>;
@@ -118,10 +118,20 @@ class SQLiteStorage {
118
118
  }
119
119
  });
120
120
  }
121
+ addColumn(tableName, column, defaultValue) {
122
+ return __awaiter(this, void 0, void 0, function* () {
123
+ const tableInfo = yield this.each(`PRAGMA table_info(${tableName});`, []);
124
+ const exists = tableInfo.some((columnInfo) => columnInfo.name === column);
125
+ if (!exists) {
126
+ yield this.run(`ALTER TABLE ${tableName} ADD COLUMN ${column} varchar ${defaultValue};`, []);
127
+ }
128
+ });
129
+ }
121
130
  updateTables() {
122
131
  return __awaiter(this, void 0, void 0, function* () {
123
132
  yield this.addColumns(['app_secret', 'domain', 'user_id', 'agent_id', 'organization_id', 'base_url'], 'crowdin_credentials');
124
133
  yield this.addColumns(['crowdin_id'], 'app_metadata');
134
+ yield this.addColumn('job', 'attempt', 'DEFAULT 0');
125
135
  });
126
136
  }
127
137
  moveIntegrationSettings() {
@@ -252,6 +262,7 @@ class SQLiteStorage {
252
262
  payload varchar null,
253
263
  info varchar null,
254
264
  data varchar null,
265
+ attempt varchar DEFAULT 0,
255
266
  created_at varchar not null,
256
267
  updated_at varchar null,
257
268
  finished_at varchar null
@@ -491,6 +502,11 @@ class SQLiteStorage {
491
502
  return this.run(`DELETE FROM user_errors WHERE created_at < ? AND crowdin_id = ? AND ${whereIntegrationCondition}`, params);
492
503
  });
493
504
  }
505
+ deleteAllUsersErrorsOlderThan(createAt) {
506
+ return __awaiter(this, void 0, void 0, function* () {
507
+ yield this.run('DELETE FROM user_errors where created_at < ?', [createAt]);
508
+ });
509
+ }
494
510
  saveIntegrationConfig(integrationId, crowdinId, config) {
495
511
  return this.run('INSERT INTO integration_settings(integration_id, crowdin_id, config) VALUES (?, ?, ?)', [
496
512
  integrationId,
@@ -525,7 +541,7 @@ class SQLiteStorage {
525
541
  return id;
526
542
  });
527
543
  }
528
- updateJob({ id, progress, status, info, data }) {
544
+ updateJob({ id, progress, status, info, data, attempt }) {
529
545
  const updateFields = ['updated_at = ?'];
530
546
  const updateParams = [Date.now().toString()];
531
547
  if (progress) {
@@ -552,6 +568,10 @@ class SQLiteStorage {
552
568
  updateFields.push('info = ?');
553
569
  updateParams.push(info);
554
570
  }
571
+ if (attempt) {
572
+ updateFields.push('attempt = ?');
573
+ updateParams.push(attempt);
574
+ }
555
575
  updateParams.push(id);
556
576
  const query = `
557
577
  UPDATE job
@@ -564,7 +584,7 @@ class SQLiteStorage {
564
584
  return __awaiter(this, void 0, void 0, function* () {
565
585
  const row = yield this.get(`
566
586
  SELECT id, integration_id as integrationId, crowdin_id as crowdinId, type, payload, progress, status,
567
- title, info, payload, data, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
587
+ title, info, data, attempt, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
568
588
  FROM job
569
589
  WHERE id = ?
570
590
  `, [id]);
@@ -576,7 +596,7 @@ class SQLiteStorage {
576
596
  getActiveJobs({ integrationId, crowdinId }) {
577
597
  return __awaiter(this, void 0, void 0, function* () {
578
598
  return this.each(`
579
- SELECT id, integration_id as integrationId, crowdin_id as crowdinId, type, payload, progress, status, title, info, payload, data, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
599
+ SELECT id, integration_id as integrationId, crowdin_id as crowdinId, type, payload, progress, status, title, info, data, attempt, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
580
600
  FROM job
581
601
  WHERE integration_id = ? AND crowdin_id = ? AND finished_at is NULL
582
602
  `, [integrationId, crowdinId]);
@@ -587,6 +607,15 @@ class SQLiteStorage {
587
607
  yield this.run('DELETE FROM job WHERE finished_at is not NULL', []);
588
608
  });
589
609
  }
610
+ getAllInProgressJobs() {
611
+ return __awaiter(this, void 0, void 0, function* () {
612
+ return this.each(`
613
+ SELECT id, integration_id as integrationId, crowdin_id as crowdinId, type, payload, progress, status, title, info, data, attempt, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
614
+ FROM job
615
+ WHERE status = ? AND finished_at is NULL
616
+ `, [types_1.JobStatus.IN_PROGRESS]);
617
+ });
618
+ }
590
619
  saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag }) {
591
620
  return this.run(`
592
621
  INSERT
@@ -1,6 +1,6 @@
1
1
  /// <reference types="qs" />
2
2
  import { CrowdinClientRequest, UiModule } from '../types';
3
3
  import { Request, Response } from 'express';
4
- declare function postRequestCredentialsMasker(moduleConfig?: UiModule, credentialsExtractor?: Function): (req: CrowdinClientRequest | Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: Function) => void;
5
4
  declare function getRequestCredentialsMasker(moduleConfig?: UiModule): (req: Request | CrowdinClientRequest, res: Response, next: Function) => any;
5
+ declare function postRequestCredentialsMasker(moduleConfig?: UiModule, credentialsExtractor?: Function): (req: CrowdinClientRequest | Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: Function) => void;
6
6
  export { getRequestCredentialsMasker, postRequestCredentialsMasker };
@@ -32,8 +32,39 @@ function getMaskableFieldsKeys(moduleConfig) {
32
32
  return (0, lodash_get_1.default)(moduleConfig, `formUiSchema[${fieldKey}]['ui:widget']`) === 'password';
33
33
  });
34
34
  }
35
+ function getRequestCredentialsMasker(moduleConfig) {
36
+ return function (req, res, next) {
37
+ // we can't find "password" fields without ui schema
38
+ if (!moduleConfig || !moduleConfig.formSchema || !moduleConfig.formUiSchema) {
39
+ return next();
40
+ }
41
+ // temporary
42
+ if (!moduleConfig.maskPasswords) {
43
+ return next();
44
+ }
45
+ const maskableFieldsKeys = getMaskableFieldsKeys(moduleConfig);
46
+ if (!maskableFieldsKeys.length) {
47
+ return next();
48
+ }
49
+ const originalSend = res.send;
50
+ res.send = function (body) {
51
+ if (body.formData) {
52
+ maskableFieldsKeys.forEach((fieldKey) => {
53
+ if (!body.formData[fieldKey]) {
54
+ return;
55
+ }
56
+ body.formData[fieldKey] = maskKey(body.formData[fieldKey]);
57
+ });
58
+ }
59
+ return originalSend.apply(res, [body]);
60
+ };
61
+ return next();
62
+ };
63
+ }
64
+ exports.getRequestCredentialsMasker = getRequestCredentialsMasker;
35
65
  function postRequestCredentialsMasker(moduleConfig, credentialsExtractor) {
36
66
  return (0, index_1.runAsyncWrapper)((req, res, next) => __awaiter(this, void 0, void 0, function* () {
67
+ var _a;
37
68
  if (!moduleConfig || !moduleConfig.formSchema || !moduleConfig.formUiSchema) {
38
69
  return next();
39
70
  }
@@ -41,7 +72,7 @@ function postRequestCredentialsMasker(moduleConfig, credentialsExtractor) {
41
72
  if (!moduleConfig.maskPasswords) {
42
73
  return next();
43
74
  }
44
- const fieldsKeysInRequest = Object.keys(req.body.data);
75
+ const fieldsKeysInRequest = Object.keys(((_a = req.body) === null || _a === void 0 ? void 0 : _a.data) || []);
45
76
  let unmaskedFields = {};
46
77
  if (credentialsExtractor) {
47
78
  unmaskedFields = yield credentialsExtractor(req, res);
@@ -66,37 +97,8 @@ function postRequestCredentialsMasker(moduleConfig, credentialsExtractor) {
66
97
  }
67
98
  return acc;
68
99
  }, {}));
69
- next();
100
+ // run getRequestCredentialsMasker to mask fields that can be in response
101
+ return getRequestCredentialsMasker(moduleConfig)(req, res, next);
70
102
  }));
71
103
  }
72
104
  exports.postRequestCredentialsMasker = postRequestCredentialsMasker;
73
- function getRequestCredentialsMasker(moduleConfig) {
74
- return function (req, res, next) {
75
- // we can't find "password" fields without ui schema
76
- if (!moduleConfig || !moduleConfig.formSchema || !moduleConfig.formUiSchema) {
77
- return next();
78
- }
79
- // temporary
80
- if (!moduleConfig.maskPasswords) {
81
- return next();
82
- }
83
- const maskableFieldsKeys = getMaskableFieldsKeys(moduleConfig);
84
- if (!maskableFieldsKeys.length) {
85
- return next();
86
- }
87
- const originalSend = res.send;
88
- res.send = function (body) {
89
- if (body.formData) {
90
- maskableFieldsKeys.forEach((fieldKey) => {
91
- if (!body.formData[fieldKey]) {
92
- return;
93
- }
94
- body.formData[fieldKey] = maskKey(body.formData[fieldKey]);
95
- });
96
- }
97
- return originalSend.apply(res, [body]);
98
- };
99
- return next();
100
- };
101
- }
102
- exports.getRequestCredentialsMasker = getRequestCredentialsMasker;
@@ -10,3 +10,6 @@ export declare function decryptData(config: Config, data: string): string;
10
10
  export declare function executeWithRetry<T>(func: () => Promise<T>, numOfRetries?: number): Promise<T>;
11
11
  export declare function getLogoUrl(moduleConfig?: ImagePath, modulePath?: string): string;
12
12
  export declare function isAuthorizedConfig(config: Config | UnauthorizedConfig): config is Config;
13
+ export declare function hasFormSchema(moduleConfig: any): boolean;
14
+ export declare function isJson(string: string): boolean;
15
+ export declare function getPreviousDate(days: number): Date;
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.isAuthorizedConfig = exports.getLogoUrl = exports.executeWithRetry = exports.decryptData = exports.encryptData = exports.runAsyncWrapper = exports.CodeError = void 0;
35
+ exports.getPreviousDate = exports.isJson = exports.hasFormSchema = 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");
@@ -118,3 +118,27 @@ function isAuthorizedConfig(config) {
118
118
  return !!config.clientId && !!config.clientSecret && config.authenticationType !== types_1.AuthenticationType.NONE;
119
119
  }
120
120
  exports.isAuthorizedConfig = isAuthorizedConfig;
121
+ function hasFormSchema(moduleConfig) {
122
+ var _a;
123
+ if (typeof moduleConfig === 'object' && moduleConfig !== null) {
124
+ return moduleConfig.formSchema || ((_a = moduleConfig.settingsUiModule) === null || _a === void 0 ? void 0 : _a.formSchema);
125
+ }
126
+ return false;
127
+ }
128
+ exports.hasFormSchema = hasFormSchema;
129
+ function isJson(string) {
130
+ try {
131
+ JSON.parse(string);
132
+ }
133
+ catch (e) {
134
+ return false;
135
+ }
136
+ return true;
137
+ }
138
+ exports.isJson = isJson;
139
+ function getPreviousDate(days) {
140
+ const date = new Date();
141
+ date.setDate(date.getDate() - days);
142
+ return date;
143
+ }
144
+ exports.getPreviousDate = getPreviousDate;
@@ -224,11 +224,6 @@ function storeUserError({ action, error, crowdinId, clientId, }) {
224
224
  yield (0, storage_1.getStorage)().saveUserError(action, (error === null || error === void 0 ? void 0 : error.message) || JSON.stringify(error, null, 2), JSON.stringify(data), `${Date.now()}`, crowdinId, clientId);
225
225
  });
226
226
  }
227
- function clearOldUserErrors(crowdinId, clientId) {
228
- const date = new Date();
229
- date.setMonth(date.getMonth() - 1); // previous month
230
- (0, storage_1.getStorage)().deleteUserErrors(`${date.getTime()}`, crowdinId, clientId);
231
- }
232
227
  function mergeAppModuleAggregateErrors(errors) {
233
228
  const result = [];
234
229
  const mergedData = {};
@@ -269,7 +264,6 @@ function handleUserError({ action, error, crowdinId, clientId, }) {
269
264
  }
270
265
  else {
271
266
  yield storeUserError({ action, error, crowdinId, clientId });
272
- clearOldUserErrors(crowdinId, clientId);
273
267
  }
274
268
  });
275
269
  }
@@ -5,9 +5,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const express_1 = __importDefault(require("express"));
7
7
  const terminus_1 = require("@godaddy/terminus");
8
+ const job_1 = require("../modules/integration/util/job");
8
9
  express_1.default.application.baseListen = express_1.default.application.listen;
9
10
  express_1.default.application.listen = function (...args) {
10
- const [port, callback] = args;
11
+ const [config, callback] = args;
12
+ let port;
13
+ if (typeof config === 'object') {
14
+ port = config.port;
15
+ (0, job_1.reRunInProgressJobs)(config);
16
+ }
17
+ else {
18
+ port = config;
19
+ }
11
20
  const server = this.baseListen.call(this, port);
12
21
  if (callback) {
13
22
  callback();
@@ -418,7 +418,11 @@
418
418
  .then(checkResponse)
419
419
  .then((res) => {
420
420
  project = res;
421
- const languagesSorted = project.targetLanguages.sort((a, b) => a.name.localeCompare(b.name));
421
+ const languagesSorted = project.targetLanguages.map(language => ({
422
+ sourceLanguageId: res.sourceLanguage.editorCode,
423
+ projectEditorLink: res.projectEditorLink,
424
+ ...language,
425
+ })).sort((a, b) => a.name.localeCompare(b.name));
422
426
 
423
427
  if (project.inContext && config?.inContext) {
424
428
  languagesSorted.push({...project.inContextPseudoLanguage, inContext: true});
@@ -1204,7 +1208,7 @@
1204
1208
  } else {
1205
1209
  html += `<tr><td>Message</td><td>${sanitizeHTML(error.message)}</td></tr>`;
1206
1210
  }
1207
- html += `<tr><td>Date/time</td><td>${sanitizeHTML(error.createdAt)}</td></tr>`;
1211
+ html += `<tr><td>Date/time</td><td>${sanitizeHTML(error.formattedDate)}</td></tr>`;
1208
1212
 
1209
1213
  if (data.requestParams) {
1210
1214
  html += `<tr><td>Method</td><td>${sanitizeHTML(data.requestParams.method)}</td></tr>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.64.0",
3
+ "version": "0.65.1",
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",