@stackbit/cms-core 0.2.1 → 0.3.0-develop.0

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.
Files changed (54) hide show
  1. package/dist/content-store-utils.d.ts +5 -1
  2. package/dist/content-store-utils.d.ts.map +1 -1
  3. package/dist/content-store-utils.js +28 -3
  4. package/dist/content-store-utils.js.map +1 -1
  5. package/dist/content-store.d.ts +12 -1
  6. package/dist/content-store.d.ts.map +1 -1
  7. package/dist/content-store.js +399 -177
  8. package/dist/content-store.js.map +1 -1
  9. package/dist/types/content-store-document-fields.d.ts +26 -4
  10. package/dist/types/content-store-document-fields.d.ts.map +1 -1
  11. package/dist/types/content-store-documents.d.ts +14 -3
  12. package/dist/types/content-store-documents.d.ts.map +1 -1
  13. package/dist/types/content-store-types.d.ts +7 -1
  14. package/dist/types/content-store-types.d.ts.map +1 -1
  15. package/dist/utils/backward-compatibility.d.ts +184 -0
  16. package/dist/utils/backward-compatibility.d.ts.map +1 -0
  17. package/dist/utils/backward-compatibility.js +151 -0
  18. package/dist/utils/backward-compatibility.js.map +1 -0
  19. package/dist/utils/config-delegate.d.ts +11 -0
  20. package/dist/utils/config-delegate.d.ts.map +1 -0
  21. package/dist/utils/config-delegate.js +226 -0
  22. package/dist/utils/config-delegate.js.map +1 -0
  23. package/dist/utils/create-update-csi-docs.d.ts +7 -5
  24. package/dist/utils/create-update-csi-docs.d.ts.map +1 -1
  25. package/dist/utils/create-update-csi-docs.js +24 -24
  26. package/dist/utils/create-update-csi-docs.js.map +1 -1
  27. package/dist/utils/csi-to-store-docs-converter.d.ts +17 -3
  28. package/dist/utils/csi-to-store-docs-converter.d.ts.map +1 -1
  29. package/dist/utils/csi-to-store-docs-converter.js +187 -47
  30. package/dist/utils/csi-to-store-docs-converter.js.map +1 -1
  31. package/dist/utils/site-map.d.ts.map +1 -1
  32. package/dist/utils/site-map.js +4 -1
  33. package/dist/utils/site-map.js.map +1 -1
  34. package/dist/utils/store-to-api-docs-converter.d.ts +6 -1
  35. package/dist/utils/store-to-api-docs-converter.d.ts.map +1 -1
  36. package/dist/utils/store-to-api-docs-converter.js +140 -51
  37. package/dist/utils/store-to-api-docs-converter.js.map +1 -1
  38. package/dist/utils/store-to-csi-docs-converter.d.ts +1 -0
  39. package/dist/utils/store-to-csi-docs-converter.d.ts.map +1 -1
  40. package/dist/utils/store-to-csi-docs-converter.js +2 -1
  41. package/dist/utils/store-to-csi-docs-converter.js.map +1 -1
  42. package/package.json +5 -5
  43. package/src/content-store-utils.ts +40 -6
  44. package/src/content-store.ts +552 -299
  45. package/src/types/content-store-document-fields.ts +16 -4
  46. package/src/types/content-store-documents.ts +12 -3
  47. package/src/types/content-store-types.ts +4 -1
  48. package/src/utils/backward-compatibility.ts +269 -0
  49. package/src/utils/config-delegate.ts +277 -0
  50. package/src/utils/create-update-csi-docs.ts +47 -50
  51. package/src/utils/csi-to-store-docs-converter.ts +256 -43
  52. package/src/utils/site-map.ts +19 -7
  53. package/src/utils/store-to-api-docs-converter.ts +185 -52
  54. package/src/utils/store-to-csi-docs-converter.ts +1 -1
@@ -18,7 +18,7 @@ import {
18
18
  Preset,
19
19
  PresetMap
20
20
  } from '@stackbit/sdk';
21
- import { append, deferWhileRunning, mapPromise, reducePromise } from '@stackbit/utils';
21
+ import { append, DeferredPromise, deferredPromise, deferWhileRunning, mapPromise, reducePromise } from '@stackbit/utils';
22
22
 
23
23
  import * as ContentStoreTypes from './types';
24
24
  import { Timer } from './utils/timer';
@@ -33,7 +33,10 @@ import {
33
33
  getUserContextForSrcType,
34
34
  groupDocumentsByContentSource,
35
35
  groupModelsByContentSource,
36
- updateOperationValueFieldWithCrossReference
36
+ isContentChangesEmpty,
37
+ isContentChangeResultEmpty,
38
+ updateOperationValueFieldWithCrossReference,
39
+ getErrorAtLine
37
40
  } from './content-store-utils';
38
41
  import {
39
42
  getSiteMapEntriesFromStackbitConfig,
@@ -48,6 +51,8 @@ import { mergeObjectWithDocument } from './utils/duplicate-document';
48
51
  import { normalizeModels, validateModels } from './utils/model-utils';
49
52
  import { IMAGE_MODEL } from './common/common-schema';
50
53
  import { getDocumentObjectFromPreset, getPresetFromDocument } from './utils/preset-utils';
54
+ import { BackCompatContentSourceInterface, backwardCompatibleContentSource } from './utils/backward-compatibility';
55
+ import { createConfigDelegate, getCreateConfigDelegateThunk } from './utils/config-delegate';
51
56
  import { GitService } from './services';
52
57
  import { CommandRunner } from '@stackbit/types';
53
58
 
@@ -70,6 +75,31 @@ export interface ContentSourceOptions {
70
75
 
71
76
  type ContentSourceData = ContentStoreTypes.ContentSourceData;
72
77
  type ContentSourceRawData = Omit<ContentSourceData, 'models' | 'modelMap' | 'documents' | 'documentMap'>;
78
+ type ContentStoreEventQueue = ContentStoreEvent[];
79
+
80
+ const ContentStoreEventType = {
81
+ YamlModelFilesChange: 'yamlModelFilesChange',
82
+ PresetFilesChange: 'presetFilesChange',
83
+ ContentSourceInvalidateSchema: 'contentSourceInvalidateSchema',
84
+ ContentSourceContentChange: 'contentSourceContentChange'
85
+ } as const;
86
+
87
+ type ContentStoreEvent =
88
+ | {
89
+ eventName: typeof ContentStoreEventType.YamlModelFilesChange;
90
+ }
91
+ | {
92
+ eventName: typeof ContentStoreEventType.PresetFilesChange;
93
+ }
94
+ | {
95
+ eventName: typeof ContentStoreEventType.ContentSourceInvalidateSchema;
96
+ contentSourceId: string;
97
+ }
98
+ | {
99
+ eventName: typeof ContentStoreEventType.ContentSourceContentChange;
100
+ contentSourceId: string;
101
+ contentChanges: CSITypes.ContentChanges;
102
+ };
73
103
 
74
104
  export const StackbitPresetModelName = 'stackbitPreset';
75
105
 
@@ -86,9 +116,9 @@ export class ContentStore {
86
116
  private readonly onContentChangeCallback: (contentChanges: ContentStoreTypes.ContentChangeResult) => void;
87
117
  private readonly handleConfigAssets: HandleConfigAssets;
88
118
  private readonly devAppRestartNeeded?: () => void;
89
- private contentSources: CSITypes.ContentSourceInterface[] = [];
119
+ private contentSources: BackCompatContentSourceInterface[] = [];
90
120
  private contentSourceDataById: Record<string, ContentSourceData> = {};
91
- private presetsContentSource?: CSITypes.ContentSourceInterface;
121
+ private presetsContentSource?: BackCompatContentSourceInterface;
92
122
  private contentUpdatesWatchTimer: Timer;
93
123
  private stackbitConfig: Config | null = null;
94
124
  private yamlModels: Model[] = [];
@@ -96,6 +126,8 @@ export class ContentStore {
96
126
  private modelExtensions: ModelExtension[] | null = null;
97
127
  private presets: PresetMap = {};
98
128
  private siteMapEntryGroups: SiteMapEntryGroups = {};
129
+ private processingContentSourcesPromise: DeferredPromise<void> | null = null;
130
+ private contentStoreEventQueue: ContentStoreEventQueue = [];
99
131
 
100
132
  constructor(options: ContentSourceOptions) {
101
133
  this.logger = options.logger.createLogger({ label: 'content-store' });
@@ -188,7 +220,7 @@ export class ContentStore {
188
220
  */
189
221
  private handleTimerTimeout() {
190
222
  for (const contentSourceInstance of this.contentSources) {
191
- contentSourceInstance.stopWatchingContentUpdates();
223
+ contentSourceInstance.stopWatchingContentUpdates?.();
192
224
  }
193
225
  }
194
226
 
@@ -215,8 +247,6 @@ export class ContentStore {
215
247
  async onFilesChange(updatedFiles: string[]): Promise<void> {
216
248
  this.logger.debug('onFilesChange');
217
249
 
218
- let schemaChanged = false;
219
-
220
250
  if (this.stackbitConfig && !this.stackbitConfig.modelExtensions) {
221
251
  // Check if any of the yaml models files were changed. If yaml model files were changed,
222
252
  // reload them and merge them with models defined in stackbit config.
@@ -224,9 +254,11 @@ export class ContentStore {
224
254
  const yamlModelsChanged = updatedFiles.find((updatedFile) => _.some(modelDirs, (modelDir) => updatedFile.startsWith(modelDir)));
225
255
  if (yamlModelsChanged) {
226
256
  this.logger.debug('identified change in stackbit model files');
227
- schemaChanged = true;
228
257
  this.yamlModels = await this.loadYamlModels({ stackbitConfig: this.stackbitConfig });
229
258
  this.configModels = this.mergeConfigModels(this.stackbitConfig.models ?? [], this.yamlModels);
259
+ this.pushContentSourceEvent({
260
+ eventName: ContentStoreEventType.YamlModelFilesChange
261
+ });
230
262
  }
231
263
  }
232
264
 
@@ -236,62 +268,38 @@ export class ContentStore {
236
268
  const presetsChanged = updatedFiles.find((updatedFile) => _.some(presetDirs, (presetDir) => updatedFile.startsWith(presetDir)));
237
269
  if (presetsChanged && !this.usesContentSourcePresets()) {
238
270
  this.logger.debug('identified change in stackbit preset files');
239
- schemaChanged = true;
240
271
  this.presets = await this.loadPresetsFromConfig({ stackbitConfig: this.stackbitConfig });
272
+ this.pushContentSourceEvent({
273
+ eventName: ContentStoreEventType.PresetFilesChange
274
+ });
241
275
  }
242
276
  }
243
277
 
244
- const contentSourceIdsWithChangedSchema: string[] = [];
245
- const contentChangeEvents: { contentSourceId: string; contentChangeEvent: CSITypes.ContentChangeEvent }[] = [];
246
-
247
278
  for (const contentSourceInstance of this.contentSources) {
248
279
  const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
249
280
  this.logger.debug(`call onFilesChange for contentSource: ${contentSourceId}`);
250
- const onFilesChangeResult = (await contentSourceInstance.onFilesChange?.({ updatedFiles: updatedFiles })) ?? {};
251
- this.logger.debug(`schemaChanged: ${onFilesChangeResult.schemaChanged}, has contentChangeEvent: ${!!onFilesChangeResult.contentChangeEvent}`);
252
-
253
- // if schema is changed, there is no need to return contentChanges
254
- // because schema changes reloads everything and implies content changes
255
- if (onFilesChangeResult.schemaChanged) {
256
- schemaChanged = true;
257
- contentSourceIdsWithChangedSchema.push(contentSourceId);
258
- } else if (onFilesChangeResult.contentChangeEvent) {
259
- contentChangeEvents.push({ contentSourceId, contentChangeEvent: onFilesChangeResult.contentChangeEvent });
281
+ const onFilesChangeResult = await contentSourceInstance.onFilesChange({ updatedFiles: updatedFiles });
282
+
283
+ // If the schema was changed in a specific content source, there is no need to process and notify for content changes.
284
+ // Because the schema changes will trigger loadContentSourcesAndProcessData and reload all models and content of that
285
+ // content source and send the schemaChanged notification that will cause the Studio to reload the schema and documents.
286
+ if (onFilesChangeResult.invalidateSchema) {
287
+ this.logger.debug(`schema was invalidated for contentSource: ${contentSourceId}`);
288
+ this.pushContentSourceEvent({
289
+ eventName: ContentStoreEventType.ContentSourceInvalidateSchema,
290
+ contentSourceId: contentSourceId
291
+ });
292
+ } else if (!isContentChangesEmpty(onFilesChangeResult.contentChanges)) {
293
+ this.logger.debug(`content was changed for contentSource: ${contentSourceId}`);
294
+ this.pushContentSourceEvent({
295
+ eventName: ContentStoreEventType.ContentSourceContentChange,
296
+ contentSourceId: contentSourceId,
297
+ contentChanges: onFilesChangeResult.contentChanges
298
+ });
260
299
  }
261
300
  }
262
301
 
263
- // If the schema was changed, there is no need to accumulate or notify about content changes.
264
- // The processData will update the store with the latest data. And once the Studio receives
265
- // the schemaChanged notification it will reload all the models and the documents with their latest state.
266
- if (schemaChanged) {
267
- await this.loadContentSourcesAndProcessData({ init: false, contentSourceIds: contentSourceIdsWithChangedSchema });
268
-
269
- this.onSchemaChangeCallback();
270
- } else {
271
- const contentChanges = contentChangeEvents.reduce((contentChanges: ContentStoreTypes.ContentChangeResult, { contentSourceId, contentChangeEvent }) => {
272
- const contentChangeResult = this.onContentChange(contentSourceId, contentChangeEvent);
273
- return {
274
- updatedDocuments: contentChanges.updatedDocuments.concat(contentChangeResult.updatedDocuments),
275
- updatedAssets: contentChanges.updatedAssets.concat(contentChangeResult.updatedAssets),
276
- deletedDocuments: contentChanges.deletedDocuments.concat(contentChangeResult.deletedDocuments),
277
- deletedAssets: contentChanges.deletedAssets.concat(contentChangeResult.deletedAssets)
278
- };
279
- }, {
280
- updatedDocuments: [],
281
- updatedAssets: [],
282
- deletedDocuments: [],
283
- deletedAssets: []
284
- });
285
-
286
- this.siteMapEntryGroups = await updateSiteMapEntriesWithContentChanges({
287
- siteMapEntryGroups: this.siteMapEntryGroups,
288
- contentChanges,
289
- stackbitConfig: this.stackbitConfig,
290
- contentSourceDataById: this.contentSourceDataById
291
- });
292
-
293
- this.onContentChangeCallback(contentChanges);
294
- }
302
+ await this.processContentStoreEvents();
295
303
  }
296
304
 
297
305
  private async loadYamlModels({ stackbitConfig }: { stackbitConfig: Config }): Promise<Model[]> {
@@ -371,19 +379,20 @@ export class ContentStore {
371
379
  */
372
380
  private async loadContentSourcesAndProcessData({ init, contentSourceIds }: { init: boolean; contentSourceIds?: string[] }) {
373
381
  this.logger.debug('loadContentSourcesAndProcessData', { init, contentSourceIds });
382
+ this.processingContentSourcesPromise = deferredPromise();
374
383
 
375
- const contentSources = this.stackbitConfig?.contentSources ?? [];
384
+ const contentSources = (this.stackbitConfig?.contentSources ?? []).map((contentSource) => {
385
+ return backwardCompatibleContentSource(contentSource);
386
+ });
376
387
 
377
- const promises = contentSources.map(
378
- (contentSourceInstance): Promise<ContentSourceRawData> => {
379
- const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
380
- if (init || !contentSourceIds || contentSourceIds.includes(contentSourceId)) {
381
- return this.loadContentSourceData({ contentSourceInstance, init });
382
- } else {
383
- return Promise.resolve(_.omit(this.contentSourceDataById[contentSourceId], ['models', 'modelMap', 'documents', 'documentMap']));
384
- }
388
+ const promises = contentSources.map((contentSourceInstance): Promise<ContentSourceRawData> => {
389
+ const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
390
+ if (init || !contentSourceIds || contentSourceIds.includes(contentSourceId)) {
391
+ return this.loadContentSourceData({ contentSourceInstance, init });
392
+ } else {
393
+ return Promise.resolve(_.omit(this.contentSourceDataById[contentSourceId], ['models', 'modelMap', 'documents', 'documentMap']));
385
394
  }
386
- );
395
+ });
387
396
 
388
397
  const contentSourceRawDataArr = await Promise.all(promises);
389
398
 
@@ -391,13 +400,10 @@ export class ContentStore {
391
400
  for (let i = 0; i < contentSources.length; i++) {
392
401
  const contentSourceDataRaw = contentSourceRawDataArr[i];
393
402
  if (contentSourceDataRaw?.csiModelMap?.[StackbitPresetModelName]) {
394
- this.presetsContentSource = contentSources[i];
395
- if (this.presetsContentSource) {
396
- const contentSourceId = getContentSourceIdForContentSource(this.presetsContentSource);
397
- // reload presets from content source only if needed
398
- if (init || !contentSourceIds || contentSourceIds.includes(contentSourceId)) {
399
- this.presets = await this.loadPresetsFromContentSource(contentSourceDataRaw);
400
- }
403
+ this.presetsContentSource = contentSourceDataRaw.instance;
404
+ // reload presets from content source only if needed
405
+ if (init || !contentSourceIds || contentSourceIds.includes(contentSourceDataRaw.id)) {
406
+ this.presets = await this.loadPresetsFromContentSource(contentSourceDataRaw);
401
407
  }
402
408
  break;
403
409
  }
@@ -422,47 +428,302 @@ export class ContentStore {
422
428
  stackbitConfig: this.stackbitConfig,
423
429
  contentSourceDataById: this.contentSourceDataById
424
430
  });
431
+
432
+ if (!init) {
433
+ this.onSchemaChangeCallback();
434
+ }
435
+
436
+ const processingPromise = this.processingContentSourcesPromise;
437
+ this.processingContentSourcesPromise = null;
438
+
439
+ // Do not "await" on processContentStoreEvents as it may introduce a deadlock with
440
+ // the nested loadContentSourcesAndProcessData call which is wrapped by deferWhileRunning.
441
+ this.processContentStoreEvents()
442
+ .catch((error) => {
443
+ this.logger.error('error processing content source events', { error });
444
+ })
445
+ .finally(() => {
446
+ processingPromise.resolve();
447
+ });
448
+ }
449
+
450
+ private async processContentStoreEvents() {
451
+ // If the ContentStore is currently loading content sources, return to prevent parallel data updates.
452
+ // This method will be called once current loading cycle ends.
453
+ if (this.processingContentSourcesPromise) {
454
+ return this.processingContentSourcesPromise.promise;
455
+ }
456
+
457
+ const contentSourceIdsWithInvalidatedSchema: string[] = [];
458
+ const contentChanges: ContentStoreTypes.ContentChangeResult = {
459
+ updatedDocuments: [],
460
+ updatedAssets: [],
461
+ deletedDocuments: [],
462
+ deletedAssets: []
463
+ };
464
+ let invalidateSchema = false;
465
+ let presetsUpdated = false;
466
+
467
+ const contentSourceEvents = this.contentStoreEventQueue;
468
+ this.contentStoreEventQueue = [];
469
+ for (const contentSourceEvent of contentSourceEvents) {
470
+ if (
471
+ contentSourceEvent.eventName === ContentStoreEventType.YamlModelFilesChange ||
472
+ contentSourceEvent.eventName === ContentStoreEventType.PresetFilesChange
473
+ ) {
474
+ invalidateSchema = true;
475
+ } else if (contentSourceEvent.eventName === ContentStoreEventType.ContentSourceInvalidateSchema) {
476
+ invalidateSchema = true;
477
+ contentSourceIdsWithInvalidatedSchema.push(contentSourceEvent.contentSourceId);
478
+ } else if (contentSourceEvent.eventName === ContentStoreEventType.ContentSourceContentChange) {
479
+ const result = this.onContentChange(contentSourceEvent.contentSourceId, contentSourceEvent.contentChanges);
480
+ contentChanges.updatedDocuments = contentChanges.updatedDocuments.concat(result.contentChangeResult.updatedDocuments);
481
+ contentChanges.updatedAssets = contentChanges.updatedAssets.concat(result.contentChangeResult.updatedAssets);
482
+ contentChanges.deletedDocuments = contentChanges.deletedDocuments.concat(result.contentChangeResult.deletedDocuments);
483
+ contentChanges.deletedAssets = contentChanges.deletedAssets.concat(result.contentChangeResult.deletedAssets);
484
+ if (result.presetsUpdated) {
485
+ presetsUpdated = true;
486
+ }
487
+ }
488
+ }
489
+
490
+ // If the schema was changed, call loadContentSourcesAndProcessData method, this will reload all the SiteMapEntries and call the onSchemaChangeCallback.
491
+ // As soon as the Studio receives the schemaChanged notification it will reload all the models and the documents.
492
+ if (invalidateSchema) {
493
+ await this.loadContentSourcesAndProcessData({
494
+ init: false,
495
+ contentSourceIds: contentSourceIdsWithInvalidatedSchema
496
+ });
497
+ } else {
498
+ // If the schema wasn't changed, update SiteMapEntries with the changed content.
499
+ this.siteMapEntryGroups = await updateSiteMapEntriesWithContentChanges({
500
+ siteMapEntryGroups: this.siteMapEntryGroups,
501
+ contentChanges: contentChanges,
502
+ stackbitConfig: this.stackbitConfig,
503
+ contentSourceDataById: this.contentSourceDataById
504
+ });
505
+ // If presets were updated, call onSchemaChangeCallback to notify the Studio.
506
+ // The Studio will refresh the models and the documents, so no need to notify
507
+ // content changes in this case.
508
+ if (presetsUpdated) {
509
+ this.onSchemaChangeCallback();
510
+ } else if (!isContentChangeResultEmpty(contentChanges)) {
511
+ this.onContentChangeCallback(contentChanges);
512
+ }
513
+ }
514
+ }
515
+
516
+ private pushContentSourceEvent(contentStoreEvent: ContentStoreEvent) {
517
+ if (contentStoreEvent.eventName === ContentStoreEventType.ContentSourceContentChange) {
518
+ // If a content source enqueued the 'contentSourceInvalidateSchema' event,
519
+ // don't push the 'contentSourceContentChange' event, because 'contentSourceInvalidateSchema'
520
+ // will reload all the content source data.
521
+ const hasContentSourceSchemaChangeEvent = this.contentStoreEventQueue.find(
522
+ (event) =>
523
+ event.eventName === ContentStoreEventType.ContentSourceInvalidateSchema && event.contentSourceId === contentStoreEvent.contentSourceId
524
+ );
525
+ if (!hasContentSourceSchemaChangeEvent) {
526
+ this.contentStoreEventQueue.push(contentStoreEvent);
527
+ }
528
+ } else if (contentStoreEvent.eventName === ContentStoreEventType.ContentSourceInvalidateSchema) {
529
+ // Clear any 'contentSourceContentChange' events for a content source,
530
+ // the 'contentSourceInvalidateSchema' will reload all the content source data.
531
+ this.clearEventsForContentSourceId(contentStoreEvent.contentSourceId);
532
+ this.contentStoreEventQueue.push(contentStoreEvent);
533
+ } else if (contentStoreEvent.eventName === ContentStoreEventType.YamlModelFilesChange) {
534
+ this.contentStoreEventQueue = this.contentStoreEventQueue.filter((event) => event.eventName !== ContentStoreEventType.YamlModelFilesChange);
535
+ this.contentStoreEventQueue.push(contentStoreEvent);
536
+ } else if (contentStoreEvent.eventName === ContentStoreEventType.PresetFilesChange) {
537
+ this.contentStoreEventQueue = this.contentStoreEventQueue.filter((event) => event.eventName !== ContentStoreEventType.PresetFilesChange);
538
+ this.contentStoreEventQueue.push(contentStoreEvent);
539
+ }
540
+ }
541
+
542
+ private clearEventsForContentSourceId(contentSourceId: string) {
543
+ this.contentStoreEventQueue = this.contentStoreEventQueue.filter((contentSourceEvent) => {
544
+ if (
545
+ contentSourceEvent.eventName === ContentStoreEventType.ContentSourceContentChange ||
546
+ contentSourceEvent.eventName === ContentStoreEventType.ContentSourceInvalidateSchema
547
+ ) {
548
+ return contentSourceEvent.contentSourceId !== contentSourceId;
549
+ }
550
+ return true;
551
+ });
425
552
  }
426
553
 
427
554
  private async loadContentSourceData({
428
555
  contentSourceInstance,
429
556
  init
430
557
  }: {
431
- contentSourceInstance: CSITypes.ContentSourceInterface;
558
+ contentSourceInstance: BackCompatContentSourceInterface;
432
559
  init: boolean;
433
560
  }): Promise<ContentSourceRawData> {
434
561
  const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
435
562
  this.logger.debug('loadContentSourceData', { contentSourceId, init });
436
563
 
564
+ // clear content source events emitted by this content source because all the content source data is reloaded
565
+ this.clearEventsForContentSourceId(contentSourceId);
566
+
567
+ const localCache: Partial<
568
+ Pick<ContentSourceRawData, 'csiSchema' | 'csiModels' | 'csiModelMap' | 'locales' | 'csiDocuments' | 'csiDocumentMap' | 'csiAssets' | 'csiAssetMap'>
569
+ > = {};
570
+
571
+ const getContentSourceDataForCurrentInstance = (methodName: keyof CSITypes.Cache | 'getModelMap' | 'getDocument' | 'getAsset') => {
572
+ const contentSourceData = this.contentSourceDataById[contentSourceId];
573
+ if (!contentSourceData) {
574
+ // When loading the content sources for the first time, this.contentSourceDataById will be empty.
575
+ // However, while being loaded, content sources may call cache methods, for example a content
576
+ // source may call getModelByName from within getDocuments. In this case, return locally cached data.
577
+ if (this.processingContentSourcesPromise) {
578
+ return localCache;
579
+ }
580
+ const atLine = getErrorAtLine(2, getContentSourceDataForCurrentInstance);
581
+ const errorMessage = `Error executing 'cache.${methodName}' method${atLine}. The content source with id '${contentSourceId}' was not found.`;
582
+ this.logger.error(errorMessage);
583
+ return;
584
+ }
585
+ if (contentSourceData.instance !== contentSourceInstance) {
586
+ const atLine = getErrorAtLine(2, getContentSourceDataForCurrentInstance);
587
+ const errorMessage =
588
+ `Content source life cycle error! The content source with id '${contentSourceId}' called the 'cache.${methodName}' ` +
589
+ `method${atLine} after the destroy() method was called.`;
590
+ this.logger.error(errorMessage);
591
+ return;
592
+ }
593
+ // While loading the content source, it may call cache methods, when this happens, return the
594
+ // stale data overridden with the most frequent loaded data
595
+ if (this.processingContentSourcesPromise) {
596
+ return Object.assign(contentSourceData, localCache);
597
+ }
598
+ return contentSourceData;
599
+ };
600
+
601
+ const cache: CSITypes.Cache = {
602
+ getSchema: () => {
603
+ const contentSourceData = getContentSourceDataForCurrentInstance('getSchema');
604
+ if (!contentSourceData?.csiSchema) {
605
+ const atLine = getErrorAtLine(1);
606
+ const errorMessage =
607
+ `Content source life cycle error! The content source with id '${contentSourceId}' called the 'cache.getSchema' ` +
608
+ `method${atLine} before the content source's getSchema() method was called.`;
609
+ this.logger.error(errorMessage);
610
+ return { models: [], locales: [], context: null };
611
+ }
612
+ return contentSourceData.csiSchema;
613
+ },
614
+ getModelByName: (modelName) => {
615
+ const contentSourceData = getContentSourceDataForCurrentInstance('getModelByName');
616
+ if (!contentSourceData?.csiModelMap) {
617
+ const atLine = getErrorAtLine(1);
618
+ const errorMessage =
619
+ `Content source life cycle error! The content source with id '${contentSourceId}' called the 'cache.getModelByName' ` +
620
+ `method${atLine} before the content source's getSchema() method was called.`;
621
+ this.logger.error(errorMessage);
622
+ return;
623
+ }
624
+ return contentSourceData.csiModelMap[modelName];
625
+ },
626
+ getDocuments: () => {
627
+ const contentSourceData = getContentSourceDataForCurrentInstance('getDocuments');
628
+ return contentSourceData?.csiDocuments ?? [];
629
+ },
630
+ getDocumentById: (documentId) => {
631
+ const contentSourceData = getContentSourceDataForCurrentInstance('getDocumentById');
632
+ return contentSourceData?.csiDocumentMap?.[documentId];
633
+ },
634
+ getAssets: () => {
635
+ const contentSourceData = getContentSourceDataForCurrentInstance('getAssets');
636
+ return contentSourceData?.csiAssets ?? [];
637
+ },
638
+ getAssetById: (assetId) => {
639
+ const contentSourceData = getContentSourceDataForCurrentInstance('getAssetById');
640
+ return contentSourceData?.csiAssetMap?.[assetId];
641
+ },
642
+ updateContent: async (contentChanges: CSITypes.ContentChanges): Promise<void> => {
643
+ const contentSourceData = getContentSourceDataForCurrentInstance('updateContent');
644
+ if (!contentSourceData) {
645
+ return;
646
+ }
647
+ this.logger.debug('content source called updateContent', { contentSourceId });
648
+ this.pushContentSourceEvent({
649
+ eventName: ContentStoreEventType.ContentSourceContentChange,
650
+ contentSourceId: contentSourceId,
651
+ contentChanges: contentChanges
652
+ });
653
+ await this.processContentStoreEvents();
654
+ },
655
+
656
+ invalidateSchema: async () => {
657
+ const contentSourceData = getContentSourceDataForCurrentInstance('invalidateSchema');
658
+ if (!contentSourceData) {
659
+ return;
660
+ }
661
+ this.logger.debug('content source called invalidateSchema', { contentSourceId });
662
+ this.pushContentSourceEvent({
663
+ eventName: ContentStoreEventType.ContentSourceInvalidateSchema,
664
+ contentSourceId: contentSourceId
665
+ });
666
+ await this.processContentStoreEvents();
667
+ }
668
+ };
669
+
437
670
  if (init) {
671
+ this.userLogger.info(
672
+ `Initializing content source: ${contentSourceInstance.getContentSourceType()} (project: ${contentSourceInstance.getProjectId()})`
673
+ );
674
+ // When stackbit.config.js reloads, it loads new content source instances.
675
+ // Previously loaded content source instances must be destroyed.
676
+ const contentSourceData = this.contentSourceDataById[contentSourceId];
677
+ if (contentSourceData && contentSourceData.instance !== contentSourceInstance) {
678
+ this.logger.debug('destroy previous content source instance', { contentSourceId });
679
+ try {
680
+ contentSourceData.instance.stopWatchingContentUpdates?.();
681
+ await contentSourceData.instance.destroy();
682
+ } catch (error) {
683
+ this.logger.debug('error destroying content source instance', { error });
684
+ }
685
+ }
686
+
687
+ // If an instance that wasn't destroyed calls one of the InitOptions method don't return any data.
438
688
  await contentSourceInstance.init({
439
689
  logger: this.logger,
440
690
  userLogger: this.userLogger,
441
- userCommandSpawner: this.userCommandSpawner,
442
691
  localDev: this.localDev,
443
692
  webhookUrl: this.getWebhookUrl(contentSourceInstance.getContentSourceType(), contentSourceInstance.getProjectId()),
693
+ userCommandSpawner: this.userCommandSpawner,
444
694
  devAppRestartNeeded: this.devAppRestartNeeded,
695
+ cache: cache,
445
696
  runCommand: this.runCommand,
446
697
  git: this.git
447
698
  });
448
699
  } else {
449
- contentSourceInstance.stopWatchingContentUpdates();
450
700
  await contentSourceInstance.reset();
451
701
  }
452
702
 
453
- const csiModels = await contentSourceInstance.getModels();
703
+ const version = await contentSourceInstance.getVersion();
704
+ const csiSchema = await contentSourceInstance.getSchema();
705
+ const csiModels = csiSchema.models;
454
706
  const csiModelMap = _.keyBy(csiModels, 'name');
455
-
456
- const locales = await contentSourceInstance.getLocales();
707
+ const locales = csiSchema.locales;
457
708
  const defaultLocaleCode = locales?.find((locale) => locale.default)?.code;
458
709
 
710
+ localCache.csiSchema = csiSchema;
711
+ localCache.csiModels = csiModels;
712
+ localCache.csiModelMap = csiModelMap;
713
+ localCache.locales = locales;
714
+
459
715
  const csiDocuments = await contentSourceInstance.getDocuments({ modelMap: csiModelMap });
460
- const csiAssets = await contentSourceInstance.getAssets();
461
716
  const csiDocumentMap = _.keyBy(csiDocuments, 'id');
717
+ localCache.csiDocuments = csiDocuments;
718
+ localCache.csiDocumentMap = csiDocumentMap;
719
+
720
+ const csiAssets = await contentSourceInstance.getAssets();
462
721
  const csiAssetMap = _.keyBy(csiAssets, 'id');
722
+ localCache.csiAssets = csiAssets;
723
+ localCache.csiAssetMap = csiAssetMap;
463
724
 
464
725
  const contentStoreAssets = mapCSIAssetsToStoreAssets({
465
- csiAssets,
726
+ csiAssets: csiAssets,
466
727
  contentSourceInstance,
467
728
  defaultLocaleCode
468
729
  });
@@ -471,56 +732,47 @@ export class ContentStore {
471
732
  this.logger.debug('loaded content source data', {
472
733
  contentSourceId,
473
734
  defaultLocaleCode,
474
- localesCount: locales.length,
735
+ localesCount: locales?.length ?? 0,
475
736
  modelCount: csiModels.length,
476
737
  documentCount: csiDocuments.length,
477
738
  assetCount: csiAssets.length
478
739
  });
479
740
 
480
- contentSourceInstance.startWatchingContentUpdates({
481
- getModelMap: () => {
482
- return csiModelMap;
483
- },
484
- getDocument({ documentId }: { documentId: string }) {
485
- return csiDocumentMap[documentId];
486
- },
487
- getAsset({ assetId }: { assetId: string }) {
488
- return csiAssetMap[assetId];
489
- },
490
- onContentChange: async (contentChangeEvent: CSITypes.ContentChangeEvent) => {
491
- if (this.contentSourceDataById[contentSourceId]?.instance !== contentSourceInstance) {
492
- this.logger.debug('old content source called onContentChange', { contentSourceId });
493
- return;
494
- }
495
- this.logger.debug('content source called onContentChange.', { contentSourceId });
496
- const result = this.onContentChange(contentSourceId, contentChangeEvent);
497
-
498
- this.siteMapEntryGroups = await updateSiteMapEntriesWithContentChanges({
499
- siteMapEntryGroups: this.siteMapEntryGroups,
500
- contentChanges: result,
501
- stackbitConfig: this.stackbitConfig,
502
- contentSourceDataById: this.contentSourceDataById
503
- });
504
- this.onContentChangeCallback(result);
505
- },
506
- onSchemaChange: async () => {
507
- if (this.contentSourceDataById[contentSourceId]?.instance !== contentSourceInstance) {
508
- this.logger.debug('old content source called onSchemaChange', { contentSourceId });
509
- return;
510
- }
511
- this.logger.debug('content source called onSchemaChange', { contentSourceId });
512
- await this.loadContentSourcesAndProcessData({ init: false, contentSourceIds: [contentSourceId] });
513
- this.onSchemaChangeCallback();
514
- }
515
- });
741
+ if (init) {
742
+ this.userLogger.info(
743
+ `→ Loaded content source data: ${csiModels.length} model${csiModels.length !== 1 ? 's' : ''}, ${csiDocuments.length} document${
744
+ csiDocuments.length !== 1 ? 's' : ''
745
+ } and ${csiAssets.length} asset${csiAssets.length !== 1 ? 's' : ''}`
746
+ );
747
+
748
+ // backward compatibility
749
+ contentSourceInstance.startWatchingContentUpdates?.({
750
+ getModelMap: () => {
751
+ const contentSourceData = getContentSourceDataForCurrentInstance('getModelMap');
752
+ return contentSourceData?.csiModelMap ?? {};
753
+ },
754
+ getDocument: ({ documentId }) => {
755
+ const contentSourceData = getContentSourceDataForCurrentInstance('getDocument');
756
+ return contentSourceData?.csiDocumentMap?.[documentId];
757
+ },
758
+ getAsset: ({ assetId }) => {
759
+ const contentSourceData = getContentSourceDataForCurrentInstance('getAsset');
760
+ return contentSourceData?.csiAssetMap?.[assetId];
761
+ },
762
+ onContentChange: cache.updateContent,
763
+ onSchemaChange: cache.invalidateSchema
764
+ });
765
+ }
516
766
 
517
767
  return {
518
768
  id: contentSourceId,
769
+ version: version,
519
770
  srcType: contentSourceInstance.getContentSourceType(),
520
771
  srcProjectId: contentSourceInstance.getProjectId(),
521
772
  instance: contentSourceInstance,
522
773
  locales: locales,
523
774
  defaultLocaleCode: defaultLocaleCode,
775
+ csiSchema: csiSchema,
524
776
  csiModels: csiModels,
525
777
  csiModelMap: csiModelMap,
526
778
  csiDocuments: csiDocuments,
@@ -532,18 +784,26 @@ export class ContentStore {
532
784
  };
533
785
  }
534
786
 
535
- private onContentChange(contentSourceId: string, contentChangeEvent: CSITypes.ContentChangeEvent): ContentStoreTypes.ContentChangeResult {
536
- // TODO: prevent content change process for contentSourceId if loading content is in progress
537
-
787
+ private onContentChange(
788
+ contentSourceId: string,
789
+ contentChanges: CSITypes.ContentChanges
790
+ ): { contentChangeResult: ContentStoreTypes.ContentChangeResult; presetsUpdated: boolean } {
538
791
  // certain content changes, like preset changes are interpreted as schema changes
539
- let schemaChanged = false;
792
+ let presetsUpdated = false;
793
+
794
+ const contentChangesReq: Required<CSITypes.ContentChanges> = {
795
+ documents: contentChanges.documents ?? [],
796
+ assets: contentChanges.assets ?? [],
797
+ deletedDocumentIds: contentChanges.deletedDocumentIds ?? [],
798
+ deletedAssetIds: contentChanges.deletedAssetIds ?? []
799
+ };
540
800
 
541
801
  this.logger.debug('onContentChange', {
542
802
  contentSourceId,
543
- documentCount: contentChangeEvent.documents.length,
544
- assetCount: contentChangeEvent.assets.length,
545
- deletedDocumentCount: contentChangeEvent.deletedDocumentIds.length,
546
- deletedAssetCount: contentChangeEvent.deletedAssetIds.length
803
+ documentCount: contentChangesReq.documents.length,
804
+ assetCount: contentChangesReq.assets.length,
805
+ deletedDocumentCount: contentChangesReq.deletedDocumentIds.length,
806
+ deletedAssetCount: contentChangesReq.deletedAssetIds.length
547
807
  });
548
808
 
549
809
  const result: ContentStoreTypes.ContentChangeResult = {
@@ -556,11 +816,11 @@ export class ContentStore {
556
816
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
557
817
 
558
818
  // update contentSourceData with deleted documents
559
- contentChangeEvent.deletedDocumentIds.forEach((docId) => {
819
+ contentChangesReq.deletedDocumentIds.forEach((docId) => {
560
820
  // remove preset, make sure there is something to remove first because
561
821
  // were explicitly calling onContentChange from deletePreset as well
562
822
  if (this.presets[docId] && contentSourceData.csiDocumentMap[docId]?.modelName === StackbitPresetModelName) {
563
- schemaChanged = true;
823
+ presetsUpdated = true;
564
824
  const preset = this.presets[docId]!;
565
825
  const model = contentSourceData.modelMap[preset.modelName];
566
826
  delete this.presets[docId];
@@ -590,7 +850,7 @@ export class ContentStore {
590
850
  });
591
851
 
592
852
  // update contentSourceData with deleted assets
593
- contentChangeEvent.deletedAssetIds.forEach((assetId) => {
853
+ contentChangesReq.deletedAssetIds.forEach((assetId) => {
594
854
  // delete document from asset map
595
855
  delete contentSourceData.assetMap[assetId];
596
856
  delete contentSourceData.csiAssetMap[assetId];
@@ -611,9 +871,9 @@ export class ContentStore {
611
871
  });
612
872
 
613
873
  // map csi documents through stackbitConfig.mapDocuments
614
- let mappedDocs = contentChangeEvent.documents;
874
+ let mappedDocs = contentChangesReq.documents;
615
875
  if (this.stackbitConfig?.mapDocuments) {
616
- const csiDocumentsWithSource = contentChangeEvent.documents.map(
876
+ const csiDocumentsWithSource = contentChangesReq.documents.map(
617
877
  (csiDocument): CSITypes.DocumentWithSource => ({
618
878
  srcType: contentSourceData.srcType,
619
879
  srcProjectId: contentSourceData.srcProjectId,
@@ -621,15 +881,13 @@ export class ContentStore {
621
881
  })
622
882
  );
623
883
 
624
- const modelsWithSource = contentSourceData.models.map(
625
- (model): CSITypes.ModelWithSource => {
626
- return {
627
- srcType: contentSourceData.srcType,
628
- srcProjectId: contentSourceData.srcProjectId,
629
- ...model
630
- };
631
- }
632
- );
884
+ const modelsWithSource = contentSourceData.models.map((model): CSITypes.ModelWithSource => {
885
+ return {
886
+ srcType: contentSourceData.srcType,
887
+ srcProjectId: contentSourceData.srcProjectId,
888
+ ...model
889
+ };
890
+ });
633
891
 
634
892
  mappedDocs =
635
893
  this.stackbitConfig?.mapDocuments?.({
@@ -643,24 +901,28 @@ export class ContentStore {
643
901
  csiDocuments: mappedDocs,
644
902
  contentSourceInstance: contentSourceData.instance,
645
903
  modelMap: contentSourceData.modelMap,
646
- defaultLocaleCode: contentSourceData.defaultLocaleCode
904
+ defaultLocaleCode: contentSourceData.defaultLocaleCode,
905
+ createConfigDelegate: getCreateConfigDelegateThunk({
906
+ getContentSourceDataById: () => this.contentSourceDataById,
907
+ logger: this.userLogger
908
+ })
647
909
  });
648
910
  const assets = mapCSIAssetsToStoreAssets({
649
- csiAssets: contentChangeEvent.assets,
911
+ csiAssets: contentChangesReq.assets,
650
912
  contentSourceInstance: contentSourceData.instance,
651
913
  defaultLocaleCode: contentSourceData.defaultLocaleCode
652
914
  });
653
915
 
654
916
  // update contentSourceData with new or updated documents and assets
655
- Object.assign(contentSourceData.csiDocumentMap, _.keyBy(contentChangeEvent.documents, 'id'));
656
- Object.assign(contentSourceData.csiAssetMap, _.keyBy(contentChangeEvent.assets, 'id'));
917
+ Object.assign(contentSourceData.csiDocumentMap, _.keyBy(contentChangesReq.documents, 'id'));
918
+ Object.assign(contentSourceData.csiAssetMap, _.keyBy(contentChangesReq.assets, 'id'));
657
919
  Object.assign(contentSourceData.documentMap, _.keyBy(documents, 'srcObjectId'));
658
920
  Object.assign(contentSourceData.assetMap, _.keyBy(assets, 'srcObjectId'));
659
921
 
660
922
  for (let idx = 0; idx < documents.length; idx++) {
661
923
  // the indexes of mapped documents and documents from changeEvent are the same
662
924
  const document = documents[idx]!;
663
- const csiDocument = contentChangeEvent.documents[idx]!;
925
+ const csiDocument = contentChangesReq.documents[idx]!;
664
926
  const dataIndex = contentSourceData.documents.findIndex((existingDoc) => existingDoc.srcObjectId === document.srcObjectId);
665
927
  if (dataIndex === -1) {
666
928
  contentSourceData.documents.push(document);
@@ -670,7 +932,7 @@ export class ContentStore {
670
932
  contentSourceData.csiDocuments.splice(dataIndex, 1, csiDocument);
671
933
  }
672
934
  if (csiDocument.modelName === StackbitPresetModelName) {
673
- schemaChanged = true;
935
+ presetsUpdated = true;
674
936
  const preset = getPresetFromDocument({
675
937
  srcType: contentSourceData.srcType,
676
938
  srcProjectId: contentSourceData.srcProjectId,
@@ -696,7 +958,7 @@ export class ContentStore {
696
958
  for (let idx = 0; idx < assets.length; idx++) {
697
959
  // the indexes of mapped assets and assets from changeEvent are the same
698
960
  const asset = assets[idx]!;
699
- const csiAsset = contentChangeEvent.assets[idx]!;
961
+ const csiAsset = contentChangesReq.assets[idx]!;
700
962
  const index = contentSourceData.assets.findIndex((existingAsset) => existingAsset.srcObjectId === asset.srcObjectId);
701
963
  if (index === -1) {
702
964
  contentSourceData.assets.push(asset);
@@ -713,11 +975,10 @@ export class ContentStore {
713
975
  });
714
976
  }
715
977
 
716
- if (schemaChanged) {
717
- this.onSchemaChangeCallback?.();
718
- }
719
-
720
- return result;
978
+ return {
979
+ contentChangeResult: result,
980
+ presetsUpdated
981
+ };
721
982
  }
722
983
 
723
984
  private async processData({
@@ -807,15 +1068,13 @@ export class ContentStore {
807
1068
  externalModels: csData.csiModels,
808
1069
  logger: this.userLogger
809
1070
  });
810
- const modelsWithSource = mergedModels.map(
811
- (model): CSITypes.ModelWithSource => {
812
- return {
813
- srcType: csData.srcType,
814
- srcProjectId: csData.srcProjectId,
815
- ...model
816
- };
817
- }
818
- );
1071
+ const modelsWithSource = mergedModels.map((model): CSITypes.ModelWithSource => {
1072
+ return {
1073
+ srcType: csData.srcType,
1074
+ srcProjectId: csData.srcProjectId,
1075
+ ...model
1076
+ };
1077
+ });
819
1078
  return accum.concat(modelsWithSource);
820
1079
  }, []);
821
1080
 
@@ -852,31 +1111,48 @@ export class ContentStore {
852
1111
 
853
1112
  const modelMapByContentSource = groupModelsByContentSource({ models: models });
854
1113
 
855
- const contentSourceDataArr = contentSourceRawDataArr.map(
856
- (csData): ContentSourceData => {
857
- const modelMap = _.get(modelMapByContentSource, [csData.srcType, csData.srcProjectId], {});
858
- const csiDocuments = documentMapByContentSource
859
- ? _.get(documentMapByContentSource, [csData.srcType, csData.srcProjectId], [])
860
- : csData.csiDocuments;
861
- const documents = mapCSIDocumentsToStoreDocuments({
862
- csiDocuments: csiDocuments,
863
- contentSourceInstance: csData.instance,
864
- defaultLocaleCode: csData.defaultLocaleCode,
865
- modelMap: modelMap
866
- });
867
- return {
868
- ...csData,
869
- models: Object.values(modelMap),
870
- modelMap,
871
- documents,
872
- documentMap: _.keyBy(documents, 'srcObjectId')
873
- };
874
- }
875
- );
1114
+ const contentSourceDataArr = contentSourceRawDataArr.map((csData): ContentSourceData => {
1115
+ const modelMap = _.get(modelMapByContentSource, [csData.srcType, csData.srcProjectId], {});
1116
+ const csiDocuments = documentMapByContentSource
1117
+ ? _.get(documentMapByContentSource, [csData.srcType, csData.srcProjectId], [])
1118
+ : csData.csiDocuments;
1119
+ const documents = mapCSIDocumentsToStoreDocuments({
1120
+ csiDocuments: csiDocuments,
1121
+ contentSourceInstance: csData.instance,
1122
+ modelMap: modelMap,
1123
+ defaultLocaleCode: csData.defaultLocaleCode,
1124
+ createConfigDelegate: getCreateConfigDelegateThunk({
1125
+ getContentSourceDataById: () => this.contentSourceDataById,
1126
+ logger: this.userLogger
1127
+ })
1128
+ });
1129
+ return {
1130
+ ...csData,
1131
+ models: Object.values(modelMap),
1132
+ modelMap,
1133
+ documents,
1134
+ documentMap: _.keyBy(documents, 'srcObjectId')
1135
+ };
1136
+ });
876
1137
 
877
1138
  return _.keyBy(contentSourceDataArr, 'id');
878
1139
  }
879
1140
 
1141
+ getContentSourceMeta(): { srcType: string; srcProjectId: string; srcVersion: string; csiVersion: string }[] {
1142
+ return _.reduce(
1143
+ this.contentSourceDataById,
1144
+ (result: { srcType: string; srcProjectId: string; srcVersion: string; csiVersion: string }[], contentSourceData) => {
1145
+ return result.concat({
1146
+ srcType: contentSourceData.srcType,
1147
+ srcProjectId: contentSourceData.srcProjectId,
1148
+ srcVersion: contentSourceData.version.contentSourceVersion,
1149
+ csiVersion: contentSourceData.version.interfaceVersion
1150
+ });
1151
+ },
1152
+ []
1153
+ );
1154
+ }
1155
+
880
1156
  getModels(): Record<string, Record<string, Record<string, Model | ImageModel>>> {
881
1157
  return _.reduce(
882
1158
  this.contentSourceDataById,
@@ -942,14 +1218,7 @@ export class ContentStore {
942
1218
  const srcType = contentSourceData.srcType;
943
1219
  const srcProjectId = contentSourceData.srcProjectId;
944
1220
  const userContext = getUserContextForSrcType(srcType, user);
945
- let result = await contentSourceData.instance.hasAccess({ userContext });
946
- // backwards compatibility with older CSI version
947
- if (typeof result === 'boolean') {
948
- result = {
949
- hasConnection: result,
950
- hasPermissions: result
951
- };
952
- }
1221
+ const result = await contentSourceData.instance.hasAccess({ userContext });
953
1222
  return {
954
1223
  hasConnection: accum.hasConnection && result.hasConnection,
955
1224
  hasPermissions: accum.hasPermissions && result.hasPermissions,
@@ -1091,7 +1360,7 @@ export class ContentStore {
1091
1360
  return contentSourceData.documentMap[srcDocumentId];
1092
1361
  }
1093
1362
 
1094
- getDocuments({ locale }: { locale?: string }): ContentStoreTypes.Document[] {
1363
+ getDocuments({ locale }: { locale?: string } = {}): ContentStoreTypes.Document[] {
1095
1364
  return _.reduce(
1096
1365
  this.contentSourceDataById,
1097
1366
  (documents: ContentStoreTypes.Document[], contentSourceData) => {
@@ -1124,7 +1393,7 @@ export class ContentStore {
1124
1393
  );
1125
1394
  }
1126
1395
 
1127
- getLocalizedApiObjects({ locale, objectIds }: { locale?: string, objectIds?: string[] }): ContentStoreTypes.APIObject[] {
1396
+ getLocalizedApiObjects({ locale, objectIds }: { locale?: string; objectIds?: string[] }): ContentStoreTypes.APIObject[] {
1128
1397
  const hasExplicitLocale = !_.isEmpty(locale);
1129
1398
  return _.reduce(
1130
1399
  this.contentSourceDataById,
@@ -1132,18 +1401,16 @@ export class ContentStore {
1132
1401
  let documents = objectIds
1133
1402
  ? contentSourceData.documents.filter((document) => objectIds.includes(document.srcObjectId))
1134
1403
  : contentSourceData.documents;
1135
- documents = hasExplicitLocale
1136
- ? documents.filter((document) => !document.locale || document.locale === locale)
1137
- : documents;
1138
- let assets = objectIds
1139
- ? contentSourceData.assets.filter((asset) => objectIds.includes(asset.srcObjectId))
1140
- : contentSourceData.assets;
1141
- assets = hasExplicitLocale
1142
- ? assets.filter((asset) => !asset.locale || asset.locale === locale)
1143
- : assets;
1404
+ documents = hasExplicitLocale ? documents.filter((document) => !document.locale || document.locale === locale) : documents;
1405
+ let assets = objectIds ? contentSourceData.assets.filter((asset) => objectIds.includes(asset.srcObjectId)) : contentSourceData.assets;
1406
+ assets = hasExplicitLocale ? assets.filter((asset) => !asset.locale || asset.locale === locale) : assets;
1144
1407
  const currentLocale = locale ?? contentSourceData.defaultLocaleCode;
1145
1408
  const filteredDocuments = documents.filter((document) => document.srcModelName !== StackbitPresetModelName);
1146
- const documentObjects = mapDocumentsToLocalizedApiObjects(filteredDocuments, currentLocale);
1409
+ const documentObjects = mapDocumentsToLocalizedApiObjects({
1410
+ documents: filteredDocuments,
1411
+ locale: currentLocale,
1412
+ delegate: createConfigDelegate({ contentSourceDataById: this.contentSourceDataById, logger: this.userLogger })
1413
+ });
1147
1414
  const imageObjects = mapAssetsToLocalizedApiImages(assets, this.staticAssetsPublicPath, currentLocale);
1148
1415
  return objects.concat(documentObjects, imageObjects);
1149
1416
  },
@@ -1305,7 +1572,7 @@ export class ContentStore {
1305
1572
  refProjectId: refProjectId
1306
1573
  });
1307
1574
  }
1308
- const updatedDocument = await contentSourceData.instance.updateDocument({
1575
+ await contentSourceData.instance.updateDocument({
1309
1576
  document: csiDocument,
1310
1577
  modelMap: csiModelMap,
1311
1578
  userContext: userContext,
@@ -1328,7 +1595,7 @@ export class ContentStore {
1328
1595
  }
1329
1596
  ]
1330
1597
  });
1331
- return { srcDocumentId: updatedDocument.id, createdDocumentId: result.srcDocumentId };
1598
+ return { srcDocumentId: srcDocumentId, createdDocumentId: result.srcDocumentId };
1332
1599
  }
1333
1600
 
1334
1601
  async createPreset({
@@ -1380,12 +1647,18 @@ export class ContentStore {
1380
1647
 
1381
1648
  // we delete presets immediately because some CMSs don't notify us
1382
1649
  // when documents have been deleted.
1383
- this.onContentChange(getContentSourceIdForContentSource(this.presetsContentSource), {
1384
- documents: [],
1385
- deletedDocumentIds: [presetId],
1386
- assets: [],
1387
- deletedAssetIds: []
1650
+ const contentSourceId = getContentSourceIdForContentSource(this.presetsContentSource);
1651
+ this.pushContentSourceEvent({
1652
+ eventName: ContentStoreEventType.ContentSourceContentChange,
1653
+ contentSourceId: contentSourceId,
1654
+ contentChanges: {
1655
+ documents: [],
1656
+ deletedDocumentIds: [presetId],
1657
+ assets: [],
1658
+ deletedAssetIds: []
1659
+ }
1388
1660
  });
1661
+ await this.processContentStoreEvents();
1389
1662
  }
1390
1663
 
1391
1664
  async uploadAndLinkAsset({
@@ -1453,7 +1726,7 @@ export class ContentStore {
1453
1726
  refType: 'asset',
1454
1727
  refId: result.id
1455
1728
  } as const;
1456
- const updatedDocument = await contentSourceData.instance.updateDocument({
1729
+ await contentSourceData.instance.updateDocument({
1457
1730
  document: csiDocument,
1458
1731
  modelMap: csiModelMap,
1459
1732
  userContext: userContext,
@@ -1476,7 +1749,7 @@ export class ContentStore {
1476
1749
  }
1477
1750
  ]
1478
1751
  });
1479
- return { srcDocumentId: updatedDocument.id };
1752
+ return { srcDocumentId: srcDocumentId };
1480
1753
  }
1481
1754
 
1482
1755
  async createDocument({
@@ -1514,15 +1787,9 @@ export class ContentStore {
1514
1787
  })
1515
1788
  });
1516
1789
 
1517
- this.logger.debug('created document', { srcType, srcProjectId, srcDocumentId: result.document.id, modelName });
1518
-
1519
- // do not update cache in contentSourceData.documents and documentMap,
1520
- // instead wait for contentSource to call onContentChange(contentChangeEvent)
1521
- // and use data from contentChangeEvent to update the cache
1522
- // const newDocuments = [result.document, ...result.referencedDocuments];
1523
- // contentSourceData.documentMap = Object.assign(contentSourceData.documentMap, _.keyBy(newDocuments, 'srcObjectId'));
1790
+ this.logger.debug('created document', { srcType, srcProjectId, srcDocumentId: result.documentId, modelName });
1524
1791
 
1525
- return { srcDocumentId: result.document.id };
1792
+ return { srcDocumentId: result.documentId };
1526
1793
  }
1527
1794
 
1528
1795
  async updateDocument({
@@ -1558,88 +1825,80 @@ export class ContentStore {
1558
1825
  throw new Error(`error updating document, could not find document model: '${documentModelName}'`);
1559
1826
  }
1560
1827
 
1561
- const operations = await mapPromise(
1562
- updateOperations,
1563
- async (updateOperation): Promise<CSITypes.UpdateOperation> => {
1564
- const locale = updateOperation.locale ?? contentSourceData.defaultLocaleCode;
1565
- const modelField = getModelFieldForFieldAtPath(document, model, updateOperation.fieldPath, modelMap, locale);
1566
- const csiModelField = getModelFieldForFieldAtPath(document, csiModel, updateOperation.fieldPath, csiModelMap, locale);
1567
- switch (updateOperation.opType) {
1568
- case 'set':
1569
- const field = await convertOperationField({
1570
- operationField: updateOperation.field,
1571
- fieldPath: [csiModel.name, ...updateOperation.fieldPath],
1572
- modelField,
1573
- csiModelField,
1574
- modelMap,
1575
- csiModelMap,
1576
- contentSourceId,
1577
- contentSourceDataById: this.contentSourceDataById,
1578
- createDocument: getCreateDocumentThunk({
1579
- locale: updateOperation.locale,
1580
- user
1581
- })
1582
- });
1583
- return {
1584
- ...updateOperation,
1585
- modelField: csiModelField,
1586
- field
1587
- };
1588
- case 'unset':
1589
- return {
1590
- ...updateOperation,
1591
- modelField: csiModelField
1592
- };
1593
- case 'insert':
1594
- if (modelField.type !== 'list' || csiModelField.type !== 'list') {
1595
- throw new Error(`error updating document, 'insert' operation can be performed on 'list' fields only`);
1596
- }
1597
- const item = await convertOperationField({
1598
- operationField: updateOperation.item,
1599
- fieldPath: [csiModel.name, ...updateOperation.fieldPath],
1600
- modelField: modelField.items,
1601
- csiModelField: csiModelField.items,
1602
- modelMap,
1603
- csiModelMap,
1604
- contentSourceId,
1605
- contentSourceDataById: this.contentSourceDataById,
1606
- createDocument: getCreateDocumentThunk({
1607
- locale: updateOperation.locale,
1608
- user
1609
- })
1610
- });
1611
- return {
1612
- ...updateOperation,
1613
- modelField: csiModelField,
1614
- item
1615
- } as CSITypes.UpdateOperationInsert;
1616
- case 'remove':
1617
- return {
1618
- ...updateOperation,
1619
- modelField: csiModelField
1620
- };
1621
- case 'reorder':
1622
- return {
1623
- ...updateOperation,
1624
- modelField: csiModelField
1625
- };
1626
- }
1828
+ const operations = await mapPromise(updateOperations, async (updateOperation): Promise<CSITypes.UpdateOperation> => {
1829
+ const locale = updateOperation.locale ?? contentSourceData.defaultLocaleCode;
1830
+ const modelField = getModelFieldForFieldAtPath(document, model, updateOperation.fieldPath, modelMap, locale);
1831
+ const csiModelField = getModelFieldForFieldAtPath(document, csiModel, updateOperation.fieldPath, csiModelMap, locale);
1832
+ switch (updateOperation.opType) {
1833
+ case 'set':
1834
+ const field = await convertOperationField({
1835
+ operationField: updateOperation.field,
1836
+ fieldPath: [csiModel.name, ...updateOperation.fieldPath],
1837
+ modelField,
1838
+ csiModelField,
1839
+ modelMap,
1840
+ csiModelMap,
1841
+ contentSourceId,
1842
+ contentSourceDataById: this.contentSourceDataById,
1843
+ createDocument: getCreateDocumentThunk({
1844
+ locale: updateOperation.locale,
1845
+ user
1846
+ })
1847
+ });
1848
+ return {
1849
+ ...updateOperation,
1850
+ modelField: csiModelField,
1851
+ field
1852
+ };
1853
+ case 'unset':
1854
+ return {
1855
+ ...updateOperation,
1856
+ modelField: csiModelField
1857
+ };
1858
+ case 'insert':
1859
+ if (modelField.type !== 'list' || csiModelField.type !== 'list') {
1860
+ throw new Error(`error updating document, 'insert' operation can be performed on 'list' fields only`);
1861
+ }
1862
+ const item = await convertOperationField({
1863
+ operationField: updateOperation.item,
1864
+ fieldPath: [csiModel.name, ...updateOperation.fieldPath],
1865
+ modelField: modelField.items,
1866
+ csiModelField: csiModelField.items,
1867
+ modelMap,
1868
+ csiModelMap,
1869
+ contentSourceId,
1870
+ contentSourceDataById: this.contentSourceDataById,
1871
+ createDocument: getCreateDocumentThunk({
1872
+ locale: updateOperation.locale,
1873
+ user
1874
+ })
1875
+ });
1876
+ return {
1877
+ ...updateOperation,
1878
+ modelField: csiModelField,
1879
+ item
1880
+ } as CSITypes.UpdateOperationInsert;
1881
+ case 'remove':
1882
+ return {
1883
+ ...updateOperation,
1884
+ modelField: csiModelField
1885
+ };
1886
+ case 'reorder':
1887
+ return {
1888
+ ...updateOperation,
1889
+ modelField: csiModelField
1890
+ };
1627
1891
  }
1628
- );
1892
+ });
1629
1893
 
1630
- const updatedDocumentResult = await contentSourceData.instance.updateDocument({
1894
+ await contentSourceData.instance.updateDocument({
1631
1895
  document: csiDocument,
1632
1896
  modelMap: csiModelMap,
1633
1897
  userContext,
1634
1898
  operations
1635
1899
  });
1636
1900
 
1637
- // do not update cache in contentSourceData.documents and documentMap,
1638
- // instead wait for contentSource to call onContentChange(contentChangeEvent)
1639
- // and use data from contentChangeEvent to update the cache
1640
- // contentSourceData.documentMap = Object.assign(contentSourceData.documentMap, { [document.srcObjectId]: document });
1641
-
1642
- return { srcDocumentId: updatedDocumentResult.id };
1901
+ return { srcDocumentId: srcDocumentId };
1643
1902
  }
1644
1903
 
1645
1904
  async duplicateDocument({
@@ -1666,7 +1925,6 @@ export class ContentStore {
1666
1925
  throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
1667
1926
  }
1668
1927
  const modelMap = contentSourceData.modelMap;
1669
- const csiModelMap = contentSourceData.csiModelMap;
1670
1928
  const model = modelMap[document.srcModelName];
1671
1929
  if (!model) {
1672
1930
  throw new Error(`no model with name '${document.srcModelName}' was found`);
@@ -1696,9 +1954,9 @@ export class ContentStore {
1696
1954
  })
1697
1955
  });
1698
1956
 
1699
- this.logger.debug('duplicated document', { srcType, srcProjectId, srcDocumentId, newDocumentId: result.document.id, modelName: model.name });
1957
+ this.logger.debug('duplicated document', { srcType, srcProjectId, srcDocumentId, newDocumentId: result.documentId, modelName: model.name });
1700
1958
 
1701
- return { srcDocumentId: result.document.id };
1959
+ return { srcDocumentId: result.documentId };
1702
1960
  }
1703
1961
 
1704
1962
  async uploadAssets({
@@ -1770,11 +2028,6 @@ export class ContentStore {
1770
2028
  throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
1771
2029
  }
1772
2030
  await contentSourceData.instance.deleteDocument({ document: csiDocument, userContext });
1773
-
1774
- // do not update cache in contentSourceData.documents and documentMap,
1775
- // instead wait for contentSource to call onContentChange(contentChangeEvent)
1776
- // and use data from contentChangeEvent to update the cache
1777
- // delete contentSourceData.documentMap[srcDocumentId];
1778
2031
  }
1779
2032
 
1780
2033
  async validateDocuments({