@stackbit/cms-core 0.0.20 → 0.0.21-alpha.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.
Files changed (64) hide show
  1. package/dist/annotator/html.d.ts +1 -0
  2. package/dist/annotator/html.d.ts.map +1 -0
  3. package/dist/annotator/index.d.ts +3 -2
  4. package/dist/annotator/index.d.ts.map +1 -0
  5. package/dist/annotator/react.d.ts +1 -0
  6. package/dist/annotator/react.d.ts.map +1 -0
  7. package/dist/common/common-schema.d.ts +3 -10
  8. package/dist/common/common-schema.d.ts.map +1 -0
  9. package/dist/common/common-schema.js +3 -4
  10. package/dist/common/common-schema.js.map +1 -1
  11. package/dist/common/common-types.d.ts +10 -0
  12. package/dist/common/common-types.d.ts.map +1 -0
  13. package/dist/common/common-types.js +3 -0
  14. package/dist/common/common-types.js.map +1 -0
  15. package/dist/consts.d.ts +1 -0
  16. package/dist/consts.d.ts.map +1 -0
  17. package/dist/content-source-interface.d.ts +338 -0
  18. package/dist/content-source-interface.d.ts.map +1 -0
  19. package/dist/content-source-interface.js +28 -0
  20. package/dist/content-source-interface.js.map +1 -0
  21. package/dist/content-store-types.d.ts +347 -0
  22. package/dist/content-store-types.d.ts.map +1 -0
  23. package/dist/content-store-types.js +3 -0
  24. package/dist/content-store-types.js.map +1 -0
  25. package/dist/content-store.d.ts +210 -0
  26. package/dist/content-store.d.ts.map +1 -0
  27. package/dist/content-store.js +1810 -0
  28. package/dist/content-store.js.map +1 -0
  29. package/dist/encoder.d.ts +36 -7
  30. package/dist/encoder.d.ts.map +1 -0
  31. package/dist/encoder.js +63 -40
  32. package/dist/encoder.js.map +1 -1
  33. package/dist/index.d.ts +12 -6
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +38 -11
  36. package/dist/index.js.map +1 -1
  37. package/dist/stackbit/index.d.ts +7 -3
  38. package/dist/stackbit/index.d.ts.map +1 -0
  39. package/dist/stackbit/index.js +13 -10
  40. package/dist/stackbit/index.js.map +1 -1
  41. package/dist/utils/index.d.ts +8 -7
  42. package/dist/utils/index.d.ts.map +1 -0
  43. package/dist/utils/schema-utils.d.ts +3 -2
  44. package/dist/utils/schema-utils.d.ts.map +1 -0
  45. package/dist/utils/timer.d.ts +18 -0
  46. package/dist/utils/timer.d.ts.map +1 -0
  47. package/dist/utils/timer.js +36 -0
  48. package/dist/utils/timer.js.map +1 -0
  49. package/package.json +8 -4
  50. package/src/common/common-schema.ts +12 -0
  51. package/src/common/common-types.ts +10 -0
  52. package/src/content-source-interface.ts +495 -0
  53. package/src/content-store-types.ts +430 -0
  54. package/src/content-store.ts +2329 -0
  55. package/src/{encoder.js → encoder.ts} +55 -17
  56. package/src/index.ts +11 -0
  57. package/src/stackbit/{index.js → index.ts} +5 -9
  58. package/src/utils/timer.ts +42 -0
  59. package/dist/utils/lazy-poller.d.ts +0 -21
  60. package/dist/utils/lazy-poller.js +0 -56
  61. package/dist/utils/lazy-poller.js.map +0 -1
  62. package/src/common/common-schema.js +0 -14
  63. package/src/index.js +0 -13
  64. package/src/utils/lazy-poller.ts +0 -74
@@ -0,0 +1,2329 @@
1
+ import _ from 'lodash';
2
+ import slugify from 'slugify';
3
+ import path from 'path';
4
+ import sanitizeFilename from 'sanitize-filename';
5
+ import {
6
+ Config,
7
+ extendConfig,
8
+ Field,
9
+ FieldList,
10
+ FieldListItems,
11
+ FieldListProps,
12
+ FieldModelProps,
13
+ FieldObjectProps,
14
+ FieldSpecificProps,
15
+ loadConfigFromDir,
16
+ Model
17
+ } from '@stackbit/sdk';
18
+ import { mapPromise, omitByNil } from '@stackbit/utils';
19
+ import * as CSITypes from './content-source-interface';
20
+ import { isLocalizedField, getLocalizedFieldForLocale } from './content-source-interface';
21
+ import * as ContentStoreTypes from './content-store-types';
22
+ import { IMAGE_MODEL } from './common/common-schema';
23
+ import { Timer } from './utils/timer';
24
+ import { UserCommandSpawner } from './common/common-types';
25
+
26
+ export interface ContentSourceOptions {
27
+ logger: ContentStoreTypes.Logger;
28
+ userLogger: ContentStoreTypes.Logger;
29
+ localDev: boolean;
30
+ stackbitYamlDir: string;
31
+ contentSources: CSITypes.ContentSourceInterface[];
32
+ userCommandSpawner?: UserCommandSpawner;
33
+ onSchemaChangeCallback: () => void;
34
+ onContentChangeCallback: (contentChanges: ContentStoreTypes.ContentChangeResult) => void;
35
+ handleConfigAssets: (config: Config) => Promise<Config>;
36
+ }
37
+
38
+ interface ContentSourceData {
39
+ id: string;
40
+ instance: CSITypes.ContentSourceInterface;
41
+ type: string;
42
+ projectId: string;
43
+ locales?: CSITypes.Locale[];
44
+ defaultLocaleCode?: string;
45
+ /* Array of extended and validated Models */
46
+ models: Model[];
47
+ /* Map of extended and validated Models by model name */
48
+ modelMap: Record<string, Model>;
49
+ /* Map of original Models (as provided by content source) by model name */
50
+ csiModelMap: Record<string, Model>;
51
+ /* Array of original content source Documents */
52
+ csiDocuments: CSITypes.Document[];
53
+ /* Map of original content source Documents by document ID */
54
+ csiDocumentMap: Record<string, CSITypes.Document>;
55
+ /* Array of converted content-store Documents */
56
+ documents: ContentStoreTypes.Document[];
57
+ /* Map of converted content-store Documents by document ID */
58
+ documentMap: Record<string, ContentStoreTypes.Document>;
59
+ /* Array of original content source Assets */
60
+ csiAssets: CSITypes.Asset[];
61
+ /* Map of original content source Assets by asset ID */
62
+ csiAssetMap: Record<string, CSITypes.Asset>;
63
+ /* Array of converted content-store Assets */
64
+ assets: ContentStoreTypes.Asset[];
65
+ /* Map of converted content-store Assets by asset ID */
66
+ assetMap: Record<string, ContentStoreTypes.Asset>;
67
+ }
68
+
69
+ export class ContentStore {
70
+ private readonly contentSources: CSITypes.ContentSourceInterface[];
71
+ private readonly logger: ContentStoreTypes.Logger;
72
+ private readonly userLogger: ContentStoreTypes.Logger;
73
+ private readonly userCommandSpawner?: UserCommandSpawner;
74
+ private readonly localDev: boolean;
75
+ private readonly stackbitYamlDir: string; // TODO: remove stackbitYamlDir, ContentStore should not be aware of filesystem, instead pass autoreloading Config object
76
+ private readonly onSchemaChangeCallback: () => void;
77
+ private readonly onContentChangeCallback: (contentChanges: ContentStoreTypes.ContentChangeResult) => void;
78
+ private readonly handleConfigAssets: (config: Config) => Promise<Config>;
79
+ private contentSourceDataById: Record<string, ContentSourceData> = {};
80
+ private contentUpdatesWatchTimer: Timer;
81
+ private rawStackbitConfig: any;
82
+ private presets?: Record<string, any>;
83
+
84
+ constructor(options: ContentSourceOptions) {
85
+ this.logger = options.logger.createLogger({ label: 'content-store' });
86
+ this.userLogger = options.userLogger.createLogger({ label: 'content-store' });
87
+ this.localDev = options.localDev;
88
+ this.userCommandSpawner = options.userCommandSpawner;
89
+ this.stackbitYamlDir = options.stackbitYamlDir; // TODO: remove stackbitYamlDir, ContentStore should not be aware of filesystem, instead pass autoreloading Config object
90
+ this.contentSources = options.contentSources;
91
+ this.onSchemaChangeCallback = options.onSchemaChangeCallback;
92
+ this.onContentChangeCallback = options.onContentChangeCallback;
93
+ this.handleConfigAssets = options.handleConfigAssets;
94
+ this.contentUpdatesWatchTimer = new Timer({ timerCallback: () => this.handleTimerTimeout(), logger: this.logger });
95
+ }
96
+
97
+ async init(): Promise<void> {
98
+ this.logger.debug('init');
99
+ this.rawStackbitConfig = await this.loadStackbitConfig();
100
+
101
+ this.logger.debug('init => load content source data');
102
+ for (const contentSourceInstance of this.contentSources) {
103
+ await this.loadContentSourceData({ contentSourceInstance, init: true });
104
+ }
105
+
106
+ this.contentUpdatesWatchTimer.startTimer();
107
+ }
108
+
109
+ async reset() {
110
+ this.logger.debug('reset');
111
+
112
+ this.contentUpdatesWatchTimer.stopTimer();
113
+ this.rawStackbitConfig = await this.loadStackbitConfig();
114
+
115
+ this.logger.debug('reset => load content source data');
116
+ for (const contentSourceInstance of this.contentSources) {
117
+ await this.loadContentSourceData({ contentSourceInstance, init: false });
118
+ }
119
+
120
+ this.contentUpdatesWatchTimer.startTimer();
121
+ }
122
+
123
+ /**
124
+ * This method is called when contentUpdatesWatchTimer receives timeout.
125
+ * It then notifies all content sources to stop watching for content changes.
126
+ */
127
+ handleTimerTimeout() {
128
+ for (const contentSourceInstance of this.contentSources) {
129
+ contentSourceInstance.stopWatchingContentUpdates();
130
+ }
131
+ }
132
+
133
+ /**
134
+ * This method is called when user interacts with Stackbit application.
135
+ * It is used to reset contentUpdatesWatchTimer. When the timer is over
136
+ * all content sources are notified to stop watching for content updates.
137
+ */
138
+ async keepAlive() {
139
+ if (!this.contentUpdatesWatchTimer.isRunning()) {
140
+ this.logger.debug('keepAlive => contentUpdatesWatchTimer is not running => load content source data');
141
+ for (const contentSourceInstance of this.contentSources) {
142
+ await this.loadContentSourceData({ contentSourceInstance, init: false });
143
+ }
144
+ }
145
+ this.contentUpdatesWatchTimer.resetTimer();
146
+ }
147
+
148
+ /**
149
+ * This method is called when a content source notifies Stackbit of models
150
+ * changes via webhook. When this happens, all content source data
151
+ *
152
+ * For example, Contentful notifies Stackbit of any content-type changes via
153
+ * special webhook.
154
+ *
155
+ * @param contentSourceId
156
+ */
157
+ async onContentSourceSchemaChange({ contentSourceId }: { contentSourceId: string }) {
158
+ this.logger.debug('onContentSourceSchemaChange', { contentSourceId });
159
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
160
+ await this.loadContentSourceData({
161
+ contentSourceInstance: contentSourceData.instance,
162
+ init: false
163
+ });
164
+ this.onSchemaChangeCallback();
165
+ }
166
+
167
+ async onFilesChange(
168
+ updatedFiles: string[]
169
+ ): Promise<{ stackbitConfigUpdated?: boolean; schemaChanged?: boolean; contentChanges: ContentStoreTypes.ContentChangeResult }> {
170
+ this.logger.debug('onFilesChange');
171
+
172
+ const stackbitConfigUpdated = _.some(updatedFiles, (filePath) => isStackbitConfigFile(filePath));
173
+ this.logger.debug(`stackbitConfigUpdated: ${stackbitConfigUpdated}`);
174
+ if (stackbitConfigUpdated) {
175
+ this.rawStackbitConfig = await this.loadStackbitConfig();
176
+ }
177
+
178
+ let someContentSourceSchemaUpdated = false;
179
+ const contentChanges: ContentStoreTypes.ContentChangeResult = {
180
+ updatedDocuments: [],
181
+ updatedAssets: [],
182
+ deletedDocuments: [],
183
+ deletedAssets: []
184
+ };
185
+
186
+ for (const contentSourceInstance of this.contentSources) {
187
+ const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
188
+ this.logger.debug(`call onFilesChange for contentSource: ${contentSourceId}`);
189
+ const { schemaChanged, contentChangeEvent } = contentSourceInstance.onFilesChange?.({ updatedFiles: updatedFiles }) ?? {};
190
+ this.logger.debug(`schemaChanged: ${schemaChanged}, has contentChangeEvent: ${!!contentChangeEvent}`);
191
+ // if schema is changed, there is no need to return contentChanges
192
+ // because schema changes reloads everything and implies content changes
193
+ if (stackbitConfigUpdated || schemaChanged) {
194
+ if (schemaChanged) {
195
+ someContentSourceSchemaUpdated = true;
196
+ }
197
+ await this.loadContentSourceData({ contentSourceInstance, init: false });
198
+ } else if (contentChangeEvent) {
199
+ const contentChangeResult = this.onContentChange(contentSourceId, contentChangeEvent);
200
+ contentChanges.updatedDocuments = contentChanges.updatedDocuments.concat(contentChangeResult.updatedDocuments);
201
+ contentChanges.updatedAssets = contentChanges.updatedAssets.concat(contentChangeResult.updatedAssets);
202
+ contentChanges.deletedDocuments = contentChanges.deletedDocuments.concat(contentChangeResult.deletedDocuments);
203
+ contentChanges.deletedAssets = contentChanges.deletedAssets.concat(contentChangeResult.deletedAssets);
204
+ }
205
+ }
206
+
207
+ return {
208
+ stackbitConfigUpdated,
209
+ schemaChanged: someContentSourceSchemaUpdated || stackbitConfigUpdated,
210
+ contentChanges: contentChanges
211
+ };
212
+ }
213
+
214
+ async loadStackbitConfig() {
215
+ this.logger.debug('loadStackbitConfig');
216
+ // TODO: use esbuild to watch for stackbit.config.js changes and notify
217
+ // the content-store via onStackbitConfigChange with updated rawConfig
218
+ const { config, errors } = await loadConfigFromDir({ dirPath: this.stackbitYamlDir });
219
+ for (const error of errors) {
220
+ this.userLogger.warn(error.message);
221
+ }
222
+ if (!config) {
223
+ this.userLogger.error(`could not load stackbit config`);
224
+ }
225
+ return config;
226
+ }
227
+
228
+ async loadContentSourceData({ contentSourceInstance, init }: { contentSourceInstance: CSITypes.ContentSourceInterface; init: boolean }) {
229
+ // TODO: defer loading content if content is already loading for a specific content source
230
+
231
+ // TODO: optimize: cache raw responses from contentSource
232
+ // e.g.: getModels(), getDocuments(), getAssets()
233
+ // ana use the cached response to remap documents when only stackbitConfig changes
234
+
235
+ const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
236
+ this.logger.debug('loadContentSourceData', { contentSourceId, init });
237
+
238
+ if (init) {
239
+ await contentSourceInstance.init({
240
+ logger: this.logger,
241
+ userLogger: this.userLogger,
242
+ userCommandSpawner: this.userCommandSpawner,
243
+ localDev: this.localDev
244
+ });
245
+ } else {
246
+ contentSourceInstance.stopWatchingContentUpdates();
247
+ await contentSourceInstance.reset();
248
+ }
249
+
250
+ const csiModels = await contentSourceInstance.getModels();
251
+ const locales = await contentSourceInstance?.getLocales();
252
+ const result = await extendConfig({ dirPath: this.stackbitYamlDir, config: this.rawStackbitConfig, externalModels: csiModels });
253
+ const config = await this.handleConfigAssets(result.config);
254
+ const models = config.models;
255
+ const modelMap = _.keyBy(models, 'name');
256
+ const defaultLocaleCode = locales?.find((locale) => locale.default)?.code;
257
+
258
+ for (const error of result.errors) {
259
+ this.userLogger.warn(error.message);
260
+ }
261
+
262
+ // TODO: load presets externally from config, and create additional map
263
+ // that maps presetIds by model name instead of storing that map inside every model
264
+ this.presets = config.presets;
265
+
266
+ const csiDocuments = await contentSourceInstance.getDocuments({ modelMap });
267
+ const csiAssets = await contentSourceInstance.getAssets();
268
+ const csiDocumentMap = _.keyBy(csiDocuments, 'id');
269
+ const csiAssetMap = _.keyBy(csiAssets, 'id');
270
+
271
+ const contentStoreDocuments = mapCSIDocumentsToStoreDocuments({
272
+ csiDocuments,
273
+ contentSourceInstance,
274
+ modelMap,
275
+ defaultLocaleCode
276
+ });
277
+ const contentStoreAssets = mapCSIAssetsToStoreAssets({
278
+ csiAssets,
279
+ contentSourceInstance,
280
+ defaultLocaleCode
281
+ });
282
+ const documentMap = _.keyBy(contentStoreDocuments, 'srcObjectId');
283
+ const assetMap = _.keyBy(contentStoreAssets, 'srcObjectId');
284
+
285
+ this.logger.debug('loaded content source data', {
286
+ contentSourceId,
287
+ locales,
288
+ defaultLocaleCode,
289
+ modelCount: models.length,
290
+ documentCount: contentStoreDocuments.length,
291
+ assetCount: contentStoreAssets.length
292
+ });
293
+
294
+ this.contentSourceDataById[contentSourceId] = {
295
+ id: contentSourceId,
296
+ type: contentSourceInstance.getContentSourceType(),
297
+ projectId: contentSourceInstance.getProjectId(),
298
+ instance: contentSourceInstance,
299
+ locales: locales,
300
+ defaultLocaleCode: defaultLocaleCode,
301
+ models: models,
302
+ modelMap: modelMap,
303
+ csiModelMap: _.keyBy(csiModels, 'name'),
304
+ csiDocuments: csiDocuments,
305
+ csiDocumentMap: csiDocumentMap,
306
+ documents: contentStoreDocuments,
307
+ documentMap: documentMap,
308
+ csiAssets: csiAssets,
309
+ csiAssetMap: csiAssetMap,
310
+ assets: contentStoreAssets,
311
+ assetMap: assetMap
312
+ };
313
+
314
+ contentSourceInstance.startWatchingContentUpdates({
315
+ getModelMap: () => {
316
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
317
+ return contentSourceData.csiModelMap;
318
+ },
319
+ getDocument({ documentId }: { documentId: string }) {
320
+ return csiDocumentMap[documentId];
321
+ },
322
+ getAsset({ assetId }: { assetId: string }) {
323
+ return csiAssetMap[assetId];
324
+ },
325
+ onContentChange: (contentChangeEvent: CSITypes.ContentChangeEvent) => {
326
+ this.logger.debug('content source called onContentChange', { contentSourceId });
327
+ const result = this.onContentChange(contentSourceId, contentChangeEvent);
328
+ this.onContentChangeCallback(result);
329
+ },
330
+ onSchemaChange: async () => {
331
+ this.logger.debug('content source called onSchemaChange', { contentSourceId });
332
+ await this.loadContentSourceData({
333
+ contentSourceInstance: contentSourceInstance,
334
+ init: false
335
+ });
336
+ this.onSchemaChangeCallback();
337
+ }
338
+ });
339
+ }
340
+
341
+ onContentChange(contentSourceId: string, contentChangeEvent: CSITypes.ContentChangeEvent): ContentStoreTypes.ContentChangeResult {
342
+ // TODO: prevent content change process for contentSourceId if loading content is in progress
343
+
344
+ this.logger.debug('onContentChange', {
345
+ contentSourceId,
346
+ documentCount: contentChangeEvent.documents.length,
347
+ assetCount: contentChangeEvent.assets.length,
348
+ deletedDocumentCount: contentChangeEvent.deletedDocumentIds.length,
349
+ deletedAssetCount: contentChangeEvent.deletedAssetIds.length
350
+ });
351
+
352
+ const result: ContentStoreTypes.ContentChangeResult = {
353
+ updatedDocuments: [],
354
+ updatedAssets: [],
355
+ deletedDocuments: [],
356
+ deletedAssets: []
357
+ };
358
+
359
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
360
+
361
+ // update contentSourceData with deleted documents
362
+ contentChangeEvent.deletedDocumentIds.forEach((docId) => {
363
+ // delete document from documents map
364
+ delete contentSourceData.documentMap[docId];
365
+ delete contentSourceData.csiDocumentMap[docId];
366
+
367
+ // delete document from document array
368
+ const index = contentSourceData.documents.findIndex((document) => document.srcObjectId === docId);
369
+ if (index !== -1) {
370
+ // the indexes of documents and csiDocuments are always the same as they are always updated at the same time
371
+ contentSourceData.documents.splice(index, 1);
372
+ contentSourceData.csiDocuments.splice(index, 1);
373
+ }
374
+
375
+ result.deletedDocuments.push({
376
+ srcType: contentSourceData.type,
377
+ srcProjectId: contentSourceData.projectId,
378
+ srcObjectId: docId
379
+ });
380
+ });
381
+
382
+ // update contentSourceData with deleted assets
383
+ contentChangeEvent.deletedAssetIds.forEach((assetId) => {
384
+ // delete document from asset map
385
+ delete contentSourceData.assetMap[assetId];
386
+ delete contentSourceData.csiAssetMap[assetId];
387
+
388
+ // delete document from asset array
389
+ const index = contentSourceData.assets.findIndex((asset) => asset.srcObjectId === assetId);
390
+ if (index !== -1) {
391
+ // the indexes of assets and csiAssets are always the same as they are always updated at the same time
392
+ contentSourceData.assets.splice(index, 1);
393
+ contentSourceData.csiAssets.splice(index, 1);
394
+ }
395
+
396
+ result.deletedAssets.push({
397
+ srcType: contentSourceData.type,
398
+ srcProjectId: contentSourceData.projectId,
399
+ srcObjectId: assetId
400
+ });
401
+ });
402
+
403
+ // map csi documents and assets to content store documents and assets
404
+ const documents = mapCSIDocumentsToStoreDocuments({
405
+ csiDocuments: contentChangeEvent.documents,
406
+ contentSourceInstance: contentSourceData.instance,
407
+ modelMap: contentSourceData.modelMap,
408
+ defaultLocaleCode: contentSourceData.defaultLocaleCode
409
+ });
410
+ const assets = mapCSIAssetsToStoreAssets({
411
+ csiAssets: contentChangeEvent.assets,
412
+ contentSourceInstance: contentSourceData.instance,
413
+ defaultLocaleCode: contentSourceData.defaultLocaleCode
414
+ });
415
+
416
+ // update contentSourceData with new or updated documents and assets
417
+ Object.assign(contentSourceData.csiDocumentMap, _.keyBy(contentChangeEvent.documents, 'id'));
418
+ Object.assign(contentSourceData.csiAssets, _.keyBy(contentChangeEvent.assets, 'id'));
419
+ Object.assign(contentSourceData.documentMap, _.keyBy(documents, 'srcObjectId'));
420
+ Object.assign(contentSourceData.assetMap, _.keyBy(assets, 'srcObjectId'));
421
+
422
+ for (let idx = 0; idx < documents.length; idx++) {
423
+ // the indexes of mapped documents and documents from changeEvent are the same
424
+ const document = documents[idx]!;
425
+ const csiDocument = contentChangeEvent.documents[idx]!;
426
+ const dataIndex = contentSourceData.documents.findIndex((existingDoc) => existingDoc.srcObjectId === document.srcObjectId);
427
+ if (dataIndex === -1) {
428
+ contentSourceData.documents.push(document);
429
+ contentSourceData.csiDocuments.push(csiDocument);
430
+ } else {
431
+ // the indexes of documents and csiDocuments are always the same as they are always updated at the same time
432
+ contentSourceData.documents.splice(dataIndex, 1, document);
433
+ contentSourceData.csiDocuments.splice(dataIndex, 1, csiDocument);
434
+ }
435
+ result.updatedDocuments.push({
436
+ srcType: contentSourceData.type,
437
+ srcProjectId: contentSourceData.projectId,
438
+ srcObjectId: document.srcObjectId
439
+ });
440
+ }
441
+
442
+ for (let idx = 0; idx < assets.length; idx++) {
443
+ // the indexes of mapped assets and assets from changeEvent are the same
444
+ const asset = assets[idx]!;
445
+ const csiAsset = contentChangeEvent.assets[idx]!;
446
+ const index = contentSourceData.assets.findIndex((existingAsset) => existingAsset.srcObjectId === asset.srcObjectId);
447
+ if (index === -1) {
448
+ contentSourceData.assets.push(asset);
449
+ contentSourceData.csiAssets.push(csiAsset);
450
+ } else {
451
+ // the indexes of assets and csiAssets are always the same as they are always updated at the same time
452
+ contentSourceData.assets.splice(index, 1, asset);
453
+ contentSourceData.csiAssets.splice(index, 1, csiAsset);
454
+ }
455
+ result.updatedAssets.push({
456
+ srcType: contentSourceData.type,
457
+ srcProjectId: contentSourceData.projectId,
458
+ srcObjectId: asset.srcObjectId
459
+ });
460
+ }
461
+
462
+ return result;
463
+ }
464
+
465
+ getModels(): Record<string, Record<string, Record<string, Model>>> {
466
+ return _.reduce(
467
+ this.contentSourceDataById,
468
+ (result: Record<string, Record<string, Record<string, Model>>>, contentSourceData) => {
469
+ const contentSourceType = contentSourceData.instance.getContentSourceType();
470
+ const srcProjectId = contentSourceData.instance.getProjectId();
471
+ _.set(result, [contentSourceType, srcProjectId], contentSourceData.modelMap);
472
+ return result;
473
+ },
474
+ {}
475
+ );
476
+ }
477
+
478
+ getLocales(): string[] {
479
+ return _.reduce(
480
+ this.contentSourceDataById,
481
+ (result: string[], contentSourceData) => {
482
+ const locales = (contentSourceData.locales ?? []).map((locale) => locale.code);
483
+ return result.concat(locales);
484
+ },
485
+ []
486
+ );
487
+ }
488
+
489
+ getPresets(): Record<string, any> {
490
+ return this.presets ?? {};
491
+ }
492
+
493
+ getContentSourceEnvironment({ srcProjectId, srcType }: { srcProjectId: string; srcType: string }): string {
494
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
495
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
496
+ return contentSourceData.instance.getProjectEnvironment();
497
+ }
498
+
499
+ hasAccess({ srcType, srcProjectId, user }: { srcType: string; srcProjectId: string; user?: ContentStoreTypes.User }): Promise<boolean> {
500
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
501
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
502
+ const userContext = getUserContextForSrcType(srcType, user);
503
+ return contentSourceData.instance.hasAccess({ userContext });
504
+ }
505
+
506
+ hasChanges({
507
+ srcType,
508
+ srcProjectId,
509
+ documents
510
+ }: {
511
+ srcType?: string;
512
+ srcProjectId?: string;
513
+ documents?: { srcType: string; srcProjectId: string; srcObjectId: string }[];
514
+ }): {
515
+ hasChanges: boolean;
516
+ changedObjects: {
517
+ srcType: string;
518
+ srcProjectId: string;
519
+ srcObjectId: string;
520
+ }[];
521
+ } {
522
+ let result: (ContentStoreTypes.Document | ContentStoreTypes.Asset)[];
523
+ if (srcType && srcProjectId) {
524
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
525
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
526
+ result = [...contentSourceData.documents, ...contentSourceData.assets];
527
+ } else if (documents && documents.length > 0) {
528
+ const documentsBySourceId = _.groupBy(documents, (document) => getContentSourceId(document.srcType, document.srcProjectId));
529
+ result = _.reduce(
530
+ documentsBySourceId,
531
+ (result: (ContentStoreTypes.Document | ContentStoreTypes.Asset)[], documents, contentSourceId) => {
532
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
533
+ for (const document of documents) {
534
+ if (document.srcObjectId in contentSourceData.documentMap) {
535
+ result.push(contentSourceData.documentMap[document.srcObjectId]!);
536
+ } else if (document.srcObjectId in contentSourceData.assetMap) {
537
+ result.push(contentSourceData.assetMap[document.srcObjectId]!);
538
+ }
539
+ }
540
+ return result;
541
+ },
542
+ []
543
+ );
544
+ } else {
545
+ result = _.reduce(
546
+ this.contentSourceDataById,
547
+ (result: (ContentStoreTypes.Document | ContentStoreTypes.Asset)[], contentSourceData) => {
548
+ return result.concat(contentSourceData.documents, contentSourceData.assets);
549
+ },
550
+ []
551
+ );
552
+ }
553
+ const changedDocuments = result.filter((document) => document.status === 'added' || document.status === 'modified');
554
+ return {
555
+ hasChanges: !_.isEmpty(changedDocuments),
556
+ changedObjects: changedDocuments.map((item) => ({
557
+ srcType: item.srcType,
558
+ srcProjectId: item.srcProjectId,
559
+ srcObjectId: item.srcObjectId
560
+ }))
561
+ };
562
+ }
563
+
564
+ getDocument({
565
+ srcDocumentId,
566
+ srcProjectId,
567
+ srcType
568
+ }: {
569
+ srcDocumentId: string;
570
+ srcProjectId: string;
571
+ srcType: string;
572
+ }): ContentStoreTypes.Document | undefined {
573
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
574
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
575
+ return contentSourceData.documentMap[srcDocumentId];
576
+ }
577
+
578
+ getDocuments(): ContentStoreTypes.Document[] {
579
+ return _.reduce(
580
+ this.contentSourceDataById,
581
+ (documents: ContentStoreTypes.Document[], contentSourceData) => {
582
+ return documents.concat(contentSourceData.documents);
583
+ },
584
+ []
585
+ );
586
+ }
587
+
588
+ getAsset({ srcAssetId, srcProjectId, srcType }: { srcAssetId: string; srcProjectId: string; srcType: string }): ContentStoreTypes.Asset | undefined {
589
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
590
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
591
+ return contentSourceData.assetMap[srcAssetId];
592
+ }
593
+
594
+ getAssets(): ContentStoreTypes.Asset[] {
595
+ return _.reduce(
596
+ this.contentSourceDataById,
597
+ (assets: ContentStoreTypes.Asset[], contentSourceData) => {
598
+ return assets.concat(contentSourceData.assets);
599
+ },
600
+ []
601
+ );
602
+ }
603
+
604
+ getLocalizedApiObjects({ locale }: { locale?: string }): ContentStoreTypes.APIObject[] {
605
+ return _.reduce(
606
+ this.contentSourceDataById,
607
+ (objects: ContentStoreTypes.APIObject[], contentSourceData) => {
608
+ locale = locale ?? contentSourceData.defaultLocaleCode;
609
+ const documentObjects = mapDocumentsToLocalizedApiObjects(contentSourceData.documents, locale);
610
+ const imageObjects = mapAssetsToLocalizedApiImages(contentSourceData.assets, locale);
611
+ return objects.concat(documentObjects, imageObjects);
612
+ },
613
+ []
614
+ );
615
+ }
616
+
617
+ getApiAssets({
618
+ srcType,
619
+ srcProjectId,
620
+ pageSize = 20,
621
+ pageNum = 1,
622
+ searchQuery
623
+ }: { srcType?: string; srcProjectId?: string; pageSize?: number; pageNum?: number; searchQuery?: string } = {}): {
624
+ assets: ContentStoreTypes.APIAsset[];
625
+ pageSize: number;
626
+ pageNum: number;
627
+ totalPages: number;
628
+ } {
629
+ let assets: ContentStoreTypes.APIAsset[];
630
+ if (srcProjectId && srcType) {
631
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
632
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
633
+ assets = mapStoreAssetsToAPIAssets(contentSourceData.assets, contentSourceData.defaultLocaleCode);
634
+ } else {
635
+ assets = _.reduce(
636
+ this.contentSourceDataById,
637
+ (result: ContentStoreTypes.APIAsset[], contentSourceData) => {
638
+ const assets = mapStoreAssetsToAPIAssets(contentSourceData.assets, contentSourceData.defaultLocaleCode);
639
+ return result.concat(assets);
640
+ },
641
+ []
642
+ );
643
+ }
644
+
645
+ let filteredFiles = assets;
646
+ if (searchQuery) {
647
+ const sanitizedSearchQuery = sanitizeFilename(searchQuery).toLowerCase();
648
+ filteredFiles = assets.filter((asset) => asset.fileName && path.basename(asset.fileName).toLowerCase().includes(sanitizedSearchQuery));
649
+ }
650
+ const sortedAssets = _.orderBy(filteredFiles, ['fileName'], ['asc']);
651
+ const skip = (pageNum - 1) * pageSize;
652
+ const totalPages = Math.ceil(filteredFiles.length / pageSize);
653
+ const pagesAssets = sortedAssets.slice(skip, skip + pageSize);
654
+
655
+ return {
656
+ assets: pagesAssets,
657
+ pageSize: pageSize,
658
+ pageNum: pageNum,
659
+ totalPages: totalPages
660
+ };
661
+ }
662
+
663
+ async createAndLinkDocument({
664
+ srcType,
665
+ srcProjectId,
666
+ srcDocumentId,
667
+ fieldPath,
668
+ modelName,
669
+ object,
670
+ index,
671
+ locale,
672
+ user
673
+ }: {
674
+ srcType: string;
675
+ srcProjectId: string;
676
+ srcDocumentId: string;
677
+ fieldPath: (string | number)[];
678
+ modelName?: string;
679
+ object?: Record<string, any>;
680
+ index?: number;
681
+ locale?: string;
682
+ user?: ContentStoreTypes.User;
683
+ }): Promise<{ srcDocumentId: string }> {
684
+ this.logger.debug('createAndLinkDocument', { srcType, srcProjectId, srcDocumentId, fieldPath, modelName, index, locale });
685
+
686
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
687
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
688
+
689
+ // get the document that is being updated
690
+ const document = contentSourceData.documentMap[srcDocumentId];
691
+ const csiDocument = contentSourceData.csiDocumentMap[srcDocumentId];
692
+ if (!document || !csiDocument) {
693
+ throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
694
+ }
695
+
696
+ // get the document model
697
+ const documentModelName = document.srcModelName;
698
+ const csiModelMap = contentSourceData.csiModelMap;
699
+ const model = csiModelMap[documentModelName];
700
+ if (!model) {
701
+ throw new Error(`error updating document, could not find document model: '${documentModelName}'`);
702
+ }
703
+
704
+ // get the 'reference' model field in the updated document that will be used to link the new document
705
+ locale = locale ?? contentSourceData.defaultLocaleCode;
706
+ const modelField = getModelFieldForFieldAtPath(document, model, fieldPath, csiModelMap, locale);
707
+ if (!modelField) {
708
+ throw Error(`the "fieldPath" points to non existing model field: ${fieldPath.join('.')}`);
709
+ }
710
+ const fieldProps = modelField.type === 'list' ? modelField.items! : modelField;
711
+ if (fieldProps.type !== 'reference') {
712
+ throw Error(`error in "createAndLinkDocument", this operation can only be used on reference field: ${fieldPath.join('.')}`);
713
+ }
714
+
715
+ // get the model name for the new document
716
+ if (!modelName && fieldProps.models.length === 1) {
717
+ modelName = fieldProps.models[0];
718
+ }
719
+ if (!modelName) {
720
+ throw Error(`error in "createAndLinkDocument", missing "modelName": ${fieldPath.join('.')}`);
721
+ }
722
+
723
+ // create the new document
724
+ const result = await this.createDocument({
725
+ object: object,
726
+ srcProjectId: srcProjectId,
727
+ srcType: srcType,
728
+ modelName: modelName,
729
+ locale: locale,
730
+ user: user
731
+ });
732
+
733
+ // update the document by linking the field to the created document
734
+ const userContext = getUserContextForSrcType(srcType, user);
735
+ const field = {
736
+ type: 'reference',
737
+ refType: 'document',
738
+ refId: result.srcDocumentId
739
+ } as const;
740
+ const updatedDocument = await contentSourceData.instance.updateDocument({
741
+ document: csiDocument,
742
+ modelMap: csiModelMap,
743
+ userContext: userContext,
744
+ operations: [
745
+ modelField.type === 'list'
746
+ ? {
747
+ opType: 'insert',
748
+ fieldPath: fieldPath,
749
+ modelField: modelField,
750
+ locale: locale,
751
+ index: index,
752
+ item: field
753
+ }
754
+ : {
755
+ opType: 'set',
756
+ fieldPath: fieldPath,
757
+ modelField: modelField,
758
+ locale: locale,
759
+ field: field
760
+ }
761
+ ]
762
+ });
763
+ return { srcDocumentId: updatedDocument.id };
764
+ }
765
+
766
+ async uploadAndLinkAsset({
767
+ srcType,
768
+ srcProjectId,
769
+ srcDocumentId,
770
+ fieldPath,
771
+ asset,
772
+ index,
773
+ locale,
774
+ user
775
+ }: {
776
+ srcType: string;
777
+ srcProjectId: string;
778
+ srcDocumentId: string;
779
+ fieldPath: (string | number)[];
780
+ asset: ContentStoreTypes.UploadAssetData;
781
+ index?: number;
782
+ locale?: string;
783
+ user?: ContentStoreTypes.User;
784
+ }): Promise<{ srcDocumentId: string }> {
785
+ this.logger.debug('uploadAndLinkAsset', { srcType, srcProjectId, srcDocumentId, fieldPath, index, locale });
786
+
787
+ // get the document that is being updated
788
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
789
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
790
+ const document = contentSourceData.documentMap[srcDocumentId];
791
+ const csiDocument = contentSourceData.csiDocumentMap[srcDocumentId];
792
+ if (!document || !csiDocument) {
793
+ throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
794
+ }
795
+
796
+ // get the document model
797
+ const documentModelName = document.srcModelName;
798
+ const csiModelMap = contentSourceData.csiModelMap;
799
+ const csiModel = csiModelMap[documentModelName];
800
+ if (!csiModel) {
801
+ throw new Error(`error updating document, could not find document model: '${documentModelName}'`);
802
+ }
803
+
804
+ // get the 'reference' model field in the updated document that will be used to link the new asset
805
+ locale = locale ?? contentSourceData.defaultLocaleCode;
806
+ const modelField = getModelFieldForFieldAtPath(document, csiModel, fieldPath, csiModelMap, locale);
807
+ if (!modelField) {
808
+ throw Error(`the "fieldPath" points to non existing model field: ${fieldPath.join('.')}`);
809
+ }
810
+ const fieldProps = modelField.type === 'list' ? modelField.items! : modelField;
811
+ if (fieldProps.type !== 'reference' && fieldProps.type !== 'image') {
812
+ throw Error(`error in "uploadAndLinkAsset", this operation can only be used on reference and image fields: ${fieldPath.join('.')}`);
813
+ }
814
+
815
+ // upload the new asset
816
+ const userContext = getUserContextForSrcType(srcType, user);
817
+ const result = await contentSourceData.instance.uploadAsset({
818
+ url: asset.url,
819
+ fileName: asset.metadata.name,
820
+ mimeType: asset.metadata.type,
821
+ locale: locale,
822
+ userContext: userContext
823
+ });
824
+
825
+ // update the document by linking the field to the created asset
826
+ const field = {
827
+ type: 'reference',
828
+ refType: 'asset',
829
+ refId: result.id
830
+ } as const;
831
+ const updatedDocument = await contentSourceData.instance.updateDocument({
832
+ document: csiDocument,
833
+ modelMap: csiModelMap,
834
+ userContext: userContext,
835
+ operations: [
836
+ modelField.type === 'list'
837
+ ? {
838
+ opType: 'insert',
839
+ fieldPath: fieldPath,
840
+ modelField: modelField,
841
+ locale: locale,
842
+ index: index,
843
+ item: field
844
+ }
845
+ : {
846
+ opType: 'set',
847
+ fieldPath: fieldPath,
848
+ modelField: modelField,
849
+ locale: locale,
850
+ field: field
851
+ }
852
+ ]
853
+ });
854
+ return { srcDocumentId: updatedDocument.id };
855
+ }
856
+
857
+ async createDocument({
858
+ srcType,
859
+ srcProjectId,
860
+ modelName,
861
+ object,
862
+ locale,
863
+ user
864
+ }: {
865
+ srcType: string;
866
+ srcProjectId: string;
867
+ modelName: string;
868
+ object?: Record<string, any>;
869
+ locale?: string;
870
+ user?: ContentStoreTypes.User;
871
+ }): Promise<{ srcDocumentId: string }> {
872
+ this.logger.debug('createDocument', { srcType, srcProjectId, modelName, locale });
873
+
874
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
875
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
876
+ const modelMap = contentSourceData.modelMap;
877
+ const model = modelMap[modelName];
878
+ if (!model) {
879
+ throw new Error(`no model with name '${modelName}' was found`);
880
+ }
881
+
882
+ locale = locale ?? contentSourceData.defaultLocaleCode;
883
+ const userContext = getUserContextForSrcType(srcType, user);
884
+ const result = await createDocumentRecursively({
885
+ object,
886
+ model,
887
+ modelMap,
888
+ locale,
889
+ userContext,
890
+ contentSourceInstance: contentSourceData.instance
891
+ });
892
+ this.logger.debug('created document', { srcType, srcProjectId, srcDocumentId: result.document.id, modelName });
893
+
894
+ // do not update cache in contentSourceData.documents and documentMap,
895
+ // instead wait for contentSource to call onContentChange(contentChangeEvent)
896
+ // and use data from contentChangeEvent to update the cache
897
+ // const newDocuments = [result.document, ...result.referencedDocuments];
898
+ // contentSourceData.documentMap = Object.assign(contentSourceData.documentMap, _.keyBy(newDocuments, 'srcObjectId'));
899
+
900
+ return { srcDocumentId: result.document.id };
901
+ }
902
+
903
+ async updateDocument({
904
+ srcType,
905
+ srcProjectId,
906
+ srcDocumentId,
907
+ updateOperations,
908
+ user
909
+ }: {
910
+ srcType: string;
911
+ srcProjectId: string;
912
+ srcDocumentId: string;
913
+ updateOperations: ContentStoreTypes.UpdateOperation[];
914
+ user?: ContentStoreTypes.User;
915
+ }): Promise<{ srcDocumentId: string }> {
916
+ this.logger.debug('updateDocument');
917
+
918
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
919
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
920
+ const userContext = getUserContextForSrcType(srcType, user);
921
+ const document = contentSourceData.documentMap[srcDocumentId];
922
+ const csiDocument = contentSourceData.csiDocumentMap[srcDocumentId];
923
+ if (!document || !csiDocument) {
924
+ throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
925
+ }
926
+
927
+ const modelMap = contentSourceData.modelMap;
928
+ const csiModelMap = contentSourceData.csiModelMap;
929
+ const documentModelName = document.srcModelName;
930
+ const model = modelMap[documentModelName];
931
+ if (!model) {
932
+ throw new Error(`error updating document, could not find document model: '${documentModelName}'`);
933
+ }
934
+
935
+ const operations = await mapPromise(
936
+ updateOperations,
937
+ async (updateOperation): Promise<CSITypes.UpdateOperation> => {
938
+ const locale = updateOperation.locale ?? contentSourceData.defaultLocaleCode;
939
+ const modelField = getModelFieldForFieldAtPath(document, model, updateOperation.fieldPath, modelMap, locale);
940
+ switch (updateOperation.opType) {
941
+ case 'set':
942
+ const field = await convertOperationField({
943
+ operationField: updateOperation.field,
944
+ fieldPath: updateOperation.fieldPath,
945
+ locale: updateOperation.locale,
946
+ modelField,
947
+ modelMap,
948
+ userContext,
949
+ contentSourceInstance: contentSourceData.instance
950
+ });
951
+ return {
952
+ ...updateOperation,
953
+ modelField,
954
+ field
955
+ };
956
+ case 'unset':
957
+ return { ...updateOperation, modelField };
958
+ case 'insert':
959
+ const item = await convertOperationField({
960
+ operationField: updateOperation.item,
961
+ fieldPath: updateOperation.fieldPath,
962
+ locale: updateOperation.locale,
963
+ modelField,
964
+ modelMap,
965
+ userContext,
966
+ contentSourceInstance: contentSourceData.instance
967
+ });
968
+ return {
969
+ ...updateOperation,
970
+ modelField,
971
+ item
972
+ };
973
+ case 'remove':
974
+ return { ...updateOperation, modelField };
975
+ case 'reorder':
976
+ return { ...updateOperation, modelField };
977
+ }
978
+ }
979
+ );
980
+
981
+ const updatedDocumentResult = await contentSourceData.instance.updateDocument({
982
+ document: csiDocument,
983
+ modelMap: csiModelMap,
984
+ userContext,
985
+ operations
986
+ });
987
+
988
+ // do not update cache in contentSourceData.documents and documentMap,
989
+ // instead wait for contentSource to call onContentChange(contentChangeEvent)
990
+ // and use data from contentChangeEvent to update the cache
991
+ // contentSourceData.documentMap = Object.assign(contentSourceData.documentMap, { [document.srcObjectId]: document });
992
+
993
+ return { srcDocumentId: updatedDocumentResult.id };
994
+ }
995
+
996
+ async duplicateDocument({
997
+ srcType,
998
+ srcProjectId,
999
+ srcDocumentId,
1000
+ object,
1001
+ user
1002
+ }: {
1003
+ srcType: string;
1004
+ srcProjectId: string;
1005
+ srcDocumentId: string;
1006
+ object?: Record<string, any>;
1007
+ user?: ContentStoreTypes.User;
1008
+ }): Promise<{ srcDocumentId: string }> {
1009
+ this.logger.debug('duplicateDocument');
1010
+
1011
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
1012
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1013
+ const document = contentSourceData.documentMap[srcDocumentId];
1014
+ if (!document) {
1015
+ throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
1016
+ }
1017
+ const modelMap = contentSourceData.modelMap;
1018
+ const model = modelMap[document.srcModelName];
1019
+ if (!model) {
1020
+ throw new Error(`no model with name '${document.srcModelName}' was found`);
1021
+ }
1022
+
1023
+ const userContext = getUserContextForSrcType(srcType, user);
1024
+
1025
+ // TODO: take the data from the provided 'object' and merge them with
1026
+ // DocumentFields of the existing Document:
1027
+ // Option 1: Map the DocumentFields of the existing Document into flat
1028
+ // object with '$$ref' and '$type' properties for references and
1029
+ // nested objects (needs to be implemented), and then merge it with
1030
+ // the provided object recursively, and then pass that object to
1031
+ // createNestedObjectRecursively()
1032
+ // Options 2: Converting the provided object into a Document with
1033
+ // DocumentFields and then merging both documents into a single
1034
+ // Document and then calling mapStoreFieldsToOperationFields (needs to be implemented)
1035
+ // While doing all that, need to handle existing references in the
1036
+ // existing Document and to duplicate the referenced documents if their
1037
+ // model is marked as 'duplicatable' and to reuse the existing documents
1038
+ // if their model is marked as non-duplicatable.
1039
+ const updateOperationFields = mapStoreFieldsToOperationFields({
1040
+ documentFields: document.fields,
1041
+ modelFields: model.fields!,
1042
+ modelMap: contentSourceData.modelMap
1043
+ });
1044
+
1045
+ const documentResult = await contentSourceData.instance.createDocument({
1046
+ updateOperationFields,
1047
+ // TODO: pass csiModel
1048
+ model,
1049
+ // TODO: pass csiModelMap
1050
+ modelMap,
1051
+ locale: contentSourceData.defaultLocaleCode,
1052
+ userContext
1053
+ });
1054
+
1055
+ return { srcDocumentId: documentResult.id };
1056
+ }
1057
+
1058
+ async uploadAssets({
1059
+ srcType,
1060
+ srcProjectId,
1061
+ assets,
1062
+ locale,
1063
+ user
1064
+ }: {
1065
+ srcType: string;
1066
+ srcProjectId: string;
1067
+ assets: ContentStoreTypes.UploadAssetData[];
1068
+ locale?: string;
1069
+ user?: ContentStoreTypes.User;
1070
+ }): Promise<ContentStoreTypes.APIAsset[]> {
1071
+ this.logger.debug('uploadAssets');
1072
+
1073
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
1074
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1075
+ const sourceAssets: CSITypes.Asset[] = [];
1076
+ const userContext = getUserContextForSrcType(srcType, user);
1077
+
1078
+ locale = locale ?? contentSourceData.defaultLocaleCode;
1079
+
1080
+ for (const asset of assets) {
1081
+ let base64 = undefined;
1082
+ if (asset.data) {
1083
+ const matchResult = asset.data.match(/;base64,([\s\S]+)$/);
1084
+ if (matchResult) {
1085
+ base64 = matchResult[1];
1086
+ }
1087
+ }
1088
+ const sourceAsset = await contentSourceData.instance.uploadAsset({
1089
+ url: asset.url,
1090
+ base64: base64,
1091
+ fileName: asset.metadata.name,
1092
+ mimeType: asset.metadata.type,
1093
+ locale: locale,
1094
+ userContext: userContext
1095
+ });
1096
+ sourceAssets.push(sourceAsset);
1097
+ }
1098
+ const storeAssets = mapCSIAssetsToStoreAssets({
1099
+ csiAssets: sourceAssets,
1100
+ contentSourceInstance: contentSourceData.instance,
1101
+ defaultLocaleCode: contentSourceData.defaultLocaleCode
1102
+ });
1103
+ return mapStoreAssetsToAPIAssets(storeAssets, locale);
1104
+ }
1105
+
1106
+ async deleteDocument({
1107
+ srcType,
1108
+ srcProjectId,
1109
+ srcDocumentId,
1110
+ user
1111
+ }: {
1112
+ srcType: string;
1113
+ srcProjectId: string;
1114
+ srcDocumentId: string;
1115
+ user?: ContentStoreTypes.User;
1116
+ }) {
1117
+ this.logger.debug('deleteDocument');
1118
+
1119
+ const userContext = getUserContextForSrcType(srcType, user);
1120
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
1121
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1122
+ const csiDocument = contentSourceData.csiDocumentMap[srcDocumentId];
1123
+ if (!csiDocument) {
1124
+ throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
1125
+ }
1126
+ await contentSourceData.instance.deleteDocument({ document: csiDocument, userContext });
1127
+
1128
+ // do not update cache in contentSourceData.documents and documentMap,
1129
+ // instead wait for contentSource to call onContentChange(contentChangeEvent)
1130
+ // and use data from contentChangeEvent to update the cache
1131
+ // delete contentSourceData.documentMap[srcDocumentId];
1132
+ }
1133
+
1134
+ async validateDocuments({
1135
+ objects,
1136
+ locale,
1137
+ user
1138
+ }: {
1139
+ objects: { srcType: string; srcProjectId: string; srcObjectId: string }[];
1140
+ locale?: string;
1141
+ user?: ContentStoreTypes.User;
1142
+ }): Promise<{ errors: ContentStoreTypes.ValidationError[] }> {
1143
+ this.logger.debug('validateDocuments');
1144
+
1145
+ const objectsBySourceId = _.groupBy(objects, (object) => getContentSourceId(object.srcType, object.srcProjectId));
1146
+ let errors: ContentStoreTypes.ValidationError[] = [];
1147
+ for (const [contentSourceId, contentSourceObjects] of Object.entries(objectsBySourceId)) {
1148
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1149
+ locale = locale ?? contentSourceData.defaultLocaleCode;
1150
+ const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
1151
+ const userContext = getUserContextForSrcType(contentSourceData.type, user);
1152
+ const internalValidationErrors = internalValidateContent(documents, assets, contentSourceData);
1153
+ const validationResult = await contentSourceData.instance.validateDocuments({ documents, assets, locale, userContext });
1154
+ errors = errors.concat(
1155
+ internalValidationErrors,
1156
+ validationResult.errors.map((validationError) => ({
1157
+ message: validationError.message,
1158
+ srcType: contentSourceData.type,
1159
+ srcProjectId: contentSourceData.projectId,
1160
+ srcObjectType: validationError.objectType,
1161
+ srcObjectId: validationError.objectId,
1162
+ fieldPath: validationError.fieldPath,
1163
+ isUniqueValidation: validationError.isUniqueValidation
1164
+ }))
1165
+ );
1166
+ }
1167
+
1168
+ return { errors };
1169
+
1170
+ /* validate for multiple sources
1171
+ const objectsBySourceId = _.groupBy(objects, (document) => getContentSourceId(document.srcType, document.srcProjectId));
1172
+ const contentSourceIds = Object.keys(objectsBySourceId);
1173
+ return reducePromise(
1174
+ contentSourceIds,
1175
+ async (result: ContentStoreTypes.ValidationError[], contentSourceId) => {
1176
+ const documents = documentsBySourceId[contentSourceId]!;
1177
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1178
+ const validationErrors = await contentSourceData.instance.validateDocuments({ documentIds: documents.map((document) => document.srcObjectId) });
1179
+ return result.concat(validationErrors);
1180
+ },
1181
+ []
1182
+ );
1183
+ */
1184
+ }
1185
+
1186
+ async publishDocuments({ objects, user }: { objects: { srcType: string; srcProjectId: string; srcObjectId: string }[]; user?: ContentStoreTypes.User }) {
1187
+ this.logger.debug('publishDocuments');
1188
+
1189
+ const objectsBySourceId = _.groupBy(objects, (object) => getContentSourceId(object.srcType, object.srcProjectId));
1190
+ for (const [contentSourceId, contentSourceObjects] of Object.entries(objectsBySourceId)) {
1191
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1192
+ const userContext = getUserContextForSrcType(contentSourceData.type, user);
1193
+ const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
1194
+ await contentSourceData.instance.publishDocuments({ documents, assets, userContext });
1195
+ }
1196
+ }
1197
+
1198
+ private getContentSourceDataByIdOrThrow(contentSourceId: string): ContentSourceData {
1199
+ const contentSourceData = this.contentSourceDataById[contentSourceId];
1200
+ if (!contentSourceData) {
1201
+ throw new Error(`no content source for id '${contentSourceId}' was found`);
1202
+ }
1203
+ return contentSourceData;
1204
+ }
1205
+ }
1206
+
1207
+ export function getContentSourceId(contentSourceType: string, srcProjectId: string) {
1208
+ return contentSourceType + ':' + srcProjectId;
1209
+ }
1210
+
1211
+ function getUserContextForSrcType(srcType: string, user?: ContentStoreTypes.User): unknown {
1212
+ return user?.connections?.find((connection) => connection.type === srcType);
1213
+ }
1214
+
1215
+ function mapCSIAssetsToStoreAssets({
1216
+ csiAssets,
1217
+ contentSourceInstance,
1218
+ defaultLocaleCode
1219
+ }: {
1220
+ csiAssets: CSITypes.Asset[];
1221
+ contentSourceInstance: CSITypes.ContentSourceInterface;
1222
+ defaultLocaleCode?: string;
1223
+ }): ContentStoreTypes.Asset[] {
1224
+ const extra = {
1225
+ srcType: contentSourceInstance.getContentSourceType(),
1226
+ srcProjectId: contentSourceInstance.getProjectId(),
1227
+ srcProjectUrl: contentSourceInstance.getProjectManageUrl(),
1228
+ srcEnvironment: contentSourceInstance.getProjectEnvironment()
1229
+ };
1230
+ return csiAssets.map((csiAsset) => sourceAssetToStoreAsset({ csiAsset, defaultLocaleCode, extra }));
1231
+ }
1232
+
1233
+ function sourceAssetToStoreAsset({
1234
+ csiAsset,
1235
+ defaultLocaleCode,
1236
+ extra
1237
+ }: {
1238
+ csiAsset: CSITypes.Asset;
1239
+ defaultLocaleCode?: string;
1240
+ extra: { srcType: string; srcProjectId: string; srcProjectUrl: string; srcEnvironment: string };
1241
+ }): ContentStoreTypes.Asset {
1242
+ return {
1243
+ type: 'asset',
1244
+ ...extra,
1245
+ srcObjectId: csiAsset.id,
1246
+ srcObjectUrl: csiAsset.manageUrl,
1247
+ srcObjectLabel: getObjectLabel(csiAsset.fields, IMAGE_MODEL, defaultLocaleCode),
1248
+ srcModelName: IMAGE_MODEL.name,
1249
+ srcModelLabel: IMAGE_MODEL.label!,
1250
+ isChanged: csiAsset.status === 'added' || csiAsset.status === 'modified',
1251
+ status: csiAsset.status,
1252
+ createdAt: csiAsset.createdAt,
1253
+ createdBy: csiAsset.createdBy,
1254
+ updatedAt: csiAsset.updatedAt,
1255
+ updatedBy: csiAsset.updatedBy,
1256
+ fields: {
1257
+ title: {
1258
+ label: 'Title',
1259
+ ...csiAsset.fields.title
1260
+ },
1261
+ file: {
1262
+ label: 'File',
1263
+ ...csiAsset.fields.file
1264
+ }
1265
+ }
1266
+ };
1267
+ }
1268
+
1269
+ function mapCSIDocumentsToStoreDocuments({
1270
+ csiDocuments,
1271
+ contentSourceInstance,
1272
+ modelMap,
1273
+ defaultLocaleCode
1274
+ }: {
1275
+ csiDocuments: CSITypes.Document[];
1276
+ contentSourceInstance: CSITypes.ContentSourceInterface;
1277
+ modelMap: Record<string, Model>;
1278
+ defaultLocaleCode?: string;
1279
+ }): ContentStoreTypes.Document[] {
1280
+ const extra = {
1281
+ srcType: contentSourceInstance.getContentSourceType(),
1282
+ srcProjectId: contentSourceInstance.getProjectId(),
1283
+ srcProjectUrl: contentSourceInstance.getProjectManageUrl(),
1284
+ srcEnvironment: contentSourceInstance.getProjectEnvironment()
1285
+ };
1286
+ return csiDocuments.map((csiDocument) =>
1287
+ mapCSIDocumentToStoreDocument({ csiDocument, model: modelMap[csiDocument.modelName]!, modelMap, defaultLocaleCode, extra })
1288
+ );
1289
+ }
1290
+
1291
+ function mapCSIDocumentToStoreDocument({
1292
+ csiDocument,
1293
+ model,
1294
+ modelMap,
1295
+ defaultLocaleCode,
1296
+ extra
1297
+ }: {
1298
+ csiDocument: CSITypes.Document;
1299
+ model: Model;
1300
+ modelMap: Record<string, Model>;
1301
+ defaultLocaleCode?: string;
1302
+ extra: { srcType: string; srcProjectId: string; srcProjectUrl: string; srcEnvironment: string };
1303
+ }): ContentStoreTypes.Document {
1304
+ return {
1305
+ type: 'document',
1306
+ ...extra,
1307
+ srcObjectId: csiDocument.id,
1308
+ srcObjectUrl: csiDocument.manageUrl,
1309
+ srcObjectLabel: getObjectLabel(csiDocument.fields, model, defaultLocaleCode),
1310
+ srcModelLabel: model.label ?? _.startCase(csiDocument.modelName),
1311
+ srcModelName: csiDocument.modelName,
1312
+ isChanged: csiDocument.status === 'added' || csiDocument.status === 'modified',
1313
+ status: csiDocument.status,
1314
+ createdAt: csiDocument.createdAt,
1315
+ createdBy: csiDocument.createdBy,
1316
+ updatedAt: csiDocument.updatedAt,
1317
+ updatedBy: csiDocument.updatedBy,
1318
+ fields: mapCSIFieldsToStoreFields({
1319
+ csiDocumentFields: csiDocument.fields,
1320
+ modelFields: model.fields ?? [],
1321
+ context: {
1322
+ modelMap,
1323
+ defaultLocaleCode
1324
+ }
1325
+ })
1326
+ };
1327
+ }
1328
+
1329
+ type MapContext = {
1330
+ modelMap: Record<string, Model>;
1331
+ defaultLocaleCode?: string;
1332
+ };
1333
+
1334
+ function mapCSIFieldsToStoreFields({
1335
+ csiDocumentFields,
1336
+ modelFields,
1337
+ context
1338
+ }: {
1339
+ csiDocumentFields: Record<string, CSITypes.DocumentField>;
1340
+ modelFields: Field[];
1341
+ context: MapContext;
1342
+ }): Record<string, ContentStoreTypes.DocumentField> {
1343
+ return modelFields.reduce((result: Record<string, ContentStoreTypes.DocumentField>, modelField) => {
1344
+ const csiDocumentField = csiDocumentFields[modelField.name];
1345
+ const docField = mapCSIFieldToStoreField({
1346
+ csiDocumentField,
1347
+ modelField,
1348
+ context
1349
+ });
1350
+ docField.label = modelField.label;
1351
+ result[modelField.name] = docField;
1352
+ return result;
1353
+ }, {});
1354
+ }
1355
+
1356
+ function mapCSIFieldToStoreField({
1357
+ csiDocumentField,
1358
+ modelField,
1359
+ context
1360
+ }: {
1361
+ csiDocumentField: CSITypes.DocumentField | undefined;
1362
+ modelField: FieldSpecificProps;
1363
+ context: MapContext;
1364
+ }): ContentStoreTypes.DocumentField {
1365
+ if (!csiDocumentField) {
1366
+ const isUnset = ['object', 'model', 'reference', 'richText', 'markdown', 'image', 'file', 'json'].includes(modelField.type);
1367
+ return {
1368
+ type: modelField.type,
1369
+ ...(isUnset ? { isUnset } : null),
1370
+ ...(modelField.type === 'list' ? { items: [] } : null)
1371
+ } as ContentStoreTypes.DocumentField;
1372
+ }
1373
+ // TODO: check if need to add "options" to "enum" and subtype/min/max to "number"
1374
+ switch (modelField.type) {
1375
+ case 'object':
1376
+ return mapObjectField(csiDocumentField as CSITypes.DocumentObjectField, modelField, context);
1377
+ case 'model':
1378
+ return mapModelField(csiDocumentField as CSITypes.DocumentModelField, modelField, context);
1379
+ case 'list':
1380
+ return mapListField(csiDocumentField as CSITypes.DocumentListField, modelField, context);
1381
+ case 'richText':
1382
+ return mapRichTextField(csiDocumentField as CSITypes.DocumentRichTextField);
1383
+ case 'markdown':
1384
+ return mapMarkdownField(csiDocumentField as CSITypes.DocumentValueField);
1385
+ default:
1386
+ return csiDocumentField as ContentStoreTypes.DocumentField;
1387
+ }
1388
+ }
1389
+
1390
+ function mapObjectField(
1391
+ csiDocumentField: CSITypes.DocumentObjectField,
1392
+ modelField: FieldObjectProps,
1393
+ context: MapContext
1394
+ ): ContentStoreTypes.DocumentObjectField {
1395
+ if (!isLocalizedField(csiDocumentField)) {
1396
+ return {
1397
+ type: csiDocumentField.type,
1398
+ srcObjectLabel: getObjectLabel(csiDocumentField.fields ?? {}, modelField ?? [], context.defaultLocaleCode),
1399
+ fields: mapCSIFieldsToStoreFields({
1400
+ csiDocumentFields: csiDocumentField.fields ?? {},
1401
+ modelFields: modelField.fields ?? [],
1402
+ context
1403
+ })
1404
+ };
1405
+ }
1406
+ return {
1407
+ type: csiDocumentField.type,
1408
+ localized: true,
1409
+ locales: _.mapValues(csiDocumentField.locales, (locale) => {
1410
+ return {
1411
+ locale: locale.locale,
1412
+ srcObjectLabel: getObjectLabel(locale.fields ?? {}, modelField, locale.locale),
1413
+ fields: mapCSIFieldsToStoreFields({
1414
+ csiDocumentFields: locale.fields ?? {},
1415
+ modelFields: modelField.fields ?? [],
1416
+ context
1417
+ })
1418
+ };
1419
+ })
1420
+ };
1421
+ }
1422
+
1423
+ function mapModelField(csiDocumentField: CSITypes.DocumentModelField, modelField: FieldModelProps, context: MapContext): ContentStoreTypes.DocumentModelField {
1424
+ if (!isLocalizedField(csiDocumentField)) {
1425
+ const model = context.modelMap[csiDocumentField.modelName]!;
1426
+ return {
1427
+ type: csiDocumentField.type,
1428
+ srcObjectLabel: getObjectLabel(csiDocumentField.fields ?? {}, model, context.defaultLocaleCode),
1429
+ srcModelName: csiDocumentField.modelName,
1430
+ srcModelLabel: model.label ?? _.startCase(model.name),
1431
+ fields: mapCSIFieldsToStoreFields({
1432
+ csiDocumentFields: csiDocumentField.fields ?? {},
1433
+ modelFields: model.fields ?? [],
1434
+ context
1435
+ })
1436
+ };
1437
+ }
1438
+ return {
1439
+ type: csiDocumentField.type,
1440
+ localized: true,
1441
+ locales: _.mapValues(csiDocumentField.locales, (locale) => {
1442
+ const model = context.modelMap[locale.modelName]!;
1443
+ return {
1444
+ locale: locale.locale,
1445
+ srcObjectLabel: getObjectLabel(locale.fields ?? {}, model, locale.locale),
1446
+ srcModelName: locale.modelName,
1447
+ srcModelLabel: model.label ?? _.startCase(model.name),
1448
+ fields: mapCSIFieldsToStoreFields({
1449
+ csiDocumentFields: locale.fields ?? {},
1450
+ modelFields: model.fields ?? [],
1451
+ context
1452
+ })
1453
+ };
1454
+ })
1455
+ };
1456
+ }
1457
+
1458
+ function mapListField(csiDocumentField: CSITypes.DocumentListField, modelField: FieldListProps, context: MapContext): ContentStoreTypes.DocumentListField {
1459
+ if (!isLocalizedField(csiDocumentField)) {
1460
+ return {
1461
+ type: csiDocumentField.type,
1462
+ items: csiDocumentField.items.map((item) =>
1463
+ mapCSIFieldToStoreField({
1464
+ csiDocumentField: item,
1465
+ modelField: modelField.items ?? { type: 'string' },
1466
+ context
1467
+ })
1468
+ )
1469
+ };
1470
+ }
1471
+ return {
1472
+ type: csiDocumentField.type,
1473
+ localized: true,
1474
+ locales: _.mapValues(csiDocumentField.locales, (locale) => {
1475
+ return {
1476
+ locale: locale.locale,
1477
+ items: (locale.items ?? []).map((item) =>
1478
+ mapCSIFieldToStoreField({
1479
+ csiDocumentField: item,
1480
+ modelField: modelField.items ?? { type: 'string' },
1481
+ context
1482
+ })
1483
+ )
1484
+ };
1485
+ })
1486
+ };
1487
+ }
1488
+
1489
+ function mapRichTextField(csiDocumentField: CSITypes.DocumentRichTextField): ContentStoreTypes.DocumentRichTextField {
1490
+ if (!isLocalizedField(csiDocumentField)) {
1491
+ return {
1492
+ ...csiDocumentField,
1493
+ multiElement: true
1494
+ };
1495
+ }
1496
+ return {
1497
+ type: csiDocumentField.type,
1498
+ localized: true,
1499
+ locales: _.mapValues(csiDocumentField.locales, (locale) => {
1500
+ return {
1501
+ ...locale,
1502
+ multiElement: true
1503
+ };
1504
+ })
1505
+ };
1506
+ }
1507
+
1508
+ function mapMarkdownField(csiDocumentField: CSITypes.DocumentValueField): ContentStoreTypes.DocumentMarkdownField {
1509
+ if (!isLocalizedField(csiDocumentField)) {
1510
+ return {
1511
+ type: 'markdown',
1512
+ value: csiDocumentField.value,
1513
+ multiElement: true
1514
+ };
1515
+ }
1516
+ return {
1517
+ type: 'markdown',
1518
+ localized: true,
1519
+ locales: _.mapValues(csiDocumentField.locales, (locale) => {
1520
+ return {
1521
+ ...locale,
1522
+ multiElement: true
1523
+ };
1524
+ })
1525
+ };
1526
+ }
1527
+
1528
+ function mapStoreFieldsToOperationFields({
1529
+ documentFields,
1530
+ modelFields,
1531
+ modelMap
1532
+ }: {
1533
+ documentFields: Record<string, ContentStoreTypes.DocumentField>;
1534
+ modelFields: Field[];
1535
+ modelMap: Record<string, Model>;
1536
+ }): Record<string, CSITypes.UpdateOperationField> {
1537
+ // TODO: implement
1538
+ throw new Error(`duplicateDocument not implemented yet`);
1539
+ }
1540
+
1541
+ function getContentSourceIdForContentSource(contentSource: CSITypes.ContentSourceInterface): string {
1542
+ return getContentSourceId(contentSource.getContentSourceType(), contentSource.getProjectId());
1543
+ }
1544
+
1545
+ function extractTokensFromString(input: string): string[] {
1546
+ return input.match(/(?<={)[^}]+(?=})/g) || [];
1547
+ }
1548
+
1549
+ function sanitizeSlug(slug: string) {
1550
+ return slug
1551
+ .split('/')
1552
+ .map((part) => slugify(part, { lower: true }))
1553
+ .join('/');
1554
+ }
1555
+
1556
+ function getObjectLabel(
1557
+ documentFields: Record<string, CSITypes.DocumentField | CSITypes.AssetFileField>,
1558
+ modelOrObjectField: Model | FieldObjectProps,
1559
+ locale?: string
1560
+ ): string {
1561
+ const labelField = modelOrObjectField.labelField;
1562
+ let label = null;
1563
+ if (labelField) {
1564
+ const field = _.get(documentFields, labelField, null);
1565
+ if (field && ['string', 'url', 'slug', 'text', 'markdown', 'number', 'enum', 'date', 'datetime', 'color', 'image', 'file'].includes(field.type)) {
1566
+ if (isLocalizedField(field) && locale) {
1567
+ label = _.get(field, ['locales', locale, 'value'], null);
1568
+ } else if (!isLocalizedField(field)) {
1569
+ label = _.get(field, 'value', null);
1570
+ }
1571
+ }
1572
+ }
1573
+ if (!label) {
1574
+ label = _.get(modelOrObjectField, 'label');
1575
+ }
1576
+ if (!label && _.has(modelOrObjectField, 'name')) {
1577
+ label = _.startCase(_.get(modelOrObjectField, 'name'));
1578
+ }
1579
+ return label;
1580
+ }
1581
+
1582
+ function mapDocumentsToLocalizedApiObjects(documents: ContentStoreTypes.Document[], locale?: string): ContentStoreTypes.APIDocumentObject[] {
1583
+ return documents.map((document) => documentToLocalizedApiObject(document, locale));
1584
+ }
1585
+
1586
+ function documentToLocalizedApiObject(document: ContentStoreTypes.Document, locale?: string): ContentStoreTypes.APIDocumentObject {
1587
+ const { type, fields, ...rest } = document;
1588
+ return {
1589
+ type: 'object',
1590
+ ...rest,
1591
+ fields: toLocalizedAPIFields(fields, locale)
1592
+ };
1593
+ }
1594
+
1595
+ function toLocalizedAPIFields(docFields: Record<string, ContentStoreTypes.DocumentField>, locale?: string): Record<string, ContentStoreTypes.DocumentFieldAPI> {
1596
+ return _.mapValues(docFields, (docField) => toLocalizedAPIField(docField, locale));
1597
+ }
1598
+
1599
+ function toLocalizedAPIField(docField: ContentStoreTypes.DocumentField, locale?: string, isListItem = false): ContentStoreTypes.DocumentFieldAPI {
1600
+ const hasUnsetFlag = ['object', 'model', 'reference', 'richText', 'markdown', 'image', 'file', 'json'].includes(docField.type);
1601
+ let docFieldLocalized: ContentStoreTypes.DocumentFieldNonLocalized;
1602
+ let unset = false;
1603
+ if (docField.localized) {
1604
+ const { locales, localized, ...base } = docField;
1605
+ const localeProps = locale ? locales[locale] : undefined;
1606
+ docFieldLocalized = {
1607
+ ...base,
1608
+ ...localeProps,
1609
+ ...(hasUnsetFlag && !localeProps ? { isUnset: true } : null)
1610
+ } as ContentStoreTypes.DocumentFieldNonLocalized;
1611
+ } else {
1612
+ docFieldLocalized = docField;
1613
+ }
1614
+
1615
+ locale = locale ?? docFieldLocalized.locale;
1616
+ const commonProps = isListItem
1617
+ ? null
1618
+ : {
1619
+ localized: !!docField.localized,
1620
+ ...(locale ? { locale } : null)
1621
+ };
1622
+
1623
+ if (docFieldLocalized.type === 'object' || docFieldLocalized.type === 'model') {
1624
+ return {
1625
+ ...docFieldLocalized,
1626
+ type: 'object',
1627
+ ...commonProps,
1628
+ ...(docFieldLocalized.isUnset
1629
+ ? null
1630
+ : {
1631
+ fields: toLocalizedAPIFields(docFieldLocalized.fields, locale)
1632
+ })
1633
+ } as ContentStoreTypes.DocumentObjectFieldAPI | ContentStoreTypes.DocumentModelFieldAPI;
1634
+ } else if (docFieldLocalized.type === 'reference') {
1635
+ const { type, refType, ...rest } = docFieldLocalized;
1636
+ // if reference field isUnset === true, it behaves like a regular object
1637
+ if (rest.isUnset) {
1638
+ return {
1639
+ type: 'object',
1640
+ ...rest,
1641
+ ...commonProps
1642
+ };
1643
+ }
1644
+ return {
1645
+ type: 'unresolved_reference',
1646
+ refType: refType === 'asset' ? 'image' : 'object',
1647
+ ...rest,
1648
+ ...commonProps
1649
+ };
1650
+ } else if (docFieldLocalized.type === 'list') {
1651
+ const { items, ...rest } = docFieldLocalized;
1652
+ return {
1653
+ ...rest,
1654
+ ...commonProps,
1655
+ items: items.map((field) => toLocalizedAPIField(field, locale, true))
1656
+ };
1657
+ } else {
1658
+ return {
1659
+ ...docFieldLocalized,
1660
+ ...commonProps
1661
+ };
1662
+ }
1663
+ }
1664
+
1665
+ function mapAssetsToLocalizedApiImages(assets: ContentStoreTypes.Asset[], locale?: string): ContentStoreTypes.APIImageObject[] {
1666
+ return assets.map((asset) => assetToLocalizedApiImage(asset, locale));
1667
+ }
1668
+
1669
+ function assetToLocalizedApiImage(asset: ContentStoreTypes.Asset, locale?: string): ContentStoreTypes.APIImageObject {
1670
+ const { type, fields, ...rest } = asset;
1671
+ return {
1672
+ type: 'image',
1673
+ ...rest,
1674
+ fields: localizeAssetFields(fields, locale)
1675
+ };
1676
+ }
1677
+
1678
+ function localizeAssetFields(assetFields: ContentStoreTypes.AssetFields, locale?: string): ContentStoreTypes.AssetFieldsAPI {
1679
+ const fields: ContentStoreTypes.AssetFieldsAPI = {
1680
+ title: {
1681
+ type: 'string' as const,
1682
+ value: null as any
1683
+ },
1684
+ url: {
1685
+ type: 'string' as const,
1686
+ value: null as any
1687
+ }
1688
+ };
1689
+ const titleFieldNonLocalized = getDocumentFieldForLocale(assetFields.title, locale);
1690
+ fields.title.value = titleFieldNonLocalized?.value;
1691
+ fields.title.locale = locale ?? titleFieldNonLocalized?.locale;
1692
+ const assetFileField = assetFields.file;
1693
+ if (assetFileField.localized) {
1694
+ if (locale) {
1695
+ fields.url.value = assetFileField.locales[locale]?.url ?? null;
1696
+ fields.url.locale = locale;
1697
+ }
1698
+ } else {
1699
+ fields.url.value = assetFileField.url;
1700
+ fields.url.locale = assetFileField.locale;
1701
+ }
1702
+ return fields;
1703
+ }
1704
+
1705
+ function mapStoreAssetsToAPIAssets(assets: ContentStoreTypes.Asset[], locale?: string): ContentStoreTypes.APIAsset[] {
1706
+ return assets.map((asset) => storeAssetToAPIAsset(asset, locale));
1707
+ }
1708
+
1709
+ function storeAssetToAPIAsset(asset: ContentStoreTypes.Asset, locale?: string): ContentStoreTypes.APIAsset {
1710
+ const assetTitleField = asset.fields.title;
1711
+ const localizedTitleField = assetTitleField.localized ? assetTitleField.locales[locale!]! : assetTitleField;
1712
+ const assetFileField = asset.fields.file;
1713
+ const localizedFileField = assetFileField.localized ? assetFileField.locales[locale!]! : assetFileField;
1714
+ return {
1715
+ objectId: asset.srcObjectId,
1716
+ createdAt: asset.createdAt,
1717
+ url: localizedFileField.url,
1718
+ ...omitByNil({
1719
+ title: localizedTitleField.value,
1720
+ fileName: localizedFileField.fileName,
1721
+ contentType: localizedFileField.contentType,
1722
+ size: localizedFileField.size,
1723
+ width: localizedFileField.dimensions?.width,
1724
+ height: localizedFileField.dimensions?.height
1725
+ })
1726
+ };
1727
+ }
1728
+
1729
+ /**
1730
+ * Iterates recursively objects with $$type and $$ref, creating nested objects
1731
+ * as needed and returns standard ContentSourceInterface Documents
1732
+ */
1733
+ async function createDocumentRecursively({
1734
+ object,
1735
+ model,
1736
+ modelMap,
1737
+ locale,
1738
+ userContext,
1739
+ contentSourceInstance
1740
+ }: {
1741
+ object?: Record<string, any>;
1742
+ model: Model;
1743
+ modelMap: Record<string, Model>;
1744
+ locale?: string;
1745
+ userContext: unknown;
1746
+ contentSourceInstance: CSITypes.ContentSourceInterface;
1747
+ }): Promise<{ document: CSITypes.Document; newRefDocuments: CSITypes.Document[] }> {
1748
+ if (model.type === 'page') {
1749
+ const tokens = extractTokensFromString(String(model.urlPath));
1750
+ const slugField = _.last(tokens);
1751
+ if (object && slugField && slugField in object) {
1752
+ const slugFieldValue = object[slugField];
1753
+ object[slugField] = sanitizeSlug(slugFieldValue);
1754
+ }
1755
+ }
1756
+
1757
+ const nestedResult = await createNestedObjectRecursively({
1758
+ object,
1759
+ modelFields: model.fields ?? [],
1760
+ fieldPath: [],
1761
+ modelMap,
1762
+ locale,
1763
+ userContext,
1764
+ contentSourceInstance
1765
+ });
1766
+ const document = await contentSourceInstance.createDocument({
1767
+ updateOperationFields: nestedResult.fields,
1768
+ // TODO: pass csiModel
1769
+ model,
1770
+ // TODO: pass csiModelMap
1771
+ modelMap,
1772
+ locale,
1773
+ userContext
1774
+ });
1775
+ return {
1776
+ document: document,
1777
+ newRefDocuments: nestedResult.newRefDocuments
1778
+ };
1779
+ }
1780
+
1781
+ async function createNestedObjectRecursively({
1782
+ object,
1783
+ modelFields,
1784
+ fieldPath,
1785
+ modelMap,
1786
+ locale,
1787
+ userContext,
1788
+ contentSourceInstance
1789
+ }: {
1790
+ object?: Record<string, any>;
1791
+ modelFields: Field[];
1792
+ fieldPath: (string | number)[];
1793
+ modelMap: Record<string, Model>;
1794
+ locale?: string;
1795
+ userContext: unknown;
1796
+ contentSourceInstance: CSITypes.ContentSourceInterface;
1797
+ }): Promise<{
1798
+ fields: Record<string, CSITypes.UpdateOperationField>;
1799
+ newRefDocuments: CSITypes.Document[];
1800
+ }> {
1801
+ object = object ?? {};
1802
+ const result: {
1803
+ fields: Record<string, CSITypes.UpdateOperationField>;
1804
+ newRefDocuments: CSITypes.Document[];
1805
+ } = {
1806
+ fields: {},
1807
+ newRefDocuments: []
1808
+ };
1809
+ const objectFieldNames = Object.keys(object);
1810
+ for (const modelField of modelFields) {
1811
+ const fieldName = modelField.name;
1812
+ let value;
1813
+ if (fieldName in object) {
1814
+ value = object[fieldName];
1815
+ _.pull(objectFieldNames, fieldName);
1816
+ } else if (modelField.const) {
1817
+ value = modelField.const;
1818
+ } else if (!_.isNil(modelField.default)) {
1819
+ value = modelField.default;
1820
+ }
1821
+ if (!_.isNil(value)) {
1822
+ const fieldResult = await createNestedField({
1823
+ value,
1824
+ modelField,
1825
+ fieldPath: fieldPath.concat(fieldName),
1826
+ modelMap,
1827
+ locale,
1828
+ userContext,
1829
+ contentSourceInstance
1830
+ });
1831
+ result.fields[fieldName] = fieldResult.field;
1832
+ result.newRefDocuments = result.newRefDocuments.concat(fieldResult.newRefDocuments);
1833
+ }
1834
+ }
1835
+ if (objectFieldNames.length > 0) {
1836
+ throw new Error(`no model fields found when creating a document with fields: '${objectFieldNames.join(', ')}'`);
1837
+ }
1838
+
1839
+ return result;
1840
+ }
1841
+
1842
+ async function createNestedField({
1843
+ value,
1844
+ modelField,
1845
+ fieldPath,
1846
+ modelMap,
1847
+ locale,
1848
+ userContext,
1849
+ contentSourceInstance
1850
+ }: {
1851
+ value: any;
1852
+ modelField: FieldSpecificProps;
1853
+ fieldPath: (string | number)[];
1854
+ modelMap: Record<string, Model>;
1855
+ locale?: string;
1856
+ userContext: unknown;
1857
+ contentSourceInstance: CSITypes.ContentSourceInterface;
1858
+ }): Promise<{ field: CSITypes.UpdateOperationField; newRefDocuments: CSITypes.Document[] }> {
1859
+ if (modelField.type === 'object') {
1860
+ const result = await createNestedObjectRecursively({
1861
+ object: value,
1862
+ modelFields: modelField.fields,
1863
+ fieldPath,
1864
+ modelMap,
1865
+ locale,
1866
+ userContext,
1867
+ contentSourceInstance
1868
+ });
1869
+ return {
1870
+ field: {
1871
+ type: 'object',
1872
+ fields: result.fields
1873
+ },
1874
+ newRefDocuments: result.newRefDocuments
1875
+ };
1876
+ } else if (modelField.type === 'model') {
1877
+ let { $$type, ...rest } = value;
1878
+ const modelNames = modelField.models;
1879
+ // for backward compatibility check if the object has 'type' instead of '$$type' because older projects use
1880
+ // the 'type' property in default values
1881
+ if (!$$type && 'type' in rest) {
1882
+ $$type = rest.type;
1883
+ rest = _.omit(rest, 'type');
1884
+ }
1885
+ const modelName = $$type ?? (modelNames.length === 1 ? modelNames[0] : null);
1886
+ if (!modelName) {
1887
+ throw new Error(`no $$type was specified for nested model`);
1888
+ }
1889
+ const model = modelMap[modelName];
1890
+ if (!model) {
1891
+ throw new Error(`no model with name '${modelName}' was found`);
1892
+ }
1893
+ const result = await createNestedObjectRecursively({
1894
+ object: rest,
1895
+ modelFields: model.fields ?? [],
1896
+ fieldPath,
1897
+ modelMap,
1898
+ locale,
1899
+ userContext,
1900
+ contentSourceInstance
1901
+ });
1902
+ return {
1903
+ field: {
1904
+ type: 'model',
1905
+ modelName: modelName,
1906
+ fields: result.fields
1907
+ },
1908
+ newRefDocuments: result.newRefDocuments
1909
+ };
1910
+ } else if (modelField.type === 'image') {
1911
+ let refId: string | undefined;
1912
+ if (_.isPlainObject(value)) {
1913
+ refId = value.$$ref;
1914
+ } else {
1915
+ refId = value;
1916
+ }
1917
+ if (!refId) {
1918
+ throw new Error(`reference field must specify a value`);
1919
+ }
1920
+ return {
1921
+ field: {
1922
+ type: 'reference',
1923
+ refType: 'asset',
1924
+ refId: refId
1925
+ },
1926
+ newRefDocuments: []
1927
+ };
1928
+ } else if (modelField.type === 'reference') {
1929
+ let { $$ref: refId = null, $$type: modelName = null, ...rest } = _.isPlainObject(value) ? value : { $$ref: value };
1930
+ if (refId) {
1931
+ return {
1932
+ field: {
1933
+ type: 'reference',
1934
+ refType: 'document',
1935
+ refId: refId
1936
+ },
1937
+ newRefDocuments: []
1938
+ };
1939
+ } else {
1940
+ const modelNames = modelField.models;
1941
+ if (!modelName) {
1942
+ // for backward compatibility check if the object has 'type' instead of '$$type' because older projects use
1943
+ // the 'type' property in default values
1944
+ if ('type' in rest) {
1945
+ modelName = rest.type;
1946
+ rest = _.omit(rest, 'type');
1947
+ } else if (modelNames.length === 1) {
1948
+ modelName = modelNames[0];
1949
+ }
1950
+ }
1951
+ const model = modelMap[modelName];
1952
+ if (!model) {
1953
+ throw new Error(`no model with name '${modelName}' was found`);
1954
+ }
1955
+ const { document, newRefDocuments } = await createDocumentRecursively({
1956
+ object: rest,
1957
+ model: model,
1958
+ modelMap,
1959
+ locale,
1960
+ userContext,
1961
+ contentSourceInstance
1962
+ });
1963
+ return {
1964
+ field: {
1965
+ type: 'reference',
1966
+ refType: 'document',
1967
+ refId: document.id
1968
+ },
1969
+ newRefDocuments: [document, ...newRefDocuments]
1970
+ };
1971
+ }
1972
+ } else if (modelField.type === 'list') {
1973
+ if (!Array.isArray(value)) {
1974
+ throw new Error(`value for list field must be array`);
1975
+ }
1976
+ const itemsField = modelField.items;
1977
+ if (!itemsField) {
1978
+ throw new Error(`list field does not define items`);
1979
+ }
1980
+ const arrayResult = await mapPromise(value, async (item, index) => {
1981
+ return createNestedField({
1982
+ value: item,
1983
+ modelField: itemsField,
1984
+ fieldPath: fieldPath.concat(index),
1985
+ modelMap,
1986
+ locale,
1987
+ userContext,
1988
+ contentSourceInstance
1989
+ });
1990
+ });
1991
+ return {
1992
+ field: {
1993
+ type: 'list',
1994
+ items: arrayResult.map((result) => result.field)
1995
+ },
1996
+ newRefDocuments: arrayResult.reduce((result: CSITypes.Document[], { newRefDocuments }) => result.concat(newRefDocuments), [])
1997
+ };
1998
+ }
1999
+ return {
2000
+ field: {
2001
+ type: modelField.type,
2002
+ value: value
2003
+ },
2004
+ newRefDocuments: []
2005
+ };
2006
+ }
2007
+
2008
+ function getModelFieldForFieldAtPath(
2009
+ document: ContentStoreTypes.Document,
2010
+ model: Model,
2011
+ fieldPath: (string | number)[],
2012
+ modelMap: Record<string, Model>,
2013
+ locale?: string
2014
+ ): Field {
2015
+ if (_.isEmpty(fieldPath)) {
2016
+ throw new Error('the fieldPath can not be empty');
2017
+ }
2018
+
2019
+ function getField(docField: ContentStoreTypes.DocumentField, modelField: FieldSpecificProps, fieldPath: (string | number)[]): Field {
2020
+ const fieldName = _.head(fieldPath);
2021
+ if (typeof fieldName === 'undefined') {
2022
+ throw new Error('the first fieldPath item must be string');
2023
+ }
2024
+ const childFieldPath = _.tail(fieldPath);
2025
+ let childDocField: ContentStoreTypes.DocumentField | undefined;
2026
+ let childModelField: Field | undefined;
2027
+ switch (docField.type) {
2028
+ case 'object':
2029
+ const localizedObjectField = getDocumentFieldForLocale(docField, locale);
2030
+ if (!localizedObjectField) {
2031
+ throw new Error(`locale for field was not found`);
2032
+ }
2033
+ if (localizedObjectField.isUnset) {
2034
+ throw new Error(`field is not set`);
2035
+ }
2036
+ childDocField = localizedObjectField.fields[fieldName];
2037
+ childModelField = _.find((modelField as FieldObjectProps).fields, (field) => field.name === fieldName);
2038
+ if (!childDocField || !childModelField) {
2039
+ throw new Error(`field ${fieldName} doesn't exist`);
2040
+ }
2041
+ if (childFieldPath.length === 0) {
2042
+ return childModelField;
2043
+ }
2044
+ return getField(childDocField, childModelField, childFieldPath);
2045
+ case 'model':
2046
+ const localizedModelField = getDocumentFieldForLocale(docField, locale);
2047
+ if (!localizedModelField) {
2048
+ throw new Error(`locale for field was not found`);
2049
+ }
2050
+ if (localizedModelField.isUnset) {
2051
+ throw new Error(`field is not set`);
2052
+ }
2053
+ const modelName = localizedModelField.srcModelName;
2054
+ const childModel = modelMap[modelName];
2055
+ if (!childModel) {
2056
+ throw new Error(`model ${modelName} doesn't exist`);
2057
+ }
2058
+ childModelField = _.find(childModel.fields, (field) => field.name === fieldName);
2059
+ childDocField = localizedModelField.fields![fieldName];
2060
+ if (!childDocField || !childModelField) {
2061
+ throw new Error(`field ${fieldName} doesn't exist`);
2062
+ }
2063
+ if (childFieldPath.length === 0) {
2064
+ return childModelField;
2065
+ }
2066
+ return getField(childDocField, childModelField!, childFieldPath);
2067
+ case 'list':
2068
+ const localizedListField = getDocumentFieldForLocale(docField, locale);
2069
+ if (!localizedListField) {
2070
+ throw new Error(`locale for field was not found`);
2071
+ }
2072
+ const listItem = localizedListField.items && localizedListField.items[fieldName as number];
2073
+ const listItemsModel = (modelField as FieldListProps).items;
2074
+ if (!listItem || !listItemsModel) {
2075
+ throw new Error(`field ${fieldName} doesn't exist`);
2076
+ }
2077
+ if (childFieldPath.length === 0) {
2078
+ return modelField as FieldList;
2079
+ }
2080
+ if (!Array.isArray(listItemsModel)) {
2081
+ return getField(listItem, listItemsModel, childFieldPath);
2082
+ } else {
2083
+ const fieldListItems = (listItemsModel as FieldListItems[]).find((listItemsModel) => listItemsModel.type === listItem.type);
2084
+ if (!fieldListItems) {
2085
+ throw new Error('cannot find matching field model');
2086
+ }
2087
+ return getField(listItem, fieldListItems, childFieldPath);
2088
+ }
2089
+ default:
2090
+ if (!_.isEmpty(childFieldPath)) {
2091
+ throw new Error('illegal fieldPath');
2092
+ }
2093
+ return modelField as Field;
2094
+ }
2095
+ }
2096
+
2097
+ const fieldName = _.head(fieldPath);
2098
+ const childFieldPath = _.tail(fieldPath);
2099
+
2100
+ if (typeof fieldName !== 'string') {
2101
+ throw new Error('the first fieldPath item must be string');
2102
+ }
2103
+
2104
+ const childDocField = document.fields[fieldName];
2105
+ const childModelField = _.find(model.fields, { name: fieldName });
2106
+
2107
+ if (!childDocField || !childModelField) {
2108
+ throw new Error(`field ${fieldName} doesn't exist`);
2109
+ }
2110
+
2111
+ if (childFieldPath.length === 0) {
2112
+ return childModelField;
2113
+ }
2114
+
2115
+ return getField(childDocField, childModelField, childFieldPath);
2116
+ }
2117
+
2118
+ async function convertOperationField({
2119
+ operationField,
2120
+ fieldPath,
2121
+ modelField,
2122
+ modelMap,
2123
+ locale,
2124
+ userContext,
2125
+ contentSourceInstance
2126
+ }: {
2127
+ operationField: ContentStoreTypes.UpdateOperationField;
2128
+ fieldPath: (string | number)[];
2129
+ modelField: Field;
2130
+ modelMap: Record<string, Model>;
2131
+ locale?: string;
2132
+ userContext: unknown;
2133
+ contentSourceInstance: CSITypes.ContentSourceInterface;
2134
+ }): Promise<CSITypes.UpdateOperationField> {
2135
+ // for insert operations, the modelField will be of the list, so get the modelField of the list items
2136
+ const modelFieldOrListItems: FieldSpecificProps = modelField.type === 'list' ? modelField.items! : modelField;
2137
+ switch (operationField.type) {
2138
+ case 'object': {
2139
+ const result = await createNestedObjectRecursively({
2140
+ object: operationField.object,
2141
+ modelFields: (modelFieldOrListItems as FieldObjectProps).fields,
2142
+ fieldPath: fieldPath,
2143
+ modelMap,
2144
+ locale,
2145
+ userContext,
2146
+ contentSourceInstance
2147
+ });
2148
+ return {
2149
+ type: operationField.type,
2150
+ fields: result.fields
2151
+ };
2152
+ }
2153
+ case 'model': {
2154
+ const model = modelMap[operationField.modelName];
2155
+ if (!model) {
2156
+ throw new Error(`error updating document, could not find document model: '${operationField.modelName}'`);
2157
+ }
2158
+ const result = await createNestedObjectRecursively({
2159
+ object: operationField.object,
2160
+ modelFields: model.fields!,
2161
+ fieldPath,
2162
+ modelMap,
2163
+ locale,
2164
+ userContext,
2165
+ contentSourceInstance
2166
+ });
2167
+ return {
2168
+ type: operationField.type,
2169
+ modelName: operationField.modelName,
2170
+ fields: result.fields
2171
+ };
2172
+ }
2173
+ case 'list': {
2174
+ if (modelField.type !== 'list') {
2175
+ throw new Error(`'the operation field type '${operationField.type}' does not match the model field type '${modelField.type}'`);
2176
+ }
2177
+ const result = await mapPromise(operationField.items, async (item, index) => {
2178
+ const result = await createNestedField({
2179
+ value: item,
2180
+ modelField: modelField.items!,
2181
+ fieldPath,
2182
+ modelMap,
2183
+ locale,
2184
+ userContext,
2185
+ contentSourceInstance
2186
+ });
2187
+ return result.field;
2188
+ });
2189
+ return {
2190
+ type: operationField.type,
2191
+ items: result
2192
+ };
2193
+ }
2194
+ case 'string':
2195
+ if (typeof operationField.value !== 'string') {
2196
+ return {
2197
+ type: operationField.type,
2198
+ value: ''
2199
+ };
2200
+ }
2201
+ return operationField as CSITypes.UpdateOperationField;
2202
+ case 'enum':
2203
+ if (typeof operationField.value !== 'string') {
2204
+ if (modelFieldOrListItems.type !== 'enum') {
2205
+ throw new Error(`'the operation field type 'enum' does not match the model field type '${modelFieldOrListItems.type}'`);
2206
+ }
2207
+ const option = modelFieldOrListItems.options[0]!;
2208
+ const optionValue = typeof option === 'object' ? option.value : option;
2209
+ return {
2210
+ type: operationField.type,
2211
+ value: optionValue
2212
+ };
2213
+ }
2214
+ return operationField as CSITypes.UpdateOperationField;
2215
+ case 'image':
2216
+ return operationField as CSITypes.UpdateOperationField;
2217
+ default:
2218
+ return operationField as CSITypes.UpdateOperationField;
2219
+ }
2220
+ }
2221
+
2222
+ function getDocumentFieldForLocale<Type extends ContentStoreTypes.FieldType>(
2223
+ docField: ContentStoreTypes.DocumentFieldForType<Type>,
2224
+ locale?: string
2225
+ ): ContentStoreTypes.DocumentFieldNonLocalizedForType<Type> | null {
2226
+ if (docField.localized) {
2227
+ if (!locale) {
2228
+ return null;
2229
+ }
2230
+ const { localized, locales, ...base } = docField;
2231
+ const localizedField = locales[locale];
2232
+ if (!localizedField) {
2233
+ return null;
2234
+ }
2235
+ return ({
2236
+ ...base,
2237
+ ...localizedField
2238
+ } as unknown) as ContentStoreTypes.DocumentFieldNonLocalizedForType<Type>;
2239
+ } else {
2240
+ return docField;
2241
+ }
2242
+ }
2243
+
2244
+ function isStackbitConfigFile(filePath: string) {
2245
+ const pathObject = path.parse(filePath);
2246
+ const isInDotStackbitFolder = pathObject.dir.startsWith('.stackbit');
2247
+ const isMainStackbitConfigFile = pathObject.name === 'stackbit' && ['yaml', 'yml'].includes(pathObject.ext.substring(1));
2248
+ return isMainStackbitConfigFile || isInDotStackbitFolder;
2249
+ }
2250
+
2251
+ function getCSIDocumentsAndAssetsFromContentSourceDataByIds(
2252
+ contentSourceData: ContentSourceData,
2253
+ objects: { srcObjectId: string }[]
2254
+ ): {
2255
+ documents: CSITypes.Document[];
2256
+ assets: CSITypes.Asset[];
2257
+ } {
2258
+ const documents: CSITypes.Document[] = [];
2259
+ const assets: CSITypes.Asset[] = [];
2260
+ for (const object of objects) {
2261
+ if (object.srcObjectId in contentSourceData.csiDocumentMap) {
2262
+ documents.push(contentSourceData.csiDocumentMap[object.srcObjectId]!);
2263
+ } else if (object.srcObjectId in contentSourceData.csiAssetMap) {
2264
+ assets.push(contentSourceData.csiAssetMap[object.srcObjectId]!);
2265
+ }
2266
+ }
2267
+ return {
2268
+ documents,
2269
+ assets
2270
+ };
2271
+ }
2272
+
2273
+ function internalValidateContent(
2274
+ documents: CSITypes.Document[],
2275
+ assets: CSITypes.Asset[],
2276
+ contentSourceData: ContentSourceData
2277
+ ): ContentStoreTypes.ValidationError[] {
2278
+
2279
+ const errors: ContentStoreTypes.ValidationError[] = [];
2280
+ _.forEach(documents, (document) => {
2281
+ _.forEach(document.fields, (documentField, fieldName) => {
2282
+ const localizedField = getLocalizedFieldForLocale(documentField)!;
2283
+ errors.push(...validateDocumentFields(document, localizedField, [fieldName], contentSourceData));
2284
+ })
2285
+ })
2286
+ return errors;
2287
+ }
2288
+
2289
+ function validateDocumentFields(
2290
+ document: CSITypes.Document,
2291
+ documentField: CSITypes.DocumentFieldNonLocalized,
2292
+ fieldPath: (string | number)[],
2293
+ contentSourceData: ContentSourceData
2294
+ ): ContentStoreTypes.ValidationError[] {
2295
+ const errors: ContentStoreTypes.ValidationError[] = [];
2296
+
2297
+ if (documentField.type === 'object') {
2298
+ _.forEach(documentField.fields, (documentField, fieldName) => {
2299
+ const localizedField = getLocalizedFieldForLocale(documentField)!;
2300
+ errors.push(...validateDocumentFields(document, localizedField, fieldPath.concat(fieldName), contentSourceData));
2301
+ });
2302
+ } else if (documentField.type === 'model') {
2303
+ _.forEach(documentField.fields, (documentField, fieldName) => {
2304
+ const localizedField = getLocalizedFieldForLocale(documentField)!;
2305
+ errors.push(...validateDocumentFields(document, localizedField, fieldPath.concat(fieldName), contentSourceData));
2306
+ });
2307
+ } else if (documentField.type === 'reference') {
2308
+ const objRef = documentField.refType === 'asset'
2309
+ ? contentSourceData.assetMap[documentField.refId]
2310
+ : contentSourceData.documentMap[documentField.refId];
2311
+ if (!objRef) {
2312
+ errors.push({
2313
+ fieldPath,
2314
+ srcType: contentSourceData.type,
2315
+ srcProjectId: contentSourceData.projectId,
2316
+ srcObjectType: documentField.refType,
2317
+ srcObjectId: document.id,
2318
+ message: `Can't find referenced ${documentField.refType}: ${documentField.refId}`
2319
+ });
2320
+ }
2321
+ } else if (documentField.type === 'list') {
2322
+ _.forEach(documentField.items, (documentField, i) => {
2323
+ const localizedField = getLocalizedFieldForLocale(documentField)!;
2324
+ errors.push(...validateDocumentFields(document, documentField, fieldPath.concat(i), contentSourceData));
2325
+ });
2326
+ }
2327
+
2328
+ return errors;
2329
+ }