@stackbit/sdk
Advanced tools
Comparing version 0.2.23 to 0.2.24
import { ConfigError, ConfigLoadError, ConfigValidationError } from './config-errors'; | ||
import { Config } from './config-types'; | ||
import { Config, Model } from './config-types'; | ||
export interface ConfigLoaderOptions { | ||
[option: string]: any; | ||
dirPath: string; | ||
modelsSource?: string; | ||
} | ||
@@ -22,2 +23,2 @@ export interface ConfigLoaderResult { | ||
export declare function loadConfig({ dirPath, ...options }: ConfigLoaderOptions): Promise<ConfigLoaderResult>; | ||
export declare function validateAndNormalizeConfig(config: any): NormalizedValidationResult; | ||
export declare function validateAndNormalizeConfig(config: any, externalModels?: Model[]): NormalizedValidationResult; |
@@ -37,23 +37,12 @@ "use strict"; | ||
async function loadConfig({ dirPath, ...options }) { | ||
let configLoadResult; | ||
try { | ||
configLoadResult = await loadConfigFromDir(dirPath); | ||
} | ||
catch (error) { | ||
const { config, errors: configLoadErrors } = await loadConfigFromDir(dirPath); | ||
if (!config) { | ||
return { | ||
valid: false, | ||
config: null, | ||
errors: [new config_errors_1.ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })] | ||
errors: configLoadErrors | ||
}; | ||
} | ||
if (!configLoadResult.config) { | ||
return { | ||
valid: false, | ||
config: null, | ||
errors: configLoadResult.errors | ||
}; | ||
} | ||
const externalModelsResult = await loadModelsFromExternalSource(configLoadResult.config, options); | ||
const mergedConfig = mergeConfigWithExternalModels(configLoadResult.config, externalModelsResult.models); | ||
const normalizedResult = validateAndNormalizeConfig(mergedConfig); | ||
const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, options); | ||
const normalizedResult = validateAndNormalizeConfig(config, externalModels); | ||
const presetsResult = await presets_loader_1.loadPresets(dirPath, normalizedResult.config); | ||
@@ -63,25 +52,28 @@ return { | ||
config: presetsResult.config, | ||
errors: [...configLoadResult.errors, ...externalModelsResult.errors, ...normalizedResult.errors, ...presetsResult.errors] | ||
errors: [...configLoadErrors, ...externalModelsLoadErrors, ...normalizedResult.errors, ...presetsResult.errors] | ||
}; | ||
} | ||
exports.loadConfig = loadConfig; | ||
function validateAndNormalizeConfig(config) { | ||
function validateAndNormalizeConfig(config, externalModels) { | ||
// extend config models having the "extends" property | ||
// this must be done before any validation as some properties like | ||
// the labelField will not work when validating models without extending them first | ||
const { models: extendedModels, errors: extendModelErrors } = utils_1.extendModelMap(config.models); | ||
const extendedConfig = { | ||
...config, | ||
models: extendedModels | ||
}; | ||
const { config: mergedConfig, errors: externalModelsMergeErrors } = mergeConfigWithExternalModels(extendedConfig, externalModels); | ||
// 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 | ||
const { models, errors: extendModelErrors } = utils_1.extendModelMap((config === null || config === void 0 ? void 0 : config.models) || {}); | ||
config.models = models; | ||
const { value: configWithContentModels, errors: contentModelsErrors } = validateAndExtendContentModels(mergedConfig); | ||
// normalize config - backward compatibility updates, adding extra fields like "markdown_content", "type" and "layout", | ||
// and setting other default values. | ||
config = normalizeConfig(config); | ||
const normalizedConfig = normalizeConfig(configWithContentModels); | ||
// validate config | ||
const configValidationResult = config_validator_1.validateConfig(config); | ||
const errors = [...contentModelsValidationResult.errors, ...extendModelErrors, ...configValidationResult.errors]; | ||
const { value: validatedConfig, errors: validationErrors } = config_validator_1.validateConfig(normalizedConfig); | ||
const errors = [...extendModelErrors, ...externalModelsMergeErrors, ...contentModelsErrors, ...validationErrors]; | ||
return normalizeValidationResult({ | ||
valid: lodash_1.default.isEmpty(errors), | ||
value: configValidationResult.value, | ||
value: validatedConfig, | ||
errors: errors | ||
@@ -92,9 +84,23 @@ }); | ||
async function loadConfigFromDir(dirPath) { | ||
const { config, error } = await loadConfigFromStackbitYaml(dirPath); | ||
if (error) { | ||
return { errors: [error] }; | ||
var _a; | ||
try { | ||
const { config, error } = await loadConfigFromStackbitYaml(dirPath); | ||
if (error) { | ||
return { errors: [error] }; | ||
} | ||
const { models: modelsFromFiles, errors: fileModelsErrors } = await loadModelsFromFiles(dirPath, config); | ||
const mergedModels = mergeConfigModelsWithModelsFromFiles((_a = config.models) !== null && _a !== void 0 ? _a : {}, modelsFromFiles); | ||
return { | ||
config: { | ||
...config, | ||
models: mergedModels | ||
}, | ||
errors: fileModelsErrors | ||
}; | ||
} | ||
const modelsFromFileResult = await loadModelsFromFiles(dirPath, config); | ||
const mergedConfig = mergeConfigModelsWithModelsFromFiles(config, modelsFromFileResult.models); | ||
return { config: mergedConfig, errors: modelsFromFileResult.errors }; | ||
catch (error) { | ||
return { | ||
errors: [new config_errors_1.ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })] | ||
}; | ||
} | ||
} | ||
@@ -170,5 +176,6 @@ async function loadConfigFromStackbitYaml(dirPath) { | ||
} | ||
async function loadModelsFromExternalSource(config, options) { | ||
async function loadModelsFromExternalSource(config, dirPath, options) { | ||
var _a; | ||
const modelsSource = lodash_1.default.get(config, 'modelsSource', {}); | ||
const sourceType = lodash_1.default.get(modelsSource, 'type', 'files'); | ||
const sourceType = (_a = options.modelsSource) !== null && _a !== void 0 ? _a : lodash_1.default.get(modelsSource, 'type', 'files'); | ||
if (sourceType === 'files') { | ||
@@ -179,3 +186,4 @@ return { models: [], errors: [] }; | ||
const contentfulModule = lodash_1.default.get(modelsSource, 'module', '@stackbit/cms-contentful'); | ||
const module = await Promise.resolve().then(() => __importStar(require(contentfulModule))); | ||
const modulePath = path_1.default.join(dirPath, 'node_modules', contentfulModule); | ||
const module = await Promise.resolve().then(() => __importStar(require(modulePath))); | ||
try { | ||
@@ -228,5 +236,3 @@ const { models } = await module.fetchAndConvertSchema(options); | ||
} | ||
function mergeConfigModelsWithModelsFromFiles(config, modelsFromFiles) { | ||
var _a; | ||
const configModels = (_a = config.models) !== null && _a !== void 0 ? _a : {}; | ||
function mergeConfigModelsWithModelsFromFiles(configModels, modelsFromFiles) { | ||
const mergedModels = lodash_1.default.mapValues(modelsFromFiles, (modelFromFile, modelName) => { | ||
@@ -249,3 +255,3 @@ var _a, _b, _c; | ||
}); | ||
const configModel = configModels[modelName]; | ||
const configModel = lodash_1.default.get(configModels, modelName); | ||
if (!configModel) { | ||
@@ -258,6 +264,3 @@ return modelFromFile; | ||
}); | ||
return { | ||
...config, | ||
models: Object.assign({}, configModels, mergedModels) | ||
}; | ||
return Object.assign({}, configModels, mergedModels); | ||
} | ||
@@ -267,21 +270,26 @@ function mergeConfigWithExternalModels(config, externalModels) { | ||
if (!externalModels || externalModels.length === 0) { | ||
return config; | ||
return { | ||
config, | ||
errors: [] | ||
}; | ||
} | ||
const stackbitModels = (_a = config === null || config === void 0 ? void 0 : config.models) !== null && _a !== void 0 ? _a : {}; | ||
externalModels = externalModels.map((externalModel) => { | ||
var _a; | ||
const stackbitModel = stackbitModels[externalModel.name]; | ||
if (!stackbitModel) { | ||
return externalModel; | ||
const errors = []; | ||
const models = lodash_1.default.reduce(externalModels, (modelMap, externalModel) => { | ||
const { name, ...rest } = externalModel; | ||
return Object.assign(modelMap, { [name]: rest }); | ||
}, {}); | ||
lodash_1.default.forEach(stackbitModels, (stackbitModel, modelName) => { | ||
var _a, _b; | ||
let externalModel = models[modelName]; | ||
if (!externalModel) { | ||
return; | ||
} | ||
const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : 'object'; | ||
const urlPath = modelType === 'page' ? (_a = stackbitModel === null || stackbitModel === void 0 ? void 0 : stackbitModel.urlPath) !== null && _a !== void 0 ? _a : '/{slug}' : null; | ||
const fieldGroups = stackbitModel === null || stackbitModel === void 0 ? void 0 : stackbitModel.fieldGroups; | ||
externalModel = Object.assign(externalModel, utils_2.omitByNil({ | ||
__metadata: stackbitModel.__metadata, | ||
const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : (_a = externalModel.type) !== null && _a !== void 0 ? _a : 'object'; | ||
const urlPath = modelType === 'page' ? (_b = stackbitModel === null || stackbitModel === void 0 ? void 0 : stackbitModel.urlPath) !== null && _b !== void 0 ? _b : '/{slug}' : null; | ||
externalModel = Object.assign({}, externalModel, lodash_1.default.pick(stackbitModel, ['__metadata', 'label', 'description', 'thumbnail', 'singleInstance', 'readOnly', 'labelField', 'fieldGroups']), utils_2.omitByNil({ | ||
type: modelType, | ||
urlPath, | ||
fieldGroups | ||
urlPath | ||
})); | ||
return utils_1.mapModelFieldsRecursively(externalModel, (field, modelKeyPath) => { | ||
externalModel = utils_1.mapModelFieldsRecursively(externalModel, (field, modelKeyPath) => { | ||
const stackbitField = utils_1.getModelFieldForModelKeyPath(stackbitModel, modelKeyPath); | ||
@@ -291,20 +299,28 @@ if (!stackbitField) { | ||
} | ||
const group = 'group' in stackbitField ? stackbitField.group : null; | ||
const controlType = 'controlType' in stackbitField ? stackbitField.controlType : null; | ||
let override = {}; | ||
if ((stackbitField === null || stackbitField === void 0 ? void 0 : stackbitField.type) === 'style') { | ||
if (stackbitField.type === 'style') { | ||
override = stackbitField; | ||
} | ||
return Object.assign({}, field, utils_2.omitByNil({ | ||
group, | ||
controlType | ||
}), override); | ||
else if (field.type === 'enum') { | ||
override = lodash_1.default.pick(stackbitField, ['options']); | ||
} | ||
else if (field.type === 'color') { | ||
override = { type: 'color' }; | ||
} | ||
else if (field.type === 'number') { | ||
override = lodash_1.default.pick(stackbitField, ['subtype', 'min', 'max', 'step', 'unit']); | ||
} | ||
else if (field.type === 'object') { | ||
override = lodash_1.default.pick(stackbitField, ['labelField', 'thumbnail', 'fieldGroups']); | ||
} | ||
return Object.assign({}, field, lodash_1.default.pick(stackbitField, ['label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType']), override); | ||
}); | ||
models[modelName] = externalModel; | ||
}); | ||
return { | ||
...config, | ||
models: lodash_1.default.mapValues(lodash_1.default.keyBy(externalModels, 'name'), (model) => { | ||
lodash_1.default.unset(model, 'name'); | ||
return model; | ||
}) | ||
config: { | ||
...config, | ||
models: models | ||
}, | ||
errors: errors | ||
}; | ||
@@ -311,0 +327,0 @@ } |
{ | ||
"name": "@stackbit/sdk", | ||
"version": "0.2.23", | ||
"version": "0.2.24", | ||
"description": "Stackbit SDK", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -35,2 +35,3 @@ import path from 'path'; | ||
dirPath: string; | ||
modelsSource?: string; | ||
} | ||
@@ -56,27 +57,16 @@ | ||
export async function loadConfig({ dirPath, ...options }: ConfigLoaderOptions): Promise<ConfigLoaderResult> { | ||
let configLoadResult: TempConfigLoaderResult; | ||
try { | ||
configLoadResult = await loadConfigFromDir(dirPath); | ||
} catch (error) { | ||
return { | ||
valid: false, | ||
config: null, | ||
errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })] | ||
}; | ||
} | ||
const { config, errors: configLoadErrors } = await loadConfigFromDir(dirPath); | ||
if (!configLoadResult.config) { | ||
if (!config) { | ||
return { | ||
valid: false, | ||
config: null, | ||
errors: configLoadResult.errors | ||
errors: configLoadErrors | ||
}; | ||
} | ||
const externalModelsResult = await loadModelsFromExternalSource(configLoadResult.config, options); | ||
const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, options); | ||
const mergedConfig = mergeConfigWithExternalModels(configLoadResult.config, externalModelsResult.models); | ||
const normalizedResult = validateAndNormalizeConfig(config, externalModels); | ||
const normalizedResult = validateAndNormalizeConfig(mergedConfig); | ||
const presetsResult = await loadPresets(dirPath, normalizedResult.config); | ||
@@ -87,29 +77,34 @@ | ||
config: presetsResult.config, | ||
errors: [...configLoadResult.errors, ...externalModelsResult.errors, ...normalizedResult.errors, ...presetsResult.errors] | ||
errors: [...configLoadErrors, ...externalModelsLoadErrors, ...normalizedResult.errors, ...presetsResult.errors] | ||
}; | ||
} | ||
export function validateAndNormalizeConfig(config: any): NormalizedValidationResult { | ||
export function validateAndNormalizeConfig(config: any, externalModels?: Model[]): NormalizedValidationResult { | ||
// extend config models having the "extends" property | ||
// this must be done before any validation as some properties like | ||
// the labelField will not work when validating models without extending them first | ||
const { models: extendedModels, errors: extendModelErrors } = extendModelMap(config.models as any); | ||
const extendedConfig = { | ||
...config, | ||
models: extendedModels | ||
}; | ||
const { config: mergedConfig, errors: externalModelsMergeErrors } = mergeConfigWithExternalModels(extendedConfig, externalModels); | ||
// 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; | ||
const { value: configWithContentModels, errors: contentModelsErrors } = validateAndExtendContentModels(mergedConfig); | ||
// 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 | ||
const { models, errors: extendModelErrors } = extendModelMap(config?.models || {}); | ||
config.models = models; | ||
// normalize config - backward compatibility updates, adding extra fields like "markdown_content", "type" and "layout", | ||
// and setting other default values. | ||
config = normalizeConfig(config); | ||
const normalizedConfig = normalizeConfig(configWithContentModels); | ||
// validate config | ||
const configValidationResult = validateConfig(config); | ||
const { value: validatedConfig, errors: validationErrors } = validateConfig(normalizedConfig); | ||
const errors = [...contentModelsValidationResult.errors, ...extendModelErrors, ...configValidationResult.errors]; | ||
const errors = [...extendModelErrors, ...externalModelsMergeErrors, ...contentModelsErrors, ...validationErrors]; | ||
return normalizeValidationResult({ | ||
valid: _.isEmpty(errors), | ||
value: configValidationResult.value, | ||
value: validatedConfig, | ||
errors: errors | ||
@@ -120,9 +115,24 @@ }); | ||
async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResult> { | ||
const { config, error } = await loadConfigFromStackbitYaml(dirPath); | ||
if (error) { | ||
return { errors: [error] }; | ||
try { | ||
const { config, error } = await loadConfigFromStackbitYaml(dirPath); | ||
if (error) { | ||
return { errors: [error] }; | ||
} | ||
const { models: modelsFromFiles, errors: fileModelsErrors } = await loadModelsFromFiles(dirPath, config); | ||
const mergedModels = mergeConfigModelsWithModelsFromFiles(config.models ?? {}, modelsFromFiles); | ||
return { | ||
config: { | ||
...config, | ||
models: mergedModels | ||
}, | ||
errors: fileModelsErrors | ||
}; | ||
} catch (error) { | ||
return { | ||
errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })] | ||
}; | ||
} | ||
const modelsFromFileResult = await loadModelsFromFiles(dirPath, config); | ||
const mergedConfig = mergeConfigModelsWithModelsFromFiles(config, modelsFromFileResult.models); | ||
return { config: mergedConfig, errors: modelsFromFileResult.errors }; | ||
} | ||
@@ -214,5 +224,9 @@ | ||
async function loadModelsFromExternalSource(config: any, options: any): Promise<{ models: Model[]; errors: ConfigLoadError[] }> { | ||
async function loadModelsFromExternalSource( | ||
config: any, | ||
dirPath: string, | ||
options: Omit<ConfigLoaderOptions, 'dirPath'> | ||
): Promise<{ models: Model[]; errors: ConfigLoadError[] }> { | ||
const modelsSource = _.get(config, 'modelsSource', {}); | ||
const sourceType = _.get(modelsSource, 'type', 'files'); | ||
const sourceType = options.modelsSource ?? _.get(modelsSource, 'type', 'files'); | ||
if (sourceType === 'files') { | ||
@@ -222,3 +236,4 @@ return { models: [], errors: [] }; | ||
const contentfulModule = _.get(modelsSource, 'module', '@stackbit/cms-contentful'); | ||
const module = await import(contentfulModule); | ||
const modulePath = path.join(dirPath, 'node_modules', contentfulModule); | ||
const module = await import(modulePath); | ||
try { | ||
@@ -277,4 +292,3 @@ const { models } = await module.fetchAndConvertSchema(options); | ||
function mergeConfigModelsWithModelsFromFiles(config: any, modelsFromFiles: Record<string, any>) { | ||
const configModels = config.models ?? {}; | ||
function mergeConfigModelsWithModelsFromFiles(configModels: any, modelsFromFiles: Record<string, any>) { | ||
const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => { | ||
@@ -296,3 +310,3 @@ // resolve thumbnails of models loaded from files | ||
const configModel = configModels[modelName]; | ||
const configModel = _.get(configModels, modelName); | ||
if (!configModel) { | ||
@@ -306,36 +320,45 @@ return modelFromFile; | ||
}); | ||
return { | ||
...config, | ||
models: Object.assign({}, configModels, mergedModels) | ||
}; | ||
return Object.assign({}, configModels, mergedModels); | ||
} | ||
function mergeConfigWithExternalModels(config: any, externalModels?: Model[]) { | ||
function mergeConfigWithExternalModels(config: any, externalModels?: Model[]): { config: any; errors: ConfigValidationError[] } { | ||
if (!externalModels || externalModels.length === 0) { | ||
return config; | ||
return { | ||
config, | ||
errors: [] | ||
}; | ||
} | ||
const stackbitModels = config?.models ?? {}; | ||
const errors: ConfigValidationError[] = []; | ||
externalModels = externalModels.map((externalModel) => { | ||
const stackbitModel = stackbitModels[externalModel.name]; | ||
if (!stackbitModel) { | ||
return externalModel; | ||
const models = _.reduce( | ||
externalModels, | ||
(modelMap: Record<string, YamlModel>, externalModel) => { | ||
const { name, ...rest } = externalModel; | ||
return Object.assign(modelMap, { [name]: rest }); | ||
}, | ||
{} | ||
); | ||
_.forEach(stackbitModels, (stackbitModel: any, modelName: any) => { | ||
let externalModel = models[modelName]; | ||
if (!externalModel) { | ||
return; | ||
} | ||
const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : 'object'; | ||
const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : externalModel.type ?? 'object'; | ||
const urlPath = modelType === 'page' ? stackbitModel?.urlPath ?? '/{slug}' : null; | ||
const fieldGroups = stackbitModel?.fieldGroups; | ||
externalModel = Object.assign( | ||
{}, | ||
externalModel, | ||
_.pick(stackbitModel, ['__metadata', 'label', 'description', 'thumbnail', 'singleInstance', 'readOnly', 'labelField', 'fieldGroups']), | ||
omitByNil({ | ||
__metadata: stackbitModel.__metadata, | ||
type: modelType, | ||
urlPath, | ||
fieldGroups | ||
urlPath | ||
}) | ||
); | ||
return mapModelFieldsRecursively(externalModel, (field, modelKeyPath) => { | ||
externalModel = mapModelFieldsRecursively(externalModel as Model, (field, modelKeyPath) => { | ||
const stackbitField = getModelFieldForModelKeyPath(stackbitModel, modelKeyPath); | ||
@@ -346,8 +369,13 @@ if (!stackbitField) { | ||
const group = 'group' in stackbitField ? stackbitField.group : null; | ||
const controlType = 'controlType' in stackbitField ? stackbitField.controlType : null; | ||
let override = {}; | ||
if (stackbitField?.type === 'style') { | ||
if (stackbitField.type === 'style') { | ||
override = stackbitField; | ||
} else if (field.type === 'enum') { | ||
override = _.pick(stackbitField, ['options']); | ||
} else if (field.type === 'color') { | ||
override = { type: 'color' }; | ||
} else if (field.type === 'number') { | ||
override = _.pick(stackbitField, ['subtype', 'min', 'max', 'step', 'unit']); | ||
} else if (field.type === 'object') { | ||
override = _.pick(stackbitField, ['labelField', 'thumbnail', 'fieldGroups']); | ||
} | ||
@@ -358,17 +386,16 @@ | ||
field, | ||
omitByNil({ | ||
group, | ||
controlType | ||
}), | ||
_.pick(stackbitField, ['label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType']), | ||
override | ||
); | ||
}); | ||
}) as YamlModel; | ||
models[modelName] = externalModel; | ||
}); | ||
return { | ||
...config, | ||
models: _.mapValues(_.keyBy(externalModels, 'name'), (model) => { | ||
_.unset(model, 'name'); | ||
return model; | ||
}) | ||
config: { | ||
...config, | ||
models: models | ||
}, | ||
errors: errors | ||
}; | ||
@@ -375,0 +402,0 @@ } |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
771236
13727