@stackbit/cms-contentful 0.4.49-staging.1 → 0.4.49-staging.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.
@@ -76,10 +76,10 @@ import {
76
76
  convertDocumentVersions
77
77
  } from './contentful-entries-converter';
78
78
  import { convertAndFilterScheduledActions } from './contentful-scheduled-actions-converter';
79
- import { ContentPoller, SyncResult } from './content-poller';
79
+ import { ContentPoller, ContentPollerSyncResult, ContentPollerSyncContext } from './content-poller';
80
80
  import { Readable } from 'stream';
81
81
  import { CONTENTFUL_BUILT_IN_IMAGE_SOURCES, CONTENTFUL_BYNDER_APP, CONTENTFUL_CLOUDINARY_APP } from './contentful-consts';
82
- import { getLastUpdatedEntityDate, SyncContext } from './utils';
82
+ import { getLastUpdatedEntityDate } from './utils';
83
83
 
84
84
  export interface ContentSourceOptions {
85
85
  /** Contentful Space ID */
@@ -109,7 +109,9 @@ type UserContext = {
109
109
  accessToken: string;
110
110
  };
111
111
 
112
- export type SchemaContext = unknown;
112
+ export type SchemaContext = {
113
+ lastUpdatedContentTypeDate?: string;
114
+ };
113
115
 
114
116
  type EntityError = {
115
117
  name: string;
@@ -138,7 +140,6 @@ export class ContentfulContentSource implements ContentSourceTypes.ContentSource
138
140
  private webhookUrl?: string;
139
141
  private devAppRestartNeeded?: () => void;
140
142
  private cache!: ContentSourceTypes.Cache<SchemaContext, DocumentContext, AssetContext>;
141
- private syncContext!: SyncContext;
142
143
  private taskQueue: TaskQueue;
143
144
 
144
145
  constructor(options: ContentSourceOptions) {
@@ -197,8 +198,6 @@ export class ContentfulContentSource implements ContentSourceTypes.ContentSource
197
198
  this.userLogger.info('Using webhook for content updates');
198
199
  }
199
200
 
200
- this.syncContext = ((await this.cache.get('syncContext')) as SyncContext) ?? {};
201
-
202
201
  this.plainClient = createPlainApiClient({
203
202
  spaceId: this.spaceId,
204
203
  accessToken: this.accessToken,
@@ -217,15 +216,6 @@ export class ContentfulContentSource implements ContentSourceTypes.ContentSource
217
216
  const appInstallations: AppInstallationProps[] = await fetchAllAppInstallations(this.plainClient);
218
217
  await this.fetchUsers();
219
218
 
220
- // The contentPoller updates its syncContext property internally when
221
- // it gets an updated content. But, the actual cached data is not
222
- // updated. Therefore, when content source module is restarted, we need
223
- // to reset the contentPoller's internal syncContext, so it will be
224
- // able to fetch all the updated content from its cached state.
225
- if (this.contentPoller && this.contentPoller.pollType === 'date') {
226
- this.contentPoller.setSyncContext(this.syncContext);
227
- }
228
-
229
219
  // replace all data at once in atomic action
230
220
  this.locales = locales.filter((locale) => {
231
221
  // filter out disabled locales
@@ -339,11 +329,11 @@ export class ContentfulContentSource implements ContentSourceTypes.ContentSource
339
329
  previewToken: this.previewToken,
340
330
  managementToken: this.accessToken,
341
331
  pollType: 'date',
342
- syncContext: this.syncContext,
332
+ syncContext: this.getSyncContextForContentPollerFromCache(),
343
333
  notificationCallback: async (syncResult) => {
344
334
  if (syncResult.contentTypes.length) {
345
335
  this.logger.debug('content type was changed, invalidate schema');
346
- this.cache.invalidateSchema();
336
+ await this.cache.invalidateSchema();
347
337
  } else {
348
338
  const result = await this.convertSyncResult(syncResult);
349
339
  await this.cache.updateContent(result);
@@ -367,7 +357,32 @@ export class ContentfulContentSource implements ContentSourceTypes.ContentSource
367
357
  this.userMap = _.keyBy(users, 'sys.id');
368
358
  }
369
359
 
370
- private async convertSyncResult(syncResult: SyncResult) {
360
+ private async fetchUsersIfNeeded(entities: (EntryProps | AssetProps)[]) {
361
+ const entityHasNoCachedAuthor = entities.some((entity) => !this.userMap[entity.sys.updatedBy?.sys.id ?? '']);
362
+ if (entityHasNoCachedAuthor) {
363
+ await this.fetchUsers();
364
+ }
365
+ }
366
+
367
+ private updateContentPollerSyncContext(syncContext: ContentPollerSyncContext) {
368
+ if (this.contentPoller && this.contentPoller.pollType === 'date') {
369
+ this.contentPoller.setSyncContext({
370
+ ...this.getSyncContextForContentPollerFromCache(),
371
+ ...syncContext
372
+ });
373
+ }
374
+ }
375
+
376
+ private getSyncContextForContentPollerFromCache() {
377
+ const cacheSyncContext = this.cache.getSyncContext();
378
+ return {
379
+ lastUpdatedEntryDate: cacheSyncContext.documentsSyncContext as string | undefined,
380
+ lastUpdatedAssetDate: cacheSyncContext.assetsSyncContext as string | undefined,
381
+ lastUpdatedContentTypeDate: this.cache.getSchema().context?.lastUpdatedContentTypeDate
382
+ };
383
+ }
384
+
385
+ private async convertSyncResult(syncResult: ContentPollerSyncResult) {
371
386
  // remove deleted entries and assets from fieldData
372
387
  // generally, the "sync" method of the preview API never notifies of deleted objects, therefore we rely on
373
388
  // the deleteObject method to notify the user that restart is needed. Then, once user restarts the SSG, it
@@ -380,11 +395,7 @@ export class ContentfulContentSource implements ContentSourceTypes.ContentSource
380
395
  deletedAssets: syncResult.deletedAssets.length
381
396
  });
382
397
 
383
- const resultHasNoCachedAuthor = [...syncResult.entries, ...syncResult.assets].some((data) => !this.userMap[data.sys.updatedBy?.sys.id ?? '']);
384
-
385
- if (resultHasNoCachedAuthor) {
386
- await this.fetchUsers();
387
- }
398
+ await this.fetchUsersIfNeeded([...syncResult.entries, ...syncResult.assets]);
388
399
 
389
400
  // convert updated/created entries and assets
390
401
  const documents = this.convertEntries(syncResult.entries, this.cache.getModelByName);
@@ -413,90 +424,107 @@ export class ContentfulContentSource implements ContentSourceTypes.ContentSource
413
424
  bynderImagesAsList: this.bynderImagesAsList
414
425
  });
415
426
 
427
+ const prevLastUpdatedContentTypeDate = this.cache.getSchema().context?.lastUpdatedContentTypeDate;
428
+
416
429
  // Check if one of the content types was changed from the last time the
417
430
  // content types were fetched, in which case remove all cached content.
418
431
  const lastUpdatedContentTypeDate = getLastUpdatedEntityDate(contentTypes);
419
- if (this.syncContext.lastUpdatedContentTypeDate !== lastUpdatedContentTypeDate) {
432
+ if (prevLastUpdatedContentTypeDate !== lastUpdatedContentTypeDate) {
420
433
  this.logger.debug(
421
- `last updated content type date '${lastUpdatedContentTypeDate}' is different from cached ` +
422
- `syncContext.lastUpdatedContentTypeDate '${this.syncContext.lastUpdatedContentTypeDate}', clearing cache`
434
+ `last updated content type date '${lastUpdatedContentTypeDate}' is different ` +
435
+ `from the cached date '${prevLastUpdatedContentTypeDate}', clearing cache`
423
436
  );
424
- await this.cache.remove('entries');
425
- await this.cache.remove('assets');
426
- this.syncContext = { lastUpdatedContentTypeDate };
427
- await this.cache.set('syncContext', this.syncContext);
437
+ await this.cache.clearSyncContext({
438
+ clearDocumentsSyncContext: true,
439
+ clearAssetsSyncContext: false
440
+ });
428
441
  }
429
442
 
443
+ this.updateContentPollerSyncContext({ lastUpdatedContentTypeDate });
444
+
430
445
  return {
431
446
  models,
432
447
  locales: this.locales.map((locale) => ({
433
448
  code: locale.code,
434
449
  default: locale.default
435
450
  })),
436
- context: {}
451
+ context: {
452
+ lastUpdatedContentTypeDate: lastUpdatedContentTypeDate
453
+ }
437
454
  };
438
455
  }
439
456
 
440
- async getDocuments(): Promise<ContextualDocument[]> {
457
+ async getDocuments(options?: { syncContext?: string }): Promise<ContextualDocument[] | { documents: ContextualDocument[]; syncContext?: string }> {
441
458
  this.logger.debug('getDocuments');
459
+ let lastUpdatedEntryDate = options?.syncContext;
442
460
  let entries: EntryProps[];
443
- const cachedEntries = ((await this.cache.get('entries')) as EntryProps[]) ?? [];
444
461
  try {
445
- if (this.syncContext.lastUpdatedEntryDate && !_.isEmpty(cachedEntries)) {
446
- this.logger.debug(`got ${cachedEntries.length} entries from cache, fetching entries updated after ${this.syncContext.lastUpdatedEntryDate}`);
447
- const updatedEntries = await fetchEntriesUpdatedAfter(this.plainClient, this.syncContext.lastUpdatedEntryDate, this.userLogger);
448
- this.logger.debug(`got ${updatedEntries.length} updated/created entries after ${this.syncContext.lastUpdatedEntryDate}`);
449
- if (updatedEntries.length) {
450
- this.syncContext.lastUpdatedEntryDate = getLastUpdatedEntityDate(updatedEntries);
462
+ if (lastUpdatedEntryDate) {
463
+ this.logger.debug(`fetching entries updated after ${lastUpdatedEntryDate}`);
464
+ entries = await fetchEntriesUpdatedAfter(this.plainClient, lastUpdatedEntryDate, this.userLogger);
465
+ this.logger.debug(`got ${entries.length} updated/created entries after ${lastUpdatedEntryDate}`);
466
+ if (entries.length) {
467
+ lastUpdatedEntryDate = getLastUpdatedEntityDate(entries);
451
468
  }
452
- entries = _.unionBy(updatedEntries, cachedEntries, 'sys.id');
453
469
  } else {
454
470
  entries = await fetchAllEntries(this.plainClient, this.userLogger);
455
- this.syncContext.lastUpdatedEntryDate = getLastUpdatedEntityDate(entries);
471
+ lastUpdatedEntryDate = getLastUpdatedEntityDate(entries);
456
472
  }
457
473
  } catch (error: any) {
458
474
  // Stackbit won't be able to work properly even if one of the entries was not fetched.
459
475
  // All fetch methods use Contentful's API client that handles errors and retries automatically.
460
476
  this.logger.error(`Failed fetching documents from Contentful, error: ${error.message}`);
461
- return [];
477
+ // By returning the original syncContext we are ensuring that next
478
+ // time the getDocuments is called, it will try to get the documents
479
+ // using the same syncContext
480
+ return { documents: [], syncContext: lastUpdatedEntryDate };
462
481
  }
463
- await this.cache.set('entries', entries);
464
- await this.cache.set('syncContext', this.syncContext);
482
+
483
+ this.updateContentPollerSyncContext({ lastUpdatedEntryDate });
484
+
465
485
  this.logger.debug(
466
- `got ${entries.length} entries from space ${this.spaceId}, environment ${this.environment}, lastUpdatedEntryDate: ${this.syncContext.lastUpdatedEntryDate}`
486
+ `got ${entries.length} entries from space ${this.spaceId}, environment ${this.environment}, lastUpdatedEntryDate: ${lastUpdatedEntryDate}`
467
487
  );
468
- return this.convertEntries(entries, this.cache.getModelByName);
488
+ await this.fetchUsersIfNeeded(entries);
489
+
490
+ const documents = this.convertEntries(entries, this.cache.getModelByName);
491
+
492
+ return { documents, syncContext: lastUpdatedEntryDate };
469
493
  }
470
494
 
471
- async getAssets() {
495
+ async getAssets(options?: { syncContext?: string }): Promise<ContextualAsset[] | { assets: ContextualAsset[]; syncContext?: string }> {
472
496
  this.logger.debug('getAssets');
473
- let assets: AssetProps[];
474
- const cachedAssets = ((await this.cache.get('assets')) as AssetProps[]) ?? [];
497
+ let lastUpdatedAssetDate = options?.syncContext;
498
+ let ctflAssets: AssetProps[];
475
499
  try {
476
- if (this.syncContext.lastUpdatedAssetDate && !_.isEmpty(cachedAssets)) {
477
- this.logger.debug(`got ${cachedAssets.length} assets from cache, fetching assets updated after ${this.syncContext.lastUpdatedAssetDate}`);
478
- const updatedAssets = await fetchAssetsUpdatedAfter(this.plainClient, this.syncContext.lastUpdatedAssetDate, this.userLogger);
479
- this.logger.debug(`got ${updatedAssets.length} updated/created assets after ${this.syncContext.lastUpdatedAssetDate}`);
480
- if (updatedAssets.length) {
481
- this.syncContext.lastUpdatedAssetDate = getLastUpdatedEntityDate(updatedAssets);
500
+ if (lastUpdatedAssetDate) {
501
+ this.logger.debug(`fetching assets updated after ${lastUpdatedAssetDate}`);
502
+ ctflAssets = await fetchAssetsUpdatedAfter(this.plainClient, lastUpdatedAssetDate, this.userLogger);
503
+ this.logger.debug(`got ${ctflAssets.length} updated/created assets after ${lastUpdatedAssetDate}`);
504
+ if (ctflAssets.length) {
505
+ lastUpdatedAssetDate = getLastUpdatedEntityDate(ctflAssets);
482
506
  }
483
- assets = _.unionBy(updatedAssets, cachedAssets, 'sys.id');
484
507
  } else {
485
- assets = await fetchAllAssets(this.plainClient, this.userLogger);
486
- this.syncContext.lastUpdatedAssetDate = getLastUpdatedEntityDate(assets);
508
+ ctflAssets = await fetchAllAssets(this.plainClient, this.userLogger);
509
+ lastUpdatedAssetDate = getLastUpdatedEntityDate(ctflAssets);
487
510
  }
488
511
  } catch (error: any) {
489
512
  // Stackbit won't be able to work properly even if one of the entries or the assets was not fetched.
490
513
  // All fetch methods use Contentful's API client that handles errors and retries automatically.
491
514
  this.logger.error(`Failed fetching assets from Contentful, error: ${error.message}`);
492
- return [];
515
+ return { assets: [], syncContext: lastUpdatedAssetDate };
493
516
  }
494
- await this.cache.set('assets', assets);
495
- await this.cache.set('syncContext', this.syncContext);
517
+
518
+ this.updateContentPollerSyncContext({ lastUpdatedAssetDate });
519
+
496
520
  this.logger.debug(
497
- `got ${assets.length} assets from space ${this.spaceId}, environment ${this.environment}, lastUpdatedAssetDate: ${this.syncContext.lastUpdatedAssetDate}`
521
+ `got ${ctflAssets.length} assets from space ${this.spaceId}, environment ${this.environment}, lastUpdatedAssetDate: ${lastUpdatedAssetDate}`
498
522
  );
499
- return this.convertAssets(assets);
523
+ await this.fetchUsersIfNeeded(ctflAssets);
524
+
525
+ const assets = this.convertAssets(ctflAssets);
526
+
527
+ return { assets, syncContext: lastUpdatedAssetDate };
500
528
  }
501
529
 
502
530
  async hasAccess({ userContext }: { userContext?: UserContext }): Promise<{ hasConnection: boolean; hasPermissions: boolean }> {
package/src/utils.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  import { BasicMetaSysProps } from 'contentful-management';
2
2
 
3
- export type SyncContext = {
4
- lastUpdatedEntryDate?: string;
5
- lastUpdatedAssetDate?: string;
6
- lastUpdatedContentTypeDate?: string;
7
- };
8
-
9
3
  export function getLastUpdatedEntityDate(entities: { sys: BasicMetaSysProps }[]): string | undefined {
10
4
  let lastUpdatedDate: string = '';
11
5
  for (const entity of entities) {