@stackbit/sdk
Version:
998 lines • 43.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.stackbitConfigFullSchema = exports.stackbitConfigBaseSchema = exports.contentModelsSchema = exports.objectPreviewSchema = 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-consts");
const style_field_schema_1 = require("./config-schema/style-field-schema");
function getConfigFromValidationState(state) {
return lodash_1.default.last(state.ancestors);
}
function getModelsFromValidationState(state) {
const config = getConfigFromValidationState(state);
return config.models ?? [];
}
const arrayOfStrings = joi_1.default.array().items(joi_1.default.string());
const fieldNamePattern = /^[a-zA-Z0-9_$]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
const fieldNameError = 'Invalid field name "{{#value}}" at "{{#label}}". A field name must contain only alphanumeric characters, ' +
'hyphens and underscores, must start and end with an alphanumeric character.';
const fieldNameSchema = joi_1.default.string()
.required()
.pattern(fieldNamePattern)
.prefs({
messages: { 'string.pattern.base': fieldNameError },
errors: { wrap: { label: false } }
});
const objectModelNameErrorCode = 'model.not.object.model';
const validObjectModelNames = joi_1.default.custom((value, { error, state }) => {
const models = getModelsFromValidationState(state);
const objectModelNames = models.filter((model) => model.type === 'object').map((model) => model.name);
if (!objectModelNames.includes(value)) {
return error(objectModelNameErrorCode);
}
return value;
}).prefs({
messages: {
[objectModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "object", got "{{#value}}"'
},
errors: { wrap: { label: false } }
});
const documentModelNameErrorCode = 'model.not.document.model';
const validReferenceModelNames = joi_1.default.custom((value, { error, state }) => {
const models = getModelsFromValidationState(state);
const documentModelNames = models.filter((model) => ['page', 'data'].includes(model.type)).map((model) => model.name);
if (!documentModelNames.includes(value)) {
return error(documentModelNameErrorCode);
}
return value;
}).prefs({
messages: {
[documentModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "page" or "data", got "{{#value}}"'
},
errors: { wrap: { label: false } }
});
const validCrossReferenceModelNames = joi_1.default.object({
modelName: joi_1.default.string().required(),
srcType: joi_1.default.string(),
srcProjectId: joi_1.default.string()
})
.custom((allowedModels, { error, state }) => {
// TODO: implement custom validation logic that checks unambiguous model matching
// similar to how config models are matched to content-source models in content-store.ts
return allowedModels;
})
.prefs({
messages: {
[documentModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "page" or "data", got "{{#value}}"'
},
errors: { wrap: { label: false } }
});
const groupNotFoundErrorCode = 'group.not.found';
const groupNotObjectModelErrorCode = 'group.not.object.model';
const validModelFieldGroups = joi_1.default.string()
.custom((group, { error, state }) => {
const config = getConfigFromValidationState(state);
const groupModels = getModelNamesForGroup(group, config);
if (!lodash_1.default.isEmpty(groupModels.documentModels)) {
return error(groupNotObjectModelErrorCode, { nonObjectModels: groupModels.documentModels.join(', ') });
}
if (lodash_1.default.isEmpty(groupModels.objectModels)) {
return error(groupNotFoundErrorCode);
}
return group;
})
.prefs({
messages: {
[groupNotObjectModelErrorCode]: '{{#label}} of a "model" field must reference a group with only models ' +
'of type "object", the "{{#value}}" group includes models of type "page" or "data" ({{#nonObjectModels}})',
[groupNotFoundErrorCode]: '{{#label}} of a "model" field must reference the name of an existing group, got "{{#value}}"'
},
errors: { wrap: { label: false } }
});
const groupNotDocumentModelErrorCode = 'group.not.document.model';
const validReferenceFieldGroups = joi_1.default.string()
.custom((group, { error, state }) => {
const config = getConfigFromValidationState(state);
const groupModels = getModelNamesForGroup(group, config);
if (!lodash_1.default.isEmpty(groupModels.objectModels)) {
return error(groupNotDocumentModelErrorCode, { nonDocumentModels: groupModels.objectModels.join(', ') });
}
if (lodash_1.default.isEmpty(groupModels.documentModels)) {
return error(groupNotFoundErrorCode);
}
return group;
})
.prefs({
messages: {
[groupNotDocumentModelErrorCode]: '{{#label}} of a "reference" field must reference a group with only models of type "page" or "data", ' +
'the "{{#value}}" group includes models of type "object" ({{#nonDocumentModels}})',
[groupNotFoundErrorCode]: '{{#label}} of a "reference" field must reference the name of an existing group, got "{{#value}}"'
},
errors: { wrap: { label: false } }
});
function getModelNamesForGroup(group, config) {
const models = config.models ?? [];
return lodash_1.default.reduce(models, (result, model) => {
if (model?.groups && lodash_1.default.includes(model.groups, group)) {
if (model?.type === 'object') {
result.objectModels.push(model.name);
}
else {
result.documentModels.push(model.name);
}
}
return result;
}, { objectModels: [], documentModels: [] });
}
const labelFieldNotFoundError = 'labelField.not.found';
const labelFieldNotSimple = 'labelField.not.simple';
const labelFieldSchema = joi_1.default.custom((value, { error, state }) => {
const modelOrObjectField = lodash_1.default.head(state.ancestors);
const fields = modelOrObjectField?.fields ?? [];
if (!lodash_1.default.isArray(fields)) {
return error(labelFieldNotFoundError);
}
const field = lodash_1.default.find(fields, (field) => field.name === value);
if (!field) {
return error(labelFieldNotFoundError);
}
if (['object', 'model', 'reference', 'cross-reference', 'list'].includes(field.type)) {
return error(labelFieldNotSimple, { fieldType: field.type });
}
return value;
}).prefs({
messages: {
[labelFieldNotFoundError]: '{{#label}} must be one of model field names, got "{{#value}}"',
[labelFieldNotSimple]: '{{#label}} can not reference complex field, got "{{#value}}" field of type "{{#fieldType}}"'
},
errors: { wrap: { label: false } }
});
const variantFieldNotFoundError = 'variantField.not.found';
const variantFieldNotEnum = 'variantField.not.enum';
const variantFieldSchema = joi_1.default.custom((value, { error, state }) => {
const modelOrObjectField = lodash_1.default.head(state.ancestors);
const fields = modelOrObjectField?.fields ?? [];
if (!lodash_1.default.isArray(fields)) {
return error(variantFieldNotFoundError);
}
const field = lodash_1.default.find(fields, (field) => field.name === value);
if (!field) {
return error(variantFieldNotFoundError);
}
if (field.type !== 'enum') {
return error(variantFieldNotEnum, { fieldType: field.type });
}
return value;
}).prefs({
messages: {
[variantFieldNotFoundError]: '{{#label}} must be one of model field names, got "{{#value}}"',
[variantFieldNotEnum]: '{{#label}} should reference "enum" field, got "{{#value}}" field of type "{{#fieldType}}"'
},
errors: { wrap: { label: false } }
});
const styleObjectModelReferenceError = 'styleObjectModelName.model.missing';
const styleObjectModelNotObject = 'styleObjectModelName.model.type';
const styleObjectModelNameSchema = joi_1.default.string()
.allow('', null)
.custom((value, { error, state }) => {
const config = getConfigFromValidationState(state);
const externalModels = config.cmsName && ['contentful', 'sanity'].includes(config.cmsName);
if (externalModels) {
return value;
}
const models = getModelsFromValidationState(state);
const styleObjectModel = models.find((model) => model.name === value);
if (!styleObjectModel) {
return error(styleObjectModelReferenceError);
}
if (styleObjectModel.type !== 'data') {
return error(styleObjectModelNotObject);
}
return value;
})
.prefs({
messages: {
[styleObjectModelReferenceError]: '{{#label}} must reference an existing model',
[styleObjectModelNotObject]: 'Model defined in {{#label}} must be of type data - {{#value}}'
},
errors: { wrap: { label: false } }
});
const contentfulImportSchema = joi_1.default.object({
type: joi_1.default.string().valid('contentful').required(),
contentFile: joi_1.default.string().required(),
uploadAssets: joi_1.default.boolean(),
assetsDirectory: joi_1.default.string(),
spaceIdEnvVar: joi_1.default.string(),
accessTokenEnvVar: joi_1.default.string(),
deliveryTokenEnvVar: joi_1.default.string(),
previewTokenEnvVar: joi_1.default.string()
}).and('uploadAssets', 'assetsDirectory');
const sanityImportSchema = joi_1.default.object({
type: joi_1.default.string().valid('sanity').required(),
contentFile: joi_1.default.string().required(),
sanityStudioPath: joi_1.default.string().required(),
deployStudio: joi_1.default.boolean(),
deployGraphql: joi_1.default.boolean(),
projectIdEnvVar: joi_1.default.string(),
datasetEnvVar: joi_1.default.string(),
tokenEnvVar: joi_1.default.string()
});
const importSchema = joi_1.default.alternatives().conditional('.type', {
switch: [
{ is: 'contentful', then: contentfulImportSchema },
{ is: 'sanity', then: sanityImportSchema }
]
});
const presetSourceFilesSchema = joi_1.default.object({
type: joi_1.default.string().valid(joi_1.default.override, 'files').required(),
presetDirs: arrayOfStrings.required()
});
const presetSourceSchema = joi_1.default.object({
type: joi_1.default.string().valid('files').required()
}).when('.type', {
switch: [{ is: 'files', then: presetSourceFilesSchema }]
});
const modelsSourceFilesSchema = joi_1.default.object({
type: joi_1.default.string().valid(joi_1.default.override, 'files').required(),
modelDirs: arrayOfStrings.required()
});
const modelsSourceContentfulSchema = joi_1.default.object({
type: joi_1.default.string().valid(joi_1.default.override, 'contentful').required(),
module: joi_1.default.string()
});
const modelsSourceSanitySchema = joi_1.default.object({
type: joi_1.default.string().valid(joi_1.default.override, 'sanity').required(),
sanityStudioPath: joi_1.default.string().required(),
module: joi_1.default.string()
});
const modelsSourceSchema = joi_1.default.object({
type: joi_1.default.string().valid('files', 'contentful', 'sanity').required()
}).when('.type', {
switch: [
{ is: 'files', then: modelsSourceFilesSchema },
{ is: 'contentful', then: modelsSourceContentfulSchema },
{ is: 'sanity', then: modelsSourceSanitySchema }
]
});
const assetsSchema = joi_1.default.object({
referenceType: joi_1.default.string().valid('static', 'relative').required(),
assetsDir: joi_1.default.string().allow('').when('referenceType', {
is: 'relative',
then: joi_1.default.required()
}),
staticDir: joi_1.default.string().allow('').when('referenceType', {
is: 'static',
then: joi_1.default.required()
}),
publicPath: joi_1.default.string().allow('').when('referenceType', {
is: 'static',
then: joi_1.default.required()
}),
uploadDir: joi_1.default.string().allow('')
});
const fieldGroupsSchema = joi_1.default.array()
.items(joi_1.default.object({
name: joi_1.default.string().required(),
label: joi_1.default.string().required(),
icon: joi_1.default.string().optional()
}))
.unique('name')
.prefs({
messages: {
'array.unique': '{{#label}} contains a duplicate group name "{{#value.name}}"'
},
errors: { wrap: { label: false } }
});
const inGroups = joi_1.default.string()
.valid(
// 4 dots "...." =>
// ".." for the parent field where "group" property is defined
// + "." for the fields array
// + "." for the parent model
joi_1.default.in('....fieldGroups', {
adjust: (groups) => (lodash_1.default.isArray(groups) ? groups.map((group) => group.name) : [])
}))
.prefs({
messages: { 'any.only': '{{#label}} must be one of model field groups, got "{{#value}}"' },
errors: { wrap: { label: false } }
});
exports.objectPreviewSchema = joi_1.default.object({
title: joi_1.default.string(),
subtitle: joi_1.default.string(),
image: joi_1.default.string()
});
const customActionPermissionsSchema = joi_1.default.object({
canExecute: joi_1.default.boolean().optional()
});
const customActionBaseSchema = joi_1.default.object({
name: joi_1.default.string().required(),
label: joi_1.default.string(),
icon: joi_1.default.string(),
preferredStyle: joi_1.default.string().valid('button-primary', 'button-secondary'),
state: joi_1.default.function(),
run: joi_1.default.function().required(),
inputFields: joi_1.default.link('#fieldsSchema'),
hidden: joi_1.default.boolean(),
permissions: joi_1.default.function()
});
const customActionGlobalAndBulkSchema = customActionBaseSchema.concat(joi_1.default.object({
type: joi_1.default.string().valid('global', 'bulk').required()
}));
const customActionPageAndDataModelSchema = customActionBaseSchema.concat(joi_1.default.object({
type: joi_1.default.string().valid('document').required()
}));
const customActionObjectModelSchema = customActionBaseSchema.concat(joi_1.default.object({
type: joi_1.default.string().valid('object').required()
}));
const customActionObjectFieldSchema = customActionBaseSchema.concat(joi_1.default.object({
type: joi_1.default.string().valid('field', 'object')
}));
const customActionFieldSchema = customActionBaseSchema.concat(joi_1.default.object({
type: joi_1.default.string().valid('field')
}));
const stringOrNumber = joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.number());
const fieldValidationSchema = joi_1.default.object({
unique: joi_1.default.boolean(),
validate: joi_1.default.alternatives().try(joi_1.default.function(), joi_1.default.array().items(joi_1.default.function())),
// string and text
regexp: joi_1.default.string(),
regexpNot: joi_1.default.string(),
regexpPattern: joi_1.default.string(),
// ranges
min: stringOrNumber,
max: stringOrNumber,
after: joi_1.default.string(),
before: joi_1.default.string(),
exact: joi_1.default.number(),
step: joi_1.default.number(),
lessThan: joi_1.default.number(),
greaterThan: joi_1.default.number(),
// file
fileMinSize: joi_1.default.number(),
fileMaxSize: joi_1.default.number(),
fileTypes: arrayOfStrings,
fileTypeGroups: arrayOfStrings,
// image
minWidth: joi_1.default.number(),
maxWidth: joi_1.default.number(),
minHeight: joi_1.default.number(),
maxHeight: joi_1.default.number(),
minWidthToHeightRatio: stringOrNumber,
maxWidthToHeightRatio: stringOrNumber,
errors: joi_1.default.object({
unique: joi_1.default.string(),
regexp: joi_1.default.string(),
regexpNot: joi_1.default.string(),
regexpPattern: joi_1.default.string(),
min: joi_1.default.string(),
max: joi_1.default.string(),
after: joi_1.default.string(),
before: joi_1.default.string(),
exact: joi_1.default.string(),
step: joi_1.default.string(),
lessThan: joi_1.default.string(),
greaterThan: joi_1.default.string(),
fileMinSize: joi_1.default.string(),
fileMaxSize: joi_1.default.string(),
fileTypes: joi_1.default.string(),
fileTypeGroups: joi_1.default.string(),
minWidth: joi_1.default.string(),
maxWidth: joi_1.default.string(),
minHeight: joi_1.default.string(),
maxHeight: joi_1.default.string(),
minWidthToHeightRatio: joi_1.default.string(),
maxWidthToHeightRatio: joi_1.default.string()
})
});
const customControlTypesSchema = joi_1.default.string().valid('custom-modal-html', 'custom-inline-html', 'custom-modal-script', 'custom-inline-script');
const fieldCommonPropsSchema = joi_1.default.object({
type: joi_1.default.string()
.valid(...config_consts_1.FIELD_TYPES)
.required(),
name: fieldNameSchema,
label: joi_1.default.string(),
description: joi_1.default.string().allow(''),
required: joi_1.default.boolean(),
validations: fieldValidationSchema,
default: joi_1.default.any(),
group: inGroups,
const: joi_1.default.any(),
hidden: joi_1.default.boolean(),
readOnly: joi_1.default.boolean(),
localized: joi_1.default.boolean(),
actions: joi_1.default.array().items(customActionFieldSchema),
controlType: customControlTypesSchema
})
.oxor('const', 'default')
.when('.controlType', {
is: customControlTypesSchema.required(),
then: joi_1.default.object({
controlUrl: joi_1.default.string(),
controlFilePath: joi_1.default.string()
}).xor('controlUrl', 'controlFilePath')
});
const numberFieldPartialSchema = joi_1.default.object({
type: joi_1.default.string().valid('number').required(),
controlType: joi_1.default.string().valid('slider'),
subtype: joi_1.default.string().valid('int', 'float'),
min: joi_1.default.number(),
max: joi_1.default.number(),
step: joi_1.default.number(),
unit: joi_1.default.string()
});
const enumFieldBaseOptionSchema = joi_1.default.object({
label: joi_1.default.string().required(),
value: joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.number()).required()
});
const imageFieldPartialSchema = joi_1.default.object({
source: joi_1.default.string()
});
const enumFieldPartialSchema = joi_1.default.object({
type: joi_1.default.string().valid('enum').required(),
controlType: joi_1.default.string().valid('dropdown', 'button-group', 'thumbnails', 'palette', 'palette-colors'),
options: joi_1.default.any()
.when('..controlType', {
switch: [
{
is: 'thumbnails',
then: joi_1.default.array().items(enumFieldBaseOptionSchema.append({
thumbnail: joi_1.default.string().required()
}))
},
{
is: 'palette',
then: joi_1.default.array().items(enumFieldBaseOptionSchema.append({
textColor: joi_1.default.string(),
backgroundColor: joi_1.default.string(),
borderColor: joi_1.default.string()
}))
},
{
is: 'palette-colors',
then: joi_1.default.array().items(enumFieldBaseOptionSchema.append({
colors: arrayOfStrings.required()
}))
}
],
otherwise: joi_1.default.alternatives().try(joi_1.default.array().items(joi_1.default.string(), joi_1.default.number()), joi_1.default.array().items(enumFieldBaseOptionSchema))
})
.required()
.prefs({
messages: {
'alternatives.types': '{{#label}} must be an array of strings or numbers, or an array of objects with label and value properties',
'alternatives.match': '{{#label}} must be an array of strings or numbers, or an array of objects with label and value properties'
},
errors: { wrap: { label: false } }
})
});
const booleanFieldPartialSchema = joi_1.default.object({
type: joi_1.default.string().valid('boolean').required(),
controlType: joi_1.default.string().valid('button-group', 'checkbox')
});
const objectFieldPartialSchema = joi_1.default.object({
type: joi_1.default.string().valid('object').required(),
labelField: labelFieldSchema,
preview: joi_1.default.alternatives().try(exports.objectPreviewSchema, joi_1.default.function()),
thumbnail: joi_1.default.string(),
variantField: variantFieldSchema,
actions: joi_1.default.array().items(customActionObjectFieldSchema),
fieldGroups: fieldGroupsSchema,
fields: joi_1.default.link('#fieldsSchema').required()
});
const modelFieldPartialSchema = joi_1.default.object({
type: joi_1.default.string().valid('model').required(),
models: joi_1.default.array().items(validObjectModelNames).when('groups', {
not: joi_1.default.exist(),
then: joi_1.default.required()
}),
groups: joi_1.default.array().items(validModelFieldGroups)
});
const referenceFieldPartialSchema = joi_1.default.object({
type: joi_1.default.string().valid('reference').required(),
models: joi_1.default.array().items(validReferenceModelNames).when('groups', {
not: joi_1.default.exist(),
then: joi_1.default.required()
}),
groups: joi_1.default.array().items(validReferenceFieldGroups)
});
const crossReferenceFieldPartialSchema = joi_1.default.object({
type: joi_1.default.string().valid('cross-reference').required(),
models: joi_1.default.array().items(validCrossReferenceModelNames).required()
});
const listItemsSchema = joi_1.default.object({
// 'style' and 'list' are not allowed inside lists
type: joi_1.default.string()
.valid(...lodash_1.default.without(config_consts_1.FIELD_TYPES, 'list', 'style'))
.required(),
validations: fieldValidationSchema
}).when('.type', {
switch: [
{ is: 'number', then: numberFieldPartialSchema },
{ is: 'enum', then: enumFieldPartialSchema },
{ is: 'boolean', then: booleanFieldPartialSchema },
{ is: 'image', then: imageFieldPartialSchema },
{ is: 'object', then: objectFieldPartialSchema },
{ is: 'model', then: modelFieldPartialSchema },
{ is: 'reference', then: referenceFieldPartialSchema },
{ is: 'cross-reference', then: crossReferenceFieldPartialSchema }
]
});
const listFieldPartialSchema = joi_1.default.object({
type: joi_1.default.string().valid('list').required(),
controlType: joi_1.default.string().valid('checkbox'),
items: joi_1.default.any().when('..controlType', {
switch: [
{
is: 'checkbox',
then: enumFieldPartialSchema
}
],
otherwise: listItemsSchema
})
});
const fieldSchema = fieldCommonPropsSchema.when('.type', {
switch: [
{ is: 'number', then: numberFieldPartialSchema },
{ is: 'enum', then: enumFieldPartialSchema },
{ is: 'boolean', then: booleanFieldPartialSchema },
{ is: 'image', then: imageFieldPartialSchema },
{ is: 'object', then: objectFieldPartialSchema },
{ is: 'model', then: modelFieldPartialSchema },
{ is: 'reference', then: referenceFieldPartialSchema },
{ is: 'cross-reference', then: crossReferenceFieldPartialSchema },
{ is: 'style', then: style_field_schema_1.styleFieldPartialSchema },
{ is: 'list', then: listFieldPartialSchema }
]
});
const fieldsSchema = joi_1.default.array().items(fieldSchema).unique('name').id('fieldsSchema');
const contentModelKeyNotFound = 'contentModel.model.not.found';
const contentModelTypeNotPage = 'contentModel.type.not.page';
const contentModelTypeNotData = 'contentModel.type.not.data';
const contentModelSchema = joi_1.default.object({
isPage: joi_1.default.boolean(),
newFilePath: joi_1.default.string(),
singleInstance: joi_1.default.boolean(),
file: joi_1.default.string(),
folder: joi_1.default.string(),
match: arrayOfStrings.single(),
exclude: arrayOfStrings.single()
})
.without('file', ['folder', 'match', 'exclude'])
.when('.isPage', {
is: true,
then: joi_1.default.object({
urlPath: joi_1.default.string(),
hideContent: joi_1.default.boolean()
})
})
.custom((contentModel, { error, state, prefs }) => {
const modelMap = lodash_1.default.get(prefs, 'context.modelMap');
if (!modelMap) {
return contentModel;
}
const modelName = lodash_1.default.last(state.path);
const model = modelMap[modelName];
if (!model) {
return error(contentModelKeyNotFound, { modelName });
}
else if (contentModel.isPage && model.type && !['page', 'object'].includes(model.type)) {
return error(contentModelTypeNotPage, { modelName, modelType: model.type });
}
else if (!contentModel.isPage && model.type && !['data', 'object'].includes(model.type)) {
return error(contentModelTypeNotData, { modelName, modelType: model.type });
}
return contentModel;
})
.prefs({
messages: {
[contentModelKeyNotFound]: 'The key "{{#modelName}}" of contentModels must reference the name of an existing model',
[contentModelTypeNotPage]: 'The contentModels.{{#modelName}}.isPage is set to true, but the "{{#modelName}}" model\'s type is "{{#modelType}}". ' +
'The contentModels should reference models of "object" type only. ' +
'Set the "{{#modelName}}" model\'s type property to "object" or delete it use the default "object"',
[contentModelTypeNotData]: 'The contentModels.{{#modelName}} references a model of type "{{#modelType}}". ' +
'The contentModels should reference models of "object" type only. ' +
'Set the "{{#modelName}}" model\'s type property to "object" or delete it use the default "object"'
},
errors: { wrap: { label: false } }
});
exports.contentModelsSchema = joi_1.default.object({
contentModels: joi_1.default.object().pattern(joi_1.default.string(), contentModelSchema)
});
const modelNamePattern = /^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
const modelNameError = 'Invalid model name "{{#value}}" at "{{#label}}". A model name must contain only alphanumeric characters, dashes ' +
'and underscores, must start with a letter, and end with alphanumeric character.';
const modelNameSchema = joi_1.default.string()
.required()
.pattern(modelNamePattern)
.prefs({
messages: { 'string.pattern.base': modelNameError },
errors: { wrap: { label: false } }
});
const modelPermissionsSchema = joi_1.default.object({
canView: joi_1.default.boolean().optional(),
canEdit: joi_1.default.boolean().optional(),
canPublish: joi_1.default.boolean().optional()
});
const baseModelSchema = {
__metadata: joi_1.default.object({
filePath: joi_1.default.string()
}),
name: modelNameSchema,
srcType: joi_1.default.string(),
srcProjectId: joi_1.default.string(),
label: joi_1.default.string(),
description: joi_1.default.string().allow(''),
thumbnail: joi_1.default.string(),
extends: joi_1.default.array().items(validObjectModelNames).single(),
readOnly: joi_1.default.boolean(),
labelField: labelFieldSchema,
variantField: variantFieldSchema,
groups: arrayOfStrings,
context: joi_1.default.any(),
preview: joi_1.default.alternatives().try(exports.objectPreviewSchema, joi_1.default.function()),
fieldGroups: fieldGroupsSchema,
fields: joi_1.default.link('#fieldsSchema'),
permissions: joi_1.default.alternatives().try(modelPermissionsSchema, joi_1.default.function())
};
const localizedModelSchema = {
localized: joi_1.default.boolean(),
locale: joi_1.default.function()
};
const objectModelSchema = joi_1.default.object({
...baseModelSchema,
type: joi_1.default.string().valid('object').required(),
actions: joi_1.default.array().items(customActionObjectModelSchema)
});
const dataModelSchema = joi_1.default.object({
...baseModelSchema,
...localizedModelSchema,
type: joi_1.default.string().valid('data').required(),
actions: joi_1.default.array().items(customActionPageAndDataModelSchema),
filePath: joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.function()),
singleInstance: joi_1.default.boolean(),
file: joi_1.default.string(),
folder: joi_1.default.string(),
match: arrayOfStrings.single(),
exclude: arrayOfStrings.single(),
isList: joi_1.default.boolean(),
canDelete: joi_1.default.boolean()
})
.when('.isList', {
is: true,
then: joi_1.default.object({
items: listItemsSchema.required(),
fields: joi_1.default.forbidden()
})
})
.when('.file', {
is: joi_1.default.exist(),
then: joi_1.default.object({
folder: joi_1.default.forbidden(),
match: joi_1.default.forbidden(),
exclude: joi_1.default.forbidden()
})
});
const configModelSchema = joi_1.default.object({
...baseModelSchema,
...localizedModelSchema,
type: joi_1.default.string().valid('config').required(),
file: joi_1.default.string()
});
const pageModelSchema = joi_1.default.object({
...baseModelSchema,
...localizedModelSchema,
type: joi_1.default.string().valid('page').required(),
actions: joi_1.default.array().items(customActionPageAndDataModelSchema),
layout: joi_1.default.string(),
urlPath: joi_1.default.string(),
filePath: joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.function()),
singleInstance: joi_1.default.boolean(),
file: joi_1.default.string(),
folder: joi_1.default.string(),
match: arrayOfStrings.single(),
exclude: arrayOfStrings.single(),
hideContent: joi_1.default.boolean(),
canDelete: joi_1.default.boolean()
})
.when('.file', {
is: joi_1.default.exist(),
then: {
singleInstance: joi_1.default.valid(true).required(),
folder: joi_1.default.forbidden(),
match: joi_1.default.forbidden(),
exclude: joi_1.default.forbidden()
}
})
.when('.singleInstance', { is: true, then: { file: joi_1.default.required() } });
const modelFileExclusiveErrorCode = 'model.file.only';
const modelIsListItemsRequiredErrorCode = 'model.isList.items.required';
const modelIsListFieldsForbiddenErrorCode = 'model.isList.fields.forbidden';
const modelListForbiddenErrorCode = 'model.items.forbidden';
const fieldNameUnique = 'field.name.unique';
function errorLabelFromModelAndFieldPath(model, modelIndex, fieldPath) {
let fieldSpecificProps = model;
let fields;
let label = `models[${model.name ? `name='${model.name}'` : modelIndex}]`;
for (const pathPart of fieldPath) {
if (typeof pathPart === 'string') {
if (pathPart === 'fields' && fieldSpecificProps && 'fields' in fieldSpecificProps) {
fields = fieldSpecificProps.fields;
fieldSpecificProps = undefined;
label += '.fields';
}
else if (pathPart === 'items' && fieldSpecificProps && 'items' in fieldSpecificProps) {
fieldSpecificProps = fieldSpecificProps.items;
label += '.items';
}
else {
fieldSpecificProps = undefined;
label += `.${pathPart}`;
}
}
else if (fields && typeof pathPart === 'number') {
const field = fields[pathPart];
label += '[' + (field?.name ? `name='${field.name}'` : pathPart) + ']';
fieldSpecificProps = field;
fields = undefined;
}
else {
// when the schema is marked as Joi.array().items(...).single()
// and the validated value is not an array, Joi injects (new Number(0))
// which is an object. Don't use it to generate path
if (typeof pathPart === 'object') {
continue;
}
label += `[${pathPart}]`;
}
}
return label;
}
const modelSchema = joi_1.default.object({
type: joi_1.default.string().valid('page', 'data', 'config', 'object').required(),
name: joi_1.default.string()
})
.when('.type', {
switch: [
{ is: 'object', then: objectModelSchema },
{ is: 'data', then: dataModelSchema },
{ is: 'config', then: configModelSchema },
{ is: 'page', then: pageModelSchema }
]
})
.error(((errors) => {
return lodash_1.default.map(errors, (error) => {
if (error.path[0] === 'models' && typeof error.path[1] === 'number') {
const modelIndex = error.path[1];
const config = error.prefs.context?.config;
if (config && config.models[modelIndex]) {
const model = config.models[modelIndex];
error.path = error.path.slice();
error.path.splice(1, 1, model.name);
const label = errorLabelFromModelAndFieldPath(model, modelIndex, error.path.slice(2));
lodash_1.default.set(error, 'local.label', label);
// const label = _.get(error, 'local.label', '') as string;
// _.set(error, 'local.label', label.replace(/models\[(\d+)]/, (match, indexMatch) => {
// if (Number(indexMatch) !== modelIndex) {
// return match;
// }
// return `models[name='${model.name}']`;
// }));
}
}
if (error.code === 'any.unknown' &&
error.path.length === 3 &&
error.path[0] === 'models' &&
error.path[2] &&
['folder', 'match', 'exclude'].includes(error.path[2])) {
error.code = modelFileExclusiveErrorCode;
}
else if (error.code === 'any.required' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'items') {
error.code = modelIsListItemsRequiredErrorCode;
}
else if (error.code === 'any.unknown' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'fields') {
error.code = modelIsListFieldsForbiddenErrorCode;
}
else if (error.code === 'object.unknown' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'items') {
error.code = modelListForbiddenErrorCode;
}
else if (error.code === 'array.unique' && error.path.length > 3 && error.path[0] === 'models' && lodash_1.default.nth(error.path, -2) === 'fields') {
error.code = fieldNameUnique;
}
return error;
});
})) // the type definition of Joi.ValidationErrorFunction is wrong, so we override
.prefs({
messages: {
[modelFileExclusiveErrorCode]: '{{#label}} cannot be used with "file"',
[modelIsListItemsRequiredErrorCode]: '{{#label}} is required when "isList" is true',
[modelIsListFieldsForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is true',
[modelListForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is not true',
[fieldNameUnique]: '{{#label}} contains a duplicate field name "{{#value.name}}"'
},
errors: { wrap: { label: false } }
});
const groupModelsIncompatibleError = 'group.models.incompatible';
const modelsSchema = joi_1.default.array()
.items(modelSchema)
.custom((models, { error }) => {
const groupMap = {};
lodash_1.default.forEach(models, (model) => {
const key = model?.type === 'object' ? 'objectModels' : 'documentModels';
lodash_1.default.forEach(model.groups, (groupName) => {
(0, utils_1.append)(groupMap, [groupName, key], model.name);
});
});
const errors = lodash_1.default.reduce(groupMap, (errors, group, groupName) => {
if (group.objectModels && group.documentModels) {
const objectModels = group.objectModels.join(', ');
const documentModels = group.documentModels.join(', ');
errors.push(`group "${groupName}" include models of type "object" (${objectModels}) and objects of type "page" or "data" (${documentModels})`);
}
return errors;
}, []);
if (!lodash_1.default.isEmpty(errors)) {
return error(groupModelsIncompatibleError, { incompatibleGroups: errors.join(', ') });
}
return models;
})
.prefs({
messages: {
[groupModelsIncompatibleError]: 'Model groups must include models of the same type. The following groups have incompatible models: {{#incompatibleGroups}}'
},
errors: { wrap: { label: false } }
});
const sidebarButtonSchema = joi_1.default.object({
label: joi_1.default.string().required(),
icon: joi_1.default.string().required(),
type: joi_1.default.string().valid('link', 'document', 'model').required()
}).when('.type', {
switch: [
{
is: 'link',
then: joi_1.default.object({
url: joi_1.default.string().required()
})
},
{
is: 'document',
then: joi_1.default.object({
documentId: joi_1.default.string().required(),
srcType: joi_1.default.string(),
srcProjectId: joi_1.default.string()
})
},
{
is: 'model',
then: joi_1.default.object({
modelName: joi_1.default.string().required(),
srcType: joi_1.default.string(),
srcProjectId: joi_1.default.string()
})
}
]
});
const viewportSchema = joi_1.default.object({
label: joi_1.default.string().required(),
size: joi_1.default.object({
width: joi_1.default.number(),
height: joi_1.default.number()
}).required()
});
const contentEngineSchema = joi_1.default.object({
host: joi_1.default.string(),
port: joi_1.default.alternatives(joi_1.default.number(), joi_1.default.string())
});
const AssetSourceSchema = joi_1.default.object({
type: joi_1.default.string().required(),
name: joi_1.default.string(),
default: joi_1.default.boolean(),
transform: joi_1.default.function(),
preview: joi_1.default.alternatives().try(joi_1.default.object({
title: joi_1.default.string(),
image: joi_1.default.string().required()
}), joi_1.default.function())
}).when('.type', {
is: 'iframe',
then: joi_1.default.object({
name: joi_1.default.required(),
url: joi_1.default.string().required()
})
});
exports.stackbitConfigBaseSchema = joi_1.default.object({
stackbitVersion: joi_1.default.string().required(),
ssgName: joi_1.default.string().valid(...config_consts_1.SSG_NAMES),
ssgVersion: joi_1.default.string(),
nodeVersion: joi_1.default.string(),
useESM: joi_1.default.boolean(),
postGitCloneCommand: joi_1.default.string(),
preInstallCommand: joi_1.default.string(),
postInstallCommand: joi_1.default.string(),
installCommand: joi_1.default.string(),
devCommand: joi_1.default.string(),
cmsName: joi_1.default.string().valid(...config_consts_1.CMS_NAMES),
import: importSchema,
buildCommand: joi_1.default.string(),
publishDir: joi_1.default.string(),
cacheDir: joi_1.default.string(),
staticDir: joi_1.default.string().allow(''),
uploadDir: joi_1.default.string(),
assets: assetsSchema,
pagesDir: joi_1.default.string().allow('', null),
dataDir: joi_1.default.string().allow('', null),
pageLayoutKey: joi_1.default.string().allow(null),
objectTypeKey: joi_1.default.string(),
excludePages: arrayOfStrings.single(),
styleObjectModelName: joi_1.default.string(),
logicFields: arrayOfStrings,
contentModels: joi_1.default.any(),
presetSource: presetSourceSchema,
modelsSource: modelsSourceSchema,
sidebarButtons: joi_1.default.array().items(sidebarButtonSchema),
viewports: joi_1.default.array().items(viewportSchema),
actions: joi_1.default.array().items(customActionGlobalAndBulkSchema.shared(fieldsSchema)),
models: joi_1.default.any(),
modelExtensions: joi_1.default.array().items(joi_1.default.any()),
presetReferenceBehavior: joi_1.default.string().valid('copyReference', 'duplicateContents'),
nonDuplicatableModels: arrayOfStrings.when('presetReferenceBehavior', {
is: 'copyReference',
then: joi_1.default.forbidden()
}),
duplicatableModels: arrayOfStrings.when('presetReferenceBehavior', {
is: 'duplicateContents',
then: joi_1.default.forbidden()
}),
customContentReload: joi_1.default.boolean(),
experimental: joi_1.default.any(),
contentSources: joi_1.default.array().items(joi_1.default.any()),
connectors: joi_1.default.array().items(joi_1.default.any()),
assetSources: joi_1.default.array().items(AssetSourceSchema),
contentEngine: contentEngineSchema,
siteMap: joi_1.default.function(),
transformSitemap: joi_1.default.function(),
sitemap: joi_1.default.function(),
treeViews: joi_1.default.function(),
transformTreeViews: joi_1.default.function(),
mapModels: joi_1.default.function(),
permissionsForModel: joi_1.default.function(),
permissionsForDocument: joi_1.default.function(),
filterAsset: joi_1.default.function(),
mapDocuments: joi_1.default.function(),
onContentCreate: joi_1.default.function(),
onDocumentCreate: joi_1.default.function(),
onDocumentUpdate: joi_1.default.function(),
onDocumentDelete: joi_1.default.function(),
onDocumentsPublish: joi_1.default.function(),
// internal properties added by load
dirPath: joi_1.default.string(),
filePath: joi_1.default.string()
})
.without('assets', ['staticDir', 'uploadDir'])
.without('contentSources', ['cmsName', 'assets', 'contentModels', 'modelsSource'])
.when('.modelExtensions', {
is: joi_1.default.exist(),
then: joi_1.default.object({
models: joi_1.default.forbidden()
})
})
.when('.cmsName', {
is: ['contentful', 'sanity'],
then: joi_1.default.object({
assets: joi_1.default.forbidden(),
staticDir: joi_1.default.forbidden(),
uploadDir: joi_1.default.forbidden(),
pagesDir: joi_1.default.forbidden(),
dataDir: joi_1.default.forbidden(),
excludePages: joi_1.default.forbidden()
})
});
exports.stackbitConfigFullSchema = exports.stackbitConfigBaseSchema.concat(joi_1.default.object({
styleObjectModelName: styleObjectModelNameSchema,
models: modelsSchema
})
.unknown(true)
.shared(fieldsSchema));
//# sourceMappingURL=config-schema.js.map