Socket
Socket
Sign inDemoInstall

@stackbit/sdk

Package Overview
Dependencies
Maintainers
16
Versions
404
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@stackbit/sdk - npm Package Compare versions

Comparing version 0.2.1 to 0.2.2

dist/config/config-consts.d.ts

42

dist/analyzer/analyze-schema-types.d.ts

@@ -6,23 +6,29 @@ /**

*/
import { StricterUnion } from '../utils';
import { FieldCommonProps, FieldEnumProps, FieldModelProps, FieldNumberProps, FieldType, FieldReferenceProps, FieldSimpleNoProps } from '../config/config-schema';
export declare type FieldTypeWithUnknown = FieldType | 'unknown';
export declare type FieldListItemsTypeWithUnknown = Exclude<FieldTypeWithUnknown, 'list'>;
import { FieldCommonProps, FieldType, FieldSimpleProps, FieldEnumProps, FieldNumberProps, FieldObjectProps, FieldModelProps, FieldReferenceProps, FieldListProps, FieldListItems } from '../config/config-types';
export declare type FieldWithUnknown = FieldUnknown | FieldSimpleWithUnknown | FieldEnumWithUnknown | FieldNumberWithUnknown | FieldObjectWithUnknown | FieldModelWithUnknown | FieldReferenceWithUnknown | FieldListWithUnknown;
export declare type FieldUnknown = FieldCommonPropsWithUnknown & FieldUnknownProps;
export declare type FieldSimpleWithUnknown = FieldCommonPropsWithUnknown & FieldSimpleProps;
export declare type FieldEnumWithUnknown = FieldCommonPropsWithUnknown & FieldEnumProps;
export declare type FieldNumberWithUnknown = FieldCommonPropsWithUnknown & FieldNumberProps;
export declare type FieldObjectWithUnknown = FieldCommonPropsWithUnknown & FieldObjectPropsWithUnknown;
export declare type FieldModelWithUnknown = FieldCommonPropsWithUnknown & FieldModelProps;
export declare type FieldReferenceWithUnknown = FieldCommonPropsWithUnknown & FieldReferenceProps;
export declare type FieldListWithUnknown = FieldCommonPropsWithUnknown & FieldListPropsWithUnknown;
export declare type FieldCommonPropsWithUnknown = Omit<FieldCommonProps, 'type'> & {
type: FieldTypeWithUnknown;
};
export declare type FieldTypeWithUnknown = 'unknown' | FieldType;
export interface FieldUnknownProps {
type: 'unknown';
}
export interface FieldListPropsWithUnknown {
type: 'list';
export declare type FieldObjectPropsWithUnknown = Omit<FieldObjectProps, 'fields'> & {
fields: FieldWithUnknown[];
};
export declare type FieldListPropsWithUnknown = Omit<FieldListProps, 'items'> & {
items?: FieldListItemsWithUnknown;
}
export interface FieldObjectPropsWithUnknown {
type: 'object';
labelField?: string;
fields: FieldWithUnknown[];
}
export declare type NonStrictFieldPartialPropsWithUnknown = FieldUnknownProps | FieldEnumProps | FieldObjectPropsWithUnknown | FieldListPropsWithUnknown | FieldNumberProps | FieldModelProps | FieldReferenceProps | FieldSimpleNoProps;
export declare type FieldPartialPropsWithUnknown = StricterUnion<NonStrictFieldPartialPropsWithUnknown>;
export declare type FieldListItemsWithUnknown = StricterUnion<Exclude<NonStrictFieldPartialPropsWithUnknown, FieldListPropsWithUnknown>>;
export declare type FieldObjectWithUnknown = FieldObjectPropsWithUnknown & FieldCommonProps;
export declare type FieldListWithUnknown = FieldListPropsWithUnknown & FieldCommonProps;
export declare type FieldWithUnknown = FieldPartialPropsWithUnknown & FieldCommonProps;
};
export declare type FieldListItemsWithUnknown = Exclude<FieldListItems, FieldObjectProps> | FieldUnknownProps | FieldObjectPropsWithUnknown;
export declare function isObjectWithUnknownField(field: FieldWithUnknown): field is FieldObjectWithUnknown;
export declare function isListWithUnknownField(field: FieldWithUnknown): field is FieldListWithUnknown;
export declare function isObjectListItemsWithUnknown(items: FieldListItemsWithUnknown): items is FieldObjectPropsWithUnknown;
export declare function isModelListItemsWithUnknown(items: FieldListItemsWithUnknown): items is FieldModelProps;

@@ -8,2 +8,19 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.isModelListItemsWithUnknown = exports.isObjectListItemsWithUnknown = exports.isListWithUnknownField = exports.isObjectWithUnknownField = void 0;
function isObjectWithUnknownField(field) {
return field.type === 'object';
}
exports.isObjectWithUnknownField = isObjectWithUnknownField;
function isListWithUnknownField(field) {
return field.type === 'list';
}
exports.isListWithUnknownField = isListWithUnknownField;
function isObjectListItemsWithUnknown(items) {
return items.type === 'object';
}
exports.isObjectListItemsWithUnknown = isObjectListItemsWithUnknown;
function isModelListItemsWithUnknown(items) {
return items.type === 'model';
}
exports.isModelListItemsWithUnknown = isModelListItemsWithUnknown;
//# sourceMappingURL=analyze-schema-types.js.map

@@ -83,3 +83,3 @@ "use strict";

const ignoreProperty = findObjectProperty(optionsProperty.value, 'ignore');
let ignoreValue = ignoreProperty ? getNodeValue(ignoreProperty.value) : null;
const ignoreValue = ignoreProperty ? getNodeValue(ignoreProperty.value) : null;
result.push({

@@ -218,2 +218,3 @@ name: nameValue,

// }
// eslint-disable-next-line
const gatsbySourceFilesystemRegExp = /resolve\s*:\s*(['"`])gatsby-source-filesystem\1\s*,\s*options\s*:\s*{\s*(\w+)\s*:\s*(['"`])([^'"`]+)\3\s*,\s*(\w+)\s*:\s*(['"`])([^'"`]+)\6/g;

@@ -220,0 +221,0 @@ let match;

import { GetFileBrowserOptions } from './file-browser';
import { Config } from '../config/config-loader';
import { Config } from '../config/config-types';
export declare type CMSMatcherOptions = GetFileBrowserOptions;

@@ -4,0 +4,0 @@ declare type CmsName = NonNullable<Config['cmsName']>;

import { GetFileBrowserOptions } from './file-browser';
import { SSGMatchResult } from './ssg-matcher';
import { Model } from '../config/config-loader';
import { Model } from '../config/config-types';
export declare type SchemaGeneratorOptions = {

@@ -5,0 +5,0 @@ ssgMatchResult: SSGMatchResult | null;

@@ -14,2 +14,3 @@ "use strict";

const consts_1 = require("../consts");
const analyze_schema_types_1 = require("./analyze-schema-types");
const SAME_FOLDER_PAGE_DSC_COEFFICIENT = 0.6;

@@ -101,3 +102,10 @@ const ROOT_FOLDER_PAGE_DSC_COEFFICIENT = 0.7;

// TODO: in some projects, pages can be defined as JSON files as well
filePaths = await readDirRecursivelyWithFilter({ fileBrowser, contentDir, ssgMatchResult, excludedFiles, allowedExtensions, excludedFilesInSSGDir });
filePaths = await readDirRecursivelyWithFilter({
fileBrowser,
contentDir,
ssgMatchResult,
excludedFiles,
allowedExtensions,
excludedFilesInSSGDir
});
}

@@ -170,3 +178,3 @@ else {

async function generatePageModelsForFiles({ filePaths, dirPathFromRoot, fileBrowser, pageTypeKey, objectModels }) {
let pageModels = [];
const pageModels = [];
let modelNameCounter = 1;

@@ -212,3 +220,3 @@ for (const filePath of filePaths) {

const modelsByFolder = {};
for (let pageModel of pageModels) {
for (const pageModel of pageModels) {
const filePath = pageModel.filePaths[0];

@@ -225,3 +233,3 @@ const dir = path_1.default.parse(filePath).dir;

// merge page models from same sub-folders (excluding LCA folder) with lowest similarity coefficient
for (let folderPath in modelsByFolder) {
for (const folderPath in modelsByFolder) {
const pageModelsInFolder = modelsByFolder[folderPath];

@@ -239,3 +247,3 @@ const mergeResult = mergeSimilarPageModels(pageModelsInFolder, objectModels, SAME_FOLDER_PAGE_DSC_COEFFICIENT);

// remove 'unknown' field type
let pageModelsWithFilePaths = lodash_1.default.reduce(mergeResult.pageModels, (mergedPageModels, pageModel) => {
const pageModelsWithFilePaths = lodash_1.default.reduce(mergeResult.pageModels, (mergedPageModels, pageModel) => {
const fields = removeUnknownTypesFromFields(pageModel.fields);

@@ -256,3 +264,3 @@ if (lodash_1.default.isEmpty(fields)) {

for (const filePath of filePaths) {
let data = await fileBrowser.getFileData(path_1.default.join(dirPathFromRoot, filePath));
const data = await fileBrowser.getFileData(path_1.default.join(dirPathFromRoot, filePath));
const modelName = `data_${modelNameCounter++}`;

@@ -551,3 +559,3 @@ if (lodash_1.default.isPlainObject(data)) {

switch (type) {
case 'unknown':
case 'unknown': {
return {

@@ -557,3 +565,4 @@ items: { type: 'unknown' },

};
case 'number':
}
case 'number': {
const subtypes = lodash_1.default.compact(lodash_1.default.uniq(lodash_1.default.map(listItemModels, 'subtype')));

@@ -568,3 +577,4 @@ const subtype = subtypes.length === 1 ? subtypes[0] : 'float';

};
case 'object':
}
case 'object': {
const fieldsList = lodash_1.default.map(listItemModels, (itemModels) => itemModels.fields);

@@ -601,3 +611,4 @@ const result = consolidateObjectFieldsListWithOverlap(fieldsList, fieldPath, LIST_OBJECT_DSC_COEFFICIENT, objectModels);

}
case 'model':
}
case 'model': {
const modelNames = lodash_1.default.compact(lodash_1.default.uniq(lodash_1.default.flatten(lodash_1.default.map(listItemModels, 'models'))));

@@ -611,2 +622,3 @@ return {

};
}
case 'enum':

@@ -624,5 +636,5 @@ case 'reference':

if (lodash_1.default.every(itemTypes, (itemsType) => ['object', 'model'].includes(itemsType))) {
const modelListItems = lodash_1.default.filter(listItemModels, { type: 'model' });
const modelListItems = lodash_1.default.filter(listItemModels, analyze_schema_types_1.isModelListItemsWithUnknown);
const modelNames = lodash_1.default.compact(lodash_1.default.uniq(lodash_1.default.flatten(lodash_1.default.map(modelListItems, 'models'))));
const objectListItems = lodash_1.default.filter(listItemModels, { type: 'object' });
const objectListItems = lodash_1.default.filter(listItemModels, analyze_schema_types_1.isObjectListItemsWithUnknown);
const fieldsList = lodash_1.default.map(objectListItems, (listItems) => listItems.fields);

@@ -660,3 +672,2 @@ const result = consolidateObjectFieldsListWithOverlap(fieldsList, fieldPath, LIST_OBJECT_DSC_COEFFICIENT, objectModels);

let unmergedFieldsList = lodash_1.default.orderBy(fieldsList.slice(), ['length'], ['desc']);
let idx = 0;
const mergedFieldsList = [];

@@ -669,3 +680,2 @@ while (unmergedFieldsList.length > 0) {

objectModels = result.objectModels;
idx++;
}

@@ -704,3 +714,9 @@ return {

}
return { mergedFields, unmergedFieldsList, mergedIndexes, unmergedIndexes, objectModels };
return {
mergedFields,
unmergedFieldsList,
mergedIndexes,
unmergedIndexes,
objectModels
};
}

@@ -750,3 +766,3 @@ /**

const fields = fieldsByName[fieldName];
const result = consolidateFields(fields, fieldPath.concat(fieldName), objectModels);
const result = consolidateFields(fields, fieldName, fieldPath.concat(fieldName), objectModels);
// if one of the fields cannot be consolidated, then the object cannot be consolidated as well

@@ -768,3 +784,3 @@ if (!result) {

}
function consolidateFields(fields, fieldPath, objectModels) {
function consolidateFields(fields, fieldName, fieldPath, objectModels) {
if (fields.length === 1) {

@@ -787,8 +803,12 @@ const field = fields[0];

switch (type) {
case 'unknown':
case 'unknown': {
return {
field: { type: 'unknown' },
field: {
type: 'unknown',
name: fieldName
},
objectModels
};
case 'number':
}
case 'number': {
const subtypes = lodash_1.default.compact(lodash_1.default.uniq(lodash_1.default.map(fields, 'subtype')));

@@ -799,2 +819,3 @@ const subtype = subtypes.length === 1 ? subtypes[0] : 'float';

type: 'number',
name: fieldName,
...(subtype && { subtype })

@@ -804,5 +825,7 @@ },

};
case 'object':
const fieldsList = lodash_1.default.map(fields, (field) => field.fields);
const mergeResult = mergeObjectFieldsList(fieldsList, fieldPath, objectModels);
}
case 'object': {
const objectWithUnknownFields = lodash_1.default.filter(fields, analyze_schema_types_1.isObjectWithUnknownField);
const fieldsWithUnknownList = lodash_1.default.compact(lodash_1.default.map(objectWithUnknownFields, (field) => field.fields));
const mergeResult = mergeObjectFieldsList(fieldsWithUnknownList, fieldPath, objectModels);
if (!mergeResult) {

@@ -814,2 +837,3 @@ return null;

type: 'object',
name: fieldName,
fields: mergeResult.fields

@@ -819,5 +843,7 @@ },

};
case 'list':
const listItemsArr = lodash_1.default.map(fields, (field) => field.items);
const itemsResult = consolidateListItems(listItemsArr, fieldPath, objectModels);
}
case 'list': {
const listWithUnknownFields = lodash_1.default.filter(fields, analyze_schema_types_1.isListWithUnknownField);
const listItemsWithUnknownArr = lodash_1.default.compact(lodash_1.default.map(listWithUnknownFields, (field) => field.items));
const itemsResult = consolidateListItems(listItemsWithUnknownArr, fieldPath, objectModels);
if (!itemsResult) {

@@ -829,2 +855,3 @@ return null;

type: 'list',
name: fieldName,
items: itemsResult.items

@@ -834,5 +861,5 @@ },

};
}
case 'enum':
case 'model':
// we don't produce 'model' fields as direct child of 'object' fields, only as list items
case 'model': // we don't produce 'model' fields as direct child of 'object' fields, only as list items
case 'reference':

@@ -843,3 +870,3 @@ // these cases cannot happen because we don't generate these fields,

return {
field: { type },
field: { type, name: fieldName },
objectModels

@@ -852,3 +879,3 @@ };

? {
field: { type: fieldType },
field: { type: fieldType, name: fieldName },
objectModels

@@ -1016,16 +1043,6 @@ }

const modelLabel = lodash_1.default.startCase(modelName);
let items;
let fields;
if (dataModelWithFilePaths.isList && dataModelWithFilePaths.items) {
items = removeUnknownTypesFromListItem(dataModelWithFilePaths.items);
if (!items) {
continue;
}
const itemsOrFields = removeUnknownTypesFromDataModel(dataModelWithFilePaths);
if (!itemsOrFields) {
continue;
}
else {
fields = removeUnknownTypesFromFields(dataModelWithFilePaths.fields);
if (lodash_1.default.isEmpty(fields)) {
continue;
}
}
dataModels.push({

@@ -1036,3 +1053,3 @@ type: 'data',

file: dataModelWithFilePaths.filePaths[0],
...(items ? { isList: true, items } : { fields })
...itemsOrFields
});

@@ -1052,16 +1069,6 @@ }

const modelLabel = lodash_1.default.startCase(modelName);
let items;
let fields;
if (dataModelWithFilePaths.isList && dataModelWithFilePaths.items) {
items = removeUnknownTypesFromListItem(dataModelWithFilePaths.items);
if (!items) {
continue;
}
const itemsOrFields = removeUnknownTypesFromDataModel(dataModelWithFilePaths);
if (!itemsOrFields) {
continue;
}
else {
fields = removeUnknownTypesFromFields(dataModelWithFilePaths.fields);
if (lodash_1.default.isEmpty(fields)) {
continue;
}
}
dataModels.push({

@@ -1072,3 +1079,3 @@ type: 'data',

folder: folder,
...(items ? { isList: true, items } : { fields })
...itemsOrFields
});

@@ -1079,8 +1086,24 @@ }

}
function removeUnknownTypesFromDataModel(partialDataModel) {
if (partialDataModel.isList && partialDataModel.items) {
const items = removeUnknownTypesFromListItem(partialDataModel.items);
if (items) {
return { isList: true, items };
}
}
else {
const fields = removeUnknownTypesFromFields(partialDataModel.fields);
if (!lodash_1.default.isEmpty(fields)) {
return { fields };
}
}
return null;
}
function removeUnknownTypesFromFields(fields) {
return lodash_1.default.reduce(fields, (accum, field) => {
switch (field.type) {
case 'unknown':
case 'unknown': {
return accum;
case 'object':
}
case 'object': {
const fields = removeUnknownTypesFromFields(field.fields);

@@ -1091,3 +1114,4 @@ if (lodash_1.default.isEmpty(fields)) {

return accum.concat(Object.assign(field, { fields }));
case 'list':
}
case 'list': {
const items = removeUnknownTypesFromListItem(field.items);

@@ -1098,4 +1122,6 @@ if (!items) {

return accum.concat(Object.assign(field, { items }));
default:
}
default: {
return accum.concat(field);
}
}

@@ -1119,3 +1145,3 @@ }, []);

let commonDir = null;
for (let model of models) {
for (const model of models) {
let dir;

@@ -1136,3 +1162,3 @@ if (model.file) {

else {
let common = [];
const common = [];
let j = 0;

@@ -1187,3 +1213,3 @@ while (j < commonDir.length && j < dir.length && commonDir[j] === dir[j]) {

const dirParts = lodash_1.default.split(dir, path_1.default.sep);
let common = [];
const common = [];
let j = 0;

@@ -1190,0 +1216,0 @@ while (j < commonDirParts.length && j < dirParts.length && commonDirParts[j] === dirParts[j]) {

import { GetFileBrowserOptions } from './file-browser';
import { SSGMatchResult } from './ssg-matcher';
import { CMSMatchResult } from './cms-matcher';
import { Config } from '../config/config-loader';
import { Config } from '../config/config-types';
export declare type SiteAnalyzerOptions = GetFileBrowserOptions;

@@ -6,0 +6,0 @@ export interface SiteAnalyzerResult {

import { GetFileBrowserOptions } from './file-browser';
import { Config } from '../config/config-loader';
import { Config } from '../config/config-types';
export declare type SSGMatcherOptions = GetFileBrowserOptions;

@@ -4,0 +4,0 @@ declare type AssetsReferenceType = 'static' | 'relative';

import { ConfigValidationError } from './config-validator';
import { YamlConfig, YamlConfigModel, YamlDataModel, YamlObjectModel, YamlPageModel } from './config-schema';
import { ConfigLoadError } from './config-errors';
import { StricterUnion } from '../utils';
export declare type BaseModel = {
name: string;
__metadata?: {
filePath?: string;
invalid?: boolean;
};
};
export declare type ObjectModel = YamlObjectModel & BaseModel;
export declare type DataModel = YamlDataModel & BaseModel;
export declare type ConfigModel = YamlConfigModel & BaseModel;
export declare type PageModel = YamlPageModel & BaseModel;
export declare type Model = StricterUnion<ObjectModel | DataModel | ConfigModel | PageModel>;
export interface Config extends Omit<YamlConfig, 'models'> {
models: Model[];
}
import { Config } from './config-types';
export interface ConfigNormalizedValidationError extends ConfigValidationError {

@@ -32,2 +16,12 @@ normFieldPath: (string | number)[];

}
export interface NormalizedValidationResult {
config: Config;
valid: boolean;
errors: ConfigNormalizedValidationError[];
}
export interface TempConfigLoaderResult {
config?: any;
errors: ConfigLoadError[];
}
export declare function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<ConfigLoaderResult>;
export declare function validateAndNormalizeConfig(config: any): NormalizedValidationResult;

@@ -6,3 +6,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.loadConfig = void 0;
exports.validateAndNormalizeConfig = exports.loadConfig = void 0;
const path_1 = __importDefault(require("path"));

@@ -36,16 +36,34 @@ const fs_extra_1 = __importDefault(require("fs-extra"));

}
const config = normalizeConfig(configLoadResult.config);
const validationResult = config_validator_1.validate(config);
convertModelCategoriesToModels(validationResult);
const convertedResult = convertModelsToArray(validationResult);
const errors = [...configLoadResult.errors, ...convertedResult.errors];
const normalizedResult = validateAndNormalizeConfig(configLoadResult.config);
return {
valid: validationResult.valid,
config: convertedResult.config,
errors: errors
valid: normalizedResult.valid,
config: normalizedResult.config,
errors: [...configLoadResult.errors, ...normalizedResult.errors]
};
}
exports.loadConfig = loadConfig;
function validateAndNormalizeConfig(config) {
// validate the "contentModels" and extend config models with "contentModels"
// this must be done before main config validation to make it independent of "contentModels".
const contentModelsValidationResult = validateAndExtendContentModels(config);
config = contentModelsValidationResult.value;
// extend config models having the "extends" property
// this must be done before main config validation as some properties like
// the labelField will not work when validating models without extending them first
config.models = utils_1.extendModelMap((config === null || config === void 0 ? void 0 : config.models) || {});
// extend config - backward compatibility updates, adding extra fields like "markdown_content", "type" and "layout",
// and setting other default values.
config = extendConfig(config);
// validate config
const configValidationResult = config_validator_1.validateConfig(config);
const errors = [...contentModelsValidationResult.errors, ...configValidationResult.errors];
return normalizeValidationResult({
valid: lodash_1.default.isEmpty(errors),
value: configValidationResult.value,
errors: errors
});
}
exports.validateAndNormalizeConfig = validateAndNormalizeConfig;
async function loadConfigFromDir(dirPath) {
let { config, error } = await loadConfigFromStackbitYaml(dirPath);
const { config, error } = await loadConfigFromStackbitYaml(dirPath);
if (error) {

@@ -124,2 +142,3 @@ return { errors: [error] };

}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function loadConfigFromDotStackbit(dirPath) {

@@ -152,3 +171,3 @@ const stackbitDotPath = path_1.default.join(dirPath, '.stackbit');

}
function normalizeConfig(config) {
function extendConfig(config) {
const pageLayoutKey = lodash_1.default.get(config, 'pageLayoutKey', 'layout');

@@ -159,11 +178,4 @@ const objectTypeKey = lodash_1.default.get(config, 'objectTypeKey', 'type');

const isStackbitYamlV2 = ver ? semver_1.default.satisfies(ver, '<0.3.0') : false;
let models = (config === null || config === void 0 ? void 0 : config.models) || {};
const models = (config === null || config === void 0 ? void 0 : config.models) || {};
let referencedModelNames = [];
try {
models = utils_1.extendModelMap(models);
}
catch (error) {
// TODO: gracefully extend and return error rather throwing
throw error;
}
lodash_1.default.forEach(models, (model) => {

@@ -194,3 +206,3 @@ var _a;

resolveThumbnailPathForModel(model, (_a = model === null || model === void 0 ? void 0 : model.__metadata) === null || _a === void 0 ? void 0 : _a.filePath);
utils_1.iterateModelFieldsRecursively(model, (field, fieldPath) => {
utils_1.iterateModelFieldsRecursively(model, (field) => {
var _a, _b;

@@ -355,19 +367,73 @@ // add field label if label is not set

}
function convertModelCategoriesToModels(validationResult) {
function validateAndExtendContentModels(config) {
var _a, _b;
const contentModels = (_a = config.contentModels) !== null && _a !== void 0 ? _a : {};
const models = (_b = config.models) !== null && _b !== void 0 ? _b : {};
if (!contentModels) {
return config;
}
const validationResult = config_validator_1.validateContentModels(contentModels, models);
const extendedModels = lodash_1.default.mapValues(models, (model, modelName) => {
const contentModel = validationResult.value.contentModels[modelName];
if (!contentModel) {
return {
// if a model does not define a type, use the default "object" type
type: model.type || 'object',
...lodash_1.default.omit(model, 'type')
};
}
if (lodash_1.default.get(contentModel, '__metadata.invalid')) {
return model;
}
if (contentModel.isPage && (!model.type || ['object', 'page'].includes(model.type))) {
return {
type: 'page',
...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
...lodash_1.default.omit(contentModel, ['isPage', 'newFilePath']),
...lodash_1.default.omit(model, 'type')
};
}
else if (!contentModel.isPage && (!model.type || ['object', 'data'].includes(model.type))) {
return {
type: 'data',
...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
...lodash_1.default.omit(contentModel, ['newFilePath']),
...lodash_1.default.omit(model, 'type')
};
}
else {
return model;
}
});
return {
valid: validationResult.valid,
value: {
...lodash_1.default.omit(config, ['contentModels', 'models']),
models: extendedModels
},
errors: validationResult.errors
};
}
function normalizeValidationResult(validationResult) {
convertModelGroupsToModelList(validationResult);
return convertModelsToArray(validationResult);
}
function convertModelGroupsToModelList(validationResult) {
var _a, _b;
const models = (_b = (_a = validationResult.value) === null || _a === void 0 ? void 0 : _a.models) !== null && _b !== void 0 ? _b : {};
const categoryMap = lodash_1.default.reduce(models, (categoryMap, model, modelName) => {
if (!model.categories) {
return categoryMap;
const groupMap = lodash_1.default.reduce(models, (groupMap, model, modelName) => {
if (!model.groups) {
return groupMap;
}
let key = (model === null || model === void 0 ? void 0 : model.type) === 'object' ? 'objectModels' : 'documentModels';
lodash_1.default.forEach(model.categories, (categoryName) => {
utils_2.append(categoryMap, [categoryName, key], modelName);
const key = (model === null || model === void 0 ? void 0 : model.type) === 'object' ? 'objectModels' : 'documentModels';
lodash_1.default.forEach(model.groups, (groupName) => {
utils_2.append(groupMap, [groupName, key], modelName);
});
delete model.categories;
return categoryMap;
delete model.groups;
return groupMap;
}, {});
lodash_1.default.forEach(categoryMap, (category, categoryName) => {
lodash_1.default.forEach(category, (modelCategory, key) => {
lodash_1.default.set(category, key, lodash_1.default.uniq(modelCategory));
// update groups to have unique model names
lodash_1.default.forEach(groupMap, (group) => {
lodash_1.default.forEach(group, (modelGroup, key) => {
lodash_1.default.set(group, key, lodash_1.default.uniq(modelGroup));
});

@@ -380,3 +446,3 @@ });

}
if (field.categories) {
if (field.groups) {
let key = null;

@@ -390,8 +456,8 @@ if (utils_1.isModelField(field)) {

if (key) {
field.models = lodash_1.default.reduce(field.categories, (modelNames, categoryName) => {
const objectModelNames = lodash_1.default.get(categoryMap, [categoryName, key], []);
field.models = lodash_1.default.reduce(field.groups, (modelNames, groupName) => {
const objectModelNames = lodash_1.default.get(groupMap, [groupName, key], []);
return lodash_1.default.uniq(modelNames.concat(objectModelNames));
}, field.models || []);
}
delete field.categories;
delete field.groups;
}

@@ -403,3 +469,3 @@ });

var _a;
const config = lodash_1.default.cloneDeep(validationResult.value);
const config = validationResult.value;
// in stackbit.yaml 'models' are defined as object where keys are the model names,

@@ -409,3 +475,3 @@ // convert 'models' to array of objects and set their 'name' property to the

const modelMap = (_a = config.models) !== null && _a !== void 0 ? _a : {};
let modelArray = lodash_1.default.map(modelMap, (yamlModel, modelName) => {
const modelArray = lodash_1.default.map(modelMap, (yamlModel, modelName) => {
return {

@@ -433,2 +499,3 @@ name: modelName,

return {
valid: validationResult.valid,
config: {

@@ -435,0 +502,0 @@ ...config,

import Joi from 'joi';
import { StricterUnion } from '../utils';
declare const SSG_NAMES: readonly ["unibit", "jekyll", "hugo", "gatsby", "nextjs", "custom", "eleventy", "vuepress", "gridsome", "nuxt", "sapper", "hexo"];
declare const CMS_NAMES: readonly ["git", "contentful", "sanity", "forestry", "netlifycms"];
export declare const FIELD_TYPES: readonly ["string", "url", "slug", "text", "markdown", "html", "number", "boolean", "enum", "date", "datetime", "color", "image", "file", "object", "model", "reference", "list"];
export declare type FieldType = typeof FIELD_TYPES[number];
export declare type LogicField = string;
export interface ContentfulImport {
type: 'contentful';
contentFile: string;
uploadAssets?: boolean;
assetsDirectory?: string;
spaceIdEnvVar?: string;
accessTokenEnvVar?: string;
}
export interface SanityImport {
type: 'sanity';
contentFile: string;
sanityStudioPath: string;
deployStudio?: boolean;
deployGraphql?: boolean;
projectIdEnvVar?: string;
datasetEnvVar?: string;
tokenEnvVar?: string;
}
export declare type Import = ContentfulImport | SanityImport;
export interface StaticAssetsModal {
referenceType: 'static';
assetsDir?: string;
staticDir: string;
publicPath: string;
uploadDir?: string;
}
export interface RelativeAssetsModal {
referenceType: 'relative';
assetsDir: string;
staticDir?: string;
publicPath?: string;
uploadDir?: string;
}
export interface ModelsSource {
type: 'files';
modelDirs: string[];
}
export declare type AssetsModel = StricterUnion<StaticAssetsModal | RelativeAssetsModal>;
export interface FieldCommonProps {
name: string;
label?: string;
description?: string;
required?: boolean;
default?: unknown;
group?: string;
const?: unknown;
hidden?: boolean;
readOnly?: boolean;
}
export declare type FieldEnumOptionValue = string | number;
export interface FieldEnumOptionObject {
label: string;
value: FieldEnumOptionValue;
}
export interface FieldEnumOptionThumbnails extends FieldEnumOptionObject {
thumbnail?: string;
}
export interface FieldEnumOptionPalette extends FieldEnumOptionObject {
textColor?: string;
backgroundColor?: string;
borderColor?: string;
}
export interface FieldEnumDropdownProps {
type: 'enum';
controlType?: 'dropdown' | 'button-group';
options: FieldEnumOptionValue[] | FieldEnumOptionObject[];
}
export interface FieldEnumThumbnailsProps {
type: 'enum';
controlType: 'thumbnails';
options: FieldEnumOptionThumbnails[];
}
export interface FieldEnumPaletteProps {
type: 'enum';
controlType: 'palette';
options: FieldEnumOptionPalette[];
}
export declare type FieldEnumProps = FieldEnumDropdownProps | FieldEnumThumbnailsProps | FieldEnumPaletteProps;
export interface FieldObjectProps {
type: 'object';
labelField?: string;
thumbnail?: string;
variantField?: string;
fieldGroups?: string;
fields: Field[];
}
export interface FieldListProps {
type: 'list';
items?: FieldListItems;
}
export interface FieldNumberProps {
type: 'number';
subtype?: 'int' | 'float';
min?: number;
max?: number;
step?: number;
}
export interface FieldModelProps {
type: 'model';
models: string[];
categories?: string[];
}
export interface FieldReferenceProps {
type: 'reference';
models: string[];
categories?: string[];
}
export interface FieldSimpleNoProps {
type: Exclude<FieldType, 'enum' | 'number' | 'object' | 'model' | 'reference' | 'list'>;
}
declare type NonStrictFieldPartialProps = FieldEnumProps | FieldObjectProps | FieldListProps | FieldNumberProps | FieldModelProps | FieldReferenceProps | FieldSimpleNoProps;
export declare type FieldPartialProps = StricterUnion<NonStrictFieldPartialProps>;
export declare type FieldListItems = StricterUnion<Exclude<NonStrictFieldPartialProps, FieldListProps>>;
export declare type SimpleField = FieldSimpleNoProps & FieldCommonProps;
export declare type FieldEnum = FieldEnumProps & FieldCommonProps;
export declare type FieldNumber = FieldNumberProps & FieldCommonProps;
export declare type FieldObject = FieldObjectProps & FieldCommonProps;
export declare type FieldModel = FieldModelProps & FieldCommonProps;
export declare type FieldReference = FieldReferenceProps & FieldCommonProps;
export declare type FieldList = FieldListProps & FieldCommonProps;
export declare type FieldListObject = FieldList & {
items?: FieldObjectProps;
};
export declare type FieldListModel = FieldList & {
items?: FieldModelProps;
};
export declare type FieldListReference = FieldList & {
items?: FieldReferenceProps;
};
export declare type Field = FieldPartialProps & FieldCommonProps;
export interface FieldGroupItem {
name: string;
label: string;
}
export interface YamlBaseModel {
__metadata?: {
filePath?: string;
};
label: string;
description?: string;
thumbnail?: string;
extends?: string | string[];
labelField?: string;
variantField?: string;
categories?: string[];
fieldGroups?: FieldGroupItem[];
fields?: Field[];
}
interface BaseMatch {
folder?: string;
match?: string | string[];
exclude?: string | string[];
}
export interface YamlObjectModel extends YamlBaseModel {
type: 'object';
}
export interface BaseDataModel extends YamlBaseModel {
type: 'data';
}
export interface BaseDataModelFileSingle extends BaseDataModel {
file: string;
isList?: false;
}
export interface BaseDataModelFileList extends Omit<BaseDataModel, 'fields'> {
file: string;
isList: true;
items: FieldListItems;
}
export interface BaseDataModelMatchSingle extends BaseDataModel, BaseMatch {
isList?: false;
}
export interface BaseDataModelMatchList extends Omit<BaseDataModel, 'fields'>, BaseMatch {
isList: true;
items: FieldListItems;
}
export declare type YamlDataModel = StricterUnion<BaseDataModelFileSingle | BaseDataModelFileList | BaseDataModelMatchSingle | BaseDataModelMatchList>;
export interface YamlConfigModel extends YamlBaseModel {
type: 'config';
file?: string;
}
export interface BasePageModel extends YamlBaseModel {
type: 'page';
layout?: string;
urlPath?: string;
filePath?: string;
hideContent?: boolean;
}
export interface PageModelSingle extends BasePageModel {
singleInstance: true;
file: string;
}
export interface PageModelMatch extends BasePageModel, BaseMatch {
singleInstance?: false;
}
export declare type YamlPageModel = StricterUnion<PageModelSingle | PageModelMatch>;
export declare type YamlModel = StricterUnion<YamlObjectModel | YamlDataModel | YamlConfigModel | YamlPageModel>;
export declare type YamlModels = Record<string, YamlModel>;
export interface YamlConfig {
stackbitVersion: string;
ssgName?: typeof SSG_NAMES[number];
ssgVersion?: string;
nodeVersion?: string;
devCommand?: string;
cmsName?: typeof CMS_NAMES[number];
import?: Import;
buildCommand?: string;
publishDir?: string;
staticDir?: string;
uploadDir?: string;
assets?: AssetsModel;
pagesDir?: string | null;
dataDir?: string | null;
pageLayoutKey?: string | null;
objectTypeKey?: string;
excludePages?: string | string[];
logicFields?: LogicField[];
modelsSource?: ModelsSource;
models?: YamlModels;
}
import { YamlConfig } from './config-types';
export declare const contentModelsSchema: Joi.ObjectSchema<any>;
export declare const stackbitConfigSchema: Joi.ObjectSchema<YamlConfig>;
export {};

@@ -6,32 +6,18 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.stackbitConfigSchema = exports.FIELD_TYPES = void 0;
exports.stackbitConfigSchema = exports.contentModelsSchema = void 0;
const joi_1 = __importDefault(require("joi"));
const lodash_1 = __importDefault(require("lodash"));
const utils_1 = require("@stackbit/utils");
// SSGs Stackbit Stuio supports
const SSG_NAMES = ['unibit', 'jekyll', 'hugo', 'gatsby', 'nextjs', 'custom', 'eleventy', 'vuepress', 'gridsome', 'nuxt', 'sapper', 'hexo'];
// CMSes Stackbit Stuio supports
const CMS_NAMES = ['git', 'contentful', 'sanity', 'forestry', 'netlifycms'];
exports.FIELD_TYPES = [
'string',
'url',
'slug',
'text',
'markdown',
'html',
'number',
'boolean',
'enum',
'date',
'datetime',
'color',
'image',
'file',
'object',
'model',
'reference',
'list'
];
const config_consts_1 = require("./config-consts");
function getConfigFromValidationState(state) {
return lodash_1.default.last(state.ancestors);
}
function getModelsFromValidationState(state) {
var _a;
const config = getConfigFromValidationState(state);
return (_a = config.models) !== null && _a !== void 0 ? _a : {};
}
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 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()

@@ -46,5 +32,3 @@ .required()

const validObjectModelNames = joi_1.default.custom((value, { error, state }) => {
var _a;
const config = lodash_1.default.last(state.ancestors);
const models = (_a = config.models) !== null && _a !== void 0 ? _a : {};
const models = getModelsFromValidationState(state);
const modelNames = Object.keys(models);

@@ -63,6 +47,4 @@ const objectModelNames = modelNames.filter((modelName) => models[modelName].type === 'object');

const documentModelNameErrorCode = 'model.not.document.model';
const validPageOrDataModelNames = joi_1.default.custom((value, { error, state }) => {
var _a;
const config = lodash_1.default.last(state.ancestors);
const models = (_a = config.models) !== null && _a !== void 0 ? _a : {};
const validReferenceModelNames = joi_1.default.custom((value, { error, state }) => {
const models = getModelsFromValidationState(state);
const modelNames = Object.keys(models);

@@ -80,48 +62,50 @@ const documentModels = modelNames.filter((modelName) => ['page', 'data'].includes(models[modelName].type));

});
const categoryNotFoundErrorCode = 'category.not.found';
const categoryNotObjectModelErrorCode = 'category.not.object.model';
const validModelFieldCategories = joi_1.default.string()
.custom((category, { error, state }) => {
const config = lodash_1.default.last(state.ancestors);
const categoryModels = getModelNamesForCategory(category, config);
if (!lodash_1.default.isEmpty(categoryModels.documentModels)) {
return error(categoryNotObjectModelErrorCode, { nonObjectModels: categoryModels.documentModels.join(', ') });
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(categoryModels.objectModels)) {
return error(categoryNotFoundErrorCode);
if (lodash_1.default.isEmpty(groupModels.objectModels)) {
return error(groupNotFoundErrorCode);
}
return category;
return group;
})
.prefs({
messages: {
[categoryNotObjectModelErrorCode]: '{{#label}} of a "model" field must reference a category with only models of type "object", the "{{#value}}" category includes models of type "page" or "data" ({{#nonObjectModels}})',
[categoryNotFoundErrorCode]: '{{#label}} of a "model" field must reference the name of an existing category, got "{{#value}}"'
[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 categoryNotDocumentModelErrorCode = 'category.not.document.model';
const validReferenceFieldCategories = joi_1.default.string()
.custom((category, { error, state }) => {
const config = lodash_1.default.last(state.ancestors);
const categoryModels = getModelNamesForCategory(category, config);
if (!lodash_1.default.isEmpty(categoryModels.objectModels)) {
return error(categoryNotDocumentModelErrorCode, { nonDocumentModels: categoryModels.objectModels.join(', ') });
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(categoryModels.documentModels)) {
return error(categoryNotFoundErrorCode);
if (lodash_1.default.isEmpty(groupModels.documentModels)) {
return error(groupNotFoundErrorCode);
}
return category;
return group;
})
.prefs({
messages: {
[categoryNotDocumentModelErrorCode]: '{{#label}} of a "reference" field must reference a category with only models of type "page" or "data", the "{{#value}}" category includes models of type "object" ({{#nonDocumentModels}})',
[categoryNotFoundErrorCode]: '{{#label}} of a "reference" field must reference the name of an existing category, got "{{#value}}"'
[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 getModelNamesForCategory(category, config) {
function getModelNamesForGroup(group, config) {
var _a;
const models = (_a = config.models) !== null && _a !== void 0 ? _a : {};
return lodash_1.default.reduce(models, (result, model, modelName) => {
if ((model === null || model === void 0 ? void 0 : model.categories) && lodash_1.default.includes(model.categories, category)) {
if ((model === null || model === void 0 ? void 0 : model.groups) && lodash_1.default.includes(model.groups, group)) {
if ((model === null || model === void 0 ? void 0 : model.type) === 'object') {

@@ -261,3 +245,3 @@ result.objectModels.push(modelName);

type: joi_1.default.string()
.valid(...exports.FIELD_TYPES)
.valid(...config_consts_1.FIELD_TYPES)
.required(),

@@ -327,9 +311,15 @@ name: fieldNameSchema,

type: joi_1.default.string().valid('model').required(),
models: joi_1.default.array().items(validObjectModelNames).when('categories', { not: joi_1.default.exist(), then: joi_1.default.required() }),
categories: joi_1.default.array().items(validModelFieldCategories)
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(validPageOrDataModelNames).when('categories', { not: joi_1.default.exist(), then: joi_1.default.required() }),
categories: joi_1.default.array().items(validReferenceFieldCategories)
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)
});

@@ -347,3 +337,3 @@ const partialFieldSchema = joi_1.default.object().when('.type', {

type: joi_1.default.string()
.valid(...lodash_1.default.without(exports.FIELD_TYPES, 'list'))
.valid(...lodash_1.default.without(config_consts_1.FIELD_TYPES, 'list'))
.required()

@@ -355,5 +345,57 @@ }).concat(partialFieldSchema);

});
const partialFieldWithListSchema = partialFieldSchema.when('.type', { is: 'list', then: listFieldPartialSchema });
const partialFieldWithListSchema = partialFieldSchema.when('.type', {
is: 'list',
then: listFieldPartialSchema
});
const fieldSchema = fieldCommonPropsSchema.concat(partialFieldWithListSchema);
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(),
file: joi_1.default.string(),
folder: joi_1.default.string(),
match: joi_1.default.array().items(joi_1.default.string()).single(),
exclude: joi_1.default.array().items(joi_1.default.string()).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 models = lodash_1.default.get(prefs, 'context.models');
const modelName = lodash_1.default.last(state.path);
const model = models[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 baseModelSchema = joi_1.default.object({

@@ -364,3 +406,6 @@ __metadata: joi_1.default.object({

type: joi_1.default.string().valid('page', 'data', 'config', 'object').required(),
label: joi_1.default.string().required().when(joi_1.default.ref('/import'), { is: joi_1.default.exist(), then: joi_1.default.optional() }),
label: joi_1.default.string().required().when(joi_1.default.ref('/import'), {
is: joi_1.default.exist(),
then: joi_1.default.optional()
}),
description: joi_1.default.string(),

@@ -371,3 +416,3 @@ thumbnail: joi_1.default.string(),

variantField: variantFieldSchema,
categories: joi_1.default.array().items(joi_1.default.string()),
groups: joi_1.default.array().items(joi_1.default.string()),
fieldGroups: fieldGroupsSchema,

@@ -382,2 +427,3 @@ fields: joi_1.default.link('#fieldsSchema')

type: joi_1.default.string().valid('data').required(),
filePath: joi_1.default.string(),
file: joi_1.default.string(),

@@ -448,18 +494,18 @@ folder: joi_1.default.string(),

const fieldNameUnique = 'field.name.unique';
const categoryModelsIncompatibleError = 'category.models.incompatible';
const groupModelsIncompatibleError = 'group.models.incompatible';
const modelsSchema = joi_1.default.object()
.pattern(modelNamePattern, modelSchema)
.custom((models, { error, state }) => {
const categoryMap = {};
.custom((models, { error }) => {
const groupMap = {};
lodash_1.default.forEach(models, (model, modelName) => {
let key = (model === null || model === void 0 ? void 0 : model.type) === 'object' ? 'objectModels' : 'documentModels';
lodash_1.default.forEach(model.categories, (categoryName) => {
utils_1.append(categoryMap, [categoryName, key], modelName);
const key = (model === null || model === void 0 ? void 0 : model.type) === 'object' ? 'objectModels' : 'documentModels';
lodash_1.default.forEach(model.groups, (groupName) => {
utils_1.append(groupMap, [groupName, key], modelName);
});
});
const errors = lodash_1.default.reduce(categoryMap, (errors, category, categoryName) => {
if (category.objectModels && category.documentModels) {
const objectModels = category.objectModels.join(', ');
const documentModels = category.documentModels.join(', ');
errors.push(`category "${categoryName}" include models of type "object" (${objectModels}) and objects of type "page" or "data" (${documentModels})`);
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})`);
}

@@ -469,3 +515,3 @@ return errors;

if (!lodash_1.default.isEmpty(errors)) {
return error(categoryModelsIncompatibleError, { incompatibleCategories: errors.join(', ') });
return error(groupModelsIncompatibleError, { incompatibleGroups: errors.join(', ') });
}

@@ -503,4 +549,5 @@ return models;

messages: {
[categoryModelsIncompatibleError]: 'Model categories must include models of the same type. The following categories have incompatible models: {{#incompatibleCategories}}',
[modelNamePatternMatchErrorCode]: 'Invalid model name "{{#key}}" at "{{#label}}". A model name must contain only alphanumeric characters and underscores, must start with a letter, and end with alphanumeric character.',
[groupModelsIncompatibleError]: 'Model groups must include models of the same type. The following groups have incompatible models: {{#incompatibleGroups}}',
[modelNamePatternMatchErrorCode]: 'Invalid model name "{{#key}}" at "{{#label}}". A model name must contain only alphanumeric characters ' +
'and underscores, must start with a letter, and end with alphanumeric character.',
[modelFileExclusiveErrorCode]: '{{#label}} cannot be used with "file"',

@@ -514,9 +561,9 @@ [modelIsListItemsRequiredErrorCode]: '{{#label}} is required when "isList" is true',

});
const schema = joi_1.default.object({
exports.stackbitConfigSchema = joi_1.default.object({
stackbitVersion: joi_1.default.string().required(),
ssgName: joi_1.default.string().valid(...SSG_NAMES),
ssgName: joi_1.default.string().valid(...config_consts_1.SSG_NAMES),
ssgVersion: joi_1.default.string(),
nodeVersion: joi_1.default.string(),
devCommand: joi_1.default.string(),
cmsName: joi_1.default.string().valid(...CMS_NAMES),
cmsName: joi_1.default.string().valid(...config_consts_1.CMS_NAMES),
import: importSchema,

@@ -551,3 +598,2 @@ buildCommand: joi_1.default.string(),

.shared(fieldsSchema);
exports.stackbitConfigSchema = schema;
//# sourceMappingURL=config-schema.js.map

@@ -14,2 +14,3 @@ export interface ConfigValidationError {

}
export declare function validate(config: any): ConfigValidationResult;
export declare function validateConfig(config: any): ConfigValidationResult;
export declare function validateContentModels(contentModels: any, models: any): ConfigValidationResult;

@@ -6,12 +6,41 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.validate = void 0;
exports.validateContentModels = exports.validateConfig = void 0;
const lodash_1 = __importDefault(require("lodash"));
const config_schema_1 = require("./config-schema");
function validate(config) {
var _a;
function validateConfig(config) {
const validationOptions = { abortEarly: false };
const validationResult = config_schema_1.stackbitConfigSchema.validate(config, validationOptions);
const value = validationResult.value;
const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
const valid = lodash_1.default.isEmpty(errors);
markInvalidModels(value, errors, 'models');
return {
value,
valid,
errors
};
}
exports.validateConfig = validateConfig;
function validateContentModels(contentModels, models) {
const validationResult = config_schema_1.contentModelsSchema.validate({ contentModels: contentModels }, {
abortEarly: false,
context: {
models: models
}
});
const value = validationResult.value;
const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
const valid = lodash_1.default.isEmpty(errors);
markInvalidModels(value, errors, 'contentModels');
return {
value,
valid,
errors
};
}
exports.validateContentModels = validateContentModels;
function mapJoiErrorsToConfigValidationErrors(validationResult) {
var _a;
const joiErrors = ((_a = validationResult.error) === null || _a === void 0 ? void 0 : _a.details) || [];
const errors = joiErrors.map((validationError) => {
return joiErrors.map((validationError) => {
var _a;

@@ -26,15 +55,7 @@ return {

});
markInvalidModels(value, errors);
const valid = lodash_1.default.isEmpty(errors);
return {
value,
valid,
errors
};
}
exports.validate = validate;
function markInvalidModels(config, errors) {
function markInvalidModels(config, errors, configKey) {
var _a;
const invalidModelNames = getInvalidModelNames(errors);
const models = (_a = config.models) !== null && _a !== void 0 ? _a : {};
const invalidModelNames = getInvalidModelNames(errors, configKey);
const models = (_a = config[configKey]) !== null && _a !== void 0 ? _a : {};
lodash_1.default.forEach(models, (model, modelName) => {

@@ -46,7 +67,7 @@ if (invalidModelNames.includes(modelName)) {

}
function getInvalidModelNames(errors) {
function getInvalidModelNames(errors, configKey) {
// get array of invalid model names by iterating errors and filtering these
// having fieldPath starting with ['models', modelName]
return lodash_1.default.reduce(errors, (modelNames, error) => {
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
if (error.fieldPath[0] === configKey && typeof error.fieldPath[1] == 'string') {
const modelName = error.fieldPath[1];

@@ -53,0 +74,0 @@ modelNames.push(modelName);

@@ -1,3 +0,2 @@

import { Config } from './config-loader';
import { YamlConfig } from './config-schema';
import { Config, YamlConfig } from './config-types';
export interface WriteConfigOptions {

@@ -4,0 +3,0 @@ dirPath: string;

@@ -16,3 +16,4 @@ "use strict";

const yamlString = js_yaml_1.default.dump(yamlConfig);
const info = `# This file was generated by @stackbit/sdk v${packageJson.version}\n# To learn more about stackbit.yaml please visit https://www.stackbit.com/docs/stackbit-yaml/\n`;
const info = `# This file was generated by @stackbit/sdk v${packageJson.version}\n` +
'# To learn more about stackbit.yaml please visit https://www.stackbit.com/docs/stackbit-yaml/\n';
const data = info + yamlString;

@@ -19,0 +20,0 @@ await fs_extra_1.default.outputFile(filePath, data);

@@ -1,2 +0,2 @@

import { Config } from '../config/config-loader';
import { Config } from '../config/config-types';
interface BaseMetadata {

@@ -3,0 +3,0 @@ filePath: string;

import Joi from 'joi';
import { Model } from '../config/config-loader';
import { FieldNumberProps, FieldEnumProps, FieldObjectProps, FieldModelProps, FieldReferenceProps, FieldListProps } from '..';
import { Config, FieldEnumProps, FieldListProps, FieldModelProps, FieldNumberProps, FieldObjectProps, FieldReferenceProps, Model } from '../config/config-types';
declare type ModelSchemaMap = Record<string, Joi.ObjectSchema>;
export declare function joiSchemasForModels(models: Model[]): ModelSchemaMap;
export declare function joiSchemaForModel(model: Model): Joi.ObjectSchema<any>;
export declare function joiSchemasForModels(config: Config): ModelSchemaMap;
export declare function joiSchemaForModel(model: Model, config: Config): Joi.ObjectSchema<any>;
export declare type FieldPropsByType = {

@@ -8,0 +7,0 @@ enum: FieldEnumProps;

@@ -15,4 +15,4 @@ "use strict";

});
function joiSchemasForModels(models) {
const modelSchemas = lodash_1.default.reduce(models, (modelSchemas, model) => {
function joiSchemasForModels(config) {
const modelSchemas = lodash_1.default.reduce(config.models, (modelSchemas, model) => {
var _a;

@@ -33,3 +33,3 @@ let joiSchema;

else {
joiSchema = joiSchemaForModel(model);
joiSchema = joiSchemaForModel(model, config);
}

@@ -55,21 +55,21 @@ modelSchemas[model.name] = joiSchema.id(`${model.name}_model_schema`);

exports.joiSchemasForModels = joiSchemasForModels;
function joiSchemaForModel(model) {
function joiSchemaForModel(model, config) {
if (utils_1.isDataModel(model) && model.isList) {
return joi_1.default.object({
items: joi_1.default.array().items(joiSchemaForField(model.items, [model.name, 'items']))
items: joi_1.default.array().items(joiSchemaForField(model.items, config, [model.name, 'items']))
});
}
else {
return joiSchemaForModelFields(model.fields, [model.name]);
return joiSchemaForModelFields(model.fields, config, [model.name]);
}
}
exports.joiSchemaForModel = joiSchemaForModel;
function joiSchemaForModelFields(fields, fieldPath) {
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, childFieldPath);
schema[field.name] = joiSchemaForField(field, config, childFieldPath);
return schema;
}, {}));
}
function joiSchemaForField(field, fieldPath) {
function joiSchemaForField(field, config, fieldPath) {
let fieldSchema;

@@ -96,12 +96,12 @@ switch (field.type) {

case 'enum':
fieldSchema = FieldSchemas.enum(field, fieldPath);
fieldSchema = FieldSchemas.enum(field, config, fieldPath);
break;
case 'number':
fieldSchema = FieldSchemas.number(field, fieldPath);
fieldSchema = FieldSchemas.number(field, config, fieldPath);
break;
case 'object':
fieldSchema = FieldSchemas.object(field, fieldPath);
fieldSchema = FieldSchemas.object(field, config, fieldPath);
break;
case 'model':
fieldSchema = FieldSchemas.model(field, fieldPath);
fieldSchema = FieldSchemas.model(field, config, fieldPath);
break;

@@ -112,3 +112,3 @@ case 'reference':

case 'list':
fieldSchema = FieldSchemas.list(field, fieldPath);
fieldSchema = FieldSchemas.list(field, config, fieldPath);
break;

@@ -145,10 +145,11 @@ }

},
object: (field, fieldPath) => {
object: (field, config, fieldPath) => {
const childFieldPath = fieldPath.concat('fields');
return joiSchemaForModelFields(field.fields, childFieldPath);
return joiSchemaForModelFields(field.fields, config, childFieldPath);
},
model: (field) => {
model: (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);

@@ -161,4 +162,3 @@ if (field.models.length === 1 && field.models[0]) {

__metadata: metadataSchema,
// TODO: change to objectTypeKey
type: typeSchema
[objectTypeKey]: typeSchema
}));

@@ -170,3 +170,3 @@ }

return joi_1.default.alternatives()
.conditional('.type', {
.conditional(`.${objectTypeKey}`, {
switch: lodash_1.default.map(field.models, (modelName) => {

@@ -179,4 +179,3 @@ return {

__metadata: metadataSchema,
// TODO: change to objectTypeKey
type: joi_1.default.string()
[objectTypeKey]: joi_1.default.string()
}))

@@ -188,3 +187,3 @@ };

messages: {
'alternatives.any': `"{{#label}}.type" is required and must be one of [${field.models.join(', ')}].`
'alternatives.any': `{{#label}}.${objectTypeKey} is required and must be one of [${field.models.join(', ')}].`
},

@@ -197,6 +196,6 @@ errors: { wrap: { label: false } }

reference: () => joi_1.default.string(),
list: (field, fieldPath) => {
list: (field, config, fieldPath) => {
if (field.items) {
const childFieldPath = fieldPath.concat('items');
const itemsSchema = joiSchemaForField(field.items, childFieldPath);
const itemsSchema = joiSchemaForField(field.items, config, childFieldPath);
return joi_1.default.array().items(itemsSchema);

@@ -203,0 +202,0 @@ }

import { ContentItem } from './content-loader';
import { Config } from '../config/config-loader';
import { ContentValidationError } from './content-errors';
import { Config } from '../config/config-types';
interface ContentValidationOptions {

@@ -5,0 +5,0 @@ contentItems: ContentItem[];

@@ -14,3 +14,3 @@ "use strict";

const errors = [];
const joiModelSchemas = content_schema_1.joiSchemasForModels(config.models);
const joiModelSchemas = content_schema_1.joiSchemasForModels(config);
const value = lodash_1.default.map(contentItems, (contentItem) => {

@@ -48,2 +48,8 @@ var _a;

}
else if (utils_1.isDataModel(model)) {
const objectTypeKey = config.objectTypeKey || 'layout';
if (!lodash_1.default.find(model.fields, { name: objectTypeKey })) {
modelSchema = modelSchema.keys({ [objectTypeKey]: joi_1.default.string().valid(model.name) });
}
}
modelSchema = modelSchema.keys({

@@ -50,0 +56,0 @@ __metadata: joi_1.default.object({

export * from './config/config-schema';
export * from './config/config-types';
export * from './config/config-consts';
export * from './content/content-errors';
export { loadConfig, ObjectModel, DataModel, ConfigModel, PageModel, Model, ConfigLoaderOptions, ConfigLoaderResult, Config, ConfigError, ConfigNormalizedValidationError } from './config/config-loader';
export * from './config/config-errors';
export * from './analyzer/file-browser';
export * from './utils';
export { loadConfig, ConfigLoaderOptions, ConfigLoaderResult, ConfigError, ConfigNormalizedValidationError } from './config/config-loader';
export { writeConfig, WriteConfigOptions, convertToYamlConfig } from './config/config-writer';

@@ -10,3 +14,1 @@ export { loadContent, ContentItem, ContentLoaderOptions, ContentLoaderResult } from './content/content-loader';

export { analyzeSite, SiteAnalyzerOptions, SiteAnalyzerResult } from './analyzer/site-analyzer';
export * from './utils';
export * from './analyzer/file-browser';

@@ -15,6 +15,10 @@ "use strict";

__exportStar(require("./config/config-schema"), exports);
__exportStar(require("./config/config-types"), exports);
__exportStar(require("./config/config-consts"), exports);
__exportStar(require("./content/content-errors"), exports);
__exportStar(require("./config/config-errors"), exports);
__exportStar(require("./analyzer/file-browser"), exports);
__exportStar(require("./utils"), exports);
var config_loader_1 = require("./config/config-loader");
Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return config_loader_1.loadConfig; } });
__exportStar(require("./config/config-errors"), exports);
var config_writer_1 = require("./config/config-writer");

@@ -31,4 +35,2 @@ Object.defineProperty(exports, "writeConfig", { enumerable: true, get: function () { return config_writer_1.writeConfig; } });

Object.defineProperty(exports, "analyzeSite", { enumerable: true, get: function () { return site_analyzer_1.analyzeSite; } });
__exportStar(require("./utils"), exports);
__exportStar(require("./analyzer/file-browser"), exports);
//# sourceMappingURL=index.js.map

@@ -1,4 +0,3 @@

import { Model } from '../config/config-loader';
import { YamlModels } from '../config/config-schema';
import { Model, ModelMap } from '../config/config-types';
export declare function extendModels(models: Model[]): Model[];
export declare function extendModelMap(models: YamlModels): YamlModels;
export declare function extendModelMap(models: ModelMap): ModelMap;

@@ -42,4 +42,4 @@ "use strict";

let superModel = lodash_1.default.get(modelsByName, superModelName);
assert(superModel, `model '${modelName}' extends non defined model '${superModelName}'`);
assert(superModel.type === 'object', `only object model types can be extended`);
assert(superModel, `model '${modelName}' extends non existing model '${superModelName}'`);
assert(superModel.type === 'object', `model '${modelName}' extends models of type '${superModel.type}', only model of the 'object' type can be extended`);
superModel = extendModel(superModel, superModelName, modelsByName, _extendPath.concat(modelName));

@@ -51,3 +51,3 @@ utils_1.copyIfNotSet(superModel, 'hideContent', model, 'hideContent');

lodash_1.default.forEach(superModel.fields, (superField) => {
let field = lodash_1.default.find(fields, { name: superField.name });
const field = lodash_1.default.find(fields, { name: superField.name });
if (field) {

@@ -54,0 +54,0 @@ lodash_1.default.defaultsDeep(field, lodash_1.default.cloneDeep(superField));

@@ -1,3 +0,2 @@

import { Model } from '../config/config-loader';
import { Field, FieldListItems, FieldModelProps } from '../config/config-schema';
import { Field, FieldListItems, FieldModelProps, Model } from '../config/config-types';
/**

@@ -4,0 +3,0 @@ * This function invokes the `iteratee` function for every field of the `model`.

@@ -89,3 +89,3 @@ "use strict";

if (!model && !field && !fieldListItem) {
error = `could not match model/field ${modelKeyPath.join('.')} to content at ${valueKeyPath.join('.')}`;
error = `could not match model/field ${modelKeyPath.join('.')} for content at ${valueKeyPath.join('.')}`;
}

@@ -120,4 +120,6 @@ if (field && model_utils_1.isModelField(field)) {

if (lodash_1.default.isPlainObject(value)) {
const modelOrField = model || field || fieldListItem;
const fields = (modelOrField === null || modelOrField === void 0 ? void 0 : modelOrField.fields) || [];
// if fields will not be resolved or the object will have a key that
// doesn't exist among fields, the nested calls to _iterateDeep will
// include an error.
const fields = getFieldsOfModelOrField(model, field, fieldListItem);
const fieldsByName = lodash_1.default.keyBy(fields, 'name');

@@ -208,4 +210,6 @@ modelKeyPath = lodash_1.default.concat(modelKeyPath, 'fields');

if (lodash_1.default.isPlainObject(value)) {
const modelOrField = model || field || fieldListItem;
const fields = (modelOrField === null || modelOrField === void 0 ? void 0 : modelOrField.fields) || [];
// if fields will not be resolved or the object will have a key that
// doesn't exist among fields, the nested calls to _iterateDeep will
// include an error.
const fields = getFieldsOfModelOrField(model, field, fieldListItem);
const fieldsByName = lodash_1.default.keyBy(fields, 'name');

@@ -290,2 +294,14 @@ modelKeyPath = lodash_1.default.concat(modelKeyPath, 'fields');

exports.getModelOfObject = getModelOfObject;
function getFieldsOfModelOrField(model, field, fieldListItems) {
if (model && model.fields) {
return model.fields;
}
else if (field && model_utils_1.isObjectField(field)) {
return field.fields;
}
else if (fieldListItems && model_utils_1.isObjectListItems(fieldListItems)) {
return fieldListItems.fields;
}
return [];
}
//# sourceMappingURL=model-iterators.js.map

@@ -1,2 +0,2 @@

import { Model } from '../config/config-loader';
import { Model } from '../config/config-types';
interface BaseModelQuery {

@@ -17,3 +17,4 @@ filePath: string;

* @param {string} [query.type] The type of the data file. For example, can be page's layout that maps to page's model.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`. Required if `query.type` is provided.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`.
* Required if `query.type` is provided.
* @param {Array.<Object>} models Array of stackbit.yaml `models`.

@@ -42,3 +43,4 @@ * @return {Object} stackbit.yaml model matching the `query`.

* @param {string} [query.type] The type of the data file. For example, can be page's layout that maps to page's model.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`. Required if `query.type` is provided.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`.
* Required if `query.type` is provided.
* @param {Array.<Object>} models Array of stackbit.yaml `models`.

@@ -45,0 +47,0 @@ * @return {Array.<Model>} Array of stackbit.yaml models matching the `query`.

@@ -17,3 +17,4 @@ "use strict";

* @param {string} [query.type] The type of the data file. For example, can be page's layout that maps to page's model.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`. Required if `query.type` is provided.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`.
* Required if `query.type` is provided.
* @param {Array.<Object>} models Array of stackbit.yaml `models`.

@@ -50,3 +51,4 @@ * @return {Object} stackbit.yaml model matching the `query`.

* @param {string} [query.type] The type of the data file. For example, can be page's layout that maps to page's model.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`. Required if `query.type` is provided.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`.
* Required if `query.type` is provided.
* @param {Array.<Object>} models Array of stackbit.yaml `models`.

@@ -53,0 +55,0 @@ * @return {Array.<Model>} Array of stackbit.yaml models matching the `query`.

@@ -1,3 +0,2 @@

import { Model, ConfigModel, DataModel, PageModel, ObjectModel } from '../config/config-loader';
import { Field, FieldModel, FieldObject, FieldReference, FieldList, FieldListItems, FieldObjectProps, FieldListObject, FieldListModel, FieldListReference, FieldReferenceProps, FieldModelProps, FieldEnum } from '../config/config-schema';
import { Model, ObjectModel, DataModel, PageModel, ConfigModel, Field, FieldEnum, FieldList, FieldListItems, FieldListModel, FieldListObject, FieldListReference, FieldModel, FieldModelProps, FieldObject, FieldObjectProps, FieldReference, FieldReferenceProps } from '../config/config-types';
export declare function getModelByName(models: Model[], modelName: string): Model | undefined;

@@ -4,0 +3,0 @@ export declare function isConfigModel(model: Model): model is ConfigModel;

@@ -8,3 +8,3 @@ "use strict";

const lodash_1 = __importDefault(require("lodash"));
const config_schema_1 = require("../config/config-schema");
const config_consts_1 = require("../config/config-consts");
function getModelByName(models, modelName) {

@@ -61,3 +61,3 @@ return models.find((model) => model.name === modelName);

// custom model field types are deprecated
return !config_schema_1.FIELD_TYPES.includes(field.type) && lodash_1.default.has(modelsByName, field.type);
return !config_consts_1.FIELD_TYPES.includes(field.type) && lodash_1.default.has(modelsByName, field.type);
}

@@ -105,3 +105,3 @@ exports.isCustomModelField = isCustomModelField;

// custom model field types are deprecated
return !config_schema_1.FIELD_TYPES.includes(items.type) && (!modelsByName || lodash_1.default.has(modelsByName, items.type));
return !config_consts_1.FIELD_TYPES.includes(items.type) && (!modelsByName || lodash_1.default.has(modelsByName, items.type));
}

@@ -108,0 +108,0 @@ exports.isCustomModelListItems = isCustomModelListItems;

{
"name": "@stackbit/sdk",
"version": "0.2.1",
"version": "0.2.2",
"description": "Stackbit SDK",

@@ -58,2 +58,6 @@ "main": "dist/index.js",

"@types/yargs": "^16.0.0",
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"jest": "^26.6.3",

@@ -60,0 +64,0 @@ "prettier": "2.2.1",

@@ -7,16 +7,40 @@ /**

import { StricterUnion } from '../utils';
import {
FieldCommonProps,
FieldType,
FieldSimpleProps,
FieldEnumProps,
FieldNumberProps,
FieldObjectProps,
FieldModelProps,
FieldNumberProps,
FieldType,
FieldReferenceProps,
FieldSimpleNoProps
} from '../config/config-schema';
FieldListProps,
FieldListItems
} from '../config/config-types';
export type FieldTypeWithUnknown = FieldType | 'unknown';
export type FieldListItemsTypeWithUnknown = Exclude<FieldTypeWithUnknown, 'list'>;
export type FieldWithUnknown =
| FieldUnknown
| FieldSimpleWithUnknown
| FieldEnumWithUnknown
| FieldNumberWithUnknown
| FieldObjectWithUnknown
| FieldModelWithUnknown
| FieldReferenceWithUnknown
| FieldListWithUnknown;
export type FieldUnknown = FieldCommonPropsWithUnknown & FieldUnknownProps;
export type FieldSimpleWithUnknown = FieldCommonPropsWithUnknown & FieldSimpleProps;
export type FieldEnumWithUnknown = FieldCommonPropsWithUnknown & FieldEnumProps;
export type FieldNumberWithUnknown = FieldCommonPropsWithUnknown & FieldNumberProps;
export type FieldObjectWithUnknown = FieldCommonPropsWithUnknown & FieldObjectPropsWithUnknown;
export type FieldModelWithUnknown = FieldCommonPropsWithUnknown & FieldModelProps;
export type FieldReferenceWithUnknown = FieldCommonPropsWithUnknown & FieldReferenceProps;
export type FieldListWithUnknown = FieldCommonPropsWithUnknown & FieldListPropsWithUnknown;
export type FieldCommonPropsWithUnknown = Omit<FieldCommonProps, 'type'> & {
type: FieldTypeWithUnknown;
};
export type FieldTypeWithUnknown = 'unknown' | FieldType;
export interface FieldUnknownProps {

@@ -26,27 +50,26 @@ type: 'unknown';

export interface FieldListPropsWithUnknown {
type: 'list';
export type FieldObjectPropsWithUnknown = Omit<FieldObjectProps, 'fields'> & {
fields: FieldWithUnknown[];
};
export type FieldListPropsWithUnknown = Omit<FieldListProps, 'items'> & {
items?: FieldListItemsWithUnknown;
};
export type FieldListItemsWithUnknown = Exclude<FieldListItems, FieldObjectProps> | FieldUnknownProps | FieldObjectPropsWithUnknown;
export function isObjectWithUnknownField(field: FieldWithUnknown): field is FieldObjectWithUnknown {
return field.type === 'object';
}
export interface FieldObjectPropsWithUnknown {
type: 'object';
labelField?: string;
fields: FieldWithUnknown[];
export function isListWithUnknownField(field: FieldWithUnknown): field is FieldListWithUnknown {
return field.type === 'list';
}
export type NonStrictFieldPartialPropsWithUnknown =
| FieldUnknownProps
| FieldEnumProps
| FieldObjectPropsWithUnknown
| FieldListPropsWithUnknown
| FieldNumberProps
| FieldModelProps
| FieldReferenceProps
| FieldSimpleNoProps;
export function isObjectListItemsWithUnknown(items: FieldListItemsWithUnknown): items is FieldObjectPropsWithUnknown {
return items.type === 'object';
}
export type FieldPartialPropsWithUnknown = StricterUnion<NonStrictFieldPartialPropsWithUnknown>;
export type FieldListItemsWithUnknown = StricterUnion<Exclude<NonStrictFieldPartialPropsWithUnknown, FieldListPropsWithUnknown>>;
export type FieldObjectWithUnknown = FieldObjectPropsWithUnknown & FieldCommonProps;
export type FieldListWithUnknown = FieldListPropsWithUnknown & FieldCommonProps;
export type FieldWithUnknown = FieldPartialPropsWithUnknown & FieldCommonProps;
export function isModelListItemsWithUnknown(items: FieldListItemsWithUnknown): items is FieldModelProps {
return items.type === 'model';
}

@@ -99,3 +99,3 @@ import path from 'path';

const ignoreProperty = findObjectProperty(optionsProperty.value, 'ignore');
let ignoreValue = ignoreProperty ? getNodeValue(ignoreProperty.value) : null;
const ignoreValue = ignoreProperty ? getNodeValue(ignoreProperty.value) : null;
result.push({

@@ -262,2 +262,3 @@ name: nameValue,

// }
// eslint-disable-next-line
const gatsbySourceFilesystemRegExp = /resolve\s*:\s*(['"`])gatsby-source-filesystem\1\s*,\s*options\s*:\s*{\s*(\w+)\s*:\s*(['"`])([^'"`]+)\3\s*,\s*(\w+)\s*:\s*(['"`])([^'"`]+)\6/g;

@@ -264,0 +265,0 @@ let match: RegExpExecArray | null;

@@ -7,3 +7,3 @@ import path from 'path';

import { findDirsWithPackageDependency } from './analyzer-utils';
import { Config } from '../config/config-loader';
import { Config } from '../config/config-types';

@@ -10,0 +10,0 @@ export type CMSMatcherOptions = GetFileBrowserOptions;

@@ -10,10 +10,13 @@ import path from 'path';

import { DATA_FILE_EXTENSIONS, EXCLUDED_DATA_FILES, EXCLUDED_MARKDOWN_FILES, EXCLUDED_COMMON_FILES, MARKDOWN_FILE_EXTENSIONS } from '../consts';
import { DataModel, Model, ObjectModel, PageModel } from '../config/config-loader';
import { Field, FieldType, FieldListItems, FieldModelProps } from '../config/config-schema';
import { Model, ObjectModel, DataModel, PageModel, Field, FieldType, FieldListItems, FieldModelProps } from '../config/config-types';
import {
FieldListItemsWithUnknown,
FieldListPropsWithUnknown,
FieldPartialPropsWithUnknown,
FieldObjectPropsWithUnknown,
FieldTypeWithUnknown,
FieldWithUnknown
FieldWithUnknown,
isListWithUnknownField,
isModelListItemsWithUnknown,
isObjectListItemsWithUnknown,
isObjectWithUnknownField
} from './analyze-schema-types';

@@ -27,3 +30,2 @@

type PartialDataModel = Omit<DataModel, 'label' | 'fields' | 'items'> & { fields?: FieldWithUnknown[]; items?: FieldListItemsWithUnknown; filePaths: string[] };
type PartialModel = PartialPageModel | PartialDataModel;

@@ -147,3 +149,10 @@ const SAME_FOLDER_PAGE_DSC_COEFFICIENT = 0.6;

// TODO: in some projects, pages can be defined as JSON files as well
filePaths = await readDirRecursivelyWithFilter({ fileBrowser, contentDir, ssgMatchResult, excludedFiles, allowedExtensions, excludedFilesInSSGDir });
filePaths = await readDirRecursivelyWithFilter({
fileBrowser,
contentDir,
ssgMatchResult,
excludedFiles,
allowedExtensions,
excludedFilesInSSGDir
});
} else {

@@ -237,2 +246,3 @@ contentDirFromRoot = ssgDir;

}
async function generatePageModelsForFiles({

@@ -245,3 +255,3 @@ filePaths,

}: GeneratePageModelsOptions): Promise<{ pageModels: PartialPageModelWithFilePaths[]; objectModels: PartialObjectModel[] }> {
let pageModels: PartialPageModel[] = [];
const pageModels: PartialPageModel[] = [];
let modelNameCounter = 1;

@@ -290,3 +300,3 @@

const modelsByFolder: Record<string, PartialPageModel[]> = {};
for (let pageModel of pageModels) {
for (const pageModel of pageModels) {
const filePath = pageModel.filePaths[0]!;

@@ -304,3 +314,3 @@ const dir = path.parse(filePath).dir;

// merge page models from same sub-folders (excluding LCA folder) with lowest similarity coefficient
for (let folderPath in modelsByFolder) {
for (const folderPath in modelsByFolder) {
const pageModelsInFolder = modelsByFolder[folderPath]!;

@@ -321,3 +331,3 @@ const mergeResult = mergeSimilarPageModels(pageModelsInFolder, objectModels, SAME_FOLDER_PAGE_DSC_COEFFICIENT);

// remove 'unknown' field type
let pageModelsWithFilePaths = _.reduce(
const pageModelsWithFilePaths = _.reduce(
mergeResult.pageModels,

@@ -357,3 +367,3 @@ (mergedPageModels: PartialPageModelWithFilePaths[], pageModel) => {

for (const filePath of filePaths) {
let data = await fileBrowser.getFileData(path.join(dirPathFromRoot, filePath));
const data = await fileBrowser.getFileData(path.join(dirPathFromRoot, filePath));
const modelName = `data_${modelNameCounter++}`;

@@ -668,3 +678,3 @@ if (_.isPlainObject(data)) {

switch (type) {
case 'unknown':
case 'unknown': {
return {

@@ -674,3 +684,4 @@ items: { type: 'unknown' },

};
case 'number':
}
case 'number': {
const subtypes = _.compact(_.uniq(_.map(listItemModels, 'subtype')));

@@ -685,4 +696,5 @@ const subtype = subtypes.length === 1 ? subtypes[0] : 'float';

};
case 'object':
const fieldsList = _.map(listItemModels, (itemModels) => itemModels.fields!);
}
case 'object': {
const fieldsList = _.map(listItemModels as FieldObjectPropsWithUnknown[], (itemModels) => itemModels.fields!);
const result = consolidateObjectFieldsListWithOverlap(fieldsList, fieldPath, LIST_OBJECT_DSC_COEFFICIENT, objectModels);

@@ -719,3 +731,4 @@ if (result.fieldsList.length === 1) {

}
case 'model':
}
case 'model': {
const modelNames = _.compact(_.uniq(_.flatten(_.map(listItemModels, 'models'))));

@@ -729,2 +742,3 @@ return {

};
}
case 'enum':

@@ -742,5 +756,5 @@ case 'reference':

if (_.every(itemTypes, (itemsType) => ['object', 'model'].includes(itemsType))) {
const modelListItems = _.filter(listItemModels, { type: 'model' });
const modelListItems = _.filter(listItemModels, isModelListItemsWithUnknown);
const modelNames = _.compact(_.uniq(_.flatten(_.map(modelListItems, 'models'))));
const objectListItems = _.filter(listItemModels, { type: 'object' });
const objectListItems = _.filter(listItemModels, isObjectListItemsWithUnknown);
const fieldsList = _.map(objectListItems, (listItems) => listItems.fields!);

@@ -786,3 +800,2 @@ const result = consolidateObjectFieldsListWithOverlap(fieldsList, fieldPath, LIST_OBJECT_DSC_COEFFICIENT, objectModels);

let unmergedFieldsList = _.orderBy(fieldsList.slice(), ['length'], ['desc']);
let idx = 0;
const mergedFieldsList = [];

@@ -796,3 +809,2 @@

objectModels = result.objectModels;
idx++;
}

@@ -840,3 +852,9 @@

}
return { mergedFields, unmergedFieldsList, mergedIndexes, unmergedIndexes, objectModels };
return {
mergedFields,
unmergedFieldsList,
mergedIndexes,
unmergedIndexes,
objectModels
};
}

@@ -894,3 +912,3 @@

const fields = fieldsByName[fieldName]!;
const result = consolidateFields(fields, fieldPath.concat(fieldName), objectModels);
const result = consolidateFields(fields, fieldName, fieldPath.concat(fieldName), objectModels);
// if one of the fields cannot be consolidated, then the object cannot be consolidated as well

@@ -920,5 +938,6 @@ if (!result) {

fields: FieldWithUnknown[],
fieldName: string,
fieldPath: FieldPath,
objectModels: PartialObjectModel[]
): { field: FieldPartialPropsWithUnknown; objectModels: PartialObjectModel[] } | null {
): { field: FieldWithUnknown; objectModels: PartialObjectModel[] } | null {
if (fields.length === 1) {

@@ -941,8 +960,12 @@ const field = fields[0]!;

switch (type) {
case 'unknown':
case 'unknown': {
return {
field: { type: 'unknown' },
field: {
type: 'unknown',
name: fieldName
},
objectModels
};
case 'number':
}
case 'number': {
const subtypes = _.compact(_.uniq(_.map(fields, 'subtype')));

@@ -953,2 +976,3 @@ const subtype = subtypes.length === 1 ? subtypes[0] : 'float';

type: 'number',
name: fieldName,
...(subtype && { subtype })

@@ -958,5 +982,7 @@ },

};
case 'object':
const fieldsList = _.map(fields, (field) => field.fields!);
const mergeResult = mergeObjectFieldsList(fieldsList, fieldPath, objectModels);
}
case 'object': {
const objectWithUnknownFields = _.filter(fields, isObjectWithUnknownField);
const fieldsWithUnknownList = _.compact(_.map(objectWithUnknownFields, (field) => field.fields));
const mergeResult = mergeObjectFieldsList(fieldsWithUnknownList, fieldPath, objectModels);
if (!mergeResult) {

@@ -968,2 +994,3 @@ return null;

type: 'object',
name: fieldName,
fields: mergeResult.fields

@@ -973,5 +1000,7 @@ },

};
case 'list':
const listItemsArr = _.map(fields, (field) => field.items!);
const itemsResult = consolidateListItems(listItemsArr, fieldPath, objectModels);
}
case 'list': {
const listWithUnknownFields = _.filter(fields, isListWithUnknownField);
const listItemsWithUnknownArr = _.compact(_.map(listWithUnknownFields, (field) => field.items));
const itemsResult = consolidateListItems(listItemsWithUnknownArr, fieldPath, objectModels);
if (!itemsResult) {

@@ -983,2 +1012,3 @@ return null;

type: 'list',
name: fieldName,
items: itemsResult.items

@@ -988,5 +1018,5 @@ },

};
}
case 'enum':
case 'model':
// we don't produce 'model' fields as direct child of 'object' fields, only as list items
case 'model': // we don't produce 'model' fields as direct child of 'object' fields, only as list items
case 'reference':

@@ -997,3 +1027,3 @@ // these cases cannot happen because we don't generate these fields,

return {
field: { type },
field: { type, name: fieldName },
objectModels

@@ -1006,3 +1036,3 @@ };

? {
field: { type: fieldType },
field: { type: fieldType, name: fieldName },
objectModels

@@ -1188,14 +1218,5 @@ }

const modelLabel = _.startCase(modelName);
let items: undefined | null | FieldListItems;
let fields: undefined | Field[];
if (dataModelWithFilePaths.isList && dataModelWithFilePaths.items) {
items = removeUnknownTypesFromListItem(dataModelWithFilePaths.items);
if (!items) {
continue;
}
} else {
fields = removeUnknownTypesFromFields(dataModelWithFilePaths.fields!);
if (_.isEmpty(fields)) {
continue;
}
const itemsOrFields = removeUnknownTypesFromDataModel(dataModelWithFilePaths);
if (!itemsOrFields) {
continue;
}

@@ -1207,3 +1228,3 @@ dataModels.push({

file: dataModelWithFilePaths.filePaths[0]!,
...(items ? { isList: true, items } : { fields })
...itemsOrFields
});

@@ -1221,14 +1242,5 @@ } else {

const modelLabel = _.startCase(modelName);
let items: undefined | null | FieldListItems;
let fields: undefined | Field[];
if (dataModelWithFilePaths.isList && dataModelWithFilePaths.items) {
items = removeUnknownTypesFromListItem(dataModelWithFilePaths.items);
if (!items) {
continue;
}
} else {
fields = removeUnknownTypesFromFields(dataModelWithFilePaths.fields!);
if (_.isEmpty(fields)) {
continue;
}
const itemsOrFields = removeUnknownTypesFromDataModel(dataModelWithFilePaths);
if (!itemsOrFields) {
continue;
}

@@ -1240,3 +1252,3 @@ dataModels.push({

folder: folder,
...(items ? { isList: true, items } : { fields })
...itemsOrFields
});

@@ -1248,2 +1260,17 @@ }

function removeUnknownTypesFromDataModel(partialDataModel: PartialDataModel): { isList: true; items: FieldListItems } | { fields: Field[] } | null {
if (partialDataModel.isList && partialDataModel.items) {
const items = removeUnknownTypesFromListItem(partialDataModel.items);
if (items) {
return { isList: true, items };
}
} else {
const fields = removeUnknownTypesFromFields(partialDataModel.fields!);
if (!_.isEmpty(fields)) {
return { fields };
}
}
return null;
}
function removeUnknownTypesFromFields(fields: FieldWithUnknown[]): Field[] {

@@ -1254,5 +1281,6 @@ return _.reduce(

switch (field.type) {
case 'unknown':
case 'unknown': {
return accum;
case 'object':
}
case 'object': {
const fields = removeUnknownTypesFromFields(field.fields!);

@@ -1263,3 +1291,4 @@ if (_.isEmpty(fields)) {

return accum.concat(Object.assign(field, { fields }));
case 'list':
}
case 'list': {
const items = removeUnknownTypesFromListItem(field.items!);

@@ -1270,4 +1299,6 @@ if (!items) {

return accum.concat(Object.assign(field, { items }));
default:
}
default: {
return accum.concat(field);
}
}

@@ -1294,3 +1325,3 @@ },

let commonDir: null | string[] = null;
for (let model of models) {
for (const model of models) {
let dir;

@@ -1308,3 +1339,3 @@ if (model.file) {

} else {
let common: string[] = [];
const common: string[] = [];
let j = 0;

@@ -1362,3 +1393,3 @@ while (j < commonDir.length && j < dir.length && commonDir[j] === dir[j]) {

const dirParts = _.split(dir, path.sep);
let common = [];
const common = [];
let j = 0;

@@ -1365,0 +1396,0 @@ while (j < commonDirParts.length && j < dirParts.length && commonDirParts[j] === dirParts[j]) {

@@ -6,5 +6,4 @@ import _ from 'lodash';

import { CMSMatchResult, matchCMS } from './cms-matcher';
import { Config } from '../config/config-loader';
import { generateSchema, SchemaGeneratorResult } from './schema-generator';
import { AssetsModel } from '../config/config-schema';
import { Assets, Config } from '../config/config-types';

@@ -55,3 +54,3 @@ export type SiteAnalyzerOptions = GetFileBrowserOptions;

function generateAssets(ssgMatchResult: SSGMatchResult | null): AssetsModel | undefined {
function generateAssets(ssgMatchResult: SSGMatchResult | null): Assets | undefined {
if (ssgMatchResult?.assetsReferenceType === 'static' && ssgMatchResult?.staticDir) {

@@ -58,0 +57,0 @@ return {

@@ -7,3 +7,3 @@ import path from 'path';

import { extractNodeEnvironmentVariablesFromFile, findDirsWithPackageDependency, getGatsbySourceFilesystemOptions } from './analyzer-utils';
import { Config } from '../config/config-loader';
import { Config } from '../config/config-types';

@@ -10,0 +10,0 @@ export type SSGMatcherOptions = GetFileBrowserOptions;

@@ -7,14 +7,3 @@ import path from 'path';

import { ConfigValidationError, ConfigValidationResult, validate } from './config-validator';
import {
FieldEnum,
FieldModel,
FieldObjectProps,
YamlConfig,
YamlConfigModel,
YamlDataModel,
YamlModel,
YamlObjectModel,
YamlPageModel
} from './config-schema';
import { ConfigValidationError, ConfigValidationResult, validateConfig, validateContentModels } from './config-validator';
import { ConfigLoadError } from './config-errors';

@@ -34,24 +23,7 @@ import {

isReferenceField,
iterateModelFieldsRecursively,
StricterUnion
iterateModelFieldsRecursively
} from '../utils';
import { append, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
import { Config, FieldEnum, FieldModel, FieldObjectProps, Model, PageModel, YamlModel } from './config-types';
export type BaseModel = {
name: string;
__metadata?: {
filePath?: string;
invalid?: boolean;
};
};
export type ObjectModel = YamlObjectModel & BaseModel;
export type DataModel = YamlDataModel & BaseModel;
export type ConfigModel = YamlConfigModel & BaseModel;
export type PageModel = YamlPageModel & BaseModel;
export type Model = StricterUnion<ObjectModel | DataModel | ConfigModel | PageModel>;
export interface Config extends Omit<YamlConfig, 'models'> {
models: Model[];
}
export interface ConfigNormalizedValidationError extends ConfigValidationError {

@@ -73,4 +45,15 @@ normFieldPath: (string | number)[];

export interface NormalizedValidationResult {
config: Config;
valid: boolean;
errors: ConfigNormalizedValidationError[];
}
export interface TempConfigLoaderResult {
config?: any;
errors: ConfigLoadError[];
}
export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
let configLoadResult;
let configLoadResult: TempConfigLoaderResult;
try {

@@ -94,16 +77,38 @@ configLoadResult = await loadConfigFromDir(dirPath);

const config = normalizeConfig(configLoadResult.config);
const validationResult = validate(config);
convertModelCategoriesToModels(validationResult);
const convertedResult = convertModelsToArray(validationResult);
const errors = [...configLoadResult.errors, ...convertedResult.errors];
const normalizedResult = validateAndNormalizeConfig(configLoadResult.config);
return {
valid: validationResult.valid,
config: convertedResult.config,
errors: errors
valid: normalizedResult.valid,
config: normalizedResult.config,
errors: [...configLoadResult.errors, ...normalizedResult.errors]
};
}
async function loadConfigFromDir(dirPath: string): Promise<{ config?: any; errors: ConfigLoadError[] }> {
let { config, error } = await loadConfigFromStackbitYaml(dirPath);
export function validateAndNormalizeConfig(config: any): NormalizedValidationResult {
// validate the "contentModels" and extend config models with "contentModels"
// this must be done before main config validation to make it independent of "contentModels".
const contentModelsValidationResult = validateAndExtendContentModels(config);
config = contentModelsValidationResult.value;
// extend config models having the "extends" property
// this must be done before main config validation as some properties like
// the labelField will not work when validating models without extending them first
config.models = extendModelMap(config?.models || {});
// extend config - backward compatibility updates, adding extra fields like "markdown_content", "type" and "layout",
// and setting other default values.
config = extendConfig(config);
// validate config
const configValidationResult = validateConfig(config);
const errors = [...contentModelsValidationResult.errors, ...configValidationResult.errors];
return normalizeValidationResult({
valid: _.isEmpty(errors),
value: configValidationResult.value,
errors: errors
});
}
async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResult> {
const { config, error } = await loadConfigFromStackbitYaml(dirPath);
if (error) {

@@ -195,2 +200,3 @@ return { errors: [error] };

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function loadConfigFromDotStackbit(dirPath: string) {

@@ -229,3 +235,3 @@ const stackbitDotPath = path.join(dirPath, '.stackbit');

function normalizeConfig(config: any): any {
function extendConfig(config: any): any {
const pageLayoutKey = _.get(config, 'pageLayoutKey', 'layout');

@@ -236,12 +242,5 @@ const objectTypeKey = _.get(config, 'objectTypeKey', 'type');

const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
let models = config?.models || {};
const models = config?.models || {};
let referencedModelNames: string[] = [];
try {
models = extendModelMap(models);
} catch (error) {
// TODO: gracefully extend and return error rather throwing
throw error;
}
_.forEach(models, (model) => {

@@ -276,3 +275,3 @@ if (!model) {

iterateModelFieldsRecursively(model, (field: any, fieldPath) => {
iterateModelFieldsRecursively(model, (field: any) => {
// add field label if label is not set

@@ -444,20 +443,81 @@ if (!_.has(field, 'label')) {

function convertModelCategoriesToModels(validationResult: ConfigValidationResult) {
const models = validationResult.value?.models ?? {};
function validateAndExtendContentModels(config: any) {
const contentModels = config.contentModels ?? {};
const models = config.models ?? {};
const categoryMap = _.reduce(models, (categoryMap, model, modelName) => {
if (!model.categories) {
return categoryMap;
if (!contentModels) {
return config;
}
const validationResult = validateContentModels(contentModels, models);
const extendedModels = _.mapValues(models, (model, modelName) => {
const contentModel = validationResult.value.contentModels[modelName];
if (!contentModel) {
return {
// if a model does not define a type, use the default "object" type
type: model.type || 'object',
..._.omit(model, 'type')
};
}
let key = model?.type === 'object' ? 'objectModels' : 'documentModels';
_.forEach(model.categories, (categoryName) => {
append(categoryMap, [categoryName, key], modelName);
});
delete model.categories;
return categoryMap;
}, {} as Record<string, { objectModels?: string[], documentModels?: string[] }>);
if (_.get(contentModel, '__metadata.invalid')) {
return model;
}
if (contentModel.isPage && (!model.type || ['object', 'page'].includes(model.type))) {
return {
type: 'page',
...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
..._.omit(contentModel, ['isPage', 'newFilePath']),
..._.omit(model, 'type')
};
} else if (!contentModel.isPage && (!model.type || ['object', 'data'].includes(model.type))) {
return {
type: 'data',
...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
..._.omit(contentModel, ['newFilePath']),
..._.omit(model, 'type')
};
} else {
return model;
}
});
_.forEach(categoryMap, (category, categoryName) => {
_.forEach(category, (modelCategory, key) => {
_.set(category, key, _.uniq(modelCategory));
return {
valid: validationResult.valid,
value: {
..._.omit(config, ['contentModels', 'models']),
models: extendedModels
},
errors: validationResult.errors
};
}
function normalizeValidationResult(validationResult: ConfigValidationResult): NormalizedValidationResult {
convertModelGroupsToModelList(validationResult);
return convertModelsToArray(validationResult);
}
function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
const models = validationResult.value?.models ?? {};
const groupMap = _.reduce(
models,
(groupMap, model, modelName) => {
if (!model.groups) {
return groupMap;
}
const key = model?.type === 'object' ? 'objectModels' : 'documentModels';
_.forEach(model.groups, (groupName) => {
append(groupMap, [groupName, key], modelName);
});
delete model.groups;
return groupMap;
},
{} as Record<string, { objectModels?: string[]; documentModels?: string[] }>
);
// update groups to have unique model names
_.forEach(groupMap, (group) => {
_.forEach(group, (modelGroup, key) => {
_.set(group, key, _.uniq(modelGroup));
});

@@ -471,3 +531,3 @@ });

}
if (field.categories) {
if (field.groups) {
let key: string | null = null;

@@ -480,15 +540,19 @@ if (isModelField(field)) {

if (key) {
field.models = _.reduce(field.categories, (modelNames, categoryName) => {
const objectModelNames = _.get(categoryMap, [categoryName, key], []);
return _.uniq(modelNames.concat(objectModelNames));
}, field.models || []);
field.models = _.reduce(
field.groups,
(modelNames, groupName) => {
const objectModelNames = _.get(groupMap, [groupName, key], []);
return _.uniq(modelNames.concat(objectModelNames));
},
field.models || []
);
}
delete field.categories;
delete field.groups;
}
});
})
});
}
function convertModelsToArray(validationResult: ConfigValidationResult): { config: Config; errors: ConfigNormalizedValidationError[] } {
const config = _.cloneDeep(validationResult.value);
function convertModelsToArray(validationResult: ConfigValidationResult): NormalizedValidationResult {
const config = validationResult.value;

@@ -499,3 +563,3 @@ // in stackbit.yaml 'models' are defined as object where keys are the model names,

const modelMap = config.models ?? {};
let modelArray: Model[] = _.map(
const modelArray: Model[] = _.map(
modelMap,

@@ -528,2 +592,3 @@ (yamlModel: YamlModel, modelName: string): Model => {

return {
valid: validationResult.valid,
config: {

@@ -530,0 +595,0 @@ ...config,

import Joi from 'joi';
import _ from 'lodash';
import { append } from '@stackbit/utils';
import { StricterUnion } from '../utils';
import { CMS_NAMES, FIELD_TYPES, SSG_NAMES } from './config-consts';
import {
Assets,
ContentfulImport,
Field,
FieldObjectProps,
ModelsSource,
SanityImport,
YamlBaseModel,
YamlConfig,
YamlConfigModel,
YamlDataModel,
YamlModel,
ModelMap,
YamlPageModel,
FieldGroupItem,
YamlObjectModel,
ContentModelMap,
ContentModel
} from './config-types';
// SSGs Stackbit Stuio supports
const SSG_NAMES = ['unibit', 'jekyll', 'hugo', 'gatsby', 'nextjs', 'custom', 'eleventy', 'vuepress', 'gridsome', 'nuxt', 'sapper', 'hexo'] as const;
function getConfigFromValidationState(state: Joi.State): YamlConfig {
return _.last(state.ancestors)!;
}
// CMSes Stackbit Stuio supports
const CMS_NAMES = ['git', 'contentful', 'sanity', 'forestry', 'netlifycms'] as const;
function getModelsFromValidationState(state: Joi.State): ModelMap {
const config = getConfigFromValidationState(state);
return config.models ?? {};
}
export const FIELD_TYPES = [
'string',
'url',
'slug',
'text',
'markdown',
'html',
'number',
'boolean',
'enum',
'date',
'datetime',
'color',
'image',
'file',
'object',
'model',
'reference',
'list'
] as const;
export type FieldType = typeof FIELD_TYPES[number];
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.';
'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.string()

@@ -48,4 +48,3 @@ .required()

const validObjectModelNames = Joi.custom((value, { error, state }) => {
const config: YamlConfig = _.last(state.ancestors)!;
const models = config.models ?? {};
const models = getModelsFromValidationState(state);
const modelNames = Object.keys(models);

@@ -65,5 +64,4 @@ const objectModelNames = modelNames.filter((modelName) => models[modelName]!.type === 'object');

const documentModelNameErrorCode = 'model.not.document.model';
const validPageOrDataModelNames = Joi.custom((value, { error, state }) => {
const config: YamlConfig = _.last(state.ancestors)!;
const models = config.models ?? {};
const validReferenceModelNames = Joi.custom((value, { error, state }) => {
const models = getModelsFromValidationState(state);
const modelNames = Object.keys(models);

@@ -82,21 +80,22 @@ const documentModels = modelNames.filter((modelName) => ['page', 'data'].includes(models[modelName]!.type));

const categoryNotFoundErrorCode = 'category.not.found';
const categoryNotObjectModelErrorCode = 'category.not.object.model';
const validModelFieldCategories = Joi.string()
.custom((category, { error, state }) => {
const config: YamlConfig = _.last(state.ancestors)!;
const categoryModels = getModelNamesForCategory(category, config);
if (!_.isEmpty(categoryModels.documentModels)) {
return error(categoryNotObjectModelErrorCode, { nonObjectModels: categoryModels.documentModels.join(', ') });
const groupNotFoundErrorCode = 'group.not.found';
const groupNotObjectModelErrorCode = 'group.not.object.model';
const validModelFieldGroups = Joi.string()
.custom((group, { error, state }) => {
const config = getConfigFromValidationState(state);
const groupModels = getModelNamesForGroup(group, config);
if (!_.isEmpty(groupModels.documentModels)) {
return error(groupNotObjectModelErrorCode, { nonObjectModels: groupModels.documentModels.join(', ') });
}
if (_.isEmpty(categoryModels.objectModels)) {
return error(categoryNotFoundErrorCode);
if (_.isEmpty(groupModels.objectModels)) {
return error(groupNotFoundErrorCode);
}
return category;
return group;
})
.prefs({
messages: {
[categoryNotObjectModelErrorCode]:
'{{#label}} of a "model" field must reference a category with only models of type "object", the "{{#value}}" category includes models of type "page" or "data" ({{#nonObjectModels}})',
[categoryNotFoundErrorCode]: '{{#label}} of a "model" field must reference the name of an existing category, got "{{#value}}"'
[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}}"'
},

@@ -106,20 +105,21 @@ errors: { wrap: { label: false } }

const categoryNotDocumentModelErrorCode = 'category.not.document.model';
const validReferenceFieldCategories = Joi.string()
.custom((category, { error, state }) => {
const config: YamlConfig = _.last(state.ancestors)!;
const categoryModels = getModelNamesForCategory(category, config);
if (!_.isEmpty(categoryModels.objectModels)) {
return error(categoryNotDocumentModelErrorCode, { nonDocumentModels: categoryModels.objectModels.join(', ') });
const groupNotDocumentModelErrorCode = 'group.not.document.model';
const validReferenceFieldGroups = Joi.string()
.custom((group, { error, state }) => {
const config = getConfigFromValidationState(state);
const groupModels = getModelNamesForGroup(group, config);
if (!_.isEmpty(groupModels.objectModels)) {
return error(groupNotDocumentModelErrorCode, { nonDocumentModels: groupModels.objectModels.join(', ') });
}
if (_.isEmpty(categoryModels.documentModels)) {
return error(categoryNotFoundErrorCode);
if (_.isEmpty(groupModels.documentModels)) {
return error(groupNotFoundErrorCode);
}
return category;
return group;
})
.prefs({
messages: {
[categoryNotDocumentModelErrorCode]:
'{{#label}} of a "reference" field must reference a category with only models of type "page" or "data", the "{{#value}}" category includes models of type "object" ({{#nonDocumentModels}})',
[categoryNotFoundErrorCode]: '{{#label}} of a "reference" field must reference the name of an existing category, got "{{#value}}"'
[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}}"'
},

@@ -129,3 +129,3 @@ errors: { wrap: { label: false } }

function getModelNamesForCategory(category: string, config: YamlConfig) {
function getModelNamesForGroup(group: string, config: YamlConfig) {
const models = config.models ?? {};

@@ -135,3 +135,3 @@ return _.reduce(

(result: { objectModels: string[]; documentModels: string[] }, model, modelName) => {
if (model?.categories && _.includes(model.categories, category)) {
if (model?.groups && _.includes(model.groups, group)) {
if (model?.type === 'object') {

@@ -149,4 +149,2 @@ result.objectModels.push(modelName);

export type LogicField = string;
const logicField = Joi.string();

@@ -206,11 +204,2 @@ // TODO: validate that all logicFields reference existing fields

export interface ContentfulImport {
type: 'contentful';
contentFile: string;
uploadAssets?: boolean;
assetsDirectory?: string;
spaceIdEnvVar?: string;
accessTokenEnvVar?: string;
}
const contentfulImportSchema = Joi.object<ContentfulImport>({

@@ -225,13 +214,2 @@ type: Joi.string().valid('contentful').required(),

export interface SanityImport {
type: 'sanity';
contentFile: string;
sanityStudioPath: string;
deployStudio?: boolean;
deployGraphql?: boolean;
projectIdEnvVar?: string;
datasetEnvVar?: string;
tokenEnvVar?: string;
}
const sanityImportSchema = Joi.object<SanityImport>({

@@ -248,4 +226,2 @@ type: Joi.string().valid('sanity').required(),

export type Import = ContentfulImport | SanityImport;
const importSchema = Joi.alternatives().conditional('.type', {

@@ -258,23 +234,2 @@ switch: [

export interface StaticAssetsModal {
referenceType: 'static';
assetsDir?: string;
staticDir: string;
publicPath: string;
uploadDir?: string;
}
export interface RelativeAssetsModal {
referenceType: 'relative';
assetsDir: string;
staticDir?: string;
publicPath?: string;
uploadDir?: string;
}
export interface ModelsSource {
type: 'files';
modelDirs: string[];
}
const modelsSourceSchema = Joi.object<ModelsSource>({

@@ -285,5 +240,3 @@ type: 'files',

export type AssetsModel = StricterUnion<StaticAssetsModal | RelativeAssetsModal>;
const assetsSchema = Joi.object<AssetsModel>({
const assetsSchema = Joi.object<Assets>({
referenceType: Joi.string().valid('static', 'relative').required(),

@@ -305,119 +258,5 @@ assetsDir: Joi.string().allow('').when('referenceType', {

export interface FieldCommonProps {
name: string;
label?: string;
description?: string;
required?: boolean;
default?: unknown;
group?: string;
const?: unknown;
hidden?: boolean;
readOnly?: boolean;
}
export type FieldEnumOptionValue = string | number;
export interface FieldEnumOptionObject {
label: string;
value: FieldEnumOptionValue;
}
export interface FieldEnumOptionThumbnails extends FieldEnumOptionObject {
thumbnail?: string;
}
export interface FieldEnumOptionPalette extends FieldEnumOptionObject {
textColor?: string;
backgroundColor?: string;
borderColor?: string;
}
export interface FieldEnumDropdownProps {
type: 'enum';
controlType?: 'dropdown' | 'button-group';
options: FieldEnumOptionValue[] | FieldEnumOptionObject[];
}
export interface FieldEnumThumbnailsProps {
type: 'enum';
controlType: 'thumbnails';
options: FieldEnumOptionThumbnails[];
}
export interface FieldEnumPaletteProps {
type: 'enum';
controlType: 'palette';
options: FieldEnumOptionPalette[];
}
export type FieldEnumProps = FieldEnumDropdownProps | FieldEnumThumbnailsProps | FieldEnumPaletteProps;
export interface FieldObjectProps {
type: 'object';
labelField?: string;
thumbnail?: string;
variantField?: string;
fieldGroups?: string;
fields: Field[];
}
export interface FieldListProps {
type: 'list';
items?: FieldListItems;
}
export interface FieldNumberProps {
type: 'number';
subtype?: 'int' | 'float';
min?: number;
max?: number;
step?: number;
}
export interface FieldModelProps {
type: 'model';
models: string[];
categories?: string[];
}
export interface FieldReferenceProps {
type: 'reference';
models: string[];
categories?: string[];
}
export interface FieldSimpleNoProps {
type: Exclude<FieldType, 'enum' | 'number' | 'object' | 'model' | 'reference' | 'list'>;
}
type NonStrictFieldPartialProps =
| FieldEnumProps
| FieldObjectProps
| FieldListProps
| FieldNumberProps
| FieldModelProps
| FieldReferenceProps
| FieldSimpleNoProps;
export type FieldPartialProps = StricterUnion<NonStrictFieldPartialProps>;
export type FieldListItems = StricterUnion<Exclude<NonStrictFieldPartialProps, FieldListProps>>;
export type SimpleField = FieldSimpleNoProps & FieldCommonProps;
export type FieldEnum = FieldEnumProps & FieldCommonProps;
export type FieldNumber = FieldNumberProps & FieldCommonProps;
export type FieldObject = FieldObjectProps & FieldCommonProps;
export type FieldModel = FieldModelProps & FieldCommonProps;
export type FieldReference = FieldReferenceProps & FieldCommonProps;
export type FieldList = FieldListProps & FieldCommonProps;
export type FieldListObject = FieldList & { items?: FieldObjectProps };
export type FieldListModel = FieldList & { items?: FieldModelProps };
export type FieldListReference = FieldList & { items?: FieldReferenceProps };
export type Field = FieldPartialProps & FieldCommonProps;
const fieldGroupsSchema = Joi.array()
.items(
Joi.object({
Joi.object<FieldGroupItem>({
name: Joi.string().required(),

@@ -526,4 +365,7 @@ label: Joi.string().required()

type: Joi.string().valid('model').required(),
models: Joi.array().items(validObjectModelNames).when('categories', { not: Joi.exist(), then: Joi.required() }),
categories: Joi.array().items(validModelFieldCategories)
models: Joi.array().items(validObjectModelNames).when('groups', {
not: Joi.exist(),
then: Joi.required()
}),
groups: Joi.array().items(validModelFieldGroups)
});

@@ -533,4 +375,7 @@

type: Joi.string().valid('reference').required(),
models: Joi.array().items(validPageOrDataModelNames).when('categories', { not: Joi.exist(), then: Joi.required() }),
categories: Joi.array().items(validReferenceFieldCategories)
models: Joi.array().items(validReferenceModelNames).when('groups', {
not: Joi.exist(),
then: Joi.required()
}),
groups: Joi.array().items(validReferenceFieldGroups)
});

@@ -559,3 +404,6 @@

const partialFieldWithListSchema = partialFieldSchema.when('.type', { is: 'list', then: listFieldPartialSchema });
const partialFieldWithListSchema = partialFieldSchema.when('.type', {
is: 'list',
then: listFieldPartialSchema
});

@@ -566,29 +414,54 @@ const fieldSchema: Joi.ObjectSchema<Field> = fieldCommonPropsSchema.concat(partialFieldWithListSchema);

export interface FieldGroupItem {
name: string;
label: string;
}
const contentModelKeyNotFound = 'contentModel.model.not.found';
const contentModelTypeNotPage = 'contentModel.type.not.page';
const contentModelTypeNotData = 'contentModel.type.not.data';
const contentModelSchema = Joi.object<ContentModel>({
isPage: Joi.boolean(),
newFilePath: Joi.string(),
file: Joi.string(),
folder: Joi.string(),
match: Joi.array().items(Joi.string()).single(),
exclude: Joi.array().items(Joi.string()).single()
})
.without('file', ['folder', 'match', 'exclude'])
.when('.isPage', {
is: true,
then: Joi.object({
urlPath: Joi.string(),
hideContent: Joi.boolean()
})
})
.custom((contentModel, { error, state, prefs }) => {
const models = _.get(prefs, 'context.models');
const modelName = _.last(state.path)!;
const model = models[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 } }
});
export interface YamlBaseModel {
__metadata?: {
filePath?: string;
};
label: string;
description?: string;
thumbnail?: string;
extends?: string | string[];
labelField?: string;
variantField?: string;
categories?: string[];
fieldGroups?: FieldGroupItem[];
fields?: Field[];
}
export const contentModelsSchema = Joi.object({
contentModels: Joi.object<ContentModelMap>().pattern(Joi.string(), contentModelSchema)
});
interface BaseMatch {
folder?: string;
match?: string | string[];
exclude?: string | string[];
}
const baseModelSchema = Joi.object({
const baseModelSchema = Joi.object<YamlBaseModel>({
__metadata: Joi.object({

@@ -598,3 +471,6 @@ filePath: Joi.string()

type: Joi.string().valid('page', 'data', 'config', 'object').required(),
label: Joi.string().required().when(Joi.ref('/import'), { is: Joi.exist(), then: Joi.optional() }),
label: Joi.string().required().when(Joi.ref('/import'), {
is: Joi.exist(),
then: Joi.optional()
}),
description: Joi.string(),

@@ -605,3 +481,3 @@ thumbnail: Joi.string(),

variantField: variantFieldSchema,
categories: Joi.array().items(Joi.string()),
groups: Joi.array().items(Joi.string()),
fieldGroups: fieldGroupsSchema,

@@ -611,7 +487,3 @@ fields: Joi.link('#fieldsSchema')

export interface YamlObjectModel extends YamlBaseModel {
type: 'object';
}
const objectModelSchema = baseModelSchema.concat(
const objectModelSchema: Joi.ObjectSchema<YamlObjectModel> = baseModelSchema.concat(
Joi.object({

@@ -622,28 +494,2 @@ type: Joi.string().valid('object').required()

export interface BaseDataModel extends YamlBaseModel {
type: 'data';
}
export interface BaseDataModelFileSingle extends BaseDataModel {
file: string;
isList?: false;
}
export interface BaseDataModelFileList extends Omit<BaseDataModel, 'fields'> {
file: string;
isList: true;
items: FieldListItems;
}
export interface BaseDataModelMatchSingle extends BaseDataModel, BaseMatch {
isList?: false;
}
export interface BaseDataModelMatchList extends Omit<BaseDataModel, 'fields'>, BaseMatch {
isList: true;
items: FieldListItems;
}
export type YamlDataModel = StricterUnion<BaseDataModelFileSingle | BaseDataModelFileList | BaseDataModelMatchSingle | BaseDataModelMatchList>;
const dataModelSchema: Joi.ObjectSchema<YamlDataModel> = baseModelSchema

@@ -653,2 +499,3 @@ .concat(

type: Joi.string().valid('data').required(),
filePath: Joi.string(),
file: Joi.string(),

@@ -677,6 +524,2 @@ folder: Joi.string(),

export interface YamlConfigModel extends YamlBaseModel {
type: 'config';
file?: string;
}
const configModelSchema: Joi.ObjectSchema<YamlConfigModel> = baseModelSchema.concat(

@@ -689,21 +532,2 @@ Joi.object({

export interface BasePageModel extends YamlBaseModel {
type: 'page';
layout?: string;
urlPath?: string;
filePath?: string;
hideContent?: boolean;
}
export interface PageModelSingle extends BasePageModel {
singleInstance: true;
file: string;
}
export interface PageModelMatch extends BasePageModel, BaseMatch {
singleInstance?: false;
}
export type YamlPageModel = StricterUnion<PageModelSingle | PageModelMatch>;
const pageModelSchema: Joi.ObjectSchema<YamlPageModel> = baseModelSchema

@@ -735,4 +559,2 @@ .concat(

export type YamlModel = StricterUnion<YamlObjectModel | YamlDataModel | YamlConfigModel | YamlPageModel>;
const modelSchema = Joi.object<YamlModel>({

@@ -756,15 +578,13 @@ type: Joi.string().valid('page', 'data', 'config', 'object').required()

const fieldNameUnique = 'field.name.unique';
const categoryModelsIncompatibleError = 'category.models.incompatible';
const groupModelsIncompatibleError = 'group.models.incompatible';
export type YamlModels = Record<string, YamlModel>;
const modelsSchema = Joi.object<YamlModels>()
const modelsSchema = Joi.object<ModelMap>()
.pattern(modelNamePattern, modelSchema)
.custom((models: YamlModels, { error, state }) => {
const categoryMap: Record<string, Record<'objectModels' | 'documentModels', string[]>> = {};
.custom((models: ModelMap, { error }) => {
const groupMap: Record<string, Record<'objectModels' | 'documentModels', string[]>> = {};
_.forEach(models, (model, modelName) => {
let key = model?.type === 'object' ? 'objectModels' : 'documentModels';
_.forEach(model.categories, (categoryName) => {
append(categoryMap, [categoryName, key], modelName);
const key = model?.type === 'object' ? 'objectModels' : 'documentModels';
_.forEach(model.groups, (groupName) => {
append(groupMap, [groupName, key], modelName);
});

@@ -774,9 +594,9 @@ });

const errors = _.reduce(
categoryMap,
(errors: string[], category, categoryName) => {
if (category.objectModels && category.documentModels) {
const objectModels = category.objectModels.join(', ');
const documentModels = category.documentModels.join(', ');
groupMap,
(errors: string[], group, groupName) => {
if (group.objectModels && group.documentModels) {
const objectModels = group.objectModels.join(', ');
const documentModels = group.documentModels.join(', ');
errors.push(
`category "${categoryName}" include models of type "object" (${objectModels}) and objects of type "page" or "data" (${documentModels})`
`group "${groupName}" include models of type "object" (${objectModels}) and objects of type "page" or "data" (${documentModels})`
);

@@ -790,3 +610,3 @@ }

if (!_.isEmpty(errors)) {
return error(categoryModelsIncompatibleError, { incompatibleCategories: errors.join(', ') });
return error(groupModelsIncompatibleError, { incompatibleGroups: errors.join(', ') });
}

@@ -822,6 +642,7 @@

messages: {
[categoryModelsIncompatibleError]:
'Model categories must include models of the same type. The following categories have incompatible models: {{#incompatibleCategories}}',
[groupModelsIncompatibleError]:
'Model groups must include models of the same type. The following groups have incompatible models: {{#incompatibleGroups}}',
[modelNamePatternMatchErrorCode]:
'Invalid model name "{{#key}}" at "{{#label}}". A model name must contain only alphanumeric characters and underscores, must start with a letter, and end with alphanumeric character.',
'Invalid model name "{{#key}}" at "{{#label}}". A model name must contain only alphanumeric characters ' +
'and underscores, must start with a letter, and end with alphanumeric character.',
[modelFileExclusiveErrorCode]: '{{#label}} cannot be used with "file"',

@@ -836,26 +657,3 @@ [modelIsListItemsRequiredErrorCode]: '{{#label}} is required when "isList" is true',

export interface YamlConfig {
stackbitVersion: string;
ssgName?: typeof SSG_NAMES[number];
ssgVersion?: string;
nodeVersion?: string;
devCommand?: string;
cmsName?: typeof CMS_NAMES[number];
import?: Import;
buildCommand?: string;
publishDir?: string;
staticDir?: string;
uploadDir?: string;
assets?: AssetsModel;
pagesDir?: string | null;
dataDir?: string | null;
pageLayoutKey?: string | null;
objectTypeKey?: string;
excludePages?: string | string[];
logicFields?: LogicField[];
modelsSource?: ModelsSource;
models?: YamlModels;
}
const schema = Joi.object<YamlConfig>({
export const stackbitConfigSchema = Joi.object<YamlConfig>({
stackbitVersion: Joi.string().required(),

@@ -896,3 +694,1 @@ ssgName: Joi.string().valid(...SSG_NAMES),

.shared(fieldsSchema);
export const stackbitConfigSchema = schema;
import _ from 'lodash';
import { stackbitConfigSchema, YamlModel } from './config-schema';
import { Model } from './config-loader';
import Joi from 'joi';
import { stackbitConfigSchema, contentModelsSchema } from './config-schema';
export interface ConfigValidationError {

@@ -20,8 +21,40 @@ name: 'ConfigValidationError';

export function validate(config: any): ConfigValidationResult {
export function validateConfig(config: any): ConfigValidationResult {
const validationOptions = { abortEarly: false };
const validationResult = stackbitConfigSchema.validate(config, validationOptions);
const value = validationResult.value;
const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
const valid = _.isEmpty(errors);
markInvalidModels(value, errors, 'models');
return {
value,
valid,
errors
};
}
export function validateContentModels(contentModels: any, models: any): ConfigValidationResult {
const validationResult = contentModelsSchema.validate(
{ contentModels: contentModels },
{
abortEarly: false,
context: {
models: models
}
}
);
const value = validationResult.value;
const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
const valid = _.isEmpty(errors);
markInvalidModels(value, errors, 'contentModels');
return {
value,
valid,
errors
};
}
function mapJoiErrorsToConfigValidationErrors(validationResult: Joi.ValidationResult): ConfigValidationError[] {
const joiErrors = validationResult.error?.details || [];
const errors = joiErrors.map(
return joiErrors.map(
(validationError): ConfigValidationError => {

@@ -37,14 +70,7 @@ return {

);
markInvalidModels(value, errors);
const valid = _.isEmpty(errors);
return {
value,
valid,
errors
};
}
function markInvalidModels(config: any, errors: ConfigValidationError[]) {
const invalidModelNames = getInvalidModelNames(errors);
const models = config.models ?? {};
function markInvalidModels(config: any, errors: ConfigValidationError[], configKey: string) {
const invalidModelNames = getInvalidModelNames(errors, configKey);
const models = config[configKey] ?? {};
_.forEach(models, (model: any, modelName: string): any => {

@@ -57,3 +83,3 @@ if (invalidModelNames.includes(modelName)) {

function getInvalidModelNames(errors: ConfigValidationError[]) {
function getInvalidModelNames(errors: ConfigValidationError[], configKey: string) {
// get array of invalid model names by iterating errors and filtering these

@@ -64,3 +90,3 @@ // having fieldPath starting with ['models', modelName]

(modelNames: string[], error: ConfigValidationError) => {
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
if (error.fieldPath[0] === configKey && typeof error.fieldPath[1] == 'string') {
const modelName = error.fieldPath[1];

@@ -67,0 +93,0 @@ modelNames.push(modelName);

@@ -6,4 +6,4 @@ import path from 'path';

import { Config, Model } from './config-loader';
import { YamlConfig, YamlModel, YamlModels } from './config-schema';
import { Config, Model, YamlConfig, YamlModel, ModelMap } from './config-types';
const packageJson = require('../../package.json');

@@ -20,3 +20,5 @@

const yamlString = yaml.dump(yamlConfig);
const info = `# This file was generated by @stackbit/sdk v${packageJson.version}\n# To learn more about stackbit.yaml please visit https://www.stackbit.com/docs/stackbit-yaml/\n`;
const info =
`# This file was generated by @stackbit/sdk v${packageJson.version}\n` +
'# To learn more about stackbit.yaml please visit https://www.stackbit.com/docs/stackbit-yaml/\n';
const data = info + yamlString;

@@ -31,3 +33,3 @@ await fse.outputFile(filePath, data);

config.models,
(yamlModels: YamlModels, model: Model) => {
(yamlModels: ModelMap, model: Model) => {
const yamlModel = _.omit(model, ['name', '__metadata']) as YamlModel;

@@ -34,0 +36,0 @@ if (yamlModel.type === 'page' && !yamlModel.hideContent && yamlModel.fields) {

@@ -7,4 +7,2 @@ import _ from 'lodash';

import { Config, ConfigModel, Model } from '../config/config-loader';
import { Field, FieldListItems, FieldModelProps } from '../config/config-schema';
import { FileForModelNotFoundError, FileMatchedMultipleModelsError, FileNotMatchedModelError, FileReadError, FolderReadError } from './content-errors';

@@ -25,2 +23,3 @@ import {

import { DATA_FILE_EXTENSIONS, EXCLUDED_DATA_FILES, EXCLUDED_MARKDOWN_FILES, EXCLUDED_COMMON_FILES, MARKDOWN_FILE_EXTENSIONS } from '../consts';
import { Config, ConfigModel, Field, FieldListItems, FieldModelProps, Model } from '../config/config-types';

@@ -27,0 +26,0 @@ interface BaseMetadata {

import Joi from 'joi';
import _ from 'lodash';
import { Model } from '../config/config-loader';
import { isDataModel, isPageModel } from '../utils';
import {
Config,
Field,
FieldEnumOptionObject,
FieldEnumOptionValue,
FieldEnumProps,
FieldListItems,
FieldListProps,
FieldModelProps,
FieldNumberProps,
FieldEnumProps,
FieldEnumOptionValue,
FieldEnumOptionObject,
FieldObjectProps,
FieldModelProps,
FieldReferenceProps,
FieldListProps,
FieldListItems
} from '..';
import { isDataModel, isPageModel } from '../utils';
Model
} from '../config/config-types';

@@ -29,5 +30,5 @@ type FieldPath = (string | number)[];

export function joiSchemasForModels(models: Model[]) {
export function joiSchemasForModels(config: Config) {
const modelSchemas = _.reduce(
models,
config.models,
(modelSchemas: ModelSchemaMap, model: Model) => {

@@ -47,3 +48,3 @@ let joiSchema: Joi.ObjectSchema;

} else {
joiSchema = joiSchemaForModel(model);
joiSchema = joiSchemaForModel(model, config);
}

@@ -80,13 +81,13 @@ modelSchemas[model.name] = joiSchema.id(`${model.name}_model_schema`);

export function joiSchemaForModel(model: Model) {
export function joiSchemaForModel(model: Model, config: Config) {
if (isDataModel(model) && model.isList) {
return Joi.object({
items: Joi.array().items(joiSchemaForField(model.items, [model.name, 'items']))
items: Joi.array().items(joiSchemaForField(model.items, config, [model.name, 'items']))
});
} else {
return joiSchemaForModelFields(model.fields, [model.name]);
return joiSchemaForModelFields(model.fields, config, [model.name]);
}
}
function joiSchemaForModelFields(fields: Field[] | undefined, fieldPath: FieldPath) {
function joiSchemaForModelFields(fields: Field[] | undefined, config: Config, fieldPath: FieldPath) {
return Joi.object(

@@ -97,3 +98,3 @@ _.reduce(

const childFieldPath = fieldPath.concat(`[name='${field.name}']`);
schema[field.name] = joiSchemaForField(field, childFieldPath);
schema[field.name] = joiSchemaForField(field, config, childFieldPath);
return schema;

@@ -106,3 +107,3 @@ },

function joiSchemaForField(field: Field | FieldListItems, fieldPath: FieldPath) {
function joiSchemaForField(field: Field | FieldListItems, config: Config, fieldPath: FieldPath) {
let fieldSchema;

@@ -129,12 +130,12 @@ switch (field.type) {

case 'enum':
fieldSchema = FieldSchemas.enum(field, fieldPath);
fieldSchema = FieldSchemas.enum(field, config, fieldPath);
break;
case 'number':
fieldSchema = FieldSchemas.number(field, fieldPath);
fieldSchema = FieldSchemas.number(field, config, fieldPath);
break;
case 'object':
fieldSchema = FieldSchemas.object(field, fieldPath);
fieldSchema = FieldSchemas.object(field, config, fieldPath);
break;
case 'model':
fieldSchema = FieldSchemas.model(field, fieldPath);
fieldSchema = FieldSchemas.model(field, config, fieldPath);
break;

@@ -145,3 +146,3 @@ case 'reference':

case 'list':
fieldSchema = FieldSchemas.list(field, fieldPath);
fieldSchema = FieldSchemas.list(field, config, fieldPath);
break;

@@ -166,3 +167,3 @@ }

const FieldSchemas: { [fieldType in keyof FieldPropsByType]: (field: FieldPropsByType[fieldType], fieldPath: FieldPath) => Joi.Schema } = {
const FieldSchemas: { [fieldType in keyof FieldPropsByType]: (field: FieldPropsByType[fieldType], config: Config, fieldPath: FieldPath) => Joi.Schema } = {
enum: (field) => {

@@ -190,10 +191,11 @@ if (field.options) {

},
object: (field, fieldPath: FieldPath) => {
object: (field, config, fieldPath: FieldPath) => {
const childFieldPath = fieldPath.concat('fields');
return joiSchemaForModelFields(field.fields, childFieldPath);
return joiSchemaForModelFields(field.fields, config, childFieldPath);
},
model: (field) => {
model: (field, config) => {
if (field.models.length === 0) {
return Joi.any().forbidden();
}
const objectTypeKey = config.objectTypeKey || 'type';
const typeSchema = Joi.string().valid(...field.models);

@@ -207,4 +209,3 @@ if (field.models.length === 1 && field.models[0]) {

__metadata: metadataSchema,
// TODO: change to objectTypeKey
type: typeSchema
[objectTypeKey]: typeSchema
})

@@ -216,3 +217,3 @@ );

return Joi.alternatives()
.conditional('.type', {
.conditional(`.${objectTypeKey}`, {
switch: _.map(field.models, (modelName) => {

@@ -226,4 +227,3 @@ return {

__metadata: metadataSchema,
// TODO: change to objectTypeKey
type: Joi.string()
[objectTypeKey]: Joi.string()
})

@@ -236,3 +236,3 @@ )

messages: {
'alternatives.any': `"{{#label}}.type" is required and must be one of [${field.models.join(', ')}].`
'alternatives.any': `{{#label}}.${objectTypeKey} is required and must be one of [${field.models.join(', ')}].`
},

@@ -245,6 +245,6 @@ errors: { wrap: { label: false } }

reference: () => Joi.string(),
list: (field, fieldPath: FieldPath) => {
list: (field, config, fieldPath: FieldPath) => {
if (field.items) {
const childFieldPath = fieldPath.concat('items');
const itemsSchema = joiSchemaForField(field.items, childFieldPath);
const itemsSchema = joiSchemaForField(field.items, config, childFieldPath);
return Joi.array().items(itemsSchema);

@@ -251,0 +251,0 @@ }

@@ -5,6 +5,6 @@ import Joi from 'joi';

import { ContentItem } from './content-loader';
import { Config } from '../config/config-loader';
import { joiSchemasForModels } from './content-schema';
import { ContentValidationError } from './content-errors';
import { getModelByName, isConfigModel, isPageModel } from '../utils';
import { getModelByName, isConfigModel, isDataModel, isPageModel } from '../utils';
import { Config } from '../config/config-types';

@@ -25,3 +25,3 @@ interface ContentValidationOptions {

const joiModelSchemas = joiSchemasForModels(config.models);
const joiModelSchemas = joiSchemasForModels(config);

@@ -60,2 +60,7 @@ const value = _.map(

}
} else if (isDataModel(model)) {
const objectTypeKey = config.objectTypeKey || 'layout';
if (!_.find(model.fields, { name: objectTypeKey })) {
modelSchema = modelSchema.keys({ [objectTypeKey]: Joi.string().valid(model.name) });
}
}

@@ -62,0 +67,0 @@ modelSchema = modelSchema.keys({

export * from './config/config-schema';
export * from './config/config-types';
export * from './config/config-consts';
export * from './content/content-errors';
export {
loadConfig,
ObjectModel,
DataModel,
ConfigModel,
PageModel,
Model,
ConfigLoaderOptions,
ConfigLoaderResult,
Config,
ConfigError,
ConfigNormalizedValidationError
} from './config/config-loader';
export * from './config/config-errors';
export * from './analyzer/file-browser';
export * from './utils';
export { loadConfig, ConfigLoaderOptions, ConfigLoaderResult, ConfigError, ConfigNormalizedValidationError } from './config/config-loader';
export { writeConfig, WriteConfigOptions, convertToYamlConfig } from './config/config-writer';

@@ -22,3 +14,1 @@ export { loadContent, ContentItem, ContentLoaderOptions, ContentLoaderResult } from './content/content-loader';

export { analyzeSite, SiteAnalyzerOptions, SiteAnalyzerResult } from './analyzer/site-analyzer';
export * from './utils';
export * from './analyzer/file-browser';
import _ from 'lodash';
import { copyIfNotSet } from '@stackbit/utils';
import { Model } from '../config/config-loader';
import { YamlModel, YamlModels } from '../config/config-schema';
import { Model, YamlModel, ModelMap } from '../config/config-types';

@@ -16,3 +15,3 @@ export function extendModels(models: Model[]): Model[] {

export function extendModelMap(models: YamlModels): YamlModels {
export function extendModelMap(models: ModelMap): ModelMap {
const memorized = _.memoize(extendModel, (model: YamlModel, modelName: string) => modelName);

@@ -47,4 +46,7 @@ return _.mapValues(models, (model, modelName) => {

let superModel = _.get(modelsByName, superModelName);
assert(superModel, `model '${modelName}' extends non defined model '${superModelName}'`);
assert(superModel.type === 'object', `only object model types can be extended`);
assert(superModel, `model '${modelName}' extends non existing model '${superModelName}'`);
assert(
superModel.type === 'object',
`model '${modelName}' extends models of type '${superModel.type}', only model of the 'object' type can be extended`
);
superModel = extendModel(superModel, superModelName, modelsByName, _extendPath.concat(modelName));

@@ -56,3 +58,3 @@ copyIfNotSet(superModel, 'hideContent', model, 'hideContent');

_.forEach(superModel.fields, (superField) => {
let field = _.find(fields, { name: superField.name });
const field = _.find(fields, { name: superField.name });
if (field) {

@@ -59,0 +61,0 @@ _.defaultsDeep(field, _.cloneDeep(superField));

import _ from 'lodash';
import { Model } from '../config/config-loader';
import { Field, FieldListItems, FieldModelProps } from '../config/config-schema';
import { getListItemsField, isListDataModel, isListField, isObjectListItems, isModelField, isObjectField, isModelListItems } from './model-utils';
import { Field, FieldListItems, FieldModelProps, Model } from '../config/config-types';

@@ -116,3 +115,3 @@ /**

if (!model && !field && !fieldListItem) {
error = `could not match model/field ${modelKeyPath.join('.')} to content at ${valueKeyPath.join('.')}`;
error = `could not match model/field ${modelKeyPath.join('.')} for content at ${valueKeyPath.join('.')}`;
}

@@ -149,4 +148,6 @@

if (_.isPlainObject(value)) {
const modelOrField = model || field || fieldListItem;
const fields = modelOrField?.fields || [];
// if fields will not be resolved or the object will have a key that
// doesn't exist among fields, the nested calls to _iterateDeep will
// include an error.
const fields = getFieldsOfModelOrField(model, field, fieldListItem);
const fieldsByName = _.keyBy(fields, 'name');

@@ -270,4 +271,6 @@ modelKeyPath = _.concat(modelKeyPath, 'fields');

if (_.isPlainObject(value)) {
const modelOrField = model || field || fieldListItem;
const fields = modelOrField?.fields || [];
// if fields will not be resolved or the object will have a key that
// doesn't exist among fields, the nested calls to _iterateDeep will
// include an error.
const fields = getFieldsOfModelOrField(model, field, fieldListItem);
const fieldsByName = _.keyBy(fields, 'name');

@@ -365,1 +368,12 @@ modelKeyPath = _.concat(modelKeyPath, 'fields');

}
function getFieldsOfModelOrField(model: Model | null, field: Field | null, fieldListItems: FieldListItems | null): Field[] {
if (model && model.fields) {
return model.fields;
} else if (field && isObjectField(field)) {
return field.fields;
} else if (fieldListItems && isObjectListItems(fieldListItems)) {
return fieldListItems.fields;
}
return [];
}
import micromatch from 'micromatch';
import _ from 'lodash';
import { Model } from '../config/config-loader';
import { FileMatchedMultipleModelsError, FileNotMatchedModelError } from '../content/content-errors';
import { Model } from '../config/config-types';

@@ -25,3 +25,4 @@ interface BaseModelQuery {

* @param {string} [query.type] The type of the data file. For example, can be page's layout that maps to page's model.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`. Required if `query.type` is provided.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`.
* Required if `query.type` is provided.
* @param {Array.<Object>} models Array of stackbit.yaml `models`.

@@ -57,3 +58,4 @@ * @return {Object} stackbit.yaml model matching the `query`.

* @param {string} [query.type] The type of the data file. For example, can be page's layout that maps to page's model.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`. Required if `query.type` is provided.
* @param {Array|string} [query.modelTypeKeyPath] Used to compare the value of `query.type` with the value of a model at `modelTypeKeyPath`.
* Required if `query.type` is provided.
* @param {Array.<Object>} models Array of stackbit.yaml `models`.

@@ -60,0 +62,0 @@ * @return {Array.<Model>} Array of stackbit.yaml models matching the `query`.

import _ from 'lodash';
import { Model, ConfigModel, DataModel, PageModel, ObjectModel } from '../config/config-loader';
import { FIELD_TYPES } from '../config/config-consts';
import {
FIELD_TYPES,
Model,
ObjectModel,
DataModel,
PageModel,
ConfigModel,
Field,
FieldModel,
FieldObject,
FieldReference,
FieldEnum,
FieldList,
FieldListItems,
FieldObjectProps,
FieldListModel,
FieldListObject,
FieldListModel,
FieldListReference,
FieldReferenceProps,
FieldModel,
FieldModelProps,
FieldEnum
} from '../config/config-schema';
FieldObject,
FieldObjectProps,
FieldReference,
FieldReferenceProps
} from '../config/config-types';

@@ -21,0 +25,0 @@ export function getModelByName(models: Model[], modelName: string): Model | undefined {

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc