@stackbit/cms-core 0.0.19-alpha.0 → 0.0.20

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