@crowdin/app-project-module 0.74.0 → 0.75.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.
@@ -14,6 +14,8 @@ const crowdin_file_progress_1 = __importDefault(require("../integration/handlers
14
14
  const crowdin_files_1 = __importDefault(require("../integration/handlers/crowdin-files"));
15
15
  const crowdin_update_1 = __importDefault(require("../integration/handlers/crowdin-update"));
16
16
  const integration_data_1 = __importDefault(require("../integration/handlers/integration-data"));
17
+ const job_info_1 = __importDefault(require("../integration/handlers/job-info"));
18
+ const job_cancel_1 = __importDefault(require("../integration/handlers/job-cancel"));
17
19
  const integration_login_1 = __importDefault(require("../integration/handlers/integration-login"));
18
20
  const integration_update_1 = __importDefault(require("../integration/handlers/integration-update"));
19
21
  const settings_1 = __importDefault(require("../integration/handlers/settings"));
@@ -55,7 +57,7 @@ function getDefaultApiEndpointsManifest(config) {
55
57
  description: 'Get a list of synced files',
56
58
  documentationUrl: '/api-docs#tag/Files/operation/crowdin.files',
57
59
  }, {
58
- key: 'crowdin-files-api',
60
+ key: 'file-translation-progress-api',
59
61
  name: 'File Translation Progress',
60
62
  url: '/file-progress',
61
63
  method: types_1.RequestMethods.GET,
@@ -82,6 +84,20 @@ function getDefaultApiEndpointsManifest(config) {
82
84
  method: types_1.RequestMethods.POST,
83
85
  description: 'Update integration data',
84
86
  documentationUrl: '/api-docs#tag/Files/operation/integration.update',
87
+ }, {
88
+ key: 'job-get-api',
89
+ name: 'Job Status',
90
+ url: '/jobs',
91
+ method: types_1.RequestMethods.GET,
92
+ description: 'Get Job Info',
93
+ documentationUrl: '/api-docs#tag/Jobs/operation/job.get',
94
+ }, {
95
+ key: 'job-cancel-api',
96
+ name: 'Job Status',
97
+ url: '/jobs',
98
+ method: types_1.RequestMethods.DELETE,
99
+ description: 'Cancel Job',
100
+ documentationUrl: '/api-docs#tag/Jobs/operation/job.cancel',
85
101
  }, {
86
102
  key: 'settings-api',
87
103
  name: 'Get App Settings',
@@ -191,7 +207,7 @@ function addDefaultApiEndpoints(app, config) {
191
207
  * name: fileId
192
208
  * in: query
193
209
  * required: true
194
- * description: 'Filter branch by name. Get via [List Crowdin Files](#operation/crowdin.files)'
210
+ * description: 'Get via [List Crowdin Files](#operation/crowdin.files)'
195
211
  * schema:
196
212
  * type: integer
197
213
  * example: 102
@@ -294,6 +310,62 @@ function addDefaultApiEndpoints(app, config) {
294
310
  checkSubscriptionExpiration: true,
295
311
  moduleKey: config.projectIntegration.key,
296
312
  }), (0, integration_credentials_1.default)(config, config.projectIntegration), (0, integration_update_1.default)(config, config.projectIntegration));
313
+ /**
314
+ * @openapi
315
+ * /jobs:
316
+ * get:
317
+ * tags:
318
+ * - 'Jobs'
319
+ * summary: 'Get Job Info'
320
+ * operationId: job.get
321
+ * parameters:
322
+ * - $ref: '#/components/parameters/ProjectId'
323
+ * - name: jobId
324
+ * in: query
325
+ * required: false
326
+ * schema:
327
+ * type: string
328
+ * example: 067da473-fc0b-43e3-b0a2-09d26af130c1
329
+ * responses:
330
+ * 200:
331
+ * description: 'Job information retrieved successfully'
332
+ * content:
333
+ * application/json:
334
+ * schema:
335
+ * $ref: '#/components/schemas/JobResponse'
336
+ */
337
+ app.get('/jobs', json_response_1.default, (0, crowdin_client_1.default)({
338
+ config,
339
+ optional: false,
340
+ checkSubscriptionExpiration: true,
341
+ moduleKey: config.projectIntegration.key,
342
+ }), (0, job_info_1.default)(config));
343
+ /**
344
+ * @openapi
345
+ * /jobs:
346
+ * delete:
347
+ * tags:
348
+ * - 'Jobs'
349
+ * summary: 'Cancel Job'
350
+ * operationId: job.cancel
351
+ * parameters:
352
+ * - $ref: '#/components/parameters/ProjectId'
353
+ * - name: jobId
354
+ * in: query
355
+ * required: false
356
+ * schema:
357
+ * type: string
358
+ * example: 067da473-fc0b-43e3-b0a2-09d26af130c1
359
+ * responses:
360
+ * 204:
361
+ * description: 'Job canceled successfully'
362
+ */
363
+ app.delete('/jobs', json_response_1.default, (0, crowdin_client_1.default)({
364
+ config,
365
+ optional: false,
366
+ checkSubscriptionExpiration: true,
367
+ moduleKey: config.projectIntegration.key,
368
+ }), (0, job_cancel_1.default)(config));
297
369
  /**
298
370
  * @openapi
299
371
  * /settings:
@@ -413,7 +485,7 @@ function addDefaultApiEndpoints(app, config) {
413
485
  * operationId: integration.fields
414
486
  * responses:
415
487
  * 200:
416
- * description: 'File translation progress'
488
+ * description: 'Login Form Fields'
417
489
  * content:
418
490
  * application/json:
419
491
  * schema:
@@ -70,25 +70,32 @@
70
70
  * - files
71
71
  * properties:
72
72
  * projectId:
73
- * description: 'Project Identifier. Get via [List Projects](https://developer.crowdin.com/api/v2/#operation/api.projects.getMany)'
73
+ * description: 'Project Id. Get via [List Projects](https://developer.crowdin.com/api/v2/#operation/api.projects.getMany)'
74
74
  * type: integer
75
75
  * example: 12
76
76
  * files:
77
+ * example: { 102: ['de', 'fr'], 999: ['uk'] }
77
78
  * type: object
78
- * example:
79
- * 102: ['uk', 'de']
80
- * additionalProperties:
81
- * type: array
82
- * items:
83
- * type: string
79
+ * description: |
80
+ * - **{fileId}** _(integer)_: Crowdin File Id. Get via [List Crowdin Files](#operation/crowdin.files)
81
+ * - **[{languageCode}]** _(array of strings)_: List Of Language Id. Get via [List Supported Languages](https://support.crowdin.com/developer/api/v2/#tag/Languages/operation/api.languages.getMany)
82
+ *
83
+ * **Example:**
84
+ * ```json
85
+ * {
86
+ * 102: ["de", "fr"],
87
+ * 999: ["uk"]
88
+ * }
89
+ * ```
84
90
  * UpdateResponse:
85
91
  * type: object
86
- * items:
87
- * anyOf:
88
- * - properties:
89
- * message:
90
- * type: string
91
- * example: 'File 102 Not Found'
92
+ * properties:
93
+ * jobId:
94
+ * type: string
95
+ * example: '067da473-fc0b-43e3-b0a2-09d26af130c1'
96
+ * message:
97
+ * type: string
98
+ * example: 'Another sync is running'
92
99
  * SettingsData:
93
100
  * type: object
94
101
  * example: {schedule: 0, condition: 0}
@@ -195,8 +202,17 @@
195
202
  * type: integer
196
203
  * example: 86
197
204
  * LoginFieldsResponse:
198
- * example: [{ name: 'email', description: 'User email' }, { name: 'password', description: 'User password' }]
199
- * type: object
205
+ * type: array
206
+ * items:
207
+ * type: object
208
+ * properties:
209
+ * key:
210
+ * type: string
211
+ * example: 'apiKey'
212
+ * name:
213
+ * type: string
214
+ * example: 'Service API key'
215
+ * example: [{ key: 'email', name: 'User email' }, { key: 'password', name: 'User password' }]
200
216
  * Login:
201
217
  * title: 'Login'
202
218
  * required:
@@ -211,9 +227,34 @@
211
227
  * $ref: '#/components/schemas/LoginData'
212
228
  * LoginData:
213
229
  * type: object
230
+ * description: 'Login Form Fields. Get via [Integration Login Form Fields](#operation/integration.fields)'
214
231
  * example: { email: 'user@crowdin.com', password: 'password' }
215
- * additionalProperties:
216
- * type: string
232
+ * Job:
233
+ * type: object
234
+ * properties:
235
+ * id:
236
+ * type: string
237
+ * description: 'The Unique Identifier For The Job.'
238
+ * example: '067da473-fc0b-43e3-b0a2-09d26af130c1'
239
+ * progress:
240
+ * type: integer
241
+ * description: 'The Progress Of The Job.'
242
+ * example: 94
243
+ * status:
244
+ * type: string
245
+ * description: 'The Current Status Of The Job.'
246
+ * example: 'inProgress'
247
+ * title:
248
+ * type: string
249
+ * description: 'The Title Of The Job.'
250
+ * example: 'Sync files to Crowdin'
251
+ * JobResponse:
252
+ * type: object
253
+ * properties:
254
+ * data:
255
+ * type: array
256
+ * items:
257
+ * $ref: '#/components/schemas/Job'
217
258
  *
218
259
  * parameters:
219
260
  * ProjectId:
@@ -71,25 +71,32 @@
71
71
  * - files
72
72
  * properties:
73
73
  * projectId:
74
- * description: 'Project Identifier. Get via [List Projects](https://developer.crowdin.com/api/v2/#operation/api.projects.getMany)'
74
+ * description: 'Project Id. Get via [List Projects](https://developer.crowdin.com/api/v2/#operation/api.projects.getMany)'
75
75
  * type: integer
76
76
  * example: 12
77
77
  * files:
78
+ * example: { 102: ['de', 'fr'], 999: ['uk'] }
78
79
  * type: object
79
- * example:
80
- * 102: ['uk', 'de']
81
- * additionalProperties:
82
- * type: array
83
- * items:
84
- * type: string
80
+ * description: |
81
+ * - **{fileId}** _(integer)_: Crowdin File Id. Get via [List Crowdin Files](#operation/crowdin.files)
82
+ * - **[{languageCode}]** _(array of strings)_: List Of Language Id. Get via [List Supported Languages](https://support.crowdin.com/developer/api/v2/#tag/Languages/operation/api.languages.getMany)
83
+ *
84
+ * **Example:**
85
+ * ```json
86
+ * {
87
+ * 102: ["de", "fr"],
88
+ * 999: ["uk"]
89
+ * }
90
+ * ```
85
91
  * UpdateResponse:
86
92
  * type: object
87
- * items:
88
- * anyOf:
89
- * - properties:
90
- * message:
91
- * type: string
92
- * example: 'File 102 Not Found'
93
+ * properties:
94
+ * jobId:
95
+ * type: string
96
+ * example: '067da473-fc0b-43e3-b0a2-09d26af130c1'
97
+ * message:
98
+ * type: string
99
+ * example: 'Another sync is running'
93
100
  * SettingsData:
94
101
  * type: object
95
102
  * example: {schedule: 0, condition: 0}
@@ -196,8 +203,17 @@
196
203
  * type: integer
197
204
  * example: 86
198
205
  * LoginFieldsResponse:
199
- * example: [{ name: 'email', description: 'User email' }, { name: 'password', description: 'User password' }]
200
- * type: object
206
+ * type: array
207
+ * items:
208
+ * type: object
209
+ * properties:
210
+ * key:
211
+ * type: string
212
+ * example: 'apiKey'
213
+ * name:
214
+ * type: string
215
+ * example: 'Service API key'
216
+ * example: [{ key: 'email', name: 'User email' }, { key: 'password', name: 'User password' }]
201
217
  * Login:
202
218
  * title: 'Login'
203
219
  * required:
@@ -212,9 +228,34 @@
212
228
  * $ref: '#/components/schemas/LoginData'
213
229
  * LoginData:
214
230
  * type: object
231
+ * description: 'Login Form Fields. Get via [Integration Login Form Fields](#operation/integration.fields)'
215
232
  * example: { email: 'user@crowdin.com', password: 'password' }
216
- * additionalProperties:
217
- * type: string
233
+ * Job:
234
+ * type: object
235
+ * properties:
236
+ * id:
237
+ * type: string
238
+ * description: 'The Unique Identifier For The Job.'
239
+ * example: '067da473-fc0b-43e3-b0a2-09d26af130c1'
240
+ * progress:
241
+ * type: integer
242
+ * description: 'The Progress Of The Job.'
243
+ * example: 94
244
+ * status:
245
+ * type: string
246
+ * description: 'The Current Status Of The Job.'
247
+ * example: 'inProgress'
248
+ * title:
249
+ * type: string
250
+ * description: 'The Title Of The Job.'
251
+ * example: 'Sync files to Crowdin'
252
+ * JobResponse:
253
+ * type: object
254
+ * properties:
255
+ * data:
256
+ * type: array
257
+ * items:
258
+ * $ref: '#/components/schemas/Job'
218
259
  *
219
260
  * parameters:
220
261
  * ProjectId:
@@ -1,3 +1,4 @@
1
1
  /// <reference types="qs" />
2
2
  import { Response } from 'express';
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;
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;
@@ -12,9 +12,17 @@ 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() {
15
+ function handle(config) {
16
16
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
17
- const id = req.query.job_id || req.body.job_id;
17
+ var _a;
18
+ 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
+ }
18
26
  if (!id) {
19
27
  req.logInfo('Job id is absent');
20
28
  res.status(400).send('Job id is required');
@@ -35,13 +35,33 @@ function getHumanETA(ms) {
35
35
  }
36
36
  function handle(config) {
37
37
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
38
- const id = req.query.job_id || req.body.job_id;
38
+ var _a;
39
+ 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
+ }
39
47
  if (!id) {
40
48
  req.logInfo('Get active jobs');
41
49
  const jobs = yield (0, storage_1.getStorage)().getActiveJobs({
42
50
  integrationId: req.crowdinContext.clientId,
43
51
  crowdinId: req.crowdinContext.crowdinId,
44
52
  });
53
+ if (isApi && jobs) {
54
+ const filteredJobs = jobs.map((job) => ({
55
+ id: job.id,
56
+ progress: job.progress,
57
+ status: job.status,
58
+ title: job.title,
59
+ }));
60
+ req.logInfo(`Returning active filtered jobs info ${JSON.stringify(filteredJobs, null, 2)}`);
61
+ res.send(filteredJobs);
62
+ return;
63
+ }
64
+ req.logInfo(`Returning active jobs info ${JSON.stringify(jobs, null, 2)}`);
45
65
  res.send(jobs);
46
66
  return;
47
67
  }
@@ -51,6 +71,17 @@ function handle(config) {
51
71
  job.eta = ((Date.now() - job.createdAt) / job.progress) * (100 - job.progress);
52
72
  job.info = getHumanETA(job.eta) + (job.info ? `\n${job.info}` : '');
53
73
  }
74
+ if (isApi && job) {
75
+ const filteredJob = {
76
+ id: job.id,
77
+ progress: job.progress,
78
+ status: job.status,
79
+ title: job.title,
80
+ };
81
+ req.logInfo(`Returning filtered job info ${JSON.stringify(filteredJob, null, 2)}`);
82
+ res.send([filteredJob]);
83
+ return;
84
+ }
54
85
  if (job && (job === null || job === void 0 ? void 0 : job.updatedAt) && Date.now() - job.updatedAt >= MINUTES * 60 * 1000) {
55
86
  const context = req.crowdinContext;
56
87
  const projectId = context.jwtPayload.context.project_id;
@@ -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)());
102
+ }), (0, job_cancel_1.default)(config));
103
103
  app.post('/api/settings', (0, crowdin_client_1.default)({
104
104
  config,
105
105
  optional: false,
@@ -45,8 +45,8 @@ const connection_1 = require("../../../util/connection");
45
45
  const defaults_1 = require("./defaults");
46
46
  const index_1 = require("../../../util/index");
47
47
  const logger_1 = require("../../../util/logger");
48
- const prefetchCount = 20;
49
- const forceProcessDelay = 5000;
48
+ const prefetchCount = 10;
49
+ const forceProcessDelay = 10000;
50
50
  exports.HookEvents = {
51
51
  fileAdded: 'file.added',
52
52
  fileDeleted: 'file.deleted',
@@ -407,7 +407,7 @@ function consumer({ channel, config, integration, }) {
407
407
  webhooksInfo[clientId].data.push(data);
408
408
  }
409
409
  if (messagesCounter < prefetchCount) {
410
- // if all messages are not received, wait 5 seconds to force process messages
410
+ // if all messages are not received, wait 10 seconds to force process messages
411
411
  timeoutId = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
412
412
  yield processMessages({
413
413
  webhooksData,
@@ -436,16 +436,22 @@ function consumer({ channel, config, integration, }) {
436
436
  }
437
437
  function processMessages({ channel, msg, webhooksData, webhooksInfo, }) {
438
438
  return __awaiter(this, void 0, void 0, function* () {
439
- yield Promise.all(webhooksData);
440
- for (const { data, integration, webhookData } of Object.values(webhooksInfo)) {
441
- if (webhookData && webhookData.crowdinClient) {
442
- yield updateCrowdinFromWebhookRequest({
443
- integration: integration,
444
- webhookData: webhookData,
445
- req: data,
446
- });
439
+ try {
440
+ yield Promise.all(webhooksData);
441
+ for (const { data, integration, webhookData } of Object.values(webhooksInfo)) {
442
+ if (webhookData && webhookData.crowdinClient) {
443
+ yield updateCrowdinFromWebhookRequest({
444
+ integration: integration,
445
+ webhookData: webhookData,
446
+ req: data,
447
+ });
448
+ }
447
449
  }
450
+ channel.ack(msg, true);
451
+ }
452
+ catch (e) {
453
+ (0, logger_1.logError)(e);
454
+ channel.nack(msg, false, false);
448
455
  }
449
- channel.ack(msg, true);
450
456
  });
451
457
  }
@@ -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) {
@@ -8,18 +31,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
31
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
32
  });
10
33
  };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
11
37
  Object.defineProperty(exports, "__esModule", { value: true });
12
38
  exports.getStorage = exports.initialize = void 0;
39
+ const types_1 = require("../types");
13
40
  const logger_1 = require("../util/logger");
14
41
  const mysql_1 = require("./mysql");
15
42
  const postgre_1 = require("./postgre");
16
43
  const sqlite_1 = require("./sqlite");
44
+ const path_1 = __importStar(require("path"));
45
+ const fs_1 = __importDefault(require("fs"));
46
+ const child_process_1 = require("child_process");
47
+ const util = __importStar(require("node:util"));
17
48
  let storage;
18
49
  function initialize(config) {
19
50
  return __awaiter(this, void 0, void 0, function* () {
20
51
  if (config.postgreConfig) {
21
52
  (0, logger_1.log)('Using PostgreSQL database');
22
- storage = new postgre_1.PostgreStorage(config.postgreConfig);
53
+ let dumpDirectory = null;
54
+ if (config.migrateToPostgreFromSQLite) {
55
+ dumpDirectory = config.dbFolder;
56
+ createDumpForMigration(config);
57
+ }
58
+ storage = new postgre_1.PostgreStorage(config.postgreConfig, dumpDirectory);
23
59
  }
24
60
  else if (config.mysqlConfig) {
25
61
  (0, logger_1.log)('Using MySQL database');
@@ -27,6 +63,10 @@ function initialize(config) {
27
63
  }
28
64
  else {
29
65
  (0, logger_1.log)('Using SQLite database');
66
+ if (config.migrateToPostgreFromSQLite === false) {
67
+ (0, logger_1.log)('Try to get SQLite file from backup');
68
+ getSqLiteFileFromBackup(config);
69
+ }
30
70
  storage = new sqlite_1.SQLiteStorage({ dbFolder: config.dbFolder });
31
71
  }
32
72
  yield storage.migrate();
@@ -40,3 +80,55 @@ function getStorage() {
40
80
  return storage;
41
81
  }
42
82
  exports.getStorage = getStorage;
83
+ function createDumpForMigration(config) {
84
+ const sqliteFilePath = (0, path_1.join)(config.dbFolder, types_1.storageFiles.SQLITE);
85
+ const backupFilePath = (0, path_1.join)(config.dbFolder, types_1.storageFiles.SQLITE_BACKUP);
86
+ const dumpFilePath = (0, path_1.join)(config.dbFolder, 'dump_sqlite.sql');
87
+ if (!fs_1.default.existsSync(sqliteFilePath)) {
88
+ (0, logger_1.log)('SQLite database not found, skipping migration dump creation');
89
+ return;
90
+ }
91
+ (0, logger_1.log)('Creating dump for migration from SQLite to PostgreSQL');
92
+ (0, child_process_1.execSync)(`sqlite3 ${sqliteFilePath} .dump > ${dumpFilePath}`);
93
+ fs_1.default.renameSync(sqliteFilePath, backupFilePath);
94
+ let modifiedContent = fs_1.default.readFileSync(dumpFilePath).toString();
95
+ // 1. Remove SQLite-specific PRAGMA statements
96
+ modifiedContent = modifiedContent.replace(/PRAGMA foreign_keys=OFF;\n/g, '');
97
+ // 2. Adjust transaction syntax for PostgreSQL
98
+ modifiedContent = modifiedContent.replace(/BEGIN TRANSACTION;\n/g, '');
99
+ modifiedContent = modifiedContent.replace(/COMMIT TRANSACTION;\n/g, '');
100
+ // 3. Ensure tables are only created if they don't already exist
101
+ modifiedContent = modifiedContent
102
+ .replace(/CREATE TABLE IF NOT EXISTS/g, 'CREATE TABLE') // Remove duplicate IF NOT EXISTS
103
+ .replace(/CREATE TABLE/g, 'CREATE TABLE IF NOT EXISTS'); // Add IF NOT EXISTS if missing
104
+ // 4. Add `ON CONFLICT DO NOTHING` to INSERT statements to handle conflicts gracefully
105
+ modifiedContent = modifiedContent.replace(/(INSERT INTO [^;]+)(;)/g, '$1 ON CONFLICT DO NOTHING;');
106
+ // 5. Convert SQLite-specific data types to PostgreSQL equivalents
107
+ modifiedContent = modifiedContent.replace(/integer not null primary key autoincrement/gi, 'serial primary key');
108
+ modifiedContent = modifiedContent.replace(/varchar not null primary key/gi, 'varchar primary key');
109
+ // 6. Remove SQLite-specific function replace()
110
+ modifiedContent = modifiedContent.replace(/replace\s*\('([^']+?)',\s*'?\\[rn]'?\s*,\s*char\s*\([0-9]+\)\s*\)/gi, "'$1'");
111
+ // 7. Remove SQLite-specific sequences
112
+ modifiedContent = modifiedContent.replace(/DELETE FROM sqlite_sequence;\n/g, '');
113
+ modifiedContent = modifiedContent.replace(/INSERT INTO sqlite_sequence .*;\n/g, '');
114
+ // 8. Split the dump into separate statements
115
+ const tables = modifiedContent.split(/(?=CREATE TABLE)/g);
116
+ tables.forEach((tableSql, index) => {
117
+ const match = tableSql.match(/CREATE TABLE IF NOT EXISTS (\w+)/);
118
+ if (match) {
119
+ const dumpFileName = util.format(types_1.storageFiles.DUMP_CHUNK, index);
120
+ (0, logger_1.log)(`Creating dump chunk ${dumpFileName} file for table ${match[1]}`);
121
+ const outputFilePath = path_1.default.join(config.dbFolder, dumpFileName);
122
+ fs_1.default.writeFileSync(outputFilePath, tableSql);
123
+ }
124
+ });
125
+ fs_1.default.unlinkSync(dumpFilePath);
126
+ }
127
+ function getSqLiteFileFromBackup(config) {
128
+ const sqliteFilePath = (0, path_1.join)(config.dbFolder, types_1.storageFiles.SQLITE);
129
+ const backupFilePath = (0, path_1.join)(config.dbFolder, types_1.storageFiles.SQLITE_BACKUP);
130
+ if (fs_1.default.existsSync(backupFilePath) && !fs_1.default.existsSync(sqliteFilePath)) {
131
+ (0, logger_1.log)('Restoring SQLite database from backup');
132
+ fs_1.default.renameSync(backupFilePath, sqliteFilePath);
133
+ }
134
+ }
@@ -18,10 +18,11 @@ export interface PostgreStorageConfig {
18
18
  }
19
19
  export declare class PostgreStorage implements Storage {
20
20
  private config;
21
+ private directoryPath;
21
22
  private _res?;
22
23
  private _rej?;
23
24
  private dbPromise;
24
- constructor(config: PostgreStorageConfig);
25
+ constructor(config: PostgreStorageConfig, directoryPath: string | null);
25
26
  executeQuery<T>(command: (client: Client) => Promise<T>): Promise<T>;
26
27
  migrate(): Promise<void>;
27
28
  alterTables(client: Client): Promise<void>;
@@ -75,4 +76,5 @@ export declare class PostgreStorage implements Storage {
75
76
  getFileTranslationCache({ integrationId, crowdinId, fileId, }: GetFileTranslationCache): Promise<TranslationCache[] | undefined>;
76
77
  getFileTranslationCacheByLanguage({ integrationId, crowdinId, fileId, languageId, }: GetFileTranslationCacheByLanguageParams): Promise<TranslationCache | undefined>;
77
78
  updateTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }: UpdateTranslationCacheParams): Promise<void>;
79
+ private migrateFromSqlite;
78
80
  }
@@ -9,19 +9,26 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  step((generator = generator.apply(thisArg, _arguments || [])).next());
10
10
  });
11
11
  };
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
12
15
  Object.defineProperty(exports, "__esModule", { value: true });
13
16
  exports.PostgreStorage = void 0;
14
17
  const pg_1 = require("pg");
15
18
  const uuid_1 = require("uuid");
16
- const types_1 = require("../modules/integration/util/types");
19
+ const types_1 = require("../types");
20
+ const types_2 = require("../modules/integration/util/types");
17
21
  const util_1 = require("../util");
22
+ const fs_1 = __importDefault(require("fs"));
23
+ const path_1 = require("path");
18
24
  class PostgreStorage {
19
- constructor(config) {
25
+ constructor(config, directoryPath) {
20
26
  this.dbPromise = new Promise((res, rej) => {
21
27
  this._res = res;
22
28
  this._rej = rej;
23
29
  });
24
30
  this.config = config;
31
+ this.directoryPath = directoryPath;
25
32
  }
26
33
  executeQuery(command) {
27
34
  return __awaiter(this, void 0, void 0, function* () {
@@ -48,6 +55,9 @@ class PostgreStorage {
48
55
  migrate() {
49
56
  return __awaiter(this, void 0, void 0, function* () {
50
57
  try {
58
+ if (this.directoryPath && fs_1.default.existsSync(this.directoryPath)) {
59
+ yield this.migrateFromSqlite(this.directoryPath);
60
+ }
51
61
  yield this.executeQuery(this.addTables);
52
62
  this._res && this._res();
53
63
  // TODO: temporary code
@@ -64,6 +74,7 @@ class PostgreStorage {
64
74
  yield this.addColumns(client, ['crowdin_id'], 'app_metadata');
65
75
  yield this.addColumns(client, ['agent_id'], 'crowdin_credentials');
66
76
  yield this.addColumn(client, 'attempt', 'job', 'int default 0');
77
+ yield this.addColumn(client, 'managers', 'integration_credentials', 'varchar NULL');
67
78
  });
68
79
  }
69
80
  addColumns(client, newColumns, tableName) {
@@ -185,7 +196,7 @@ class PostgreStorage {
185
196
  type varchar not null,
186
197
  title varchar null,
187
198
  progress int default 0,
188
- status varchar default '${types_1.JobStatus.CREATED}',
199
+ status varchar default '${types_2.JobStatus.CREATED}',
189
200
  payload varchar null,
190
201
  info varchar null,
191
202
  data varchar null,
@@ -588,7 +599,7 @@ class PostgreStorage {
588
599
  if (status) {
589
600
  updateFields.push('status = $' + updateParams.length.toString());
590
601
  updateParams.push(status);
591
- if ((!progress || progress <= 100) && [types_1.JobStatus.FAILED, types_1.JobStatus.CANCELED].includes(status)) {
602
+ if ((!progress || progress <= 100) && [types_2.JobStatus.FAILED, types_2.JobStatus.CANCELED].includes(status)) {
592
603
  updateFields.push('finished_at = $' + updateParams.length.toString());
593
604
  updateParams.push(Date.now().toString());
594
605
  }
@@ -657,7 +668,7 @@ class PostgreStorage {
657
668
  title, info, data, attempt, created_at as createdAt, updated_at as updatedAt, finished_at as finishedAt
658
669
  FROM job
659
670
  WHERE status IN ($1, $2) AND finished_at is NULL
660
- `, [types_1.JobStatus.IN_PROGRESS, types_1.JobStatus.CREATED]);
671
+ `, [types_2.JobStatus.IN_PROGRESS, types_2.JobStatus.CREATED]);
661
672
  return (res === null || res === void 0 ? void 0 : res.rows) || [];
662
673
  }));
663
674
  });
@@ -708,5 +719,27 @@ class PostgreStorage {
708
719
  `, [etag, integrationId, crowdinId, fileId, languageId]));
709
720
  });
710
721
  }
722
+ migrateFromSqlite(directoryPath) {
723
+ return __awaiter(this, void 0, void 0, function* () {
724
+ const [name, extension] = types_1.storageFiles.DUMP_CHUNK.split('%d');
725
+ const files = fs_1.default
726
+ .readdirSync(directoryPath)
727
+ .filter((file) => file.startsWith(name) && file.endsWith(extension))
728
+ .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
729
+ for (const file of files) {
730
+ const filePath = (0, path_1.join)(directoryPath, file);
731
+ const sql = fs_1.default.readFileSync(filePath, 'utf8');
732
+ try {
733
+ yield this.executeQuery((client) => client.query(sql));
734
+ fs_1.default.unlinkSync(filePath);
735
+ }
736
+ catch (e) {
737
+ console.error('Error while executing', file);
738
+ console.error(e);
739
+ fs_1.default.renameSync(filePath, filePath.replace('dump_chunk_', 'error_dump_chunk_'));
740
+ }
741
+ }
742
+ });
743
+ }
711
744
  }
712
745
  exports.PostgreStorage = PostgreStorage;
@@ -16,7 +16,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.SQLiteStorage = void 0;
17
17
  const path_1 = require("path");
18
18
  const uuid_1 = require("uuid");
19
- const types_1 = require("../modules/integration/util/types");
19
+ const types_1 = require("../types");
20
+ const types_2 = require("../modules/integration/util/types");
20
21
  class SQLiteStorage {
21
22
  constructor(config) {
22
23
  this.dbPromise = new Promise((res, rej) => {
@@ -132,6 +133,7 @@ class SQLiteStorage {
132
133
  yield this.addColumns(['app_secret', 'domain', 'user_id', 'agent_id', 'organization_id', 'base_url'], 'crowdin_credentials');
133
134
  yield this.addColumns(['crowdin_id'], 'app_metadata');
134
135
  yield this.addColumn('job', 'attempt', 'DEFAULT 0');
136
+ yield this.addColumn('managers', 'integration_credentials', 'null');
135
137
  });
136
138
  }
137
139
  moveIntegrationSettings() {
@@ -175,7 +177,7 @@ class SQLiteStorage {
175
177
  });
176
178
  const sqlite = require('sqlite3');
177
179
  //@ts-ignore
178
- this.db = new sqlite.Database((0, path_1.join)(this.config.dbFolder, 'app.sqlite'), (error) => {
180
+ this.db = new sqlite.Database((0, path_1.join)(this.config.dbFolder, types_1.storageFiles.SQLITE), (error) => {
179
181
  if (error) {
180
182
  _connection_rej(error);
181
183
  }
@@ -279,7 +281,7 @@ class SQLiteStorage {
279
281
  type varchar not null,
280
282
  title varchar null,
281
283
  progress integer DEFAULT 0,
282
- status varchar DEFAULT '${types_1.JobStatus.CREATED}',
284
+ status varchar DEFAULT '${types_2.JobStatus.CREATED}',
283
285
  payload varchar null,
284
286
  info varchar null,
285
287
  data varchar null,
@@ -583,7 +585,7 @@ class SQLiteStorage {
583
585
  if (status) {
584
586
  updateFields.push('status = ?');
585
587
  updateParams.push(status);
586
- if (!updateFields.includes('finished_at = ?') && [types_1.JobStatus.FAILED, types_1.JobStatus.CANCELED].includes(status)) {
588
+ if (!updateFields.includes('finished_at = ?') && [types_2.JobStatus.FAILED, types_2.JobStatus.CANCELED].includes(status)) {
587
589
  updateFields.push('finished_at = ?');
588
590
  updateParams.push(Date.now().toString());
589
591
  }
@@ -641,7 +643,7 @@ class SQLiteStorage {
641
643
  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
642
644
  FROM job
643
645
  WHERE status IN (?,?) AND finished_at is NULL
644
- `, [types_1.JobStatus.IN_PROGRESS, types_1.JobStatus.CREATED]);
646
+ `, [types_2.JobStatus.IN_PROGRESS, types_2.JobStatus.CREATED]);
645
647
  });
646
648
  }
647
649
  saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag }) {
package/out/types.d.ts CHANGED
@@ -82,6 +82,10 @@ export interface ClientConfig extends ImagePath {
82
82
  * folder where module will create sqlite db file to persist credentials (e.g. {@example __dirname})
83
83
  */
84
84
  dbFolder?: string;
85
+ /**
86
+ * migrate from SQLite to PostgreSQL
87
+ */
88
+ migrateToPostgreFromSQLite?: boolean;
85
89
  /**
86
90
  * config to configure PostgreSQL as a storage
87
91
  */
@@ -449,4 +453,9 @@ export interface SignaturePatterns {
449
453
  fileName?: string;
450
454
  fileContent?: string;
451
455
  }
456
+ export declare enum storageFiles {
457
+ SQLITE = "app.sqlite",
458
+ SQLITE_BACKUP = "backup_app.sqlite",
459
+ DUMP_CHUNK = "dump_chunk_%d.sql"
460
+ }
452
461
  export {};
package/out/types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ProjectPermissions = exports.UserPermissions = exports.EditorMode = exports.SubscriptionInfoType = exports.AccountType = exports.Scope = exports.AuthenticationType = void 0;
3
+ exports.storageFiles = exports.ProjectPermissions = exports.UserPermissions = exports.EditorMode = exports.SubscriptionInfoType = exports.AccountType = exports.Scope = exports.AuthenticationType = void 0;
4
4
  var AuthenticationType;
5
5
  (function (AuthenticationType) {
6
6
  AuthenticationType["CODE"] = "authorization_code";
@@ -66,3 +66,9 @@ var ProjectPermissions;
66
66
  ProjectPermissions["OWN"] = "own";
67
67
  ProjectPermissions["RESTRICTED"] = "restricted";
68
68
  })(ProjectPermissions = exports.ProjectPermissions || (exports.ProjectPermissions = {}));
69
+ var storageFiles;
70
+ (function (storageFiles) {
71
+ storageFiles["SQLITE"] = "app.sqlite";
72
+ storageFiles["SQLITE_BACKUP"] = "backup_app.sqlite";
73
+ storageFiles["DUMP_CHUNK"] = "dump_chunk_%d.sql";
74
+ })(storageFiles = exports.storageFiles || (exports.storageFiles = {}));
@@ -14,3 +14,4 @@ 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;
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.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.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;
36
36
  const crypto = __importStar(require("crypto-js"));
37
37
  const storage_1 = require("../storage");
38
38
  const types_1 = require("../types");
@@ -162,3 +162,8 @@ 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;
@@ -573,16 +573,14 @@
573
573
  } else if (tree.length) {
574
574
  appComponent.pushIntegrationFilesData(tree).then(() => {
575
575
  if (parentId) {
576
- appIntegrationFiles.getSelected().then(selection => {
577
- const selectedIds = selection?.filter((node) => node).map(({id}) => id.toString());
578
- if (!selectedIds?.length) {
579
- return;
580
- }
581
- const filteredNodes = tree.filter((node) => selectedIds.includes(node.parent_id.toString()));
582
- filteredNodes.forEach((node) => {
583
- selectedIds.push(node.id);
584
- });
585
- appIntegrationFiles.setSelected(selectedIds);
576
+ appIntegrationFiles.getSelected().then(async selection => {
577
+ const selectedIds = selection?.filter((node) => node).map(({id}) => id.toString());
578
+ const filteredNodes = tree.filter((node) => !selectedIds.includes(node.id.toString()) && selectedIds.includes(node.parent_id.toString()));
579
+
580
+ if (filteredNodes?.length) {
581
+ const filteredNodesId = filteredNodes.map(({id}) => id.toString());
582
+ await appIntegrationFiles.setSelected(filteredNodesId);
583
+ }
586
584
  });
587
585
  }
588
586
  });
@@ -726,7 +724,7 @@
726
724
  }
727
725
 
728
726
  checkOrigin()
729
- .then(restParams => fetch('api/jobs' + restParams + '&job_id=' + jobId, {
727
+ .then(restParams => fetch('api/jobs' + restParams + '&jobId=' + jobId, {
730
728
  method: 'DELETE',
731
729
  headers: { 'Content-Type': 'application/json' },
732
730
  }))
@@ -767,7 +765,7 @@
767
765
  }
768
766
 
769
767
  checkOrigin()
770
- .then(restParams => fetch('api/jobs' + restParams + '&job_id=' + jobId))
768
+ .then(restParams => fetch('api/jobs' + restParams + '&jobId=' + jobId))
771
769
  .then(checkResponse)
772
770
  .then((job) => {
773
771
  const isFailed = [JOB_STATUS.failed, JOB_STATUS.canceled].includes(job.status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.74.0",
3
+ "version": "0.75.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",