@codesocietyou/contentedge-cms-sdk 0.2.2 → 1.0.0
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 +371 -177
- package/package.json +33 -23
package/README.md
CHANGED
|
@@ -1,233 +1,427 @@
|
|
|
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
|
-
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
- Generic content model
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
9
|
+
- **Generic content model**: `ContentDto<C>` with consumer-defined custom fields
|
|
10
|
+
- **Public API key authentication**: Simple header-based authentication
|
|
11
|
+
- **Service layer pattern**: Centralized API client with interceptors
|
|
12
|
+
- **React Query integration**: Optional query factory and hooks
|
|
13
|
+
- **Resilient pagination**: `fetchAllContent` aggregates pages with dedupe and safe stop
|
|
14
|
+
- **Asset/file helpers**: `buildAssetUrl` and safe `downloadFile`
|
|
15
|
+
- **Normalization utilities**: Transform API responses to normalized structures
|
|
16
|
+
- **Structured error model**: `CmsError` with status and response data
|
|
17
|
+
- **Framework-agnostic**: Core services work anywhere; React Query layer is optional
|
|
18
|
+
- **TypeScript-first**: Full type safety with generics
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
## Installation
|
|
17
21
|
|
|
18
22
|
```bash
|
|
19
23
|
npm install @codesocietyou/contentedge-cms-sdk
|
|
20
24
|
# or
|
|
21
25
|
yarn add @codesocietyou/contentedge-cms-sdk
|
|
26
|
+
# or
|
|
27
|
+
pnpm add @codesocietyou/contentedge-cms-sdk
|
|
22
28
|
```
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
For React Query integration, also install:
|
|
25
31
|
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
KeycloakClientCredentialsAuth,
|
|
30
|
-
type ContentDto
|
|
31
|
-
} from '@codesocietyou/contentedge-cms-sdk';
|
|
32
|
+
```bash
|
|
33
|
+
npm install @tanstack/react-query
|
|
34
|
+
```
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
clientId: 'contentedge-client',
|
|
37
|
-
clientSecret: 'xxxxxx'
|
|
38
|
-
});
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### 1. Initialize the SDK
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
```typescript
|
|
41
|
+
import { createApiClient } from '@codesocietyou/contentedge-cms-sdk';
|
|
42
|
+
|
|
43
|
+
// Initialize once at app startup
|
|
44
|
+
createApiClient({
|
|
45
|
+
baseUrl: 'https://api.contentedge.com',
|
|
46
|
+
fileBaseUrl: 'https://cdn.contentedge.com', // optional
|
|
47
|
+
apiKey: 'your-api-key', // optional
|
|
48
|
+
tenant: 'your-tenant', // optional
|
|
49
|
+
timeoutMs: 30_000, // optional (default: 30s)
|
|
46
50
|
});
|
|
51
|
+
```
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
### 2. Fetch Content (Basic)
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { fetchContentByType } from '@codesocietyou/contentedge-cms-sdk';
|
|
57
|
+
|
|
58
|
+
// Fetch paginated content
|
|
59
|
+
const response = await fetchContentByType({
|
|
60
|
+
type: 'NEWS',
|
|
51
61
|
page: 0,
|
|
52
62
|
size: 10,
|
|
53
63
|
sortBy: 'id',
|
|
54
64
|
direction: 'DESC',
|
|
55
|
-
filters: { publicationType: 'GAMEHEARTS' } // arbitrary
|
|
65
|
+
filters: { publicationType: 'GAMEHEARTS' }, // arbitrary filters
|
|
56
66
|
});
|
|
57
67
|
|
|
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
|
-
|
|
68
|
+
const items = response.data.content;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 3. Fetch Content (React Query)
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { useQuery } from '@tanstack/react-query';
|
|
75
|
+
import { contentQueries } from '@codesocietyou/contentedge-cms-sdk';
|
|
76
|
+
|
|
77
|
+
function NewsList() {
|
|
78
|
+
const { data, isLoading, error } = useQuery(
|
|
79
|
+
contentQueries.list({ type: 'NEWS', page: 0, size: 10 })
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (isLoading) return <div>Loading...</div>;
|
|
83
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<ul>
|
|
87
|
+
{data.data.content.map(item => (
|
|
88
|
+
<li key={item.id}>{item.title}</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 4. Normalization
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import {
|
|
99
|
+
normalizeContentItem,
|
|
100
|
+
fetchContentByType
|
|
101
|
+
} from '@codesocietyou/contentedge-cms-sdk';
|
|
102
|
+
|
|
103
|
+
// Fetch and normalize
|
|
104
|
+
const response = await fetchContentByType({ type: 'NEWS' });
|
|
105
|
+
const normalized = response.data.content.map(normalizeContentItem);
|
|
106
|
+
|
|
107
|
+
// normalized items have resolved asset URLs and standardized fields
|
|
108
|
+
console.log(normalized[0].insideImage); // "https://cdn.contentedge.com/images/news.jpg"
|
|
109
|
+
console.log(normalized[0].pdfPath); // "https://cdn.contentedge.com/files/doc.pdf"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## API Reference
|
|
113
|
+
|
|
114
|
+
### Configuration
|
|
115
|
+
|
|
116
|
+
#### `createApiClient(config: SdkConfig): AxiosInstance`
|
|
117
|
+
|
|
118
|
+
Initialize the SDK with your CMS configuration. Call this once at app startup.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
interface SdkConfig {
|
|
122
|
+
baseUrl: string; // CMS API base URL (required)
|
|
123
|
+
fileBaseUrl?: string; // Preferred file/asset base
|
|
124
|
+
apiKey?: string; // API key for authentication
|
|
125
|
+
tenant?: string; // Tenant identifier (sent as X-Tenant header)
|
|
126
|
+
timeoutMs?: number; // Request timeout (default: 30000)
|
|
127
|
+
logger?: {
|
|
128
|
+
debug?: (...args: unknown[]) => void;
|
|
129
|
+
warn?: (...args: unknown[]) => void;
|
|
130
|
+
error?: (...args: unknown[]) => void;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Service Layer
|
|
136
|
+
|
|
137
|
+
#### `fetchContentByType<C>(params: ContentListParams): Promise<ContentResponse<C>>`
|
|
138
|
+
|
|
139
|
+
Fetch paginated content by type with filters and sorting.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
interface ContentListParams {
|
|
143
|
+
type?: string; // Content type (default: 'ALL')
|
|
144
|
+
page?: number; // Page number (0-indexed)
|
|
145
|
+
size?: number; // Items per page
|
|
146
|
+
sortBy?: string; // Sort field (default: 'id')
|
|
147
|
+
direction?: 'ASC' | 'DESC'; // Sort direction (default: 'DESC')
|
|
148
|
+
filters?: Record<string, any>; // Arbitrary query filters
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `fetchContentById<C>(id: number): Promise<ApiResponse<ContentDto<C>>>`
|
|
153
|
+
|
|
154
|
+
Fetch a single content item by ID.
|
|
155
|
+
|
|
156
|
+
#### `fetchAllContent<C, T>(params, options): Promise<T[]>`
|
|
157
|
+
|
|
158
|
+
Fetch all content items across multiple pages with automatic pagination.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface FetchAllOptions<C, T> {
|
|
162
|
+
mapItem?: (item: ContentDto<C>) => T; // Transform each item
|
|
163
|
+
dedupeBy?: (item: T) => string | number; // Dedupe key extractor
|
|
164
|
+
hardStopMaxPages?: number; // Safety limit (default: 20)
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### `downloadFile(path: string): Promise<Blob>`
|
|
169
|
+
|
|
170
|
+
Download a file from a given path (with proper authentication for CMS files).
|
|
171
|
+
|
|
172
|
+
### React Query Integration
|
|
173
|
+
|
|
174
|
+
#### `contentQueries`
|
|
175
|
+
|
|
176
|
+
Query options factory for use with `useQuery`:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// Paginated list
|
|
180
|
+
contentQueries.list({ type: 'NEWS', page: 0, size: 10 })
|
|
181
|
+
|
|
182
|
+
// Single item
|
|
183
|
+
contentQueries.detail(123)
|
|
184
|
+
|
|
185
|
+
// Fetch all (across pages)
|
|
186
|
+
contentQueries.listAll({ type: 'NEWS', size: 100 }, { mapItem: normalizeContentItem })
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Hooks
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// List hook
|
|
193
|
+
const { data } = useContentList({ type: 'NEWS', page: 0, size: 10 });
|
|
194
|
+
|
|
195
|
+
// Detail hook
|
|
196
|
+
const { data } = useContentDetail(123);
|
|
197
|
+
|
|
198
|
+
// Fetch all hook
|
|
199
|
+
const { data } = useContentAll({ type: 'NEWS' });
|
|
200
|
+
|
|
201
|
+
// Prefetch utilities
|
|
202
|
+
const { prefetchList, prefetchDetail, invalidateLists } = useContentPrefetch();
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### Query Keys
|
|
206
|
+
|
|
207
|
+
Hierarchical query key factory for manual cache manipulation:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { contentKeys } from '@codesocietyou/contentedge-cms-sdk';
|
|
211
|
+
|
|
212
|
+
// Invalidate all lists
|
|
213
|
+
queryClient.invalidateQueries({ queryKey: contentKeys.lists() });
|
|
214
|
+
|
|
215
|
+
// Invalidate specific detail
|
|
216
|
+
queryClient.invalidateQueries({ queryKey: contentKeys.detail(123) });
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Normalization
|
|
220
|
+
|
|
221
|
+
#### `normalizeContentItem<C>(item: ContentDto<C>): NormalizedContentItem`
|
|
222
|
+
|
|
223
|
+
Transform a content item to a normalized structure with resolved asset URLs:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
interface NormalizedContentItem {
|
|
119
227
|
id: number;
|
|
120
228
|
title: string;
|
|
121
229
|
text: string;
|
|
122
|
-
insideImage: string;
|
|
123
|
-
outsideImage: string;
|
|
124
|
-
references: string | null;
|
|
125
|
-
pdfPath: string | null;
|
|
126
230
|
type: string;
|
|
231
|
+
insideImage: string; // Resolved URL
|
|
232
|
+
outsideImage: string; // Resolved URL
|
|
233
|
+
pdfPath: string | null; // Resolved URL (type-aware)
|
|
234
|
+
references: string | null;
|
|
127
235
|
citation: string | null;
|
|
128
|
-
fake: boolean | null;
|
|
129
236
|
abstract: string | null;
|
|
130
237
|
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
|
-
);
|
|
238
|
+
publicationType: string | null;
|
|
239
|
+
fake: boolean | null;
|
|
240
|
+
}
|
|
155
241
|
```
|
|
156
242
|
|
|
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
|
|
243
|
+
#### `buildAssetUrlWithConfig(path?: string | null): string`
|
|
244
|
+
|
|
245
|
+
Build an asset URL using the SDK's configured base URLs.
|
|
246
|
+
|
|
247
|
+
### Error Handling
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { CmsError } from '@codesocietyou/contentedge-cms-sdk';
|
|
251
|
+
|
|
189
252
|
try {
|
|
190
|
-
await
|
|
253
|
+
await fetchContentByType({ type: 'NEWS' });
|
|
191
254
|
} catch (e) {
|
|
192
255
|
if (e instanceof CmsError) {
|
|
193
|
-
console.error('
|
|
256
|
+
console.error('CMS error:', e.status, e.data);
|
|
257
|
+
// e.status: HTTP status code
|
|
258
|
+
// e.data: Response body (if any)
|
|
194
259
|
} else {
|
|
195
|
-
console.error('Unknown error', e);
|
|
260
|
+
console.error('Unknown error:', e);
|
|
196
261
|
}
|
|
197
262
|
}
|
|
198
263
|
```
|
|
199
264
|
|
|
200
|
-
|
|
265
|
+
## Advanced Usage
|
|
201
266
|
|
|
202
|
-
###
|
|
267
|
+
### Custom Fields Type
|
|
203
268
|
|
|
204
|
-
|
|
205
|
-
- For browsers, you can implement a simple bearer token strategy:
|
|
269
|
+
Define your own custom fields type for full type safety:
|
|
206
270
|
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
271
|
+
```typescript
|
|
272
|
+
interface MyCustomFields {
|
|
273
|
+
title: string;
|
|
274
|
+
text: string;
|
|
275
|
+
author?: string;
|
|
276
|
+
tags?: string[];
|
|
277
|
+
publishedAt?: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Use with type parameter
|
|
281
|
+
const response = await fetchContentByType<MyCustomFields>({ type: 'ARTICLE' });
|
|
282
|
+
const items = response.data.content; // ContentDto<MyCustomFields>[]
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Custom Normalization
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
interface MyNormalizedItem {
|
|
289
|
+
id: number;
|
|
290
|
+
title: string;
|
|
291
|
+
author: string;
|
|
292
|
+
tags: string[];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function myNormalize(item: ContentDto<MyCustomFields>): MyNormalizedItem {
|
|
296
|
+
return {
|
|
297
|
+
id: item.id,
|
|
298
|
+
title: item.customFields.title || item.title,
|
|
299
|
+
author: item.customFields.author || 'Unknown',
|
|
300
|
+
tags: item.customFields.tags || [],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Use with fetchAllContent
|
|
305
|
+
const items = await fetchAllContent<MyCustomFields, MyNormalizedItem>(
|
|
306
|
+
{ type: 'ARTICLE', size: 100 },
|
|
307
|
+
{ mapItem: myNormalize }
|
|
308
|
+
);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### React Query Configuration
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
315
|
+
import { createApiClient } from '@codesocietyou/contentedge-cms-sdk';
|
|
316
|
+
|
|
317
|
+
// Initialize SDK
|
|
318
|
+
createApiClient({
|
|
319
|
+
baseUrl: process.env.VITE_API_URL!,
|
|
320
|
+
apiKey: process.env.VITE_API_KEY,
|
|
321
|
+
tenant: 'my-tenant',
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Create query client
|
|
325
|
+
const queryClient = new QueryClient({
|
|
326
|
+
defaultOptions: {
|
|
327
|
+
queries: {
|
|
328
|
+
retry: 2,
|
|
329
|
+
refetchOnWindowFocus: false,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Wrap app
|
|
335
|
+
function App() {
|
|
336
|
+
return (
|
|
337
|
+
<QueryClientProvider client={queryClient}>
|
|
338
|
+
<YourApp />
|
|
339
|
+
</QueryClientProvider>
|
|
340
|
+
);
|
|
213
341
|
}
|
|
214
342
|
```
|
|
215
343
|
|
|
216
|
-
|
|
217
|
-
|
|
344
|
+
## Migration from v0.2.x
|
|
345
|
+
|
|
346
|
+
The v1.0.0 release includes breaking changes:
|
|
347
|
+
|
|
348
|
+
### Removed
|
|
349
|
+
|
|
350
|
+
- `CmsClient` class → Use service functions + `createApiClient()`
|
|
351
|
+
- `KeycloakClientCredentialsAuth` → Use API key authentication
|
|
352
|
+
- `AuthProvider` interface → No longer needed
|
|
218
353
|
|
|
219
|
-
###
|
|
354
|
+
### Migration Steps
|
|
355
|
+
|
|
356
|
+
**Before (v0.2.x):**
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { CmsClient, KeycloakClientCredentialsAuth } from '@codesocietyou/contentedge-cms-sdk';
|
|
360
|
+
|
|
361
|
+
const auth = new KeycloakClientCredentialsAuth({
|
|
362
|
+
tokenUrl: 'https://auth.example.com/token',
|
|
363
|
+
clientId: 'client',
|
|
364
|
+
clientSecret: 'secret',
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const client = new CmsClient({
|
|
368
|
+
baseUrl: 'https://api.example.com',
|
|
369
|
+
auth,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const list = await client.listContent({ type: 'NEWS' });
|
|
373
|
+
const detail = await client.getContentById(123);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**After (v1.0.0):**
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import {
|
|
380
|
+
createApiClient,
|
|
381
|
+
fetchContentByType,
|
|
382
|
+
fetchContentById
|
|
383
|
+
} from '@codesocietyou/contentedge-cms-sdk';
|
|
384
|
+
|
|
385
|
+
// Initialize once
|
|
386
|
+
createApiClient({
|
|
387
|
+
baseUrl: 'https://api.example.com',
|
|
388
|
+
apiKey: 'your-api-key',
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Use service functions
|
|
392
|
+
const list = await fetchContentByType({ type: 'NEWS' });
|
|
393
|
+
const detail = await fetchContentById(123);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Security
|
|
397
|
+
|
|
398
|
+
- **API Keys**: Store in environment variables, never commit to source control
|
|
399
|
+
- **Runtime Configuration**: Inject secrets at runtime in production
|
|
400
|
+
- **CORS**: Ensure your CMS API allows requests from your domain
|
|
401
|
+
- **Rate Limiting**: The SDK logs 429 errors; implement retry logic if needed
|
|
402
|
+
|
|
403
|
+
## Endpoint Assumptions
|
|
220
404
|
|
|
221
405
|
By default, the SDK uses:
|
|
222
|
-
- `GET /content/type/:type`
|
|
223
|
-
- `GET /content/:id`
|
|
224
406
|
|
|
225
|
-
|
|
407
|
+
- `GET /content/type/:type` - List content by type
|
|
408
|
+
- `GET /content/:id` - Get content by ID
|
|
409
|
+
|
|
410
|
+
If your deployment uses different endpoints, wrap or extend the service functions.
|
|
226
411
|
|
|
227
|
-
|
|
412
|
+
## Contributing
|
|
228
413
|
|
|
229
|
-
|
|
414
|
+
Contributions are welcome! Please follow the existing code style and add tests for new features.
|
|
230
415
|
|
|
231
|
-
|
|
416
|
+
## Versioning
|
|
417
|
+
|
|
418
|
+
This project follows [Semantic Versioning](https://semver.org/). Breaking changes bump MAJOR.
|
|
419
|
+
|
|
420
|
+
## License
|
|
232
421
|
|
|
233
422
|
MIT
|
|
423
|
+
|
|
424
|
+
## Support
|
|
425
|
+
|
|
426
|
+
- Issues: https://github.com/ParapluOU/contentedge-cms-sdk/issues
|
|
427
|
+
- Docs: https://github.com/ParapluOU/contentedge-cms-sdk#readme
|
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.0",
|
|
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
|
}
|