@stackbit/cms-core 0.1.6 → 0.1.7
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 -7
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +248 -156
- 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 +326 -170
- 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,7 +196,7 @@ export class ContentStore {
|
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
this.logger.debug('keepAlive => contentUpdatesWatchTimer is not running => load content source data');
|
|
182
|
-
await this.
|
|
199
|
+
await this.loadAllContentSourcesAndProcessData({ init: false });
|
|
183
200
|
}
|
|
184
201
|
|
|
185
202
|
/**
|
|
@@ -193,18 +210,55 @@ export class ContentStore {
|
|
|
193
210
|
*/
|
|
194
211
|
async onContentSourceSchemaChange({ contentSourceId }: { contentSourceId: string }) {
|
|
195
212
|
this.logger.debug('onContentSourceSchemaChange', { contentSourceId });
|
|
196
|
-
|
|
197
|
-
this.contentSourceDataById[contentSourceId] = await this.loadContentSourceData({
|
|
198
|
-
contentSourceInstance: contentSourceData.instance,
|
|
199
|
-
init: false
|
|
200
|
-
});
|
|
201
|
-
this.onSchemaChangeCallback();
|
|
213
|
+
await this.reloadContentSourcesByIdAndProcessData({ contentSourceIds: [contentSourceId] });
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
async onFilesChange(updatedFiles: string[]): Promise<{ schemaChanged?: boolean; contentChanges: ContentStoreTypes.ContentChangeResult }> {
|
|
205
217
|
this.logger.debug('onFilesChange');
|
|
206
218
|
|
|
207
|
-
let
|
|
219
|
+
let schemaChanged = false;
|
|
220
|
+
|
|
221
|
+
if (this.stackbitConfig) {
|
|
222
|
+
// Check if any of the yaml models files were changed. If yaml model files were changed,
|
|
223
|
+
// reload them and merge them with models defined in stackbit config.
|
|
224
|
+
const modelDirs = getYamlModelDirs(this.stackbitConfig);
|
|
225
|
+
const yamlModelsChanged = updatedFiles.find((updatedFile) => _.some(modelDirs, (modelDir) => updatedFile.startsWith(modelDir)));
|
|
226
|
+
if (yamlModelsChanged) {
|
|
227
|
+
this.logger.debug('identified change in stackbit model files');
|
|
228
|
+
schemaChanged = true;
|
|
229
|
+
this.yamlModels = await this.loadYamlModels({ stackbitConfig: this.stackbitConfig });
|
|
230
|
+
this.configModels = this.mergeConfigModels(this.stackbitConfig, this.yamlModels);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check if any of the preset files were changed. If presets were changed, reload them.
|
|
234
|
+
const presetDirs = getPresetDirs(this.stackbitConfig);
|
|
235
|
+
const presetsChanged = updatedFiles.find((updatedFile) => _.some(presetDirs, (presetDir) => updatedFile.startsWith(presetDir)));
|
|
236
|
+
if (presetsChanged) {
|
|
237
|
+
this.logger.debug('identified change in stackbit preset files');
|
|
238
|
+
schemaChanged = true;
|
|
239
|
+
this.presets = await this.loadPresets({ stackbitConfig: this.stackbitConfig });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const contentSourceIdsWithChangedSchema: string[] = [];
|
|
244
|
+
const contentChangeEvents: { contentSourceId: string; contentChangeEvent: CSITypes.ContentChangeEvent }[] = [];
|
|
245
|
+
|
|
246
|
+
for (const contentSourceInstance of this.contentSources) {
|
|
247
|
+
const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
|
|
248
|
+
this.logger.debug(`call onFilesChange for contentSource: ${contentSourceId}`);
|
|
249
|
+
const onFilesChangeResult = (await contentSourceInstance.onFilesChange?.({ updatedFiles: updatedFiles })) ?? {};
|
|
250
|
+
this.logger.debug(`schemaChanged: ${onFilesChangeResult.schemaChanged}, has contentChangeEvent: ${!!onFilesChangeResult.contentChangeEvent}`);
|
|
251
|
+
|
|
252
|
+
// if schema is changed, there is no need to return contentChanges
|
|
253
|
+
// because schema changes reloads everything and implies content changes
|
|
254
|
+
if (onFilesChangeResult.schemaChanged) {
|
|
255
|
+
schemaChanged = true;
|
|
256
|
+
contentSourceIdsWithChangedSchema.push(contentSourceId);
|
|
257
|
+
} else if (onFilesChangeResult.contentChangeEvent) {
|
|
258
|
+
contentChangeEvents.push({ contentSourceId, contentChangeEvent: onFilesChangeResult.contentChangeEvent });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
208
262
|
const contentChanges: ContentStoreTypes.ContentChangeResult = {
|
|
209
263
|
updatedDocuments: [],
|
|
210
264
|
updatedAssets: [],
|
|
@@ -212,49 +266,115 @@ export class ContentStore {
|
|
|
212
266
|
deletedAssets: []
|
|
213
267
|
};
|
|
214
268
|
|
|
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) {
|
|
269
|
+
// If the schema was changed, there is no need to accumulate or notify about content changes.
|
|
270
|
+
// The processData will update the store with the latest data. And once the Studio receives
|
|
271
|
+
// the schemaChanged notification it will reload all the models and the documents with their latest state.
|
|
272
|
+
if (schemaChanged) {
|
|
273
|
+
await this.reloadContentSourcesByIdAndProcessData({ contentSourceIds: contentSourceIdsWithChangedSchema });
|
|
274
|
+
} else {
|
|
275
|
+
contentChangeEvents.reduce((contentChanges, { contentSourceId, contentChangeEvent }) => {
|
|
226
276
|
const contentChangeResult = this.onContentChange(contentSourceId, contentChangeEvent);
|
|
227
277
|
contentChanges.updatedDocuments = contentChanges.updatedDocuments.concat(contentChangeResult.updatedDocuments);
|
|
228
278
|
contentChanges.updatedAssets = contentChanges.updatedAssets.concat(contentChangeResult.updatedAssets);
|
|
229
279
|
contentChanges.deletedDocuments = contentChanges.deletedDocuments.concat(contentChangeResult.deletedDocuments);
|
|
230
280
|
contentChanges.deletedAssets = contentChanges.deletedAssets.concat(contentChangeResult.deletedAssets);
|
|
231
|
-
|
|
281
|
+
return contentChanges;
|
|
282
|
+
}, contentChanges);
|
|
232
283
|
}
|
|
233
284
|
|
|
234
285
|
return {
|
|
235
|
-
schemaChanged:
|
|
286
|
+
schemaChanged: schemaChanged,
|
|
236
287
|
contentChanges: contentChanges
|
|
237
288
|
};
|
|
238
289
|
}
|
|
239
290
|
|
|
240
|
-
private async
|
|
241
|
-
|
|
291
|
+
private async loadYamlModels({ stackbitConfig }: { stackbitConfig: Config }): Promise<Model[]> {
|
|
292
|
+
const yamlModelsResult = await loadYamlModelsFromFiles(stackbitConfig);
|
|
293
|
+
for (const error of yamlModelsResult.errors) {
|
|
294
|
+
this.userLogger.warn(error.message);
|
|
295
|
+
}
|
|
296
|
+
return yamlModelsResult.models;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private mergeConfigModels(stackbitConfig: Config | null, modelsFromFiles: Model[]) {
|
|
300
|
+
const configModelsResult = mergeConfigModelsWithModelsFromFiles(stackbitConfig?.models ?? [], modelsFromFiles);
|
|
301
|
+
for (const error of configModelsResult.errors) {
|
|
302
|
+
this.userLogger.warn(error.message);
|
|
303
|
+
}
|
|
304
|
+
return configModelsResult.models;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private async loadPresets({ stackbitConfig }: { stackbitConfig: Config }): Promise<Record<string, Preset>> {
|
|
308
|
+
const contentSources = stackbitConfig?.contentSources ?? [];
|
|
309
|
+
const singleContentSource = contentSources.length === 1 ? contentSources[0] : null;
|
|
310
|
+
const presetResult = await loadPresets({
|
|
311
|
+
config: stackbitConfig,
|
|
312
|
+
...(singleContentSource
|
|
313
|
+
? {
|
|
314
|
+
fallbackSrcType: singleContentSource.getContentSourceType(),
|
|
315
|
+
fallbackSrcProjectId: singleContentSource.getProjectId()
|
|
316
|
+
}
|
|
317
|
+
: null)
|
|
318
|
+
});
|
|
319
|
+
for (const error of presetResult.errors) {
|
|
320
|
+
this.userLogger.warn(error.message);
|
|
321
|
+
}
|
|
322
|
+
const { presets } = await this.handleConfigAssets({ presets: presetResult.presets });
|
|
323
|
+
return presets;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async loadAllContentSourcesAndProcessData({ init }: { init: boolean }) {
|
|
327
|
+
this.logger.debug('loadAllContentSourcesAndProcessData', { init });
|
|
242
328
|
|
|
243
329
|
this.contentUpdatesWatchTimer.stopTimer();
|
|
244
330
|
|
|
245
|
-
|
|
246
|
-
const contentSources: CSITypes.ContentSourceInterface[] = (this.rawStackbitConfig?.contentSources ?? []) as CSITypes.ContentSourceInterface[];
|
|
331
|
+
const contentSources = this.stackbitConfig?.contentSources ?? [];
|
|
247
332
|
|
|
248
333
|
const promises = contentSources.map((contentSourceInstance) => {
|
|
249
334
|
return this.loadContentSourceData({ contentSourceInstance, init });
|
|
250
335
|
});
|
|
251
336
|
|
|
252
|
-
const
|
|
253
|
-
const contentSourceDataById: Record<string, ContentSourceData> = _.keyBy(contentSourceDataArr, 'id');
|
|
337
|
+
const contentSourceRawDataArr = await Promise.all(promises);
|
|
254
338
|
|
|
255
339
|
// update all content sources at once to prevent race conditions
|
|
340
|
+
this.contentSourceDataById = await this.processData({
|
|
341
|
+
stackbitConfig: this.stackbitConfig,
|
|
342
|
+
configModels: this.configModels,
|
|
343
|
+
presets: this.presets,
|
|
344
|
+
contentSourceRawDataArr: contentSourceRawDataArr
|
|
345
|
+
});
|
|
256
346
|
this.contentSources = contentSources;
|
|
257
|
-
|
|
347
|
+
|
|
348
|
+
this.contentUpdatesWatchTimer.startTimer();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private async reloadContentSourcesByIdAndProcessData({ contentSourceIds }: { contentSourceIds: string[] }) {
|
|
352
|
+
this.logger.debug('reloadContentSourcesByIdAndProcessData', { contentSourceIds });
|
|
353
|
+
|
|
354
|
+
this.contentUpdatesWatchTimer.stopTimer();
|
|
355
|
+
|
|
356
|
+
const promises = this.contentSources.map(
|
|
357
|
+
(contentSourceInstance): Promise<ContentSourceRawData> => {
|
|
358
|
+
const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
|
|
359
|
+
if (contentSourceIds.includes(contentSourceId)) {
|
|
360
|
+
return this.loadContentSourceData({
|
|
361
|
+
contentSourceInstance: contentSourceInstance,
|
|
362
|
+
init: false
|
|
363
|
+
});
|
|
364
|
+
} else {
|
|
365
|
+
return Promise.resolve(_.omit(this.contentSourceDataById[contentSourceId], ['models', 'modelMap', 'documents', 'documentMap']));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const contentSourceRawDataArr = await Promise.all(promises);
|
|
371
|
+
|
|
372
|
+
this.contentSourceDataById = await this.processData({
|
|
373
|
+
stackbitConfig: this.stackbitConfig,
|
|
374
|
+
configModels: this.configModels,
|
|
375
|
+
presets: this.presets,
|
|
376
|
+
contentSourceRawDataArr: contentSourceRawDataArr
|
|
377
|
+
});
|
|
258
378
|
|
|
259
379
|
this.contentUpdatesWatchTimer.startTimer();
|
|
260
380
|
}
|
|
@@ -265,7 +385,7 @@ export class ContentStore {
|
|
|
265
385
|
}: {
|
|
266
386
|
contentSourceInstance: CSITypes.ContentSourceInterface;
|
|
267
387
|
init: boolean;
|
|
268
|
-
}): Promise<
|
|
388
|
+
}): Promise<ContentSourceRawData> {
|
|
269
389
|
const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
|
|
270
390
|
this.logger.debug('loadContentSourceData', { contentSourceId, init });
|
|
271
391
|
|
|
@@ -283,98 +403,31 @@ export class ContentStore {
|
|
|
283
403
|
await contentSourceInstance.reset();
|
|
284
404
|
}
|
|
285
405
|
|
|
286
|
-
// TODO: introduce optimization: don't fetch content source models,
|
|
287
|
-
// documents, assets if only stackbitConfig was changed
|
|
288
|
-
|
|
289
406
|
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
|
-
}
|
|
407
|
+
const csiModelMap = _.keyBy(csiModels, 'name');
|
|
331
408
|
|
|
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
|
-
}
|
|
409
|
+
const locales = await contentSourceInstance.getLocales();
|
|
410
|
+
const defaultLocaleCode = locales?.find((locale) => locale.default)?.code;
|
|
348
411
|
|
|
349
|
-
const models: Model[] = imageModel ? [...modelsNoImage, imageModel] : modelsNoImage;
|
|
350
|
-
const modelMap = _.keyBy(models, 'name');
|
|
351
|
-
const csiModelMap = _.keyBy(csiModels, 'name');
|
|
352
412
|
const csiDocuments = await contentSourceInstance.getDocuments({ modelMap: csiModelMap });
|
|
353
413
|
const csiAssets = await contentSourceInstance.getAssets();
|
|
354
414
|
const csiDocumentMap = _.keyBy(csiDocuments, 'id');
|
|
355
415
|
const csiAssetMap = _.keyBy(csiAssets, 'id');
|
|
356
416
|
|
|
357
|
-
const contentStoreDocuments = mapCSIDocumentsToStoreDocuments({
|
|
358
|
-
csiDocuments,
|
|
359
|
-
contentSourceInstance,
|
|
360
|
-
modelMap,
|
|
361
|
-
defaultLocaleCode
|
|
362
|
-
});
|
|
363
417
|
const contentStoreAssets = mapCSIAssetsToStoreAssets({
|
|
364
418
|
csiAssets,
|
|
365
419
|
contentSourceInstance,
|
|
366
420
|
defaultLocaleCode
|
|
367
421
|
});
|
|
368
|
-
const documentMap = _.keyBy(contentStoreDocuments, 'srcObjectId');
|
|
369
422
|
const assetMap = _.keyBy(contentStoreAssets, 'srcObjectId');
|
|
370
423
|
|
|
371
424
|
this.logger.debug('loaded content source data', {
|
|
372
425
|
contentSourceId,
|
|
373
|
-
locales,
|
|
374
426
|
defaultLocaleCode,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
427
|
+
localesCount: locales.length,
|
|
428
|
+
modelCount: csiModels.length,
|
|
429
|
+
documentCount: csiDocuments.length,
|
|
430
|
+
assetCount: csiAssets.length
|
|
378
431
|
});
|
|
379
432
|
|
|
380
433
|
contentSourceInstance.startWatchingContentUpdates({
|
|
@@ -394,28 +447,21 @@ export class ContentStore {
|
|
|
394
447
|
},
|
|
395
448
|
onSchemaChange: async () => {
|
|
396
449
|
this.logger.debug('content source called onSchemaChange', { contentSourceId });
|
|
397
|
-
this.
|
|
398
|
-
contentSourceInstance: contentSourceInstance,
|
|
399
|
-
init: false
|
|
400
|
-
});
|
|
401
|
-
this.onSchemaChangeCallback();
|
|
450
|
+
await this.reloadContentSourcesByIdAndProcessData({ contentSourceIds: [contentSourceId] });
|
|
402
451
|
}
|
|
403
452
|
});
|
|
404
453
|
|
|
405
454
|
return {
|
|
406
455
|
id: contentSourceId,
|
|
407
|
-
|
|
408
|
-
|
|
456
|
+
srcType: contentSourceInstance.getContentSourceType(),
|
|
457
|
+
srcProjectId: contentSourceInstance.getProjectId(),
|
|
409
458
|
instance: contentSourceInstance,
|
|
410
459
|
locales: locales,
|
|
411
460
|
defaultLocaleCode: defaultLocaleCode,
|
|
412
|
-
|
|
413
|
-
modelMap: modelMap,
|
|
461
|
+
csiModels: csiModels,
|
|
414
462
|
csiModelMap: csiModelMap,
|
|
415
463
|
csiDocuments: csiDocuments,
|
|
416
464
|
csiDocumentMap: csiDocumentMap,
|
|
417
|
-
documents: contentStoreDocuments,
|
|
418
|
-
documentMap: documentMap,
|
|
419
465
|
csiAssets: csiAssets,
|
|
420
466
|
csiAssetMap: csiAssetMap,
|
|
421
467
|
assets: contentStoreAssets,
|
|
@@ -458,8 +504,8 @@ export class ContentStore {
|
|
|
458
504
|
}
|
|
459
505
|
|
|
460
506
|
result.deletedDocuments.push({
|
|
461
|
-
srcType: contentSourceData.
|
|
462
|
-
srcProjectId: contentSourceData.
|
|
507
|
+
srcType: contentSourceData.srcType,
|
|
508
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
463
509
|
srcObjectId: docId
|
|
464
510
|
});
|
|
465
511
|
});
|
|
@@ -479,15 +525,43 @@ export class ContentStore {
|
|
|
479
525
|
}
|
|
480
526
|
|
|
481
527
|
result.deletedAssets.push({
|
|
482
|
-
srcType: contentSourceData.
|
|
483
|
-
srcProjectId: contentSourceData.
|
|
528
|
+
srcType: contentSourceData.srcType,
|
|
529
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
484
530
|
srcObjectId: assetId
|
|
485
531
|
});
|
|
486
532
|
});
|
|
487
533
|
|
|
534
|
+
// map csi documents through stackbitConfig.mapDocuments
|
|
535
|
+
let mappedDocs = contentChangeEvent.documents;
|
|
536
|
+
if (this.stackbitConfig?.mapDocuments) {
|
|
537
|
+
const csiDocumentsWithSource = contentChangeEvent.documents.map(
|
|
538
|
+
(csiDocument): CSITypes.DocumentWithSource => ({
|
|
539
|
+
srcType: contentSourceData.srcType,
|
|
540
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
541
|
+
...csiDocument
|
|
542
|
+
})
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const modelsWithSource = contentSourceData.models.map(
|
|
546
|
+
(model): CSITypes.ModelWithSource => {
|
|
547
|
+
return {
|
|
548
|
+
srcType: contentSourceData.srcType,
|
|
549
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
550
|
+
...model
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
mappedDocs =
|
|
556
|
+
this.stackbitConfig?.mapDocuments?.({
|
|
557
|
+
documents: _.cloneDeep(csiDocumentsWithSource),
|
|
558
|
+
models: _.cloneDeep(modelsWithSource)
|
|
559
|
+
}) ?? csiDocumentsWithSource;
|
|
560
|
+
}
|
|
561
|
+
|
|
488
562
|
// map csi documents and assets to content store documents and assets
|
|
489
563
|
const documents = mapCSIDocumentsToStoreDocuments({
|
|
490
|
-
csiDocuments:
|
|
564
|
+
csiDocuments: mappedDocs,
|
|
491
565
|
contentSourceInstance: contentSourceData.instance,
|
|
492
566
|
modelMap: contentSourceData.modelMap,
|
|
493
567
|
defaultLocaleCode: contentSourceData.defaultLocaleCode
|
|
@@ -500,7 +574,7 @@ export class ContentStore {
|
|
|
500
574
|
|
|
501
575
|
// update contentSourceData with new or updated documents and assets
|
|
502
576
|
Object.assign(contentSourceData.csiDocumentMap, _.keyBy(contentChangeEvent.documents, 'id'));
|
|
503
|
-
Object.assign(contentSourceData.
|
|
577
|
+
Object.assign(contentSourceData.csiAssetMap, _.keyBy(contentChangeEvent.assets, 'id'));
|
|
504
578
|
Object.assign(contentSourceData.documentMap, _.keyBy(documents, 'srcObjectId'));
|
|
505
579
|
Object.assign(contentSourceData.assetMap, _.keyBy(assets, 'srcObjectId'));
|
|
506
580
|
|
|
@@ -518,8 +592,8 @@ export class ContentStore {
|
|
|
518
592
|
contentSourceData.csiDocuments.splice(dataIndex, 1, csiDocument);
|
|
519
593
|
}
|
|
520
594
|
result.updatedDocuments.push({
|
|
521
|
-
srcType: contentSourceData.
|
|
522
|
-
srcProjectId: contentSourceData.
|
|
595
|
+
srcType: contentSourceData.srcType,
|
|
596
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
523
597
|
srcObjectId: document.srcObjectId
|
|
524
598
|
});
|
|
525
599
|
}
|
|
@@ -538,8 +612,8 @@ export class ContentStore {
|
|
|
538
612
|
contentSourceData.csiAssets.splice(index, 1, csiAsset);
|
|
539
613
|
}
|
|
540
614
|
result.updatedAssets.push({
|
|
541
|
-
srcType: contentSourceData.
|
|
542
|
-
srcProjectId: contentSourceData.
|
|
615
|
+
srcType: contentSourceData.srcType,
|
|
616
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
543
617
|
srcObjectId: asset.srcObjectId
|
|
544
618
|
});
|
|
545
619
|
}
|
|
@@ -547,13 +621,95 @@ export class ContentStore {
|
|
|
547
621
|
return result;
|
|
548
622
|
}
|
|
549
623
|
|
|
550
|
-
|
|
624
|
+
private async processData({
|
|
625
|
+
stackbitConfig,
|
|
626
|
+
configModels,
|
|
627
|
+
presets,
|
|
628
|
+
contentSourceRawDataArr
|
|
629
|
+
}: {
|
|
630
|
+
stackbitConfig: Config | null;
|
|
631
|
+
configModels: Model[];
|
|
632
|
+
presets: Record<string, Preset>;
|
|
633
|
+
contentSourceRawDataArr: ContentSourceRawData[];
|
|
634
|
+
}): Promise<Record<string, ContentSourceData>> {
|
|
635
|
+
const modelsWithSource = contentSourceRawDataArr.reduce((accum: CSITypes.ModelWithSource[], csData) => {
|
|
636
|
+
const mergedModels = mergeConfigModelsWithExternalModels({
|
|
637
|
+
configModels: configModels,
|
|
638
|
+
externalModels: csData.csiModels
|
|
639
|
+
});
|
|
640
|
+
const modelsWithSource = mergedModels.map(
|
|
641
|
+
(model): CSITypes.ModelWithSource => {
|
|
642
|
+
return {
|
|
643
|
+
srcType: csData.srcType,
|
|
644
|
+
srcProjectId: csData.id,
|
|
645
|
+
...model
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
);
|
|
649
|
+
return accum.concat(modelsWithSource);
|
|
650
|
+
}, []);
|
|
651
|
+
|
|
652
|
+
// TODO: Is there a better way than deep cloning objects before passing them to user methods?
|
|
653
|
+
// Not cloning mutable objects will break the internal state if user mutates the objects.
|
|
654
|
+
const mappedModels = stackbitConfig?.mapModels?.({ models: _.cloneDeep(modelsWithSource) }) ?? modelsWithSource;
|
|
655
|
+
const normalizedModels = normalizeModels({ models: mappedModels, logger: this.userLogger });
|
|
656
|
+
const validatedModels = validateModels({ models: normalizedModels, logger: this.userLogger });
|
|
657
|
+
const modelsWithPresetsIds = extendModelsWithPresetsIds({ models: validatedModels, presets });
|
|
658
|
+
const { models } = await this.handleConfigAssets({ models: modelsWithPresetsIds });
|
|
659
|
+
|
|
660
|
+
const csiDocumentsWithSource = contentSourceRawDataArr.reduce((accum: CSITypes.DocumentWithSource[], csData) => {
|
|
661
|
+
const csiDocumentsWithSource = csData.csiDocuments.map(
|
|
662
|
+
(csiDocument): CSITypes.DocumentWithSource => ({
|
|
663
|
+
srcType: csData.srcType,
|
|
664
|
+
srcProjectId: csData.id,
|
|
665
|
+
...csiDocument
|
|
666
|
+
})
|
|
667
|
+
);
|
|
668
|
+
return accum.concat(csiDocumentsWithSource);
|
|
669
|
+
}, []);
|
|
670
|
+
|
|
671
|
+
// TODO: Is there a better way than deep cloning objects before passing them to user methods?
|
|
672
|
+
// Not cloning mutable objects will break the internal state if user mutates the objects.
|
|
673
|
+
const mappedDocs =
|
|
674
|
+
stackbitConfig?.mapDocuments?.({
|
|
675
|
+
documents: _.cloneDeep(csiDocumentsWithSource),
|
|
676
|
+
models: _.cloneDeep(models)
|
|
677
|
+
}) ?? csiDocumentsWithSource;
|
|
678
|
+
|
|
679
|
+
const modelMapByContentSource = groupModelsByContentSource({ models: models });
|
|
680
|
+
const documentMapByContentSource = groupDocumentsByContentSource({ documents: mappedDocs });
|
|
681
|
+
|
|
682
|
+
const contentSourceDataArr = contentSourceRawDataArr.map(
|
|
683
|
+
(csData): ContentSourceData => {
|
|
684
|
+
const modelMap = _.get(modelMapByContentSource, [csData.srcType, csData.id], {});
|
|
685
|
+
const mappedCSIDocuments = _.get(documentMapByContentSource, [csData.srcType, csData.id]);
|
|
686
|
+
const documents = mapCSIDocumentsToStoreDocuments({
|
|
687
|
+
csiDocuments: mappedCSIDocuments,
|
|
688
|
+
contentSourceInstance: csData.instance,
|
|
689
|
+
defaultLocaleCode: csData.defaultLocaleCode,
|
|
690
|
+
modelMap: modelMap
|
|
691
|
+
});
|
|
692
|
+
return {
|
|
693
|
+
...csData,
|
|
694
|
+
models: Object.values(modelMap),
|
|
695
|
+
modelMap,
|
|
696
|
+
documents,
|
|
697
|
+
documentMap: _.keyBy(documents, 'srcObjectId')
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
return _.keyBy(contentSourceDataArr, 'id');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
getModels(): Record<string, Record<string, Record<string, Model | ImageModel>>> {
|
|
551
706
|
return _.reduce(
|
|
552
707
|
this.contentSourceDataById,
|
|
553
|
-
(result: Record<string, Record<string, Record<string, Model>>>, contentSourceData) => {
|
|
708
|
+
(result: Record<string, Record<string, Record<string, Model | ImageModel>>>, contentSourceData) => {
|
|
554
709
|
const contentSourceType = contentSourceData.instance.getContentSourceType();
|
|
555
710
|
const srcProjectId = contentSourceData.instance.getProjectId();
|
|
556
711
|
_.set(result, [contentSourceType, srcProjectId], contentSourceData.modelMap);
|
|
712
|
+
_.set(result, [contentSourceType, srcProjectId, '__image_model'], IMAGE_MODEL);
|
|
557
713
|
return result;
|
|
558
714
|
},
|
|
559
715
|
{}
|
|
@@ -601,8 +757,8 @@ export class ContentStore {
|
|
|
601
757
|
return reducePromise(
|
|
602
758
|
contentSourceDataArr,
|
|
603
759
|
async (accum: ContentStoreTypes.HasAccessResult, contentSourceData) => {
|
|
604
|
-
const srcType = contentSourceData.
|
|
605
|
-
const srcProjectId = contentSourceData.
|
|
760
|
+
const srcType = contentSourceData.srcType;
|
|
761
|
+
const srcProjectId = contentSourceData.srcProjectId;
|
|
606
762
|
const userContext = getUserContextForSrcType(srcType, user);
|
|
607
763
|
let result = await contentSourceData.instance.hasAccess({ userContext });
|
|
608
764
|
// backwards compatibility with older CSI version
|
|
@@ -1286,15 +1442,15 @@ export class ContentStore {
|
|
|
1286
1442
|
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
1287
1443
|
locale = locale ?? contentSourceData.defaultLocaleCode;
|
|
1288
1444
|
const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
|
|
1289
|
-
const userContext = getUserContextForSrcType(contentSourceData.
|
|
1445
|
+
const userContext = getUserContextForSrcType(contentSourceData.srcType, user);
|
|
1290
1446
|
const internalValidationErrors = internalValidateContent(documents, assets, contentSourceData);
|
|
1291
1447
|
const validationResult = await contentSourceData.instance.validateDocuments({ documents, assets, locale, userContext });
|
|
1292
1448
|
errors = errors.concat(
|
|
1293
1449
|
internalValidationErrors,
|
|
1294
1450
|
validationResult.errors.map((validationError) => ({
|
|
1295
1451
|
message: validationError.message,
|
|
1296
|
-
srcType: contentSourceData.
|
|
1297
|
-
srcProjectId: contentSourceData.
|
|
1452
|
+
srcType: contentSourceData.srcType,
|
|
1453
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
1298
1454
|
srcObjectType: validationError.objectType,
|
|
1299
1455
|
srcObjectId: validationError.objectId,
|
|
1300
1456
|
fieldPath: validationError.fieldPath,
|
|
@@ -1344,7 +1500,7 @@ export class ContentStore {
|
|
|
1344
1500
|
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
1345
1501
|
|
|
1346
1502
|
documents.push(...contentSourceData.documents);
|
|
1347
|
-
_.set(schema, [contentSourceData.
|
|
1503
|
+
_.set(schema, [contentSourceData.srcType, contentSourceData.srcProjectId], contentSourceData.modelMap);
|
|
1348
1504
|
});
|
|
1349
1505
|
|
|
1350
1506
|
return searchDocuments({
|
|
@@ -1360,7 +1516,7 @@ export class ContentStore {
|
|
|
1360
1516
|
const objectsBySourceId = _.groupBy(objects, (object) => getContentSourceId(object.srcType, object.srcProjectId));
|
|
1361
1517
|
for (const [contentSourceId, contentSourceObjects] of Object.entries(objectsBySourceId)) {
|
|
1362
1518
|
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
1363
|
-
const userContext = getUserContextForSrcType(contentSourceData.
|
|
1519
|
+
const userContext = getUserContextForSrcType(contentSourceData.srcType, user);
|
|
1364
1520
|
const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
|
|
1365
1521
|
await contentSourceData.instance.publishDocuments({ documents, assets, userContext });
|
|
1366
1522
|
}
|
|
@@ -1461,8 +1617,8 @@ function validateDocumentFields(
|
|
|
1461
1617
|
if (!objRef) {
|
|
1462
1618
|
errors.push({
|
|
1463
1619
|
fieldPath,
|
|
1464
|
-
srcType: contentSourceData.
|
|
1465
|
-
srcProjectId: contentSourceData.
|
|
1620
|
+
srcType: contentSourceData.srcType,
|
|
1621
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
1466
1622
|
srcObjectType: documentField.refType,
|
|
1467
1623
|
srcObjectId: document.id,
|
|
1468
1624
|
message: `Can't find referenced ${documentField.refType}: ${documentField.refId}`
|