@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/dist/content-store-types.d.ts +6 -1
- package/dist/content-store-types.d.ts.map +1 -1
- package/dist/content-store-utils.js +1 -1
- package/dist/content-store-utils.js.map +1 -1
- package/dist/content-store.d.ts +3 -3
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +124 -50
- package/dist/content-store.js.map +1 -1
- package/dist/utils/csi-to-store-docs-converter.js +9 -1
- package/dist/utils/csi-to-store-docs-converter.js.map +1 -1
- package/dist/utils/duplicate-document.d.ts +2 -2
- package/dist/utils/duplicate-document.d.ts.map +1 -1
- package/dist/utils/duplicate-document.js +7 -4
- package/dist/utils/duplicate-document.js.map +1 -1
- package/dist/utils/search-utils.d.ts +1 -0
- package/dist/utils/search-utils.d.ts.map +1 -1
- package/dist/utils/search-utils.js +7 -4
- package/dist/utils/search-utils.js.map +1 -1
- package/dist/utils/store-to-api-docs-converter.d.ts.map +1 -1
- package/dist/utils/store-to-api-docs-converter.js +12 -6
- package/dist/utils/store-to-api-docs-converter.js.map +1 -1
- package/package.json +5 -5
- package/src/content-store-types.ts +1 -1
- package/src/content-store-utils.ts +1 -1
- package/src/content-store.ts +133 -47
- package/src/utils/csi-to-store-docs-converter.ts +9 -1
- package/src/utils/duplicate-document.ts +14 -10
- package/src/utils/search-utils.ts +5 -2
- package/src/utils/store-to-api-docs-converter.ts +13 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackbit/cms-core",
|
|
3
|
-
"version": "0.1.12
|
|
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
|
|
33
|
-
"@stackbit/types": "^0.1.6
|
|
34
|
-
"@stackbit/utils": "^0.2.
|
|
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": "
|
|
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
|
}
|
package/src/content-store.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
291
|
-
const configModelsResult = mergeConfigModelsWithModelsFromFiles(
|
|
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:
|
|
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({
|
|
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
|
-
|
|
851
|
-
|
|
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
|
|
875
|
-
|
|
876
|
-
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
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[] =
|
|
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
|
-
|
|
1502
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
52
|
+
object: Record<string, unknown> | undefined;
|
|
53
53
|
documentFields: Record<string, ContentStoreTypes.DocumentField>;
|
|
54
|
-
} & Context): Record<string,
|
|
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:
|
|
77
|
+
value: unknown;
|
|
78
78
|
documentField: ContentStoreTypes.DocumentField;
|
|
79
|
-
} & Context):
|
|
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:
|
|
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:
|
|
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!]
|
|
250
|
+
const localizedTitleField = assetTitleField.localized ? assetTitleField.locales?.[locale!] : assetTitleField;
|
|
248
251
|
const assetFileField = asset.fields.file;
|
|
249
|
-
const localizedFileField = assetFileField.localized ? assetFileField.locales?.[locale!]
|
|
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
|
|
261
|
+
title: localizedTitleField?.value ?? null,
|
|
256
262
|
fileName: localizedFileField.fileName,
|
|
257
263
|
contentType: localizedFileField.contentType,
|
|
258
264
|
size: localizedFileField.size,
|