@stackbit/cms-core 0.0.19-alpha.1 → 0.0.19-alpha.4

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.
@@ -17,10 +17,11 @@ import {
17
17
  } from '@stackbit/sdk';
18
18
  import { mapPromise, omitByNil } from '@stackbit/utils';
19
19
  import * as CSITypes from './content-source-interface';
20
- import { isLocalizedField } from './content-source-interface';
20
+ import { isLocalizedField, getLocalizedFieldForLocale } from './content-source-interface';
21
21
  import * as ContentStoreTypes from './content-store-types';
22
22
  import { IMAGE_MODEL } from './common/common-schema';
23
23
  import { Timer } from './utils/timer';
24
+ import { UserCommandSpawner } from './common/common-types';
24
25
 
25
26
  export interface ContentSourceOptions {
26
27
  logger: ContentStoreTypes.Logger;
@@ -28,6 +29,7 @@ export interface ContentSourceOptions {
28
29
  localDev: boolean;
29
30
  stackbitYamlDir: string;
30
31
  contentSources: CSITypes.ContentSourceInterface[];
32
+ userCommandSpawner?: UserCommandSpawner;
31
33
  onSchemaChangeCallback: () => void;
32
34
  onContentChangeCallback: (contentChanges: ContentStoreTypes.ContentChangeResult) => void;
33
35
  handleConfigAssets: (config: Config) => Promise<Config>;
@@ -38,17 +40,29 @@ interface ContentSourceData {
38
40
  instance: CSITypes.ContentSourceInterface;
39
41
  type: string;
40
42
  projectId: string;
41
- models: Model[];
42
- modelMap: Record<string, Model>;
43
43
  locales?: CSITypes.Locale[];
44
44
  defaultLocaleCode?: string;
45
+ /* Array of extended and validated Models */
46
+ models: Model[];
47
+ /* Map of extended and validated Models by model name */
48
+ modelMap: Record<string, Model>;
49
+ /* Map of original Models (as provided by content source) by model name */
50
+ csiModelMap: Record<string, Model>;
51
+ /* Array of original content source Documents */
45
52
  csiDocuments: CSITypes.Document[];
53
+ /* Map of original content source Documents by document ID */
46
54
  csiDocumentMap: Record<string, CSITypes.Document>;
55
+ /* Array of converted content-store Documents */
47
56
  documents: ContentStoreTypes.Document[];
57
+ /* Map of converted content-store Documents by document ID */
48
58
  documentMap: Record<string, ContentStoreTypes.Document>;
59
+ /* Array of original content source Assets */
49
60
  csiAssets: CSITypes.Asset[];
61
+ /* Map of original content source Assets by asset ID */
50
62
  csiAssetMap: Record<string, CSITypes.Asset>;
63
+ /* Array of converted content-store Assets */
51
64
  assets: ContentStoreTypes.Asset[];
65
+ /* Map of converted content-store Assets by asset ID */
52
66
  assetMap: Record<string, ContentStoreTypes.Asset>;
53
67
  }
54
68
 
@@ -56,6 +70,7 @@ export class ContentStore {
56
70
  private readonly contentSources: CSITypes.ContentSourceInterface[];
57
71
  private readonly logger: ContentStoreTypes.Logger;
58
72
  private readonly userLogger: ContentStoreTypes.Logger;
73
+ private readonly userCommandSpawner?: UserCommandSpawner;
59
74
  private readonly localDev: boolean;
60
75
  private readonly stackbitYamlDir: string; // TODO: remove stackbitYamlDir, ContentStore should not be aware of filesystem, instead pass autoreloading Config object
61
76
  private readonly onSchemaChangeCallback: () => void;
@@ -70,6 +85,7 @@ export class ContentStore {
70
85
  this.logger = options.logger.createLogger({ label: 'content-store' });
71
86
  this.userLogger = options.userLogger.createLogger({ label: 'content-store' });
72
87
  this.localDev = options.localDev;
88
+ this.userCommandSpawner = options.userCommandSpawner;
73
89
  this.stackbitYamlDir = options.stackbitYamlDir; // TODO: remove stackbitYamlDir, ContentStore should not be aware of filesystem, instead pass autoreloading Config object
74
90
  this.contentSources = options.contentSources;
75
91
  this.onSchemaChangeCallback = options.onSchemaChangeCallback;
@@ -223,6 +239,7 @@ export class ContentStore {
223
239
  await contentSourceInstance.init({
224
240
  logger: this.logger,
225
241
  userLogger: this.userLogger,
242
+ userCommandSpawner: this.userCommandSpawner,
226
243
  localDev: this.localDev
227
244
  });
228
245
  } else {
@@ -230,9 +247,9 @@ export class ContentStore {
230
247
  await contentSourceInstance.reset();
231
248
  }
232
249
 
233
- const csModels = await contentSourceInstance.getModels();
250
+ const csiModels = await contentSourceInstance.getModels();
234
251
  const locales = await contentSourceInstance?.getLocales();
235
- const result = await extendConfig({ dirPath: this.stackbitYamlDir, config: this.rawStackbitConfig, externalModels: csModels });
252
+ const result = await extendConfig({ dirPath: this.stackbitYamlDir, config: this.rawStackbitConfig, externalModels: csiModels });
236
253
  const config = await this.handleConfigAssets(result.config);
237
254
  const models = config.models;
238
255
  const modelMap = _.keyBy(models, 'name');
@@ -283,6 +300,7 @@ export class ContentStore {
283
300
  defaultLocaleCode: defaultLocaleCode,
284
301
  models: models,
285
302
  modelMap: modelMap,
303
+ csiModelMap: _.keyBy(csiModels, 'name'),
286
304
  csiDocuments: csiDocuments,
287
305
  csiDocumentMap: csiDocumentMap,
288
306
  documents: contentStoreDocuments,
@@ -296,7 +314,7 @@ export class ContentStore {
296
314
  contentSourceInstance.startWatchingContentUpdates({
297
315
  getModelMap: () => {
298
316
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
299
- return contentSourceData.modelMap;
317
+ return contentSourceData.csiModelMap;
300
318
  },
301
319
  getDocument({ documentId }: { documentId: string }) {
302
320
  return csiDocumentMap[documentId];
@@ -677,15 +695,15 @@ export class ContentStore {
677
695
 
678
696
  // get the document model
679
697
  const documentModelName = document.srcModelName;
680
- const modelMap = contentSourceData.modelMap;
681
- const model = modelMap[documentModelName];
698
+ const csiModelMap = contentSourceData.csiModelMap;
699
+ const model = csiModelMap[documentModelName];
682
700
  if (!model) {
683
701
  throw new Error(`error updating document, could not find document model: '${documentModelName}'`);
684
702
  }
685
703
 
686
704
  // get the 'reference' model field in the updated document that will be used to link the new document
687
705
  locale = locale ?? contentSourceData.defaultLocaleCode;
688
- const modelField = getModelFieldForFieldAtPath(document, model, fieldPath, modelMap, locale);
706
+ const modelField = getModelFieldForFieldAtPath(document, model, fieldPath, csiModelMap, locale);
689
707
  if (!modelField) {
690
708
  throw Error(`the "fieldPath" points to non existing model field: ${fieldPath.join('.')}`);
691
709
  }
@@ -721,7 +739,7 @@ export class ContentStore {
721
739
  } as const;
722
740
  const updatedDocument = await contentSourceData.instance.updateDocument({
723
741
  document: csiDocument,
724
- modelMap: modelMap,
742
+ modelMap: csiModelMap,
725
743
  userContext: userContext,
726
744
  operations: [
727
745
  modelField.type === 'list'
@@ -777,15 +795,15 @@ export class ContentStore {
777
795
 
778
796
  // get the document model
779
797
  const documentModelName = document.srcModelName;
780
- const modelMap = contentSourceData.modelMap;
781
- const model = modelMap[documentModelName];
782
- if (!model) {
798
+ const csiModelMap = contentSourceData.csiModelMap;
799
+ const csiModel = csiModelMap[documentModelName];
800
+ if (!csiModel) {
783
801
  throw new Error(`error updating document, could not find document model: '${documentModelName}'`);
784
802
  }
785
803
 
786
804
  // get the 'reference' model field in the updated document that will be used to link the new asset
787
805
  locale = locale ?? contentSourceData.defaultLocaleCode;
788
- const modelField = getModelFieldForFieldAtPath(document, model, fieldPath, modelMap, locale);
806
+ const modelField = getModelFieldForFieldAtPath(document, csiModel, fieldPath, csiModelMap, locale);
789
807
  if (!modelField) {
790
808
  throw Error(`the "fieldPath" points to non existing model field: ${fieldPath.join('.')}`);
791
809
  }
@@ -812,7 +830,7 @@ export class ContentStore {
812
830
  } as const;
813
831
  const updatedDocument = await contentSourceData.instance.updateDocument({
814
832
  document: csiDocument,
815
- modelMap: modelMap,
833
+ modelMap: csiModelMap,
816
834
  userContext: userContext,
817
835
  operations: [
818
836
  modelField.type === 'list'
@@ -907,6 +925,7 @@ export class ContentStore {
907
925
  }
908
926
 
909
927
  const modelMap = contentSourceData.modelMap;
928
+ const csiModelMap = contentSourceData.csiModelMap;
910
929
  const documentModelName = document.srcModelName;
911
930
  const model = modelMap[documentModelName];
912
931
  if (!model) {
@@ -961,7 +980,7 @@ export class ContentStore {
961
980
 
962
981
  const updatedDocumentResult = await contentSourceData.instance.updateDocument({
963
982
  document: csiDocument,
964
- modelMap,
983
+ modelMap: csiModelMap,
965
984
  userContext,
966
985
  operations
967
986
  });
@@ -1005,7 +1024,7 @@ export class ContentStore {
1005
1024
  // TODO: handle duplicatable and non duplicatable models
1006
1025
  // TODO: handle fields from the provided object
1007
1026
  const documentResult = await contentSourceData.instance.createDocument({
1008
- documentFields: mapStoreFieldsToSourceFields({
1027
+ updateOperationFields: mapStoreFieldsToOperationFields({
1009
1028
  documentFields: document.fields,
1010
1029
  modelFields: model.fields!,
1011
1030
  modelMap: contentSourceData.modelMap
@@ -1113,8 +1132,10 @@ export class ContentStore {
1113
1132
  locale = locale ?? contentSourceData.defaultLocaleCode;
1114
1133
  const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
1115
1134
  const userContext = getUserContextForSrcType(contentSourceData.type, user);
1135
+ const internalValidationErrors = internalValidateContent(documents, assets, contentSourceData);
1116
1136
  const validationResult = await contentSourceData.instance.validateDocuments({ documents, assets, locale, userContext });
1117
1137
  errors = errors.concat(
1138
+ internalValidationErrors,
1118
1139
  validationResult.errors.map((validationError) => ({
1119
1140
  message: validationError.message,
1120
1141
  srcType: contentSourceData.type,
@@ -1487,7 +1508,7 @@ function mapMarkdownField(csiDocumentField: CSITypes.DocumentValueField): Conten
1487
1508
  };
1488
1509
  }
1489
1510
 
1490
- function mapStoreFieldsToSourceFields({
1511
+ function mapStoreFieldsToOperationFields({
1491
1512
  documentFields,
1492
1513
  modelFields,
1493
1514
  modelMap
@@ -1495,7 +1516,7 @@ function mapStoreFieldsToSourceFields({
1495
1516
  documentFields: Record<string, ContentStoreTypes.DocumentField>;
1496
1517
  modelFields: Field[];
1497
1518
  modelMap: Record<string, Model>;
1498
- }): Record<string, CSITypes.DocumentField> {
1519
+ }): Record<string, CSITypes.UpdateOperationField> {
1499
1520
  // TODO: implement
1500
1521
  throw new Error(`duplicateDocument not implemented yet`);
1501
1522
  }
@@ -1585,6 +1606,7 @@ function toLocalizedAPIField(docField: ContentStoreTypes.DocumentField, locale?:
1585
1606
  if (docFieldLocalized.type === 'object' || docFieldLocalized.type === 'model') {
1586
1607
  return {
1587
1608
  ...docFieldLocalized,
1609
+ type: 'object',
1588
1610
  ...commonProps,
1589
1611
  ...(docFieldLocalized.isUnset
1590
1612
  ? null
@@ -1725,8 +1747,10 @@ async function createDocumentRecursively({
1725
1747
  contentSourceInstance
1726
1748
  });
1727
1749
  const document = await contentSourceInstance.createDocument({
1728
- documentFields: nestedResult.fields,
1750
+ updateOperationFields: nestedResult.fields,
1751
+ // TODO: pass csiModel
1729
1752
  model,
1753
+ // TODO: pass csiModelMap
1730
1754
  modelMap,
1731
1755
  locale,
1732
1756
  userContext
@@ -2172,9 +2196,6 @@ async function convertOperationField({
2172
2196
  }
2173
2197
  return operationField as CSITypes.UpdateOperationField;
2174
2198
  case 'image':
2175
- if (typeof operationField.value !== 'string') {
2176
- throw new Error('image value was not specified');
2177
- }
2178
2199
  return operationField as CSITypes.UpdateOperationField;
2179
2200
  default:
2180
2201
  return operationField as CSITypes.UpdateOperationField;
@@ -2231,3 +2252,61 @@ function getCSIDocumentsAndAssetsFromContentSourceDataByIds(
2231
2252
  assets
2232
2253
  };
2233
2254
  }
2255
+
2256
+ function internalValidateContent(
2257
+ documents: CSITypes.Document[],
2258
+ assets: CSITypes.Asset[],
2259
+ contentSourceData: ContentSourceData
2260
+ ): ContentStoreTypes.ValidationError[] {
2261
+
2262
+ const errors: ContentStoreTypes.ValidationError[] = [];
2263
+ _.forEach(documents, (document) => {
2264
+ _.forEach(document.fields, (documentField, fieldName) => {
2265
+ const localizedField = getLocalizedFieldForLocale(documentField)!;
2266
+ errors.push(...validateDocumentFields(document, localizedField, [fieldName], contentSourceData));
2267
+ })
2268
+ })
2269
+ return errors;
2270
+ }
2271
+
2272
+ function validateDocumentFields(
2273
+ document: CSITypes.Document,
2274
+ documentField: CSITypes.DocumentFieldNonLocalized,
2275
+ fieldPath: (string | number)[],
2276
+ contentSourceData: ContentSourceData
2277
+ ): ContentStoreTypes.ValidationError[] {
2278
+ const errors: ContentStoreTypes.ValidationError[] = [];
2279
+
2280
+ if (documentField.type === 'object') {
2281
+ _.forEach(documentField.fields, (documentField, fieldName) => {
2282
+ const localizedField = getLocalizedFieldForLocale(documentField)!;
2283
+ errors.push(...validateDocumentFields(document, localizedField, fieldPath.concat(fieldName), contentSourceData));
2284
+ });
2285
+ } else if (documentField.type === 'model') {
2286
+ _.forEach(documentField.fields, (documentField, fieldName) => {
2287
+ const localizedField = getLocalizedFieldForLocale(documentField)!;
2288
+ errors.push(...validateDocumentFields(document, localizedField, fieldPath.concat(fieldName), contentSourceData));
2289
+ });
2290
+ } else if (documentField.type === 'reference') {
2291
+ const objRef = documentField.refType === 'asset'
2292
+ ? contentSourceData.assetMap[documentField.refId]
2293
+ : contentSourceData.documentMap[documentField.refId];
2294
+ if (!objRef) {
2295
+ errors.push({
2296
+ fieldPath,
2297
+ srcType: contentSourceData.type,
2298
+ srcProjectId: contentSourceData.projectId,
2299
+ srcObjectType: documentField.refType,
2300
+ srcObjectId: document.id,
2301
+ message: `Can't find referenced ${documentField.refType}: ${documentField.refId}`
2302
+ });
2303
+ }
2304
+ } else if (documentField.type === 'list') {
2305
+ _.forEach(documentField.items, (documentField, i) => {
2306
+ const localizedField = getLocalizedFieldForLocale(documentField)!;
2307
+ errors.push(...validateDocumentFields(document, documentField, fieldPath.concat(i), contentSourceData));
2308
+ });
2309
+ }
2310
+
2311
+ return errors;
2312
+ }
package/src/index.ts CHANGED
@@ -3,8 +3,9 @@ export * as annotator from './annotator';
3
3
  export * as utils from './utils';
4
4
  export * as consts from './consts';
5
5
  export * from './common/common-schema';
6
+ export * from './common/common-types';
6
7
  export * from './content-store';
7
8
  export * as ContentSourceTypes from './content-source-interface';
8
9
  export * as ContentStoreTypes from './content-store-types';
9
10
  export { default as encodeData } from './encoder';
10
- export * from './encoder';
11
+ export * from './encoder';