@tandem-language-exchange/content-store 1.1.2 → 1.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.
- package/README.md +106 -20
- package/dist/{chunk-YOREZCXB.js → chunk-UCBZUEUP.js} +220 -29
- package/dist/chunk-UCBZUEUP.js.map +1 -0
- package/dist/{chunk-JBFJU4JA.js → chunk-VRWRAFDK.js} +59 -13
- package/dist/chunk-VRWRAFDK.js.map +1 -0
- package/dist/chunk-XP3USUQC.js +82 -0
- package/dist/chunk-XP3USUQC.js.map +1 -0
- package/dist/chunk-YZSLCPN6.js +272 -0
- package/dist/chunk-YZSLCPN6.js.map +1 -0
- package/dist/client/cli.js +31 -8
- package/dist/client/cli.js.map +1 -1
- package/dist/client/{fetch-bundles.js → fetch-content-bundles.js} +8 -7
- package/dist/client/fetch-content-bundles.js.map +1 -0
- package/dist/client/fetch-translation-bundles.js +36 -0
- package/dist/client/fetch-translation-bundles.js.map +1 -0
- package/dist/{index-DJXkO17k.d.ts → index-Db97SUTy.d.ts} +35 -15
- package/dist/index.d.ts +1 -1
- package/dist/node.browser.js +13 -6
- package/dist/node.browser.js.map +1 -1
- package/dist/node.d.ts +12 -5
- package/dist/node.js +345 -39
- package/dist/node.js.map +1 -1
- package/package.json +23 -11
- package/dist/chunk-JBFJU4JA.js.map +0 -1
- package/dist/chunk-UWGOF36L.js +0 -85
- package/dist/chunk-UWGOF36L.js.map +0 -1
- package/dist/chunk-YOREZCXB.js.map +0 -1
- package/dist/client/fetch-bundles.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Content Store
|
|
2
2
|
|
|
3
|
-
SDK for fetching CMS
|
|
3
|
+
SDK for fetching **CMS** and **translation** bundles from Amazon S3 and, for CMS data, querying them locally from the filesystem. CMS bundles come from syncs of **Contentful** or **Sanity**; translation bundles come from **Lingohub** projects synced to S3 (see the [Server & CLI README](src/server/README.md) for the upload pipeline).
|
|
4
4
|
|
|
5
|
-
For the
|
|
5
|
+
For the Express API, server CLI, scheduling, and deployment, see the [Server & CLI README](src/server/README.md).
|
|
6
6
|
|
|
7
7
|
### Package entry points
|
|
8
8
|
|
|
9
9
|
- **`@tandem-language-exchange/content-store`** (default) — **types only** at runtime. Safe to import from shared code that Next.js, Vite, or Turbopack may bundle for the browser.
|
|
10
|
-
- **`@tandem-language-exchange/content-store/node`** — `ContentStoreSDK`, `
|
|
10
|
+
- **`@tandem-language-exchange/content-store/node`** — `ContentStoreSDK`, `fetchCmsBundles`, `fetchTranslationBundles`, `queryCmsBundle`, `ContentStore`, `getDefaultS3RetryConfig`, and `trimDepth`. Real implementations use the filesystem and S3 and run only under the Node (`node`) export condition (Route Handlers, `getServerSideProps`, CLI, etc.).
|
|
11
11
|
|
|
12
12
|
For **browser** bundles (including anything imported from `_app.tsx`, client components, or shared modules that reach the client graph), bundlers should resolve the `browser` / `edge-light` conditions to a **stub** that does not import `fs`. That stub throws if you call server-only APIs; **`trimDepth` is fully implemented** and safe on the client.
|
|
13
13
|
|
|
@@ -16,9 +16,11 @@ For the server, CLI, and deployment documentation, see the [Server & CLI README]
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
npm install content-store
|
|
19
|
+
npm install @tandem-language-exchange/content-store
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
Use the same scoped name in `package.json` dependencies and in `npx` / import paths.
|
|
23
|
+
|
|
22
24
|
## Initialisation
|
|
23
25
|
|
|
24
26
|
```typescript
|
|
@@ -70,12 +72,12 @@ const sdk = new ContentStoreSDK({
|
|
|
70
72
|
|
|
71
73
|
---
|
|
72
74
|
|
|
73
|
-
## `
|
|
75
|
+
## `fetchCmsBundles(options)`
|
|
74
76
|
|
|
75
77
|
Downloads the latest content bundles from S3 and saves them as JSON files to `outputDir`.
|
|
76
78
|
|
|
77
79
|
```typescript
|
|
78
|
-
const files = await sdk.
|
|
80
|
+
const files = await sdk.fetchCmsBundles({
|
|
79
81
|
cms: 'contentful',
|
|
80
82
|
contentTypes: ['gridLayout', 'iconWithText', 'page'],
|
|
81
83
|
});
|
|
@@ -87,6 +89,7 @@ const files = await sdk.fetchBundles({
|
|
|
87
89
|
| --- | --- | --- |
|
|
88
90
|
| `cms` | `'contentful' \| 'sanity'` | Which CMS the bundles were synced from |
|
|
89
91
|
| `contentTypes` | `string[]` | Content types to download |
|
|
92
|
+
| `retry` | `S3RetryConfig` | Optional. Overrides [S3 download retries](#s3-download-retries). |
|
|
90
93
|
|
|
91
94
|
**Returns:** `Record<string, string>` — a map of content type to absolute file path.
|
|
92
95
|
|
|
@@ -100,12 +103,66 @@ const files = await sdk.fetchBundles({
|
|
|
100
103
|
|
|
101
104
|
Files are written to `outputDir` with the naming pattern `{cms}-{contentType}.json`.
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## `fetchTranslationBundles(options)`
|
|
109
|
+
|
|
110
|
+
Downloads translation bundles that were previously uploaded to S3 (after a Lingohub sync). Files are written under `outputDir` using the same **S3 object key** as the path (see below).
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const files = await sdk.fetchTranslationBundles({
|
|
114
|
+
projects: ['tandem', 'tandem-(website)'],
|
|
115
|
+
locales: ['en', 'de'], // omit or leave empty to use the package default locale list
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Parameters:**
|
|
120
|
+
|
|
121
|
+
| Field | Type | Description |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `projects` | `string[]` | Lingohub project ids to fetch (must match keys configured in the package, e.g. `tandem`, `tandem-(android)`). |
|
|
124
|
+
| `locales` | `string[]` | Optional. Locale codes to fetch (e.g. `pt-br`, `zh-hans`). If omitted or empty, a built-in default list is used. |
|
|
125
|
+
| `retry` | `S3RetryConfig` | Optional. Overrides [S3 download retries](#s3-download-retries). |
|
|
126
|
+
|
|
127
|
+
**Returns:** `TranslationBundleInfo` — nested map **`project → S3 object key → absolute file path`** on disk.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
{
|
|
131
|
+
'tandem-(website)': {
|
|
132
|
+
'lingohub-tandem-(website).en.json': '/abs/path/to/content-cache/lingohub-tandem-(website).en.json',
|
|
133
|
+
'lingohub-tandem-(website).AI.en.json': '/abs/path/to/...'
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**S3 object keys** follow `lingohub-{project}.{fileName}` where `{fileName}` is the Lingohub resource template with `[locale]` replaced by the **mapped** locale when a resource defines `localeMapping` (same rules as the server sync). Consumers should use the returned keys or list objects by project as shown above.
|
|
139
|
+
|
|
140
|
+
Downloads are run **in parallel** (per project); [S3 download retries](#s3-download-retries) apply by default.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## S3 download retries
|
|
145
|
+
|
|
146
|
+
`fetchCmsBundles` and `fetchTranslationBundles` use retry + exponential backoff on **transient** S3/network failures (for example HTTP 503 “Slow Down”, throttling, timeouts). They do **not** retry clear client errors such as **404** (missing key).
|
|
147
|
+
|
|
148
|
+
Default limits are read from the environment (highest precedence first):
|
|
149
|
+
|
|
150
|
+
| Variable | Fallback | Purpose |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| `S3_RETRY_MAX_RETRIES` | `RETRY_MAX_RETRIES` (default `5`) | Maximum retry attempts after the first try |
|
|
153
|
+
| `S3_RETRY_BASE_DELAY_MS` | `RETRY_BASE_DELAY_MS` (default `1000`) | Base delay for exponential backoff |
|
|
154
|
+
| `S3_RETRY_MAX_DELAY_MS` | `RETRY_MAX_DELAY_MS` (default `60000`) | Cap on backoff delay |
|
|
155
|
+
|
|
156
|
+
Override per call with **`retry: { maxRetries, baseDelayMs, maxDelayMs }`**, or import **`getDefaultS3RetryConfig()`** to merge with your own defaults.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## `queryCmsBundle(cms, contentType, options?)`
|
|
104
161
|
|
|
105
162
|
Reads a previously fetched bundle from the local filesystem and returns a filtered, shaped result set.
|
|
106
163
|
|
|
107
164
|
```typescript
|
|
108
|
-
const results = await sdk.
|
|
165
|
+
const results = await sdk.queryCmsBundle('contentful', 'gridLayout', {
|
|
109
166
|
fields: { columns: '2' },
|
|
110
167
|
select: ['title', 'bodyBefore'],
|
|
111
168
|
limit: 10,
|
|
@@ -231,10 +288,16 @@ Query options are applied in this order:
|
|
|
231
288
|
|
|
232
289
|
## Standalone functions
|
|
233
290
|
|
|
234
|
-
The
|
|
291
|
+
The same operations are available as standalone imports (no `ContentStoreSDK` wrapper):
|
|
235
292
|
|
|
236
293
|
```typescript
|
|
237
|
-
import {
|
|
294
|
+
import {
|
|
295
|
+
fetchCmsBundles,
|
|
296
|
+
fetchTranslationBundles,
|
|
297
|
+
queryCmsBundle,
|
|
298
|
+
getDefaultS3RetryConfig,
|
|
299
|
+
ContentStore,
|
|
300
|
+
} from '@tandem-language-exchange/content-store/node';
|
|
238
301
|
|
|
239
302
|
const store = new ContentStore({
|
|
240
303
|
bucket: 'beta-content-store',
|
|
@@ -243,12 +306,18 @@ const store = new ContentStore({
|
|
|
243
306
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
244
307
|
});
|
|
245
308
|
|
|
246
|
-
await
|
|
309
|
+
await fetchCmsBundles(store, './content-cache', {
|
|
247
310
|
cms: 'contentful',
|
|
248
311
|
contentTypes: ['gridLayout'],
|
|
249
312
|
});
|
|
250
313
|
|
|
251
|
-
|
|
314
|
+
await fetchTranslationBundles(store, './content-cache', {
|
|
315
|
+
projects: ['tandem-(website)'],
|
|
316
|
+
locales: ['en', 'fr'],
|
|
317
|
+
retry: getDefaultS3RetryConfig(),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const results = await queryCmsBundle('./content-cache', 'contentful', 'gridLayout', {
|
|
252
321
|
fields: { columns: '2' },
|
|
253
322
|
limit: 5,
|
|
254
323
|
});
|
|
@@ -269,14 +338,19 @@ const sdk = new ContentStoreSDK({
|
|
|
269
338
|
outputDir: './.content-cache',
|
|
270
339
|
});
|
|
271
340
|
|
|
272
|
-
// 1. Pull latest bundles from S3 to disk
|
|
273
|
-
await sdk.
|
|
341
|
+
// 1. Pull latest CMS bundles from S3 to disk
|
|
342
|
+
await sdk.fetchCmsBundles({
|
|
274
343
|
cms: 'contentful',
|
|
275
344
|
contentTypes: ['page', 'gridLayout'],
|
|
276
345
|
});
|
|
277
346
|
|
|
278
|
-
//
|
|
279
|
-
|
|
347
|
+
// Optional: pull translation bundles written by the Lingohub → S3 sync
|
|
348
|
+
await sdk.fetchTranslationBundles({
|
|
349
|
+
projects: ['tandem-(website)'],
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// 2. Query CMS bundle locally — no further network calls
|
|
353
|
+
const grids = await sdk.queryCmsBundle('contentful', 'gridLayout', {
|
|
280
354
|
fields: { columns: '2' },
|
|
281
355
|
select: ['title', 'refs'],
|
|
282
356
|
include: 2,
|
|
@@ -287,7 +361,7 @@ console.log(grids);
|
|
|
287
361
|
```
|
|
288
362
|
## CLI
|
|
289
363
|
|
|
290
|
-
The package
|
|
364
|
+
The published package exposes one **global binary** — **`fetch-content-bundles`** (CMS downloads). Other commands live in **`dist/client/cli.js`** as subcommands; call them with **`node node_modules/@tandem-language-exchange/content-store/dist/client/cli.js <command>`** after install, or wrap them in your app’s **`package.json` `scripts`** (see examples below).
|
|
291
365
|
|
|
292
366
|
### `fetch-content-bundles` — download bundles from S3
|
|
293
367
|
|
|
@@ -313,12 +387,14 @@ Files are written as `{cms}-{contentType}.json` inside the output directory.
|
|
|
313
387
|
}
|
|
314
388
|
```
|
|
315
389
|
|
|
316
|
-
|
|
390
|
+
The published **`bin`** only registers **`fetch-content-bundles`** (CMS). To **download translation bundles** from a script without embedding credentials in the shell, use a small Node script that calls **`fetchTranslationBundles`** (or `ContentStoreSDK`) with the same env vars as in [S3 config via environment variables](#s3-config-via-environment-variables), or call the server’s **`POST /getTranslationBundles`** API if you run the content-store service (see [Server & CLI README](src/server/README.md)).
|
|
317
391
|
|
|
318
|
-
|
|
392
|
+
### `query-cms` — query a local bundle
|
|
393
|
+
|
|
394
|
+
Reads a previously fetched bundle from disk and prints JSON to stdout. This command is **not** a separate `bin`; run the built client CLI (after `npm install` of this package):
|
|
319
395
|
|
|
320
396
|
```bash
|
|
321
|
-
|
|
397
|
+
node node_modules/@tandem-language-exchange/content-store/dist/client/cli.js query-cms \
|
|
322
398
|
--cms contentful --type gridLayout \
|
|
323
399
|
--fields '{"columns":"2"}' \
|
|
324
400
|
--select title,bodyBefore \
|
|
@@ -326,6 +402,16 @@ npx @tandem-language-exchange/content-store query \
|
|
|
326
402
|
--include 2
|
|
327
403
|
```
|
|
328
404
|
|
|
405
|
+
**Typical `package.json` shortcut:**
|
|
406
|
+
|
|
407
|
+
```json
|
|
408
|
+
"scripts": {
|
|
409
|
+
"query:cms": "node ./node_modules/@tandem-language-exchange/content-store/dist/client/cli.js query-cms"
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Then: `npm run query:cms -- --cms contentful --type gridLayout …`
|
|
414
|
+
|
|
329
415
|
| Flag | Required | Default | Description |
|
|
330
416
|
| --- | --- | --- | --- |
|
|
331
417
|
| `--cms <provider>` | Yes | | `contentful` or `sanity` |
|
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ContentStore,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
allProjects,
|
|
5
|
+
buildCmsObjectKey,
|
|
6
|
+
buildTranslationObjectKey,
|
|
7
|
+
config,
|
|
8
|
+
defaultLocales
|
|
9
|
+
} from "./chunk-YZSLCPN6.js";
|
|
6
10
|
|
|
7
11
|
// src/server/config.ts
|
|
8
12
|
import dotenv from "dotenv";
|
|
9
13
|
dotenv.config({ path: ".env.local" });
|
|
10
14
|
dotenv.config();
|
|
15
|
+
function parseScheduledCmsJobConfig() {
|
|
16
|
+
const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? "0", 60);
|
|
17
|
+
const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;
|
|
18
|
+
const runOnStart = (process.env.SCHEDULE_RUN_ON_START ?? "true").toLowerCase() !== "false";
|
|
19
|
+
const task = (process.env.SCHEDULE_TASK ?? "sync").trim();
|
|
20
|
+
const syncCms = (process.env.SCHEDULE_SYNC_CMS ?? "").trim();
|
|
21
|
+
const rawTypes = process.env.SCHEDULE_SYNC_TYPES?.trim();
|
|
22
|
+
const syncTypes = rawTypes ? rawTypes.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
23
|
+
return {
|
|
24
|
+
enabled,
|
|
25
|
+
intervalMinutes,
|
|
26
|
+
runOnStart,
|
|
27
|
+
task,
|
|
28
|
+
syncCms: syncCms || void 0,
|
|
29
|
+
syncTypes
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function parseScheduledTranslationJobConfig() {
|
|
33
|
+
const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? "0", 60);
|
|
34
|
+
const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;
|
|
35
|
+
const runOnStart = (process.env.SCHEDULE_RUN_ON_START ?? "true").toLowerCase() !== "false";
|
|
36
|
+
const task = (process.env.SCHEDULE_TASK ?? "sync").trim();
|
|
37
|
+
const rawProjects = process.env.SCHEDULE_SYNC_TRANSLATION_PROJECTS?.trim();
|
|
38
|
+
const syncProjects = rawProjects ? rawProjects.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
39
|
+
return {
|
|
40
|
+
enabled,
|
|
41
|
+
intervalMinutes,
|
|
42
|
+
runOnStart,
|
|
43
|
+
task,
|
|
44
|
+
syncProjects
|
|
45
|
+
};
|
|
46
|
+
}
|
|
11
47
|
var config2 = {
|
|
12
48
|
...config,
|
|
13
49
|
contentful: {
|
|
@@ -17,11 +53,14 @@ var config2 = {
|
|
|
17
53
|
batchSize: 1e3,
|
|
18
54
|
maxDepth: 4,
|
|
19
55
|
contentTypes: [
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
56
|
+
"asset",
|
|
57
|
+
"page",
|
|
58
|
+
"longtailPage",
|
|
59
|
+
"customJson",
|
|
60
|
+
"banner",
|
|
61
|
+
"cookieBanner",
|
|
62
|
+
"downloadPage"
|
|
23
63
|
// Add Contentful content type IDs here to limit sync scope.
|
|
24
|
-
// Leave empty to sync all content types in the space.
|
|
25
64
|
]
|
|
26
65
|
},
|
|
27
66
|
sanity: {
|
|
@@ -30,6 +69,10 @@ var config2 = {
|
|
|
30
69
|
token: process.env.SANITY_API_TOKEN ?? "",
|
|
31
70
|
apiVersion: "2024-01-01"
|
|
32
71
|
},
|
|
72
|
+
lingohub: {
|
|
73
|
+
authToken: process.env.LINGOHUB_AUTH_TOKEN ?? "",
|
|
74
|
+
workspace: process.env.LINGOHUB_WORKSPACE ?? ""
|
|
75
|
+
},
|
|
33
76
|
retry: {
|
|
34
77
|
maxRetries: parseInt(process.env.RETRY_MAX_RETRIES ?? "5", 10),
|
|
35
78
|
baseDelayMs: parseInt(process.env.RETRY_BASE_DELAY_MS ?? "1000", 10),
|
|
@@ -38,6 +81,18 @@ var config2 = {
|
|
|
38
81
|
api: {
|
|
39
82
|
port: parseInt(process.env.PORT ?? "3010"),
|
|
40
83
|
apiToken: process.env.CONTENT_STORE_API_TOKEN ?? ""
|
|
84
|
+
},
|
|
85
|
+
scheduledCmsJob: parseScheduledCmsJobConfig(),
|
|
86
|
+
scheduledTranslationJob: parseScheduledTranslationJobConfig(),
|
|
87
|
+
cleanup: {
|
|
88
|
+
enabled: (process.env.CLEANUP_ENABLED ?? "true").toLowerCase() !== "false",
|
|
89
|
+
retentionDays: parseInt(process.env.CLEANUP_RETENTION_DAYS ?? "30", 10)
|
|
90
|
+
},
|
|
91
|
+
slack: {
|
|
92
|
+
enabled: (process.env.SLACK_BOT_TOKEN ?? "").length > 0,
|
|
93
|
+
botToken: process.env.SLACK_BOT_TOKEN ?? "",
|
|
94
|
+
signingSecret: process.env.SLACK_SIGNING_SECRET ?? "",
|
|
95
|
+
appToken: process.env.SLACK_APP_TOKEN ?? ""
|
|
41
96
|
}
|
|
42
97
|
};
|
|
43
98
|
|
|
@@ -134,15 +189,15 @@ var ContentfulAdapter = class {
|
|
|
134
189
|
maxDepth;
|
|
135
190
|
allowedTypes;
|
|
136
191
|
retryConfig;
|
|
137
|
-
constructor(
|
|
192
|
+
constructor(cfg2, retryConfig) {
|
|
138
193
|
this.client = createClient({
|
|
139
|
-
space:
|
|
140
|
-
accessToken:
|
|
141
|
-
host:
|
|
194
|
+
space: cfg2.spaceId,
|
|
195
|
+
accessToken: cfg2.accessToken,
|
|
196
|
+
host: cfg2.host
|
|
142
197
|
});
|
|
143
|
-
this.batchSize =
|
|
144
|
-
this.maxDepth =
|
|
145
|
-
this.allowedTypes =
|
|
198
|
+
this.batchSize = cfg2.batchSize;
|
|
199
|
+
this.maxDepth = cfg2.maxDepth;
|
|
200
|
+
this.allowedTypes = cfg2.contentTypes;
|
|
146
201
|
this.retryConfig = retryConfig;
|
|
147
202
|
}
|
|
148
203
|
async getContentTypes() {
|
|
@@ -160,8 +215,13 @@ var ContentfulAdapter = class {
|
|
|
160
215
|
* Fetches every entry for a content type using batched pagination.
|
|
161
216
|
* Contentful caps `getEntries` at 1 000 items per call, so we page through
|
|
162
217
|
* with `skip` until all items are collected.
|
|
218
|
+
*
|
|
219
|
+
* The reserved content type `"asset"` fetches from `getAssets()` instead.
|
|
163
220
|
*/
|
|
164
221
|
async fetchAll(contentType, includeLevels = 4) {
|
|
222
|
+
if (contentType === "asset") {
|
|
223
|
+
return this.fetchAllAssets();
|
|
224
|
+
}
|
|
165
225
|
const allItems = [];
|
|
166
226
|
let skip = 0;
|
|
167
227
|
let total = 0;
|
|
@@ -188,6 +248,30 @@ var ContentfulAdapter = class {
|
|
|
188
248
|
total
|
|
189
249
|
};
|
|
190
250
|
}
|
|
251
|
+
async fetchAllAssets() {
|
|
252
|
+
const allItems = [];
|
|
253
|
+
let skip = 0;
|
|
254
|
+
let total = 0;
|
|
255
|
+
do {
|
|
256
|
+
const response = await withRetry(
|
|
257
|
+
() => this.client.getAssets({ limit: this.batchSize, skip }),
|
|
258
|
+
this.retryConfig
|
|
259
|
+
);
|
|
260
|
+
total = response.total;
|
|
261
|
+
allItems.push(...response.items);
|
|
262
|
+
skip += response.items.length;
|
|
263
|
+
if (total > this.batchSize) {
|
|
264
|
+
console.log(
|
|
265
|
+
` [contentful] asset: fetched ${allItems.length}/${total}`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
} while (skip < total);
|
|
269
|
+
return {
|
|
270
|
+
contentType: "asset",
|
|
271
|
+
items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),
|
|
272
|
+
total
|
|
273
|
+
};
|
|
274
|
+
}
|
|
191
275
|
};
|
|
192
276
|
|
|
193
277
|
// src/server/adapters/sanity.ts
|
|
@@ -196,19 +280,19 @@ var SanityAdapter = class {
|
|
|
196
280
|
name = "sanity";
|
|
197
281
|
client;
|
|
198
282
|
retryConfig;
|
|
199
|
-
constructor(
|
|
283
|
+
constructor(cfg2, retryConfig) {
|
|
200
284
|
this.client = createClient2({
|
|
201
|
-
projectId:
|
|
202
|
-
dataset:
|
|
203
|
-
token:
|
|
204
|
-
apiVersion:
|
|
285
|
+
projectId: cfg2.projectId,
|
|
286
|
+
dataset: cfg2.dataset,
|
|
287
|
+
token: cfg2.token,
|
|
288
|
+
apiVersion: cfg2.apiVersion,
|
|
205
289
|
useCdn: false
|
|
206
290
|
});
|
|
207
291
|
this.retryConfig = retryConfig;
|
|
208
292
|
}
|
|
209
293
|
async getContentTypes() {
|
|
210
294
|
const types = await withRetry(
|
|
211
|
-
() => this.client.fetch(
|
|
295
|
+
() => this.client.fetch("array::unique(*[]._type)"),
|
|
212
296
|
this.retryConfig
|
|
213
297
|
);
|
|
214
298
|
return types.filter(
|
|
@@ -217,7 +301,7 @@ var SanityAdapter = class {
|
|
|
217
301
|
}
|
|
218
302
|
async fetchAll(contentType) {
|
|
219
303
|
const items = await withRetry(
|
|
220
|
-
() => this.client.fetch(
|
|
304
|
+
() => this.client.fetch("*[_type == $type]", { type: contentType }),
|
|
221
305
|
this.retryConfig
|
|
222
306
|
);
|
|
223
307
|
console.log(` [sanity] ${contentType}: fetched ${items.length} items`);
|
|
@@ -237,8 +321,72 @@ function createAdapter(cms) {
|
|
|
237
321
|
}
|
|
238
322
|
}
|
|
239
323
|
|
|
324
|
+
// src/shared/utils.ts
|
|
325
|
+
import convert from "xml-js";
|
|
326
|
+
import set from "lodash.set";
|
|
327
|
+
import merge from "lodash.merge";
|
|
328
|
+
var convertXMLToJS = (xml) => {
|
|
329
|
+
const converted = convert.xml2js(xml, {
|
|
330
|
+
ignoreComment: true,
|
|
331
|
+
ignoreDeclaration: true,
|
|
332
|
+
ignoreInstruction: true,
|
|
333
|
+
compact: true
|
|
334
|
+
});
|
|
335
|
+
let mapped = {};
|
|
336
|
+
converted.resources.string.forEach((item) => {
|
|
337
|
+
mapped = {
|
|
338
|
+
...mapped,
|
|
339
|
+
[item._attributes.name]: item._text
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
return mapped;
|
|
343
|
+
};
|
|
344
|
+
var parseIOSStrings = (strings) => {
|
|
345
|
+
const parsedObj = {};
|
|
346
|
+
strings.split("\n").filter((line) => line.startsWith('"') && line.endsWith(";")).map((line) => line.trim().slice(0, -1)).forEach((line) => {
|
|
347
|
+
let [key, value] = line.split(" = ");
|
|
348
|
+
if (!key || !value) return;
|
|
349
|
+
key = key.slice(1, -1);
|
|
350
|
+
value = value.slice(1, -1);
|
|
351
|
+
parsedObj[key] = value;
|
|
352
|
+
});
|
|
353
|
+
return parsedObj;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/server/adapters/lingohub.ts
|
|
357
|
+
var cfg = config2.lingohub;
|
|
358
|
+
var apiUrl = "https://api.lingohub.com/v1/" + cfg.workspace + "/projects/";
|
|
359
|
+
var getResourceForLocale = async (project, resource, locale) => {
|
|
360
|
+
const urlForResourceLocalised = `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace("[locale]", locale);
|
|
361
|
+
const result = await fetch(
|
|
362
|
+
urlForResourceLocalised,
|
|
363
|
+
{
|
|
364
|
+
method: "GET"
|
|
365
|
+
}
|
|
366
|
+
).then(async (res) => {
|
|
367
|
+
if (!res.ok) {
|
|
368
|
+
throw new Error(`Failed to fetch resource: ${res.status} - ${res.statusText}`);
|
|
369
|
+
}
|
|
370
|
+
if (resource.type === "json") {
|
|
371
|
+
return res.json();
|
|
372
|
+
}
|
|
373
|
+
if (resource.type === "xml") {
|
|
374
|
+
const xml = await res.text();
|
|
375
|
+
return convertXMLToJS(xml);
|
|
376
|
+
}
|
|
377
|
+
if (resource.type === "strings") {
|
|
378
|
+
const strings = await res.text();
|
|
379
|
+
return parseIOSStrings(strings);
|
|
380
|
+
}
|
|
381
|
+
throw new Error("Invalid resource type");
|
|
382
|
+
}).catch((err) => {
|
|
383
|
+
throw err;
|
|
384
|
+
});
|
|
385
|
+
return result;
|
|
386
|
+
};
|
|
387
|
+
|
|
240
388
|
// src/server/sync/engine.ts
|
|
241
|
-
async function
|
|
389
|
+
async function syncCmsContent(cms, contentTypes, includeLevels) {
|
|
242
390
|
const adapter = createAdapter(cms);
|
|
243
391
|
const store = new ContentStore(config2.s3);
|
|
244
392
|
const timestamp = Math.floor(Date.now() / 1e3);
|
|
@@ -252,17 +400,15 @@ Starting sync from ${cms} at ${new Date(timestamp * 1e3).toISOString()}`);
|
|
|
252
400
|
for (const contentType of typesToSync) {
|
|
253
401
|
try {
|
|
254
402
|
const result = await adapter.fetchAll(contentType, includeLevels);
|
|
255
|
-
const
|
|
256
|
-
await store.upload(
|
|
257
|
-
const latestKey = await store.copyToLatest(versionedKey, cms, contentType);
|
|
403
|
+
const objectKey = buildCmsObjectKey(cms, contentType);
|
|
404
|
+
await store.upload(objectKey, result.items);
|
|
258
405
|
entries.push({
|
|
259
406
|
contentType,
|
|
260
407
|
itemCount: result.total,
|
|
261
|
-
|
|
262
|
-
latestKey
|
|
408
|
+
objectKey
|
|
263
409
|
});
|
|
264
410
|
console.log(
|
|
265
|
-
` + ${contentType}: ${result.total} items -> ${
|
|
411
|
+
` + ${contentType}: ${result.total} items -> ${objectKey}`
|
|
266
412
|
);
|
|
267
413
|
} catch (err) {
|
|
268
414
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -277,9 +423,54 @@ Sync complete: ${entries.length} succeeded, ${errors.length} failed
|
|
|
277
423
|
);
|
|
278
424
|
return { cms, timestamp, entries, errors };
|
|
279
425
|
}
|
|
426
|
+
async function syncTranslations(projects, locales) {
|
|
427
|
+
const store = new ContentStore(config2.s3);
|
|
428
|
+
const entries = [];
|
|
429
|
+
const errors = [];
|
|
430
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
431
|
+
if (!locales) {
|
|
432
|
+
locales = defaultLocales;
|
|
433
|
+
}
|
|
434
|
+
if (!projects) {
|
|
435
|
+
projects = Object.keys(allProjects);
|
|
436
|
+
}
|
|
437
|
+
for (const project of projects) {
|
|
438
|
+
const resources = allProjects[project];
|
|
439
|
+
if (!resources) {
|
|
440
|
+
console.error(`No resources found for ${project}`);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
for (const resource of resources) {
|
|
444
|
+
for (const loc of locales) {
|
|
445
|
+
const locale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
|
|
446
|
+
try {
|
|
447
|
+
const result = await getResourceForLocale(project, resource, locale);
|
|
448
|
+
const objectKey = buildTranslationObjectKey(project, resource.fileName, locale);
|
|
449
|
+
await store.upload(objectKey, result);
|
|
450
|
+
const itemCount = Object.keys(result).length;
|
|
451
|
+
entries.push({
|
|
452
|
+
project,
|
|
453
|
+
locale,
|
|
454
|
+
itemCount,
|
|
455
|
+
objectKey
|
|
456
|
+
});
|
|
457
|
+
console.log(
|
|
458
|
+
` + ${project} - ${locale}: ${itemCount} items -> ${objectKey}`
|
|
459
|
+
);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
462
|
+
errors.push({ project, locale, error: message });
|
|
463
|
+
console.error(` x ${project} - ${locale}: ${message}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return { timestamp, entries, errors };
|
|
469
|
+
}
|
|
280
470
|
|
|
281
471
|
export {
|
|
282
472
|
config2 as config,
|
|
283
|
-
|
|
473
|
+
syncCmsContent,
|
|
474
|
+
syncTranslations
|
|
284
475
|
};
|
|
285
|
-
//# sourceMappingURL=chunk-
|
|
476
|
+
//# sourceMappingURL=chunk-UCBZUEUP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/config.ts","../src/server/adapters/contentful.ts","../src/server/sync/retry.ts","../src/server/adapters/sanity.ts","../src/server/adapters/index.ts","../src/shared/utils.ts","../src/server/adapters/lingohub.ts","../src/server/sync/engine.ts"],"sourcesContent":["import dotenv from 'dotenv';\nimport type { S3Config, CMSProvider } from '../shared/types';\nimport {SharedConfig, config as sharedConfig} from '../shared/config';\n\ndotenv.config({ path: '.env.local' });\ndotenv.config();\n\nexport type { CMSProvider, S3Config };\n\nexport interface ContentfulConfig {\n spaceId: string;\n accessToken: string;\n host: string;\n batchSize: number;\n /** Content types to sync. When empty, all content types in the space are synced. */\n contentTypes: string[];\n /** Max nesting depth when unwrapping resolved entries. Deeper references are dropped. */\n maxDepth: number;\n}\n\nexport interface SanityConfig {\n projectId: string;\n dataset: string;\n token: string;\n apiVersion: string;\n}\n\nexport interface LingohubConfig {\n authToken: string;\n workspace: string;\n}\n\nexport interface RetryConfig {\n maxRetries: number;\n baseDelayMs: number;\n maxDelayMs: number;\n}\n\nexport interface RestApiConfig {\n port: number;\n apiToken: string;\n}\n\n/** Background schedule (same behaviour as `content-store sync` in the CLI). */\nexport interface ScheduledCmsJobConfig {\n enabled: boolean;\n intervalMinutes: number;\n /** When true, run once when the server starts, then on every interval. */\n runOnStart: boolean;\n /** Job kind; only `sync` is implemented. */\n task: string;\n syncCms?: CMSProvider;\n syncTypes?: string[];\n}\n\nexport interface ScheduledTranslationJobConfig {\n enabled: boolean;\n intervalMinutes: number;\n /** When true, run once when the server starts, then on every interval. */\n runOnStart: boolean;\n /** Job kind; only `sync` is implemented. */\n task: string;\n syncProjects?: string[];\n}\n\nfunction parseScheduledCmsJobConfig(): ScheduledCmsJobConfig {\n const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? '0', 60);\n const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;\n const runOnStart =\n (process.env.SCHEDULE_RUN_ON_START ?? 'true').toLowerCase() !== 'false';\n const task = (process.env.SCHEDULE_TASK ?? 'sync').trim();\n const syncCms = (process.env.SCHEDULE_SYNC_CMS ?? '').trim() as CMSProvider;\n const rawTypes = process.env.SCHEDULE_SYNC_TYPES?.trim();\n const syncTypes = rawTypes\n ? rawTypes.split(',').map((s) => s.trim()).filter(Boolean)\n : undefined;\n\n return {\n enabled,\n intervalMinutes,\n runOnStart,\n task,\n syncCms: syncCms || undefined,\n syncTypes,\n };\n}\n\nfunction parseScheduledTranslationJobConfig(): ScheduledTranslationJobConfig {\n const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? '0', 60);\n const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;\n const runOnStart =\n (process.env.SCHEDULE_RUN_ON_START ?? 'true').toLowerCase() !== 'false';\n const task = (process.env.SCHEDULE_TASK ?? 'sync').trim();\n const rawProjects = process.env.SCHEDULE_SYNC_TRANSLATION_PROJECTS?.trim();\n const syncProjects = rawProjects\n ? rawProjects.split(',').map((s) => s.trim()).filter(Boolean)\n : undefined;\n\n return {\n enabled,\n intervalMinutes,\n runOnStart,\n task,\n syncProjects,\n };\n}\n\nexport interface CleanupConfig {\n enabled: boolean;\n /** Delete versioned S3 objects older than this many days. */\n retentionDays: number;\n}\n\nexport interface SlackConfig {\n enabled: boolean;\n botToken: string;\n signingSecret: string;\n /** Socket Mode app-level token (xapp-…). Required when enabled. */\n appToken: string;\n}\n\nexport interface ServerConfig {\n contentful: ContentfulConfig;\n sanity: SanityConfig;\n lingohub: LingohubConfig;\n retry: RetryConfig;\n api: RestApiConfig;\n scheduledCmsJob: ScheduledCmsJobConfig;\n scheduledTranslationJob: ScheduledTranslationJobConfig;\n cleanup: CleanupConfig;\n slack: SlackConfig;\n}\n\nexport const config: ServerConfig & SharedConfig = {\n ...sharedConfig,\n contentful: {\n spaceId: process.env.CONTENTFUL_SPACE_ID ?? '',\n accessToken: process.env.CONTENTFUL_WEBSITE_TOKEN ?? '',\n host: process.env.CONTENTFUL_HOST ?? 'preview.contentful.com',\n batchSize: 1000,\n maxDepth: 4,\n contentTypes: [\n 'asset','page','longtailPage','customJson','banner','cookieBanner','downloadPage'\n // Add Contentful content type IDs here to limit sync scope.\n ],\n },\n\n sanity: {\n projectId: process.env.SANITY_PROJECT_ID ?? '',\n dataset: process.env.SANITY_DATASET ?? 'main',\n token: process.env.SANITY_API_TOKEN ?? '',\n apiVersion: '2024-01-01',\n },\n\n lingohub: {\n authToken : process.env.LINGOHUB_AUTH_TOKEN ?? '',\n workspace: process.env.LINGOHUB_WORKSPACE ?? '',\n },\n\n retry: {\n maxRetries: parseInt(process.env.RETRY_MAX_RETRIES ?? '5', 10),\n baseDelayMs: parseInt(process.env.RETRY_BASE_DELAY_MS ?? '1000', 10),\n maxDelayMs: parseInt(process.env.RETRY_MAX_DELAY_MS ?? '60000', 10),\n },\n\n api: {\n port: parseInt(process.env.PORT ?? '3010'),\n apiToken: process.env.CONTENT_STORE_API_TOKEN ?? '',\n },\n\n scheduledCmsJob: parseScheduledCmsJobConfig(),\n scheduledTranslationJob: parseScheduledTranslationJobConfig(),\n\n cleanup: {\n enabled: (process.env.CLEANUP_ENABLED ?? 'true').toLowerCase() !== 'false',\n retentionDays: parseInt(process.env.CLEANUP_RETENTION_DAYS ?? '30', 10),\n },\n\n slack: {\n enabled: (process.env.SLACK_BOT_TOKEN ?? '').length > 0,\n botToken: process.env.SLACK_BOT_TOKEN ?? '',\n signingSecret: process.env.SLACK_SIGNING_SECRET ?? '',\n appToken: process.env.SLACK_APP_TOKEN ?? '',\n },\n};\n","import {\n createClient,\n type ContentfulClientApi,\n type ContentTypeCollection,\n type AssetCollection,\n} from 'contentful';\nimport type { ContentfulConfig, RetryConfig } from '../config';\nimport type { CMSAdapter, FetchResult } from './types';\nimport { withRetry } from '../sync/retry';\n\ntype CfCollection = {\n items: CfItem[],\n total: number\n}\n\ntype CfItem = {\n metadata:{\n tags: string[],\n concepts: string[]\n }\n sys:{\n type: string,\n id: string\n space: {\n sys: {\n type: string\n linkType: string\n id: string\n }\n },\n environment: {\n sys: {\n id: string\n type: 'Link',\n linkType: 'Environment'\n }\n },\n contentType: {\n sys: {\n type: 'Link',\n linkType: 'ContentType',\n id: string\n }\n },\n createdBy: {\n sys: {\n type: 'Link',\n linkType: 'User',\n id: string,\n }\n },\n updatedBy: {\n sys: {\n type: 'Link',\n linkType: 'User',\n id: string\n }\n },\n 'revision': number,\n 'createdAt': string,\n 'updatedAt': string,\n 'publishedVersion': string\n },\n fields:{\n [key:string]: unknown\n }\n}\n\n\n/**\n * Recursively unwraps Contentful's { metadata, sys, fields } envelope.\n * `depth` tracks how many entry/asset envelopes deep we are — anything\n * beyond `maxDepth` is dropped to avoid blowing the call stack on\n * circular or extremely deep reference chains.\n *\n * `path` holds objects on the current recursion branch only. That way a\n * shared reference (e.g. the same asset on `image` and `mobileImage`) is\n * unwrapped for each sibling; only true cycles (an object recurring as a\n * descendant of itself) yield `undefined`.\n */\nfunction stripEnvelope(\n value: unknown,\n maxDepth: number,\n depth = 0,\n path = new WeakSet<object>(),\n): unknown {\n if (value === null || typeof value !== 'object') return value;\n\n const obj = value as CfItem;\n\n if (path.has(obj)) return undefined;\n path.add(obj);\n\n try {\n if (Array.isArray(value)) {\n return value.map((item) => stripEnvelope(item, maxDepth, depth, path));\n }\n\n const isEnvelope = 'sys' in obj && 'fields' in obj && typeof obj.fields === 'object';\n\n if (isEnvelope) {\n if (depth >= maxDepth) return undefined;\n\n const fields = obj.fields as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(fields)) {\n result[k] = stripEnvelope(v, maxDepth, depth + 1, path);\n }\n\n const sys = obj.sys;\n if (sys) {\n const existingMeta = result.meta;\n const metaBase =\n typeof existingMeta === 'object' &&\n existingMeta !== null &&\n !Array.isArray(existingMeta)\n ? (existingMeta as Record<string, unknown>)\n : {};\n const _contentType = sys.contentType ? sys.contentType.sys.id : 'Asset'\n result.meta = { ...metaBase, _id: sys.id, _contentType, _updatedAt: sys.updatedAt };\n }\n\n return result;\n }\n\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n result[k] = stripEnvelope(v, maxDepth, depth, path);\n }\n return result;\n } finally {\n path.delete(obj);\n }\n}\n\nexport class ContentfulAdapter implements CMSAdapter {\n readonly name = 'contentful';\n private client: ContentfulClientApi<undefined>;\n private batchSize: number;\n private maxDepth: number;\n private allowedTypes: string[];\n private retryConfig: RetryConfig;\n\n constructor(cfg: ContentfulConfig, retryConfig: RetryConfig) {\n this.client = createClient({\n space: cfg.spaceId,\n accessToken: cfg.accessToken,\n host: cfg.host,\n });\n this.batchSize = cfg.batchSize;\n this.maxDepth = cfg.maxDepth;\n this.allowedTypes = cfg.contentTypes;\n this.retryConfig = retryConfig;\n }\n\n async getContentTypes(): Promise<string[]> {\n const response = await withRetry<ContentTypeCollection>(\n () => this.client.getContentTypes(),\n this.retryConfig,\n );\n\n const allTypes = response.items.map((ct) => ct.sys.id);\n\n if (this.allowedTypes.length > 0) {\n return allTypes.filter((t) => this.allowedTypes.includes(t));\n }\n return allTypes;\n }\n\n /**\n * Fetches every entry for a content type using batched pagination.\n * Contentful caps `getEntries` at 1 000 items per call, so we page through\n * with `skip` until all items are collected.\n *\n * The reserved content type `\"asset\"` fetches from `getAssets()` instead.\n */\n async fetchAll(contentType: string, includeLevels: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 = 4): Promise<FetchResult> {\n if (contentType === 'asset') {\n return this.fetchAllAssets();\n }\n\n const allItems: unknown[] = [];\n let skip = 0;\n let total = 0;\n\n do {\n const payload = {\n content_type: contentType,\n limit: this.batchSize,\n skip,\n include: includeLevels,\n };\n const response = await this.client.getEntries(payload) as unknown as CfCollection;\n total = response.total;\n allItems.push(...response.items);\n skip += response.items.length;\n\n if (total > this.batchSize) {\n console.log(\n ` [contentful] ${contentType}: fetched ${allItems.length}/${total}`,\n );\n }\n } while (skip < total);\n\n return {\n contentType,\n items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),\n total,\n };\n }\n\n private async fetchAllAssets(): Promise<FetchResult> {\n const allItems: unknown[] = [];\n let skip = 0;\n let total = 0;\n\n do {\n const response = await withRetry<AssetCollection>(\n () => this.client.getAssets({ limit: this.batchSize, skip }),\n this.retryConfig,\n );\n total = response.total;\n allItems.push(...response.items);\n skip += response.items.length;\n\n if (total > this.batchSize) {\n console.log(\n ` [contentful] asset: fetched ${allItems.length}/${total}`,\n );\n }\n } while (skip < total);\n\n return {\n contentType: 'asset',\n items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),\n total,\n };\n }\n}\n","import type { RetryConfig } from '../config';\n\n/**\n * Inspects an error to determine if it represents an API rate-limit (HTTP 429).\n * Returns the suggested wait time in ms when available, otherwise `0` to signal\n * that the caller should fall back to computed backoff. Returns `null` when the\n * error is *not* a rate-limit error.\n */\nfunction rateLimitDelayMs(err: unknown): number | null {\n const e = err as Record<string, unknown>;\n\n if (e?.status === 429 || e?.statusCode === 429) {\n const reset = (e?.headers as Record<string, string> | undefined)?.[\n 'x-contentful-ratelimit-reset'\n ];\n return reset ? parseFloat(reset) * 1000 : 0;\n }\n\n const resp = e?.response as Record<string, unknown> | undefined;\n if (resp?.status === 429) {\n const headers = resp?.headers as Record<string, string> | undefined;\n const reset = headers?.['x-contentful-ratelimit-reset'];\n return reset ? parseFloat(reset) * 1000 : 0;\n }\n\n return null;\n}\n\nfunction computeDelay(\n attempt: number,\n baseDelayMs: number,\n maxDelayMs: number,\n): number {\n const exponential = baseDelayMs * Math.pow(2, attempt);\n const jitter = Math.random() * baseDelayMs;\n return Math.min(exponential + jitter, maxDelayMs);\n}\n\n/**\n * Executes `fn` with automatic retry + exponential backoff.\n * Rate-limit (429) responses are handled specially: if the API provides a\n * Retry-After / reset header, that value is respected instead of computed backoff.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n { maxRetries, baseDelayMs, maxDelayMs }: RetryConfig,\n): Promise<T> {\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n if (attempt === maxRetries) throw err;\n\n const rlDelay = rateLimitDelayMs(err);\n let delay: number;\n\n if (rlDelay !== null) {\n delay =\n rlDelay > 0 ? rlDelay : computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` Rate limited (attempt ${attempt + 1}/${maxRetries}). ` +\n `Waiting ${Math.round(delay)}ms…`,\n );\n } else {\n delay = computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` Request failed (attempt ${attempt + 1}/${maxRetries}): ` +\n `${(err as Error).message}. Retrying in ${Math.round(delay)}ms…`,\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n\n throw new Error('withRetry: unreachable');\n}\n","import { createClient, type SanityClient } from '@sanity/client';\nimport type { SanityConfig, RetryConfig } from '../config';\nimport type { CMSAdapter, FetchResult } from './types';\nimport { withRetry } from '../sync/retry';\n\nexport class SanityAdapter implements CMSAdapter {\n readonly name = 'sanity';\n private client: SanityClient;\n private retryConfig: RetryConfig;\n\n constructor(cfg: SanityConfig, retryConfig: RetryConfig) {\n this.client = createClient({\n projectId: cfg.projectId,\n dataset: cfg.dataset,\n token: cfg.token,\n apiVersion: cfg.apiVersion,\n useCdn: false,\n });\n this.retryConfig = retryConfig;\n }\n\n async getContentTypes(): Promise<string[]> {\n const types: string[] = await withRetry(\n () => this.client.fetch('array::unique(*[]._type)'),\n this.retryConfig,\n );\n return types.filter(\n (t) => !t.startsWith('system.') && !t.startsWith('sanity.'),\n );\n }\n\n async fetchAll(contentType: string): Promise<FetchResult> {\n const items: unknown[] = await withRetry(\n () => this.client.fetch('*[_type == $type]', { type: contentType }),\n this.retryConfig,\n );\n\n console.log(` [sanity] ${contentType}: fetched ${items.length} items`);\n return { contentType, items, total: items.length };\n }\n}\n","import { config, type CMSProvider } from '../config';\nimport type { CMSAdapter } from './types';\nimport { ContentfulAdapter } from './contentful';\nimport { SanityAdapter } from './sanity';\n\nexport function createAdapter(cms: CMSProvider): CMSAdapter {\n switch (cms) {\n case 'contentful':\n return new ContentfulAdapter(config.contentful, config.retry);\n case 'sanity':\n return new SanityAdapter(config.sanity, config.retry);\n default:\n throw new Error(`Unknown CMS provider: ${cms as string}`);\n }\n}\n\nexport type { CMSAdapter, FetchResult } from './types';\n","import convert from 'xml-js';\nimport set from 'lodash.set';\nimport merge from 'lodash.merge';\n\nexport const transformObjectToNested = (data: Record<string, unknown>): Record<string, unknown> => {\n const result: Record<string, unknown> = {};\n\n Object.entries(data).forEach(([key, value]) => {\n const tempObject = {};\n set(tempObject, key, value);\n merge(result, tempObject);\n });\n\n return result;\n};\n\nexport const transformObjectToFlat = (data: Record<string, any>): Record<string, any> => { // eslint-disable-line @typescript-eslint/no-explicit-any\n const result: Record<string, unknown> = {};\n\n const flatten = (obj: Record<string, any>, path: string[] = []) => { // eslint-disable-line @typescript-eslint/no-explicit-any\n Object.entries(obj).forEach(([key, value]) => {\n if (typeof value === 'object') {\n flatten(value, path.concat(key));\n } else {\n result[path.concat(key).join('.')] = value;\n }\n });\n };\n\n flatten(data);\n\n return result;\n}\n\nexport const convertXMLToJS = (xml: string): Record<string, string> => {\n const converted = convert.xml2js(xml, {\n ignoreComment: true,\n ignoreDeclaration: true,\n ignoreInstruction: true,\n compact: true,\n }) as {\n resources: {\n string: {\n _attributes: { name: string },\n _text: 'User does not exist'\n }[];\n }\n };\n\n let mapped = {};\n converted.resources.string.forEach((item) => {\n mapped = {\n ...mapped,\n [item._attributes.name]: item._text,\n };\n });\n\n return mapped;\n};\n\nexport const parseIOSStrings = (strings: string): Record<string, string> => {\n const parsedObj: Record<string, string> = {};\n strings\n .split('\\n')\n .filter((line) => line.startsWith('\"') && line.endsWith(';'))\n .map((line) => line.trim().slice(0, -1))\n .forEach((line) => {\n let [key, value] = line.split(' = ');\n if (!key || !value) return;\n key = key.slice(1, -1);\n value = value.slice(1, -1);\n parsedObj[key] = value;\n });\n\n return parsedObj;\n};","import { config } from '../config';\nimport { LingohubResource} from '../../shared/lingohub';\nimport {convertXMLToJS, parseIOSStrings} from '../../shared/utils';\n\nconst cfg = config.lingohub\nconst apiUrl = 'https://api.lingohub.com/v1/' + cfg.workspace + '/projects/';\n\nexport const getResourceForLocale = async (project: string, resource: LingohubResource, locale: string) => {\n const urlForResourceLocalised = `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace('[locale]', locale);\n const result = await fetch(urlForResourceLocalised,\n {\n method: 'GET',\n }).then(async (res) => {\n\n if (!res.ok) {\n throw new Error(`Failed to fetch resource: ${res.status} - ${res.statusText}`);\n }\n\n if (resource.type === 'json') {\n return res.json();\n }\n\n if (resource.type === 'xml') {\n const xml = await res.text();\n return convertXMLToJS(xml);\n }\n\n if (resource.type === 'strings') {\n const strings = await res.text();\n return parseIOSStrings(strings);\n }\n\n throw new Error('Invalid resource type');\n })\n .catch((err) => {\n throw err;\n });\n return result;\n}\n","import type { CMSProvider } from '../config';\nimport { config } from '../config';\nimport { createAdapter } from '../adapters';\nimport {buildCmsObjectKey, buildTranslationObjectKey, ContentStore} from '../../shared/s3';\nimport { allProjects, defaultLocales } from '../../shared/lingohub';\nimport { getResourceForLocale} from '../adapters/lingohub';\n\nexport interface CmsSyncResultEntry {\n contentType: string;\n itemCount: number;\n objectKey: string;\n}\n\nexport interface CmsSyncResult {\n cms: CMSProvider;\n timestamp: number;\n entries: CmsSyncResultEntry[];\n errors: Array<{ contentType: string; error: string }>;\n}\n\nexport interface TranslationSyncResultEntry {\n project: string;\n locale: string;\n itemCount: number;\n objectKey: string;\n}\n\nexport interface TranslationSyncResult {\n timestamp: number;\n entries: TranslationSyncResultEntry[];\n errors: Array<{ locale: string; project: string, error: string }>;\n}\n\nexport async function runSync(cms: CMSProvider, contentTypes?: string[], includeLevels?: number){\n await syncCmsContent(cms, contentTypes, includeLevels )\n}\n\nexport async function syncCmsContent(\n cms: CMSProvider,\n contentTypes?: string[],\n includeLevels?: number\n): Promise<CmsSyncResult> {\n const adapter = createAdapter(cms);\n const store = new ContentStore(config.s3);\n const timestamp = Math.floor(Date.now() / 1000);\n\n console.log(`\\nStarting sync from ${cms} at ${new Date(timestamp * 1000).toISOString()}`);\n\n const typesToSync =\n contentTypes && contentTypes.length > 0\n ? contentTypes\n : await adapter.getContentTypes();\n\n console.log(`Content types to sync: ${typesToSync.join(', ')}\\n`);\n\n const entries: CmsSyncResultEntry[] = [];\n const errors: Array<{ contentType: string; error: string }> = [];\n\n for (const contentType of typesToSync) {\n try {\n const result = await adapter.fetchAll(contentType, includeLevels);\n const objectKey = buildCmsObjectKey(cms, contentType);\n await store.upload(objectKey, result.items);\n\n entries.push({\n contentType,\n itemCount: result.total,\n objectKey,\n });\n\n console.log(\n ` + ${contentType}: ${result.total} items -> ${objectKey}`,\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ contentType, error: message });\n console.error(` x ${contentType}: ${message}`);\n }\n }\n\n console.log(\n `\\nSync complete: ${entries.length} succeeded, ${errors.length} failed\\n`,\n );\n\n return { cms, timestamp, entries, errors };\n}\n\nexport async function syncTranslations(projects?: string[], locales?:string[]):Promise<TranslationSyncResult> {\n\n const store = new ContentStore(config.s3);\n const entries: TranslationSyncResultEntry[] = [];\n const errors: Array<{ project: string; locale: string, error: string }> = [];\n const timestamp = Math.floor(Date.now() / 1000);\n if(!locales){\n locales = defaultLocales;\n }\n if(!projects){\n projects = Object.keys(allProjects);\n }\n\n for(const project of projects) {\n const resources = allProjects[project];\n if(!resources){\n console.error(`No resources found for ${project}`);\n continue;\n }\n for(const resource of resources) {\n for(const loc of locales){\n const locale = (resource.localeMapping && resource.localeMapping[loc]) ? resource.localeMapping[loc] : loc;\n try{\n const result = await getResourceForLocale(project, resource, locale);\n const objectKey = buildTranslationObjectKey(project, resource.fileName, locale);\n await store.upload(objectKey, result);\n const itemCount = Object.keys(result).length\n entries.push({\n project,\n locale,\n itemCount,\n objectKey,\n });\n\n console.log(\n ` + ${project} - ${locale}: ${itemCount} items -> ${objectKey}`,\n );\n\n // await new Promise(resolve => setTimeout(resolve, 1000)); // Rate limiting\n\n }catch(err){\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ project,locale, error: message });\n console.error(` x ${project} - ${locale}: ${message}`);\n }\n }\n }\n }\n\n return { timestamp, entries, errors };\n}\n"],"mappings":"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","names":["config","result","cfg","createClient","cfg","createClient","config","config","config"]}
|