@crowdin/app-project-module 0.85.1 → 0.86.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.
@@ -19,6 +19,7 @@ const defaults_1 = require("../util/defaults");
19
19
  const logger_1 = require("../../../util/logger");
20
20
  const job_1 = require("../util/job");
21
21
  const files_1 = require("../util/files");
22
+ const types_2 = require("../types");
22
23
  function handle(config, integration) {
23
24
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
24
25
  var _a, _b, _c;
@@ -33,6 +34,7 @@ function handle(config, integration) {
33
34
  if (((_b = config.api) === null || _b === void 0 ? void 0 : _b.default) && req.body.files) {
34
35
  req.body = req.body.files;
35
36
  }
37
+ let payload = req.body;
36
38
  const excludedTargetLanguages = yield (0, files_1.getExcludedTargetLanguages)({
37
39
  client: req.crowdinApiClient,
38
40
  projectId,
@@ -43,29 +45,34 @@ function handle(config, integration) {
43
45
  crowdinId: req.crowdinContext.crowdinId,
44
46
  type: types_1.JobType.UPDATE_TO_CROWDIN,
45
47
  title: 'Sync files to Crowdin',
46
- payload: req.body,
48
+ payload,
47
49
  res,
48
50
  projectId,
49
51
  client: req.crowdinApiClient,
50
52
  jobType: types_1.JobClientType.MANUAL,
51
53
  jobStoreType: integration.jobStoreType,
52
54
  jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
53
- var _d;
54
- if (req.body && ((_d = req.body) === null || _d === void 0 ? void 0 : _d.length)) {
55
- req.body = yield (0, files_1.expandFilesTree)(req.body, req, integration, job);
56
- req.body = (0, lodash_uniqby_1.default)(req.body, 'id');
55
+ if (payload && (payload === null || payload === void 0 ? void 0 : payload.length)) {
56
+ payload = yield (0, files_1.expandFilesTree)(payload, req, integration, job);
57
+ payload = (0, lodash_uniqby_1.default)(payload, 'id');
57
58
  }
58
59
  const result = yield integration.updateCrowdin({
59
60
  projectId,
60
61
  client: req.crowdinApiClient,
61
62
  credentials: req.integrationCredentials,
62
- request: req.body,
63
+ request: payload,
63
64
  rootFolder,
64
65
  appSettings: req.integrationSettings,
65
66
  uploadTranslations,
66
67
  job,
67
68
  excludedTargetLanguages: req.query.languages ? excludedTargetLanguages : undefined,
68
69
  });
70
+ try {
71
+ yield (0, files_1.updateSyncedData)(req.crowdinContext.clientId, req.crowdinContext.crowdinId, payload, types_2.Provider.INTEGRATION);
72
+ }
73
+ catch (e) {
74
+ (0, logger_1.logError)(e, req.crowdinContext);
75
+ }
69
76
  let message;
70
77
  if ((0, files_1.isExtendedResultType)(result)) {
71
78
  message = result.message;
@@ -12,31 +12,75 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const util_1 = require("../../../util");
13
13
  const logger_1 = require("../../../util/logger");
14
14
  const files_1 = require("../util/files");
15
+ const types_1 = require("../types");
16
+ const storage_1 = require("../../../storage");
15
17
  function handle(integration) {
16
18
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
19
+ var _a, _b, _c, _d, _e, _f, _g, _h;
17
20
  const { parent_id: parentId, search, page } = req.query;
18
21
  req.logInfo('Received request to get integration data');
19
22
  let message;
20
23
  let stopPagination;
21
- let files;
24
+ let files = [];
25
+ const { crowdinId, clientId } = req.crowdinContext;
22
26
  try {
23
- const result = yield integration.getIntegrationFiles(req.integrationCredentials, req.integrationSettings, parentId, search, page);
27
+ const appSettings = req.integrationSettings;
28
+ const result = yield integration.getIntegrationFiles(req.integrationCredentials, appSettings, parentId, search, page);
24
29
  if ((0, files_1.isExtendedResultType)(result)) {
25
- files = result.data;
30
+ files = result.data || [];
26
31
  message = result.message;
27
32
  stopPagination = result.stopPagination;
28
33
  }
29
34
  else {
30
- files = result;
35
+ files = result || [];
31
36
  }
32
37
  files = (0, files_1.skipFilesByRegex)(files, integration.skipIntegrationNodes);
38
+ if (integration.filterByPathIntegrationFiles) {
39
+ const includePatterns = (_a = appSettings === null || appSettings === void 0 ? void 0 : appSettings.includeByFilePath) === null || _a === void 0 ? void 0 : _a.split('\n').filter(Boolean);
40
+ const excludePatterns = (_b = appSettings === null || appSettings === void 0 ? void 0 : appSettings.excludeByFilePath) === null || _b === void 0 ? void 0 : _b.split('\n').filter(Boolean);
41
+ files = (0, files_1.filterFilesByPath)(files, includePatterns, excludePatterns);
42
+ }
43
+ if (((_d = (_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.integrationFileStatus) === null || _d === void 0 ? void 0 : _d.isNew) ||
44
+ ((_f = (_e = integration.filtering) === null || _e === void 0 ? void 0 : _e.integrationFileStatus) === null || _f === void 0 ? void 0 : _f.isUpdated) ||
45
+ ((_h = (_g = integration.filtering) === null || _g === void 0 ? void 0 : _g.integrationFileStatus) === null || _h === void 0 ? void 0 : _h.notSynced)) {
46
+ const syncedData = yield (0, storage_1.getStorage)().getSyncedData(clientId, crowdinId, types_1.Provider.INTEGRATION);
47
+ const syncedFiles = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.files) ? JSON.parse(syncedData.files) : [];
48
+ const lastSyncTimestamp = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.updatedAt) ? Number(syncedData.updatedAt) : null;
49
+ files = files.map((file) => {
50
+ var _a, _b, _c, _d, _e, _f;
51
+ if (!file.type) {
52
+ return file;
53
+ }
54
+ const notSynced = ((_b = (_a = integration.filtering) === null || _a === void 0 ? void 0 : _a.integrationFileStatus) === null || _b === void 0 ? void 0 : _b.notSynced) === true
55
+ ? !syncedFiles.some((syncedItem) => syncedItem.id === file.id)
56
+ : false;
57
+ const isNew = ((_d = (_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.integrationFileStatus) === null || _d === void 0 ? void 0 : _d.isNew) === true
58
+ ? !syncedFiles.some((syncedItem) => syncedItem.id === file.id) &&
59
+ lastSyncTimestamp &&
60
+ file.createdAt &&
61
+ (typeof file.createdAt === 'string'
62
+ ? new Date(file.createdAt).getTime()
63
+ : file.createdAt) > Number(lastSyncTimestamp)
64
+ : false;
65
+ const isUpdated = ((_f = (_e = integration.filtering) === null || _e === void 0 ? void 0 : _e.integrationFileStatus) === null || _f === void 0 ? void 0 : _f.isUpdated) === true
66
+ ? lastSyncTimestamp &&
67
+ file.updatedAt &&
68
+ (typeof file.updatedAt === 'string'
69
+ ? new Date(file.updatedAt).getTime()
70
+ : file.updatedAt) > Number(lastSyncTimestamp)
71
+ : false;
72
+ return Object.assign(Object.assign({}, file), { isNew,
73
+ notSynced,
74
+ isUpdated });
75
+ });
76
+ }
33
77
  }
34
78
  catch (e) {
35
79
  yield (0, logger_1.handleUserError)({
36
80
  action: 'Get External Service data',
37
81
  error: e,
38
- crowdinId: req.crowdinContext.crowdinId,
39
- clientId: req.crowdinContext.clientId,
82
+ crowdinId,
83
+ clientId,
40
84
  });
41
85
  res.send({ data: [], message: (e === null || e === void 0 ? void 0 : e.message) || e });
42
86
  throw e;
@@ -39,7 +39,7 @@ const defaults_1 = require("../util/defaults");
39
39
  const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-functions"));
40
40
  function handle(config, integration) {
41
41
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
42
- var _a, _b, _c;
42
+ var _a, _b, _c, _d;
43
43
  const logger = req.logInfo || logger_1.log;
44
44
  const installed = !!req.crowdinApiClient;
45
45
  const loggedIn = !!req.integrationCredentials;
@@ -82,7 +82,7 @@ function handle(config, integration) {
82
82
  }
83
83
  options.infoModal = integration.infoModal;
84
84
  options.syncNewElements = integration.syncNewElements;
85
- options.filtering = integration.filtering;
85
+ options.filtering = Object.assign(Object.assign({}, integration.filtering), { integrationFilterConfig: JSON.stringify(((_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.integrationFilterConfig) || []) });
86
86
  options.withCronSync = integration.withCronSync;
87
87
  options.webhooks = integration.webhooks
88
88
  ? {
@@ -103,7 +103,7 @@ function handle(config, integration) {
103
103
  : null;
104
104
  options.notice = integration.notice;
105
105
  options.asyncProgress = {
106
- checkInterval: ((_c = integration.asyncProgress) === null || _c === void 0 ? void 0 : _c.checkInterval) || 1000,
106
+ checkInterval: ((_d = integration.asyncProgress) === null || _d === void 0 ? void 0 : _d.checkInterval) || 1000,
107
107
  };
108
108
  logger(`Routing user to ${view} view`);
109
109
  return res.render(view, options);
@@ -107,8 +107,36 @@ export interface IntegrationLogic extends ModuleKey {
107
107
  crowdin: boolean;
108
108
  integration: boolean;
109
109
  };
110
+ /**
111
+ * Enable file filtering
112
+ */
110
113
  filtering?: {
111
- crowdinLanguages: boolean;
114
+ crowdinLanguages?: boolean;
115
+ /**
116
+ * Configuration for integration file filtering
117
+ */
118
+ integrationFilterConfig?: any;
119
+ /**
120
+ * Enable file status filtering
121
+ */
122
+ integrationFileStatus?: {
123
+ /**
124
+ * Enable file filtering by "isNew" status
125
+ */
126
+ isNew?: boolean;
127
+ /**
128
+ * Enable file filtering by "isUpdated" status
129
+ */
130
+ isUpdated?: boolean;
131
+ /**
132
+ * Enable file filtering by "failed" status
133
+ */
134
+ failed?: boolean;
135
+ /**
136
+ * Enable file filtering by "notSynced" status
137
+ */
138
+ notSynced?: boolean;
139
+ };
112
140
  };
113
141
  /**
114
142
  * Enable integration folder open event
@@ -130,6 +158,10 @@ export interface IntegrationLogic extends ModuleKey {
130
158
  * Enable the option to upload file for translation into selected languages.
131
159
  */
132
160
  excludedTargetLanguages?: boolean;
161
+ /**
162
+ * Enable the option to add 'Exclude paths' and 'Include paths' text fields to integration settings
163
+ */
164
+ filterByPathIntegrationFiles?: boolean;
133
165
  /**
134
166
  * function to get crowdin file translation progress
135
167
  */
@@ -305,6 +337,7 @@ export interface File {
305
337
  labels?: LabelTreeElement[];
306
338
  failed?: boolean;
307
339
  excludedTargetLanguages?: string[];
340
+ path?: string;
308
341
  }
309
342
  export interface Folder {
310
343
  id: string;
@@ -313,6 +346,7 @@ export interface Folder {
313
346
  nodeType?: IntegrationTreeElementType;
314
347
  customContent?: string;
315
348
  labels?: LabelTreeElement[];
349
+ path?: string;
316
350
  }
317
351
  export type TreeItem = File | Folder;
318
352
  /**
@@ -43,6 +43,7 @@ const types_1 = require("../types");
43
43
  const job_1 = require("./job");
44
44
  const types_2 = require("./types");
45
45
  const subscription_1 = require("../../../util/subscription");
46
+ const files_1 = require("./files");
46
47
  function runJob({ config, integration, job, }) {
47
48
  return __awaiter(this, void 0, void 0, function* () {
48
49
  (0, logger_1.log)(`Starting cron job with expression [${job.expression}]`);
@@ -106,6 +107,12 @@ function runUpdateProviderJob({ integrationId, crowdinId, type, title, payload,
106
107
  };
107
108
  if (type === types_2.JobType.UPDATE_TO_CROWDIN) {
108
109
  yield integration.updateCrowdin(updateParams);
110
+ try {
111
+ yield (0, files_1.updateSyncedData)(integrationId, crowdinId, payload, types_1.Provider.INTEGRATION);
112
+ }
113
+ catch (e) {
114
+ (0, logger_1.logError)(e, context);
115
+ }
109
116
  }
110
117
  else if (type === types_2.JobType.UPDATE_TO_INTEGRATION) {
111
118
  yield integration.updateIntegration(updateParams);
@@ -57,7 +57,7 @@ function getOauthRoute(integration) {
57
57
  }
58
58
  exports.getOauthRoute = getOauthRoute;
59
59
  function applyIntegrationModuleDefaults(config, integration) {
60
- var _a, _b;
60
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
61
61
  if (!integration.getCrowdinFiles) {
62
62
  integration.getCrowdinFiles = (projectId, client, rootFolder) => __awaiter(this, void 0, void 0, function* () {
63
63
  const allBranches = (yield client.sourceFilesApi.withFetchAll().listProjectBranches(projectId)).data.map((d) => d.data);
@@ -139,9 +139,13 @@ function applyIntegrationModuleDefaults(config, integration) {
139
139
  ],
140
140
  };
141
141
  }
142
+ if ((integration.filterByPathIntegrationFiles === undefined || integration.filterByPathIntegrationFiles) &&
143
+ !integration.integrationOneLevelFetching) {
144
+ integration.filterByPathIntegrationFiles = true;
145
+ }
142
146
  const getUserSettings = integration.getConfiguration;
143
147
  integration.getConfiguration = (projectId, crowdinClient, integrationCredentials) => __awaiter(this, void 0, void 0, function* () {
144
- var _c, _d;
148
+ var _m, _o;
145
149
  let fields = [];
146
150
  const project = (yield crowdinClient.projectsGroupsApi.getProject(projectId));
147
151
  if (getUserSettings) {
@@ -190,7 +194,7 @@ function applyIntegrationModuleDefaults(config, integration) {
190
194
  },
191
195
  ],
192
196
  });
193
- if ((_c = integration.syncNewElements) === null || _c === void 0 ? void 0 : _c.crowdin) {
197
+ if ((_m = integration.syncNewElements) === null || _m === void 0 ? void 0 : _m.crowdin) {
194
198
  defaultSettings.push({
195
199
  key: 'new-crowdin-files',
196
200
  label: 'Automatically sync new translations from Crowdin',
@@ -198,7 +202,7 @@ function applyIntegrationModuleDefaults(config, integration) {
198
202
  dependencySettings: JSON.stringify([{ '#schedule-settings': { type: '!equal', value: ['0'] } }]),
199
203
  });
200
204
  }
201
- if ((_d = integration.syncNewElements) === null || _d === void 0 ? void 0 : _d.integration) {
205
+ if ((_o = integration.syncNewElements) === null || _o === void 0 ? void 0 : _o.integration) {
202
206
  defaultSettings.push({
203
207
  key: 'new-integration-files',
204
208
  label: `Automatically sync new content from ${config.name}`,
@@ -245,6 +249,21 @@ function applyIntegrationModuleDefaults(config, integration) {
245
249
  ],
246
250
  });
247
251
  }
252
+ if (integration.filterByPathIntegrationFiles) {
253
+ defaultSettings.push({
254
+ label: 'File Filters',
255
+ }, {
256
+ key: 'includeByFilePath',
257
+ label: 'Source files path',
258
+ type: 'textarea',
259
+ helpText: 'Enter the file path patterns to include files for synchronization. Use wildcard selectors like `*` and `**` to match multiple files. Example: `/pages/**',
260
+ }, {
261
+ key: 'excludeByFilePath',
262
+ label: 'Ignore files or folders',
263
+ type: 'textarea',
264
+ helpText: 'Enter the path patterns for files or folders to exclude. Use wildcard selectors like `*` and `**` to match multiple files. Example: `/drafts/**`',
265
+ });
266
+ }
248
267
  return [...defaultSettings, ...fields];
249
268
  });
250
269
  if (!integration.checkConnection) {
@@ -258,6 +277,51 @@ function applyIntegrationModuleDefaults(config, integration) {
258
277
  if (!((_b = integration.filtering) === null || _b === void 0 ? void 0 : _b.hasOwnProperty('crowdinLanguages'))) {
259
278
  integration.filtering = Object.assign(Object.assign({}, (integration.filtering || {})), { crowdinLanguages: true });
260
279
  }
280
+ integration.filtering.integrationFileStatus = Object.assign({ notSynced: true }, integration.filtering.integrationFileStatus);
281
+ if (!((_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.hasOwnProperty('integrationFilterConfig'))) {
282
+ const filterItems = [
283
+ {
284
+ value: 'all',
285
+ label: 'All',
286
+ },
287
+ ];
288
+ if ((_e = (_d = integration.filtering) === null || _d === void 0 ? void 0 : _d.integrationFileStatus) === null || _e === void 0 ? void 0 : _e.isNew) {
289
+ filterItems.push({
290
+ value: 'isNew',
291
+ label: 'New',
292
+ });
293
+ }
294
+ if ((_g = (_f = integration.filtering) === null || _f === void 0 ? void 0 : _f.integrationFileStatus) === null || _g === void 0 ? void 0 : _g.isUpdated) {
295
+ filterItems.push({
296
+ value: 'isUpdated',
297
+ label: 'Modified',
298
+ });
299
+ }
300
+ if ((_j = (_h = integration.filtering) === null || _h === void 0 ? void 0 : _h.integrationFileStatus) === null || _j === void 0 ? void 0 : _j.failed) {
301
+ filterItems.push({
302
+ value: 'failed',
303
+ label: 'Sync Error',
304
+ });
305
+ }
306
+ if ((_l = (_k = integration.filtering) === null || _k === void 0 ? void 0 : _k.integrationFileStatus) === null || _l === void 0 ? void 0 : _l.notSynced) {
307
+ filterItems.push({
308
+ value: 'notSynced',
309
+ label: 'Never Synced',
310
+ });
311
+ }
312
+ integration.filtering = Object.assign(Object.assign({}, (integration.filtering || {})), { integrationFilterConfig: filterItems.length > 1
313
+ ? [
314
+ {
315
+ key: 'file',
316
+ type: 'list_single',
317
+ label: 'File',
318
+ items: filterItems,
319
+ defaultValue: 'all',
320
+ defaultLabel: 'All',
321
+ },
322
+ ]
323
+ : [] });
324
+ }
261
325
  if (!integration.userErrorLifetimeDays) {
262
326
  integration.userErrorLifetimeDays = 30;
263
327
  }
@@ -1,4 +1,4 @@
1
- import { ExtendedResult, IntegrationFile, IntegrationLogic, IntegrationRequest, SkipIntegrationNodes, TreeItem, UpdateIntegrationRequest } from '../types';
1
+ import { ExtendedResult, IntegrationFile, IntegrationLogic, IntegrationRequest, SkipIntegrationNodes, TreeItem, UpdateIntegrationRequest, Provider } from '../types';
2
2
  import { JobClient } from './types';
3
3
  import Crowdin from '@crowdin/crowdin-api-client';
4
4
  import { SourceFilesModel } from '@crowdin/crowdin-api-client';
@@ -17,3 +17,9 @@ export declare function getExcludedTargetLanguages({ client, projectId, language
17
17
  languages: string[];
18
18
  }): Promise<string[]>;
19
19
  export declare function filterLanguages(request: UpdateIntegrationRequest, files: SourceFilesModel.File[]): UpdateIntegrationRequest;
20
+ export declare function buildPath(file: TreeItem, files: TreeItem[]): string;
21
+ export declare function getParentPaths(filePath: string): Set<string>;
22
+ export declare function getParentFiles(allFiles: TreeItem[], fileCollection: TreeItem[]): TreeItem[];
23
+ export declare function filterFilesByPatterns(files: TreeItem[], patterns: string[]): TreeItem[];
24
+ export declare function filterFilesByPath(files: TreeItem[], includePatterns?: string[], excludePatterns?: string[]): TreeItem[];
25
+ export declare function updateSyncedData(clientId: string, crowdinId: string, payload: any[], provider?: Provider): Promise<void>;
@@ -8,11 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.filterLanguages = exports.getExcludedTargetLanguages = exports.markUnsyncedFiles = exports.isExtendedResultType = exports.expandFilesTree = exports.skipFilesByRegex = void 0;
13
- const types_1 = require("./types");
15
+ exports.updateSyncedData = exports.filterFilesByPath = exports.filterFilesByPatterns = exports.getParentFiles = exports.getParentPaths = exports.buildPath = exports.filterLanguages = exports.getExcludedTargetLanguages = exports.markUnsyncedFiles = exports.isExtendedResultType = exports.expandFilesTree = exports.skipFilesByRegex = void 0;
16
+ const types_1 = require("../types");
17
+ const types_2 = require("./types");
14
18
  const storage_1 = require("../../../storage");
15
19
  const util_1 = require("../../../util");
20
+ const minimatch_1 = require("minimatch");
21
+ const lodash_uniqby_1 = __importDefault(require("lodash.uniqby"));
16
22
  function skipFilesByRegex(files, skipIntegrationNodes) {
17
23
  if (!Array.isArray(files)) {
18
24
  return [];
@@ -34,7 +40,7 @@ exports.skipFilesByRegex = skipFilesByRegex;
34
40
  function expandFilesTree(nodes, req, integration, job) {
35
41
  var _a;
36
42
  return __awaiter(this, void 0, void 0, function* () {
37
- if (job && types_1.JobStatus.CANCELED === ((_a = (yield job.get())) === null || _a === void 0 ? void 0 : _a.status)) {
43
+ if (job && types_2.JobStatus.CANCELED === ((_a = (yield job.get())) === null || _a === void 0 ? void 0 : _a.status)) {
38
44
  throw new Error('Job canceled');
39
45
  }
40
46
  const files = nodes.filter((file) => file.nodeType === undefined || file.nodeType === '1');
@@ -121,3 +127,73 @@ function filterLanguages(request, files) {
121
127
  return result;
122
128
  }
123
129
  exports.filterLanguages = filterLanguages;
130
+ function buildPath(file, files) {
131
+ const filePath = `/${file.name}`;
132
+ if (!file.parentId) {
133
+ return filePath;
134
+ }
135
+ const parent = files.find((f) => f.id === file.parentId);
136
+ if (!parent) {
137
+ return filePath;
138
+ }
139
+ const parentPath = buildPath(parent, files);
140
+ return `${parentPath}${filePath}`;
141
+ }
142
+ exports.buildPath = buildPath;
143
+ function getParentPaths(filePath) {
144
+ const parentPaths = new Set();
145
+ const parts = filePath.split('/');
146
+ let currentPath = '';
147
+ for (let i = 1; i < parts.length - 1; i++) {
148
+ currentPath += '/' + parts[i];
149
+ parentPaths.add(currentPath);
150
+ }
151
+ return parentPaths;
152
+ }
153
+ exports.getParentPaths = getParentPaths;
154
+ function getParentFiles(allFiles, fileCollection) {
155
+ const parentPaths = new Set();
156
+ fileCollection
157
+ .filter((file) => !!file.path)
158
+ .forEach((file) => {
159
+ const paths = getParentPaths(file.path);
160
+ paths.forEach((path) => parentPaths.add(path));
161
+ });
162
+ return allFiles.filter((file) => file.path && parentPaths.has(file.path));
163
+ }
164
+ exports.getParentFiles = getParentFiles;
165
+ function filterFilesByPatterns(files, patterns) {
166
+ return files.filter((file) => patterns.some((pattern) => (0, minimatch_1.minimatch)(file.path || '', pattern.trim())));
167
+ }
168
+ exports.filterFilesByPatterns = filterFilesByPatterns;
169
+ function filterFilesByPath(files, includePatterns, excludePatterns) {
170
+ const filesWithPaths = files.map((file) => (Object.assign(Object.assign({}, file), { path: buildPath(file, files) })));
171
+ let filteredFiles = [...filesWithPaths];
172
+ if (includePatterns === null || includePatterns === void 0 ? void 0 : includePatterns.length) {
173
+ const includedFiles = filterFilesByPatterns(filteredFiles, includePatterns);
174
+ const parentFiles = getParentFiles(filesWithPaths, includedFiles);
175
+ filteredFiles = [...new Set([...includedFiles, ...parentFiles])];
176
+ }
177
+ if (excludePatterns === null || excludePatterns === void 0 ? void 0 : excludePatterns.length) {
178
+ const excludedFiles = filterFilesByPatterns(filteredFiles, excludePatterns);
179
+ const remainingFiles = filteredFiles.filter((file) => !excludedFiles.includes(file));
180
+ const parentFiles = getParentFiles(filesWithPaths, remainingFiles);
181
+ filteredFiles = [...new Set([...remainingFiles, ...parentFiles])];
182
+ }
183
+ return filteredFiles;
184
+ }
185
+ exports.filterFilesByPath = filterFilesByPath;
186
+ function updateSyncedData(clientId, crowdinId, payload, provider = types_1.Provider.INTEGRATION) {
187
+ return __awaiter(this, void 0, void 0, function* () {
188
+ const existingSyncedData = yield (0, storage_1.getStorage)().getSyncedData(clientId, crowdinId, provider);
189
+ if (existingSyncedData) {
190
+ const existingFiles = JSON.parse(existingSyncedData.files);
191
+ const mergedFiles = (0, lodash_uniqby_1.default)([...existingFiles, ...payload], 'id');
192
+ yield (0, storage_1.getStorage)().updateSyncedData(JSON.stringify(mergedFiles), clientId, crowdinId, provider);
193
+ }
194
+ else {
195
+ yield (0, storage_1.getStorage)().saveSyncedData(JSON.stringify(payload), clientId, crowdinId, provider);
196
+ }
197
+ });
198
+ }
199
+ exports.updateSyncedData = updateSyncedData;
@@ -113,3 +113,11 @@ export interface UnsyncedFiles {
113
113
  files: string;
114
114
  }
115
115
  export type GetUnsyncedFiles = Pick<UnsyncedFiles, 'integrationId' | 'crowdinId'>;
116
+ export interface IntegrationSyncedData {
117
+ id: number;
118
+ integrationId: string;
119
+ crowdinId: string;
120
+ type: string;
121
+ updatedAt: string;
122
+ files?: any;
123
+ }
@@ -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, GetFileTranslationCacheByLanguageParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams, GetFileTranslationCache, UnsyncedFiles, GetUnsyncedFiles } from '../modules/integration/util/types';
3
+ import { CreateJobParams, GetActiveJobsParams, GetJobParams, GetFileTranslationCacheByLanguageParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams, GetFileTranslationCache, UnsyncedFiles, GetUnsyncedFiles, IntegrationSyncedData } from '../modules/integration/util/types';
4
4
  import { UserErrors } from './types';
5
5
  export declare const TABLES: {
6
6
  crowdin_credentials: string;
@@ -79,6 +79,9 @@ export interface Storage {
79
79
  }, params?: any[]): Promise<any[]>;
80
80
  updateRecord(tableName: string, data: Record<string, any>, whereClause: string, params?: any[]): Promise<void>;
81
81
  deleteRecord(tableName: string, whereClause: string, params?: any[]): Promise<void>;
82
+ saveSyncedData(files: any, integrationId: string, crowdinId: string, type: string): Promise<void>;
83
+ updateSyncedData(files: any, integrationId: string, crowdinId: string, type: string): Promise<void>;
84
+ getSyncedData(integrationId: string, crowdinId: string, type: string): Promise<IntegrationSyncedData | undefined>;
82
85
  }
83
86
  export declare function initialize(config: Config | UnauthorizedConfig): Promise<void>;
84
87
  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, GetFileTranslationCacheByLanguageParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams, GetFileTranslationCache, UnsyncedFiles, GetUnsyncedFiles } from '../modules/integration/util/types';
3
+ import { CreateJobParams, GetActiveJobsParams, GetJobParams, GetFileTranslationCacheByLanguageParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams, GetFileTranslationCache, UnsyncedFiles, GetUnsyncedFiles, IntegrationSyncedData } 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 {
@@ -29,6 +29,7 @@ export declare class MySQLStorage implements Storage {
29
29
  job: string;
30
30
  translation_file_cache: string;
31
31
  unsynced_files: string;
32
+ synced_data: string;
32
33
  };
33
34
  constructor(config: MySQLStorageConfig);
34
35
  executeQuery<T>(command: (connection: any) => Promise<T>): Promise<T>;
@@ -95,4 +96,7 @@ export declare class MySQLStorage implements Storage {
95
96
  }, params?: any[]): Promise<any[]>;
96
97
  updateRecord(tableName: string, data: Record<string, any>, whereClause: string, params?: any[]): Promise<void>;
97
98
  deleteRecord(tableName: string, whereClause: string, params?: any[]): Promise<void>;
99
+ saveSyncedData(files: any, integrationId: string, crowdinId: string, type: string): Promise<void>;
100
+ updateSyncedData(files: any, integrationId: string, crowdinId: string, type: string): Promise<void>;
101
+ getSyncedData(integrationId: string, crowdinId: string, type: string): Promise<IntegrationSyncedData | undefined>;
98
102
  }
@@ -116,6 +116,14 @@ class MySQLStorage {
116
116
  integration_id varchar(255) not null,
117
117
  crowdin_id varchar(255) not null,
118
118
  files text
119
+ )`,
120
+ synced_data: `(
121
+ id int auto_increment primary key,
122
+ files text,
123
+ integration_id varchar(255) not null,
124
+ crowdin_id varchar(255) not null,
125
+ type varchar(255) not null,
126
+ updated_at varchar(255) null,
119
127
  )`,
120
128
  };
121
129
  this.config = config;
@@ -841,5 +849,26 @@ class MySQLStorage {
841
849
  }));
842
850
  });
843
851
  }
852
+ saveSyncedData(files, integrationId, crowdinId, type) {
853
+ return __awaiter(this, void 0, void 0, function* () {
854
+ yield this.dbPromise;
855
+ yield this.executeQuery((connection) => connection.execute('INSERT INTO synced_data(files, integration_id, crowdin_id, type, updated_at) VALUES (?, ?, ?, ?, ?)', [files, integrationId, crowdinId, type, Date.now().toString()]));
856
+ });
857
+ }
858
+ updateSyncedData(files, integrationId, crowdinId, type) {
859
+ return __awaiter(this, void 0, void 0, function* () {
860
+ yield this.dbPromise;
861
+ yield this.executeQuery((connection) => connection.execute('UPDATE synced_data SET files = ?, updated_at = ? WHERE integration_id = ? AND crowdin_id = ? AND type = ?', [files, Date.now().toString(), integrationId, crowdinId, type]));
862
+ });
863
+ }
864
+ getSyncedData(integrationId, crowdinId, type) {
865
+ return __awaiter(this, void 0, void 0, function* () {
866
+ yield this.dbPromise;
867
+ return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
868
+ const [rows] = yield connection.execute('SELECT id, files, integration_id as "integrationId", crowdin_id as "crowdinId", type, updated_at as "updatedAt" FROM synced_data WHERE integration_id = ? AND crowdin_id = ? AND type = ?', [integrationId, crowdinId, type]);
869
+ return (rows || [])[0];
870
+ }));
871
+ });
872
+ }
844
873
  }
845
874
  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, GetFileTranslationCacheByLanguageParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams, GetFileTranslationCache, UnsyncedFiles, GetUnsyncedFiles } from '../modules/integration/util/types';
5
+ import { CreateJobParams, GetActiveJobsParams, GetJobParams, GetFileTranslationCacheByLanguageParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams, GetFileTranslationCache, UnsyncedFiles, GetUnsyncedFiles, IntegrationSyncedData } from '../modules/integration/util/types';
6
6
  import { UserErrors } from './types';
7
7
  export interface PostgreStorageConfig {
8
8
  host?: string;
@@ -39,6 +39,7 @@ export declare class PostgreStorage implements Storage {
39
39
  job: string;
40
40
  translation_file_cache: string;
41
41
  unsynced_files: string;
42
+ synced_data: string;
42
43
  };
43
44
  tableIndexes: TableIndexes;
44
45
  constructor(config: PostgreStorageConfig, directoryPath: string | null);
@@ -110,5 +111,8 @@ export declare class PostgreStorage implements Storage {
110
111
  }, params?: any[]): Promise<any[]>;
111
112
  updateRecord(tableName: string, data: Record<string, any>, whereClause: string, params?: any[]): Promise<void>;
112
113
  deleteRecord(tableName: string, whereClause: string, params?: any[]): Promise<void>;
114
+ saveSyncedData(files: any, integrationId: string, crowdinId: string, type: string): Promise<void>;
115
+ updateSyncedData(files: any, integrationId: string, crowdinId: string, type: string): Promise<void>;
116
+ getSyncedData(integrationId: string, crowdinId: string, type: string): Promise<IntegrationSyncedData | undefined>;
113
117
  }
114
118
  export {};
@@ -119,6 +119,14 @@ class PostgreStorage {
119
119
  integration_id varchar not null,
120
120
  crowdin_id varchar not null,
121
121
  files varchar
122
+ )`,
123
+ synced_data: `(
124
+ id serial primary key,
125
+ files varchar,
126
+ integration_id varchar not null,
127
+ crowdin_id varchar not null,
128
+ type varchar not null,
129
+ updated_at varchar null
122
130
  )`,
123
131
  };
124
132
  this.tableIndexes = {
@@ -889,5 +897,26 @@ class PostgreStorage {
889
897
  }));
890
898
  });
891
899
  }
900
+ saveSyncedData(files, integrationId, crowdinId, type) {
901
+ return __awaiter(this, void 0, void 0, function* () {
902
+ yield this.dbPromise;
903
+ yield this.executeQuery((client) => client.query('INSERT INTO synced_data(files, integration_id, crowdin_id, type, updated_at) VALUES ($1, $2, $3, $4, $5)', [files, integrationId, crowdinId, type, Date.now().toString()]));
904
+ });
905
+ }
906
+ updateSyncedData(files, integrationId, crowdinId, type) {
907
+ return __awaiter(this, void 0, void 0, function* () {
908
+ yield this.dbPromise;
909
+ yield this.executeQuery((client) => client.query('UPDATE synced_data SET files = $1, updated_at = $2 WHERE integration_id = $3 AND crowdin_id = $4 AND type = $5', [files, Date.now().toString(), integrationId, crowdinId, type]));
910
+ });
911
+ }
912
+ getSyncedData(integrationId, crowdinId, type) {
913
+ return __awaiter(this, void 0, void 0, function* () {
914
+ yield this.dbPromise;
915
+ return this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
916
+ const res = yield client.query('SELECT id, files, integration_id as "integrationId", crowdin_id as "crowdinId", type, updated_at as "updatedAt" FROM synced_data WHERE integration_id = $1 AND crowdin_id = $2 AND type = $3', [integrationId, crowdinId, type]);
917
+ return res === null || res === void 0 ? void 0 : res.rows[0];
918
+ }));
919
+ });
920
+ }
892
921
  }
893
922
  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, GetFileTranslationCacheByLanguageParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams, GetFileTranslationCache, UnsyncedFiles, GetUnsyncedFiles } from '../modules/integration/util/types';
4
+ import { CreateJobParams, GetActiveJobsParams, GetJobParams, GetFileTranslationCacheByLanguageParams, Job, TranslationCache, UpdateJobParams, UpdateTranslationCacheParams, GetFileTranslationCache, UnsyncedFiles, GetUnsyncedFiles, IntegrationSyncedData } from '../modules/integration/util/types';
5
5
  import { UserErrors } from './types';
6
6
  export interface SQLiteStorageConfig {
7
7
  dbFolder: string;
@@ -24,6 +24,7 @@ export declare class SQLiteStorage implements Storage {
24
24
  job: string;
25
25
  translation_file_cache: string;
26
26
  unsynced_files: string;
27
+ synced_data: string;
27
28
  };
28
29
  constructor(config: SQLiteStorageConfig);
29
30
  private _run;
@@ -98,4 +99,7 @@ export declare class SQLiteStorage implements Storage {
98
99
  }, params?: any[]): Promise<any[]>;
99
100
  updateRecord(tableName: string, data: Record<string, any>, whereClause: string, params?: any[]): Promise<void>;
100
101
  deleteRecord(tableName: string, whereClause: string, params?: any[]): Promise<void>;
102
+ saveSyncedData(files: any, integrationId: string, crowdinId: string, type: string): Promise<void>;
103
+ updateSyncedData(files: any, integrationId: string, crowdinId: string, type: string): Promise<void>;
104
+ getSyncedData(integrationId: string, crowdinId: string, type: string): Promise<IntegrationSyncedData | undefined>;
101
105
  }
@@ -115,6 +115,14 @@ class SQLiteStorage {
115
115
  integration_id varchar not null,
116
116
  crowdin_id varchar not null,
117
117
  files varchar null
118
+ )`,
119
+ synced_data: `(
120
+ id integer not null primary key autoincrement,
121
+ files varchar null,
122
+ integration_id varchar not null,
123
+ crowdin_id varchar not null,
124
+ type varchar not null,
125
+ updated_at varchar null
118
126
  )`,
119
127
  };
120
128
  this.config = config;
@@ -751,5 +759,19 @@ class SQLiteStorage {
751
759
  yield this.run(query, params);
752
760
  });
753
761
  }
762
+ saveSyncedData(files, integrationId, crowdinId, type) {
763
+ return this.run('INSERT INTO synced_data(files, integration_id, crowdin_id, type, updated_at) VALUES (?, ?, ?, ?, ?)', [files, integrationId, crowdinId, type, Date.now().toString()]);
764
+ }
765
+ updateSyncedData(files, integrationId, crowdinId, type) {
766
+ return this.run('UPDATE synced_data SET files = ?, updated_at = ? WHERE integration_id = ? AND crowdin_id = ? AND type = ?', [files, Date.now().toString(), integrationId, crowdinId, type]);
767
+ }
768
+ getSyncedData(integrationId, crowdinId, type) {
769
+ return __awaiter(this, void 0, void 0, function* () {
770
+ const row = yield this.get('SELECT id, files, integration_id as integrationId, crowdin_id as crowdinId, type, updated_at as updatedAt FROM synced_data WHERE integration_id = ? AND crowdin_id = ? AND type = ?', [integrationId, crowdinId, type]);
771
+ if (row) {
772
+ return row;
773
+ }
774
+ });
775
+ }
754
776
  }
755
777
  exports.SQLiteStorage = SQLiteStorage;
@@ -54,44 +54,48 @@
54
54
  </div>
55
55
  </div>
56
56
  <crowdin-simple-integration
57
- async-progress
58
- {{#if syncNewElements.crowdin}}
59
- skip-crowdin-auto-schedule
60
- {{/if}}
61
- {{#if syncNewElements.integration}}
62
- skip-integration-auto-schedule
63
- {{/if}}
64
- {{#or withCronSync.crowdin webhooks.crowdin}}
65
- crowdin-schedule="true"
66
- {{/or}}
67
- {{#or withCronSync.integration webhooks.integration}}
68
- integration-schedule="true"
69
- {{/or}}
70
- {{#if integrationOneLevelFetching}}
71
- integration-one-level-fetching="true"
72
- {{/if}}
73
- {{#if integrationSearchListener}}
74
- allow-integration-filter-change-listener="true"
75
- {{/if}}
76
- {{#if integrationPagination}}
77
- integration-load-more-files="true"
78
- {{/if}}
79
- integration-name="{{name}}"
80
- integration-logo="logo.png"
81
- {{#if uploadTranslations}}
82
- {{#if excludedTargetLanguages}}
83
- integration-button-menu-items='[{"label":"Sync translations", "title":"Sync existing translations from {{name}} to Crowdin. Falls back to heuristics if string keys are not defined. Accuracy may vary.", "action":"uploadTranslations"}, {"label":"Select target languages", "title":"Upload file for translation into selected languages", "action":"excludedTargetLanguages"}]'
84
- {{else}}
85
- integration-button-menu-items='[{"label":"Sync translations", "title":"Sync existing translations from {{name}} to Crowdin. Falls back to heuristics if string keys are not defined. Accuracy may vary.", "action":"uploadTranslations"}]'
86
- {{/if}}
87
- {{else}}
88
- {{#if excludedTargetLanguages}}
89
- integration-button-menu-items='[{"label":"Select target languages", "title":"Upload file for translation into selected languages", "action":"excludedTargetLanguages"}]'
90
- {{/if}}
91
- {{/if}}
92
- {{#if filtering.crowdinLanguages}}
93
- crowdin-filter
94
- {{/if}}
57
+ async-progress
58
+ {{#if syncNewElements.crowdin}}
59
+ skip-crowdin-auto-schedule
60
+ {{/if}}
61
+ {{#if syncNewElements.integration}}
62
+ skip-integration-auto-schedule
63
+ {{/if}}
64
+ {{#or withCronSync.crowdin webhooks.crowdin}}
65
+ crowdin-schedule="true"
66
+ {{/or}}
67
+ {{#or withCronSync.integration webhooks.integration}}
68
+ integration-schedule="true"
69
+ {{/or}}
70
+ {{#if integrationOneLevelFetching}}
71
+ integration-one-level-fetching="true"
72
+ {{/if}}
73
+ {{#if integrationSearchListener}}
74
+ allow-integration-filter-change-listener="true"
75
+ {{/if}}
76
+ {{#if integrationPagination}}
77
+ integration-load-more-files="true"
78
+ {{/if}}
79
+ integration-name="{{name}}"
80
+ integration-logo="logo.png"
81
+ {{#if uploadTranslations}}
82
+ {{#if excludedTargetLanguages}}
83
+ integration-button-menu-items='[{"label":"Sync translations", "title":"Sync translations to Crowdin", "action":"uploadTranslations"}, {"label":"Select target languages", "title":"Upload file for translation into selected languages", "action":"excludedTargetLanguages"}]'
84
+ {{else}}
85
+ integration-button-menu-items='[{"label":"Sync translations", "title":"Sync translations to Crowdin", "action":"uploadTranslations"}]'
86
+ {{/if}}
87
+ {{else}}
88
+ {{#if excludedTargetLanguages}}
89
+ integration-button-menu-items='[{"label":"Select target languages", "title":"Upload file for translation into selected languages", "action":"excludedTargetLanguages"}]'
90
+ {{/if}}
91
+ {{/if}}
92
+ {{#if filtering.crowdinLanguages}}
93
+ crowdin-filter
94
+ {{/if}}
95
+ {{#if filtering.integrationFilterConfig}}
96
+ integration-filter
97
+ integration-filter-config='{{filtering.integrationFilterConfig}}'
98
+ {{/if}}
95
99
  >
96
100
  </crowdin-simple-integration>
97
101
  <div id="user-errors" class="hidden">
@@ -582,6 +586,9 @@
582
586
  labels: e.labels,
583
587
  };
584
588
  if (e.type) {
589
+ item.isNew = e.isNew;
590
+ item.isUpdated = e.isUpdated;
591
+ item.notSynced = e.notSynced;
585
592
  item.type = e.type;
586
593
  item.node_type = fileType;
587
594
  } else {
@@ -1135,7 +1142,7 @@
1135
1142
  {{#if configurationFields}}
1136
1143
  const settingsModal = document.getElementById('settings-modal');
1137
1144
  const settingsSaveBtn = document.getElementById('settings-save-btn');
1138
- let config = JSON.parse('{{{config}}}');
1145
+ let config = JSON.parse('{{{config}}}'.replace(/\n/g, '\\n'));
1139
1146
  const newCrowdinFiles = config?.['new-crowdin-files'];
1140
1147
  const newIntegrationFiles = config?.['new-integration-files'];
1141
1148
 
@@ -1157,6 +1164,9 @@
1157
1164
  }
1158
1165
  } else if (el.tagName.toLowerCase() === 'crowdin-checkbox') {
1159
1166
  el.checked = !!value;
1167
+ } else if (el.tagName.toLowerCase() === 'crowdin-textarea') {
1168
+ el.value = value;
1169
+ el.setValue(value);
1160
1170
  } else {
1161
1171
  el.value = value;
1162
1172
  }
@@ -1169,7 +1179,7 @@
1169
1179
  function saveSettings() {
1170
1180
  setLoader('#settings-modal');
1171
1181
  const settingsElements = Array.from(document.getElementById('modal-content').children);
1172
- const tags = ['crowdin-checkbox', 'crowdin-select', 'crowdin-input'];
1182
+ const tags = ['crowdin-checkbox', 'crowdin-select', 'crowdin-input', 'crowdin-textarea'];
1173
1183
  const configReq = {};
1174
1184
  settingsElements
1175
1185
  .filter(e => tags.includes(e.tagName.toLowerCase()))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.85.1",
3
+ "version": "0.86.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",
@@ -22,7 +22,7 @@
22
22
  "@aws-sdk/client-s3": "^3.758.0",
23
23
  "@aws-sdk/s3-request-presigner": "^3.758.0",
24
24
  "@crowdin/crowdin-apps-functions": "^0.12.0",
25
- "@crowdin/logs-formatter": "^2.1.7",
25
+ "@crowdin/logs-formatter": "^2.1.8",
26
26
  "@godaddy/terminus": "^4.12.1",
27
27
  "@monaco-editor/react": "^4.7.0",
28
28
  "amqplib": "^0.10.5",
@@ -34,6 +34,7 @@
34
34
  "lodash.isstring": "^4.0.1",
35
35
  "lodash.snakecase": "^4.1.1",
36
36
  "lodash.uniqby": "^4.7.0",
37
+ "minimatch": "^10.0.1",
37
38
  "mysql2": "^3.12.0",
38
39
  "node-cron": "^3.0.3",
39
40
  "pg": "^8.13.3",
@@ -68,6 +69,7 @@
68
69
  "@types/lodash.isstring": "^4.0.9",
69
70
  "@types/lodash.snakecase": "^4.1.9",
70
71
  "@types/lodash.uniqby": "^4.7.9",
72
+ "@types/minimatch": "^5.1.2",
71
73
  "@types/node": "^16.18.126",
72
74
  "@types/node-cron": "^3.0.11",
73
75
  "@types/pg": "^8.11.11",