@stackbit/cms-contentstack 0.1.2-develop.2 → 0.1.2-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.
@@ -15,7 +15,7 @@ import {
15
15
  convertAssets,
16
16
  DocumentWithContext
17
17
  } from './contentstack-entries-converter';
18
- import { createEntryFromOperationFields, updateEntryFromUpdateOperations } from './contentstack-operation-converter';
18
+ import { createEntryFromOperationFields, updateEntryFromUpdateOperations, GetLeanDocumentById } from './contentstack-operation-converter';
19
19
  import { getContentstackDashboardUrl, resolvePublishEnvironment, downloadFile, createWebhookIfNeeded, userMissingInMap } from './contentstack-utils';
20
20
  import { ContentPoller, getLastUpdatedEntityDate, SyncContext, SyncResult } from './contentstack-content-poller';
21
21
  import type { User, Asset, Entry, Environment, WebhookPayload } from './contentstack-types';
@@ -85,6 +85,7 @@ export class ContentstackContentSource
85
85
  private publishEnvironmentName?: string;
86
86
  private publishEnvironment!: Environment;
87
87
  private usersById: Record<string, User> = {};
88
+ private newUnsyncedEntryMap: Record<string, string> = {};
88
89
 
89
90
  constructor({ apiKey, managementToken, branch, authtoken, masterLocale, publishEnvironmentName, skipFetchOnStartIfCache }: ContentStackSourceOptions) {
90
91
  this.apiKey = apiKey;
@@ -214,6 +215,11 @@ export class ContentstackContentSource
214
215
  } else {
215
216
  const result = await this.convertSyncResult(syncResult);
216
217
  await this.cache.updateContent(result);
218
+ for (const document of result.documents) {
219
+ if (this.newUnsyncedEntryMap[document.id]) {
220
+ delete this.newUnsyncedEntryMap[document.id];
221
+ }
222
+ }
217
223
  }
218
224
  }
219
225
  });
@@ -336,7 +342,7 @@ export class ContentstackContentSource
336
342
  let entries: Entry[];
337
343
  const cachedEntries = ((await this.cache.get('entries')) as Entry[]) ?? [];
338
344
  try {
339
- if (this.syncContext.lastUpdatedEntryDate && cachedEntries) {
345
+ if (this.syncContext.lastUpdatedEntryDate && !_.isEmpty(cachedEntries)) {
340
346
  entries = cachedEntries;
341
347
  if (!this.skipFetchOnStartIfCache) {
342
348
  this.logger.debug(
@@ -382,7 +388,7 @@ export class ContentstackContentSource
382
388
  let assets: Asset[];
383
389
  const cachedAssets = ((await this.cache.get('assets')) as Asset[]) ?? [];
384
390
  try {
385
- if (this.syncContext.lastUpdatedAssetDate && cachedAssets) {
391
+ if (this.syncContext.lastUpdatedAssetDate && !_.isEmpty(cachedAssets)) {
386
392
  assets = cachedAssets;
387
393
  if (!this.skipFetchOnStartIfCache) {
388
394
  this.logger.debug(`got ${cachedAssets.length} assets from cache, fetching assets updated after ${this.syncContext.lastUpdatedAssetDate}`);
@@ -439,11 +445,14 @@ export class ContentstackContentSource
439
445
  const entry = createEntryFromOperationFields({
440
446
  updateOperationFields: options.updateOperationFields,
441
447
  model: options.model,
442
- getDocumentById: this.cache.getDocumentById,
448
+ getDocumentById: this.getCachedDocumentById.bind(this),
443
449
  getModelByName: this.cache.getModelByName,
444
450
  logger: this.logger
445
451
  });
446
452
  const newEntry = await this.contentStackClient.createEntry(options.model.name, entry);
453
+ // cache the created entry's uid and model name until it is received from Contentstack
454
+ // via onWebhook or contentPoller's notificationCallback methods.
455
+ this.newUnsyncedEntryMap[newEntry.uid] = options.model.name;
447
456
  return { documentId: newEntry.uid };
448
457
  }
449
458
 
@@ -458,13 +467,28 @@ export class ContentstackContentSource
458
467
  const updatedEntry = updateEntryFromUpdateOperations({
459
468
  entry,
460
469
  updateOperations: operations,
461
- getDocumentById: this.cache.getDocumentById,
470
+ getDocumentById: this.getCachedDocumentById.bind(this),
462
471
  getModelByName: this.cache.getModelByName,
463
472
  logger: this.logger
464
473
  });
465
474
  await this.contentStackClient.updateEntry(updatedEntry);
466
475
  }
467
476
 
477
+ getCachedDocumentById(documentId: string): ReturnType<GetLeanDocumentById> {
478
+ const document = this.cache.getDocumentById(documentId);
479
+ if (document) {
480
+ return document;
481
+ }
482
+ if (this.newUnsyncedEntryMap[documentId]) {
483
+ this.logger.debug(`the created document ${documentId} was not yet fetched from Contentstack, using the cached document`);
484
+ return {
485
+ id: documentId,
486
+ modelName: this.newUnsyncedEntryMap[documentId]!
487
+ };
488
+ }
489
+ return;
490
+ }
491
+
468
492
  async deleteDocument(options: { document: StackbitTypes.Document<unknown>; userContext?: UserWithContext }): Promise<void> {
469
493
  this.logger.debug('deleteDocument');
470
494
  await this.contentStackClient.deleteEntry(options.document.modelName, options.document.id);
@@ -483,7 +507,7 @@ export class ContentstackContentSource
483
507
  let asset: Asset;
484
508
  try {
485
509
  if (options.base64) {
486
- // TODO: write to .stackbit/cache folder
510
+ // TODO: pass the path to .stackbit/cache folder in init() options and use it to write the temp file
487
511
  await writeFile(tempName, Buffer.from(options.base64, 'base64'));
488
512
  } else {
489
513
  await downloadFile(options.url!, tempName);
@@ -549,41 +573,31 @@ export class ContentstackContentSource
549
573
 
550
574
  this.logger.debug(`got webhook for ${webhookData.event} ${webhookData.module}`);
551
575
 
576
+ let unsyncedEntryUid: string | undefined;
577
+
552
578
  if (webhookData.module === 'entry') {
553
579
  switch (webhookData.event) {
554
580
  case 'create':
555
- case 'update': {
556
- if (userMissingInMap(this.usersById, webhookData.data.entry as Entry)) {
557
- await this.fetchUsers();
558
- }
559
- const document = convertEntry({
560
- entry: {
561
- ...webhookData.data.entry,
562
- content_type_uid: webhookData.data.content_type.uid,
563
- publish_details: [],
564
- _branch: webhookData.data.branch.uid
565
- } as unknown as Entry,
566
- status: webhookData.event === 'create' ? 'added' : 'modified',
567
- apiKey: this.apiKey,
568
- getModelByName: this.cache.getModelByName,
569
- usersById: this.usersById,
570
- publishEnvironment: this.publishEnvironment,
571
- logger: this.logger
572
- });
573
- updates.documents!.push(document);
574
- break;
575
- }
581
+ case 'update':
576
582
  case 'publish': {
577
- if (webhookData.data.environment.uid !== this.publishEnvironment.uid) {
583
+ // Generally, entry publish webhooks created by the content source are already configured to filter
584
+ // only the publishEnvironment. However, still check for the publish event environment in case the
585
+ // webhook was manually changed.
586
+ if (webhookData.event === 'publish' && webhookData.data.environment.uid !== this.publishEnvironment.uid) {
578
587
  this.logger.debug(
579
588
  `entry was published in an environment '${webhookData.data.environment.name}' ` +
580
589
  `different from the publish environment '${this.publishEnvironment.name}', ignoring the webhook'`
581
590
  );
582
591
  break;
583
592
  }
593
+ if (webhookData.event === 'create' && webhookData.data.entry.uid in this.newUnsyncedEntryMap) {
594
+ unsyncedEntryUid = webhookData.data.entry.uid;
595
+ }
584
596
  if (userMissingInMap(this.usersById, webhookData.data.entry as Entry)) {
585
597
  await this.fetchUsers();
586
598
  }
599
+ const status: StackbitTypes.DocumentStatus =
600
+ webhookData.event === 'publish' ? 'published' : webhookData.event === 'create' ? 'added' : 'modified';
587
601
  const document = convertEntry({
588
602
  entry: {
589
603
  ...webhookData.data.entry,
@@ -591,7 +605,7 @@ export class ContentstackContentSource
591
605
  publish_details: [],
592
606
  _branch: webhookData.data.branch.uid
593
607
  } as unknown as Entry,
594
- status: 'published',
608
+ status: status,
595
609
  apiKey: this.apiKey,
596
610
  getModelByName: this.cache.getModelByName,
597
611
  usersById: this.usersById,
@@ -618,25 +632,9 @@ export class ContentstackContentSource
618
632
  } else if (webhookData.module === 'asset') {
619
633
  switch (webhookData.event) {
620
634
  case 'create':
621
- case 'update': {
622
- if (userMissingInMap(this.usersById, webhookData.data.asset as Asset)) {
623
- await this.fetchUsers();
624
- }
625
- const asset = convertAsset({
626
- asset: {
627
- ...webhookData.data.asset,
628
- publish_details: [],
629
- _branch: webhookData.data.branch.uid
630
- } as unknown as Asset,
631
- apiKey: this.apiKey,
632
- usersById: this.usersById,
633
- publishEnvironment: this.publishEnvironment
634
- });
635
- updates.assets!.push(asset);
636
- break;
637
- }
635
+ case 'update':
638
636
  case 'publish': {
639
- if (webhookData.data.environment.uid !== this.publishEnvironment.uid) {
637
+ if (webhookData.event === 'publish' && webhookData.data.environment.uid !== this.publishEnvironment.uid) {
640
638
  this.logger.debug(
641
639
  `asset was published in an environment '${webhookData.data.environment.name}' ` +
642
640
  `different from the publish environment '${this.publishEnvironment.name}', ignoring the webhook'`
@@ -686,6 +684,10 @@ export class ContentstackContentSource
686
684
  `deleted documents: ${updates.deletedDocumentIds?.length}, ` +
687
685
  `deleted assets: ${updates.deletedAssetIds?.length}`
688
686
  );
689
- this.cache.updateContent(updates);
687
+ await this.cache.updateContent(updates);
688
+
689
+ if (unsyncedEntryUid) {
690
+ delete this.newUnsyncedEntryMap[unsyncedEntryUid];
691
+ }
690
692
  }
691
693
  }
@@ -4,10 +4,9 @@ import * as StackbitTypes from '@stackbit/types';
4
4
  import type { EntryData } from '@contentstack/management/types/stack/contentType/entry';
5
5
  import type { Entry, Asset } from './contentstack-types';
6
6
  import type { ModelWithContext } from './contentstack-schema-converter';
7
- import type { DocumentWithContext } from './contentstack-entries-converter';
8
7
 
9
8
  type GetModelByName = (modelName: string) => ModelWithContext | undefined;
10
- type GetDocumentById = (documentId: string) => DocumentWithContext | undefined;
9
+ export type GetLeanDocumentById = (documentId: string) => { id: string; modelName: string } | undefined;
11
10
 
12
11
  export function createEntryFromOperationFields({
13
12
  updateOperationFields,
@@ -18,7 +17,7 @@ export function createEntryFromOperationFields({
18
17
  }: {
19
18
  updateOperationFields: Record<string, StackbitTypes.UpdateOperationField>;
20
19
  model: ModelWithContext;
21
- getDocumentById: GetDocumentById;
20
+ getDocumentById: GetLeanDocumentById;
22
21
  getModelByName: GetModelByName;
23
22
  logger: StackbitTypes.Logger;
24
23
  }): EntryData {
@@ -55,7 +54,7 @@ export function updateEntryFromUpdateOperations({
55
54
  }: {
56
55
  entry: Entry;
57
56
  updateOperations: StackbitTypes.UpdateOperation[];
58
- getDocumentById: GetDocumentById;
57
+ getDocumentById: GetLeanDocumentById;
59
58
  getModelByName: GetModelByName;
60
59
  logger: StackbitTypes.Logger;
61
60
  }): Entry {
@@ -117,7 +116,7 @@ export function getUpdatedEntryAtFieldPathWithOperation({
117
116
  entry: Entry;
118
117
  updateOperation: StackbitTypes.UpdateOperation;
119
118
  model: ModelWithContext;
120
- getDocumentById: GetDocumentById;
119
+ getDocumentById: GetLeanDocumentById;
121
120
  getModelByName: GetModelByName;
122
121
  logger: StackbitTypes.Logger;
123
122
  }): Entry {
@@ -378,7 +377,7 @@ function getValueForOperation({
378
377
  rootModel: ModelWithContext;
379
378
  entryFieldPath: (string | number)[];
380
379
  modelFieldPath: string[];
381
- getDocumentById: GetDocumentById;
380
+ getDocumentById: GetLeanDocumentById;
382
381
  getModelByName: GetModelByName;
383
382
  errorPrefix: string;
384
383
  logger: StackbitTypes.Logger;
@@ -468,7 +467,7 @@ export function convertOperationFieldValue({
468
467
  isInList: boolean;
469
468
  entryFieldPath: (string | number)[];
470
469
  modelFieldPath: string[];
471
- getDocumentById: GetDocumentById;
470
+ getDocumentById: GetLeanDocumentById;
472
471
  getModelByName: GetModelByName;
473
472
  errorPrefix: string;
474
473
  }): any {
@@ -284,6 +284,11 @@ export type Entry = ContentStackEntry & {
284
284
  updated_at: string;
285
285
  created_by: string;
286
286
  updated_by: string;
287
+ /**
288
+ * The entry's locale.
289
+ * Note, this can be different from the `publish_details.locale` if the
290
+ * entry was not localized for the published locale.
291
+ */
287
292
  locale?: string;
288
293
  tags?: string[];
289
294
  publish_details?: PublishDetails[];
@@ -297,23 +302,49 @@ export type PublishDetails = {
297
302
  environment: string;
298
303
  time: string;
299
304
  user: string;
305
+ /**
306
+ * The version of the entry that was published.
307
+ * If the entry_locale is provided, then the version is of that entry locale.
308
+ * Otherwise, it is a version of the entry's own locale.
309
+ */
300
310
  version: number;
301
311
  /**
302
- * The locale of the published document.
303
- * Note, the locale may be different from entry's own locale.
312
+ * The published locale.
313
+ * Note, the published locale may be different from the entry's locale if a
314
+ * non-localized entry was published.
315
+ *
316
+ * For example, a non-localized "fr-fr" entry can be published to the "fr-fr"
317
+ * locale. In this case, the entry will be published from the values stored
318
+ * in the entry of the fallback locale, e.g., 'en-us'. When this entry is
319
+ * fetched using the content delivery API with `locale=fr-fr` it will return
320
+ * the entry with `locale: 'en-us'`, and `publish_details.locale: 'fr-fr'`.
321
+ *
322
+ * If an entry was not published to a `fr-fr` locale but was published to its
323
+ * fallback locale, then it will be returned from the Content Delivery API
324
+ * for `locale=fr-fr` parameter only if it also has `include_fallback=false`.
325
+ * In other words, non-published locales will be returned only when
326
+ * `include_fallback=true`.
304
327
  */
305
328
  locale: string;
306
329
  /**
307
- * The 'entry_locale' specified the original entry locale under which the
308
- * entry was published, it may be different from the 'locale'.
330
+ * The 'publish_details.entry_locale' specifies the original entry locale
331
+ * from which the entry was published, and it may be different from the
332
+ * 'publish_details.locale' if the entry was localized after it was published.
333
+ *
334
+ * It also specifies the entry's locale the version field relates to.
309
335
  *
310
- * For example, if an entry with locale "A" has been published to locale "B",
311
- * for which it isn't localized. Then the 'locale' will be locale "B", while
312
- * the entry's own locale will remain "A" (because it is not localized),
313
- * even when entries are fetched with locale=B filter.
314
- * But, once the entry will be localized to locale "B", the 'entry_locale'
315
- * will be set to locale "A", and the 'locale' will be set to locale "B"
316
- * matching the entry's own locale.
336
+ * For example, if an entry with locale "en-us" has been published to "fr-fr"
337
+ * locale without having a localized entry for "fr-fr". Then, when the entry
338
+ * is fetched with the locale=fr-fr filter, the `publish_details.locale`
339
+ * will be set to "fr-fr", but the entry's locale will remain "en-us" because
340
+ * it was not localized, and the `publish_details.entry_locale` will be undefined.
341
+ * Then, if the entry will be localized to "fr-fr" locale, the locale of
342
+ * the entry will be changed to "fr-fr", the `publish_details.locale` will
343
+ * remain "fr-fr" and the `publish_details.entry_locale` will be set to
344
+ * "en-us" specifying that the published entry's locale doesn't match the
345
+ * current's entry locale. Only after the localized "fr-fr" entry will be
346
+ * published to "fr-fr", then the `publish_details.entry_locale` will be set
347
+ * back to undefined.
317
348
  */
318
349
  entry_locale?: string;
319
350
  };