@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/content-poller.d.ts +9 -5
- package/dist/content-poller.d.ts.map +1 -1
- package/dist/content-poller.js +6 -7
- package/dist/content-poller.js.map +1 -1
- package/dist/contentful-api-client.d.ts +4 -3
- package/dist/contentful-api-client.d.ts.map +1 -1
- package/dist/contentful-api-client.js +20 -8
- package/dist/contentful-api-client.js.map +1 -1
- package/dist/contentful-content-source.d.ts +18 -4
- package/dist/contentful-content-source.d.ts.map +1 -1
- package/dist/contentful-content-source.js +72 -55
- package/dist/contentful-content-source.js.map +1 -1
- package/dist/utils.d.ts +0 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/content-poller.ts +20 -15
- package/src/contentful-api-client.ts +19 -7
- package/src/contentful-content-source.ts +91 -63
- package/src/utils.ts +0 -6
|
@@ -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,
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 (
|
|
432
|
+
if (prevLastUpdatedContentTypeDate !== lastUpdatedContentTypeDate) {
|
|
420
433
|
this.logger.debug(
|
|
421
|
-
`last updated content type date '${lastUpdatedContentTypeDate}' is different
|
|
422
|
-
`
|
|
434
|
+
`last updated content type date '${lastUpdatedContentTypeDate}' is different ` +
|
|
435
|
+
`from the cached date '${prevLastUpdatedContentTypeDate}', clearing cache`
|
|
423
436
|
);
|
|
424
|
-
await this.cache.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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 (
|
|
446
|
-
this.logger.debug(`
|
|
447
|
-
|
|
448
|
-
this.logger.debug(`got ${
|
|
449
|
-
if (
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
482
|
+
|
|
483
|
+
this.updateContentPollerSyncContext({ lastUpdatedEntryDate });
|
|
484
|
+
|
|
465
485
|
this.logger.debug(
|
|
466
|
-
`got ${entries.length} entries from space ${this.spaceId}, environment ${this.environment}, lastUpdatedEntryDate: ${
|
|
486
|
+
`got ${entries.length} entries from space ${this.spaceId}, environment ${this.environment}, lastUpdatedEntryDate: ${lastUpdatedEntryDate}`
|
|
467
487
|
);
|
|
468
|
-
|
|
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
|
|
474
|
-
|
|
497
|
+
let lastUpdatedAssetDate = options?.syncContext;
|
|
498
|
+
let ctflAssets: AssetProps[];
|
|
475
499
|
try {
|
|
476
|
-
if (
|
|
477
|
-
this.logger.debug(`
|
|
478
|
-
|
|
479
|
-
this.logger.debug(`got ${
|
|
480
|
-
if (
|
|
481
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
495
|
-
|
|
517
|
+
|
|
518
|
+
this.updateContentPollerSyncContext({ lastUpdatedAssetDate });
|
|
519
|
+
|
|
496
520
|
this.logger.debug(
|
|
497
|
-
`got ${
|
|
521
|
+
`got ${ctflAssets.length} assets from space ${this.spaceId}, environment ${this.environment}, lastUpdatedAssetDate: ${lastUpdatedAssetDate}`
|
|
498
522
|
);
|
|
499
|
-
|
|
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) {
|