@tandem-language-exchange/content-store 1.0.13 → 1.0.15

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.
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ContentStore,
4
4
  config
5
- } from "./chunk-DPWIBUHQ.js";
5
+ } from "./chunk-UWGOF36L.js";
6
6
 
7
7
  // src/server/config.ts
8
8
  import dotenv from "dotenv";
@@ -269,4 +269,4 @@ export {
269
269
  config2 as config,
270
270
  runSync
271
271
  };
272
- //# sourceMappingURL=chunk-QH4EH2NU.js.map
272
+ //# sourceMappingURL=chunk-ORHMX5UP.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  config
4
- } from "./chunk-DPWIBUHQ.js";
4
+ } from "./chunk-UWGOF36L.js";
5
5
 
6
6
  // src/client/config.ts
7
7
  import dotenv from "dotenv";
@@ -51,7 +51,7 @@ async function fetchBundles(store, outputDir, options) {
51
51
  const data = await store.download(key);
52
52
  const filePath = path.resolve(
53
53
  outputDir,
54
- `content-${cms}-${contentType}.json`
54
+ `${cms}-${contentType}.json`
55
55
  );
56
56
  await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
57
57
  result[contentType] = filePath;
@@ -62,7 +62,7 @@ async function fetchBundles(store, outputDir, options) {
62
62
  async function queryBundle(outputDir, cms, contentType, options = {}) {
63
63
  const filePath = path.resolve(
64
64
  outputDir,
65
- `content-${cms}-${contentType}.json`
65
+ `${cms}-${contentType}.json`
66
66
  );
67
67
  const raw = await fs.readFile(filePath, "utf-8");
68
68
  let items = JSON.parse(raw);
@@ -102,4 +102,4 @@ export {
102
102
  fetchBundles,
103
103
  queryBundle
104
104
  };
105
- //# sourceMappingURL=chunk-R6THP5E4.js.map
105
+ //# sourceMappingURL=chunk-U5SXPLFL.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 FetchBundlesOptions {\n cms: CMSProvider;\n contentTypes: string[];\n}\n\nexport interface QueryOptions {\n /** Filter items by matching top-level property values (exact equality, or array for IN). */\n fields?: Record<string, unknown>;\n /** Properties to include in each result object. Omit to return all properties. */\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/**\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: Record<string, string> = {};\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 = 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 actual = item[key];\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 if (k in item) picked[k] = item[k];\n }\n return picked;\n });\n }\n\n return items;\n}\n"],"mappings":"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","names":["config"]}
@@ -20,13 +20,13 @@ var ContentStore = class {
20
20
  });
21
21
  this.bucket = cfg.bucket;
22
22
  }
23
- /** content-{cms}-{contentType}-{timestamp}.json */
23
+ /** {cms}-{contentType}-{timestamp}.json */
24
24
  buildVersionedKey(cms, contentType, timestamp) {
25
- return `content-${cms}-${contentType}-${timestamp}.json`;
25
+ return `${cms}-${contentType}-${timestamp}.json`;
26
26
  }
27
- /** content-{cms}-{contentType}.json (always points at the latest version) */
27
+ /** {cms}-{contentType}.json (always points at the latest version) */
28
28
  buildLatestKey(cms, contentType) {
29
- return `content-${cms}-${contentType}.json`;
29
+ return `${cms}-${contentType}.json`;
30
30
  }
31
31
  async upload(key, data) {
32
32
  await this.client.send(
@@ -82,4 +82,4 @@ export {
82
82
  config,
83
83
  ContentStore
84
84
  };
85
- //# sourceMappingURL=chunk-DPWIBUHQ.js.map
85
+ //# sourceMappingURL=chunk-UWGOF36L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/s3.ts","../src/shared/config.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 dotenv from 'dotenv';\nimport type { S3Config, CMSProvider } from './types';\n\ndotenv.config({ path: '.env.local' });\ndotenv.config();\n\nexport type { CMSProvider, S3Config };\n\nexport interface SharedConfig {\n s3: S3Config;\n}\n\nexport const config: SharedConfig = {\n s3: {\n bucket: process.env.CONTENT_STORE_S3_BUCKET ?? '',\n region: process.env.CONTENT_STORE_S3_REGION ?? 'eu-central-1',\n accessKeyId: process.env.AWS_ACCESS_KEY ?? '',\n secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',\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,YAAY;AAGnB,OAAO,OAAO,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,OAAO;AAQP,IAAM,SAAuB;AAAA,EAChC,IAAI;AAAA,IACA,QAAQ,QAAQ,IAAI,2BAA2B;AAAA,IAC/C,QAAQ,QAAQ,IAAI,2BAA2B;AAAA,IAC/C,aAAa,QAAQ,IAAI,kBAAkB;AAAA,IAC3C,iBAAiB,QAAQ,IAAI,yBAAyB;AAAA,EAC1D;AACJ;","names":[]}
@@ -3,10 +3,10 @@ import {
3
3
  config,
4
4
  fetchBundles,
5
5
  queryBundle
6
- } from "../chunk-R6THP5E4.js";
6
+ } from "../chunk-U5SXPLFL.js";
7
7
  import {
8
8
  ContentStore
9
- } from "../chunk-DPWIBUHQ.js";
9
+ } from "../chunk-UWGOF36L.js";
10
10
 
11
11
  // src/client/cli.ts
12
12
  import { Command } from "commander";
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  config,
4
4
  fetchBundles
5
- } from "../chunk-R6THP5E4.js";
5
+ } from "../chunk-U5SXPLFL.js";
6
6
  import {
7
7
  ContentStore
8
- } from "../chunk-DPWIBUHQ.js";
8
+ } from "../chunk-UWGOF36L.js";
9
9
 
10
10
  // src/client/fetch-bundles.ts
11
11
  import { Command } from "commander";
package/dist/index.d.ts CHANGED
@@ -10,9 +10,9 @@ declare class ContentStore {
10
10
  private client;
11
11
  private bucket;
12
12
  constructor(cfg: S3Config);
13
- /** content-{cms}-{contentType}-{timestamp}.json */
13
+ /** {cms}-{contentType}-{timestamp}.json */
14
14
  buildVersionedKey(cms: string, contentType: string, timestamp: number): string;
15
- /** content-{cms}-{contentType}.json (always points at the latest version) */
15
+ /** {cms}-{contentType}.json (always points at the latest version) */
16
16
  buildLatestKey(cms: string, contentType: string): string;
17
17
  upload(key: string, data: unknown): Promise<string>;
18
18
  download(key: string): Promise<unknown>;
package/dist/index.js CHANGED
@@ -18,13 +18,13 @@ var ContentStore = class {
18
18
  });
19
19
  this.bucket = cfg.bucket;
20
20
  }
21
- /** content-{cms}-{contentType}-{timestamp}.json */
21
+ /** {cms}-{contentType}-{timestamp}.json */
22
22
  buildVersionedKey(cms, contentType, timestamp) {
23
- return `content-${cms}-${contentType}-${timestamp}.json`;
23
+ return `${cms}-${contentType}-${timestamp}.json`;
24
24
  }
25
- /** content-{cms}-{contentType}.json (always points at the latest version) */
25
+ /** {cms}-{contentType}.json (always points at the latest version) */
26
26
  buildLatestKey(cms, contentType) {
27
- return `content-${cms}-${contentType}.json`;
27
+ return `${cms}-${contentType}.json`;
28
28
  }
29
29
  async upload(key, data) {
30
30
  await this.client.send(
@@ -103,7 +103,7 @@ async function fetchBundles(store, outputDir, options) {
103
103
  const data = await store.download(key);
104
104
  const filePath = path.resolve(
105
105
  outputDir,
106
- `content-${cms}-${contentType}.json`
106
+ `${cms}-${contentType}.json`
107
107
  );
108
108
  await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
109
109
  result[contentType] = filePath;
@@ -114,7 +114,7 @@ async function fetchBundles(store, outputDir, options) {
114
114
  async function queryBundle(outputDir, cms, contentType, options = {}) {
115
115
  const filePath = path.resolve(
116
116
  outputDir,
117
- `content-${cms}-${contentType}.json`
117
+ `${cms}-${contentType}.json`
118
118
  );
119
119
  const raw = await fs.readFile(filePath, "utf-8");
120
120
  let items = JSON.parse(raw);
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 /** content-{cms}-{contentType}-{timestamp}.json */\n buildVersionedKey(cms: string, contentType: string, timestamp: number): string {\n return `content-${cms}-${contentType}-${timestamp}.json`;\n }\n\n /** content-{cms}-{contentType}.json (always points at the latest version) */\n buildLatestKey(cms: string, contentType: string): string {\n return `content-${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 FetchBundlesOptions {\n cms: CMSProvider;\n contentTypes: string[];\n}\n\nexport interface QueryOptions {\n /** Filter items by matching top-level property values (exact equality, or array for IN). */\n fields?: Record<string, unknown>;\n /** Properties to include in each result object. Omit to return all properties. */\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/**\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: Record<string, string> = {};\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 `content-${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 `content-${cms}-${contentType}.json`,\n );\n const raw = await fs.readFile(filePath, 'utf-8');\n let items = 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 actual = item[key];\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 if (k in item) picked[k] = item[k];\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} from '../shared/bundles';\n\nexport type { FetchBundlesOptions, QueryOptions };\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":["../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 FetchBundlesOptions {\n cms: CMSProvider;\n contentTypes: string[];\n}\n\nexport interface QueryOptions {\n /** Filter items by matching top-level property values (exact equality, or array for IN). */\n fields?: Record<string, unknown>;\n /** Properties to include in each result object. Omit to return all properties. */\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/**\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: Record<string, string> = {};\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 = 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 actual = item[key];\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 if (k in item) picked[k] = item[k];\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} from '../shared/bundles';\n\nexport type { FetchBundlesOptions, QueryOptions };\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tandem-language-exchange/content-store",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/shared/s3.ts","../src/shared/config.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 /** content-{cms}-{contentType}-{timestamp}.json */\n buildVersionedKey(cms: string, contentType: string, timestamp: number): string {\n return `content-${cms}-${contentType}-${timestamp}.json`;\n }\n\n /** content-{cms}-{contentType}.json (always points at the latest version) */\n buildLatestKey(cms: string, contentType: string): string {\n return `content-${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 dotenv from 'dotenv';\nimport type { S3Config, CMSProvider } from './types';\n\ndotenv.config({ path: '.env.local' });\ndotenv.config();\n\nexport type { CMSProvider, S3Config };\n\nexport interface SharedConfig {\n s3: S3Config;\n}\n\nexport const config: SharedConfig = {\n s3: {\n bucket: process.env.CONTENT_STORE_S3_BUCKET ?? '',\n region: process.env.CONTENT_STORE_S3_REGION ?? 'eu-central-1',\n accessKeyId: process.env.AWS_ACCESS_KEY ?? '',\n secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',\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,WAAW,GAAG,IAAI,WAAW,IAAI,SAAS;AAAA,EACnD;AAAA;AAAA,EAGA,eAAe,KAAa,aAA6B;AACvD,WAAO,WAAW,GAAG,IAAI,WAAW;AAAA,EACtC;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,YAAY;AAGnB,OAAO,OAAO,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,OAAO;AAQP,IAAM,SAAuB;AAAA,EAChC,IAAI;AAAA,IACA,QAAQ,QAAQ,IAAI,2BAA2B;AAAA,IAC/C,QAAQ,QAAQ,IAAI,2BAA2B;AAAA,IAC/C,aAAa,QAAQ,IAAI,kBAAkB;AAAA,IAC3C,iBAAiB,QAAQ,IAAI,yBAAyB;AAAA,EAC1D;AACJ;","names":[]}
@@ -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 FetchBundlesOptions {\n cms: CMSProvider;\n contentTypes: string[];\n}\n\nexport interface QueryOptions {\n /** Filter items by matching top-level property values (exact equality, or array for IN). */\n fields?: Record<string, unknown>;\n /** Properties to include in each result object. Omit to return all properties. */\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/**\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: Record<string, string> = {};\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 `content-${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 `content-${cms}-${contentType}.json`,\n );\n const raw = await fs.readFile(filePath, 'utf-8');\n let items = 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 actual = item[key];\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 if (k in item) picked[k] = item[k];\n }\n return picked;\n });\n }\n\n return items;\n}\n"],"mappings":"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","names":["config"]}