@codesocietyou/contentedge-cms-sdk 0.2.2 → 1.0.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
@@ -1,233 +1,455 @@
1
- ## ContentEdge SDK (TypeScript)
1
+ # ContentEdge SDK (TypeScript)
2
2
 
3
- A lightweight, framework-agnostic TypeScript client for the ContentEdge headless CMS. It provides a small, typed façade over the HTTP API with pluggable auth, robust pagination helpers, asset/file utilities, and structured errors—without embedding project-specific domain models (e.g., News/Blog/Reports).
3
+ A lightweight, framework-agnostic TypeScript client for the ContentEdge headless CMS. It provides a clean, typed interface over the HTTP API with public API key authentication, robust pagination helpers, asset/file utilities, and structured errors—without embedding project-specific domain models.
4
4
 
5
5
  This SDK models the wire contract via a generic `ContentDto<C>` where `C` represents your custom fields. It does not import or depend on the CMS server code.
6
6
 
7
- ### Features
8
-
9
- - Generic content model: `ContentDto<C>` with consumer-defined custom fields
10
- - Pluggable auth strategy: `AuthProvider` (includes Keycloak client-credentials)
11
- - Resilient pagination: `listAllContent` aggregates pages with dedupe and safe stop
12
- - Asset/file helpers: `buildAssetUrl` and safe `downloadFile`
13
- - Structured error model: `CmsError` with status and response data
14
- - Framework-agnostic; optional React Query adapter pattern
15
-
16
- ### Installation
7
+ > ### Looking for IT services?
8
+ > <img src="https://fromulo.com/codesociety.png" align="left" width="80" alt="CodeSociety">
9
+ >
10
+ > **[CodeSociety](https://codesocietyhub.com/)** is our consulting & contracting arm — specializing in
11
+ > **IT architecture**, **XML authoring systems**, **FontoXML integration**, and **TerminusDB consulting**.
12
+ > We build structured content platforms and data solutions that power digital publishing.
13
+ >
14
+ > **[Let's talk! &#8594;](https://codesocietyhub.com/contact.html)**
15
+
16
+ ## Features
17
+
18
+ - **Generic content model**: `ContentDto<C>` with consumer-defined custom fields
19
+ - **Public API key authentication**: Simple header-based authentication
20
+ - **Service layer pattern**: Centralized API client with interceptors
21
+ - **React Query integration**: Optional query factory and hooks
22
+ - **Resilient pagination**: `fetchAllContent` aggregates pages with dedupe and safe stop
23
+ - **Asset/file helpers**: `buildAssetUrl` and safe `downloadFile`
24
+ - **Normalization utilities**: Transform API responses to normalized structures
25
+ - **Structured error model**: `CmsError` with status and response data
26
+ - **Framework-agnostic**: Core services work anywhere; React Query layer is optional
27
+ - **TypeScript-first**: Full type safety with generics
28
+
29
+ ## Installation
17
30
 
18
31
  ```bash
19
32
  npm install @codesocietyou/contentedge-cms-sdk
20
33
  # or
21
34
  yarn add @codesocietyou/contentedge-cms-sdk
35
+ # or
36
+ pnpm add @codesocietyou/contentedge-cms-sdk
22
37
  ```
23
38
 
24
- ### Quick Start
39
+ For React Query integration, also install:
25
40
 
26
- ```ts
27
- import {
28
- CmsClient,
29
- KeycloakClientCredentialsAuth,
30
- type ContentDto
31
- } from '@codesocietyou/contentedge-cms-sdk';
41
+ ```bash
42
+ npm install @tanstack/react-query
43
+ ```
32
44
 
33
- // Auth (Keycloak client-credentials) - server-side only
34
- const auth = new KeycloakClientCredentialsAuth({
35
- tokenUrl: 'https://auth.example.com/realms/contentedge/protocol/openid-connect/token',
36
- clientId: 'contentedge-client',
37
- clientSecret: 'xxxxxx'
38
- });
45
+ ## Quick Start
46
+
47
+ ### 1. Initialize the SDK
39
48
 
40
- // Client
41
- const contentedge = new CmsClient({
42
- baseUrl: 'https://cms.example.com/api',
43
- fileBaseUrl: 'https://cms.example.com', // optional (asset host)
44
- tenant: 'your-tenant', // optional; sent as X-Tenant
45
- auth
49
+ ```typescript
50
+ import { createApiClient } from '@codesocietyou/contentedge-cms-sdk';
51
+
52
+ // Initialize once at app startup
53
+ createApiClient({
54
+ baseUrl: 'https://api.contentedge.com',
55
+ fileBaseUrl: 'https://cdn.contentedge.com', // optional
56
+ apiKey: 'your-api-key', // optional
57
+ tenant: 'your-tenant', // optional
58
+ timeoutMs: 30_000, // optional (default: 30s)
46
59
  });
60
+ ```
61
+
62
+ ### 2. Fetch Content (Basic)
47
63
 
48
- // List content by type with filters/pagination
49
- const list = await contentedge.listContent({
50
- type: 'REPORT',
64
+ ```typescript
65
+ import { fetchContentByType } from '@codesocietyou/contentedge-cms-sdk';
66
+
67
+ // Fetch paginated content
68
+ const response = await fetchContentByType({
69
+ type: 'NEWS',
51
70
  page: 0,
52
71
  size: 10,
53
72
  sortBy: 'id',
54
73
  direction: 'DESC',
55
- filters: { publicationType: 'GAMEHEARTS' } // arbitrary query params
74
+ filters: { publicationType: 'GAMEHEARTS' }, // arbitrary filters
56
75
  });
57
76
 
58
- // Get detail by id
59
- const detail = await contentedge.getContentById(123);
60
-
61
- // Download a file (Blob in browsers)
62
- const pdf = await contentedge.downloadFile('https://cms.example.com/files/doc.pdf');
63
- ```
64
-
65
- ### API
66
-
67
- - Client
68
- - `new CmsClient(config)`
69
- - `baseUrl`: CMS API base URL (e.g., `https://cms.example.com/api`)
70
- - `fileBaseUrl?`: Preferred file/asset base (often origin w/o `/api`)
71
- - `tenant?`: Adds `X-Tenant` header
72
- - `timeoutMs?`: Default 30000
73
- - `logger?`: `{ debug?, warn?, error? }`
74
- - `auth?`: `AuthProvider`
75
- - `listContent<C>(params?: ContentListParams): Promise<ContentResponse<C>>`
76
- - `getContentById<C>(id: number): Promise<ApiResponse<ContentDto<C>>>`
77
- - `listAllContent<C, T = ContentDto<C>>(params: Omit<ContentListParams, 'page'>, opts?: { mapItem?, dedupeBy?, hardStopMaxPages? }): Promise<T[]>`
78
- - `buildAssetUrl(path?: string | null): string`
79
- - `downloadFile(path: string): Promise<Blob>`
80
-
81
- - Auth
82
- - `AuthProvider`: `getAccessToken(opts?: { forceRefresh?: boolean }): Promise<string>`
83
- - `KeycloakClientCredentialsAuth({ tokenUrl, clientId, clientSecret })`
84
-
85
- - Types
86
- - `ContentDto<C extends Record<string, JsonValue>>`
87
- - `ApiResponse<T>`
88
- - `PaginatedData<T>`
89
- - `ContentResponse<C>`
90
- - `ContentListParams`:
91
- - `type?`, `page?`, `size?`, `sortBy?`, `direction?`
92
- - `filters?`: arbitrary query params
93
-
94
- - Errors
95
- - `CmsError extends Error` with `.status?: number` and `.data?: unknown`
96
-
97
- ### Mapping to your app models
98
-
99
- Keep domain mapping out of the SDK. Define custom fields and a mapper in your app:
100
-
101
- ```ts
102
- // Your custom fields
103
- type MyCustomFields = {
104
- title?: string;
105
- text?: string;
106
- insideImage?: string;
107
- outsideImage?: string;
108
- references?: string | null;
109
- pdfPath?: string | null;
110
- citation?: string | null;
111
- abstract?: string | null;
112
- team?: string | null;
113
- publicationType?: 'GAMEHEARTS' | 'EXTERNAL' | null;
114
- fake?: boolean | null;
115
- };
116
-
117
- // Your view model
118
- type NormalizedItem = {
77
+ const items = response.data.content;
78
+ ```
79
+
80
+ ### 3. Fetch Content (React Query)
81
+
82
+ ```typescript
83
+ import { useQuery } from '@tanstack/react-query';
84
+ import { contentQueries } from '@codesocietyou/contentedge-cms-sdk';
85
+
86
+ function NewsList() {
87
+ const { data, isLoading, error } = useQuery(
88
+ contentQueries.list({ type: 'NEWS', page: 0, size: 10 })
89
+ );
90
+
91
+ if (isLoading) return <div>Loading...</div>;
92
+ if (error) return <div>Error: {error.message}</div>;
93
+
94
+ return (
95
+ <ul>
96
+ {data.data.content.map(item => (
97
+ <li key={item.id}>{item.title}</li>
98
+ ))}
99
+ </ul>
100
+ );
101
+ }
102
+ ```
103
+
104
+ ### 4. Normalization
105
+
106
+ ```typescript
107
+ import {
108
+ normalizeContentItem,
109
+ fetchContentByType
110
+ } from '@codesocietyou/contentedge-cms-sdk';
111
+
112
+ // Fetch and normalize
113
+ const response = await fetchContentByType({ type: 'NEWS' });
114
+ const normalized = response.data.content.map(normalizeContentItem);
115
+
116
+ // normalized items have resolved asset URLs and standardized fields
117
+ console.log(normalized[0].insideImage); // "https://cdn.contentedge.com/images/news.jpg"
118
+ console.log(normalized[0].pdfPath); // "https://cdn.contentedge.com/files/doc.pdf"
119
+ ```
120
+
121
+ ## API Reference
122
+
123
+ ### Configuration
124
+
125
+ #### `createApiClient(config: SdkConfig): AxiosInstance`
126
+
127
+ Initialize the SDK with your CMS configuration. Call this once at app startup.
128
+
129
+ ```typescript
130
+ interface SdkConfig {
131
+ baseUrl: string; // CMS API base URL (required)
132
+ fileBaseUrl?: string; // Preferred file/asset base
133
+ apiKey?: string; // API key for authentication
134
+ tenant?: string; // Tenant identifier (sent as X-Tenant header)
135
+ timeoutMs?: number; // Request timeout (default: 30000)
136
+ logger?: {
137
+ debug?: (...args: unknown[]) => void;
138
+ warn?: (...args: unknown[]) => void;
139
+ error?: (...args: unknown[]) => void;
140
+ };
141
+ }
142
+ ```
143
+
144
+ ### Service Layer
145
+
146
+ #### `fetchContentByType<C>(params: ContentListParams): Promise<ContentResponse<C>>`
147
+
148
+ Fetch paginated content by type with filters and sorting.
149
+
150
+ ```typescript
151
+ interface ContentListParams {
152
+ type?: string; // Content type (default: 'ALL')
153
+ page?: number; // Page number (0-indexed)
154
+ size?: number; // Items per page
155
+ sortBy?: string; // Sort field (default: 'id')
156
+ direction?: 'ASC' | 'DESC'; // Sort direction (default: 'DESC')
157
+ filters?: Record<string, any>; // Arbitrary query filters
158
+ }
159
+ ```
160
+
161
+ #### `fetchContentById<C>(id: number): Promise<ApiResponse<ContentDto<C>>>`
162
+
163
+ Fetch a single content item by ID.
164
+
165
+ #### `fetchAllContent<C, T>(params, options): Promise<T[]>`
166
+
167
+ Fetch all content items across multiple pages with automatic pagination.
168
+
169
+ ```typescript
170
+ interface FetchAllOptions<C, T> {
171
+ mapItem?: (item: ContentDto<C>) => T; // Transform each item
172
+ dedupeBy?: (item: T) => string | number; // Dedupe key extractor
173
+ hardStopMaxPages?: number; // Safety limit (default: 20)
174
+ }
175
+ ```
176
+
177
+ #### `downloadFile(path: string): Promise<Blob>`
178
+
179
+ Download a file from a given path (with proper authentication for CMS files).
180
+
181
+ ### React Query Integration
182
+
183
+ #### `contentQueries`
184
+
185
+ Query options factory for use with `useQuery`:
186
+
187
+ ```typescript
188
+ // Paginated list
189
+ contentQueries.list({ type: 'NEWS', page: 0, size: 10 })
190
+
191
+ // Single item
192
+ contentQueries.detail(123)
193
+
194
+ // Fetch all (across pages)
195
+ contentQueries.listAll({ type: 'NEWS', size: 100 }, { mapItem: normalizeContentItem })
196
+ ```
197
+
198
+ #### Hooks
199
+
200
+ ```typescript
201
+ // List hook
202
+ const { data } = useContentList({ type: 'NEWS', page: 0, size: 10 });
203
+
204
+ // Detail hook
205
+ const { data } = useContentDetail(123);
206
+
207
+ // Fetch all hook
208
+ const { data } = useContentAll({ type: 'NEWS' });
209
+
210
+ // Prefetch utilities
211
+ const { prefetchList, prefetchDetail, invalidateLists } = useContentPrefetch();
212
+ ```
213
+
214
+ #### Query Keys
215
+
216
+ Hierarchical query key factory for manual cache manipulation:
217
+
218
+ ```typescript
219
+ import { contentKeys } from '@codesocietyou/contentedge-cms-sdk';
220
+
221
+ // Invalidate all lists
222
+ queryClient.invalidateQueries({ queryKey: contentKeys.lists() });
223
+
224
+ // Invalidate specific detail
225
+ queryClient.invalidateQueries({ queryKey: contentKeys.detail(123) });
226
+ ```
227
+
228
+ ### Normalization
229
+
230
+ #### `normalizeContentItem<C>(item: ContentDto<C>): NormalizedContentItem`
231
+
232
+ Transform a content item to a normalized structure with resolved asset URLs:
233
+
234
+ ```typescript
235
+ interface NormalizedContentItem {
119
236
  id: number;
120
237
  title: string;
121
238
  text: string;
122
- insideImage: string;
123
- outsideImage: string;
124
- references: string | null;
125
- pdfPath: string | null;
126
239
  type: string;
240
+ insideImage: string; // Resolved URL
241
+ outsideImage: string; // Resolved URL
242
+ pdfPath: string | null; // Resolved URL (type-aware)
243
+ references: string | null;
127
244
  citation: string | null;
128
- fake: boolean | null;
129
245
  abstract: string | null;
130
246
  team: string | null;
131
- publicationType: 'GAMEHEARTS' | 'EXTERNAL' | null;
132
- };
133
-
134
- const mapToNormalized = (item: ContentDto<MyCustomFields>): NormalizedItem => ({
135
- id: item.id,
136
- title: item.customFields.title || item.title,
137
- text: item.customFields.text || item.text,
138
- insideImage: contentedge.buildAssetUrl(item.customFields.insideImage || ''),
139
- outsideImage: contentedge.buildAssetUrl(item.customFields.outsideImage || ''),
140
- references: item.customFields.references ?? null,
141
- pdfPath: contentedge.buildAssetUrl(item.customFields.pdfPath ?? null),
142
- type: item.type,
143
- citation: item.customFields.citation ?? null,
144
- fake: item.customFields.fake ?? null,
145
- abstract: item.customFields.abstract ?? null,
146
- team: item.customFields.team ?? null,
147
- publicationType: item.customFields.publicationType ?? null
148
- });
149
-
150
- // Fetch-all with mapping + dedupe
151
- const items = await contentedge.listAllContent<MyCustomFields, NormalizedItem>(
152
- { type: 'REPORT', size: 100, sortBy: 'id', direction: 'DESC', filters: { publicationType: 'GAMEHEARTS' } },
153
- { mapItem: mapToNormalized, dedupeBy: (i) => i.id }
154
- );
247
+ publicationType: string | null;
248
+ fake: boolean | null;
249
+ }
155
250
  ```
156
251
 
157
- ### React Query (optional pattern)
158
-
159
- ```ts
160
- import { queryOptions } from '@tanstack/react-query';
161
- import type { ContentListParams, ContentDto } from '@codesocietyou/contentedge-cms-sdk';
162
-
163
- export const contentQueries = {
164
- list: (params: ContentListParams) => queryOptions({
165
- queryKey: ['content', 'list', params.type ?? 'ALL', params],
166
- queryFn: () => contentedge.listContent(params),
167
- staleTime: 5 * 60_000,
168
- gcTime: 10 * 60_000
169
- }),
170
- detail: (id: number) => queryOptions({
171
- queryKey: ['content', 'detail', id],
172
- queryFn: () => contentedge.getContentById(id),
173
- staleTime: 10 * 60_000,
174
- gcTime: 30 * 60_000
175
- }),
176
- listAll: <C, T = ContentDto<C>>(params: Omit<ContentListParams, 'page'>, mapItem: (i: ContentDto<C>) => T) =>
177
- queryOptions({
178
- queryKey: ['content', 'all', params.type ?? 'ALL', { ...params, mode: 'all' }],
179
- queryFn: () => contentedge.listAllContent(params, { mapItem }),
180
- staleTime: 5 * 60_000,
181
- gcTime: 10 * 60_000
182
- })
183
- };
184
- ```
185
-
186
- ### Error handling
187
-
188
- ```ts
252
+ #### `buildAssetUrlWithConfig(path?: string | null): string`
253
+
254
+ Build an asset URL using the SDK's configured base URLs.
255
+
256
+ ### Error Handling
257
+
258
+ ```typescript
259
+ import { CmsError } from '@codesocietyou/contentedge-cms-sdk';
260
+
189
261
  try {
190
- await contentedge.listContent({ type: 'NEWS' });
262
+ await fetchContentByType({ type: 'NEWS' });
191
263
  } catch (e) {
192
264
  if (e instanceof CmsError) {
193
- console.error('ContentEdge error', e.status, e.data);
265
+ console.error('CMS error:', e.status, e.data);
266
+ // e.status: HTTP status code
267
+ // e.data: Response body (if any)
194
268
  } else {
195
- console.error('Unknown error', e);
269
+ console.error('Unknown error:', e);
196
270
  }
197
271
  }
198
272
  ```
199
273
 
200
- 401s are retried once with a forced token refresh when an `AuthProvider` is provided.
274
+ ## Advanced Usage
201
275
 
202
- ### Security
276
+ ### Custom Fields Type
203
277
 
204
- - `KeycloakClientCredentialsAuth` is intended for server-side usage. Do not expose client secrets in browsers.
205
- - For browsers, you can implement a simple bearer token strategy:
278
+ Define your own custom fields type for full type safety:
206
279
 
207
- ```ts
208
- class BearerTokenAuth implements AuthProvider {
209
- constructor(private readonly getToken: () => Promise<string> | string) {}
210
- async getAccessToken() {
211
- return typeof this.getToken === 'function' ? await this.getToken() : this.getToken;
212
- }
280
+ ```typescript
281
+ interface MyCustomFields {
282
+ title: string;
283
+ text: string;
284
+ author?: string;
285
+ tags?: string[];
286
+ publishedAt?: string;
287
+ }
288
+
289
+ // Use with type parameter
290
+ const response = await fetchContentByType<MyCustomFields>({ type: 'ARTICLE' });
291
+ const items = response.data.content; // ContentDto<MyCustomFields>[]
292
+ ```
293
+
294
+ ### Custom Normalization
295
+
296
+ ```typescript
297
+ interface MyNormalizedItem {
298
+ id: number;
299
+ title: string;
300
+ author: string;
301
+ tags: string[];
302
+ }
303
+
304
+ function myNormalize(item: ContentDto<MyCustomFields>): MyNormalizedItem {
305
+ return {
306
+ id: item.id,
307
+ title: item.customFields.title || item.title,
308
+ author: item.customFields.author || 'Unknown',
309
+ tags: item.customFields.tags || [],
310
+ };
311
+ }
312
+
313
+ // Use with fetchAllContent
314
+ const items = await fetchAllContent<MyCustomFields, MyNormalizedItem>(
315
+ { type: 'ARTICLE', size: 100 },
316
+ { mapItem: myNormalize }
317
+ );
318
+ ```
319
+
320
+ ### React Query Configuration
321
+
322
+ ```typescript
323
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
324
+ import { createApiClient } from '@codesocietyou/contentedge-cms-sdk';
325
+
326
+ // Initialize SDK
327
+ createApiClient({
328
+ baseUrl: process.env.VITE_API_URL!,
329
+ apiKey: process.env.VITE_API_KEY,
330
+ tenant: 'my-tenant',
331
+ });
332
+
333
+ // Create query client
334
+ const queryClient = new QueryClient({
335
+ defaultOptions: {
336
+ queries: {
337
+ retry: 2,
338
+ refetchOnWindowFocus: false,
339
+ },
340
+ },
341
+ });
342
+
343
+ // Wrap app
344
+ function App() {
345
+ return (
346
+ <QueryClientProvider client={queryClient}>
347
+ <YourApp />
348
+ </QueryClientProvider>
349
+ );
213
350
  }
214
351
  ```
215
352
 
216
- - `downloadFile` avoids sending Authorization headers to non-CMS domains.
217
- - Prefer runtime configuration to inject secrets; limit scopes on your Keycloak client.
353
+ ## Migration from v0.2.x
218
354
 
219
- ### Endpoint assumptions
355
+ The v1.0.0 release includes breaking changes:
220
356
 
221
- By default, the SDK uses:
222
- - `GET /content/type/:type`
223
- - `GET /content/:id`
357
+ ### Removed
224
358
 
225
- If your deployment differs, wrap or extend `CmsClient`.
359
+ - `CmsClient` class Use service functions + `createApiClient()`
360
+ - `KeycloakClientCredentialsAuth` → Use API key authentication
361
+ - `AuthProvider` interface → No longer needed
226
362
 
227
- ### Versioning
363
+ ### Migration Steps
228
364
 
229
- Semantic Versioning (SemVer). Breaking changes bump MAJOR.
365
+ **Before (v0.2.x):**
230
366
 
231
- ### License
367
+ ```typescript
368
+ import { CmsClient, KeycloakClientCredentialsAuth } from '@codesocietyou/contentedge-cms-sdk';
369
+
370
+ const auth = new KeycloakClientCredentialsAuth({
371
+ tokenUrl: 'https://auth.example.com/token',
372
+ clientId: 'client',
373
+ clientSecret: 'secret',
374
+ });
375
+
376
+ const client = new CmsClient({
377
+ baseUrl: 'https://api.example.com',
378
+ auth,
379
+ });
380
+
381
+ const list = await client.listContent({ type: 'NEWS' });
382
+ const detail = await client.getContentById(123);
383
+ ```
384
+
385
+ **After (v1.0.0):**
386
+
387
+ ```typescript
388
+ import {
389
+ createApiClient,
390
+ fetchContentByType,
391
+ fetchContentById
392
+ } from '@codesocietyou/contentedge-cms-sdk';
393
+
394
+ // Initialize once
395
+ createApiClient({
396
+ baseUrl: 'https://api.example.com',
397
+ apiKey: 'your-api-key',
398
+ });
399
+
400
+ // Use service functions
401
+ const list = await fetchContentByType({ type: 'NEWS' });
402
+ const detail = await fetchContentById(123);
403
+ ```
404
+
405
+ ## Security
406
+
407
+ - **API Keys**: Store in environment variables, never commit to source control
408
+ - **Runtime Configuration**: Inject secrets at runtime in production
409
+ - **CORS**: Ensure your CMS API allows requests from your domain
410
+ - **Rate Limiting**: The SDK logs 429 errors; implement retry logic if needed
411
+
412
+ ## ContentEdge CMS Endpoints
413
+
414
+ This SDK is designed specifically for the ContentEdge CMS API. The endpoint paths are fixed as part of the CMS API contract:
415
+
416
+ - `GET /content/type/:type` - List content by type with pagination
417
+ - `GET /content/:id` - Get single content item by ID
418
+
419
+ ### Environment Configuration
420
+
421
+ The `baseUrl` configuration allows you to connect to different ContentEdge CMS deployments:
422
+
423
+ **Development:**
424
+ ```typescript
425
+ createApiClient({ baseUrl: 'http://localhost:8080/api' });
426
+ ```
427
+
428
+ **Staging:**
429
+ ```typescript
430
+ createApiClient({ baseUrl: 'https://staging-cms.contentedge.com/api' });
431
+ ```
432
+
433
+ **Production:**
434
+ ```typescript
435
+ createApiClient({ baseUrl: 'https://cms.contentedge.com/api' });
436
+ ```
437
+
438
+ This deployment flexibility is intentional and does not mean the SDK supports different API contracts. All ContentEdge CMS instances use the same endpoint structure.
439
+
440
+ ## Contributing
441
+
442
+ Contributions are welcome! Please follow the existing code style and add tests for new features.
443
+
444
+ ## Versioning
445
+
446
+ This project follows [Semantic Versioning](https://semver.org/). Breaking changes bump MAJOR.
447
+
448
+ ## License
232
449
 
233
450
  MIT
451
+
452
+ ## Support
453
+
454
+ - Issues: https://github.com/ParapluOU/contentedge-cms-sdk/issues
455
+ - Docs: https://github.com/ParapluOU/contentedge-cms-sdk#readme
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=s(require(`axios`)),l=s(require(`@tanstack/react-query`));var u=class extends Error{status;data;constructor(e,t){super(e,t?.cause?{cause:t.cause}:void 0),this.name=`CmsError`,this.status=t?.status,this.data=t?.data}};let d=null;function f(e){return e?/^(\$\{[^}]+\}|\$[A-Z_]+|\{\{[^}]+\}\})$/.test(e.trim()):!1}function p(){if(!d)throw Error(`SDK not initialized. Call createApiClient() first.`);return d}function m(e){let t=e.baseUrl.replace(/\/+$/,``),n=e.fileBaseUrl?.replace(/\/+$/,``);if(!t||f(t))throw Error(`Invalid baseUrl: must be a valid URL string`);d={...e,baseUrl:t,fileBaseUrl:n,timeoutMs:e.timeoutMs??3e4}}function h(){return d!==null}function g(){d=null}let _=null;function v(e){m(e);let t=p();return _=c.default.create({baseURL:t.baseUrl,timeout:t.timeoutMs??3e4,headers:{"Content-Type":`application/json`,Accept:`application/json`,...t.tenant?{"X-Tenant":t.tenant}:{},...t.apiKey?{"X-API-Key":t.apiKey}:{}}}),_.interceptors.response.use(e=>e,e=>{let n=e.response?.status,r=e.response?.data;if(t.logger?.error){let i=`API request failed`;n&&(i+=` (${n})`),n===401&&(i+=`: Unauthorized - check your API key`),n===403&&(i+=`: Permission denied`),n===404&&(i+=`: Resource not found`),n===429&&(i+=`: Rate limit exceeded`),n&&n>=500&&(i+=`: Server error`),t.logger.error(i,{status:n,data:r,url:e.config?.url})}throw new u(`CMS request failed${n?` (${n})`:``}`,{status:n,data:r,cause:e})}),_}function y(){if(!_||!h())throw Error(`API client not initialized. Call createApiClient() first.`);return _}function b(){_=null}const x=new Proxy({},{get(e,t){let n=y(),r=n[t];return typeof r==`function`?r.bind(n):r}});async function S(e={}){let t=y(),n=encodeURIComponent(e.type??`ALL`),r=Number.isInteger(e.page)&&e.page>=0?e.page:0,i=Number.isInteger(e.size)&&e.size>0&&e.size<=1e3?e.size:10,a=new URLSearchParams({page:String(r),size:String(i),sortBy:e.sortBy??`id`,direction:e.direction??`DESC`});if(e.filters)for(let[t,n]of Object.entries(e.filters)){if(n==null)continue;a.append(t,String(n))}let o=`/content/type/${n}?${a.toString()}`,s=await t.get(o);return s.data}async function C(e){let t=y(),n=await t.get(`/content/${e}`);return n.data}async function w(e){let t=p(),n=e.toLowerCase(),r=(t.baseUrl||``).toLowerCase(),i=(t.fileBaseUrl||``).toLowerCase(),a=r&&n.startsWith(r)||i&&n.startsWith(i);if(a){let t=y(),n=await t.get(e,{responseType:`blob`});return n.data}let o=await c.default.get(e,{responseType:`blob`});return o.data}async function T(e,t={}){let n=e.size??100,r=e.sortBy??`id`,i=e.direction??`DESC`,a=t.mapItem??(e=>e),o=t.dedupeBy??(e=>e.id),s=t.hardStopMaxPages??20,c=0,l=new Map;for(let t=0;t<s;t++){let t=await S({...e,page:c,size:n,sortBy:r,direction:i}),s=t?.data;if(!s)break;let u=s.content??[];if(u.length===0)break;let d=0;for(let e of u){let t=a(e),n=o(t);l.has(n)||(l.set(n,t),d++)}if(d===0)break;let f=typeof s.number==`number`?s.number:c,p=typeof s.size==`number`?s.size:n,m=typeof s.numberOfElements==`number`?s.numberOfElements:u.length,h=typeof s.totalPages==`number`?f+1<s.totalPages:typeof s.totalElements==`number`?f*p+m<s.totalElements:typeof s.last==`boolean`?!s.last:m===p;if(!h)break;c=f+1}return Array.from(l.values())}const E={all:[`content`],lists:()=>[...E.all,`list`],list:(e,t)=>[...E.lists(),e??`ALL`,t],details:()=>[...E.all,`detail`],detail:e=>[...E.details(),e],allLists:()=>[...E.all,`all`],allList:(e,t)=>[...E.allLists(),e??`ALL`,{...t,mode:`all`}]},D={list:(e={})=>(0,l.queryOptions)({queryKey:E.list(e.type,e),queryFn:()=>S(e),staleTime:5*6e4,gcTime:10*6e4}),detail:e=>(0,l.queryOptions)({queryKey:E.detail(e),queryFn:()=>C(e),staleTime:10*6e4,gcTime:30*6e4}),listAll:(e,t={})=>(0,l.queryOptions)({queryKey:E.allList(e.type,e),queryFn:()=>T(e,t),staleTime:5*6e4,gcTime:10*6e4})};function O(e={}){return(0,l.useQuery)(D.list(e))}function k(e){return(0,l.useQuery)(D.detail(e))}function A(e,t={}){return(0,l.useQuery)(D.listAll(e,t))}function j(){let e=(0,l.useQueryClient)();return{prefetchList:t=>e.prefetchQuery(D.list(t)),prefetchDetail:t=>e.prefetchQuery(D.detail(t)),invalidateLists:()=>e.invalidateQueries({queryKey:E.lists()}),invalidateDetail:t=>e.invalidateQueries({queryKey:E.detail(t)}),invalidateAll:()=>e.invalidateQueries({queryKey:E.all})}}const M=(e,t)=>{if(!e)return``;let n=e.trim();if(!n)return``;let r=(t?.apiBase||``).replace(/\/+$/,``),i=(t?.fileBase||``).replace(/\/+$/,``),a=r.replace(/\/api$/,``),o=i||a||r,s=n.toLowerCase(),c=s.startsWith(`http://`)||s.startsWith(`https://`)||s.startsWith(`data:`)||s.startsWith(`blob:`)||n.startsWith(`//`);if(c){if(r&&o&&s.startsWith(r.toLowerCase())){let e=n.slice(r.length);return`${o}${e.startsWith(`/`)?``:`/`}${e}`}let e=`${a}/api`.toLowerCase();if(a&&o&&s.startsWith(e)){let e=n.slice((a+`/api`).length);return`${o}${e.startsWith(`/`)?``:`/`}${e}`}return i&&s.startsWith(i.toLowerCase()),n}let l=n.replace(/^\/+/,``);return l=l.replace(/^api\//,``),o?`${o}/${l}`:`/${l}`};function N(e){let t=p(),n=e.customFields,r=e=>{let t=n[e];return typeof t==`string`?t:null},i=()=>{let t=e.type.toUpperCase();return t===`GAMEHEARTS_PUBLICATION`||t===`REPORT`?r(`publication_pdf`)??r(`pdfPath`):t===`EXTERNAL_PUBLICATION`?r(`pdf`)??r(`pdfPath`):r(`pdfPath`)},a=i();return{id:e.id,title:r(`title`)||e.title,text:r(`text`)||e.text,type:e.type,insideImage:M(r(`insideImage`)||``,{apiBase:t.baseUrl,fileBase:t.fileBaseUrl}),outsideImage:M(r(`outsideImage`)||``,{apiBase:t.baseUrl,fileBase:t.fileBaseUrl}),pdfPath:M(a,{apiBase:t.baseUrl,fileBase:t.fileBaseUrl}),references:r(`referencesList`)??r(`references`),citation:r(`citation`),abstract:r(`abstractText`)??r(`abstract`),team:r(`team`),publicationType:r(`publicationType`),fake:n.fake===!0?!0:null}}function P(e){let t=p();return M(e,{apiBase:t.baseUrl,fileBase:t.fileBaseUrl})}exports.CmsError=u,exports.apiClient=x,exports.buildAssetUrl=M,exports.buildAssetUrlWithConfig=P,exports.contentKeys=E,exports.contentQueries=D,exports.createApiClient=v,exports.downloadFile=w,exports.fetchAllContent=T,exports.fetchContentById=C,exports.fetchContentByType=S,exports.getApiClient=y,exports.getConfig=p,exports.isInitialized=h,exports.normalizeContentItem=N,exports.resetApiClient=b,exports.resetConfig=g,exports.setConfig=m,exports.useContentAll=A,exports.useContentDetail=k,exports.useContentList=O,exports.useContentPrefetch=j;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["message: string","opts?: { status?: number; data?: unknown; cause?: unknown }","currentConfig: SdkConfig | null","value: string | undefined","config: SdkConfig","axiosInstance: AxiosInstance | null","config: SdkConfig","response: AxiosResponse","error: AxiosError","params: ContentListParams","id: number","path: string","response","params: Omit<ContentListParams, 'page'> & { page?: never }","options: FetchAllOptions<C, T>","item: ContentDto<C>","item: T","type: string | undefined","params: ContentListParams","id: number","params: Omit<ContentListParams, 'page'>","params: ContentListParams","id: number","params: Omit<ContentListParams, 'page'> & { page?: never }","options: FetchAllOptions<C, T>","params: ContentListParams","id: number","params: Omit<ContentListParams, 'page'> & { page?: never }","options: FetchAllOptions<C, T>","rawPath?: string | null","opts?: BuildAssetUrlOpts","item: ContentDto<C>","key: string","path?: string | null"],"sources":["../src/errors/CmsError.ts","../src/config/sdkConfig.ts","../src/services/apiClient.ts","../src/services/contentApi.ts","../src/services/contentFetchAll.ts","../src/queries/queryKeys.ts","../src/queries/contentQueries.ts","../src/queries/useContentQueries.ts","../src/utils/assetUrl.ts","../src/utils/normalization.ts"],"sourcesContent":["export class CmsError extends Error {\n readonly status?: number;\n readonly data?: unknown;\n constructor(message: string, opts?: { status?: number; data?: unknown; cause?: unknown }) {\n super(message, opts?.cause ? { cause: opts.cause } : undefined);\n this.name = 'CmsError';\n this.status = opts?.status;\n this.data = opts?.data;\n }\n}","// src/config/sdkConfig.ts\nimport type { SdkConfig } from '../types/config';\n\nlet currentConfig: SdkConfig | null = null;\n\n/**\n * Checks if a value looks like an unresolved CI/CD template variable\n */\nfunction isTemplateString(value: string | undefined): boolean {\n if (!value) return false;\n // Detect common CI/CD template patterns: ${VAR}, $VAR, {{VAR}}, etc.\n return /^(\\$\\{[^}]+\\}|\\$[A-Z_]+|\\{\\{[^}]+\\}\\})$/.test(value.trim());\n}\n\n/**\n * Get the current SDK configuration\n */\nexport function getConfig(): SdkConfig {\n if (!currentConfig) {\n throw new Error('SDK not initialized. Call createApiClient() first.');\n }\n return currentConfig;\n}\n\n/**\n * Set the SDK configuration\n */\nexport function setConfig(config: SdkConfig): void {\n // Clean up base URLs\n const baseUrl = config.baseUrl.replace(/\\/+$/, '');\n const fileBaseUrl = config.fileBaseUrl?.replace(/\\/+$/, '');\n\n // Validate required fields\n if (!baseUrl || isTemplateString(baseUrl)) {\n throw new Error('Invalid baseUrl: must be a valid URL string');\n }\n\n currentConfig = {\n ...config,\n baseUrl,\n fileBaseUrl,\n timeoutMs: config.timeoutMs ?? 30_000,\n };\n}\n\n/**\n * Check if SDK is initialized\n */\nexport function isInitialized(): boolean {\n return currentConfig !== null;\n}\n\n/**\n * Reset configuration (useful for testing)\n */\nexport function resetConfig(): void {\n currentConfig = null;\n}\n","import axios, { type AxiosInstance, type AxiosResponse, AxiosError } from 'axios';\nimport { CmsError } from '../errors/CmsError';\nimport { getConfig, setConfig, isInitialized } from '../config/sdkConfig';\nimport type { SdkConfig } from '../types/config';\n\nlet axiosInstance: AxiosInstance | null = null;\n\n/**\n * Create and configure the API client with the given configuration\n */\nexport function createApiClient(config: SdkConfig): AxiosInstance {\n setConfig(config);\n \n const cfg = getConfig();\n \n axiosInstance = axios.create({\n baseURL: cfg.baseUrl,\n timeout: cfg.timeoutMs ?? 30_000,\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n ...(cfg.tenant ? { 'X-Tenant': cfg.tenant } : {}),\n ...(cfg.apiKey ? { 'X-API-Key': cfg.apiKey } : {}),\n },\n });\n\n // Response interceptor for error handling\n axiosInstance.interceptors.response.use(\n (response: AxiosResponse) => response,\n (error: AxiosError) => {\n const status = error.response?.status;\n const data = error.response?.data;\n \n // Log errors if logger is configured\n if (cfg.logger?.error) {\n let message = `API request failed`;\n if (status) message += ` (${status})`;\n if (status === 401) message += ': Unauthorized - check your API key';\n if (status === 403) message += ': Permission denied';\n if (status === 404) message += ': Resource not found';\n if (status === 429) message += ': Rate limit exceeded';\n if (status && status >= 500) message += ': Server error';\n \n cfg.logger.error(message, { status, data, url: error.config?.url });\n }\n \n throw new CmsError(\n `CMS request failed${status ? ` (${status})` : ''}`,\n { status, data, cause: error }\n );\n }\n );\n\n return axiosInstance;\n}\n\n/**\n * Get the current API client instance\n * Throws if not initialized\n */\nexport function getApiClient(): AxiosInstance {\n if (!axiosInstance || !isInitialized()) {\n throw new Error('API client not initialized. Call createApiClient() first.');\n }\n return axiosInstance;\n}\n\n/**\n * Reset the API client (useful for testing)\n */\nexport function resetApiClient(): void {\n axiosInstance = null;\n}\n\n// Export a default instance getter for convenience\nexport const apiClient = new Proxy({} as AxiosInstance, {\n get(_target, prop) {\n const instance = getApiClient();\n const value = instance[prop as keyof AxiosInstance];\n return typeof value === 'function' ? value.bind(instance) : value;\n },\n});\n","import axios from 'axios';\nimport { getApiClient } from './apiClient';\nimport { getConfig } from '../config/sdkConfig';\nimport type {\n ApiResponse,\n ContentDto,\n ContentListParams,\n ContentResponse,\n CustomFields,\n} from '../types/content';\n\n/**\n * Fetch content by type with pagination and filters\n */\nexport async function fetchContentByType<C extends CustomFields = CustomFields>(\n params: ContentListParams = {}\n): Promise<ContentResponse<C>> {\n const client = getApiClient();\n \n // Sanitize and validate inputs\n const type = encodeURIComponent(params.type ?? 'ALL');\n const page = Number.isInteger(params.page) && (params.page as number) >= 0 ? params.page : 0;\n const size = Number.isInteger(params.size) && (params.size as number) > 0 && (params.size as number) <= 1000\n ? params.size\n : 10;\n\n // Build query parameters\n const queryParams = new URLSearchParams({\n page: String(page),\n size: String(size),\n sortBy: params.sortBy ?? 'id',\n direction: params.direction ?? 'DESC',\n });\n\n // Add arbitrary filters\n if (params.filters) {\n for (const [key, value] of Object.entries(params.filters)) {\n if (value === undefined || value === null) continue;\n queryParams.append(key, String(value));\n }\n }\n\n const url = `/content/type/${type}?${queryParams.toString()}`;\n const response = await client.get<ContentResponse<C>>(url);\n return response.data;\n}\n\n/**\n * Fetch a single content item by ID\n */\nexport async function fetchContentById<C extends CustomFields = CustomFields>(\n id: number\n): Promise<ApiResponse<ContentDto<C>>> {\n const client = getApiClient();\n const response = await client.get<ApiResponse<ContentDto<C>>>(`/content/${id}`);\n return response.data;\n}\n\n/**\n * Download a file from a given path\n * @param path - Full URL or relative path to the file\n * @returns Blob containing the file data\n */\nexport async function downloadFile(path: string): Promise<Blob> {\n const config = getConfig();\n const lower = path.toLowerCase();\n const api = (config.baseUrl || '').toLowerCase();\n const file = (config.fileBaseUrl || '').toLowerCase();\n \n // Check if this is a CMS-hosted file\n const isCmsFile = (api && lower.startsWith(api)) || (file && lower.startsWith(file));\n\n if (isCmsFile) {\n // Use authenticated client for CMS files\n const client = getApiClient();\n const response = await client.get(path, { responseType: 'blob' });\n return response.data as Blob;\n }\n \n // For external files, use axios directly without auth headers\n const response = await axios.get(path, { responseType: 'blob' });\n return response.data as Blob;\n}\n","import { fetchContentByType } from './contentApi';\nimport type {\n ContentDto,\n ContentListParams,\n CustomFields,\n PaginatedData,\n} from '../types/content';\n\nexport interface FetchAllOptions<C extends CustomFields, T> {\n /**\n * Optional function to transform each content item\n */\n mapItem?: (item: ContentDto<C>) => T;\n \n /**\n * Optional function to extract a unique key for deduplication\n */\n dedupeBy?: (item: T) => string | number;\n \n /**\n * Maximum number of pages to fetch (safety limit)\n * @default 20\n */\n hardStopMaxPages?: number;\n}\n\n/**\n * Fetch all content items across multiple pages with automatic pagination\n * \n * This function will continue fetching pages until:\n * - No more items are returned\n * - The last page is reached (based on pagination metadata)\n * - The hard stop limit is reached\n * - No new items are added (all items are duplicates)\n * \n * @param params - Content list parameters (without page number)\n * @param options - Fetch options including mapping and deduplication\n * @returns Array of all fetched items\n */\nexport async function fetchAllContent<\n C extends CustomFields = CustomFields,\n T = ContentDto<C>\n>(\n params: Omit<ContentListParams, 'page'> & { page?: never },\n options: FetchAllOptions<C, T> = {}\n): Promise<T[]> {\n const size = params.size ?? 100;\n const sortBy = params.sortBy ?? 'id';\n const direction = params.direction ?? 'DESC';\n \n const mapItem = options.mapItem ?? ((item: ContentDto<C>) => item as unknown as T);\n const dedupeBy = options.dedupeBy ?? ((item: T) => (item as { id: string | number }).id);\n const hardStopMaxPages = options.hardStopMaxPages ?? 20;\n\n let page = 0;\n const dedupMap = new Map<string | number, T>();\n\n for (let i = 0; i < hardStopMaxPages; i++) {\n const response = await fetchContentByType<C>({\n ...params,\n page,\n size,\n sortBy,\n direction,\n });\n\n const data = response?.data as PaginatedData<ContentDto<C>> | undefined;\n if (!data) break;\n\n const items = data.content ?? [];\n if (items.length === 0) break;\n\n // Process and deduplicate items\n let addedThisPage = 0;\n for (const item of items) {\n const mapped = mapItem(item);\n const key = dedupeBy(mapped);\n if (!dedupMap.has(key)) {\n dedupMap.set(key, mapped);\n addedThisPage++;\n }\n }\n\n // Stop if no new items were added (all duplicates)\n if (addedThisPage === 0) break;\n\n // Determine if there are more pages\n const current = typeof data.number === 'number' ? data.number : page;\n const pageSize = typeof data.size === 'number' ? data.size : size;\n const numberOfElements =\n typeof data.numberOfElements === 'number' ? data.numberOfElements : items.length;\n\n const hasNext =\n typeof data.totalPages === 'number'\n ? current + 1 < data.totalPages\n : typeof data.totalElements === 'number'\n ? current * pageSize + numberOfElements < data.totalElements\n : typeof data.last === 'boolean'\n ? !data.last\n : numberOfElements === pageSize;\n\n if (!hasNext) break;\n page = current + 1;\n }\n\n return Array.from(dedupMap.values());\n}\n","// src/queries/queryKeys.ts\nimport type { ContentListParams } from '../types/content';\n\n/**\n * Hierarchical query key factory for content queries\n * Following TanStack Query best practices for key structure\n */\nexport const contentKeys = {\n /**\n * Base key for all content queries\n */\n all: ['content'] as const,\n \n /**\n * Key for all list queries\n */\n lists: () => [...contentKeys.all, 'list'] as const,\n \n /**\n * Key for a specific list query with parameters\n * @param type - Content type (e.g., 'NEWS', 'REPORT')\n * @param params - Additional list parameters\n */\n list: (type: string | undefined, params: ContentListParams) =>\n [...contentKeys.lists(), type ?? 'ALL', params] as const,\n \n /**\n * Key for all detail queries\n */\n details: () => [...contentKeys.all, 'detail'] as const,\n \n /**\n * Key for a specific detail query by ID\n * @param id - Content item ID\n */\n detail: (id: number) => [...contentKeys.details(), id] as const,\n \n /**\n * Key for all \"fetch all\" queries\n */\n allLists: () => [...contentKeys.all, 'all'] as const,\n \n /**\n * Key for a specific \"fetch all\" query\n * @param type - Content type\n * @param params - List parameters (without page)\n */\n allList: (type: string | undefined, params: Omit<ContentListParams, 'page'>) =>\n [...contentKeys.allLists(), type ?? 'ALL', { ...params, mode: 'all' }] as const,\n};\n","import { queryOptions } from '@tanstack/react-query';\nimport { fetchContentByType, fetchContentById } from '../services/contentApi';\nimport { fetchAllContent, type FetchAllOptions } from '../services/contentFetchAll';\nimport { contentKeys } from './queryKeys';\nimport type {\n ContentDto,\n ContentListParams,\n CustomFields,\n} from '../types/content';\n\n/**\n * Query options factory for content queries\n * Use with TanStack Query's useQuery hook\n */\nexport const contentQueries = {\n /**\n * Query options for paginated content list\n * @param params - List parameters including type, pagination, filters\n */\n list: <C extends CustomFields = CustomFields>(params: ContentListParams = {}) =>\n queryOptions({\n queryKey: contentKeys.list(params.type, params),\n queryFn: () => fetchContentByType<C>(params),\n staleTime: 5 * 60_000, // 5 minutes\n gcTime: 10 * 60_000, // 10 minutes (formerly cacheTime)\n }),\n\n /**\n * Query options for a single content item by ID\n * @param id - Content item ID\n */\n detail: <C extends CustomFields = CustomFields>(id: number) =>\n queryOptions({\n queryKey: contentKeys.detail(id),\n queryFn: () => fetchContentById<C>(id),\n staleTime: 10 * 60_000, // 10 minutes\n gcTime: 30 * 60_000, // 30 minutes\n }),\n\n /**\n * Query options for fetching all content items (across all pages)\n * @param params - List parameters (without page)\n * @param options - Fetch options including mapping and deduplication\n */\n listAll: <C extends CustomFields = CustomFields, T = ContentDto<C>>(\n params: Omit<ContentListParams, 'page'> & { page?: never },\n options: FetchAllOptions<C, T> = {}\n ) =>\n queryOptions({\n queryKey: contentKeys.allList(params.type, params),\n queryFn: () => fetchAllContent<C, T>(params, options),\n staleTime: 5 * 60_000, // 5 minutes\n gcTime: 10 * 60_000, // 10 minutes\n }),\n};\n","// src/queries/useContentQueries.ts\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport { contentQueries } from './contentQueries';\nimport { contentKeys } from './queryKeys';\nimport type { FetchAllOptions } from '../services/contentFetchAll';\nimport type {\n ApiResponse,\n ContentDto,\n ContentListParams,\n ContentResponse,\n CustomFields,\n} from '../types/content';\n\n/**\n * Hook for fetching paginated content list\n * @param params - List parameters including type, pagination, filters\n */\nexport function useContentList<C extends CustomFields = CustomFields>(\n params: ContentListParams = {}\n): UseQueryResult<ContentResponse<C>> {\n return useQuery(contentQueries.list<C>(params));\n}\n\n/**\n * Hook for fetching a single content item by ID\n * @param id - Content item ID\n */\nexport function useContentDetail<C extends CustomFields = CustomFields>(\n id: number\n): UseQueryResult<ApiResponse<ContentDto<C>>> {\n return useQuery(contentQueries.detail<C>(id));\n}\n\n/**\n * Hook for fetching all content items across pages\n * @param params - List parameters (without page)\n * @param options - Fetch options including mapping and deduplication\n */\nexport function useContentAll<C extends CustomFields = CustomFields, T = ContentDto<C>>(\n params: Omit<ContentListParams, 'page'> & { page?: never },\n options: FetchAllOptions<C, T> = {}\n): UseQueryResult<T[]> {\n return useQuery(contentQueries.listAll<C, T>(params, options));\n}\n\n/**\n * Hook for prefetching content queries\n * Useful for optimistic navigation and hover effects\n */\nexport function useContentPrefetch() {\n const queryClient = useQueryClient();\n\n return {\n /**\n * Prefetch a content list\n */\n prefetchList: <C extends CustomFields = CustomFields>(params: ContentListParams) => {\n return queryClient.prefetchQuery(contentQueries.list<C>(params));\n },\n\n /**\n * Prefetch a content detail\n */\n prefetchDetail: <C extends CustomFields = CustomFields>(id: number) => {\n return queryClient.prefetchQuery(contentQueries.detail<C>(id));\n },\n\n /**\n * Invalidate content lists (force refetch)\n */\n invalidateLists: () => {\n return queryClient.invalidateQueries({ queryKey: contentKeys.lists() });\n },\n\n /**\n * Invalidate a specific content detail\n */\n invalidateDetail: (id: number) => {\n return queryClient.invalidateQueries({ queryKey: contentKeys.detail(id) });\n },\n\n /**\n * Invalidate all content queries\n */\n invalidateAll: () => {\n return queryClient.invalidateQueries({ queryKey: contentKeys.all });\n },\n };\n}\n","// src/utils/assetUrl.ts\ntype BuildAssetUrlOpts = {\n apiBase?: string;\n fileBase?: string;\n};\n\nexport const buildAssetUrl = (rawPath?: string | null, opts?: BuildAssetUrlOpts): string => {\n if (!rawPath) return '';\n const trimmed = rawPath.trim();\n if (!trimmed) return '';\n\n const apiBase = (opts?.apiBase || '').replace(/\\/+$/, '');\n const fileBase = (opts?.fileBase || '').replace(/\\/+$/, '');\n const originBase = apiBase.replace(/\\/api$/, '');\n const preferredBase = fileBase || originBase || apiBase;\n\n const lower = trimmed.toLowerCase();\n const isAbsolute = lower.startsWith('http://')\n || lower.startsWith('https://')\n || lower.startsWith('data:')\n || lower.startsWith('blob:')\n || trimmed.startsWith('//');\n\n if (isAbsolute) {\n if (apiBase && preferredBase && lower.startsWith(apiBase.toLowerCase())) {\n const afterApiBase = trimmed.slice(apiBase.length);\n return `${preferredBase}${afterApiBase.startsWith('/') ? '' : '/'}${afterApiBase}`;\n }\n const originApiPrefix = `${originBase}/api`.toLowerCase();\n if (originBase && preferredBase && lower.startsWith(originApiPrefix)) {\n const afterOriginApi = trimmed.slice((originBase + '/api').length);\n return `${preferredBase}${afterOriginApi.startsWith('/') ? '' : '/'}${afterOriginApi}`;\n }\n if (fileBase && lower.startsWith(fileBase.toLowerCase())) return trimmed;\n return trimmed;\n }\n\n let relative = trimmed.replace(/^\\/+/, '');\n relative = relative.replace(/^api\\//, '');\n return preferredBase ? `${preferredBase}/${relative}` : `/${relative}`;\n};","import { buildAssetUrl } from './assetUrl';\nimport { getConfig } from '../config/sdkConfig';\nimport type { ContentDto, CustomFields, NormalizedContentItem } from '../types/content';\n\n/**\n * Normalize a content item to a standard format with asset URLs resolved\n * \n * This function provides a default normalization strategy that:\n * - Extracts common fields from customFields\n * - Resolves asset URLs using SDK configuration\n * - Handles PDF path resolution for different content types\n * - Provides sensible defaults for missing fields\n * \n * @param item - Raw content item from the API\n * @returns Normalized content item\n */\nexport function normalizeContentItem<C extends CustomFields = CustomFields>(\n item: ContentDto<C>\n): NormalizedContentItem {\n const config = getConfig();\n const customFields = item.customFields as Record<string, unknown>;\n\n // Helper to get custom field value\n const getField = (key: string): string | null => {\n const value = customFields[key];\n return typeof value === 'string' ? value : null;\n };\n\n // Resolve PDF path based on content type\n const resolvePdfPath = (): string | null => {\n const type = item.type.toUpperCase();\n \n // For GAMEHEARTS_PUBLICATION type\n if (type === 'GAMEHEARTS_PUBLICATION' || type === 'REPORT') {\n return getField('publication_pdf') ?? getField('pdfPath');\n }\n \n // For EXTERNAL_PUBLICATION type\n if (type === 'EXTERNAL_PUBLICATION') {\n return getField('pdf') ?? getField('pdfPath');\n }\n \n // Default to pdfPath field\n return getField('pdfPath');\n };\n\n const pdfPath = resolvePdfPath();\n\n return {\n id: item.id,\n title: getField('title') || item.title,\n text: getField('text') || item.text,\n type: item.type,\n insideImage: buildAssetUrl(getField('insideImage') || '', {\n apiBase: config.baseUrl,\n fileBase: config.fileBaseUrl,\n }),\n outsideImage: buildAssetUrl(getField('outsideImage') || '', {\n apiBase: config.baseUrl,\n fileBase: config.fileBaseUrl,\n }),\n pdfPath: buildAssetUrl(pdfPath, {\n apiBase: config.baseUrl,\n fileBase: config.fileBaseUrl,\n }),\n references: getField('referencesList') ?? getField('references'),\n citation: getField('citation'),\n abstract: getField('abstractText') ?? getField('abstract'),\n team: getField('team'),\n publicationType: getField('publicationType'),\n fake: customFields.fake === true ? true : null,\n };\n}\n\n/**\n * Build an asset URL using the SDK's configured base URLs\n * This is a convenience wrapper that automatically uses SDK config\n * \n * @param path - Path to the asset (can be relative or absolute)\n * @returns Full URL to the asset\n */\nexport function buildAssetUrlWithConfig(path?: string | null): string {\n const config = getConfig();\n return buildAssetUrl(path, {\n apiBase: config.baseUrl,\n fileBase: config.fileBaseUrl,\n });\n}\n"],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import e from"axios";import{queryOptions as t,useQuery as n,useQueryClient as r}from"@tanstack/react-query";var i=class extends Error{status;data;constructor(e,t){super(e,t?.cause?{cause:t.cause}:void 0),this.name=`CmsError`,this.status=t?.status,this.data=t?.data}};let a=null;function o(e){return e?/^(\$\{[^}]+\}|\$[A-Z_]+|\{\{[^}]+\}\})$/.test(e.trim()):!1}function s(){if(!a)throw Error(`SDK not initialized. Call createApiClient() first.`);return a}function c(e){let t=e.baseUrl.replace(/\/+$/,``),n=e.fileBaseUrl?.replace(/\/+$/,``);if(!t||o(t))throw Error(`Invalid baseUrl: must be a valid URL string`);a={...e,baseUrl:t,fileBaseUrl:n,timeoutMs:e.timeoutMs??3e4}}function l(){return a!==null}function u(){a=null}let d=null;function f(t){c(t);let n=s();return d=e.create({baseURL:n.baseUrl,timeout:n.timeoutMs??3e4,headers:{"Content-Type":`application/json`,Accept:`application/json`,...n.tenant?{"X-Tenant":n.tenant}:{},...n.apiKey?{"X-API-Key":n.apiKey}:{}}}),d.interceptors.response.use(e=>e,e=>{let t=e.response?.status,r=e.response?.data;if(n.logger?.error){let i=`API request failed`;t&&(i+=` (${t})`),t===401&&(i+=`: Unauthorized - check your API key`),t===403&&(i+=`: Permission denied`),t===404&&(i+=`: Resource not found`),t===429&&(i+=`: Rate limit exceeded`),t&&t>=500&&(i+=`: Server error`),n.logger.error(i,{status:t,data:r,url:e.config?.url})}throw new i(`CMS request failed${t?` (${t})`:``}`,{status:t,data:r,cause:e})}),d}function p(){if(!d||!l())throw Error(`API client not initialized. Call createApiClient() first.`);return d}function m(){d=null}const h=new Proxy({},{get(e,t){let n=p(),r=n[t];return typeof r==`function`?r.bind(n):r}});async function g(e={}){let t=p(),n=encodeURIComponent(e.type??`ALL`),r=Number.isInteger(e.page)&&e.page>=0?e.page:0,i=Number.isInteger(e.size)&&e.size>0&&e.size<=1e3?e.size:10,a=new URLSearchParams({page:String(r),size:String(i),sortBy:e.sortBy??`id`,direction:e.direction??`DESC`});if(e.filters)for(let[t,n]of Object.entries(e.filters)){if(n==null)continue;a.append(t,String(n))}let o=`/content/type/${n}?${a.toString()}`,s=await t.get(o);return s.data}async function _(e){let t=p(),n=await t.get(`/content/${e}`);return n.data}async function v(t){let n=s(),r=t.toLowerCase(),i=(n.baseUrl||``).toLowerCase(),a=(n.fileBaseUrl||``).toLowerCase(),o=i&&r.startsWith(i)||a&&r.startsWith(a);if(o){let e=p(),n=await e.get(t,{responseType:`blob`});return n.data}let c=await e.get(t,{responseType:`blob`});return c.data}async function y(e,t={}){let n=e.size??100,r=e.sortBy??`id`,i=e.direction??`DESC`,a=t.mapItem??(e=>e),o=t.dedupeBy??(e=>e.id),s=t.hardStopMaxPages??20,c=0,l=new Map;for(let t=0;t<s;t++){let t=await g({...e,page:c,size:n,sortBy:r,direction:i}),s=t?.data;if(!s)break;let u=s.content??[];if(u.length===0)break;let d=0;for(let e of u){let t=a(e),n=o(t);l.has(n)||(l.set(n,t),d++)}if(d===0)break;let f=typeof s.number==`number`?s.number:c,p=typeof s.size==`number`?s.size:n,m=typeof s.numberOfElements==`number`?s.numberOfElements:u.length,h=typeof s.totalPages==`number`?f+1<s.totalPages:typeof s.totalElements==`number`?f*p+m<s.totalElements:typeof s.last==`boolean`?!s.last:m===p;if(!h)break;c=f+1}return Array.from(l.values())}const b={all:[`content`],lists:()=>[...b.all,`list`],list:(e,t)=>[...b.lists(),e??`ALL`,t],details:()=>[...b.all,`detail`],detail:e=>[...b.details(),e],allLists:()=>[...b.all,`all`],allList:(e,t)=>[...b.allLists(),e??`ALL`,{...t,mode:`all`}]},x={list:(e={})=>t({queryKey:b.list(e.type,e),queryFn:()=>g(e),staleTime:5*6e4,gcTime:10*6e4}),detail:e=>t({queryKey:b.detail(e),queryFn:()=>_(e),staleTime:10*6e4,gcTime:30*6e4}),listAll:(e,n={})=>t({queryKey:b.allList(e.type,e),queryFn:()=>y(e,n),staleTime:5*6e4,gcTime:10*6e4})};function S(e={}){return n(x.list(e))}function C(e){return n(x.detail(e))}function w(e,t={}){return n(x.listAll(e,t))}function T(){let e=r();return{prefetchList:t=>e.prefetchQuery(x.list(t)),prefetchDetail:t=>e.prefetchQuery(x.detail(t)),invalidateLists:()=>e.invalidateQueries({queryKey:b.lists()}),invalidateDetail:t=>e.invalidateQueries({queryKey:b.detail(t)}),invalidateAll:()=>e.invalidateQueries({queryKey:b.all})}}const E=(e,t)=>{if(!e)return``;let n=e.trim();if(!n)return``;let r=(t?.apiBase||``).replace(/\/+$/,``),i=(t?.fileBase||``).replace(/\/+$/,``),a=r.replace(/\/api$/,``),o=i||a||r,s=n.toLowerCase(),c=s.startsWith(`http://`)||s.startsWith(`https://`)||s.startsWith(`data:`)||s.startsWith(`blob:`)||n.startsWith(`//`);if(c){if(r&&o&&s.startsWith(r.toLowerCase())){let e=n.slice(r.length);return`${o}${e.startsWith(`/`)?``:`/`}${e}`}let e=`${a}/api`.toLowerCase();if(a&&o&&s.startsWith(e)){let e=n.slice((a+`/api`).length);return`${o}${e.startsWith(`/`)?``:`/`}${e}`}return i&&s.startsWith(i.toLowerCase()),n}let l=n.replace(/^\/+/,``);return l=l.replace(/^api\//,``),o?`${o}/${l}`:`/${l}`};function D(e){let t=s(),n=e.customFields,r=e=>{let t=n[e];return typeof t==`string`?t:null},i=()=>{let t=e.type.toUpperCase();return t===`GAMEHEARTS_PUBLICATION`||t===`REPORT`?r(`publication_pdf`)??r(`pdfPath`):t===`EXTERNAL_PUBLICATION`?r(`pdf`)??r(`pdfPath`):r(`pdfPath`)},a=i();return{id:e.id,title:r(`title`)||e.title,text:r(`text`)||e.text,type:e.type,insideImage:E(r(`insideImage`)||``,{apiBase:t.baseUrl,fileBase:t.fileBaseUrl}),outsideImage:E(r(`outsideImage`)||``,{apiBase:t.baseUrl,fileBase:t.fileBaseUrl}),pdfPath:E(a,{apiBase:t.baseUrl,fileBase:t.fileBaseUrl}),references:r(`referencesList`)??r(`references`),citation:r(`citation`),abstract:r(`abstractText`)??r(`abstract`),team:r(`team`),publicationType:r(`publicationType`),fake:n.fake===!0?!0:null}}function O(e){let t=s();return E(e,{apiBase:t.baseUrl,fileBase:t.fileBaseUrl})}export{i as CmsError,h as apiClient,E as buildAssetUrl,O as buildAssetUrlWithConfig,b as contentKeys,x as contentQueries,f as createApiClient,v as downloadFile,y as fetchAllContent,_ as fetchContentById,g as fetchContentByType,p as getApiClient,s as getConfig,l as isInitialized,D as normalizeContentItem,m as resetApiClient,u as resetConfig,c as setConfig,w as useContentAll,C as useContentDetail,S as useContentList,T as useContentPrefetch};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["message: string","opts?: { status?: number; data?: unknown; cause?: unknown }","currentConfig: SdkConfig | null","value: string | undefined","config: SdkConfig","axiosInstance: AxiosInstance | null","config: SdkConfig","response: AxiosResponse","error: AxiosError","params: ContentListParams","id: number","path: string","response","params: Omit<ContentListParams, 'page'> & { page?: never }","options: FetchAllOptions<C, T>","item: ContentDto<C>","item: T","type: string | undefined","params: ContentListParams","id: number","params: Omit<ContentListParams, 'page'>","params: ContentListParams","id: number","params: Omit<ContentListParams, 'page'> & { page?: never }","options: FetchAllOptions<C, T>","params: ContentListParams","id: number","params: Omit<ContentListParams, 'page'> & { page?: never }","options: FetchAllOptions<C, T>","rawPath?: string | null","opts?: BuildAssetUrlOpts","item: ContentDto<C>","key: string","path?: string | null"],"sources":["../src/errors/CmsError.ts","../src/config/sdkConfig.ts","../src/services/apiClient.ts","../src/services/contentApi.ts","../src/services/contentFetchAll.ts","../src/queries/queryKeys.ts","../src/queries/contentQueries.ts","../src/queries/useContentQueries.ts","../src/utils/assetUrl.ts","../src/utils/normalization.ts"],"sourcesContent":["export class CmsError extends Error {\n readonly status?: number;\n readonly data?: unknown;\n constructor(message: string, opts?: { status?: number; data?: unknown; cause?: unknown }) {\n super(message, opts?.cause ? { cause: opts.cause } : undefined);\n this.name = 'CmsError';\n this.status = opts?.status;\n this.data = opts?.data;\n }\n}","// src/config/sdkConfig.ts\nimport type { SdkConfig } from '../types/config';\n\nlet currentConfig: SdkConfig | null = null;\n\n/**\n * Checks if a value looks like an unresolved CI/CD template variable\n */\nfunction isTemplateString(value: string | undefined): boolean {\n if (!value) return false;\n // Detect common CI/CD template patterns: ${VAR}, $VAR, {{VAR}}, etc.\n return /^(\\$\\{[^}]+\\}|\\$[A-Z_]+|\\{\\{[^}]+\\}\\})$/.test(value.trim());\n}\n\n/**\n * Get the current SDK configuration\n */\nexport function getConfig(): SdkConfig {\n if (!currentConfig) {\n throw new Error('SDK not initialized. Call createApiClient() first.');\n }\n return currentConfig;\n}\n\n/**\n * Set the SDK configuration\n */\nexport function setConfig(config: SdkConfig): void {\n // Clean up base URLs\n const baseUrl = config.baseUrl.replace(/\\/+$/, '');\n const fileBaseUrl = config.fileBaseUrl?.replace(/\\/+$/, '');\n\n // Validate required fields\n if (!baseUrl || isTemplateString(baseUrl)) {\n throw new Error('Invalid baseUrl: must be a valid URL string');\n }\n\n currentConfig = {\n ...config,\n baseUrl,\n fileBaseUrl,\n timeoutMs: config.timeoutMs ?? 30_000,\n };\n}\n\n/**\n * Check if SDK is initialized\n */\nexport function isInitialized(): boolean {\n return currentConfig !== null;\n}\n\n/**\n * Reset configuration (useful for testing)\n */\nexport function resetConfig(): void {\n currentConfig = null;\n}\n","import axios, { type AxiosInstance, type AxiosResponse, AxiosError } from 'axios';\nimport { CmsError } from '../errors/CmsError';\nimport { getConfig, setConfig, isInitialized } from '../config/sdkConfig';\nimport type { SdkConfig } from '../types/config';\n\nlet axiosInstance: AxiosInstance | null = null;\n\n/**\n * Create and configure the API client with the given configuration\n */\nexport function createApiClient(config: SdkConfig): AxiosInstance {\n setConfig(config);\n \n const cfg = getConfig();\n \n axiosInstance = axios.create({\n baseURL: cfg.baseUrl,\n timeout: cfg.timeoutMs ?? 30_000,\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n ...(cfg.tenant ? { 'X-Tenant': cfg.tenant } : {}),\n ...(cfg.apiKey ? { 'X-API-Key': cfg.apiKey } : {}),\n },\n });\n\n // Response interceptor for error handling\n axiosInstance.interceptors.response.use(\n (response: AxiosResponse) => response,\n (error: AxiosError) => {\n const status = error.response?.status;\n const data = error.response?.data;\n \n // Log errors if logger is configured\n if (cfg.logger?.error) {\n let message = `API request failed`;\n if (status) message += ` (${status})`;\n if (status === 401) message += ': Unauthorized - check your API key';\n if (status === 403) message += ': Permission denied';\n if (status === 404) message += ': Resource not found';\n if (status === 429) message += ': Rate limit exceeded';\n if (status && status >= 500) message += ': Server error';\n \n cfg.logger.error(message, { status, data, url: error.config?.url });\n }\n \n throw new CmsError(\n `CMS request failed${status ? ` (${status})` : ''}`,\n { status, data, cause: error }\n );\n }\n );\n\n return axiosInstance;\n}\n\n/**\n * Get the current API client instance\n * Throws if not initialized\n */\nexport function getApiClient(): AxiosInstance {\n if (!axiosInstance || !isInitialized()) {\n throw new Error('API client not initialized. Call createApiClient() first.');\n }\n return axiosInstance;\n}\n\n/**\n * Reset the API client (useful for testing)\n */\nexport function resetApiClient(): void {\n axiosInstance = null;\n}\n\n// Export a default instance getter for convenience\nexport const apiClient = new Proxy({} as AxiosInstance, {\n get(_target, prop) {\n const instance = getApiClient();\n const value = instance[prop as keyof AxiosInstance];\n return typeof value === 'function' ? value.bind(instance) : value;\n },\n});\n","import axios from 'axios';\nimport { getApiClient } from './apiClient';\nimport { getConfig } from '../config/sdkConfig';\nimport type {\n ApiResponse,\n ContentDto,\n ContentListParams,\n ContentResponse,\n CustomFields,\n} from '../types/content';\n\n/**\n * Fetch content by type with pagination and filters\n */\nexport async function fetchContentByType<C extends CustomFields = CustomFields>(\n params: ContentListParams = {}\n): Promise<ContentResponse<C>> {\n const client = getApiClient();\n \n // Sanitize and validate inputs\n const type = encodeURIComponent(params.type ?? 'ALL');\n const page = Number.isInteger(params.page) && (params.page as number) >= 0 ? params.page : 0;\n const size = Number.isInteger(params.size) && (params.size as number) > 0 && (params.size as number) <= 1000\n ? params.size\n : 10;\n\n // Build query parameters\n const queryParams = new URLSearchParams({\n page: String(page),\n size: String(size),\n sortBy: params.sortBy ?? 'id',\n direction: params.direction ?? 'DESC',\n });\n\n // Add arbitrary filters\n if (params.filters) {\n for (const [key, value] of Object.entries(params.filters)) {\n if (value === undefined || value === null) continue;\n queryParams.append(key, String(value));\n }\n }\n\n const url = `/content/type/${type}?${queryParams.toString()}`;\n const response = await client.get<ContentResponse<C>>(url);\n return response.data;\n}\n\n/**\n * Fetch a single content item by ID\n */\nexport async function fetchContentById<C extends CustomFields = CustomFields>(\n id: number\n): Promise<ApiResponse<ContentDto<C>>> {\n const client = getApiClient();\n const response = await client.get<ApiResponse<ContentDto<C>>>(`/content/${id}`);\n return response.data;\n}\n\n/**\n * Download a file from a given path\n * @param path - Full URL or relative path to the file\n * @returns Blob containing the file data\n */\nexport async function downloadFile(path: string): Promise<Blob> {\n const config = getConfig();\n const lower = path.toLowerCase();\n const api = (config.baseUrl || '').toLowerCase();\n const file = (config.fileBaseUrl || '').toLowerCase();\n \n // Check if this is a CMS-hosted file\n const isCmsFile = (api && lower.startsWith(api)) || (file && lower.startsWith(file));\n\n if (isCmsFile) {\n // Use authenticated client for CMS files\n const client = getApiClient();\n const response = await client.get(path, { responseType: 'blob' });\n return response.data as Blob;\n }\n \n // For external files, use axios directly without auth headers\n const response = await axios.get(path, { responseType: 'blob' });\n return response.data as Blob;\n}\n","import { fetchContentByType } from './contentApi';\nimport type {\n ContentDto,\n ContentListParams,\n CustomFields,\n PaginatedData,\n} from '../types/content';\n\nexport interface FetchAllOptions<C extends CustomFields, T> {\n /**\n * Optional function to transform each content item\n */\n mapItem?: (item: ContentDto<C>) => T;\n \n /**\n * Optional function to extract a unique key for deduplication\n */\n dedupeBy?: (item: T) => string | number;\n \n /**\n * Maximum number of pages to fetch (safety limit)\n * @default 20\n */\n hardStopMaxPages?: number;\n}\n\n/**\n * Fetch all content items across multiple pages with automatic pagination\n * \n * This function will continue fetching pages until:\n * - No more items are returned\n * - The last page is reached (based on pagination metadata)\n * - The hard stop limit is reached\n * - No new items are added (all items are duplicates)\n * \n * @param params - Content list parameters (without page number)\n * @param options - Fetch options including mapping and deduplication\n * @returns Array of all fetched items\n */\nexport async function fetchAllContent<\n C extends CustomFields = CustomFields,\n T = ContentDto<C>\n>(\n params: Omit<ContentListParams, 'page'> & { page?: never },\n options: FetchAllOptions<C, T> = {}\n): Promise<T[]> {\n const size = params.size ?? 100;\n const sortBy = params.sortBy ?? 'id';\n const direction = params.direction ?? 'DESC';\n \n const mapItem = options.mapItem ?? ((item: ContentDto<C>) => item as unknown as T);\n const dedupeBy = options.dedupeBy ?? ((item: T) => (item as { id: string | number }).id);\n const hardStopMaxPages = options.hardStopMaxPages ?? 20;\n\n let page = 0;\n const dedupMap = new Map<string | number, T>();\n\n for (let i = 0; i < hardStopMaxPages; i++) {\n const response = await fetchContentByType<C>({\n ...params,\n page,\n size,\n sortBy,\n direction,\n });\n\n const data = response?.data as PaginatedData<ContentDto<C>> | undefined;\n if (!data) break;\n\n const items = data.content ?? [];\n if (items.length === 0) break;\n\n // Process and deduplicate items\n let addedThisPage = 0;\n for (const item of items) {\n const mapped = mapItem(item);\n const key = dedupeBy(mapped);\n if (!dedupMap.has(key)) {\n dedupMap.set(key, mapped);\n addedThisPage++;\n }\n }\n\n // Stop if no new items were added (all duplicates)\n if (addedThisPage === 0) break;\n\n // Determine if there are more pages\n const current = typeof data.number === 'number' ? data.number : page;\n const pageSize = typeof data.size === 'number' ? data.size : size;\n const numberOfElements =\n typeof data.numberOfElements === 'number' ? data.numberOfElements : items.length;\n\n const hasNext =\n typeof data.totalPages === 'number'\n ? current + 1 < data.totalPages\n : typeof data.totalElements === 'number'\n ? current * pageSize + numberOfElements < data.totalElements\n : typeof data.last === 'boolean'\n ? !data.last\n : numberOfElements === pageSize;\n\n if (!hasNext) break;\n page = current + 1;\n }\n\n return Array.from(dedupMap.values());\n}\n","// src/queries/queryKeys.ts\nimport type { ContentListParams } from '../types/content';\n\n/**\n * Hierarchical query key factory for content queries\n * Following TanStack Query best practices for key structure\n */\nexport const contentKeys = {\n /**\n * Base key for all content queries\n */\n all: ['content'] as const,\n \n /**\n * Key for all list queries\n */\n lists: () => [...contentKeys.all, 'list'] as const,\n \n /**\n * Key for a specific list query with parameters\n * @param type - Content type (e.g., 'NEWS', 'REPORT')\n * @param params - Additional list parameters\n */\n list: (type: string | undefined, params: ContentListParams) =>\n [...contentKeys.lists(), type ?? 'ALL', params] as const,\n \n /**\n * Key for all detail queries\n */\n details: () => [...contentKeys.all, 'detail'] as const,\n \n /**\n * Key for a specific detail query by ID\n * @param id - Content item ID\n */\n detail: (id: number) => [...contentKeys.details(), id] as const,\n \n /**\n * Key for all \"fetch all\" queries\n */\n allLists: () => [...contentKeys.all, 'all'] as const,\n \n /**\n * Key for a specific \"fetch all\" query\n * @param type - Content type\n * @param params - List parameters (without page)\n */\n allList: (type: string | undefined, params: Omit<ContentListParams, 'page'>) =>\n [...contentKeys.allLists(), type ?? 'ALL', { ...params, mode: 'all' }] as const,\n};\n","import { queryOptions } from '@tanstack/react-query';\nimport { fetchContentByType, fetchContentById } from '../services/contentApi';\nimport { fetchAllContent, type FetchAllOptions } from '../services/contentFetchAll';\nimport { contentKeys } from './queryKeys';\nimport type {\n ContentDto,\n ContentListParams,\n CustomFields,\n} from '../types/content';\n\n/**\n * Query options factory for content queries\n * Use with TanStack Query's useQuery hook\n */\nexport const contentQueries = {\n /**\n * Query options for paginated content list\n * @param params - List parameters including type, pagination, filters\n */\n list: <C extends CustomFields = CustomFields>(params: ContentListParams = {}) =>\n queryOptions({\n queryKey: contentKeys.list(params.type, params),\n queryFn: () => fetchContentByType<C>(params),\n staleTime: 5 * 60_000, // 5 minutes\n gcTime: 10 * 60_000, // 10 minutes (formerly cacheTime)\n }),\n\n /**\n * Query options for a single content item by ID\n * @param id - Content item ID\n */\n detail: <C extends CustomFields = CustomFields>(id: number) =>\n queryOptions({\n queryKey: contentKeys.detail(id),\n queryFn: () => fetchContentById<C>(id),\n staleTime: 10 * 60_000, // 10 minutes\n gcTime: 30 * 60_000, // 30 minutes\n }),\n\n /**\n * Query options for fetching all content items (across all pages)\n * @param params - List parameters (without page)\n * @param options - Fetch options including mapping and deduplication\n */\n listAll: <C extends CustomFields = CustomFields, T = ContentDto<C>>(\n params: Omit<ContentListParams, 'page'> & { page?: never },\n options: FetchAllOptions<C, T> = {}\n ) =>\n queryOptions({\n queryKey: contentKeys.allList(params.type, params),\n queryFn: () => fetchAllContent<C, T>(params, options),\n staleTime: 5 * 60_000, // 5 minutes\n gcTime: 10 * 60_000, // 10 minutes\n }),\n};\n","// src/queries/useContentQueries.ts\nimport { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';\nimport { contentQueries } from './contentQueries';\nimport { contentKeys } from './queryKeys';\nimport type { FetchAllOptions } from '../services/contentFetchAll';\nimport type {\n ApiResponse,\n ContentDto,\n ContentListParams,\n ContentResponse,\n CustomFields,\n} from '../types/content';\n\n/**\n * Hook for fetching paginated content list\n * @param params - List parameters including type, pagination, filters\n */\nexport function useContentList<C extends CustomFields = CustomFields>(\n params: ContentListParams = {}\n): UseQueryResult<ContentResponse<C>> {\n return useQuery(contentQueries.list<C>(params));\n}\n\n/**\n * Hook for fetching a single content item by ID\n * @param id - Content item ID\n */\nexport function useContentDetail<C extends CustomFields = CustomFields>(\n id: number\n): UseQueryResult<ApiResponse<ContentDto<C>>> {\n return useQuery(contentQueries.detail<C>(id));\n}\n\n/**\n * Hook for fetching all content items across pages\n * @param params - List parameters (without page)\n * @param options - Fetch options including mapping and deduplication\n */\nexport function useContentAll<C extends CustomFields = CustomFields, T = ContentDto<C>>(\n params: Omit<ContentListParams, 'page'> & { page?: never },\n options: FetchAllOptions<C, T> = {}\n): UseQueryResult<T[]> {\n return useQuery(contentQueries.listAll<C, T>(params, options));\n}\n\n/**\n * Hook for prefetching content queries\n * Useful for optimistic navigation and hover effects\n */\nexport function useContentPrefetch() {\n const queryClient = useQueryClient();\n\n return {\n /**\n * Prefetch a content list\n */\n prefetchList: <C extends CustomFields = CustomFields>(params: ContentListParams) => {\n return queryClient.prefetchQuery(contentQueries.list<C>(params));\n },\n\n /**\n * Prefetch a content detail\n */\n prefetchDetail: <C extends CustomFields = CustomFields>(id: number) => {\n return queryClient.prefetchQuery(contentQueries.detail<C>(id));\n },\n\n /**\n * Invalidate content lists (force refetch)\n */\n invalidateLists: () => {\n return queryClient.invalidateQueries({ queryKey: contentKeys.lists() });\n },\n\n /**\n * Invalidate a specific content detail\n */\n invalidateDetail: (id: number) => {\n return queryClient.invalidateQueries({ queryKey: contentKeys.detail(id) });\n },\n\n /**\n * Invalidate all content queries\n */\n invalidateAll: () => {\n return queryClient.invalidateQueries({ queryKey: contentKeys.all });\n },\n };\n}\n","// src/utils/assetUrl.ts\ntype BuildAssetUrlOpts = {\n apiBase?: string;\n fileBase?: string;\n};\n\nexport const buildAssetUrl = (rawPath?: string | null, opts?: BuildAssetUrlOpts): string => {\n if (!rawPath) return '';\n const trimmed = rawPath.trim();\n if (!trimmed) return '';\n\n const apiBase = (opts?.apiBase || '').replace(/\\/+$/, '');\n const fileBase = (opts?.fileBase || '').replace(/\\/+$/, '');\n const originBase = apiBase.replace(/\\/api$/, '');\n const preferredBase = fileBase || originBase || apiBase;\n\n const lower = trimmed.toLowerCase();\n const isAbsolute = lower.startsWith('http://')\n || lower.startsWith('https://')\n || lower.startsWith('data:')\n || lower.startsWith('blob:')\n || trimmed.startsWith('//');\n\n if (isAbsolute) {\n if (apiBase && preferredBase && lower.startsWith(apiBase.toLowerCase())) {\n const afterApiBase = trimmed.slice(apiBase.length);\n return `${preferredBase}${afterApiBase.startsWith('/') ? '' : '/'}${afterApiBase}`;\n }\n const originApiPrefix = `${originBase}/api`.toLowerCase();\n if (originBase && preferredBase && lower.startsWith(originApiPrefix)) {\n const afterOriginApi = trimmed.slice((originBase + '/api').length);\n return `${preferredBase}${afterOriginApi.startsWith('/') ? '' : '/'}${afterOriginApi}`;\n }\n if (fileBase && lower.startsWith(fileBase.toLowerCase())) return trimmed;\n return trimmed;\n }\n\n let relative = trimmed.replace(/^\\/+/, '');\n relative = relative.replace(/^api\\//, '');\n return preferredBase ? `${preferredBase}/${relative}` : `/${relative}`;\n};","import { buildAssetUrl } from './assetUrl';\nimport { getConfig } from '../config/sdkConfig';\nimport type { ContentDto, CustomFields, NormalizedContentItem } from '../types/content';\n\n/**\n * Normalize a content item to a standard format with asset URLs resolved\n * \n * This function provides a default normalization strategy that:\n * - Extracts common fields from customFields\n * - Resolves asset URLs using SDK configuration\n * - Handles PDF path resolution for different content types\n * - Provides sensible defaults for missing fields\n * \n * @param item - Raw content item from the API\n * @returns Normalized content item\n */\nexport function normalizeContentItem<C extends CustomFields = CustomFields>(\n item: ContentDto<C>\n): NormalizedContentItem {\n const config = getConfig();\n const customFields = item.customFields as Record<string, unknown>;\n\n // Helper to get custom field value\n const getField = (key: string): string | null => {\n const value = customFields[key];\n return typeof value === 'string' ? value : null;\n };\n\n // Resolve PDF path based on content type\n const resolvePdfPath = (): string | null => {\n const type = item.type.toUpperCase();\n \n // For GAMEHEARTS_PUBLICATION type\n if (type === 'GAMEHEARTS_PUBLICATION' || type === 'REPORT') {\n return getField('publication_pdf') ?? getField('pdfPath');\n }\n \n // For EXTERNAL_PUBLICATION type\n if (type === 'EXTERNAL_PUBLICATION') {\n return getField('pdf') ?? getField('pdfPath');\n }\n \n // Default to pdfPath field\n return getField('pdfPath');\n };\n\n const pdfPath = resolvePdfPath();\n\n return {\n id: item.id,\n title: getField('title') || item.title,\n text: getField('text') || item.text,\n type: item.type,\n insideImage: buildAssetUrl(getField('insideImage') || '', {\n apiBase: config.baseUrl,\n fileBase: config.fileBaseUrl,\n }),\n outsideImage: buildAssetUrl(getField('outsideImage') || '', {\n apiBase: config.baseUrl,\n fileBase: config.fileBaseUrl,\n }),\n pdfPath: buildAssetUrl(pdfPath, {\n apiBase: config.baseUrl,\n fileBase: config.fileBaseUrl,\n }),\n references: getField('referencesList') ?? getField('references'),\n citation: getField('citation'),\n abstract: getField('abstractText') ?? getField('abstract'),\n team: getField('team'),\n publicationType: getField('publicationType'),\n fake: customFields.fake === true ? true : null,\n };\n}\n\n/**\n * Build an asset URL using the SDK's configured base URLs\n * This is a convenience wrapper that automatically uses SDK config\n * \n * @param path - Path to the asset (can be relative or absolute)\n * @returns Full URL to the asset\n */\nexport function buildAssetUrlWithConfig(path?: string | null): string {\n const config = getConfig();\n return buildAssetUrl(path, {\n apiBase: config.baseUrl,\n fileBase: config.fileBaseUrl,\n });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@codesocietyou/contentedge-cms-sdk",
3
3
  "private": false,
4
- "version": "0.2.2",
5
- "description": "A lightweight, framework-agnostic TypeScript client for the ContentEdge headless CMS.",
4
+ "version": "1.0.1",
5
+ "description": "A lightweight, framework-agnostic TypeScript client for the ContentEdge headless CMS with API key authentication and optional React Query integration.",
6
6
  "type": "module",
7
7
  "main": "./dist/index.cjs",
8
8
  "module": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "dev": "vite",
23
23
  "preview": "vite preview",
24
24
  "lint": "eslint .",
25
- "build": "tsup",
25
+ "build": "tsdown",
26
26
  "test": "vitest run",
27
27
  "test:coverage": "vitest run --coverage",
28
28
  "test:watch": "vitest",
@@ -35,7 +35,9 @@
35
35
  "cms",
36
36
  "sdk",
37
37
  "typescript",
38
- "keycloak"
38
+ "headless-cms",
39
+ "react-query",
40
+ "tanstack"
39
41
  ],
40
42
  "author": "ContentEdge",
41
43
  "license": "MIT",
@@ -48,33 +50,41 @@
48
50
  },
49
51
  "homepage": "https://github.com/ParapluOU/contentedge-cms-sdk#readme",
50
52
  "engines": {
51
- "node": ">=18"
53
+ "node": ">=24.13.1",
54
+ "npm": ">=10.0.0"
52
55
  },
53
56
  "publishConfig": {
54
57
  "access": "public",
55
58
  "provenance": true
56
59
  },
57
60
  "dependencies": {
58
- "axios": "^1.7.7"
61
+ "axios": "^1.13.3"
62
+ },
63
+ "peerDependencies": {
64
+ "@tanstack/react-query": ">=5.90.0"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "@tanstack/react-query": {
68
+ "optional": true
69
+ }
59
70
  },
60
71
  "devDependencies": {
61
- "@changesets/cli": "^2.29.7",
62
- "@eslint/js": "^9.39.1",
63
- "@types/node": "^24.10.0",
64
- "@types/react": "^19.2.2",
65
- "@types/react-dom": "^19.2.2",
66
- "@vitejs/plugin-react": "^5.1.0",
67
- "@vitest/coverage-v8": "^4.0.8",
68
- "eslint": "^9.39.1",
69
- "eslint-plugin-react-hooks": "^5.2.0",
70
- "eslint-plugin-react-refresh": "^0.4.24",
71
- "globals": "^16.5.0",
72
- "react": "^19.2.0",
73
- "react-dom": "^19.2.0",
74
- "tsup": "^8.3.0",
72
+ "@changesets/cli": "^2.29.8",
73
+ "@eslint/js": "^10.0.1",
74
+ "@tanstack/react-query": "^5.90.21",
75
+ "@types/node": "^25.2.3",
76
+ "@types/react": "^19.2.14",
77
+ "@types/react-dom": "^19.2.3",
78
+ "@vitejs/plugin-react": "^5.1.4",
79
+ "@vitest/coverage-v8": "^4.0.18",
80
+ "eslint": "^10.0.0",
81
+ "globals": "^17.3.0",
82
+ "react": "^19.2.4",
83
+ "react-dom": "^19.2.4",
84
+ "tsdown": "^0.3.0",
75
85
  "typescript": "~5.9.3",
76
- "typescript-eslint": "^8.46.4",
77
- "vite": "^7.2.2",
78
- "vitest": "^4.0.8"
86
+ "typescript-eslint": "8.55.1-alpha.4",
87
+ "vite": "^7.3.1",
88
+ "vitest": "^4.0.18"
79
89
  }
80
90
  }