UNPKG

@stackbit/sdk

Version:
449 lines 18.8 kB
"use strict"; 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