@stackbit/cms-core 0.1.6 → 0.1.8

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.
@@ -16,11 +16,16 @@ const csi_to_store_docs_converter_1 = require("./utils/csi-to-store-docs-convert
16
16
  const content_store_utils_1 = require("./content-store-utils");
17
17
  const store_to_api_docs_converter_1 = require("./utils/store-to-api-docs-converter");
18
18
  const create_update_csi_docs_1 = require("./utils/create-update-csi-docs");
19
+ const model_utils_1 = require("./utils/model-utils");
20
+ const common_schema_1 = require("./common/common-schema");
19
21
  class ContentStore {
20
22
  constructor(options) {
21
23
  this.contentSources = [];
22
24
  this.contentSourceDataById = {};
23
- this.rawStackbitConfig = null;
25
+ this.stackbitConfig = null;
26
+ this.yamlModels = [];
27
+ this.configModels = [];
28
+ this.presets = {};
24
29
  this.logger = options.logger.createLogger({ label: 'content-store' });
25
30
  this.userLogger = options.userLogger.createLogger({ label: 'content-store' });
26
31
  this.localDev = options.localDev;
@@ -63,6 +68,10 @@ class ContentStore {
63
68
  }
64
69
  async init({ stackbitConfig }) {
65
70
  this.logger.debug('init');
71
+ if (stackbitConfig) {
72
+ this.yamlModels = await this.loadYamlModels({ stackbitConfig });
73
+ this.presets = await this.loadPresets({ stackbitConfig });
74
+ }
66
75
  await this.setStackbitConfig({ stackbitConfig, init: true });
67
76
  }
68
77
  async onStackbitConfigChange({ stackbitConfig }) {
@@ -70,38 +79,17 @@ class ContentStore {
70
79
  await this.setStackbitConfig({ stackbitConfig, init: true });
71
80
  }
72
81
  async setStackbitConfig({ stackbitConfig, init }) {
73
- // TODO: Use stackbitConfig instead of loading rawStackbitConfig here
74
- // by splitting config loader into independent phases:
75
- // 1. load and validate only the root config props
76
- // 2. load content-source models
77
- // 3. merge content-source models with config.models or via config.mapModels, sanitize and validate
78
- // 4. load presets, adjust presets to have srcType and srcProjectId
79
- if (!stackbitConfig) {
80
- this.rawStackbitConfig = null;
81
- }
82
- else {
83
- const rawConfigResult = await (0, sdk_1.loadConfigFromDir)({
84
- dirPath: stackbitConfig.dirPath,
85
- logger: this.logger
86
- });
87
- for (const error of rawConfigResult.errors) {
88
- this.userLogger.warn(error.message);
89
- }
90
- if (rawConfigResult.config) {
91
- this.rawStackbitConfig = {
92
- ...rawConfigResult.config,
93
- contentSources: stackbitConfig.contentSources
94
- };
95
- }
96
- else {
97
- this.rawStackbitConfig = null;
98
- }
99
- }
100
- await this.loadContentSources({ init });
82
+ this.stackbitConfig = stackbitConfig;
83
+ this.configModels = this.mergeConfigModels(this.stackbitConfig, this.yamlModels);
84
+ await this.loadAllContentSourcesAndProcessData({ init });
101
85
  }
102
86
  /**
103
87
  * This method is called when contentUpdatesWatchTimer receives timeout.
104
- * It then notifies all content sources to stop watching for content changes.
88
+ * This happens when the user is not using the Stackbit app for some time
89
+ * but container is not hibernated.
90
+ * It then notifies all content sources to stop watching for content
91
+ * changes, which in turn stops polling CMS for content changes and helps
92
+ * reducing the CMS API usage.
105
93
  */
106
94
  handleTimerTimeout() {
107
95
  for (const contentSourceInstance of this.contentSources) {
@@ -119,78 +107,157 @@ class ContentStore {
119
107
  return;
120
108
  }
121
109
  this.logger.debug('keepAlive => contentUpdatesWatchTimer is not running => load content source data');
122
- await this.loadContentSources({ init: false });
123
- }
124
- /**
125
- * This method is called when a content source notifies Stackbit of models
126
- * changes via webhook. When this happens, all content source data
127
- *
128
- * For example, Contentful notifies Stackbit of any content-type changes via
129
- * special webhook.
130
- *
131
- * @param contentSourceId
132
- */
133
- async onContentSourceSchemaChange({ contentSourceId }) {
134
- this.logger.debug('onContentSourceSchemaChange', { contentSourceId });
135
- const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
136
- this.contentSourceDataById[contentSourceId] = await this.loadContentSourceData({
137
- contentSourceInstance: contentSourceData.instance,
138
- init: false
139
- });
140
- this.onSchemaChangeCallback();
110
+ await this.loadAllContentSourcesAndProcessData({ init: false });
141
111
  }
142
112
  async onFilesChange(updatedFiles) {
143
113
  var _a, _b;
144
114
  this.logger.debug('onFilesChange');
145
- let someContentSourceSchemaUpdated = false;
146
- const contentChanges = {
147
- updatedDocuments: [],
148
- updatedAssets: [],
149
- deletedDocuments: [],
150
- deletedAssets: []
151
- };
115
+ let schemaChanged = false;
116
+ if (this.stackbitConfig) {
117
+ // Check if any of the yaml models files were changed. If yaml model files were changed,
118
+ // reload them and merge them with models defined in stackbit config.
119
+ const modelDirs = (0, sdk_1.getYamlModelDirs)(this.stackbitConfig);
120
+ const yamlModelsChanged = updatedFiles.find((updatedFile) => lodash_1.default.some(modelDirs, (modelDir) => updatedFile.startsWith(modelDir)));
121
+ if (yamlModelsChanged) {
122
+ this.logger.debug('identified change in stackbit model files');
123
+ schemaChanged = true;
124
+ this.yamlModels = await this.loadYamlModels({ stackbitConfig: this.stackbitConfig });
125
+ this.configModels = this.mergeConfigModels(this.stackbitConfig, this.yamlModels);
126
+ }
127
+ // Check if any of the preset files were changed. If presets were changed, reload them.
128
+ const presetDirs = (0, sdk_1.getPresetDirs)(this.stackbitConfig);
129
+ const presetsChanged = updatedFiles.find((updatedFile) => lodash_1.default.some(presetDirs, (presetDir) => updatedFile.startsWith(presetDir)));
130
+ if (presetsChanged) {
131
+ this.logger.debug('identified change in stackbit preset files');
132
+ schemaChanged = true;
133
+ this.presets = await this.loadPresets({ stackbitConfig: this.stackbitConfig });
134
+ }
135
+ }
136
+ const contentSourceIdsWithChangedSchema = [];
137
+ const contentChangeEvents = [];
152
138
  for (const contentSourceInstance of this.contentSources) {
153
139
  const contentSourceId = (0, content_store_utils_1.getContentSourceIdForContentSource)(contentSourceInstance);
154
140
  this.logger.debug(`call onFilesChange for contentSource: ${contentSourceId}`);
155
- const { schemaChanged, contentChangeEvent } = (_b = (await ((_a = contentSourceInstance.onFilesChange) === null || _a === void 0 ? void 0 : _a.call(contentSourceInstance, { updatedFiles: updatedFiles })))) !== null && _b !== void 0 ? _b : {};
156
- this.logger.debug(`schemaChanged: ${schemaChanged}, has contentChangeEvent: ${!!contentChangeEvent}`);
141
+ const onFilesChangeResult = (_b = (await ((_a = contentSourceInstance.onFilesChange) === null || _a === void 0 ? void 0 : _a.call(contentSourceInstance, { updatedFiles: updatedFiles })))) !== null && _b !== void 0 ? _b : {};
142
+ this.logger.debug(`schemaChanged: ${onFilesChangeResult.schemaChanged}, has contentChangeEvent: ${!!onFilesChangeResult.contentChangeEvent}`);
157
143
  // if schema is changed, there is no need to return contentChanges
158
144
  // because schema changes reloads everything and implies content changes
159
- if (schemaChanged) {
160
- someContentSourceSchemaUpdated = true;
161
- this.contentSourceDataById[contentSourceId] = await this.loadContentSourceData({ contentSourceInstance, init: false });
145
+ if (onFilesChangeResult.schemaChanged) {
146
+ schemaChanged = true;
147
+ contentSourceIdsWithChangedSchema.push(contentSourceId);
148
+ }
149
+ else if (onFilesChangeResult.contentChangeEvent) {
150
+ contentChangeEvents.push({ contentSourceId, contentChangeEvent: onFilesChangeResult.contentChangeEvent });
162
151
  }
163
- else if (contentChangeEvent) {
152
+ }
153
+ const contentChanges = {
154
+ updatedDocuments: [],
155
+ updatedAssets: [],
156
+ deletedDocuments: [],
157
+ deletedAssets: []
158
+ };
159
+ // If the schema was changed, there is no need to accumulate or notify about content changes.
160
+ // The processData will update the store with the latest data. And once the Studio receives
161
+ // the schemaChanged notification it will reload all the models and the documents with their latest state.
162
+ if (schemaChanged) {
163
+ await this.reloadContentSourcesByIdAndProcessData({ contentSourceIds: contentSourceIdsWithChangedSchema });
164
+ }
165
+ else {
166
+ contentChangeEvents.reduce((contentChanges, { contentSourceId, contentChangeEvent }) => {
164
167
  const contentChangeResult = this.onContentChange(contentSourceId, contentChangeEvent);
165
168
  contentChanges.updatedDocuments = contentChanges.updatedDocuments.concat(contentChangeResult.updatedDocuments);
166
169
  contentChanges.updatedAssets = contentChanges.updatedAssets.concat(contentChangeResult.updatedAssets);
167
170
  contentChanges.deletedDocuments = contentChanges.deletedDocuments.concat(contentChangeResult.deletedDocuments);
168
171
  contentChanges.deletedAssets = contentChanges.deletedAssets.concat(contentChangeResult.deletedAssets);
169
- }
172
+ return contentChanges;
173
+ }, contentChanges);
170
174
  }
175
+ // TODO: maybe instead of returning object with results
176
+ // replace with this.onSchemaChangeCallback() and this.onContentChangeCallback(contentChanges) for consistency of data flow?
171
177
  return {
172
- schemaChanged: someContentSourceSchemaUpdated,
178
+ schemaChanged: schemaChanged,
173
179
  contentChanges: contentChanges
174
180
  };
175
181
  }
176
- async loadContentSources({ init }) {
182
+ async loadYamlModels({ stackbitConfig }) {
183
+ const yamlModelsResult = await (0, sdk_1.loadYamlModelsFromFiles)(stackbitConfig);
184
+ for (const error of yamlModelsResult.errors) {
185
+ this.userLogger.warn(error.message);
186
+ }
187
+ return yamlModelsResult.models;
188
+ }
189
+ mergeConfigModels(stackbitConfig, modelsFromFiles) {
190
+ var _a;
191
+ const configModelsResult = (0, sdk_1.mergeConfigModelsWithModelsFromFiles)((_a = stackbitConfig === null || stackbitConfig === void 0 ? void 0 : stackbitConfig.models) !== null && _a !== void 0 ? _a : [], modelsFromFiles);
192
+ for (const error of configModelsResult.errors) {
193
+ this.userLogger.warn(error.message);
194
+ }
195
+ return configModelsResult.models;
196
+ }
197
+ async loadPresets({ stackbitConfig }) {
198
+ var _a;
199
+ const contentSources = (_a = stackbitConfig === null || stackbitConfig === void 0 ? void 0 : stackbitConfig.contentSources) !== null && _a !== void 0 ? _a : [];
200
+ const singleContentSource = contentSources.length === 1 ? contentSources[0] : null;
201
+ const presetResult = await (0, sdk_1.loadPresets)({
202
+ config: stackbitConfig,
203
+ ...(singleContentSource
204
+ ? {
205
+ fallbackSrcType: singleContentSource.getContentSourceType(),
206
+ fallbackSrcProjectId: singleContentSource.getProjectId()
207
+ }
208
+ : null)
209
+ });
210
+ for (const error of presetResult.errors) {
211
+ this.userLogger.warn(error.message);
212
+ }
213
+ const { presets } = await this.handleConfigAssets({ presets: presetResult.presets });
214
+ return presets;
215
+ }
216
+ async loadAllContentSourcesAndProcessData({ init }) {
177
217
  var _a, _b;
178
- this.logger.debug('loadContentSources', { init });
218
+ this.logger.debug('loadAllContentSourcesAndProcessData', { init });
179
219
  this.contentUpdatesWatchTimer.stopTimer();
180
- // TODO: move CSITypes to separate package so Config will be able to use them
181
- const contentSources = ((_b = (_a = this.rawStackbitConfig) === null || _a === void 0 ? void 0 : _a.contentSources) !== null && _b !== void 0 ? _b : []);
220
+ const contentSources = (_b = (_a = this.stackbitConfig) === null || _a === void 0 ? void 0 : _a.contentSources) !== null && _b !== void 0 ? _b : [];
182
221
  const promises = contentSources.map((contentSourceInstance) => {
183
222
  return this.loadContentSourceData({ contentSourceInstance, init });
184
223
  });
185
- const contentSourceDataArr = await Promise.all(promises);
186
- const contentSourceDataById = lodash_1.default.keyBy(contentSourceDataArr, 'id');
224
+ const contentSourceRawDataArr = await Promise.all(promises);
187
225
  // update all content sources at once to prevent race conditions
226
+ this.contentSourceDataById = await this.processData({
227
+ stackbitConfig: this.stackbitConfig,
228
+ configModels: this.configModels,
229
+ presets: this.presets,
230
+ contentSourceRawDataArr: contentSourceRawDataArr
231
+ });
188
232
  this.contentSources = contentSources;
189
- this.contentSourceDataById = contentSourceDataById;
233
+ this.contentUpdatesWatchTimer.startTimer();
234
+ }
235
+ async reloadContentSourcesByIdAndProcessData({ contentSourceIds }) {
236
+ this.logger.debug('reloadContentSourcesByIdAndProcessData', { contentSourceIds });
237
+ this.contentUpdatesWatchTimer.stopTimer();
238
+ const promises = this.contentSources.map((contentSourceInstance) => {
239
+ const contentSourceId = (0, content_store_utils_1.getContentSourceIdForContentSource)(contentSourceInstance);
240
+ if (contentSourceIds.includes(contentSourceId)) {
241
+ return this.loadContentSourceData({
242
+ contentSourceInstance: contentSourceInstance,
243
+ init: false
244
+ });
245
+ }
246
+ else {
247
+ return Promise.resolve(lodash_1.default.omit(this.contentSourceDataById[contentSourceId], ['models', 'modelMap', 'documents', 'documentMap']));
248
+ }
249
+ });
250
+ const contentSourceRawDataArr = await Promise.all(promises);
251
+ this.contentSourceDataById = await this.processData({
252
+ stackbitConfig: this.stackbitConfig,
253
+ configModels: this.configModels,
254
+ presets: this.presets,
255
+ contentSourceRawDataArr: contentSourceRawDataArr
256
+ });
190
257
  this.contentUpdatesWatchTimer.startTimer();
191
258
  }
192
259
  async loadContentSourceData({ contentSourceInstance, init }) {
193
- var _a, _b, _c, _d, _e;
260
+ var _a;
194
261
  const contentSourceId = (0, content_store_utils_1.getContentSourceIdForContentSource)(contentSourceInstance);
195
262
  this.logger.debug('loadContentSourceData', { contentSourceId, init });
196
263
  if (init) {
@@ -207,87 +274,27 @@ class ContentStore {
207
274
  contentSourceInstance.stopWatchingContentUpdates();
208
275
  await contentSourceInstance.reset();
209
276
  }
210
- // TODO: introduce optimization: don't fetch content source models,
211
- // documents, assets if only stackbitConfig was changed
212
277
  const csiModels = await contentSourceInstance.getModels();
213
- const locales = await (contentSourceInstance === null || contentSourceInstance === void 0 ? void 0 : contentSourceInstance.getLocales());
214
- const defaultLocaleCode = (_a = locales === null || locales === void 0 ? void 0 : locales.find((locale) => locale.default)) === null || _a === void 0 ? void 0 : _a.code;
215
- // for older versions of stackbit, it uses models to extend content source models
216
- let modelsNoImage = [];
217
- let imageModel;
218
- if (this.rawStackbitConfig) {
219
- const result = await (0, sdk_1.extendConfig)({
220
- config: this.rawStackbitConfig,
221
- externalModels: csiModels
222
- });
223
- for (const error of (_b = result === null || result === void 0 ? void 0 : result.errors) !== null && _b !== void 0 ? _b : []) {
224
- this.userLogger.warn(error.message);
225
- }
226
- const config = await this.handleConfigAssets(result.config);
227
- const modelsWithImageModel = (_c = config === null || config === void 0 ? void 0 : config.models) !== null && _c !== void 0 ? _c : [];
228
- const imageModelIndex = modelsWithImageModel.findIndex((model) => (0, sdk_1.isImageModel)(model));
229
- if (imageModelIndex > -1) {
230
- imageModel = modelsWithImageModel[imageModelIndex];
231
- modelsWithImageModel.splice(imageModelIndex, 1);
232
- }
233
- modelsNoImage = modelsWithImageModel;
234
- // TODO: load presets externally from config, and create additional map
235
- // that maps presetIds by model name instead of storing that map inside every model
236
- // Augment presets with srcType and srcProjectId if they don't exist
237
- this.presets = lodash_1.default.reduce(Object.keys((_d = config === null || config === void 0 ? void 0 : config.presets) !== null && _d !== void 0 ? _d : {}), (accum, presetId) => {
238
- var _a, _b, _c;
239
- const preset = (_a = config === null || config === void 0 ? void 0 : config.presets) === null || _a === void 0 ? void 0 : _a[presetId];
240
- lodash_1.default.set(accum, [presetId], {
241
- ...preset,
242
- srcType: (_b = preset === null || preset === void 0 ? void 0 : preset.srcType) !== null && _b !== void 0 ? _b : contentSourceInstance.getContentSourceType(),
243
- srcProjectId: (_c = preset === null || preset === void 0 ? void 0 : preset.srcProjectId) !== null && _c !== void 0 ? _c : contentSourceInstance.getProjectId()
244
- });
245
- return accum;
246
- }, {});
247
- }
248
- if ((_e = this.rawStackbitConfig) === null || _e === void 0 ? void 0 : _e.mapModels) {
249
- const srcType = contentSourceInstance.getContentSourceType();
250
- const srcProjectId = contentSourceInstance.getProjectId();
251
- const modelsWithSource = modelsNoImage.map((model) => ({
252
- srcType,
253
- srcProjectId,
254
- ...model
255
- }));
256
- const mappedModels = this.rawStackbitConfig.mapModels({
257
- models: modelsWithSource
258
- });
259
- modelsNoImage = mappedModels.map((model) => {
260
- const { srcType, srcProjectId, ...rest } = model;
261
- return rest;
262
- });
263
- }
264
- const models = imageModel ? [...modelsNoImage, imageModel] : modelsNoImage;
265
- const modelMap = lodash_1.default.keyBy(models, 'name');
266
278
  const csiModelMap = lodash_1.default.keyBy(csiModels, 'name');
279
+ const locales = await contentSourceInstance.getLocales();
280
+ const defaultLocaleCode = (_a = locales === null || locales === void 0 ? void 0 : locales.find((locale) => locale.default)) === null || _a === void 0 ? void 0 : _a.code;
267
281
  const csiDocuments = await contentSourceInstance.getDocuments({ modelMap: csiModelMap });
268
282
  const csiAssets = await contentSourceInstance.getAssets();
269
283
  const csiDocumentMap = lodash_1.default.keyBy(csiDocuments, 'id');
270
284
  const csiAssetMap = lodash_1.default.keyBy(csiAssets, 'id');
271
- const contentStoreDocuments = (0, csi_to_store_docs_converter_1.mapCSIDocumentsToStoreDocuments)({
272
- csiDocuments,
273
- contentSourceInstance,
274
- modelMap,
275
- defaultLocaleCode
276
- });
277
285
  const contentStoreAssets = (0, csi_to_store_docs_converter_1.mapCSIAssetsToStoreAssets)({
278
286
  csiAssets,
279
287
  contentSourceInstance,
280
288
  defaultLocaleCode
281
289
  });
282
- const documentMap = lodash_1.default.keyBy(contentStoreDocuments, 'srcObjectId');
283
290
  const assetMap = lodash_1.default.keyBy(contentStoreAssets, 'srcObjectId');
284
291
  this.logger.debug('loaded content source data', {
285
292
  contentSourceId,
286
- locales,
287
293
  defaultLocaleCode,
288
- modelCount: models.length,
289
- documentCount: contentStoreDocuments.length,
290
- assetCount: contentStoreAssets.length
294
+ localesCount: locales.length,
295
+ modelCount: csiModels.length,
296
+ documentCount: csiDocuments.length,
297
+ assetCount: csiAssets.length
291
298
  });
292
299
  contentSourceInstance.startWatchingContentUpdates({
293
300
  getModelMap: () => {
@@ -306,27 +313,21 @@ class ContentStore {
306
313
  },
307
314
  onSchemaChange: async () => {
308
315
  this.logger.debug('content source called onSchemaChange', { contentSourceId });
309
- this.contentSourceDataById[contentSourceId] = await this.loadContentSourceData({
310
- contentSourceInstance: contentSourceInstance,
311
- init: false
312
- });
316
+ await this.reloadContentSourcesByIdAndProcessData({ contentSourceIds: [contentSourceId] });
313
317
  this.onSchemaChangeCallback();
314
318
  }
315
319
  });
316
320
  return {
317
321
  id: contentSourceId,
318
- type: contentSourceInstance.getContentSourceType(),
319
- projectId: contentSourceInstance.getProjectId(),
322
+ srcType: contentSourceInstance.getContentSourceType(),
323
+ srcProjectId: contentSourceInstance.getProjectId(),
320
324
  instance: contentSourceInstance,
321
325
  locales: locales,
322
326
  defaultLocaleCode: defaultLocaleCode,
323
- models: models,
324
- modelMap: modelMap,
327
+ csiModels: csiModels,
325
328
  csiModelMap: csiModelMap,
326
329
  csiDocuments: csiDocuments,
327
330
  csiDocumentMap: csiDocumentMap,
328
- documents: contentStoreDocuments,
329
- documentMap: documentMap,
330
331
  csiAssets: csiAssets,
331
332
  csiAssetMap: csiAssetMap,
332
333
  assets: contentStoreAssets,
@@ -335,6 +336,7 @@ class ContentStore {
335
336
  }
336
337
  onContentChange(contentSourceId, contentChangeEvent) {
337
338
  // TODO: prevent content change process for contentSourceId if loading content is in progress
339
+ var _a, _b, _c, _d;
338
340
  this.logger.debug('onContentChange', {
339
341
  contentSourceId,
340
342
  documentCount: contentChangeEvent.documents.length,
@@ -362,8 +364,8 @@ class ContentStore {
362
364
  contentSourceData.csiDocuments.splice(index, 1);
363
365
  }
364
366
  result.deletedDocuments.push({
365
- srcType: contentSourceData.type,
366
- srcProjectId: contentSourceData.projectId,
367
+ srcType: contentSourceData.srcType,
368
+ srcProjectId: contentSourceData.srcProjectId,
367
369
  srcObjectId: docId
368
370
  });
369
371
  });
@@ -380,14 +382,35 @@ class ContentStore {
380
382
  contentSourceData.csiAssets.splice(index, 1);
381
383
  }
382
384
  result.deletedAssets.push({
383
- srcType: contentSourceData.type,
384
- srcProjectId: contentSourceData.projectId,
385
+ srcType: contentSourceData.srcType,
386
+ srcProjectId: contentSourceData.srcProjectId,
385
387
  srcObjectId: assetId
386
388
  });
387
389
  });
390
+ // map csi documents through stackbitConfig.mapDocuments
391
+ let mappedDocs = contentChangeEvent.documents;
392
+ if ((_a = this.stackbitConfig) === null || _a === void 0 ? void 0 : _a.mapDocuments) {
393
+ const csiDocumentsWithSource = contentChangeEvent.documents.map((csiDocument) => ({
394
+ srcType: contentSourceData.srcType,
395
+ srcProjectId: contentSourceData.srcProjectId,
396
+ ...csiDocument
397
+ }));
398
+ const modelsWithSource = contentSourceData.models.map((model) => {
399
+ return {
400
+ srcType: contentSourceData.srcType,
401
+ srcProjectId: contentSourceData.srcProjectId,
402
+ ...model
403
+ };
404
+ });
405
+ mappedDocs =
406
+ (_d = (_c = (_b = this.stackbitConfig) === null || _b === void 0 ? void 0 : _b.mapDocuments) === null || _c === void 0 ? void 0 : _c.call(_b, {
407
+ documents: lodash_1.default.cloneDeep(csiDocumentsWithSource),
408
+ models: lodash_1.default.cloneDeep(modelsWithSource)
409
+ })) !== null && _d !== void 0 ? _d : csiDocumentsWithSource;
410
+ }
388
411
  // map csi documents and assets to content store documents and assets
389
412
  const documents = (0, csi_to_store_docs_converter_1.mapCSIDocumentsToStoreDocuments)({
390
- csiDocuments: contentChangeEvent.documents,
413
+ csiDocuments: mappedDocs,
391
414
  contentSourceInstance: contentSourceData.instance,
392
415
  modelMap: contentSourceData.modelMap,
393
416
  defaultLocaleCode: contentSourceData.defaultLocaleCode
@@ -399,7 +422,7 @@ class ContentStore {
399
422
  });
400
423
  // update contentSourceData with new or updated documents and assets
401
424
  Object.assign(contentSourceData.csiDocumentMap, lodash_1.default.keyBy(contentChangeEvent.documents, 'id'));
402
- Object.assign(contentSourceData.csiAssets, lodash_1.default.keyBy(contentChangeEvent.assets, 'id'));
425
+ Object.assign(contentSourceData.csiAssetMap, lodash_1.default.keyBy(contentChangeEvent.assets, 'id'));
403
426
  Object.assign(contentSourceData.documentMap, lodash_1.default.keyBy(documents, 'srcObjectId'));
404
427
  Object.assign(contentSourceData.assetMap, lodash_1.default.keyBy(assets, 'srcObjectId'));
405
428
  for (let idx = 0; idx < documents.length; idx++) {
@@ -417,8 +440,8 @@ class ContentStore {
417
440
  contentSourceData.csiDocuments.splice(dataIndex, 1, csiDocument);
418
441
  }
419
442
  result.updatedDocuments.push({
420
- srcType: contentSourceData.type,
421
- srcProjectId: contentSourceData.projectId,
443
+ srcType: contentSourceData.srcType,
444
+ srcProjectId: contentSourceData.srcProjectId,
422
445
  srcObjectId: document.srcObjectId
423
446
  });
424
447
  }
@@ -437,18 +460,77 @@ class ContentStore {
437
460
  contentSourceData.csiAssets.splice(index, 1, csiAsset);
438
461
  }
439
462
  result.updatedAssets.push({
440
- srcType: contentSourceData.type,
441
- srcProjectId: contentSourceData.projectId,
463
+ srcType: contentSourceData.srcType,
464
+ srcProjectId: contentSourceData.srcProjectId,
442
465
  srcObjectId: asset.srcObjectId
443
466
  });
444
467
  }
445
468
  return result;
446
469
  }
470
+ async processData({ stackbitConfig, configModels, presets, contentSourceRawDataArr }) {
471
+ var _a, _b, _c, _d;
472
+ const modelsWithSource = contentSourceRawDataArr.reduce((accum, csData) => {
473
+ const mergedModels = (0, sdk_1.mergeConfigModelsWithExternalModels)({
474
+ configModels: configModels,
475
+ externalModels: csData.csiModels
476
+ });
477
+ const modelsWithSource = mergedModels.map((model) => {
478
+ return {
479
+ srcType: csData.srcType,
480
+ srcProjectId: csData.id,
481
+ ...model
482
+ };
483
+ });
484
+ return accum.concat(modelsWithSource);
485
+ }, []);
486
+ // TODO: Is there a better way than deep cloning objects before passing them to user methods?
487
+ // Not cloning mutable objects will break the internal state if user mutates the objects.
488
+ const mappedModels = (_b = (_a = stackbitConfig === null || stackbitConfig === void 0 ? void 0 : stackbitConfig.mapModels) === null || _a === void 0 ? void 0 : _a.call(stackbitConfig, { models: lodash_1.default.cloneDeep(modelsWithSource) })) !== null && _b !== void 0 ? _b : modelsWithSource;
489
+ const normalizedModels = (0, model_utils_1.normalizeModels)({ models: mappedModels, logger: this.userLogger });
490
+ const validatedModels = (0, model_utils_1.validateModels)({ models: normalizedModels, logger: this.userLogger });
491
+ const modelsWithPresetsIds = (0, sdk_1.extendModelsWithPresetsIds)({ models: validatedModels, presets });
492
+ const { models } = await this.handleConfigAssets({ models: modelsWithPresetsIds });
493
+ const csiDocumentsWithSource = contentSourceRawDataArr.reduce((accum, csData) => {
494
+ const csiDocumentsWithSource = csData.csiDocuments.map((csiDocument) => ({
495
+ srcType: csData.srcType,
496
+ srcProjectId: csData.id,
497
+ ...csiDocument
498
+ }));
499
+ return accum.concat(csiDocumentsWithSource);
500
+ }, []);
501
+ // TODO: Is there a better way than deep cloning objects before passing them to user methods?
502
+ // Not cloning mutable objects will break the internal state if user mutates the objects.
503
+ const mappedDocs = (_d = (_c = stackbitConfig === null || stackbitConfig === void 0 ? void 0 : stackbitConfig.mapDocuments) === null || _c === void 0 ? void 0 : _c.call(stackbitConfig, {
504
+ documents: lodash_1.default.cloneDeep(csiDocumentsWithSource),
505
+ models: lodash_1.default.cloneDeep(models)
506
+ })) !== null && _d !== void 0 ? _d : csiDocumentsWithSource;
507
+ const modelMapByContentSource = (0, content_store_utils_1.groupModelsByContentSource)({ models: models });
508
+ const documentMapByContentSource = (0, content_store_utils_1.groupDocumentsByContentSource)({ documents: mappedDocs });
509
+ const contentSourceDataArr = contentSourceRawDataArr.map((csData) => {
510
+ const modelMap = lodash_1.default.get(modelMapByContentSource, [csData.srcType, csData.id], {});
511
+ const mappedCSIDocuments = lodash_1.default.get(documentMapByContentSource, [csData.srcType, csData.id]);
512
+ const documents = (0, csi_to_store_docs_converter_1.mapCSIDocumentsToStoreDocuments)({
513
+ csiDocuments: mappedCSIDocuments,
514
+ contentSourceInstance: csData.instance,
515
+ defaultLocaleCode: csData.defaultLocaleCode,
516
+ modelMap: modelMap
517
+ });
518
+ return {
519
+ ...csData,
520
+ models: Object.values(modelMap),
521
+ modelMap,
522
+ documents,
523
+ documentMap: lodash_1.default.keyBy(documents, 'srcObjectId')
524
+ };
525
+ });
526
+ return lodash_1.default.keyBy(contentSourceDataArr, 'id');
527
+ }
447
528
  getModels() {
448
529
  return lodash_1.default.reduce(this.contentSourceDataById, (result, contentSourceData) => {
449
530
  const contentSourceType = contentSourceData.instance.getContentSourceType();
450
531
  const srcProjectId = contentSourceData.instance.getProjectId();
451
532
  lodash_1.default.set(result, [contentSourceType, srcProjectId], contentSourceData.modelMap);
533
+ lodash_1.default.set(result, [contentSourceType, srcProjectId, '__image_model'], common_schema_1.IMAGE_MODEL);
452
534
  return result;
453
535
  }, {});
454
536
  }
@@ -479,8 +561,8 @@ class ContentStore {
479
561
  contentSourceDataArr = Object.values(this.contentSourceDataById);
480
562
  }
481
563
  return (0, utils_1.reducePromise)(contentSourceDataArr, async (accum, contentSourceData) => {
482
- const srcType = contentSourceData.type;
483
- const srcProjectId = contentSourceData.projectId;
564
+ const srcType = contentSourceData.srcType;
565
+ const srcProjectId = contentSourceData.srcProjectId;
484
566
  const userContext = (0, content_store_utils_1.getUserContextForSrcType)(srcType, user);
485
567
  let result = await contentSourceData.instance.hasAccess({ userContext });
486
568
  // backwards compatibility with older CSI version
@@ -949,13 +1031,13 @@ class ContentStore {
949
1031
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
950
1032
  locale = locale !== null && locale !== void 0 ? locale : contentSourceData.defaultLocaleCode;
951
1033
  const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
952
- const userContext = (0, content_store_utils_1.getUserContextForSrcType)(contentSourceData.type, user);
1034
+ const userContext = (0, content_store_utils_1.getUserContextForSrcType)(contentSourceData.srcType, user);
953
1035
  const internalValidationErrors = internalValidateContent(documents, assets, contentSourceData);
954
1036
  const validationResult = await contentSourceData.instance.validateDocuments({ documents, assets, locale, userContext });
955
1037
  errors = errors.concat(internalValidationErrors, validationResult.errors.map((validationError) => ({
956
1038
  message: validationError.message,
957
- srcType: contentSourceData.type,
958
- srcProjectId: contentSourceData.projectId,
1039
+ srcType: contentSourceData.srcType,
1040
+ srcProjectId: contentSourceData.srcProjectId,
959
1041
  srcObjectType: validationError.objectType,
960
1042
  srcObjectId: validationError.objectId,
961
1043
  fieldPath: validationError.fieldPath,
@@ -986,7 +1068,7 @@ class ContentStore {
986
1068
  Object.keys(objectsBySourceId).forEach((contentSourceId) => {
987
1069
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
988
1070
  documents.push(...contentSourceData.documents);
989
- lodash_1.default.set(schema, [contentSourceData.type, contentSourceData.projectId], contentSourceData.modelMap);
1071
+ lodash_1.default.set(schema, [contentSourceData.srcType, contentSourceData.srcProjectId], contentSourceData.modelMap);
990
1072
  });
991
1073
  return (0, search_utils_1.searchDocuments)({
992
1074
  ...data,
@@ -999,7 +1081,7 @@ class ContentStore {
999
1081
  const objectsBySourceId = lodash_1.default.groupBy(objects, (object) => (0, content_store_utils_1.getContentSourceId)(object.srcType, object.srcProjectId));
1000
1082
  for (const [contentSourceId, contentSourceObjects] of Object.entries(objectsBySourceId)) {
1001
1083
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1002
- const userContext = (0, content_store_utils_1.getUserContextForSrcType)(contentSourceData.type, user);
1084
+ const userContext = (0, content_store_utils_1.getUserContextForSrcType)(contentSourceData.srcType, user);
1003
1085
  const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
1004
1086
  await contentSourceData.instance.publishDocuments({ documents, assets, userContext });
1005
1087
  }
@@ -1074,8 +1156,8 @@ function validateDocumentFields(document, documentField, fieldPath, contentSourc
1074
1156
  if (!objRef) {
1075
1157
  errors.push({
1076
1158
  fieldPath,
1077
- srcType: contentSourceData.type,
1078
- srcProjectId: contentSourceData.projectId,
1159
+ srcType: contentSourceData.srcType,
1160
+ srcProjectId: contentSourceData.srcProjectId,
1079
1161
  srcObjectType: documentField.refType,
1080
1162
  srcObjectId: document.id,
1081
1163
  message: `Can't find referenced ${documentField.refType}: ${documentField.refId}`