@stackbit/cms-core 0.1.3 → 0.1.4-alpha.1

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 (57) hide show
  1. package/dist/common/common-types.d.ts +1 -9
  2. package/dist/common/common-types.d.ts.map +1 -1
  3. package/dist/consts.d.ts +1 -24
  4. package/dist/consts.d.ts.map +1 -1
  5. package/dist/consts.js +5 -25
  6. package/dist/consts.js.map +1 -1
  7. package/dist/content-store-types.d.ts +42 -39
  8. package/dist/content-store-types.d.ts.map +1 -1
  9. package/dist/content-store-utils.d.ts +10 -0
  10. package/dist/content-store-utils.d.ts.map +1 -0
  11. package/dist/content-store-utils.js +139 -0
  12. package/dist/content-store-utils.js.map +1 -0
  13. package/dist/content-store.d.ts +18 -5
  14. package/dist/content-store.d.ts.map +1 -1
  15. package/dist/content-store.js +177 -962
  16. package/dist/content-store.js.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/types/search-filter.d.ts +42 -0
  22. package/dist/types/search-filter.d.ts.map +1 -0
  23. package/dist/types/search-filter.js +3 -0
  24. package/dist/types/search-filter.js.map +1 -0
  25. package/dist/utils/create-update-csi-docs.d.ts +69 -0
  26. package/dist/utils/create-update-csi-docs.d.ts.map +1 -0
  27. package/dist/utils/create-update-csi-docs.js +386 -0
  28. package/dist/utils/create-update-csi-docs.js.map +1 -0
  29. package/dist/utils/csi-to-store-docs-converter.d.ts +15 -0
  30. package/dist/utils/csi-to-store-docs-converter.d.ts.map +1 -0
  31. package/dist/utils/csi-to-store-docs-converter.js +287 -0
  32. package/dist/utils/csi-to-store-docs-converter.js.map +1 -0
  33. package/dist/utils/search-utils.d.ts +21 -0
  34. package/dist/utils/search-utils.d.ts.map +1 -0
  35. package/dist/utils/search-utils.js +323 -0
  36. package/dist/utils/search-utils.js.map +1 -0
  37. package/dist/utils/store-to-api-docs-converter.d.ts +5 -0
  38. package/dist/utils/store-to-api-docs-converter.d.ts.map +1 -0
  39. package/dist/utils/store-to-api-docs-converter.js +247 -0
  40. package/dist/utils/store-to-api-docs-converter.js.map +1 -0
  41. package/package.json +7 -5
  42. package/src/common/common-types.ts +1 -10
  43. package/src/consts.ts +1 -26
  44. package/src/content-store-types.ts +59 -45
  45. package/src/content-store-utils.ts +150 -0
  46. package/src/content-store.ts +168 -1090
  47. package/src/index.ts +3 -2
  48. package/src/types/search-filter.ts +53 -0
  49. package/src/utils/create-update-csi-docs.ts +457 -0
  50. package/src/utils/csi-to-store-docs-converter.ts +366 -0
  51. package/src/utils/search-utils.ts +437 -0
  52. package/src/utils/store-to-api-docs-converter.ts +246 -0
  53. package/dist/content-source-interface.d.ts +0 -338
  54. package/dist/content-source-interface.d.ts.map +0 -1
  55. package/dist/content-source-interface.js +0 -28
  56. package/dist/content-source-interface.js.map +0 -1
  57. package/src/content-source-interface.ts +0 -495
@@ -1,28 +1,20 @@
1
1
  import _ from 'lodash';
2
- import slugify from 'slugify';
3
2
  import path from 'path';
4
3
  import sanitizeFilename from 'sanitize-filename';
5
- import {
6
- Config,
7
- Model,
8
- Field,
9
- FieldList,
10
- FieldListItems,
11
- FieldListProps,
12
- FieldModelProps,
13
- FieldObjectProps,
14
- FieldSpecificProps,
15
- RawConfigWithPaths,
16
- loadConfigFromDir,
17
- extendConfig
18
- } from '@stackbit/sdk';
19
- import { deferWhileRunning, mapPromise, omitByNil } from '@stackbit/utils';
20
- import * as CSITypes from './content-source-interface';
21
- import { isLocalizedField, getLocalizedFieldForLocale } from './content-source-interface';
4
+
5
+ import * as CSITypes from '@stackbit/types';
6
+ import { getLocalizedFieldForLocale, UserCommandSpawner } from '@stackbit/types';
7
+ import { Config, extendConfig, Field, loadConfigFromDir, Model, ImageModel, isImageModel, Preset, RawConfigWithPaths } from '@stackbit/sdk';
8
+ import { deferWhileRunning, mapPromise, reducePromise } from '@stackbit/utils';
9
+
22
10
  import * as ContentStoreTypes from './content-store-types';
23
- import { IMAGE_MODEL } from './common/common-schema';
24
11
  import { Timer } from './utils/timer';
25
- import { UserCommandSpawner } from './common/common-types';
12
+ import { SearchFilter } from './types/search-filter';
13
+ import { searchDocuments } from './utils/search-utils';
14
+ import { mapCSIAssetsToStoreAssets, mapCSIDocumentsToStoreDocuments } from './utils/csi-to-store-docs-converter';
15
+ import { getContentSourceId, getContentSourceIdForContentSource, getModelFieldForFieldAtPath, getUserContextForSrcType } from './content-store-utils';
16
+ import { mapAssetsToLocalizedApiImages, mapDocumentsToLocalizedApiObjects, mapStoreAssetsToAPIAssets } from './utils/store-to-api-docs-converter';
17
+ import { convertOperationField, createDocumentRecursively, getCreateDocumentThunk } from './utils/create-update-csi-docs';
26
18
 
27
19
  export interface ContentSourceOptions {
28
20
  logger: ContentStoreTypes.Logger;
@@ -46,7 +38,7 @@ interface ContentSourceData {
46
38
  /* Map of extended and validated Models by model name */
47
39
  modelMap: Record<string, Model>;
48
40
  /* Map of original Models (as provided by content source) by model name */
49
- csiModelMap: Record<string, Model>;
41
+ csiModelMap: Record<string, CSITypes.Model>;
50
42
  /* Array of original content source Documents */
51
43
  csiDocuments: CSITypes.Document[];
52
44
  /* Map of original content source Documents by document ID */
@@ -155,7 +147,6 @@ export class ContentStore {
155
147
  } else {
156
148
  this.rawStackbitConfig = null;
157
149
  }
158
-
159
150
  }
160
151
  await this.loadContentSources({ init });
161
152
  }
@@ -218,7 +209,7 @@ export class ContentStore {
218
209
  for (const contentSourceInstance of this.contentSources) {
219
210
  const contentSourceId = getContentSourceIdForContentSource(contentSourceInstance);
220
211
  this.logger.debug(`call onFilesChange for contentSource: ${contentSourceId}`);
221
- const { schemaChanged, contentChangeEvent } = contentSourceInstance.onFilesChange?.({ updatedFiles: updatedFiles }) ?? {};
212
+ const { schemaChanged, contentChangeEvent } = (await contentSourceInstance.onFilesChange?.({ updatedFiles: updatedFiles })) ?? {};
222
213
  this.logger.debug(`schemaChanged: ${schemaChanged}, has contentChangeEvent: ${!!contentChangeEvent}`);
223
214
  // if schema is changed, there is no need to return contentChanges
224
215
  // because schema changes reloads everything and implies content changes
@@ -250,7 +241,7 @@ export class ContentStore {
250
241
 
251
242
  const promises = contentSources.map((contentSourceInstance) => {
252
243
  return this.loadContentSourceData({ contentSourceInstance, init });
253
- })
244
+ });
254
245
 
255
246
  const contentSourceDataArr = await Promise.all(promises);
256
247
  const contentSourceDataById: Record<string, ContentSourceData> = _.keyBy(contentSourceDataArr, 'id');
@@ -292,7 +283,8 @@ export class ContentStore {
292
283
  const defaultLocaleCode = locales?.find((locale) => locale.default)?.code;
293
284
 
294
285
  // for older versions of stackbit, it uses models to extend content source models
295
- let models: Model[] = [];
286
+ let modelsNoImage: Exclude<Model, ImageModel>[] = [];
287
+ let imageModel: ImageModel | undefined;
296
288
  if (this.rawStackbitConfig) {
297
289
  const result = await extendConfig({
298
290
  config: this.rawStackbitConfig,
@@ -302,28 +294,57 @@ export class ContentStore {
302
294
  this.userLogger.warn(error.message);
303
295
  }
304
296
  const config = await this.handleConfigAssets(result.config);
305
- models = config?.models ?? [];
297
+ const modelsWithImageModel = config?.models ?? [];
298
+ const imageModelIndex = modelsWithImageModel.findIndex((model) => isImageModel(model));
299
+ if (imageModelIndex > -1) {
300
+ imageModel = modelsWithImageModel[imageModelIndex] as ImageModel;
301
+ modelsWithImageModel.splice(imageModelIndex, 1);
302
+ }
303
+ modelsNoImage = modelsWithImageModel as Exclude<Model, ImageModel>[];
306
304
 
307
305
  // TODO: load presets externally from config, and create additional map
308
306
  // that maps presetIds by model name instead of storing that map inside every model
309
- // TODO: adjust presets to have srcType and srcProjectId
310
- this.presets = config?.presets;
307
+
308
+ // Augment presets with srcType and srcProjectId if they don't exist
309
+ this.presets = _.reduce(
310
+ Object.keys(config?.presets ?? {}),
311
+ (accum: Record<string, Preset>, presetId) => {
312
+ const preset = config?.presets?.[presetId];
313
+ _.set(accum, [presetId], {
314
+ ...preset,
315
+ srcType: preset?.srcType ?? contentSourceInstance.getContentSourceType(),
316
+ srcProjectId: preset?.srcProjectId ?? contentSourceInstance.getProjectId()
317
+ });
318
+ return accum;
319
+ },
320
+ {}
321
+ );
311
322
  }
312
323
 
313
324
  if (this.rawStackbitConfig?.mapModels) {
314
- models = this.rawStackbitConfig.mapModels({
315
- models: models,
316
- contentSourceType: contentSourceInstance.getContentSourceType(),
317
- contentSourceProjectId: contentSourceInstance.getProjectId()
325
+ const srcType = contentSourceInstance.getContentSourceType();
326
+ const srcProjectId = contentSourceInstance.getProjectId();
327
+ const modelsWithSource = modelsNoImage.map((model) => ({
328
+ srcType,
329
+ srcProjectId,
330
+ ...model
331
+ }));
332
+ const mappedModels = this.rawStackbitConfig.mapModels({
333
+ models: modelsWithSource
334
+ });
335
+ modelsNoImage = mappedModels.map((model) => {
336
+ const { srcType, srcProjectId, ...rest } = model;
337
+ return rest;
318
338
  });
319
339
  }
320
340
 
341
+ const models: Model[] = imageModel ? [...modelsNoImage, imageModel] : modelsNoImage;
321
342
  const modelMap = _.keyBy(models, 'name');
322
- const csiDocuments = await contentSourceInstance.getDocuments({ modelMap });
343
+ const csiModelMap = _.keyBy(csiModels, 'name');
344
+ const csiDocuments = await contentSourceInstance.getDocuments({ modelMap: csiModelMap });
323
345
  const csiAssets = await contentSourceInstance.getAssets();
324
346
  const csiDocumentMap = _.keyBy(csiDocuments, 'id');
325
347
  const csiAssetMap = _.keyBy(csiAssets, 'id');
326
- const csiModelMap = _.keyBy(csiModels, 'name');
327
348
 
328
349
  const contentStoreDocuments = mapCSIDocumentsToStoreDocuments({
329
350
  csiDocuments,
@@ -552,11 +573,53 @@ export class ContentStore {
552
573
  return contentSourceData.instance.getProjectEnvironment();
553
574
  }
554
575
 
555
- hasAccess({ srcType, srcProjectId, user }: { srcType: string; srcProjectId: string; user?: ContentStoreTypes.User }): Promise<boolean> {
556
- const contentSourceId = getContentSourceId(srcType, srcProjectId);
557
- const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
558
- const userContext = getUserContextForSrcType(srcType, user);
559
- return contentSourceData.instance.hasAccess({ userContext });
576
+ async hasAccess({
577
+ srcType,
578
+ srcProjectId,
579
+ user
580
+ }: {
581
+ srcType?: string;
582
+ srcProjectId?: string;
583
+ user?: ContentStoreTypes.User;
584
+ }): Promise<ContentStoreTypes.HasAccessResult> {
585
+ let contentSourceDataArr: ContentSourceData[];
586
+ if (srcType && srcProjectId) {
587
+ const contentSourceId = getContentSourceId(srcType, srcProjectId);
588
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
589
+ contentSourceDataArr = [contentSourceData];
590
+ } else {
591
+ contentSourceDataArr = Object.values(this.contentSourceDataById);
592
+ }
593
+ return reducePromise(
594
+ contentSourceDataArr,
595
+ async (accum: ContentStoreTypes.HasAccessResult, contentSourceData) => {
596
+ const srcType = contentSourceData.type;
597
+ const srcProjectId = contentSourceData.projectId;
598
+ const userContext = getUserContextForSrcType(srcType, user);
599
+ let result = await contentSourceData.instance.hasAccess({ userContext });
600
+ // backwards compatibility with older CSI version
601
+ if (typeof result === 'boolean') {
602
+ result = {
603
+ hasConnection: result,
604
+ hasPermissions: result
605
+ };
606
+ }
607
+ return {
608
+ hasConnection: accum.hasConnection && result.hasConnection,
609
+ hasPermissions: accum.hasPermissions && result.hasPermissions,
610
+ contentSources: accum.contentSources.concat({
611
+ srcType,
612
+ srcProjectId,
613
+ ...result
614
+ })
615
+ };
616
+ },
617
+ {
618
+ hasConnection: true,
619
+ hasPermissions: true,
620
+ contentSources: []
621
+ }
622
+ );
560
623
  }
561
624
 
562
625
  hasChanges({
@@ -930,21 +993,22 @@ export class ContentStore {
930
993
  const contentSourceId = getContentSourceId(srcType, srcProjectId);
931
994
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
932
995
  const modelMap = contentSourceData.modelMap;
933
- const model = modelMap[modelName];
934
- if (!model) {
935
- throw new Error(`no model with name '${modelName}' was found`);
936
- }
937
-
938
- locale = locale ?? contentSourceData.defaultLocaleCode;
996
+ const csiModelMap = contentSourceData.csiModelMap;
939
997
  const userContext = getUserContextForSrcType(srcType, user);
998
+ const resolvedLocale = locale ?? contentSourceData.defaultLocaleCode;
999
+
940
1000
  const result = await createDocumentRecursively({
941
1001
  object,
942
- model,
1002
+ modelName,
943
1003
  modelMap,
944
- locale,
945
- userContext,
946
- contentSourceInstance: contentSourceData.instance
1004
+ createDocument: getCreateDocumentThunk({
1005
+ locale: resolvedLocale,
1006
+ csiModelMap,
1007
+ userContext,
1008
+ contentSourceInstance: contentSourceData.instance
1009
+ })
947
1010
  });
1011
+
948
1012
  this.logger.debug('created document', { srcType, srcProjectId, srcDocumentId: result.document.id, modelName });
949
1013
 
950
1014
  // do not update cache in contentSourceData.documents and documentMap,
@@ -998,11 +1062,14 @@ export class ContentStore {
998
1062
  const field = await convertOperationField({
999
1063
  operationField: updateOperation.field,
1000
1064
  fieldPath: updateOperation.fieldPath,
1001
- locale: updateOperation.locale,
1002
1065
  modelField,
1003
1066
  modelMap,
1004
- userContext,
1005
- contentSourceInstance: contentSourceData.instance
1067
+ createDocument: getCreateDocumentThunk({
1068
+ locale: updateOperation.locale,
1069
+ csiModelMap,
1070
+ userContext,
1071
+ contentSourceInstance: contentSourceData.instance
1072
+ })
1006
1073
  });
1007
1074
  return {
1008
1075
  ...updateOperation,
@@ -1015,11 +1082,14 @@ export class ContentStore {
1015
1082
  const item = await convertOperationField({
1016
1083
  operationField: updateOperation.item,
1017
1084
  fieldPath: updateOperation.fieldPath,
1018
- locale: updateOperation.locale,
1019
1085
  modelField,
1020
1086
  modelMap,
1021
- userContext,
1022
- contentSourceInstance: contentSourceData.instance
1087
+ createDocument: getCreateDocumentThunk({
1088
+ locale: updateOperation.locale,
1089
+ csiModelMap,
1090
+ userContext,
1091
+ contentSourceInstance: contentSourceData.instance
1092
+ })
1023
1093
  });
1024
1094
  return {
1025
1095
  ...updateOperation,
@@ -1071,8 +1141,10 @@ export class ContentStore {
1071
1141
  throw new Error(`no document with id '${srcDocumentId}' was found in ${contentSourceData.id}`);
1072
1142
  }
1073
1143
  const modelMap = contentSourceData.modelMap;
1144
+ const csiModelMap = contentSourceData.csiModelMap;
1074
1145
  const model = modelMap[document.srcModelName];
1075
- if (!model) {
1146
+ const csiModel = csiModelMap[document.srcModelName];
1147
+ if (!model || !csiModel) {
1076
1148
  throw new Error(`no model with name '${document.srcModelName}' was found`);
1077
1149
  }
1078
1150
 
@@ -1081,7 +1153,7 @@ export class ContentStore {
1081
1153
  // TODO: take the data from the provided 'object' and merge them with
1082
1154
  // DocumentFields of the existing Document:
1083
1155
  // Option 1: Map the DocumentFields of the existing Document into flat
1084
- // object with '$$ref' and '$type' properties for references and
1156
+ // object with '$$ref' and '$$type' properties for references and
1085
1157
  // nested objects (needs to be implemented), and then merge it with
1086
1158
  // the provided object recursively, and then pass that object to
1087
1159
  // createNestedObjectRecursively()
@@ -1098,12 +1170,14 @@ export class ContentStore {
1098
1170
  modelMap: contentSourceData.modelMap
1099
1171
  });
1100
1172
 
1173
+ // When passing model and modelMap to contentSourceInstance, we have to pass
1174
+ // the original models (i.e., csiModel and csiModelMap) that we've received
1175
+ // from that contentSourceInstance. We can't pass internal models as they
1176
+ // might
1101
1177
  const documentResult = await contentSourceData.instance.createDocument({
1102
1178
  updateOperationFields,
1103
- // TODO: pass csiModel
1104
- model,
1105
- // TODO: pass csiModelMap
1106
- modelMap,
1179
+ model: csiModel,
1180
+ modelMap: csiModelMap,
1107
1181
  locale: contentSourceData.defaultLocaleCode,
1108
1182
  userContext
1109
1183
  });
@@ -1239,6 +1313,39 @@ export class ContentStore {
1239
1313
  */
1240
1314
  }
1241
1315
 
1316
+ async searchDocuments(data: {
1317
+ query?: string;
1318
+ filter?: SearchFilter;
1319
+ models: Array<{
1320
+ srcProjectId: string;
1321
+ srcType: string;
1322
+ modelName: string;
1323
+ }>;
1324
+ locale?: string;
1325
+ }): Promise<{
1326
+ total: number;
1327
+ items: ContentStoreTypes.Document[];
1328
+ }> {
1329
+ this.logger.debug('searchDocuments');
1330
+
1331
+ const objectsBySourceId = _.groupBy(data.models, (object) => getContentSourceId(object.srcType, object.srcProjectId));
1332
+ const documents: ContentStoreTypes.Document[] = [];
1333
+ const schema: Record<string, Record<string, Record<string, Model>>> = {};
1334
+
1335
+ Object.keys(objectsBySourceId).forEach((contentSourceId) => {
1336
+ const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1337
+
1338
+ documents.push(...contentSourceData.documents);
1339
+ _.set(schema, [contentSourceData.type, contentSourceData.projectId], contentSourceData.modelMap);
1340
+ });
1341
+
1342
+ return searchDocuments({
1343
+ ...data,
1344
+ documents,
1345
+ schema
1346
+ });
1347
+ }
1348
+
1242
1349
  async publishDocuments({ objects, user }: { objects: { srcType: string; srcProjectId: string; srcObjectId: string }[]; user?: ContentStoreTypes.User }) {
1243
1350
  this.logger.debug('publishDocuments');
1244
1351
 
@@ -1260,327 +1367,6 @@ export class ContentStore {
1260
1367
  }
1261
1368
  }
1262
1369
 
1263
- export function getContentSourceId(contentSourceType: string, srcProjectId: string) {
1264
- return contentSourceType + ':' + srcProjectId;
1265
- }
1266
-
1267
- function getUserContextForSrcType(srcType: string, user?: ContentStoreTypes.User): unknown {
1268
- return user?.connections?.find((connection) => connection.type === srcType);
1269
- }
1270
-
1271
- function mapCSIAssetsToStoreAssets({
1272
- csiAssets,
1273
- contentSourceInstance,
1274
- defaultLocaleCode
1275
- }: {
1276
- csiAssets: CSITypes.Asset[];
1277
- contentSourceInstance: CSITypes.ContentSourceInterface;
1278
- defaultLocaleCode?: string;
1279
- }): ContentStoreTypes.Asset[] {
1280
- const extra = {
1281
- srcType: contentSourceInstance.getContentSourceType(),
1282
- srcProjectId: contentSourceInstance.getProjectId(),
1283
- srcProjectUrl: contentSourceInstance.getProjectManageUrl(),
1284
- srcEnvironment: contentSourceInstance.getProjectEnvironment()
1285
- };
1286
- return csiAssets.map((csiAsset) => sourceAssetToStoreAsset({ csiAsset, defaultLocaleCode, extra }));
1287
- }
1288
-
1289
- function sourceAssetToStoreAsset({
1290
- csiAsset,
1291
- defaultLocaleCode,
1292
- extra
1293
- }: {
1294
- csiAsset: CSITypes.Asset;
1295
- defaultLocaleCode?: string;
1296
- extra: { srcType: string; srcProjectId: string; srcProjectUrl: string; srcEnvironment: string };
1297
- }): ContentStoreTypes.Asset {
1298
- return {
1299
- type: 'asset',
1300
- ...extra,
1301
- srcObjectId: csiAsset.id,
1302
- srcObjectUrl: csiAsset.manageUrl,
1303
- srcObjectLabel: getObjectLabel(csiAsset.fields, IMAGE_MODEL, defaultLocaleCode),
1304
- srcModelName: IMAGE_MODEL.name,
1305
- srcModelLabel: IMAGE_MODEL.label!,
1306
- isChanged: csiAsset.status === 'added' || csiAsset.status === 'modified',
1307
- status: csiAsset.status,
1308
- createdAt: csiAsset.createdAt,
1309
- createdBy: csiAsset.createdBy,
1310
- updatedAt: csiAsset.updatedAt,
1311
- updatedBy: csiAsset.updatedBy,
1312
- fields: {
1313
- title: {
1314
- label: 'Title',
1315
- ...csiAsset.fields.title
1316
- },
1317
- file: {
1318
- label: 'File',
1319
- ...csiAsset.fields.file
1320
- }
1321
- }
1322
- };
1323
- }
1324
-
1325
- function mapCSIDocumentsToStoreDocuments({
1326
- csiDocuments,
1327
- contentSourceInstance,
1328
- modelMap,
1329
- defaultLocaleCode
1330
- }: {
1331
- csiDocuments: CSITypes.Document[];
1332
- contentSourceInstance: CSITypes.ContentSourceInterface;
1333
- modelMap: Record<string, Model>;
1334
- defaultLocaleCode?: string;
1335
- }): ContentStoreTypes.Document[] {
1336
- const extra = {
1337
- srcType: contentSourceInstance.getContentSourceType(),
1338
- srcProjectId: contentSourceInstance.getProjectId(),
1339
- srcProjectUrl: contentSourceInstance.getProjectManageUrl(),
1340
- srcEnvironment: contentSourceInstance.getProjectEnvironment()
1341
- };
1342
- return csiDocuments.map((csiDocument) =>
1343
- mapCSIDocumentToStoreDocument({ csiDocument, model: modelMap[csiDocument.modelName]!, modelMap, defaultLocaleCode, extra })
1344
- );
1345
- }
1346
-
1347
- function mapCSIDocumentToStoreDocument({
1348
- csiDocument,
1349
- model,
1350
- modelMap,
1351
- defaultLocaleCode,
1352
- extra
1353
- }: {
1354
- csiDocument: CSITypes.Document;
1355
- model: Model;
1356
- modelMap: Record<string, Model>;
1357
- defaultLocaleCode?: string;
1358
- extra: { srcType: string; srcProjectId: string; srcProjectUrl: string; srcEnvironment: string };
1359
- }): ContentStoreTypes.Document {
1360
- return {
1361
- type: 'document',
1362
- ...extra,
1363
- srcObjectId: csiDocument.id,
1364
- srcObjectUrl: csiDocument.manageUrl,
1365
- srcObjectLabel: getObjectLabel(csiDocument.fields, model, defaultLocaleCode),
1366
- srcModelLabel: model.label ?? _.startCase(csiDocument.modelName),
1367
- srcModelName: csiDocument.modelName,
1368
- isChanged: csiDocument.status === 'added' || csiDocument.status === 'modified',
1369
- status: csiDocument.status,
1370
- createdAt: csiDocument.createdAt,
1371
- createdBy: csiDocument.createdBy,
1372
- updatedAt: csiDocument.updatedAt,
1373
- updatedBy: csiDocument.updatedBy,
1374
- fields: mapCSIFieldsToStoreFields({
1375
- csiDocumentFields: csiDocument.fields,
1376
- modelFields: model.fields ?? [],
1377
- context: {
1378
- modelMap,
1379
- defaultLocaleCode
1380
- }
1381
- })
1382
- };
1383
- }
1384
-
1385
- type MapContext = {
1386
- modelMap: Record<string, Model>;
1387
- defaultLocaleCode?: string;
1388
- };
1389
-
1390
- function mapCSIFieldsToStoreFields({
1391
- csiDocumentFields,
1392
- modelFields,
1393
- context
1394
- }: {
1395
- csiDocumentFields: Record<string, CSITypes.DocumentField>;
1396
- modelFields: Field[];
1397
- context: MapContext;
1398
- }): Record<string, ContentStoreTypes.DocumentField> {
1399
- return modelFields.reduce((result: Record<string, ContentStoreTypes.DocumentField>, modelField) => {
1400
- const csiDocumentField = csiDocumentFields[modelField.name];
1401
- const docField = mapCSIFieldToStoreField({
1402
- csiDocumentField,
1403
- modelField,
1404
- context
1405
- });
1406
- docField.label = modelField.label;
1407
- result[modelField.name] = docField;
1408
- return result;
1409
- }, {});
1410
- }
1411
-
1412
- function mapCSIFieldToStoreField({
1413
- csiDocumentField,
1414
- modelField,
1415
- context
1416
- }: {
1417
- csiDocumentField: CSITypes.DocumentField | undefined;
1418
- modelField: FieldSpecificProps;
1419
- context: MapContext;
1420
- }): ContentStoreTypes.DocumentField {
1421
- if (!csiDocumentField) {
1422
- const isUnset = ['object', 'model', 'reference', 'richText', 'markdown', 'image', 'file', 'json'].includes(modelField.type);
1423
- return {
1424
- type: modelField.type,
1425
- ...(isUnset ? { isUnset } : null),
1426
- ...(modelField.type === 'list' ? { items: [] } : null)
1427
- } as ContentStoreTypes.DocumentField;
1428
- }
1429
- // TODO: check if need to add "options" to "enum" and subtype/min/max to "number"
1430
- switch (modelField.type) {
1431
- case 'object':
1432
- return mapObjectField(csiDocumentField as CSITypes.DocumentObjectField, modelField, context);
1433
- case 'model':
1434
- return mapModelField(csiDocumentField as CSITypes.DocumentModelField, modelField, context);
1435
- case 'list':
1436
- return mapListField(csiDocumentField as CSITypes.DocumentListField, modelField, context);
1437
- case 'richText':
1438
- return mapRichTextField(csiDocumentField as CSITypes.DocumentRichTextField);
1439
- case 'markdown':
1440
- return mapMarkdownField(csiDocumentField as CSITypes.DocumentValueField);
1441
- default:
1442
- return csiDocumentField as ContentStoreTypes.DocumentField;
1443
- }
1444
- }
1445
-
1446
- function mapObjectField(
1447
- csiDocumentField: CSITypes.DocumentObjectField,
1448
- modelField: FieldObjectProps,
1449
- context: MapContext
1450
- ): ContentStoreTypes.DocumentObjectField {
1451
- if (!isLocalizedField(csiDocumentField)) {
1452
- return {
1453
- type: csiDocumentField.type,
1454
- srcObjectLabel: getObjectLabel(csiDocumentField.fields ?? {}, modelField ?? [], context.defaultLocaleCode),
1455
- fields: mapCSIFieldsToStoreFields({
1456
- csiDocumentFields: csiDocumentField.fields ?? {},
1457
- modelFields: modelField.fields ?? [],
1458
- context
1459
- })
1460
- };
1461
- }
1462
- return {
1463
- type: csiDocumentField.type,
1464
- localized: true,
1465
- locales: _.mapValues(csiDocumentField.locales, (locale) => {
1466
- return {
1467
- locale: locale.locale,
1468
- srcObjectLabel: getObjectLabel(locale.fields ?? {}, modelField, locale.locale),
1469
- fields: mapCSIFieldsToStoreFields({
1470
- csiDocumentFields: locale.fields ?? {},
1471
- modelFields: modelField.fields ?? [],
1472
- context
1473
- })
1474
- };
1475
- })
1476
- };
1477
- }
1478
-
1479
- function mapModelField(csiDocumentField: CSITypes.DocumentModelField, modelField: FieldModelProps, context: MapContext): ContentStoreTypes.DocumentModelField {
1480
- if (!isLocalizedField(csiDocumentField)) {
1481
- const model = context.modelMap[csiDocumentField.modelName]!;
1482
- return {
1483
- type: csiDocumentField.type,
1484
- srcObjectLabel: getObjectLabel(csiDocumentField.fields ?? {}, model, context.defaultLocaleCode),
1485
- srcModelName: csiDocumentField.modelName,
1486
- srcModelLabel: model.label ?? _.startCase(model.name),
1487
- fields: mapCSIFieldsToStoreFields({
1488
- csiDocumentFields: csiDocumentField.fields ?? {},
1489
- modelFields: model.fields ?? [],
1490
- context
1491
- })
1492
- };
1493
- }
1494
- return {
1495
- type: csiDocumentField.type,
1496
- localized: true,
1497
- locales: _.mapValues(csiDocumentField.locales, (locale) => {
1498
- const model = context.modelMap[locale.modelName]!;
1499
- return {
1500
- locale: locale.locale,
1501
- srcObjectLabel: getObjectLabel(locale.fields ?? {}, model, locale.locale),
1502
- srcModelName: locale.modelName,
1503
- srcModelLabel: model.label ?? _.startCase(model.name),
1504
- fields: mapCSIFieldsToStoreFields({
1505
- csiDocumentFields: locale.fields ?? {},
1506
- modelFields: model.fields ?? [],
1507
- context
1508
- })
1509
- };
1510
- })
1511
- };
1512
- }
1513
-
1514
- function mapListField(csiDocumentField: CSITypes.DocumentListField, modelField: FieldListProps, context: MapContext): ContentStoreTypes.DocumentListField {
1515
- if (!isLocalizedField(csiDocumentField)) {
1516
- return {
1517
- type: csiDocumentField.type,
1518
- items: csiDocumentField.items.map((item) =>
1519
- mapCSIFieldToStoreField({
1520
- csiDocumentField: item,
1521
- modelField: modelField.items ?? { type: 'string' },
1522
- context
1523
- })
1524
- )
1525
- };
1526
- }
1527
- return {
1528
- type: csiDocumentField.type,
1529
- localized: true,
1530
- locales: _.mapValues(csiDocumentField.locales, (locale) => {
1531
- return {
1532
- locale: locale.locale,
1533
- items: (locale.items ?? []).map((item) =>
1534
- mapCSIFieldToStoreField({
1535
- csiDocumentField: item,
1536
- modelField: modelField.items ?? { type: 'string' },
1537
- context
1538
- })
1539
- )
1540
- };
1541
- })
1542
- };
1543
- }
1544
-
1545
- function mapRichTextField(csiDocumentField: CSITypes.DocumentRichTextField): ContentStoreTypes.DocumentRichTextField {
1546
- if (!isLocalizedField(csiDocumentField)) {
1547
- return {
1548
- ...csiDocumentField,
1549
- multiElement: true
1550
- };
1551
- }
1552
- return {
1553
- type: csiDocumentField.type,
1554
- localized: true,
1555
- locales: _.mapValues(csiDocumentField.locales, (locale) => {
1556
- return {
1557
- ...locale,
1558
- multiElement: true
1559
- };
1560
- })
1561
- };
1562
- }
1563
-
1564
- function mapMarkdownField(csiDocumentField: CSITypes.DocumentValueField): ContentStoreTypes.DocumentMarkdownField {
1565
- if (!isLocalizedField(csiDocumentField)) {
1566
- return {
1567
- type: 'markdown',
1568
- value: csiDocumentField.value,
1569
- multiElement: true
1570
- };
1571
- }
1572
- return {
1573
- type: 'markdown',
1574
- localized: true,
1575
- locales: _.mapValues(csiDocumentField.locales, (locale) => {
1576
- return {
1577
- ...locale,
1578
- multiElement: true
1579
- };
1580
- })
1581
- };
1582
- }
1583
-
1584
1370
  function mapStoreFieldsToOperationFields({
1585
1371
  documentFields,
1586
1372
  modelFields,
@@ -1594,714 +1380,6 @@ function mapStoreFieldsToOperationFields({
1594
1380
  throw new Error(`duplicateDocument not implemented yet`);
1595
1381
  }
1596
1382
 
1597
- function getContentSourceIdForContentSource(contentSource: CSITypes.ContentSourceInterface): string {
1598
- return getContentSourceId(contentSource.getContentSourceType(), contentSource.getProjectId());
1599
- }
1600
-
1601
- function extractTokensFromString(input: string): string[] {
1602
- return input.match(/(?<={)[^}]+(?=})/g) || [];
1603
- }
1604
-
1605
- function sanitizeSlug(slug: string) {
1606
- return slug
1607
- .split('/')
1608
- .map((part) => slugify(part, { lower: true }))
1609
- .join('/');
1610
- }
1611
-
1612
- function getObjectLabel(
1613
- documentFields: Record<string, CSITypes.DocumentField | CSITypes.AssetFileField>,
1614
- modelOrObjectField: Model | FieldObjectProps,
1615
- locale?: string
1616
- ): string {
1617
- const labelField = modelOrObjectField.labelField;
1618
- let label = null;
1619
- if (labelField) {
1620
- const field = _.get(documentFields, labelField, null);
1621
- if (field && ['string', 'url', 'slug', 'text', 'markdown', 'number', 'enum', 'date', 'datetime', 'color', 'image', 'file'].includes(field.type)) {
1622
- if (isLocalizedField(field) && locale) {
1623
- label = _.get(field, ['locales', locale, 'value'], null);
1624
- } else if (!isLocalizedField(field)) {
1625
- label = _.get(field, 'value', null);
1626
- }
1627
- }
1628
- }
1629
- if (!label) {
1630
- label = _.get(modelOrObjectField, 'label');
1631
- }
1632
- if (!label && _.has(modelOrObjectField, 'name')) {
1633
- label = _.startCase(_.get(modelOrObjectField, 'name'));
1634
- }
1635
- return label;
1636
- }
1637
-
1638
- function mapDocumentsToLocalizedApiObjects(documents: ContentStoreTypes.Document[], locale?: string): ContentStoreTypes.APIDocumentObject[] {
1639
- return documents.map((document) => documentToLocalizedApiObject(document, locale));
1640
- }
1641
-
1642
- function documentToLocalizedApiObject(document: ContentStoreTypes.Document, locale?: string): ContentStoreTypes.APIDocumentObject {
1643
- const { type, fields, ...rest } = document;
1644
- return {
1645
- type: 'object',
1646
- ...rest,
1647
- fields: toLocalizedAPIFields(fields, locale)
1648
- };
1649
- }
1650
-
1651
- function toLocalizedAPIFields(docFields: Record<string, ContentStoreTypes.DocumentField>, locale?: string): Record<string, ContentStoreTypes.DocumentFieldAPI> {
1652
- return _.mapValues(docFields, (docField) => toLocalizedAPIField(docField, locale));
1653
- }
1654
-
1655
- function toLocalizedAPIField(docField: ContentStoreTypes.DocumentField, locale?: string, isListItem = false): ContentStoreTypes.DocumentFieldAPI {
1656
- const hasUnsetFlag = ['object', 'model', 'reference', 'richText', 'markdown', 'image', 'file', 'json'].includes(docField.type);
1657
- let docFieldLocalized: ContentStoreTypes.DocumentFieldNonLocalized;
1658
- let unset = false;
1659
- if (docField.localized) {
1660
- const { locales, localized, ...base } = docField;
1661
- const localeProps = locale ? locales[locale] : undefined;
1662
- docFieldLocalized = {
1663
- ...base,
1664
- ...localeProps,
1665
- ...(hasUnsetFlag && !localeProps ? { isUnset: true } : null)
1666
- } as ContentStoreTypes.DocumentFieldNonLocalized;
1667
- } else {
1668
- docFieldLocalized = docField;
1669
- }
1670
-
1671
- locale = locale ?? docFieldLocalized.locale;
1672
- const commonProps = isListItem
1673
- ? null
1674
- : {
1675
- localized: !!docField.localized,
1676
- ...(locale ? { locale } : null)
1677
- };
1678
-
1679
- if (docFieldLocalized.type === 'object' || docFieldLocalized.type === 'model') {
1680
- return {
1681
- ...docFieldLocalized,
1682
- type: 'object',
1683
- ...commonProps,
1684
- ...(docFieldLocalized.isUnset
1685
- ? null
1686
- : {
1687
- fields: toLocalizedAPIFields(docFieldLocalized.fields, locale)
1688
- })
1689
- } as ContentStoreTypes.DocumentObjectFieldAPI | ContentStoreTypes.DocumentModelFieldAPI;
1690
- } else if (docFieldLocalized.type === 'reference') {
1691
- const { type, refType, ...rest } = docFieldLocalized;
1692
- // if reference field isUnset === true, it behaves like a regular object
1693
- if (rest.isUnset) {
1694
- return {
1695
- type: 'object',
1696
- ...rest,
1697
- ...commonProps
1698
- };
1699
- }
1700
- return {
1701
- type: 'unresolved_reference',
1702
- refType: refType === 'asset' ? 'image' : 'object',
1703
- ...rest,
1704
- ...commonProps
1705
- };
1706
- } else if (docFieldLocalized.type === 'list') {
1707
- // items can be undefined if the requested locale doesn't exist on a localized field
1708
- const { items, ...rest } = docFieldLocalized;
1709
- return {
1710
- ...rest,
1711
- ...commonProps,
1712
- items: (items ?? []).map((field) => toLocalizedAPIField(field, locale, true))
1713
- };
1714
- } else {
1715
- return {
1716
- ...docFieldLocalized,
1717
- ...commonProps
1718
- };
1719
- }
1720
- }
1721
-
1722
- function mapAssetsToLocalizedApiImages(assets: ContentStoreTypes.Asset[], locale?: string): ContentStoreTypes.APIImageObject[] {
1723
- return assets.map((asset) => assetToLocalizedApiImage(asset, locale));
1724
- }
1725
-
1726
- function assetToLocalizedApiImage(asset: ContentStoreTypes.Asset, locale?: string): ContentStoreTypes.APIImageObject {
1727
- const { type, fields, ...rest } = asset;
1728
- return {
1729
- type: 'image',
1730
- ...rest,
1731
- fields: localizeAssetFields(fields, locale)
1732
- };
1733
- }
1734
-
1735
- function localizeAssetFields(assetFields: ContentStoreTypes.AssetFields, locale?: string): ContentStoreTypes.AssetFieldsAPI {
1736
- const fields: ContentStoreTypes.AssetFieldsAPI = {
1737
- title: {
1738
- type: 'string' as const,
1739
- value: null as any
1740
- },
1741
- url: {
1742
- type: 'string' as const,
1743
- value: null as any
1744
- }
1745
- };
1746
- const titleFieldNonLocalized = getDocumentFieldForLocale(assetFields.title, locale);
1747
- fields.title.value = titleFieldNonLocalized?.value;
1748
- fields.title.locale = locale ?? titleFieldNonLocalized?.locale;
1749
- const assetFileField = assetFields.file;
1750
- if (assetFileField.localized) {
1751
- if (locale) {
1752
- fields.url.value = assetFileField.locales[locale]?.url ?? null;
1753
- fields.url.locale = locale;
1754
- }
1755
- } else {
1756
- fields.url.value = assetFileField.url;
1757
- fields.url.locale = assetFileField.locale;
1758
- }
1759
- return fields;
1760
- }
1761
-
1762
- function mapStoreAssetsToAPIAssets(assets: ContentStoreTypes.Asset[], locale?: string): ContentStoreTypes.APIAsset[] {
1763
- return assets.map((asset) => storeAssetToAPIAsset(asset, locale));
1764
- }
1765
-
1766
- function storeAssetToAPIAsset(asset: ContentStoreTypes.Asset, locale?: string): ContentStoreTypes.APIAsset {
1767
- const assetTitleField = asset.fields.title;
1768
- const localizedTitleField = assetTitleField.localized ? assetTitleField.locales[locale!]! : assetTitleField;
1769
- const assetFileField = asset.fields.file;
1770
- const localizedFileField = assetFileField.localized ? assetFileField.locales[locale!]! : assetFileField;
1771
- return {
1772
- objectId: asset.srcObjectId,
1773
- createdAt: asset.createdAt,
1774
- url: localizedFileField.url,
1775
- ...omitByNil({
1776
- title: localizedTitleField.value,
1777
- fileName: localizedFileField.fileName,
1778
- contentType: localizedFileField.contentType,
1779
- size: localizedFileField.size,
1780
- width: localizedFileField.dimensions?.width,
1781
- height: localizedFileField.dimensions?.height
1782
- })
1783
- };
1784
- }
1785
-
1786
- /**
1787
- * Iterates recursively objects with $$type and $$ref, creating nested objects
1788
- * as needed and returns standard ContentSourceInterface Documents
1789
- */
1790
- async function createDocumentRecursively({
1791
- object,
1792
- model,
1793
- modelMap,
1794
- locale,
1795
- userContext,
1796
- contentSourceInstance
1797
- }: {
1798
- object?: Record<string, any>;
1799
- model: Model;
1800
- modelMap: Record<string, Model>;
1801
- locale?: string;
1802
- userContext: unknown;
1803
- contentSourceInstance: CSITypes.ContentSourceInterface;
1804
- }): Promise<{ document: CSITypes.Document; newRefDocuments: CSITypes.Document[] }> {
1805
- if (model.type === 'page') {
1806
- const tokens = extractTokensFromString(String(model.urlPath));
1807
- const slugField = _.last(tokens);
1808
- if (object && slugField && slugField in object) {
1809
- const slugFieldValue = object[slugField];
1810
- object[slugField] = sanitizeSlug(slugFieldValue);
1811
- }
1812
- }
1813
-
1814
- const nestedResult = await createNestedObjectRecursively({
1815
- object,
1816
- modelFields: model.fields ?? [],
1817
- fieldPath: [],
1818
- modelMap,
1819
- locale,
1820
- userContext,
1821
- contentSourceInstance
1822
- });
1823
- const document = await contentSourceInstance.createDocument({
1824
- updateOperationFields: nestedResult.fields,
1825
- // TODO: pass csiModel
1826
- model,
1827
- // TODO: pass csiModelMap
1828
- modelMap,
1829
- locale,
1830
- userContext
1831
- });
1832
- return {
1833
- document: document,
1834
- newRefDocuments: nestedResult.newRefDocuments
1835
- };
1836
- }
1837
-
1838
- async function createNestedObjectRecursively({
1839
- object,
1840
- modelFields,
1841
- fieldPath,
1842
- modelMap,
1843
- locale,
1844
- userContext,
1845
- contentSourceInstance
1846
- }: {
1847
- object?: Record<string, any>;
1848
- modelFields: Field[];
1849
- fieldPath: (string | number)[];
1850
- modelMap: Record<string, Model>;
1851
- locale?: string;
1852
- userContext: unknown;
1853
- contentSourceInstance: CSITypes.ContentSourceInterface;
1854
- }): Promise<{
1855
- fields: Record<string, CSITypes.UpdateOperationField>;
1856
- newRefDocuments: CSITypes.Document[];
1857
- }> {
1858
- object = object ?? {};
1859
- const result: {
1860
- fields: Record<string, CSITypes.UpdateOperationField>;
1861
- newRefDocuments: CSITypes.Document[];
1862
- } = {
1863
- fields: {},
1864
- newRefDocuments: []
1865
- };
1866
- const objectFieldNames = Object.keys(object);
1867
- for (const modelField of modelFields) {
1868
- const fieldName = modelField.name;
1869
- let value;
1870
- if (fieldName in object) {
1871
- value = object[fieldName];
1872
- _.pull(objectFieldNames, fieldName);
1873
- } else if (modelField.const) {
1874
- value = modelField.const;
1875
- } else if (!_.isNil(modelField.default)) {
1876
- value = modelField.default;
1877
- }
1878
- if (!_.isNil(value)) {
1879
- const fieldResult = await createNestedField({
1880
- value,
1881
- modelField,
1882
- fieldPath: fieldPath.concat(fieldName),
1883
- modelMap,
1884
- locale,
1885
- userContext,
1886
- contentSourceInstance
1887
- });
1888
- result.fields[fieldName] = fieldResult.field;
1889
- result.newRefDocuments = result.newRefDocuments.concat(fieldResult.newRefDocuments);
1890
- }
1891
- }
1892
- if (objectFieldNames.length > 0) {
1893
- throw new Error(`no model fields found when creating a document with fields: '${objectFieldNames.join(', ')}'`);
1894
- }
1895
-
1896
- return result;
1897
- }
1898
-
1899
- async function createNestedField({
1900
- value,
1901
- modelField,
1902
- fieldPath,
1903
- modelMap,
1904
- locale,
1905
- userContext,
1906
- contentSourceInstance
1907
- }: {
1908
- value: any;
1909
- modelField: FieldSpecificProps;
1910
- fieldPath: (string | number)[];
1911
- modelMap: Record<string, Model>;
1912
- locale?: string;
1913
- userContext: unknown;
1914
- contentSourceInstance: CSITypes.ContentSourceInterface;
1915
- }): Promise<{ field: CSITypes.UpdateOperationField; newRefDocuments: CSITypes.Document[] }> {
1916
- if (modelField.type === 'object') {
1917
- const result = await createNestedObjectRecursively({
1918
- object: value,
1919
- modelFields: modelField.fields,
1920
- fieldPath,
1921
- modelMap,
1922
- locale,
1923
- userContext,
1924
- contentSourceInstance
1925
- });
1926
- return {
1927
- field: {
1928
- type: 'object',
1929
- fields: result.fields
1930
- },
1931
- newRefDocuments: result.newRefDocuments
1932
- };
1933
- } else if (modelField.type === 'model') {
1934
- let { $$type, ...rest } = value;
1935
- const modelNames = modelField.models;
1936
- // for backward compatibility check if the object has 'type' instead of '$$type' because older projects use
1937
- // the 'type' property in default values
1938
- if (!$$type && 'type' in rest) {
1939
- $$type = rest.type;
1940
- rest = _.omit(rest, 'type');
1941
- }
1942
- const modelName = $$type ?? (modelNames.length === 1 ? modelNames[0] : null);
1943
- if (!modelName) {
1944
- throw new Error(`no $$type was specified for nested model`);
1945
- }
1946
- const model = modelMap[modelName];
1947
- if (!model) {
1948
- throw new Error(`no model with name '${modelName}' was found`);
1949
- }
1950
- const result = await createNestedObjectRecursively({
1951
- object: rest,
1952
- modelFields: model.fields ?? [],
1953
- fieldPath,
1954
- modelMap,
1955
- locale,
1956
- userContext,
1957
- contentSourceInstance
1958
- });
1959
- return {
1960
- field: {
1961
- type: 'model',
1962
- modelName: modelName,
1963
- fields: result.fields
1964
- },
1965
- newRefDocuments: result.newRefDocuments
1966
- };
1967
- } else if (modelField.type === 'image') {
1968
- let refId: string | undefined;
1969
- if (_.isPlainObject(value)) {
1970
- refId = value.$$ref;
1971
- } else {
1972
- refId = value;
1973
- }
1974
- if (!refId) {
1975
- throw new Error(`reference field must specify a value`);
1976
- }
1977
- return {
1978
- field: {
1979
- type: 'reference',
1980
- refType: 'asset',
1981
- refId: refId
1982
- },
1983
- newRefDocuments: []
1984
- };
1985
- } else if (modelField.type === 'reference') {
1986
- let { $$ref: refId = null, $$type: modelName = null, ...rest } = _.isPlainObject(value) ? value : { $$ref: value };
1987
- if (refId) {
1988
- return {
1989
- field: {
1990
- type: 'reference',
1991
- refType: 'document',
1992
- refId: refId
1993
- },
1994
- newRefDocuments: []
1995
- };
1996
- } else {
1997
- const modelNames = modelField.models;
1998
- if (!modelName) {
1999
- // for backward compatibility check if the object has 'type' instead of '$$type' because older projects use
2000
- // the 'type' property in default values
2001
- if ('type' in rest) {
2002
- modelName = rest.type;
2003
- rest = _.omit(rest, 'type');
2004
- } else if (modelNames.length === 1) {
2005
- modelName = modelNames[0];
2006
- }
2007
- }
2008
- const model = modelMap[modelName];
2009
- if (!model) {
2010
- throw new Error(`no model with name '${modelName}' was found`);
2011
- }
2012
- const { document, newRefDocuments } = await createDocumentRecursively({
2013
- object: rest,
2014
- model: model,
2015
- modelMap,
2016
- locale,
2017
- userContext,
2018
- contentSourceInstance
2019
- });
2020
- return {
2021
- field: {
2022
- type: 'reference',
2023
- refType: 'document',
2024
- refId: document.id
2025
- },
2026
- newRefDocuments: [document, ...newRefDocuments]
2027
- };
2028
- }
2029
- } else if (modelField.type === 'list') {
2030
- if (!Array.isArray(value)) {
2031
- throw new Error(`value for list field must be array`);
2032
- }
2033
- const itemsField = modelField.items;
2034
- if (!itemsField) {
2035
- throw new Error(`list field does not define items`);
2036
- }
2037
- const arrayResult = await mapPromise(value, async (item, index) => {
2038
- return createNestedField({
2039
- value: item,
2040
- modelField: itemsField,
2041
- fieldPath: fieldPath.concat(index),
2042
- modelMap,
2043
- locale,
2044
- userContext,
2045
- contentSourceInstance
2046
- });
2047
- });
2048
- return {
2049
- field: {
2050
- type: 'list',
2051
- items: arrayResult.map((result) => result.field)
2052
- },
2053
- newRefDocuments: arrayResult.reduce((result: CSITypes.Document[], { newRefDocuments }) => result.concat(newRefDocuments), [])
2054
- };
2055
- }
2056
- return {
2057
- field: {
2058
- type: modelField.type,
2059
- value: value
2060
- },
2061
- newRefDocuments: []
2062
- };
2063
- }
2064
-
2065
- function getModelFieldForFieldAtPath(
2066
- document: ContentStoreTypes.Document,
2067
- model: Model,
2068
- fieldPath: (string | number)[],
2069
- modelMap: Record<string, Model>,
2070
- locale?: string
2071
- ): Field {
2072
- if (_.isEmpty(fieldPath)) {
2073
- throw new Error('the fieldPath can not be empty');
2074
- }
2075
-
2076
- function getField(docField: ContentStoreTypes.DocumentField, modelField: FieldSpecificProps, fieldPath: (string | number)[]): Field {
2077
- const fieldName = _.head(fieldPath);
2078
- if (typeof fieldName === 'undefined') {
2079
- throw new Error('the first fieldPath item must be string');
2080
- }
2081
- const childFieldPath = _.tail(fieldPath);
2082
- let childDocField: ContentStoreTypes.DocumentField | undefined;
2083
- let childModelField: Field | undefined;
2084
- switch (docField.type) {
2085
- case 'object':
2086
- const localizedObjectField = getDocumentFieldForLocale(docField, locale);
2087
- if (!localizedObjectField) {
2088
- throw new Error(`locale for field was not found`);
2089
- }
2090
- if (localizedObjectField.isUnset) {
2091
- throw new Error(`field is not set`);
2092
- }
2093
- childDocField = localizedObjectField.fields[fieldName];
2094
- childModelField = _.find((modelField as FieldObjectProps).fields, (field) => field.name === fieldName);
2095
- if (!childDocField || !childModelField) {
2096
- throw new Error(`field ${fieldName} doesn't exist`);
2097
- }
2098
- if (childFieldPath.length === 0) {
2099
- return childModelField;
2100
- }
2101
- return getField(childDocField, childModelField, childFieldPath);
2102
- case 'model':
2103
- const localizedModelField = getDocumentFieldForLocale(docField, locale);
2104
- if (!localizedModelField) {
2105
- throw new Error(`locale for field was not found`);
2106
- }
2107
- if (localizedModelField.isUnset) {
2108
- throw new Error(`field is not set`);
2109
- }
2110
- const modelName = localizedModelField.srcModelName;
2111
- const childModel = modelMap[modelName];
2112
- if (!childModel) {
2113
- throw new Error(`model ${modelName} doesn't exist`);
2114
- }
2115
- childModelField = _.find(childModel.fields, (field) => field.name === fieldName);
2116
- childDocField = localizedModelField.fields![fieldName];
2117
- if (!childDocField || !childModelField) {
2118
- throw new Error(`field ${fieldName} doesn't exist`);
2119
- }
2120
- if (childFieldPath.length === 0) {
2121
- return childModelField;
2122
- }
2123
- return getField(childDocField, childModelField!, childFieldPath);
2124
- case 'list':
2125
- const localizedListField = getDocumentFieldForLocale(docField, locale);
2126
- if (!localizedListField) {
2127
- throw new Error(`locale for field was not found`);
2128
- }
2129
- const listItem = localizedListField.items && localizedListField.items[fieldName as number];
2130
- const listItemsModel = (modelField as FieldListProps).items;
2131
- if (!listItem || !listItemsModel) {
2132
- throw new Error(`field ${fieldName} doesn't exist`);
2133
- }
2134
- if (childFieldPath.length === 0) {
2135
- return modelField as FieldList;
2136
- }
2137
- if (!Array.isArray(listItemsModel)) {
2138
- return getField(listItem, listItemsModel, childFieldPath);
2139
- } else {
2140
- const fieldListItems = (listItemsModel as FieldListItems[]).find((listItemsModel) => listItemsModel.type === listItem.type);
2141
- if (!fieldListItems) {
2142
- throw new Error('cannot find matching field model');
2143
- }
2144
- return getField(listItem, fieldListItems, childFieldPath);
2145
- }
2146
- default:
2147
- if (!_.isEmpty(childFieldPath)) {
2148
- throw new Error('illegal fieldPath');
2149
- }
2150
- return modelField as Field;
2151
- }
2152
- }
2153
-
2154
- const fieldName = _.head(fieldPath);
2155
- const childFieldPath = _.tail(fieldPath);
2156
-
2157
- if (typeof fieldName !== 'string') {
2158
- throw new Error('the first fieldPath item must be string');
2159
- }
2160
-
2161
- const childDocField = document.fields[fieldName];
2162
- const childModelField = _.find(model.fields, { name: fieldName });
2163
-
2164
- if (!childDocField || !childModelField) {
2165
- throw new Error(`field ${fieldName} doesn't exist`);
2166
- }
2167
-
2168
- if (childFieldPath.length === 0) {
2169
- return childModelField;
2170
- }
2171
-
2172
- return getField(childDocField, childModelField, childFieldPath);
2173
- }
2174
-
2175
- async function convertOperationField({
2176
- operationField,
2177
- fieldPath,
2178
- modelField,
2179
- modelMap,
2180
- locale,
2181
- userContext,
2182
- contentSourceInstance
2183
- }: {
2184
- operationField: ContentStoreTypes.UpdateOperationField;
2185
- fieldPath: (string | number)[];
2186
- modelField: Field;
2187
- modelMap: Record<string, Model>;
2188
- locale?: string;
2189
- userContext: unknown;
2190
- contentSourceInstance: CSITypes.ContentSourceInterface;
2191
- }): Promise<CSITypes.UpdateOperationField> {
2192
- // for insert operations, the modelField will be of the list, so get the modelField of the list items
2193
- const modelFieldOrListItems: FieldSpecificProps = modelField.type === 'list' ? modelField.items! : modelField;
2194
- switch (operationField.type) {
2195
- case 'object': {
2196
- const result = await createNestedObjectRecursively({
2197
- object: operationField.object,
2198
- modelFields: (modelFieldOrListItems as FieldObjectProps).fields,
2199
- fieldPath: fieldPath,
2200
- modelMap,
2201
- locale,
2202
- userContext,
2203
- contentSourceInstance
2204
- });
2205
- return {
2206
- type: operationField.type,
2207
- fields: result.fields
2208
- };
2209
- }
2210
- case 'model': {
2211
- const model = modelMap[operationField.modelName];
2212
- if (!model) {
2213
- throw new Error(`error updating document, could not find document model: '${operationField.modelName}'`);
2214
- }
2215
- const result = await createNestedObjectRecursively({
2216
- object: operationField.object,
2217
- modelFields: model.fields!,
2218
- fieldPath,
2219
- modelMap,
2220
- locale,
2221
- userContext,
2222
- contentSourceInstance
2223
- });
2224
- return {
2225
- type: operationField.type,
2226
- modelName: operationField.modelName,
2227
- fields: result.fields
2228
- };
2229
- }
2230
- case 'list': {
2231
- if (modelField.type !== 'list') {
2232
- throw new Error(`'the operation field type '${operationField.type}' does not match the model field type '${modelField.type}'`);
2233
- }
2234
- const result = await mapPromise(operationField.items, async (item, index) => {
2235
- const result = await createNestedField({
2236
- value: item,
2237
- modelField: modelField.items!,
2238
- fieldPath,
2239
- modelMap,
2240
- locale,
2241
- userContext,
2242
- contentSourceInstance
2243
- });
2244
- return result.field;
2245
- });
2246
- return {
2247
- type: operationField.type,
2248
- items: result
2249
- };
2250
- }
2251
- case 'string':
2252
- // When inserting new string value into a list, the client does not
2253
- // send value. Set an empty string value.
2254
- if (typeof operationField.value !== 'string') {
2255
- return {
2256
- type: operationField.type,
2257
- value: ''
2258
- };
2259
- }
2260
- return operationField as CSITypes.UpdateOperationField;
2261
- case 'enum':
2262
- // When inserting new enum value into a list, the client does not
2263
- // send value. Set first option as the value.
2264
- if (typeof operationField.value !== 'string') {
2265
- if (modelFieldOrListItems.type !== 'enum') {
2266
- throw new Error(`'the operation field type 'enum' does not match the model field type '${modelFieldOrListItems.type}'`);
2267
- }
2268
- const option = modelFieldOrListItems.options[0]!;
2269
- const optionValue = typeof option === 'object' ? option.value : option;
2270
- return {
2271
- type: operationField.type,
2272
- value: optionValue
2273
- };
2274
- }
2275
- return operationField as CSITypes.UpdateOperationField;
2276
- case 'image':
2277
- return operationField as CSITypes.UpdateOperationField;
2278
- default:
2279
- return operationField as CSITypes.UpdateOperationField;
2280
- }
2281
- }
2282
-
2283
- function getDocumentFieldForLocale<Type extends ContentStoreTypes.FieldType>(
2284
- docField: ContentStoreTypes.DocumentFieldForType<Type>,
2285
- locale?: string
2286
- ): ContentStoreTypes.DocumentFieldNonLocalizedForType<Type> | null {
2287
- if (docField.localized) {
2288
- if (!locale) {
2289
- return null;
2290
- }
2291
- const { localized, locales, ...base } = docField;
2292
- const localizedField = locales[locale];
2293
- if (!localizedField) {
2294
- return null;
2295
- }
2296
- return ({
2297
- ...base,
2298
- ...localizedField
2299
- } as unknown) as ContentStoreTypes.DocumentFieldNonLocalizedForType<Type>;
2300
- } else {
2301
- return docField;
2302
- }
2303
- }
2304
-
2305
1383
  function getCSIDocumentsAndAssetsFromContentSourceDataByIds(
2306
1384
  contentSourceData: ContentSourceData,
2307
1385
  objects: { srcObjectId: string }[]