@stackbit/cms-core 0.1.5 → 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 +36 -7
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +265 -157
- 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 +348 -171
- 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,31 +26,46 @@ 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;
|
|
21
46
|
userLogger: ContentStoreTypes.Logger;
|
|
22
47
|
localDev: boolean;
|
|
48
|
+
webhookUrl?: string;
|
|
23
49
|
userCommandSpawner?: UserCommandSpawner;
|
|
24
50
|
onSchemaChangeCallback: () => void;
|
|
25
51
|
onContentChangeCallback: (contentChanges: ContentStoreTypes.ContentChangeResult) => void;
|
|
26
|
-
handleConfigAssets:
|
|
52
|
+
handleConfigAssets: HandleConfigAssets;
|
|
53
|
+
devAppRestartNeeded?: () => void;
|
|
27
54
|
}
|
|
28
55
|
|
|
29
56
|
interface ContentSourceData {
|
|
30
57
|
id: string;
|
|
31
58
|
instance: CSITypes.ContentSourceInterface;
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
srcType: string;
|
|
60
|
+
srcProjectId: string;
|
|
34
61
|
locales?: CSITypes.Locale[];
|
|
35
62
|
defaultLocaleCode?: string;
|
|
36
63
|
/* Array of extended and validated Models */
|
|
37
64
|
models: Model[];
|
|
38
65
|
/* Map of extended and validated Models by model name */
|
|
39
66
|
modelMap: Record<string, Model>;
|
|
67
|
+
/* Array of original Models (as provided by content source) */
|
|
68
|
+
csiModels: CSITypes.Model[];
|
|
40
69
|
/* Map of original Models (as provided by content source) by model name */
|
|
41
70
|
csiModelMap: Record<string, CSITypes.Model>;
|
|
42
71
|
/* Array of original content source Documents */
|
|
@@ -57,29 +86,37 @@ interface ContentSourceData {
|
|
|
57
86
|
assetMap: Record<string, ContentStoreTypes.Asset>;
|
|
58
87
|
}
|
|
59
88
|
|
|
89
|
+
type ContentSourceRawData = Omit<ContentSourceData, 'models' | 'modelMap' | 'documents' | 'documentMap'>;
|
|
90
|
+
|
|
60
91
|
export class ContentStore {
|
|
61
92
|
private readonly logger: ContentStoreTypes.Logger;
|
|
62
93
|
private readonly userLogger: ContentStoreTypes.Logger;
|
|
63
94
|
private readonly userCommandSpawner?: UserCommandSpawner;
|
|
64
95
|
private readonly localDev: boolean;
|
|
96
|
+
private readonly webhookUrl?: string;
|
|
65
97
|
private readonly onSchemaChangeCallback: () => void;
|
|
66
98
|
private readonly onContentChangeCallback: (contentChanges: ContentStoreTypes.ContentChangeResult) => void;
|
|
67
|
-
private readonly handleConfigAssets:
|
|
99
|
+
private readonly handleConfigAssets: HandleConfigAssets;
|
|
100
|
+
private readonly devAppRestartNeeded?: () => void;
|
|
68
101
|
private contentSources: CSITypes.ContentSourceInterface[] = [];
|
|
69
102
|
private contentSourceDataById: Record<string, ContentSourceData> = {};
|
|
70
103
|
private contentUpdatesWatchTimer: Timer;
|
|
71
|
-
private
|
|
72
|
-
private
|
|
104
|
+
private stackbitConfig: Config | null = null;
|
|
105
|
+
private yamlModels: Model[] = [];
|
|
106
|
+
private configModels: Model[] = [];
|
|
107
|
+
private presets: PresetMap = {};
|
|
73
108
|
|
|
74
109
|
constructor(options: ContentSourceOptions) {
|
|
75
110
|
this.logger = options.logger.createLogger({ label: 'content-store' });
|
|
76
111
|
this.userLogger = options.userLogger.createLogger({ label: 'content-store' });
|
|
77
112
|
this.localDev = options.localDev;
|
|
113
|
+
this.webhookUrl = options.webhookUrl;
|
|
78
114
|
this.userCommandSpawner = options.userCommandSpawner;
|
|
79
115
|
this.onSchemaChangeCallback = options.onSchemaChangeCallback;
|
|
80
116
|
this.onContentChangeCallback = options.onContentChangeCallback;
|
|
81
117
|
this.handleConfigAssets = options.handleConfigAssets;
|
|
82
118
|
this.contentUpdatesWatchTimer = new Timer({ timerCallback: () => this.handleTimerTimeout(), logger: this.logger });
|
|
119
|
+
this.devAppRestartNeeded = options.devAppRestartNeeded;
|
|
83
120
|
|
|
84
121
|
// The `loadContentSourceData` method can be called for different
|
|
85
122
|
// reasons: user restarted SSG from Stackbit, Stackbit Config updated,
|
|
@@ -114,6 +151,10 @@ export class ContentStore {
|
|
|
114
151
|
|
|
115
152
|
async init({ stackbitConfig }: { stackbitConfig: Config | null }): Promise<void> {
|
|
116
153
|
this.logger.debug('init');
|
|
154
|
+
if (stackbitConfig) {
|
|
155
|
+
this.yamlModels = await this.loadYamlModels({ stackbitConfig });
|
|
156
|
+
this.presets = await this.loadPresets({ stackbitConfig });
|
|
157
|
+
}
|
|
117
158
|
await this.setStackbitConfig({ stackbitConfig, init: true });
|
|
118
159
|
}
|
|
119
160
|
|
|
@@ -123,37 +164,19 @@ export class ContentStore {
|
|
|
123
164
|
}
|
|
124
165
|
|
|
125
166
|
private async setStackbitConfig({ stackbitConfig, init }: { stackbitConfig: Config | null; init: boolean }) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// 3. merge content-source models with config.models or via config.mapModels, sanitize and validate
|
|
131
|
-
// 4. load presets, adjust presets to have srcType and srcProjectId
|
|
132
|
-
if (!stackbitConfig) {
|
|
133
|
-
this.rawStackbitConfig = null;
|
|
134
|
-
} else {
|
|
135
|
-
const rawConfigResult = await loadConfigFromDir({
|
|
136
|
-
dirPath: stackbitConfig.dirPath,
|
|
137
|
-
logger: this.logger
|
|
138
|
-
});
|
|
139
|
-
for (const error of rawConfigResult.errors) {
|
|
140
|
-
this.userLogger.warn(error.message);
|
|
141
|
-
}
|
|
142
|
-
if (rawConfigResult.config) {
|
|
143
|
-
this.rawStackbitConfig = {
|
|
144
|
-
...rawConfigResult.config,
|
|
145
|
-
contentSources: stackbitConfig.contentSources
|
|
146
|
-
};
|
|
147
|
-
} else {
|
|
148
|
-
this.rawStackbitConfig = null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
await this.loadContentSources({ init });
|
|
167
|
+
this.stackbitConfig = stackbitConfig;
|
|
168
|
+
this.configModels = this.mergeConfigModels(this.stackbitConfig, this.yamlModels);
|
|
169
|
+
|
|
170
|
+
await this.loadAllContentSourcesAndProcessData({ init });
|
|
152
171
|
}
|
|
153
172
|
|
|
154
173
|
/**
|
|
155
174
|
* This method is called when contentUpdatesWatchTimer receives timeout.
|
|
156
|
-
*
|
|
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.
|
|
157
180
|
*/
|
|
158
181
|
private handleTimerTimeout() {
|
|
159
182
|
for (const contentSourceInstance of this.contentSources) {
|
|
@@ -173,7 +196,7 @@ export class ContentStore {
|
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
this.logger.debug('keepAlive => contentUpdatesWatchTimer is not running => load content source data');
|
|
176
|
-
await this.
|
|
199
|
+
await this.loadAllContentSourcesAndProcessData({ init: false });
|
|
177
200
|
}
|
|
178
201
|
|
|
179
202
|
/**
|
|
@@ -187,18 +210,55 @@ export class ContentStore {
|
|
|
187
210
|
*/
|
|
188
211
|
async onContentSourceSchemaChange({ contentSourceId }: { contentSourceId: string }) {
|
|
189
212
|
this.logger.debug('onContentSourceSchemaChange', { contentSourceId });
|
|
190
|
-
|
|
191
|
-
this.contentSourceDataById[contentSourceId] = await this.loadContentSourceData({
|
|
192
|
-
contentSourceInstance: contentSourceData.instance,
|
|
193
|
-
init: false
|
|
194
|
-
});
|
|
195
|
-
this.onSchemaChangeCallback();
|
|
213
|
+
await this.reloadContentSourcesByIdAndProcessData({ contentSourceIds: [contentSourceId] });
|
|
196
214
|
}
|
|
197
215
|
|
|
198
216
|
async onFilesChange(updatedFiles: string[]): Promise<{ schemaChanged?: boolean; contentChanges: ContentStoreTypes.ContentChangeResult }> {
|
|
199
217
|
this.logger.debug('onFilesChange');
|
|
200
218
|
|
|
201
|
-
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
|
+
|
|
202
262
|
const contentChanges: ContentStoreTypes.ContentChangeResult = {
|
|
203
263
|
updatedDocuments: [],
|
|
204
264
|
updatedAssets: [],
|
|
@@ -206,49 +266,115 @@ export class ContentStore {
|
|
|
206
266
|
deletedAssets: []
|
|
207
267
|
};
|
|
208
268
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (schemaChanged) {
|
|
217
|
-
someContentSourceSchemaUpdated = true;
|
|
218
|
-
this.contentSourceDataById[contentSourceId] = await this.loadContentSourceData({ contentSourceInstance, init: false });
|
|
219
|
-
} 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 }) => {
|
|
220
276
|
const contentChangeResult = this.onContentChange(contentSourceId, contentChangeEvent);
|
|
221
277
|
contentChanges.updatedDocuments = contentChanges.updatedDocuments.concat(contentChangeResult.updatedDocuments);
|
|
222
278
|
contentChanges.updatedAssets = contentChanges.updatedAssets.concat(contentChangeResult.updatedAssets);
|
|
223
279
|
contentChanges.deletedDocuments = contentChanges.deletedDocuments.concat(contentChangeResult.deletedDocuments);
|
|
224
280
|
contentChanges.deletedAssets = contentChanges.deletedAssets.concat(contentChangeResult.deletedAssets);
|
|
225
|
-
|
|
281
|
+
return contentChanges;
|
|
282
|
+
}, contentChanges);
|
|
226
283
|
}
|
|
227
284
|
|
|
228
285
|
return {
|
|
229
|
-
schemaChanged:
|
|
286
|
+
schemaChanged: schemaChanged,
|
|
230
287
|
contentChanges: contentChanges
|
|
231
288
|
};
|
|
232
289
|
}
|
|
233
290
|
|
|
234
|
-
private async
|
|
235
|
-
|
|
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 });
|
|
236
328
|
|
|
237
329
|
this.contentUpdatesWatchTimer.stopTimer();
|
|
238
330
|
|
|
239
|
-
|
|
240
|
-
const contentSources: CSITypes.ContentSourceInterface[] = (this.rawStackbitConfig?.contentSources ?? []) as CSITypes.ContentSourceInterface[];
|
|
331
|
+
const contentSources = this.stackbitConfig?.contentSources ?? [];
|
|
241
332
|
|
|
242
333
|
const promises = contentSources.map((contentSourceInstance) => {
|
|
243
334
|
return this.loadContentSourceData({ contentSourceInstance, init });
|
|
244
335
|
});
|
|
245
336
|
|
|
246
|
-
const
|
|
247
|
-
const contentSourceDataById: Record<string, ContentSourceData> = _.keyBy(contentSourceDataArr, 'id');
|
|
337
|
+
const contentSourceRawDataArr = await Promise.all(promises);
|
|
248
338
|
|
|
249
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
|
+
});
|
|
250
346
|
this.contentSources = contentSources;
|
|
251
|
-
|
|
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
|
+
});
|
|
252
378
|
|
|
253
379
|
this.contentUpdatesWatchTimer.startTimer();
|
|
254
380
|
}
|
|
@@ -259,7 +385,7 @@ export class ContentStore {
|
|
|
259
385
|
}: {
|
|
260
386
|
contentSourceInstance: CSITypes.ContentSourceInterface;
|
|
261
387
|
init: boolean;
|
|
262
|
-
}): Promise<
|
|
388
|
+
}): Promise<ContentSourceRawData> {
|
|
263
389
|
const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
|
|
264
390
|
this.logger.debug('loadContentSourceData', { contentSourceId, init });
|
|
265
391
|
|
|
@@ -268,105 +394,40 @@ export class ContentStore {
|
|
|
268
394
|
logger: this.logger,
|
|
269
395
|
userLogger: this.userLogger,
|
|
270
396
|
userCommandSpawner: this.userCommandSpawner,
|
|
271
|
-
localDev: this.localDev
|
|
397
|
+
localDev: this.localDev,
|
|
398
|
+
webhookUrl: this.getWebhookUrl(contentSourceInstance.getContentSourceType(), contentSourceInstance.getProjectId()),
|
|
399
|
+
devAppRestartNeeded: this.devAppRestartNeeded
|
|
272
400
|
});
|
|
273
401
|
} else {
|
|
274
402
|
contentSourceInstance.stopWatchingContentUpdates();
|
|
275
403
|
await contentSourceInstance.reset();
|
|
276
404
|
}
|
|
277
405
|
|
|
278
|
-
// TODO: introduce optimization: don't fetch content source models,
|
|
279
|
-
// documents, assets if only stackbitConfig was changed
|
|
280
|
-
|
|
281
406
|
const csiModels = await contentSourceInstance.getModels();
|
|
282
|
-
const
|
|
283
|
-
const defaultLocaleCode = locales?.find((locale) => locale.default)?.code;
|
|
284
|
-
|
|
285
|
-
// for older versions of stackbit, it uses models to extend content source models
|
|
286
|
-
let modelsNoImage: Exclude<Model, ImageModel>[] = [];
|
|
287
|
-
let imageModel: ImageModel | undefined;
|
|
288
|
-
if (this.rawStackbitConfig) {
|
|
289
|
-
const result = await extendConfig({
|
|
290
|
-
config: this.rawStackbitConfig,
|
|
291
|
-
externalModels: csiModels
|
|
292
|
-
});
|
|
293
|
-
for (const error of result?.errors ?? []) {
|
|
294
|
-
this.userLogger.warn(error.message);
|
|
295
|
-
}
|
|
296
|
-
const config = await this.handleConfigAssets(result.config);
|
|
297
|
-
const modelsWithImageModel = config?.models ?? [];
|
|
298
|
-
const imageModelIndex = modelsWithImageModel.findIndex((model) => isImageModel(model));
|
|
299
|
-
if (imageModelIndex > -1) {
|
|
300
|
-
imageModel = modelsWithImageModel[imageModelIndex] as ImageModel;
|
|
301
|
-
modelsWithImageModel.splice(imageModelIndex, 1);
|
|
302
|
-
}
|
|
303
|
-
modelsNoImage = modelsWithImageModel as Exclude<Model, ImageModel>[];
|
|
304
|
-
|
|
305
|
-
// TODO: load presets externally from config, and create additional map
|
|
306
|
-
// that maps presetIds by model name instead of storing that map inside every model
|
|
307
|
-
|
|
308
|
-
// Augment presets with srcType and srcProjectId if they don't exist
|
|
309
|
-
this.presets = _.reduce(
|
|
310
|
-
Object.keys(config?.presets ?? {}),
|
|
311
|
-
(accum: Record<string, Preset>, presetId) => {
|
|
312
|
-
const preset = config?.presets?.[presetId];
|
|
313
|
-
_.set(accum, [presetId], {
|
|
314
|
-
...preset,
|
|
315
|
-
srcType: preset?.srcType ?? contentSourceInstance.getContentSourceType(),
|
|
316
|
-
srcProjectId: preset?.srcProjectId ?? contentSourceInstance.getProjectId()
|
|
317
|
-
});
|
|
318
|
-
return accum;
|
|
319
|
-
},
|
|
320
|
-
{}
|
|
321
|
-
);
|
|
322
|
-
}
|
|
407
|
+
const csiModelMap = _.keyBy(csiModels, 'name');
|
|
323
408
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const srcProjectId = contentSourceInstance.getProjectId();
|
|
327
|
-
const modelsWithSource = modelsNoImage.map((model) => ({
|
|
328
|
-
srcType,
|
|
329
|
-
srcProjectId,
|
|
330
|
-
...model
|
|
331
|
-
}));
|
|
332
|
-
const mappedModels = this.rawStackbitConfig.mapModels({
|
|
333
|
-
models: modelsWithSource
|
|
334
|
-
});
|
|
335
|
-
modelsNoImage = mappedModels.map((model) => {
|
|
336
|
-
const { srcType, srcProjectId, ...rest } = model;
|
|
337
|
-
return rest;
|
|
338
|
-
});
|
|
339
|
-
}
|
|
409
|
+
const locales = await contentSourceInstance.getLocales();
|
|
410
|
+
const defaultLocaleCode = locales?.find((locale) => locale.default)?.code;
|
|
340
411
|
|
|
341
|
-
const models: Model[] = imageModel ? [...modelsNoImage, imageModel] : modelsNoImage;
|
|
342
|
-
const modelMap = _.keyBy(models, 'name');
|
|
343
|
-
const csiModelMap = _.keyBy(csiModels, 'name');
|
|
344
412
|
const csiDocuments = await contentSourceInstance.getDocuments({ modelMap: csiModelMap });
|
|
345
413
|
const csiAssets = await contentSourceInstance.getAssets();
|
|
346
414
|
const csiDocumentMap = _.keyBy(csiDocuments, 'id');
|
|
347
415
|
const csiAssetMap = _.keyBy(csiAssets, 'id');
|
|
348
416
|
|
|
349
|
-
const contentStoreDocuments = mapCSIDocumentsToStoreDocuments({
|
|
350
|
-
csiDocuments,
|
|
351
|
-
contentSourceInstance,
|
|
352
|
-
modelMap,
|
|
353
|
-
defaultLocaleCode
|
|
354
|
-
});
|
|
355
417
|
const contentStoreAssets = mapCSIAssetsToStoreAssets({
|
|
356
418
|
csiAssets,
|
|
357
419
|
contentSourceInstance,
|
|
358
420
|
defaultLocaleCode
|
|
359
421
|
});
|
|
360
|
-
const documentMap = _.keyBy(contentStoreDocuments, 'srcObjectId');
|
|
361
422
|
const assetMap = _.keyBy(contentStoreAssets, 'srcObjectId');
|
|
362
423
|
|
|
363
424
|
this.logger.debug('loaded content source data', {
|
|
364
425
|
contentSourceId,
|
|
365
|
-
locales,
|
|
366
426
|
defaultLocaleCode,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
427
|
+
localesCount: locales.length,
|
|
428
|
+
modelCount: csiModels.length,
|
|
429
|
+
documentCount: csiDocuments.length,
|
|
430
|
+
assetCount: csiAssets.length
|
|
370
431
|
});
|
|
371
432
|
|
|
372
433
|
contentSourceInstance.startWatchingContentUpdates({
|
|
@@ -386,28 +447,21 @@ export class ContentStore {
|
|
|
386
447
|
},
|
|
387
448
|
onSchemaChange: async () => {
|
|
388
449
|
this.logger.debug('content source called onSchemaChange', { contentSourceId });
|
|
389
|
-
this.
|
|
390
|
-
contentSourceInstance: contentSourceInstance,
|
|
391
|
-
init: false
|
|
392
|
-
});
|
|
393
|
-
this.onSchemaChangeCallback();
|
|
450
|
+
await this.reloadContentSourcesByIdAndProcessData({ contentSourceIds: [contentSourceId] });
|
|
394
451
|
}
|
|
395
452
|
});
|
|
396
453
|
|
|
397
454
|
return {
|
|
398
455
|
id: contentSourceId,
|
|
399
|
-
|
|
400
|
-
|
|
456
|
+
srcType: contentSourceInstance.getContentSourceType(),
|
|
457
|
+
srcProjectId: contentSourceInstance.getProjectId(),
|
|
401
458
|
instance: contentSourceInstance,
|
|
402
459
|
locales: locales,
|
|
403
460
|
defaultLocaleCode: defaultLocaleCode,
|
|
404
|
-
|
|
405
|
-
modelMap: modelMap,
|
|
461
|
+
csiModels: csiModels,
|
|
406
462
|
csiModelMap: csiModelMap,
|
|
407
463
|
csiDocuments: csiDocuments,
|
|
408
464
|
csiDocumentMap: csiDocumentMap,
|
|
409
|
-
documents: contentStoreDocuments,
|
|
410
|
-
documentMap: documentMap,
|
|
411
465
|
csiAssets: csiAssets,
|
|
412
466
|
csiAssetMap: csiAssetMap,
|
|
413
467
|
assets: contentStoreAssets,
|
|
@@ -450,8 +504,8 @@ export class ContentStore {
|
|
|
450
504
|
}
|
|
451
505
|
|
|
452
506
|
result.deletedDocuments.push({
|
|
453
|
-
srcType: contentSourceData.
|
|
454
|
-
srcProjectId: contentSourceData.
|
|
507
|
+
srcType: contentSourceData.srcType,
|
|
508
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
455
509
|
srcObjectId: docId
|
|
456
510
|
});
|
|
457
511
|
});
|
|
@@ -471,15 +525,43 @@ export class ContentStore {
|
|
|
471
525
|
}
|
|
472
526
|
|
|
473
527
|
result.deletedAssets.push({
|
|
474
|
-
srcType: contentSourceData.
|
|
475
|
-
srcProjectId: contentSourceData.
|
|
528
|
+
srcType: contentSourceData.srcType,
|
|
529
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
476
530
|
srcObjectId: assetId
|
|
477
531
|
});
|
|
478
532
|
});
|
|
479
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
|
+
|
|
480
562
|
// map csi documents and assets to content store documents and assets
|
|
481
563
|
const documents = mapCSIDocumentsToStoreDocuments({
|
|
482
|
-
csiDocuments:
|
|
564
|
+
csiDocuments: mappedDocs,
|
|
483
565
|
contentSourceInstance: contentSourceData.instance,
|
|
484
566
|
modelMap: contentSourceData.modelMap,
|
|
485
567
|
defaultLocaleCode: contentSourceData.defaultLocaleCode
|
|
@@ -492,7 +574,7 @@ export class ContentStore {
|
|
|
492
574
|
|
|
493
575
|
// update contentSourceData with new or updated documents and assets
|
|
494
576
|
Object.assign(contentSourceData.csiDocumentMap, _.keyBy(contentChangeEvent.documents, 'id'));
|
|
495
|
-
Object.assign(contentSourceData.
|
|
577
|
+
Object.assign(contentSourceData.csiAssetMap, _.keyBy(contentChangeEvent.assets, 'id'));
|
|
496
578
|
Object.assign(contentSourceData.documentMap, _.keyBy(documents, 'srcObjectId'));
|
|
497
579
|
Object.assign(contentSourceData.assetMap, _.keyBy(assets, 'srcObjectId'));
|
|
498
580
|
|
|
@@ -510,8 +592,8 @@ export class ContentStore {
|
|
|
510
592
|
contentSourceData.csiDocuments.splice(dataIndex, 1, csiDocument);
|
|
511
593
|
}
|
|
512
594
|
result.updatedDocuments.push({
|
|
513
|
-
srcType: contentSourceData.
|
|
514
|
-
srcProjectId: contentSourceData.
|
|
595
|
+
srcType: contentSourceData.srcType,
|
|
596
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
515
597
|
srcObjectId: document.srcObjectId
|
|
516
598
|
});
|
|
517
599
|
}
|
|
@@ -530,8 +612,8 @@ export class ContentStore {
|
|
|
530
612
|
contentSourceData.csiAssets.splice(index, 1, csiAsset);
|
|
531
613
|
}
|
|
532
614
|
result.updatedAssets.push({
|
|
533
|
-
srcType: contentSourceData.
|
|
534
|
-
srcProjectId: contentSourceData.
|
|
615
|
+
srcType: contentSourceData.srcType,
|
|
616
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
535
617
|
srcObjectId: asset.srcObjectId
|
|
536
618
|
});
|
|
537
619
|
}
|
|
@@ -539,13 +621,95 @@ export class ContentStore {
|
|
|
539
621
|
return result;
|
|
540
622
|
}
|
|
541
623
|
|
|
542
|
-
|
|
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>>> {
|
|
543
706
|
return _.reduce(
|
|
544
707
|
this.contentSourceDataById,
|
|
545
|
-
(result: Record<string, Record<string, Record<string, Model>>>, contentSourceData) => {
|
|
708
|
+
(result: Record<string, Record<string, Record<string, Model | ImageModel>>>, contentSourceData) => {
|
|
546
709
|
const contentSourceType = contentSourceData.instance.getContentSourceType();
|
|
547
710
|
const srcProjectId = contentSourceData.instance.getProjectId();
|
|
548
711
|
_.set(result, [contentSourceType, srcProjectId], contentSourceData.modelMap);
|
|
712
|
+
_.set(result, [contentSourceType, srcProjectId, '__image_model'], IMAGE_MODEL);
|
|
549
713
|
return result;
|
|
550
714
|
},
|
|
551
715
|
{}
|
|
@@ -593,8 +757,8 @@ export class ContentStore {
|
|
|
593
757
|
return reducePromise(
|
|
594
758
|
contentSourceDataArr,
|
|
595
759
|
async (accum: ContentStoreTypes.HasAccessResult, contentSourceData) => {
|
|
596
|
-
const srcType = contentSourceData.
|
|
597
|
-
const srcProjectId = contentSourceData.
|
|
760
|
+
const srcType = contentSourceData.srcType;
|
|
761
|
+
const srcProjectId = contentSourceData.srcProjectId;
|
|
598
762
|
const userContext = getUserContextForSrcType(srcType, user);
|
|
599
763
|
let result = await contentSourceData.instance.hasAccess({ userContext });
|
|
600
764
|
// backwards compatibility with older CSI version
|
|
@@ -1278,15 +1442,15 @@ export class ContentStore {
|
|
|
1278
1442
|
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
1279
1443
|
locale = locale ?? contentSourceData.defaultLocaleCode;
|
|
1280
1444
|
const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
|
|
1281
|
-
const userContext = getUserContextForSrcType(contentSourceData.
|
|
1445
|
+
const userContext = getUserContextForSrcType(contentSourceData.srcType, user);
|
|
1282
1446
|
const internalValidationErrors = internalValidateContent(documents, assets, contentSourceData);
|
|
1283
1447
|
const validationResult = await contentSourceData.instance.validateDocuments({ documents, assets, locale, userContext });
|
|
1284
1448
|
errors = errors.concat(
|
|
1285
1449
|
internalValidationErrors,
|
|
1286
1450
|
validationResult.errors.map((validationError) => ({
|
|
1287
1451
|
message: validationError.message,
|
|
1288
|
-
srcType: contentSourceData.
|
|
1289
|
-
srcProjectId: contentSourceData.
|
|
1452
|
+
srcType: contentSourceData.srcType,
|
|
1453
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
1290
1454
|
srcObjectType: validationError.objectType,
|
|
1291
1455
|
srcObjectId: validationError.objectId,
|
|
1292
1456
|
fieldPath: validationError.fieldPath,
|
|
@@ -1336,7 +1500,7 @@ export class ContentStore {
|
|
|
1336
1500
|
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
1337
1501
|
|
|
1338
1502
|
documents.push(...contentSourceData.documents);
|
|
1339
|
-
_.set(schema, [contentSourceData.
|
|
1503
|
+
_.set(schema, [contentSourceData.srcType, contentSourceData.srcProjectId], contentSourceData.modelMap);
|
|
1340
1504
|
});
|
|
1341
1505
|
|
|
1342
1506
|
return searchDocuments({
|
|
@@ -1352,7 +1516,7 @@ export class ContentStore {
|
|
|
1352
1516
|
const objectsBySourceId = _.groupBy(objects, (object) => getContentSourceId(object.srcType, object.srcProjectId));
|
|
1353
1517
|
for (const [contentSourceId, contentSourceObjects] of Object.entries(objectsBySourceId)) {
|
|
1354
1518
|
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
1355
|
-
const userContext = getUserContextForSrcType(contentSourceData.
|
|
1519
|
+
const userContext = getUserContextForSrcType(contentSourceData.srcType, user);
|
|
1356
1520
|
const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
|
|
1357
1521
|
await contentSourceData.instance.publishDocuments({ documents, assets, userContext });
|
|
1358
1522
|
}
|
|
@@ -1365,6 +1529,19 @@ export class ContentStore {
|
|
|
1365
1529
|
}
|
|
1366
1530
|
return contentSourceData;
|
|
1367
1531
|
}
|
|
1532
|
+
|
|
1533
|
+
onWebhook({srcType, srcProjectId, data, headers}: {srcType: string; srcProjectId: string; data: unknown; headers: Record<string, string>}) {
|
|
1534
|
+
const contentSourceId = getContentSourceId(srcType, srcProjectId);
|
|
1535
|
+
const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
|
|
1536
|
+
return contentSourceData.instance.onWebhook?.({ data, headers });
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
getWebhookUrl(contentSourceType: string, projectId: string) {
|
|
1540
|
+
if (!this.webhookUrl) {
|
|
1541
|
+
return undefined;
|
|
1542
|
+
}
|
|
1543
|
+
return `${this.webhookUrl}/${encodeURIComponent(contentSourceType)}/${encodeURIComponent(projectId)}`
|
|
1544
|
+
}
|
|
1368
1545
|
}
|
|
1369
1546
|
|
|
1370
1547
|
function mapStoreFieldsToOperationFields({
|
|
@@ -1440,8 +1617,8 @@ function validateDocumentFields(
|
|
|
1440
1617
|
if (!objRef) {
|
|
1441
1618
|
errors.push({
|
|
1442
1619
|
fieldPath,
|
|
1443
|
-
srcType: contentSourceData.
|
|
1444
|
-
srcProjectId: contentSourceData.
|
|
1620
|
+
srcType: contentSourceData.srcType,
|
|
1621
|
+
srcProjectId: contentSourceData.srcProjectId,
|
|
1445
1622
|
srcObjectType: documentField.refType,
|
|
1446
1623
|
srcObjectId: document.id,
|
|
1447
1624
|
message: `Can't find referenced ${documentField.refType}: ${documentField.refId}`
|