@stackbit/sdk 0.3.5 → 0.3.6

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 (56) hide show
  1. package/dist/config/config-loader-static.js +1 -1
  2. package/dist/config/config-loader-static.js.map +1 -1
  3. package/dist/config/config-loader-utils.d.ts +12 -7
  4. package/dist/config/config-loader-utils.d.ts.map +1 -1
  5. package/dist/config/config-loader-utils.js +104 -68
  6. package/dist/config/config-loader-utils.js.map +1 -1
  7. package/dist/config/config-loader.d.ts +46 -27
  8. package/dist/config/config-loader.d.ts.map +1 -1
  9. package/dist/config/config-loader.js +294 -265
  10. package/dist/config/config-loader.js.map +1 -1
  11. package/dist/config/config-schema.d.ts +2 -2
  12. package/dist/config/config-schema.d.ts.map +1 -1
  13. package/dist/config/config-schema.js +123 -55
  14. package/dist/config/config-schema.js.map +1 -1
  15. package/dist/config/config-types.d.ts +4 -3
  16. package/dist/config/config-types.d.ts.map +1 -1
  17. package/dist/config/config-validator.d.ts +9 -5
  18. package/dist/config/config-validator.d.ts.map +1 -1
  19. package/dist/config/config-validator.js +42 -23
  20. package/dist/config/config-validator.js.map +1 -1
  21. package/dist/config/presets-loader.d.ts +13 -4
  22. package/dist/config/presets-loader.d.ts.map +1 -1
  23. package/dist/config/presets-loader.js +49 -23
  24. package/dist/config/presets-loader.js.map +1 -1
  25. package/dist/content/content-schema.js +1 -1
  26. package/dist/content/content-schema.js.map +1 -1
  27. package/dist/index.d.ts +4 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +12 -2
  30. package/dist/index.js.map +1 -1
  31. package/dist/utils/index.d.ts +1 -8
  32. package/dist/utils/index.d.ts.map +1 -1
  33. package/dist/utils/index.js.map +1 -1
  34. package/dist/utils/model-extender.js +1 -1
  35. package/dist/utils/model-extender.js.map +1 -1
  36. package/dist/utils/model-iterators.d.ts +3 -2
  37. package/dist/utils/model-iterators.d.ts.map +1 -1
  38. package/dist/utils/model-iterators.js.map +1 -1
  39. package/dist/utils/model-utils.d.ts +9 -8
  40. package/dist/utils/model-utils.d.ts.map +1 -1
  41. package/dist/utils/model-utils.js +26 -9
  42. package/dist/utils/model-utils.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/config/config-loader-static.ts +1 -1
  45. package/src/config/config-loader-utils.ts +111 -78
  46. package/src/config/config-loader.ts +457 -394
  47. package/src/config/config-schema.ts +150 -81
  48. package/src/config/config-types.ts +6 -3
  49. package/src/config/config-validator.ts +51 -29
  50. package/src/config/presets-loader.ts +59 -30
  51. package/src/content/content-schema.ts +1 -1
  52. package/src/index.ts +21 -2
  53. package/src/utils/index.ts +1 -13
  54. package/src/utils/model-extender.ts +1 -1
  55. package/src/utils/model-iterators.ts +6 -5
  56. package/src/utils/model-utils.ts +38 -16
@@ -2,7 +2,6 @@ import Joi from 'joi';
2
2
  import _ from 'lodash';
3
3
  import { append } from '@stackbit/utils';
4
4
  import {
5
- StackbitConfig,
6
5
  ContentfulImport,
7
6
  SanityImport,
8
7
  Assets,
@@ -16,20 +15,21 @@ import {
16
15
  ModelsSourceSanity,
17
16
  Field,
18
17
  FieldObjectProps,
19
- FieldGroupItem
18
+ FieldGroupItem,
19
+ FieldSpecificProps
20
20
  } from '@stackbit/types';
21
21
 
22
22
  import { CMS_NAMES, FIELD_TYPES, SSG_NAMES } from './config-consts';
23
23
  import { styleFieldPartialSchema } from './config-schema/style-field-schema';
24
- import { YamlBaseModel, YamlConfigModel, YamlDataModel, YamlModel, YamlModelMap, YamlPageModel, YamlObjectModel } from './config-types';
24
+ import { Config, Model, PageModel, ObjectModel, DataModel, ConfigModel } from './config-types';
25
25
 
26
- function getConfigFromValidationState(state: Joi.State): StackbitConfig {
26
+ function getConfigFromValidationState(state: Joi.State): Config {
27
27
  return _.last(state.ancestors)!;
28
28
  }
29
29
 
30
- function getModelsFromValidationState(state: Joi.State): YamlModelMap {
30
+ function getModelsFromValidationState(state: Joi.State): Model[] {
31
31
  const config = getConfigFromValidationState(state);
32
- return config.models ?? {};
32
+ return config.models ?? [];
33
33
  }
34
34
 
35
35
  const fieldNamePattern = /^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
@@ -47,8 +47,7 @@ const fieldNameSchema = Joi.string()
47
47
  const objectModelNameErrorCode = 'model.not.object.model';
48
48
  const validObjectModelNames = Joi.custom((value, { error, state }) => {
49
49
  const models = getModelsFromValidationState(state);
50
- const modelNames = Object.keys(models);
51
- const objectModelNames = modelNames.filter((modelName) => models[modelName]!.type === 'object');
50
+ const objectModelNames = models.filter((model) => model.type === 'object').map((model) => model.name);
52
51
  if (!objectModelNames.includes(value)) {
53
52
  return error(objectModelNameErrorCode);
54
53
  }
@@ -63,9 +62,8 @@ const validObjectModelNames = Joi.custom((value, { error, state }) => {
63
62
  const documentModelNameErrorCode = 'model.not.document.model';
64
63
  const validReferenceModelNames = Joi.custom((value, { error, state }) => {
65
64
  const models = getModelsFromValidationState(state);
66
- const modelNames = Object.keys(models);
67
- const documentModels = modelNames.filter((modelName) => ['page', 'data'].includes(models[modelName]!.type));
68
- if (!documentModels.includes(value)) {
65
+ const documentModelNames = models.filter((model) => ['page', 'data'].includes(model.type)).map((model) => model.name);
66
+ if (!documentModelNames.includes(value)) {
69
67
  return error(documentModelNameErrorCode);
70
68
  }
71
69
  return value;
@@ -123,16 +121,16 @@ const validReferenceFieldGroups = Joi.string()
123
121
  errors: { wrap: { label: false } }
124
122
  });
125
123
 
126
- function getModelNamesForGroup(group: string, config: StackbitConfig) {
127
- const models = config.models ?? {};
124
+ function getModelNamesForGroup(group: string, config: Config) {
125
+ const models = config.models ?? [];
128
126
  return _.reduce(
129
127
  models,
130
- (result: { objectModels: string[]; documentModels: string[] }, model, modelName) => {
128
+ (result: { objectModels: string[]; documentModels: string[] }, model) => {
131
129
  if (model?.groups && _.includes(model.groups, group)) {
132
130
  if (model?.type === 'object') {
133
- result.objectModels.push(modelName);
131
+ result.objectModels.push(model.name);
134
132
  } else {
135
- result.documentModels.push(modelName);
133
+ result.documentModels.push(model.name);
136
134
  }
137
135
  }
138
136
  return result;
@@ -150,7 +148,7 @@ const logicField = Joi.string();
150
148
  const labelFieldNotFoundError = 'labelField.not.found';
151
149
  const labelFieldNotSimple = 'labelField.not.simple';
152
150
  const labelFieldSchema = Joi.custom((value, { error, state }) => {
153
- const modelOrObjectField: YamlBaseModel | FieldObjectProps = _.head(state.ancestors)!;
151
+ const modelOrObjectField: Model | FieldObjectProps = _.head(state.ancestors)!;
154
152
  const fields = modelOrObjectField?.fields ?? [];
155
153
  if (!_.isArray(fields)) {
156
154
  return error(labelFieldNotFoundError);
@@ -174,7 +172,7 @@ const labelFieldSchema = Joi.custom((value, { error, state }) => {
174
172
  const variantFieldNotFoundError = 'variantField.not.found';
175
173
  const variantFieldNotEnum = 'variantField.not.enum';
176
174
  const variantFieldSchema = Joi.custom((value, { error, state }) => {
177
- const modelOrObjectField: YamlBaseModel | FieldObjectProps = _.head(state.ancestors)!;
175
+ const modelOrObjectField: Model | FieldObjectProps = _.head(state.ancestors)!;
178
176
  const fields = modelOrObjectField?.fields ?? [];
179
177
  if (!_.isArray(fields)) {
180
178
  return error(variantFieldNotFoundError);
@@ -206,11 +204,11 @@ const styleObjectModelNameSchema = Joi.string()
206
204
  return value;
207
205
  }
208
206
  const models = getModelsFromValidationState(state);
209
- const modelNames = Object.keys(models);
210
- if (!modelNames.includes(value)) {
207
+ const styleObjectModel = models.find((model) => model.name === value);
208
+ if (!styleObjectModel) {
211
209
  return error(styleObjectModelReferenceError);
212
210
  }
213
- if (models[value]!.type !== 'data') {
211
+ if (styleObjectModel.type !== 'data') {
214
212
  return error(styleObjectModelNotObject);
215
213
  }
216
214
  return value;
@@ -484,6 +482,7 @@ const fieldsSchema = Joi.array().items(fieldSchema).unique('name').id('fieldsSch
484
482
  const contentModelKeyNotFound = 'contentModel.model.not.found';
485
483
  const contentModelTypeNotPage = 'contentModel.type.not.page';
486
484
  const contentModelTypeNotData = 'contentModel.type.not.data';
485
+
487
486
  const contentModelSchema = Joi.object<ContentModel>({
488
487
  isPage: Joi.boolean(),
489
488
  newFilePath: Joi.string(),
@@ -502,12 +501,12 @@ const contentModelSchema = Joi.object<ContentModel>({
502
501
  })
503
502
  })
504
503
  .custom((contentModel, { error, state, prefs }) => {
505
- const models = _.get(prefs, 'context.models');
506
- if (!models) {
504
+ const modelMap = _.get(prefs, 'context.modelMap');
505
+ if (!modelMap) {
507
506
  return contentModel;
508
507
  }
509
508
  const modelName = _.last(state.path)!;
510
- const model = models[modelName];
509
+ const model = modelMap[modelName];
511
510
  if (!model) {
512
511
  return error(contentModelKeyNotFound, { modelName });
513
512
  } else if (contentModel.isPage && model.type && !['page', 'object'].includes(model.type)) {
@@ -536,10 +535,25 @@ export const contentModelsSchema = Joi.object({
536
535
  contentModels: Joi.object<ContentModelMap>().pattern(Joi.string(), contentModelSchema)
537
536
  });
538
537
 
539
- const baseModelSchema = Joi.object<YamlBaseModel>({
538
+ const modelNamePattern = /^[a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])?$/;
539
+ const modelNameError =
540
+ 'Invalid model name "{{#value}}" at "{{#label}}". A model name must contain only alphanumeric characters ' +
541
+ 'and underscores, must start with a letter, and end with alphanumeric character.';
542
+ const modelNameSchema = Joi.string()
543
+ .required()
544
+ .pattern(modelNamePattern)
545
+ .prefs({
546
+ messages: { 'string.pattern.base': modelNameError },
547
+ errors: { wrap: { label: false } }
548
+ });
549
+
550
+ const baseModelSchema = Joi.object<Model & { srcType: string; srcProjectId: string }>({
540
551
  __metadata: Joi.object({
541
552
  filePath: Joi.string()
542
553
  }),
554
+ name: modelNameSchema,
555
+ srcType: Joi.string(),
556
+ srcProjectId: Joi.string(),
543
557
  type: Joi.string().valid('page', 'data', 'config', 'object').required(),
544
558
  label: Joi.string(),
545
559
  description: Joi.string(),
@@ -553,13 +567,13 @@ const baseModelSchema = Joi.object<YamlBaseModel>({
553
567
  fields: Joi.link('#fieldsSchema')
554
568
  });
555
569
 
556
- const objectModelSchema: Joi.ObjectSchema<YamlObjectModel> = baseModelSchema.concat(
570
+ const objectModelSchema: Joi.ObjectSchema<ObjectModel> = baseModelSchema.concat(
557
571
  Joi.object({
558
572
  type: Joi.string().valid('object').required()
559
573
  })
560
574
  );
561
575
 
562
- const dataModelSchema: Joi.ObjectSchema<YamlDataModel> = baseModelSchema
576
+ const dataModelSchema: Joi.ObjectSchema<DataModel> = baseModelSchema
563
577
  .concat(
564
578
  Joi.object({
565
579
  type: Joi.string().valid('data').required(),
@@ -588,14 +602,14 @@ const dataModelSchema: Joi.ObjectSchema<YamlDataModel> = baseModelSchema
588
602
  })
589
603
  });
590
604
 
591
- const configModelSchema: Joi.ObjectSchema<YamlConfigModel> = baseModelSchema.concat(
605
+ const configModelSchema: Joi.ObjectSchema<ConfigModel> = baseModelSchema.concat(
592
606
  Joi.object({
593
607
  type: Joi.string().valid('config').required(),
594
608
  file: Joi.string()
595
609
  })
596
610
  );
597
611
 
598
- const pageModelSchema: Joi.ObjectSchema<YamlPageModel> = baseModelSchema
612
+ const pageModelSchema: Joi.ObjectSchema<PageModel> = baseModelSchema
599
613
  .concat(
600
614
  Joi.object({
601
615
  type: Joi.string().valid('page').required(),
@@ -621,35 +635,122 @@ const pageModelSchema: Joi.ObjectSchema<YamlPageModel> = baseModelSchema
621
635
  })
622
636
  .when('.singleInstance', { is: true, then: { file: Joi.required() } });
623
637
 
624
- const modelSchema = Joi.object<YamlModel>({
625
- type: Joi.string().valid('page', 'data', 'config', 'object').required()
626
- }).when('.type', {
627
- switch: [
628
- { is: 'object', then: objectModelSchema },
629
- { is: 'data', then: dataModelSchema },
630
- { is: 'config', then: configModelSchema },
631
- { is: 'page', then: pageModelSchema }
632
- ]
633
- });
634
-
635
- const modelNamePattern = /^[a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])?$/;
636
- const modelNamePatternMatchErrorCode = 'model.name.pattern.match';
637
638
  const modelFileExclusiveErrorCode = 'model.file.only';
638
639
  const modelIsListItemsRequiredErrorCode = 'model.isList.items.required';
639
640
  const modelIsListFieldsForbiddenErrorCode = 'model.isList.fields.forbidden';
640
641
  const modelListForbiddenErrorCode = 'model.items.forbidden';
641
642
  const fieldNameUnique = 'field.name.unique';
643
+
644
+ function errorLabelFromModelAndFieldPath(model: Model, modelIndex: number, fieldPath: (string | number | object)[]): string {
645
+ let fieldSpecificProps: Model | FieldSpecificProps | undefined = model;
646
+ let fields: Field[] | undefined;
647
+ let label = `models[${ model.name ? `name='${model.name}'` : modelIndex }]`;
648
+
649
+ for (const pathPart of fieldPath) {
650
+ if (typeof pathPart === 'string') {
651
+ if (pathPart === 'fields' && fieldSpecificProps && 'fields' in fieldSpecificProps) {
652
+ fields = fieldSpecificProps.fields;
653
+ fieldSpecificProps = undefined;
654
+ label += '.fields';
655
+ } else if (pathPart === 'items' && fieldSpecificProps && 'items' in fieldSpecificProps) {
656
+ fieldSpecificProps = fieldSpecificProps.items;
657
+ label += '.items';
658
+ } else {
659
+ fieldSpecificProps = undefined;
660
+ label += `.${pathPart}`;
661
+ }
662
+ } else if (fields && typeof pathPart === 'number') {
663
+ const field = fields[pathPart];
664
+ label += '[' + (field?.name ? `name='${field.name}'` : pathPart) + ']';
665
+ fieldSpecificProps = field;
666
+ fields = undefined;
667
+ } else {
668
+ // when the schema is marked as Joi.array().items(...).single()
669
+ // and the validated value is not an array, Joi injects (new Number(0))
670
+ // which is an object. Don't use it to generate path
671
+ if (typeof pathPart === 'object') {
672
+ continue;
673
+ }
674
+ label += `[${pathPart}]`;
675
+ }
676
+ }
677
+ return label;
678
+ }
679
+
680
+ const modelSchema = Joi.object<Model>({
681
+ type: Joi.string().valid('page', 'data', 'config', 'object').required(),
682
+ name: Joi.string()
683
+ })
684
+ .when('.type', {
685
+ switch: [
686
+ { is: 'object', then: objectModelSchema },
687
+ { is: 'data', then: dataModelSchema },
688
+ { is: 'config', then: configModelSchema },
689
+ { is: 'page', then: pageModelSchema }
690
+ ]
691
+ })
692
+ .error(((errors: Joi.ErrorReport[]): Joi.ErrorReport[] => {
693
+ return _.map(errors, (error) => {
694
+ if (error.path[0] === 'models' && typeof error.path[1] === 'number') {
695
+ const modelIndex = error.path[1];
696
+ const config = error.prefs.context?.config as Config;
697
+ if (config && config.models[modelIndex]) {
698
+ const model = config.models[modelIndex]!;
699
+ error.path = error.path.slice();
700
+ error.path.splice(1, 1, model.name);
701
+ const label = errorLabelFromModelAndFieldPath(model, modelIndex, error.path.slice(2));
702
+ _.set(error, 'local.label', label);
703
+ // const label = _.get(error, 'local.label', '') as string;
704
+ // _.set(error, 'local.label', label.replace(/models\[(\d+)]/, (match, indexMatch) => {
705
+ // if (Number(indexMatch) !== modelIndex) {
706
+ // return match;
707
+ // }
708
+ // return `models[name='${model.name}']`;
709
+ // }));
710
+ }
711
+ }
712
+ if (
713
+ error.code === 'any.unknown' &&
714
+ error.path.length === 3 &&
715
+ error.path[0] === 'models' &&
716
+ error.path[2] &&
717
+ ['folder', 'match', 'exclude'].includes(error.path[2])
718
+ ) {
719
+ error.code = modelFileExclusiveErrorCode;
720
+ } else if (error.code === 'any.required' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'items') {
721
+ error.code = modelIsListItemsRequiredErrorCode;
722
+ } else if (error.code === 'any.unknown' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'fields') {
723
+ error.code = modelIsListFieldsForbiddenErrorCode;
724
+ } else if (error.code === 'object.unknown' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'items') {
725
+ error.code = modelListForbiddenErrorCode;
726
+ } else if (error.code === 'array.unique' && error.path.length > 3 && error.path[0] === 'models' && _.nth(error.path, -2) === 'fields') {
727
+ error.code = fieldNameUnique;
728
+ }
729
+ return error;
730
+ });
731
+ }) as any) // the type definition of Joi.ValidationErrorFunction is wrong, so we override
732
+ .prefs({
733
+ messages: {
734
+ [modelFileExclusiveErrorCode]: '{{#label}} cannot be used with "file"',
735
+ [modelIsListItemsRequiredErrorCode]: '{{#label}} is required when "isList" is true',
736
+ [modelIsListFieldsForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is true',
737
+ [modelListForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is not true',
738
+ [fieldNameUnique]: '{{#label}} contains a duplicate field name "{{#value.name}}"'
739
+ },
740
+ errors: { wrap: { label: false } }
741
+ });
742
+
642
743
  const groupModelsIncompatibleError = 'group.models.incompatible';
643
744
 
644
- const modelsSchema = Joi.object<YamlModelMap>()
645
- .pattern(modelNamePattern, modelSchema)
646
- .custom((models: YamlModelMap, { error }) => {
745
+ const modelsSchema = Joi.array()
746
+ .items(modelSchema)
747
+ .custom((models: Model[], { error }) => {
647
748
  const groupMap: Record<string, Record<'objectModels' | 'documentModels', string[]>> = {};
648
749
 
649
- _.forEach(models, (model, modelName) => {
750
+ _.forEach(models, (model) => {
650
751
  const key = model?.type === 'object' ? 'objectModels' : 'documentModels';
651
752
  _.forEach(model.groups, (groupName) => {
652
- append(groupMap, [groupName, key], modelName);
753
+ append(groupMap, [groupName, key], model.name);
653
754
  });
654
755
  });
655
756
 
@@ -674,42 +775,10 @@ const modelsSchema = Joi.object<YamlModelMap>()
674
775
 
675
776
  return models;
676
777
  })
677
- .error(((errors: Joi.ErrorReport[]): Joi.ErrorReport[] => {
678
- return _.map(errors, (error) => {
679
- if (error.code === 'object.unknown' && error.path.length === 2 && error.path[0] === 'models') {
680
- error.code = modelNamePatternMatchErrorCode;
681
- } else if (
682
- error.code === 'any.unknown' &&
683
- error.path.length === 3 &&
684
- error.path[0] === 'models' &&
685
- error.path[2] &&
686
- ['folder', 'match', 'exclude'].includes(error.path[2])
687
- ) {
688
- error.code = modelFileExclusiveErrorCode;
689
- } else if (error.code === 'any.required' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'items') {
690
- error.code = modelIsListItemsRequiredErrorCode;
691
- } else if (error.code === 'any.unknown' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'fields') {
692
- error.code = modelIsListFieldsForbiddenErrorCode;
693
- } else if (error.code === 'object.unknown' && error.path.length === 3 && error.path[0] === 'models' && error.path[2] === 'items') {
694
- error.code = modelListForbiddenErrorCode;
695
- } else if (error.code === 'array.unique' && error.path.length > 3 && error.path[0] === 'models' && _.nth(error.path, -2) === 'fields') {
696
- error.code = fieldNameUnique;
697
- }
698
- return error;
699
- });
700
- }) as any) // the type definition of Joi.ValidationErrorFunction is wrong, so we override
701
778
  .prefs({
702
779
  messages: {
703
780
  [groupModelsIncompatibleError]:
704
- 'Model groups must include models of the same type. The following groups have incompatible models: {{#incompatibleGroups}}',
705
- [modelNamePatternMatchErrorCode]:
706
- 'Invalid model name "{{#key}}" at "{{#label}}". A model name must contain only alphanumeric characters ' +
707
- 'and underscores, must start with a letter, and end with alphanumeric character.',
708
- [modelFileExclusiveErrorCode]: '{{#label}} cannot be used with "file"',
709
- [modelIsListItemsRequiredErrorCode]: '{{#label}} is required when "isList" is true',
710
- [modelIsListFieldsForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is true',
711
- [modelListForbiddenErrorCode]: '{{#label}} is not allowed when "isList" is not true',
712
- [fieldNameUnique]: '{{#label}} contains a duplicate field name "{{#value.name}}"'
781
+ 'Model groups must include models of the same type. The following groups have incompatible models: {{#incompatibleGroups}}'
713
782
  },
714
783
  errors: { wrap: { label: false } }
715
784
  });
@@ -745,7 +814,7 @@ const sidebarButtonSchema = Joi.object({
745
814
  ]
746
815
  });
747
816
 
748
- export const stackbitConfigSchema = Joi.object<StackbitConfig>({
817
+ export const stackbitConfigSchema = Joi.object<Config>({
749
818
  stackbitVersion: Joi.string().required(),
750
819
  ssgName: Joi.string().valid(...SSG_NAMES),
751
820
  ssgVersion: Joi.string(),
@@ -64,11 +64,13 @@ export interface Config extends Omit<StackbitConfig, 'models' | 'customContentRe
64
64
  dirPath: string;
65
65
  filePath: string;
66
66
  models: Model[];
67
- presets?: Record<string, Preset>;
67
+ presets?: PresetMap;
68
68
  internalStackbitRunnerOptions?: SSGRunOptions;
69
69
  hcrHandled?: boolean;
70
70
  }
71
71
 
72
+ export type PresetMap = Record<string, Preset>;
73
+
72
74
  export interface Preset {
73
75
  label: string;
74
76
  modelName: string;
@@ -91,12 +93,13 @@ export interface SSGRunOptions {
91
93
  *** Normalized Model Types ***
92
94
  ******************************/
93
95
 
94
- export type Model = ObjectModel | DataModel | PageModel | ConfigModel | ImageModel;
96
+ export type Model = ObjectModel | DataModel | PageModel | ConfigModel;
95
97
 
96
98
  export type ObjectModel = StackbitConfigObjectModel & BaseModel;
97
99
  export type DataModel = DataModelSingle | DataModelList;
98
100
  export type PageModel = StackbitConfigPageModel & BaseModel;
99
101
  export type ConfigModel = StackbitConfigConfigModel & BaseModel;
102
+
100
103
  export type ImageModel = {
101
104
  type: 'image';
102
105
  name: '__image_model';
@@ -115,7 +118,7 @@ export interface DataModelList extends StackbitConfigDataModel, BaseModel {
115
118
  items: FieldListItems;
116
119
  }
117
120
 
118
- type BaseModel = ModelMetadata & { presets?: string[] };
121
+ export type BaseModel = ModelMetadata & { presets?: string[] };
119
122
 
120
123
  interface ModelMetadata {
121
124
  __metadata?: {
@@ -2,47 +2,67 @@ import _ from 'lodash';
2
2
  import Joi from 'joi';
3
3
  import { ContentModelMap } from '@stackbit/types';
4
4
 
5
- import { stackbitConfigSchema, contentModelsSchema } from './config-schema';
5
+ import { contentModelsSchema, stackbitConfigSchema } from './config-schema';
6
6
  import { ConfigValidationError } from './config-errors';
7
- import { RawConfigWithPaths } from './config-loader-utils';
8
- import { YamlModelMap } from './config-types';
7
+ import { Config, Model } from './config-types';
9
8
 
10
9
  export interface ConfigValidationResult {
11
- value: RawConfigWithPaths;
10
+ config: Config;
12
11
  valid: boolean;
13
12
  errors: ConfigValidationError[];
14
13
  }
15
14
 
16
- export function validateConfig(config: RawConfigWithPaths): ConfigValidationResult {
17
- const validationOptions = { abortEarly: false };
15
+ export function validateConfig(config: Config): ConfigValidationResult {
16
+ const validationOptions = { abortEarly: false, context: { config: config } };
18
17
  const validationResult = stackbitConfigSchema.validate(config, validationOptions);
19
- const value = validationResult.value;
18
+ const validatedConfig: Config = validationResult.value;
20
19
  const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
21
20
  const valid = _.isEmpty(errors);
22
- markInvalidModels(value, errors, 'models');
21
+ const invalidModelNames = getInvalidModelNames(errors, 'models', config);
22
+ const validatedModels = validatedConfig.models ?? [];
23
+ _.forEach(validatedModels, (model): any => {
24
+ if (invalidModelNames.includes(model.name)) {
25
+ _.set(model, '__metadata.invalid', true);
26
+ }
27
+ });
23
28
  return {
24
- value,
29
+ config: validatedConfig,
25
30
  valid,
26
31
  errors
27
32
  };
28
33
  }
29
34
 
30
- export function validateContentModels(contentModels: ContentModelMap, models: YamlModelMap): ConfigValidationResult {
35
+ export interface ContentModelsValidationResult {
36
+ contentModels: ContentModelMap;
37
+ valid: boolean;
38
+ errors: ConfigValidationError[];
39
+ }
40
+
41
+ export function validateContentModels(contentModels: ContentModelMap, models: Model[]): ContentModelsValidationResult {
42
+ const modelMap: Record<string, Model> = _.keyBy(models, 'name');
43
+ const config = { contentModels: contentModels };
31
44
  const validationResult = contentModelsSchema.validate(
32
- { contentModels: contentModels },
45
+ config,
33
46
  {
34
47
  abortEarly: false,
35
48
  context: {
36
- models: models
49
+ modelMap: modelMap
37
50
  }
38
51
  }
39
52
  );
40
- const value = validationResult.value;
53
+ const validatedConfig: Config = validationResult.value;
41
54
  const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
42
55
  const valid = _.isEmpty(errors);
43
- markInvalidModels(value, errors, 'contentModels');
56
+ const invalidModelNames = getInvalidModelNames(errors, 'contentModels', config);
57
+ const validatedContentModels = validatedConfig.contentModels ?? {};
58
+ _.forEach(validatedContentModels, (contentModel, modelName) => {
59
+ if (invalidModelNames.includes(modelName)) {
60
+ _.set(contentModel, '__metadata.invalid', true);
61
+ }
62
+ });
63
+
44
64
  return {
45
- value,
65
+ contentModels: validatedContentModels,
46
66
  valid,
47
67
  errors
48
68
  };
@@ -52,35 +72,37 @@ function mapJoiErrorsToConfigValidationErrors(validationResult: Joi.ValidationRe
52
72
  const joiErrors = validationResult.error?.details || [];
53
73
  return joiErrors.map(
54
74
  (validationError): ConfigValidationError => {
75
+ const normFieldPath = validationError.path.slice();
76
+ if (validationError.path[0] === 'models' && typeof validationError.path[1] == 'string') {
77
+ const modelName = validationError.path[1];
78
+ normFieldPath[1] = _.findIndex(validationResult.value.models, { name: modelName });
79
+ }
55
80
  return new ConfigValidationError({
56
81
  type: validationError.type,
57
82
  message: validationError.message,
58
83
  fieldPath: validationError.path,
84
+ normFieldPath: normFieldPath,
59
85
  value: validationError.context?.value
60
86
  });
61
87
  }
62
88
  );
63
89
  }
64
90
 
65
- function markInvalidModels(config: any, errors: ConfigValidationError[], configKey: string) {
66
- const invalidModelNames = getInvalidModelNames(errors, configKey);
67
- const models = config[configKey] ?? {};
68
- _.forEach(models, (model: any, modelName: string): any => {
69
- if (invalidModelNames.includes(modelName)) {
70
- _.set(model, '__metadata.invalid', true);
71
- }
72
- });
73
- }
74
-
75
- function getInvalidModelNames(errors: ConfigValidationError[], configKey: string) {
91
+ function getInvalidModelNames(errors: ConfigValidationError[], configKey: string, value: any) {
76
92
  // get array of invalid model names by iterating errors and filtering these
77
93
  // having fieldPath starting with ['models', modelName]
78
94
  return _.reduce(
79
95
  errors,
80
96
  (modelNames: string[], error: ConfigValidationError) => {
81
- if (error.fieldPath[0] === configKey && typeof error.fieldPath[1] == 'string') {
82
- const modelName = error.fieldPath[1];
83
- modelNames.push(modelName);
97
+ if (error.fieldPath[0] === configKey) {
98
+ if (typeof error.fieldPath[1] == 'string') {
99
+ modelNames.push(error.fieldPath[1]);
100
+ } else if (typeof error.fieldPath[1] == 'number') {
101
+ const model = value[configKey][error.fieldPath[1]];
102
+ if (model?.name) {
103
+ modelNames.push(model?.name);
104
+ }
105
+ }
84
106
  }
85
107
  return modelNames;
86
108
  },