@stackbit/sdk
Version:
122 lines (112 loc) • 4.67 kB
text/typescript
import _ from 'lodash';
import Joi from 'joi';
import { ContentModelMap, StackbitConfig } from '@stackbit/types';
import { contentModelsSchema, stackbitConfigBaseSchema, stackbitConfigFullSchema } from './config-schema';
import { ConfigValidationError } from './config-errors';
import { Config, Model } from './config-types';
export interface ConfigValidationResult {
config: Config;
valid: boolean;
errors: ConfigValidationError[];
}
export function validateBaseConfig(config: StackbitConfig): {
config: StackbitConfig;
valid: boolean;
errors: ConfigValidationError[];
} {
const validationOptions = { abortEarly: false, context: { config: config } };
const validationResult = stackbitConfigBaseSchema.validate(config, validationOptions);
const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
return {
config: validationResult.value,
valid: _.isEmpty(errors),
errors
};
}
export function validateConfig(config: Config): ConfigValidationResult {
const validationOptions = { abortEarly: false, context: { config: config } };
const validationResult = stackbitConfigFullSchema.validate(config, validationOptions);
const validatedConfig: Config = validationResult.value;
const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
const valid = _.isEmpty(errors);
const invalidModelNames = getInvalidModelNames(errors, 'models', config);
const validatedModels = validatedConfig.models ?? [];
_.forEach(validatedModels, (model): any => {
if (invalidModelNames.includes(model.name)) {
_.set(model, '__metadata.invalid', true);
}
});
return {
config: validatedConfig,
valid,
errors
};
}
export interface ContentModelsValidationResult {
contentModels: ContentModelMap;
valid: boolean;
errors: ConfigValidationError[];
}
export function validateContentModels(contentModels: ContentModelMap, models: Model[]): ContentModelsValidationResult {
const modelMap: Record<string, Model> = _.keyBy(models, 'name');
const config = { contentModels: contentModels };
const validationResult = contentModelsSchema.validate(config, {
abortEarly: false,
context: {
modelMap: modelMap
}
});
const validatedConfig: Config = validationResult.value;
const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
const valid = _.isEmpty(errors);
const invalidModelNames = getInvalidModelNames(errors, 'contentModels', config);
const validatedContentModels = validatedConfig.contentModels ?? {};
_.forEach(validatedContentModels, (contentModel, modelName) => {
if (invalidModelNames.includes(modelName)) {
_.set(contentModel, '__metadata.invalid', true);
}
});
return {
contentModels: validatedContentModels,
valid,
errors
};
}
function mapJoiErrorsToConfigValidationErrors(validationResult: Joi.ValidationResult): ConfigValidationError[] {
const joiErrors = validationResult.error?.details || [];
return joiErrors.map((validationError): ConfigValidationError => {
const normFieldPath = validationError.path.slice();
if (validationError.path[0] === 'models' && typeof validationError.path[1] == 'string') {
const modelName = validationError.path[1];
normFieldPath[1] = _.findIndex(validationResult.value.models, { name: modelName });
}
return new ConfigValidationError({
type: validationError.type,
message: validationError.message,
fieldPath: validationError.path,
normFieldPath: normFieldPath,
value: validationError.context?.value
});
});
}
function getInvalidModelNames(errors: ConfigValidationError[], configKey: string, value: any) {
// get array of invalid model names by iterating errors and filtering these
// having fieldPath starting with ['models', modelName]
return _.reduce(
errors,
(modelNames: string[], error: ConfigValidationError) => {
if (error.fieldPath[0] === configKey) {
if (typeof error.fieldPath[1] == 'string') {
modelNames.push(error.fieldPath[1]);
} else if (typeof error.fieldPath[1] == 'number') {
const model = value[configKey][error.fieldPath[1]];
if (model?.name) {
modelNames.push(model?.name);
}
}
}
return modelNames;
},
[]
);
}