@codesocietyou/contentedge-cms-sdk 0.2.2
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/LICENSE +22 -0
- package/README.md +233 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ContentEdge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
## ContentEdge SDK (TypeScript)
|
|
2
|
+
|
|
3
|
+
A lightweight, framework-agnostic TypeScript client for the ContentEdge headless CMS. It provides a small, typed façade over the HTTP API with pluggable auth, robust pagination helpers, asset/file utilities, and structured errors—without embedding project-specific domain models (e.g., News/Blog/Reports).
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Generic content model: `ContentDto<C>` with consumer-defined custom fields
|
|
10
|
+
- Pluggable auth strategy: `AuthProvider` (includes Keycloak client-credentials)
|
|
11
|
+
- Resilient pagination: `listAllContent` aggregates pages with dedupe and safe stop
|
|
12
|
+
- Asset/file helpers: `buildAssetUrl` and safe `downloadFile`
|
|
13
|
+
- Structured error model: `CmsError` with status and response data
|
|
14
|
+
- Framework-agnostic; optional React Query adapter pattern
|
|
15
|
+
|
|
16
|
+
### Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @codesocietyou/contentedge-cms-sdk
|
|
20
|
+
# or
|
|
21
|
+
yarn add @codesocietyou/contentedge-cms-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Quick Start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {
|
|
28
|
+
CmsClient,
|
|
29
|
+
KeycloakClientCredentialsAuth,
|
|
30
|
+
type ContentDto
|
|
31
|
+
} from '@codesocietyou/contentedge-cms-sdk';
|
|
32
|
+
|
|
33
|
+
// Auth (Keycloak client-credentials) - server-side only
|
|
34
|
+
const auth = new KeycloakClientCredentialsAuth({
|
|
35
|
+
tokenUrl: 'https://auth.example.com/realms/contentedge/protocol/openid-connect/token',
|
|
36
|
+
clientId: 'contentedge-client',
|
|
37
|
+
clientSecret: 'xxxxxx'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Client
|
|
41
|
+
const contentedge = new CmsClient({
|
|
42
|
+
baseUrl: 'https://cms.example.com/api',
|
|
43
|
+
fileBaseUrl: 'https://cms.example.com', // optional (asset host)
|
|
44
|
+
tenant: 'your-tenant', // optional; sent as X-Tenant
|
|
45
|
+
auth
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// List content by type with filters/pagination
|
|
49
|
+
const list = await contentedge.listContent({
|
|
50
|
+
type: 'REPORT',
|
|
51
|
+
page: 0,
|
|
52
|
+
size: 10,
|
|
53
|
+
sortBy: 'id',
|
|
54
|
+
direction: 'DESC',
|
|
55
|
+
filters: { publicationType: 'GAMEHEARTS' } // arbitrary query params
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Get detail by id
|
|
59
|
+
const detail = await contentedge.getContentById(123);
|
|
60
|
+
|
|
61
|
+
// Download a file (Blob in browsers)
|
|
62
|
+
const pdf = await contentedge.downloadFile('https://cms.example.com/files/doc.pdf');
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### API
|
|
66
|
+
|
|
67
|
+
- Client
|
|
68
|
+
- `new CmsClient(config)`
|
|
69
|
+
- `baseUrl`: CMS API base URL (e.g., `https://cms.example.com/api`)
|
|
70
|
+
- `fileBaseUrl?`: Preferred file/asset base (often origin w/o `/api`)
|
|
71
|
+
- `tenant?`: Adds `X-Tenant` header
|
|
72
|
+
- `timeoutMs?`: Default 30000
|
|
73
|
+
- `logger?`: `{ debug?, warn?, error? }`
|
|
74
|
+
- `auth?`: `AuthProvider`
|
|
75
|
+
- `listContent<C>(params?: ContentListParams): Promise<ContentResponse<C>>`
|
|
76
|
+
- `getContentById<C>(id: number): Promise<ApiResponse<ContentDto<C>>>`
|
|
77
|
+
- `listAllContent<C, T = ContentDto<C>>(params: Omit<ContentListParams, 'page'>, opts?: { mapItem?, dedupeBy?, hardStopMaxPages? }): Promise<T[]>`
|
|
78
|
+
- `buildAssetUrl(path?: string | null): string`
|
|
79
|
+
- `downloadFile(path: string): Promise<Blob>`
|
|
80
|
+
|
|
81
|
+
- Auth
|
|
82
|
+
- `AuthProvider`: `getAccessToken(opts?: { forceRefresh?: boolean }): Promise<string>`
|
|
83
|
+
- `KeycloakClientCredentialsAuth({ tokenUrl, clientId, clientSecret })`
|
|
84
|
+
|
|
85
|
+
- Types
|
|
86
|
+
- `ContentDto<C extends Record<string, JsonValue>>`
|
|
87
|
+
- `ApiResponse<T>`
|
|
88
|
+
- `PaginatedData<T>`
|
|
89
|
+
- `ContentResponse<C>`
|
|
90
|
+
- `ContentListParams`:
|
|
91
|
+
- `type?`, `page?`, `size?`, `sortBy?`, `direction?`
|
|
92
|
+
- `filters?`: arbitrary query params
|
|
93
|
+
|
|
94
|
+
- Errors
|
|
95
|
+
- `CmsError extends Error` with `.status?: number` and `.data?: unknown`
|
|
96
|
+
|
|
97
|
+
### Mapping to your app models
|
|
98
|
+
|
|
99
|
+
Keep domain mapping out of the SDK. Define custom fields and a mapper in your app:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// Your custom fields
|
|
103
|
+
type MyCustomFields = {
|
|
104
|
+
title?: string;
|
|
105
|
+
text?: string;
|
|
106
|
+
insideImage?: string;
|
|
107
|
+
outsideImage?: string;
|
|
108
|
+
references?: string | null;
|
|
109
|
+
pdfPath?: string | null;
|
|
110
|
+
citation?: string | null;
|
|
111
|
+
abstract?: string | null;
|
|
112
|
+
team?: string | null;
|
|
113
|
+
publicationType?: 'GAMEHEARTS' | 'EXTERNAL' | null;
|
|
114
|
+
fake?: boolean | null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Your view model
|
|
118
|
+
type NormalizedItem = {
|
|
119
|
+
id: number;
|
|
120
|
+
title: string;
|
|
121
|
+
text: string;
|
|
122
|
+
insideImage: string;
|
|
123
|
+
outsideImage: string;
|
|
124
|
+
references: string | null;
|
|
125
|
+
pdfPath: string | null;
|
|
126
|
+
type: string;
|
|
127
|
+
citation: string | null;
|
|
128
|
+
fake: boolean | null;
|
|
129
|
+
abstract: string | null;
|
|
130
|
+
team: string | null;
|
|
131
|
+
publicationType: 'GAMEHEARTS' | 'EXTERNAL' | null;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const mapToNormalized = (item: ContentDto<MyCustomFields>): NormalizedItem => ({
|
|
135
|
+
id: item.id,
|
|
136
|
+
title: item.customFields.title || item.title,
|
|
137
|
+
text: item.customFields.text || item.text,
|
|
138
|
+
insideImage: contentedge.buildAssetUrl(item.customFields.insideImage || ''),
|
|
139
|
+
outsideImage: contentedge.buildAssetUrl(item.customFields.outsideImage || ''),
|
|
140
|
+
references: item.customFields.references ?? null,
|
|
141
|
+
pdfPath: contentedge.buildAssetUrl(item.customFields.pdfPath ?? null),
|
|
142
|
+
type: item.type,
|
|
143
|
+
citation: item.customFields.citation ?? null,
|
|
144
|
+
fake: item.customFields.fake ?? null,
|
|
145
|
+
abstract: item.customFields.abstract ?? null,
|
|
146
|
+
team: item.customFields.team ?? null,
|
|
147
|
+
publicationType: item.customFields.publicationType ?? null
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Fetch-all with mapping + dedupe
|
|
151
|
+
const items = await contentedge.listAllContent<MyCustomFields, NormalizedItem>(
|
|
152
|
+
{ type: 'REPORT', size: 100, sortBy: 'id', direction: 'DESC', filters: { publicationType: 'GAMEHEARTS' } },
|
|
153
|
+
{ mapItem: mapToNormalized, dedupeBy: (i) => i.id }
|
|
154
|
+
);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### React Query (optional pattern)
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { queryOptions } from '@tanstack/react-query';
|
|
161
|
+
import type { ContentListParams, ContentDto } from '@codesocietyou/contentedge-cms-sdk';
|
|
162
|
+
|
|
163
|
+
export const contentQueries = {
|
|
164
|
+
list: (params: ContentListParams) => queryOptions({
|
|
165
|
+
queryKey: ['content', 'list', params.type ?? 'ALL', params],
|
|
166
|
+
queryFn: () => contentedge.listContent(params),
|
|
167
|
+
staleTime: 5 * 60_000,
|
|
168
|
+
gcTime: 10 * 60_000
|
|
169
|
+
}),
|
|
170
|
+
detail: (id: number) => queryOptions({
|
|
171
|
+
queryKey: ['content', 'detail', id],
|
|
172
|
+
queryFn: () => contentedge.getContentById(id),
|
|
173
|
+
staleTime: 10 * 60_000,
|
|
174
|
+
gcTime: 30 * 60_000
|
|
175
|
+
}),
|
|
176
|
+
listAll: <C, T = ContentDto<C>>(params: Omit<ContentListParams, 'page'>, mapItem: (i: ContentDto<C>) => T) =>
|
|
177
|
+
queryOptions({
|
|
178
|
+
queryKey: ['content', 'all', params.type ?? 'ALL', { ...params, mode: 'all' }],
|
|
179
|
+
queryFn: () => contentedge.listAllContent(params, { mapItem }),
|
|
180
|
+
staleTime: 5 * 60_000,
|
|
181
|
+
gcTime: 10 * 60_000
|
|
182
|
+
})
|
|
183
|
+
};
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Error handling
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
try {
|
|
190
|
+
await contentedge.listContent({ type: 'NEWS' });
|
|
191
|
+
} catch (e) {
|
|
192
|
+
if (e instanceof CmsError) {
|
|
193
|
+
console.error('ContentEdge error', e.status, e.data);
|
|
194
|
+
} else {
|
|
195
|
+
console.error('Unknown error', e);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
401s are retried once with a forced token refresh when an `AuthProvider` is provided.
|
|
201
|
+
|
|
202
|
+
### Security
|
|
203
|
+
|
|
204
|
+
- `KeycloakClientCredentialsAuth` is intended for server-side usage. Do not expose client secrets in browsers.
|
|
205
|
+
- For browsers, you can implement a simple bearer token strategy:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
class BearerTokenAuth implements AuthProvider {
|
|
209
|
+
constructor(private readonly getToken: () => Promise<string> | string) {}
|
|
210
|
+
async getAccessToken() {
|
|
211
|
+
return typeof this.getToken === 'function' ? await this.getToken() : this.getToken;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- `downloadFile` avoids sending Authorization headers to non-CMS domains.
|
|
217
|
+
- Prefer runtime configuration to inject secrets; limit scopes on your Keycloak client.
|
|
218
|
+
|
|
219
|
+
### Endpoint assumptions
|
|
220
|
+
|
|
221
|
+
By default, the SDK uses:
|
|
222
|
+
- `GET /content/type/:type`
|
|
223
|
+
- `GET /content/:id`
|
|
224
|
+
|
|
225
|
+
If your deployment differs, wrap or extend `CmsClient`.
|
|
226
|
+
|
|
227
|
+
### Versioning
|
|
228
|
+
|
|
229
|
+
Semantic Versioning (SemVer). Breaking changes bump MAJOR.
|
|
230
|
+
|
|
231
|
+
### License
|
|
232
|
+
|
|
233
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codesocietyou/contentedge-cms-sdk",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.2.2",
|
|
5
|
+
"description": "A lightweight, framework-agnostic TypeScript client for the ContentEdge headless CMS.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "vite",
|
|
23
|
+
"preview": "vite preview",
|
|
24
|
+
"lint": "eslint .",
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:coverage": "vitest run --coverage",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"changeset": "changeset",
|
|
30
|
+
"version:apply": "changeset version",
|
|
31
|
+
"release": "changeset publish"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"content",
|
|
35
|
+
"cms",
|
|
36
|
+
"sdk",
|
|
37
|
+
"typescript",
|
|
38
|
+
"keycloak"
|
|
39
|
+
],
|
|
40
|
+
"author": "ContentEdge",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/ParapluOU/contentedge-cms-sdk.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/ParapluOU/contentedge-cms-sdk/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/ParapluOU/contentedge-cms-sdk#readme",
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public",
|
|
55
|
+
"provenance": true
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"axios": "^1.7.7"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@changesets/cli": "^2.29.7",
|
|
62
|
+
"@eslint/js": "^9.39.1",
|
|
63
|
+
"@types/node": "^24.10.0",
|
|
64
|
+
"@types/react": "^19.2.2",
|
|
65
|
+
"@types/react-dom": "^19.2.2",
|
|
66
|
+
"@vitejs/plugin-react": "^5.1.0",
|
|
67
|
+
"@vitest/coverage-v8": "^4.0.8",
|
|
68
|
+
"eslint": "^9.39.1",
|
|
69
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
70
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
71
|
+
"globals": "^16.5.0",
|
|
72
|
+
"react": "^19.2.0",
|
|
73
|
+
"react-dom": "^19.2.0",
|
|
74
|
+
"tsup": "^8.3.0",
|
|
75
|
+
"typescript": "~5.9.3",
|
|
76
|
+
"typescript-eslint": "^8.46.4",
|
|
77
|
+
"vite": "^7.2.2",
|
|
78
|
+
"vitest": "^4.0.8"
|
|
79
|
+
}
|
|
80
|
+
}
|