@stackbit/cms-core 0.1.14-alpha.0 → 0.1.14
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-types.d.ts.map +1 -1
- package/dist/content-store-utils.d.ts +1 -6
- package/dist/content-store-utils.d.ts.map +1 -1
- package/dist/content-store-utils.js +1 -27
- package/dist/content-store-utils.js.map +1 -1
- package/dist/content-store.d.ts +18 -2
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +237 -102
- package/dist/content-store.js.map +1 -1
- package/dist/utils/preset-utils.d.ts +12 -0
- package/dist/utils/preset-utils.d.ts.map +1 -0
- package/dist/utils/preset-utils.js +92 -0
- package/dist/utils/preset-utils.js.map +1 -0
- package/dist/utils/site-map.d.ts +57 -0
- package/dist/utils/site-map.d.ts.map +1 -0
- package/dist/utils/site-map.js +149 -0
- package/dist/utils/site-map.js.map +1 -0
- package/package.json +5 -5
- package/src/content-store-types.ts +2 -0
- package/src/content-store-utils.ts +0 -33
- package/src/content-store.ts +275 -119
- package/src/utils/preset-utils.ts +94 -0
- package/src/utils/site-map.ts +224 -0
|
@@ -192,36 +192,3 @@ export function getCSIDocumentsAndAssetsFromContentSourceDataByIds(
|
|
|
192
192
|
assets
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
-
|
|
196
|
-
export function getDocumentFieldLabelValueForSiteMapEntry({
|
|
197
|
-
siteMapEntry,
|
|
198
|
-
locale,
|
|
199
|
-
contentSourceDataById
|
|
200
|
-
}: {
|
|
201
|
-
siteMapEntry: SiteMapEntry;
|
|
202
|
-
locale?: string;
|
|
203
|
-
contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
|
|
204
|
-
}): string | null {
|
|
205
|
-
if (!('document' in siteMapEntry)) {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
const contentSourceId = getContentSourceId(siteMapEntry.document.srcType, siteMapEntry.document.srcProjectId);
|
|
209
|
-
const contentSourceData = contentSourceDataById[contentSourceId];
|
|
210
|
-
if (!contentSourceData) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
const labelFieldName = contentSourceData.modelMap[siteMapEntry.document.modelName]?.labelField;
|
|
214
|
-
const document = contentSourceData.documentMap[siteMapEntry.document.id];
|
|
215
|
-
if (!labelFieldName || !document) {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
const labelField = document.fields[labelFieldName];
|
|
219
|
-
if (!labelField) {
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
const localizedLabelField = getDocumentFieldForLocale(labelField, locale);
|
|
223
|
-
if (!localizedLabelField || !('value' in localizedLabelField) || !localizedLabelField.value) {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
return String(localizedLabelField.value);
|
|
227
|
-
}
|
package/src/content-store.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import sanitizeFilename from 'sanitize-filename';
|
|
4
4
|
|
|
5
5
|
import * as CSITypes from '@stackbit/types';
|
|
6
|
-
import {
|
|
6
|
+
import { getLocalizedFieldForLocale, ModelExtension, UserCommandSpawner } from '@stackbit/types';
|
|
7
7
|
import {
|
|
8
8
|
Config,
|
|
9
9
|
extendModelsWithPresetsIds,
|
|
@@ -18,29 +18,34 @@ import {
|
|
|
18
18
|
Preset,
|
|
19
19
|
PresetMap
|
|
20
20
|
} from '@stackbit/sdk';
|
|
21
|
-
import { deferWhileRunning, mapPromise, reducePromise } from '@stackbit/utils';
|
|
21
|
+
import { append, deferWhileRunning, mapPromise, reducePromise } from '@stackbit/utils';
|
|
22
22
|
|
|
23
23
|
import * as ContentStoreTypes from './content-store-types';
|
|
24
24
|
import { Timer } from './utils/timer';
|
|
25
25
|
import { SearchFilter } from './types/search-filter';
|
|
26
26
|
import { searchDocuments } from './utils/search-utils';
|
|
27
27
|
import { mapCSIAssetsToStoreAssets, mapCSIDocumentsToStoreDocuments } from './utils/csi-to-store-docs-converter';
|
|
28
|
-
import { mapStoreDocumentsToCSIDocumentsWithSource } from './utils/store-to-csi-docs-converter';
|
|
29
28
|
import {
|
|
30
29
|
getContentSourceId,
|
|
31
30
|
getContentSourceIdForContentSource,
|
|
32
|
-
getDocumentFieldLabelValueForSiteMapEntry,
|
|
33
31
|
getCSIDocumentsAndAssetsFromContentSourceDataByIds,
|
|
34
32
|
getModelFieldForFieldAtPath,
|
|
35
33
|
getUserContextForSrcType,
|
|
36
34
|
groupDocumentsByContentSource,
|
|
37
35
|
groupModelsByContentSource
|
|
38
36
|
} from './content-store-utils';
|
|
37
|
+
import {
|
|
38
|
+
getSiteMapEntriesFromStackbitConfig,
|
|
39
|
+
updateSiteMapEntriesWithContentChanges,
|
|
40
|
+
getDocumentFieldLabelValueForSiteMapEntry,
|
|
41
|
+
SiteMapEntryGroups
|
|
42
|
+
} from './utils/site-map';
|
|
39
43
|
import { mapAssetsToLocalizedApiImages, mapDocumentsToLocalizedApiObjects, mapStoreAssetsToAPIAssets } from './utils/store-to-api-docs-converter';
|
|
40
44
|
import { convertOperationField, createDocumentRecursively, getCreateDocumentThunk } from './utils/create-update-csi-docs';
|
|
41
45
|
import { mergeObjectWithDocument } from './utils/duplicate-document';
|
|
42
46
|
import { normalizeModels, validateModels } from './utils/model-utils';
|
|
43
47
|
import { IMAGE_MODEL } from './common/common-schema';
|
|
48
|
+
import { getDocumentObjectFromPreset, getPresetFromDocument } from './utils/preset-utils';
|
|
44
49
|
|
|
45
50
|
export type HandleConfigAssets = <T extends Model>({ models, presets }: { models?: T[]; presets?: PresetMap }) => Promise<{ models: T[]; presets: PresetMap }>;
|
|
46
51
|
|
|
@@ -59,6 +64,8 @@ export interface ContentSourceOptions {
|
|
|
59
64
|
type ContentSourceData = ContentStoreTypes.ContentSourceData;
|
|
60
65
|
type ContentSourceRawData = Omit<ContentSourceData, 'models' | 'modelMap' | 'documents' | 'documentMap'>;
|
|
61
66
|
|
|
67
|
+
export const StackbitPresetModelName = 'stackbitPreset';
|
|
68
|
+
|
|
62
69
|
export class ContentStore {
|
|
63
70
|
private readonly logger: ContentStoreTypes.Logger;
|
|
64
71
|
private readonly userLogger: ContentStoreTypes.Logger;
|
|
@@ -71,12 +78,14 @@ export class ContentStore {
|
|
|
71
78
|
private readonly devAppRestartNeeded?: () => void;
|
|
72
79
|
private contentSources: CSITypes.ContentSourceInterface[] = [];
|
|
73
80
|
private contentSourceDataById: Record<string, ContentSourceData> = {};
|
|
81
|
+
private presetsContentSource?: CSITypes.ContentSourceInterface;
|
|
74
82
|
private contentUpdatesWatchTimer: Timer;
|
|
75
83
|
private stackbitConfig: Config | null = null;
|
|
76
84
|
private yamlModels: Model[] = [];
|
|
77
85
|
private configModels: Model[] = [];
|
|
78
86
|
private modelExtensions: ModelExtension[] | null = null;
|
|
79
87
|
private presets: PresetMap = {};
|
|
88
|
+
private siteMapEntryGroups: SiteMapEntryGroups = {};
|
|
80
89
|
|
|
81
90
|
constructor(options: ContentSourceOptions) {
|
|
82
91
|
this.logger = options.logger.createLogger({ label: 'content-store' });
|
|
@@ -133,7 +142,6 @@ export class ContentStore {
|
|
|
133
142
|
this.yamlModels = await this.loadYamlModels({ stackbitConfig });
|
|
134
143
|
this.configModels = this.mergeConfigModels(stackbitConfig.models ?? [], this.yamlModels);
|
|
135
144
|
}
|
|
136
|
-
this.presets = await this.loadPresets({ stackbitConfig });
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
await this.loadContentSourcesAndProcessData({ init: true });
|
|
@@ -211,10 +219,10 @@ export class ContentStore {
|
|
|
211
219
|
// Check if any of the preset files were changed. If presets were changed, reload them.
|
|
212
220
|
const presetDirs = getPresetDirs(this.stackbitConfig);
|
|
213
221
|
const presetsChanged = updatedFiles.find((updatedFile) => _.some(presetDirs, (presetDir) => updatedFile.startsWith(presetDir)));
|
|
214
|
-
if (presetsChanged) {
|
|
222
|
+
if (presetsChanged && !this.usesContentSourcePresets()) {
|
|
215
223
|
this.logger.debug('identified change in stackbit preset files');
|
|
216
224
|
schemaChanged = true;
|
|
217
|
-
this.presets = await this.
|
|
225
|
+
this.presets = await this.loadPresetsFromConfig({ stackbitConfig: this.stackbitConfig });
|
|
218
226
|
}
|
|
219
227
|
}
|
|
220
228
|
|
|
@@ -237,7 +245,7 @@ export class ContentStore {
|
|
|
237
245
|
}
|
|
238
246
|
}
|
|
239
247
|
|
|
240
|
-
|
|
248
|
+
let contentChanges: ContentStoreTypes.ContentChangeResult = {
|
|
241
249
|
updatedDocuments: [],
|
|
242
250
|
updatedAssets: [],
|
|
243
251
|
deletedDocuments: [],
|
|
@@ -250,14 +258,22 @@ export class ContentStore {
|
|
|
250
258
|
if (schemaChanged) {
|
|
251
259
|
await this.loadContentSourcesAndProcessData({ init: false, contentSourceIds: contentSourceIdsWithChangedSchema });
|
|
252
260
|
} else {
|
|
253
|
-
contentChangeEvents.reduce((contentChanges, { contentSourceId, contentChangeEvent }) => {
|
|
261
|
+
contentChanges = contentChangeEvents.reduce((contentChanges, { contentSourceId, contentChangeEvent }) => {
|
|
254
262
|
const contentChangeResult = this.onContentChange(contentSourceId, contentChangeEvent);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
return {
|
|
264
|
+
updatedDocuments: contentChanges.updatedDocuments.concat(contentChangeResult.updatedDocuments),
|
|
265
|
+
updatedAssets: contentChanges.updatedAssets.concat(contentChangeResult.updatedAssets),
|
|
266
|
+
deletedDocuments: contentChanges.deletedDocuments.concat(contentChangeResult.deletedDocuments),
|
|
267
|
+
deletedAssets: contentChanges.deletedAssets.concat(contentChangeResult.deletedAssets)
|
|
268
|
+
};
|
|
260
269
|
}, contentChanges);
|
|
270
|
+
|
|
271
|
+
this.siteMapEntryGroups = await updateSiteMapEntriesWithContentChanges({
|
|
272
|
+
siteMapEntryGroups: this.siteMapEntryGroups,
|
|
273
|
+
contentChanges,
|
|
274
|
+
stackbitConfig: this.stackbitConfig,
|
|
275
|
+
contentSourceDataById: this.contentSourceDataById
|
|
276
|
+
});
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
// TODO: maybe instead of returning object with results
|
|
@@ -284,7 +300,7 @@ export class ContentStore {
|
|
|
284
300
|
return configModelsResult.models;
|
|
285
301
|
}
|
|
286
302
|
|
|
287
|
-
private async
|
|
303
|
+
private async loadPresetsFromConfig({ stackbitConfig }: { stackbitConfig: Config }): Promise<PresetMap> {
|
|
288
304
|
const contentSources = stackbitConfig?.contentSources ?? [];
|
|
289
305
|
const singleContentSource = contentSources.length === 1 ? contentSources[0] : null;
|
|
290
306
|
const presetResult = await loadPresets({
|
|
@@ -303,6 +319,29 @@ export class ContentStore {
|
|
|
303
319
|
return presets;
|
|
304
320
|
}
|
|
305
321
|
|
|
322
|
+
private async loadPresetsFromContentSource(contentSourceData: ContentSourceRawData): Promise<PresetMap> {
|
|
323
|
+
const presets = _.reduce(
|
|
324
|
+
contentSourceData.csiDocuments,
|
|
325
|
+
(result: Record<string, Preset>, csiDocument) => {
|
|
326
|
+
if (csiDocument.modelName === StackbitPresetModelName) {
|
|
327
|
+
const preset = getPresetFromDocument({
|
|
328
|
+
srcType: contentSourceData.srcType,
|
|
329
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
330
|
+
csiDocument,
|
|
331
|
+
csiAssetMap: contentSourceData.csiAssetMap,
|
|
332
|
+
logger: this.logger
|
|
333
|
+
});
|
|
334
|
+
if (preset) {
|
|
335
|
+
result[csiDocument.id] = preset;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
},
|
|
340
|
+
{}
|
|
341
|
+
);
|
|
342
|
+
return presets;
|
|
343
|
+
}
|
|
344
|
+
|
|
306
345
|
/**
|
|
307
346
|
* This function reloads the data of the specified content-sources, while
|
|
308
347
|
* reusing the cached data of the rest of the content-sources, then processes
|
|
@@ -338,6 +377,27 @@ export class ContentStore {
|
|
|
338
377
|
|
|
339
378
|
const contentSourceRawDataArr = await Promise.all(promises);
|
|
340
379
|
|
|
380
|
+
// find first content source that supports presets
|
|
381
|
+
for (let i = 0; i < contentSources.length; i++) {
|
|
382
|
+
const contentSourceDataRaw = contentSourceRawDataArr[i];
|
|
383
|
+
if (contentSourceDataRaw?.csiModelMap?.[StackbitPresetModelName]) {
|
|
384
|
+
this.presetsContentSource = contentSources[i];
|
|
385
|
+
if (this.presetsContentSource) {
|
|
386
|
+
const contentSourceId = getContentSourceIdForContentSource(this.presetsContentSource);
|
|
387
|
+
// reload presets from content source only if needed
|
|
388
|
+
if (init || !contentSourceIds || contentSourceIds.includes(contentSourceId)) {
|
|
389
|
+
this.presets = await this.loadPresetsFromContentSource(contentSourceDataRaw);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// fallback to loading presets from config as usual
|
|
397
|
+
if (init && this.stackbitConfig && !this.presetsContentSource) {
|
|
398
|
+
this.presets = await this.loadPresetsFromConfig({ stackbitConfig: this.stackbitConfig });
|
|
399
|
+
}
|
|
400
|
+
|
|
341
401
|
// update all content sources at once to prevent race conditions
|
|
342
402
|
this.contentSourceDataById = await this.processData({
|
|
343
403
|
stackbitConfig: this.stackbitConfig,
|
|
@@ -346,6 +406,12 @@ export class ContentStore {
|
|
|
346
406
|
contentSourceRawDataArr: contentSourceRawDataArr
|
|
347
407
|
});
|
|
348
408
|
this.contentSources = contentSources;
|
|
409
|
+
|
|
410
|
+
// generate create site map entries
|
|
411
|
+
this.siteMapEntryGroups = await getSiteMapEntriesFromStackbitConfig({
|
|
412
|
+
stackbitConfig: this.stackbitConfig,
|
|
413
|
+
contentSourceDataById: this.contentSourceDataById
|
|
414
|
+
});
|
|
349
415
|
}
|
|
350
416
|
|
|
351
417
|
private async loadContentSourceData({
|
|
@@ -409,9 +475,15 @@ export class ContentStore {
|
|
|
409
475
|
getAsset({ assetId }: { assetId: string }) {
|
|
410
476
|
return csiAssetMap[assetId];
|
|
411
477
|
},
|
|
412
|
-
onContentChange: (contentChangeEvent: CSITypes.ContentChangeEvent) => {
|
|
478
|
+
onContentChange: async (contentChangeEvent: CSITypes.ContentChangeEvent) => {
|
|
413
479
|
this.logger.debug('content source called onContentChange', { contentSourceId });
|
|
414
480
|
const result = this.onContentChange(contentSourceId, contentChangeEvent);
|
|
481
|
+
this.siteMapEntryGroups = await updateSiteMapEntriesWithContentChanges({
|
|
482
|
+
siteMapEntryGroups: this.siteMapEntryGroups,
|
|
483
|
+
contentChanges: result,
|
|
484
|
+
stackbitConfig: this.stackbitConfig,
|
|
485
|
+
contentSourceDataById: this.contentSourceDataById
|
|
486
|
+
});
|
|
415
487
|
this.onContentChangeCallback(result);
|
|
416
488
|
},
|
|
417
489
|
onSchemaChange: async () => {
|
|
@@ -442,6 +514,9 @@ export class ContentStore {
|
|
|
442
514
|
private onContentChange(contentSourceId: string, contentChangeEvent: CSITypes.ContentChangeEvent): ContentStoreTypes.ContentChangeResult {
|
|
443
515
|
// TODO: prevent content change process for contentSourceId if loading content is in progress
|
|
444
516
|
|
|
517
|
+
// certain content changes, like preset changes are interpreted as schema changes
|
|
518
|
+
let schemaChanged = false;
|
|
519
|
+
|
|
445
520
|
this.logger.debug('onContentChange', {
|
|
446
521
|
contentSourceId,
|
|
447
522
|
documentCount: contentChangeEvent.documents.length,
|
|
@@ -461,6 +536,19 @@ export class ContentStore {
|
|
|
461
536
|
|
|
462
537
|
// update contentSourceData with deleted documents
|
|
463
538
|
contentChangeEvent.deletedDocumentIds.forEach((docId) => {
|
|
539
|
+
// remove preset, make sure there is something to remove first because
|
|
540
|
+
// were explicitly calling onContentChange from deletePreset as well
|
|
541
|
+
if (this.presets[docId] && contentSourceData.csiDocumentMap[docId]?.modelName === StackbitPresetModelName) {
|
|
542
|
+
schemaChanged = true;
|
|
543
|
+
const preset = this.presets[docId]!;
|
|
544
|
+
const model = contentSourceData.modelMap[preset.modelName];
|
|
545
|
+
delete this.presets[docId];
|
|
546
|
+
if (model && model.presets) {
|
|
547
|
+
const presetIdIndex = model.presets.findIndex((presetId) => presetId === docId);
|
|
548
|
+
model.presets.splice(presetIdIndex, 1);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
464
552
|
// delete document from documents map
|
|
465
553
|
delete contentSourceData.documentMap[docId];
|
|
466
554
|
delete contentSourceData.csiDocumentMap[docId];
|
|
@@ -557,10 +645,26 @@ export class ContentStore {
|
|
|
557
645
|
contentSourceData.documents.push(document);
|
|
558
646
|
contentSourceData.csiDocuments.push(csiDocument);
|
|
559
647
|
} else {
|
|
560
|
-
// the indexes of documents and csiDocuments are always the same as they are always updated at the same time
|
|
561
648
|
contentSourceData.documents.splice(dataIndex, 1, document);
|
|
562
649
|
contentSourceData.csiDocuments.splice(dataIndex, 1, csiDocument);
|
|
563
650
|
}
|
|
651
|
+
if (csiDocument.modelName === StackbitPresetModelName) {
|
|
652
|
+
schemaChanged = true;
|
|
653
|
+
const preset = getPresetFromDocument({
|
|
654
|
+
srcType: contentSourceData.srcType,
|
|
655
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
656
|
+
csiDocument,
|
|
657
|
+
csiAssetMap: contentSourceData.csiAssetMap,
|
|
658
|
+
logger: this.logger
|
|
659
|
+
});
|
|
660
|
+
if (preset) {
|
|
661
|
+
this.presets[csiDocument.id] = preset;
|
|
662
|
+
if (dataIndex === -1) {
|
|
663
|
+
//TODO recalculate assets as well
|
|
664
|
+
contentSourceData.modelMap[preset.modelName]?.presets?.push(csiDocument.id);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
564
668
|
result.updatedDocuments.push({
|
|
565
669
|
srcType: contentSourceData.srcType,
|
|
566
670
|
srcProjectId: contentSourceData.srcProjectId,
|
|
@@ -588,6 +692,10 @@ export class ContentStore {
|
|
|
588
692
|
});
|
|
589
693
|
}
|
|
590
694
|
|
|
695
|
+
if (schemaChanged) {
|
|
696
|
+
this.onSchemaChangeCallback?.();
|
|
697
|
+
}
|
|
698
|
+
|
|
591
699
|
return result;
|
|
592
700
|
}
|
|
593
701
|
|
|
@@ -623,48 +731,58 @@ export class ContentStore {
|
|
|
623
731
|
// srcProjectId. If after the comparison, there are more than one model left,
|
|
624
732
|
// log a warning and filter out that config model so it won't be merged with any
|
|
625
733
|
// of the content source models.
|
|
626
|
-
const
|
|
627
|
-
const
|
|
734
|
+
const nonMatchedModels: { configModel: ModelExtension; matchedCSIModels: CSITypes.ModelWithSource[] }[] = [];
|
|
735
|
+
const configModelsByContentSourceId = (configModels as ModelExtension[]).reduce((modelGroups: Record<string, Model[]>, configModel) => {
|
|
628
736
|
const csiModels = csiModelGroups[configModel.name];
|
|
629
737
|
if (!csiModels) {
|
|
630
|
-
|
|
738
|
+
nonMatchedModels.push({
|
|
739
|
+
configModel,
|
|
740
|
+
matchedCSIModels: []
|
|
741
|
+
});
|
|
742
|
+
return modelGroups;
|
|
631
743
|
}
|
|
632
744
|
const matchedCSIModels = csiModels.filter((model) => {
|
|
633
745
|
const matchesType = !configModel.srcType || model.srcType === configModel.srcType;
|
|
634
746
|
const matchesId = !configModel.srcProjectId || model.srcProjectId === configModel.srcProjectId;
|
|
635
747
|
return matchesType && matchesId;
|
|
636
748
|
});
|
|
637
|
-
if (matchedCSIModels.length
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
749
|
+
if (matchedCSIModels.length !== 1) {
|
|
750
|
+
nonMatchedModels.push({
|
|
751
|
+
configModel,
|
|
752
|
+
matchedCSIModels
|
|
753
|
+
});
|
|
754
|
+
return modelGroups;
|
|
642
755
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}) as Model[];
|
|
756
|
+
const contentSource = matchedCSIModels[0]!;
|
|
757
|
+
const contentSourceId = getContentSourceId(contentSource.srcType, contentSource.srcProjectId);
|
|
758
|
+
append(modelGroups, contentSourceId, configModel);
|
|
759
|
+
return modelGroups;
|
|
760
|
+
}, {});
|
|
649
761
|
|
|
650
762
|
// Log model matching warnings using user logger
|
|
651
|
-
for (const { configModel, matchedCSIModels } of
|
|
652
|
-
let
|
|
763
|
+
for (const { configModel, matchedCSIModels } of nonMatchedModels) {
|
|
764
|
+
let configModelMessage = `model name: '${configModel.name}'`;
|
|
653
765
|
if (configModel.srcType) {
|
|
654
|
-
|
|
766
|
+
configModelMessage += `, srcType: '${configModel.srcType}'`;
|
|
655
767
|
}
|
|
656
768
|
if (configModel.srcProjectId) {
|
|
657
|
-
|
|
769
|
+
configModelMessage += `, srcProjectId: '${configModel.srcProjectId}'`;
|
|
658
770
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
771
|
+
configModelMessage = configModelMessage + ` defined in stackbit config`;
|
|
772
|
+
let contentSourceModelsMessage;
|
|
773
|
+
if (matchedCSIModels.length) {
|
|
774
|
+
const matchesModelsMessage = matchedCSIModels.map((model) => `srcType: '${model.srcType}', srcProjectId: '${model.srcProjectId}'`).join('; ');
|
|
775
|
+
contentSourceModelsMessage = ` matches more that 1 model in the following content sources: ${matchesModelsMessage}`;
|
|
776
|
+
} else {
|
|
777
|
+
contentSourceModelsMessage = ' does not match any content source model';
|
|
778
|
+
}
|
|
779
|
+
this.userLogger.warn(configModelMessage + contentSourceModelsMessage);
|
|
663
780
|
}
|
|
664
781
|
|
|
665
782
|
const modelsWithSource = contentSourceRawDataArr.reduce((accum: CSITypes.ModelWithSource[], csData) => {
|
|
783
|
+
const contentSourceId = getContentSourceId(csData.srcType, csData.srcProjectId);
|
|
666
784
|
const mergedModels = mergeConfigModelsWithExternalModels({
|
|
667
|
-
configModels:
|
|
785
|
+
configModels: configModelsByContentSourceId[contentSourceId] ?? [],
|
|
668
786
|
externalModels: csData.csiModels
|
|
669
787
|
});
|
|
670
788
|
const modelsWithSource = mergedModels.map(
|
|
@@ -687,34 +805,39 @@ export class ContentStore {
|
|
|
687
805
|
const modelsWithPresetsIds = extendModelsWithPresetsIds({ models: validatedModels, presets });
|
|
688
806
|
const { models } = await this.handleConfigAssets({ models: modelsWithPresetsIds });
|
|
689
807
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
808
|
+
let documentMapByContentSource: Record<string, Record<string, CSITypes.Document[]>> | null = null;
|
|
809
|
+
if (stackbitConfig?.mapDocuments) {
|
|
810
|
+
const csiDocumentsWithSource = contentSourceRawDataArr.reduce((accum: CSITypes.DocumentWithSource[], csData) => {
|
|
811
|
+
const csiDocumentsWithSource = csData.csiDocuments.map(
|
|
812
|
+
(csiDocument): CSITypes.DocumentWithSource => ({
|
|
813
|
+
srcType: csData.srcType,
|
|
814
|
+
srcProjectId: csData.srcProjectId,
|
|
815
|
+
...csiDocument
|
|
816
|
+
})
|
|
817
|
+
);
|
|
818
|
+
return accum.concat(csiDocumentsWithSource);
|
|
819
|
+
}, []);
|
|
820
|
+
|
|
821
|
+
// TODO: Is there a better way than deep cloning objects before passing them to user methods?
|
|
822
|
+
// Not cloning mutable objects will break the internal state if user mutates the objects.
|
|
823
|
+
const mappedDocs =
|
|
824
|
+
stackbitConfig?.mapDocuments?.({
|
|
825
|
+
documents: _.cloneDeep(csiDocumentsWithSource),
|
|
826
|
+
models: _.cloneDeep(models)
|
|
827
|
+
}) ?? csiDocumentsWithSource;
|
|
828
|
+
documentMapByContentSource = groupDocumentsByContentSource({ documents: mappedDocs });
|
|
829
|
+
}
|
|
708
830
|
|
|
709
831
|
const modelMapByContentSource = groupModelsByContentSource({ models: models });
|
|
710
|
-
const documentMapByContentSource = groupDocumentsByContentSource({ documents: mappedDocs });
|
|
711
832
|
|
|
712
833
|
const contentSourceDataArr = contentSourceRawDataArr.map(
|
|
713
834
|
(csData): ContentSourceData => {
|
|
714
835
|
const modelMap = _.get(modelMapByContentSource, [csData.srcType, csData.srcProjectId], {});
|
|
715
|
-
const
|
|
836
|
+
const csiDocuments = documentMapByContentSource
|
|
837
|
+
? _.get(documentMapByContentSource, [csData.srcType, csData.srcProjectId], [])
|
|
838
|
+
: csData.csiDocuments;
|
|
716
839
|
const documents = mapCSIDocumentsToStoreDocuments({
|
|
717
|
-
csiDocuments:
|
|
840
|
+
csiDocuments: csiDocuments,
|
|
718
841
|
contentSourceInstance: csData.instance,
|
|
719
842
|
defaultLocaleCode: csData.defaultLocaleCode,
|
|
720
843
|
modelMap: modelMap
|
|
@@ -770,6 +893,10 @@ export class ContentStore {
|
|
|
770
893
|
return contentSourceData.instance.getProjectEnvironment();
|
|
771
894
|
}
|
|
772
895
|
|
|
896
|
+
usesContentSourcePresets() {
|
|
897
|
+
return Boolean(this.presetsContentSource);
|
|
898
|
+
}
|
|
899
|
+
|
|
773
900
|
async hasAccess({
|
|
774
901
|
srcType,
|
|
775
902
|
srcProjectId,
|
|
@@ -878,64 +1005,32 @@ export class ContentStore {
|
|
|
878
1005
|
}
|
|
879
1006
|
|
|
880
1007
|
getSiteMapEntries({ locale }: { locale?: string } = {}): CSITypes.SiteMapEntry[] {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1008
|
+
const siteMapEntries = _.reduce(
|
|
1009
|
+
this.siteMapEntryGroups,
|
|
1010
|
+
(accum: CSITypes.SiteMapEntry[], siteMapEntryGroup) => {
|
|
1011
|
+
return _.reduce(
|
|
1012
|
+
siteMapEntryGroup,
|
|
1013
|
+
(accum: CSITypes.SiteMapEntry[], siteMapEntry) => {
|
|
1014
|
+
if (!siteMapEntry.label) {
|
|
1015
|
+
const fieldLabelValue = getDocumentFieldLabelValueForSiteMapEntry({
|
|
1016
|
+
siteMapEntry,
|
|
1017
|
+
locale,
|
|
1018
|
+
contentSourceDataById: this.contentSourceDataById
|
|
1019
|
+
});
|
|
1020
|
+
siteMapEntry = {
|
|
1021
|
+
...siteMapEntry,
|
|
1022
|
+
label: fieldLabelValue ?? siteMapEntry.urlPath
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
accum.push(siteMapEntry);
|
|
1026
|
+
return accum;
|
|
1027
|
+
},
|
|
1028
|
+
accum
|
|
1029
|
+
);
|
|
899
1030
|
},
|
|
900
|
-
|
|
1031
|
+
[]
|
|
901
1032
|
);
|
|
902
1033
|
|
|
903
|
-
const siteMapEntries = this.stackbitConfig.siteMap(siteMapOptions).reduce((accum: CSITypes.SiteMapEntry[], siteMapEntry) => {
|
|
904
|
-
// The site map entries are provided by user, sanitize them and filter out illegal entries
|
|
905
|
-
if (!siteMapEntry) {
|
|
906
|
-
return accum;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
if (typeof siteMapEntry.urlPath !== 'string') {
|
|
910
|
-
return accum;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
if ('document' in siteMapEntry) {
|
|
914
|
-
const doc = siteMapEntry.document;
|
|
915
|
-
if (!doc.srcType || !doc.srcProjectId || !doc.modelName || !doc.id) {
|
|
916
|
-
return accum;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
if (!siteMapEntry.label) {
|
|
921
|
-
const fieldLabelValue = getDocumentFieldLabelValueForSiteMapEntry({ siteMapEntry, locale, contentSourceDataById: this.contentSourceDataById });
|
|
922
|
-
siteMapEntry = {
|
|
923
|
-
...siteMapEntry,
|
|
924
|
-
label: fieldLabelValue ?? siteMapEntry.urlPath
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
if (!siteMapEntry.stableId) {
|
|
929
|
-
siteMapEntry = {
|
|
930
|
-
...siteMapEntry,
|
|
931
|
-
stableId: 'document' in siteMapEntry ? siteMapEntry.document.id : siteMapEntry.urlPath
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
accum.push(siteMapEntry);
|
|
936
|
-
return accum;
|
|
937
|
-
}, []);
|
|
938
|
-
|
|
939
1034
|
return _.isEmpty(locale) ? siteMapEntries : siteMapEntries.filter((siteMapEntry) => !siteMapEntry.locale || siteMapEntry.locale === locale);
|
|
940
1035
|
}
|
|
941
1036
|
|
|
@@ -960,7 +1055,8 @@ export class ContentStore {
|
|
|
960
1055
|
const currentDocuments = _.isEmpty(locale)
|
|
961
1056
|
? contentSourceData.documents
|
|
962
1057
|
: contentSourceData.documents.filter((document) => !document.locale || document.locale === locale);
|
|
963
|
-
|
|
1058
|
+
const filteredDocuments = currentDocuments.filter((document) => document.srcModelName !== StackbitPresetModelName);
|
|
1059
|
+
return documents.concat(filteredDocuments);
|
|
964
1060
|
},
|
|
965
1061
|
[]
|
|
966
1062
|
);
|
|
@@ -997,7 +1093,8 @@ export class ContentStore {
|
|
|
997
1093
|
? contentSourceData.assets.filter((asset) => !asset.locale || asset.locale === locale)
|
|
998
1094
|
: contentSourceData.assets;
|
|
999
1095
|
const currentLocale = locale ?? contentSourceData.defaultLocaleCode;
|
|
1000
|
-
const
|
|
1096
|
+
const filteredDocuments = documents.filter((document) => document.srcModelName !== StackbitPresetModelName);
|
|
1097
|
+
const documentObjects = mapDocumentsToLocalizedApiObjects(filteredDocuments, currentLocale);
|
|
1001
1098
|
const imageObjects = mapAssetsToLocalizedApiImages(assets, currentLocale);
|
|
1002
1099
|
return objects.concat(documentObjects, imageObjects);
|
|
1003
1100
|
},
|
|
@@ -1154,6 +1251,63 @@ export class ContentStore {
|
|
|
1154
1251
|
return { srcDocumentId: updatedDocument.id };
|
|
1155
1252
|
}
|
|
1156
1253
|
|
|
1254
|
+
async createPreset({
|
|
1255
|
+
preset,
|
|
1256
|
+
thumbnailAsset,
|
|
1257
|
+
user
|
|
1258
|
+
}: {
|
|
1259
|
+
preset: Preset;
|
|
1260
|
+
thumbnailAsset: ContentStoreTypes.UploadAssetData;
|
|
1261
|
+
user?: ContentStoreTypes.User;
|
|
1262
|
+
}): Promise<{ srcDocumentId: string }> {
|
|
1263
|
+
if (!this.presetsContentSource) {
|
|
1264
|
+
throw new Error('No content source available for preset saving');
|
|
1265
|
+
}
|
|
1266
|
+
let thumbnail: string | undefined;
|
|
1267
|
+
if (thumbnailAsset) {
|
|
1268
|
+
const assets = await this.uploadAssets({
|
|
1269
|
+
srcType: this.presetsContentSource.getContentSourceType(),
|
|
1270
|
+
srcProjectId: this.presetsContentSource.getProjectId(),
|
|
1271
|
+
assets: [thumbnailAsset],
|
|
1272
|
+
user
|
|
1273
|
+
});
|
|
1274
|
+
thumbnail = assets[0]?.objectId;
|
|
1275
|
+
}
|
|
1276
|
+
const contentSourceData = this.getContentSourceDataByIdOrThrow(getContentSourceIdForContentSource(this.presetsContentSource));
|
|
1277
|
+
const document = await this.createDocument({
|
|
1278
|
+
srcType: this.presetsContentSource.getContentSourceType(),
|
|
1279
|
+
srcProjectId: this.presetsContentSource.getProjectId(),
|
|
1280
|
+
modelName: StackbitPresetModelName,
|
|
1281
|
+
object: {
|
|
1282
|
+
...getDocumentObjectFromPreset(preset, contentSourceData.modelMap[StackbitPresetModelName]),
|
|
1283
|
+
thumbnail
|
|
1284
|
+
},
|
|
1285
|
+
user
|
|
1286
|
+
});
|
|
1287
|
+
return { srcDocumentId: document.srcDocumentId };
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async deletePreset({ presetId, user }: { presetId: string; user?: ContentStoreTypes.User }) {
|
|
1291
|
+
if (!this.presetsContentSource) {
|
|
1292
|
+
throw new Error('No content source available for preset deleting');
|
|
1293
|
+
}
|
|
1294
|
+
await this.deleteDocument({
|
|
1295
|
+
srcType: this.presetsContentSource.getContentSourceType(),
|
|
1296
|
+
srcProjectId: this.presetsContentSource.getProjectId(),
|
|
1297
|
+
srcDocumentId: presetId,
|
|
1298
|
+
user
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// we delete presets immediately because some CMSs don't notify us
|
|
1302
|
+
// when documents have been deleted.
|
|
1303
|
+
this.onContentChange(getContentSourceIdForContentSource(this.presetsContentSource), {
|
|
1304
|
+
documents: [],
|
|
1305
|
+
deletedDocumentIds: [presetId],
|
|
1306
|
+
assets: [],
|
|
1307
|
+
deletedAssetIds: []
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1157
1311
|
async uploadAndLinkAsset({
|
|
1158
1312
|
srcType,
|
|
1159
1313
|
srcProjectId,
|
|
@@ -1608,7 +1762,9 @@ export class ContentStore {
|
|
|
1608
1762
|
const contentSourceDocuments = _.isEmpty(locale)
|
|
1609
1763
|
? contentSourceData.documents
|
|
1610
1764
|
: contentSourceData.documents.filter((document) => !document.locale || document.locale === locale);
|
|
1611
|
-
|
|
1765
|
+
|
|
1766
|
+
const filteredDocuments = contentSourceDocuments.filter((document) => document.srcModelName !== StackbitPresetModelName);
|
|
1767
|
+
documents.push(...filteredDocuments);
|
|
1612
1768
|
|
|
1613
1769
|
if (contentSourceData.defaultLocaleCode) {
|
|
1614
1770
|
defaultLocales[contentSourceId] = contentSourceData.defaultLocaleCode;
|