@crowdin/app-project-module 0.28.0-3 → 0.28.0-4
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.
- package/CONTRIBUTING.md +19 -1
- package/README.md +1 -1004
- package/out/handlers/form-data-display.js +5 -1
- package/out/handlers/form-data-save.js +5 -1
- package/out/index.js +6 -2
- package/out/models/index.d.ts +6 -6
- package/out/util/cron.js +5 -1
- package/out/util/defaults.js +5 -1
- package/out/util/index.js +5 -1
- package/out/util/webhooks.js +6 -2
- package/package.json +8 -8
package/CONTRIBUTING.md
CHANGED
|
@@ -59,12 +59,30 @@ Unsure where to begin contributing to Crowdin App Project module? You can start
|
|
|
59
59
|
|
|
60
60
|
Before sending your pull requests, make sure you followed the list below:
|
|
61
61
|
|
|
62
|
-
- Read this
|
|
62
|
+
- Read this guideline.
|
|
63
63
|
- Read [Code of Conduct](/CODE_OF_CONDUCT.md).
|
|
64
64
|
- Ensure that your code adheres to standard conventions, as used in the rest of the project.
|
|
65
65
|
- Ensure that there are unit tests for your code.
|
|
66
66
|
- Run unit tests.
|
|
67
67
|
|
|
68
|
+
##### Contributing to the docs
|
|
69
|
+
|
|
70
|
+
First of all, you need to install Python (v3). Then install dependencies:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install mkdocs
|
|
74
|
+
pip install mkdocs-autorefs
|
|
75
|
+
pip install mkdocs-material
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Run:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
mkdocs serve
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Visit http://127.0.0.1:8000/.
|
|
85
|
+
|
|
68
86
|
#### Philosophy of code contribution
|
|
69
87
|
|
|
70
88
|
- Include unit tests when you contribute new features, as they help to a) prove that your code works correctly, and b) guard against future breaking changes to lower the maintenance cost.
|
package/README.md
CHANGED
|
@@ -2,1011 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Module that will automatically add all necessary endpoints for Crowdin App.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
:bookmark: See the [docs](/docs) for more information.
|
|
6
6
|
|
|
7
|
-
Module expose two main methods:
|
|
8
|
-
|
|
9
|
-
- `createApp` to fully create for you Express application
|
|
10
|
-
- `addCrowdinEndpoints` to extend your Express application
|
|
11
|
-
|
|
12
|
-
In both options you will need to provide Crowdin App configuration file. Please refer to jsdoc for more details.
|
|
13
|
-
|
|
14
|
-
`addCrowdinEndpoints` will return an object with several methods to help you add your own custom logic [see example](#resources).
|
|
15
|
-
|
|
16
|
-
- `saveMetadata` to save metadata (may be associated with an organization, project, etc)
|
|
17
|
-
- `getMetadata` to get metadata
|
|
18
|
-
- `deleteMetadata` to delete metadata (usually useful in `onUninstall` hook)
|
|
19
|
-
- `getUserSettings` to get settings that users manage in the integration module
|
|
20
|
-
- `establishCrowdinConnection` method that accept jwt token that you may forward from module UI and it will return back Crowdin client instance and the context.
|
|
21
|
-
|
|
22
|
-
## Table of Contents
|
|
23
|
-
|
|
24
|
-
- [Installation](#installation)
|
|
25
|
-
- [Sample Project Integration App](#sample-project-integration-app)
|
|
26
|
-
- [Payment](#payment)
|
|
27
|
-
- [Authorization](#authorization)
|
|
28
|
-
- [Custom login form](#customize-your-app-login-form)
|
|
29
|
-
- [OAuth2 login](#oauth2-support)
|
|
30
|
-
- [Storage](#storage)
|
|
31
|
-
- [SQLite](#sqlite)
|
|
32
|
-
- [PostgreSQL](#postgresql)
|
|
33
|
-
- [MySQL](#mysql)
|
|
34
|
-
- [Settings window](#settings-window)
|
|
35
|
-
- [Info window](#info-window)
|
|
36
|
-
- [Background tasks](#background-tasks)
|
|
37
|
-
- [Errors handling](#errors-handling)
|
|
38
|
-
- [Error propagation](#error-propagation)
|
|
39
|
-
- [Error interceptor](#error-interceptor)
|
|
40
|
-
- [Debug mode](#debug-mode)
|
|
41
|
-
- [Modules](#modules)
|
|
42
|
-
- [Custom File Format](#custom-file-format)
|
|
43
|
-
- [Custom MT](#custom-mt)
|
|
44
|
-
- [Profile Resources Menu](#profile-resources-menu)
|
|
45
|
-
- [Other modules](#other-modules)
|
|
46
|
-
- [Other options](#other-options)
|
|
47
|
-
- [Contributing](#contributing)
|
|
48
|
-
- [Seeking Assistance](#seeking-assistance)
|
|
49
|
-
- [License](#license)
|
|
50
|
-
|
|
51
|
-
## Installation
|
|
52
|
-
|
|
53
|
-
### npm
|
|
54
|
-
|
|
55
|
-
`npm i @crowdin/app-project-module`
|
|
56
|
-
|
|
57
|
-
### yarn
|
|
58
|
-
|
|
59
|
-
`yarn add @crowdin/app-project-module`
|
|
60
|
-
|
|
61
|
-
## Sample Project Integration App
|
|
62
|
-
|
|
63
|
-
```javascript
|
|
64
|
-
const crowdinModule = require('@crowdin/app-project-module');
|
|
65
|
-
const crowdinAppFunctions = require('@crowdin/crowdin-apps-functions');
|
|
66
|
-
const axios = require('axios').default;
|
|
67
|
-
|
|
68
|
-
const configuration = {
|
|
69
|
-
baseUrl: 'https://123.ngrok.io',
|
|
70
|
-
clientId: 'clientId',
|
|
71
|
-
clientSecret: 'clientSecret',
|
|
72
|
-
scopes: [
|
|
73
|
-
crowdinModule.Scope.PROJECTS,
|
|
74
|
-
crowdinModule.Scope.TRANSLATION_MEMORIES
|
|
75
|
-
],
|
|
76
|
-
name: 'Sample App',
|
|
77
|
-
identifier: 'sample-app',
|
|
78
|
-
description: 'Sample App description',
|
|
79
|
-
dbFolder: __dirname,
|
|
80
|
-
imagePath: __dirname + '/' + 'logo.png',
|
|
81
|
-
crowdinUrls: { // custom urls to override
|
|
82
|
-
// apiUrl: 'https://<copy_name>.crowdin.dev/api/v2', // 'https://<org_name>.<copy_name>.crowdin.dev/api/v2' for enterprise
|
|
83
|
-
// accountUrl: 'https://accounts.<copy_name>.crowdin.dev/oauth/token', // (default https://accounts.crowdin.com/oauth/token)
|
|
84
|
-
// subscriptionUrl: 'https://<copy_name>.crowdin.dev' // (default https://crowdin.com or https://org.api.crowdin.com)
|
|
85
|
-
},
|
|
86
|
-
projectIntegration: {
|
|
87
|
-
withRootFolder: true,
|
|
88
|
-
withCronSync: {
|
|
89
|
-
crowdin: true,
|
|
90
|
-
integration: true,
|
|
91
|
-
},
|
|
92
|
-
webhooks: { //enable webhook
|
|
93
|
-
crowdinWebhookUrl: '/notify', // override crowdin webhook endpoint
|
|
94
|
-
integrationWebhookUrl: '/update-entity', // override integration webhook endpoint
|
|
95
|
-
urlParam: 'information', // override request param name
|
|
96
|
-
crowdinWebhooks: async (client, projectId, available, appSettings) => { // optional
|
|
97
|
-
// here you need to create crowdin webhooks if the default doesn't work for you
|
|
98
|
-
// used with crowdinWebhookInterceptor
|
|
99
|
-
await client.webhooksApi.addWebhook(projectId, {
|
|
100
|
-
'Application webhook',
|
|
101
|
-
url: 'https://123.ngrok.io/notify',
|
|
102
|
-
events: ['file.translated'],
|
|
103
|
-
requestType: 'POST',
|
|
104
|
-
});
|
|
105
|
-
},
|
|
106
|
-
crowdinWebhookInterceptor: async (projectId, client, appRootFolder, appSettings, syncSettings) => { // optional
|
|
107
|
-
// used with crowdinWebhooks
|
|
108
|
-
return {
|
|
109
|
-
'101': ['de', 'fr'],
|
|
110
|
-
'103': ['de'],
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
integrationWebhooks: async (credentials, urlParam, available, appSettings, syncSettings) => {
|
|
114
|
-
// here you need to create webhooks on the integration side
|
|
115
|
-
// used with integrationWebhookInterceptor or RabbitMQ queue
|
|
116
|
-
await axios.post('https://site.com/api/webhooks');
|
|
117
|
-
},
|
|
118
|
-
integrationWebhookInterceptor: async (projectId, client, rootFolder, appSettings, syncSettings, webhookRequest) => {
|
|
119
|
-
// used with integrationWebhooks
|
|
120
|
-
return [
|
|
121
|
-
{
|
|
122
|
-
id: '47282999980',
|
|
123
|
-
name: 'Home page',
|
|
124
|
-
parentId: '1',
|
|
125
|
-
type: 'json',
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
id: '73291251883',
|
|
129
|
-
name: 'Intro',
|
|
130
|
-
parentId: '2',
|
|
131
|
-
type: 'json',
|
|
132
|
-
},
|
|
133
|
-
];
|
|
134
|
-
},
|
|
135
|
-
queueUrl: 'amqp://localhost:5672', // RabbitMQ url to listen to the webhook queue,
|
|
136
|
-
},
|
|
137
|
-
integrationPagination: true, //enable pagination on integration files list
|
|
138
|
-
integrationOneLevelFetching: true, //turn on request when opening a directory and pass its id
|
|
139
|
-
integrationSearchListener: true, //turn on search listener and pass search string
|
|
140
|
-
uploadTranslations: true, //enable the option to download translations from integration.
|
|
141
|
-
getIntegrationFiles: async (credentials, appSettings, parentId, search, page) => {
|
|
142
|
-
//here you need to fetch files/objects from integration
|
|
143
|
-
const res = [
|
|
144
|
-
{
|
|
145
|
-
id: '12',
|
|
146
|
-
name: 'File from integration',
|
|
147
|
-
type: 'json',
|
|
148
|
-
parentId: '10'
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
id: '14',
|
|
152
|
-
name: 'File from integration2',
|
|
153
|
-
type: 'xml',
|
|
154
|
-
parentId: '11'
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
id: '10',
|
|
158
|
-
name: 'Folder from integration'
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
id: '11',
|
|
162
|
-
name: 'Folder from integratio2'
|
|
163
|
-
customContent: `<div style='color: red;'>Custom Folder name</div>`,
|
|
164
|
-
parentId: '1'
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
id: '1',
|
|
168
|
-
name: 'Branch from integratio',
|
|
169
|
-
nodeType: '2'
|
|
170
|
-
},
|
|
171
|
-
];
|
|
172
|
-
return {
|
|
173
|
-
data: res,
|
|
174
|
-
message: 'Test',
|
|
175
|
-
stopPagination: true, //send if no next files page
|
|
176
|
-
};
|
|
177
|
-
},
|
|
178
|
-
getFileProgress: async (projectId, client, fileId) => { //Use this function if you require a customized translation progress.
|
|
179
|
-
return {
|
|
180
|
-
[fileId]: [
|
|
181
|
-
{
|
|
182
|
-
languageId: 'uk',
|
|
183
|
-
eTag: '49a76a1632973b00175dac942976f7c6',
|
|
184
|
-
words: {
|
|
185
|
-
total: 180,
|
|
186
|
-
translated: 0,
|
|
187
|
-
approved: 0
|
|
188
|
-
},
|
|
189
|
-
phrases: {
|
|
190
|
-
total: 6,
|
|
191
|
-
translated: 0,
|
|
192
|
-
approved: 0
|
|
193
|
-
},
|
|
194
|
-
translationProgress: 100,
|
|
195
|
-
approvalProgress: 0
|
|
196
|
-
}
|
|
197
|
-
]
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
updateCrowdin: async (projectId, client, credentials, request, rootFolder, appSettings, uploadTranslations) => {
|
|
201
|
-
//here you need to get data from integration and upload it to Crowdin
|
|
202
|
-
console.log(`Request for updating data in Crowdin ${JSON.stringify(request)}`);
|
|
203
|
-
const directories = await client.sourceFilesApi
|
|
204
|
-
.withFetchAll()
|
|
205
|
-
.listProjectDirectories(projectId);
|
|
206
|
-
const { folder, files } = await crowdinAppFunctions.getOrCreateFolder({
|
|
207
|
-
directories: directories.data.map((d) => d.data),
|
|
208
|
-
client,
|
|
209
|
-
projectId,
|
|
210
|
-
directoryName: 'Folder from integration',
|
|
211
|
-
parentDirectory: rootFolder
|
|
212
|
-
});
|
|
213
|
-
const fileContent = {
|
|
214
|
-
title: 'Hello World',
|
|
215
|
-
};
|
|
216
|
-
const fileName = 'integration.json';
|
|
217
|
-
const fileId = await crowdinAppFunctions.updateOrCreateFile({
|
|
218
|
-
client,
|
|
219
|
-
projectId,
|
|
220
|
-
name: fileName,
|
|
221
|
-
title: 'Sample file from integration',
|
|
222
|
-
type: 'json',
|
|
223
|
-
directoryId: folder.id,
|
|
224
|
-
data: fileContent,
|
|
225
|
-
file: files.find((f) => f.name === 'integration.json'),
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
if (uploadTranslations) {
|
|
229
|
-
const fileTranslation = {
|
|
230
|
-
title: 'Hola Mundo',
|
|
231
|
-
};
|
|
232
|
-
await crowdinAppFunctions.uploadTranslations(
|
|
233
|
-
client,
|
|
234
|
-
projectId,
|
|
235
|
-
fileId,
|
|
236
|
-
'es',
|
|
237
|
-
fileName,
|
|
238
|
-
fileTranslation,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
message: 'Some message',
|
|
244
|
-
};
|
|
245
|
-
},
|
|
246
|
-
updateIntegration: async (projectId, client, credentials, request, rootFolder, appSettings) => {
|
|
247
|
-
//here should be logic to get translations from Crowdin and upload them to integration
|
|
248
|
-
console.log(`Request for updating data in Integration ${JSON.stringify(request)}`);
|
|
249
|
-
const directories = await client.sourceFilesApi
|
|
250
|
-
.withFetchAll()
|
|
251
|
-
.listProjectDirectories(projectId);
|
|
252
|
-
const { files } = await crowdinAppFunctions.getFolder({
|
|
253
|
-
directories: directories.data.map((d) => d.data),
|
|
254
|
-
client,
|
|
255
|
-
projectId,
|
|
256
|
-
directoryName: 'Folder from integration',
|
|
257
|
-
parentDirectory: rootFolder
|
|
258
|
-
});
|
|
259
|
-
const file = files.find((f) => f.name === 'integration.json');
|
|
260
|
-
if (file) {
|
|
261
|
-
const translationsLink =
|
|
262
|
-
await client.translationsApi.buildProjectFileTranslation(
|
|
263
|
-
projectId,
|
|
264
|
-
file.id,
|
|
265
|
-
{ targetLanguageId: 'uk' },
|
|
266
|
-
);
|
|
267
|
-
if (!translationsLink) {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
const response = await axios.get(translationsLink.data.url);
|
|
271
|
-
console.log(response.data);
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
onLogout: async (projectId, client, credentials, appSettings) => {
|
|
275
|
-
//cleanup logic
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
crowdinModule.createApp(configuration);
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
## Payment
|
|
284
|
-
|
|
285
|
-
By default App does not have any subscription and it's free to use. But you can override this.
|
|
286
|
-
|
|
287
|
-
```javascript
|
|
288
|
-
configuration.pricing = {
|
|
289
|
-
plantType: 'recurring',
|
|
290
|
-
trial: 14, //amount of days to use app for free
|
|
291
|
-
trialCrowdin: 14, //amount of days specifically in crowdin workspace
|
|
292
|
-
trialEnterprise: 30, //amount of days specifically for enterprise
|
|
293
|
-
cachingSeconds: 12 * 60 * 60, //time in seconds of how long to cache subscription info
|
|
294
|
-
infoDisplayDaysThreshold: 14 //number of days threshold to check if subscription info should be displayed (if not defined then info will be always visible)
|
|
295
|
-
};
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
## Authorization
|
|
299
|
-
|
|
300
|
-
### Customize your app login form
|
|
301
|
-
|
|
302
|
-
By default, login page for your app will require only to enter `apiToken` to communicate with third party service.
|
|
303
|
-
But there is also a possibility to customize it.
|
|
304
|
-
|
|
305
|
-
```javascript
|
|
306
|
-
configuration.projectIntegration.loginForm = {
|
|
307
|
-
fields: [
|
|
308
|
-
{
|
|
309
|
-
key: 'username',
|
|
310
|
-
label: 'Username',
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
key: 'password',
|
|
314
|
-
label: 'Password',
|
|
315
|
-
type: 'password'
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
label: 'Api creds',
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
helpText: 'Api Key for http requests',
|
|
322
|
-
key: 'apiKey',
|
|
323
|
-
label: 'Api Key'
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
key: 'server',
|
|
327
|
-
label: 'Data center',
|
|
328
|
-
type: 'select',
|
|
329
|
-
defaultValue: '1',
|
|
330
|
-
options: [
|
|
331
|
-
{
|
|
332
|
-
value: '1',
|
|
333
|
-
label: 'USA'
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
value: '2',
|
|
337
|
-
label: 'EU'
|
|
338
|
-
}
|
|
339
|
-
]
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
key: 'apiKey',
|
|
343
|
-
label: 'Api key',
|
|
344
|
-
type: 'textarea'
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
key: 'fileSecret',
|
|
348
|
-
label: 'Upload file',
|
|
349
|
-
type: 'file',
|
|
350
|
-
accept: '.txt'
|
|
351
|
-
},
|
|
352
|
-
]
|
|
353
|
-
};
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
### OAuth2 support
|
|
357
|
-
|
|
358
|
-
In case if third party service uses OAuth2 for authorization use `oauthLogin` field to configure it.
|
|
359
|
-
`loginForm` **in this case should remain undefined**.
|
|
360
|
-
|
|
361
|
-
Github example:
|
|
362
|
-
|
|
363
|
-
```javascript
|
|
364
|
-
configuration.projectIntegration.oauthLogin = {
|
|
365
|
-
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
|
366
|
-
clientId: 'github_app_client_id',
|
|
367
|
-
clientSecret: 'github_app_client_secret',
|
|
368
|
-
accessTokenUrl: 'https://github.com/login/oauth/access_token'
|
|
369
|
-
}
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
Google example:
|
|
373
|
-
|
|
374
|
-
```javascript
|
|
375
|
-
configuration.projectIntegration.oauthLogin = {
|
|
376
|
-
scope: 'https%3A//www.googleapis.com/auth/userinfo.email',
|
|
377
|
-
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
378
|
-
clientId: 'google_web_app_client_id',
|
|
379
|
-
clientSecret: 'google_web_app_client_secret',
|
|
380
|
-
accessTokenUrl: 'https://oauth2.googleapis.com/token',
|
|
381
|
-
extraAutorizationUrlParameters: {
|
|
382
|
-
response_type: 'code',
|
|
383
|
-
access_type: 'offline',
|
|
384
|
-
prompt: 'consent'
|
|
385
|
-
},
|
|
386
|
-
extraAccessTokenParameters: {
|
|
387
|
-
grant_type: 'authorization_code'
|
|
388
|
-
},
|
|
389
|
-
extraRefreshTokenParameters: {
|
|
390
|
-
grant_type: 'refresh_token'
|
|
391
|
-
},
|
|
392
|
-
refresh: true
|
|
393
|
-
}
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
The `oauthLogin` property allows you to customize many different properties, mappings, etc. So that you can integrate with any OAuth2 implementation.
|
|
397
|
-
Main default values:
|
|
398
|
-
|
|
399
|
-
- redirect uri prefix will be `/oauth/code`
|
|
400
|
-
- client id field name in url parameters and in request payload will be `client_id`
|
|
401
|
-
- client secret field name in request payload will be `client_secret`
|
|
402
|
-
- access token field name should be `access_token`
|
|
403
|
-
- be default assumption is that token do not have any expiration date, to change this behavior use `refresh` flag so then refresh token and expires in will be taken into consideration
|
|
404
|
-
- access token field name should be `refresh_token`
|
|
405
|
-
- expires in field name should be `expires_in` (value should be in seconds)
|
|
406
|
-
|
|
407
|
-
This module rely that OAuth2 protocol is implemented by third party service in this way:
|
|
408
|
-
|
|
409
|
-
- request for access token should be done via POST request to `accessTokenUrl` with JSON body that will contain at least `clientId`, `clientSecret`, `code` and `redirectUri` (also possible to add extra fields via `extraAccessTokenParameters` property)
|
|
410
|
-
- request to refresh token should be done via POST request to `accessTokenUrl` (or `refreshTokenUrl` if defined) with JSON body that will contain at least `clientId`, `clientSecret` and `refreshToken` (also possible to add extra fields via `extraRefreshTokenParameters` property)
|
|
411
|
-
- both requests will return JSON response with body that contains `accessToken` and, if enabled, `refreshToken` (optional) and `expireIn`
|
|
412
|
-
|
|
413
|
-
To override those requests please use `performGetTokenRequest` and `performRefreshTokenRequest` (e.g. when requests should be done with different HTTP methods or data should be tranfered as query string or form data).
|
|
414
|
-
|
|
415
|
-
Mailup example:
|
|
416
|
-
|
|
417
|
-
```javascript
|
|
418
|
-
const clientId = 'client_id';
|
|
419
|
-
const clientSecret = 'client_secret';
|
|
420
|
-
const tokenUrl = 'https://services.mailup.com/Authorization/OAuth/Token';
|
|
421
|
-
|
|
422
|
-
configuration.projectIntegration.oauthLogin = {
|
|
423
|
-
authorizationUrl: 'https://services.mailup.com/Authorization/OAuth/LogOn',
|
|
424
|
-
clientId,
|
|
425
|
-
clientSecret,
|
|
426
|
-
extraAutorizationUrlParameters: {
|
|
427
|
-
response_type: 'code'
|
|
428
|
-
},
|
|
429
|
-
refresh: true,
|
|
430
|
-
performGetTokenRequest: async (code, query, url) => {
|
|
431
|
-
//query is an object with all query params
|
|
432
|
-
//url is an url string that OAuth server used to call us back
|
|
433
|
-
const url = `${tokenUrl}?code=${code}&grant_type=authorization_code`;
|
|
434
|
-
const headers = {
|
|
435
|
-
'Authorization': `Bearer ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
|
|
436
|
-
};
|
|
437
|
-
return (await axios.get(url, { headers })).data;
|
|
438
|
-
},
|
|
439
|
-
performRefreshTokenRequest: async (credentials) => {
|
|
440
|
-
const params = {
|
|
441
|
-
refresh_token: credentials.refreshToken,
|
|
442
|
-
grant_type: 'refresh_token',
|
|
443
|
-
client_id: clientId,
|
|
444
|
-
client_secret: clientSecret
|
|
445
|
-
};
|
|
446
|
-
const data = Object.keys(params)
|
|
447
|
-
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
|
|
448
|
-
.join('&');
|
|
449
|
-
const headers = {
|
|
450
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
451
|
-
};
|
|
452
|
-
return (await axios.post(tokenUrl, data, { headers })).data;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
In addition you can specify extra fields on login screen that you can use to dynamically build authorization url.
|
|
458
|
-
|
|
459
|
-
```javascript
|
|
460
|
-
configuration.projectIntegration.oauthLogin.loginFields = [
|
|
461
|
-
{
|
|
462
|
-
key: 'region',
|
|
463
|
-
label: 'Region',
|
|
464
|
-
type: 'select',
|
|
465
|
-
defaultValue: 'NA',
|
|
466
|
-
options: [
|
|
467
|
-
{
|
|
468
|
-
value: 'NA',
|
|
469
|
-
label: 'North American'
|
|
470
|
-
},
|
|
471
|
-
{
|
|
472
|
-
value: 'EU',
|
|
473
|
-
label: 'Europe'
|
|
474
|
-
},
|
|
475
|
-
{
|
|
476
|
-
value: 'AZURE_NA',
|
|
477
|
-
label: 'Azure North American'
|
|
478
|
-
}
|
|
479
|
-
]
|
|
480
|
-
}
|
|
481
|
-
];
|
|
482
|
-
|
|
483
|
-
configuration.projectIntegration.oauthLogin.getAuthorizationUrl = (redirectUrl, loginForm) => {
|
|
484
|
-
const region = loginForm.region;
|
|
485
|
-
if (region === 'EU') {
|
|
486
|
-
return `https://eu-region.com?client_id=<client-id>&redirect_uri=${redirectUrl}`;
|
|
487
|
-
} else {
|
|
488
|
-
return `https://default-region.com?client_id=<client-id>&redirect_uri=${redirectUrl}`;
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
Please refer to jsdoc for more details.
|
|
494
|
-
|
|
495
|
-
## Storage
|
|
496
|
-
|
|
497
|
-
Module can be configured to use following storages:
|
|
498
|
-
|
|
499
|
-
### SQLite
|
|
500
|
-
|
|
501
|
-
```javascript
|
|
502
|
-
//specify folder where sqlite db file will be located
|
|
503
|
-
configuration.dbFolder = __dirname;
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
### PostgreSQL
|
|
507
|
-
|
|
508
|
-
```javascript
|
|
509
|
-
configuration.postgreConfig = {
|
|
510
|
-
host: 'localhost',
|
|
511
|
-
user: 'postgres',
|
|
512
|
-
password: 'password',
|
|
513
|
-
database: 'test'
|
|
514
|
-
};
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
### MySQL
|
|
518
|
-
|
|
519
|
-
```javascript
|
|
520
|
-
configuration.mysqlConfig = {
|
|
521
|
-
host: 'localhost',
|
|
522
|
-
user: 'root',
|
|
523
|
-
password: 'password',
|
|
524
|
-
database: 'test'
|
|
525
|
-
};
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
## Settings window
|
|
529
|
-
|
|
530
|
-
It is also possible to define settings window for your app where users can customize integration flow.
|
|
531
|
-
|
|
532
|
-
```javascript
|
|
533
|
-
configuration.projectIntegration.getConfiguration = (projectId, crowdinClient, integrationCredentials) => {
|
|
534
|
-
return [
|
|
535
|
-
{
|
|
536
|
-
label: 'GENERAL'
|
|
537
|
-
},
|
|
538
|
-
{
|
|
539
|
-
key: 'flag',
|
|
540
|
-
label: 'Checkbox',
|
|
541
|
-
type: 'checkbox'
|
|
542
|
-
},
|
|
543
|
-
{
|
|
544
|
-
key: 'text',
|
|
545
|
-
label: 'Text input',
|
|
546
|
-
type: 'text',
|
|
547
|
-
helpText: 'Help text'
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
key: 'option',
|
|
551
|
-
label: 'Select',
|
|
552
|
-
type: 'select',
|
|
553
|
-
defaultValue: '12',
|
|
554
|
-
isSearchable: true, //allow to search for option(s), default is `false`
|
|
555
|
-
isMulti: true, //allow to select multiple options, default is `false`
|
|
556
|
-
options: [
|
|
557
|
-
{
|
|
558
|
-
value: '12',
|
|
559
|
-
label: 'Option'
|
|
560
|
-
}
|
|
561
|
-
]
|
|
562
|
-
}
|
|
563
|
-
]
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
//auto reload on settings updates
|
|
567
|
-
configuration.projectIntegration.reloadOnConfigSave = true;
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
## Info window
|
|
571
|
-
|
|
572
|
-
You also can define section with some information notes or help section for your app.
|
|
573
|
-
|
|
574
|
-
```javascript
|
|
575
|
-
configuration.projectIntegration.infoModal = {
|
|
576
|
-
title: 'Info',
|
|
577
|
-
content: `
|
|
578
|
-
<h1>This is your app help section</h1>
|
|
579
|
-
</br>
|
|
580
|
-
<h2>This is just an example</h2>
|
|
581
|
-
`
|
|
582
|
-
}
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
## Background tasks
|
|
586
|
-
|
|
587
|
-
In order to register background tasks that app will invoke periodically invoke you can use `cronJobs` field.
|
|
588
|
-
|
|
589
|
-
```javascript
|
|
590
|
-
configuration.projectIntegration.cronJobs = [
|
|
591
|
-
{
|
|
592
|
-
//every 10 seconds
|
|
593
|
-
expression: '*/10 * * * * *',
|
|
594
|
-
task: (projectId, client, apiCredentials, appRootFolder, config) => {
|
|
595
|
-
console.log(`Running background task for project : ${projectId}`);
|
|
596
|
-
console.log(`Api credentials : ${JSON.stringify(apiCredentials)}`);
|
|
597
|
-
console.log(`App config : ${JSON.stringify(config)}`);
|
|
598
|
-
console.log(appRootFolder ? JSON.stringify(appRootFolder) : 'No root folder');
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
]
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
For cron syntax guide please refer to this [documentation](https://github.com/node-cron/node-cron#cron-syntax).
|
|
605
|
-
|
|
606
|
-
## Errors Handling
|
|
607
|
-
|
|
608
|
-
### Error propagation
|
|
609
|
-
|
|
610
|
-
In case if something is wrong with app settings or credentials are invalid you can throw an explanation message that will be then visible on the UI side.
|
|
611
|
-
e.g. check if entered credentials are valid:
|
|
612
|
-
|
|
613
|
-
```javascript
|
|
614
|
-
configuration.projectIntegration.checkConnection = (credentials) => {
|
|
615
|
-
if (!credentials.password || credentials.password.length < 6) {
|
|
616
|
-
throw 'Password is too weak';
|
|
617
|
-
}
|
|
618
|
-
//or call an service API with those credentials and check if request will be successful
|
|
619
|
-
};
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
Or if you need to manually control users liveness session you can throw an error with `401` code then your app will automatically do a log out action.
|
|
623
|
-
e.g. when your service has some specific session duration timeout or extra conditions which are not covered by this framework
|
|
624
|
-
|
|
625
|
-
```javascript
|
|
626
|
-
configuration.projectIntegration.getIntegrationFiles = async (credentials, appSettings) => {
|
|
627
|
-
//do a request/custom logic here
|
|
628
|
-
const sessionStillValid = false;
|
|
629
|
-
if (!sessionStillValid) {
|
|
630
|
-
throw {
|
|
631
|
-
message: 'session expired',
|
|
632
|
-
code: 401
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
//business logic
|
|
636
|
-
}
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
### Error interceptor
|
|
640
|
-
|
|
641
|
-
You can also provide interceptor to catch errors and process them (e.g. to log them in the centralized place).
|
|
642
|
-
|
|
643
|
-
```javascript
|
|
644
|
-
const Sentry = require('@sentry/node');
|
|
645
|
-
|
|
646
|
-
Sentry.init({
|
|
647
|
-
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
|
|
648
|
-
tracesSampleRate: 1.0,
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
configuration.onError = (error) => {
|
|
652
|
-
Sentry.captureException(e);
|
|
653
|
-
};
|
|
654
|
-
```
|
|
655
|
-
|
|
656
|
-
### Debug mode
|
|
657
|
-
|
|
658
|
-
Also you can turn on the debug mode and application will log everything (useful of debugging).
|
|
659
|
-
|
|
660
|
-
```javascript
|
|
661
|
-
configuration.logger = {
|
|
662
|
-
enabled: true
|
|
663
|
-
};
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
Or even you can pass you own function to log messages (e.g. send them to external monitoring system).
|
|
667
|
-
|
|
668
|
-
```javascript
|
|
669
|
-
configuration.logger = {
|
|
670
|
-
enabled: true,
|
|
671
|
-
log: (message) => {
|
|
672
|
-
console.log(message);
|
|
673
|
-
}
|
|
674
|
-
};
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
## Modules
|
|
678
|
-
|
|
679
|
-
### Custom File Format
|
|
680
|
-
|
|
681
|
-
Example of [custom file format module](https://support.crowdin.com/crowdin-apps-modules/#custom-file-format-module).
|
|
682
|
-
|
|
683
|
-
```javascript
|
|
684
|
-
const crowdinModule = require('@crowdin/app-project-module');
|
|
685
|
-
const convert = require('xml-js');
|
|
686
|
-
|
|
687
|
-
const configuration = {
|
|
688
|
-
baseUrl: 'https://123.ngrok.io',
|
|
689
|
-
clientId: 'clientId',
|
|
690
|
-
clientSecret: 'clientSecret',
|
|
691
|
-
name: 'Sample App',
|
|
692
|
-
identifier: 'sample-app',
|
|
693
|
-
description: 'Sample App description',
|
|
694
|
-
dbFolder: __dirname,
|
|
695
|
-
imagePath: __dirname + '/' + 'logo.png',
|
|
696
|
-
customFileFormat: {
|
|
697
|
-
filesFolder: __dirname,
|
|
698
|
-
type: 'type-xyz',
|
|
699
|
-
multilingual: false,
|
|
700
|
-
autoUploadTranslations: true, //useful when single language format
|
|
701
|
-
signaturePatterns: {
|
|
702
|
-
fileName: '^.+\.xml$'
|
|
703
|
-
},
|
|
704
|
-
customSrxSupported: true,
|
|
705
|
-
parseFile: async (file, req, client, context, projectId) => {
|
|
706
|
-
const xml = convert.xml2json(file, { compact: true, spaces: 4 });
|
|
707
|
-
const fileContent = JSON.parse(xml);
|
|
708
|
-
//parse logic
|
|
709
|
-
const strings = [];
|
|
710
|
-
return { strings, error: 'Some error message' };
|
|
711
|
-
},
|
|
712
|
-
buildFile: async (file, req, strings, client, context, projectId) => {
|
|
713
|
-
const xml = convert.xml2json(file, { compact: true, spaces: 4 });
|
|
714
|
-
const fileContent = JSON.parse(xml);
|
|
715
|
-
//build logic
|
|
716
|
-
const contentFile = convert.json2xml(
|
|
717
|
-
fileContent,
|
|
718
|
-
{
|
|
719
|
-
compact: true,
|
|
720
|
-
ignoreComment: false,
|
|
721
|
-
spaces: 4
|
|
722
|
-
}
|
|
723
|
-
);
|
|
724
|
-
return { contentFile }
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
};
|
|
728
|
-
|
|
729
|
-
crowdinModule.createApp(configuration);
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
Also custom file format module can support strings export.
|
|
733
|
-
|
|
734
|
-
```javascript
|
|
735
|
-
const crowdinModule = require('@crowdin/app-project-module');
|
|
736
|
-
const convert = require('xml-js');
|
|
737
|
-
|
|
738
|
-
const configuration = {
|
|
739
|
-
baseUrl: 'https://123.ngrok.io',
|
|
740
|
-
clientId: 'clientId',
|
|
741
|
-
clientSecret: 'clientSecret',
|
|
742
|
-
name: 'Sample App',
|
|
743
|
-
identifier: 'sample-app',
|
|
744
|
-
description: 'Sample App description',
|
|
745
|
-
dbFolder: __dirname,
|
|
746
|
-
imagePath: __dirname + '/' + 'logo.png',
|
|
747
|
-
customFileFormat: {
|
|
748
|
-
filesFolder: __dirname,
|
|
749
|
-
type: 'type-xyz',
|
|
750
|
-
stringsExport: true,
|
|
751
|
-
multilingualExport: true,
|
|
752
|
-
extensions: [
|
|
753
|
-
'.resx'
|
|
754
|
-
],
|
|
755
|
-
exportStrings: async (req, strings, client, context, projectId) => {
|
|
756
|
-
const file = req.file;
|
|
757
|
-
//export logic
|
|
758
|
-
return { contentFile: '' }
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
crowdinModule.createApp(configuration);
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
By default custom file format will use `filesFolder` as a temporary folder to store huge responses. But it can be overridden:
|
|
767
|
-
|
|
768
|
-
```javascript
|
|
769
|
-
configuration.customFileFormat.storeFile = (content) => {
|
|
770
|
-
//logic to store file, e.g. put it to AWS S3
|
|
771
|
-
return '<url-to-download>';
|
|
772
|
-
};
|
|
773
|
-
```
|
|
774
|
-
|
|
775
|
-
### Custom MT
|
|
776
|
-
|
|
777
|
-
Example of [custom mt module](https://support.crowdin.com/crowdin-apps-modules/#custom-mt-machine-translation-module).
|
|
778
|
-
|
|
779
|
-
```javascript
|
|
780
|
-
const crowdinModule = require('@crowdin/app-project-module');
|
|
781
|
-
|
|
782
|
-
const configuration = {
|
|
783
|
-
baseUrl: 'https://123.ngrok.io',
|
|
784
|
-
clientId: 'clientId',
|
|
785
|
-
clientSecret: 'clientSecret',
|
|
786
|
-
name: 'Sample App',
|
|
787
|
-
identifier: 'sample-app',
|
|
788
|
-
description: 'Sample App description',
|
|
789
|
-
dbFolder: __dirname,
|
|
790
|
-
imagePath: __dirname + '/' + 'logo.png',
|
|
791
|
-
customMT: {
|
|
792
|
-
translate: async (client, context, projectId, source, target, strings) => {
|
|
793
|
-
//translate strings
|
|
794
|
-
const translations = ['hello', 'world'];
|
|
795
|
-
if (source === 'fr') {
|
|
796
|
-
throw 'Source language is not supported by the model';
|
|
797
|
-
}
|
|
798
|
-
return translations;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
};
|
|
802
|
-
|
|
803
|
-
crowdinModule.createApp(configuration);
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
### Profile Resources Menu
|
|
807
|
-
|
|
808
|
-
Example of [resources module](https://support.crowdin.com/crowdin-apps-modules/#resources-module).
|
|
809
|
-
|
|
810
|
-
```javascript
|
|
811
|
-
const crowdinModule = require('@crowdin/app-project-module');
|
|
812
|
-
|
|
813
|
-
const configuration = {
|
|
814
|
-
baseUrl: 'https://123.ngrok.io',
|
|
815
|
-
clientId: 'clientId',
|
|
816
|
-
clientSecret: 'clientSecret',
|
|
817
|
-
name: 'Sample App',
|
|
818
|
-
identifier: 'sample-app',
|
|
819
|
-
description: 'Sample App description',
|
|
820
|
-
dbFolder: __dirname,
|
|
821
|
-
imagePath: __dirname + '/' + 'logo.png',
|
|
822
|
-
profileResourcesMenu: {
|
|
823
|
-
fileName: 'setup.html',
|
|
824
|
-
uiPath: __dirname + '/' + 'public',
|
|
825
|
-
environments: 'crowdin-enterprise',
|
|
826
|
-
},
|
|
827
|
-
};
|
|
828
|
-
|
|
829
|
-
crowdinModule.createApp(configuration);
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
The `profileResourcesMenu` module can work as an extension to other modules be something like a configuration UI for them.
|
|
833
|
-
|
|
834
|
-
```javascript
|
|
835
|
-
const crowdinModule = require('@crowdin/app-project-module');
|
|
836
|
-
const express = require('express');
|
|
837
|
-
|
|
838
|
-
const app = express();
|
|
839
|
-
|
|
840
|
-
const configuration = {
|
|
841
|
-
baseUrl: 'https://123.ngrok.io',
|
|
842
|
-
clientId: 'clientId',
|
|
843
|
-
clientSecret: 'clientSecret',
|
|
844
|
-
name: 'Sample App',
|
|
845
|
-
identifier: 'sample-app',
|
|
846
|
-
description: 'Sample App description',
|
|
847
|
-
dbFolder: __dirname,
|
|
848
|
-
imagePath: __dirname + '/' + 'logo.png',
|
|
849
|
-
profileResourcesMenu: {
|
|
850
|
-
fileName: 'setup.html',
|
|
851
|
-
uiPath: __dirname + '/' + 'public',
|
|
852
|
-
environments: 'crowdin',
|
|
853
|
-
},
|
|
854
|
-
customMT: {
|
|
855
|
-
translate,
|
|
856
|
-
},
|
|
857
|
-
onUninstall: cleanup
|
|
858
|
-
};
|
|
859
|
-
|
|
860
|
-
const crowdinApp = crowdinModule.addCrowdinEndpoints(app, configuration);
|
|
861
|
-
|
|
862
|
-
async function cleanup(organization, allCredentials) {
|
|
863
|
-
//Cleanup logic
|
|
864
|
-
await crowdinApp.deleteMetadata(organization);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
async function translate(crowdinClient, context, projectId, source, target, strings) {
|
|
868
|
-
const organization = context.jwtPayload.domain || context.jwtPayload.context.organization_id;
|
|
869
|
-
const metadata = await crowdinApp.getMetadata(organization);
|
|
870
|
-
//do translation based on metadata
|
|
871
|
-
const translations = ['hello', 'world'];
|
|
872
|
-
return translations;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
//extra endpoints for resources UI
|
|
876
|
-
app.post('/metadata', async (req, res) => {
|
|
877
|
-
const { context } = await crowdinApp.establishCrowdinConnection(req.query.jwt);
|
|
878
|
-
const organization = context.jwtPayload.domain || context.jwtPayload.context.organization_id;
|
|
879
|
-
const metadata = await crowdinApp.getMetadata(organization);
|
|
880
|
-
await crowdinApp.saveMetadata(organization, req.body);
|
|
881
|
-
res.status(204).end();
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
app.get('/metadata', async (req, res) => {
|
|
885
|
-
const { context } = await crowdinApp.establishCrowdinConnection(req.query.jwt);
|
|
886
|
-
const organization = context.jwtPayload.domain || context.jwtPayload.context.organization_id;
|
|
887
|
-
const metadata = await crowdinApp.getMetadata(organization) || {};
|
|
888
|
-
res.status(200).send(metadata);
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
app.listen(3000, () => console.log('Crowdin app started'));
|
|
892
|
-
```
|
|
893
|
-
|
|
894
|
-
### Other modules
|
|
895
|
-
|
|
896
|
-
Framework also supports following modules:
|
|
897
|
-
|
|
898
|
-
- [organization-menu module](https://support.crowdin.com/enterprise/crowdin-apps-modules/#organization-menu-module)
|
|
899
|
-
- [editor-right-panel module](https://support.crowdin.com/crowdin-apps-modules/#editor-panels-module)
|
|
900
|
-
- [project-menu module](https://support.crowdin.com/crowdin-apps-modules/#project-menu-module)
|
|
901
|
-
- [project-menu-crowdsource module](https://developer.crowdin.com/crowdin-apps-module-project-menu-crowdsource/)
|
|
902
|
-
- [project-tools module](https://support.crowdin.com/crowdin-apps-modules/#tools-module)
|
|
903
|
-
- [project-reports module](https://support.crowdin.com/crowdin-apps-modules/#reports-module)
|
|
904
|
-
|
|
905
|
-
Example of Project Reports Module:
|
|
906
|
-
|
|
907
|
-
```javascript
|
|
908
|
-
const crowdinModule = require('@crowdin/app-project-module');
|
|
909
|
-
|
|
910
|
-
const configuration = {
|
|
911
|
-
baseUrl: 'https://123.ngrok.io',
|
|
912
|
-
clientId: 'clientId',
|
|
913
|
-
clientSecret: 'clientSecret',
|
|
914
|
-
name: 'Sample App',
|
|
915
|
-
identifier: 'sample-app',
|
|
916
|
-
description: 'Sample App description',
|
|
917
|
-
dbFolder: __dirname,
|
|
918
|
-
imagePath: __dirname + '/' + 'logo.png',
|
|
919
|
-
projectReports: { //can be editorRightPanel, projectMenu, projectTools, projectMenuCrowdsource
|
|
920
|
-
imagePath: __dirname + '/' + 'reports.png',
|
|
921
|
-
fileName: 'reports.html', //optional, only needed if file is not index.html
|
|
922
|
-
uiPath: __dirname + '/' + 'public', // folder where UI of the module is located (js, html, css files)
|
|
923
|
-
allowUnauthorized: true //make module publicly available without crowdin context
|
|
924
|
-
},
|
|
925
|
-
};
|
|
926
|
-
|
|
927
|
-
crowdinModule.createApp(configuration);
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
## Other options
|
|
931
|
-
|
|
932
|
-
### Options for Crowdin JWT token validation
|
|
933
|
-
|
|
934
|
-
```js
|
|
935
|
-
configuration.jwtValidationOptions = {
|
|
936
|
-
ignoreExpiration: false, //ignore check if jwt is expired or not
|
|
937
|
-
};
|
|
938
|
-
```
|
|
939
|
-
|
|
940
|
-
### App authentication type
|
|
941
|
-
|
|
942
|
-
```js
|
|
943
|
-
configuration.authenticationType = 'authorization_code'; //default is "crowdin_app"
|
|
944
|
-
```
|
|
945
|
-
|
|
946
|
-
### Disable global error handling
|
|
947
|
-
|
|
948
|
-
This module will handle all `unhandledRejection` and `uncaughtException` errors, log them and not kill the Node process.
|
|
949
|
-
Usually this means that code was not properly designed and contains unsafe places. And not always this built in behaviour will be suitable.
|
|
950
|
-
Therefore you can disable it:
|
|
951
|
-
|
|
952
|
-
```js
|
|
953
|
-
configuration.disableGlobalErrorHandling = true;
|
|
954
|
-
```
|
|
955
|
-
|
|
956
|
-
### Use React JSON Schema forms as module front-end
|
|
957
|
-
|
|
958
|
-
For all modules that have UI (except of `projectIntegration` module) you can use React JSON Schema forms ([docs](/https://github.com/rjsf-team/react-jsonschema-form)) as front-end.
|
|
959
|
-
To achieve this you can specify property `formSchema` in module definition.
|
|
960
|
-
```js
|
|
961
|
-
configuration.projectMenu = {
|
|
962
|
-
formSchema: {
|
|
963
|
-
"title": "A registration form",
|
|
964
|
-
"description": "A simple form example.",
|
|
965
|
-
"type": "object",
|
|
966
|
-
"required": [
|
|
967
|
-
"firstName",
|
|
968
|
-
"lastName"
|
|
969
|
-
],
|
|
970
|
-
"properties": {
|
|
971
|
-
"firstName": {
|
|
972
|
-
"type": "string",
|
|
973
|
-
"title": "First name",
|
|
974
|
-
"default": "Chuck"
|
|
975
|
-
},
|
|
976
|
-
"lastName": {
|
|
977
|
-
"type": "string",
|
|
978
|
-
"title": "Last name"
|
|
979
|
-
},
|
|
980
|
-
}
|
|
981
|
-
},
|
|
982
|
-
formUiSchema: {
|
|
983
|
-
"ui:submitButtonOptions": {
|
|
984
|
-
"submitText": "Confirm Details",
|
|
985
|
-
},
|
|
986
|
-
"lastName": {
|
|
987
|
-
"ui:help": "Hint: Choose cool lastname!"
|
|
988
|
-
}
|
|
989
|
-
},
|
|
990
|
-
};
|
|
991
|
-
```
|
|
992
|
-
By default, form data will be stored in app metadata. To retrieve data you can use this code:
|
|
993
|
-
```js
|
|
994
|
-
crowdinApp.getMetadata(`${organizationId}-${projectId}`);
|
|
995
|
-
```
|
|
996
|
-
To customize this behavior you can provide `formDataUrl` property that will contain URL to custom action in your app. This action should support both GET request for fetching data and POST request to save new data.
|
|
997
|
-
Response structure for GET request example:
|
|
998
|
-
```js
|
|
999
|
-
res.status(200).send({
|
|
1000
|
-
formData: {
|
|
1001
|
-
firstName: 'Norris',
|
|
1002
|
-
},
|
|
1003
|
-
}).end();
|
|
1004
|
-
```
|
|
1005
|
-
Also, you can provide next properties:
|
|
1006
|
-
- formScheme - new schema to replace active schema;
|
|
1007
|
-
- formUiScheme - new uiSchema to replace current;
|
|
1008
|
-
- message - custom message that will be displayed in toasts;
|
|
1009
|
-
- redirect - url for redirect (useful for files download).
|
|
1010
7
|
## Contributing
|
|
1011
8
|
|
|
1012
9
|
If you want to contribute please read the [Contributing](/CONTRIBUTING.md) guidelines.
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
package/out/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
|
@@ -163,7 +167,7 @@ function addCrowdinEndpoints(app, plainConfig) {
|
|
|
163
167
|
: '/api/integration/webhook'}`, (0, integration_webhook_1.default)(config, integrationLogic));
|
|
164
168
|
}
|
|
165
169
|
if ((_c = integrationLogic.webhooks) === null || _c === void 0 ? void 0 : _c.queueUrl) {
|
|
166
|
-
(0, webhooks_1.listenQueueMessage)(config, integrationLogic, integrationLogic.webhooks.queueUrl, config.
|
|
170
|
+
(0, webhooks_1.listenQueueMessage)(config, integrationLogic, integrationLogic.webhooks.queueUrl, config.identifier);
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
}
|
package/out/models/index.d.ts
CHANGED
|
@@ -250,7 +250,7 @@ export interface IntegrationLogic {
|
|
|
250
250
|
*/
|
|
251
251
|
webhooks?: Webhooks;
|
|
252
252
|
}
|
|
253
|
-
export
|
|
253
|
+
export type FormEntity = FormField | FormDelimeter;
|
|
254
254
|
export interface FormDelimeter {
|
|
255
255
|
label: string;
|
|
256
256
|
}
|
|
@@ -403,13 +403,13 @@ export interface ExtendedResult<T> {
|
|
|
403
403
|
message?: string;
|
|
404
404
|
stopPagination?: boolean;
|
|
405
405
|
}
|
|
406
|
-
export
|
|
406
|
+
export type TreeItem = File | Folder;
|
|
407
407
|
/**
|
|
408
408
|
* 0 - folder
|
|
409
409
|
* 1 - file
|
|
410
410
|
* 2 - branch
|
|
411
411
|
*/
|
|
412
|
-
|
|
412
|
+
type IntegrationTreeElementType = '0' | '1' | '2';
|
|
413
413
|
export interface File {
|
|
414
414
|
id: string;
|
|
415
415
|
name: string;
|
|
@@ -680,7 +680,7 @@ export interface Webhooks {
|
|
|
680
680
|
crowdinWebhooks?: (client: Crowdin, projectId: number, available: boolean, config?: any) => Promise<void>;
|
|
681
681
|
integrationWebhooks?: (apiCredentials: any, urlParam: string, available: boolean, config?: any, syncSettings?: any) => Promise<void>;
|
|
682
682
|
crowdinWebhookInterceptor?: (projectId: number, client: Crowdin, appRootFolder?: SourceFilesModel.Directory, config?: any, syncSettings?: any) => Promise<UpdateIntegrationRequest>;
|
|
683
|
-
integrationWebhookInterceptor?: (projectId: number, client: Crowdin, appRootFolder?: SourceFilesModel.Directory, config?: any, syncSettings?: any, webhookRequest?: any) => Promise<IntegrationFile[]>;
|
|
683
|
+
integrationWebhookInterceptor?: (projectId: number, client: Crowdin, apiCredentials: any, appRootFolder?: SourceFilesModel.Directory, config?: any, syncSettings?: any, webhookRequest?: any) => Promise<IntegrationFile[]>;
|
|
684
684
|
queueUrl: string;
|
|
685
685
|
}
|
|
686
686
|
export declare enum SyncCondition {
|
|
@@ -693,13 +693,13 @@ export declare enum SyncType {
|
|
|
693
693
|
SCHEDULE = 1,
|
|
694
694
|
WEBHOOKS = 2
|
|
695
695
|
}
|
|
696
|
-
export
|
|
696
|
+
export type Payload = {
|
|
697
697
|
event: string;
|
|
698
698
|
projectId: string;
|
|
699
699
|
language: string;
|
|
700
700
|
fileId: string;
|
|
701
701
|
};
|
|
702
|
-
export
|
|
702
|
+
export type WebhookUrlParams = {
|
|
703
703
|
projectId: number;
|
|
704
704
|
crowdinId: string;
|
|
705
705
|
clientId: string;
|
package/out/util/cron.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
package/out/util/defaults.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
package/out/util/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
package/out/util/webhooks.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
|
@@ -249,7 +253,7 @@ function updateCrowdinFromWebhookRequest(integration, webhookData, req) {
|
|
|
249
253
|
let filesToSync = [];
|
|
250
254
|
const { projectId, crowdinClient, preparedIntegrationCredentials, rootFolder, appSettings, syncSettings } = webhookData;
|
|
251
255
|
if ((_a = integration.webhooks) === null || _a === void 0 ? void 0 : _a.integrationWebhookInterceptor) {
|
|
252
|
-
filesToSync = yield ((_b = integration.webhooks) === null || _b === void 0 ? void 0 : _b.integrationWebhookInterceptor(projectId, crowdinClient.client, rootFolder, appSettings, syncSettings, req));
|
|
256
|
+
filesToSync = yield ((_b = integration.webhooks) === null || _b === void 0 ? void 0 : _b.integrationWebhookInterceptor(projectId, crowdinClient.client, preparedIntegrationCredentials, rootFolder, appSettings, syncSettings, req));
|
|
253
257
|
}
|
|
254
258
|
return yield integration.updateCrowdin(projectId, crowdinClient.client, preparedIntegrationCredentials, filesToSync, rootFolder, appSettings);
|
|
255
259
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crowdin/app-project-module",
|
|
3
|
-
"version": "0.28.0-
|
|
3
|
+
"version": "0.28.0-4",
|
|
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",
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@crowdin/crowdin-apps-functions": "0.3.1",
|
|
17
|
-
"@types/pg": "^8.6.
|
|
17
|
+
"@types/pg": "^8.6.6",
|
|
18
18
|
"amqplib": "^0.10.3",
|
|
19
19
|
"crypto-js": "^4.0.0",
|
|
20
20
|
"express": "4.17.1",
|
|
21
21
|
"express-handlebars": "^5.3.4",
|
|
22
22
|
"mysql2": "^2.3.3",
|
|
23
|
-
"node-cron": "^3.0.
|
|
24
|
-
"pg": "^8.
|
|
25
|
-
"sqlite3": "^5.
|
|
23
|
+
"node-cron": "^3.0.2",
|
|
24
|
+
"pg": "^8.10.0",
|
|
25
|
+
"sqlite3": "^5.1.6",
|
|
26
26
|
"uuid": "^8.3.2"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@types/express-handlebars": "^5.3.1",
|
|
48
48
|
"@types/jest": "^29.2.5",
|
|
49
49
|
"@types/node": "^12.0.10",
|
|
50
|
-
"@types/node-cron": "^3.0.
|
|
50
|
+
"@types/node-cron": "^3.0.7",
|
|
51
51
|
"@typescript-eslint/eslint-plugin": "^2.3.1",
|
|
52
52
|
"@typescript-eslint/parser": "^2.3.1",
|
|
53
53
|
"eslint": "^6.4.0",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"react": "^18.2.0",
|
|
60
60
|
"react-dom": "^18.2.0",
|
|
61
61
|
"rollup": "^3.18.0",
|
|
62
|
-
"ts-jest": "^29.0.
|
|
63
|
-
"typescript": "^4.
|
|
62
|
+
"ts-jest": "^29.0.5",
|
|
63
|
+
"typescript": "^4.9.5"
|
|
64
64
|
},
|
|
65
65
|
"repository": {
|
|
66
66
|
"type": "git",
|