@crowdin/app-project-module 3.2.2 → 3.3.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,17 +19,9 @@ const axios_1 = __importDefault(require("axios"));
19
19
  const util_1 = require("../util");
20
20
  const crowdin_client_1 = require("../middlewares/crowdin-client");
21
21
  const jsx_renderer_1 = require("../util/jsx-renderer");
22
- function escapeHtml(value) {
23
- return value
24
- .replace(/&/g, '&')
25
- .replace(/</g, '&lt;')
26
- .replace(/>/g, '&gt;')
27
- .replace(/"/g, '&quot;')
28
- .replace(/'/g, '&#39;');
29
- }
30
22
  function getAboutPageHead({ name, storeLink }) {
31
- const escapedName = escapeHtml(name);
32
- const escapedStoreLink = storeLink ? escapeHtml(storeLink) : '';
23
+ const escapedName = (0, util_1.escapeHtml)(name);
24
+ const escapedStoreLink = storeLink ? (0, util_1.escapeHtml)(storeLink) : '';
33
25
  return `
34
26
  <head>
35
27
  <title>${escapedName}</title>
@@ -443,6 +443,13 @@ function addDefaultApiEndpoints(app, config) {
443
443
  * default: 0
444
444
  * minimum: 0
445
445
  * example: 0
446
+ * - name: finished
447
+ * in: query
448
+ * required: false
449
+ * description: 'Filter by completion: true returns only finished jobs, false returns only active jobs. Omit to list all jobs'
450
+ * schema:
451
+ * type: boolean
452
+ * example: false
446
453
  * responses:
447
454
  * 200:
448
455
  * description: 'List of jobs retrieved successfully'
@@ -121,7 +121,6 @@
121
121
  * title: 'Sync Files to Crowdin'
122
122
  * required:
123
123
  * - projectId
124
- * - files
125
124
  * properties:
126
125
  * projectId:
127
126
  * description: 'Project Identifier. Get via [List Projects](https://developer.crowdin.com/api/v2/#operation/api.projects.getMany)'
@@ -129,6 +128,12 @@
129
128
  * example: 12
130
129
  * files:
131
130
  * $ref: '#/components/schemas/IntegrationFiles'
131
+ * entryIds:
132
+ * description: 'List of integration file IDs to sync. The framework resolves these IDs to files via getIntegrationFiles and runs the normal sync path. Takes precedence over files when both are provided. Returns 501 if the integration does not support filtering by entry ID. Either files or entryIds is required for API calls.'
133
+ * type: array
134
+ * items:
135
+ * type: string
136
+ * example: ['cam_1', 'cam_2', 'cam_3']
132
137
  * uploadTranslations:
133
138
  * description: 'Upload exist translation from integration'
134
139
  * type: boolean
@@ -122,7 +122,6 @@
122
122
  * title: 'Sync Files to Crowdin'
123
123
  * required:
124
124
  * - projectId
125
- * - files
126
125
  * properties:
127
126
  * projectId:
128
127
  * description: 'Project Identifier. Get via [List Projects](https://developer.crowdin.com/api/v2/#operation/api.projects.getMany)'
@@ -130,6 +129,12 @@
130
129
  * example: 12
131
130
  * files:
132
131
  * $ref: '#/components/schemas/IntegrationFiles'
132
+ * entryIds:
133
+ * description: 'List of integration file IDs to sync. The framework resolves these IDs to files via getIntegrationFiles and runs the normal sync path. Takes precedence over files when both are provided. Returns 501 if the integration does not support filtering by entry ID. Either files or entryIds is required for API calls.'
134
+ * type: array
135
+ * items:
136
+ * type: string
137
+ * example: ['cam_1', 'cam_2', 'cam_3']
133
138
  * uploadTranslations:
134
139
  * description: 'Upload exist translation from integration'
135
140
  * type: boolean
@@ -21,7 +21,7 @@ const types_2 = require("../types");
21
21
  const storage_1 = require("../../../storage");
22
22
  function handle(config, integration) {
23
23
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
24
- var _a, _b, _c, _d;
24
+ var _a, _b, _c, _d, _e, _f, _g;
25
25
  const projectId = req.crowdinContext.jwtPayload.context.project_id || (req === null || req === void 0 ? void 0 : req.body.projectId);
26
26
  const uploadTranslations = req.query.uploadTranslations === 'true' || ((_a = req.body) === null || _a === void 0 ? void 0 : _a.uploadTranslations);
27
27
  const forcePushSources = req.query.forcePushSources === 'true' || ((_b = req.body) === null || _b === void 0 ? void 0 : _b.forcePushSources);
@@ -31,15 +31,42 @@ function handle(config, integration) {
31
31
  if (rootFolder) {
32
32
  req.logInfo(`Updating crowdin files under folder ${rootFolder.id}`);
33
33
  }
34
- if (req.isApiCall && !req.body.files) {
34
+ if (req.isApiCall && !req.body.files && !((_c = req.body.entryIds) === null || _c === void 0 ? void 0 : _c.length)) {
35
35
  return res.status(400).json({
36
36
  error: {
37
- message: 'Missing required parameter: files',
37
+ message: 'Missing required parameter: files or entryIds',
38
38
  },
39
39
  });
40
40
  }
41
+ if ((_d = req.body.entryIds) === null || _d === void 0 ? void 0 : _d.length) {
42
+ if (integration.supportsEntryIdSync === false) {
43
+ return res.status(501).json({
44
+ error: { message: 'This integration does not support syncing by entry ID.' },
45
+ });
46
+ }
47
+ const entryIds = req.body.entryIds;
48
+ const resolved = yield integration.getIntegrationFiles({
49
+ credentials: req.integrationCredentials,
50
+ client: req.crowdinApiClient,
51
+ projectId,
52
+ settings: req.integrationSettings,
53
+ entryIds,
54
+ });
55
+ const files = (0, files_1.isExtendedResultType)(resolved) ? ((_e = resolved.data) !== null && _e !== void 0 ? _e : []) : resolved;
56
+ if (integration.supportsEntryIdSync !== true) {
57
+ const entryIdSet = new Set(entryIds);
58
+ if (files.some((f) => !entryIdSet.has(f.id))) {
59
+ return res.status(501).json({
60
+ error: {
61
+ message: 'This integration does not support syncing by entry ID. Use the files parameter instead.',
62
+ },
63
+ });
64
+ }
65
+ }
66
+ req.body = files;
67
+ }
41
68
  // A request via API has a different structure
42
- if (((_c = config.api) === null || _c === void 0 ? void 0 : _c.default) && req.body.files) {
69
+ if (((_f = config.api) === null || _f === void 0 ? void 0 : _f.default) && req.body.files) {
43
70
  req.body = req.body.files;
44
71
  }
45
72
  let payload = req.body;
@@ -72,7 +99,7 @@ function handle(config, integration) {
72
99
  const excludedTargetLanguages = yield (0, files_1.getExcludedTargetLanguages)({
73
100
  client: req.crowdinApiClient,
74
101
  projectId,
75
- languages: ((_d = req.query.languages) === null || _d === void 0 ? void 0 : _d.split(',')) || [],
102
+ languages: ((_g = req.query.languages) === null || _g === void 0 ? void 0 : _g.split(',')) || [],
76
103
  });
77
104
  yield (0, job_1.runAsJob)({
78
105
  integrationId: req.crowdinContext.clientId,
@@ -128,7 +155,7 @@ function handle(config, integration) {
128
155
  }
129
156
  if (matchingWarnings.length > 0) {
130
157
  req.logInfo(`Sync completed with warnings: ${matchingWarnings.length} file(s) could not be matched.`);
131
- const error = new logger_2.AppModuleAggregateError(matchingWarnings, 'Finished with errors. See error logs.');
158
+ const error = new logger_2.AppModuleAggregateError(matchingWarnings, 'Finished with errors. See logs.');
132
159
  error.isPartialSuccess = true;
133
160
  throw error;
134
161
  }
@@ -102,7 +102,7 @@ function handle(config, integration) {
102
102
  });
103
103
  if (matchingWarnings.length > 0) {
104
104
  req.logInfo(`Sync completed with warnings: ${matchingWarnings.length} file(s) could not be matched.`);
105
- const error = new logger_2.AppModuleAggregateError(matchingWarnings, 'Finished with errors. See error logs.');
105
+ const error = new logger_2.AppModuleAggregateError(matchingWarnings, 'Finished with errors. See logs.');
106
106
  error.isPartialSuccess = true;
107
107
  throw error;
108
108
  }
@@ -1,2 +1,8 @@
1
- import { Response } from 'express';
2
- export default function handle(): (req: import("express").Request | import("../../..").CrowdinClientRequest, res: Response, next: Function) => void;
1
+ import { Request, Response } from 'express';
2
+ export declare function parseJobListQuery(query?: Request['query']): {
3
+ limit: number;
4
+ offset: number;
5
+ finished: boolean | undefined;
6
+ invalidFinished: string | undefined;
7
+ };
8
+ export default function handle(): (req: Request | import("../../..").CrowdinClientRequest, res: Response, next: Function) => void;
@@ -9,31 +9,51 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.parseJobListQuery = parseJobListQuery;
12
13
  exports.default = handle;
13
14
  const util_1 = require("../../../util");
14
15
  const storage_1 = require("../../../storage");
15
16
  const types_1 = require("../util/types");
17
+ const DEFAULT_LIMIT = 25;
18
+ const MAX_LIMIT = 100;
19
+ function parseInteger(value, fallback) {
20
+ const parsed = Number.parseInt(`${value}`, 10);
21
+ return Number.isFinite(parsed) ? parsed : fallback;
22
+ }
23
+ function parseJobListQuery(query = {}) {
24
+ const finished = query.finished === undefined ? undefined : `${query.finished}`;
25
+ return {
26
+ limit: Math.min(Math.max(parseInteger(query.limit, DEFAULT_LIMIT), 1), MAX_LIMIT),
27
+ offset: Math.max(parseInteger(query.offset, 0), 0),
28
+ finished: finished === undefined ? undefined : finished === 'true',
29
+ invalidFinished: finished !== undefined && !['true', 'false'].includes(finished) ? finished : undefined,
30
+ };
31
+ }
16
32
  function handle() {
17
33
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
18
- const limit = req.query.limit ? parseInt(req.query.limit, 10) : 25;
19
- const offset = req.query.offset ? parseInt(req.query.offset, 10) : 0;
20
- req.logInfo('Get all jobs');
34
+ const { limit, offset, finished, invalidFinished } = parseJobListQuery(req.query);
35
+ if (invalidFinished !== undefined) {
36
+ return res.status(400).json({
37
+ error: { message: `Invalid finished value: ${invalidFinished}. Allowed values: true,false` },
38
+ });
39
+ }
40
+ req.logInfo(`Get all jobs${finished !== undefined ? ` (finished: ${finished})` : ''}`);
21
41
  const jobs = yield (0, storage_1.getStorage)().getAllJobs({
22
42
  integrationId: req.crowdinContext.clientId,
23
43
  crowdinId: req.crowdinContext.crowdinId,
24
44
  limit,
25
45
  offset,
46
+ finished,
47
+ excludeTypes: [types_1.JobType.INTEGRATION_SETTINGS_SAVE],
26
48
  });
27
49
  if (!jobs) {
28
50
  req.logInfo('No jobs found');
29
51
  res.send([]);
30
52
  return;
31
53
  }
32
- const filteredJobs = jobs
33
- .filter((job) => job.type !== types_1.JobType.INTEGRATION_SETTINGS_SAVE)
34
- .map((job) => {
54
+ const result = jobs.map((job) => {
35
55
  var _a;
36
- const baseJob = {
56
+ return ({
37
57
  id: job.id,
38
58
  progress: job.progress,
39
59
  status: job.status,
@@ -42,10 +62,9 @@ function handle() {
42
62
  initiatedBy: (_a = job.initiatedBy) !== null && _a !== void 0 ? _a : null,
43
63
  createdAt: new Date(+job.createdAt).toISOString(),
44
64
  finishedAt: job.finishedAt ? new Date(+job.finishedAt).toISOString() : null,
45
- };
46
- return baseJob;
65
+ });
47
66
  });
48
- req.logInfo(`Returning ${filteredJobs.length} jobs`);
49
- res.send(filteredJobs);
67
+ req.logInfo(`Returning ${result.length} jobs`);
68
+ res.send(result);
50
69
  }));
51
70
  }
@@ -69,7 +69,15 @@ export interface IntegrationLogic extends ModuleKey {
69
69
  search?: any;
70
70
  page?: any;
71
71
  paginationData?: any;
72
+ entryIds?: string[];
72
73
  }) => Promise<TreeItem[] | ExtendedResult<TreeItem[]>>;
74
+ /**
75
+ * Indicates whether getIntegrationFiles supports filtering by entryIds.
76
+ * true — trusted; the 501 auto-detection guard is skipped.
77
+ * false — opted out; POST /crowdin-update returns 501 immediately when entryIds is supplied.
78
+ * undefined — auto-detected: if no returned file ID matches any requested entryId, returns 501.
79
+ */
80
+ supportsEntryIdSync?: boolean;
73
81
  /**
74
82
  * function to update crowdin files (e.g. pull integration data to crowdin source files)
75
83
  */
@@ -131,4 +131,6 @@ export type GetAllJobsParams = {
131
131
  crowdinId: string;
132
132
  limit: number;
133
133
  offset: number;
134
+ finished?: boolean;
135
+ excludeTypes?: JobType[];
134
136
  };