@stackbit/sdk 0.3.5 → 0.3.7
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/config/config-loader-static.js +1 -1
- package/dist/config/config-loader-static.js.map +1 -1
- package/dist/config/config-loader-utils.d.ts +12 -7
- package/dist/config/config-loader-utils.d.ts.map +1 -1
- package/dist/config/config-loader-utils.js +104 -68
- package/dist/config/config-loader-utils.js.map +1 -1
- package/dist/config/config-loader.d.ts +46 -27
- package/dist/config/config-loader.d.ts.map +1 -1
- package/dist/config/config-loader.js +294 -265
- package/dist/config/config-loader.js.map +1 -1
- package/dist/config/config-schema.d.ts +2 -2
- package/dist/config/config-schema.d.ts.map +1 -1
- package/dist/config/config-schema.js +123 -55
- package/dist/config/config-schema.js.map +1 -1
- package/dist/config/config-types.d.ts +4 -3
- package/dist/config/config-types.d.ts.map +1 -1
- package/dist/config/config-validator.d.ts +9 -5
- package/dist/config/config-validator.d.ts.map +1 -1
- package/dist/config/config-validator.js +42 -23
- package/dist/config/config-validator.js.map +1 -1
- package/dist/config/presets-loader.d.ts +13 -4
- package/dist/config/presets-loader.d.ts.map +1 -1
- package/dist/config/presets-loader.js +49 -23
- package/dist/config/presets-loader.js.map +1 -1
- package/dist/content/content-schema.js +1 -1
- package/dist/content/content-schema.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -8
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/model-extender.js +1 -1
- package/dist/utils/model-extender.js.map +1 -1
- package/dist/utils/model-iterators.d.ts +3 -2
- package/dist/utils/model-iterators.d.ts.map +1 -1
- package/dist/utils/model-iterators.js.map +1 -1
- package/dist/utils/model-utils.d.ts +9 -8
- package/dist/utils/model-utils.d.ts.map +1 -1
- package/dist/utils/model-utils.js +26 -9
- package/dist/utils/model-utils.js.map +1 -1
- package/package.json +4 -4
- package/src/config/config-loader-static.ts +1 -1
- package/src/config/config-loader-utils.ts +111 -78
- package/src/config/config-loader.ts +457 -394
- package/src/config/config-schema.ts +150 -81
- package/src/config/config-types.ts +6 -3
- package/src/config/config-validator.ts +51 -29
- package/src/config/presets-loader.ts +59 -30
- package/src/content/content-schema.ts +1 -1
- package/src/index.ts +21 -2
- package/src/utils/index.ts +1 -13
- package/src/utils/model-extender.ts +1 -1
- package/src/utils/model-iterators.ts +6 -5
- package/src/utils/model-utils.ts +38 -16
|
@@ -2,7 +2,6 @@ import Joi from 'joi';
|
|
|
2
2
|
import _ from 'lodash';
|
|
3
3
|
import { append } from '@stackbit/utils';
|
|
4
4
|
import {
|
|
5
|
-
StackbitConfig,
|
|
6
5
|
ContentfulImport,
|
|
7
6
|
SanityImport,
|
|
8
7
|
Assets,
|
|
@@ -16,20 +15,21 @@ import {
|
|
|
16
15
|
ModelsSourceSanity,
|
|
17
16
|
Field,
|
|
18
17
|
FieldObjectProps,
|
|
19
|
-
FieldGroupItem
|
|
18
|
+
FieldGroupItem,
|
|
19
|
+
FieldSpecificProps
|
|
20
20
|
} from '@stackbit/types';
|
|
21
21
|
|
|
22
22
|
import { CMS_NAMES, FIELD_TYPES, SSG_NAMES } from './config-consts';
|
|
23
23
|
import { styleFieldPartialSchema } from './config-schema/style-field-schema';
|
|
24
|
-
import {
|
|
24
|
+
import { Config, Model, PageModel, ObjectModel, DataModel, ConfigModel } from './config-types';
|
|
25
25
|
|
|
26
|
-
function getConfigFromValidationState(state: Joi.State):
|
|
26
|
+
function getConfigFromValidationState(state: Joi.State): Config {
|
|
27
27
|
return _.last(state.ancestors)!;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function getModelsFromValidationState(state: Joi.State):
|
|
30
|
+
function getModelsFromValidationState(state: Joi.State): Model[] {
|
|
31
31
|
const config = getConfigFromValidationState(state);
|
|
32
|
-
return config.models ??
|
|
32
|
+
return config.models ?? [];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const fieldNamePattern = /^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
|
|
@@ -47,8 +47,7 @@ const fieldNameSchema = Joi.string()
|
|
|
47
47
|
const objectModelNameErrorCode = 'model.not.object.model';
|
|
48
48
|
const validObjectModelNames = Joi.custom((value, { error, state }) => {
|
|
49
49
|
const models = getModelsFromValidationState(state);
|
|
50
|
-
const
|
|
51
|
-
const objectModelNames = modelNames.filter((modelName) => models[modelName]!.type === 'object');
|
|
50
|
+
const objectModelNames = models.filter((model) => model.type === 'object').map((model) => model.name);
|
|
52
51
|
if (!objectModelNames.includes(value)) {
|
|
53
52
|
return error(objectModelNameErrorCode);
|
|
54
53
|
}
|
|
@@ -63,9 +62,8 @@ const validObjectModelNames = Joi.custom((value, { error, state }) => {
|
|
|
63
62
|
const documentModelNameErrorCode = 'model.not.document.model';
|
|
64
63
|
const validReferenceModelNames = Joi.custom((value, { error, state }) => {
|
|
65
64
|
const models = getModelsFromValidationState(state);
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if (!documentModels.includes(value)) {
|
|
65
|
+
const documentModelNames = models.filter((model) => ['page', 'data'].includes(model.type)).map((model) => model.name);
|
|
66
|
+
if (!documentModelNames.includes(value)) {
|
|
69
67
|
return error(documentModelNameErrorCode);
|
|
70
68
|
}
|
|
71
69
|
return value;
|
|
@@ -123,16 +121,16 @@ const validReferenceFieldGroups = Joi.string()
|
|
|
123
121
|
errors: { wrap: { label: false } }
|
|
124
122
|
});
|
|
125
123
|
|
|
126
|
-
function getModelNamesForGroup(group: string, config:
|
|
127
|
-
const models = config.models ??
|
|
124
|
+
function getModelNamesForGroup(group: string, config: Config) {
|
|
125
|
+
const models = config.models ?? [];
|
|
128
126
|
return _.reduce(
|
|
129
127
|
models,
|
|
130
|
-
(result: { objectModels: string[]; documentModels: string[] }, model
|
|
128
|
+
(result: { objectModels: string[]; documentModels: string[] }, model) => {
|
|
131
129
|
if (model?.groups && _.includes(model.groups, group)) {
|
|
132
130
|
if (model?.type === 'object') {
|
|
133
|
-
result.objectModels.push(
|
|
131
|
+
result.objectModels.push(model.name);
|
|
134
132
|
} else {
|
|
135
|
-
result.documentModels.push(
|
|
133
|
+
result.documentModels.push(model.name);
|
|
136
134
|
}
|
|
137
135
|
}
|
|
138
136
|
return result;
|
|
@@ -150,7 +148,7 @@ const logicField = Joi.string();
|
|
|
150
148
|
const labelFieldNotFoundError = 'labelField.not.found';
|
|
151
149
|
const labelFieldNotSimple = 'labelField.not.simple';
|
|
152
150
|
const labelFieldSchema = Joi.custom((value, { error, state }) => {
|
|
153
|
-
const modelOrObjectField:
|
|
151
|
+
const modelOrObjectField: Model | FieldObjectProps = _.head(state.ancestors)!;
|
|
154
152
|
const fields = modelOrObjectField?.fields ?? [];
|
|
155
153
|
if (!_.isArray(fields)) {
|
|
156
154
|
return error(labelFieldNotFoundError);
|
|
@@ -174,7 +172,7 @@ const labelFieldSchema = Joi.custom((value, { error, state }) => {
|
|
|
174
172
|
const variantFieldNotFoundError = 'variantField.not.found';
|
|
175
173
|
const variantFieldNotEnum = 'variantField.not.enum';
|
|
176
174
|
const variantFieldSchema = Joi.custom((value, { error, state }) => {
|
|
177
|
-
const modelOrObjectField:
|
|
175
|
+
const modelOrObjectField: Model | FieldObjectProps = _.head(state.ancestors)!;
|
|
178
176
|
const fields = modelOrObjectField?.fields ?? [];
|
|
179
177
|
if (!_.isArray(fields)) {
|
|
180
178
|
return error(variantFieldNotFoundError);
|
|
@@ -206,11 +204,11 @@ const styleObjectModelNameSchema = Joi.string()
|
|
|
206
204
|
return value;
|
|
207
205
|
}
|
|
208
206
|
const models = getModelsFromValidationState(state);
|
|
209
|
-
const
|
|
210
|
-
if (!
|
|
207
|
+
const styleObjectModel = models.find((model) => model.name === value);
|
|
208
|
+
if (!styleObjectModel) {
|
|
211
209
|
return error(styleObjectModelReferenceError);
|
|
212
210
|
}
|
|
213
|
-
if (
|
|
211
|
+
if (styleObjectModel.type !== 'data') {
|
|
214
212
|
return error(styleObjectModelNotObject);
|
|
215
213
|
}
|
|
216
214
|
return value;
|
|
@@ -484,6 +482,7 @@ const fieldsSchema = Joi.array().items(fieldSchema).unique('name').id('fieldsSch
|
|
|
484
482
|
const contentModelKeyNotFound = 'contentModel.model.not.found';
|
|
485
483
|
const contentModelTypeNotPage = 'contentModel.type.not.page';
|
|
486
484
|
const contentModelTypeNotData = 'contentModel.type.not.data';
|
|
485
|
+
|
|
487
486
|
const contentModelSchema = Joi.object<ContentModel>({
|
|
488
487
|
isPage: Joi.boolean(),
|
|
489
488
|
newFilePath: Joi.string(),
|
|
@@ -502,12 +501,12 @@ const contentModelSchema = Joi.object<ContentModel>({
|
|
|
502
501
|
})
|
|
503
502
|
})
|
|
504
503
|
.custom((contentModel, { error, state, prefs }) => {
|
|
505
|
-
const
|
|
506
|
-
if (!
|
|
504
|
+
const modelMap = _.get(prefs, 'context.modelMap');
|
|
505
|
+
if (!modelMap) {
|
|
507
506
|
return contentModel;
|
|
508
507
|
}
|
|
509
508
|
const modelName = _.last(state.path)!;
|
|
510
|
-
const model =
|
|
509
|
+
const model = modelMap[modelName];
|
|
511
510
|
if (!model) {
|
|
512
511
|
return error(contentModelKeyNotFound, { modelName });
|
|
513
512
|
} else if (contentModel.isPage && model.type && !['page', 'object'].includes(model.type)) {
|
|
@@ -536,10 +535,25 @@ export const contentModelsSchema = Joi.object({
|
|
|
536
535
|
contentModels: Joi.object<ContentModelMap>().pattern(Joi.string(), contentModelSchema)
|
|
537
536
|
});
|
|
538
537
|
|
|
539
|
-
const
|
|
538
|
+
const modelNamePattern = /^[a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])?$/;
|
|
539
|
+
const modelNameError =
|
|
540
|
+
'Invalid model name "{{#value}}" at "{{#label}}". A model name must contain only alphanumeric characters ' +
|
|
541
|
+
'and underscores, must start with a letter, and end with alphanumeric character.';
|
|
542
|
+
const modelNameSchema = Joi.string()
|
|
543
|
+
.required()
|
|
544
|
+
.pattern(modelNamePattern)
|
|
545
|
+
.prefs({
|
|
546
|
+
messages: { 'string.pattern.base': modelNameError },
|
|
547
|
+
errors: { wrap: { label: false } }
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const baseModelSchema = Joi.object<Model & { srcType: string; srcProjectId: string }>({
|
|
540
551
|
__metadata: Joi.object({
|
|
541
552
|
filePath: Joi.string()
|
|
542
553
|
}),
|
|
554
|
+
name: modelNameSchema,
|
|
555
|
+
srcType: Joi.string(),
|
|
556
|
+
srcProjectId: Joi.string(),
|
|
543
557
|
type: Joi.string().valid('page', 'data', 'config', 'object').required(),
|
|
544
558
|
label: Joi.string(),
|
|
545
559
|
description: Joi.string(),
|
|
@@ -553,13 +567,13 @@ const baseModelSchema = Joi.object<YamlBaseModel>({
|
|
|
553
567
|
fields: Joi.link('#fieldsSchema')
|
|
554
568
|
});
|
|
555
569
|
|
|
556
|
-
const objectModelSchema: Joi.ObjectSchema<
|
|
570
|
+
const objectModelSchema: Joi.ObjectSchema<ObjectModel> = baseModelSchema.concat(
|
|
557
571
|
Joi.object({
|
|
558
572
|
type: Joi.string().valid('object').required()
|
|
559
573
|
})
|
|
560
574
|
);
|
|
561
575
|
|
|
562
|
-
const dataModelSchema: Joi.ObjectSchema<
|
|
576
|
+
const dataModelSchema: Joi.ObjectSchema<DataModel> = baseModelSchema
|
|
563
577
|
.concat(
|
|
564
578
|
Joi.object({
|
|
565
579
|
type: Joi.string().valid('data').required(),
|
|
@@ -588,14 +602,14 @@ const dataModelSchema: Joi.ObjectSchema<YamlDataModel> = baseModelSchema
|
|
|
588
602
|
})
|
|
589
603
|
});
|
|
590
604
|
|
|
591
|
-
const configModelSchema: Joi.ObjectSchema<
|
|
605
|
+
const configModelSchema: Joi.ObjectSchema<ConfigModel> = baseModelSchema.concat(
|
|
592
606
|
Joi.object({
|
|
593
607
|
type: Joi.string().valid('config').required(),
|
|
594
608
|
file: Joi.string()
|
|
595
609
|
})
|
|
596
610
|
);
|
|
597
611
|
|
|
598
|
-
const pageModelSchema: Joi.ObjectSchema<
|
|
612
|
+
const pageModelSchema: Joi.ObjectSchema<PageModel> = baseModelSchema
|
|
599
613
|
.concat(
|
|
600
614
|
Joi.object({
|
|
601
615
|
type: Joi.string().valid('page').required(),
|
|
@@ -621,35 +635,122 @@ const pageModelSchema: Joi.ObjectSchema<YamlPageModel> = baseModelSchema
|
|
|
621
635
|
})
|
|
622
636
|
.when('.singleInstance', { is: true, then: { file: Joi.required() } });
|
|
623
637
|
|
|
624
|
-
const modelSchema = Joi.object<YamlModel>({
|
|
625
|
-
type: Joi.string().valid('page', 'data', 'config', 'object').required()
|
|
626
|
-
}).when('.type', {
|
|
627
|
-
switch: [
|
|
628
|
-
{ is: 'object', then: objectModelSchema },
|
|
629
|
-
{ is: 'data', then: dataModelSchema },
|
|
630
|
-
{ is: 'config', then: configModelSchema },
|
|
631
|
-
{ is: 'page', then: pageModelSchema }
|
|
632
|
-
]
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
const modelNamePattern = /^[a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])?$/;
|
|
636
|
-
const modelNamePatternMatchErrorCode = 'model.name.pattern.match';
|
|
637
638
|
const modelFileExclusiveErrorCode = 'model.file.only';
|
|
638
639
|
const modelIsListItemsRequiredErrorCode = 'model.isList.items.required';
|
|
639
640
|
const modelIsListFieldsForbiddenErrorCode = 'model.isList.fields.forbidden';
|
|
640
641
|
const modelListForbiddenErrorCode = 'model.items.forbidden';
|
|
641
642
|
const fieldNameUnique = 'field.name.unique';
|
|
643
|
+
|
|
644
|
+
function errorLabelFromModelAndFieldPath(model: Model, modelIndex: number, fieldPath: (string | number | object)[]): string {
|
|
645
|
+
let fieldSpecificProps: Model | FieldSpecificProps | undefined = model;
|
|
646
|
+
let fields: Field[] | undefined;
|
|
647
|
+
let label = `models[${ model.name ? `name='${model.name}'` : modelIndex }]`;
|
|
648
|
+
|
|
649
|
+
for (const pathPart of fieldPath) {
|
|
650
|
+
if (typeof pathPart === 'string') {
|
|
651
|
+
if (pathPart === 'fields' && fieldSpecificProps && 'fields' in fieldSpecificProps) {
|
|
652
|
+
fields = fieldSpecificProps.fields;
|
|
653
|
+
fieldSpecificProps = undefined;
|
|
654
|
+
label += '.fields';
|
|
655
|
+
} else if (pathPart === 'items' && fieldSpecificProps && 'items' in fieldSpecificProps) {
|
|
656
|
+
fieldSpecificProps = fieldSpecificProps.items;
|
|
657
|
+
label += '.items';
|
|
658
|
+
} else {
|
|
659
|
+
fieldSpecificProps = undefined;
|
|
660
|
+
label += `.${pathPart}`;
|
|
661
|
+
}
|
|
662
|
+
} else if (fields && typeof pathPart === 'number') {
|
|
663
|
+
const field = fields[pathPart];
|
|
664
|
+
label += '[' + (field?.name ? `name='${field.name}'` : pathPart) + ']';
|
|
665
|
+
fieldSpecificProps = field;
|
|
666
|
+
fields = undefined;
|
|
667
|
+
} else {
|
|
668
|
+
// when the schema is marked as Joi.array().items(...).single()
|
|
669
|
+
// and the validated value is not an array, Joi injects (new Number(0))
|
|
670
|
+
// which is an object. Don't use it to generate path
|
|
671
|
+
if (typeof pathPart === 'object') {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
label += `[${pathPart}]`;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return label;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const modelSchema = Joi.object<Model>({
|
|
681
|
+
type: Joi.string().valid('page', 'data', 'config', 'object').required(),
|
|
682
|
+
name: Joi.string()
|
|
683
|
+
})
|
|
684
|
+
.when('.type', {
|
|
685
|
+
switch: [
|
|
686
|
+
{ is: 'object', then: objectModelSchema },
|
|
687
|
+
{ is: 'data', then: dataModelSchema },
|
|
688
|
+
{ is: 'config', then: configModelSchema },
|
|
689
|
+
{ is: 'page', then: pageModelSchema }
|
|
690
|
+
]
|
|
691
|
+
})
|
|
692
|
+
.error(((errors: Joi.ErrorReport[]): Joi.ErrorReport[] => {
|
|
693
|
+
return _.map(errors, (error) => {
|
|
694
|
+
if (error.path[0] === 'models' && typeof error.path[1] === 'number') {
|
|
695
|
+
const modelIndex = error.path[1];
|
|
696
|
+
const config = error.prefs.context?.config as Config;
|
|
697
|
+
if (config && config.models[modelIndex]) {
|
|
698
|
+
const model = config.models[modelIndex]!;
|
|
699
|
+
error.path = error.path.slice();
|
|
700
|
+
error.path.splice(1, 1, model.name);
|
|
701
|
+
const label = errorLabelFromModelAndFieldPath(model, modelIndex, error.path.slice(2));
|
|
702
|
+
_.set(error, 'local.label', label);
|
|
703
|
+
// const label = _.get(error, 'local.label', '') as string;
|
|
704
|
+
// _.set(error, 'local.label', label.replace(/models\[(\d+)]/, (match, indexMatch) => {
|
|
705
|
+
// if (Number(indexMatch) !== modelIndex) {
|
|
706
|
+
// return match;
|
|
707
|
+
// }
|
|
708
|
+
// return `models[name='${model.name}']`;
|
|
709
|
+
// }));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (
|
|
713
|
+
error.code === 'any.unknown' &&
|
|
714
|
+
error.path.length === 3 &&
|
|
715
|
+
error.path[0] === 'models' &&
|
|
716
|
+
error.path[2] &&
|
|
717
|
+
['folder', 'match', 'exclude'].includes(error.path[2])
|
|
718
|
+
) {
|
|
719
|
+
error.code = modelFileExclusiveErrorCode;
|
|
720
|
+
} else if (error.code === 'any.required' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'items') {
|
|
721
|
+
error.code = modelIsListItemsRequiredErrorCode;
|
|
722
|
+
} else if (error.code === 'any.unknown' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'fields') {
|
|
723
|
+
error.code = modelIsListFieldsForbiddenErrorCode;
|
|
724
|
+
} else if (error.code === 'object.unknown' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'items') {
|
|
725
|
+
error.code = modelListForbiddenErrorCode;
|
|
726
|
+
} else if (error.code === 'array.unique' && error.path.length > 3 && error.path[0] === 'models' && _.nth(error.path, -2) === 'fields') {
|
|
727
|
+
error.code = fieldNameUnique;
|
|
728
|
+
}
|
|
729
|
+
return error;
|
|
730
|
+
});
|
|
731
|
+
}) as any) // the type definition of Joi.ValidationErrorFunction is wrong, so we override
|
|
732
|
+
.prefs({
|
|
733
|
+
messages: {
|
|
734
|
+
[modelFileExclusiveErrorCode]: '{{#label}} cannot be used with "file"',
|
|
735
|
+
[modelIsListItemsRequiredErrorCode]: '{{#label}} is required when "isList" is true',
|
|
736
|
+
[modelIsListFieldsForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is true',
|
|
737
|
+
[modelListForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is not true',
|
|
738
|
+
[fieldNameUnique]: '{{#label}} contains a duplicate field name "{{#value.name}}"'
|
|
739
|
+
},
|
|
740
|
+
errors: { wrap: { label: false } }
|
|
741
|
+
});
|
|
742
|
+
|
|
642
743
|
const groupModelsIncompatibleError = 'group.models.incompatible';
|
|
643
744
|
|
|
644
|
-
const modelsSchema = Joi.
|
|
645
|
-
.
|
|
646
|
-
.custom((models:
|
|
745
|
+
const modelsSchema = Joi.array()
|
|
746
|
+
.items(modelSchema)
|
|
747
|
+
.custom((models: Model[], { error }) => {
|
|
647
748
|
const groupMap: Record<string, Record<'objectModels' | 'documentModels', string[]>> = {};
|
|
648
749
|
|
|
649
|
-
_.forEach(models, (model
|
|
750
|
+
_.forEach(models, (model) => {
|
|
650
751
|
const key = model?.type === 'object' ? 'objectModels' : 'documentModels';
|
|
651
752
|
_.forEach(model.groups, (groupName) => {
|
|
652
|
-
append(groupMap, [groupName, key],
|
|
753
|
+
append(groupMap, [groupName, key], model.name);
|
|
653
754
|
});
|
|
654
755
|
});
|
|
655
756
|
|
|
@@ -674,42 +775,10 @@ const modelsSchema = Joi.object<YamlModelMap>()
|
|
|
674
775
|
|
|
675
776
|
return models;
|
|
676
777
|
})
|
|
677
|
-
.error(((errors: Joi.ErrorReport[]): Joi.ErrorReport[] => {
|
|
678
|
-
return _.map(errors, (error) => {
|
|
679
|
-
if (error.code === 'object.unknown' && error.path.length === 2 && error.path[0] === 'models') {
|
|
680
|
-
error.code = modelNamePatternMatchErrorCode;
|
|
681
|
-
} else if (
|
|
682
|
-
error.code === 'any.unknown' &&
|
|
683
|
-
error.path.length === 3 &&
|
|
684
|
-
error.path[0] === 'models' &&
|
|
685
|
-
error.path[2] &&
|
|
686
|
-
['folder', 'match', 'exclude'].includes(error.path[2])
|
|
687
|
-
) {
|
|
688
|
-
error.code = modelFileExclusiveErrorCode;
|
|
689
|
-
} else if (error.code === 'any.required' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'items') {
|
|
690
|
-
error.code = modelIsListItemsRequiredErrorCode;
|
|
691
|
-
} else if (error.code === 'any.unknown' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'fields') {
|
|
692
|
-
error.code = modelIsListFieldsForbiddenErrorCode;
|
|
693
|
-
} else if (error.code === 'object.unknown' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'items') {
|
|
694
|
-
error.code = modelListForbiddenErrorCode;
|
|
695
|
-
} else if (error.code === 'array.unique' && error.path.length > 3 && error.path[0] === 'models' && _.nth(error.path, -2) === 'fields') {
|
|
696
|
-
error.code = fieldNameUnique;
|
|
697
|
-
}
|
|
698
|
-
return error;
|
|
699
|
-
});
|
|
700
|
-
}) as any) // the type definition of Joi.ValidationErrorFunction is wrong, so we override
|
|
701
778
|
.prefs({
|
|
702
779
|
messages: {
|
|
703
780
|
[groupModelsIncompatibleError]:
|
|
704
|
-
'Model groups must include models of the same type. The following groups have incompatible models: {{#incompatibleGroups}}'
|
|
705
|
-
[modelNamePatternMatchErrorCode]:
|
|
706
|
-
'Invalid model name "{{#key}}" at "{{#label}}". A model name must contain only alphanumeric characters ' +
|
|
707
|
-
'and underscores, must start with a letter, and end with alphanumeric character.',
|
|
708
|
-
[modelFileExclusiveErrorCode]: '{{#label}} cannot be used with "file"',
|
|
709
|
-
[modelIsListItemsRequiredErrorCode]: '{{#label}} is required when "isList" is true',
|
|
710
|
-
[modelIsListFieldsForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is true',
|
|
711
|
-
[modelListForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is not true',
|
|
712
|
-
[fieldNameUnique]: '{{#label}} contains a duplicate field name "{{#value.name}}"'
|
|
781
|
+
'Model groups must include models of the same type. The following groups have incompatible models: {{#incompatibleGroups}}'
|
|
713
782
|
},
|
|
714
783
|
errors: { wrap: { label: false } }
|
|
715
784
|
});
|
|
@@ -745,7 +814,7 @@ const sidebarButtonSchema = Joi.object({
|
|
|
745
814
|
]
|
|
746
815
|
});
|
|
747
816
|
|
|
748
|
-
export const stackbitConfigSchema = Joi.object<
|
|
817
|
+
export const stackbitConfigSchema = Joi.object<Config>({
|
|
749
818
|
stackbitVersion: Joi.string().required(),
|
|
750
819
|
ssgName: Joi.string().valid(...SSG_NAMES),
|
|
751
820
|
ssgVersion: Joi.string(),
|
|
@@ -64,11 +64,13 @@ export interface Config extends Omit<StackbitConfig, 'models' | 'customContentRe
|
|
|
64
64
|
dirPath: string;
|
|
65
65
|
filePath: string;
|
|
66
66
|
models: Model[];
|
|
67
|
-
presets?:
|
|
67
|
+
presets?: PresetMap;
|
|
68
68
|
internalStackbitRunnerOptions?: SSGRunOptions;
|
|
69
69
|
hcrHandled?: boolean;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
export type PresetMap = Record<string, Preset>;
|
|
73
|
+
|
|
72
74
|
export interface Preset {
|
|
73
75
|
label: string;
|
|
74
76
|
modelName: string;
|
|
@@ -91,12 +93,13 @@ export interface SSGRunOptions {
|
|
|
91
93
|
*** Normalized Model Types ***
|
|
92
94
|
******************************/
|
|
93
95
|
|
|
94
|
-
export type Model = ObjectModel | DataModel | PageModel | ConfigModel
|
|
96
|
+
export type Model = ObjectModel | DataModel | PageModel | ConfigModel;
|
|
95
97
|
|
|
96
98
|
export type ObjectModel = StackbitConfigObjectModel & BaseModel;
|
|
97
99
|
export type DataModel = DataModelSingle | DataModelList;
|
|
98
100
|
export type PageModel = StackbitConfigPageModel & BaseModel;
|
|
99
101
|
export type ConfigModel = StackbitConfigConfigModel & BaseModel;
|
|
102
|
+
|
|
100
103
|
export type ImageModel = {
|
|
101
104
|
type: 'image';
|
|
102
105
|
name: '__image_model';
|
|
@@ -115,7 +118,7 @@ export interface DataModelList extends StackbitConfigDataModel, BaseModel {
|
|
|
115
118
|
items: FieldListItems;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
type BaseModel = ModelMetadata & { presets?: string[] };
|
|
121
|
+
export type BaseModel = ModelMetadata & { presets?: string[] };
|
|
119
122
|
|
|
120
123
|
interface ModelMetadata {
|
|
121
124
|
__metadata?: {
|
|
@@ -2,47 +2,67 @@ import _ from 'lodash';
|
|
|
2
2
|
import Joi from 'joi';
|
|
3
3
|
import { ContentModelMap } from '@stackbit/types';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { contentModelsSchema, stackbitConfigSchema } from './config-schema';
|
|
6
6
|
import { ConfigValidationError } from './config-errors';
|
|
7
|
-
import {
|
|
8
|
-
import { YamlModelMap } from './config-types';
|
|
7
|
+
import { Config, Model } from './config-types';
|
|
9
8
|
|
|
10
9
|
export interface ConfigValidationResult {
|
|
11
|
-
|
|
10
|
+
config: Config;
|
|
12
11
|
valid: boolean;
|
|
13
12
|
errors: ConfigValidationError[];
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
export function validateConfig(config:
|
|
17
|
-
const validationOptions = { abortEarly: false };
|
|
15
|
+
export function validateConfig(config: Config): ConfigValidationResult {
|
|
16
|
+
const validationOptions = { abortEarly: false, context: { config: config } };
|
|
18
17
|
const validationResult = stackbitConfigSchema.validate(config, validationOptions);
|
|
19
|
-
const
|
|
18
|
+
const validatedConfig: Config = validationResult.value;
|
|
20
19
|
const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
|
|
21
20
|
const valid = _.isEmpty(errors);
|
|
22
|
-
|
|
21
|
+
const invalidModelNames = getInvalidModelNames(errors, 'models', config);
|
|
22
|
+
const validatedModels = validatedConfig.models ?? [];
|
|
23
|
+
_.forEach(validatedModels, (model): any => {
|
|
24
|
+
if (invalidModelNames.includes(model.name)) {
|
|
25
|
+
_.set(model, '__metadata.invalid', true);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
23
28
|
return {
|
|
24
|
-
|
|
29
|
+
config: validatedConfig,
|
|
25
30
|
valid,
|
|
26
31
|
errors
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
export
|
|
35
|
+
export interface ContentModelsValidationResult {
|
|
36
|
+
contentModels: ContentModelMap;
|
|
37
|
+
valid: boolean;
|
|
38
|
+
errors: ConfigValidationError[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateContentModels(contentModels: ContentModelMap, models: Model[]): ContentModelsValidationResult {
|
|
42
|
+
const modelMap: Record<string, Model> = _.keyBy(models, 'name');
|
|
43
|
+
const config = { contentModels: contentModels };
|
|
31
44
|
const validationResult = contentModelsSchema.validate(
|
|
32
|
-
|
|
45
|
+
config,
|
|
33
46
|
{
|
|
34
47
|
abortEarly: false,
|
|
35
48
|
context: {
|
|
36
|
-
|
|
49
|
+
modelMap: modelMap
|
|
37
50
|
}
|
|
38
51
|
}
|
|
39
52
|
);
|
|
40
|
-
const
|
|
53
|
+
const validatedConfig: Config = validationResult.value;
|
|
41
54
|
const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
|
|
42
55
|
const valid = _.isEmpty(errors);
|
|
43
|
-
|
|
56
|
+
const invalidModelNames = getInvalidModelNames(errors, 'contentModels', config);
|
|
57
|
+
const validatedContentModels = validatedConfig.contentModels ?? {};
|
|
58
|
+
_.forEach(validatedContentModels, (contentModel, modelName) => {
|
|
59
|
+
if (invalidModelNames.includes(modelName)) {
|
|
60
|
+
_.set(contentModel, '__metadata.invalid', true);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
44
64
|
return {
|
|
45
|
-
|
|
65
|
+
contentModels: validatedContentModels,
|
|
46
66
|
valid,
|
|
47
67
|
errors
|
|
48
68
|
};
|
|
@@ -52,35 +72,37 @@ function mapJoiErrorsToConfigValidationErrors(validationResult: Joi.ValidationRe
|
|
|
52
72
|
const joiErrors = validationResult.error?.details || [];
|
|
53
73
|
return joiErrors.map(
|
|
54
74
|
(validationError): ConfigValidationError => {
|
|
75
|
+
const normFieldPath = validationError.path.slice();
|
|
76
|
+
if (validationError.path[0] === 'models' && typeof validationError.path[1] == 'string') {
|
|
77
|
+
const modelName = validationError.path[1];
|
|
78
|
+
normFieldPath[1] = _.findIndex(validationResult.value.models, { name: modelName });
|
|
79
|
+
}
|
|
55
80
|
return new ConfigValidationError({
|
|
56
81
|
type: validationError.type,
|
|
57
82
|
message: validationError.message,
|
|
58
83
|
fieldPath: validationError.path,
|
|
84
|
+
normFieldPath: normFieldPath,
|
|
59
85
|
value: validationError.context?.value
|
|
60
86
|
});
|
|
61
87
|
}
|
|
62
88
|
);
|
|
63
89
|
}
|
|
64
90
|
|
|
65
|
-
function
|
|
66
|
-
const invalidModelNames = getInvalidModelNames(errors, configKey);
|
|
67
|
-
const models = config[configKey] ?? {};
|
|
68
|
-
_.forEach(models, (model: any, modelName: string): any => {
|
|
69
|
-
if (invalidModelNames.includes(modelName)) {
|
|
70
|
-
_.set(model, '__metadata.invalid', true);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function getInvalidModelNames(errors: ConfigValidationError[], configKey: string) {
|
|
91
|
+
function getInvalidModelNames(errors: ConfigValidationError[], configKey: string, value: any) {
|
|
76
92
|
// get array of invalid model names by iterating errors and filtering these
|
|
77
93
|
// having fieldPath starting with ['models', modelName]
|
|
78
94
|
return _.reduce(
|
|
79
95
|
errors,
|
|
80
96
|
(modelNames: string[], error: ConfigValidationError) => {
|
|
81
|
-
if (error.fieldPath[0] === configKey
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
if (error.fieldPath[0] === configKey) {
|
|
98
|
+
if (typeof error.fieldPath[1] == 'string') {
|
|
99
|
+
modelNames.push(error.fieldPath[1]);
|
|
100
|
+
} else if (typeof error.fieldPath[1] == 'number') {
|
|
101
|
+
const model = value[configKey][error.fieldPath[1]];
|
|
102
|
+
if (model?.name) {
|
|
103
|
+
modelNames.push(model?.name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
84
106
|
}
|
|
85
107
|
return modelNames;
|
|
86
108
|
},
|