@stackbit/sdk 0.2.20 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/config-consts.d.ts +1 -1
- package/dist/config/config-consts.js +2 -0
- package/dist/config/config-consts.js.map +1 -1
- package/dist/config/config-loader.d.ts +6 -4
- package/dist/config/config-loader.js +297 -100
- package/dist/config/config-loader.js.map +1 -1
- package/dist/config/config-schema.js +28 -7
- package/dist/config/config-schema.js.map +1 -1
- package/dist/config/config-types.d.ts +22 -5
- package/dist/config/config-writer.js +3 -0
- package/dist/config/config-writer.js.map +1 -1
- package/dist/config/presets-loader.js +18 -11
- package/dist/config/presets-loader.js.map +1 -1
- package/dist/content/content-loader.js +1 -1
- package/dist/content/content-schema.js +8 -0
- package/dist/content/content-schema.js.map +1 -1
- package/dist/utils/model-extender.js.map +1 -1
- package/dist/utils/model-iterators.d.ts +61 -1
- package/dist/utils/model-iterators.js +60 -11
- package/dist/utils/model-iterators.js.map +1 -1
- package/dist/utils/model-utils.d.ts +44 -3
- package/dist/utils/model-utils.js +93 -10
- package/dist/utils/model-utils.js.map +1 -1
- package/package.json +2 -2
- package/src/.DS_Store +0 -0
- package/src/config/config-consts.ts +2 -0
- package/src/config/config-loader.ts +333 -111
- package/src/config/config-schema.ts +35 -8
- package/src/config/config-types.ts +26 -5
- package/src/config/config-writer.ts +3 -0
- package/src/config/presets-loader.ts +19 -15
- package/src/content/content-loader.ts +2 -2
- package/src/content/content-schema.ts +9 -0
- package/src/utils/model-extender.ts +1 -1
- package/src/utils/model-iterators.ts +61 -13
- package/src/utils/model-utils.ts +91 -8
|
@@ -9,7 +9,7 @@ import { ConfigError, ConfigLoadError, ConfigValidationError } from './config-er
|
|
|
9
9
|
import {
|
|
10
10
|
assignLabelFieldIfNeeded,
|
|
11
11
|
extendModelMap,
|
|
12
|
-
|
|
12
|
+
getListFieldItems,
|
|
13
13
|
isCustomModelField,
|
|
14
14
|
isEnumField,
|
|
15
15
|
isListDataModel,
|
|
@@ -20,14 +20,19 @@ import {
|
|
|
20
20
|
isPageModel,
|
|
21
21
|
isDataModel,
|
|
22
22
|
isReferenceField,
|
|
23
|
-
iterateModelFieldsRecursively
|
|
23
|
+
iterateModelFieldsRecursively,
|
|
24
|
+
mapModelFieldsRecursively,
|
|
25
|
+
normalizeListFieldInPlace,
|
|
26
|
+
getModelFieldForModelKeyPath
|
|
24
27
|
} from '../utils';
|
|
25
|
-
import { append, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
|
|
28
|
+
import { append, omitByNil, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
|
|
26
29
|
import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, PageModel, YamlModel } from './config-types';
|
|
27
30
|
import { loadPresets } from './presets-loader';
|
|
28
31
|
|
|
29
32
|
export interface ConfigLoaderOptions {
|
|
33
|
+
[option: string]: any;
|
|
30
34
|
dirPath: string;
|
|
35
|
+
modelsSource?: string;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export interface ConfigLoaderResult {
|
|
@@ -43,81 +48,94 @@ export interface NormalizedValidationResult {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export interface TempConfigLoaderResult {
|
|
46
|
-
config?:
|
|
51
|
+
config?: Record<string, unknown>;
|
|
47
52
|
errors: ConfigLoadError[];
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
configLoadResult = await loadConfigFromDir(dirPath);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
return {
|
|
56
|
-
valid: false,
|
|
57
|
-
config: null,
|
|
58
|
-
errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
|
|
59
|
-
};
|
|
60
|
-
}
|
|
55
|
+
export async function loadConfig({ dirPath, ...options }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
|
|
56
|
+
const { config, errors: configLoadErrors } = await loadConfigFromDir(dirPath);
|
|
61
57
|
|
|
62
|
-
if (!
|
|
58
|
+
if (!config) {
|
|
63
59
|
return {
|
|
64
60
|
valid: false,
|
|
65
61
|
config: null,
|
|
66
|
-
errors:
|
|
62
|
+
errors: configLoadErrors
|
|
67
63
|
};
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
const
|
|
66
|
+
const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, options);
|
|
67
|
+
|
|
68
|
+
const normalizedResult = validateAndNormalizeConfig(config, externalModels);
|
|
71
69
|
|
|
72
70
|
const presetsResult = await loadPresets(dirPath, normalizedResult.config);
|
|
73
71
|
|
|
74
72
|
return {
|
|
75
73
|
valid: normalizedResult.valid,
|
|
76
74
|
config: presetsResult.config,
|
|
77
|
-
errors: [...
|
|
75
|
+
errors: [...configLoadErrors, ...externalModelsLoadErrors, ...normalizedResult.errors, ...presetsResult.errors]
|
|
78
76
|
};
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
export function validateAndNormalizeConfig(config: any): NormalizedValidationResult {
|
|
82
|
-
// validate the "contentModels" and extend config models with "contentModels"
|
|
83
|
-
// this must be done before main config validation to make it independent of "contentModels".
|
|
84
|
-
const contentModelsValidationResult = validateAndExtendContentModels(config);
|
|
85
|
-
config = contentModelsValidationResult.value;
|
|
86
|
-
|
|
79
|
+
export function validateAndNormalizeConfig(config: any, externalModels?: Model[]): NormalizedValidationResult {
|
|
87
80
|
// extend config models having the "extends" property
|
|
88
|
-
// this must be done before
|
|
81
|
+
// this must be done before any validation as some properties like
|
|
89
82
|
// the labelField will not work when validating models without extending them first
|
|
90
|
-
const { models, errors: extendModelErrors } = extendModelMap(config
|
|
91
|
-
|
|
83
|
+
const { models: extendedModels, errors: extendModelErrors } = extendModelMap(config.models as any);
|
|
84
|
+
const extendedConfig = {
|
|
85
|
+
...config,
|
|
86
|
+
models: extendedModels
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const { config: mergedConfig, errors: externalModelsMergeErrors } = mergeConfigWithExternalModels(extendedConfig, externalModels);
|
|
90
|
+
|
|
91
|
+
// validate the "contentModels" and extend config models with "contentModels"
|
|
92
|
+
// this must be done before main config validation to make it independent of "contentModels".
|
|
93
|
+
const { value: configWithContentModels, errors: contentModelsErrors } = validateAndExtendContentModels(mergedConfig);
|
|
92
94
|
|
|
93
95
|
// normalize config - backward compatibility updates, adding extra fields like "markdown_content", "type" and "layout",
|
|
94
96
|
// and setting other default values.
|
|
95
|
-
|
|
97
|
+
const normalizedConfig = normalizeConfig(configWithContentModels);
|
|
96
98
|
|
|
97
99
|
// validate config
|
|
98
|
-
const
|
|
100
|
+
const { value: validatedConfig, errors: validationErrors } = validateConfig(normalizedConfig);
|
|
101
|
+
|
|
102
|
+
const errors = [...extendModelErrors, ...externalModelsMergeErrors, ...contentModelsErrors, ...validationErrors];
|
|
99
103
|
|
|
100
|
-
const errors = [...contentModelsValidationResult.errors, ...extendModelErrors, ...configValidationResult.errors];
|
|
101
104
|
return normalizeValidationResult({
|
|
102
105
|
valid: _.isEmpty(errors),
|
|
103
|
-
value:
|
|
106
|
+
value: validatedConfig,
|
|
104
107
|
errors: errors
|
|
105
108
|
});
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResult> {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
try {
|
|
113
|
+
const { config, error } = await loadConfigFromStackbitYaml(dirPath);
|
|
114
|
+
if (error) {
|
|
115
|
+
return { errors: [error] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { models: modelsFromFiles, errors: fileModelsErrors } = await loadModelsFromFiles(dirPath, config);
|
|
119
|
+
|
|
120
|
+
const mergedModels = mergeConfigModelsWithModelsFromFiles(config.models ?? {}, modelsFromFiles);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
config: {
|
|
124
|
+
...config,
|
|
125
|
+
models: mergedModels
|
|
126
|
+
},
|
|
127
|
+
errors: fileModelsErrors
|
|
128
|
+
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return {
|
|
131
|
+
errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
|
|
132
|
+
};
|
|
112
133
|
}
|
|
113
|
-
const externalModelsResult = await loadExternalModels(dirPath, config);
|
|
114
|
-
config.models = _.assign(externalModelsResult.models, config.models);
|
|
115
|
-
return { config, errors: externalModelsResult.errors };
|
|
116
134
|
}
|
|
117
135
|
|
|
118
|
-
type
|
|
136
|
+
type StackbitYamlConfigResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
|
|
119
137
|
|
|
120
|
-
async function loadConfigFromStackbitYaml(dirPath: string): Promise<
|
|
138
|
+
async function loadConfigFromStackbitYaml(dirPath: string): Promise<StackbitYamlConfigResult> {
|
|
121
139
|
const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
|
|
122
140
|
const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
|
|
123
141
|
if (!stackbitYamlExists) {
|
|
@@ -135,54 +153,56 @@ async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFr
|
|
|
135
153
|
return { config };
|
|
136
154
|
}
|
|
137
155
|
|
|
138
|
-
async function
|
|
156
|
+
async function loadModelsFromFiles(dirPath: string, config: any): Promise<{ models: Record<string, any>; errors: ConfigLoadError[] }> {
|
|
139
157
|
const modelsSource = _.get(config, 'modelsSource', {});
|
|
140
158
|
const sourceType = _.get(modelsSource, 'type', 'files');
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return modelFiles
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
models: result.models,
|
|
173
|
-
errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
result.models[modelName] = _.omit(model, 'name');
|
|
177
|
-
result.models[modelName].__metadata = {
|
|
178
|
-
filePath: modelFile
|
|
159
|
+
const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
|
|
160
|
+
const modelDirs =
|
|
161
|
+
sourceType === 'files'
|
|
162
|
+
? _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'))
|
|
163
|
+
: defaultModelDirs;
|
|
164
|
+
|
|
165
|
+
const modelFiles = await reducePromise(
|
|
166
|
+
modelDirs,
|
|
167
|
+
async (modelFiles: string[], modelDir) => {
|
|
168
|
+
const absModelsDir = path.join(dirPath, modelDir);
|
|
169
|
+
const dirExists = await fse.pathExists(absModelsDir);
|
|
170
|
+
if (!dirExists) {
|
|
171
|
+
return modelFiles;
|
|
172
|
+
}
|
|
173
|
+
const files = await readModelFilesFromDir(absModelsDir);
|
|
174
|
+
return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
|
|
175
|
+
},
|
|
176
|
+
[]
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return reducePromise(
|
|
180
|
+
modelFiles,
|
|
181
|
+
async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
|
|
182
|
+
let model;
|
|
183
|
+
try {
|
|
184
|
+
model = await parseFile(path.join(dirPath, modelFile));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return {
|
|
187
|
+
models: result.models,
|
|
188
|
+
errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
|
|
179
189
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
190
|
+
}
|
|
191
|
+
const modelName = model?.name;
|
|
192
|
+
if (!modelName) {
|
|
193
|
+
return {
|
|
194
|
+
models: result.models,
|
|
195
|
+
errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
result.models[modelName] = _.omit(model, 'name');
|
|
199
|
+
result.models[modelName].__metadata = {
|
|
200
|
+
filePath: modelFile
|
|
201
|
+
};
|
|
202
|
+
return result;
|
|
203
|
+
},
|
|
204
|
+
{ models: {}, errors: [] }
|
|
205
|
+
);
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
async function readModelFilesFromDir(modelsDir: string) {
|
|
@@ -197,6 +217,38 @@ async function readModelFilesFromDir(modelsDir: string) {
|
|
|
197
217
|
});
|
|
198
218
|
}
|
|
199
219
|
|
|
220
|
+
async function loadModelsFromExternalSource(
|
|
221
|
+
config: any,
|
|
222
|
+
dirPath: string,
|
|
223
|
+
options: Omit<ConfigLoaderOptions, 'dirPath'>
|
|
224
|
+
): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
|
|
225
|
+
const modelsSource = _.get(config, 'modelsSource', {});
|
|
226
|
+
const sourceType = options.modelsSource ?? _.get(modelsSource, 'type', 'files');
|
|
227
|
+
if (sourceType === 'files') {
|
|
228
|
+
return { models: [], errors: [] };
|
|
229
|
+
} else if (sourceType === 'contentful') {
|
|
230
|
+
const contentfulModule = _.get(modelsSource, 'module', '@stackbit/cms-contentful');
|
|
231
|
+
const modulePath = path.join(dirPath, 'node_modules', contentfulModule);
|
|
232
|
+
const module = await import(modulePath);
|
|
233
|
+
try {
|
|
234
|
+
const { models } = await module.fetchAndConvertSchema(options);
|
|
235
|
+
return {
|
|
236
|
+
models: models,
|
|
237
|
+
errors: []
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return {
|
|
241
|
+
models: [],
|
|
242
|
+
errors: [new ConfigLoadError(`Error fetching and converting Contentful schema, error: ${error.message}`, { originalError: error })]
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
models: [],
|
|
248
|
+
errors: [new ConfigLoadError(`modelsSource ${modelsSource} is unsupported`)]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
200
252
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
201
253
|
async function loadConfigFromDotStackbit(dirPath: string) {
|
|
202
254
|
const stackbitDotPath = path.join(dirPath, '.stackbit');
|
|
@@ -231,6 +283,113 @@ async function loadConfigFromDotStackbit(dirPath: string) {
|
|
|
231
283
|
return _.isEmpty(config) ? null : config;
|
|
232
284
|
}
|
|
233
285
|
|
|
286
|
+
function mergeConfigModelsWithModelsFromFiles(configModels: any, modelsFromFiles: Record<string, any>) {
|
|
287
|
+
const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => {
|
|
288
|
+
// resolve thumbnails of models loaded from files
|
|
289
|
+
const modelFilePath = modelFromFile.__metadata?.filePath;
|
|
290
|
+
resolveThumbnailPathForModel(modelFromFile, modelFilePath);
|
|
291
|
+
iterateModelFieldsRecursively(modelFromFile, (field: any) => {
|
|
292
|
+
if (isListField(field)) {
|
|
293
|
+
field = normalizeListFieldInPlace(field);
|
|
294
|
+
field = field.items;
|
|
295
|
+
}
|
|
296
|
+
if (isObjectField(field)) {
|
|
297
|
+
resolveThumbnailPathForModel(field, modelFilePath);
|
|
298
|
+
} else if (isEnumField(field)) {
|
|
299
|
+
resolveThumbnailPathForEnumField(field, modelFilePath);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const configModel = _.get(configModels, modelName);
|
|
304
|
+
if (!configModel) {
|
|
305
|
+
return modelFromFile;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return _.assign({}, modelFromFile, configModel, {
|
|
309
|
+
fields: _.unionBy(configModel?.fields ?? [], modelFromFile?.fields ?? [], 'name')
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
return Object.assign({}, configModels, mergedModels);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function mergeConfigWithExternalModels(config: any, externalModels?: Model[]): { config: any; errors: ConfigValidationError[] } {
|
|
316
|
+
if (!externalModels || externalModels.length === 0) {
|
|
317
|
+
return {
|
|
318
|
+
config,
|
|
319
|
+
errors: []
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const stackbitModels = config?.models ?? {};
|
|
324
|
+
const errors: ConfigValidationError[] = [];
|
|
325
|
+
|
|
326
|
+
const models = _.reduce(
|
|
327
|
+
externalModels,
|
|
328
|
+
(modelMap: Record<string, YamlModel>, externalModel) => {
|
|
329
|
+
const { name, ...rest } = externalModel;
|
|
330
|
+
return Object.assign(modelMap, { [name]: rest });
|
|
331
|
+
},
|
|
332
|
+
{}
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
_.forEach(stackbitModels, (stackbitModel: any, modelName: any) => {
|
|
336
|
+
let externalModel = models[modelName];
|
|
337
|
+
if (!externalModel) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : externalModel.type ?? 'object';
|
|
342
|
+
const urlPath = modelType === 'page' ? stackbitModel?.urlPath ?? '/{slug}' : null;
|
|
343
|
+
|
|
344
|
+
externalModel = Object.assign(
|
|
345
|
+
{},
|
|
346
|
+
externalModel,
|
|
347
|
+
_.pick(stackbitModel, ['__metadata', 'label', 'description', 'thumbnail', 'singleInstance', 'readOnly', 'labelField', 'fieldGroups']),
|
|
348
|
+
omitByNil({
|
|
349
|
+
type: modelType,
|
|
350
|
+
urlPath
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
externalModel = mapModelFieldsRecursively(externalModel as Model, (field, modelKeyPath) => {
|
|
355
|
+
const stackbitField = getModelFieldForModelKeyPath(stackbitModel, modelKeyPath);
|
|
356
|
+
if (!stackbitField) {
|
|
357
|
+
return field;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let override = {};
|
|
361
|
+
if (stackbitField.type === 'style') {
|
|
362
|
+
override = stackbitField;
|
|
363
|
+
} else if (field.type === 'enum') {
|
|
364
|
+
override = _.pick(stackbitField, ['options']);
|
|
365
|
+
} else if (field.type === 'color') {
|
|
366
|
+
override = { type: 'color' };
|
|
367
|
+
} else if (field.type === 'number') {
|
|
368
|
+
override = _.pick(stackbitField, ['subtype', 'min', 'max', 'step', 'unit']);
|
|
369
|
+
} else if (field.type === 'object') {
|
|
370
|
+
override = _.pick(stackbitField, ['labelField', 'thumbnail', 'fieldGroups']);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return Object.assign(
|
|
374
|
+
{},
|
|
375
|
+
field,
|
|
376
|
+
_.pick(stackbitField, ['label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType']),
|
|
377
|
+
override
|
|
378
|
+
);
|
|
379
|
+
}) as YamlModel;
|
|
380
|
+
|
|
381
|
+
models[modelName] = externalModel;
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
config: {
|
|
386
|
+
...config,
|
|
387
|
+
models: models
|
|
388
|
+
},
|
|
389
|
+
errors: errors
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
234
393
|
function normalizeConfig(config: any): any {
|
|
235
394
|
const pageLayoutKey = _.get(config, 'pageLayoutKey', 'layout');
|
|
236
395
|
const objectTypeKey = _.get(config, 'objectTypeKey', 'type');
|
|
@@ -238,13 +397,23 @@ function normalizeConfig(config: any): any {
|
|
|
238
397
|
const ver = semver.coerce(stackbitYamlVersion);
|
|
239
398
|
const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
|
|
240
399
|
const models = config?.models || {};
|
|
400
|
+
const gitCMS = isGitCMS(config);
|
|
241
401
|
let referencedModelNames: string[] = [];
|
|
242
402
|
|
|
243
|
-
_.forEach(models, (model) => {
|
|
403
|
+
_.forEach(models, (model, modelName) => {
|
|
244
404
|
if (!model) {
|
|
245
405
|
return;
|
|
246
406
|
}
|
|
247
407
|
|
|
408
|
+
if (!_.has(model, 'type')) {
|
|
409
|
+
model.type = 'object';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// add model label if not set
|
|
413
|
+
if (!_.has(model, 'label')) {
|
|
414
|
+
model.label = _.startCase(modelName);
|
|
415
|
+
}
|
|
416
|
+
|
|
248
417
|
if (_.has(model, 'fields') && !Array.isArray(model.fields)) {
|
|
249
418
|
model.fields = [];
|
|
250
419
|
}
|
|
@@ -254,13 +423,15 @@ function normalizeConfig(config: any): any {
|
|
|
254
423
|
rename(model, 'template', 'layout');
|
|
255
424
|
|
|
256
425
|
updatePageUrlPath(model);
|
|
257
|
-
updatePageFilePath(model, config);
|
|
258
426
|
|
|
259
|
-
|
|
427
|
+
if (gitCMS) {
|
|
428
|
+
updatePageFilePath(model, config);
|
|
429
|
+
addMarkdownContentField(model);
|
|
260
430
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
431
|
+
// TODO: update schema-editor to not show layout field
|
|
432
|
+
addLayoutFieldToPageModel(model, pageLayoutKey);
|
|
433
|
+
}
|
|
434
|
+
} else if (isDataModel(model) && gitCMS) {
|
|
264
435
|
updateDataFilePath(model, config);
|
|
265
436
|
}
|
|
266
437
|
|
|
@@ -276,8 +447,6 @@ function normalizeConfig(config: any): any {
|
|
|
276
447
|
assignLabelFieldIfNeeded(model);
|
|
277
448
|
}
|
|
278
449
|
|
|
279
|
-
resolveThumbnailPathForModel(model, model?.__metadata?.filePath);
|
|
280
|
-
|
|
281
450
|
iterateModelFieldsRecursively(model, (field: any) => {
|
|
282
451
|
// add field label if label is not set
|
|
283
452
|
if (!_.has(field, 'label')) {
|
|
@@ -285,18 +454,12 @@ function normalizeConfig(config: any): any {
|
|
|
285
454
|
}
|
|
286
455
|
|
|
287
456
|
if (isListField(field)) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
_.set(field, 'items.type', 'string');
|
|
291
|
-
}
|
|
292
|
-
field = getListItemsField(field);
|
|
457
|
+
field = normalizeListFieldInPlace(field);
|
|
458
|
+
field = field.items;
|
|
293
459
|
}
|
|
294
460
|
|
|
295
461
|
if (isObjectField(field)) {
|
|
296
462
|
assignLabelFieldIfNeeded(field);
|
|
297
|
-
resolveThumbnailPathForModel(field, model?.__metadata?.filePath);
|
|
298
|
-
} else if (isEnumField(field)) {
|
|
299
|
-
resolveThumbnailPathForEnumField(field, model?.__metadata?.filePath);
|
|
300
463
|
} else if (isCustomModelField(field, models)) {
|
|
301
464
|
// stackbit v0.2.0 compatibility
|
|
302
465
|
// convert the old custom model field type: { type: 'action' }
|
|
@@ -326,7 +489,9 @@ function normalizeConfig(config: any): any {
|
|
|
326
489
|
}
|
|
327
490
|
}
|
|
328
491
|
|
|
329
|
-
|
|
492
|
+
if (gitCMS) {
|
|
493
|
+
referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
|
|
494
|
+
}
|
|
330
495
|
});
|
|
331
496
|
});
|
|
332
497
|
|
|
@@ -483,7 +648,7 @@ function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
|
|
|
483
648
|
*/
|
|
484
649
|
function getReferencedModelNames(field: any) {
|
|
485
650
|
if (isListField(field)) {
|
|
486
|
-
field =
|
|
651
|
+
field = getListFieldItems(field);
|
|
487
652
|
}
|
|
488
653
|
// TODO: add type field to model fields inside container update/create object logic rather adding type to schema
|
|
489
654
|
// 'object' models referenced by 'model' fields should have 'type' field
|
|
@@ -504,7 +669,10 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
504
669
|
const contentModels = config.contentModels ?? {};
|
|
505
670
|
const models = config.models ?? {};
|
|
506
671
|
|
|
507
|
-
|
|
672
|
+
const externalModels = !isGitCMS(config);
|
|
673
|
+
const emptyContentModels = _.isEmpty(contentModels);
|
|
674
|
+
|
|
675
|
+
if (externalModels || emptyContentModels) {
|
|
508
676
|
return {
|
|
509
677
|
valid: true,
|
|
510
678
|
value: config,
|
|
@@ -515,17 +683,17 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
515
683
|
const validationResult = validateContentModels(contentModels, models);
|
|
516
684
|
|
|
517
685
|
if (_.isEmpty(models)) {
|
|
518
|
-
return
|
|
686
|
+
return {
|
|
687
|
+
valid: validationResult.valid,
|
|
688
|
+
value: config,
|
|
689
|
+
errors: validationResult.errors
|
|
690
|
+
};
|
|
519
691
|
}
|
|
520
692
|
|
|
521
693
|
const extendedModels = _.mapValues(models, (model, modelName) => {
|
|
522
694
|
const contentModel = validationResult.value.contentModels[modelName];
|
|
523
695
|
if (!contentModel) {
|
|
524
|
-
return
|
|
525
|
-
// if a model does not define a type, use the default "object" type
|
|
526
|
-
type: model.type || 'object',
|
|
527
|
-
..._.omit(model, 'type')
|
|
528
|
-
};
|
|
696
|
+
return model;
|
|
529
697
|
}
|
|
530
698
|
if (_.get(contentModel, '__metadata.invalid')) {
|
|
531
699
|
return model;
|
|
@@ -560,10 +728,43 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
560
728
|
}
|
|
561
729
|
|
|
562
730
|
function normalizeValidationResult(validationResult: ConfigValidationResult): NormalizedValidationResult {
|
|
731
|
+
validationResult = filterAndOrderConfigFields(validationResult);
|
|
563
732
|
convertModelGroupsToModelList(validationResult);
|
|
564
733
|
return convertModelsToArray(validationResult);
|
|
565
734
|
}
|
|
566
735
|
|
|
736
|
+
function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
|
|
737
|
+
// TODO: see if we move filtering and sorting to Joi
|
|
738
|
+
return {
|
|
739
|
+
...validationResult,
|
|
740
|
+
value: _.pick(validationResult.value, [
|
|
741
|
+
'stackbitVersion',
|
|
742
|
+
'ssgName',
|
|
743
|
+
'ssgVersion',
|
|
744
|
+
'cmsName',
|
|
745
|
+
'import',
|
|
746
|
+
'buildCommand',
|
|
747
|
+
'publishDir',
|
|
748
|
+
'nodeVersion',
|
|
749
|
+
'devCommand',
|
|
750
|
+
'staticDir',
|
|
751
|
+
'uploadDir',
|
|
752
|
+
'assets',
|
|
753
|
+
'pagesDir',
|
|
754
|
+
'dataDir',
|
|
755
|
+
'pageLayoutKey',
|
|
756
|
+
'objectTypeKey',
|
|
757
|
+
'styleObjectModelName',
|
|
758
|
+
'excludePages',
|
|
759
|
+
'logicFields',
|
|
760
|
+
'contentModels',
|
|
761
|
+
'modelsSource',
|
|
762
|
+
'models',
|
|
763
|
+
'presets'
|
|
764
|
+
])
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
567
768
|
function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
|
|
568
769
|
const models = validationResult.value?.models ?? {};
|
|
569
770
|
|
|
@@ -593,7 +794,7 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
|
|
|
593
794
|
_.forEach(models, (model) => {
|
|
594
795
|
iterateModelFieldsRecursively(model, (field: any) => {
|
|
595
796
|
if (isListField(field)) {
|
|
596
|
-
field =
|
|
797
|
+
field = field.items;
|
|
597
798
|
}
|
|
598
799
|
if (field.groups) {
|
|
599
800
|
let key: string | null = null;
|
|
@@ -635,6 +836,10 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
|
|
|
635
836
|
}
|
|
636
837
|
);
|
|
637
838
|
|
|
839
|
+
if (!isGitCMS(config)) {
|
|
840
|
+
addImageModel(modelArray);
|
|
841
|
+
}
|
|
842
|
+
|
|
638
843
|
const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
|
|
639
844
|
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
|
|
640
845
|
const modelName = error.fieldPath[1];
|
|
@@ -655,3 +860,20 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
|
|
|
655
860
|
errors: convertedErrors
|
|
656
861
|
};
|
|
657
862
|
}
|
|
863
|
+
|
|
864
|
+
function addImageModel(models: Model[]) {
|
|
865
|
+
models.push({
|
|
866
|
+
type: 'image',
|
|
867
|
+
name: '__image_model',
|
|
868
|
+
label: 'Image',
|
|
869
|
+
labelField: 'title',
|
|
870
|
+
fields: [
|
|
871
|
+
{ name: 'title', type: 'string' },
|
|
872
|
+
{ name: 'url', type: 'string' }
|
|
873
|
+
]
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function isGitCMS(config: any) {
|
|
878
|
+
return !config.cmsName || config.cmsName === 'git';
|
|
879
|
+
}
|