@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.
Files changed (3) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +233 -0
  3. 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
+ }