@stackbit/cms-core 0.1.13 → 0.1.14-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +4 -4
- 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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { Preset } from '@stackbit/sdk';
|
|
3
|
+
import * as CSITypes from '@stackbit/types';
|
|
4
|
+
import { DocumentField, getLocalizedFieldForLocale, Logger, Model } from '@stackbit/types';
|
|
5
|
+
|
|
6
|
+
function getPresetData(presetId: string, field: DocumentField, logger?: Logger) {
|
|
7
|
+
const dataField = getLocalizedFieldForLocale(field);
|
|
8
|
+
let data;
|
|
9
|
+
if (dataField?.type === 'json') {
|
|
10
|
+
data = dataField.value;
|
|
11
|
+
} else if (dataField?.type === 'string' || dataField?.type === 'text') {
|
|
12
|
+
try {
|
|
13
|
+
data = JSON.parse(dataField.value);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
logger?.error(`Can't parse data field for preset ${presetId}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getPresetThumbnail(presetId: string, thumbnailField: DocumentField, csiAssetMap: Record<string, CSITypes.Asset>, logger?: Logger) {
|
|
22
|
+
let thumbnail;
|
|
23
|
+
if (thumbnailField?.type === 'image') {
|
|
24
|
+
const imageFields = getLocalizedFieldForLocale(thumbnailField)?.fields;
|
|
25
|
+
if (imageFields) {
|
|
26
|
+
const urlField = imageFields.url;
|
|
27
|
+
if (urlField?.localized) {
|
|
28
|
+
const vals = Object.values(urlField.locales);
|
|
29
|
+
if (vals?.length) {
|
|
30
|
+
thumbnail = vals[0]?.value;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
thumbnail = urlField.value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else if (thumbnailField?.type === 'reference' && thumbnailField.refType === 'asset') {
|
|
37
|
+
const refId = getLocalizedFieldForLocale(thumbnailField)?.refId;
|
|
38
|
+
if (refId) {
|
|
39
|
+
const fileField = csiAssetMap[refId]?.fields.file;
|
|
40
|
+
if (fileField?.localized) {
|
|
41
|
+
const vals = Object.values(fileField.locales);
|
|
42
|
+
if (vals?.length) {
|
|
43
|
+
thumbnail = vals[0]?.url;
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
thumbnail = fileField?.url;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
logger?.warn(`No thumbnail reference found for preset ${presetId}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return thumbnail;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getPresetFromDocument({
|
|
56
|
+
srcType,
|
|
57
|
+
srcProjectId,
|
|
58
|
+
csiDocument,
|
|
59
|
+
csiAssetMap,
|
|
60
|
+
logger
|
|
61
|
+
}: {
|
|
62
|
+
srcType: string;
|
|
63
|
+
srcProjectId: string;
|
|
64
|
+
csiDocument: CSITypes.Document;
|
|
65
|
+
csiAssetMap: Record<string, CSITypes.Asset>;
|
|
66
|
+
logger?: Logger;
|
|
67
|
+
}): Preset | null {
|
|
68
|
+
const data = csiDocument.fields['data'] ? getPresetData(csiDocument.id, csiDocument.fields['data'], logger) : null;
|
|
69
|
+
if (!data) {
|
|
70
|
+
logger?.warn(`Error finding preset data for preset ${csiDocument.id}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const thumbnail = csiDocument.fields['thumbnail'] ? getPresetThumbnail(csiDocument.id, csiDocument.fields['thumbnail'], csiAssetMap, logger) : null;
|
|
74
|
+
return {
|
|
75
|
+
srcType,
|
|
76
|
+
srcProjectId,
|
|
77
|
+
...data,
|
|
78
|
+
thumbnail
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getDocumentObjectFromPreset(preset: Preset, model?: Model): Record<string, any> {
|
|
83
|
+
let presetData: Record<string, any> | string = _.omit(preset, 'thumbnail');
|
|
84
|
+
if (model && model.fields) {
|
|
85
|
+
const dataField = model.fields.find((field) => field.name === 'data');
|
|
86
|
+
if (dataField?.type === 'string' || dataField?.type === 'text') {
|
|
87
|
+
presetData = JSON.stringify(presetData, null, 2);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
label: preset.label,
|
|
92
|
+
data: presetData
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { Config } from '@stackbit/sdk';
|
|
3
|
+
import { append } from '@stackbit/utils';
|
|
4
|
+
import { SiteMapEntry } from '@stackbit/types';
|
|
5
|
+
import * as CSITypes from '@stackbit/types';
|
|
6
|
+
import * as ContentStoreTypes from '../content-store-types';
|
|
7
|
+
import { mapStoreDocumentsToCSIDocumentsWithSource } from './store-to-csi-docs-converter';
|
|
8
|
+
import { getContentSourceId, getDocumentFieldForLocale } from '../content-store-utils';
|
|
9
|
+
|
|
10
|
+
export const SiteMapStaticEntriesKey = Symbol.for('SiteMapStaticEntriesKey');
|
|
11
|
+
export type SiteMapEntriesSourceKeys = string | symbol;
|
|
12
|
+
/**
|
|
13
|
+
* SiteMapEntryGroups is a two level hashmap.
|
|
14
|
+
* If the SiteMapEntry is document-related, the first level key will be an
|
|
15
|
+
* identifier of the document including content source, and the second key will
|
|
16
|
+
* be the stableId. If the SiteMapEntry is static entry, the first key will
|
|
17
|
+
* be a constant Symbol and the second key will be the stableId.
|
|
18
|
+
* {
|
|
19
|
+
* `{srcType}:{srcProjectId}:{srcDocumentId}`: {
|
|
20
|
+
* `{stableId}`: SiteMapEntry
|
|
21
|
+
* },
|
|
22
|
+
* [SiteMapStaticEntriesKey]: {
|
|
23
|
+
* `{stableId}`: SiteMapEntry
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
*/
|
|
27
|
+
export type SiteMapEntryGroups = Record<SiteMapEntriesSourceKeys, Record<string, SiteMapEntry>>;
|
|
28
|
+
|
|
29
|
+
export async function getSiteMapEntriesFromStackbitConfig({
|
|
30
|
+
stackbitConfig,
|
|
31
|
+
contentSourceDataById
|
|
32
|
+
}: {
|
|
33
|
+
stackbitConfig: Config | null;
|
|
34
|
+
contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
|
|
35
|
+
}): Promise<SiteMapEntryGroups> {
|
|
36
|
+
if (!stackbitConfig?.siteMap) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const siteMapOptions = _.reduce(
|
|
41
|
+
contentSourceDataById,
|
|
42
|
+
(accum: { models: CSITypes.ModelWithSource[]; documents: CSITypes.DocumentWithSource[] }, contentSourceData) => {
|
|
43
|
+
return {
|
|
44
|
+
models: accum.models.concat(
|
|
45
|
+
contentSourceData.models.map((model) => ({
|
|
46
|
+
srcType: contentSourceData.srcType,
|
|
47
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
48
|
+
...model
|
|
49
|
+
}))
|
|
50
|
+
),
|
|
51
|
+
documents: accum.documents.concat(mapStoreDocumentsToCSIDocumentsWithSource(contentSourceData.documents))
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
{ models: [], documents: [] }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const rawSiteMapEntries = await stackbitConfig.siteMap(siteMapOptions);
|
|
58
|
+
|
|
59
|
+
// The rawSiteMapEntries entries are provided by the user, sanitize them and filter out illegal entries
|
|
60
|
+
return sanitizeAndGroupSiteMapEntries(rawSiteMapEntries);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Because the sitemap is directly affected by documents, the sitemap can change
|
|
65
|
+
* whenever there is a content change. For example, if a new document is added
|
|
66
|
+
* or deleted, a new sitemap entry would be added or deleted respectively.
|
|
67
|
+
* Likewise, if a slug of an existing document is changed, the sitemap entry for
|
|
68
|
+
* that document would also change.
|
|
69
|
+
*
|
|
70
|
+
* However, to improve overall performance, we don't want to call
|
|
71
|
+
* stackbitConfig.siteMap() with all the documents when a small set of documents
|
|
72
|
+
* is changed. Instead, we want to call stackbitConfig.siteMap() with only the
|
|
73
|
+
* changed documents. Then we merge the partial sitemap entries with the
|
|
74
|
+
* existing sitemap entries using sitemap entry identifiers such as
|
|
75
|
+
* srcDocumentId for document-related entries and stackbitId for static entries.
|
|
76
|
+
*
|
|
77
|
+
* @param siteMapEntries Existing sitemap entries
|
|
78
|
+
* @param contentChanges A ContentChangeResult including new, changed and
|
|
79
|
+
* deleted documents
|
|
80
|
+
* @param stackbitConfig Stackbit config
|
|
81
|
+
* @param contentSourceDataById ContentSourceData by content source IDs
|
|
82
|
+
*/
|
|
83
|
+
export async function updateSiteMapEntriesWithContentChanges({
|
|
84
|
+
siteMapEntryGroups,
|
|
85
|
+
contentChanges,
|
|
86
|
+
stackbitConfig,
|
|
87
|
+
contentSourceDataById
|
|
88
|
+
}: {
|
|
89
|
+
siteMapEntryGroups: SiteMapEntryGroups;
|
|
90
|
+
contentChanges: ContentStoreTypes.ContentChangeResult;
|
|
91
|
+
stackbitConfig: Config | null;
|
|
92
|
+
contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
|
|
93
|
+
}): Promise<SiteMapEntryGroups> {
|
|
94
|
+
if (!stackbitConfig?.siteMap) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Create a map of changed documents by content source id
|
|
99
|
+
const changedDocumentsByContentSourceId = contentChanges.updatedDocuments.reduce(
|
|
100
|
+
(accum: Record<string, ContentStoreTypes.Document[]>, contentChangeResultItem) => {
|
|
101
|
+
const contentSourceId = getContentSourceId(contentChangeResultItem.srcType, contentChangeResultItem.srcProjectId);
|
|
102
|
+
const document = contentSourceDataById[contentSourceId]?.documentMap[contentChangeResultItem.srcObjectId];
|
|
103
|
+
if (document) {
|
|
104
|
+
append(accum, contentSourceId, document);
|
|
105
|
+
}
|
|
106
|
+
return accum;
|
|
107
|
+
},
|
|
108
|
+
{}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Create siteMap parameters from changed documents
|
|
112
|
+
const partialSiteMapOptions = _.reduce(
|
|
113
|
+
contentSourceDataById,
|
|
114
|
+
(accum: { models: CSITypes.ModelWithSource[]; documents: CSITypes.DocumentWithSource[] }, contentSourceData) => {
|
|
115
|
+
const changedDocuments = changedDocumentsByContentSourceId[contentSourceData.id] ?? [];
|
|
116
|
+
return {
|
|
117
|
+
models: accum.models.concat(
|
|
118
|
+
contentSourceData.models.map((model) => ({
|
|
119
|
+
srcType: contentSourceData.srcType,
|
|
120
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
121
|
+
...model
|
|
122
|
+
}))
|
|
123
|
+
),
|
|
124
|
+
documents: accum.documents.concat(mapStoreDocumentsToCSIDocumentsWithSource(changedDocuments))
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
{ models: [], documents: [] }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const partialRawSiteMapEntries = await stackbitConfig.siteMap(partialSiteMapOptions);
|
|
131
|
+
|
|
132
|
+
// The partialRawSiteMapEntries entries are provided by the user, sanitize them and filter out illegal entries
|
|
133
|
+
const partialSiteMapEntryGroups = sanitizeAndGroupSiteMapEntries(partialRawSiteMapEntries);
|
|
134
|
+
|
|
135
|
+
siteMapEntryGroups = _.reduce(
|
|
136
|
+
contentChanges.deletedDocuments,
|
|
137
|
+
(accum, contentChangeResultItem: ContentStoreTypes.ContentChangeResultItem) => {
|
|
138
|
+
const siteMapGroupKey = `${contentChangeResultItem.srcType}:${contentChangeResultItem.srcProjectId}:${contentChangeResultItem.srcObjectId}`;
|
|
139
|
+
delete accum[siteMapGroupKey];
|
|
140
|
+
return accum;
|
|
141
|
+
},
|
|
142
|
+
siteMapEntryGroups
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
siteMapEntryGroups = _.reduce(
|
|
146
|
+
partialSiteMapEntryGroups,
|
|
147
|
+
(accum, newSiteMapEntriesByStableId, siteMapGroupKey) => {
|
|
148
|
+
accum[siteMapGroupKey] = newSiteMapEntriesByStableId;
|
|
149
|
+
return accum;
|
|
150
|
+
},
|
|
151
|
+
siteMapEntryGroups
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return siteMapEntryGroups;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sanitizeAndGroupSiteMapEntries(siteMapEntries: SiteMapEntry[]): SiteMapEntryGroups {
|
|
158
|
+
return siteMapEntries.reduce((accum: SiteMapEntryGroups, siteMapEntry) => {
|
|
159
|
+
if (!siteMapEntry) {
|
|
160
|
+
return accum;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (typeof siteMapEntry.urlPath !== 'string') {
|
|
164
|
+
return accum;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if ('document' in siteMapEntry) {
|
|
168
|
+
const doc = siteMapEntry.document;
|
|
169
|
+
if (!doc.srcType || !doc.srcProjectId || !doc.modelName || !doc.id) {
|
|
170
|
+
return accum;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!siteMapEntry.stableId) {
|
|
175
|
+
siteMapEntry = {
|
|
176
|
+
...siteMapEntry,
|
|
177
|
+
stableId: 'document' in siteMapEntry ? siteMapEntry.document.id : siteMapEntry.urlPath
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const groupKey = getSiteMapGroupKey(siteMapEntry);
|
|
182
|
+
_.set(accum, [groupKey, siteMapEntry.stableId!], siteMapEntry);
|
|
183
|
+
return accum;
|
|
184
|
+
}, {});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getSiteMapGroupKey(siteMapEntry: SiteMapEntry) {
|
|
188
|
+
return 'document' in siteMapEntry
|
|
189
|
+
? `${siteMapEntry.document.srcType}:${siteMapEntry.document.srcProjectId}:${siteMapEntry.document.id}`
|
|
190
|
+
: SiteMapStaticEntriesKey;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getDocumentFieldLabelValueForSiteMapEntry({
|
|
194
|
+
siteMapEntry,
|
|
195
|
+
locale,
|
|
196
|
+
contentSourceDataById
|
|
197
|
+
}: {
|
|
198
|
+
siteMapEntry: SiteMapEntry;
|
|
199
|
+
locale?: string;
|
|
200
|
+
contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
|
|
201
|
+
}): string | null {
|
|
202
|
+
if (!('document' in siteMapEntry)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const contentSourceId = getContentSourceId(siteMapEntry.document.srcType, siteMapEntry.document.srcProjectId);
|
|
206
|
+
const contentSourceData = contentSourceDataById[contentSourceId];
|
|
207
|
+
if (!contentSourceData) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const labelFieldName = contentSourceData.modelMap[siteMapEntry.document.modelName]?.labelField;
|
|
211
|
+
const document = contentSourceData.documentMap[siteMapEntry.document.id];
|
|
212
|
+
if (!labelFieldName || !document) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const labelField = document.fields[labelFieldName];
|
|
216
|
+
if (!labelField) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const localizedLabelField = getDocumentFieldForLocale(labelField, locale);
|
|
220
|
+
if (!localizedLabelField || !('value' in localizedLabelField) || !localizedLabelField.value) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return String(localizedLabelField.value);
|
|
224
|
+
}
|