@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.
@@ -0,0 +1,694 @@
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
+ import contentfulManagement, {
10
+ PlainClientAPI
11
+ } from 'contentful-management';
12
+ import {
13
+ ContentfulEntry,
14
+ ContentfulAsset,
15
+ ContentfulContentType,
16
+ ContentfulCMAConfig
17
+ } from '@bernierllc/contentful-types';
18
+ import { Logger, LogLevel, ConsoleTransport } from '@bernierllc/logger';
19
+ import {
20
+ CMAQueryOptions,
21
+ CMAEntryCreateOptions,
22
+ CMAEntryUpdateOptions,
23
+ CMAAssetCreateOptions,
24
+ CMABulkOperationResult
25
+ } from './types';
26
+
27
+ export class ContentfulCMAClient {
28
+ private client: PlainClientAPI;
29
+ private logger: Logger;
30
+ private config: Required<ContentfulCMAConfig>;
31
+
32
+ constructor(config: ContentfulCMAConfig) {
33
+ // Set defaults
34
+ this.config = {
35
+ environmentId: 'master',
36
+ host: 'api.contentful.com',
37
+ retryOnError: true,
38
+ timeout: 30000,
39
+ ...config
40
+ } as Required<ContentfulCMAConfig>;
41
+
42
+ // Initialize logger
43
+ this.logger = new Logger({
44
+ level: LogLevel.INFO,
45
+ transports: [new ConsoleTransport()],
46
+ context: {
47
+ service: 'contentful-cma-client',
48
+ version: '1.0.0',
49
+ spaceId: config.spaceId,
50
+ environmentId: this.config.environmentId
51
+ }
52
+ });
53
+
54
+ // Create contentful-management client with plain API
55
+ this.client = contentfulManagement.createClient(
56
+ {
57
+ accessToken: config.accessToken,
58
+ host: this.config.host
59
+ },
60
+ {
61
+ type: 'plain',
62
+ defaults: {
63
+ spaceId: config.spaceId,
64
+ environmentId: this.config.environmentId
65
+ }
66
+ }
67
+ );
68
+
69
+ this.logger.info('ContentfulCMAClient initialized', {
70
+ spaceId: config.spaceId,
71
+ environmentId: this.config.environmentId,
72
+ host: this.config.host
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Get entry by ID
78
+ */
79
+ async getEntry<T = Record<string, unknown>>(
80
+ entryId: string
81
+ ): Promise<ContentfulEntry<T>> {
82
+ try {
83
+ this.logger.debug('Getting entry', { entryId });
84
+
85
+ const entry = await this.client.entry.get({ entryId });
86
+
87
+ this.logger.debug('Entry retrieved', { entryId });
88
+ return entry as unknown as ContentfulEntry<T>;
89
+ } catch (error) {
90
+ const errorObj = error instanceof Error ? error : new Error(String(error));
91
+ this.logger.error('Failed to get entry', errorObj, { entryId });
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get multiple entries with optional query parameters
98
+ */
99
+ async getEntries<T = Record<string, unknown>>(
100
+ query?: CMAQueryOptions
101
+ ): Promise<ContentfulEntry<T>[]> {
102
+ try {
103
+ this.logger.debug('Getting entries', { query: query || {} });
104
+
105
+ const response = await this.client.entry.getMany({
106
+ query: query as Record<string, unknown>
107
+ });
108
+
109
+ this.logger.debug('Entries retrieved', {
110
+ count: response.items.length,
111
+ total: response.total
112
+ });
113
+
114
+ return response.items as unknown as ContentfulEntry<T>[];
115
+ } catch (error) {
116
+ const errorObj = error instanceof Error ? error : new Error(String(error));
117
+ this.logger.error('Failed to get entries', errorObj, { query: query || {} });
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Get all entries with automatic pagination
124
+ */
125
+ async getAllEntries<T = Record<string, unknown>>(
126
+ query?: Omit<CMAQueryOptions, 'skip' | 'limit'>
127
+ ): Promise<ContentfulEntry<T>[]> {
128
+ const all: ContentfulEntry<T>[] = [];
129
+ let skip = 0;
130
+ const limit = 100;
131
+
132
+ this.logger.debug('Getting all entries with pagination', { query: query || {} });
133
+
134
+ let hasMore = true;
135
+ while (hasMore) {
136
+ const response = await this.client.entry.getMany({
137
+ query: { ...query, skip, limit } as Record<string, unknown>
138
+ });
139
+
140
+ all.push(...(response.items as unknown as ContentfulEntry<T>[]));
141
+
142
+ this.logger.debug('Retrieved entry batch', {
143
+ batch: response.items.length,
144
+ total: all.length,
145
+ remaining: response.total - (skip + response.items.length)
146
+ });
147
+
148
+ if (skip + response.items.length >= response.total) {
149
+ hasMore = false;
150
+ } else {
151
+ skip += response.items.length;
152
+ }
153
+ }
154
+
155
+ this.logger.info('All entries retrieved', { totalCount: all.length });
156
+ return all;
157
+ }
158
+
159
+ /**
160
+ * Create entry
161
+ */
162
+ async createEntry<T = Record<string, unknown>>(
163
+ contentTypeId: string,
164
+ fields: T,
165
+ options?: CMAEntryCreateOptions
166
+ ): Promise<ContentfulEntry<T>> {
167
+ try {
168
+ this.logger.debug('Creating entry', { contentTypeId, options: options || {} });
169
+
170
+ const entry = await this.client.entry.create(
171
+ { contentTypeId, ...(options?.entryId ? { entryId: options.entryId } : {}) },
172
+ { fields: fields as Record<string, unknown> }
173
+ );
174
+
175
+ this.logger.info('Entry created', {
176
+ entryId: entry.sys.id,
177
+ contentTypeId
178
+ });
179
+
180
+ return entry as unknown as ContentfulEntry<T>;
181
+ } catch (error) {
182
+ const errorObj = error instanceof Error ? error : new Error(String(error));
183
+ this.logger.error('Failed to create entry', errorObj, { contentTypeId });
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Update entry
190
+ */
191
+ async updateEntry<T = Record<string, unknown>>(
192
+ entryId: string,
193
+ fields: T,
194
+ version: number,
195
+ _options?: CMAEntryUpdateOptions
196
+ ): Promise<ContentfulEntry<T>> {
197
+ try {
198
+ this.logger.debug('Updating entry', { entryId, version });
199
+
200
+ // Type assertion needed due to contentful-management strict typing
201
+ const entry = await this.client.entry.update(
202
+ { entryId },
203
+ {
204
+ sys: { version } as any,
205
+ fields: fields as Record<string, unknown>
206
+ } as any
207
+ );
208
+
209
+ this.logger.info('Entry updated', { entryId, newVersion: entry.sys.version });
210
+ return entry as unknown as ContentfulEntry<T>;
211
+ } catch (error) {
212
+ const errorObj = error instanceof Error ? error : new Error(String(error));
213
+ this.logger.error('Failed to update entry', errorObj, { entryId, version });
214
+ throw error;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Publish entry
220
+ */
221
+ async publishEntry(
222
+ entryId: string,
223
+ version: number
224
+ ): Promise<ContentfulEntry> {
225
+ try {
226
+ this.logger.debug('Publishing entry', { entryId, version });
227
+
228
+ // Type assertion needed due to contentful-management strict typing
229
+ const entry = await this.client.entry.publish(
230
+ { entryId },
231
+ { sys: { version } } as any
232
+ );
233
+
234
+ this.logger.info('Entry published', {
235
+ entryId,
236
+ publishedVersion: entry.sys.publishedVersion
237
+ });
238
+
239
+ return entry as unknown as ContentfulEntry;
240
+ } catch (error) {
241
+ const errorObj = error instanceof Error ? error : new Error(String(error));
242
+ this.logger.error('Failed to publish entry', errorObj, { entryId, version });
243
+ throw error;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Unpublish entry
249
+ */
250
+ async unpublishEntry(entryId: string): Promise<ContentfulEntry> {
251
+ try {
252
+ this.logger.debug('Unpublishing entry', { entryId });
253
+
254
+ const entry = await this.client.entry.unpublish({ entryId });
255
+
256
+ this.logger.info('Entry unpublished', { entryId });
257
+ return entry as unknown as ContentfulEntry;
258
+ } catch (error) {
259
+ const errorObj = error instanceof Error ? error : new Error(String(error));
260
+ this.logger.error('Failed to unpublish entry', errorObj, { entryId });
261
+ throw error;
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Archive entry
267
+ */
268
+ async archiveEntry(
269
+ entryId: string,
270
+ version: number
271
+ ): Promise<ContentfulEntry> {
272
+ try {
273
+ this.logger.debug('Archiving entry', { entryId, version });
274
+
275
+ // Type assertion needed due to contentful-management strict typing
276
+ const entry = await this.client.entry.archive({ entryId, version } as any);
277
+
278
+ this.logger.info('Entry archived', { entryId });
279
+ return entry as unknown as ContentfulEntry;
280
+ } catch (error) {
281
+ const errorObj = error instanceof Error ? error : new Error(String(error));
282
+ this.logger.error('Failed to archive entry', errorObj, { entryId, version });
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Unarchive entry
289
+ */
290
+ async unarchiveEntry(entryId: string): Promise<ContentfulEntry> {
291
+ try {
292
+ this.logger.debug('Unarchiving entry', { entryId });
293
+
294
+ const entry = await this.client.entry.unarchive({ entryId });
295
+
296
+ this.logger.info('Entry unarchived', { entryId });
297
+ return entry as unknown as ContentfulEntry;
298
+ } catch (error) {
299
+ const errorObj = error instanceof Error ? error : new Error(String(error));
300
+ this.logger.error('Failed to unarchive entry', errorObj, { entryId });
301
+ throw error;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Delete entry
307
+ */
308
+ async deleteEntry(entryId: string): Promise<void> {
309
+ try {
310
+ this.logger.debug('Deleting entry', { entryId });
311
+
312
+ await this.client.entry.delete({ entryId });
313
+
314
+ this.logger.info('Entry deleted', { entryId });
315
+ } catch (error) {
316
+ const errorObj = error instanceof Error ? error : new Error(String(error));
317
+ this.logger.error('Failed to delete entry', errorObj, { entryId });
318
+ throw error;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Get asset by ID
324
+ */
325
+ async getAsset(assetId: string): Promise<ContentfulAsset> {
326
+ try {
327
+ this.logger.debug('Getting asset', { assetId });
328
+
329
+ const asset = await this.client.asset.get({ assetId });
330
+
331
+ this.logger.debug('Asset retrieved', { assetId });
332
+ return asset as unknown as ContentfulAsset;
333
+ } catch (error) {
334
+ const errorObj = error instanceof Error ? error : new Error(String(error));
335
+ this.logger.error('Failed to get asset', errorObj, { assetId });
336
+ throw error;
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Get multiple assets with optional query parameters
342
+ */
343
+ async getAssets(query?: CMAQueryOptions): Promise<ContentfulAsset[]> {
344
+ try {
345
+ this.logger.debug('Getting assets', { query: query || {} });
346
+
347
+ const response = await this.client.asset.getMany({
348
+ query: query as Record<string, unknown>
349
+ });
350
+
351
+ this.logger.debug('Assets retrieved', {
352
+ count: response.items.length,
353
+ total: response.total
354
+ });
355
+
356
+ return response.items as unknown as ContentfulAsset[];
357
+ } catch (error) {
358
+ const errorObj = error instanceof Error ? error : new Error(String(error));
359
+ this.logger.error('Failed to get assets', errorObj, { query: query || {} });
360
+ throw error;
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Get all assets with automatic pagination
366
+ */
367
+ async getAllAssets(
368
+ query?: Omit<CMAQueryOptions, 'skip' | 'limit'>
369
+ ): Promise<ContentfulAsset[]> {
370
+ const all: ContentfulAsset[] = [];
371
+ let skip = 0;
372
+ const limit = 100;
373
+
374
+ this.logger.debug('Getting all assets with pagination', { query: query || {} });
375
+
376
+ let hasMore = true;
377
+ while (hasMore) {
378
+ const response = await this.client.asset.getMany({
379
+ query: { ...query, skip, limit } as Record<string, unknown>
380
+ });
381
+
382
+ all.push(...(response.items as unknown as ContentfulAsset[]));
383
+
384
+ this.logger.debug('Retrieved asset batch', {
385
+ batch: response.items.length,
386
+ total: all.length,
387
+ remaining: response.total - (skip + response.items.length)
388
+ });
389
+
390
+ if (skip + response.items.length >= response.total) {
391
+ hasMore = false;
392
+ } else {
393
+ skip += response.items.length;
394
+ }
395
+ }
396
+
397
+ this.logger.info('All assets retrieved', { totalCount: all.length });
398
+ return all;
399
+ }
400
+
401
+ /**
402
+ * Create asset
403
+ */
404
+ async createAsset(
405
+ fields: Record<string, unknown>,
406
+ options?: CMAAssetCreateOptions
407
+ ): Promise<ContentfulAsset> {
408
+ try {
409
+ this.logger.debug('Creating asset', { options: options || {} });
410
+
411
+ // Type assertion needed due to contentful-management strict typing
412
+ const asset = await this.client.asset.create(
413
+ { ...(options?.assetId ? { assetId: options.assetId } : {}) },
414
+ { fields } as any
415
+ );
416
+
417
+ this.logger.info('Asset created', { assetId: asset.sys.id });
418
+ return asset as unknown as ContentfulAsset;
419
+ } catch (error) {
420
+ const errorObj = error instanceof Error ? error : new Error(String(error));
421
+ this.logger.error('Failed to create asset', errorObj);
422
+ throw error;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Process asset (for uploaded files)
428
+ */
429
+ async processAsset(
430
+ assetId: string,
431
+ version: number,
432
+ locale: string = 'en-US'
433
+ ): Promise<ContentfulAsset> {
434
+ try {
435
+ this.logger.debug('Processing asset', { assetId, version, locale });
436
+
437
+ // Type assertion needed due to contentful-management strict typing
438
+ const asset = await this.client.asset.processForLocale(
439
+ { assetId, version } as any,
440
+ { sys: { version } } as any,
441
+ locale
442
+ );
443
+
444
+ this.logger.info('Asset processing started', { assetId });
445
+ return asset as unknown as ContentfulAsset;
446
+ } catch (error) {
447
+ const errorObj = error instanceof Error ? error : new Error(String(error));
448
+ this.logger.error('Failed to process asset', errorObj, {
449
+ assetId,
450
+ version,
451
+ locale
452
+ });
453
+ throw error;
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Publish asset
459
+ */
460
+ async publishAsset(
461
+ assetId: string,
462
+ version: number
463
+ ): Promise<ContentfulAsset> {
464
+ try {
465
+ this.logger.debug('Publishing asset', { assetId, version });
466
+
467
+ // Type assertion needed due to contentful-management strict typing
468
+ const asset = await this.client.asset.publish(
469
+ { assetId },
470
+ { sys: { version } } as any
471
+ );
472
+
473
+ this.logger.info('Asset published', {
474
+ assetId,
475
+ publishedVersion: asset.sys.publishedVersion
476
+ });
477
+
478
+ return asset as unknown as ContentfulAsset;
479
+ } catch (error) {
480
+ const errorObj = error instanceof Error ? error : new Error(String(error));
481
+ this.logger.error('Failed to publish asset', errorObj, { assetId, version });
482
+ throw error;
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Unpublish asset
488
+ */
489
+ async unpublishAsset(assetId: string): Promise<ContentfulAsset> {
490
+ try {
491
+ this.logger.debug('Unpublishing asset', { assetId });
492
+
493
+ const asset = await this.client.asset.unpublish({ assetId });
494
+
495
+ this.logger.info('Asset unpublished', { assetId });
496
+ return asset as unknown as ContentfulAsset;
497
+ } catch (error) {
498
+ const errorObj = error instanceof Error ? error : new Error(String(error));
499
+ this.logger.error('Failed to unpublish asset', errorObj, { assetId });
500
+ throw error;
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Delete asset
506
+ */
507
+ async deleteAsset(assetId: string): Promise<void> {
508
+ try {
509
+ this.logger.debug('Deleting asset', { assetId });
510
+
511
+ await this.client.asset.delete({ assetId });
512
+
513
+ this.logger.info('Asset deleted', { assetId });
514
+ } catch (error) {
515
+ const errorObj = error instanceof Error ? error : new Error(String(error));
516
+ this.logger.error('Failed to delete asset', errorObj, { assetId });
517
+ throw error;
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Get content type by ID
523
+ */
524
+ async getContentType(contentTypeId: string): Promise<ContentfulContentType> {
525
+ try {
526
+ this.logger.debug('Getting content type', { contentTypeId });
527
+
528
+ const contentType = await this.client.contentType.get({ contentTypeId });
529
+
530
+ this.logger.debug('Content type retrieved', { contentTypeId });
531
+ return contentType as unknown as ContentfulContentType;
532
+ } catch (error) {
533
+ const errorObj = error instanceof Error ? error : new Error(String(error));
534
+ this.logger.error('Failed to get content type', errorObj, { contentTypeId });
535
+ throw error;
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Get all content types
541
+ */
542
+ async getContentTypes(
543
+ query?: CMAQueryOptions
544
+ ): Promise<ContentfulContentType[]> {
545
+ try {
546
+ this.logger.debug('Getting content types', { query: query || {} });
547
+
548
+ const response = await this.client.contentType.getMany({
549
+ query: query as Record<string, unknown>
550
+ });
551
+
552
+ this.logger.info('Content types retrieved', { count: response.items.length });
553
+ return response.items as unknown as ContentfulContentType[];
554
+ } catch (error) {
555
+ const errorObj = error instanceof Error ? error : new Error(String(error));
556
+ this.logger.error('Failed to get content types', errorObj, { query: query || {} });
557
+ throw error;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Bulk create entries
563
+ */
564
+ async bulkCreateEntries<T = Record<string, unknown>>(
565
+ entries: Array<{
566
+ contentTypeId: string;
567
+ fields: T;
568
+ entryId?: string;
569
+ }>
570
+ ): Promise<CMABulkOperationResult<ContentfulEntry<T>>> {
571
+ const results: CMABulkOperationResult<ContentfulEntry<T>> = {
572
+ successful: [],
573
+ failed: []
574
+ };
575
+
576
+ this.logger.info('Starting bulk entry creation', { count: entries.length });
577
+
578
+ for (const entry of entries) {
579
+ try {
580
+ const created = await this.createEntry<T>(
581
+ entry.contentTypeId,
582
+ entry.fields,
583
+ { entryId: entry.entryId }
584
+ );
585
+ results.successful.push(created);
586
+ } catch (error) {
587
+ results.failed.push({
588
+ item: { contentTypeId: entry.contentTypeId, fields: entry.fields },
589
+ error: this.getErrorMessage(error)
590
+ });
591
+ }
592
+ }
593
+
594
+ this.logger.info('Bulk entry creation completed', {
595
+ successful: results.successful.length,
596
+ failed: results.failed.length
597
+ });
598
+
599
+ return results;
600
+ }
601
+
602
+ /**
603
+ * Bulk delete entries
604
+ */
605
+ async bulkDeleteEntries(
606
+ entryIds: string[]
607
+ ): Promise<CMABulkOperationResult<string>> {
608
+ const results: CMABulkOperationResult<string> = {
609
+ successful: [],
610
+ failed: []
611
+ };
612
+
613
+ this.logger.info('Starting bulk entry deletion', { count: entryIds.length });
614
+
615
+ for (const entryId of entryIds) {
616
+ try {
617
+ await this.deleteEntry(entryId);
618
+ results.successful.push(entryId);
619
+ } catch (error) {
620
+ results.failed.push({
621
+ item: entryId,
622
+ error: this.getErrorMessage(error)
623
+ });
624
+ }
625
+ }
626
+
627
+ this.logger.info('Bulk entry deletion completed', {
628
+ successful: results.successful.length,
629
+ failed: results.failed.length
630
+ });
631
+
632
+ return results;
633
+ }
634
+
635
+ /**
636
+ * Bulk publish entries
637
+ */
638
+ async bulkPublishEntries(
639
+ entries: Array<{ entryId: string; version: number }>
640
+ ): Promise<CMABulkOperationResult<ContentfulEntry>> {
641
+ const results: CMABulkOperationResult<ContentfulEntry> = {
642
+ successful: [],
643
+ failed: []
644
+ };
645
+
646
+ this.logger.info('Starting bulk entry publishing', { count: entries.length });
647
+
648
+ for (const entry of entries) {
649
+ try {
650
+ const published = await this.publishEntry(entry.entryId, entry.version);
651
+ results.successful.push(published);
652
+ } catch (error) {
653
+ results.failed.push({
654
+ item: entry,
655
+ error: this.getErrorMessage(error)
656
+ });
657
+ }
658
+ }
659
+
660
+ this.logger.info('Bulk entry publishing completed', {
661
+ successful: results.successful.length,
662
+ failed: results.failed.length
663
+ });
664
+
665
+ return results;
666
+ }
667
+
668
+ /**
669
+ * Helper method to extract error message
670
+ */
671
+ private getErrorMessage(error: unknown): string {
672
+ if (error instanceof Error) {
673
+ return error.message;
674
+ }
675
+ if (typeof error === 'string') {
676
+ return error;
677
+ }
678
+ return 'Unknown error';
679
+ }
680
+
681
+ /**
682
+ * Get current space ID
683
+ */
684
+ getSpaceId(): string {
685
+ return this.config.spaceId;
686
+ }
687
+
688
+ /**
689
+ * Get current environment ID
690
+ */
691
+ getEnvironmentId(): string {
692
+ return this.config.environmentId;
693
+ }
694
+ }