Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@stackbit/sdk

Package Overview
Dependencies
Maintainers
16
Versions
422
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.1.19 to 0.2.0

dist/config/config-errors.d.ts

10

dist/config/config-loader.d.ts
import { ConfigValidationError } from './config-validator';
import { YamlConfigModel, YamlDataModel, YamlObjectModel, YamlPageModel, YamlConfig } from './config-schema';
import { YamlConfig, YamlConfigModel, YamlDataModel, YamlObjectModel, YamlPageModel } from './config-schema';
import { ConfigLoadError } from './config-errors';
import { StricterUnion } from '../utils';

@@ -7,2 +8,3 @@ export declare type BaseModel = {

__metadata?: {
filePath?: string;
invalid?: boolean;

@@ -22,8 +24,2 @@ };

}
export interface ConfigLoadError {
name: 'ConfigLoadError';
message: string;
internalError?: Error;
normFieldPath?: undefined;
}
export declare type ConfigError = ConfigLoadError | ConfigNormalizedValidationError;

@@ -30,0 +26,0 @@ export interface ConfigLoaderOptions {

@@ -13,8 +13,9 @@ "use strict";

const config_validator_1 = require("./config-validator");
const config_errors_1 = require("./config-errors");
const utils_1 = require("../utils");
const utils_2 = require("@stackbit/utils");
async function loadConfig({ dirPath }) {
let config;
let configLoadResult;
try {
config = await loadConfigFromDir(dirPath);
configLoadResult = await loadConfigFromDir(dirPath);
}

@@ -25,31 +26,20 @@ catch (error) {

config: null,
errors: [
{
name: 'ConfigLoadError',
message: `Error loading Stackbit configuration: ${error.message}`,
internalError: error
}
]
errors: [new config_errors_1.ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
};
}
if (!config) {
if (!configLoadResult.config) {
return {
valid: false,
config: null,
errors: [
{
name: 'ConfigLoadError',
message: 'stackbit.yaml not found, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/'
}
]
errors: configLoadResult.errors
};
}
config = normalizeConfig(config);
const config = normalizeConfig(configLoadResult.config);
const validationResult = config_validator_1.validate(config);
const normalizedConfig = convertToTypedConfig(validationResult);
const normalizedErrors = normalizeErrors(normalizedConfig, validationResult.errors);
const convertedResult = convertModelsToArray(validationResult);
const errors = [...configLoadResult.errors, ...convertedResult.errors];
return {
valid: validationResult.valid,
config: normalizedConfig,
errors: normalizedErrors
config: convertedResult.config,
errors: errors
};

@@ -59,9 +49,9 @@ }

async function loadConfigFromDir(dirPath) {
let config = await loadConfigFromStackbitYaml(dirPath);
if (!config) {
return null;
let { config, error } = await loadConfigFromStackbitYaml(dirPath);
if (error) {
return { errors: [error] };
}
const models = await loadExternalModels(dirPath, config);
config.models = lodash_1.default.assign(models, config.models);
return config;
const externalModelsResult = await loadExternalModels(dirPath, config);
config.models = lodash_1.default.assign(externalModelsResult.models, config.models);
return { config, errors: externalModelsResult.errors };
}

@@ -72,3 +62,5 @@ async function loadConfigFromStackbitYaml(dirPath) {

if (!stackbitYamlExists) {
return null;
return {
error: new config_errors_1.ConfigLoadError('stackbit.yaml was not found, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/')
};
}

@@ -78,5 +70,7 @@ const stackbitYaml = await fs_extra_1.default.readFile(stackbitYamlPath);

if (!config || typeof config !== 'object') {
return null;
return {
error: new config_errors_1.ConfigLoadError('error parsing stackbit.yaml, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/')
};
}
return config;
return { config };
}

@@ -98,13 +92,28 @@ async function loadExternalModels(dirPath, config) {

}, []);
return utils_2.reducePromise(modelFiles, async (models, modelFile) => {
const model = await utils_2.parseFile(path_1.default.join(dirPath, modelFile));
return utils_2.reducePromise(modelFiles, async (result, modelFile) => {
let model;
try {
model = await utils_2.parseFile(path_1.default.join(dirPath, modelFile));
}
catch (error) {
return {
models: result.models,
errors: result.errors.concat(new config_errors_1.ConfigLoadError(`error parsing model, file: ${modelFile}`))
};
}
const modelName = model.name;
if (!modelName) {
return models;
return {
models: result.models,
errors: result.errors.concat(new config_errors_1.ConfigLoadError(`model does not have a name, file: ${modelFile}`))
};
}
models[modelName] = lodash_1.default.omit(model, 'name');
return models;
}, {});
result.models[modelName] = lodash_1.default.omit(model, 'name');
result.models[modelName].__metadata = {
filePath: modelFile
};
return result;
}, { models: {}, errors: [] });
}
return null;
return { models: {}, errors: [] };
}

@@ -161,2 +170,3 @@ async function readModelFilesFromDir(modelsDir) {

lodash_1.default.forEach(models, (model) => {
var _a;
if (!model) {

@@ -184,3 +194,5 @@ return;

}
normalizeThumbnailPathForModel(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) => {
var _a;
// add field label if label is not set

@@ -199,2 +211,3 @@ if (!lodash_1.default.has(field, 'label')) {

utils_1.assignLabelFieldIfNeeded(field);
normalizeThumbnailPathForModel(field, (_a = model === null || model === void 0 ? void 0 : model.__metadata) === null || _a === void 0 ? void 0 : _a.filePath);
}

@@ -240,2 +253,4 @@ else if (utils_1.isCustomModelField(field, models)) {

// TODO: update schema-editor to not show type field
// TODO: do not add objectTypeKey field to models, API/container should
// be able to add it automatically when data object or polymorphic nested model is added
addObjectTypeKeyField(model, objectTypeKey, modelName);

@@ -286,2 +301,7 @@ });

}
function normalizeThumbnailPathForModel(modelOrField, filePath) {
if (modelOrField.thumbnail && filePath) {
modelOrField.thumbnail = path_1.default.join(path_1.default.dirname(filePath), modelOrField.thumbnail);
}
}
/**

@@ -313,36 +333,19 @@ * Returns model names referenced by polymorphic 'model' and 'reference' fields.

}
function convertToTypedConfig(validationResult) {
function convertModelsToArray(validationResult) {
var _a;
const config = lodash_1.default.cloneDeep(validationResult.value);
const invalidModelNames = lodash_1.default.reduce(validationResult.errors, (modelNames, error) => {
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
const modelName = error.fieldPath[1];
modelNames.push(modelName);
}
return modelNames;
}, []);
// in stackbit.yaml 'models' are defined as object where keys are model names,
// convert 'models' to array of objects while 'name' property set to the
// in stackbit.yaml 'models' are defined as object where keys are the model names,
// convert 'models' to array of objects and set their 'name' property to the
// model name
const modelMap = (_a = config.models) !== null && _a !== void 0 ? _a : {};
let models = lodash_1.default.map(modelMap, (yamlModel, modelName) => {
const model = {
let modelArray = lodash_1.default.map(modelMap, (yamlModel, modelName) => {
return {
name: modelName,
...yamlModel
};
if (invalidModelNames.includes(modelName)) {
lodash_1.default.set(model, '__metadata.invalid', true);
}
return model;
});
return {
...config,
models: models
};
}
function normalizeErrors(config, errors) {
return lodash_1.default.map(errors, (error) => {
const convertedErrors = lodash_1.default.map(validationResult.errors, (error) => {
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
const modelName = error.fieldPath[1];
const modelIndex = lodash_1.default.findIndex(config.models, { name: modelName });
const modelIndex = lodash_1.default.findIndex(modelArray, { name: modelName });
const normFieldPath = error.fieldPath.slice();

@@ -360,3 +363,10 @@ normFieldPath[1] = modelIndex;

});
return {
config: {
...config,
models: modelArray
},
errors: convertedErrors
};
}
//# sourceMappingURL=config-loader.js.map

@@ -52,2 +52,3 @@ import Joi from 'joi';

default?: unknown;
group?: string;
const?: unknown;

@@ -65,2 +66,3 @@ hidden?: boolean;

type: 'enum';
controlType?: 'dropdown' | 'button-group' | 'thumbnails' | 'palette';
options: FieldSchemaEnumOptions;

@@ -71,2 +73,5 @@ }

labelField?: string;
thumbnail?: string;
fieldGroups?: string;
variantField?: string;
fields: Field[];

@@ -116,7 +121,17 @@ }

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;
fieldGroups?: FieldGroupItem[];
variantField?: string;
fields?: Field[];

@@ -123,0 +138,0 @@ }

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

const objectModelNameErrorCode = 'model.name.of.object.models';
const documentModelNameErrorCode = 'model.name.of.document.models';
const validObjectModelNames = joi_1.default.custom((value, { error, state }) => {

@@ -54,3 +53,9 @@ var _a;

return value;
}).prefs({
messages: {
[objectModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "object", got "{{#value}}"'
},
errors: { wrap: { label: false } }
});
const documentModelNameErrorCode = 'model.name.of.document.models';
const validPageOrDataModelNames = joi_1.default.custom((value, { error, state }) => {

@@ -65,2 +70,7 @@ var _a;

return value;
}).prefs({
messages: {
[documentModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "page" or "data", got "{{#value}}"'
},
errors: { wrap: { label: false } }
});

@@ -72,10 +82,50 @@ const logicField = joi_1.default.string();

// });
const inFields = joi_1.default.string()
.valid(joi_1.default.in('fields', {
adjust: (fields) => (lodash_1.default.isArray(fields) ? fields.map((field) => field.name) : [])
}))
.prefs({
messages: { 'any.only': '{{#label}} must be one of model field names, got "{{#value}}"' },
const labelFieldNotFoundError = 'labelField.not.found';
const labelFieldNotSimple = 'labelField.not.simple';
const labelFieldSchema = joi_1.default.custom((value, { error, state }) => {
var _a;
const modelOrObjectField = lodash_1.default.head(state.ancestors);
const fields = (_a = modelOrObjectField === null || modelOrObjectField === void 0 ? void 0 : modelOrObjectField.fields) !== null && _a !== void 0 ? _a : [];
if (!lodash_1.default.isArray(fields)) {
return error(labelFieldNotFoundError);
}
const field = lodash_1.default.find(fields, (field) => field.name === value);
if (!field) {
return error(labelFieldNotFoundError);
}
if (['object', 'model', 'reference', 'list'].includes(field.type)) {
return error(labelFieldNotSimple, { fieldType: field.type });
}
return value;
}).prefs({
messages: {
[labelFieldNotFoundError]: '{{#label}} must be one of model field names, got "{{#value}}"',
[labelFieldNotSimple]: '{{#label}} can not reference complex field, got "{{#value}}" field of type "{{#fieldType}}"'
},
errors: { wrap: { label: false } }
});
const variantFieldNotFoundError = 'variantField.not.found';
const variantFieldNotEnum = 'variantField.not.enum';
const variantFieldSchema = joi_1.default.custom((value, { error, state }) => {
var _a;
const modelOrObjectField = lodash_1.default.head(state.ancestors);
const fields = (_a = modelOrObjectField === null || modelOrObjectField === void 0 ? void 0 : modelOrObjectField.fields) !== null && _a !== void 0 ? _a : [];
if (!lodash_1.default.isArray(fields)) {
return error(variantFieldNotFoundError);
}
const field = lodash_1.default.find(fields, (field) => field.name === value);
if (!field) {
return error(variantFieldNotFoundError);
}
if (field.type !== 'enum') {
return error(variantFieldNotEnum, { fieldType: field.type });
}
return value;
}).prefs({
messages: {
[variantFieldNotFoundError]: '{{#label}} must be one of model field names, got "{{#value}}"',
[variantFieldNotEnum]: '{{#label}} should reference "enum" field, got "{{#value}}" field of type "{{#fieldType}}"'
},
errors: { wrap: { label: false } }
});
const contentfulImportSchema = joi_1.default.object({

@@ -125,2 +175,27 @@ type: joi_1.default.string().valid('contentful').required(),

});
const fieldGroupsSchema = joi_1.default.array()
.items(joi_1.default.object({
name: joi_1.default.string().required(),
label: joi_1.default.string().required()
}))
.unique('name')
.prefs({
messages: {
'array.unique': '{{#label}} contains a duplicate group name "{{#value.name}}"'
},
errors: { wrap: { label: false } }
});
const inGroups = joi_1.default.string()
.valid(
// 4 dots "...." =>
// ".." for the parent field where "group" property is defined
// + "." for the fields array
// + "." for the parent model
joi_1.default.in('....fieldGroups', {
adjust: (groups) => (lodash_1.default.isArray(groups) ? groups.map((group) => group.name) : [])
}))
.prefs({
messages: { 'any.only': '{{#label}} must be one of model field groups, got "{{#value}}"' },
errors: { wrap: { label: false } }
});
const fieldCommonPropsSchema = joi_1.default.object({

@@ -135,2 +210,3 @@ type: joi_1.default.string()

default: joi_1.default.any(),
group: inGroups,
const: joi_1.default.any(),

@@ -147,12 +223,35 @@ hidden: joi_1.default.boolean(),

});
const enumFieldBaseOptionSchema = joi_1.default.object({
label: joi_1.default.string().required(),
value: joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.number()).required()
});
const enumFieldPartialSchema = joi_1.default.object({
type: joi_1.default.string().valid('enum').required(),
options: joi_1.default.alternatives()
.try(joi_1.default.array().items(joi_1.default.string(), joi_1.default.number()), joi_1.default.array().items(joi_1.default.object({
label: joi_1.default.string().required(),
value: joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.number()).required()
})))
controlType: joi_1.default.string().valid('dropdown', 'button-group', 'thumbnails', 'palette'),
options: joi_1.default.any()
.when('..controlType', {
switch: [
{
is: 'thumbnails',
then: joi_1.default.array().items(enumFieldBaseOptionSchema.append({
thumbnail: joi_1.default.string().required()
}))
},
{
is: 'palette',
then: joi_1.default.array().items(enumFieldBaseOptionSchema.append({
textColor: joi_1.default.string(),
backgroundColor: joi_1.default.string(),
borderColor: joi_1.default.string()
}))
}
],
otherwise: joi_1.default.alternatives().try(joi_1.default.array().items(joi_1.default.string(), joi_1.default.number()), joi_1.default.array().items(enumFieldBaseOptionSchema))
})
.required()
.prefs({
messages: { 'alternatives.types': '{{#label}} must be an array of strings or numbers, or array of objects with label and value properties' },
messages: {
'alternatives.types': '{{#label}} must be an array of strings or numbers, or an array of objects with label and value properties',
'alternatives.match': '{{#label}} must be an array of strings or numbers, or an array of objects with label and value properties'
},
errors: { wrap: { label: false } }

@@ -163,3 +262,6 @@ })

type: joi_1.default.string().valid('object').required(),
labelField: inFields,
labelField: labelFieldSchema,
thumbnail: joi_1.default.string(),
fieldGroups: fieldGroupsSchema,
variantField: variantFieldSchema,
fields: joi_1.default.link('#fieldsSchema').required()

@@ -197,7 +299,13 @@ });

const baseModelSchema = joi_1.default.object({
__metadata: joi_1.default.object({
filePath: joi_1.default.string()
}),
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() }),
description: joi_1.default.string(),
thumbnail: joi_1.default.string(),
extends: joi_1.default.array().items(validObjectModelNames).single(),
labelField: inFields,
labelField: labelFieldSchema,
fieldGroups: fieldGroupsSchema,
variantField: variantFieldSchema,
fields: joi_1.default.link('#fieldsSchema')

@@ -312,4 +420,2 @@ });

[modelListForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is not true',
[objectModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "object", got "{{#value}}"',
[documentModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "page" or "data", got "{{#value}}"',
[fieldNameUnique]: '{{#label}} contains a duplicate field name "{{#value.name}}"'

@@ -316,0 +422,0 @@ },

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

fieldPath: (string | number)[];
normFieldPath?: (string | number)[];
value?: any;

@@ -8,0 +9,0 @@ }

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

});
markInvalidModels(value, errors);
const valid = lodash_1.default.isEmpty(errors);

@@ -34,2 +35,23 @@ return {

exports.validate = validate;
function markInvalidModels(config, errors) {
var _a;
const invalidModelNames = getInvalidModelNames(errors);
const models = (_a = config.models) !== null && _a !== void 0 ? _a : {};
lodash_1.default.forEach(models, (model, modelName) => {
if (invalidModelNames.includes(modelName)) {
lodash_1.default.set(model, '__metadata.invalid', true);
}
});
}
function getInvalidModelNames(errors) {
// 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') {
const modelName = error.fieldPath[1];
modelNames.push(modelName);
}
return modelNames;
}, []);
}
//# sourceMappingURL=config-validator.js.map
export * from './config/config-schema';
export * from './content/content-errors';
export { loadConfig, ObjectModel, DataModel, ConfigModel, PageModel, Model, ConfigLoaderOptions, ConfigLoaderResult, Config, ConfigError, ConfigLoadError, ConfigNormalizedValidationError } from './config/config-loader';
export { loadConfig, ObjectModel, DataModel, ConfigModel, PageModel, Model, ConfigLoaderOptions, ConfigLoaderResult, Config, ConfigError, ConfigNormalizedValidationError } from './config/config-loader';
export * from './config/config-errors';
export { writeConfig, WriteConfigOptions, convertToYamlConfig } from './config/config-writer';

@@ -5,0 +6,0 @@ export { loadContent, ContentItem, ContentLoaderOptions, ContentLoaderResult } from './content/content-loader';

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

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");

@@ -20,0 +21,0 @@ Object.defineProperty(exports, "writeConfig", { enumerable: true, get: function () { return config_writer_1.writeConfig; } });

{
"name": "@stackbit/sdk",
"version": "0.1.19",
"version": "0.2.0",
"description": "Stackbit SDK",

@@ -5,0 +5,0 @@ "main": "dist/index.js",

@@ -7,18 +7,19 @@ import path from 'path';

import { validate, ConfigValidationResult, ConfigValidationError } from './config-validator';
import { YamlConfigModel, YamlDataModel, YamlModel, YamlObjectModel, YamlPageModel, YamlConfig, Field, FieldModel, FieldListModel } from './config-schema';
import { ConfigValidationError, ConfigValidationResult, validate } from './config-validator';
import { FieldModel, FieldObjectProps, YamlConfig, YamlConfigModel, YamlDataModel, YamlModel, YamlObjectModel, YamlPageModel } from './config-schema';
import { ConfigLoadError } from './config-errors';
import {
assignLabelFieldIfNeeded,
extendModelMap,
getListItemsField,
isCustomModelField,
isListDataModel,
isListField,
isModelField,
isObjectField,
isObjectListItems,
isObjectField,
StricterUnion,
isCustomModelField,
isModelField,
isPageModel,
isReferenceField,
getListItemsField,
assignLabelFieldIfNeeded,
extendModelMap,
isPageModel,
isListField,
iterateModelFieldsRecursively
iterateModelFieldsRecursively,
StricterUnion
} from '../utils';

@@ -30,2 +31,3 @@ import { append, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';

__metadata?: {
filePath?: string;
invalid?: boolean;

@@ -48,9 +50,2 @@ };

export interface ConfigLoadError {
name: 'ConfigLoadError';
message: string;
internalError?: Error;
normFieldPath?: undefined;
}
export type ConfigError = ConfigLoadError | ConfigNormalizedValidationError;

@@ -69,5 +64,5 @@

export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
let config;
let configLoadResult;
try {
config = await loadConfigFromDir(dirPath);
configLoadResult = await loadConfigFromDir(dirPath);
} catch (error) {

@@ -77,51 +72,44 @@ return {

config: null,
errors: [
{
name: 'ConfigLoadError',
message: `Error loading Stackbit configuration: ${error.message}`,
internalError: error
}
]
errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
};
}
if (!config) {
if (!configLoadResult.config) {
return {
valid: false,
config: null,
errors: [
{
name: 'ConfigLoadError',
message: 'stackbit.yaml not found, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/'
}
]
errors: configLoadResult.errors
};
}
config = normalizeConfig(config);
const config = normalizeConfig(configLoadResult.config);
const validationResult = validate(config);
const normalizedConfig = convertToTypedConfig(validationResult);
const normalizedErrors = normalizeErrors(normalizedConfig, validationResult.errors);
const convertedResult = convertModelsToArray(validationResult);
const errors = [...configLoadResult.errors, ...convertedResult.errors];
return {
valid: validationResult.valid,
config: normalizedConfig,
errors: normalizedErrors
config: convertedResult.config,
errors: errors
};
}
async function loadConfigFromDir(dirPath: string) {
let config = await loadConfigFromStackbitYaml(dirPath);
if (!config) {
return null;
async function loadConfigFromDir(dirPath: string): Promise<{ config?: any; errors: ConfigLoadError[] }> {
let { config, error } = await loadConfigFromStackbitYaml(dirPath);
if (error) {
return { errors: [error] };
}
const models = await loadExternalModels(dirPath, config);
config.models = _.assign(models, config.models);
return config;
const externalModelsResult = await loadExternalModels(dirPath, config);
config.models = _.assign(externalModelsResult.models, config.models);
return { config, errors: externalModelsResult.errors };
}
async function loadConfigFromStackbitYaml(dirPath: string): Promise<any> {
type LoadConfigFromStackbitYamlResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFromStackbitYamlResult> {
const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
if (!stackbitYamlExists) {
return null;
return {
error: new ConfigLoadError('stackbit.yaml was not found, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/')
};
}

@@ -131,5 +119,7 @@ const stackbitYaml = await fse.readFile(stackbitYamlPath);

if (!config || typeof config !== 'object') {
return null;
return {
error: new ConfigLoadError('error parsing stackbit.yaml, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/')
};
}
return config;
return { config };
}

@@ -158,15 +148,29 @@

modelFiles,
async (models: any, modelFile) => {
const model = await parseFile(path.join(dirPath, modelFile));
async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
let model;
try {
model = await parseFile(path.join(dirPath, modelFile));
} catch (error) {
return {
models: result.models,
errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
};
}
const modelName = model.name;
if (!modelName) {
return models;
return {
models: result.models,
errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
};
}
models[modelName] = _.omit(model, 'name');
return models;
result.models[modelName] = _.omit(model, 'name');
result.models[modelName].__metadata = {
filePath: modelFile
};
return result;
},
{}
{ models: {}, errors: [] }
);
}
return null;
return { models: {}, errors: [] };
}

@@ -258,2 +262,4 @@

normalizeThumbnailPathForModel(model, model?.__metadata?.filePath);
iterateModelFieldsRecursively(model, (field: any, fieldPath) => {

@@ -275,2 +281,3 @@ // add field label if label is not set

assignLabelFieldIfNeeded(field);
normalizeThumbnailPathForModel(field, model?.__metadata?.filePath);
} else if (isCustomModelField(field, models)) {

@@ -317,2 +324,4 @@ // stackbit v0.2.0 compatibility

// TODO: update schema-editor to not show type field
// TODO: do not add objectTypeKey field to models, API/container should
// be able to add it automatically when data object or polymorphic nested model is added
addObjectTypeKeyField(model, objectTypeKey, modelName);

@@ -368,2 +377,8 @@ });

function normalizeThumbnailPathForModel(modelOrField: Model | FieldObjectProps, filePath: string | undefined) {
if (modelOrField.thumbnail && filePath) {
modelOrField.thumbnail = path.join(path.dirname(filePath), modelOrField.thumbnail);
}
}
/**

@@ -394,46 +409,23 @@ * Returns model names referenced by polymorphic 'model' and 'reference' fields.

function convertToTypedConfig(validationResult: ConfigValidationResult): Config {
function convertModelsToArray(validationResult: ConfigValidationResult): { config: Config; errors: ConfigNormalizedValidationError[] } {
const config = _.cloneDeep(validationResult.value);
const invalidModelNames = _.reduce(
validationResult.errors,
(modelNames: string[], error: ConfigValidationError) => {
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
const modelName = error.fieldPath[1];
modelNames.push(modelName);
}
return modelNames;
},
[]
);
// in stackbit.yaml 'models' are defined as object where keys are model names,
// convert 'models' to array of objects while 'name' property set to the
// in stackbit.yaml 'models' are defined as object where keys are the model names,
// convert 'models' to array of objects and set their 'name' property to the
// model name
const modelMap = config.models ?? {};
let models: Model[] = _.map(
let modelArray: Model[] = _.map(
modelMap,
(yamlModel: YamlModel, modelName: string): Model => {
const model: Model = {
return {
name: modelName,
...yamlModel
};
if (invalidModelNames.includes(modelName)) {
_.set(model, '__metadata.invalid', true);
}
return model;
}
);
return {
...config,
models: models
};
}
function normalizeErrors(config: Config, errors: ConfigValidationError[]): ConfigNormalizedValidationError[] {
return _.map(errors, (error: ConfigValidationError) => {
const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
const modelName = error.fieldPath[1];
const modelIndex = _.findIndex(config.models, { name: modelName });
const modelIndex = _.findIndex(modelArray, { name: modelName });
const normFieldPath = error.fieldPath.slice();

@@ -451,2 +443,10 @@ normFieldPath[1] = modelIndex;

});
return {
config: {
...config,
models: modelArray
},
errors: convertedErrors
};
}

@@ -46,4 +46,2 @@ import Joi from 'joi';

const objectModelNameErrorCode = 'model.name.of.object.models';
const documentModelNameErrorCode = 'model.name.of.document.models';
const validObjectModelNames = Joi.custom((value, { error, state }) => {

@@ -57,4 +55,10 @@ const models = _.last<YamlConfig>(state.ancestors)!.models ?? {};

return value;
}).prefs({
messages: {
[objectModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "object", got "{{#value}}"'
},
errors: { wrap: { label: false } }
});
const documentModelNameErrorCode = 'model.name.of.document.models';
const validPageOrDataModelNames = Joi.custom((value, { error, state }) => {

@@ -68,2 +72,7 @@ const models = _.last<YamlConfig>(state.ancestors)!.models ?? {};

return value;
}).prefs({
messages: {
[documentModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "page" or "data", got "{{#value}}"'
},
errors: { wrap: { label: false } }
});

@@ -79,13 +88,50 @@

const inFields = Joi.string()
.valid(
Joi.in('fields', {
adjust: (fields) => (_.isArray(fields) ? fields.map((field) => field.name) : [])
})
)
.prefs({
messages: { 'any.only': '{{#label}} must be one of model field names, got "{{#value}}"' },
errors: { wrap: { label: false } }
});
const labelFieldNotFoundError = 'labelField.not.found';
const labelFieldNotSimple = 'labelField.not.simple';
const labelFieldSchema = Joi.custom((value, { error, state }) => {
const modelOrObjectField: YamlBaseModel | FieldObjectProps = _.head(state.ancestors)!;
const fields = modelOrObjectField?.fields ?? [];
if (!_.isArray(fields)) {
return error(labelFieldNotFoundError);
}
const field = _.find(fields, (field) => field.name === value);
if (!field) {
return error(labelFieldNotFoundError);
}
if (['object', 'model', 'reference', 'list'].includes(field.type)) {
return error(labelFieldNotSimple, { fieldType: field.type });
}
return value;
}).prefs({
messages: {
[labelFieldNotFoundError]: '{{#label}} must be one of model field names, got "{{#value}}"',
[labelFieldNotSimple]: '{{#label}} can not reference complex field, got "{{#value}}" field of type "{{#fieldType}}"'
},
errors: { wrap: { label: false } }
});
const variantFieldNotFoundError = 'variantField.not.found';
const variantFieldNotEnum = 'variantField.not.enum';
const variantFieldSchema = Joi.custom((value, { error, state }) => {
const modelOrObjectField: YamlBaseModel | FieldObjectProps = _.head(state.ancestors)!;
const fields = modelOrObjectField?.fields ?? [];
if (!_.isArray(fields)) {
return error(variantFieldNotFoundError);
}
const field = _.find(fields, (field) => field.name === value);
if (!field) {
return error(variantFieldNotFoundError);
}
if (field.type !== 'enum') {
return error(variantFieldNotEnum, { fieldType: field.type });
}
return value;
}).prefs({
messages: {
[variantFieldNotFoundError]: '{{#label}} must be one of model field names, got "{{#value}}"',
[variantFieldNotEnum]: '{{#label}} should reference "enum" field, got "{{#value}}" field of type "{{#fieldType}}"'
},
errors: { wrap: { label: false } }
});
export interface ContentfulImport {

@@ -191,2 +237,3 @@ type: 'contentful';

default?: unknown;
group?: string;
const?: unknown;

@@ -208,2 +255,3 @@ hidden?: boolean;

type: 'enum';
controlType?: 'dropdown' | 'button-group' | 'thumbnails' | 'palette';
options: FieldSchemaEnumOptions;

@@ -215,2 +263,5 @@ }

labelField?: string;
thumbnail?: string;
fieldGroups?: string;
variantField?: string;
fields: Field[];

@@ -273,2 +324,32 @@ }

const fieldGroupsSchema = Joi.array()
.items(
Joi.object({
name: Joi.string().required(),
label: Joi.string().required()
})
)
.unique('name')
.prefs({
messages: {
'array.unique': '{{#label}} contains a duplicate group name "{{#value.name}}"'
},
errors: { wrap: { label: false } }
});
const inGroups = Joi.string()
.valid(
// 4 dots "...." =>
// ".." for the parent field where "group" property is defined
// + "." for the fields array
// + "." for the parent model
Joi.in('....fieldGroups', {
adjust: (groups) => (_.isArray(groups) ? groups.map((group) => group.name) : [])
})
)
.prefs({
messages: { 'any.only': '{{#label}} must be one of model field groups, got "{{#value}}"' },
errors: { wrap: { label: false } }
});
const fieldCommonPropsSchema = Joi.object({

@@ -283,2 +364,3 @@ type: Joi.string()

default: Joi.any(),
group: inGroups,
const: Joi.any(),

@@ -297,17 +379,40 @@ hidden: Joi.boolean(),

const enumFieldBaseOptionSchema = Joi.object({
label: Joi.string().required(),
value: Joi.alternatives().try(Joi.string(), Joi.number()).required()
});
const enumFieldPartialSchema = Joi.object({
type: Joi.string().valid('enum').required(),
options: Joi.alternatives()
.try(
Joi.array().items(Joi.string(), Joi.number()),
Joi.array().items(
Joi.object({
label: Joi.string().required(),
value: Joi.alternatives().try(Joi.string(), Joi.number()).required()
})
)
)
controlType: Joi.string().valid('dropdown', 'button-group', 'thumbnails', 'palette'),
options: Joi.any()
.when('..controlType', {
switch: [
{
is: 'thumbnails',
then: Joi.array().items(
enumFieldBaseOptionSchema.append({
thumbnail: Joi.string().required()
})
)
},
{
is: 'palette',
then: Joi.array().items(
enumFieldBaseOptionSchema.append({
textColor: Joi.string(),
backgroundColor: Joi.string(),
borderColor: Joi.string()
})
)
}
],
otherwise: Joi.alternatives().try(Joi.array().items(Joi.string(), Joi.number()), Joi.array().items(enumFieldBaseOptionSchema))
})
.required()
.prefs({
messages: { 'alternatives.types': '{{#label}} must be an array of strings or numbers, or array of objects with label and value properties' },
messages: {
'alternatives.types': '{{#label}} must be an array of strings or numbers, or an array of objects with label and value properties',
'alternatives.match': '{{#label}} must be an array of strings or numbers, or an array of objects with label and value properties'
},
errors: { wrap: { label: false } }

@@ -319,3 +424,6 @@ })

type: Joi.string().valid('object').required(),
labelField: inFields,
labelField: labelFieldSchema,
thumbnail: Joi.string(),
fieldGroups: fieldGroupsSchema,
variantField: variantFieldSchema,
fields: Joi.link('#fieldsSchema').required()

@@ -361,7 +469,18 @@ });

export interface FieldGroupItem {
name: string;
label: string;
}
export interface YamlBaseModel {
__metadata?: {
filePath?: string;
},
label: string;
description?: string;
thumbnail?: string;
extends?: string | string[];
labelField?: string;
fieldGroups?: FieldGroupItem[];
variantField?: string;
fields?: Field[];

@@ -377,7 +496,13 @@ }

const baseModelSchema = Joi.object({
__metadata: Joi.object({
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() }),
description: Joi.string(),
thumbnail: Joi.string(),
extends: Joi.array().items(validObjectModelNames).single(),
labelField: inFields,
labelField: labelFieldSchema,
fieldGroups: fieldGroupsSchema,
variantField: variantFieldSchema,
fields: Joi.link('#fieldsSchema')

@@ -562,4 +687,2 @@ });

[modelListForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is not true',
[objectModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "object", got "{{#value}}"',
[documentModelNameErrorCode]: '{{#label}} must reference the name of an existing model of type "page" or "data", got "{{#value}}"',
[fieldNameUnique]: '{{#label}} contains a duplicate field name "{{#value.name}}"'

@@ -566,0 +689,0 @@ },

import _ from 'lodash';
import { stackbitConfigSchema } from './config-schema';
import { stackbitConfigSchema, YamlModel } from './config-schema';
import { Model } from './config-loader';

@@ -9,2 +10,3 @@ export interface ConfigValidationError {

fieldPath: (string | number)[];
normFieldPath?: (string | number)[];
value?: any;

@@ -35,2 +37,3 @@ }

);
markInvalidModels(value, errors);
const valid = _.isEmpty(errors);

@@ -43,1 +46,27 @@ return {

}
function markInvalidModels(config: any, errors: ConfigValidationError[]) {
const invalidModelNames = getInvalidModelNames(errors);
const models = config.models ?? {};
_.forEach(models, (model: any, modelName: string): any => {
if (invalidModelNames.includes(modelName)) {
_.set(model, '__metadata.invalid', true);
}
});
}
function getInvalidModelNames(errors: ConfigValidationError[]) {
// get array of invalid model names by iterating errors and filtering these
// having fieldPath starting with ['models', modelName]
return _.reduce(
errors,
(modelNames: string[], error: ConfigValidationError) => {
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
const modelName = error.fieldPath[1];
modelNames.push(modelName);
}
return modelNames;
},
[]
);
}

@@ -14,5 +14,5 @@ export * from './config/config-schema';

ConfigError,
ConfigLoadError,
ConfigNormalizedValidationError
} from './config/config-loader';
export * from './config/config-errors';
export { writeConfig, WriteConfigOptions, convertToYamlConfig } from './config/config-writer';

@@ -19,0 +19,0 @@ export { loadContent, ContentItem, ContentLoaderOptions, ContentLoaderResult } from './content/content-loader';

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