@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 +402 -180
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -23
package/README.md
CHANGED
|
@@ -1,233 +1,455 @@
|
|
|
1
|
-
|
|
1
|
+
# ContentEdge SDK (TypeScript)
|
|
2
2
|
|
|
3
|
-
A lightweight, framework-agnostic TypeScript client for the ContentEdge headless CMS. It provides a
|
|
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
|
-
###
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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! →](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
|
-
|
|
39
|
+
For React Query integration, also install:
|
|
25
40
|
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
KeycloakClientCredentialsAuth,
|
|
30
|
-
type ContentDto
|
|
31
|
-
} from '@codesocietyou/contentedge-cms-sdk';
|
|
41
|
+
```bash
|
|
42
|
+
npm install @tanstack/react-query
|
|
43
|
+
```
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
clientId: 'contentedge-client',
|
|
37
|
-
clientSecret: 'xxxxxx'
|
|
38
|
-
});
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### 1. Initialize the SDK
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
74
|
+
filters: { publicationType: 'GAMEHEARTS' }, // arbitrary filters
|
|
56
75
|
});
|
|
57
76
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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:
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
262
|
+
await fetchContentByType({ type: 'NEWS' });
|
|
191
263
|
} catch (e) {
|
|
192
264
|
if (e instanceof CmsError) {
|
|
193
|
-
console.error('
|
|
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
|
-
|
|
274
|
+
## Advanced Usage
|
|
201
275
|
|
|
202
|
-
###
|
|
276
|
+
### Custom Fields Type
|
|
203
277
|
|
|
204
|
-
|
|
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
|
-
```
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
217
|
-
- Prefer runtime configuration to inject secrets; limit scopes on your Keycloak client.
|
|
353
|
+
## Migration from v0.2.x
|
|
218
354
|
|
|
219
|
-
|
|
355
|
+
The v1.0.0 release includes breaking changes:
|
|
220
356
|
|
|
221
|
-
|
|
222
|
-
- `GET /content/type/:type`
|
|
223
|
-
- `GET /content/:id`
|
|
357
|
+
### Removed
|
|
224
358
|
|
|
225
|
-
|
|
359
|
+
- `CmsClient` class → Use service functions + `createApiClient()`
|
|
360
|
+
- `KeycloakClientCredentialsAuth` → Use API key authentication
|
|
361
|
+
- `AuthProvider` interface → No longer needed
|
|
226
362
|
|
|
227
|
-
###
|
|
363
|
+
### Migration Steps
|
|
228
364
|
|
|
229
|
-
|
|
365
|
+
**Before (v0.2.x):**
|
|
230
366
|
|
|
231
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
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": ">=
|
|
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.
|
|
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.
|
|
62
|
-
"@eslint/js": "^
|
|
63
|
-
"@
|
|
64
|
-
"@types/
|
|
65
|
-
"@types/react
|
|
66
|
-
"@
|
|
67
|
-
"@
|
|
68
|
-
"
|
|
69
|
-
"eslint
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"react": "^19.2.
|
|
73
|
-
"
|
|
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": "
|
|
77
|
-
"vite": "^7.
|
|
78
|
-
"vitest": "^4.0.
|
|
86
|
+
"typescript-eslint": "8.55.1-alpha.4",
|
|
87
|
+
"vite": "^7.3.1",
|
|
88
|
+
"vitest": "^4.0.18"
|
|
79
89
|
}
|
|
80
90
|
}
|