@stackbit/sdk
Version:
449 lines • 18.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.joiSchemaForModel = exports.joiSchemasForModels = void 0;
const joi_1 = __importDefault(require("joi"));
const lodash_1 = __importDefault(require("lodash"));
const utils_1 = require("@stackbit/utils");
const config_consts_1 = require("../config/config-consts");
const utils_2 = require("../utils");
const metadataSchema = joi_1.default.object({
modelName: joi_1.default.string().allow(null),
filePath: joi_1.default.string(),
error: joi_1.default.string()
});
function joiSchemasForModels(config) {
const modelSchemas = lodash_1.default.reduce(config.models, (modelSchemas, model) => {
let joiSchema;
if (model.__metadata?.invalid) {
// if root model is invalid, replace the label with "file" otherwise joi outputs "value" which is not descriptive
let objectLabel = '{{#label}}';
if ((0, utils_2.isDataModel)(model) || (0, utils_2.isPageModel)(model)) {
objectLabel = 'file';
}
joiSchema = joi_1.default.object()
.forbidden()
.messages({
'any.unknown': `${objectLabel} cannot be validated, the model "${model.name}" is invalid. Fix the model to validate the content.`
});
}
else {
joiSchema = joiSchemaForModel(model, config);
}
modelSchemas[model.name] = joiSchema.id(`${model.name}_model_schema`);
return modelSchemas;
}, {});
// Allow linking between recursive schemas by calling shared() on every schema with every other schema
// https://joi.dev/api/?v=17.4.0#anysharedschema
// Example: given three schemas: pageSchema, sectionSchema, authorSchema
// pageSchema.shared(sectionSchema).shared(authorSchema)
// sectionSchema.shared(pageSchema).shared(authorSchema)
// authorSchema.shared(pageSchema).shared(sectionSchema)
// Future optimization - no need to link between all schemas, but only these that have internal links
return lodash_1.default.reduce(modelSchemas, (accum, modelSchema, modelName) => {
const otherModelSchemas = lodash_1.default.omit(modelSchemas, modelName);
accum[modelName] = lodash_1.default.reduce(otherModelSchemas, (modelSchema, otherModelSchema) => {
return modelSchema.shared(otherModelSchema);
}, modelSchema);
return accum;
}, {});
}
exports.joiSchemasForModels = joiSchemasForModels;
function joiSchemaForModel(model, config) {
if ((0, utils_2.isDataModel)(model) && model.isList) {
return joi_1.default.object({
items: joi_1.default.array().items(joiSchemaForField(model.items, config, [model.name, 'items']))
});
}
else {
return joiSchemaForModelFields(model.fields, config, [model.name]);
}
}
exports.joiSchemaForModel = joiSchemaForModel;
function joiSchemaForModelFields(fields, config, fieldPath) {
return joi_1.default.object(lodash_1.default.reduce(fields, (schema, field) => {
const childFieldPath = fieldPath.concat(`[name='${field.name}']`);
schema[field.name] = joiSchemaForField(field, config, childFieldPath);
return schema;
}, {}));
}
function assertUnreachable(field) {
throw new Error('Unhandled field.type case');
}
function joiSchemaForField(field, config, fieldPath) {
let fieldSchema;
switch (field.type) {
case 'string':
case 'url':
case 'slug':
case 'text':
case 'markdown':
case 'html':
case 'file':
case 'color':
fieldSchema = joi_1.default.string().allow('', null);
break;
case 'image':
fieldSchema = joi_1.default.alternatives([joi_1.default.string().allow('', null), joi_1.default.object()]);
break;
case 'boolean':
fieldSchema = joi_1.default.boolean();
break;
case 'date':
case 'datetime':
fieldSchema = joi_1.default.date();
break;
case 'enum':
fieldSchema = enumFieldValueSchema(field);
break;
case 'number':
fieldSchema = numberFieldValueSchema(field);
break;
case 'object':
fieldSchema = objectFieldValueSchema(field, config, fieldPath);
break;
case 'model':
fieldSchema = modelFieldValueSchema(field, config);
break;
case 'reference':
fieldSchema = referenceFieldValueSchema(field);
break;
case 'cross-reference':
fieldSchema = crossReferenceFieldValueSchema(field);
break;
case 'style':
fieldSchema = styleFieldValueSchema(field);
break;
case 'list':
fieldSchema = listFieldValueSchema(field, config, fieldPath);
break;
case 'json':
case 'richText':
return joi_1.default.any().forbidden();
default:
assertUnreachable(field);
}
if ('const' in field && typeof field.const !== 'function') {
fieldSchema = fieldSchema.valid(field.const).invalid(null, '').required();
}
else if ('required' in field && field.required === true) {
fieldSchema = fieldSchema.required();
}
return fieldSchema;
}
function enumFieldValueSchema(field) {
if (field.options) {
const values = field.options.map((option) => {
return typeof option === 'object' ? option.value : option;
});
return joi_1.default.valid(...values);
}
return joi_1.default.any().forbidden();
}
function numberFieldValueSchema(field) {
let result = joi_1.default.number();
if (field.subtype !== 'float') {
result = result.integer();
}
if (field.min) {
result = result.min(field.min);
}
if (field.max) {
result = result.max(field.max);
}
// TODO: fix step validation, multiple of non 0 minimum is wrong
// if (field.step) {
// result = result.multiple((field.min || 0) + field.step);
// }
return result;
}
function objectFieldValueSchema(field, config, fieldPath) {
const childFieldPath = fieldPath.concat('fields');
return joiSchemaForModelFields(field.fields, config, childFieldPath);
}
function modelFieldValueSchema(field, config) {
if (field.models.length === 0) {
return joi_1.default.any().forbidden();
}
const objectTypeKey = config.objectTypeKey || 'type';
const typeSchema = joi_1.default.string().valid(...field.models);
if (field.models.length === 1 && field.models[0]) {
const modelName = field.models[0];
return joi_1.default.link()
.ref(`#${modelName}_model_schema`)
.concat(joi_1.default.object({
__metadata: metadataSchema,
[objectTypeKey]: typeSchema
}));
}
else {
// if there is more than one model in models, then 'type' field is
// required to identify the object
return joi_1.default.alternatives()
.conditional(`.${objectTypeKey}`, {
switch: lodash_1.default.map(field.models, (modelName) => {
return {
is: modelName,
then: joi_1.default.link()
.ref(`#${modelName}_model_schema`)
.concat(joi_1.default.object({
__metadata: metadataSchema,
[objectTypeKey]: joi_1.default.string()
}))
};
})
})
.prefs({
messages: {
'alternatives.any': `{{#label}}.${objectTypeKey} is required and must be one of [${field.models.join(', ')}].`
},
errors: { wrap: { label: false } }
});
}
}
function referenceFieldValueSchema(field) {
// TODO: validate reference by looking if referenced filePath actually exists
// and the stored object has the correct type
return joi_1.default.string();
}
function crossReferenceFieldValueSchema(field) {
return joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.object({
srcType: joi_1.default.string(),
srcProjectId: joi_1.default.string(),
srcDocumentId: joi_1.default.string()
}));
}
function listFieldValueSchema(field, config, fieldPath) {
if (field.items) {
const childFieldPath = fieldPath.concat('items');
const itemsSchema = joiSchemaForField(field.items, config, childFieldPath);
return joi_1.default.array().items(itemsSchema);
}
return joi_1.default.array().items(joi_1.default.string());
}
function styleFieldValueSchema(field) {
const styleFieldSchema = lodash_1.default.mapValues(field.styles, (fieldStyles) => {
const styleProps = lodash_1.default.keys(fieldStyles);
const objectSchema = lodash_1.default.reduce(styleProps, (schema, styleProp) => {
const createSchema = StylePropContentSchemas[styleProp];
if (!createSchema) {
return schema;
}
const styleConfig = fieldStyles[styleProp];
const valueSchema = createSchema(styleConfig);
if (!valueSchema) {
return schema;
}
schema[styleProp] = valueSchema;
return schema;
}, {});
return joi_1.default.object(objectSchema);
});
return joi_1.default.object(styleFieldSchema);
}
const StylePropContentSchemas = {
objectFit: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.objectFit),
objectPosition: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.nineRegions),
flexDirection: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.flexDirection),
justifyContent: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.justifyContent),
justifyItems: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.justifyItems),
justifySelf: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.justifySelf),
alignContent: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.alignContent),
alignItems: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.alignItems),
alignSelf: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.alignSelf),
padding: stylePropSizeSchema,
margin: stylePropSizeSchema,
width: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.width),
height: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.height),
fontFamily: stylePropObjectValueSchema,
fontSize: stylePropFontSizeSchema,
fontStyle: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.fontStyle),
fontWeight: stylePropFontWeightSchema,
textAlign: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.textAlign),
textColor: stylePropObjectValueSchema,
textDecoration: stylePropTextDecorationSchema,
backgroundColor: stylePropObjectValueSchema,
backgroundPosition: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.nineRegions),
backgroundSize: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.backgroundSize),
borderRadius: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.borderRadius),
borderWidth: stylePropBorderWidthSchema,
borderColor: stylePropObjectValueSchema,
borderStyle: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.borderStyle),
boxShadow: stylePropSchemaWithValidValues(config_consts_1.STYLE_PROPS_VALUES.boxShadow),
opacity: stylePropOpacitySchema
};
function stylePropSchemaWithValidValues(validValues) {
return (styleConfig) => {
if (styleConfig === '*') {
return joi_1.default.string().valid(...validValues);
}
if (Array.isArray(styleConfig)) {
return joi_1.default.string().valid(...styleConfig);
}
return null;
};
}
const textDecorationWrongValue = 'textDecoration.wrong.value';
function stylePropTextDecorationSchema(styleConfig) {
const values = styleConfig === '*' ? config_consts_1.STYLE_PROPS_VALUES.textDecoration : styleConfig;
return joi_1.default.string()
.custom((value, { error, errorsArray }) => {
const parts = value.split?.(' ');
const errors = errorsArray();
parts.forEach((part) => {
if (!values.includes(part)) {
errors.push(error(textDecorationWrongValue));
}
});
return errors && errors.length ? errors : value;
})
.prefs({
messages: {
[textDecorationWrongValue]: `{{#label}} must be a space separated string values from [${values.join(', ')}]`
}
});
}
function stylePropSizeSchema(styleConfig) {
if (styleConfig === '*') {
return joi_1.default.object({
top: joi_1.default.number(),
bottom: joi_1.default.number(),
left: joi_1.default.number(),
right: joi_1.default.number()
});
}
styleConfig = lodash_1.default.castArray(styleConfig);
if (lodash_1.default.some(styleConfig, (style) => lodash_1.default.startsWith(style, 'tw'))) {
return joi_1.default.array().items(joi_1.default.string());
}
const dirSchemas = lodash_1.default.reduce(styleConfig, (dirSchemas, pattern) => {
if (typeof pattern !== 'string') {
return dirSchemas;
}
const directionMatch = pattern.match(/^[xylrtb]/);
const directions = [];
if (!directionMatch) {
directions.push('top', 'bottom', 'left', 'right');
}
else {
const dirMap = {
x: ['left', 'right'],
y: ['top', 'bottom'],
l: ['left'],
r: ['right'],
t: ['top'],
b: ['bottom']
};
const dirMatch = directionMatch[0];
directions.push(...dirMap[dirMatch]);
pattern = pattern.substring(1);
}
let valueSchema = joi_1.default.number();
if (pattern) {
const parts = pattern.split(':').map((value) => Number(value));
if (lodash_1.default.some(parts, lodash_1.default.isNaN)) {
return dirSchemas;
}
if (parts.length === 1) {
valueSchema = valueSchema.valid(parts[0]);
}
else {
valueSchema = valueSchema.min(parts[0]).max(parts[1]);
if (parts.length === 3) {
valueSchema = valueSchema.multiple(parts[2]);
}
}
}
lodash_1.default.forEach(directions, (direction) => {
(0, utils_1.append)(dirSchemas, direction, valueSchema);
});
return dirSchemas;
}, {});
const objectSchema = lodash_1.default.mapValues(dirSchemas, (schema) => joi_1.default.alternatives(...schema));
return joi_1.default.object(objectSchema);
}
function stylePropBorderWidthSchema(styleConfig) {
if (styleConfig === '*') {
return joi_1.default.number();
}
styleConfig = lodash_1.default.castArray(styleConfig);
const alternativeSchemas = lodash_1.default.reduce(styleConfig, (alternativeSchemas, value) => {
let valueSchema = joi_1.default.number();
if (lodash_1.default.isNumber(value)) {
return alternativeSchemas.concat(valueSchema.valid(value));
}
if (!lodash_1.default.isString(value)) {
return alternativeSchemas;
}
if (lodash_1.default.isEmpty(value)) {
return alternativeSchemas;
}
const parts = value.split(':').map((value) => Number(value));
if (lodash_1.default.some(parts, lodash_1.default.isNaN)) {
return alternativeSchemas;
}
if (parts.length === 1) {
return alternativeSchemas.concat(valueSchema.valid(value));
}
const start = parts[0];
const end = parts[1];
valueSchema = valueSchema.min(start).max(end);
if (parts.length === 3) {
valueSchema = valueSchema.multiple(parts[2]);
}
return alternativeSchemas.concat(valueSchema);
}, []);
return joi_1.default.alternatives(...alternativeSchemas);
}
function stylePropObjectValueSchema(styleConfig) {
return joi_1.default.valid(...lodash_1.default.map(styleConfig, (object) => lodash_1.default.get(object, 'value')));
}
function stylePropFontSizeSchema(styleConfig) {
if (styleConfig === '*') {
return joi_1.default.number().integer().min(0).max(100).multiple(1);
}
return joi_1.default.number().valid(...parseRange(styleConfig));
}
function stylePropFontWeightSchema(styleConfig) {
const values = config_consts_1.STYLE_PROPS_VALUES.fontWeight.map((valueStr) => Number(valueStr));
if (styleConfig === '*') {
return joi_1.default.number().valid(...values);
}
return joi_1.default.number().valid(...parseRange(styleConfig, { defaultStep: 100 }));
}
function stylePropOpacitySchema(styleConfig) {
if (styleConfig === '*') {
return joi_1.default.number().integer().min(0).max(100).multiple(5);
}
return joi_1.default.number().valid(...parseRange(styleConfig, { userDefinedStep: false, defaultStep: 5 }));
}
function parseRange(styleConfig, { userDefinedStep = true, defaultStep = 1 } = {}) {
const values = lodash_1.default.reduce(lodash_1.default.castArray(styleConfig), (validValues, value) => {
if (lodash_1.default.isNumber(value)) {
return validValues.add(value);
}
if (!lodash_1.default.isString(value)) {
return validValues;
}
if (lodash_1.default.isEmpty(value)) {
return validValues;
}
const parts = value.split(':').map((value) => Number(value));
if (lodash_1.default.some(parts, lodash_1.default.isNaN)) {
return validValues;
}
if (parts.length === 1) {
return validValues.add(parts[0]);
}
const [start, end, userStep = defaultStep] = parts;
const step = userDefinedStep ? userStep : defaultStep;
for (let i = start; i <= end; i += step) {
validValues.add(i);
}
return validValues;
}, new Set());
return Array.from(values);
}
//# sourceMappingURL=content-schema.js.map