@tandem-language-exchange/content-store 1.0.22 → 1.1.1
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 +8 -3
- package/dist/{chunk-CAC5LIGK.js → chunk-JFI26IB3.js} +31 -7
- package/dist/chunk-JFI26IB3.js.map +1 -0
- package/dist/client/cli.js +1 -1
- package/dist/client/fetch-bundles.js +1 -1
- package/dist/index-DxoMnE4K.d.ts +92 -0
- package/dist/index.d.ts +1 -102
- package/dist/index.js +0 -214
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +21 -0
- package/dist/node.js +239 -0
- package/dist/node.js.map +1 -0
- package/node.d.ts +1 -0
- package/node.js +5 -0
- package/package.json +12 -2
- package/dist/chunk-CAC5LIGK.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,6 +4,11 @@ SDK for fetching CMS content bundles from S3 and querying them locally from the
|
|
|
4
4
|
|
|
5
5
|
For the server, CLI, and deployment documentation, see the [Server & CLI README](src/server/README.md).
|
|
6
6
|
|
|
7
|
+
### Package entry points
|
|
8
|
+
|
|
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`, `fetchBundles`, `queryBundle`, `ContentStore`, and `trimDepth`. Use only in Node (Route Handlers, Server Actions, scripts, CLI).
|
|
11
|
+
|
|
7
12
|
## Installation
|
|
8
13
|
|
|
9
14
|
```bash
|
|
@@ -13,7 +18,7 @@ npm install content-store
|
|
|
13
18
|
## Initialisation
|
|
14
19
|
|
|
15
20
|
```typescript
|
|
16
|
-
import { ContentStoreSDK } from 'content-store';
|
|
21
|
+
import { ContentStoreSDK } from '@tandem-language-exchange/content-store/node';
|
|
17
22
|
|
|
18
23
|
const sdk = new ContentStoreSDK({
|
|
19
24
|
s3: {
|
|
@@ -225,7 +230,7 @@ Query options are applied in this order:
|
|
|
225
230
|
The core `fetchBundles` and `queryBundle` functions are also available as standalone imports for use outside the SDK class:
|
|
226
231
|
|
|
227
232
|
```typescript
|
|
228
|
-
import { fetchBundles, queryBundle, ContentStore } from 'content-store';
|
|
233
|
+
import { fetchBundles, queryBundle, ContentStore } from '@tandem-language-exchange/content-store/node';
|
|
229
234
|
|
|
230
235
|
const store = new ContentStore({
|
|
231
236
|
bucket: 'beta-content-store',
|
|
@@ -248,7 +253,7 @@ const results = await queryBundle('./content-cache', 'contentful', 'gridLayout',
|
|
|
248
253
|
## Full example
|
|
249
254
|
|
|
250
255
|
```typescript
|
|
251
|
-
import { ContentStoreSDK } from 'content-store';
|
|
256
|
+
import { ContentStoreSDK } from '@tandem-language-exchange/content-store/node';
|
|
252
257
|
|
|
253
258
|
const sdk = new ContentStoreSDK({
|
|
254
259
|
s3: {
|
|
@@ -55,6 +55,33 @@ function getAtPath(obj, dottedPath) {
|
|
|
55
55
|
}
|
|
56
56
|
return { found: true, value: cur };
|
|
57
57
|
}
|
|
58
|
+
var FIELD_OP_KEY = /^(.+)\[([^\]]+)\]$/;
|
|
59
|
+
function parseFieldKey(key) {
|
|
60
|
+
const m = FIELD_OP_KEY.exec(key);
|
|
61
|
+
if (!m) return { path: key };
|
|
62
|
+
return { path: m[1], operator: m[2] };
|
|
63
|
+
}
|
|
64
|
+
function isEmptyValue(value) {
|
|
65
|
+
if (value === null || value === void 0) return true;
|
|
66
|
+
if (value === "") return true;
|
|
67
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
68
|
+
if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
function matchesFieldFilter(item, key, expected) {
|
|
74
|
+
const { path: path2, operator } = parseFieldKey(key);
|
|
75
|
+
const at = getAtPath(item, path2);
|
|
76
|
+
if (operator === "exists") {
|
|
77
|
+
if (expected !== true && expected !== false) return false;
|
|
78
|
+
const empty = !at.found || isEmptyValue(at.value);
|
|
79
|
+
return expected === false ? empty : !empty;
|
|
80
|
+
}
|
|
81
|
+
const actual = at.found ? at.value : void 0;
|
|
82
|
+
if (Array.isArray(expected)) return expected.includes(actual);
|
|
83
|
+
return actual === expected;
|
|
84
|
+
}
|
|
58
85
|
function setNestedAt(target, dottedPath, value) {
|
|
59
86
|
const parts = dottedPath.split(".").filter((p) => p.length > 0);
|
|
60
87
|
if (parts.length === 0) return;
|
|
@@ -99,12 +126,9 @@ async function queryBundle(outputDir, cms, contentType, options = {}) {
|
|
|
99
126
|
if (options.fields) {
|
|
100
127
|
const filters = options.fields;
|
|
101
128
|
items = items.filter(
|
|
102
|
-
(item) => Object.entries(filters).every(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (Array.isArray(expected)) return expected.includes(actual);
|
|
106
|
-
return actual === expected;
|
|
107
|
-
})
|
|
129
|
+
(item) => Object.entries(filters).every(
|
|
130
|
+
([key, expected]) => matchesFieldFilter(item, key, expected)
|
|
131
|
+
)
|
|
108
132
|
);
|
|
109
133
|
}
|
|
110
134
|
if (options.limit !== void 0 && options.limit > 0) {
|
|
@@ -134,4 +158,4 @@ export {
|
|
|
134
158
|
fetchBundles,
|
|
135
159
|
queryBundle
|
|
136
160
|
};
|
|
137
|
-
//# sourceMappingURL=chunk-
|
|
161
|
+
//# sourceMappingURL=chunk-JFI26IB3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/config.ts","../src/shared/bundles.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 ClientConfig {\n}\n\nexport const config: ClientConfig & SharedConfig = {\n ...sharedConfig\n};\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { CMSProvider } from './types';\nimport { ContentStore } from './s3';\n\nexport interface BundleInfo {\n [key: string]: string;\n}\n\nexport interface BundleItem {\n [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any\n}\n\nexport interface FetchBundlesOptions {\n cms: CMSProvider;\n contentTypes: string[];\n}\n\nexport interface QueryOptions {\n /**\n * Filter items by matching property values (exact equality, or array for IN).\n * Keys may use dot notation for nested paths, e.g. `{ 'meta._id': 'abc' }` matches\n * `item.meta._id`.\n *\n * Contentful-style operators on the path (before `[`):\n * - `{ 'field[exists]': true }` — field is present and non-empty (not null, undefined,\n * `''`, `[]`, or `{}`).\n * - `{ 'field[exists]': false }` — field is missing or empty (same emptiness rules).\n * Nested paths work, e.g. `{ 'blocks.hero[exists]': false }`.\n */\n fields?: Record<string, unknown>;\n /**\n * Properties to include in each result object. Omit to return all properties.\n * Keys may use dot notation; nested segments become nested objects in the result,\n * e.g. `['slug', 'meta._updatedAt']` → `{ slug, meta: { _updatedAt } }`.\n */\n select?: string[];\n /** Maximum number of items to return. */\n limit?: number;\n /**\n * How many levels deep to return.\n * - 1 = the item's own scalar properties only; nested objects/refs are nulled.\n * - 2 = the item including its direct references; refs inside those are nulled.\n * - 3 = three levels deep, and so on.\n * - Omit to return the full depth.\n */\n include?: number;\n}\n\n/**\n * Trims nested object depth.\n *\n * `remaining` represents how many levels the current object is allowed.\n * - Scalar properties are always kept.\n * - Nested objects / arrays-of-objects consume one level. When `remaining`\n * drops to 1 they are replaced with `null` (no budget left for refs).\n */\nexport function trimDepth(value: unknown, remaining: number): unknown {\n if (value === null || value === undefined || typeof value !== 'object') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => trimDepth(item, remaining));\n }\n\n const obj = value as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n\n for (const [k, v] of Object.entries(obj)) {\n if (v === null || v === undefined || typeof v !== 'object') {\n result[k] = v;\n } else if (remaining <= 1) {\n result[k] = null;\n } else if (Array.isArray(v)) {\n result[k] = v.map((item) => {\n if (item !== null && typeof item === 'object' && !Array.isArray(item)) {\n return trimDepth(item, remaining - 1);\n }\n return item;\n });\n } else {\n result[k] = trimDepth(v, remaining - 1);\n }\n }\n\n return result;\n}\n\n/** Read a value at a dotted path; returns whether every segment existed. */\nfunction getAtPath(\n obj: unknown,\n dottedPath: string,\n): { found: true; value: unknown } | { found: false } {\n const parts = dottedPath.split('.').filter((p) => p.length > 0);\n if (parts.length === 0) return { found: false };\n\n let cur: unknown = obj;\n for (const p of parts) {\n if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) {\n return { found: false };\n }\n const rec = cur as Record<string, unknown>;\n if (!(p in rec)) return { found: false };\n cur = rec[p];\n }\n return { found: true, value: cur };\n}\n\nconst FIELD_OP_KEY = /^(.+)\\[([^\\]]+)\\]$/;\n\nfunction parseFieldKey(key: string): { path: string; operator?: string } {\n const m = FIELD_OP_KEY.exec(key);\n if (!m) return { path: key };\n return { path: m[1]!, operator: m[2]! };\n}\n\n/** True when a bundle value counts as “no content” (aligned with typical CMS “empty”). */\nfunction isEmptyValue(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (value === '') return true;\n if (Array.isArray(value) && value.length === 0) return true;\n if (\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value).length === 0\n ) {\n return true;\n }\n return false;\n}\n\nfunction matchesFieldFilter(\n item: BundleItem,\n key: string,\n expected: unknown,\n): boolean {\n const { path, operator } = parseFieldKey(key);\n const at = getAtPath(item, path);\n\n if (operator === 'exists') {\n if (expected !== true && expected !== false) return false;\n const empty = !at.found || isEmptyValue(at.value);\n return expected === false ? empty : !empty;\n }\n\n const actual = at.found ? at.value : undefined;\n if (Array.isArray(expected)) return expected.includes(actual);\n return actual === expected;\n}\n\n/** Set `value` on `target` at a dotted path, creating plain objects as needed. */\nfunction setNestedAt(\n target: Record<string, unknown>,\n dottedPath: string,\n value: unknown,\n): void {\n const parts = dottedPath.split('.').filter((p) => p.length > 0);\n if (parts.length === 0) return;\n\n if (parts.length === 1) {\n target[parts[0]!] = value;\n return;\n }\n\n const head = parts[0]!;\n const rest = parts.slice(1).join('.');\n let nested = target[head];\n if (\n nested === null ||\n typeof nested !== 'object' ||\n Array.isArray(nested)\n ) {\n nested = {};\n target[head] = nested;\n }\n setNestedAt(nested as Record<string, unknown>, rest, value);\n}\n\n/**\n * Downloads the latest bundles from S3 and writes them as JSON files to `outputDir`.\n *\n * @returns A map of contentType to absolute file path.\n */\nexport async function fetchBundles(\n store: ContentStore,\n outputDir: string,\n options: FetchBundlesOptions,\n): Promise<Record<string, string>> {\n const { cms, contentTypes } = options;\n await fs.mkdir(outputDir, { recursive: true });\n\n const result:BundleInfo = {};\n\n await Promise.all(\n contentTypes.map(async (contentType) => {\n const key = store.buildLatestKey(cms, contentType);\n const data = await store.download(key);\n const filePath = path.resolve(\n outputDir,\n `${cms}-${contentType}.json`,\n );\n await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');\n result[contentType] = filePath;\n }),\n );\n\n return result;\n}\n\n/**\n * Queries a previously fetched bundle from the local filesystem.\n */\nexport async function queryBundle(\n outputDir: string,\n cms: CMSProvider,\n contentType: string,\n options: QueryOptions = {},\n): Promise<unknown[]> {\n const filePath = path.resolve(\n outputDir,\n `${cms}-${contentType}.json`,\n );\n const raw = await fs.readFile(filePath, 'utf-8');\n let items:BundleItem[] = JSON.parse(raw) as Record<string, unknown>[];\n\n if (options.fields) {\n const filters = options.fields;\n items = items.filter((item) =>\n Object.entries(filters).every(([key, expected]) =>\n matchesFieldFilter(item, key, expected),\n ),\n );\n }\n\n if (options.limit !== undefined && options.limit > 0) {\n items = items.slice(0, options.limit);\n }\n\n if (options.include !== undefined) {\n items = items.map(\n (item) => trimDepth(item, options.include!) as Record<string, unknown>,\n );\n }\n\n if (options.select?.length) {\n const keys = options.select;\n items = items.map((item) => {\n const picked: Record<string, unknown> = {};\n for (const k of keys) {\n const at = getAtPath(item, k);\n if (at.found) setNestedAt(picked, k, at.value);\n }\n return picked;\n });\n }\n\n return items;\n}\n"],"mappings":"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","names":["config","path"]}
|
package/dist/client/cli.js
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
type CMSProvider = 'contentful' | 'sanity';
|
|
2
|
+
interface S3Config {
|
|
3
|
+
bucket: string;
|
|
4
|
+
region: string;
|
|
5
|
+
accessKeyId: string;
|
|
6
|
+
secretAccessKey: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare class ContentStore {
|
|
10
|
+
private client;
|
|
11
|
+
private bucket;
|
|
12
|
+
constructor(cfg: S3Config);
|
|
13
|
+
/** {cms}-{contentType}-{timestamp}.json */
|
|
14
|
+
buildVersionedKey(cms: string, contentType: string, timestamp: number): string;
|
|
15
|
+
/** {cms}-{contentType}.json (always points at the latest version) */
|
|
16
|
+
buildLatestKey(cms: string, contentType: string): string;
|
|
17
|
+
upload(key: string, data: unknown): Promise<string>;
|
|
18
|
+
download(key: string): Promise<unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* Copies a versioned object to the "latest" key so that it always reflects
|
|
21
|
+
* the most recent sync while older timestamped versions are retained.
|
|
22
|
+
*/
|
|
23
|
+
copyToLatest(sourceKey: string, cms: string, contentType: string): Promise<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface BundleInfo {
|
|
27
|
+
[key: string]: string;
|
|
28
|
+
}
|
|
29
|
+
interface BundleItem {
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}
|
|
32
|
+
interface FetchBundlesOptions {
|
|
33
|
+
cms: CMSProvider;
|
|
34
|
+
contentTypes: string[];
|
|
35
|
+
}
|
|
36
|
+
interface QueryOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Filter items by matching property values (exact equality, or array for IN).
|
|
39
|
+
* Keys may use dot notation for nested paths, e.g. `{ 'meta._id': 'abc' }` matches
|
|
40
|
+
* `item.meta._id`.
|
|
41
|
+
*
|
|
42
|
+
* Contentful-style operators on the path (before `[`):
|
|
43
|
+
* - `{ 'field[exists]': true }` — field is present and non-empty (not null, undefined,
|
|
44
|
+
* `''`, `[]`, or `{}`).
|
|
45
|
+
* - `{ 'field[exists]': false }` — field is missing or empty (same emptiness rules).
|
|
46
|
+
* Nested paths work, e.g. `{ 'blocks.hero[exists]': false }`.
|
|
47
|
+
*/
|
|
48
|
+
fields?: Record<string, unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* Properties to include in each result object. Omit to return all properties.
|
|
51
|
+
* Keys may use dot notation; nested segments become nested objects in the result,
|
|
52
|
+
* e.g. `['slug', 'meta._updatedAt']` → `{ slug, meta: { _updatedAt } }`.
|
|
53
|
+
*/
|
|
54
|
+
select?: string[];
|
|
55
|
+
/** Maximum number of items to return. */
|
|
56
|
+
limit?: number;
|
|
57
|
+
/**
|
|
58
|
+
* How many levels deep to return.
|
|
59
|
+
* - 1 = the item's own scalar properties only; nested objects/refs are nulled.
|
|
60
|
+
* - 2 = the item including its direct references; refs inside those are nulled.
|
|
61
|
+
* - 3 = three levels deep, and so on.
|
|
62
|
+
* - Omit to return the full depth.
|
|
63
|
+
*/
|
|
64
|
+
include?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Trims nested object depth.
|
|
68
|
+
*
|
|
69
|
+
* `remaining` represents how many levels the current object is allowed.
|
|
70
|
+
* - Scalar properties are always kept.
|
|
71
|
+
* - Nested objects / arrays-of-objects consume one level. When `remaining`
|
|
72
|
+
* drops to 1 they are replaced with `null` (no budget left for refs).
|
|
73
|
+
*/
|
|
74
|
+
declare function trimDepth(value: unknown, remaining: number): unknown;
|
|
75
|
+
/**
|
|
76
|
+
* Downloads the latest bundles from S3 and writes them as JSON files to `outputDir`.
|
|
77
|
+
*
|
|
78
|
+
* @returns A map of contentType to absolute file path.
|
|
79
|
+
*/
|
|
80
|
+
declare function fetchBundles(store: ContentStore, outputDir: string, options: FetchBundlesOptions): Promise<Record<string, string>>;
|
|
81
|
+
/**
|
|
82
|
+
* Queries a previously fetched bundle from the local filesystem.
|
|
83
|
+
*/
|
|
84
|
+
declare function queryBundle(outputDir: string, cms: CMSProvider, contentType: string, options?: QueryOptions): Promise<unknown[]>;
|
|
85
|
+
|
|
86
|
+
interface SDKConfig {
|
|
87
|
+
s3: S3Config;
|
|
88
|
+
/** Directory where bundle JSON files are saved on the local filesystem. */
|
|
89
|
+
outputDir: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { type BundleInfo as B, type CMSProvider as C, type FetchBundlesOptions as F, type QueryOptions as Q, type SDKConfig as S, type BundleItem as a, ContentStore as b, type S3Config as c, fetchBundles as f, queryBundle as q, trimDepth as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,102 +1 @@
|
|
|
1
|
-
|
|
2
|
-
interface S3Config {
|
|
3
|
-
bucket: string;
|
|
4
|
-
region: string;
|
|
5
|
-
accessKeyId: string;
|
|
6
|
-
secretAccessKey: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
declare class ContentStore {
|
|
10
|
-
private client;
|
|
11
|
-
private bucket;
|
|
12
|
-
constructor(cfg: S3Config);
|
|
13
|
-
/** {cms}-{contentType}-{timestamp}.json */
|
|
14
|
-
buildVersionedKey(cms: string, contentType: string, timestamp: number): string;
|
|
15
|
-
/** {cms}-{contentType}.json (always points at the latest version) */
|
|
16
|
-
buildLatestKey(cms: string, contentType: string): string;
|
|
17
|
-
upload(key: string, data: unknown): Promise<string>;
|
|
18
|
-
download(key: string): Promise<unknown>;
|
|
19
|
-
/**
|
|
20
|
-
* Copies a versioned object to the "latest" key so that it always reflects
|
|
21
|
-
* the most recent sync while older timestamped versions are retained.
|
|
22
|
-
*/
|
|
23
|
-
copyToLatest(sourceKey: string, cms: string, contentType: string): Promise<string>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface BundleInfo {
|
|
27
|
-
[key: string]: string;
|
|
28
|
-
}
|
|
29
|
-
interface BundleItem {
|
|
30
|
-
[key: string]: any;
|
|
31
|
-
}
|
|
32
|
-
interface FetchBundlesOptions {
|
|
33
|
-
cms: CMSProvider;
|
|
34
|
-
contentTypes: string[];
|
|
35
|
-
}
|
|
36
|
-
interface QueryOptions {
|
|
37
|
-
/**
|
|
38
|
-
* Filter items by matching property values (exact equality, or array for IN).
|
|
39
|
-
* Keys may use dot notation for nested paths, e.g. `{ 'meta._id': 'abc' }` matches
|
|
40
|
-
* `item.meta._id`.
|
|
41
|
-
*/
|
|
42
|
-
fields?: Record<string, unknown>;
|
|
43
|
-
/**
|
|
44
|
-
* Properties to include in each result object. Omit to return all properties.
|
|
45
|
-
* Keys may use dot notation; nested segments become nested objects in the result,
|
|
46
|
-
* e.g. `['slug', 'meta._updatedAt']` → `{ slug, meta: { _updatedAt } }`.
|
|
47
|
-
*/
|
|
48
|
-
select?: string[];
|
|
49
|
-
/** Maximum number of items to return. */
|
|
50
|
-
limit?: number;
|
|
51
|
-
/**
|
|
52
|
-
* How many levels deep to return.
|
|
53
|
-
* - 1 = the item's own scalar properties only; nested objects/refs are nulled.
|
|
54
|
-
* - 2 = the item including its direct references; refs inside those are nulled.
|
|
55
|
-
* - 3 = three levels deep, and so on.
|
|
56
|
-
* - Omit to return the full depth.
|
|
57
|
-
*/
|
|
58
|
-
include?: number;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Trims nested object depth.
|
|
62
|
-
*
|
|
63
|
-
* `remaining` represents how many levels the current object is allowed.
|
|
64
|
-
* - Scalar properties are always kept.
|
|
65
|
-
* - Nested objects / arrays-of-objects consume one level. When `remaining`
|
|
66
|
-
* drops to 1 they are replaced with `null` (no budget left for refs).
|
|
67
|
-
*/
|
|
68
|
-
declare function trimDepth(value: unknown, remaining: number): unknown;
|
|
69
|
-
/**
|
|
70
|
-
* Downloads the latest bundles from S3 and writes them as JSON files to `outputDir`.
|
|
71
|
-
*
|
|
72
|
-
* @returns A map of contentType to absolute file path.
|
|
73
|
-
*/
|
|
74
|
-
declare function fetchBundles(store: ContentStore, outputDir: string, options: FetchBundlesOptions): Promise<Record<string, string>>;
|
|
75
|
-
/**
|
|
76
|
-
* Queries a previously fetched bundle from the local filesystem.
|
|
77
|
-
*/
|
|
78
|
-
declare function queryBundle(outputDir: string, cms: CMSProvider, contentType: string, options?: QueryOptions): Promise<unknown[]>;
|
|
79
|
-
|
|
80
|
-
interface SDKConfig {
|
|
81
|
-
s3: S3Config;
|
|
82
|
-
/** Directory where bundle JSON files are saved on the local filesystem. */
|
|
83
|
-
outputDir: string;
|
|
84
|
-
}
|
|
85
|
-
declare class ContentStoreSDK {
|
|
86
|
-
private store;
|
|
87
|
-
private outputDir;
|
|
88
|
-
constructor(config: SDKConfig);
|
|
89
|
-
/**
|
|
90
|
-
* Downloads the latest bundles from S3 and writes them as JSON files
|
|
91
|
-
* to `outputDir`.
|
|
92
|
-
*
|
|
93
|
-
* @returns A map of contentType to absolute file path.
|
|
94
|
-
*/
|
|
95
|
-
fetchBundles(options: FetchBundlesOptions): Promise<Record<string, string>>;
|
|
96
|
-
/**
|
|
97
|
-
* Queries a previously fetched bundle from the local filesystem.
|
|
98
|
-
*/
|
|
99
|
-
queryBundle(cms: CMSProvider, contentType: string, options?: QueryOptions): Promise<unknown[]>;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export { type BundleInfo, type BundleItem, type CMSProvider, ContentStore, ContentStoreSDK, type FetchBundlesOptions, type QueryOptions, type S3Config, type SDKConfig, fetchBundles, queryBundle, trimDepth };
|
|
1
|
+
export { B as BundleInfo, a as BundleItem, C as CMSProvider, F as FetchBundlesOptions, Q as QueryOptions, c as S3Config, S as SDKConfig } from './index-DxoMnE4K.js';
|
package/dist/index.js
CHANGED
|
@@ -1,215 +1 @@
|
|
|
1
|
-
// src/shared/s3.ts
|
|
2
|
-
import {
|
|
3
|
-
S3Client,
|
|
4
|
-
PutObjectCommand,
|
|
5
|
-
CopyObjectCommand,
|
|
6
|
-
GetObjectCommand
|
|
7
|
-
} from "@aws-sdk/client-s3";
|
|
8
|
-
var ContentStore = class {
|
|
9
|
-
client;
|
|
10
|
-
bucket;
|
|
11
|
-
constructor(cfg) {
|
|
12
|
-
this.client = new S3Client({
|
|
13
|
-
region: cfg.region,
|
|
14
|
-
credentials: {
|
|
15
|
-
accessKeyId: cfg.accessKeyId,
|
|
16
|
-
secretAccessKey: cfg.secretAccessKey
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
this.bucket = cfg.bucket;
|
|
20
|
-
}
|
|
21
|
-
/** {cms}-{contentType}-{timestamp}.json */
|
|
22
|
-
buildVersionedKey(cms, contentType, timestamp) {
|
|
23
|
-
return `${cms}-${contentType}-${timestamp}.json`;
|
|
24
|
-
}
|
|
25
|
-
/** {cms}-{contentType}.json (always points at the latest version) */
|
|
26
|
-
buildLatestKey(cms, contentType) {
|
|
27
|
-
return `${cms}-${contentType}.json`;
|
|
28
|
-
}
|
|
29
|
-
async upload(key, data) {
|
|
30
|
-
await this.client.send(
|
|
31
|
-
new PutObjectCommand({
|
|
32
|
-
Bucket: this.bucket,
|
|
33
|
-
Key: key,
|
|
34
|
-
Body: JSON.stringify(data, null, 2),
|
|
35
|
-
ContentType: "application/json"
|
|
36
|
-
})
|
|
37
|
-
);
|
|
38
|
-
return key;
|
|
39
|
-
}
|
|
40
|
-
async download(key) {
|
|
41
|
-
const response = await this.client.send(
|
|
42
|
-
new GetObjectCommand({ Bucket: this.bucket, Key: key })
|
|
43
|
-
);
|
|
44
|
-
const body = await response.Body?.transformToString();
|
|
45
|
-
if (!body) throw new Error(`Empty response for key: ${key}`);
|
|
46
|
-
return JSON.parse(body);
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Copies a versioned object to the "latest" key so that it always reflects
|
|
50
|
-
* the most recent sync while older timestamped versions are retained.
|
|
51
|
-
*/
|
|
52
|
-
async copyToLatest(sourceKey, cms, contentType) {
|
|
53
|
-
const latestKey = this.buildLatestKey(cms, contentType);
|
|
54
|
-
await this.client.send(
|
|
55
|
-
new CopyObjectCommand({
|
|
56
|
-
Bucket: this.bucket,
|
|
57
|
-
CopySource: `${this.bucket}/${sourceKey}`,
|
|
58
|
-
Key: latestKey,
|
|
59
|
-
ContentType: "application/json"
|
|
60
|
-
})
|
|
61
|
-
);
|
|
62
|
-
return latestKey;
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// src/shared/bundles.ts
|
|
67
|
-
import fs from "fs/promises";
|
|
68
|
-
import path from "path";
|
|
69
|
-
function trimDepth(value, remaining) {
|
|
70
|
-
if (value === null || value === void 0 || typeof value !== "object") {
|
|
71
|
-
return value;
|
|
72
|
-
}
|
|
73
|
-
if (Array.isArray(value)) {
|
|
74
|
-
return value.map((item) => trimDepth(item, remaining));
|
|
75
|
-
}
|
|
76
|
-
const obj = value;
|
|
77
|
-
const result = {};
|
|
78
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
79
|
-
if (v === null || v === void 0 || typeof v !== "object") {
|
|
80
|
-
result[k] = v;
|
|
81
|
-
} else if (remaining <= 1) {
|
|
82
|
-
result[k] = null;
|
|
83
|
-
} else if (Array.isArray(v)) {
|
|
84
|
-
result[k] = v.map((item) => {
|
|
85
|
-
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
86
|
-
return trimDepth(item, remaining - 1);
|
|
87
|
-
}
|
|
88
|
-
return item;
|
|
89
|
-
});
|
|
90
|
-
} else {
|
|
91
|
-
result[k] = trimDepth(v, remaining - 1);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return result;
|
|
95
|
-
}
|
|
96
|
-
function getAtPath(obj, dottedPath) {
|
|
97
|
-
const parts = dottedPath.split(".").filter((p) => p.length > 0);
|
|
98
|
-
if (parts.length === 0) return { found: false };
|
|
99
|
-
let cur = obj;
|
|
100
|
-
for (const p of parts) {
|
|
101
|
-
if (cur === null || typeof cur !== "object" || Array.isArray(cur)) {
|
|
102
|
-
return { found: false };
|
|
103
|
-
}
|
|
104
|
-
const rec = cur;
|
|
105
|
-
if (!(p in rec)) return { found: false };
|
|
106
|
-
cur = rec[p];
|
|
107
|
-
}
|
|
108
|
-
return { found: true, value: cur };
|
|
109
|
-
}
|
|
110
|
-
function setNestedAt(target, dottedPath, value) {
|
|
111
|
-
const parts = dottedPath.split(".").filter((p) => p.length > 0);
|
|
112
|
-
if (parts.length === 0) return;
|
|
113
|
-
if (parts.length === 1) {
|
|
114
|
-
target[parts[0]] = value;
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const head = parts[0];
|
|
118
|
-
const rest = parts.slice(1).join(".");
|
|
119
|
-
let nested = target[head];
|
|
120
|
-
if (nested === null || typeof nested !== "object" || Array.isArray(nested)) {
|
|
121
|
-
nested = {};
|
|
122
|
-
target[head] = nested;
|
|
123
|
-
}
|
|
124
|
-
setNestedAt(nested, rest, value);
|
|
125
|
-
}
|
|
126
|
-
async function fetchBundles(store, outputDir, options) {
|
|
127
|
-
const { cms, contentTypes } = options;
|
|
128
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
129
|
-
const result = {};
|
|
130
|
-
await Promise.all(
|
|
131
|
-
contentTypes.map(async (contentType) => {
|
|
132
|
-
const key = store.buildLatestKey(cms, contentType);
|
|
133
|
-
const data = await store.download(key);
|
|
134
|
-
const filePath = path.resolve(
|
|
135
|
-
outputDir,
|
|
136
|
-
`${cms}-${contentType}.json`
|
|
137
|
-
);
|
|
138
|
-
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
139
|
-
result[contentType] = filePath;
|
|
140
|
-
})
|
|
141
|
-
);
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
async function queryBundle(outputDir, cms, contentType, options = {}) {
|
|
145
|
-
const filePath = path.resolve(
|
|
146
|
-
outputDir,
|
|
147
|
-
`${cms}-${contentType}.json`
|
|
148
|
-
);
|
|
149
|
-
const raw = await fs.readFile(filePath, "utf-8");
|
|
150
|
-
let items = JSON.parse(raw);
|
|
151
|
-
if (options.fields) {
|
|
152
|
-
const filters = options.fields;
|
|
153
|
-
items = items.filter(
|
|
154
|
-
(item) => Object.entries(filters).every(([key, expected]) => {
|
|
155
|
-
const at = getAtPath(item, key);
|
|
156
|
-
const actual = at.found ? at.value : void 0;
|
|
157
|
-
if (Array.isArray(expected)) return expected.includes(actual);
|
|
158
|
-
return actual === expected;
|
|
159
|
-
})
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
if (options.limit !== void 0 && options.limit > 0) {
|
|
163
|
-
items = items.slice(0, options.limit);
|
|
164
|
-
}
|
|
165
|
-
if (options.include !== void 0) {
|
|
166
|
-
items = items.map(
|
|
167
|
-
(item) => trimDepth(item, options.include)
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
if (options.select?.length) {
|
|
171
|
-
const keys = options.select;
|
|
172
|
-
items = items.map((item) => {
|
|
173
|
-
const picked = {};
|
|
174
|
-
for (const k of keys) {
|
|
175
|
-
const at = getAtPath(item, k);
|
|
176
|
-
if (at.found) setNestedAt(picked, k, at.value);
|
|
177
|
-
}
|
|
178
|
-
return picked;
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
return items;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// src/sdk/client.ts
|
|
185
|
-
var ContentStoreSDK = class {
|
|
186
|
-
store;
|
|
187
|
-
outputDir;
|
|
188
|
-
constructor(config) {
|
|
189
|
-
this.store = new ContentStore(config.s3);
|
|
190
|
-
this.outputDir = config.outputDir;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Downloads the latest bundles from S3 and writes them as JSON files
|
|
194
|
-
* to `outputDir`.
|
|
195
|
-
*
|
|
196
|
-
* @returns A map of contentType to absolute file path.
|
|
197
|
-
*/
|
|
198
|
-
async fetchBundles(options) {
|
|
199
|
-
return fetchBundles(this.store, this.outputDir, options);
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Queries a previously fetched bundle from the local filesystem.
|
|
203
|
-
*/
|
|
204
|
-
async queryBundle(cms, contentType, options = {}) {
|
|
205
|
-
return queryBundle(this.outputDir, cms, contentType, options);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
export {
|
|
209
|
-
ContentStore,
|
|
210
|
-
ContentStoreSDK,
|
|
211
|
-
fetchBundles,
|
|
212
|
-
queryBundle,
|
|
213
|
-
trimDepth
|
|
214
|
-
};
|
|
215
1
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/shared/s3.ts","../src/shared/bundles.ts","../src/sdk/client.ts"],"sourcesContent":["import {\n S3Client,\n PutObjectCommand,\n CopyObjectCommand,\n GetObjectCommand,\n} from '@aws-sdk/client-s3';\nimport type { S3Config } from './types';\n\nexport class ContentStore {\n private client: S3Client;\n private bucket: string;\n\n constructor(cfg: S3Config) {\n this.client = new S3Client({\n region: cfg.region,\n credentials: {\n accessKeyId: cfg.accessKeyId,\n secretAccessKey: cfg.secretAccessKey,\n },\n });\n this.bucket = cfg.bucket;\n }\n\n /** {cms}-{contentType}-{timestamp}.json */\n buildVersionedKey(cms: string, contentType: string, timestamp: number): string {\n return `${cms}-${contentType}-${timestamp}.json`;\n }\n\n /** {cms}-{contentType}.json (always points at the latest version) */\n buildLatestKey(cms: string, contentType: string): string {\n return `${cms}-${contentType}.json`;\n }\n\n async upload(key: string, data: unknown): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.bucket,\n Key: key,\n Body: JSON.stringify(data, null, 2),\n ContentType: 'application/json',\n }),\n );\n return key;\n }\n\n async download(key: string): Promise<unknown> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: key }),\n );\n const body = await response.Body?.transformToString();\n if (!body) throw new Error(`Empty response for key: ${key}`);\n return JSON.parse(body);\n }\n\n /**\n * Copies a versioned object to the \"latest\" key so that it always reflects\n * the most recent sync while older timestamped versions are retained.\n */\n async copyToLatest(\n sourceKey: string,\n cms: string,\n contentType: string,\n ): Promise<string> {\n const latestKey = this.buildLatestKey(cms, contentType);\n await this.client.send(\n new CopyObjectCommand({\n Bucket: this.bucket,\n CopySource: `${this.bucket}/${sourceKey}`,\n Key: latestKey,\n ContentType: 'application/json',\n }),\n );\n return latestKey;\n }\n}\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { CMSProvider } from './types';\nimport { ContentStore } from './s3';\n\nexport interface BundleInfo {\n [key: string]: string;\n}\n\nexport interface BundleItem {\n [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any\n}\n\nexport interface FetchBundlesOptions {\n cms: CMSProvider;\n contentTypes: string[];\n}\n\nexport interface QueryOptions {\n /**\n * Filter items by matching property values (exact equality, or array for IN).\n * Keys may use dot notation for nested paths, e.g. `{ 'meta._id': 'abc' }` matches\n * `item.meta._id`.\n */\n fields?: Record<string, unknown>;\n /**\n * Properties to include in each result object. Omit to return all properties.\n * Keys may use dot notation; nested segments become nested objects in the result,\n * e.g. `['slug', 'meta._updatedAt']` → `{ slug, meta: { _updatedAt } }`.\n */\n select?: string[];\n /** Maximum number of items to return. */\n limit?: number;\n /**\n * How many levels deep to return.\n * - 1 = the item's own scalar properties only; nested objects/refs are nulled.\n * - 2 = the item including its direct references; refs inside those are nulled.\n * - 3 = three levels deep, and so on.\n * - Omit to return the full depth.\n */\n include?: number;\n}\n\n/**\n * Trims nested object depth.\n *\n * `remaining` represents how many levels the current object is allowed.\n * - Scalar properties are always kept.\n * - Nested objects / arrays-of-objects consume one level. When `remaining`\n * drops to 1 they are replaced with `null` (no budget left for refs).\n */\nexport function trimDepth(value: unknown, remaining: number): unknown {\n if (value === null || value === undefined || typeof value !== 'object') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => trimDepth(item, remaining));\n }\n\n const obj = value as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n\n for (const [k, v] of Object.entries(obj)) {\n if (v === null || v === undefined || typeof v !== 'object') {\n result[k] = v;\n } else if (remaining <= 1) {\n result[k] = null;\n } else if (Array.isArray(v)) {\n result[k] = v.map((item) => {\n if (item !== null && typeof item === 'object' && !Array.isArray(item)) {\n return trimDepth(item, remaining - 1);\n }\n return item;\n });\n } else {\n result[k] = trimDepth(v, remaining - 1);\n }\n }\n\n return result;\n}\n\n/** Read a value at a dotted path; returns whether every segment existed. */\nfunction getAtPath(\n obj: unknown,\n dottedPath: string,\n): { found: true; value: unknown } | { found: false } {\n const parts = dottedPath.split('.').filter((p) => p.length > 0);\n if (parts.length === 0) return { found: false };\n\n let cur: unknown = obj;\n for (const p of parts) {\n if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) {\n return { found: false };\n }\n const rec = cur as Record<string, unknown>;\n if (!(p in rec)) return { found: false };\n cur = rec[p];\n }\n return { found: true, value: cur };\n}\n\n/** Set `value` on `target` at a dotted path, creating plain objects as needed. */\nfunction setNestedAt(\n target: Record<string, unknown>,\n dottedPath: string,\n value: unknown,\n): void {\n const parts = dottedPath.split('.').filter((p) => p.length > 0);\n if (parts.length === 0) return;\n\n if (parts.length === 1) {\n target[parts[0]!] = value;\n return;\n }\n\n const head = parts[0]!;\n const rest = parts.slice(1).join('.');\n let nested = target[head];\n if (\n nested === null ||\n typeof nested !== 'object' ||\n Array.isArray(nested)\n ) {\n nested = {};\n target[head] = nested;\n }\n setNestedAt(nested as Record<string, unknown>, rest, value);\n}\n\n/**\n * Downloads the latest bundles from S3 and writes them as JSON files to `outputDir`.\n *\n * @returns A map of contentType to absolute file path.\n */\nexport async function fetchBundles(\n store: ContentStore,\n outputDir: string,\n options: FetchBundlesOptions,\n): Promise<Record<string, string>> {\n const { cms, contentTypes } = options;\n await fs.mkdir(outputDir, { recursive: true });\n\n const result:BundleInfo = {};\n\n await Promise.all(\n contentTypes.map(async (contentType) => {\n const key = store.buildLatestKey(cms, contentType);\n const data = await store.download(key);\n const filePath = path.resolve(\n outputDir,\n `${cms}-${contentType}.json`,\n );\n await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');\n result[contentType] = filePath;\n }),\n );\n\n return result;\n}\n\n/**\n * Queries a previously fetched bundle from the local filesystem.\n */\nexport async function queryBundle(\n outputDir: string,\n cms: CMSProvider,\n contentType: string,\n options: QueryOptions = {},\n): Promise<unknown[]> {\n const filePath = path.resolve(\n outputDir,\n `${cms}-${contentType}.json`,\n );\n const raw = await fs.readFile(filePath, 'utf-8');\n let items:BundleItem[] = JSON.parse(raw) as Record<string, unknown>[];\n\n if (options.fields) {\n const filters = options.fields;\n items = items.filter((item) =>\n Object.entries(filters).every(([key, expected]) => {\n const at = getAtPath(item, key);\n const actual = at.found ? at.value : undefined;\n if (Array.isArray(expected)) return expected.includes(actual);\n return actual === expected;\n }),\n );\n }\n\n if (options.limit !== undefined && options.limit > 0) {\n items = items.slice(0, options.limit);\n }\n\n if (options.include !== undefined) {\n items = items.map(\n (item) => trimDepth(item, options.include!) as Record<string, unknown>,\n );\n }\n\n if (options.select?.length) {\n const keys = options.select;\n items = items.map((item) => {\n const picked: Record<string, unknown> = {};\n for (const k of keys) {\n const at = getAtPath(item, k);\n if (at.found) setNestedAt(picked, k, at.value);\n }\n return picked;\n });\n }\n\n return items;\n}\n","import type { S3Config, CMSProvider } from '../shared/types';\nimport { ContentStore } from '../shared/s3';\nimport {\n fetchBundles,\n queryBundle,\n type FetchBundlesOptions,\n type QueryOptions,\n type BundleInfo,\n type BundleItem\n} from '../shared/bundles';\n\nexport type { FetchBundlesOptions, QueryOptions, BundleInfo, BundleItem };\n\nexport interface SDKConfig {\n s3: S3Config;\n /** Directory where bundle JSON files are saved on the local filesystem. */\n outputDir: string;\n}\n\nexport class ContentStoreSDK {\n private store: ContentStore;\n private outputDir: string;\n\n constructor(config: SDKConfig) {\n this.store = new ContentStore(config.s3);\n this.outputDir = config.outputDir;\n }\n\n /**\n * Downloads the latest bundles from S3 and writes them as JSON files\n * to `outputDir`.\n *\n * @returns A map of contentType to absolute file path.\n */\n async fetchBundles(\n options: FetchBundlesOptions,\n ): Promise<Record<string, string>> {\n return fetchBundles(this.store, this.outputDir, options);\n }\n\n /**\n * Queries a previously fetched bundle from the local filesystem.\n */\n async queryBundle(\n cms: CMSProvider,\n contentType: string,\n options: QueryOptions = {},\n ): Promise<unknown[]> {\n return queryBundle(this.outputDir, cms, contentType, options);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { S as SDKConfig, F as FetchBundlesOptions, C as CMSProvider, Q as QueryOptions } from './index-DxoMnE4K.js';
|
|
2
|
+
export { B as BundleInfo, a as BundleItem, b as ContentStore, c as S3Config, f as fetchBundles, q as queryBundle, t as trimDepth } from './index-DxoMnE4K.js';
|
|
3
|
+
|
|
4
|
+
declare class ContentStoreSDK {
|
|
5
|
+
private store;
|
|
6
|
+
private outputDir;
|
|
7
|
+
constructor(config: SDKConfig);
|
|
8
|
+
/**
|
|
9
|
+
* Downloads the latest bundles from S3 and writes them as JSON files
|
|
10
|
+
* to `outputDir`.
|
|
11
|
+
*
|
|
12
|
+
* @returns A map of contentType to absolute file path.
|
|
13
|
+
*/
|
|
14
|
+
fetchBundles(options: FetchBundlesOptions): Promise<Record<string, string>>;
|
|
15
|
+
/**
|
|
16
|
+
* Queries a previously fetched bundle from the local filesystem.
|
|
17
|
+
*/
|
|
18
|
+
queryBundle(cms: CMSProvider, contentType: string, options?: QueryOptions): Promise<unknown[]>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { CMSProvider, ContentStoreSDK, FetchBundlesOptions, QueryOptions, SDKConfig };
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// src/shared/s3.ts
|
|
2
|
+
import {
|
|
3
|
+
S3Client,
|
|
4
|
+
PutObjectCommand,
|
|
5
|
+
CopyObjectCommand,
|
|
6
|
+
GetObjectCommand
|
|
7
|
+
} from "@aws-sdk/client-s3";
|
|
8
|
+
var ContentStore = class {
|
|
9
|
+
client;
|
|
10
|
+
bucket;
|
|
11
|
+
constructor(cfg) {
|
|
12
|
+
this.client = new S3Client({
|
|
13
|
+
region: cfg.region,
|
|
14
|
+
credentials: {
|
|
15
|
+
accessKeyId: cfg.accessKeyId,
|
|
16
|
+
secretAccessKey: cfg.secretAccessKey
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
this.bucket = cfg.bucket;
|
|
20
|
+
}
|
|
21
|
+
/** {cms}-{contentType}-{timestamp}.json */
|
|
22
|
+
buildVersionedKey(cms, contentType, timestamp) {
|
|
23
|
+
return `${cms}-${contentType}-${timestamp}.json`;
|
|
24
|
+
}
|
|
25
|
+
/** {cms}-{contentType}.json (always points at the latest version) */
|
|
26
|
+
buildLatestKey(cms, contentType) {
|
|
27
|
+
return `${cms}-${contentType}.json`;
|
|
28
|
+
}
|
|
29
|
+
async upload(key, data) {
|
|
30
|
+
await this.client.send(
|
|
31
|
+
new PutObjectCommand({
|
|
32
|
+
Bucket: this.bucket,
|
|
33
|
+
Key: key,
|
|
34
|
+
Body: JSON.stringify(data, null, 2),
|
|
35
|
+
ContentType: "application/json"
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
return key;
|
|
39
|
+
}
|
|
40
|
+
async download(key) {
|
|
41
|
+
const response = await this.client.send(
|
|
42
|
+
new GetObjectCommand({ Bucket: this.bucket, Key: key })
|
|
43
|
+
);
|
|
44
|
+
const body = await response.Body?.transformToString();
|
|
45
|
+
if (!body) throw new Error(`Empty response for key: ${key}`);
|
|
46
|
+
return JSON.parse(body);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Copies a versioned object to the "latest" key so that it always reflects
|
|
50
|
+
* the most recent sync while older timestamped versions are retained.
|
|
51
|
+
*/
|
|
52
|
+
async copyToLatest(sourceKey, cms, contentType) {
|
|
53
|
+
const latestKey = this.buildLatestKey(cms, contentType);
|
|
54
|
+
await this.client.send(
|
|
55
|
+
new CopyObjectCommand({
|
|
56
|
+
Bucket: this.bucket,
|
|
57
|
+
CopySource: `${this.bucket}/${sourceKey}`,
|
|
58
|
+
Key: latestKey,
|
|
59
|
+
ContentType: "application/json"
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
return latestKey;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/shared/bundles.ts
|
|
67
|
+
import fs from "fs/promises";
|
|
68
|
+
import path from "path";
|
|
69
|
+
function trimDepth(value, remaining) {
|
|
70
|
+
if (value === null || value === void 0 || typeof value !== "object") {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
return value.map((item) => trimDepth(item, remaining));
|
|
75
|
+
}
|
|
76
|
+
const obj = value;
|
|
77
|
+
const result = {};
|
|
78
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
79
|
+
if (v === null || v === void 0 || typeof v !== "object") {
|
|
80
|
+
result[k] = v;
|
|
81
|
+
} else if (remaining <= 1) {
|
|
82
|
+
result[k] = null;
|
|
83
|
+
} else if (Array.isArray(v)) {
|
|
84
|
+
result[k] = v.map((item) => {
|
|
85
|
+
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
86
|
+
return trimDepth(item, remaining - 1);
|
|
87
|
+
}
|
|
88
|
+
return item;
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
result[k] = trimDepth(v, remaining - 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
function getAtPath(obj, dottedPath) {
|
|
97
|
+
const parts = dottedPath.split(".").filter((p) => p.length > 0);
|
|
98
|
+
if (parts.length === 0) return { found: false };
|
|
99
|
+
let cur = obj;
|
|
100
|
+
for (const p of parts) {
|
|
101
|
+
if (cur === null || typeof cur !== "object" || Array.isArray(cur)) {
|
|
102
|
+
return { found: false };
|
|
103
|
+
}
|
|
104
|
+
const rec = cur;
|
|
105
|
+
if (!(p in rec)) return { found: false };
|
|
106
|
+
cur = rec[p];
|
|
107
|
+
}
|
|
108
|
+
return { found: true, value: cur };
|
|
109
|
+
}
|
|
110
|
+
var FIELD_OP_KEY = /^(.+)\[([^\]]+)\]$/;
|
|
111
|
+
function parseFieldKey(key) {
|
|
112
|
+
const m = FIELD_OP_KEY.exec(key);
|
|
113
|
+
if (!m) return { path: key };
|
|
114
|
+
return { path: m[1], operator: m[2] };
|
|
115
|
+
}
|
|
116
|
+
function isEmptyValue(value) {
|
|
117
|
+
if (value === null || value === void 0) return true;
|
|
118
|
+
if (value === "") return true;
|
|
119
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
120
|
+
if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function matchesFieldFilter(item, key, expected) {
|
|
126
|
+
const { path: path2, operator } = parseFieldKey(key);
|
|
127
|
+
const at = getAtPath(item, path2);
|
|
128
|
+
if (operator === "exists") {
|
|
129
|
+
if (expected !== true && expected !== false) return false;
|
|
130
|
+
const empty = !at.found || isEmptyValue(at.value);
|
|
131
|
+
return expected === false ? empty : !empty;
|
|
132
|
+
}
|
|
133
|
+
const actual = at.found ? at.value : void 0;
|
|
134
|
+
if (Array.isArray(expected)) return expected.includes(actual);
|
|
135
|
+
return actual === expected;
|
|
136
|
+
}
|
|
137
|
+
function setNestedAt(target, dottedPath, value) {
|
|
138
|
+
const parts = dottedPath.split(".").filter((p) => p.length > 0);
|
|
139
|
+
if (parts.length === 0) return;
|
|
140
|
+
if (parts.length === 1) {
|
|
141
|
+
target[parts[0]] = value;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const head = parts[0];
|
|
145
|
+
const rest = parts.slice(1).join(".");
|
|
146
|
+
let nested = target[head];
|
|
147
|
+
if (nested === null || typeof nested !== "object" || Array.isArray(nested)) {
|
|
148
|
+
nested = {};
|
|
149
|
+
target[head] = nested;
|
|
150
|
+
}
|
|
151
|
+
setNestedAt(nested, rest, value);
|
|
152
|
+
}
|
|
153
|
+
async function fetchBundles(store, outputDir, options) {
|
|
154
|
+
const { cms, contentTypes } = options;
|
|
155
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
156
|
+
const result = {};
|
|
157
|
+
await Promise.all(
|
|
158
|
+
contentTypes.map(async (contentType) => {
|
|
159
|
+
const key = store.buildLatestKey(cms, contentType);
|
|
160
|
+
const data = await store.download(key);
|
|
161
|
+
const filePath = path.resolve(
|
|
162
|
+
outputDir,
|
|
163
|
+
`${cms}-${contentType}.json`
|
|
164
|
+
);
|
|
165
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
166
|
+
result[contentType] = filePath;
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
async function queryBundle(outputDir, cms, contentType, options = {}) {
|
|
172
|
+
const filePath = path.resolve(
|
|
173
|
+
outputDir,
|
|
174
|
+
`${cms}-${contentType}.json`
|
|
175
|
+
);
|
|
176
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
177
|
+
let items = JSON.parse(raw);
|
|
178
|
+
if (options.fields) {
|
|
179
|
+
const filters = options.fields;
|
|
180
|
+
items = items.filter(
|
|
181
|
+
(item) => Object.entries(filters).every(
|
|
182
|
+
([key, expected]) => matchesFieldFilter(item, key, expected)
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (options.limit !== void 0 && options.limit > 0) {
|
|
187
|
+
items = items.slice(0, options.limit);
|
|
188
|
+
}
|
|
189
|
+
if (options.include !== void 0) {
|
|
190
|
+
items = items.map(
|
|
191
|
+
(item) => trimDepth(item, options.include)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (options.select?.length) {
|
|
195
|
+
const keys = options.select;
|
|
196
|
+
items = items.map((item) => {
|
|
197
|
+
const picked = {};
|
|
198
|
+
for (const k of keys) {
|
|
199
|
+
const at = getAtPath(item, k);
|
|
200
|
+
if (at.found) setNestedAt(picked, k, at.value);
|
|
201
|
+
}
|
|
202
|
+
return picked;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return items;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/sdk/client.ts
|
|
209
|
+
var ContentStoreSDK = class {
|
|
210
|
+
store;
|
|
211
|
+
outputDir;
|
|
212
|
+
constructor(config) {
|
|
213
|
+
this.store = new ContentStore(config.s3);
|
|
214
|
+
this.outputDir = config.outputDir;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Downloads the latest bundles from S3 and writes them as JSON files
|
|
218
|
+
* to `outputDir`.
|
|
219
|
+
*
|
|
220
|
+
* @returns A map of contentType to absolute file path.
|
|
221
|
+
*/
|
|
222
|
+
async fetchBundles(options) {
|
|
223
|
+
return fetchBundles(this.store, this.outputDir, options);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Queries a previously fetched bundle from the local filesystem.
|
|
227
|
+
*/
|
|
228
|
+
async queryBundle(cms, contentType, options = {}) {
|
|
229
|
+
return queryBundle(this.outputDir, cms, contentType, options);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
export {
|
|
233
|
+
ContentStore,
|
|
234
|
+
ContentStoreSDK,
|
|
235
|
+
fetchBundles,
|
|
236
|
+
queryBundle,
|
|
237
|
+
trimDepth
|
|
238
|
+
};
|
|
239
|
+
//# sourceMappingURL=node.js.map
|
package/dist/node.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/s3.ts","../src/shared/bundles.ts","../src/sdk/client.ts"],"sourcesContent":["import {\n S3Client,\n PutObjectCommand,\n CopyObjectCommand,\n GetObjectCommand,\n} from '@aws-sdk/client-s3';\nimport type { S3Config } from './types';\n\nexport class ContentStore {\n private client: S3Client;\n private bucket: string;\n\n constructor(cfg: S3Config) {\n this.client = new S3Client({\n region: cfg.region,\n credentials: {\n accessKeyId: cfg.accessKeyId,\n secretAccessKey: cfg.secretAccessKey,\n },\n });\n this.bucket = cfg.bucket;\n }\n\n /** {cms}-{contentType}-{timestamp}.json */\n buildVersionedKey(cms: string, contentType: string, timestamp: number): string {\n return `${cms}-${contentType}-${timestamp}.json`;\n }\n\n /** {cms}-{contentType}.json (always points at the latest version) */\n buildLatestKey(cms: string, contentType: string): string {\n return `${cms}-${contentType}.json`;\n }\n\n async upload(key: string, data: unknown): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.bucket,\n Key: key,\n Body: JSON.stringify(data, null, 2),\n ContentType: 'application/json',\n }),\n );\n return key;\n }\n\n async download(key: string): Promise<unknown> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: key }),\n );\n const body = await response.Body?.transformToString();\n if (!body) throw new Error(`Empty response for key: ${key}`);\n return JSON.parse(body);\n }\n\n /**\n * Copies a versioned object to the \"latest\" key so that it always reflects\n * the most recent sync while older timestamped versions are retained.\n */\n async copyToLatest(\n sourceKey: string,\n cms: string,\n contentType: string,\n ): Promise<string> {\n const latestKey = this.buildLatestKey(cms, contentType);\n await this.client.send(\n new CopyObjectCommand({\n Bucket: this.bucket,\n CopySource: `${this.bucket}/${sourceKey}`,\n Key: latestKey,\n ContentType: 'application/json',\n }),\n );\n return latestKey;\n }\n}\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { CMSProvider } from './types';\nimport { ContentStore } from './s3';\n\nexport interface BundleInfo {\n [key: string]: string;\n}\n\nexport interface BundleItem {\n [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any\n}\n\nexport interface FetchBundlesOptions {\n cms: CMSProvider;\n contentTypes: string[];\n}\n\nexport interface QueryOptions {\n /**\n * Filter items by matching property values (exact equality, or array for IN).\n * Keys may use dot notation for nested paths, e.g. `{ 'meta._id': 'abc' }` matches\n * `item.meta._id`.\n *\n * Contentful-style operators on the path (before `[`):\n * - `{ 'field[exists]': true }` — field is present and non-empty (not null, undefined,\n * `''`, `[]`, or `{}`).\n * - `{ 'field[exists]': false }` — field is missing or empty (same emptiness rules).\n * Nested paths work, e.g. `{ 'blocks.hero[exists]': false }`.\n */\n fields?: Record<string, unknown>;\n /**\n * Properties to include in each result object. Omit to return all properties.\n * Keys may use dot notation; nested segments become nested objects in the result,\n * e.g. `['slug', 'meta._updatedAt']` → `{ slug, meta: { _updatedAt } }`.\n */\n select?: string[];\n /** Maximum number of items to return. */\n limit?: number;\n /**\n * How many levels deep to return.\n * - 1 = the item's own scalar properties only; nested objects/refs are nulled.\n * - 2 = the item including its direct references; refs inside those are nulled.\n * - 3 = three levels deep, and so on.\n * - Omit to return the full depth.\n */\n include?: number;\n}\n\n/**\n * Trims nested object depth.\n *\n * `remaining` represents how many levels the current object is allowed.\n * - Scalar properties are always kept.\n * - Nested objects / arrays-of-objects consume one level. When `remaining`\n * drops to 1 they are replaced with `null` (no budget left for refs).\n */\nexport function trimDepth(value: unknown, remaining: number): unknown {\n if (value === null || value === undefined || typeof value !== 'object') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => trimDepth(item, remaining));\n }\n\n const obj = value as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n\n for (const [k, v] of Object.entries(obj)) {\n if (v === null || v === undefined || typeof v !== 'object') {\n result[k] = v;\n } else if (remaining <= 1) {\n result[k] = null;\n } else if (Array.isArray(v)) {\n result[k] = v.map((item) => {\n if (item !== null && typeof item === 'object' && !Array.isArray(item)) {\n return trimDepth(item, remaining - 1);\n }\n return item;\n });\n } else {\n result[k] = trimDepth(v, remaining - 1);\n }\n }\n\n return result;\n}\n\n/** Read a value at a dotted path; returns whether every segment existed. */\nfunction getAtPath(\n obj: unknown,\n dottedPath: string,\n): { found: true; value: unknown } | { found: false } {\n const parts = dottedPath.split('.').filter((p) => p.length > 0);\n if (parts.length === 0) return { found: false };\n\n let cur: unknown = obj;\n for (const p of parts) {\n if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) {\n return { found: false };\n }\n const rec = cur as Record<string, unknown>;\n if (!(p in rec)) return { found: false };\n cur = rec[p];\n }\n return { found: true, value: cur };\n}\n\nconst FIELD_OP_KEY = /^(.+)\\[([^\\]]+)\\]$/;\n\nfunction parseFieldKey(key: string): { path: string; operator?: string } {\n const m = FIELD_OP_KEY.exec(key);\n if (!m) return { path: key };\n return { path: m[1]!, operator: m[2]! };\n}\n\n/** True when a bundle value counts as “no content” (aligned with typical CMS “empty”). */\nfunction isEmptyValue(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (value === '') return true;\n if (Array.isArray(value) && value.length === 0) return true;\n if (\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value).length === 0\n ) {\n return true;\n }\n return false;\n}\n\nfunction matchesFieldFilter(\n item: BundleItem,\n key: string,\n expected: unknown,\n): boolean {\n const { path, operator } = parseFieldKey(key);\n const at = getAtPath(item, path);\n\n if (operator === 'exists') {\n if (expected !== true && expected !== false) return false;\n const empty = !at.found || isEmptyValue(at.value);\n return expected === false ? empty : !empty;\n }\n\n const actual = at.found ? at.value : undefined;\n if (Array.isArray(expected)) return expected.includes(actual);\n return actual === expected;\n}\n\n/** Set `value` on `target` at a dotted path, creating plain objects as needed. */\nfunction setNestedAt(\n target: Record<string, unknown>,\n dottedPath: string,\n value: unknown,\n): void {\n const parts = dottedPath.split('.').filter((p) => p.length > 0);\n if (parts.length === 0) return;\n\n if (parts.length === 1) {\n target[parts[0]!] = value;\n return;\n }\n\n const head = parts[0]!;\n const rest = parts.slice(1).join('.');\n let nested = target[head];\n if (\n nested === null ||\n typeof nested !== 'object' ||\n Array.isArray(nested)\n ) {\n nested = {};\n target[head] = nested;\n }\n setNestedAt(nested as Record<string, unknown>, rest, value);\n}\n\n/**\n * Downloads the latest bundles from S3 and writes them as JSON files to `outputDir`.\n *\n * @returns A map of contentType to absolute file path.\n */\nexport async function fetchBundles(\n store: ContentStore,\n outputDir: string,\n options: FetchBundlesOptions,\n): Promise<Record<string, string>> {\n const { cms, contentTypes } = options;\n await fs.mkdir(outputDir, { recursive: true });\n\n const result:BundleInfo = {};\n\n await Promise.all(\n contentTypes.map(async (contentType) => {\n const key = store.buildLatestKey(cms, contentType);\n const data = await store.download(key);\n const filePath = path.resolve(\n outputDir,\n `${cms}-${contentType}.json`,\n );\n await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');\n result[contentType] = filePath;\n }),\n );\n\n return result;\n}\n\n/**\n * Queries a previously fetched bundle from the local filesystem.\n */\nexport async function queryBundle(\n outputDir: string,\n cms: CMSProvider,\n contentType: string,\n options: QueryOptions = {},\n): Promise<unknown[]> {\n const filePath = path.resolve(\n outputDir,\n `${cms}-${contentType}.json`,\n );\n const raw = await fs.readFile(filePath, 'utf-8');\n let items:BundleItem[] = JSON.parse(raw) as Record<string, unknown>[];\n\n if (options.fields) {\n const filters = options.fields;\n items = items.filter((item) =>\n Object.entries(filters).every(([key, expected]) =>\n matchesFieldFilter(item, key, expected),\n ),\n );\n }\n\n if (options.limit !== undefined && options.limit > 0) {\n items = items.slice(0, options.limit);\n }\n\n if (options.include !== undefined) {\n items = items.map(\n (item) => trimDepth(item, options.include!) as Record<string, unknown>,\n );\n }\n\n if (options.select?.length) {\n const keys = options.select;\n items = items.map((item) => {\n const picked: Record<string, unknown> = {};\n for (const k of keys) {\n const at = getAtPath(item, k);\n if (at.found) setNestedAt(picked, k, at.value);\n }\n return picked;\n });\n }\n\n return items;\n}\n","import type { CMSProvider } from '../shared/types';\nimport { ContentStore } from '../shared/s3';\nimport {\n fetchBundles,\n queryBundle,\n type FetchBundlesOptions,\n type QueryOptions,\n} from '../shared/bundles';\nimport type { SDKConfig } from './client-types';\n\nexport type {\n FetchBundlesOptions,\n QueryOptions,\n BundleInfo,\n BundleItem,\n SDKConfig,\n} from './client-types';\n\nexport class ContentStoreSDK {\n private store: ContentStore;\n private outputDir: string;\n\n constructor(config: SDKConfig) {\n this.store = new ContentStore(config.s3);\n this.outputDir = config.outputDir;\n }\n\n /**\n * Downloads the latest bundles from S3 and writes them as JSON files\n * to `outputDir`.\n *\n * @returns A map of contentType to absolute file path.\n */\n async fetchBundles(\n options: FetchBundlesOptions,\n ): Promise<Record<string, string>> {\n return fetchBundles(this.store, this.outputDir, options);\n }\n\n /**\n * Queries a previously fetched bundle from the local filesystem.\n */\n async queryBundle(\n cms: CMSProvider,\n contentType: string,\n options: QueryOptions = {},\n ): Promise<unknown[]> {\n return queryBundle(this.outputDir, cms, contentType, options);\n }\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGA,IAAM,eAAN,MAAmB;AAAA,EAChB;AAAA,EACA;AAAA,EAER,YAAY,KAAe;AACzB,SAAK,SAAS,IAAI,SAAS;AAAA,MACzB,QAAQ,IAAI;AAAA,MACZ,aAAa;AAAA,QACX,aAAa,IAAI;AAAA,QACjB,iBAAiB,IAAI;AAAA,MACvB;AAAA,IACF,CAAC;AACD,SAAK,SAAS,IAAI;AAAA,EACpB;AAAA;AAAA,EAGA,kBAAkB,KAAa,aAAqB,WAA2B;AAC7E,WAAO,GAAG,GAAG,IAAI,WAAW,IAAI,SAAS;AAAA,EAC3C;AAAA;AAAA,EAGA,eAAe,KAAa,aAA6B;AACvD,WAAO,GAAG,GAAG,IAAI,WAAW;AAAA,EAC9B;AAAA,EAEA,MAAM,OAAO,KAAa,MAAgC;AACxD,UAAM,KAAK,OAAO;AAAA,MAChB,IAAI,iBAAiB;AAAA,QACnB,QAAQ,KAAK;AAAA,QACb,KAAK;AAAA,QACL,MAAM,KAAK,UAAU,MAAM,MAAM,CAAC;AAAA,QAClC,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAS,KAA+B;AAC5C,UAAM,WAAW,MAAM,KAAK,OAAO;AAAA,MACjC,IAAI,iBAAiB,EAAE,QAAQ,KAAK,QAAQ,KAAK,IAAI,CAAC;AAAA,IACxD;AACA,UAAM,OAAO,MAAM,SAAS,MAAM,kBAAkB;AACpD,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,2BAA2B,GAAG,EAAE;AAC3D,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,aACJ,WACA,KACA,aACiB;AACjB,UAAM,YAAY,KAAK,eAAe,KAAK,WAAW;AACtD,UAAM,KAAK,OAAO;AAAA,MAChB,IAAI,kBAAkB;AAAA,QACpB,QAAQ,KAAK;AAAA,QACb,YAAY,GAAG,KAAK,MAAM,IAAI,SAAS;AAAA,QACvC,KAAK;AAAA,QACL,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AACF;;;AC1EA,OAAO,QAAQ;AACf,OAAO,UAAU;AAwDV,SAAS,UAAU,OAAgB,WAA4B;AACpE,MAAI,UAAU,QAAQ,UAAU,UAAa,OAAO,UAAU,UAAU;AACtE,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,CAAC,SAAS,UAAU,MAAM,SAAS,CAAC;AAAA,EACvD;AAEA,QAAM,MAAM;AACZ,QAAM,SAAkC,CAAC;AAEzC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,GAAG;AACxC,QAAI,MAAM,QAAQ,MAAM,UAAa,OAAO,MAAM,UAAU;AAC1D,aAAO,CAAC,IAAI;AAAA,IACd,WAAW,aAAa,GAAG;AACzB,aAAO,CAAC,IAAI;AAAA,IACd,WAAW,MAAM,QAAQ,CAAC,GAAG;AAC3B,aAAO,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS;AAC1B,YAAI,SAAS,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,GAAG;AACrE,iBAAO,UAAU,MAAM,YAAY,CAAC;AAAA,QACtC;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH,OAAO;AACL,aAAO,CAAC,IAAI,UAAU,GAAG,YAAY,CAAC;AAAA,IACxC;AAAA,EACF;AAEA,SAAO;AACT;AAGA,SAAS,UACP,KACA,YACoD;AACpD,QAAM,QAAQ,WAAW,MAAM,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC9D,MAAI,MAAM,WAAW,EAAG,QAAO,EAAE,OAAO,MAAM;AAE9C,MAAI,MAAe;AACnB,aAAW,KAAK,OAAO;AACrB,QAAI,QAAQ,QAAQ,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACjE,aAAO,EAAE,OAAO,MAAM;AAAA,IACxB;AACA,UAAM,MAAM;AACZ,QAAI,EAAE,KAAK,KAAM,QAAO,EAAE,OAAO,MAAM;AACvC,UAAM,IAAI,CAAC;AAAA,EACb;AACA,SAAO,EAAE,OAAO,MAAM,OAAO,IAAI;AACnC;AAEA,IAAM,eAAe;AAErB,SAAS,cAAc,KAAkD;AACvE,QAAM,IAAI,aAAa,KAAK,GAAG;AAC/B,MAAI,CAAC,EAAG,QAAO,EAAE,MAAM,IAAI;AAC3B,SAAO,EAAE,MAAM,EAAE,CAAC,GAAI,UAAU,EAAE,CAAC,EAAG;AACxC;AAGA,SAAS,aAAa,OAAyB;AAC7C,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,UAAU,GAAI,QAAO;AACzB,MAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,EAAG,QAAO;AACvD,MACE,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,KAAK,KAAK,EAAE,WAAW,GAC9B;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,mBACP,MACA,KACA,UACS;AACT,QAAM,EAAE,MAAAA,OAAM,SAAS,IAAI,cAAc,GAAG;AAC5C,QAAM,KAAK,UAAU,MAAMA,KAAI;AAE/B,MAAI,aAAa,UAAU;AACzB,QAAI,aAAa,QAAQ,aAAa,MAAO,QAAO;AACpD,UAAM,QAAQ,CAAC,GAAG,SAAS,aAAa,GAAG,KAAK;AAChD,WAAO,aAAa,QAAQ,QAAQ,CAAC;AAAA,EACvC;AAEA,QAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ;AACrC,MAAI,MAAM,QAAQ,QAAQ,EAAG,QAAO,SAAS,SAAS,MAAM;AAC5D,SAAO,WAAW;AACpB;AAGA,SAAS,YACP,QACA,YACA,OACM;AACN,QAAM,QAAQ,WAAW,MAAM,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC9D,MAAI,MAAM,WAAW,EAAG;AAExB,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,MAAM,CAAC,CAAE,IAAI;AACpB;AAAA,EACF;AAEA,QAAM,OAAO,MAAM,CAAC;AACpB,QAAM,OAAO,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG;AACpC,MAAI,SAAS,OAAO,IAAI;AACxB,MACE,WAAW,QACX,OAAO,WAAW,YAClB,MAAM,QAAQ,MAAM,GACpB;AACA,aAAS,CAAC;AACV,WAAO,IAAI,IAAI;AAAA,EACjB;AACA,cAAY,QAAmC,MAAM,KAAK;AAC5D;AAOA,eAAsB,aACpB,OACA,WACA,SACiC;AACjC,QAAM,EAAE,KAAK,aAAa,IAAI;AAC9B,QAAM,GAAG,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;AAE7C,QAAM,SAAoB,CAAC;AAE3B,QAAM,QAAQ;AAAA,IACZ,aAAa,IAAI,OAAO,gBAAgB;AACtC,YAAM,MAAM,MAAM,eAAe,KAAK,WAAW;AACjD,YAAM,OAAO,MAAM,MAAM,SAAS,GAAG;AACrC,YAAM,WAAW,KAAK;AAAA,QACpB;AAAA,QACA,GAAG,GAAG,IAAI,WAAW;AAAA,MACvB;AACA,YAAM,GAAG,UAAU,UAAU,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,OAAO;AACnE,aAAO,WAAW,IAAI;AAAA,IACxB,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAKA,eAAsB,YACpB,WACA,KACA,aACA,UAAwB,CAAC,GACL;AACpB,QAAM,WAAW,KAAK;AAAA,IACpB;AAAA,IACA,GAAG,GAAG,IAAI,WAAW;AAAA,EACvB;AACA,QAAM,MAAM,MAAM,GAAG,SAAS,UAAU,OAAO;AAC/C,MAAI,QAAqB,KAAK,MAAM,GAAG;AAEvC,MAAI,QAAQ,QAAQ;AAClB,UAAM,UAAU,QAAQ;AACxB,YAAQ,MAAM;AAAA,MAAO,CAAC,SACpB,OAAO,QAAQ,OAAO,EAAE;AAAA,QAAM,CAAC,CAAC,KAAK,QAAQ,MAC3C,mBAAmB,MAAM,KAAK,QAAQ;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AAEA,MAAI,QAAQ,UAAU,UAAa,QAAQ,QAAQ,GAAG;AACpD,YAAQ,MAAM,MAAM,GAAG,QAAQ,KAAK;AAAA,EACtC;AAEA,MAAI,QAAQ,YAAY,QAAW;AACjC,YAAQ,MAAM;AAAA,MACZ,CAAC,SAAS,UAAU,MAAM,QAAQ,OAAQ;AAAA,IAC5C;AAAA,EACF;AAEA,MAAI,QAAQ,QAAQ,QAAQ;AAC1B,UAAM,OAAO,QAAQ;AACrB,YAAQ,MAAM,IAAI,CAAC,SAAS;AAC1B,YAAM,SAAkC,CAAC;AACzC,iBAAW,KAAK,MAAM;AACpB,cAAM,KAAK,UAAU,MAAM,CAAC;AAC5B,YAAI,GAAG,MAAO,aAAY,QAAQ,GAAG,GAAG,KAAK;AAAA,MAC/C;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,SAAO;AACT;;;AChPO,IAAM,kBAAN,MAAsB;AAAA,EACnB;AAAA,EACA;AAAA,EAER,YAAY,QAAmB;AAC7B,SAAK,QAAQ,IAAI,aAAa,OAAO,EAAE;AACvC,SAAK,YAAY,OAAO;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,aACJ,SACiC;AACjC,WAAO,aAAa,KAAK,OAAO,KAAK,WAAW,OAAO;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YACJ,KACA,aACA,UAAwB,CAAC,GACL;AACpB,WAAO,YAAY,KAAK,WAAW,KAAK,aAAa,OAAO;AAAA,EAC9D;AACF;","names":["path"]}
|
package/node.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/node.js';
|
package/node.js
ADDED
package/package.json
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tandem-language-exchange/content-store",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
9
10
|
"import": "./dist/index.js",
|
|
10
|
-
"
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./node": {
|
|
14
|
+
"types": "./node.d.ts",
|
|
15
|
+
"import": "./node.js",
|
|
16
|
+
"default": "./node.js"
|
|
11
17
|
}
|
|
12
18
|
},
|
|
13
19
|
"files": [
|
|
20
|
+
"node.js",
|
|
21
|
+
"node.d.ts",
|
|
14
22
|
"dist/index.*",
|
|
23
|
+
"dist/index-*.d.ts",
|
|
24
|
+
"dist/node.*",
|
|
15
25
|
"dist/sdk/",
|
|
16
26
|
"dist/client/",
|
|
17
27
|
"dist/shared/",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client/config.ts","../src/shared/bundles.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 ClientConfig {\n}\n\nexport const config: ClientConfig & SharedConfig = {\n ...sharedConfig\n};\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { CMSProvider } from './types';\nimport { ContentStore } from './s3';\n\nexport interface BundleInfo {\n [key: string]: string;\n}\n\nexport interface BundleItem {\n [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any\n}\n\nexport interface FetchBundlesOptions {\n cms: CMSProvider;\n contentTypes: string[];\n}\n\nexport interface QueryOptions {\n /**\n * Filter items by matching property values (exact equality, or array for IN).\n * Keys may use dot notation for nested paths, e.g. `{ 'meta._id': 'abc' }` matches\n * `item.meta._id`.\n */\n fields?: Record<string, unknown>;\n /**\n * Properties to include in each result object. Omit to return all properties.\n * Keys may use dot notation; nested segments become nested objects in the result,\n * e.g. `['slug', 'meta._updatedAt']` → `{ slug, meta: { _updatedAt } }`.\n */\n select?: string[];\n /** Maximum number of items to return. */\n limit?: number;\n /**\n * How many levels deep to return.\n * - 1 = the item's own scalar properties only; nested objects/refs are nulled.\n * - 2 = the item including its direct references; refs inside those are nulled.\n * - 3 = three levels deep, and so on.\n * - Omit to return the full depth.\n */\n include?: number;\n}\n\n/**\n * Trims nested object depth.\n *\n * `remaining` represents how many levels the current object is allowed.\n * - Scalar properties are always kept.\n * - Nested objects / arrays-of-objects consume one level. When `remaining`\n * drops to 1 they are replaced with `null` (no budget left for refs).\n */\nexport function trimDepth(value: unknown, remaining: number): unknown {\n if (value === null || value === undefined || typeof value !== 'object') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => trimDepth(item, remaining));\n }\n\n const obj = value as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n\n for (const [k, v] of Object.entries(obj)) {\n if (v === null || v === undefined || typeof v !== 'object') {\n result[k] = v;\n } else if (remaining <= 1) {\n result[k] = null;\n } else if (Array.isArray(v)) {\n result[k] = v.map((item) => {\n if (item !== null && typeof item === 'object' && !Array.isArray(item)) {\n return trimDepth(item, remaining - 1);\n }\n return item;\n });\n } else {\n result[k] = trimDepth(v, remaining - 1);\n }\n }\n\n return result;\n}\n\n/** Read a value at a dotted path; returns whether every segment existed. */\nfunction getAtPath(\n obj: unknown,\n dottedPath: string,\n): { found: true; value: unknown } | { found: false } {\n const parts = dottedPath.split('.').filter((p) => p.length > 0);\n if (parts.length === 0) return { found: false };\n\n let cur: unknown = obj;\n for (const p of parts) {\n if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) {\n return { found: false };\n }\n const rec = cur as Record<string, unknown>;\n if (!(p in rec)) return { found: false };\n cur = rec[p];\n }\n return { found: true, value: cur };\n}\n\n/** Set `value` on `target` at a dotted path, creating plain objects as needed. */\nfunction setNestedAt(\n target: Record<string, unknown>,\n dottedPath: string,\n value: unknown,\n): void {\n const parts = dottedPath.split('.').filter((p) => p.length > 0);\n if (parts.length === 0) return;\n\n if (parts.length === 1) {\n target[parts[0]!] = value;\n return;\n }\n\n const head = parts[0]!;\n const rest = parts.slice(1).join('.');\n let nested = target[head];\n if (\n nested === null ||\n typeof nested !== 'object' ||\n Array.isArray(nested)\n ) {\n nested = {};\n target[head] = nested;\n }\n setNestedAt(nested as Record<string, unknown>, rest, value);\n}\n\n/**\n * Downloads the latest bundles from S3 and writes them as JSON files to `outputDir`.\n *\n * @returns A map of contentType to absolute file path.\n */\nexport async function fetchBundles(\n store: ContentStore,\n outputDir: string,\n options: FetchBundlesOptions,\n): Promise<Record<string, string>> {\n const { cms, contentTypes } = options;\n await fs.mkdir(outputDir, { recursive: true });\n\n const result:BundleInfo = {};\n\n await Promise.all(\n contentTypes.map(async (contentType) => {\n const key = store.buildLatestKey(cms, contentType);\n const data = await store.download(key);\n const filePath = path.resolve(\n outputDir,\n `${cms}-${contentType}.json`,\n );\n await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');\n result[contentType] = filePath;\n }),\n );\n\n return result;\n}\n\n/**\n * Queries a previously fetched bundle from the local filesystem.\n */\nexport async function queryBundle(\n outputDir: string,\n cms: CMSProvider,\n contentType: string,\n options: QueryOptions = {},\n): Promise<unknown[]> {\n const filePath = path.resolve(\n outputDir,\n `${cms}-${contentType}.json`,\n );\n const raw = await fs.readFile(filePath, 'utf-8');\n let items:BundleItem[] = JSON.parse(raw) as Record<string, unknown>[];\n\n if (options.fields) {\n const filters = options.fields;\n items = items.filter((item) =>\n Object.entries(filters).every(([key, expected]) => {\n const at = getAtPath(item, key);\n const actual = at.found ? at.value : undefined;\n if (Array.isArray(expected)) return expected.includes(actual);\n return actual === expected;\n }),\n );\n }\n\n if (options.limit !== undefined && options.limit > 0) {\n items = items.slice(0, options.limit);\n }\n\n if (options.include !== undefined) {\n items = items.map(\n (item) => trimDepth(item, options.include!) as Record<string, unknown>,\n );\n }\n\n if (options.select?.length) {\n const keys = options.select;\n items = items.map((item) => {\n const picked: Record<string, unknown> = {};\n for (const k of keys) {\n const at = getAtPath(item, k);\n if (at.found) setNestedAt(picked, k, at.value);\n }\n return picked;\n });\n }\n\n return items;\n}\n"],"mappings":"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","names":["config"]}
|