@stackbit/sdk 0.2.21 → 0.2.25

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.
Files changed (36) hide show
  1. package/dist/config/config-consts.d.ts +1 -1
  2. package/dist/config/config-consts.js +2 -0
  3. package/dist/config/config-consts.js.map +1 -1
  4. package/dist/config/config-loader.d.ts +6 -4
  5. package/dist/config/config-loader.js +291 -99
  6. package/dist/config/config-loader.js.map +1 -1
  7. package/dist/config/config-schema.js +27 -3
  8. package/dist/config/config-schema.js.map +1 -1
  9. package/dist/config/config-types.d.ts +21 -4
  10. package/dist/config/config-writer.js +3 -0
  11. package/dist/config/config-writer.js.map +1 -1
  12. package/dist/config/presets-loader.js +18 -11
  13. package/dist/config/presets-loader.js.map +1 -1
  14. package/dist/content/content-loader.js +1 -1
  15. package/dist/content/content-schema.js +8 -0
  16. package/dist/content/content-schema.js.map +1 -1
  17. package/dist/utils/model-extender.js.map +1 -1
  18. package/dist/utils/model-iterators.d.ts +61 -1
  19. package/dist/utils/model-iterators.js +60 -11
  20. package/dist/utils/model-iterators.js.map +1 -1
  21. package/dist/utils/model-utils.d.ts +44 -3
  22. package/dist/utils/model-utils.js +93 -10
  23. package/dist/utils/model-utils.js.map +1 -1
  24. package/package.json +2 -2
  25. package/src/.DS_Store +0 -0
  26. package/src/config/config-consts.ts +2 -0
  27. package/src/config/config-loader.ts +328 -111
  28. package/src/config/config-schema.ts +34 -4
  29. package/src/config/config-types.ts +25 -4
  30. package/src/config/config-writer.ts +3 -0
  31. package/src/config/presets-loader.ts +19 -15
  32. package/src/content/content-loader.ts +2 -2
  33. package/src/content/content-schema.ts +9 -0
  34. package/src/utils/model-extender.ts +1 -1
  35. package/src/utils/model-iterators.ts +61 -13
  36. 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
- getListItemsField,
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';
26
- import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, PageModel, YamlModel } from './config-types';
28
+ import { append, omitByNil, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
29
+ import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, ModelsSource, 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?: ModelsSource;
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?: any;
51
+ config?: Record<string, unknown>;
47
52
  errors: ConfigLoadError[];
48
53
  }
49
54
 
50
- export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
51
- let configLoadResult: TempConfigLoaderResult;
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 (!configLoadResult.config) {
58
+ if (!config) {
63
59
  return {
64
60
  valid: false,
65
61
  config: null,
66
- errors: configLoadResult.errors
62
+ errors: configLoadErrors
67
63
  };
68
64
  }
69
65
 
70
- const normalizedResult = validateAndNormalizeConfig(configLoadResult.config);
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: [...configLoadResult.errors, ...normalizedResult.errors, ...presetsResult.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 main config validation as some properties like
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?.models || {});
91
- config.models = models;
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
- config = normalizeConfig(config);
97
+ const normalizedConfig = normalizeConfig(configWithContentModels);
96
98
 
97
99
  // validate config
98
- const configValidationResult = validateConfig(config);
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: configValidationResult.value,
106
+ value: validatedConfig,
104
107
  errors: errors
105
108
  });
106
109
  }
107
110
 
108
111
  async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResult> {
109
- const { config, error } = await loadConfigFromStackbitYaml(dirPath);
110
- if (error) {
111
- return { errors: [error] };
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 LoadConfigFromStackbitYamlResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
136
+ type StackbitYamlConfigResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
119
137
 
120
- async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFromStackbitYamlResult> {
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 loadExternalModels(dirPath: string, config: any) {
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
- if (sourceType === 'files') {
142
- const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
143
- const modelDirs = _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'));
144
- const modelFiles = await reducePromise(
145
- modelDirs,
146
- async (modelFiles: string[], modelDir) => {
147
- const absModelsDir = path.join(dirPath, modelDir);
148
- const dirExists = await fse.pathExists(absModelsDir);
149
- if (!dirExists) {
150
- return modelFiles;
151
- }
152
- const files = await readModelFilesFromDir(absModelsDir);
153
- return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
154
- },
155
- []
156
- );
157
- return reducePromise(
158
- modelFiles,
159
- async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
160
- let model;
161
- try {
162
- model = await parseFile(path.join(dirPath, modelFile));
163
- } catch (error) {
164
- return {
165
- models: result.models,
166
- errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
167
- };
168
- }
169
- const modelName = model?.name;
170
- if (!modelName) {
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
- return result;
181
- },
182
- { models: {}, errors: [] }
183
- );
184
- }
185
- return { models: {}, errors: [] };
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', options.modelsSource);
226
+ const sourceType = _.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.resolve(dirPath, 'node_modules', contentfulModule);
232
+ const module = await import(modulePath);
233
+ try {
234
+ const { models } = await module.fetchAndConvertSchema(_.omit(options, 'modelsSource'));
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,6 +397,7 @@ 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
403
  _.forEach(models, (model, modelName) => {
@@ -245,6 +405,10 @@ function normalizeConfig(config: any): any {
245
405
  return;
246
406
  }
247
407
 
408
+ if (!_.has(model, 'type')) {
409
+ model.type = 'object';
410
+ }
411
+
248
412
  // add model label if not set
249
413
  if (!_.has(model, 'label')) {
250
414
  model.label = _.startCase(modelName);
@@ -259,13 +423,15 @@ function normalizeConfig(config: any): any {
259
423
  rename(model, 'template', 'layout');
260
424
 
261
425
  updatePageUrlPath(model);
262
- updatePageFilePath(model, config);
263
426
 
264
- addMarkdownContentField(model);
427
+ if (gitCMS) {
428
+ updatePageFilePath(model, config);
429
+ addMarkdownContentField(model);
265
430
 
266
- // TODO: update schema-editor to not show layout field
267
- addLayoutFieldToPageModel(model, pageLayoutKey);
268
- } else if (isDataModel(model)) {
431
+ // TODO: update schema-editor to not show layout field
432
+ addLayoutFieldToPageModel(model, pageLayoutKey);
433
+ }
434
+ } else if (isDataModel(model) && gitCMS) {
269
435
  updateDataFilePath(model, config);
270
436
  }
271
437
 
@@ -281,8 +447,6 @@ function normalizeConfig(config: any): any {
281
447
  assignLabelFieldIfNeeded(model);
282
448
  }
283
449
 
284
- resolveThumbnailPathForModel(model, model?.__metadata?.filePath);
285
-
286
450
  iterateModelFieldsRecursively(model, (field: any) => {
287
451
  // add field label if label is not set
288
452
  if (!_.has(field, 'label')) {
@@ -290,18 +454,12 @@ function normalizeConfig(config: any): any {
290
454
  }
291
455
 
292
456
  if (isListField(field)) {
293
- // 'items.type' of list field default to 'string', set it explicitly
294
- if (!_.has(field, 'items.type')) {
295
- _.set(field, 'items.type', 'string');
296
- }
297
- field = getListItemsField(field);
457
+ field = normalizeListFieldInPlace(field);
458
+ field = field.items;
298
459
  }
299
460
 
300
461
  if (isObjectField(field)) {
301
462
  assignLabelFieldIfNeeded(field);
302
- resolveThumbnailPathForModel(field, model?.__metadata?.filePath);
303
- } else if (isEnumField(field)) {
304
- resolveThumbnailPathForEnumField(field, model?.__metadata?.filePath);
305
463
  } else if (isCustomModelField(field, models)) {
306
464
  // stackbit v0.2.0 compatibility
307
465
  // convert the old custom model field type: { type: 'action' }
@@ -331,7 +489,9 @@ function normalizeConfig(config: any): any {
331
489
  }
332
490
  }
333
491
 
334
- referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
492
+ if (gitCMS) {
493
+ referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
494
+ }
335
495
  });
336
496
  });
337
497
 
@@ -488,7 +648,7 @@ function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
488
648
  */
489
649
  function getReferencedModelNames(field: any) {
490
650
  if (isListField(field)) {
491
- field = getListItemsField(field);
651
+ field = getListFieldItems(field);
492
652
  }
493
653
  // TODO: add type field to model fields inside container update/create object logic rather adding type to schema
494
654
  // 'object' models referenced by 'model' fields should have 'type' field
@@ -509,7 +669,10 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
509
669
  const contentModels = config.contentModels ?? {};
510
670
  const models = config.models ?? {};
511
671
 
512
- if (_.isEmpty(contentModels)) {
672
+ const externalModels = !isGitCMS(config);
673
+ const emptyContentModels = _.isEmpty(contentModels);
674
+
675
+ if (externalModels || emptyContentModels) {
513
676
  return {
514
677
  valid: true,
515
678
  value: config,
@@ -520,17 +683,17 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
520
683
  const validationResult = validateContentModels(contentModels, models);
521
684
 
522
685
  if (_.isEmpty(models)) {
523
- return validationResult;
686
+ return {
687
+ valid: validationResult.valid,
688
+ value: config,
689
+ errors: validationResult.errors
690
+ };
524
691
  }
525
692
 
526
693
  const extendedModels = _.mapValues(models, (model, modelName) => {
527
694
  const contentModel = validationResult.value.contentModels[modelName];
528
695
  if (!contentModel) {
529
- return {
530
- // if a model does not define a type, use the default "object" type
531
- type: model.type || 'object',
532
- ..._.omit(model, 'type')
533
- };
696
+ return model;
534
697
  }
535
698
  if (_.get(contentModel, '__metadata.invalid')) {
536
699
  return model;
@@ -565,10 +728,43 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
565
728
  }
566
729
 
567
730
  function normalizeValidationResult(validationResult: ConfigValidationResult): NormalizedValidationResult {
731
+ validationResult = filterAndOrderConfigFields(validationResult);
568
732
  convertModelGroupsToModelList(validationResult);
569
733
  return convertModelsToArray(validationResult);
570
734
  }
571
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
+
572
768
  function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
573
769
  const models = validationResult.value?.models ?? {};
574
770
 
@@ -598,7 +794,7 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
598
794
  _.forEach(models, (model) => {
599
795
  iterateModelFieldsRecursively(model, (field: any) => {
600
796
  if (isListField(field)) {
601
- field = getListItemsField(field);
797
+ field = field.items;
602
798
  }
603
799
  if (field.groups) {
604
800
  let key: string | null = null;
@@ -640,6 +836,10 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
640
836
  }
641
837
  );
642
838
 
839
+ if (!isGitCMS(config)) {
840
+ addImageModel(modelArray);
841
+ }
842
+
643
843
  const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
644
844
  if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
645
845
  const modelName = error.fieldPath[1];
@@ -660,3 +860,20 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
660
860
  errors: convertedErrors
661
861
  };
662
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
+ }