@tandem-language-exchange/content-store 1.0.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 +335 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk/client.d.ts +24 -0
- package/dist/sdk/client.js +26 -0
- package/dist/sdk/client.js.map +1 -0
- package/dist/sdk/index.d.ts +5 -0
- package/dist/sdk/index.js +4 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/shared/bundles.d.ts +41 -0
- package/dist/shared/bundles.js +94 -0
- package/dist/shared/bundles.js.map +1 -0
- package/dist/shared/s3.d.ts +17 -0
- package/dist/shared/s3.js +54 -0
- package/dist/shared/s3.js.map +1 -0
- package/dist/shared/types.d.ts +7 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# Content Store
|
|
2
|
+
|
|
3
|
+
Syncs content from **Contentful** and **Sanity** into versioned JSON bundles on **S3**. Provides both a CLI for one-off or scheduled syncs and an Express REST API for on-demand triggering.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
The codebase is split into three areas so that the npm-published SDK never ships server dependencies (Express, Contentful SDK, Sanity client, dotenv, etc.):
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
├── shared/ # Shared between SDK and server
|
|
12
|
+
│ ├── types.ts # S3Config, CMSProvider
|
|
13
|
+
│ └── s3.ts # ContentStore class (S3 operations)
|
|
14
|
+
├── sdk/ # npm package surface — importable by consumers
|
|
15
|
+
│ ├── client.ts # ContentStoreSDK (fetchBundles + queryBundle)
|
|
16
|
+
│ ├── index.ts # SDK entry point + re-exports
|
|
17
|
+
│ └── README.md # SDK usage documentation
|
|
18
|
+
├── server/ # Express REST API + CLI + CMS sync engine
|
|
19
|
+
│ ├── config.ts # Env-based runtime configuration
|
|
20
|
+
│ ├── server.ts # Express app + endpoints
|
|
21
|
+
│ ├── cli.ts # Commander CLI
|
|
22
|
+
│ ├── middleware/
|
|
23
|
+
│ │ └── auth.ts # Bearer token auth
|
|
24
|
+
│ ├── adapters/ # CMS adapter pattern
|
|
25
|
+
│ │ ├── types.ts # CMSAdapter interface
|
|
26
|
+
│ │ ├── contentful.ts # Contentful (batched pagination + envelope stripping)
|
|
27
|
+
│ │ ├── sanity.ts # Sanity (GROQ queries)
|
|
28
|
+
│ │ └── index.ts # createAdapter factory
|
|
29
|
+
│ └── sync/
|
|
30
|
+
│ ├── retry.ts # Exponential backoff with jitter & 429 handling
|
|
31
|
+
│ └── engine.ts # Orchestrates fetch → upload → copy-latest
|
|
32
|
+
└── index.ts # npm entry — re-exports SDK + shared only
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
When published to npm, only `dist/index.*`, `dist/sdk/`, and `dist/shared/` are included (controlled by the `files` field in `package.json`). Everything under `dist/server/` is excluded.
|
|
36
|
+
|
|
37
|
+
### S3 Object Naming
|
|
38
|
+
|
|
39
|
+
Each sync produces two objects per content type:
|
|
40
|
+
|
|
41
|
+
| Object | Purpose |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `content-{cms}-{contentType}-{timestamp}.json` | Versioned snapshot (retained indefinitely) |
|
|
44
|
+
| `content-{cms}-{contentType}.json` | Latest pointer (overwritten on each sync) |
|
|
45
|
+
|
|
46
|
+
For example, syncing the `gridLayout` type from Contentful at Unix timestamp `1743400000` creates:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
content-contentful-gridLayout-1743400000.json ← versioned
|
|
50
|
+
content-contentful-gridLayout.json ← latest (copy of above)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Getting Started Locally
|
|
56
|
+
|
|
57
|
+
### 1. Clone this repo from Azure DevOps
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone git@ssh.dev.azure.com:v3/tripod-technology/content-store/content-store
|
|
61
|
+
cd content-store
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Setup NPM Auth
|
|
65
|
+
|
|
66
|
+
The project depends on the private `@tandem-web/web-azure-appconfig-sync` package. Authenticate with the npm registry before installing:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
//registry.npmjs.org/:_authToken={NPM_TOKEN}
|
|
70
|
+
registry=https://registry.npmjs.org/
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This token can be found in the Azure Pipelines:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
[Project] > Pipelines > Library > Variable Groups > azure-appconfig-sync > NPM_TOKEN
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 3. Install Dependencies
|
|
80
|
+
|
|
81
|
+
Requires **Node >= 24.1** and **npm >= 11.3** (see `engines` in `package.json`).
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm install
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 4. Setup Azure App Config Syncing
|
|
88
|
+
|
|
89
|
+
This allows you to pull environment variables down from Azure App Configuration, and also switch environments easily.
|
|
90
|
+
|
|
91
|
+
You need to create a file in the root of this project called `.env.appconfig` with the connection strings for Azure App Configuration for each environment:
|
|
92
|
+
|
|
93
|
+
```.env.appconfig
|
|
94
|
+
AZURE_APP_CONFIG_CONNECTION_STRING_STAGING=[string]
|
|
95
|
+
AZURE_APP_CONFIG_CONNECTION_STRING_PRODUCTION=Endpoint=[string]
|
|
96
|
+
AZURE_APP_CONFIG_CONNECTION_STRING_LOCAL=Endpoint=[string]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
These connection strings can be found in the Azure Pipelines:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
[Project] > Pipelines > Library > Variable Groups > azure-appconfig-sync
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
*See the [docs](https://github.com/viveme/web-azure-appconfig-sync) for full the config needed of this package*
|
|
106
|
+
|
|
107
|
+
Run the following command to sync the `local` config:
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm run sync-appconfig:local
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Also, run the following command to sync the `staging` config:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm run sync-appconfig:staging
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 5. Configuring Content Types
|
|
121
|
+
|
|
122
|
+
Content types to sync from Contentful are defined directly in `src/server/config.ts`:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
contentTypes: [
|
|
126
|
+
'gridLayout',
|
|
127
|
+
'iconWithText',
|
|
128
|
+
// Add or remove content type IDs here.
|
|
129
|
+
// Leave empty to sync every content type in the space.
|
|
130
|
+
]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Sanity syncs all document types by default (excluding internal `system.*` and `sanity.*` types). You can override this at runtime via the `--types` CLI flag or the `contentTypes` field in the REST API request body.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Running Local Development Server
|
|
138
|
+
|
|
139
|
+
Starts the Express server with hot-reload via `tsx watch`:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm run dev
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The server listens on `http://localhost:4000` (or the port set by `PORT`).
|
|
146
|
+
|
|
147
|
+
Available endpoints:
|
|
148
|
+
|
|
149
|
+
| Method | Path | Auth | Description |
|
|
150
|
+
| --- | --- | --- | --- |
|
|
151
|
+
| `GET` | `/health` | None | Health check |
|
|
152
|
+
| `POST` | `/sync` | Bearer token | Trigger a content sync |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Building & Running Locally
|
|
157
|
+
|
|
158
|
+
Compile TypeScript to `dist/`:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm run build
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Run the compiled server:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm start
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Docker Build
|
|
173
|
+
|
|
174
|
+
Run the following command to build the the app in a docker container locally:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npm run build:docker
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
CI builds are handled by Azure Pipelines (`deploy/staging-pipeline.yml` and `deploy/production-pipeline.yml`), which:
|
|
181
|
+
|
|
182
|
+
1. Authenticate to npm and install `@tandem-web/web-azure-appconfig-sync`
|
|
183
|
+
2. Fetch environment variables from Azure App Configuration into `.env`
|
|
184
|
+
3. Build and tag the Docker image
|
|
185
|
+
4. Push to Azure Container Registry (`tandemstaging.azurecr.io` / `tandemproduction.azurecr.io`)
|
|
186
|
+
5. Force a new ECS service deployment on the `nextgen-staging` or `nextgen-production` cluster
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Running from CLI
|
|
191
|
+
|
|
192
|
+
All CLI commands are invoked through `tsx src/server/cli.ts` under the hood. Convenience npm scripts are provided for common operations.
|
|
193
|
+
|
|
194
|
+
### `sync` — push CMS content to S3
|
|
195
|
+
|
|
196
|
+
**Sync all configured Contentful content types:**
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm run sync:contentful
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Sync all Sanity document types:**
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
npm run sync:sanity
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Sync specific content types only:**
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm run sync -- --cms contentful --types gridLayout,iconWithText
|
|
212
|
+
npm run sync -- --cms sanity --types article,author
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The process exits with code `0` on full success and `1` if any content type failed to sync. A JSON summary is printed to stdout on completion.
|
|
216
|
+
|
|
217
|
+
### `fetch` — download bundles from S3 to the local filesystem
|
|
218
|
+
|
|
219
|
+
Downloads the latest version of each requested content type bundle from S3 and writes them as JSON files to a local directory.
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
npm run fetch -- --cms contentful --types gridLayout,page
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| Flag | Required | Default | Description |
|
|
226
|
+
| --- | --- | --- | --- |
|
|
227
|
+
| `--cms <provider>` | Yes | | `contentful` or `sanity` |
|
|
228
|
+
| `--types <types>` | Yes | | Comma-separated content types |
|
|
229
|
+
| `--output <dir>` | No | `./content-cache` | Local directory to write bundle files to |
|
|
230
|
+
|
|
231
|
+
Files are written as `content-{cms}-{contentType}.json` inside the output directory.
|
|
232
|
+
|
|
233
|
+
### `query` — query a local bundle
|
|
234
|
+
|
|
235
|
+
Reads a previously fetched bundle from disk and prints filtered results as JSON.
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npm run query -- --cms contentful --type gridLayout \
|
|
239
|
+
--fields '{"columns":"2"}' \
|
|
240
|
+
--select title,bodyBefore \
|
|
241
|
+
--limit 5 \
|
|
242
|
+
--include 2
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
| Flag | Required | Default | Description |
|
|
246
|
+
| --- | --- | --- | --- |
|
|
247
|
+
| `--cms <provider>` | Yes | | `contentful` or `sanity` |
|
|
248
|
+
| `--type <type>` | Yes | | Content type to query |
|
|
249
|
+
| `--output <dir>` | No | `./content-cache` | Directory where bundles are stored |
|
|
250
|
+
| `--fields <json>` | No | | JSON filter object (e.g. `'{"columns":"2"}'`) |
|
|
251
|
+
| `--select <props>` | No | | Comma-separated properties to include in results |
|
|
252
|
+
| `--limit <n>` | No | | Maximum number of results |
|
|
253
|
+
| `--include <n>` | No | | Depth of nested references to include (see [SDK README](src/sdk/README.md)) |
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Calling the REST API
|
|
258
|
+
|
|
259
|
+
All requests to `POST /sync` must include a valid bearer token in the `Authorization` header. The token is the value of the `CONTENT_STORE_API_TOKEN` environment variable.
|
|
260
|
+
|
|
261
|
+
**Sync all configured Contentful types:**
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
curl -X POST http://localhost:3000/sync \
|
|
265
|
+
-H "Authorization: Bearer <CONTENT_STORE_API_TOKEN>" \
|
|
266
|
+
-H "Content-Type: application/json" \
|
|
267
|
+
-d '{"cms": "contentful"}'
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Sync specific types:**
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
curl -X POST http://localhost:3000/sync \
|
|
274
|
+
-H "Authorization: Bearer <CONTENT_STORE_API_TOKEN>" \
|
|
275
|
+
-H "Content-Type: application/json" \
|
|
276
|
+
-d '{"cms": "sanity", "contentTypes": ["article", "author"]}'
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Response format:**
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"cms": "contentful",
|
|
284
|
+
"timestamp": 1743400000,
|
|
285
|
+
"entries": [
|
|
286
|
+
{
|
|
287
|
+
"contentType": "gridLayout",
|
|
288
|
+
"itemCount": 42,
|
|
289
|
+
"versionedKey": "content-contentful-gridLayout-1743400000.json",
|
|
290
|
+
"latestKey": "content-contentful-gridLayout.json"
|
|
291
|
+
}
|
|
292
|
+
],
|
|
293
|
+
"errors": []
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
| Status | Meaning |
|
|
298
|
+
| --- | --- |
|
|
299
|
+
| `200` | All content types synced successfully |
|
|
300
|
+
| `207` | Partial success (some types failed — check `errors` array) |
|
|
301
|
+
| `400` | Invalid CMS provider |
|
|
302
|
+
| `401` | Missing or malformed Authorization header |
|
|
303
|
+
| `403` | Invalid bearer token |
|
|
304
|
+
| `500` | Unexpected server error |
|
|
305
|
+
|
|
306
|
+
**Health check (no auth required):**
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
curl http://localhost:3000/health
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Additional Considerations
|
|
315
|
+
|
|
316
|
+
### Rate Limiting & Retry Behaviour
|
|
317
|
+
|
|
318
|
+
Contentful's Content Delivery/Preview API enforces rate limits. The sync engine wraps every API call in a retry loop with:
|
|
319
|
+
|
|
320
|
+
- **Exponential backoff** — delay doubles on each attempt (1 s → 2 s → 4 s → …)
|
|
321
|
+
- **Random jitter** — prevents thundering-herd effects when multiple syncs run concurrently
|
|
322
|
+
- **429 awareness** — if the API returns a `429 Too Many Requests` with an `x-contentful-ratelimit-reset` header, that value is used as the wait time instead of computed backoff
|
|
323
|
+
- **Configurable limits** — `RETRY_MAX_RETRIES`, `RETRY_BASE_DELAY_MS`, and `RETRY_MAX_DELAY_MS` can be tuned via env vars
|
|
324
|
+
|
|
325
|
+
### Contentful Pagination (Batched Sync)
|
|
326
|
+
|
|
327
|
+
Contentful's `getEntries` endpoint returns a maximum of 1,000 items per request. The Contentful adapter automatically pages through the full result set using `skip` and `limit`, collecting all entries before uploading a single consolidated JSON file to S3.
|
|
328
|
+
|
|
329
|
+
### S3 Versioning Strategy
|
|
330
|
+
|
|
331
|
+
Every sync writes a timestamped object and then copies it to the "latest" key. This means:
|
|
332
|
+
|
|
333
|
+
- Consumers that always read `content-{cms}-{type}.json` get the most recent data without knowing the timestamp.
|
|
334
|
+
- Older snapshots (`content-{cms}-{type}-{timestamp}.json`) remain in the bucket and can be used for auditing, rollback, or diffing.
|
|
335
|
+
- To manage storage costs over time, consider adding an S3 lifecycle rule to expire versioned objects older than a chosen retention period.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ContentStoreSDK } from './sdk/index.js';
|
|
2
|
+
export type { SDKConfig, FetchBundlesOptions, QueryOptions } from './sdk/index.js';
|
|
3
|
+
export { fetchBundles, queryBundle, trimDepth } from './shared/bundles.js';
|
|
4
|
+
export { ContentStore } from './shared/s3.js';
|
|
5
|
+
export type { CMSProvider, S3Config } from './shared/types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { S3Config, CMSProvider } from '../shared/types.js';
|
|
2
|
+
import { type FetchBundlesOptions, type QueryOptions } from '../shared/bundles.js';
|
|
3
|
+
export type { FetchBundlesOptions, QueryOptions };
|
|
4
|
+
export interface SDKConfig {
|
|
5
|
+
s3: S3Config;
|
|
6
|
+
/** Directory where bundle JSON files are saved on the local filesystem. */
|
|
7
|
+
outputDir: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class ContentStoreSDK {
|
|
10
|
+
private store;
|
|
11
|
+
private outputDir;
|
|
12
|
+
constructor(config: SDKConfig);
|
|
13
|
+
/**
|
|
14
|
+
* Downloads the latest bundles from S3 and writes them as JSON files
|
|
15
|
+
* to `outputDir`.
|
|
16
|
+
*
|
|
17
|
+
* @returns A map of contentType to absolute file path.
|
|
18
|
+
*/
|
|
19
|
+
fetchBundles(options: FetchBundlesOptions): Promise<Record<string, string>>;
|
|
20
|
+
/**
|
|
21
|
+
* Queries a previously fetched bundle from the local filesystem.
|
|
22
|
+
*/
|
|
23
|
+
queryBundle(cms: CMSProvider, contentType: string, options?: QueryOptions): Promise<unknown[]>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ContentStore } from '../shared/s3.js';
|
|
2
|
+
import { fetchBundles, queryBundle, } from '../shared/bundles.js';
|
|
3
|
+
export class ContentStoreSDK {
|
|
4
|
+
store;
|
|
5
|
+
outputDir;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.store = new ContentStore(config.s3);
|
|
8
|
+
this.outputDir = config.outputDir;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Downloads the latest bundles from S3 and writes them as JSON files
|
|
12
|
+
* to `outputDir`.
|
|
13
|
+
*
|
|
14
|
+
* @returns A map of contentType to absolute file path.
|
|
15
|
+
*/
|
|
16
|
+
async fetchBundles(options) {
|
|
17
|
+
return fetchBundles(this.store, this.outputDir, options);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Queries a previously fetched bundle from the local filesystem.
|
|
21
|
+
*/
|
|
22
|
+
async queryBundle(cms, contentType, options = {}) {
|
|
23
|
+
return queryBundle(this.outputDir, cms, contentType, options);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/sdk/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EACL,YAAY,EACZ,WAAW,GAGZ,MAAM,sBAAsB,CAAC;AAU9B,MAAM,OAAO,eAAe;IAClB,KAAK,CAAe;IACpB,SAAS,CAAS;IAE1B,YAAY,MAAiB;QAC3B,IAAI,CAAC,KAAK,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,YAAY,CAChB,OAA4B;QAE5B,OAAO,YAAY,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC3D,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,WAAW,CACf,GAAgB,EAChB,WAAmB,EACnB,UAAwB,EAAE;QAE1B,OAAO,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;IAChE,CAAC;CACF"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ContentStoreSDK } from './client.js';
|
|
2
|
+
export type { SDKConfig, FetchBundlesOptions, QueryOptions } from './client.js';
|
|
3
|
+
export { fetchBundles, queryBundle, trimDepth } from '../shared/bundles.js';
|
|
4
|
+
export { ContentStore } from '../shared/s3.js';
|
|
5
|
+
export type { S3Config, CMSProvider } from '../shared/types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sdk/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CMSProvider } from './types.js';
|
|
2
|
+
import { ContentStore } from './s3.js';
|
|
3
|
+
export interface FetchBundlesOptions {
|
|
4
|
+
cms: CMSProvider;
|
|
5
|
+
contentTypes: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface QueryOptions {
|
|
8
|
+
/** Filter items by matching top-level property values (exact equality, or array for IN). */
|
|
9
|
+
fields?: Record<string, unknown>;
|
|
10
|
+
/** Properties to include in each result object. Omit to return all properties. */
|
|
11
|
+
select?: string[];
|
|
12
|
+
/** Maximum number of items to return. */
|
|
13
|
+
limit?: number;
|
|
14
|
+
/**
|
|
15
|
+
* How many levels deep to return.
|
|
16
|
+
* - 1 = the item's own scalar properties only; nested objects/refs are nulled.
|
|
17
|
+
* - 2 = the item including its direct references; refs inside those are nulled.
|
|
18
|
+
* - 3 = three levels deep, and so on.
|
|
19
|
+
* - Omit to return the full depth.
|
|
20
|
+
*/
|
|
21
|
+
include?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Trims nested object depth.
|
|
25
|
+
*
|
|
26
|
+
* `remaining` represents how many levels the current object is allowed.
|
|
27
|
+
* - Scalar properties are always kept.
|
|
28
|
+
* - Nested objects / arrays-of-objects consume one level. When `remaining`
|
|
29
|
+
* drops to 1 they are replaced with `null` (no budget left for refs).
|
|
30
|
+
*/
|
|
31
|
+
export declare function trimDepth(value: unknown, remaining: number): unknown;
|
|
32
|
+
/**
|
|
33
|
+
* Downloads the latest bundles from S3 and writes them as JSON files to `outputDir`.
|
|
34
|
+
*
|
|
35
|
+
* @returns A map of contentType to absolute file path.
|
|
36
|
+
*/
|
|
37
|
+
export declare function fetchBundles(store: ContentStore, outputDir: string, options: FetchBundlesOptions): Promise<Record<string, string>>;
|
|
38
|
+
/**
|
|
39
|
+
* Queries a previously fetched bundle from the local filesystem.
|
|
40
|
+
*/
|
|
41
|
+
export declare function queryBundle(outputDir: string, cms: CMSProvider, contentType: string, options?: QueryOptions): Promise<unknown[]>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Trims nested object depth.
|
|
5
|
+
*
|
|
6
|
+
* `remaining` represents how many levels the current object is allowed.
|
|
7
|
+
* - Scalar properties are always kept.
|
|
8
|
+
* - Nested objects / arrays-of-objects consume one level. When `remaining`
|
|
9
|
+
* drops to 1 they are replaced with `null` (no budget left for refs).
|
|
10
|
+
*/
|
|
11
|
+
export function trimDepth(value, remaining) {
|
|
12
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return value.map((item) => trimDepth(item, remaining));
|
|
17
|
+
}
|
|
18
|
+
const obj = value;
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
21
|
+
if (v === null || v === undefined || typeof v !== 'object') {
|
|
22
|
+
result[k] = v;
|
|
23
|
+
}
|
|
24
|
+
else if (remaining <= 1) {
|
|
25
|
+
result[k] = null;
|
|
26
|
+
}
|
|
27
|
+
else if (Array.isArray(v)) {
|
|
28
|
+
result[k] = v.map((item) => {
|
|
29
|
+
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
|
30
|
+
return trimDepth(item, remaining - 1);
|
|
31
|
+
}
|
|
32
|
+
return item;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
result[k] = trimDepth(v, remaining - 1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Downloads the latest bundles from S3 and writes them as JSON files to `outputDir`.
|
|
43
|
+
*
|
|
44
|
+
* @returns A map of contentType to absolute file path.
|
|
45
|
+
*/
|
|
46
|
+
export async function fetchBundles(store, outputDir, options) {
|
|
47
|
+
const { cms, contentTypes } = options;
|
|
48
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
49
|
+
const result = {};
|
|
50
|
+
await Promise.all(contentTypes.map(async (contentType) => {
|
|
51
|
+
const key = store.buildLatestKey(cms, contentType);
|
|
52
|
+
const data = await store.download(key);
|
|
53
|
+
const filePath = path.resolve(outputDir, `content-${cms}-${contentType}.json`);
|
|
54
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
55
|
+
result[contentType] = filePath;
|
|
56
|
+
}));
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Queries a previously fetched bundle from the local filesystem.
|
|
61
|
+
*/
|
|
62
|
+
export async function queryBundle(outputDir, cms, contentType, options = {}) {
|
|
63
|
+
const filePath = path.resolve(outputDir, `content-${cms}-${contentType}.json`);
|
|
64
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
65
|
+
let items = JSON.parse(raw);
|
|
66
|
+
if (options.fields) {
|
|
67
|
+
const filters = options.fields;
|
|
68
|
+
items = items.filter((item) => Object.entries(filters).every(([key, expected]) => {
|
|
69
|
+
const actual = item[key];
|
|
70
|
+
if (Array.isArray(expected))
|
|
71
|
+
return expected.includes(actual);
|
|
72
|
+
return actual === expected;
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
if (options.limit !== undefined && options.limit > 0) {
|
|
76
|
+
items = items.slice(0, options.limit);
|
|
77
|
+
}
|
|
78
|
+
if (options.include !== undefined) {
|
|
79
|
+
items = items.map((item) => trimDepth(item, options.include));
|
|
80
|
+
}
|
|
81
|
+
if (options.select?.length) {
|
|
82
|
+
const keys = options.select;
|
|
83
|
+
items = items.map((item) => {
|
|
84
|
+
const picked = {};
|
|
85
|
+
for (const k of keys) {
|
|
86
|
+
if (k in item)
|
|
87
|
+
picked[k] = item[k];
|
|
88
|
+
}
|
|
89
|
+
return picked;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return items;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=bundles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundles.js","sourceRoot":"","sources":["../../src/shared/bundles.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { S3Config } from './types.js';
|
|
2
|
+
export declare class ContentStore {
|
|
3
|
+
private client;
|
|
4
|
+
private bucket;
|
|
5
|
+
constructor(cfg: S3Config);
|
|
6
|
+
/** content-{cms}-{contentType}-{timestamp}.json */
|
|
7
|
+
buildVersionedKey(cms: string, contentType: string, timestamp: number): string;
|
|
8
|
+
/** content-{cms}-{contentType}.json (always points at the latest version) */
|
|
9
|
+
buildLatestKey(cms: string, contentType: string): string;
|
|
10
|
+
upload(key: string, data: unknown): Promise<string>;
|
|
11
|
+
download(key: string): Promise<unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* Copies a versioned object to the "latest" key so that it always reflects
|
|
14
|
+
* the most recent sync while older timestamped versions are retained.
|
|
15
|
+
*/
|
|
16
|
+
copyToLatest(sourceKey: string, cms: string, contentType: string): Promise<string>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { S3Client, PutObjectCommand, CopyObjectCommand, GetObjectCommand, } from '@aws-sdk/client-s3';
|
|
2
|
+
export class ContentStore {
|
|
3
|
+
client;
|
|
4
|
+
bucket;
|
|
5
|
+
constructor(cfg) {
|
|
6
|
+
this.client = new S3Client({
|
|
7
|
+
region: cfg.region,
|
|
8
|
+
credentials: {
|
|
9
|
+
accessKeyId: cfg.accessKeyId,
|
|
10
|
+
secretAccessKey: cfg.secretAccessKey,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
this.bucket = cfg.bucket;
|
|
14
|
+
}
|
|
15
|
+
/** content-{cms}-{contentType}-{timestamp}.json */
|
|
16
|
+
buildVersionedKey(cms, contentType, timestamp) {
|
|
17
|
+
return `content-${cms}-${contentType}-${timestamp}.json`;
|
|
18
|
+
}
|
|
19
|
+
/** content-{cms}-{contentType}.json (always points at the latest version) */
|
|
20
|
+
buildLatestKey(cms, contentType) {
|
|
21
|
+
return `content-${cms}-${contentType}.json`;
|
|
22
|
+
}
|
|
23
|
+
async upload(key, data) {
|
|
24
|
+
await this.client.send(new PutObjectCommand({
|
|
25
|
+
Bucket: this.bucket,
|
|
26
|
+
Key: key,
|
|
27
|
+
Body: JSON.stringify(data, null, 2),
|
|
28
|
+
ContentType: 'application/json',
|
|
29
|
+
}));
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
async download(key) {
|
|
33
|
+
const response = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
34
|
+
const body = await response.Body?.transformToString();
|
|
35
|
+
if (!body)
|
|
36
|
+
throw new Error(`Empty response for key: ${key}`);
|
|
37
|
+
return JSON.parse(body);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Copies a versioned object to the "latest" key so that it always reflects
|
|
41
|
+
* the most recent sync while older timestamped versions are retained.
|
|
42
|
+
*/
|
|
43
|
+
async copyToLatest(sourceKey, cms, contentType) {
|
|
44
|
+
const latestKey = this.buildLatestKey(cms, contentType);
|
|
45
|
+
await this.client.send(new CopyObjectCommand({
|
|
46
|
+
Bucket: this.bucket,
|
|
47
|
+
CopySource: `${this.bucket}/${sourceKey}`,
|
|
48
|
+
Key: latestKey,
|
|
49
|
+
ContentType: 'application/json',
|
|
50
|
+
}));
|
|
51
|
+
return latestKey;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=s3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.js","sourceRoot":"","sources":["../../src/shared/s3.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAG5B,MAAM,OAAO,YAAY;IACf,MAAM,CAAW;IACjB,MAAM,CAAS;IAEvB,YAAY,GAAa;QACvB,IAAI,CAAC,MAAM,GAAG,IAAI,QAAQ,CAAC;YACzB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,WAAW,EAAE;gBACX,WAAW,EAAE,GAAG,CAAC,WAAW;gBAC5B,eAAe,EAAE,GAAG,CAAC,eAAe;aACrC;SACF,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IAC3B,CAAC;IAED,mDAAmD;IACnD,iBAAiB,CAAC,GAAW,EAAE,WAAmB,EAAE,SAAiB;QACnE,OAAO,WAAW,GAAG,IAAI,WAAW,IAAI,SAAS,OAAO,CAAC;IAC3D,CAAC;IAED,8EAA8E;IAC9E,cAAc,CAAC,GAAW,EAAE,WAAmB;QAC7C,OAAO,WAAW,GAAG,IAAI,WAAW,OAAO,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW,EAAE,IAAa;QACrC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CACpB,IAAI,gBAAgB,CAAC;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,GAAG,EAAE,GAAG;YACR,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACnC,WAAW,EAAE,kBAAkB;SAChC,CAAC,CACH,CAAC;QACF,OAAO,GAAG,CAAC;IACb,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,GAAW;QACxB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CACrC,IAAI,gBAAgB,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CACxD,CAAC;QACF,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,iBAAiB,EAAE,CAAC;QACtD,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC;QAC7D,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,YAAY,CAChB,SAAiB,EACjB,GAAW,EACX,WAAmB;QAEnB,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QACxD,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CACpB,IAAI,iBAAiB,CAAC;YACpB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,UAAU,EAAE,GAAG,IAAI,CAAC,MAAM,IAAI,SAAS,EAAE;YACzC,GAAG,EAAE,SAAS;YACd,WAAW,EAAE,kBAAkB;SAChC,CAAC,CACH,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tandem-language-exchange/content-store",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/index.*",
|
|
15
|
+
"dist/sdk/",
|
|
16
|
+
"dist/shared/"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"npm": "^11.3.0",
|
|
20
|
+
"node": "^24.1.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"dev": "tsx watch src/server/cli.ts serve",
|
|
25
|
+
"start": "node dist/server/cli.js serve",
|
|
26
|
+
"sync": "tsx src/server/cli.ts sync",
|
|
27
|
+
"sync-appconfig:staging": "sync-appconfig env=staging project=content-store",
|
|
28
|
+
"sync-appconfig:production": "sync-appconfig env=production project=content-store",
|
|
29
|
+
"sync-appconfig:local": "sync-appconfig env=local project=content-store",
|
|
30
|
+
"sync:contentful": "tsx src/server/cli.ts sync --cms contentful",
|
|
31
|
+
"sync:sanity": "tsx src/server/cli.ts sync --cms sanity",
|
|
32
|
+
"fetch": "tsx src/server/cli.ts fetch",
|
|
33
|
+
"query": "tsx src/server/cli.ts query",
|
|
34
|
+
"lint": "eslint .",
|
|
35
|
+
"lint:fix": "eslint --fix ."
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@aws-sdk/client-s3": "^3.700.0",
|
|
39
|
+
"@sanity/client": "^7.20.0",
|
|
40
|
+
"commander": "^13.1.0",
|
|
41
|
+
"contentful": "11.10.3",
|
|
42
|
+
"dotenv": "16.4.5",
|
|
43
|
+
"express": "^5.1.0",
|
|
44
|
+
"nodemon": "^3.1.14"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@tandem-web/web-azure-appconfig-sync": "1.0.9",
|
|
48
|
+
"@types/express": "^5.0.2",
|
|
49
|
+
"@types/node": "22.0.0",
|
|
50
|
+
"tsx": "^4.19.0",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
}
|
|
53
|
+
}
|