@bernierllc/contentful-cma-client 1.0.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/.eslintrc.cjs ADDED
@@ -0,0 +1,34 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ module.exports = {
10
+ root: true,
11
+ parser: '@typescript-eslint/parser',
12
+ parserOptions: {
13
+ ecmaVersion: 2020,
14
+ sourceType: 'module',
15
+ project: './tsconfig.json',
16
+ tsconfigRootDir: __dirname
17
+ },
18
+ plugins: ['@typescript-eslint'],
19
+ extends: [
20
+ 'eslint:recommended',
21
+ 'plugin:@typescript-eslint/recommended',
22
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking'
23
+ ],
24
+ rules: {
25
+ '@typescript-eslint/no-explicit-any': 'warn',
26
+ '@typescript-eslint/no-unsafe-assignment': 'warn',
27
+ '@typescript-eslint/no-unsafe-argument': 'warn',
28
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
29
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
30
+ '@typescript-eslint/no-floating-promises': 'error',
31
+ '@typescript-eslint/no-misused-promises': 'error'
32
+ },
33
+ ignorePatterns: ['dist', 'node_modules', '*.cjs', '**/__tests__/**', '**/*.test.ts']
34
+ };
package/README.md ADDED
@@ -0,0 +1,441 @@
1
+ # @bernierllc/contentful-cma-client
2
+
3
+ Thin wrapper around the official `contentful-management` npm package, providing consistent error handling, logging, and typed interfaces for all CMA (Content Management API) operations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bernierllc/contentful-cma-client
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - ✅ **Typed Interfaces** - Full TypeScript support using `@bernierllc/contentful-types`
14
+ - ✅ **Comprehensive CRUD** - Entries, Assets, Content Types operations
15
+ - ✅ **Bulk Operations** - Batch create, update, publish, delete
16
+ - ✅ **Automatic Pagination** - `getAllEntries()` and `getAllAssets()` handle pagination automatically
17
+ - ✅ **Consistent Logging** - Integrated with `@bernierllc/logger`
18
+ - ✅ **Error Handling** - Graceful error handling with detailed logging
19
+ - ✅ **Publishing Workflow** - Publish, unpublish, archive, unarchive support
20
+
21
+ ## Usage
22
+
23
+ ### Basic Setup
24
+
25
+ ```typescript
26
+ import { ContentfulCMAClient } from '@bernierllc/contentful-cma-client';
27
+
28
+ const client = new ContentfulCMAClient({
29
+ accessToken: 'your-cma-token',
30
+ spaceId: 'your-space-id',
31
+ environmentId: 'master' // optional, defaults to 'master'
32
+ });
33
+ ```
34
+
35
+ ### Entry Operations
36
+
37
+ #### Get Entry
38
+
39
+ ```typescript
40
+ const entry = await client.getEntry<BlogPost>('entry-id');
41
+ console.log(entry.fields.title);
42
+ ```
43
+
44
+ #### Get Entries with Query
45
+
46
+ ```typescript
47
+ const entries = await client.getEntries<BlogPost>({
48
+ content_type: 'blogPost',
49
+ limit: 10,
50
+ skip: 0
51
+ });
52
+ ```
53
+
54
+ #### Get All Entries (with automatic pagination)
55
+
56
+ ```typescript
57
+ const allEntries = await client.getAllEntries<BlogPost>({
58
+ content_type: 'blogPost'
59
+ });
60
+ ```
61
+
62
+ #### Create Entry
63
+
64
+ ```typescript
65
+ const newEntry = await client.createEntry<BlogPost>(
66
+ 'blogPost',
67
+ {
68
+ title: { 'en-US': 'My Blog Post' },
69
+ body: { 'en-US': 'Content here...' }
70
+ }
71
+ );
72
+ ```
73
+
74
+ #### Create Entry with Custom ID
75
+
76
+ ```typescript
77
+ const newEntry = await client.createEntry<BlogPost>(
78
+ 'blogPost',
79
+ {
80
+ title: { 'en-US': 'My Blog Post' }
81
+ },
82
+ { entryId: 'custom-entry-id' }
83
+ );
84
+ ```
85
+
86
+ #### Update Entry
87
+
88
+ ```typescript
89
+ const updatedEntry = await client.updateEntry<BlogPost>(
90
+ 'entry-id',
91
+ {
92
+ title: { 'en-US': 'Updated Title' }
93
+ },
94
+ 2 // version number
95
+ );
96
+ ```
97
+
98
+ #### Publish Entry
99
+
100
+ ```typescript
101
+ const publishedEntry = await client.publishEntry('entry-id', 2);
102
+ ```
103
+
104
+ #### Unpublish Entry
105
+
106
+ ```typescript
107
+ await client.unpublishEntry('entry-id');
108
+ ```
109
+
110
+ #### Archive Entry
111
+
112
+ ```typescript
113
+ await client.archiveEntry('entry-id', 2);
114
+ ```
115
+
116
+ #### Unarchive Entry
117
+
118
+ ```typescript
119
+ await client.unarchiveEntry('entry-id');
120
+ ```
121
+
122
+ #### Delete Entry
123
+
124
+ ```typescript
125
+ await client.deleteEntry('entry-id');
126
+ ```
127
+
128
+ ### Asset Operations
129
+
130
+ #### Get Asset
131
+
132
+ ```typescript
133
+ const asset = await client.getAsset('asset-id');
134
+ console.log(asset.fields.file['en-US'].url);
135
+ ```
136
+
137
+ #### Create Asset
138
+
139
+ ```typescript
140
+ const newAsset = await client.createAsset({
141
+ title: { 'en-US': 'My Image' },
142
+ file: {
143
+ 'en-US': {
144
+ url: 'https://example.com/image.jpg',
145
+ fileName: 'image.jpg',
146
+ contentType: 'image/jpeg',
147
+ details: { size: 12345 }
148
+ }
149
+ }
150
+ });
151
+ ```
152
+
153
+ #### Process Asset (after upload)
154
+
155
+ ```typescript
156
+ await client.processAsset('asset-id', 1, 'en-US');
157
+ ```
158
+
159
+ #### Publish Asset
160
+
161
+ ```typescript
162
+ const publishedAsset = await client.publishAsset('asset-id', 2);
163
+ ```
164
+
165
+ #### Delete Asset
166
+
167
+ ```typescript
168
+ await client.deleteAsset('asset-id');
169
+ ```
170
+
171
+ ### Content Type Operations
172
+
173
+ #### Get Content Type
174
+
175
+ ```typescript
176
+ const contentType = await client.getContentType('blogPost');
177
+ console.log(contentType.fields);
178
+ ```
179
+
180
+ #### Get All Content Types
181
+
182
+ ```typescript
183
+ const contentTypes = await client.getContentTypes();
184
+ ```
185
+
186
+ ### Bulk Operations
187
+
188
+ #### Bulk Create Entries
189
+
190
+ ```typescript
191
+ const result = await client.bulkCreateEntries([
192
+ { contentTypeId: 'blogPost', fields: { title: { 'en-US': 'Post 1' } } },
193
+ { contentTypeId: 'blogPost', fields: { title: { 'en-US': 'Post 2' } } }
194
+ ]);
195
+
196
+ console.log(`Created: ${result.successful.length}`);
197
+ console.log(`Failed: ${result.failed.length}`);
198
+
199
+ // Handle failures
200
+ result.failed.forEach(failure => {
201
+ console.error(`Failed to create:`, failure.error);
202
+ });
203
+ ```
204
+
205
+ #### Bulk Publish Entries
206
+
207
+ ```typescript
208
+ const result = await client.bulkPublishEntries([
209
+ { entryId: 'entry-1', version: 2 },
210
+ { entryId: 'entry-2', version: 1 }
211
+ ]);
212
+ ```
213
+
214
+ #### Bulk Delete Entries
215
+
216
+ ```typescript
217
+ const result = await client.bulkDeleteEntries([
218
+ 'entry-1',
219
+ 'entry-2',
220
+ 'entry-3'
221
+ ]);
222
+ ```
223
+
224
+ ## Configuration Options
225
+
226
+ ```typescript
227
+ interface ContentfulCMAConfig {
228
+ accessToken: string; // Required: CMA access token
229
+ spaceId: string; // Required: Contentful space ID
230
+ environmentId?: string; // Optional: defaults to 'master'
231
+ host?: string; // Optional: defaults to 'api.contentful.com'
232
+ retryOnError?: boolean; // Optional: defaults to true
233
+ timeout?: number; // Optional: defaults to 30000ms
234
+ }
235
+ ```
236
+
237
+ ## Query Options
238
+
239
+ ```typescript
240
+ interface CMAQueryOptions {
241
+ skip?: number; // Number of items to skip
242
+ limit?: number; // Number of items to return
243
+ order?: string; // Sort order (e.g., '-sys.createdAt')
244
+ locale?: string; // Locale filter
245
+ content_type?: string; // Content type filter
246
+ [key: string]: any; // Additional Contentful query params
247
+ }
248
+ ```
249
+
250
+ ## Type Safety
251
+
252
+ This package uses types from `@bernierllc/contentful-types`:
253
+
254
+ ```typescript
255
+ import type {
256
+ ContentfulEntry,
257
+ ContentfulAsset,
258
+ ContentfulContentType
259
+ } from '@bernierllc/contentful-types';
260
+
261
+ interface BlogPost {
262
+ title: { [locale: string]: string };
263
+ body: { [locale: string]: string };
264
+ author: { [locale: string]: ContentfulLink<'Entry'> };
265
+ }
266
+
267
+ const entry = await client.getEntry<BlogPost>('entry-id');
268
+ // entry.fields is typed as BlogPost
269
+ ```
270
+
271
+ ## Error Handling
272
+
273
+ All methods log errors and throw them for you to handle:
274
+
275
+ ```typescript
276
+ try {
277
+ const entry = await client.getEntry('non-existent-id');
278
+ } catch (error) {
279
+ console.error('Failed to get entry:', error);
280
+ }
281
+ ```
282
+
283
+ Bulk operations return both successful and failed operations:
284
+
285
+ ```typescript
286
+ const result = await client.bulkCreateEntries(entries);
287
+
288
+ // Process successful
289
+ result.successful.forEach(entry => {
290
+ console.log('Created:', entry.sys.id);
291
+ });
292
+
293
+ // Handle failures
294
+ result.failed.forEach(({ item, error }) => {
295
+ console.error('Failed to create:', error);
296
+ });
297
+ ```
298
+
299
+ ## Logging
300
+
301
+ This package uses `@bernierllc/logger` for consistent logging:
302
+
303
+ - **info**: Operation completions, initialization
304
+ - **debug**: Request/response details, operation start
305
+ - **error**: Failures with context
306
+
307
+ All logs include context (spaceId, environmentId, operation details).
308
+
309
+ ## MECE Principles
310
+
311
+ This package follows MECE (Mutually Exclusive, Collectively Exhaustive) architecture:
312
+
313
+ **Includes:**
314
+ - ✅ Content Management API operations (CMA)
315
+ - ✅ CRUD for Entries, Assets, Content Types
316
+ - ✅ Publishing workflow operations
317
+ - ✅ Bulk operations
318
+
319
+ **Excludes:**
320
+ - ❌ Content Delivery API (use `@bernierllc/contentful-cda-client`)
321
+ - ❌ GraphQL queries (use `@bernierllc/contentful-graphql-client`)
322
+ - ❌ Webhook handling (use `@bernierllc/contentful-webhook-handler`)
323
+ - ❌ OAuth/Auth management (use `@bernierllc/contentful-auth`)
324
+
325
+ ## API
326
+
327
+ ### ContentfulCMAClient
328
+
329
+ Main client class for interacting with Contentful Content Management API.
330
+
331
+ **Constructor:**
332
+ ```typescript
333
+ new ContentfulCMAClient(config: ContentfulCMAConfig)
334
+ ```
335
+
336
+ **Entry Methods:**
337
+ - `getEntry<T>(entryId: string): Promise<ContentfulEntry<T>>`
338
+ - `getEntries<T>(query?: CMAQueryOptions): Promise<ContentfulCollection<ContentfulEntry<T>>>`
339
+ - `getAllEntries<T>(query?: CMAQueryOptions): Promise<ContentfulEntry<T>[]>`
340
+ - `createEntry<T>(contentTypeId: string, fields: T, options?: CreateEntryOptions): Promise<ContentfulEntry<T>>`
341
+ - `updateEntry<T>(entryId: string, fields: Partial<T>, version: number): Promise<ContentfulEntry<T>>`
342
+ - `publishEntry(entryId: string, version: number): Promise<ContentfulEntry>`
343
+ - `unpublishEntry(entryId: string): Promise<ContentfulEntry>`
344
+ - `archiveEntry(entryId: string, version: number): Promise<ContentfulEntry>`
345
+ - `unarchiveEntry(entryId: string): Promise<ContentfulEntry>`
346
+ - `deleteEntry(entryId: string): Promise<void>`
347
+
348
+ **Asset Methods:**
349
+ - `getAsset(assetId: string): Promise<ContentfulAsset>`
350
+ - `getAssets(query?: CMAQueryOptions): Promise<ContentfulCollection<ContentfulAsset>>`
351
+ - `getAllAssets(query?: CMAQueryOptions): Promise<ContentfulAsset[]>`
352
+ - `createAsset(fields: AssetFields, options?: CreateAssetOptions): Promise<ContentfulAsset>`
353
+ - `processAsset(assetId: string, version: number, locale: string): Promise<ContentfulAsset>`
354
+ - `publishAsset(assetId: string, version: number): Promise<ContentfulAsset>`
355
+ - `deleteAsset(assetId: string): Promise<void>`
356
+
357
+ **Content Type Methods:**
358
+ - `getContentType(contentTypeId: string): Promise<ContentfulContentType>`
359
+ - `getContentTypes(): Promise<ContentfulCollection<ContentfulContentType>>`
360
+
361
+ **Bulk Methods:**
362
+ - `bulkCreateEntries<T>(entries: BulkCreateEntry<T>[]): Promise<BulkOperationResult<ContentfulEntry<T>>>`
363
+ - `bulkPublishEntries(entries: BulkPublishEntry[]): Promise<BulkOperationResult<ContentfulEntry>>`
364
+ - `bulkDeleteEntries(entryIds: string[]): Promise<BulkOperationResult<void>>`
365
+ - `bulkCreateAssets(assets: AssetFields[]): Promise<BulkOperationResult<ContentfulAsset>>`
366
+
367
+ ## Integrations
368
+
369
+ ### Logger Integration
370
+
371
+ This package integrates with `@bernierllc/logger` for consistent logging across all operations. The logger is initialized automatically and provides:
372
+
373
+ - **Structured logging** with operation context (spaceId, environmentId, entryId)
374
+ - **Log levels**: info, debug, error
375
+ - **Automatic error tracking** with full error details
376
+
377
+ ```typescript
378
+ // Logger is automatically initialized
379
+ const client = new ContentfulCMAClient(config);
380
+
381
+ // Logs are emitted for all operations
382
+ await client.getEntry('entry-id');
383
+ // Logs: [INFO] Fetching entry entry-id from space xxx-space-id
384
+ ```
385
+
386
+ ### NeverHub Integration
387
+
388
+ This package does **not** require NeverHub integration as it is a core client library. However, services using this client can integrate with `@bernierllc/neverhub-adapter` for event tracking:
389
+
390
+ ```typescript
391
+ import { ContentfulCMAClient } from '@bernierllc/contentful-cma-client';
392
+ import { NeverHubAdapter } from '@bernierllc/neverhub-adapter';
393
+
394
+ class ContentfulService {
395
+ private client: ContentfulCMAClient;
396
+ private neverhub?: NeverHubAdapter;
397
+
398
+ async initialize() {
399
+ this.client = new ContentfulCMAClient(config);
400
+
401
+ // Optional NeverHub integration for event tracking
402
+ if (await NeverHubAdapter.detect()) {
403
+ this.neverhub = new NeverHubAdapter();
404
+ await this.neverhub.register({
405
+ type: 'contentful-service',
406
+ capabilities: ['content-management']
407
+ });
408
+ }
409
+ }
410
+
411
+ async createEntry<T>(contentTypeId: string, fields: T) {
412
+ const entry = await this.client.createEntry(contentTypeId, fields);
413
+
414
+ // Track event in NeverHub (if available)
415
+ if (this.neverhub) {
416
+ await this.neverhub.logEvent({
417
+ type: 'content.created',
418
+ data: { entryId: entry.sys.id, contentType: contentTypeId }
419
+ });
420
+ }
421
+
422
+ return entry;
423
+ }
424
+ }
425
+ ```
426
+
427
+ **Integration Status:**
428
+ - ✅ Logger integration: Required (built-in)
429
+ - ⚪ NeverHub integration: Optional (consumer responsibility)
430
+ - ✅ Graceful degradation: Works standalone without external services
431
+
432
+ ## Related Packages
433
+
434
+ - [@bernierllc/contentful-types](../contentful-types) - TypeScript types
435
+ - [@bernierllc/contentful-cda-client](../contentful-cda-client) - Content Delivery API
436
+ - [@bernierllc/contentful-graphql-client](../contentful-graphql-client) - GraphQL API
437
+ - [@bernierllc/contentful-gateway-service](../../service/contentful-gateway-service) - Unified gateway
438
+
439
+ ## License
440
+
441
+ Copyright (c) 2025 Bernier LLC. See LICENSE.md for details.
@@ -0,0 +1,119 @@
1
+ import { ContentfulEntry, ContentfulAsset, ContentfulContentType, ContentfulCMAConfig } from '@bernierllc/contentful-types';
2
+ import { CMAQueryOptions, CMAEntryCreateOptions, CMAEntryUpdateOptions, CMAAssetCreateOptions, CMABulkOperationResult } from './types';
3
+ export declare class ContentfulCMAClient {
4
+ private client;
5
+ private logger;
6
+ private config;
7
+ constructor(config: ContentfulCMAConfig);
8
+ /**
9
+ * Get entry by ID
10
+ */
11
+ getEntry<T = Record<string, unknown>>(entryId: string): Promise<ContentfulEntry<T>>;
12
+ /**
13
+ * Get multiple entries with optional query parameters
14
+ */
15
+ getEntries<T = Record<string, unknown>>(query?: CMAQueryOptions): Promise<ContentfulEntry<T>[]>;
16
+ /**
17
+ * Get all entries with automatic pagination
18
+ */
19
+ getAllEntries<T = Record<string, unknown>>(query?: Omit<CMAQueryOptions, 'skip' | 'limit'>): Promise<ContentfulEntry<T>[]>;
20
+ /**
21
+ * Create entry
22
+ */
23
+ createEntry<T = Record<string, unknown>>(contentTypeId: string, fields: T, options?: CMAEntryCreateOptions): Promise<ContentfulEntry<T>>;
24
+ /**
25
+ * Update entry
26
+ */
27
+ updateEntry<T = Record<string, unknown>>(entryId: string, fields: T, version: number, _options?: CMAEntryUpdateOptions): Promise<ContentfulEntry<T>>;
28
+ /**
29
+ * Publish entry
30
+ */
31
+ publishEntry(entryId: string, version: number): Promise<ContentfulEntry>;
32
+ /**
33
+ * Unpublish entry
34
+ */
35
+ unpublishEntry(entryId: string): Promise<ContentfulEntry>;
36
+ /**
37
+ * Archive entry
38
+ */
39
+ archiveEntry(entryId: string, version: number): Promise<ContentfulEntry>;
40
+ /**
41
+ * Unarchive entry
42
+ */
43
+ unarchiveEntry(entryId: string): Promise<ContentfulEntry>;
44
+ /**
45
+ * Delete entry
46
+ */
47
+ deleteEntry(entryId: string): Promise<void>;
48
+ /**
49
+ * Get asset by ID
50
+ */
51
+ getAsset(assetId: string): Promise<ContentfulAsset>;
52
+ /**
53
+ * Get multiple assets with optional query parameters
54
+ */
55
+ getAssets(query?: CMAQueryOptions): Promise<ContentfulAsset[]>;
56
+ /**
57
+ * Get all assets with automatic pagination
58
+ */
59
+ getAllAssets(query?: Omit<CMAQueryOptions, 'skip' | 'limit'>): Promise<ContentfulAsset[]>;
60
+ /**
61
+ * Create asset
62
+ */
63
+ createAsset(fields: Record<string, unknown>, options?: CMAAssetCreateOptions): Promise<ContentfulAsset>;
64
+ /**
65
+ * Process asset (for uploaded files)
66
+ */
67
+ processAsset(assetId: string, version: number, locale?: string): Promise<ContentfulAsset>;
68
+ /**
69
+ * Publish asset
70
+ */
71
+ publishAsset(assetId: string, version: number): Promise<ContentfulAsset>;
72
+ /**
73
+ * Unpublish asset
74
+ */
75
+ unpublishAsset(assetId: string): Promise<ContentfulAsset>;
76
+ /**
77
+ * Delete asset
78
+ */
79
+ deleteAsset(assetId: string): Promise<void>;
80
+ /**
81
+ * Get content type by ID
82
+ */
83
+ getContentType(contentTypeId: string): Promise<ContentfulContentType>;
84
+ /**
85
+ * Get all content types
86
+ */
87
+ getContentTypes(query?: CMAQueryOptions): Promise<ContentfulContentType[]>;
88
+ /**
89
+ * Bulk create entries
90
+ */
91
+ bulkCreateEntries<T = Record<string, unknown>>(entries: Array<{
92
+ contentTypeId: string;
93
+ fields: T;
94
+ entryId?: string;
95
+ }>): Promise<CMABulkOperationResult<ContentfulEntry<T>>>;
96
+ /**
97
+ * Bulk delete entries
98
+ */
99
+ bulkDeleteEntries(entryIds: string[]): Promise<CMABulkOperationResult<string>>;
100
+ /**
101
+ * Bulk publish entries
102
+ */
103
+ bulkPublishEntries(entries: Array<{
104
+ entryId: string;
105
+ version: number;
106
+ }>): Promise<CMABulkOperationResult<ContentfulEntry>>;
107
+ /**
108
+ * Helper method to extract error message
109
+ */
110
+ private getErrorMessage;
111
+ /**
112
+ * Get current space ID
113
+ */
114
+ getSpaceId(): string;
115
+ /**
116
+ * Get current environment ID
117
+ */
118
+ getEnvironmentId(): string;
119
+ }