@stackbit/sdk 0.2.19 → 0.2.23

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 (37) 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 +3 -2
  5. package/dist/config/config-loader.js +256 -72
  6. package/dist/config/config-loader.js.map +1 -1
  7. package/dist/config/config-schema.js +28 -7
  8. package/dist/config/config-schema.js.map +1 -1
  9. package/dist/config/config-types.d.ts +22 -5
  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 +3 -2
  18. package/dist/utils/model-extender.js.map +1 -1
  19. package/dist/utils/model-iterators.d.ts +61 -1
  20. package/dist/utils/model-iterators.js +60 -11
  21. package/dist/utils/model-iterators.js.map +1 -1
  22. package/dist/utils/model-utils.d.ts +44 -3
  23. package/dist/utils/model-utils.js +93 -10
  24. package/dist/utils/model-utils.js.map +1 -1
  25. package/package.json +2 -2
  26. package/src/.DS_Store +0 -0
  27. package/src/config/config-consts.ts +2 -0
  28. package/src/config/config-loader.ts +281 -83
  29. package/src/config/config-schema.ts +35 -8
  30. package/src/config/config-types.ts +26 -5
  31. package/src/config/config-writer.ts +3 -0
  32. package/src/config/presets-loader.ts +19 -15
  33. package/src/content/content-loader.ts +2 -2
  34. package/src/content/content-schema.ts +9 -0
  35. package/src/utils/model-extender.ts +4 -3
  36. package/src/utils/model-iterators.ts +61 -13
  37. 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,13 +20,17 @@ 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;
31
35
  }
32
36
 
@@ -43,11 +47,11 @@ export interface NormalizedValidationResult {
43
47
  }
44
48
 
45
49
  export interface TempConfigLoaderResult {
46
- config?: any;
50
+ config?: Record<string, unknown>;
47
51
  errors: ConfigLoadError[];
48
52
  }
49
53
 
50
- export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
54
+ export async function loadConfig({ dirPath, ...options }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
51
55
  let configLoadResult: TempConfigLoaderResult;
52
56
  try {
53
57
  configLoadResult = await loadConfigFromDir(dirPath);
@@ -67,14 +71,18 @@ export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<Conf
67
71
  };
68
72
  }
69
73
 
70
- const normalizedResult = validateAndNormalizeConfig(configLoadResult.config);
74
+ const externalModelsResult = await loadModelsFromExternalSource(configLoadResult.config, options);
75
+
76
+ const mergedConfig = mergeConfigWithExternalModels(configLoadResult.config, externalModelsResult.models);
77
+
78
+ const normalizedResult = validateAndNormalizeConfig(mergedConfig);
71
79
 
72
80
  const presetsResult = await loadPresets(dirPath, normalizedResult.config);
73
81
 
74
82
  return {
75
83
  valid: normalizedResult.valid,
76
84
  config: presetsResult.config,
77
- errors: [...configLoadResult.errors, ...normalizedResult.errors, ...presetsResult.errors]
85
+ errors: [...configLoadResult.errors, ...externalModelsResult.errors, ...normalizedResult.errors, ...presetsResult.errors]
78
86
  };
79
87
  }
80
88
 
@@ -110,14 +118,14 @@ async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResul
110
118
  if (error) {
111
119
  return { errors: [error] };
112
120
  }
113
- const externalModelsResult = await loadExternalModels(dirPath, config);
114
- config.models = _.assign(externalModelsResult.models, config.models);
115
- return { config, errors: externalModelsResult.errors };
121
+ const modelsFromFileResult = await loadModelsFromFiles(dirPath, config);
122
+ const mergedConfig = mergeConfigModelsWithModelsFromFiles(config, modelsFromFileResult.models);
123
+ return { config: mergedConfig, errors: modelsFromFileResult.errors };
116
124
  }
117
125
 
118
- type LoadConfigFromStackbitYamlResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
126
+ type StackbitYamlConfigResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
119
127
 
120
- async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFromStackbitYamlResult> {
128
+ async function loadConfigFromStackbitYaml(dirPath: string): Promise<StackbitYamlConfigResult> {
121
129
  const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
122
130
  const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
123
131
  if (!stackbitYamlExists) {
@@ -135,54 +143,56 @@ async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFr
135
143
  return { config };
136
144
  }
137
145
 
138
- async function loadExternalModels(dirPath: string, config: any) {
146
+ async function loadModelsFromFiles(dirPath: string, config: any): Promise<{ models: Record<string, any>; errors: ConfigLoadError[] }> {
139
147
  const modelsSource = _.get(config, 'modelsSource', {});
140
148
  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
149
+ const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
150
+ const modelDirs =
151
+ sourceType === 'files'
152
+ ? _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'))
153
+ : defaultModelDirs;
154
+
155
+ const modelFiles = await reducePromise(
156
+ modelDirs,
157
+ async (modelFiles: string[], modelDir) => {
158
+ const absModelsDir = path.join(dirPath, modelDir);
159
+ const dirExists = await fse.pathExists(absModelsDir);
160
+ if (!dirExists) {
161
+ return modelFiles;
162
+ }
163
+ const files = await readModelFilesFromDir(absModelsDir);
164
+ return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
165
+ },
166
+ []
167
+ );
168
+
169
+ return reducePromise(
170
+ modelFiles,
171
+ async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
172
+ let model;
173
+ try {
174
+ model = await parseFile(path.join(dirPath, modelFile));
175
+ } catch (error) {
176
+ return {
177
+ models: result.models,
178
+ errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
179
179
  };
180
- return result;
181
- },
182
- { models: {}, errors: [] }
183
- );
184
- }
185
- return { models: {}, errors: [] };
180
+ }
181
+ const modelName = model?.name;
182
+ if (!modelName) {
183
+ return {
184
+ models: result.models,
185
+ errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
186
+ };
187
+ }
188
+ result.models[modelName] = _.omit(model, 'name');
189
+ result.models[modelName].__metadata = {
190
+ filePath: modelFile
191
+ };
192
+ return result;
193
+ },
194
+ { models: {}, errors: [] }
195
+ );
186
196
  }
187
197
 
188
198
  async function readModelFilesFromDir(modelsDir: string) {
@@ -197,6 +207,33 @@ async function readModelFilesFromDir(modelsDir: string) {
197
207
  });
198
208
  }
199
209
 
210
+ async function loadModelsFromExternalSource(config: any, options: any): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
211
+ const modelsSource = _.get(config, 'modelsSource', {});
212
+ const sourceType = _.get(modelsSource, 'type', 'files');
213
+ if (sourceType === 'files') {
214
+ return { models: [], errors: [] };
215
+ } else if (sourceType === 'contentful') {
216
+ const contentfulModule = _.get(modelsSource, 'module', '@stackbit/cms-contentful');
217
+ const module = await import(contentfulModule);
218
+ try {
219
+ const { models } = await module.fetchAndConvertSchema(options);
220
+ return {
221
+ models: models,
222
+ errors: []
223
+ };
224
+ } catch (error) {
225
+ return {
226
+ models: [],
227
+ errors: [new ConfigLoadError(`Error fetching and converting Contentful schema, error: ${error.message}`, { originalError: error })]
228
+ };
229
+ }
230
+ }
231
+ return {
232
+ models: [],
233
+ errors: [new ConfigLoadError(`modelsSource ${modelsSource} is unsupported`)]
234
+ };
235
+ }
236
+
200
237
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
201
238
  async function loadConfigFromDotStackbit(dirPath: string) {
202
239
  const stackbitDotPath = path.join(dirPath, '.stackbit');
@@ -231,6 +268,101 @@ async function loadConfigFromDotStackbit(dirPath: string) {
231
268
  return _.isEmpty(config) ? null : config;
232
269
  }
233
270
 
271
+ function mergeConfigModelsWithModelsFromFiles(config: any, modelsFromFiles: Record<string, any>) {
272
+ const configModels = config.models ?? {};
273
+ const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => {
274
+ // resolve thumbnails of models loaded from files
275
+ const modelFilePath = modelFromFile.__metadata?.filePath;
276
+ resolveThumbnailPathForModel(modelFromFile, modelFilePath);
277
+ iterateModelFieldsRecursively(modelFromFile, (field: any) => {
278
+ if (isListField(field)) {
279
+ field = normalizeListFieldInPlace(field);
280
+ field = field.items;
281
+ }
282
+ if (isObjectField(field)) {
283
+ resolveThumbnailPathForModel(field, modelFilePath);
284
+ } else if (isEnumField(field)) {
285
+ resolveThumbnailPathForEnumField(field, modelFilePath);
286
+ }
287
+ });
288
+
289
+ const configModel = configModels[modelName];
290
+ if (!configModel) {
291
+ return modelFromFile;
292
+ }
293
+
294
+ return _.assign({}, modelFromFile, configModel, {
295
+ fields: _.unionBy(configModel?.fields ?? [], modelFromFile?.fields ?? [], 'name')
296
+ });
297
+ });
298
+ return {
299
+ ...config,
300
+ models: Object.assign({}, configModels, mergedModels)
301
+ };
302
+ }
303
+
304
+ function mergeConfigWithExternalModels(config: any, externalModels?: Model[]) {
305
+ if (!externalModels || externalModels.length === 0) {
306
+ return config;
307
+ }
308
+
309
+ const stackbitModels = config?.models ?? {};
310
+
311
+ externalModels = externalModels.map((externalModel) => {
312
+ const stackbitModel = stackbitModels[externalModel.name];
313
+ if (!stackbitModel) {
314
+ return externalModel;
315
+ }
316
+
317
+ const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : 'object';
318
+ const urlPath = modelType === 'page' ? stackbitModel?.urlPath ?? '/{slug}' : null;
319
+ const fieldGroups = stackbitModel?.fieldGroups;
320
+
321
+ externalModel = Object.assign(
322
+ externalModel,
323
+ omitByNil({
324
+ __metadata: stackbitModel.__metadata,
325
+ type: modelType,
326
+ urlPath,
327
+ fieldGroups
328
+ })
329
+ );
330
+
331
+ return mapModelFieldsRecursively(externalModel, (field, modelKeyPath) => {
332
+ const stackbitField = getModelFieldForModelKeyPath(stackbitModel, modelKeyPath);
333
+ if (!stackbitField) {
334
+ return field;
335
+ }
336
+
337
+ const group = 'group' in stackbitField ? stackbitField.group : null;
338
+ const controlType = 'controlType' in stackbitField ? stackbitField.controlType : null;
339
+ let override = {};
340
+
341
+ if (stackbitField?.type === 'style') {
342
+ override = stackbitField;
343
+ }
344
+
345
+ return Object.assign(
346
+ {},
347
+ field,
348
+ omitByNil({
349
+ group,
350
+ controlType
351
+ }),
352
+ override
353
+ );
354
+ });
355
+ });
356
+
357
+ return {
358
+ ...config,
359
+ models: _.mapValues(_.keyBy(externalModels, 'name'), (model) => {
360
+ _.unset(model, 'name');
361
+ return model;
362
+ })
363
+ };
364
+ }
365
+
234
366
  function normalizeConfig(config: any): any {
235
367
  const pageLayoutKey = _.get(config, 'pageLayoutKey', 'layout');
236
368
  const objectTypeKey = _.get(config, 'objectTypeKey', 'type');
@@ -238,13 +370,23 @@ function normalizeConfig(config: any): any {
238
370
  const ver = semver.coerce(stackbitYamlVersion);
239
371
  const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
240
372
  const models = config?.models || {};
373
+ const gitCMS = isGitCMS(config);
241
374
  let referencedModelNames: string[] = [];
242
375
 
243
- _.forEach(models, (model) => {
376
+ _.forEach(models, (model, modelName) => {
244
377
  if (!model) {
245
378
  return;
246
379
  }
247
380
 
381
+ if (!_.has(model, 'type')) {
382
+ model.type = 'object';
383
+ }
384
+
385
+ // add model label if not set
386
+ if (!_.has(model, 'label')) {
387
+ model.label = _.startCase(modelName);
388
+ }
389
+
248
390
  if (_.has(model, 'fields') && !Array.isArray(model.fields)) {
249
391
  model.fields = [];
250
392
  }
@@ -254,13 +396,15 @@ function normalizeConfig(config: any): any {
254
396
  rename(model, 'template', 'layout');
255
397
 
256
398
  updatePageUrlPath(model);
257
- updatePageFilePath(model, config);
258
399
 
259
- addMarkdownContentField(model);
400
+ if (gitCMS) {
401
+ updatePageFilePath(model, config);
402
+ addMarkdownContentField(model);
260
403
 
261
- // TODO: update schema-editor to not show layout field
262
- addLayoutFieldToPageModel(model, pageLayoutKey);
263
- } else if (isDataModel(model)) {
404
+ // TODO: update schema-editor to not show layout field
405
+ addLayoutFieldToPageModel(model, pageLayoutKey);
406
+ }
407
+ } else if (isDataModel(model) && gitCMS) {
264
408
  updateDataFilePath(model, config);
265
409
  }
266
410
 
@@ -276,8 +420,6 @@ function normalizeConfig(config: any): any {
276
420
  assignLabelFieldIfNeeded(model);
277
421
  }
278
422
 
279
- resolveThumbnailPathForModel(model, model?.__metadata?.filePath);
280
-
281
423
  iterateModelFieldsRecursively(model, (field: any) => {
282
424
  // add field label if label is not set
283
425
  if (!_.has(field, 'label')) {
@@ -285,18 +427,12 @@ function normalizeConfig(config: any): any {
285
427
  }
286
428
 
287
429
  if (isListField(field)) {
288
- // 'items.type' of list field default to 'string', set it explicitly
289
- if (!_.has(field, 'items.type')) {
290
- _.set(field, 'items.type', 'string');
291
- }
292
- field = getListItemsField(field);
430
+ field = normalizeListFieldInPlace(field);
431
+ field = field.items;
293
432
  }
294
433
 
295
434
  if (isObjectField(field)) {
296
435
  assignLabelFieldIfNeeded(field);
297
- resolveThumbnailPathForModel(field, model?.__metadata?.filePath);
298
- } else if (isEnumField(field)) {
299
- resolveThumbnailPathForEnumField(field, model?.__metadata?.filePath);
300
436
  } else if (isCustomModelField(field, models)) {
301
437
  // stackbit v0.2.0 compatibility
302
438
  // convert the old custom model field type: { type: 'action' }
@@ -326,7 +462,9 @@ function normalizeConfig(config: any): any {
326
462
  }
327
463
  }
328
464
 
329
- referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
465
+ if (gitCMS) {
466
+ referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
467
+ }
330
468
  });
331
469
  });
332
470
 
@@ -461,6 +599,9 @@ function resolveThumbnailPathForEnumField(enumField: FieldEnum, modelFilePath: s
461
599
  }
462
600
 
463
601
  function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
602
+ if (thumbnail.startsWith('//') || /https?:\/\//.test(thumbnail)) {
603
+ return thumbnail;
604
+ }
464
605
  if (thumbnail.startsWith('/')) {
465
606
  if (modelDirPath.endsWith('@stackbit/components/models')) {
466
607
  modelDirPath = modelDirPath.replace(/\/models$/, '');
@@ -480,7 +621,7 @@ function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
480
621
  */
481
622
  function getReferencedModelNames(field: any) {
482
623
  if (isListField(field)) {
483
- field = getListItemsField(field);
624
+ field = getListFieldItems(field);
484
625
  }
485
626
  // TODO: add type field to model fields inside container update/create object logic rather adding type to schema
486
627
  // 'object' models referenced by 'model' fields should have 'type' field
@@ -501,7 +642,10 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
501
642
  const contentModels = config.contentModels ?? {};
502
643
  const models = config.models ?? {};
503
644
 
504
- if (_.isEmpty(contentModels)) {
645
+ const externalModels = !isGitCMS(config);
646
+ const emptyContentModels = _.isEmpty(contentModels);
647
+
648
+ if (externalModels || emptyContentModels) {
505
649
  return {
506
650
  valid: true,
507
651
  value: config,
@@ -512,17 +656,17 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
512
656
  const validationResult = validateContentModels(contentModels, models);
513
657
 
514
658
  if (_.isEmpty(models)) {
515
- return validationResult;
659
+ return {
660
+ valid: validationResult.valid,
661
+ value: config,
662
+ errors: validationResult.errors
663
+ };
516
664
  }
517
665
 
518
666
  const extendedModels = _.mapValues(models, (model, modelName) => {
519
667
  const contentModel = validationResult.value.contentModels[modelName];
520
668
  if (!contentModel) {
521
- return {
522
- // if a model does not define a type, use the default "object" type
523
- type: model.type || 'object',
524
- ..._.omit(model, 'type')
525
- };
669
+ return model;
526
670
  }
527
671
  if (_.get(contentModel, '__metadata.invalid')) {
528
672
  return model;
@@ -557,10 +701,43 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
557
701
  }
558
702
 
559
703
  function normalizeValidationResult(validationResult: ConfigValidationResult): NormalizedValidationResult {
704
+ validationResult = filterAndOrderConfigFields(validationResult);
560
705
  convertModelGroupsToModelList(validationResult);
561
706
  return convertModelsToArray(validationResult);
562
707
  }
563
708
 
709
+ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
710
+ // TODO: see if we move filtering and sorting to Joi
711
+ return {
712
+ ...validationResult,
713
+ value: _.pick(validationResult.value, [
714
+ 'stackbitVersion',
715
+ 'ssgName',
716
+ 'ssgVersion',
717
+ 'cmsName',
718
+ 'import',
719
+ 'buildCommand',
720
+ 'publishDir',
721
+ 'nodeVersion',
722
+ 'devCommand',
723
+ 'staticDir',
724
+ 'uploadDir',
725
+ 'assets',
726
+ 'pagesDir',
727
+ 'dataDir',
728
+ 'pageLayoutKey',
729
+ 'objectTypeKey',
730
+ 'styleObjectModelName',
731
+ 'excludePages',
732
+ 'logicFields',
733
+ 'contentModels',
734
+ 'modelsSource',
735
+ 'models',
736
+ 'presets'
737
+ ])
738
+ };
739
+ }
740
+
564
741
  function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
565
742
  const models = validationResult.value?.models ?? {};
566
743
 
@@ -590,7 +767,7 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
590
767
  _.forEach(models, (model) => {
591
768
  iterateModelFieldsRecursively(model, (field: any) => {
592
769
  if (isListField(field)) {
593
- field = getListItemsField(field);
770
+ field = field.items;
594
771
  }
595
772
  if (field.groups) {
596
773
  let key: string | null = null;
@@ -632,6 +809,10 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
632
809
  }
633
810
  );
634
811
 
812
+ if (!isGitCMS(config)) {
813
+ addImageModel(modelArray);
814
+ }
815
+
635
816
  const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
636
817
  if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
637
818
  const modelName = error.fieldPath[1];
@@ -652,3 +833,20 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
652
833
  errors: convertedErrors
653
834
  };
654
835
  }
836
+
837
+ function addImageModel(models: Model[]) {
838
+ models.push({
839
+ type: 'image',
840
+ name: '__image_model',
841
+ label: 'Image',
842
+ labelField: 'title',
843
+ fields: [
844
+ { name: 'title', type: 'string' },
845
+ { name: 'url', type: 'string' }
846
+ ]
847
+ });
848
+ }
849
+
850
+ function isGitCMS(config: any) {
851
+ return !config.cmsName || config.cmsName === 'git';
852
+ }
@@ -20,7 +20,10 @@ import {
20
20
  FieldGroupItem,
21
21
  YamlObjectModel,
22
22
  ContentModelMap,
23
- ContentModel
23
+ ContentModel,
24
+ ModelsSourceFiles,
25
+ ModelsSourceContentful,
26
+ ModelsSourceSanity
24
27
  } from './config-types';
25
28
 
26
29
  function getConfigFromValidationState(state: Joi.State): YamlConfig {
@@ -200,6 +203,11 @@ const styleObjectModelNotObject = 'styleObjectModelName.model.type';
200
203
  const styleObjectModelNameSchema = Joi.string()
201
204
  .allow('', null)
202
205
  .custom((value, { error, state }) => {
206
+ const config = getConfigFromValidationState(state);
207
+ const externalModels = config.cmsName && ['contentful', 'sanity'].includes(config.cmsName);
208
+ if (externalModels) {
209
+ return value;
210
+ }
203
211
  const models = getModelsFromValidationState(state);
204
212
  const modelNames = Object.keys(models);
205
213
  if (!modelNames.includes(value)) {
@@ -245,11 +253,32 @@ const importSchema = Joi.alternatives().conditional('.type', {
245
253
  ]
246
254
  });
247
255
 
248
- const modelsSourceSchema = Joi.object<ModelsSource>({
249
- type: 'files',
256
+ const modelsSourceFilesSchema = Joi.object<ModelsSourceFiles>({
257
+ type: Joi.string().valid(Joi.override, 'files').required(),
250
258
  modelDirs: Joi.array().items(Joi.string()).required()
251
259
  });
252
260
 
261
+ const modelsSourceContentfulSchema = Joi.object<ModelsSourceContentful>({
262
+ type: Joi.string().valid(Joi.override, 'contentful').required(),
263
+ module: Joi.string()
264
+ });
265
+
266
+ const modelsSourceSanitySchema = Joi.object<ModelsSourceSanity>({
267
+ type: Joi.string().valid(Joi.override, 'sanity').required(),
268
+ sanityStudioPath: Joi.string().required(),
269
+ module: Joi.string()
270
+ });
271
+
272
+ const modelsSourceSchema = Joi.object<ModelsSource>({
273
+ type: Joi.string().valid('files', 'contentful', 'sanity').required()
274
+ }).when('.type', {
275
+ switch: [
276
+ { is: 'files', then: modelsSourceFilesSchema },
277
+ { is: 'contentful', then: modelsSourceContentfulSchema },
278
+ { is: 'sanity', then: modelsSourceSanitySchema }
279
+ ]
280
+ });
281
+
253
282
  const assetsSchema = Joi.object<Assets>({
254
283
  referenceType: Joi.string().valid('static', 'relative').required(),
255
284
  assetsDir: Joi.string().allow('').when('referenceType', {
@@ -309,7 +338,8 @@ const fieldCommonPropsSchema = Joi.object({
309
338
  group: inGroups,
310
339
  const: Joi.any(),
311
340
  hidden: Joi.boolean(),
312
- readOnly: Joi.boolean()
341
+ readOnly: Joi.boolean(),
342
+ localized: Joi.boolean()
313
343
  }).oxor('const', 'default');
314
344
 
315
345
  const numberFieldPartialSchema = Joi.object({
@@ -485,10 +515,7 @@ const baseModelSchema = Joi.object<YamlBaseModel>({
485
515
  filePath: Joi.string()
486
516
  }),
487
517
  type: Joi.string().valid('page', 'data', 'config', 'object').required(),
488
- label: Joi.string().required().when(Joi.ref('/import'), {
489
- is: Joi.exist(),
490
- then: Joi.optional()
491
- }),
518
+ label: Joi.string(),
492
519
  description: Joi.string(),
493
520
  thumbnail: Joi.string(),
494
521
  extends: Joi.array().items(validObjectModelNames).single(),