@crowdin/app-project-module 3.0.1 → 3.2.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.
Files changed (28) hide show
  1. package/out/middlewares/crowdin-client.d.ts +4 -2
  2. package/out/middlewares/crowdin-client.js +51 -14
  3. package/out/middlewares/integration-credentials.js +34 -12
  4. package/out/modules/integration/handlers/crowdin-files.js +34 -1
  5. package/out/modules/integration/handlers/user-error-detail.d.ts +2 -0
  6. package/out/modules/integration/handlers/user-error-detail.js +48 -0
  7. package/out/modules/integration/handlers/user-errors.d.ts +6 -0
  8. package/out/modules/integration/handlers/user-errors.js +39 -17
  9. package/out/modules/integration/index.js +8 -0
  10. package/out/modules/integration/types.d.ts +1 -0
  11. package/out/static/ui/main.bundle.js +116 -55
  12. package/out/static/ui/main.bundle.js.map +1 -1
  13. package/out/storage/drizzle/d1/index.js +2 -1
  14. package/out/storage/drizzle/d1/migrations/user-errors-indexes.d.ts +7 -0
  15. package/out/storage/drizzle/d1/migrations/user-errors-indexes.js +27 -0
  16. package/out/storage/drizzle/index.d.ts +8 -1
  17. package/out/storage/drizzle/index.js +75 -4
  18. package/out/storage/drizzle/postgresql/migrations/0003_user_errors_pagination_index.sql +5 -0
  19. package/out/storage/drizzle/postgresql/migrations/meta/0003_snapshot.json +646 -0
  20. package/out/storage/drizzle/postgresql/migrations/meta/_journal.json +7 -0
  21. package/out/storage/drizzle/sqlite/migrations/0002_user_errors_indexes.sql +4 -0
  22. package/out/storage/drizzle/sqlite/migrations/meta/0002_snapshot.json +687 -0
  23. package/out/storage/drizzle/sqlite/migrations/meta/_journal.json +7 -0
  24. package/out/storage/types.d.ts +30 -0
  25. package/out/types.d.ts +1 -0
  26. package/out/util/connection.d.ts +5 -0
  27. package/out/util/connection.js +7 -0
  28. package/package.json +1 -1
@@ -1,13 +1,15 @@
1
- import Crowdin from '@crowdin/crowdin-api-client';
1
+ import type Crowdin from '@crowdin/crowdin-api-client';
2
2
  import { Response } from 'express';
3
3
  import { Config, CrowdinClientRequest, CrowdinContextInfo, SubscriptionInfo } from '../types';
4
4
  import { LogErrorFunction, LogFunction } from '../util/logger';
5
- export declare function prepareCrowdinRequest({ jwtToken, config, optional, checkSubscriptionExpiration, moduleKey, }: {
5
+ export declare function prepareCrowdinRequest({ jwtToken, config, optional, checkSubscriptionExpiration, moduleKey, crowdinToken, projectId, }: {
6
6
  jwtToken: string;
7
7
  config: Config;
8
8
  optional: boolean;
9
9
  checkSubscriptionExpiration: boolean;
10
10
  moduleKey: string[] | string | undefined;
11
+ crowdinToken?: string;
12
+ projectId?: number;
11
13
  }): Promise<{
12
14
  context: CrowdinContextInfo;
13
15
  logInfo: LogFunction;
@@ -20,8 +20,17 @@ const logger_1 = require("../util/logger");
20
20
  const subscription_1 = require("../util/subscription");
21
21
  const integration_access_denied_1 = require("./integration-access-denied");
22
22
  const token_1 = require("../util/app-functions/token");
23
+ function assertModuleKey(moduleKey, jwtModule) {
24
+ if (!moduleKey || !jwtModule) {
25
+ return;
26
+ }
27
+ const matches = typeof moduleKey === 'string' ? moduleKey === jwtModule : moduleKey.includes(jwtModule);
28
+ if (!matches) {
29
+ throw new Error("Module key doesn't match");
30
+ }
31
+ }
23
32
  function prepareCrowdinRequest(_a) {
24
- return __awaiter(this, arguments, void 0, function* ({ jwtToken, config, optional = false, checkSubscriptionExpiration = true, moduleKey, }) {
33
+ return __awaiter(this, arguments, void 0, function* ({ jwtToken, config, optional = false, checkSubscriptionExpiration = true, moduleKey, crowdinToken, projectId, }) {
25
34
  (0, logger_1.log)('Validating jwt token from incoming request');
26
35
  const jwtPayload = yield (0, token_1.validateJwtToken)(jwtToken, config.clientSecret, config.jwtValidationOptions);
27
36
  const context = {
@@ -32,6 +41,29 @@ function prepareCrowdinRequest(_a) {
32
41
  };
33
42
  const logInfo = (0, logger_1.withContext)(context);
34
43
  const logError = (0, logger_1.withContextError)(context);
44
+ if (crowdinToken) {
45
+ logInfo('Using caller-supplied Crowdin token');
46
+ assertModuleKey(moduleKey, jwtPayload.module);
47
+ const client = yield (0, connection_1.buildCrowdinClientFromPat)({
48
+ config,
49
+ token: crowdinToken,
50
+ // For enterprise the org-specific URL implicitly restricts the token to this org.
51
+ // For Crowdin.com (no domain) we fall back to project-level access validation below.
52
+ organization: jwtPayload.domain || undefined,
53
+ });
54
+ try {
55
+ yield client.usersApi.getAuthenticatedUser();
56
+ // For non-enterprise (no domain) there is no org-scoped URL, so validate by
57
+ // checking that the PAT has access to the project named in the JWT / request.
58
+ if (!jwtPayload.domain && projectId) {
59
+ yield client.projectsGroupsApi.getProject(projectId);
60
+ }
61
+ }
62
+ catch (_b) {
63
+ throw new util_1.CodeError('Supplied crowdinToken is invalid or does not belong to this organization', 403);
64
+ }
65
+ return { context, client, logInfo, logError };
66
+ }
35
67
  logInfo('Loading crowdin credentials');
36
68
  const credentials = yield (0, storage_1.getStorage)().getCrowdinCredentials(context.crowdinId);
37
69
  if (!credentials) {
@@ -40,14 +72,7 @@ function prepareCrowdinRequest(_a) {
40
72
  }
41
73
  throw new Error("Can't find organization by id");
42
74
  }
43
- if (!!moduleKey && !!jwtPayload.module) {
44
- if (typeof moduleKey === 'string' && moduleKey !== jwtPayload.module) {
45
- throw new Error("Module key doesn't match");
46
- }
47
- if (Array.isArray(moduleKey) && !moduleKey.includes(jwtPayload.module)) {
48
- throw new Error("Module key doesn't match");
49
- }
50
- }
75
+ assertModuleKey(moduleKey, jwtPayload.module);
51
76
  logInfo('Building crowdin client instance');
52
77
  const { client, token } = yield (0, connection_1.prepareCrowdinClient)({ config, credentials, autoRenew: true, context });
53
78
  let subscriptionInfo;
@@ -67,22 +92,25 @@ function prepareCrowdinRequest(_a) {
67
92
  }
68
93
  function handle({ config, optional = false, checkSubscriptionExpiration = true, moduleKey, isIntegration = false, isLogout = false, }) {
69
94
  return (0, util_1.runAsyncWrapper)((req, res, next) => __awaiter(this, void 0, void 0, function* () {
70
- var _a, _b;
95
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
71
96
  const jwtToken = getToken(req);
72
97
  if (!jwtToken) {
73
98
  (0, logger_1.temporaryErrorDebug)('Access denied: crowdin-client', req);
74
99
  return res.status(403).send({ error: 'Access denied' });
75
100
  }
76
101
  try {
102
+ const isAgentCall = req.isApiCall && !!((_a = req.body) === null || _a === void 0 ? void 0 : _a.crowdinToken);
77
103
  const data = yield prepareCrowdinRequest({
78
104
  jwtToken,
79
105
  config,
80
106
  optional,
81
107
  checkSubscriptionExpiration,
82
108
  moduleKey,
109
+ crowdinToken: isAgentCall ? req.body.crowdinToken : undefined,
110
+ projectId: isAgentCall ? (_b = req.body) === null || _b === void 0 ? void 0 : _b.projectId : undefined,
83
111
  });
84
- if ((_a = config.api) === null || _a === void 0 ? void 0 : _a.default) {
85
- if (req.isApiCall && !((_b = req.body) === null || _b === void 0 ? void 0 : _b.projectId)) {
112
+ if ((_c = config.api) === null || _c === void 0 ? void 0 : _c.default) {
113
+ if (req.isApiCall && !((_d = req.body) === null || _d === void 0 ? void 0 : _d.projectId)) {
86
114
  return res.status(400).json({
87
115
  error: {
88
116
  message: 'Missing required parameter: projectId',
@@ -91,7 +119,10 @@ function handle({ config, optional = false, checkSubscriptionExpiration = true,
91
119
  }
92
120
  data.context = (0, api_1.updateCrowdinContext)(req, data.context);
93
121
  }
94
- if (isIntegration && data.client) {
122
+ // Skip integration access check when the caller supplies their own credentials —
123
+ // integration-credentials middleware handles those via the injected path.
124
+ const hasInjectedIntegrationCreds = req.isApiCall && !!((_e = req.body) === null || _e === void 0 ? void 0 : _e.integrationCredentials);
125
+ if (isIntegration && data.client && !hasInjectedIntegrationCreds) {
95
126
  const result = yield (0, connection_1.resolveIntegrationAccess)(data.context.clientId);
96
127
  if (!result.integrationCredentials && !isLogout) {
97
128
  const owners = yield (0, integration_access_denied_1.getIntegrationManagedBy)({
@@ -121,12 +152,18 @@ function handle({ config, optional = false, checkSubscriptionExpiration = true,
121
152
  req.subscriptionInfo = data.subscriptionInfo;
122
153
  req.logInfo = data.logInfo;
123
154
  req.logError = data.logError;
155
+ // Strip secrets from req.body so no downstream handler ever sees raw tokens.
156
+ // crowdinToken has been consumed; move integrationCredentials to a typed slot
157
+ // so the integration-credentials middleware can still pick it up.
158
+ req.injectedIntegrationCredentials = (_f = req.body) === null || _f === void 0 ? void 0 : _f.integrationCredentials;
159
+ (_g = req.body) === null || _g === void 0 ? true : delete _g.crowdinToken;
160
+ (_h = req.body) === null || _h === void 0 ? true : delete _h.integrationCredentials;
124
161
  next();
125
162
  }
126
163
  catch (e) {
127
164
  const errorMessage = (0, logger_1.getErrorMessage)(e);
128
165
  if (e instanceof util_1.CodeError) {
129
- return res.status(402).send({ message: errorMessage || 'Error', code: e.code });
166
+ return res.status((_j = e.code) !== null && _j !== void 0 ? _j : 402).send({ message: errorMessage || 'Error', code: e.code });
130
167
  }
131
168
  (0, logger_1.logError)(e);
132
169
  return res.status(403).send({ error: errorMessage || 'Error' });
@@ -15,9 +15,42 @@ const storage_1 = require("../storage");
15
15
  const util_1 = require("../util");
16
16
  const connection_1 = require("../util/connection");
17
17
  const integration_access_denied_1 = require("./integration-access-denied");
18
+ function loadIntegrationSettings(clientId, integration, req) {
19
+ return __awaiter(this, void 0, void 0, function* () {
20
+ const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(clientId);
21
+ let settings = (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config) ? JSON.parse(integrationConfig.config) || {} : {};
22
+ if (integration.normalizeSettings) {
23
+ settings = yield integration.normalizeSettings({
24
+ settings,
25
+ credentials: req.integrationCredentials,
26
+ client: req.crowdinApiClient,
27
+ });
28
+ }
29
+ return settings;
30
+ });
31
+ }
18
32
  function handle({ config, integration, optional = false, isLogout = false, }) {
19
33
  return (0, util_1.runAsyncWrapper)((req, res, next) => __awaiter(this, void 0, void 0, function* () {
20
34
  const isApiCall = req === null || req === void 0 ? void 0 : req.isApiCall;
35
+ // crowdin-client middleware moves integrationCredentials out of req.body before next(),
36
+ // so secrets are never visible to handlers. Read from the typed slot it left behind.
37
+ const rawInjected = req.injectedIntegrationCredentials;
38
+ if (isApiCall && rawInjected) {
39
+ delete req.injectedIntegrationCredentials;
40
+ req.logInfo('Using caller-supplied integration credentials');
41
+ req.integrationCredentials = typeof rawInjected === 'string' ? { accessToken: rawInjected } : rawInjected;
42
+ if (integration.checkConnection) {
43
+ try {
44
+ yield integration.checkConnection(req.integrationCredentials);
45
+ }
46
+ catch (e) {
47
+ return res.status(401).json({ error: { message: 'Supplied integrationCredentials are invalid' } });
48
+ }
49
+ }
50
+ const clientId = req.crowdinContext.clientId;
51
+ req.integrationSettings = yield loadIntegrationSettings(clientId, integration, req);
52
+ return next();
53
+ }
21
54
  req.logInfo(`Loading integration credentials for client ${req.crowdinContext.clientId}`);
22
55
  const { integrationCredentials: resolvedIntegrationCredentials, projectIntegrationCredentials, projectId, clientId: resolvedClientId, ownerIds, } = yield (0, connection_1.resolveIntegrationAccess)(req.crowdinContext.clientId);
23
56
  let integrationCredentials = resolvedIntegrationCredentials;
@@ -79,18 +112,7 @@ function handle({ config, integration, optional = false, isLogout = false, }) {
79
112
  throw new util_1.CodeError(message, 401);
80
113
  }
81
114
  }
82
- const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(clientId);
83
- if (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config) {
84
- let integrationSettings = JSON.parse(integrationConfig.config) || {};
85
- if (integration.normalizeSettings) {
86
- integrationSettings = yield integration.normalizeSettings({
87
- settings: integrationSettings,
88
- credentials: req.integrationCredentials,
89
- client: req.crowdinApiClient,
90
- });
91
- }
92
- req.integrationSettings = integrationSettings;
93
- }
115
+ req.integrationSettings = yield loadIntegrationSettings(clientId, integration, req);
94
116
  next();
95
117
  }));
96
118
  }
@@ -13,11 +13,24 @@ exports.default = handle;
13
13
  const util_1 = require("../../../util");
14
14
  const defaults_1 = require("../util/defaults");
15
15
  const logger_1 = require("../../../util/logger");
16
+ const types_1 = require("../types");
16
17
  const files_1 = require("../util/files");
18
+ const storage_1 = require("../../../storage");
17
19
  function handle(config, integration) {
18
20
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
19
- var _a;
21
+ var _a, _b;
20
22
  const mode = (_a = req.query) === null || _a === void 0 ? void 0 : _a.mode;
23
+ const externalId = (_b = req.query) === null || _b === void 0 ? void 0 : _b.externalId;
24
+ // Reverse lookup: given an integration file ID, return the mapped Crowdin file entry.
25
+ if (externalId) {
26
+ const syncedData = yield (0, storage_1.getStorage)().getSyncedData(req.crowdinContext.clientId, req.crowdinContext.crowdinId, types_1.Provider.INTEGRATION);
27
+ const syncedFiles = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.files) ? JSON.parse(syncedData.files) : [];
28
+ const entry = syncedFiles.find((f) => f.id === externalId);
29
+ if (!(entry === null || entry === void 0 ? void 0 : entry.crowdinFileId)) {
30
+ return res.status(404).json({ error: { message: 'No Crowdin file mapped to this external ID' } });
31
+ }
32
+ return res.json({ id: String(entry.crowdinFileId), externalFileId: externalId });
33
+ }
21
34
  req.logInfo(`Loading crowdin files${mode ? ` (mode: ${mode})` : ''}`);
22
35
  if (integration.getCrowdinFiles) {
23
36
  const rootFolder = yield (0, defaults_1.getRootFolder)(config, integration, req.crowdinApiClient, req.crowdinContext.jwtPayload.context.project_id);
@@ -41,6 +54,26 @@ function handle(config, integration) {
41
54
  files,
42
55
  });
43
56
  }
57
+ // Path B: populate externalFileId from stored integrationFileId→crowdinFileId mapping
58
+ // for files where the app developer hasn't supplied it directly (Path A).
59
+ // A leaf is either a typed file or explicitly flagged with nodeType '1';
60
+ // folders (nodeType '0') and branches (nodeType '2') without a type are skipped.
61
+ const isFileLeaf = (f) => f.type !== undefined || f.nodeType === '1';
62
+ const needsMapping = files.some((f) => isFileLeaf(f) && !f.externalFileId);
63
+ if (needsMapping) {
64
+ const syncedData = yield (0, storage_1.getStorage)().getSyncedData(req.crowdinContext.clientId, req.crowdinContext.crowdinId, types_1.Provider.INTEGRATION);
65
+ if (syncedData === null || syncedData === void 0 ? void 0 : syncedData.files) {
66
+ const syncedFiles = JSON.parse(syncedData.files);
67
+ const byCrowdinFileId = new Map(syncedFiles
68
+ .filter((e) => e.crowdinFileId !== null && e.crowdinFileId !== undefined)
69
+ .map((e) => [String(e.crowdinFileId), e.id]));
70
+ if (byCrowdinFileId.size > 0) {
71
+ files = files.map((f) => !isFileLeaf(f) || f.externalFileId || !byCrowdinFileId.has(f.id)
72
+ ? f
73
+ : Object.assign(Object.assign({}, f), { externalFileId: byCrowdinFileId.get(f.id) }));
74
+ }
75
+ }
76
+ }
44
77
  req.logInfo(`Returning ${files.length} items`);
45
78
  res.send(files);
46
79
  }
@@ -0,0 +1,2 @@
1
+ import { Response } from 'express';
2
+ export default function handle(): (req: import("express").Request | import("../../..").CrowdinClientRequest, res: Response, next: Function) => void;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.default = handle;
13
+ const storage_1 = require("../../../storage");
14
+ const util_1 = require("../../../util");
15
+ const logger_1 = require("../../../util/logger");
16
+ const INVALID_DATA = JSON.stringify({
17
+ appData: { Warning: 'Invalid response data. Please contact the Crowdin support manager.' },
18
+ });
19
+ function handle() {
20
+ return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
21
+ const id = Number.parseInt(`${req.params.id}`, 10);
22
+ if (!Number.isInteger(id)) {
23
+ return res.status(400).json({
24
+ error: {
25
+ message: 'Invalid error log id',
26
+ },
27
+ });
28
+ }
29
+ const userError = yield (0, storage_1.getStorage)().getUserError({
30
+ id,
31
+ crowdinId: req.crowdinContext.crowdinId,
32
+ integrationId: req.crowdinContext.clientId,
33
+ });
34
+ if (!userError) {
35
+ req.logInfo(`Error log ${id} not found`);
36
+ return res.status(404).json({
37
+ error: {
38
+ message: `Error log with ID ${id} not found`,
39
+ },
40
+ });
41
+ }
42
+ if (!userError.data || !(0, util_1.isJson)(userError.data)) {
43
+ (0, logger_1.logError)(userError);
44
+ return res.send({ data: INVALID_DATA });
45
+ }
46
+ res.send({ data: userError.data });
47
+ }));
48
+ }
@@ -1,2 +1,8 @@
1
1
  import { Response } from 'express';
2
+ export declare function parseUserErrorsQuery(query?: any): {
3
+ limit: number;
4
+ offset: number;
5
+ orderDirection: "asc" | "desc";
6
+ search: any;
7
+ };
2
8
  export default function handle(): (req: import("express").Request | import("../../..").CrowdinClientRequest, res: Response, next: Function) => void;
@@ -9,27 +9,49 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.parseUserErrorsQuery = parseUserErrorsQuery;
12
13
  exports.default = handle;
13
14
  const storage_1 = require("../../../storage");
14
15
  const util_1 = require("../../../util");
15
- const logger_1 = require("../../../util/logger");
16
+ const DEFAULT_LIMIT = 10;
17
+ const MAX_LIMIT = 100;
18
+ const MAX_SEARCH_LENGTH = 255;
19
+ function parseInteger(value, fallback) {
20
+ const parsed = Number.parseInt(`${value}`, 10);
21
+ return Number.isFinite(parsed) ? parsed : fallback;
22
+ }
23
+ function parseUserErrorsQuery(query = {}) {
24
+ const search = typeof query.search === 'string' ? query.search.trim().slice(0, MAX_SEARCH_LENGTH) : '';
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
+ orderDirection: (query.order === 'asc' ? 'asc' : 'desc'),
29
+ search: search || undefined,
30
+ };
31
+ }
16
32
  function handle() {
17
33
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
18
- var _a;
19
- const userTimezone = (yield req.crowdinApiClient.usersApi.getAuthenticatedUser()).data.timezone;
20
- req.logInfo(`Loading user errors for crowdinId ${req.crowdinContext.crowdinId} and integrationId ${req.crowdinContext.clientId}`);
21
- const userErrors = (_a = (yield (0, storage_1.getStorage)().getAllUserErrors(req.crowdinContext.crowdinId, req.crowdinContext.clientId))) === null || _a === void 0 ? void 0 : _a.map((userError) => {
22
- if (!(0, util_1.isJson)(userError.data)) {
23
- (0, logger_1.logError)(userError);
24
- userError.data = JSON.stringify({
25
- appData: { Warning: 'Invalid response data. Please contact the Crowdin support manager.' },
26
- });
27
- }
28
- const date = new Date(+userError.createdAt);
29
- const formattedDate = (0, util_1.getFormattedDate)({ date, userTimezone });
30
- return Object.assign(Object.assign({}, userError), { formattedDate });
31
- });
32
- req.logInfo(`Returning ${userErrors === null || userErrors === void 0 ? void 0 : userErrors.length} user errors`);
33
- res.send(userErrors);
34
+ const { limit, offset, orderDirection, search } = parseUserErrorsQuery(req.query);
35
+ const filter = {
36
+ crowdinId: req.crowdinContext.crowdinId,
37
+ integrationId: req.crowdinContext.clientId,
38
+ search,
39
+ };
40
+ req.logInfo(`Loading user errors for crowdinId ${req.crowdinContext.crowdinId} and integrationId ${req.crowdinContext.clientId} (limit ${limit}, offset ${offset})`);
41
+ const [userTimezone, userErrors, total] = yield Promise.all([
42
+ req.crowdinApiClient.usersApi
43
+ .getAuthenticatedUser()
44
+ .then(({ data }) => data.timezone)
45
+ .catch((e) => {
46
+ const errorMessage = e instanceof Error ? e.message : String(e);
47
+ req.logError(`Can't load user timezone, falling back to server timezone: ${errorMessage}`);
48
+ return undefined;
49
+ }),
50
+ (0, storage_1.getStorage)().getUserErrors(Object.assign(Object.assign({}, filter), { limit, offset, orderDirection })),
51
+ (0, storage_1.getStorage)().countUserErrors(filter),
52
+ ]);
53
+ const data = userErrors.map((userError) => (Object.assign(Object.assign({}, userError), { formattedDate: (0, util_1.getFormattedDate)({ date: new Date(+userError.createdAt), userTimezone }) })));
54
+ req.logInfo(`Returning ${data.length} of ${total} user errors`);
55
+ res.send({ data, total });
34
56
  }));
35
57
  }
@@ -43,6 +43,7 @@ const settings_save_1 = __importDefault(require("./handlers/settings-save"));
43
43
  const subscription_info_1 = __importDefault(require("./handlers/subscription-info"));
44
44
  const sync_settings_1 = __importDefault(require("./handlers/sync-settings"));
45
45
  const sync_settings_save_1 = __importDefault(require("./handlers/sync-settings-save"));
46
+ const user_error_detail_1 = __importDefault(require("./handlers/user-error-detail"));
46
47
  const user_errors_1 = __importDefault(require("./handlers/user-errors"));
47
48
  const users_1 = __importDefault(require("./handlers/users"));
48
49
  const invite_users_1 = __importDefault(require("./handlers/invite-users"));
@@ -240,6 +241,13 @@ function register({ config, app }) {
240
241
  moduleKey: integrationLogic.key,
241
242
  isIntegration: true,
242
243
  }), (0, integration_credentials_1.default)({ config, integration: integrationLogic }), (0, user_errors_1.default)());
244
+ app.get('/api/user-errors/:id', json_response_1.handle, (0, crowdin_client_1.handle)({
245
+ config,
246
+ optional: false,
247
+ checkSubscriptionExpiration: true,
248
+ moduleKey: integrationLogic.key,
249
+ isIntegration: true,
250
+ }), (0, integration_credentials_1.default)({ config, integration: integrationLogic }), (0, user_error_detail_1.default)());
243
251
  app.get('/api/users', json_response_1.handle, (0, crowdin_client_1.handle)({
244
252
  config,
245
253
  optional: false,
@@ -451,6 +451,7 @@ export interface BaseTreeItem {
451
451
  path?: string;
452
452
  type?: string;
453
453
  syncedAt?: string;
454
+ externalFileId?: string;
454
455
  }
455
456
  export interface File extends BaseTreeItem {
456
457
  type: SourceFilesModel.FileType;