@crowdin/app-project-module 0.60.2 → 0.61.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.
@@ -106,6 +106,8 @@ const updateCrowdinTest = ({ appConfig, integrationTestConfig, }) => __awaiter(v
106
106
  },
107
107
  update: updateProgressMock,
108
108
  type: types_1.JobClientType.MANUAL,
109
+ fetchTranslation: jest.fn(),
110
+ translationUploaded: jest.fn(),
109
111
  },
110
112
  });
111
113
  }, 'Fail to run method updateCrowdin()');
@@ -63,6 +63,8 @@ const updateIntegrationTest = ({ appConfig, integrationTestConfig, }) => __await
63
63
  },
64
64
  update: updateProgressMock,
65
65
  type: types_1.JobClientType.MANUAL,
66
+ fetchTranslation: jest.fn(),
67
+ translationUploaded: jest.fn(),
66
68
  },
67
69
  });
68
70
  }, 'Fail to run method updateIntegration()');
@@ -40,6 +40,8 @@ function handle(config, integration) {
40
40
  title: 'Sync files to Crowdin',
41
41
  payload: req.body,
42
42
  res,
43
+ projectId: projectId,
44
+ client: req.crowdinApiClient,
43
45
  jobType: types_1.JobClientType.MANUAL,
44
46
  jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
45
47
  var _c;
@@ -34,6 +34,8 @@ function handle(config, integration) {
34
34
  title: 'Sync files to ' + config.name,
35
35
  payload: req.body,
36
36
  res,
37
+ projectId: req.crowdinContext.jwtPayload.context.project_id,
38
+ client: req.crowdinApiClient,
37
39
  jobType: types_1.JobClientType.MANUAL,
38
40
  jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
39
41
  const result = yield integration.updateIntegration({
@@ -19,52 +19,51 @@ const logger_1 = require("../../../util/logger");
19
19
  const storage_1 = require("../../../storage");
20
20
  function handle(config, integration) {
21
21
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
22
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
22
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
23
23
  const message = {
24
24
  uid: 'oauth_popup',
25
25
  };
26
26
  const code = req.query[((_b = (_a = integration.oauthLogin) === null || _a === void 0 ? void 0 : _a.fieldsMapping) === null || _b === void 0 ? void 0 : _b.code) || 'code'];
27
- const state = ((_c = integration.oauthLogin) === null || _c === void 0 ? void 0 : _c.mode) === 'polling'
28
- ? req.query[((_e = (_d = integration.oauthLogin) === null || _d === void 0 ? void 0 : _d.fieldsMapping) === null || _e === void 0 ? void 0 : _e.state) || 'state']
29
- : undefined;
27
+ const state = req.query[((_d = (_c = integration.oauthLogin) === null || _c === void 0 ? void 0 : _c.fieldsMapping) === null || _d === void 0 ? void 0 : _d.state) || 'state'];
30
28
  (0, logger_1.log)(`Received request from OAuth login callback. Code ${code}`);
31
- if (state) {
32
- (0, logger_1.log)(`Received request from OAuth login callback. State ${state}`);
33
- }
29
+ (0, logger_1.log)(`Received request from OAuth login callback. State ${state}`);
30
+ const clientId = Buffer.from(state, 'base64').toString();
31
+ const redirectUri = `${config.baseUrl}${(0, defaults_1.getOauthRoute)(integration)}`;
34
32
  try {
35
33
  const oauthLogin = integration.oauthLogin;
36
34
  let credentials;
37
35
  if (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.performGetTokenRequest) {
38
36
  (0, logger_1.log)('Performing custom get bearer token request');
39
- credentials = yield oauthLogin.performGetTokenRequest(code, req.query, req.originalUrl);
37
+ const loginForm = yield (0, storage_1.getStorage)().getMetadata((0, defaults_1.getOAuthLoginFormId)(clientId));
38
+ credentials = yield oauthLogin.performGetTokenRequest(code, req.query, req.originalUrl, redirectUri, loginForm);
40
39
  }
41
40
  else {
42
41
  const request = {};
43
42
  const oauthLogin = integration.oauthLogin;
44
- request[((_f = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _f === void 0 ? void 0 : _f.code) || 'code'] = code;
45
- request[((_g = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _g === void 0 ? void 0 : _g.clientId) || 'client_id'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientId;
46
- request[((_h = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _h === void 0 ? void 0 : _h.clientSecret) || 'client_secret'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientSecret;
47
- request[((_j = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _j === void 0 ? void 0 : _j.redirectUri) || 'redirect_uri'] = `${config.baseUrl}${(0, defaults_1.getOauthRoute)(integration)}`;
43
+ request[((_e = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _e === void 0 ? void 0 : _e.code) || 'code'] = code;
44
+ request[((_f = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _f === void 0 ? void 0 : _f.clientId) || 'client_id'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientId;
45
+ request[((_g = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _g === void 0 ? void 0 : _g.clientSecret) || 'client_secret'] = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.clientSecret;
46
+ request[((_h = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _h === void 0 ? void 0 : _h.redirectUri) || 'redirect_uri'] = redirectUri;
48
47
  if (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraAccessTokenParameters) {
49
48
  Object.entries(oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.extraAccessTokenParameters).forEach(([key, value]) => (request[key] = value));
50
49
  }
51
- credentials = (yield axios_1.default.post(((_k = integration.oauthLogin) === null || _k === void 0 ? void 0 : _k.accessTokenUrl) || '', request, {
50
+ credentials = (yield axios_1.default.post(((_j = integration.oauthLogin) === null || _j === void 0 ? void 0 : _j.accessTokenUrl) || '', request, {
52
51
  headers: { Accept: 'application/json' },
53
52
  })).data;
54
53
  }
55
54
  const oauthCredentials = { originalUrl: req.originalUrl };
56
- oauthCredentials.accessToken = credentials[((_l = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _l === void 0 ? void 0 : _l.accessToken) || 'access_token'];
55
+ oauthCredentials.accessToken = credentials[((_k = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _k === void 0 ? void 0 : _k.accessToken) || 'access_token'];
57
56
  if (oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.refresh) {
58
- oauthCredentials.refreshToken = credentials[((_m = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _m === void 0 ? void 0 : _m.refreshToken) || 'refresh_token'];
57
+ oauthCredentials.refreshToken = credentials[((_l = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _l === void 0 ? void 0 : _l.refreshToken) || 'refresh_token'];
59
58
  oauthCredentials.expireIn =
60
- Number(credentials[((_o = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _o === void 0 ? void 0 : _o.expiresIn) || 'expires_in']) + Date.now() / 1000;
59
+ Number(credentials[((_m = oauthLogin === null || oauthLogin === void 0 ? void 0 : oauthLogin.fieldsMapping) === null || _m === void 0 ? void 0 : _m.expiresIn) || 'expires_in']) + Date.now() / 1000;
61
60
  }
62
61
  message.data = oauthCredentials;
63
- if (((_p = integration.oauthLogin) === null || _p === void 0 ? void 0 : _p.mode) === 'polling' && state) {
64
- yield (0, storage_1.getStorage)().deleteMetadata(state);
65
- yield (0, storage_1.getStorage)().saveMetadata(state, oauthCredentials);
62
+ if (((_o = integration.oauthLogin) === null || _o === void 0 ? void 0 : _o.mode) === 'polling') {
63
+ yield (0, storage_1.getStorage)().deleteMetadata((0, defaults_1.getOAuthPollingId)(clientId));
64
+ yield (0, storage_1.getStorage)().saveMetadata((0, defaults_1.getOAuthPollingId)(clientId), oauthCredentials);
66
65
  }
67
- return res.render('oauth', { message: JSON.stringify(message), oauthMode: (_q = integration.oauthLogin) === null || _q === void 0 ? void 0 : _q.mode });
66
+ return res.render('oauth', { message: JSON.stringify(message), oauthMode: (_p = integration.oauthLogin) === null || _p === void 0 ? void 0 : _p.mode });
68
67
  }
69
68
  catch (e) {
70
69
  (0, logger_1.logError)(e);
@@ -33,6 +33,8 @@ function handle(config, integration) {
33
33
  title: 'Save sync settings',
34
34
  payload: req.body,
35
35
  res,
36
+ projectId: req.crowdinContext.jwtPayload.context.project_id,
37
+ client: req.crowdinApiClient,
36
38
  jobType: types_1.JobClientType.MANUAL,
37
39
  jobCallback: () => __awaiter(this, void 0, void 0, function* () {
38
40
  if (Array.isArray(expandIntegrationFolders) && expandIntegrationFolders.length) {
@@ -233,7 +233,7 @@ export interface OAuthLogin {
233
233
  */
234
234
  expiresIn?: string;
235
235
  /**
236
- * default 'state', used for `polling' mode
236
+ * default 'state'
237
237
  */
238
238
  state?: string;
239
239
  };
@@ -264,7 +264,7 @@ export interface OAuthLogin {
264
264
  */
265
265
  performGetTokenRequest?: (code: string, query: {
266
266
  [key: string]: any;
267
- }, url: string, loginForm?: any) => Promise<any>;
267
+ }, url: string, redirectUri: string, loginForm?: any) => Promise<any>;
268
268
  /**
269
269
  * Override to implement request for refreshing token (only if 'refresh' is enabled)
270
270
  */
@@ -233,6 +233,8 @@ function filesCron({ config, integration, period, }) {
233
233
  title: `Sync files to ${config.name} [scheduled]`,
234
234
  payload: filesToProcess,
235
235
  jobType: types_2.JobClientType.CRON,
236
+ projectId: projectId,
237
+ client: crowdinClient,
236
238
  jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
237
239
  yield integration.updateIntegration({
238
240
  projectId,
@@ -283,6 +285,8 @@ function filesCron({ config, integration, period, }) {
283
285
  title: 'Sync files to Crowdin [scheduled]',
284
286
  payload: intFiles,
285
287
  jobType: types_2.JobClientType.CRON,
288
+ projectId: projectId,
289
+ client: crowdinClient,
286
290
  jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
287
291
  yield integration.updateCrowdin({
288
292
  projectId,
@@ -268,9 +268,7 @@ function constructOauthUrl({ config, integration, clientId, loginForm, }) {
268
268
  return;
269
269
  }
270
270
  let url = oauth.getAuthorizationUrl(`${config.baseUrl}${getOauthRoute(integration)}`, loginForm);
271
- if (oauth.mode === 'polling') {
272
- url += `&${((_a = oauth.fieldsMapping) === null || _a === void 0 ? void 0 : _a.state) || 'state'}=${getOAuthPollingId(clientId)}`;
273
- }
271
+ url += `&${((_a = oauth.fieldsMapping) === null || _a === void 0 ? void 0 : _a.state) || 'state'}=${Buffer.from(clientId).toString('base64')}`;
274
272
  return url;
275
273
  }
276
274
  if (!oauth.authorizationUrl) {
@@ -279,11 +277,9 @@ function constructOauthUrl({ config, integration, clientId, loginForm, }) {
279
277
  let url = oauth.authorizationUrl || '';
280
278
  url += `?${((_b = oauth.fieldsMapping) === null || _b === void 0 ? void 0 : _b.clientId) || 'client_id'}=${oauth.clientId}`;
281
279
  url += `&${((_c = oauth.fieldsMapping) === null || _c === void 0 ? void 0 : _c.redirectUri) || 'redirect_uri'}=${config.baseUrl}${getOauthRoute(integration)}`;
280
+ url += `&${((_d = oauth.fieldsMapping) === null || _d === void 0 ? void 0 : _d.state) || 'state'}=${Buffer.from(clientId).toString('base64')}`;
282
281
  if (oauth.scope) {
283
- url += `&${((_d = oauth.fieldsMapping) === null || _d === void 0 ? void 0 : _d.scope) || 'scope'}=${oauth.scope}`;
284
- }
285
- if (oauth.mode === 'polling') {
286
- url += `&${((_e = oauth.fieldsMapping) === null || _e === void 0 ? void 0 : _e.state) || 'state'}=${getOAuthPollingId(clientId)}`;
282
+ url += `&${((_e = oauth.fieldsMapping) === null || _e === void 0 ? void 0 : _e.scope) || 'scope'}=${oauth.scope}`;
287
283
  }
288
284
  if (oauth.extraAutorizationUrlParameters) {
289
285
  Object.entries(oauth.extraAutorizationUrlParameters).forEach(([key, value]) => (url += `&${key}=${value}`));
@@ -1,12 +1,15 @@
1
1
  import { JobClient, JobClientType, JobType } from './types';
2
2
  import { Response } from 'express';
3
- export declare function runAsJob({ integrationId, crowdinId, type, title, payload, res, jobType, jobCallback, onError, }: {
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
5
  integrationId: string;
5
6
  crowdinId: string;
6
7
  type: JobType;
7
8
  title?: string;
8
9
  payload?: any;
9
10
  res?: Response;
11
+ projectId: number;
12
+ client: Crowdin;
10
13
  jobType: JobClientType;
11
14
  jobCallback: (arg1: JobClient) => Promise<any>;
12
15
  onError?: (e: any) => Promise<void>;
@@ -19,7 +19,7 @@ const blockingJobs = {
19
19
  [types_1.JobType.CROWDIN_SYNC_SETTINGS_SAVE]: [types_1.JobType.CROWDIN_SYNC_SETTINGS_SAVE],
20
20
  [types_1.JobType.INTEGRATION_SYNC_SETTINGS_SAVE]: [types_1.JobType.INTEGRATION_SYNC_SETTINGS_SAVE],
21
21
  };
22
- function runAsJob({ integrationId, crowdinId, type, title, payload, res, jobType, jobCallback, onError, }) {
22
+ function runAsJob({ integrationId, crowdinId, type, title, payload, res, projectId, client, jobType, jobCallback, onError, }) {
23
23
  return __awaiter(this, void 0, void 0, function* () {
24
24
  const storage = (0, storage_1.getStorage)();
25
25
  const activeJobs = yield storage.getActiveJobs({ integrationId, crowdinId });
@@ -66,6 +66,44 @@ function runAsJob({ integrationId, crowdinId, type, title, payload, res, jobType
66
66
  });
67
67
  },
68
68
  type: jobType,
69
+ fetchTranslation: ({ fileId, languageId }) => __awaiter(this, void 0, void 0, function* () {
70
+ const translationCache = yield storage.getTranslationCache({
71
+ integrationId,
72
+ crowdinId,
73
+ fileId,
74
+ languageId,
75
+ });
76
+ if (!translationCache) {
77
+ yield storage.saveTranslationCache({ integrationId, crowdinId, fileId, languageId });
78
+ }
79
+ let translation = null;
80
+ try {
81
+ (0, logger_1.log)(`Receiving translation for file ${fileId} in language ${languageId}`);
82
+ translation = yield client.translationsApi.buildProjectFileTranslation(projectId, fileId, {
83
+ targetLanguageId: languageId,
84
+ }, (translationCache === null || translationCache === void 0 ? void 0 : translationCache.etag) ? translationCache === null || translationCache === void 0 ? void 0 : translationCache.etag : undefined);
85
+ return translation;
86
+ }
87
+ catch (e) {
88
+ (0, logger_1.log)(`Unable to get translation for file ${fileId} in language ${languageId}`);
89
+ }
90
+ return translation;
91
+ }),
92
+ translationUploaded: ({ fileId, languageId, etag }) => __awaiter(this, void 0, void 0, function* () {
93
+ const translationCache = yield storage.getTranslationCache({
94
+ integrationId,
95
+ crowdinId,
96
+ fileId,
97
+ languageId,
98
+ });
99
+ (0, logger_1.log)(`Saving etag translation for file ${fileId} in language ${languageId}`);
100
+ if (!translationCache) {
101
+ yield storage.saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag });
102
+ }
103
+ else {
104
+ yield storage.updateTranslationCache({ integrationId, crowdinId, fileId, languageId, etag });
105
+ }
106
+ }),
69
107
  };
70
108
  try {
71
109
  const data = yield jobCallback(job);
@@ -1,6 +1,8 @@
1
1
  import Crowdin, { ProjectsGroupsModel } from '@crowdin/crowdin-api-client';
2
2
  import { Config } from '../../../types';
3
3
  import { IntegrationLogic, IntegrationSyncSettings } from '../types';
4
+ import { ResponseObject } from '@crowdin/crowdin-api-client/out/core';
5
+ import { TranslationsModel } from '@crowdin/crowdin-api-client/out/translations';
4
6
  export declare enum JobType {
5
7
  UPDATE_TO_CROWDIN = "updateCrowdin",
6
8
  UPDATE_TO_INTEGRATION = "updateIntegration",
@@ -48,6 +50,8 @@ export type JobClient = {
48
50
  get: () => Promise<Job | undefined>;
49
51
  update: UpdateJobProgress;
50
52
  type: JobClientType;
53
+ translationUploaded: SaveUploadedFileTranslation;
54
+ fetchTranslation: FetchTranslation;
51
55
  };
52
56
  export type UpdateJobProgress = ({ progress, status, info, data, }: Omit<UpdateJobParams, 'id'>) => Promise<{
53
57
  isCanceled: boolean;
@@ -63,3 +67,21 @@ export interface GetAllNewFilesArgs {
63
67
  integrationSettings: any;
64
68
  syncSettings: IntegrationSyncSettings;
65
69
  }
70
+ export type SaveUploadedFileTranslation = ({ fileId, languageId, etag, }: {
71
+ fileId: number;
72
+ languageId: string;
73
+ etag: string;
74
+ }) => Promise<void>;
75
+ export type FetchTranslation = ({ fileId, languageId, }: {
76
+ fileId: number;
77
+ languageId: string;
78
+ }) => Promise<ResponseObject<TranslationsModel.BuildProjectFileTranslationResponse> | null>;
79
+ export interface TranslationCache {
80
+ integrationId: string;
81
+ crowdinId: string;
82
+ fileId: number;
83
+ languageId: string;
84
+ etag?: string;
85
+ }
86
+ export type GetTranslationCacheParams = Pick<TranslationCache, 'integrationId' | 'crowdinId' | 'fileId' | 'languageId'>;
87
+ export type UpdateTranslationCacheParams = Pick<TranslationCache, 'integrationId' | 'crowdinId' | 'fileId' | 'languageId' | 'etag'>;
@@ -1,6 +1,6 @@
1
1
  import { IntegrationConfig, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks, Provider } from '../modules/integration/types';
2
2
  import { Config, CrowdinCredentials, UnauthorizedConfig } from '../types';
3
- import { CreateJobParams, GetActiveJobsParams, GetJobParams, Job, UpdateJobParams } from '../modules/integration/util/types';
3
+ import { CreateJobParams, GetActiveJobsParams, GetJobParams, GetTranslationCacheParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams } from '../modules/integration/util/types';
4
4
  import { UserErrors } from './types';
5
5
  export interface Storage {
6
6
  migrate(): Promise<void>;
@@ -43,6 +43,9 @@ export interface Storage {
43
43
  getJob(params: GetJobParams): Promise<Job | undefined>;
44
44
  getActiveJobs(params: GetActiveJobsParams): Promise<Job[] | undefined>;
45
45
  deleteFinishedJobs(): Promise<void>;
46
+ saveTranslationCache(params: TranslationCache): Promise<void>;
47
+ getTranslationCache(params: GetTranslationCacheParams): Promise<TranslationCache | undefined>;
48
+ updateTranslationCache(params: UpdateTranslationCacheParams): Promise<void>;
46
49
  }
47
50
  export declare function initialize(config: Config | UnauthorizedConfig): Promise<void>;
48
51
  export declare function getStorage(): Storage;
@@ -1,6 +1,6 @@
1
1
  import { Storage } from '.';
2
2
  import { CrowdinCredentials } from '../types';
3
- import { CreateJobParams, GetActiveJobsParams, GetJobParams, Job, UpdateJobParams } from '../modules/integration/util/types';
3
+ import { CreateJobParams, GetActiveJobsParams, GetJobParams, GetTranslationCacheParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams } from '../modules/integration/util/types';
4
4
  import { IntegrationConfig, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks } from '../modules/integration/types';
5
5
  import { UserErrors } from './types';
6
6
  export interface MySQLStorageConfig {
@@ -60,4 +60,7 @@ export declare class MySQLStorage implements Storage {
60
60
  getJob({ id }: GetJobParams): Promise<Job | undefined>;
61
61
  getActiveJobs({ integrationId, crowdinId }: GetActiveJobsParams): Promise<Job[] | undefined>;
62
62
  deleteFinishedJobs(): Promise<void>;
63
+ saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }: TranslationCache): Promise<void>;
64
+ getTranslationCache({ integrationId, crowdinId, fileId, languageId, }: GetTranslationCacheParams): Promise<TranslationCache | undefined>;
65
+ updateTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }: UpdateTranslationCacheParams): Promise<void>;
63
66
  }
@@ -166,6 +166,17 @@ class MySQLStorage {
166
166
  finished_at varchar(255)
167
167
  )
168
168
  `);
169
+ yield connection.execute(`
170
+ create table if not exists translation_file_cache
171
+ (
172
+ id int auto_increment primary key,
173
+ integration_id varchar(255) not null,
174
+ crowdin_id varchar(255) not null,
175
+ file_id int not null,
176
+ language_id varchar(255) not null,
177
+ etag varchar(255)
178
+ )
179
+ `);
169
180
  });
170
181
  }
171
182
  saveCrowdinCredentials(credentials) {
@@ -234,6 +245,7 @@ class MySQLStorage {
234
245
  yield connection.execute('DELETE FROM user_errors WHERE crowdin_id = ?', [id]);
235
246
  yield connection.execute('DELETE FROM integration_settings WHERE crowdin_id = ?', [id]);
236
247
  yield connection.execute('DELETE FROM job WHERE crowdin_id = ?', [id]);
248
+ yield connection.execute('DELETE FROM translation_file_cache WHERE crowdin_id = ?', [id]);
237
249
  }));
238
250
  });
239
251
  }
@@ -577,5 +589,37 @@ class MySQLStorage {
577
589
  yield this.executeQuery((connection) => connection.execute('DELETE FROM job WHERE finished_at is not NULL', []));
578
590
  });
579
591
  }
592
+ saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }) {
593
+ return __awaiter(this, void 0, void 0, function* () {
594
+ yield this.dbPromise;
595
+ yield this.executeQuery((connection) => connection.execute(`
596
+ INSERT INTO translation_file_cache(integration_id, crowdin_id, file_id, language_id, etag)
597
+ VALUES (?, ?, ?, ?, ?)
598
+ `, [integrationId, crowdinId, fileId, languageId, etag]));
599
+ });
600
+ }
601
+ getTranslationCache({ integrationId, crowdinId, fileId, languageId, }) {
602
+ return __awaiter(this, void 0, void 0, function* () {
603
+ yield this.dbPromise;
604
+ return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
605
+ const [rows] = yield connection.execute(`
606
+ SELECT integration_id as integrationId, crowdin_id as crowdin_id, file_id as fileId, language_id as languageId
607
+ FROM translation_file_cache
608
+ WHERE integration_id = ? AND crowdin_id = ? AND file_id = ? AND language_id = ?
609
+ `, [integrationId, crowdinId, fileId, languageId]);
610
+ return (rows || [])[0];
611
+ }));
612
+ });
613
+ }
614
+ updateTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }) {
615
+ return __awaiter(this, void 0, void 0, function* () {
616
+ yield this.dbPromise;
617
+ yield this.executeQuery((connection) => connection.execute(`
618
+ UPDATE translation_file_cache
619
+ SET etag = ?
620
+ WHERE integration_id = ? AND crowdin_id = ? AND file_id = ? AND language_id = ?
621
+ `, [etag, integrationId, crowdinId, fileId, languageId]));
622
+ });
623
+ }
580
624
  }
581
625
  exports.MySQLStorage = MySQLStorage;
@@ -2,7 +2,7 @@ import { Client } from 'pg';
2
2
  import { Storage } from '.';
3
3
  import { CrowdinCredentials } from '../types';
4
4
  import { IntegrationConfig, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks } from '../modules/integration/types';
5
- import { CreateJobParams, GetActiveJobsParams, GetJobParams, Job, UpdateJobParams } from '../modules/integration/util/types';
5
+ import { CreateJobParams, GetActiveJobsParams, GetJobParams, GetTranslationCacheParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams } from '../modules/integration/util/types';
6
6
  import { UserErrors } from './types';
7
7
  export interface PostgreStorageConfig {
8
8
  host?: string;
@@ -66,4 +66,7 @@ export declare class PostgreStorage implements Storage {
66
66
  getJob({ id }: GetJobParams): Promise<Job | undefined>;
67
67
  getActiveJobs({ integrationId, crowdinId }: GetActiveJobsParams): Promise<Job[] | undefined>;
68
68
  deleteFinishedJobs(): Promise<void>;
69
+ saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }: TranslationCache): Promise<void>;
70
+ getTranslationCache({ integrationId, crowdinId, fileId, languageId, }: GetTranslationCacheParams): Promise<TranslationCache | undefined>;
71
+ updateTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }: UpdateTranslationCacheParams): Promise<void>;
69
72
  }
@@ -183,6 +183,17 @@ class PostgreStorage {
183
183
  finished_at varchar null
184
184
  )
185
185
  `);
186
+ yield client.query(`
187
+ create table if not exists translation_file_cache
188
+ (
189
+ id serial primary key,
190
+ integration_id varchar not null,
191
+ crowdin_id varchar not null,
192
+ file_id int not null,
193
+ language_id varchar not null,
194
+ etag varchar
195
+ )
196
+ `);
186
197
  });
187
198
  }
188
199
  saveCrowdinCredentials(credentials) {
@@ -251,6 +262,7 @@ class PostgreStorage {
251
262
  yield client.query('DELETE FROM user_errors WHERE crowdin_id = $1', [id]);
252
263
  yield client.query('DELETE FROM integration_settings WHERE crowdin_id = $1', [id]);
253
264
  yield client.query('DELETE FROM job WHERE crowdin_id = $1', [id]);
265
+ yield client.query('DELETE FROM translation_file_cache WHERE crowdin_id = $1', [id]);
254
266
  }));
255
267
  });
256
268
  }
@@ -597,5 +609,38 @@ class PostgreStorage {
597
609
  yield this.executeQuery((client) => client.query(' DELETE FROM job WHERE finished_at is not NULL', []));
598
610
  });
599
611
  }
612
+ saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }) {
613
+ return __awaiter(this, void 0, void 0, function* () {
614
+ yield this.dbPromise;
615
+ yield this.executeQuery((client) => client.query(`
616
+ INSERT
617
+ INTO translation_file_cache(integration_id, crowdin_id, file_id, language_id, etag)
618
+ VALUES ($1, $2, $3, $4, $4)
619
+ `, [integrationId, crowdinId, fileId, languageId, etag]));
620
+ });
621
+ }
622
+ getTranslationCache({ integrationId, crowdinId, fileId, languageId, }) {
623
+ return __awaiter(this, void 0, void 0, function* () {
624
+ yield this.dbPromise;
625
+ return this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
626
+ const res = yield client.query(`
627
+ SELECT integration_id as integrationId, crowdin_id as crowdinId, file_id as fileId, language_id as languageId
628
+ FROM translation_file_cache
629
+ WHERE integration_id = $1 AND crowdin_id = $2 AND file_id = $3 AND language_id = $4
630
+ `, [integrationId, crowdinId, fileId, languageId]);
631
+ return res === null || res === void 0 ? void 0 : res.rows[0];
632
+ }));
633
+ });
634
+ }
635
+ updateTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }) {
636
+ return __awaiter(this, void 0, void 0, function* () {
637
+ yield this.dbPromise;
638
+ yield this.executeQuery((client) => client.query(`
639
+ UPDATE translation_file_cache
640
+ SET etag = $1
641
+ WHERE integration_id = $2 AND crowdin_id = $3 AND file_id = $4 AND language_id = $5
642
+ `, [etag, integrationId, crowdinId, fileId, languageId]));
643
+ });
644
+ }
600
645
  }
601
646
  exports.PostgreStorage = PostgreStorage;
@@ -1,7 +1,7 @@
1
1
  import { Storage } from '.';
2
2
  import { CrowdinCredentials } from '../types';
3
3
  import { IntegrationConfig, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks } from '../modules/integration/types';
4
- import { CreateJobParams, GetActiveJobsParams, GetJobParams, Job, UpdateJobParams } from '../modules/integration/util/types';
4
+ import { CreateJobParams, GetActiveJobsParams, GetJobParams, GetTranslationCacheParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams } from '../modules/integration/util/types';
5
5
  import { UserErrors } from './types';
6
6
  export interface SQLiteStorageConfig {
7
7
  dbFolder: string;
@@ -61,4 +61,7 @@ export declare class SQLiteStorage implements Storage {
61
61
  getJob({ id }: GetJobParams): Promise<Job | undefined>;
62
62
  getActiveJobs({ integrationId, crowdinId }: GetActiveJobsParams): Promise<Job[] | undefined>;
63
63
  deleteFinishedJobs(): Promise<void>;
64
+ saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag }: TranslationCache): Promise<void>;
65
+ getTranslationCache({ integrationId, crowdinId, fileId, languageId, }: GetTranslationCacheParams): Promise<TranslationCache | undefined>;
66
+ updateTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }: UpdateTranslationCacheParams): Promise<void>;
64
67
  }
@@ -256,6 +256,17 @@ class SQLiteStorage {
256
256
  updated_at varchar null,
257
257
  finished_at varchar null
258
258
  );
259
+ `, []);
260
+ yield this._run(`
261
+ create table if not exists translation_file_cache
262
+ (
263
+ id integer not null primary key autoincrement,
264
+ integration_id varchar not null,
265
+ crowdin_id varchar not null,
266
+ file_id integer not null,
267
+ language_id varchar not null,
268
+ etag varchar
269
+ );
259
270
  `, []);
260
271
  this._res && this._res();
261
272
  // TODO: temporary code
@@ -318,6 +329,7 @@ class SQLiteStorage {
318
329
  yield this.run('DELETE FROM user_errors WHERE crowdin_id = ?', [id]);
319
330
  yield this.run('DELETE FROM integration_settings WHERE crowdin_id = ?', [id]);
320
331
  yield this.run('DELETE FROM job WHERE crowdin_id = ?', [id]);
332
+ yield this.run('DELETE FROM translation_file_cache WHERE crowdin_id = ?', [id]);
321
333
  });
322
334
  }
323
335
  saveIntegrationCredentials(id, credentials, crowdinId) {
@@ -575,5 +587,31 @@ class SQLiteStorage {
575
587
  yield this.run('DELETE FROM job WHERE finished_at is not NULL', []);
576
588
  });
577
589
  }
590
+ saveTranslationCache({ integrationId, crowdinId, fileId, languageId, etag }) {
591
+ return this.run(`
592
+ INSERT
593
+ INTO translation_file_cache(integration_id, crowdin_id, file_id, language_id, etag)
594
+ VALUES (?, ?, ?, ?, ?)
595
+ `, [integrationId, crowdinId, fileId, languageId, etag]);
596
+ }
597
+ getTranslationCache({ integrationId, crowdinId, fileId, languageId, }) {
598
+ return __awaiter(this, void 0, void 0, function* () {
599
+ const row = yield this.get(`
600
+ SELECT integration_id as integrationId, crowdin_id as crowdinId, file_id as fileId, language_id as languageId, etag
601
+ FROM translation_file_cache
602
+ WHERE integration_id = ? AND crowdin_id = ? AND file_id = ? AND language_id = ?
603
+ `, [integrationId, crowdinId, fileId, languageId]);
604
+ if (row) {
605
+ return row;
606
+ }
607
+ });
608
+ }
609
+ updateTranslationCache({ integrationId, crowdinId, fileId, languageId, etag, }) {
610
+ return this.run(`
611
+ UPDATE translation_file_cache
612
+ SET etag = ?
613
+ WHERE integration_id = ? AND crowdin_id = ? AND file_id = ? AND language_id = ?
614
+ `, [etag, integrationId, crowdinId, fileId, languageId]);
615
+ }
578
616
  }
579
617
  exports.SQLiteStorage = SQLiteStorage;
@@ -905,12 +905,48 @@
905
905
  scheduleModal.close();
906
906
  }
907
907
 
908
- function openScheduleModal(type) {
908
+ async function openScheduleModal(type) {
909
909
  const newFile = scheduleModal.querySelector('#new-files')
910
910
  const selectedFiles = scheduleModal.querySelector('#selected-files');
911
+ let newFileStatus = 'unChecked';
912
+
913
+ const integrationSyncFolders = (await document.querySelector('crowdin-simple-integration').getIntegrationScheduleSync(true))
914
+ .filter(elements => elements.node_type === '0');
915
+
916
+ const selectedIntegrationFolders = (await document.getElementById('integration-files').getSelected(true))
917
+ .filter(elements => elements.node_type === '0');
918
+
919
+ for (let i = 0; i < selectedIntegrationFolders.length; i++) {
920
+ const selectedElement = selectedIntegrationFolders[i];
921
+
922
+ const found = integrationSyncFolders.some(integrationElement =>
923
+ integrationElement.id === selectedElement.id
924
+ );
925
+
926
+ if (found) {
927
+ newFileStatus = 'checked';
928
+ } else if (!found && newFileStatus === 'checked') {
929
+ newFileStatus = 'clear-selection';
930
+ break;
931
+ } else {
932
+ newFileStatus = 'unChecked';
933
+ }
934
+ }
935
+
936
+ if (newFileStatus === 'checked') {
937
+ newFile[newFileStatus] = true;
938
+ newFile.value = true;
939
+ newFile.removeAttribute('clear-selection');
940
+ } else if (newFileStatus === 'clear-selection') {
941
+ newFile.checked = false;
942
+ newFile.value = false;
943
+ newFile.setAttribute(newFileStatus, '');
944
+ } else {
945
+ newFile.checked = false;
946
+ newFile.value = false;
947
+ newFile.removeAttribute('clear-selection');
948
+ }
911
949
 
912
- newFile.checked = false;
913
- newFile.value = false;
914
950
  selectedFiles.checked = true;
915
951
  selectedFiles.value = true;
916
952
  scheduleModal.querySelector('#save-schedule-sync').setAttribute('disabled', false);
@@ -919,11 +955,14 @@
919
955
  }
920
956
 
921
957
  function onChangeAutoSynchronizationOptions() {
922
- const newFiles = document.getElementById('new-files').checked || false;
958
+ const newFiles = document.getElementById('new-files');
959
+ newFiles.removeAttribute('clear-selection');
960
+
961
+ const newFilesStatus = newFiles.checked || false;
923
962
  const selectedFiles = document.getElementById('selected-files').checked || false;
924
963
  const buttonSaveScheduleSync = document.getElementById('save-schedule-sync');
925
964
 
926
- if (newFiles || selectedFiles) {
965
+ if (newFilesStatus || selectedFiles) {
927
966
  buttonSaveScheduleSync.removeAttribute('disabled');
928
967
  } else {
929
968
  buttonSaveScheduleSync.setAttribute('disabled', true);
@@ -953,7 +992,7 @@
953
992
  syncData = e.detail;
954
993
  const isFolder = await hasFolder(e.detail);
955
994
  if (isFolder && !newIntegrationFiles) {
956
- openScheduleModal('integration');
995
+ await openScheduleModal('integration');
957
996
  return;
958
997
  }
959
998
 
@@ -981,7 +1020,7 @@
981
1020
  syncData = e.detail;
982
1021
  const isFolder = await hasFolder(e.detail);
983
1022
  if (isFolder && !newCrowdinFiles) {
984
- openScheduleModal('crowdin');
1023
+ await openScheduleModal('crowdin');
985
1024
  return;
986
1025
  }
987
1026
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.60.2",
3
+ "version": "0.61.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",