@stackbit/cms-core 0.0.18-alpha.0 → 0.0.19-alpha.2

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.
@@ -16,7 +16,7 @@ import {
16
16
  Model
17
17
  } from '@stackbit/sdk';
18
18
  import { mapPromise, omitByNil } from '@stackbit/utils';
19
- import * as ContentSourceTypes from './content-source-interface';
19
+ import * as CSITypes from './content-source-interface';
20
20
  import { isLocalizedField } from './content-source-interface';
21
21
  import * as ContentStoreTypes from './content-store-types';
22
22
  import { IMAGE_MODEL } from './common/common-schema';
@@ -27,7 +27,7 @@ export interface ContentSourceOptions {
27
27
  userLogger: ContentStoreTypes.Logger;
28
28
  localDev: boolean;
29
29
  stackbitYamlDir: string;
30
- contentSources: ContentSourceTypes.ContentSourceInterface[];
30
+ contentSources: CSITypes.ContentSourceInterface[];
31
31
  onSchemaChangeCallback: () => void;
32
32
  onContentChangeCallback: (contentChanges: ContentStoreTypes.ContentChangeResult) => void;
33
33
  handleConfigAssets: (config: Config) => Promise<Config>;
@@ -35,21 +35,25 @@ export interface ContentSourceOptions {
35
35
 
36
36
  interface ContentSourceData {
37
37
  id: string;
38
- instance: ContentSourceTypes.ContentSourceInterface;
38
+ instance: CSITypes.ContentSourceInterface;
39
39
  type: string;
40
40
  projectId: string;
41
41
  models: Model[];
42
42
  modelMap: Record<string, Model>;
43
- locales?: ContentSourceTypes.Locale[];
43
+ locales?: CSITypes.Locale[];
44
44
  defaultLocaleCode?: string;
45
+ csiDocuments: CSITypes.Document[];
46
+ csiDocumentMap: Record<string, CSITypes.Document>;
45
47
  documents: ContentStoreTypes.Document[];
46
48
  documentMap: Record<string, ContentStoreTypes.Document>;
49
+ csiAssets: CSITypes.Asset[];
50
+ csiAssetMap: Record<string, CSITypes.Asset>;
47
51
  assets: ContentStoreTypes.Asset[];
48
52
  assetMap: Record<string, ContentStoreTypes.Asset>;
49
53
  }
50
54
 
51
55
  export class ContentStore {
52
- private readonly contentSources: ContentSourceTypes.ContentSourceInterface[];
56
+ private readonly contentSources: CSITypes.ContentSourceInterface[];
53
57
  private readonly logger: ContentStoreTypes.Logger;
54
58
  private readonly userLogger: ContentStoreTypes.Logger;
55
59
  private readonly localDev: boolean;
@@ -205,7 +209,7 @@ export class ContentStore {
205
209
  return config;
206
210
  }
207
211
 
208
- async loadContentSourceData({ contentSourceInstance, init }: { contentSourceInstance: ContentSourceTypes.ContentSourceInterface; init: boolean }) {
212
+ async loadContentSourceData({ contentSourceInstance, init }: { contentSourceInstance: CSITypes.ContentSourceInterface; init: boolean }) {
209
213
  // TODO: defer loading content if content is already loading for a specific content source
210
214
 
211
215
  // TODO: optimize: cache raw responses from contentSource
@@ -242,19 +246,24 @@ export class ContentStore {
242
246
  // that maps presetIds by model name instead of storing that map inside every model
243
247
  this.presets = config.presets;
244
248
 
245
- const documents = await contentSourceInstance.getDocuments({ modelMap });
246
- const assets = await contentSourceInstance.getAssets();
247
- const contentStoreDocuments = mapSourceDocumentsToStoreDocuments({
248
- documents,
249
+ const csiDocuments = await contentSourceInstance.getDocuments({ modelMap });
250
+ const csiAssets = await contentSourceInstance.getAssets();
251
+ const csiDocumentMap = _.keyBy(csiDocuments, 'id');
252
+ const csiAssetMap = _.keyBy(csiAssets, 'id');
253
+
254
+ const contentStoreDocuments = mapCSIDocumentsToStoreDocuments({
255
+ csiDocuments,
249
256
  contentSourceInstance,
250
257
  modelMap,
251
258
  defaultLocaleCode
252
259
  });
253
- const contentStoreAssets = mapSourceAssetsToStoreAssets({
254
- assets,
260
+ const contentStoreAssets = mapCSIAssetsToStoreAssets({
261
+ csiAssets,
255
262
  contentSourceInstance,
256
263
  defaultLocaleCode
257
264
  });
265
+ const documentMap = _.keyBy(contentStoreDocuments, 'srcObjectId');
266
+ const assetMap = _.keyBy(contentStoreAssets, 'srcObjectId');
258
267
 
259
268
  this.logger.debug('loaded content source data', {
260
269
  contentSourceId,
@@ -274,10 +283,14 @@ export class ContentStore {
274
283
  defaultLocaleCode: defaultLocaleCode,
275
284
  models: models,
276
285
  modelMap: modelMap,
286
+ csiDocuments: csiDocuments,
287
+ csiDocumentMap: csiDocumentMap,
277
288
  documents: contentStoreDocuments,
278
- documentMap: _.keyBy(contentStoreDocuments, 'srcObjectId'),
289
+ documentMap: documentMap,
290
+ csiAssets: csiAssets,
291
+ csiAssetMap: csiAssetMap,
279
292
  assets: contentStoreAssets,
280
- assetMap: _.keyBy(contentStoreAssets, 'srcObjectId')
293
+ assetMap: assetMap
281
294
  };
282
295
 
283
296
  contentSourceInstance.startWatchingContentUpdates({
@@ -285,7 +298,13 @@ export class ContentStore {
285
298
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
286
299
  return contentSourceData.modelMap;
287
300
  },
288
- onContentChange: (contentChangeEvent: ContentSourceTypes.ContentChangeEvent) => {
301
+ getDocument({ documentId }: { documentId: string }) {
302
+ return csiDocumentMap[documentId];
303
+ },
304
+ getAsset({ assetId }: { assetId: string }) {
305
+ return csiAssetMap[assetId];
306
+ },
307
+ onContentChange: (contentChangeEvent: CSITypes.ContentChangeEvent) => {
289
308
  this.logger.debug('content source called onContentChange', { contentSourceId });
290
309
  const result = this.onContentChange(contentSourceId, contentChangeEvent);
291
310
  this.onContentChangeCallback(result);
@@ -301,7 +320,7 @@ export class ContentStore {
301
320
  });
302
321
  }
303
322
 
304
- onContentChange(contentSourceId: string, contentChangeEvent: ContentSourceTypes.ContentChangeEvent): ContentStoreTypes.ContentChangeResult {
323
+ onContentChange(contentSourceId: string, contentChangeEvent: CSITypes.ContentChangeEvent): ContentStoreTypes.ContentChangeResult {
305
324
  // TODO: prevent content change process for contentSourceId if loading content is in progress
306
325
 
307
326
  this.logger.debug('onContentChange', {
@@ -320,50 +339,80 @@ export class ContentStore {
320
339
  };
321
340
 
322
341
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
342
+
343
+ // update contentSourceData with deleted documents
323
344
  contentChangeEvent.deletedDocumentIds.forEach((docId) => {
345
+ // delete document from documents map
324
346
  delete contentSourceData.documentMap[docId];
347
+ delete contentSourceData.csiDocumentMap[docId];
348
+
349
+ // delete document from document array
325
350
  const index = contentSourceData.documents.findIndex((document) => document.srcObjectId === docId);
326
351
  if (index !== -1) {
352
+ // the indexes of documents and csiDocuments are always the same as they are always updated at the same time
327
353
  contentSourceData.documents.splice(index, 1);
354
+ contentSourceData.csiDocuments.splice(index, 1);
328
355
  }
356
+
329
357
  result.deletedDocuments.push({
330
358
  srcType: contentSourceData.type,
331
359
  srcProjectId: contentSourceData.projectId,
332
360
  srcObjectId: docId
333
361
  });
334
362
  });
363
+
364
+ // update contentSourceData with deleted assets
335
365
  contentChangeEvent.deletedAssetIds.forEach((assetId) => {
366
+ // delete document from asset map
336
367
  delete contentSourceData.assetMap[assetId];
368
+ delete contentSourceData.csiAssetMap[assetId];
369
+
370
+ // delete document from asset array
337
371
  const index = contentSourceData.assets.findIndex((asset) => asset.srcObjectId === assetId);
338
372
  if (index !== -1) {
373
+ // the indexes of assets and csiAssets are always the same as they are always updated at the same time
339
374
  contentSourceData.assets.splice(index, 1);
375
+ contentSourceData.csiAssets.splice(index, 1);
340
376
  }
377
+
341
378
  result.deletedAssets.push({
342
379
  srcType: contentSourceData.type,
343
380
  srcProjectId: contentSourceData.projectId,
344
381
  srcObjectId: assetId
345
382
  });
346
383
  });
347
- const documents = mapSourceDocumentsToStoreDocuments({
348
- documents: contentChangeEvent.documents,
384
+
385
+ // map csi documents and assets to content store documents and assets
386
+ const documents = mapCSIDocumentsToStoreDocuments({
387
+ csiDocuments: contentChangeEvent.documents,
349
388
  contentSourceInstance: contentSourceData.instance,
350
389
  modelMap: contentSourceData.modelMap,
351
390
  defaultLocaleCode: contentSourceData.defaultLocaleCode
352
391
  });
353
- const assets = mapSourceAssetsToStoreAssets({
354
- assets: contentChangeEvent.assets,
392
+ const assets = mapCSIAssetsToStoreAssets({
393
+ csiAssets: contentChangeEvent.assets,
355
394
  contentSourceInstance: contentSourceData.instance,
356
395
  defaultLocaleCode: contentSourceData.defaultLocaleCode
357
396
  });
358
397
 
398
+ // update contentSourceData with new or updated documents and assets
399
+ Object.assign(contentSourceData.csiDocumentMap, _.keyBy(contentChangeEvent.documents, 'id'));
400
+ Object.assign(contentSourceData.csiAssets, _.keyBy(contentChangeEvent.assets, 'id'));
359
401
  Object.assign(contentSourceData.documentMap, _.keyBy(documents, 'srcObjectId'));
360
402
  Object.assign(contentSourceData.assetMap, _.keyBy(assets, 'srcObjectId'));
361
- for (const document of documents) {
362
- const index = contentSourceData.documents.findIndex((existingDoc) => existingDoc.srcObjectId === document.srcObjectId);
363
- if (index === -1) {
403
+
404
+ for (let idx = 0; idx < documents.length; idx++) {
405
+ // the indexes of mapped documents and documents from changeEvent are the same
406
+ const document = documents[idx]!;
407
+ const csiDocument = contentChangeEvent.documents[idx]!;
408
+ const dataIndex = contentSourceData.documents.findIndex((existingDoc) => existingDoc.srcObjectId === document.srcObjectId);
409
+ if (dataIndex === -1) {
364
410
  contentSourceData.documents.push(document);
411
+ contentSourceData.csiDocuments.push(csiDocument);
365
412
  } else {
366
- contentSourceData.documents.splice(index, 1, document);
413
+ // the indexes of documents and csiDocuments are always the same as they are always updated at the same time
414
+ contentSourceData.documents.splice(dataIndex, 1, document);
415
+ contentSourceData.csiDocuments.splice(dataIndex, 1, csiDocument);
367
416
  }
368
417
  result.updatedDocuments.push({
369
418
  srcType: contentSourceData.type,
@@ -371,12 +420,19 @@ export class ContentStore {
371
420
  srcObjectId: document.srcObjectId
372
421
  });
373
422
  }
374
- for (const asset of assets) {
423
+
424
+ for (let idx = 0; idx < assets.length; idx++) {
425
+ // the indexes of mapped assets and assets from changeEvent are the same
426
+ const asset = assets[idx]!;
427
+ const csiAsset = contentChangeEvent.assets[idx]!;
375
428
  const index = contentSourceData.assets.findIndex((existingAsset) => existingAsset.srcObjectId === asset.srcObjectId);
376
429
  if (index === -1) {
377
430
  contentSourceData.assets.push(asset);
431
+ contentSourceData.csiAssets.push(csiAsset);
378
432
  } else {
433
+ // the indexes of assets and csiAssets are always the same as they are always updated at the same time
379
434
  contentSourceData.assets.splice(index, 1, asset);
435
+ contentSourceData.csiAssets.splice(index, 1, csiAsset);
380
436
  }
381
437
  result.updatedAssets.push({
382
438
  srcType: contentSourceData.type,
@@ -614,7 +670,8 @@ export class ContentStore {
614
670
 
615
671
  // get the document that is being updated
616
672
  const document = contentSourceData.documentMap[srcDocumentId];
617
- if (!document) {
673
+ const csiDocument = contentSourceData.csiDocumentMap[srcDocumentId];
674
+ if (!document || !csiDocument) {
618
675
  throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
619
676
  }
620
677
 
@@ -663,7 +720,7 @@ export class ContentStore {
663
720
  refId: result.srcDocumentId
664
721
  } as const;
665
722
  const updatedDocument = await contentSourceData.instance.updateDocument({
666
- documentId: srcDocumentId,
723
+ document: csiDocument,
667
724
  modelMap: modelMap,
668
725
  userContext: userContext,
669
726
  operations: [
@@ -713,7 +770,8 @@ export class ContentStore {
713
770
  const contentSourceId = getContentSourceId(srcType, srcProjectId);
714
771
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
715
772
  const document = contentSourceData.documentMap[srcDocumentId];
716
- if (!document) {
773
+ const csiDocument = contentSourceData.csiDocumentMap[srcDocumentId];
774
+ if (!document || !csiDocument) {
717
775
  throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
718
776
  }
719
777
 
@@ -753,7 +811,7 @@ export class ContentStore {
753
811
  refId: result.id
754
812
  } as const;
755
813
  const updatedDocument = await contentSourceData.instance.updateDocument({
756
- documentId: srcDocumentId,
814
+ document: csiDocument,
757
815
  modelMap: modelMap,
758
816
  userContext: userContext,
759
817
  operations: [
@@ -841,21 +899,23 @@ export class ContentStore {
841
899
 
842
900
  const contentSourceId = getContentSourceId(srcType, srcProjectId);
843
901
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
844
- const modelMap = contentSourceData.modelMap;
845
902
  const userContext = getUserContextForSrcType(srcType, user);
903
+ const document = contentSourceData.documentMap[srcDocumentId];
904
+ const csiDocument = contentSourceData.csiDocumentMap[srcDocumentId];
905
+ if (!document || !csiDocument) {
906
+ throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
907
+ }
908
+
909
+ const modelMap = contentSourceData.modelMap;
910
+ const documentModelName = document.srcModelName;
911
+ const model = modelMap[documentModelName];
912
+ if (!model) {
913
+ throw new Error(`error updating document, could not find document model: '${documentModelName}'`);
914
+ }
846
915
 
847
916
  const operations = await mapPromise(
848
917
  updateOperations,
849
- async (updateOperation): Promise<ContentSourceTypes.UpdateOperation> => {
850
- const document = contentSourceData.documentMap[srcDocumentId];
851
- if (!document) {
852
- throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
853
- }
854
- const documentModelName = document.srcModelName;
855
- const model = modelMap[documentModelName];
856
- if (!model) {
857
- throw new Error(`error updating document, could not find document model: '${documentModelName}'`);
858
- }
918
+ async (updateOperation): Promise<CSITypes.UpdateOperation> => {
859
919
  const locale = updateOperation.locale ?? contentSourceData.defaultLocaleCode;
860
920
  const modelField = getModelFieldForFieldAtPath(document, model, updateOperation.fieldPath, modelMap, locale);
861
921
  switch (updateOperation.opType) {
@@ -899,8 +959,8 @@ export class ContentStore {
899
959
  }
900
960
  );
901
961
 
902
- const document = await contentSourceData.instance.updateDocument({
903
- documentId: srcDocumentId,
962
+ const updatedDocumentResult = await contentSourceData.instance.updateDocument({
963
+ document: csiDocument,
904
964
  modelMap,
905
965
  userContext,
906
966
  operations
@@ -911,7 +971,7 @@ export class ContentStore {
911
971
  // and use data from contentChangeEvent to update the cache
912
972
  // contentSourceData.documentMap = Object.assign(contentSourceData.documentMap, { [document.srcObjectId]: document });
913
973
 
914
- return { srcDocumentId: document.id };
974
+ return { srcDocumentId: updatedDocumentResult.id };
915
975
  }
916
976
 
917
977
  async duplicateDocument({
@@ -976,7 +1036,7 @@ export class ContentStore {
976
1036
 
977
1037
  const contentSourceId = getContentSourceId(srcType, srcProjectId);
978
1038
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
979
- const sourceAssets: ContentSourceTypes.Asset[] = [];
1039
+ const sourceAssets: CSITypes.Asset[] = [];
980
1040
  const userContext = getUserContextForSrcType(srcType, user);
981
1041
 
982
1042
  locale = locale ?? contentSourceData.defaultLocaleCode;
@@ -999,8 +1059,8 @@ export class ContentStore {
999
1059
  });
1000
1060
  sourceAssets.push(sourceAsset);
1001
1061
  }
1002
- const storeAssets = mapSourceAssetsToStoreAssets({
1003
- assets: sourceAssets,
1062
+ const storeAssets = mapCSIAssetsToStoreAssets({
1063
+ csiAssets: sourceAssets,
1004
1064
  contentSourceInstance: contentSourceData.instance,
1005
1065
  defaultLocaleCode: contentSourceData.defaultLocaleCode
1006
1066
  });
@@ -1023,7 +1083,11 @@ export class ContentStore {
1023
1083
  const userContext = getUserContextForSrcType(srcType, user);
1024
1084
  const contentSourceId = getContentSourceId(srcType, srcProjectId);
1025
1085
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1026
- await contentSourceData.instance.deleteDocument({ documentId: srcDocumentId, userContext });
1086
+ const csiDocument = contentSourceData.csiDocumentMap[srcDocumentId];
1087
+ if (!csiDocument) {
1088
+ throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
1089
+ }
1090
+ await contentSourceData.instance.deleteDocument({ document: csiDocument, userContext });
1027
1091
 
1028
1092
  // do not update cache in contentSourceData.documents and documentMap,
1029
1093
  // instead wait for contentSource to call onContentChange(contentChangeEvent)
@@ -1044,20 +1108,12 @@ export class ContentStore {
1044
1108
 
1045
1109
  const objectsBySourceId = _.groupBy(objects, (object) => getContentSourceId(object.srcType, object.srcProjectId));
1046
1110
  let errors: ContentStoreTypes.ValidationError[] = [];
1047
- for (const [contentSourceId, objects] of Object.entries(objectsBySourceId)) {
1111
+ for (const [contentSourceId, contentSourceObjects] of Object.entries(objectsBySourceId)) {
1048
1112
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1049
1113
  locale = locale ?? contentSourceData.defaultLocaleCode;
1050
- const documentIds = [];
1051
- const assetIds = [];
1052
- for (const object of objects) {
1053
- if (object.srcObjectId in contentSourceData.documentMap) {
1054
- documentIds.push(object.srcObjectId);
1055
- } else if (object.srcObjectId in contentSourceData.assetMap) {
1056
- assetIds.push(object.srcObjectId);
1057
- }
1058
- }
1114
+ const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
1059
1115
  const userContext = getUserContextForSrcType(contentSourceData.type, user);
1060
- const validationResult = await contentSourceData.instance.validateDocuments({ documentIds, assetIds, locale, userContext });
1116
+ const validationResult = await contentSourceData.instance.validateDocuments({ documents, assets, locale, userContext });
1061
1117
  errors = errors.concat(
1062
1118
  validationResult.errors.map((validationError) => ({
1063
1119
  message: validationError.message,
@@ -1093,19 +1149,11 @@ export class ContentStore {
1093
1149
  this.logger.debug('publishDocuments');
1094
1150
 
1095
1151
  const objectsBySourceId = _.groupBy(objects, (object) => getContentSourceId(object.srcType, object.srcProjectId));
1096
- for (const [contentSourceId, objects] of Object.entries(objectsBySourceId)) {
1152
+ for (const [contentSourceId, contentSourceObjects] of Object.entries(objectsBySourceId)) {
1097
1153
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1098
1154
  const userContext = getUserContextForSrcType(contentSourceData.type, user);
1099
- const documentIds = [];
1100
- const assetIds = [];
1101
- for (const object of objects) {
1102
- if (object.srcObjectId in contentSourceData.documentMap) {
1103
- documentIds.push(object.srcObjectId);
1104
- } else if (object.srcObjectId in contentSourceData.assetMap) {
1105
- assetIds.push(object.srcObjectId);
1106
- }
1107
- }
1108
- await contentSourceData.instance.publishDocuments({ documentIds, assetIds, userContext });
1155
+ const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
1156
+ await contentSourceData.instance.publishDocuments({ documents, assets, userContext });
1109
1157
  }
1110
1158
  }
1111
1159
 
@@ -1126,13 +1174,13 @@ function getUserContextForSrcType(srcType: string, user?: ContentStoreTypes.User
1126
1174
  return user?.connections?.find((connection) => connection.type === srcType);
1127
1175
  }
1128
1176
 
1129
- function mapSourceAssetsToStoreAssets({
1130
- assets,
1177
+ function mapCSIAssetsToStoreAssets({
1178
+ csiAssets,
1131
1179
  contentSourceInstance,
1132
1180
  defaultLocaleCode
1133
1181
  }: {
1134
- assets: ContentSourceTypes.Asset[];
1135
- contentSourceInstance: ContentSourceTypes.ContentSourceInterface;
1182
+ csiAssets: CSITypes.Asset[];
1183
+ contentSourceInstance: CSITypes.ContentSourceInterface;
1136
1184
  defaultLocaleCode?: string;
1137
1185
  }): ContentStoreTypes.Asset[] {
1138
1186
  const extra = {
@@ -1141,53 +1189,53 @@ function mapSourceAssetsToStoreAssets({
1141
1189
  srcProjectUrl: contentSourceInstance.getProjectManageUrl(),
1142
1190
  srcEnvironment: contentSourceInstance.getProjectEnvironment()
1143
1191
  };
1144
- return assets.map((asset) => sourceAssetToStoreAsset({ asset, defaultLocaleCode, extra }));
1192
+ return csiAssets.map((csiAsset) => sourceAssetToStoreAsset({ csiAsset, defaultLocaleCode, extra }));
1145
1193
  }
1146
1194
 
1147
1195
  function sourceAssetToStoreAsset({
1148
- asset,
1196
+ csiAsset,
1149
1197
  defaultLocaleCode,
1150
1198
  extra
1151
1199
  }: {
1152
- asset: ContentSourceTypes.Asset;
1200
+ csiAsset: CSITypes.Asset;
1153
1201
  defaultLocaleCode?: string;
1154
1202
  extra: { srcType: string; srcProjectId: string; srcProjectUrl: string; srcEnvironment: string };
1155
1203
  }): ContentStoreTypes.Asset {
1156
1204
  return {
1157
1205
  type: 'asset',
1158
1206
  ...extra,
1159
- srcObjectId: asset.id,
1160
- srcObjectUrl: asset.manageUrl,
1161
- srcObjectLabel: getObjectLabel(asset.fields, IMAGE_MODEL, defaultLocaleCode),
1207
+ srcObjectId: csiAsset.id,
1208
+ srcObjectUrl: csiAsset.manageUrl,
1209
+ srcObjectLabel: getObjectLabel(csiAsset.fields, IMAGE_MODEL, defaultLocaleCode),
1162
1210
  srcModelName: IMAGE_MODEL.name,
1163
1211
  srcModelLabel: IMAGE_MODEL.label!,
1164
- isChanged: asset.status === 'added' || asset.status === 'modified',
1165
- status: asset.status,
1166
- createdAt: asset.createdAt,
1167
- createdBy: asset.createdBy,
1168
- updatedAt: asset.updatedAt,
1169
- updatedBy: asset.updatedBy,
1212
+ isChanged: csiAsset.status === 'added' || csiAsset.status === 'modified',
1213
+ status: csiAsset.status,
1214
+ createdAt: csiAsset.createdAt,
1215
+ createdBy: csiAsset.createdBy,
1216
+ updatedAt: csiAsset.updatedAt,
1217
+ updatedBy: csiAsset.updatedBy,
1170
1218
  fields: {
1171
1219
  title: {
1172
1220
  label: 'Title',
1173
- ...asset.fields.title
1221
+ ...csiAsset.fields.title
1174
1222
  },
1175
1223
  file: {
1176
1224
  label: 'File',
1177
- ...asset.fields.file
1225
+ ...csiAsset.fields.file
1178
1226
  }
1179
1227
  }
1180
1228
  };
1181
1229
  }
1182
1230
 
1183
- function mapSourceDocumentsToStoreDocuments({
1184
- documents,
1231
+ function mapCSIDocumentsToStoreDocuments({
1232
+ csiDocuments,
1185
1233
  contentSourceInstance,
1186
1234
  modelMap,
1187
1235
  defaultLocaleCode
1188
1236
  }: {
1189
- documents: ContentSourceTypes.Document[];
1190
- contentSourceInstance: ContentSourceTypes.ContentSourceInterface;
1237
+ csiDocuments: CSITypes.Document[];
1238
+ contentSourceInstance: CSITypes.ContentSourceInterface;
1191
1239
  modelMap: Record<string, Model>;
1192
1240
  defaultLocaleCode?: string;
1193
1241
  }): ContentStoreTypes.Document[] {
@@ -1197,19 +1245,19 @@ function mapSourceDocumentsToStoreDocuments({
1197
1245
  srcProjectUrl: contentSourceInstance.getProjectManageUrl(),
1198
1246
  srcEnvironment: contentSourceInstance.getProjectEnvironment()
1199
1247
  };
1200
- return documents.map((document) =>
1201
- mapSourceDocumentToStoreDocument({ document, model: modelMap[document.modelName]!, modelMap, defaultLocaleCode, extra })
1248
+ return csiDocuments.map((csiDocument) =>
1249
+ mapCSIDocumentToStoreDocument({ csiDocument, model: modelMap[csiDocument.modelName]!, modelMap, defaultLocaleCode, extra })
1202
1250
  );
1203
1251
  }
1204
1252
 
1205
- function mapSourceDocumentToStoreDocument({
1206
- document,
1253
+ function mapCSIDocumentToStoreDocument({
1254
+ csiDocument,
1207
1255
  model,
1208
1256
  modelMap,
1209
1257
  defaultLocaleCode,
1210
1258
  extra
1211
1259
  }: {
1212
- document: ContentSourceTypes.Document;
1260
+ csiDocument: CSITypes.Document;
1213
1261
  model: Model;
1214
1262
  modelMap: Record<string, Model>;
1215
1263
  defaultLocaleCode?: string;
@@ -1218,44 +1266,48 @@ function mapSourceDocumentToStoreDocument({
1218
1266
  return {
1219
1267
  type: 'document',
1220
1268
  ...extra,
1221
- srcObjectId: document.id,
1222
- srcObjectUrl: document.manageUrl,
1223
- srcObjectLabel: getObjectLabel(document.fields, model, defaultLocaleCode),
1224
- srcModelLabel: model.label ?? _.startCase(document.modelName),
1225
- srcModelName: document.modelName,
1226
- isChanged: document.status === 'added' || document.status === 'modified',
1227
- status: document.status,
1228
- createdAt: document.createdAt,
1229
- createdBy: document.createdBy,
1230
- updatedAt: document.updatedAt,
1231
- updatedBy: document.updatedBy,
1232
- fields: mapSourceFieldsToStoreFields({
1233
- documentFields: document.fields,
1269
+ srcObjectId: csiDocument.id,
1270
+ srcObjectUrl: csiDocument.manageUrl,
1271
+ srcObjectLabel: getObjectLabel(csiDocument.fields, model, defaultLocaleCode),
1272
+ srcModelLabel: model.label ?? _.startCase(csiDocument.modelName),
1273
+ srcModelName: csiDocument.modelName,
1274
+ isChanged: csiDocument.status === 'added' || csiDocument.status === 'modified',
1275
+ status: csiDocument.status,
1276
+ createdAt: csiDocument.createdAt,
1277
+ createdBy: csiDocument.createdBy,
1278
+ updatedAt: csiDocument.updatedAt,
1279
+ updatedBy: csiDocument.updatedBy,
1280
+ fields: mapCSIFieldsToStoreFields({
1281
+ csiDocumentFields: csiDocument.fields,
1234
1282
  modelFields: model.fields ?? [],
1235
- modelMap,
1236
- defaultLocaleCode
1283
+ context: {
1284
+ modelMap,
1285
+ defaultLocaleCode
1286
+ }
1237
1287
  })
1238
1288
  };
1239
1289
  }
1240
1290
 
1241
- function mapSourceFieldsToStoreFields({
1242
- documentFields,
1291
+ type MapContext = {
1292
+ modelMap: Record<string, Model>;
1293
+ defaultLocaleCode?: string;
1294
+ };
1295
+
1296
+ function mapCSIFieldsToStoreFields({
1297
+ csiDocumentFields,
1243
1298
  modelFields,
1244
- modelMap,
1245
- defaultLocaleCode
1299
+ context
1246
1300
  }: {
1247
- documentFields: Record<string, ContentSourceTypes.DocumentField>;
1301
+ csiDocumentFields: Record<string, CSITypes.DocumentField>;
1248
1302
  modelFields: Field[];
1249
- modelMap: Record<string, Model>;
1250
- defaultLocaleCode?: string;
1303
+ context: MapContext;
1251
1304
  }): Record<string, ContentStoreTypes.DocumentField> {
1252
1305
  return modelFields.reduce((result: Record<string, ContentStoreTypes.DocumentField>, modelField) => {
1253
- const documentField = documentFields[modelField.name];
1254
- const docField = mapSourceFieldToStoreField({
1255
- documentField,
1306
+ const csiDocumentField = csiDocumentFields[modelField.name];
1307
+ const docField = mapCSIFieldToStoreField({
1308
+ csiDocumentField,
1256
1309
  modelField,
1257
- modelMap,
1258
- defaultLocaleCode
1310
+ context
1259
1311
  });
1260
1312
  docField.label = modelField.label;
1261
1313
  result[modelField.name] = docField;
@@ -1263,18 +1315,16 @@ function mapSourceFieldsToStoreFields({
1263
1315
  }, {});
1264
1316
  }
1265
1317
 
1266
- function mapSourceFieldToStoreField({
1267
- documentField,
1318
+ function mapCSIFieldToStoreField({
1319
+ csiDocumentField,
1268
1320
  modelField,
1269
- modelMap,
1270
- defaultLocaleCode
1321
+ context
1271
1322
  }: {
1272
- documentField: ContentSourceTypes.DocumentField | undefined;
1323
+ csiDocumentField: CSITypes.DocumentField | undefined;
1273
1324
  modelField: FieldSpecificProps;
1274
- modelMap: Record<string, Model>;
1275
- defaultLocaleCode?: string;
1325
+ context: MapContext;
1276
1326
  }): ContentStoreTypes.DocumentField {
1277
- if (!documentField) {
1327
+ if (!csiDocumentField) {
1278
1328
  const isUnset = ['object', 'model', 'reference', 'richText', 'markdown', 'image', 'file', 'json'].includes(modelField.type);
1279
1329
  return {
1280
1330
  type: modelField.type,
@@ -1285,129 +1335,112 @@ function mapSourceFieldToStoreField({
1285
1335
  // TODO: check if need to add "options" to "enum" and subtype/min/max to "number"
1286
1336
  switch (modelField.type) {
1287
1337
  case 'object':
1288
- return mapObjectField(documentField as ContentSourceTypes.DocumentFieldForType<typeof modelField.type>, modelField, modelMap, defaultLocaleCode);
1338
+ return mapObjectField(csiDocumentField as CSITypes.DocumentObjectField, modelField, context);
1289
1339
  case 'model':
1290
- return mapModelField(documentField as ContentSourceTypes.DocumentModelField, modelField, modelMap, defaultLocaleCode);
1340
+ return mapModelField(csiDocumentField as CSITypes.DocumentModelField, modelField, context);
1291
1341
  case 'list':
1292
- return mapListField(documentField as ContentSourceTypes.DocumentListField, modelField, modelMap, defaultLocaleCode);
1342
+ return mapListField(csiDocumentField as CSITypes.DocumentListField, modelField, context);
1293
1343
  case 'richText':
1294
- return mapRichTextField(documentField as ContentSourceTypes.DocumentRichTextField);
1344
+ return mapRichTextField(csiDocumentField as CSITypes.DocumentRichTextField);
1295
1345
  case 'markdown':
1296
- return mapMarkdownField(documentField as ContentSourceTypes.DocumentValueField);
1346
+ return mapMarkdownField(csiDocumentField as CSITypes.DocumentValueField);
1297
1347
  default:
1298
- return documentField as ContentStoreTypes.DocumentField;
1348
+ return csiDocumentField as ContentStoreTypes.DocumentField;
1299
1349
  }
1300
1350
  }
1301
1351
 
1302
1352
  function mapObjectField(
1303
- documentField: ContentSourceTypes.DocumentObjectField,
1353
+ csiDocumentField: CSITypes.DocumentObjectField,
1304
1354
  modelField: FieldObjectProps,
1305
- modelMap: Record<string, Model>,
1306
- defaultLocaleCode?: string
1355
+ context: MapContext
1307
1356
  ): ContentStoreTypes.DocumentObjectField {
1308
- if (!isLocalizedField(documentField)) {
1357
+ if (!isLocalizedField(csiDocumentField)) {
1309
1358
  return {
1310
- type: documentField.type,
1311
- srcObjectLabel: getObjectLabel(documentField.fields ?? {}, modelField ?? [], defaultLocaleCode),
1312
- fields: mapSourceFieldsToStoreFields({
1313
- documentFields: documentField.fields ?? {},
1359
+ type: csiDocumentField.type,
1360
+ srcObjectLabel: getObjectLabel(csiDocumentField.fields ?? {}, modelField ?? [], context.defaultLocaleCode),
1361
+ fields: mapCSIFieldsToStoreFields({
1362
+ csiDocumentFields: csiDocumentField.fields ?? {},
1314
1363
  modelFields: modelField.fields ?? [],
1315
- modelMap,
1316
- defaultLocaleCode
1364
+ context
1317
1365
  })
1318
1366
  };
1319
1367
  }
1320
1368
  return {
1321
- type: documentField.type,
1369
+ type: csiDocumentField.type,
1322
1370
  localized: true,
1323
- locales: _.mapValues(documentField.locales, (locale) => {
1371
+ locales: _.mapValues(csiDocumentField.locales, (locale) => {
1324
1372
  return {
1325
1373
  locale: locale.locale,
1326
1374
  srcObjectLabel: getObjectLabel(locale.fields ?? {}, modelField, locale.locale),
1327
- fields: mapSourceFieldsToStoreFields({
1328
- documentFields: locale.fields ?? {},
1375
+ fields: mapCSIFieldsToStoreFields({
1376
+ csiDocumentFields: locale.fields ?? {},
1329
1377
  modelFields: modelField.fields ?? [],
1330
- modelMap,
1331
- defaultLocaleCode
1378
+ context
1332
1379
  })
1333
1380
  };
1334
1381
  })
1335
1382
  };
1336
1383
  }
1337
1384
 
1338
- function mapModelField(
1339
- documentField: ContentSourceTypes.DocumentModelField,
1340
- modelField: FieldModelProps,
1341
- modelMap: Record<string, Model>,
1342
- defaultLocaleCode?: string
1343
- ): ContentStoreTypes.DocumentModelField {
1344
- if (!isLocalizedField(documentField)) {
1345
- const model = modelMap[documentField.modelName]!;
1385
+ function mapModelField(csiDocumentField: CSITypes.DocumentModelField, modelField: FieldModelProps, context: MapContext): ContentStoreTypes.DocumentModelField {
1386
+ if (!isLocalizedField(csiDocumentField)) {
1387
+ const model = context.modelMap[csiDocumentField.modelName]!;
1346
1388
  return {
1347
- type: documentField.type,
1348
- srcObjectLabel: getObjectLabel(documentField.fields ?? {}, model, defaultLocaleCode),
1349
- srcModelName: documentField.modelName,
1389
+ type: csiDocumentField.type,
1390
+ srcObjectLabel: getObjectLabel(csiDocumentField.fields ?? {}, model, context.defaultLocaleCode),
1391
+ srcModelName: csiDocumentField.modelName,
1350
1392
  srcModelLabel: model.label ?? _.startCase(model.name),
1351
- fields: mapSourceFieldsToStoreFields({
1352
- documentFields: documentField.fields ?? {},
1393
+ fields: mapCSIFieldsToStoreFields({
1394
+ csiDocumentFields: csiDocumentField.fields ?? {},
1353
1395
  modelFields: model.fields ?? [],
1354
- modelMap,
1355
- defaultLocaleCode
1396
+ context
1356
1397
  })
1357
1398
  };
1358
1399
  }
1359
1400
  return {
1360
- type: documentField.type,
1401
+ type: csiDocumentField.type,
1361
1402
  localized: true,
1362
- locales: _.mapValues(documentField.locales, (locale) => {
1363
- const model = modelMap[locale.modelName]!;
1403
+ locales: _.mapValues(csiDocumentField.locales, (locale) => {
1404
+ const model = context.modelMap[locale.modelName]!;
1364
1405
  return {
1365
1406
  locale: locale.locale,
1366
1407
  srcObjectLabel: getObjectLabel(locale.fields ?? {}, model, locale.locale),
1367
1408
  srcModelName: locale.modelName,
1368
1409
  srcModelLabel: model.label ?? _.startCase(model.name),
1369
- fields: mapSourceFieldsToStoreFields({
1370
- documentFields: locale.fields ?? {},
1410
+ fields: mapCSIFieldsToStoreFields({
1411
+ csiDocumentFields: locale.fields ?? {},
1371
1412
  modelFields: model.fields ?? [],
1372
- modelMap,
1373
- defaultLocaleCode
1413
+ context
1374
1414
  })
1375
1415
  };
1376
1416
  })
1377
1417
  };
1378
1418
  }
1379
1419
 
1380
- function mapListField(
1381
- documentField: ContentSourceTypes.DocumentListField,
1382
- modelField: FieldListProps,
1383
- modelMap: Record<string, Model>,
1384
- defaultLocaleCode?: string
1385
- ): ContentStoreTypes.DocumentListField {
1386
- if (!isLocalizedField(documentField)) {
1420
+ function mapListField(csiDocumentField: CSITypes.DocumentListField, modelField: FieldListProps, context: MapContext): ContentStoreTypes.DocumentListField {
1421
+ if (!isLocalizedField(csiDocumentField)) {
1387
1422
  return {
1388
- type: documentField.type,
1389
- items: documentField.items.map((item) =>
1390
- mapSourceFieldToStoreField({
1391
- documentField: item,
1423
+ type: csiDocumentField.type,
1424
+ items: csiDocumentField.items.map((item) =>
1425
+ mapCSIFieldToStoreField({
1426
+ csiDocumentField: item,
1392
1427
  modelField: modelField.items ?? { type: 'string' },
1393
- modelMap,
1394
- defaultLocaleCode
1428
+ context
1395
1429
  })
1396
1430
  )
1397
1431
  };
1398
1432
  }
1399
1433
  return {
1400
- type: documentField.type,
1434
+ type: csiDocumentField.type,
1401
1435
  localized: true,
1402
- locales: _.mapValues(documentField.locales, (locale) => {
1436
+ locales: _.mapValues(csiDocumentField.locales, (locale) => {
1403
1437
  return {
1404
1438
  locale: locale.locale,
1405
1439
  items: (locale.items ?? []).map((item) =>
1406
- mapSourceFieldToStoreField({
1407
- documentField: item,
1440
+ mapCSIFieldToStoreField({
1441
+ csiDocumentField: item,
1408
1442
  modelField: modelField.items ?? { type: 'string' },
1409
- modelMap,
1410
- defaultLocaleCode
1443
+ context
1411
1444
  })
1412
1445
  )
1413
1446
  };
@@ -1415,17 +1448,17 @@ function mapListField(
1415
1448
  };
1416
1449
  }
1417
1450
 
1418
- function mapRichTextField(documentField: ContentSourceTypes.DocumentRichTextField): ContentStoreTypes.DocumentRichTextField {
1419
- if (!isLocalizedField(documentField)) {
1451
+ function mapRichTextField(csiDocumentField: CSITypes.DocumentRichTextField): ContentStoreTypes.DocumentRichTextField {
1452
+ if (!isLocalizedField(csiDocumentField)) {
1420
1453
  return {
1421
- ...documentField,
1454
+ ...csiDocumentField,
1422
1455
  multiElement: true
1423
1456
  };
1424
1457
  }
1425
1458
  return {
1426
- type: documentField.type,
1459
+ type: csiDocumentField.type,
1427
1460
  localized: true,
1428
- locales: _.mapValues(documentField.locales, (locale) => {
1461
+ locales: _.mapValues(csiDocumentField.locales, (locale) => {
1429
1462
  return {
1430
1463
  ...locale,
1431
1464
  multiElement: true
@@ -1434,18 +1467,18 @@ function mapRichTextField(documentField: ContentSourceTypes.DocumentRichTextFiel
1434
1467
  };
1435
1468
  }
1436
1469
 
1437
- function mapMarkdownField(documentField: ContentSourceTypes.DocumentValueField): ContentStoreTypes.DocumentMarkdownField {
1438
- if (!isLocalizedField(documentField)) {
1470
+ function mapMarkdownField(csiDocumentField: CSITypes.DocumentValueField): ContentStoreTypes.DocumentMarkdownField {
1471
+ if (!isLocalizedField(csiDocumentField)) {
1439
1472
  return {
1440
1473
  type: 'markdown',
1441
- value: documentField.value,
1474
+ value: csiDocumentField.value,
1442
1475
  multiElement: true
1443
1476
  };
1444
1477
  }
1445
1478
  return {
1446
1479
  type: 'markdown',
1447
1480
  localized: true,
1448
- locales: _.mapValues(documentField.locales, (locale) => {
1481
+ locales: _.mapValues(csiDocumentField.locales, (locale) => {
1449
1482
  return {
1450
1483
  ...locale,
1451
1484
  multiElement: true
@@ -1462,12 +1495,12 @@ function mapStoreFieldsToSourceFields({
1462
1495
  documentFields: Record<string, ContentStoreTypes.DocumentField>;
1463
1496
  modelFields: Field[];
1464
1497
  modelMap: Record<string, Model>;
1465
- }): Record<string, ContentSourceTypes.DocumentField> {
1466
- // TODO:
1498
+ }): Record<string, CSITypes.DocumentField> {
1499
+ // TODO: implement
1467
1500
  throw new Error(`duplicateDocument not implemented yet`);
1468
1501
  }
1469
1502
 
1470
- function getContentSourceIdForContentSource(contentSource: ContentSourceTypes.ContentSourceInterface): string {
1503
+ function getContentSourceIdForContentSource(contentSource: CSITypes.ContentSourceInterface): string {
1471
1504
  return getContentSourceId(contentSource.getContentSourceType(), contentSource.getProjectId());
1472
1505
  }
1473
1506
 
@@ -1483,7 +1516,7 @@ function sanitizeSlug(slug: string) {
1483
1516
  }
1484
1517
 
1485
1518
  function getObjectLabel(
1486
- documentFields: Record<string, ContentSourceTypes.DocumentField | ContentSourceTypes.AssetFileField>,
1519
+ documentFields: Record<string, CSITypes.DocumentField | CSITypes.AssetFileField>,
1487
1520
  modelOrObjectField: Model | FieldObjectProps,
1488
1521
  locale?: string
1489
1522
  ): string {
@@ -1552,6 +1585,7 @@ function toLocalizedAPIField(docField: ContentStoreTypes.DocumentField, locale?:
1552
1585
  if (docFieldLocalized.type === 'object' || docFieldLocalized.type === 'model') {
1553
1586
  return {
1554
1587
  ...docFieldLocalized,
1588
+ type: 'object',
1555
1589
  ...commonProps,
1556
1590
  ...(docFieldLocalized.isUnset
1557
1591
  ? null
@@ -1671,8 +1705,8 @@ async function createDocumentRecursively({
1671
1705
  modelMap: Record<string, Model>;
1672
1706
  locale?: string;
1673
1707
  userContext: unknown;
1674
- contentSourceInstance: ContentSourceTypes.ContentSourceInterface;
1675
- }): Promise<{ document: ContentSourceTypes.Document; newRefDocuments: ContentSourceTypes.Document[] }> {
1708
+ contentSourceInstance: CSITypes.ContentSourceInterface;
1709
+ }): Promise<{ document: CSITypes.Document; newRefDocuments: CSITypes.Document[] }> {
1676
1710
  if (model.type === 'page') {
1677
1711
  const tokens = extractTokensFromString(String(model.urlPath));
1678
1712
  const slugField = _.last(tokens);
@@ -1692,7 +1726,7 @@ async function createDocumentRecursively({
1692
1726
  contentSourceInstance
1693
1727
  });
1694
1728
  const document = await contentSourceInstance.createDocument({
1695
- documentFields: nestedResult.fields,
1729
+ documentFields: nestedResult.fields as Record<string, CSITypes.DocumentField>,
1696
1730
  model,
1697
1731
  modelMap,
1698
1732
  locale,
@@ -1719,200 +1753,216 @@ async function createNestedObjectRecursively({
1719
1753
  modelMap: Record<string, Model>;
1720
1754
  locale?: string;
1721
1755
  userContext: unknown;
1722
- contentSourceInstance: ContentSourceTypes.ContentSourceInterface;
1756
+ contentSourceInstance: CSITypes.ContentSourceInterface;
1723
1757
  }): Promise<{
1724
- fields: Record<string, ContentSourceTypes.DocumentFieldNonLocalized>;
1725
- newRefDocuments: ContentSourceTypes.Document[];
1758
+ fields: Record<string, CSITypes.UpdateOperationField>;
1759
+ newRefDocuments: CSITypes.Document[];
1726
1760
  }> {
1727
- const createNestedField = async ({
1728
- value,
1729
- modelField,
1730
- fieldPath
1731
- }: {
1732
- value: any;
1733
- modelField: FieldSpecificProps;
1734
- fieldPath: (string | number)[];
1735
- }): Promise<{ field: ContentSourceTypes.DocumentFieldNonLocalized; newRefDocuments: ContentSourceTypes.Document[] }> => {
1736
- if (modelField.type === 'object') {
1737
- const result = await createNestedObjectRecursively({
1738
- object: value,
1739
- modelFields: modelField.fields,
1740
- fieldPath,
1761
+ object = object ?? {};
1762
+ const result: {
1763
+ fields: Record<string, CSITypes.UpdateOperationField>;
1764
+ newRefDocuments: CSITypes.Document[];
1765
+ } = {
1766
+ fields: {},
1767
+ newRefDocuments: []
1768
+ };
1769
+ const objectFieldNames = Object.keys(object);
1770
+ for (const modelField of modelFields) {
1771
+ const fieldName = modelField.name;
1772
+ let value;
1773
+ if (fieldName in object) {
1774
+ value = object[fieldName];
1775
+ _.pull(objectFieldNames, fieldName);
1776
+ } else if (modelField.const) {
1777
+ value = modelField.const;
1778
+ } else if (!_.isNil(modelField.default)) {
1779
+ value = modelField.default;
1780
+ }
1781
+ if (!_.isNil(value)) {
1782
+ const fieldResult = await createNestedField({
1783
+ value,
1784
+ modelField,
1785
+ fieldPath: fieldPath.concat(fieldName),
1741
1786
  modelMap,
1742
1787
  locale,
1743
1788
  userContext,
1744
1789
  contentSourceInstance
1745
1790
  });
1791
+ result.fields[fieldName] = fieldResult.field;
1792
+ result.newRefDocuments = result.newRefDocuments.concat(fieldResult.newRefDocuments);
1793
+ }
1794
+ }
1795
+ if (objectFieldNames.length > 0) {
1796
+ throw new Error(`no model fields found when creating a document with fields: '${objectFieldNames.join(', ')}'`);
1797
+ }
1798
+
1799
+ return result;
1800
+ }
1801
+
1802
+ async function createNestedField({
1803
+ value,
1804
+ modelField,
1805
+ fieldPath,
1806
+ modelMap,
1807
+ locale,
1808
+ userContext,
1809
+ contentSourceInstance
1810
+ }: {
1811
+ value: any;
1812
+ modelField: FieldSpecificProps;
1813
+ fieldPath: (string | number)[];
1814
+ modelMap: Record<string, Model>;
1815
+ locale?: string;
1816
+ userContext: unknown;
1817
+ contentSourceInstance: CSITypes.ContentSourceInterface;
1818
+ }): Promise<{ field: CSITypes.UpdateOperationField; newRefDocuments: CSITypes.Document[] }> {
1819
+ if (modelField.type === 'object') {
1820
+ const result = await createNestedObjectRecursively({
1821
+ object: value,
1822
+ modelFields: modelField.fields,
1823
+ fieldPath,
1824
+ modelMap,
1825
+ locale,
1826
+ userContext,
1827
+ contentSourceInstance
1828
+ });
1829
+ return {
1830
+ field: {
1831
+ type: 'object',
1832
+ fields: result.fields
1833
+ },
1834
+ newRefDocuments: result.newRefDocuments
1835
+ };
1836
+ } else if (modelField.type === 'model') {
1837
+ let { $$type, ...rest } = value;
1838
+ const modelNames = modelField.models;
1839
+ // for backward compatibility check if the object has 'type' instead of '$$type' because older projects use
1840
+ // the 'type' property in default values
1841
+ if (!$$type && 'type' in rest) {
1842
+ $$type = rest.type;
1843
+ rest = _.omit(rest, 'type');
1844
+ }
1845
+ const modelName = $$type ?? (modelNames.length === 1 ? modelNames[0] : null);
1846
+ if (!modelName) {
1847
+ throw new Error(`no $$type was specified for nested model`);
1848
+ }
1849
+ const model = modelMap[modelName];
1850
+ if (!model) {
1851
+ throw new Error(`no model with name '${modelName}' was found`);
1852
+ }
1853
+ const result = await createNestedObjectRecursively({
1854
+ object: rest,
1855
+ modelFields: model.fields ?? [],
1856
+ fieldPath,
1857
+ modelMap,
1858
+ locale,
1859
+ userContext,
1860
+ contentSourceInstance
1861
+ });
1862
+ return {
1863
+ field: {
1864
+ type: 'model',
1865
+ modelName: modelName,
1866
+ fields: result.fields
1867
+ },
1868
+ newRefDocuments: result.newRefDocuments
1869
+ };
1870
+ } else if (modelField.type === 'image') {
1871
+ let refId: string | undefined;
1872
+ if (_.isPlainObject(value)) {
1873
+ refId = value.$$ref;
1874
+ } else {
1875
+ refId = value;
1876
+ }
1877
+ if (!refId) {
1878
+ throw new Error(`reference field must specify a value`);
1879
+ }
1880
+ return {
1881
+ field: {
1882
+ type: 'reference',
1883
+ refType: 'asset',
1884
+ refId: refId
1885
+ },
1886
+ newRefDocuments: []
1887
+ };
1888
+ } else if (modelField.type === 'reference') {
1889
+ let { $$ref: refId = null, $$type: modelName = null, ...rest } = _.isPlainObject(value) ? value : { $$ref: value };
1890
+ if (refId) {
1746
1891
  return {
1747
1892
  field: {
1748
- type: 'object',
1749
- fields: result.fields
1893
+ type: 'reference',
1894
+ refType: 'document',
1895
+ refId: refId
1750
1896
  },
1751
- newRefDocuments: result.newRefDocuments
1897
+ newRefDocuments: []
1752
1898
  };
1753
- } else if (modelField.type === 'model') {
1754
- let { $$type, ...rest } = value;
1899
+ } else {
1755
1900
  const modelNames = modelField.models;
1756
- // for backward compatibility check if the object has 'type' instead of '$$type' because older projects use
1757
- // the 'type' property in default values
1758
- if (!$$type && 'type' in rest) {
1759
- $$type = rest.type;
1760
- rest = _.omit(rest, 'type');
1761
- }
1762
- const modelName = $$type ?? (modelNames.length === 1 ? modelNames[0] : null);
1763
1901
  if (!modelName) {
1764
- throw new Error(`no $$type was specified for nested model`);
1902
+ // for backward compatibility check if the object has 'type' instead of '$$type' because older projects use
1903
+ // the 'type' property in default values
1904
+ if ('type' in rest) {
1905
+ modelName = rest.type;
1906
+ rest = _.omit(rest, 'type');
1907
+ } else if (modelNames.length === 1) {
1908
+ modelName = modelNames[0];
1909
+ }
1765
1910
  }
1766
1911
  const model = modelMap[modelName];
1767
1912
  if (!model) {
1768
1913
  throw new Error(`no model with name '${modelName}' was found`);
1769
1914
  }
1770
- const result = await createNestedObjectRecursively({
1915
+ const { document, newRefDocuments } = await createDocumentRecursively({
1771
1916
  object: rest,
1772
- modelFields: model.fields ?? [],
1773
- fieldPath,
1917
+ model: model,
1774
1918
  modelMap,
1775
1919
  locale,
1776
1920
  userContext,
1777
1921
  contentSourceInstance
1778
1922
  });
1779
- return {
1780
- field: {
1781
- type: 'model',
1782
- modelName: modelName,
1783
- fields: result.fields
1784
- },
1785
- newRefDocuments: result.newRefDocuments
1786
- };
1787
- } else if (modelField.type === 'image') {
1788
- let refId: string | undefined;
1789
- if (_.isPlainObject(value)) {
1790
- refId = value.$$ref;
1791
- } else {
1792
- refId = value;
1793
- }
1794
- if (!refId) {
1795
- throw new Error(`reference field must specify a value`);
1796
- }
1797
1923
  return {
1798
1924
  field: {
1799
1925
  type: 'reference',
1800
- refType: 'asset',
1801
- refId: refId
1926
+ refType: 'document',
1927
+ refId: document.id
1802
1928
  },
1803
- newRefDocuments: []
1804
- };
1805
- } else if (modelField.type === 'reference') {
1806
- let { $$ref: refId = null, $$type: modelName = null, ...rest } = _.isPlainObject(value) ? value : { $$ref: value };
1807
- if (refId) {
1808
- return {
1809
- field: {
1810
- type: 'reference',
1811
- refType: 'document',
1812
- refId: refId
1813
- },
1814
- newRefDocuments: []
1815
- };
1816
- } else {
1817
- const modelNames = modelField.models;
1818
- if (!modelName) {
1819
- // for backward compatibility check if the object has 'type' instead of '$$type' because older projects use
1820
- // the 'type' property in default values
1821
- if ('type' in rest) {
1822
- modelName = rest.type;
1823
- rest = _.omit(rest, 'type');
1824
- } else if (modelNames.length === 1) {
1825
- modelName = modelNames[0];
1826
- }
1827
- }
1828
- const model = modelMap[modelName];
1829
- if (!model) {
1830
- throw new Error(`no model with name '${modelName}' was found`);
1831
- }
1832
- const { document, newRefDocuments } = await createDocumentRecursively({
1833
- object: rest,
1834
- model: model,
1835
- modelMap,
1836
- locale,
1837
- userContext,
1838
- contentSourceInstance
1839
- });
1840
- return {
1841
- field: {
1842
- type: 'reference',
1843
- refType: 'document',
1844
- refId: document.id
1845
- },
1846
- newRefDocuments: [document, ...newRefDocuments]
1847
- };
1848
- }
1849
- } else if (modelField.type === 'list') {
1850
- if (!Array.isArray(value)) {
1851
- throw new Error(`value for list field must be array`);
1852
- }
1853
- const itemsField = modelField.items;
1854
- if (!itemsField) {
1855
- throw new Error(`list field does not define items`);
1856
- }
1857
- const arrayResult = await mapPromise(value, async (item, index) => {
1858
- return createNestedField({
1859
- value: item,
1860
- modelField: itemsField,
1861
- fieldPath: fieldPath.concat(index)
1862
- });
1863
- });
1864
- return {
1865
- field: {
1866
- type: 'list',
1867
- items: arrayResult.map((result) => result.field)
1868
- },
1869
- newRefDocuments: arrayResult.reduce((result: ContentSourceTypes.Document[], { newRefDocuments }) => result.concat(newRefDocuments), [])
1929
+ newRefDocuments: [document, ...newRefDocuments]
1870
1930
  };
1871
1931
  }
1932
+ } else if (modelField.type === 'list') {
1933
+ if (!Array.isArray(value)) {
1934
+ throw new Error(`value for list field must be array`);
1935
+ }
1936
+ const itemsField = modelField.items;
1937
+ if (!itemsField) {
1938
+ throw new Error(`list field does not define items`);
1939
+ }
1940
+ const arrayResult = await mapPromise(value, async (item, index) => {
1941
+ return createNestedField({
1942
+ value: item,
1943
+ modelField: itemsField,
1944
+ fieldPath: fieldPath.concat(index),
1945
+ modelMap,
1946
+ locale,
1947
+ userContext,
1948
+ contentSourceInstance
1949
+ });
1950
+ });
1872
1951
  return {
1873
1952
  field: {
1874
- type: modelField.type,
1875
- value: value
1876
- } as ContentSourceTypes.DocumentFieldNonLocalized,
1877
- newRefDocuments: []
1953
+ type: 'list',
1954
+ items: arrayResult.map((result) => result.field)
1955
+ },
1956
+ newRefDocuments: arrayResult.reduce((result: CSITypes.Document[], { newRefDocuments }) => result.concat(newRefDocuments), [])
1878
1957
  };
1879
- };
1880
-
1881
- object = object ?? {};
1882
- const result: {
1883
- fields: Record<string, ContentSourceTypes.DocumentFieldNonLocalized>;
1884
- newRefDocuments: ContentSourceTypes.Document[];
1885
- } = {
1886
- fields: {},
1958
+ }
1959
+ return {
1960
+ field: {
1961
+ type: modelField.type,
1962
+ value: value
1963
+ },
1887
1964
  newRefDocuments: []
1888
1965
  };
1889
- const objectFieldNames = Object.keys(object);
1890
- for (const modelField of modelFields) {
1891
- const fieldName = modelField.name;
1892
- let value;
1893
- if (fieldName in object) {
1894
- value = object[fieldName];
1895
- _.pull(objectFieldNames, fieldName);
1896
- } else if (modelField.const) {
1897
- value = modelField.const;
1898
- } else if (!_.isNil(modelField.default)) {
1899
- value = modelField.default;
1900
- }
1901
- if (!_.isNil(value)) {
1902
- const fieldResult = await createNestedField({
1903
- value,
1904
- modelField,
1905
- fieldPath: fieldPath.concat(fieldName)
1906
- });
1907
- result.fields[fieldName] = fieldResult.field;
1908
- result.newRefDocuments = result.newRefDocuments.concat(fieldResult.newRefDocuments);
1909
- }
1910
- }
1911
- if (objectFieldNames.length > 0) {
1912
- throw new Error(`no model fields found when creating a document with fields: '${objectFieldNames.join(', ')}'`);
1913
- }
1914
-
1915
- return result;
1916
1966
  }
1917
1967
 
1918
1968
  function getModelFieldForFieldAtPath(
@@ -2040,14 +2090,15 @@ async function convertOperationField({
2040
2090
  modelMap: Record<string, Model>;
2041
2091
  locale?: string;
2042
2092
  userContext: unknown;
2043
- contentSourceInstance: ContentSourceTypes.ContentSourceInterface;
2044
- }): Promise<ContentSourceTypes.UpdateOperationField> {
2045
- let result;
2093
+ contentSourceInstance: CSITypes.ContentSourceInterface;
2094
+ }): Promise<CSITypes.UpdateOperationField> {
2095
+ // for insert operations, the modelField will be of the list, so get the modelField of the list items
2096
+ const modelFieldOrListItems: FieldSpecificProps = modelField.type === 'list' ? modelField.items! : modelField;
2046
2097
  switch (operationField.type) {
2047
- case 'object':
2048
- result = await createNestedObjectRecursively({
2098
+ case 'object': {
2099
+ const result = await createNestedObjectRecursively({
2049
2100
  object: operationField.object,
2050
- modelFields: (modelField as FieldObjectProps).fields,
2101
+ modelFields: (modelFieldOrListItems as FieldObjectProps).fields,
2051
2102
  fieldPath: fieldPath,
2052
2103
  modelMap,
2053
2104
  locale,
@@ -2058,12 +2109,13 @@ async function convertOperationField({
2058
2109
  type: operationField.type,
2059
2110
  fields: result.fields
2060
2111
  };
2061
- case 'model':
2112
+ }
2113
+ case 'model': {
2062
2114
  const model = modelMap[operationField.modelName];
2063
2115
  if (!model) {
2064
2116
  throw new Error(`error updating document, could not find document model: '${operationField.modelName}'`);
2065
2117
  }
2066
- result = await createNestedObjectRecursively({
2118
+ const result = await createNestedObjectRecursively({
2067
2119
  object: operationField.object,
2068
2120
  modelFields: model.fields!,
2069
2121
  fieldPath,
@@ -2077,8 +2129,53 @@ async function convertOperationField({
2077
2129
  modelName: operationField.modelName,
2078
2130
  fields: result.fields
2079
2131
  };
2132
+ }
2133
+ case 'list': {
2134
+ if (modelField.type !== 'list') {
2135
+ throw new Error(`'the operation field type '${operationField.type}' does not match the model field type '${modelField.type}'`);
2136
+ }
2137
+ const result = await mapPromise(operationField.items, async (item, index) => {
2138
+ const result = await createNestedField({
2139
+ value: item,
2140
+ modelField: modelField.items!,
2141
+ fieldPath,
2142
+ modelMap,
2143
+ locale,
2144
+ userContext,
2145
+ contentSourceInstance
2146
+ });
2147
+ return result.field;
2148
+ });
2149
+ return {
2150
+ type: operationField.type,
2151
+ items: result
2152
+ };
2153
+ }
2154
+ case 'string':
2155
+ if (typeof operationField.value !== 'string') {
2156
+ return {
2157
+ type: operationField.type,
2158
+ value: ''
2159
+ };
2160
+ }
2161
+ return operationField as CSITypes.UpdateOperationField;
2162
+ case 'enum':
2163
+ if (typeof operationField.value !== 'string') {
2164
+ if (modelFieldOrListItems.type !== 'enum') {
2165
+ throw new Error(`'the operation field type 'enum' does not match the model field type '${modelFieldOrListItems.type}'`);
2166
+ }
2167
+ const option = modelFieldOrListItems.options[0]!;
2168
+ const optionValue = typeof option === 'object' ? option.value : option;
2169
+ return {
2170
+ type: operationField.type,
2171
+ value: optionValue
2172
+ };
2173
+ }
2174
+ return operationField as CSITypes.UpdateOperationField;
2175
+ case 'image':
2176
+ return operationField as CSITypes.UpdateOperationField;
2080
2177
  default:
2081
- return operationField as ContentSourceTypes.UpdateOperationField;
2178
+ return operationField as CSITypes.UpdateOperationField;
2082
2179
  }
2083
2180
  }
2084
2181
 
@@ -2110,3 +2207,25 @@ function isStackbitConfigFile(filePath: string) {
2110
2207
  const isMainStackbitConfigFile = pathObject.name === 'stackbit' && ['yaml', 'yml'].includes(pathObject.ext.substring(1));
2111
2208
  return isMainStackbitConfigFile || isInDotStackbitFolder;
2112
2209
  }
2210
+
2211
+ function getCSIDocumentsAndAssetsFromContentSourceDataByIds(
2212
+ contentSourceData: ContentSourceData,
2213
+ objects: { srcObjectId: string }[]
2214
+ ): {
2215
+ documents: CSITypes.Document[];
2216
+ assets: CSITypes.Asset[];
2217
+ } {
2218
+ const documents: CSITypes.Document[] = [];
2219
+ const assets: CSITypes.Asset[] = [];
2220
+ for (const object of objects) {
2221
+ if (object.srcObjectId in contentSourceData.csiDocumentMap) {
2222
+ documents.push(contentSourceData.csiDocumentMap[object.srcObjectId]!);
2223
+ } else if (object.srcObjectId in contentSourceData.csiAssetMap) {
2224
+ assets.push(contentSourceData.csiAssetMap[object.srcObjectId]!);
2225
+ }
2226
+ }
2227
+ return {
2228
+ documents,
2229
+ assets
2230
+ };
2231
+ }