@unchainedshop/cockpit-api 2.1.2 → 2.2.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 CHANGED
@@ -103,13 +103,17 @@ const localizedPost = await cockpit.getContentItem({
103
103
  queryParams: { fields: { title: 1, content: 1 } }
104
104
  });
105
105
 
106
- // Get multiple content items with pagination
107
- const posts = await cockpit.getContentItems('posts', {
106
+ // Get multiple content items - always returns { data, meta? }
107
+ const response = await cockpit.getContentItems('posts', {
108
108
  limit: 10,
109
- skip: 0,
110
109
  sort: { _created: -1 },
111
110
  filter: { published: true }
112
111
  });
112
+ // response: { data: Post[], meta?: { total: number } } | null
113
+
114
+ // Access items and metadata
115
+ const items = response?.data || [];
116
+ const total = response?.meta?.total;
113
117
 
114
118
  // Get tree structure
115
119
  const tree = await cockpit.getContentTree('categories', {
@@ -133,8 +137,10 @@ await cockpit.deleteContentItem('posts', '123');
133
137
  ### Pages
134
138
 
135
139
  ```typescript
136
- // List pages
137
- const allPages = await cockpit.pages({ locale: 'en', limit: 50 });
140
+ // List pages - always returns { data, meta? }
141
+ const response = await cockpit.pages({ locale: 'en', limit: 50 });
142
+ const allPages = response?.data || [];
143
+ const total = response?.meta?.total;
138
144
 
139
145
  // Get page by ID
140
146
  const page = await cockpit.pageById({ page: 'blog', id: '123', locale: 'en' });
@@ -206,9 +212,9 @@ const image = await cockpit.imageAssetById('asset-id', {
206
212
  // Health check
207
213
  const health = await cockpit.healthCheck();
208
214
 
209
- // Clear cache
210
- cockpit.clearCache(); // Clear all
211
- cockpit.clearCache('pages'); // Clear by pattern
215
+ // Clear cache (async in v2.2.0+)
216
+ await cockpit.clearCache(); // Clear all
217
+ await cockpit.clearCache('pages'); // Clear by pattern
212
218
  ```
213
219
 
214
220
  ## Lightweight Fetch Client API
@@ -228,9 +234,16 @@ const cockpit = createFetchClient({
228
234
 
229
235
  // Available methods
230
236
  const page = await cockpit.pageByRoute('/about', { locale: 'en' });
231
- const pages = await cockpit.pages({ locale: 'en' });
237
+
238
+ // List methods return { data, meta? }
239
+ const pagesResponse = await cockpit.pages({ locale: 'en' });
240
+ const pages = pagesResponse?.data || [];
241
+
232
242
  const pageById = await cockpit.pageById('blog', '123', { locale: 'en' });
233
- const items = await cockpit.getContentItems('news', { locale: 'en', limit: 10 });
243
+
244
+ const itemsResponse = await cockpit.getContentItems('news', { locale: 'en', limit: 10 });
245
+ const items = itemsResponse?.data || [];
246
+
234
247
  const item = await cockpit.getContentItem('news', '123', { locale: 'en' });
235
248
  const custom = await cockpit.fetchRaw('/custom/endpoint', { param: 'value' });
236
249
  ```
@@ -275,7 +288,10 @@ const cockpit = await CockpitAPI({
275
288
  cache: {
276
289
  max: 100, // Falls back to COCKPIT_CACHE_MAX (default: 100)
277
290
  ttl: 100000, // Falls back to COCKPIT_CACHE_TTL (default: 100000)
291
+ store: customStore, // Optional: custom async cache store (Redis, Keyv, etc.)
278
292
  },
293
+ // Or disable caching entirely
294
+ // cache: false,
279
295
  });
280
296
  ```
281
297
 
@@ -305,6 +321,79 @@ const { tenant, slug } = resolveTenantFromUrl('https://mytenant.example.com/page
305
321
  const allTenants = getTenantIds(); // From COCKPIT_SECRET_* env vars
306
322
  ```
307
323
 
324
+ ## Custom Cache Stores
325
+
326
+ v2.2.0+ supports pluggable async cache stores for Redis, Keyv, or custom implementations:
327
+
328
+ ```typescript
329
+ import { createClient } from 'redis';
330
+ import type { AsyncCacheStore } from '@unchainedshop/cockpit-api';
331
+
332
+ // Redis example
333
+ const redisClient = createClient({ url: process.env.REDIS_URL });
334
+ await redisClient.connect();
335
+
336
+ const redisStore: AsyncCacheStore = {
337
+ async get(key: string) {
338
+ const value = await redisClient.get(key);
339
+ return value ? JSON.parse(value) : undefined;
340
+ },
341
+ async set(key: string, value: unknown) {
342
+ await redisClient.set(key, JSON.stringify(value), { EX: 100 });
343
+ },
344
+ async clear(pattern?: string) {
345
+ if (pattern) {
346
+ const keys = await redisClient.keys(`${pattern}*`);
347
+ if (keys.length > 0) await redisClient.del(keys);
348
+ } else {
349
+ await redisClient.flushDb();
350
+ }
351
+ }
352
+ };
353
+
354
+ const cockpit = await CockpitAPI({
355
+ endpoint: 'https://cms.example.com/api/graphql',
356
+ cache: { store: redisStore }
357
+ });
358
+ ```
359
+
360
+ ## Response Format (v3.0.0+)
361
+
362
+ All list methods return a **consistent response format** regardless of parameters:
363
+
364
+ ```typescript
365
+ interface CockpitListResponse<T> {
366
+ data: T[];
367
+ meta?: CockpitListMeta; // Present when using pagination (skip parameter)
368
+ }
369
+ ```
370
+
371
+ ### Methods with Consistent Response Format
372
+
373
+ - `getContentItems()` - Always returns `CockpitListResponse<T> | null`
374
+ - `pages()` - Always returns `CockpitListResponse<T> | null`
375
+ - Fetch client methods - Always return `CockpitListResponse<T> | null`
376
+
377
+ ### Usage Example
378
+
379
+ ```typescript
380
+ import type { CockpitListResponse } from '@unchainedshop/cockpit-api';
381
+
382
+ // Always get { data, meta? } format
383
+ const response = await cockpit.getContentItems('posts', { limit: 10, skip: 0 });
384
+
385
+ // Access items
386
+ const items = response?.data || [];
387
+
388
+ // Access metadata (available when using skip parameter)
389
+ const total = response?.meta?.total;
390
+ ```
391
+
392
+ **Benefits:**
393
+ - No need to check if response is array or object
394
+ - Predictable type signatures
395
+ - Easier to work with pagination
396
+
308
397
  ## TypeScript Support
309
398
 
310
399
  ```typescript
@@ -314,6 +403,7 @@ import type {
314
403
  CockpitAPIOptions,
315
404
  CacheManager,
316
405
  CacheOptions,
406
+ AsyncCacheStore,
317
407
 
318
408
  // Query Options
319
409
  ContentItemQueryOptions,
@@ -330,6 +420,8 @@ import type {
330
420
  CockpitRoute,
331
421
  CockpitSearchResult,
332
422
  CockpitContentItem,
423
+ CockpitListResponse, // New: for paginated content responses
424
+ CockpitListMeta, // New: metadata in paginated responses
333
425
 
334
426
  // Schema Types
335
427
  MakeCockpitSchemaOptions,
@@ -360,6 +452,77 @@ import { ImageSizeMode, MimeType } from '@unchainedshop/cockpit-api';
360
452
  - `defaultLanguage` option to configure which language maps to Cockpit's "default" locale
361
453
  - Expanded tenant utilities: `resolveTenantFromUrl()`, `resolveTenantFromSubdomain()`
362
454
 
455
+ ### v2.2.0 (Breaking Changes)
456
+
457
+ **Async Cache Operations:**
458
+ - All cache operations are now async and return Promises
459
+ - `await cockpit.clearCache()` is now required (was synchronous in v2.1.x)
460
+ - Custom cache stores can be provided via `cache.store` option
461
+ - Cache can be explicitly disabled with `cache: false`
462
+
463
+ **Migration:**
464
+ ```typescript
465
+ // Before (v2.1.x)
466
+ cockpit.clearCache();
467
+ cockpit.clearCache('ROUTE');
468
+
469
+ // After (v2.2.0)
470
+ await cockpit.clearCache();
471
+ await cockpit.clearCache('ROUTE');
472
+ ```
473
+
474
+ ### v3.0.0 (Breaking Changes)
475
+
476
+ **Consistent List Response Format:**
477
+
478
+ All list methods now return `CockpitListResponse<T> | null` instead of varying between arrays and wrapped responses:
479
+
480
+ **Changed Methods:**
481
+ - `getContentItems()` - Now always returns `{ data: T[], meta?: {...} } | null`
482
+ - `pages()` - Now always returns `{ data: T[], meta?: {...} } | null`
483
+ - Fetch client `getContentItems()` and `pages()` - Now always return `{ data: T[], meta?: {...} } | null`
484
+
485
+ **Migration:**
486
+ ```typescript
487
+ // Before (v2.x)
488
+ const items = await cockpit.getContentItems('posts', { limit: 10 });
489
+ // items could be Post[] or null
490
+
491
+ const pages = await cockpit.pages({ limit: 10 });
492
+ // pages could be Page[] or null
493
+
494
+ // After (v3.0.0)
495
+ const itemsResponse = await cockpit.getContentItems('posts', { limit: 10 });
496
+ const items = itemsResponse?.data || [];
497
+ const total = itemsResponse?.meta?.total;
498
+
499
+ const pagesResponse = await cockpit.pages({ limit: 10 });
500
+ const pages = pagesResponse?.data || [];
501
+ const total = pagesResponse?.meta?.total;
502
+ ```
503
+
504
+ **Benefits:**
505
+ - Single, predictable return type for all list methods
506
+ - No need to check `Array.isArray()` or normalize responses
507
+ - Cleaner TypeScript types
508
+ - Metadata always accessible via `.meta` property
509
+
510
+ **TreeQueryOptions Type Correction:**
511
+
512
+ `TreeQueryOptions` no longer incorrectly includes `limit` and `skip` parameters (which were always ignored). Tree structures use `parent`, `populate`, `filter`, and `fields` instead.
513
+
514
+ ```typescript
515
+ // Before (v2.x) - allowed but ignored
516
+ await cockpit.getContentTree('categories', { limit: 10 }); // ❌ TypeScript allowed this
517
+
518
+ // After (v3.0.0) - TypeScript prevents invalid usage
519
+ await cockpit.getContentTree('categories', {
520
+ parent: 'root-id', // ✅ Correct
521
+ populate: 2, // ✅ Correct
522
+ filter: { active: true } // ✅ Correct
523
+ });
524
+ ```
525
+
363
526
  ## Peer Dependencies
364
527
 
365
528
  - `graphql` (optional) - Required for the `graphQL()` method
package/dist/client.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { DocumentNode } from "graphql";
5
5
  import { type CockpitAPIOptions } from "./core/config.ts";
6
- import { type ContentItemQueryOptions, type ContentListQueryOptions, type TreeQueryOptions, type AggregateQueryOptions, type CockpitContentItem, type CockpitTreeNode } from "./methods/content.ts";
6
+ import { type ContentItemQueryOptions, type ContentListQueryOptions, type TreeQueryOptions, type AggregateQueryOptions, type CockpitContentItem, type CockpitTreeNode, type CockpitListResponse } from "./methods/content.ts";
7
7
  import { type PageByIdOptions, type CockpitPage } from "./methods/pages.ts";
8
8
  import { type MenuQueryOptions, type CockpitMenu } from "./methods/menus.ts";
9
9
  import { type CockpitRoutesResponse, type CockpitSitemapEntry, type CockpitSettings } from "./methods/routes.ts";
@@ -16,12 +16,12 @@ import { type LocalizeOptions } from "./methods/localize.ts";
16
16
  export interface CockpitAPIClient {
17
17
  graphQL<T = unknown>(document: DocumentNode, variables?: Record<string, unknown>): Promise<T | null>;
18
18
  getContentItem<T = unknown>(options: ContentItemQueryOptions): Promise<T | null>;
19
- getContentItems<T = CockpitContentItem>(model: string, options?: ContentListQueryOptions): Promise<T[] | null>;
19
+ getContentItems<T = CockpitContentItem>(model: string, options?: ContentListQueryOptions): Promise<CockpitListResponse<T> | null>;
20
20
  getContentTree<T = CockpitContentItem>(model: string, options?: TreeQueryOptions): Promise<CockpitTreeNode<T>[] | null>;
21
21
  getAggregateModel<T = unknown>(options: AggregateQueryOptions): Promise<T[] | null>;
22
22
  postContentItem<T = unknown>(model: string, item: Record<string, unknown>): Promise<T | null>;
23
23
  deleteContentItem<T = unknown>(model: string, id: string): Promise<T | null>;
24
- pages<T = CockpitPage>(options?: ContentListQueryOptions): Promise<T[] | null>;
24
+ pages<T = CockpitPage>(options?: ContentListQueryOptions): Promise<CockpitListResponse<T> | null>;
25
25
  pageById<T = CockpitPage>(id: string, options?: PageByIdOptions): Promise<T | null>;
26
26
  pageByRoute<T = CockpitPage>(route: string, options?: {
27
27
  locale?: string;
@@ -48,7 +48,25 @@ export interface CockpitAPIClient {
48
48
  */
49
49
  imageAssetById(assetId: string, queryParams?: ImageAssetQueryParams): Promise<string | null>;
50
50
  getFullRouteForSlug(slug: string): Promise<string | undefined>;
51
- clearCache(pattern?: string): void;
51
+ /**
52
+ * Clear cache entries matching pattern
53
+ *
54
+ * **BREAKING CHANGE (v3.0.0)**: This method is now async and returns a Promise
55
+ *
56
+ * @param pattern - Optional pattern to clear specific cache entries
57
+ * @returns Promise that resolves when clearing is complete
58
+ *
59
+ * @example Clear all cache
60
+ * ```typescript
61
+ * await client.clearCache();
62
+ * ```
63
+ *
64
+ * @example Clear route cache only
65
+ * ```typescript
66
+ * await client.clearCache('ROUTE');
67
+ * ```
68
+ */
69
+ clearCache(pattern?: string): Promise<void>;
52
70
  }
53
71
  /**
54
72
  * Creates a Cockpit API client
package/dist/client.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Cockpit API Client Factory
3
3
  */
4
4
  import { createConfig } from "./core/config.js";
5
- import { createCacheManager } from "./core/cache.js";
5
+ import { createCacheManager, createNoOpCacheManager, } from "./core/cache.js";
6
6
  import { createUrlBuilder } from "./core/url-builder.js";
7
7
  import { createHttpClient } from "./core/http.js";
8
8
  import { createImagePathTransformer } from "./transformers/image-path.js";
@@ -39,19 +39,40 @@ export async function CockpitAPI(options = {}) {
39
39
  // Create configuration
40
40
  const config = createConfig(options);
41
41
  const endpointString = config.endpoint.toString();
42
- // Create cache manager - env vars take precedence, then options, then cache.ts defaults
43
- const envCacheMax = process.env["COCKPIT_CACHE_MAX"];
44
- const envCacheTtl = process.env["COCKPIT_CACHE_TTL"];
45
- const cacheOptions = {};
46
- const maxValue = options.cache?.max ??
47
- (envCacheMax !== undefined ? parseInt(envCacheMax, 10) : undefined);
48
- const ttlValue = options.cache?.ttl ??
49
- (envCacheTtl !== undefined ? parseInt(envCacheTtl, 10) : undefined);
50
- if (maxValue !== undefined)
51
- cacheOptions.max = maxValue;
52
- if (ttlValue !== undefined)
53
- cacheOptions.ttl = ttlValue;
54
- const cache = createCacheManager(config.cachePrefix, cacheOptions);
42
+ // Create cache manager based on options
43
+ let cache;
44
+ if (options.cache === false) {
45
+ // Cache explicitly disabled - use no-op cache
46
+ cache = createNoOpCacheManager();
47
+ }
48
+ else {
49
+ // Cache enabled - determine options
50
+ const envCacheMax = process.env["COCKPIT_CACHE_MAX"];
51
+ const envCacheTtl = process.env["COCKPIT_CACHE_TTL"];
52
+ const cacheOptions = {};
53
+ // If custom store provided, use it directly
54
+ if (options.cache && "store" in options.cache) {
55
+ cacheOptions.store = options.cache.store;
56
+ }
57
+ else {
58
+ // Use max/ttl from options or env vars (for default LRU store)
59
+ const maxValue = options.cache && "max" in options.cache
60
+ ? options.cache.max
61
+ : envCacheMax !== undefined
62
+ ? parseInt(envCacheMax, 10)
63
+ : undefined;
64
+ const ttlValue = options.cache && "ttl" in options.cache
65
+ ? options.cache.ttl
66
+ : envCacheTtl !== undefined
67
+ ? parseInt(envCacheTtl, 10)
68
+ : undefined;
69
+ if (maxValue !== undefined)
70
+ cacheOptions.max = maxValue;
71
+ if (ttlValue !== undefined)
72
+ cacheOptions.ttl = ttlValue;
73
+ }
74
+ cache = createCacheManager(config.cachePrefix, cacheOptions);
75
+ }
55
76
  // Generate route replacements for image path transformer (optional)
56
77
  const routeReplacements = options.preloadRoutes === true
57
78
  ? await generateCmsRouteReplacements(endpointString, options.tenant, cache)
@@ -1,19 +1,207 @@
1
1
  /**
2
- * Cache management with LRU cache and tenant isolation
2
+ * Cache management with pluggable async stores and tenant isolation
3
+ *
4
+ * v3.0.0 Breaking Change: All cache operations are now async
5
+ */
6
+ /**
7
+ * Async cache store interface that custom cache implementations must implement
8
+ *
9
+ * @example Redis implementation
10
+ * ```typescript
11
+ * import { createClient } from 'redis';
12
+ * import type { AsyncCacheStore } from '@unchainedshop/cockpit-api';
13
+ *
14
+ * const redisClient = createClient({ url: 'redis://localhost:6379' });
15
+ * await redisClient.connect();
16
+ *
17
+ * const redisStore: AsyncCacheStore = {
18
+ * async get(key: string) {
19
+ * const value = await redisClient.get(key);
20
+ * return value ? JSON.parse(value) : undefined;
21
+ * },
22
+ * async set(key: string, value: unknown) {
23
+ * await redisClient.set(key, JSON.stringify(value), { EX: 100 });
24
+ * },
25
+ * async clear(pattern?: string) {
26
+ * if (pattern) {
27
+ * const keys = await redisClient.keys(`${pattern}*`);
28
+ * if (keys.length > 0) await redisClient.del(keys);
29
+ * } else {
30
+ * await redisClient.flushDb();
31
+ * }
32
+ * }
33
+ * };
34
+ * ```
35
+ *
36
+ * @example Keyv implementation
37
+ * ```typescript
38
+ * import Keyv from 'keyv';
39
+ * import type { AsyncCacheStore } from '@unchainedshop/cockpit-api';
40
+ *
41
+ * const keyv = new Keyv('redis://localhost:6379');
42
+ *
43
+ * const keyvStore: AsyncCacheStore = {
44
+ * async get(key: string) {
45
+ * return await keyv.get(key);
46
+ * },
47
+ * async set(key: string, value: unknown) {
48
+ * await keyv.set(key, value, 100000); // 100000ms TTL
49
+ * },
50
+ * async clear(pattern?: string) {
51
+ * if (!pattern) {
52
+ * await keyv.clear();
53
+ * }
54
+ * // Note: Keyv doesn't have native pattern matching
55
+ * // Pattern matching requires custom implementation
56
+ * }
57
+ * };
58
+ * ```
59
+ */
60
+ export interface AsyncCacheStore {
61
+ /**
62
+ * Retrieve a value from cache
63
+ * @param key - Cache key
64
+ * @returns Promise resolving to the cached value, or undefined if not found
65
+ */
66
+ get(key: string): Promise<unknown>;
67
+ /**
68
+ * Store a value in cache
69
+ * @param key - Cache key
70
+ * @param value - Value to store (must be serializable for external stores)
71
+ * @returns Promise that resolves when storage is complete
72
+ */
73
+ set(key: string, value: NonNullable<unknown>): Promise<void>;
74
+ /**
75
+ * Clear cache entries
76
+ * @param pattern - Optional pattern to match keys (implementation-specific)
77
+ * If not provided, clears all entries
78
+ * @returns Promise that resolves when clearing is complete
79
+ */
80
+ clear(pattern?: string): Promise<void>;
81
+ }
82
+ /**
83
+ * Cache configuration options
3
84
  */
4
85
  export interface CacheOptions {
5
- /** Maximum number of entries (default: 100) */
86
+ /**
87
+ * Maximum number of entries (default: 100)
88
+ * Only used with default LRU store. Ignored when custom store is provided.
89
+ */
6
90
  max?: number;
7
- /** Time-to-live in milliseconds (default: 100000) */
91
+ /**
92
+ * Time-to-live in milliseconds (default: 100000)
93
+ * Only used with default LRU store. Ignored when custom store is provided.
94
+ */
8
95
  ttl?: number;
96
+ /**
97
+ * Custom async cache store implementation
98
+ * If provided, max and ttl options are ignored
99
+ *
100
+ * @example Redis store
101
+ * ```typescript
102
+ * import { createClient } from 'redis';
103
+ *
104
+ * const redisClient = createClient();
105
+ * await redisClient.connect();
106
+ *
107
+ * const client = await CockpitAPI({
108
+ * endpoint: 'https://cms.example.com/api/graphql',
109
+ * cache: {
110
+ * store: {
111
+ * async get(key) {
112
+ * const val = await redisClient.get(key);
113
+ * return val ? JSON.parse(val) : undefined;
114
+ * },
115
+ * async set(key, value) {
116
+ * await redisClient.set(key, JSON.stringify(value), { EX: 100 });
117
+ * },
118
+ * async clear(pattern) {
119
+ * if (pattern) {
120
+ * const keys = await redisClient.keys(`${pattern}*`);
121
+ * if (keys.length > 0) await redisClient.del(keys);
122
+ * } else {
123
+ * await redisClient.flushDb();
124
+ * }
125
+ * }
126
+ * }
127
+ * }
128
+ * });
129
+ * ```
130
+ */
131
+ store?: AsyncCacheStore;
9
132
  }
133
+ /**
134
+ * Async cache manager interface
135
+ * All cache operations return promises in v3.0.0+
136
+ */
10
137
  export interface CacheManager {
11
- get(key: string): unknown;
12
- set(key: string, value: NonNullable<unknown>): void;
13
- clear(pattern?: string): void;
138
+ /**
139
+ * Get a value from cache
140
+ * @param key - Cache key (will be prefixed internally)
141
+ * @returns Promise resolving to cached value or undefined if not found
142
+ */
143
+ get(key: string): Promise<unknown>;
144
+ /**
145
+ * Set a value in cache
146
+ * @param key - Cache key (will be prefixed internally)
147
+ * @param value - Value to cache
148
+ * @returns Promise that resolves when caching is complete
149
+ */
150
+ set(key: string, value: NonNullable<unknown>): Promise<void>;
151
+ /**
152
+ * Clear cache entries matching pattern
153
+ * @param pattern - Optional pattern to match (relative to cache prefix)
154
+ * @returns Promise that resolves when clearing is complete
155
+ */
156
+ clear(pattern?: string): Promise<void>;
14
157
  }
15
158
  /**
16
- * Creates a cache manager with prefixed keys
17
- * Each call creates a new LRU cache instance - no shared state
159
+ * Creates a cache manager with prefixed keys and async operations
160
+ * Each call creates a new cache instance - no shared state
161
+ *
162
+ * @param cachePrefix - Prefix for all cache keys (includes endpoint and tenant)
163
+ * @param options - Cache configuration options
164
+ * @returns Async cache manager
165
+ *
166
+ * @example Using default LRU cache
167
+ * ```typescript
168
+ * const cache = createCacheManager('https://cms.example.com:default:', {
169
+ * max: 100,
170
+ * ttl: 100000
171
+ * });
172
+ *
173
+ * await cache.set('key1', { data: 'value' });
174
+ * const value = await cache.get('key1');
175
+ * await cache.clear('ROUTE');
176
+ * ```
177
+ *
178
+ * @example Using custom Redis store
179
+ * ```typescript
180
+ * const redisClient = createClient();
181
+ * await redisClient.connect();
182
+ *
183
+ * const cache = createCacheManager('https://cms.example.com:default:', {
184
+ * store: {
185
+ * async get(key) { ... },
186
+ * async set(key, value) { ... },
187
+ * async clear(pattern) { ... }
188
+ * }
189
+ * });
190
+ * ```
18
191
  */
19
192
  export declare function createCacheManager(cachePrefix: string, options?: CacheOptions): CacheManager;
193
+ /**
194
+ * Creates a no-op cache manager that doesn't cache anything
195
+ * Used when caching is explicitly disabled
196
+ *
197
+ * @returns No-op cache manager
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * const cache = createNoOpCacheManager();
202
+ * await cache.set('key', 'value'); // Does nothing
203
+ * const result = await cache.get('key'); // Always returns undefined
204
+ * await cache.clear(); // Does nothing
205
+ * ```
206
+ */
207
+ export declare function createNoOpCacheManager(): CacheManager;
@@ -1,32 +1,119 @@
1
1
  /**
2
- * Cache management with LRU cache and tenant isolation
2
+ * Cache management with pluggable async stores and tenant isolation
3
+ *
4
+ * v3.0.0 Breaking Change: All cache operations are now async
3
5
  */
4
6
  import { LRUCache } from "lru-cache";
5
7
  /**
6
- * Creates a cache manager with prefixed keys
7
- * Each call creates a new LRU cache instance - no shared state
8
+ * Creates a default LRU-based async cache store
9
+ * Wraps lru-cache in async interface for consistency
8
10
  */
9
- export function createCacheManager(cachePrefix, options = {}) {
11
+ function createDefaultLRUStore(options) {
10
12
  const cache = new LRUCache({
11
13
  max: options.max ?? 100,
12
14
  ttl: options.ttl ?? 100000,
13
15
  allowStale: false,
14
16
  });
15
- const prefixedKey = (key) => `${cachePrefix}${key}`;
16
17
  return {
17
- get(key) {
18
- return cache.get(prefixedKey(key));
18
+ // eslint-disable-next-line @typescript-eslint/require-await
19
+ async get(key) {
20
+ return cache.get(key);
19
21
  },
20
- set(key, value) {
21
- cache.set(prefixedKey(key), value);
22
+ // eslint-disable-next-line @typescript-eslint/require-await
23
+ async set(key, value) {
24
+ cache.set(key, value);
22
25
  },
23
- clear(pattern) {
24
- const prefix = pattern !== undefined ? `${cachePrefix}${pattern}` : cachePrefix;
25
- for (const key of cache.keys()) {
26
- if (key.startsWith(prefix)) {
27
- cache.delete(key);
26
+ // eslint-disable-next-line @typescript-eslint/require-await
27
+ async clear(pattern) {
28
+ if (pattern === undefined) {
29
+ cache.clear();
30
+ }
31
+ else {
32
+ for (const key of cache.keys()) {
33
+ if (key.startsWith(pattern)) {
34
+ cache.delete(key);
35
+ }
28
36
  }
29
37
  }
30
38
  },
31
39
  };
32
40
  }
41
+ /**
42
+ * Creates a cache manager with prefixed keys and async operations
43
+ * Each call creates a new cache instance - no shared state
44
+ *
45
+ * @param cachePrefix - Prefix for all cache keys (includes endpoint and tenant)
46
+ * @param options - Cache configuration options
47
+ * @returns Async cache manager
48
+ *
49
+ * @example Using default LRU cache
50
+ * ```typescript
51
+ * const cache = createCacheManager('https://cms.example.com:default:', {
52
+ * max: 100,
53
+ * ttl: 100000
54
+ * });
55
+ *
56
+ * await cache.set('key1', { data: 'value' });
57
+ * const value = await cache.get('key1');
58
+ * await cache.clear('ROUTE');
59
+ * ```
60
+ *
61
+ * @example Using custom Redis store
62
+ * ```typescript
63
+ * const redisClient = createClient();
64
+ * await redisClient.connect();
65
+ *
66
+ * const cache = createCacheManager('https://cms.example.com:default:', {
67
+ * store: {
68
+ * async get(key) { ... },
69
+ * async set(key, value) { ... },
70
+ * async clear(pattern) { ... }
71
+ * }
72
+ * });
73
+ * ```
74
+ */
75
+ export function createCacheManager(cachePrefix, options = {}) {
76
+ // Use custom store if provided, otherwise create default LRU store
77
+ const store = options.store ?? createDefaultLRUStore(options);
78
+ const prefixedKey = (key) => `${cachePrefix}${key}`;
79
+ return {
80
+ async get(key) {
81
+ return await store.get(prefixedKey(key));
82
+ },
83
+ async set(key, value) {
84
+ await store.set(prefixedKey(key), value);
85
+ },
86
+ async clear(pattern) {
87
+ const prefix = pattern !== undefined ? `${cachePrefix}${pattern}` : cachePrefix;
88
+ await store.clear(prefix);
89
+ },
90
+ };
91
+ }
92
+ /**
93
+ * Creates a no-op cache manager that doesn't cache anything
94
+ * Used when caching is explicitly disabled
95
+ *
96
+ * @returns No-op cache manager
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const cache = createNoOpCacheManager();
101
+ * await cache.set('key', 'value'); // Does nothing
102
+ * const result = await cache.get('key'); // Always returns undefined
103
+ * await cache.clear(); // Does nothing
104
+ * ```
105
+ */
106
+ export function createNoOpCacheManager() {
107
+ return {
108
+ // eslint-disable-next-line @typescript-eslint/require-await
109
+ async get() {
110
+ return undefined;
111
+ },
112
+ async set() {
113
+ // No-op
114
+ },
115
+ async clear() {
116
+ // No-op
117
+ },
118
+ };
119
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Configuration management for Cockpit API client
3
3
  */
4
+ import type { CacheOptions } from "./cache.ts";
4
5
  export interface CockpitAPIOptions {
5
6
  /** Cockpit CMS endpoint URL (falls back to COCKPIT_GRAPHQL_ENDPOINT env var) */
6
7
  endpoint?: string;
@@ -15,13 +16,61 @@ export interface CockpitAPIOptions {
15
16
  * When a request uses this language, it will be sent as "default" to Cockpit.
16
17
  */
17
18
  defaultLanguage?: string | null;
18
- /** Cache configuration */
19
- cache?: {
20
- /** Max entries (falls back to COCKPIT_CACHE_MAX env var, default: 100) */
21
- max?: number;
22
- /** TTL in ms (falls back to COCKPIT_CACHE_TTL env var, default: 100000) */
23
- ttl?: number;
24
- };
19
+ /**
20
+ * Cache configuration
21
+ *
22
+ * - Set to `false` to disable caching entirely
23
+ * - Set to an object to configure cache behavior
24
+ * - Omit to use default LRU cache with env var fallbacks
25
+ *
26
+ * @example Disable cache
27
+ * ```typescript
28
+ * const client = await CockpitAPI({
29
+ * endpoint: 'https://cms.example.com',
30
+ * cache: false
31
+ * });
32
+ * ```
33
+ *
34
+ * @example Custom cache options
35
+ * ```typescript
36
+ * const client = await CockpitAPI({
37
+ * endpoint: 'https://cms.example.com',
38
+ * cache: { max: 200, ttl: 300000 }
39
+ * });
40
+ * ```
41
+ *
42
+ * @example Redis store
43
+ * ```typescript
44
+ * import { createClient } from 'redis';
45
+ *
46
+ * const redisClient = createClient({ url: process.env.REDIS_URL });
47
+ * await redisClient.connect();
48
+ *
49
+ * const client = await CockpitAPI({
50
+ * endpoint: 'https://cms.example.com',
51
+ * cache: {
52
+ * store: {
53
+ * async get(key) {
54
+ * const val = await redisClient.get(key);
55
+ * return val ? JSON.parse(val) : undefined;
56
+ * },
57
+ * async set(key, value) {
58
+ * await redisClient.set(key, JSON.stringify(value), { EX: 100 });
59
+ * },
60
+ * async clear(pattern) {
61
+ * if (pattern) {
62
+ * const keys = await redisClient.keys(\`\${pattern}*\`);
63
+ * if (keys.length > 0) await redisClient.del(keys);
64
+ * } else {
65
+ * await redisClient.flushDb();
66
+ * }
67
+ * }
68
+ * }
69
+ * }
70
+ * });
71
+ * ```
72
+ */
73
+ cache?: false | CacheOptions;
25
74
  /**
26
75
  * Preload route replacements during client initialization.
27
76
  * When true, fetches page routes to enable `pages://id` link resolution in responses.
@@ -2,8 +2,8 @@
2
2
  * Core module exports
3
3
  */
4
4
  export { buildQueryString, encodeQueryParam } from "./query-string.ts";
5
- export type { CacheManager, CacheOptions } from "./cache.ts";
6
- export { createCacheManager } from "./cache.ts";
5
+ export type { CacheManager, CacheOptions, AsyncCacheStore } from "./cache.ts";
6
+ export { createCacheManager, createNoOpCacheManager } from "./cache.ts";
7
7
  export type { CockpitConfig } from "./config.ts";
8
8
  export { createConfig } from "./config.ts";
9
9
  export type { UrlBuilder, UrlBuildOptions } from "./url-builder.ts";
@@ -2,7 +2,7 @@
2
2
  * Core module exports
3
3
  */
4
4
  export { buildQueryString, encodeQueryParam } from "./query-string.js";
5
- export { createCacheManager } from "./cache.js";
5
+ export { createCacheManager, createNoOpCacheManager } from "./cache.js";
6
6
  export { createConfig } from "./config.js";
7
7
  export { createUrlBuilder } from "./url-builder.js";
8
8
  export { createHttpClient } from "./http.js";
@@ -9,7 +9,7 @@
9
9
  * - Minimal memory footprint
10
10
  */
11
11
  import type { CockpitPage } from "../methods/pages.ts";
12
- import type { CockpitContentItem } from "../methods/content.ts";
12
+ import type { CockpitContentItem, CockpitListResponse } from "../methods/content.ts";
13
13
  /**
14
14
  * Request cache mode for fetch requests
15
15
  */
@@ -52,12 +52,12 @@ export interface PageFetchParams {
52
52
  export interface FetchClient {
53
53
  /** Fetch a page by route */
54
54
  pageByRoute<T = CockpitPage>(route: string, params?: PageFetchParams): Promise<T | null>;
55
- /** Fetch pages list */
56
- pages<T = CockpitPage>(params?: PageFetchParams): Promise<T[] | null>;
55
+ /** Fetch pages list. Always returns { data, meta? } or null. */
56
+ pages<T = CockpitPage>(params?: PageFetchParams): Promise<CockpitListResponse<T> | null>;
57
57
  /** Fetch a page by ID */
58
58
  pageById<T = CockpitPage>(id: string, params?: PageFetchParams): Promise<T | null>;
59
- /** Fetch content items */
60
- getContentItems<T = CockpitContentItem>(model: string, params?: PageFetchParams): Promise<T[] | null>;
59
+ /** Fetch content items. Always returns { data, meta? } or null. */
60
+ getContentItems<T = CockpitContentItem>(model: string, params?: PageFetchParams): Promise<CockpitListResponse<T> | null>;
61
61
  /** Fetch a single content item */
62
62
  getContentItem<T = unknown>(model: string, id?: string, params?: PageFetchParams): Promise<T | null>;
63
63
  /** Raw fetch for custom paths */
@@ -96,10 +96,18 @@ export function createFetchClient(options = {}) {
96
96
  */
97
97
  async pages(params = {}) {
98
98
  const { locale, ...rest } = params;
99
- return fetchRaw("/pages/pages", {
99
+ const result = await fetchRaw("/pages/pages", {
100
100
  locale: normalizeLocale(locale),
101
101
  ...rest,
102
102
  });
103
+ // Normalize response to always return { data, meta? }
104
+ if (result === null) {
105
+ return null;
106
+ }
107
+ if (Array.isArray(result)) {
108
+ return { data: result };
109
+ }
110
+ return result;
103
111
  },
104
112
  /**
105
113
  * Fetch a page by ID
@@ -117,10 +125,18 @@ export function createFetchClient(options = {}) {
117
125
  */
118
126
  async getContentItems(model, params = {}) {
119
127
  const { locale, ...rest } = params;
120
- return fetchRaw(`/content/items/${model}`, {
128
+ const result = await fetchRaw(`/content/items/${model}`, {
121
129
  locale: normalizeLocale(locale),
122
130
  ...rest,
123
131
  });
132
+ // Normalize response to always return { data, meta? }
133
+ if (result === null) {
134
+ return null;
135
+ }
136
+ if (Array.isArray(result)) {
137
+ return { data: result };
138
+ }
139
+ return result;
124
140
  },
125
141
  /**
126
142
  * Fetch a single content item
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  export { CockpitAPI } from "./client.ts";
7
7
  export type { CockpitAPIClient } from "./client.ts";
8
8
  export type { CockpitAPIOptions } from "./core/config.ts";
9
- export type { CacheManager, CacheOptions } from "./core/cache.ts";
9
+ export type { CacheManager, CacheOptions, AsyncCacheStore, } from "./core/cache.ts";
10
10
  export { getTenantIds, resolveTenantFromUrl, resolveTenantFromSubdomain, } from "./utils/tenant.ts";
11
11
  export type { TenantUrlResult, ResolveTenantFromUrlOptions, ResolveTenantFromSubdomainOptions, } from "./utils/tenant.ts";
12
12
  export { generateCmsRouteReplacements, generateCollectionAndSingletonSlugRouteMap, } from "./utils/route-map.ts";
@@ -18,7 +18,7 @@ export type { PageByIdOptions, PageByRouteOptions } from "./methods/pages.ts";
18
18
  export type { MenuQueryOptions } from "./methods/menus.ts";
19
19
  export type { SearchQueryOptions } from "./methods/search.ts";
20
20
  export type { LocalizeOptions } from "./methods/localize.ts";
21
- export type { CockpitContentItem, CockpitNewsItem, CockpitTreeNode, } from "./methods/content.ts";
21
+ export type { CockpitContentItem, CockpitNewsItem, CockpitTreeNode, CockpitListResponse, CockpitListMeta, } from "./methods/content.ts";
22
22
  export { ImageSizeMode, MimeType } from "./methods/assets.ts";
23
23
  export type { CockpitAsset, ImageAssetQueryParams } from "./methods/assets.ts";
24
24
  export type { CockpitPageType, CockpitPageMeta, CockpitPageSeo, CockpitLayoutBlock, CockpitPage, } from "./methods/pages.ts";
@@ -30,9 +30,15 @@ export interface ContentItemQueryOptions extends ListQueryOptions {
30
30
  export interface ContentListQueryOptions extends ListQueryOptions {
31
31
  queryParams?: Record<string, unknown>;
32
32
  }
33
- export interface TreeQueryOptions extends ListQueryOptions {
33
+ export interface TreeQueryOptions {
34
34
  parent?: string;
35
+ filter?: Record<string, unknown>;
36
+ fields?: Record<string, 0 | 1>;
37
+ populate?: number;
38
+ locale?: string;
35
39
  queryParams?: Record<string, unknown>;
40
+ /** Override the client-level useAdminAccess setting for this request */
41
+ useAdminAccess?: boolean;
36
42
  }
37
43
  export interface AggregateQueryOptions {
38
44
  model: string;
@@ -66,9 +72,35 @@ export interface CockpitTreeNode<T = CockpitContentItem> {
66
72
  children?: CockpitTreeNode<T>[];
67
73
  data?: T;
68
74
  }
75
+ /**
76
+ * Metadata returned with paginated content responses
77
+ */
78
+ export interface CockpitListMeta {
79
+ total?: number;
80
+ [key: string]: unknown;
81
+ }
82
+ /**
83
+ * Wrapper response format returned by Cockpit when using pagination (skip parameter)
84
+ */
85
+ export interface CockpitListResponse<T> {
86
+ data: T[];
87
+ meta?: CockpitListMeta;
88
+ }
69
89
  export interface ContentMethods {
70
90
  getContentItem<T = unknown>(options: ContentItemQueryOptions): Promise<T | null>;
71
- getContentItems<T = CockpitContentItem>(model: string, options?: ContentListQueryOptions): Promise<T[] | null>;
91
+ /**
92
+ * Get multiple content items from a collection.
93
+ *
94
+ * @returns Always returns `CockpitListResponse<T>` with data and optional meta.
95
+ * Returns `null` if collection doesn't exist.
96
+ *
97
+ * @example
98
+ * const response = await cockpit.getContentItems('posts', { limit: 10 });
99
+ * // response: { data: Post[], meta?: { total: number } } | null
100
+ * const items = response?.data || [];
101
+ * const total = response?.meta?.total;
102
+ */
103
+ getContentItems<T = CockpitContentItem>(model: string, options?: ContentListQueryOptions): Promise<CockpitListResponse<T> | null>;
72
104
  getContentTree<T = CockpitContentItem>(model: string, options?: TreeQueryOptions): Promise<CockpitTreeNode<T>[] | null>;
73
105
  getAggregateModel<T = unknown>(options: AggregateQueryOptions): Promise<T[] | null>;
74
106
  postContentItem<T = unknown>(model: string, item: Record<string, unknown>): Promise<T | null>;
@@ -33,7 +33,15 @@ export function createContentMethods(ctx) {
33
33
  populate,
34
34
  },
35
35
  });
36
- return ctx.http.fetch(url, buildFetchOptions(useAdminAccess));
36
+ const result = await ctx.http.fetch(url, buildFetchOptions(useAdminAccess));
37
+ // Normalize response to always return { data, meta? }
38
+ if (result === null) {
39
+ return null;
40
+ }
41
+ if (Array.isArray(result)) {
42
+ return { data: result };
43
+ }
44
+ return result;
37
45
  },
38
46
  async getContentTree(model, options = {}) {
39
47
  requireParam(model, "a model");
@@ -24,7 +24,7 @@ export interface CockpitMenuLink {
24
24
  meta?: {
25
25
  key: string;
26
26
  value: string;
27
- }[];
27
+ }[] | Record<string, string>;
28
28
  }
29
29
  export interface CockpitMenu {
30
30
  _id: string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pages API methods
3
3
  */
4
- import type { MethodContext, ContentListQueryOptions } from "./content.ts";
4
+ import type { MethodContext, ContentListQueryOptions, CockpitListResponse } from "./content.ts";
5
5
  import type { CockpitAsset } from "./assets.ts";
6
6
  export interface PageByIdOptions {
7
7
  locale?: string;
@@ -42,7 +42,19 @@ export interface CockpitPage extends CockpitPageMeta {
42
42
  _p?: string;
43
43
  }
44
44
  export interface PagesMethods {
45
- pages<T = CockpitPage>(options?: ContentListQueryOptions): Promise<T[] | null>;
45
+ /**
46
+ * Get pages list.
47
+ *
48
+ * @returns Always returns `CockpitListResponse<T>` with data and optional meta.
49
+ * Returns `null` if pages cannot be fetched.
50
+ *
51
+ * @example
52
+ * const response = await cockpit.pages({ limit: 10, skip: 0 });
53
+ * // response: { data: CockpitPage[], meta?: { total: number } } | null
54
+ * const pages = response?.data || [];
55
+ * const total = response?.meta?.total;
56
+ */
57
+ pages<T = CockpitPage>(options?: ContentListQueryOptions): Promise<CockpitListResponse<T> | null>;
46
58
  pageById<T = CockpitPage>(id: string, options?: PageByIdOptions): Promise<T | null>;
47
59
  pageByRoute<T = CockpitPage>(route: string, options?: PageByRouteOptions | string): Promise<T | null>;
48
60
  }
@@ -10,7 +10,15 @@ export function createPagesMethods(ctx) {
10
10
  locale,
11
11
  queryParams: { ...queryParams, limit, skip, sort, filter, fields },
12
12
  });
13
- return ctx.http.fetch(url);
13
+ const result = await ctx.http.fetch(url);
14
+ // Normalize response to always return { data, meta? }
15
+ if (result === null) {
16
+ return null;
17
+ }
18
+ if (Array.isArray(result)) {
19
+ return { data: result };
20
+ }
21
+ return result;
14
22
  },
15
23
  async pageById(id, options = {}) {
16
24
  requireParam(id, "a page id");
@@ -10,6 +10,24 @@ export interface CockpitHealthCheck {
10
10
  }
11
11
  export interface SystemMethods {
12
12
  healthCheck<T = unknown>(): Promise<T | null>;
13
- clearCache(pattern?: string): void;
13
+ /**
14
+ * Clear cache entries matching pattern
15
+ *
16
+ * **BREAKING CHANGE (v3.0.0)**: This method is now async and returns a Promise
17
+ *
18
+ * @param pattern - Optional pattern to clear specific cache entries
19
+ * @returns Promise that resolves when clearing is complete
20
+ *
21
+ * @example Clear all cache
22
+ * ```typescript
23
+ * await client.clearCache();
24
+ * ```
25
+ *
26
+ * @example Clear route cache only
27
+ * ```typescript
28
+ * await client.clearCache('ROUTE');
29
+ * ```
30
+ */
31
+ clearCache(pattern?: string): Promise<void>;
14
32
  }
15
33
  export declare function createSystemMethods(ctx: MethodContext): SystemMethods;
@@ -7,8 +7,8 @@ export function createSystemMethods(ctx) {
7
7
  const url = ctx.url.build("/system/healthcheck");
8
8
  return ctx.http.fetch(url);
9
9
  },
10
- clearCache(pattern) {
11
- ctx.cache.clear(pattern);
10
+ async clearCache(pattern) {
11
+ await ctx.cache.clear(pattern);
12
12
  },
13
13
  };
14
14
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Meta transformer for converting Cockpit CMS meta arrays to objects
3
+ *
4
+ * This transformer converts meta properties from array format:
5
+ * meta: [{ key: 'foo', value: 'bar' }]
6
+ *
7
+ * To object format:
8
+ * meta: { foo: 'bar' }
9
+ */
10
+ import type { ResponseTransformer } from "./image-path.ts";
11
+ /**
12
+ * Creates a transformer that converts meta arrays to objects.
13
+ * This transformation affects all meta properties in the response,
14
+ * including nested ones in children arrays.
15
+ */
16
+ export declare function createMetaTransformer(): ResponseTransformer;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Meta transformer for converting Cockpit CMS meta arrays to objects
3
+ *
4
+ * This transformer converts meta properties from array format:
5
+ * meta: [{ key: 'foo', value: 'bar' }]
6
+ *
7
+ * To object format:
8
+ * meta: { foo: 'bar' }
9
+ */
10
+ import { logger } from "../cockpit-logger.js";
11
+ import { transformCockpitMeta } from "../utils/meta.js";
12
+ /**
13
+ * Recursively transforms all meta arrays in a data structure to objects
14
+ */
15
+ function transformMetaRecursive(data) {
16
+ if (data === null || data === undefined || typeof data !== "object") {
17
+ return data;
18
+ }
19
+ if (Array.isArray(data)) {
20
+ return data.map(transformMetaRecursive);
21
+ }
22
+ const result = {};
23
+ for (const [key, value] of Object.entries(data)) {
24
+ if (key === "meta" && Array.isArray(value)) {
25
+ // Transform meta array to object
26
+ result[key] = transformCockpitMeta(value);
27
+ }
28
+ else {
29
+ // Recursively transform nested objects
30
+ result[key] = transformMetaRecursive(value);
31
+ }
32
+ }
33
+ return result;
34
+ }
35
+ /**
36
+ * Creates a transformer that converts meta arrays to objects.
37
+ * This transformation affects all meta properties in the response,
38
+ * including nested ones in children arrays.
39
+ */
40
+ export function createMetaTransformer() {
41
+ return {
42
+ transform(originalResponse) {
43
+ try {
44
+ return transformMetaRecursive(originalResponse);
45
+ }
46
+ catch (error) {
47
+ logger.warn("Cockpit: Failed to transform meta arrays", error);
48
+ return originalResponse;
49
+ }
50
+ },
51
+ };
52
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Transforms Cockpit meta array into an object for easier access
3
+ *
4
+ * @param meta Array of {key, value} objects from Cockpit API
5
+ * @returns Object with key-value pairs
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const meta = [
10
+ * { key: 'layout', value: 'shop' },
11
+ * { key: 'isCallToAction', value: 'true' },
12
+ * ];
13
+ * const metaObj = transformCockpitMeta(meta);
14
+ * if (metaObj.layout === 'shop') {
15
+ * // Handle shop layout
16
+ * }
17
+ * ```
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // Safe to use with optional meta
22
+ * const metaObj = transformCockpitMeta(link.meta);
23
+ * const layout = metaObj.layout ?? 'default';
24
+ * ```
25
+ */
26
+ export declare function transformCockpitMeta(meta?: {
27
+ key: string;
28
+ value: string;
29
+ }[]): Record<string, string>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Transforms Cockpit meta array into an object for easier access
3
+ *
4
+ * @param meta Array of {key, value} objects from Cockpit API
5
+ * @returns Object with key-value pairs
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const meta = [
10
+ * { key: 'layout', value: 'shop' },
11
+ * { key: 'isCallToAction', value: 'true' },
12
+ * ];
13
+ * const metaObj = transformCockpitMeta(meta);
14
+ * if (metaObj.layout === 'shop') {
15
+ * // Handle shop layout
16
+ * }
17
+ * ```
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // Safe to use with optional meta
22
+ * const metaObj = transformCockpitMeta(link.meta);
23
+ * const layout = metaObj.layout ?? 'default';
24
+ * ```
25
+ */
26
+ export function transformCockpitMeta(meta) {
27
+ if (!meta || !Array.isArray(meta))
28
+ return {};
29
+ return meta.reduce((acc, { key, value }) => {
30
+ acc[key] = value;
31
+ return acc;
32
+ }, {});
33
+ }
@@ -8,7 +8,7 @@ import { logger } from "../cockpit-logger.js";
8
8
  export async function generateCmsRouteReplacements(endpoint, tenant, cache) {
9
9
  const cacheKey = `ROUTE_REPLACEMENT_MAP:${tenant ?? "default"}`;
10
10
  if (cache) {
11
- const cached = cache.get(cacheKey);
11
+ const cached = (await cache.get(cacheKey));
12
12
  if (cached)
13
13
  return cached;
14
14
  }
@@ -31,7 +31,7 @@ export async function generateCmsRouteReplacements(endpoint, tenant, cache) {
31
31
  return { ...result, [key]: value };
32
32
  }, {});
33
33
  if (cache) {
34
- cache.set(cacheKey, replacement);
34
+ await cache.set(cacheKey, replacement);
35
35
  }
36
36
  return replacement;
37
37
  }
@@ -46,7 +46,7 @@ export async function generateCmsRouteReplacements(endpoint, tenant, cache) {
46
46
  export async function generateCollectionAndSingletonSlugRouteMap(endpoint, tenant, cache) {
47
47
  const cacheKey = `SLUG_ROUTE_MAP:${tenant ?? "default"}`;
48
48
  if (cache) {
49
- const cached = cache.get(cacheKey);
49
+ const cached = (await cache.get(cacheKey));
50
50
  if (cached)
51
51
  return cached;
52
52
  }
@@ -75,7 +75,7 @@ export async function generateCollectionAndSingletonSlugRouteMap(endpoint, tenan
75
75
  return { ...result, [entityName]: _r };
76
76
  }, {});
77
77
  if (cache) {
78
- cache.set(cacheKey, pageMap);
78
+ await cache.set(cacheKey, pageMap);
79
79
  }
80
80
  return pageMap;
81
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unchainedshop/cockpit-api",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "description": "A package to interact with the Cockpit CMS API, including functionalities to handle GraphQL requests and various CMS content manipulations.",
5
5
  "main": "dist/index.js",
6
6
  "homepage": "https://unchained.shop",