@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.
- package/dist/content-store-utils.d.ts +6 -0
- package/dist/content-store-utils.d.ts.map +1 -1
- package/dist/content-store-utils.js +20 -1
- package/dist/content-store-utils.js.map +1 -1
- package/dist/content-store.d.ts +25 -19
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +249 -167
- package/dist/content-store.js.map +1 -1
- package/dist/stackbit/index.d.ts.map +1 -1
- package/dist/stackbit/index.js +1 -1
- package/dist/stackbit/index.js.map +1 -1
- package/dist/utils/csi-to-store-docs-converter.d.ts.map +1 -1
- package/dist/utils/csi-to-store-docs-converter.js.map +1 -1
- package/dist/utils/model-utils.d.ts +11 -0
- package/dist/utils/model-utils.d.ts.map +1 -0
- package/dist/utils/model-utils.js +64 -0
- package/dist/utils/model-utils.js.map +1 -0
- package/dist/utils/search-utils.d.ts +2 -2
- package/dist/utils/search-utils.d.ts.map +1 -1
- package/dist/utils/search-utils.js +1 -1
- package/dist/utils/search-utils.js.map +1 -1
- package/package.json +4 -4
- package/src/content-store-utils.ts +19 -0
- package/src/content-store.ts +327 -182
- package/src/stackbit/index.ts +2 -2
- package/src/utils/csi-to-store-docs-converter.ts +2 -2
- package/src/utils/model-utils.ts +72 -0
- package/src/utils/search-utils.ts +3 -3
package/src/content-store.ts
CHANGED
|
@@ -4,7 +4,21 @@ import sanitizeFilename from 'sanitize-filename';
|
|
|
4
4
|
|
|
5
5
|
import * as CSITypes from '@stackbit/types';
|
|
6
6
|
import { getLocalizedFieldForLocale, UserCommandSpawner } from '@stackbit/types';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
Config,
|
|
9
|
+
Field,
|
|
10
|
+
Model,
|
|
11
|
+
ImageModel,
|
|
12
|
+
Preset,
|
|
13
|
+
PresetMap,
|
|
14
|
+
loadPresets,
|
|
15
|
+
getYamlModelDirs,
|
|
16
|
+
getPresetDirs,
|
|
17
|
+
loadYamlModelsFromFiles,
|
|
18
|
+
mergeConfigModelsWithModelsFromFiles,
|
|
19
|
+
extendModelsWithPresetsIds,
|
|
20
|
+
mergeConfigModelsWithExternalModels
|
|
21
|
+
} from '@stackbit/sdk';
|
|
8
22
|
import { deferWhileRunning, mapPromise, reducePromise } from '@stackbit/utils';
|
|
9
23
|
|
|
10
24
|
import * as ContentStoreTypes from './content-store-types';
|
|
@@ -12,9 +26,20 @@ import { Timer } from './utils/timer';
|
|
|
12
26
|
import { SearchFilter } from './types/search-filter';
|
|
13
27
|
import { searchDocuments } from './utils/search-utils';
|
|
14
28
|
import { mapCSIAssetsToStoreAssets, mapCSIDocumentsToStoreDocuments } from './utils/csi-to-store-docs-converter';
|
|
15
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
getContentSourceId,
|
|
31
|
+
getContentSourceIdForContentSource,
|
|
32
|
+
getModelFieldForFieldAtPath,
|
|
33
|
+
getUserContextForSrcType,
|
|
34
|
+
groupModelsByContentSource,
|
|
35
|
+
groupDocumentsByContentSource
|
|
36
|
+
} from './content-store-utils';
|
|
16
37
|
import { mapAssetsToLocalizedApiImages, mapDocumentsToLocalizedApiObjects, mapStoreAssetsToAPIAssets } from './utils/store-to-api-docs-converter';
|
|
17
38
|
import { convertOperationField, createDocumentRecursively, getCreateDocumentThunk } from './utils/create-update-csi-docs';
|
|
39
|
+
import { normalizeModels, validateModels } from './utils/model-utils';
|
|
40
|
+
import { IMAGE_MODEL } from './common/common-schema';
|
|
41
|
+
|
|
42
|
+
export type HandleConfigAssets = <T extends Model>({ models, presets }: { models?: T[]; presets?: PresetMap }) => Promise<{ models: T[]; presets: PresetMap }>;
|
|
18
43
|
|
|
19
44
|
export interface ContentSourceOptions {
|
|
20
45
|
logger: ContentStoreTypes.Logger;
|
|
@@ -24,21 +49,23 @@ export interface ContentSourceOptions {
|
|
|
24
49
|
userCommandSpawner?: UserCommandSpawner;
|
|
25
50
|
onSchemaChangeCallback: () => void;
|
|
26
51
|
onContentChangeCallback: (contentChanges: ContentStoreTypes.ContentChangeResult) => void;
|
|
27
|
-
handleConfigAssets:
|
|
52
|
+
handleConfigAssets: HandleConfigAssets;
|
|
28
53
|
devAppRestartNeeded?: () => void;
|
|
29
54
|
}
|
|
30
55
|
|
|
31
56
|
interface ContentSourceData {
|
|
32
57
|
id: string;
|
|
33
58
|
instance: CSITypes.ContentSourceInterface;
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
srcType: string;
|
|
60
|
+
srcProjectId: string;
|
|
36
61
|
locales?: CSITypes.Locale[];
|
|
37
62
|
defaultLocaleCode?: string;
|
|
38
63
|
/* Array of extended and validated Models */
|
|
39
64
|
models: Model[];
|
|
40
65
|
/* Map of extended and validated Models by model name */
|
|
41
66
|
modelMap: Record<string, Model>;
|
|
67
|
+
/* Array of original Models (as provided by content source) */
|
|
68
|
+
csiModels: CSITypes.Model[];
|
|
42
69
|
/* Map of original Models (as provided by content source) by model name */
|
|
43
70
|
csiModelMap: Record<string, CSITypes.Model>;
|
|
44
71
|
/* Array of original content source Documents */
|
|
@@ -59,6 +86,8 @@ interface ContentSourceData {
|
|
|
59
86
|
assetMap: Record<string, ContentStoreTypes.Asset>;
|
|
60
87
|
}
|
|
61
88
|
|
|
89
|
+
type ContentSourceRawData = Omit<ContentSourceData, 'models' | 'modelMap' | 'documents' | 'documentMap'>;
|
|
90
|
+
|
|
62
91
|
export class ContentStore {
|
|
63
92
|
private readonly logger: ContentStoreTypes.Logger;
|
|
64
93
|
private readonly userLogger: ContentStoreTypes.Logger;
|
|
@@ -67,13 +96,15 @@ export class ContentStore {
|
|
|
67
96
|
private readonly webhookUrl?: string;
|
|
68
97
|
private readonly onSchemaChangeCallback: () => void;
|
|
69
98
|
private readonly onContentChangeCallback: (contentChanges: ContentStoreTypes.ContentChangeResult) => void;
|
|
70
|
-
private readonly handleConfigAssets:
|
|
99
|
+
private readonly handleConfigAssets: HandleConfigAssets;
|
|
71
100
|
private readonly devAppRestartNeeded?: () => void;
|
|
72
101
|
private contentSources: CSITypes.ContentSourceInterface[] = [];
|
|
73
102
|
private contentSourceDataById: Record<string, ContentSourceData> = {};
|
|
74
103
|
private contentUpdatesWatchTimer: Timer;
|
|
75
|
-
private
|
|
76
|
-
private
|
|
104
|
+
private stackbitConfig: Config | null = null;
|
|
105
|
+
private yamlModels: Model[] = [];
|
|
106
|
+
private configModels: Model[] = [];
|
|
107
|
+
private presets: PresetMap = {};
|
|
77
108
|
|
|
78
109
|
constructor(options: ContentSourceOptions) {
|
|
79
110
|
this.logger = options.logger.createLogger({ label: 'content-store' });
|
|
@@ -120,6 +151,10 @@ export class ContentStore {
|
|
|
120
151
|
|
|
121
152
|
async init({ stackbitConfig }: { stackbitConfig: Config | null }): Promise<void> {
|
|
122
153
|
this.logger.debug('init');
|
|
154
|
+
if (stackbitConfig) {
|
|
155
|
+
this.yamlModels = await this.loadYamlModels({ stackbitConfig });
|
|
156
|
+
this.presets = await this.loadPresets({ stackbitConfig });
|
|
157
|
+
}
|
|
123
158
|
await this.setStackbitConfig({ stackbitConfig, init: true });
|
|
124
159
|
}
|
|
125
160
|
|
|
@@ -129,37 +164,19 @@ export class ContentStore {
|
|
|
129
164
|
}
|
|
130
165
|
|
|
131
166
|
private async setStackbitConfig({ stackbitConfig, init }: { stackbitConfig: Config | null; init: boolean }) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// 3. merge content-source models with config.models or via config.mapModels, sanitize and validate
|
|
137
|
-
// 4. load presets, adjust presets to have srcType and srcProjectId
|
|
138
|
-
if (!stackbitConfig) {
|
|
139
|
-
this.rawStackbitConfig = null;
|
|
140
|
-
} else {
|
|
141
|
-
const rawConfigResult = await loadConfigFromDir({
|
|
142
|
-
dirPath: stackbitConfig.dirPath,
|
|
143
|
-
logger: this.logger
|
|
144
|
-
});
|
|
145
|
-
for (const error of rawConfigResult.errors) {
|
|
146
|
-
this.userLogger.warn(error.message);
|
|
147
|
-
}
|
|
148
|
-
if (rawConfigResult.config) {
|
|
149
|
-
this.rawStackbitConfig = {
|
|
150
|
-
...rawConfigResult.config,
|
|
151
|
-
contentSources: stackbitConfig.contentSources
|
|
152
|
-
};
|
|
153
|
-
} else {
|
|
154
|
-
this.rawStackbitConfig = null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
await this.loadContentSources({ init });
|
|
167
|
+
this.stackbitConfig = stackbitConfig;
|
|
168
|
+
this.configModels = this.mergeConfigModels(this.stackbitConfig, this.yamlModels);
|
|
169
|
+
|
|
170
|
+
await this.loadAllContentSourcesAndProcessData({ init });
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
/**
|
|
161
174
|
* This method is called when contentUpdatesWatchTimer receives timeout.
|
|
162
|
-
*
|
|
175
|
+
* This happens when the user is not using the Stackbit app for some time
|
|
176
|
+
* but container is not hibernated.
|
|
177
|
+
* It then notifies all content sources to stop watching for content
|
|
178
|
+
* changes, which in turn stops polling CMS for content changes and helps
|
|
179
|
+
* reducing the CMS API usage.
|
|
163
180
|
*/
|
|
164
181
|
private handleTimerTimeout() {
|
|
165
182
|
for (const contentSourceInstance of this.contentSources) {
|
|
@@ -179,32 +196,55 @@ export class ContentStore {
|
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
this.logger.debug('keepAlive => contentUpdatesWatchTimer is not running => load content source data');
|
|
182
|
-
await this.
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* This method is called when a content source notifies Stackbit of models
|
|
187
|
-
* changes via webhook. When this happens, all content source data
|
|
188
|
-
*
|
|
189
|
-
* For example, Contentful notifies Stackbit of any content-type changes via
|
|
190
|
-
* special webhook.
|
|
191
|
-
*
|
|
192
|
-
* @param contentSourceId
|
|
193
|
-
*/
|
|
194
|
-
async onContentSourceSchemaChange({ contentSourceId }: { contentSourceId: string }) {
|
|
195
|
-
this.logger.debug('onContentSourceSchemaChange', { contentSourceId });
|
|
196
|
-
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
197
|
-
this.contentSourceDataById[contentSourceId] = await this.loadContentSourceData({
|
|
198
|
-
contentSourceInstance: contentSourceData.instance,
|
|
199
|
-
init: false
|
|
200
|
-
});
|
|
201
|
-
this.onSchemaChangeCallback();
|
|
199
|
+
await this.loadAllContentSourcesAndProcessData({ init: false });
|
|
202
200
|
}
|
|
203
201
|
|
|
204
202
|
async onFilesChange(updatedFiles: string[]): Promise<{ schemaChanged?: boolean; contentChanges: ContentStoreTypes.ContentChangeResult }> {
|
|
205
203
|
this.logger.debug('onFilesChange');
|
|
206
204
|
|
|
207
|
-
let
|
|
205
|
+
let schemaChanged = false;
|
|
206
|
+
|
|
207
|
+
if (this.stackbitConfig) {
|
|
208
|
+
// Check if any of the yaml models files were changed. If yaml model files were changed,
|
|
209
|
+
// reload them and merge them with models defined in stackbit config.
|
|
210
|
+
const modelDirs = getYamlModelDirs(this.stackbitConfig);
|
|
211
|
+
const yamlModelsChanged = updatedFiles.find((updatedFile) => _.some(modelDirs, (modelDir) => updatedFile.startsWith(modelDir)));
|
|
212
|
+
if (yamlModelsChanged) {
|
|
213
|
+
this.logger.debug('identified change in stackbit model files');
|
|
214
|
+
schemaChanged = true;
|
|
215
|
+
this.yamlModels = await this.loadYamlModels({ stackbitConfig: this.stackbitConfig });
|
|
216
|
+
this.configModels = this.mergeConfigModels(this.stackbitConfig, this.yamlModels);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check if any of the preset files were changed. If presets were changed, reload them.
|
|
220
|
+
const presetDirs = getPresetDirs(this.stackbitConfig);
|
|
221
|
+
const presetsChanged = updatedFiles.find((updatedFile) => _.some(presetDirs, (presetDir) => updatedFile.startsWith(presetDir)));
|
|
222
|
+
if (presetsChanged) {
|
|
223
|
+
this.logger.debug('identified change in stackbit preset files');
|
|
224
|
+
schemaChanged = true;
|
|
225
|
+
this.presets = await this.loadPresets({ stackbitConfig: this.stackbitConfig });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const contentSourceIdsWithChangedSchema: string[] = [];
|
|
230
|
+
const contentChangeEvents: { contentSourceId: string; contentChangeEvent: CSITypes.ContentChangeEvent }[] = [];
|
|
231
|
+
|
|
232
|
+
for (const contentSourceInstance of this.contentSources) {
|
|
233
|
+
const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
|
|
234
|
+
this.logger.debug(`call onFilesChange for contentSource: ${contentSourceId}`);
|
|
235
|
+
const onFilesChangeResult = (await contentSourceInstance.onFilesChange?.({ updatedFiles: updatedFiles })) ?? {};
|
|
236
|
+
this.logger.debug(`schemaChanged: ${onFilesChangeResult.schemaChanged}, has contentChangeEvent: ${!!onFilesChangeResult.contentChangeEvent}`);
|
|
237
|
+
|
|
238
|
+
// if schema is changed, there is no need to return contentChanges
|
|
239
|
+
// because schema changes reloads everything and implies content changes
|
|
240
|
+
if (onFilesChangeResult.schemaChanged) {
|
|
241
|
+
schemaChanged = true;
|
|
242
|
+
contentSourceIdsWithChangedSchema.push(contentSourceId);
|
|
243
|
+
} else if (onFilesChangeResult.contentChangeEvent) {
|
|
244
|
+
contentChangeEvents.push({ contentSourceId, contentChangeEvent: onFilesChangeResult.contentChangeEvent });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
208
248
|
const contentChanges: ContentStoreTypes.ContentChangeResult = {
|
|
209
249
|
updatedDocuments: [],
|
|
210
250
|
updatedAssets: [],
|
|
@@ -212,49 +252,117 @@ export class ContentStore {
|
|
|
212
252
|
deletedAssets: []
|
|
213
253
|
};
|
|
214
254
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (schemaChanged) {
|
|
223
|
-
someContentSourceSchemaUpdated = true;
|
|
224
|
-
this.contentSourceDataById[contentSourceId] = await this.loadContentSourceData({ contentSourceInstance, init: false });
|
|
225
|
-
} else if (contentChangeEvent) {
|
|
255
|
+
// If the schema was changed, there is no need to accumulate or notify about content changes.
|
|
256
|
+
// The processData will update the store with the latest data. And once the Studio receives
|
|
257
|
+
// the schemaChanged notification it will reload all the models and the documents with their latest state.
|
|
258
|
+
if (schemaChanged) {
|
|
259
|
+
await this.reloadContentSourcesByIdAndProcessData({ contentSourceIds: contentSourceIdsWithChangedSchema });
|
|
260
|
+
} else {
|
|
261
|
+
contentChangeEvents.reduce((contentChanges, { contentSourceId, contentChangeEvent }) => {
|
|
226
262
|
const contentChangeResult = this.onContentChange(contentSourceId, contentChangeEvent);
|
|
227
263
|
contentChanges.updatedDocuments = contentChanges.updatedDocuments.concat(contentChangeResult.updatedDocuments);
|
|
228
264
|
contentChanges.updatedAssets = contentChanges.updatedAssets.concat(contentChangeResult.updatedAssets);
|
|
229
265
|
contentChanges.deletedDocuments = contentChanges.deletedDocuments.concat(contentChangeResult.deletedDocuments);
|
|
230
266
|
contentChanges.deletedAssets = contentChanges.deletedAssets.concat(contentChangeResult.deletedAssets);
|
|
231
|
-
|
|
267
|
+
return contentChanges;
|
|
268
|
+
}, contentChanges);
|
|
232
269
|
}
|
|
233
270
|
|
|
271
|
+
// TODO: maybe instead of returning object with results
|
|
272
|
+
// replace with this.onSchemaChangeCallback() and this.onContentChangeCallback(contentChanges) for consistency of data flow?
|
|
234
273
|
return {
|
|
235
|
-
schemaChanged:
|
|
274
|
+
schemaChanged: schemaChanged,
|
|
236
275
|
contentChanges: contentChanges
|
|
237
276
|
};
|
|
238
277
|
}
|
|
239
278
|
|
|
240
|
-
private async
|
|
241
|
-
|
|
279
|
+
private async loadYamlModels({ stackbitConfig }: { stackbitConfig: Config }): Promise<Model[]> {
|
|
280
|
+
const yamlModelsResult = await loadYamlModelsFromFiles(stackbitConfig);
|
|
281
|
+
for (const error of yamlModelsResult.errors) {
|
|
282
|
+
this.userLogger.warn(error.message);
|
|
283
|
+
}
|
|
284
|
+
return yamlModelsResult.models;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private mergeConfigModels(stackbitConfig: Config | null, modelsFromFiles: Model[]) {
|
|
288
|
+
const configModelsResult = mergeConfigModelsWithModelsFromFiles(stackbitConfig?.models ?? [], modelsFromFiles);
|
|
289
|
+
for (const error of configModelsResult.errors) {
|
|
290
|
+
this.userLogger.warn(error.message);
|
|
291
|
+
}
|
|
292
|
+
return configModelsResult.models;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async loadPresets({ stackbitConfig }: { stackbitConfig: Config }): Promise<Record<string, Preset>> {
|
|
296
|
+
const contentSources = stackbitConfig?.contentSources ?? [];
|
|
297
|
+
const singleContentSource = contentSources.length === 1 ? contentSources[0] : null;
|
|
298
|
+
const presetResult = await loadPresets({
|
|
299
|
+
config: stackbitConfig,
|
|
300
|
+
...(singleContentSource
|
|
301
|
+
? {
|
|
302
|
+
fallbackSrcType: singleContentSource.getContentSourceType(),
|
|
303
|
+
fallbackSrcProjectId: singleContentSource.getProjectId()
|
|
304
|
+
}
|
|
305
|
+
: null)
|
|
306
|
+
});
|
|
307
|
+
for (const error of presetResult.errors) {
|
|
308
|
+
this.userLogger.warn(error.message);
|
|
309
|
+
}
|
|
310
|
+
const { presets } = await this.handleConfigAssets({ presets: presetResult.presets });
|
|
311
|
+
return presets;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private async loadAllContentSourcesAndProcessData({ init }: { init: boolean }) {
|
|
315
|
+
this.logger.debug('loadAllContentSourcesAndProcessData', { init });
|
|
242
316
|
|
|
243
317
|
this.contentUpdatesWatchTimer.stopTimer();
|
|
244
318
|
|
|
245
|
-
|
|
246
|
-
const contentSources: CSITypes.ContentSourceInterface[] = (this.rawStackbitConfig?.contentSources ?? []) as CSITypes.ContentSourceInterface[];
|
|
319
|
+
const contentSources = this.stackbitConfig?.contentSources ?? [];
|
|
247
320
|
|
|
248
321
|
const promises = contentSources.map((contentSourceInstance) => {
|
|
249
322
|
return this.loadContentSourceData({ contentSourceInstance, init });
|
|
250
323
|
});
|
|
251
324
|
|
|
252
|
-
const
|
|
253
|
-
const contentSourceDataById: Record<string, ContentSourceData> = _.keyBy(contentSourceDataArr, 'id');
|
|
325
|
+
const contentSourceRawDataArr = await Promise.all(promises);
|
|
254
326
|
|
|
255
327
|
// update all content sources at once to prevent race conditions
|
|
328
|
+
this.contentSourceDataById = await this.processData({
|
|
329
|
+
stackbitConfig: this.stackbitConfig,
|
|
330
|
+
configModels: this.configModels,
|
|
331
|
+
presets: this.presets,
|
|
332
|
+
contentSourceRawDataArr: contentSourceRawDataArr
|
|
333
|
+
});
|
|
256
334
|
this.contentSources = contentSources;
|
|
257
|
-
|
|
335
|
+
|
|
336
|
+
this.contentUpdatesWatchTimer.startTimer();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private async reloadContentSourcesByIdAndProcessData({ contentSourceIds }: { contentSourceIds: string[] }) {
|
|
340
|
+
this.logger.debug('reloadContentSourcesByIdAndProcessData', { contentSourceIds });
|
|
341
|
+
|
|
342
|
+
this.contentUpdatesWatchTimer.stopTimer();
|
|
343
|
+
|
|
344
|
+
const promises = this.contentSources.map(
|
|
345
|
+
(contentSourceInstance): Promise<ContentSourceRawData> => {
|
|
346
|
+
const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
|
|
347
|
+
if (contentSourceIds.includes(contentSourceId)) {
|
|
348
|
+
return this.loadContentSourceData({
|
|
349
|
+
contentSourceInstance: contentSourceInstance,
|
|
350
|
+
init: false
|
|
351
|
+
});
|
|
352
|
+
} else {
|
|
353
|
+
return Promise.resolve(_.omit(this.contentSourceDataById[contentSourceId], ['models', 'modelMap', 'documents', 'documentMap']));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const contentSourceRawDataArr = await Promise.all(promises);
|
|
359
|
+
|
|
360
|
+
this.contentSourceDataById = await this.processData({
|
|
361
|
+
stackbitConfig: this.stackbitConfig,
|
|
362
|
+
configModels: this.configModels,
|
|
363
|
+
presets: this.presets,
|
|
364
|
+
contentSourceRawDataArr: contentSourceRawDataArr
|
|
365
|
+
});
|
|
258
366
|
|
|
259
367
|
this.contentUpdatesWatchTimer.startTimer();
|
|
260
368
|
}
|
|
@@ -265,7 +373,7 @@ export class ContentStore {
|
|
|
265
373
|
}: {
|
|
266
374
|
contentSourceInstance: CSITypes.ContentSourceInterface;
|
|
267
375
|
init: boolean;
|
|
268
|
-
}): Promise<
|
|
376
|
+
}): Promise<ContentSourceRawData> {
|
|
269
377
|
const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
|
|
270
378
|
this.logger.debug('loadContentSourceData', { contentSourceId, init });
|
|
271
379
|
|
|
@@ -283,98 +391,31 @@ export class ContentStore {
|
|
|
283
391
|
await contentSourceInstance.reset();
|
|
284
392
|
}
|
|
285
393
|
|
|
286
|
-
// TODO: introduce optimization: don't fetch content source models,
|
|
287
|
-
// documents, assets if only stackbitConfig was changed
|
|
288
|
-
|
|
289
394
|
const csiModels = await contentSourceInstance.getModels();
|
|
290
|
-
const
|
|
291
|
-
const defaultLocaleCode = locales?.find((locale) => locale.default)?.code;
|
|
292
|
-
|
|
293
|
-
// for older versions of stackbit, it uses models to extend content source models
|
|
294
|
-
let modelsNoImage: Exclude<Model, ImageModel>[] = [];
|
|
295
|
-
let imageModel: ImageModel | undefined;
|
|
296
|
-
if (this.rawStackbitConfig) {
|
|
297
|
-
const result = await extendConfig({
|
|
298
|
-
config: this.rawStackbitConfig,
|
|
299
|
-
externalModels: csiModels
|
|
300
|
-
});
|
|
301
|
-
for (const error of result?.errors ?? []) {
|
|
302
|
-
this.userLogger.warn(error.message);
|
|
303
|
-
}
|
|
304
|
-
const config = await this.handleConfigAssets(result.config);
|
|
305
|
-
const modelsWithImageModel = config?.models ?? [];
|
|
306
|
-
const imageModelIndex = modelsWithImageModel.findIndex((model) => isImageModel(model));
|
|
307
|
-
if (imageModelIndex > -1) {
|
|
308
|
-
imageModel = modelsWithImageModel[imageModelIndex] as ImageModel;
|
|
309
|
-
modelsWithImageModel.splice(imageModelIndex, 1);
|
|
310
|
-
}
|
|
311
|
-
modelsNoImage = modelsWithImageModel as Exclude<Model, ImageModel>[];
|
|
312
|
-
|
|
313
|
-
// TODO: load presets externally from config, and create additional map
|
|
314
|
-
// that maps presetIds by model name instead of storing that map inside every model
|
|
315
|
-
|
|
316
|
-
// Augment presets with srcType and srcProjectId if they don't exist
|
|
317
|
-
this.presets = _.reduce(
|
|
318
|
-
Object.keys(config?.presets ?? {}),
|
|
319
|
-
(accum: Record<string, Preset>, presetId) => {
|
|
320
|
-
const preset = config?.presets?.[presetId];
|
|
321
|
-
_.set(accum, [presetId], {
|
|
322
|
-
...preset,
|
|
323
|
-
srcType: preset?.srcType ?? contentSourceInstance.getContentSourceType(),
|
|
324
|
-
srcProjectId: preset?.srcProjectId ?? contentSourceInstance.getProjectId()
|
|
325
|
-
});
|
|
326
|
-
return accum;
|
|
327
|
-
},
|
|
328
|
-
{}
|
|
329
|
-
);
|
|
330
|
-
}
|
|
395
|
+
const csiModelMap = _.keyBy(csiModels, 'name');
|
|
331
396
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const srcProjectId = contentSourceInstance.getProjectId();
|
|
335
|
-
const modelsWithSource = modelsNoImage.map((model) => ({
|
|
336
|
-
srcType,
|
|
337
|
-
srcProjectId,
|
|
338
|
-
...model
|
|
339
|
-
}));
|
|
340
|
-
const mappedModels = this.rawStackbitConfig.mapModels({
|
|
341
|
-
models: modelsWithSource
|
|
342
|
-
});
|
|
343
|
-
modelsNoImage = mappedModels.map((model) => {
|
|
344
|
-
const { srcType, srcProjectId, ...rest } = model;
|
|
345
|
-
return rest;
|
|
346
|
-
});
|
|
347
|
-
}
|
|
397
|
+
const locales = await contentSourceInstance.getLocales();
|
|
398
|
+
const defaultLocaleCode = locales?.find((locale) => locale.default)?.code;
|
|
348
399
|
|
|
349
|
-
const models: Model[] = imageModel ? [...modelsNoImage, imageModel] : modelsNoImage;
|
|
350
|
-
const modelMap = _.keyBy(models, 'name');
|
|
351
|
-
const csiModelMap = _.keyBy(csiModels, 'name');
|
|
352
400
|
const csiDocuments = await contentSourceInstance.getDocuments({ modelMap: csiModelMap });
|
|
353
401
|
const csiAssets = await contentSourceInstance.getAssets();
|
|
354
402
|
const csiDocumentMap = _.keyBy(csiDocuments, 'id');
|
|
355
403
|
const csiAssetMap = _.keyBy(csiAssets, 'id');
|
|
356
404
|
|
|
357
|
-
const contentStoreDocuments = mapCSIDocumentsToStoreDocuments({
|
|
358
|
-
csiDocuments,
|
|
359
|
-
contentSourceInstance,
|
|
360
|
-
modelMap,
|
|
361
|
-
defaultLocaleCode
|
|
362
|
-
});
|
|
363
405
|
const contentStoreAssets = mapCSIAssetsToStoreAssets({
|
|
364
406
|
csiAssets,
|
|
365
407
|
contentSourceInstance,
|
|
366
408
|
defaultLocaleCode
|
|
367
409
|
});
|
|
368
|
-
const documentMap = _.keyBy(contentStoreDocuments, 'srcObjectId');
|
|
369
410
|
const assetMap = _.keyBy(contentStoreAssets, 'srcObjectId');
|
|
370
411
|
|
|
371
412
|
this.logger.debug('loaded content source data', {
|
|
372
413
|
contentSourceId,
|
|
373
|
-
locales,
|
|
374
414
|
defaultLocaleCode,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
415
|
+
localesCount: locales.length,
|
|
416
|
+
modelCount: csiModels.length,
|
|
417
|
+
documentCount: csiDocuments.length,
|
|
418
|
+
assetCount: csiAssets.length
|
|
378
419
|
});
|
|
379
420
|
|
|
380
421
|
contentSourceInstance.startWatchingContentUpdates({
|
|
@@ -394,28 +435,22 @@ export class ContentStore {
|
|
|
394
435
|
},
|
|
395
436
|
onSchemaChange: async () => {
|
|
396
437
|
this.logger.debug('content source called onSchemaChange', { contentSourceId });
|
|
397
|
-
this.
|
|
398
|
-
contentSourceInstance: contentSourceInstance,
|
|
399
|
-
init: false
|
|
400
|
-
});
|
|
438
|
+
await this.reloadContentSourcesByIdAndProcessData({ contentSourceIds: [contentSourceId] });
|
|
401
439
|
this.onSchemaChangeCallback();
|
|
402
440
|
}
|
|
403
441
|
});
|
|
404
442
|
|
|
405
443
|
return {
|
|
406
444
|
id: contentSourceId,
|
|
407
|
-
|
|
408
|
-
|
|
445
|
+
srcType: contentSourceInstance.getContentSourceType(),
|
|
446
|
+
srcProjectId: contentSourceInstance.getProjectId(),
|
|
409
447
|
instance: contentSourceInstance,
|
|
410
448
|
locales: locales,
|
|
411
449
|
defaultLocaleCode: defaultLocaleCode,
|
|
412
|
-
|
|
413
|
-
modelMap: modelMap,
|
|
450
|
+
csiModels: csiModels,
|
|
414
451
|
csiModelMap: csiModelMap,
|
|
415
452
|
csiDocuments: csiDocuments,
|
|
416
453
|
csiDocumentMap: csiDocumentMap,
|
|
417
|
-
documents: contentStoreDocuments,
|
|
418
|
-
documentMap: documentMap,
|
|
419
454
|
csiAssets: csiAssets,
|
|
420
455
|
csiAssetMap: csiAssetMap,
|
|
421
456
|
assets: contentStoreAssets,
|
|
@@ -458,8 +493,8 @@ export class ContentStore {
|
|
|
458
493
|
}
|
|
459
494
|
|
|
460
495
|
result.deletedDocuments.push({
|
|
461
|
-
srcType: contentSourceData.
|
|
462
|
-
srcProjectId: contentSourceData.
|
|
496
|
+
srcType: contentSourceData.srcType,
|
|
497
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
463
498
|
srcObjectId: docId
|
|
464
499
|
});
|
|
465
500
|
});
|
|
@@ -479,15 +514,43 @@ export class ContentStore {
|
|
|
479
514
|
}
|
|
480
515
|
|
|
481
516
|
result.deletedAssets.push({
|
|
482
|
-
srcType: contentSourceData.
|
|
483
|
-
srcProjectId: contentSourceData.
|
|
517
|
+
srcType: contentSourceData.srcType,
|
|
518
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
484
519
|
srcObjectId: assetId
|
|
485
520
|
});
|
|
486
521
|
});
|
|
487
522
|
|
|
523
|
+
// map csi documents through stackbitConfig.mapDocuments
|
|
524
|
+
let mappedDocs = contentChangeEvent.documents;
|
|
525
|
+
if (this.stackbitConfig?.mapDocuments) {
|
|
526
|
+
const csiDocumentsWithSource = contentChangeEvent.documents.map(
|
|
527
|
+
(csiDocument): CSITypes.DocumentWithSource => ({
|
|
528
|
+
srcType: contentSourceData.srcType,
|
|
529
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
530
|
+
...csiDocument
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const modelsWithSource = contentSourceData.models.map(
|
|
535
|
+
(model): CSITypes.ModelWithSource => {
|
|
536
|
+
return {
|
|
537
|
+
srcType: contentSourceData.srcType,
|
|
538
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
539
|
+
...model
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
mappedDocs =
|
|
545
|
+
this.stackbitConfig?.mapDocuments?.({
|
|
546
|
+
documents: _.cloneDeep(csiDocumentsWithSource),
|
|
547
|
+
models: _.cloneDeep(modelsWithSource)
|
|
548
|
+
}) ?? csiDocumentsWithSource;
|
|
549
|
+
}
|
|
550
|
+
|
|
488
551
|
// map csi documents and assets to content store documents and assets
|
|
489
552
|
const documents = mapCSIDocumentsToStoreDocuments({
|
|
490
|
-
csiDocuments:
|
|
553
|
+
csiDocuments: mappedDocs,
|
|
491
554
|
contentSourceInstance: contentSourceData.instance,
|
|
492
555
|
modelMap: contentSourceData.modelMap,
|
|
493
556
|
defaultLocaleCode: contentSourceData.defaultLocaleCode
|
|
@@ -500,7 +563,7 @@ export class ContentStore {
|
|
|
500
563
|
|
|
501
564
|
// update contentSourceData with new or updated documents and assets
|
|
502
565
|
Object.assign(contentSourceData.csiDocumentMap, _.keyBy(contentChangeEvent.documents, 'id'));
|
|
503
|
-
Object.assign(contentSourceData.
|
|
566
|
+
Object.assign(contentSourceData.csiAssetMap, _.keyBy(contentChangeEvent.assets, 'id'));
|
|
504
567
|
Object.assign(contentSourceData.documentMap, _.keyBy(documents, 'srcObjectId'));
|
|
505
568
|
Object.assign(contentSourceData.assetMap, _.keyBy(assets, 'srcObjectId'));
|
|
506
569
|
|
|
@@ -518,8 +581,8 @@ export class ContentStore {
|
|
|
518
581
|
contentSourceData.csiDocuments.splice(dataIndex, 1, csiDocument);
|
|
519
582
|
}
|
|
520
583
|
result.updatedDocuments.push({
|
|
521
|
-
srcType: contentSourceData.
|
|
522
|
-
srcProjectId: contentSourceData.
|
|
584
|
+
srcType: contentSourceData.srcType,
|
|
585
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
523
586
|
srcObjectId: document.srcObjectId
|
|
524
587
|
});
|
|
525
588
|
}
|
|
@@ -538,8 +601,8 @@ export class ContentStore {
|
|
|
538
601
|
contentSourceData.csiAssets.splice(index, 1, csiAsset);
|
|
539
602
|
}
|
|
540
603
|
result.updatedAssets.push({
|
|
541
|
-
srcType: contentSourceData.
|
|
542
|
-
srcProjectId: contentSourceData.
|
|
604
|
+
srcType: contentSourceData.srcType,
|
|
605
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
543
606
|
srcObjectId: asset.srcObjectId
|
|
544
607
|
});
|
|
545
608
|
}
|
|
@@ -547,13 +610,95 @@ export class ContentStore {
|
|
|
547
610
|
return result;
|
|
548
611
|
}
|
|
549
612
|
|
|
550
|
-
|
|
613
|
+
private async processData({
|
|
614
|
+
stackbitConfig,
|
|
615
|
+
configModels,
|
|
616
|
+
presets,
|
|
617
|
+
contentSourceRawDataArr
|
|
618
|
+
}: {
|
|
619
|
+
stackbitConfig: Config | null;
|
|
620
|
+
configModels: Model[];
|
|
621
|
+
presets: Record<string, Preset>;
|
|
622
|
+
contentSourceRawDataArr: ContentSourceRawData[];
|
|
623
|
+
}): Promise<Record<string, ContentSourceData>> {
|
|
624
|
+
const modelsWithSource = contentSourceRawDataArr.reduce((accum: CSITypes.ModelWithSource[], csData) => {
|
|
625
|
+
const mergedModels = mergeConfigModelsWithExternalModels({
|
|
626
|
+
configModels: configModels,
|
|
627
|
+
externalModels: csData.csiModels
|
|
628
|
+
});
|
|
629
|
+
const modelsWithSource = mergedModels.map(
|
|
630
|
+
(model): CSITypes.ModelWithSource => {
|
|
631
|
+
return {
|
|
632
|
+
srcType: csData.srcType,
|
|
633
|
+
srcProjectId: csData.id,
|
|
634
|
+
...model
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
);
|
|
638
|
+
return accum.concat(modelsWithSource);
|
|
639
|
+
}, []);
|
|
640
|
+
|
|
641
|
+
// TODO: Is there a better way than deep cloning objects before passing them to user methods?
|
|
642
|
+
// Not cloning mutable objects will break the internal state if user mutates the objects.
|
|
643
|
+
const mappedModels = stackbitConfig?.mapModels?.({ models: _.cloneDeep(modelsWithSource) }) ?? modelsWithSource;
|
|
644
|
+
const normalizedModels = normalizeModels({ models: mappedModels, logger: this.userLogger });
|
|
645
|
+
const validatedModels = validateModels({ models: normalizedModels, logger: this.userLogger });
|
|
646
|
+
const modelsWithPresetsIds = extendModelsWithPresetsIds({ models: validatedModels, presets });
|
|
647
|
+
const { models } = await this.handleConfigAssets({ models: modelsWithPresetsIds });
|
|
648
|
+
|
|
649
|
+
const csiDocumentsWithSource = contentSourceRawDataArr.reduce((accum: CSITypes.DocumentWithSource[], csData) => {
|
|
650
|
+
const csiDocumentsWithSource = csData.csiDocuments.map(
|
|
651
|
+
(csiDocument): CSITypes.DocumentWithSource => ({
|
|
652
|
+
srcType: csData.srcType,
|
|
653
|
+
srcProjectId: csData.id,
|
|
654
|
+
...csiDocument
|
|
655
|
+
})
|
|
656
|
+
);
|
|
657
|
+
return accum.concat(csiDocumentsWithSource);
|
|
658
|
+
}, []);
|
|
659
|
+
|
|
660
|
+
// TODO: Is there a better way than deep cloning objects before passing them to user methods?
|
|
661
|
+
// Not cloning mutable objects will break the internal state if user mutates the objects.
|
|
662
|
+
const mappedDocs =
|
|
663
|
+
stackbitConfig?.mapDocuments?.({
|
|
664
|
+
documents: _.cloneDeep(csiDocumentsWithSource),
|
|
665
|
+
models: _.cloneDeep(models)
|
|
666
|
+
}) ?? csiDocumentsWithSource;
|
|
667
|
+
|
|
668
|
+
const modelMapByContentSource = groupModelsByContentSource({ models: models });
|
|
669
|
+
const documentMapByContentSource = groupDocumentsByContentSource({ documents: mappedDocs });
|
|
670
|
+
|
|
671
|
+
const contentSourceDataArr = contentSourceRawDataArr.map(
|
|
672
|
+
(csData): ContentSourceData => {
|
|
673
|
+
const modelMap = _.get(modelMapByContentSource, [csData.srcType, csData.id], {});
|
|
674
|
+
const mappedCSIDocuments = _.get(documentMapByContentSource, [csData.srcType, csData.id]);
|
|
675
|
+
const documents = mapCSIDocumentsToStoreDocuments({
|
|
676
|
+
csiDocuments: mappedCSIDocuments,
|
|
677
|
+
contentSourceInstance: csData.instance,
|
|
678
|
+
defaultLocaleCode: csData.defaultLocaleCode,
|
|
679
|
+
modelMap: modelMap
|
|
680
|
+
});
|
|
681
|
+
return {
|
|
682
|
+
...csData,
|
|
683
|
+
models: Object.values(modelMap),
|
|
684
|
+
modelMap,
|
|
685
|
+
documents,
|
|
686
|
+
documentMap: _.keyBy(documents, 'srcObjectId')
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
return _.keyBy(contentSourceDataArr, 'id');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
getModels(): Record<string, Record<string, Record<string, Model | ImageModel>>> {
|
|
551
695
|
return _.reduce(
|
|
552
696
|
this.contentSourceDataById,
|
|
553
|
-
(result: Record<string, Record<string, Record<string, Model>>>, contentSourceData) => {
|
|
697
|
+
(result: Record<string, Record<string, Record<string, Model | ImageModel>>>, contentSourceData) => {
|
|
554
698
|
const contentSourceType = contentSourceData.instance.getContentSourceType();
|
|
555
699
|
const srcProjectId = contentSourceData.instance.getProjectId();
|
|
556
700
|
_.set(result, [contentSourceType, srcProjectId], contentSourceData.modelMap);
|
|
701
|
+
_.set(result, [contentSourceType, srcProjectId, '__image_model'], IMAGE_MODEL);
|
|
557
702
|
return result;
|
|
558
703
|
},
|
|
559
704
|
{}
|
|
@@ -601,8 +746,8 @@ export class ContentStore {
|
|
|
601
746
|
return reducePromise(
|
|
602
747
|
contentSourceDataArr,
|
|
603
748
|
async (accum: ContentStoreTypes.HasAccessResult, contentSourceData) => {
|
|
604
|
-
const srcType = contentSourceData.
|
|
605
|
-
const srcProjectId = contentSourceData.
|
|
749
|
+
const srcType = contentSourceData.srcType;
|
|
750
|
+
const srcProjectId = contentSourceData.srcProjectId;
|
|
606
751
|
const userContext = getUserContextForSrcType(srcType, user);
|
|
607
752
|
let result = await contentSourceData.instance.hasAccess({ userContext });
|
|
608
753
|
// backwards compatibility with older CSI version
|
|
@@ -1286,15 +1431,15 @@ export class ContentStore {
|
|
|
1286
1431
|
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
1287
1432
|
locale = locale ?? contentSourceData.defaultLocaleCode;
|
|
1288
1433
|
const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
|
|
1289
|
-
const userContext = getUserContextForSrcType(contentSourceData.
|
|
1434
|
+
const userContext = getUserContextForSrcType(contentSourceData.srcType, user);
|
|
1290
1435
|
const internalValidationErrors = internalValidateContent(documents, assets, contentSourceData);
|
|
1291
1436
|
const validationResult = await contentSourceData.instance.validateDocuments({ documents, assets, locale, userContext });
|
|
1292
1437
|
errors = errors.concat(
|
|
1293
1438
|
internalValidationErrors,
|
|
1294
1439
|
validationResult.errors.map((validationError) => ({
|
|
1295
1440
|
message: validationError.message,
|
|
1296
|
-
srcType: contentSourceData.
|
|
1297
|
-
srcProjectId: contentSourceData.
|
|
1441
|
+
srcType: contentSourceData.srcType,
|
|
1442
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
1298
1443
|
srcObjectType: validationError.objectType,
|
|
1299
1444
|
srcObjectId: validationError.objectId,
|
|
1300
1445
|
fieldPath: validationError.fieldPath,
|
|
@@ -1344,7 +1489,7 @@ export class ContentStore {
|
|
|
1344
1489
|
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
1345
1490
|
|
|
1346
1491
|
documents.push(...contentSourceData.documents);
|
|
1347
|
-
_.set(schema, [contentSourceData.
|
|
1492
|
+
_.set(schema, [contentSourceData.srcType, contentSourceData.srcProjectId], contentSourceData.modelMap);
|
|
1348
1493
|
});
|
|
1349
1494
|
|
|
1350
1495
|
return searchDocuments({
|
|
@@ -1360,7 +1505,7 @@ export class ContentStore {
|
|
|
1360
1505
|
const objectsBySourceId = _.groupBy(objects, (object) => getContentSourceId(object.srcType, object.srcProjectId));
|
|
1361
1506
|
for (const [contentSourceId, contentSourceObjects] of Object.entries(objectsBySourceId)) {
|
|
1362
1507
|
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
1363
|
-
const userContext = getUserContextForSrcType(contentSourceData.
|
|
1508
|
+
const userContext = getUserContextForSrcType(contentSourceData.srcType, user);
|
|
1364
1509
|
const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
|
|
1365
1510
|
await contentSourceData.instance.publishDocuments({ documents, assets, userContext });
|
|
1366
1511
|
}
|
|
@@ -1461,8 +1606,8 @@ function validateDocumentFields(
|
|
|
1461
1606
|
if (!objRef) {
|
|
1462
1607
|
errors.push({
|
|
1463
1608
|
fieldPath,
|
|
1464
|
-
srcType: contentSourceData.
|
|
1465
|
-
srcProjectId: contentSourceData.
|
|
1609
|
+
srcType: contentSourceData.srcType,
|
|
1610
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
1466
1611
|
srcObjectType: documentField.refType,
|
|
1467
1612
|
srcObjectId: document.id,
|
|
1468
1613
|
message: `Can't find referenced ${documentField.refType}: ${documentField.refId}`
|