@stackbit/cms-core 0.1.12-locale.0 → 0.1.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbit/cms-core",
3
- "version": "0.1.12-locale.0",
3
+ "version": "0.1.12",
4
4
  "description": "stackbit-dev",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,9 +29,9 @@
29
29
  "@babel/parser": "^7.11.5",
30
30
  "@babel/traverse": "^7.11.5",
31
31
  "@iarna/toml": "^2.2.3",
32
- "@stackbit/sdk": "^0.3.8-locale.0",
33
- "@stackbit/types": "^0.1.6-locale.0",
34
- "@stackbit/utils": "^0.2.11",
32
+ "@stackbit/sdk": "^0.3.8",
33
+ "@stackbit/types": "^0.1.6",
34
+ "@stackbit/utils": "^0.2.12",
35
35
  "chalk": "^4.0.1",
36
36
  "esm": "^3.2.25",
37
37
  "fs-extra": "^8.1.0",
@@ -45,5 +45,5 @@
45
45
  "sanitize-filename": "^1.6.3",
46
46
  "slugify": "^1.6.5"
47
47
  },
48
- "gitHead": "5ae211b3a7770c097d36efe3b860ed4d4d8a3c25"
48
+ "gitHead": "6704c5d8fe0e139797c6e29555cc1e71e611d281"
49
49
  }
@@ -188,7 +188,7 @@ export type DocumentFieldTypeAPI<BaseFieldProps, LocalizedFieldProps> = BaseFiel
188
188
  description?: string;
189
189
  locale?: string;
190
190
  localized?: boolean;
191
- };
191
+ } & ({ localized?: false; } | { localized: true; locale: string; });
192
192
 
193
193
  // any field that is not 'object' | 'model' | 'reference' | 'richText' | 'markdown' | 'list'
194
194
  export type DocumentValueFieldType = Exclude<FieldType, 'object' | 'model' | 'reference' | 'markdown' | 'richText' | 'list' | 'image'>;
@@ -27,7 +27,7 @@ export function getDocumentFieldForLocale<Type extends ContentStoreTypes.FieldTy
27
27
  return null;
28
28
  }
29
29
  const { localized, locales, ...base } = docField;
30
- const localizedField = locales[locale];
30
+ const localizedField = locales?.[locale];
31
31
  if (!localizedField) {
32
32
  return null;
33
33
  }
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import sanitizeFilename from 'sanitize-filename';
4
4
 
5
5
  import * as CSITypes from '@stackbit/types';
6
- import { getLocalizedFieldForLocale, UserCommandSpawner } from '@stackbit/types';
6
+ import { getLocalizedFieldForLocale, ModelExtension, UserCommandSpawner } from '@stackbit/types';
7
7
  import {
8
8
  Config,
9
9
  Field,
@@ -105,6 +105,7 @@ export class ContentStore {
105
105
  private stackbitConfig: Config | null = null;
106
106
  private yamlModels: Model[] = [];
107
107
  private configModels: Model[] = [];
108
+ private modelExtensions: ModelExtension[] | null = null;
108
109
  private presets: PresetMap = {};
109
110
 
110
111
  constructor(options: ContentSourceOptions) {
@@ -152,22 +153,36 @@ export class ContentStore {
152
153
 
153
154
  async init({ stackbitConfig }: { stackbitConfig: Config | null }): Promise<void> {
154
155
  this.logger.debug('init');
156
+
157
+ this.stackbitConfig = stackbitConfig;
158
+
155
159
  if (stackbitConfig) {
156
- this.yamlModels = await this.loadYamlModels({ stackbitConfig });
160
+ if (stackbitConfig.modelExtensions) {
161
+ this.modelExtensions = stackbitConfig.modelExtensions;
162
+ } else {
163
+ this.yamlModels = await this.loadYamlModels({ stackbitConfig });
164
+ this.configModels = this.mergeConfigModels(stackbitConfig.models ?? [], this.yamlModels);
165
+ }
157
166
  this.presets = await this.loadPresets({ stackbitConfig });
158
167
  }
159
- await this.setStackbitConfig({ stackbitConfig });
168
+
169
+ await this.loadContentSourcesAndProcessData({ init: true });
170
+
160
171
  this.contentUpdatesWatchTimer.startTimer();
161
172
  }
162
173
 
163
174
  async onStackbitConfigChange({ stackbitConfig }: { stackbitConfig: Config | null }) {
164
175
  this.logger.debug('onStackbitConfigChange');
165
- await this.setStackbitConfig({ stackbitConfig });
166
- }
167
176
 
168
- private async setStackbitConfig({ stackbitConfig }: { stackbitConfig: Config | null }) {
169
177
  this.stackbitConfig = stackbitConfig;
170
- this.configModels = this.mergeConfigModels(this.stackbitConfig, this.yamlModels);
178
+
179
+ if (stackbitConfig) {
180
+ if (stackbitConfig.modelExtensions) {
181
+ this.modelExtensions = stackbitConfig.modelExtensions;
182
+ } else {
183
+ this.configModels = this.mergeConfigModels(stackbitConfig.models ?? [], this.yamlModels);
184
+ }
185
+ }
171
186
 
172
187
  await this.loadContentSourcesAndProcessData({ init: true });
173
188
  }
@@ -202,12 +217,16 @@ export class ContentStore {
202
217
  this.contentUpdatesWatchTimer.startTimer();
203
218
  }
204
219
 
220
+ stop() {
221
+ this.contentUpdatesWatchTimer.stopTimer();
222
+ }
223
+
205
224
  async onFilesChange(updatedFiles: string[]): Promise<{ schemaChanged?: boolean; contentChanges: ContentStoreTypes.ContentChangeResult }> {
206
225
  this.logger.debug('onFilesChange');
207
226
 
208
227
  let schemaChanged = false;
209
228
 
210
- if (this.stackbitConfig) {
229
+ if (this.stackbitConfig && !this.stackbitConfig.modelExtensions) {
211
230
  // Check if any of the yaml models files were changed. If yaml model files were changed,
212
231
  // reload them and merge them with models defined in stackbit config.
213
232
  const modelDirs = getYamlModelDirs(this.stackbitConfig);
@@ -216,7 +235,7 @@ export class ContentStore {
216
235
  this.logger.debug('identified change in stackbit model files');
217
236
  schemaChanged = true;
218
237
  this.yamlModels = await this.loadYamlModels({ stackbitConfig: this.stackbitConfig });
219
- this.configModels = this.mergeConfigModels(this.stackbitConfig, this.yamlModels);
238
+ this.configModels = this.mergeConfigModels(this.stackbitConfig.models ?? [], this.yamlModels);
220
239
  }
221
240
 
222
241
  // Check if any of the preset files were changed. If presets were changed, reload them.
@@ -287,8 +306,8 @@ export class ContentStore {
287
306
  return yamlModelsResult.models;
288
307
  }
289
308
 
290
- private mergeConfigModels(stackbitConfig: Config | null, modelsFromFiles: Model[]) {
291
- const configModelsResult = mergeConfigModelsWithModelsFromFiles(stackbitConfig?.models ?? [], modelsFromFiles);
309
+ private mergeConfigModels(configModels: Model[], modelsFromFiles: Model[]) {
310
+ const configModelsResult = mergeConfigModelsWithModelsFromFiles(configModels, modelsFromFiles);
292
311
  for (const error of configModelsResult.errors) {
293
312
  this.userLogger.warn(error.message);
294
313
  }
@@ -352,7 +371,7 @@ export class ContentStore {
352
371
  // update all content sources at once to prevent race conditions
353
372
  this.contentSourceDataById = await this.processData({
354
373
  stackbitConfig: this.stackbitConfig,
355
- configModels: this.configModels,
374
+ configModels: (this.modelExtensions as Model[]) ?? this.configModels ?? [],
356
375
  presets: this.presets,
357
376
  contentSourceRawDataArr: contentSourceRawDataArr
358
377
  });
@@ -613,9 +632,67 @@ export class ContentStore {
613
632
  presets: Record<string, Preset>;
614
633
  contentSourceRawDataArr: ContentSourceRawData[];
615
634
  }): Promise<Record<string, ContentSourceData>> {
635
+ // Group models from all content sources by their names
636
+
637
+ const csiModelGroups = contentSourceRawDataArr.reduce((modelGroups: Record<string, CSITypes.ModelWithSource[]>, csData) => {
638
+ return csData.csiModels.reduce((modelGroups, model) => {
639
+ if (!(model.name in modelGroups)) {
640
+ modelGroups[model.name] = [];
641
+ }
642
+ modelGroups[model.name]!.push({
643
+ srcType: csData.srcType,
644
+ srcProjectId: csData.srcProjectId,
645
+ ...model
646
+ });
647
+ return modelGroups;
648
+ }, modelGroups);
649
+ }, {});
650
+
651
+ // Match config models to the group of content source models with the same name.
652
+ // Then, match the config model to content source model by comparing srcType and
653
+ // srcProjectId. If after the comparison, there are more than one model left,
654
+ // log a warning and filter out that config model so it won't be merged with any
655
+ // of the content source models.
656
+ const modelMatchErrors: { configModel: ModelExtension; matchedCSIModels: CSITypes.ModelWithSource[] }[] = [];
657
+ const filteredConfigModels = (configModels as ModelExtension[]).filter((configModel) => {
658
+ const csiModels = csiModelGroups[configModel.name];
659
+ if (!csiModels) {
660
+ return false;
661
+ }
662
+ const matchedCSIModels = csiModels.filter((model) => {
663
+ const matchesType = !configModel.srcType || model.srcType === configModel.srcType;
664
+ const matchesId = !configModel.srcProjectId || model.srcProjectId === configModel.srcProjectId;
665
+ return matchesType && matchesId;
666
+ });
667
+ if (matchedCSIModels.length === 0) {
668
+ return false;
669
+ }
670
+ if (matchedCSIModels.length === 1) {
671
+ return true;
672
+ }
673
+ modelMatchErrors.push({
674
+ configModel,
675
+ matchedCSIModels
676
+ });
677
+ return false;
678
+ }) as Model[];
679
+
680
+ // Log model matching warnings using user logger
681
+ for (const { configModel, matchedCSIModels } of modelMatchErrors) {
682
+ let message = `name: '${configModel.name}'`;
683
+ if (configModel.srcType) {
684
+ message += `, srcType: '${configModel.srcType}'`;
685
+ }
686
+ if (configModel.srcProjectId) {
687
+ message += `, srcProjectId: '${configModel.srcProjectId}'`;
688
+ }
689
+ const matchesModelsMessage = matchedCSIModels.map((model) => `srcType: '${model.srcType}', srcProjectId: '${model.srcProjectId}'`).join('; ');
690
+ this.userLogger.warn(`model ${message} defined in stackbit config matches more that 1 model in the following content sources: ${matchesModelsMessage}`);
691
+ }
692
+
616
693
  const modelsWithSource = contentSourceRawDataArr.reduce((accum: CSITypes.ModelWithSource[], csData) => {
617
694
  const mergedModels = mergeConfigModelsWithExternalModels({
618
- configModels: configModels,
695
+ configModels: filteredConfigModels,
619
696
  externalModels: csData.csiModels
620
697
  });
621
698
  const modelsWithSource = mergedModels.map(
@@ -712,7 +789,7 @@ export class ContentStore {
712
789
  if (!this.presets || !locale) {
713
790
  return this.presets ?? {};
714
791
  }
715
- return _.pickBy(this.presets, preset => !preset.locale || preset.locale === locale);
792
+ return _.pickBy(this.presets, (preset) => !preset.locale || preset.locale === locale);
716
793
  }
717
794
 
718
795
  getContentSourceEnvironment({ srcProjectId, srcType }: { srcProjectId: string; srcType: string }): string {
@@ -842,18 +919,13 @@ export class ContentStore {
842
919
  return contentSourceData.documentMap[srcDocumentId];
843
920
  }
844
921
 
845
- getDocuments({ contentSourceIds, locale }: { contentSourceIds?: string[], locale?: string }): ContentStoreTypes.Document[] {
846
- const hasExplicitLocale = !_.isEmpty(locale);
922
+ getDocuments({ locale }: { locale?: string }): ContentStoreTypes.Document[] {
847
923
  return _.reduce(
848
924
  this.contentSourceDataById,
849
925
  (documents: ContentStoreTypes.Document[], contentSourceData) => {
850
- if (contentSourceIds && !contentSourceIds.includes(contentSourceData.id)) {
851
- return documents;
852
- }
853
- const currentLocale = locale ?? contentSourceData.defaultLocaleCode;
854
- const currentDocuments = hasExplicitLocale
855
- ? contentSourceData.documents.filter(document => !document.locale || document.locale === currentLocale)
856
- : contentSourceData.documents;
926
+ const currentDocuments = _.isEmpty(locale)
927
+ ? contentSourceData.documents
928
+ : contentSourceData.documents.filter(document => !document.locale || document.locale === locale)
857
929
  return documents.concat(currentDocuments);
858
930
  },
859
931
  []
@@ -867,14 +939,12 @@ export class ContentStore {
867
939
  }
868
940
 
869
941
  getAssets({ locale }: { locale?: string }): ContentStoreTypes.Asset[] {
870
- const hasExplicitLocale = !_.isEmpty(locale);
871
942
  return _.reduce(
872
943
  this.contentSourceDataById,
873
944
  (assets: ContentStoreTypes.Asset[], contentSourceData) => {
874
- const currentLocale = locale ?? contentSourceData.defaultLocaleCode;
875
- const currentAssets = hasExplicitLocale
876
- ? contentSourceData.assets.filter(asset => !asset.locale || asset.locale === currentLocale)
877
- : contentSourceData.assets;
945
+ const currentAssets = _.isEmpty(locale)
946
+ ? contentSourceData.assets
947
+ : contentSourceData.assets.filter(asset => !asset.locale || asset.locale === locale)
878
948
  return assets.concat(currentAssets);
879
949
  },
880
950
  []
@@ -886,13 +956,13 @@ export class ContentStore {
886
956
  return _.reduce(
887
957
  this.contentSourceDataById,
888
958
  (objects: ContentStoreTypes.APIObject[], contentSourceData) => {
889
- const currentLocale = locale ?? contentSourceData.defaultLocaleCode;
890
959
  const documents = hasExplicitLocale
891
- ? contentSourceData.documents.filter(document => !document.locale || document.locale === currentLocale)
960
+ ? contentSourceData.documents.filter(document => !document.locale || document.locale === locale)
892
961
  : contentSourceData.documents;
893
962
  const assets = hasExplicitLocale
894
- ? contentSourceData.assets.filter(asset => !asset.locale || asset.locale === currentLocale)
963
+ ? contentSourceData.assets.filter(asset => !asset.locale || asset.locale === locale)
895
964
  : contentSourceData.assets;
965
+ const currentLocale = locale ?? contentSourceData.defaultLocaleCode;
896
966
  const documentObjects = mapDocumentsToLocalizedApiObjects(documents, currentLocale);
897
967
  const imageObjects = mapAssetsToLocalizedApiImages(assets, currentLocale);
898
968
  return objects.concat(documentObjects, imageObjects);
@@ -1441,7 +1511,7 @@ export class ContentStore {
1441
1511
  locale = locale ?? contentSourceData.defaultLocaleCode;
1442
1512
  const { documents, assets } = getCSIDocumentsAndAssetsFromContentSourceDataByIds(contentSourceData, contentSourceObjects);
1443
1513
  const userContext = getUserContextForSrcType(contentSourceData.srcType, user);
1444
- const internalValidationErrors = internalValidateContent(documents, assets, contentSourceData);
1514
+ const internalValidationErrors = internalValidateContent(documents, assets, contentSourceData, locale);
1445
1515
  const validationResult = await contentSourceData.instance.validateDocuments({ documents, assets, locale, userContext });
1446
1516
  errors = errors.concat(
1447
1517
  internalValidationErrors,
@@ -1489,17 +1559,25 @@ export class ContentStore {
1489
1559
  items: ContentStoreTypes.Document[];
1490
1560
  }> {
1491
1561
  this.logger.debug('searchDocuments');
1492
- let locale = data.locale;
1562
+ const locale = data.locale;
1493
1563
  const objectsBySourceId = _.groupBy(data.models, (object) => getContentSourceId(object.srcType, object.srcProjectId));
1494
1564
  const contentSourceIds = Object.keys(objectsBySourceId);
1495
- const documents: ContentStoreTypes.Document[] = this.getDocuments({ contentSourceIds, locale });
1565
+ const documents: ContentStoreTypes.Document[] = [];
1496
1566
  const schema: Record<string, Record<string, Record<string, Model>>> = {};
1567
+ const defaultLocales: Record<string, string> = {};
1497
1568
 
1498
1569
  contentSourceIds.forEach((contentSourceId) => {
1499
1570
  const contentSourceData = this.getContentSourceDataByIdOrThrow(contentSourceId);
1571
+
1500
1572
  _.set(schema, [contentSourceData.srcType, contentSourceData.srcProjectId], contentSourceData.modelMap);
1501
- if (!locale && contentSourceData.defaultLocaleCode) {
1502
- locale = contentSourceData.defaultLocaleCode;
1573
+
1574
+ const contentSourceDocuments = _.isEmpty(locale)
1575
+ ? contentSourceData.documents
1576
+ : contentSourceData.documents.filter(document => !document.locale || document.locale === locale)
1577
+ documents.push(...contentSourceDocuments);
1578
+
1579
+ if (contentSourceData.defaultLocaleCode) {
1580
+ defaultLocales[contentSourceId] = contentSourceData.defaultLocaleCode;
1503
1581
  }
1504
1582
  });
1505
1583
 
@@ -1507,7 +1585,8 @@ export class ContentStore {
1507
1585
  ...data,
1508
1586
  documents,
1509
1587
  schema,
1510
- locale
1588
+ locale,
1589
+ defaultLocales
1511
1590
  });
1512
1591
  }
1513
1592
 
@@ -1570,13 +1649,16 @@ function getCSIDocumentsAndAssetsFromContentSourceDataByIds(
1570
1649
  function internalValidateContent(
1571
1650
  documents: CSITypes.Document[],
1572
1651
  assets: CSITypes.Asset[],
1573
- contentSourceData: ContentSourceData
1652
+ contentSourceData: ContentSourceData,
1653
+ locale?: string
1574
1654
  ): ContentStoreTypes.ValidationError[] {
1575
1655
  const errors: ContentStoreTypes.ValidationError[] = [];
1576
1656
  _.forEach(documents, (document) => {
1577
1657
  _.forEach(document.fields, (documentField, fieldName) => {
1578
- const localizedField = getLocalizedFieldForLocale(documentField)!;
1579
- errors.push(...validateDocumentFields(document, localizedField, [fieldName], contentSourceData));
1658
+ const localizedField = getLocalizedFieldForLocale(documentField, locale);
1659
+ if (localizedField) {
1660
+ errors.push(...validateDocumentFields(document, localizedField, [fieldName], contentSourceData, locale));
1661
+ }
1580
1662
  });
1581
1663
  });
1582
1664
  return errors;
@@ -1586,19 +1668,24 @@ function validateDocumentFields(
1586
1668
  document: CSITypes.Document,
1587
1669
  documentField: CSITypes.DocumentFieldNonLocalized,
1588
1670
  fieldPath: (string | number)[],
1589
- contentSourceData: ContentSourceData
1671
+ contentSourceData: ContentSourceData,
1672
+ locale?: string
1590
1673
  ): ContentStoreTypes.ValidationError[] {
1591
1674
  const errors: ContentStoreTypes.ValidationError[] = [];
1592
1675
 
1593
1676
  if (documentField.type === 'object') {
1594
1677
  _.forEach(documentField.fields, (documentField, fieldName) => {
1595
- const localizedField = getLocalizedFieldForLocale(documentField)!;
1596
- errors.push(...validateDocumentFields(document, localizedField, fieldPath.concat(fieldName), contentSourceData));
1678
+ const localizedField = getLocalizedFieldForLocale(documentField, locale);
1679
+ if (localizedField) {
1680
+ errors.push(...validateDocumentFields(document, localizedField, fieldPath.concat(fieldName), contentSourceData, locale));
1681
+ }
1597
1682
  });
1598
1683
  } else if (documentField.type === 'model') {
1599
1684
  _.forEach(documentField.fields, (documentField, fieldName) => {
1600
- const localizedField = getLocalizedFieldForLocale(documentField)!;
1601
- errors.push(...validateDocumentFields(document, localizedField, fieldPath.concat(fieldName), contentSourceData));
1685
+ const localizedField = getLocalizedFieldForLocale(documentField, locale);
1686
+ if (localizedField) {
1687
+ errors.push(...validateDocumentFields(document, localizedField, fieldPath.concat(fieldName), contentSourceData, locale));
1688
+ }
1602
1689
  });
1603
1690
  } else if (documentField.type === 'reference') {
1604
1691
  const objRef = documentField.refType === 'asset' ? contentSourceData.assetMap[documentField.refId] : contentSourceData.documentMap[documentField.refId];
@@ -1614,8 +1701,7 @@ function validateDocumentFields(
1614
1701
  }
1615
1702
  } else if (documentField.type === 'list') {
1616
1703
  _.forEach(documentField.items, (documentField, i) => {
1617
- const localizedField = getLocalizedFieldForLocale(documentField)!;
1618
- errors.push(...validateDocumentFields(document, documentField, fieldPath.concat(i), contentSourceData));
1704
+ errors.push(...validateDocumentFields(document, documentField, fieldPath.concat(i), contentSourceData, locale));
1619
1705
  });
1620
1706
  }
1621
1707
 
@@ -165,8 +165,16 @@ function mapCSIFieldsToStoreFields({
165
165
  modelField,
166
166
  context
167
167
  });
168
+ // Override document field types with specific model field types.
169
+ // For example when developer re-mapped content-source model "string"
170
+ // field to stackbit "color" field.
171
+ if (modelField.type === 'color' || modelField.type === 'style') {
172
+ docField.type = modelField.type;
173
+ }
168
174
  docField.label = modelField.label;
169
- docField.localized = modelField.localized;
175
+ if ('localized' in modelField) {
176
+ docField.localized = modelField.localized;
177
+ }
170
178
  result[modelField.name] = docField;
171
179
  return result;
172
180
  }, {});
@@ -14,7 +14,7 @@ export function mergeObjectWithDocument({
14
14
  duplicatableModels,
15
15
  nonDuplicatableModels
16
16
  }: {
17
- object: Record<string, any> | undefined;
17
+ object: Record<string, unknown> | undefined;
18
18
  document: ContentStoreTypes.Document;
19
19
  locale?: string;
20
20
  documentMap: Record<string, ContentStoreTypes.Document>;
@@ -22,7 +22,7 @@ export function mergeObjectWithDocument({
22
22
  referenceBehavior?: 'copyReference' | 'duplicateContents';
23
23
  duplicatableModels?: string[];
24
24
  nonDuplicatableModels?: string[];
25
- }): Record<string, any> {
25
+ }): Record<string, unknown> {
26
26
  return mergeObjectWithDocumentFields({
27
27
  object,
28
28
  documentFields: document.fields,
@@ -49,9 +49,9 @@ function mergeObjectWithDocumentFields({
49
49
  documentFields,
50
50
  ...context
51
51
  }: {
52
- object: Record<string, any> | undefined;
52
+ object: Record<string, unknown> | undefined;
53
53
  documentFields: Record<string, ContentStoreTypes.DocumentField>;
54
- } & Context): Record<string, any> {
54
+ } & Context): Record<string, unknown> {
55
55
  return _.reduce(
56
56
  documentFields,
57
57
  (object, documentField, fieldName) => {
@@ -74,9 +74,9 @@ function mergeObjectWithDocumentField({
74
74
  documentField,
75
75
  ...context
76
76
  }: {
77
- value: any;
77
+ value: unknown;
78
78
  documentField: ContentStoreTypes.DocumentField;
79
- } & Context): any {
79
+ } & Context): unknown {
80
80
  const locale = context.locale;
81
81
  switch (documentField.type) {
82
82
  case 'string':
@@ -108,7 +108,7 @@ function mergeObjectWithDocumentField({
108
108
  }
109
109
  case 'image': {
110
110
  const localizedField = getDocumentFieldForLocale(documentField, locale);
111
- if (localizedField && !localizedField.isUnset) {
111
+ if (localizedField && !localizedField.isUnset && isPlainObjectOrUndefined(value)) {
112
112
  return mergeObjectWithDocumentFields({
113
113
  object: value,
114
114
  documentFields: localizedField.fields,
@@ -119,7 +119,7 @@ function mergeObjectWithDocumentField({
119
119
  }
120
120
  case 'object': {
121
121
  const localizedField = getDocumentFieldForLocale(documentField, locale);
122
- if (localizedField && !localizedField.isUnset) {
122
+ if (localizedField && !localizedField.isUnset && isPlainObjectOrUndefined(value)) {
123
123
  return mergeObjectWithDocumentFields({
124
124
  object: value,
125
125
  documentFields: localizedField.fields,
@@ -130,7 +130,7 @@ function mergeObjectWithDocumentField({
130
130
  }
131
131
  case 'model': {
132
132
  const localizedField = getDocumentFieldForLocale(documentField, locale);
133
- if (localizedField && !localizedField.isUnset) {
133
+ if (localizedField && !localizedField.isUnset && isPlainObjectOrUndefined(value)) {
134
134
  if (value && value.$$type !== localizedField.srcModelName) {
135
135
  // if the override object's $$type isn't equal to the type
136
136
  // of the current object in the field, then use whatever
@@ -150,7 +150,7 @@ function mergeObjectWithDocumentField({
150
150
  }
151
151
  case 'reference': {
152
152
  const localizedField = getDocumentFieldForLocale(documentField, locale);
153
- if (localizedField && !localizedField.isUnset) {
153
+ if (localizedField && !localizedField.isUnset && isPlainObjectOrUndefined(value)) {
154
154
  if (value && value.$$ref) {
155
155
  // if the override object has $$ref, use it
156
156
  break;
@@ -230,6 +230,10 @@ function mergeObjectWithDocumentField({
230
230
  return value;
231
231
  }
232
232
 
233
+ function isPlainObjectOrUndefined(value: unknown): value is Record<string, unknown> | undefined {
234
+ return typeof value === 'undefined' || _.isPlainObject(value);
235
+ }
236
+
233
237
  function shouldDuplicate({
234
238
  referenceField,
235
239
  modelName,
@@ -5,6 +5,7 @@ import { getLocalizedFieldForLocale } from '@stackbit/types';
5
5
 
6
6
  import { SearchFilter, SearchFilterItem } from '../types/search-filter';
7
7
  import { ContentStoreTypes } from '..';
8
+ import { getContentSourceId } from '../content-store-utils';
8
9
 
9
10
  const META_FIELD = {
10
11
  createdAt: {
@@ -28,6 +29,7 @@ export const searchDocuments = (data: {
28
29
  documents: ContentStoreTypes.Document[];
29
30
  schema: Schema;
30
31
  locale?: string;
32
+ defaultLocales?: Record<string, string>
31
33
  }): {
32
34
  total: number;
33
35
  items: ContentStoreTypes.Document[];
@@ -39,6 +41,7 @@ export const searchDocuments = (data: {
39
41
  let allDocuments = 0;
40
42
 
41
43
  const matchedDocuments = documents.filter((document) => {
44
+ const contentSourceId = getContentSourceId(document.srcType, document.srcProjectId);
42
45
  const isIncludedModel = _.find(data.models, {
43
46
  srcType: document.srcType,
44
47
  srcProjectId: document.srcProjectId,
@@ -51,7 +54,7 @@ export const searchDocuments = (data: {
51
54
  allDocuments += 1;
52
55
 
53
56
  if (query) {
54
- const matches = isDocumentMatchesPattern(document, query, data.locale);
57
+ const matches = isDocumentMatchesPattern(document, query, data.locale ?? data.defaultLocales?.[contentSourceId]);
55
58
  if (!matches) {
56
59
  return false;
57
60
  }
@@ -61,7 +64,7 @@ export const searchDocuments = (data: {
61
64
  // only 'and' supported for now; later we can add e.g. 'or'
62
65
  const matches = data.filter.and.every((filter) => {
63
66
  const field = getFieldForFilter(document, filter);
64
- return isFieldMatchesFilter({ field, filter, document, schema, locale: data.locale });
67
+ return isFieldMatchesFilter({ field, filter, document, schema, locale: data.locale ?? data.defaultLocales?.[contentSourceId] });
65
68
  });
66
69
 
67
70
  if (!matches) {
@@ -22,16 +22,19 @@ function toLocalizedAPIFields(docFields: Record<string, ContentStoreTypes.Docume
22
22
 
23
23
  function toLocalizedAPIField(docField: ContentStoreTypes.DocumentField, locale?: string, isListItem = false): ContentStoreTypes.DocumentFieldAPI {
24
24
  type ToBoolean<T extends boolean | undefined> = T extends true ? true : false;
25
- function localeFields<T extends boolean | undefined>(localized: T): null | { localized: ToBoolean<T>; locale?: string } {
25
+ function localeFields<T extends boolean | undefined>(localized: T): null | { localized: false } | { localized: true; locale: string } {
26
26
  const isLocalized = !!localized as ToBoolean<T>;
27
27
  if (isListItem) {
28
28
  return null;
29
29
  }
30
30
  if (!isLocalized) {
31
31
  return {
32
- localized: isLocalized
32
+ localized: false
33
33
  };
34
34
  }
35
+ if (!locale) {
36
+ return null;
37
+ }
35
38
  return {
36
39
  localized: isLocalized,
37
40
  locale: locale
@@ -239,20 +242,23 @@ function localizeAssetFields(assetFields: ContentStoreTypes.AssetFields, locale?
239
242
  }
240
243
 
241
244
  export function mapStoreAssetsToAPIAssets(assets: ContentStoreTypes.Asset[], locale?: string): ContentStoreTypes.APIAsset[] {
242
- return assets.map((asset) => storeAssetToAPIAsset(asset, locale));
245
+ return assets.map((asset) => storeAssetToAPIAsset(asset, locale)).filter((asset): asset is ContentStoreTypes.APIAsset => !!asset);
243
246
  }
244
247
 
245
- function storeAssetToAPIAsset(asset: ContentStoreTypes.Asset, locale?: string): ContentStoreTypes.APIAsset {
248
+ function storeAssetToAPIAsset(asset: ContentStoreTypes.Asset, locale?: string): ContentStoreTypes.APIAsset | null {
246
249
  const assetTitleField = asset.fields.title;
247
- const localizedTitleField = assetTitleField.localized ? assetTitleField.locales?.[locale!]! : assetTitleField;
250
+ const localizedTitleField = assetTitleField.localized ? assetTitleField.locales?.[locale!] : assetTitleField;
248
251
  const assetFileField = asset.fields.file;
249
- const localizedFileField = assetFileField.localized ? assetFileField.locales?.[locale!]! : assetFileField;
252
+ const localizedFileField = assetFileField.localized ? assetFileField.locales?.[locale!] : assetFileField;
253
+ if (!localizedFileField) {
254
+ return null;
255
+ }
250
256
  return {
251
257
  objectId: asset.srcObjectId,
252
258
  createdAt: asset.createdAt,
253
259
  url: localizedFileField.url,
254
260
  ...omitByNil({
255
- title: localizedTitleField.value,
261
+ title: localizedTitleField?.value ?? null,
256
262
  fileName: localizedFileField.fileName,
257
263
  contentType: localizedFileField.contentType,
258
264
  size: localizedFileField.size,