@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
@@ -3,7 +3,7 @@ import { CMS_NAMES, FIELD_TYPES, SSG_NAMES, STYLE_PROPS } from './config-consts'
3
3
 
4
4
  export interface Config extends BaseConfig {
5
5
  models: Model[];
6
- presets?: any;
6
+ presets?: Record<string, any>;
7
7
  }
8
8
 
9
9
  export interface YamlConfig extends BaseConfig {
@@ -85,16 +85,29 @@ export interface ContentModel extends BaseMatch {
85
85
  newFilePath?: string;
86
86
  }
87
87
 
88
- export interface ModelsSource {
88
+ export type ModelsSource = ModelsSourceFiles | ModelsSourceContentful | ModelsSourceSanity;
89
+
90
+ export interface ModelsSourceFiles {
89
91
  type: 'files';
90
92
  modelDirs: string[];
91
93
  }
92
94
 
95
+ export interface ModelsSourceContentful {
96
+ type: 'contentful';
97
+ module?: string;
98
+ }
99
+
100
+ export interface ModelsSourceSanity {
101
+ type: 'sanity';
102
+ sanityStudioPath: string;
103
+ module?: string;
104
+ }
105
+
93
106
  /*******************
94
107
  *** Model Types ***
95
108
  *******************/
96
109
 
97
- export type Model = StricterUnion<ObjectModel | DataModel | PageModel | ConfigModel>;
110
+ export type Model = StricterUnion<ObjectModel | DataModel | PageModel | ConfigModel | ImageModel>;
98
111
 
99
112
  export type ObjectModel = YamlObjectModel & BaseModel;
100
113
  export type DataModel = YamlDataModel & BaseModel;
@@ -116,7 +129,7 @@ export interface YamlBaseModel {
116
129
  filePath?: string;
117
130
  invalid?: boolean;
118
131
  };
119
- label: string;
132
+ label?: string;
120
133
  description?: string;
121
134
  thumbnail?: string;
122
135
  extends?: string | string[];
@@ -174,6 +187,14 @@ export interface FieldGroupItem {
174
187
  label: string;
175
188
  }
176
189
 
190
+ export interface ImageModel {
191
+ type: 'image';
192
+ name: '__image_model';
193
+ label?: string;
194
+ labelField?: string;
195
+ fields?: Field[];
196
+ }
197
+
177
198
  /*******************
178
199
  *** Field Types ***
179
200
  *******************/
@@ -209,7 +230,7 @@ export interface FieldCommonProps {
209
230
  export type FieldType = typeof FIELD_TYPES[number];
210
231
 
211
232
  export interface FieldSimpleProps {
212
- type: 'string' | 'url' | 'slug' | 'text' | 'markdown' | 'html' | 'boolean' | 'date' | 'datetime' | 'color' | 'image' | 'file';
233
+ type: 'string' | 'url' | 'slug' | 'text' | 'markdown' | 'html' | 'boolean' | 'date' | 'datetime' | 'color' | 'image' | 'file' | 'json' | 'richText';
213
234
  }
214
235
 
215
236
  export type FieldEnumProps = FieldEnumDropdownProps | FieldEnumThumbnailsProps | FieldEnumPaletteProps;
@@ -29,6 +29,9 @@ export function convertToYamlConfig({ config }: { config: Config }): YamlConfig
29
29
  yamlConfig.models = _.reduce(
30
30
  config.models,
31
31
  (yamlModels: ModelMap, model: Model) => {
32
+ if (model.type === 'image') {
33
+ return yamlModels;
34
+ }
32
35
  const yamlModel = _.omit(model, ['name', '__metadata']) as YamlModel;
33
36
  if (yamlModel.type === 'page' && !yamlModel.hideContent && yamlModel.fields) {
34
37
  _.remove(yamlModel.fields, (field) => field.name === 'markdown_content');
@@ -20,13 +20,14 @@ export async function loadPresets(dirPath: string, config: Config): Promise<Pres
20
20
  if (!(await fse.pathExists(presetsDir))) {
21
21
  continue;
22
22
  }
23
- presetFiles.push(
24
- ...(await fse.readdir(presetsDir)).filter((fileName) => path.parse(fileName).ext === '.json').map((fileName) => path.join(presetsRelDir, fileName))
25
- );
23
+ const files = (await fse.readdir(presetsDir))
24
+ .filter((fileName) => ['.json', '.yaml', '.yml'].includes(path.parse(fileName).ext))
25
+ .map((fileName) => path.join(presetsRelDir, fileName));
26
+ presetFiles.push(...files);
26
27
  }
27
28
 
28
- const presets: any = {};
29
- const presetsByModel: any = {};
29
+ const presets: Record<string, any> = {};
30
+ const presetsIdsByModel: Record<string, any> = {};
30
31
  const errors: ConfigPresetsError[] = [];
31
32
 
32
33
  for (const presetFile of presetFiles) {
@@ -45,22 +46,25 @@ export async function loadPresets(dirPath: string, config: Config): Promise<Pres
45
46
  if (preset.thumbnail) {
46
47
  preset.thumbnail = resolveThumbnailPath(preset.thumbnail, presetsRelDir);
47
48
  }
48
- append(presetsByModel, presetData.model, presetId);
49
+ _.set(preset, 'modelName', presetData.model);
50
+ append(presetsIdsByModel, presetData.model, presetId);
49
51
  });
50
52
  }
51
53
 
52
- // update config with presets
53
- for (const model of config.models) {
54
- const presetsForModel = presetsByModel[model.name];
55
- if (presetsForModel) {
56
- model.presets = presetsForModel;
54
+ // update models with presets IDs
55
+ const models = _.map(config.models, (model) => {
56
+ const presetIdsForModel = presetsIdsByModel[model.name];
57
+ if (!presetIdsForModel) {
58
+ return model;
57
59
  }
58
- }
59
-
60
- config.presets = presets;
60
+ return { ...model, presets: presetIdsForModel };
61
+ });
61
62
 
62
63
  return {
63
- config,
64
+ config: Object.assign({}, config, {
65
+ models,
66
+ presets
67
+ }),
64
68
  errors
65
69
  };
66
70
  }
@@ -10,7 +10,7 @@ import {
10
10
  isDataModel,
11
11
  isPageModel,
12
12
  getModelsByQuery,
13
- getListItemsField,
13
+ getListFieldItems,
14
14
  isListField,
15
15
  isModelField,
16
16
  isListDataModel,
@@ -480,7 +480,7 @@ function addMetadataRecursively({
480
480
  } else if (_.isArray(value)) {
481
481
  let fieldListItems: FieldListItems;
482
482
  if (field && isListField(field)) {
483
- fieldListItems = getListItemsField(field);
483
+ fieldListItems = getListFieldItems(field);
484
484
  } else if (model && isListDataModel(model)) {
485
485
  fieldListItems = model.items;
486
486
  } else {
@@ -106,6 +106,10 @@ function joiSchemaForModelFields(fields: Field[] | undefined, config: Config, fi
106
106
  );
107
107
  }
108
108
 
109
+ function assertUnreachable(field: never): never {
110
+ throw new Error('Unhandled field.type case');
111
+ }
112
+
109
113
  function joiSchemaForField(field: Field | FieldListItems, config: Config, fieldPath: FieldPath) {
110
114
  let fieldSchema;
111
115
  switch (field.type) {
@@ -148,6 +152,11 @@ function joiSchemaForField(field: Field | FieldListItems, config: Config, fieldP
148
152
  case 'list':
149
153
  fieldSchema = listFieldValueSchema(field, config, fieldPath);
150
154
  break;
155
+ case 'json':
156
+ case 'richText':
157
+ return Joi.any().forbidden();
158
+ default:
159
+ assertUnreachable(field);
151
160
  }
152
161
  if ('const' in field) {
153
162
  fieldSchema = fieldSchema.valid(field.const).invalid(null, '').required();
@@ -11,7 +11,7 @@ export function extendModelArray(models: Model[]): { models: Model[]; errors: Co
11
11
  models,
12
12
  (result: { models: Model[]; errors: ConfigValidationError[] }, model) => {
13
13
  // YamlModel is the same as Model just without 'name' and '__metadata' properties
14
- const { model: extendedModel, errors } = memorized(model, model.name, modelsByName);
14
+ const { model: extendedModel, errors } = memorized(model, model.name, modelsByName as ModelMap);
15
15
  return {
16
16
  models: result.models.concat(extendedModel),
17
17
  errors: result.errors.concat(errors)
@@ -101,10 +101,11 @@ function extendModel<T extends Model | YamlModel>(
101
101
  let idx = 0;
102
102
  _.forEach(extendedSuperModel.fields, (superField) => {
103
103
  const field = _.find(fields, { name: superField.name });
104
+ superField = _.cloneDeep(superField);
104
105
  if (field) {
105
- _.defaultsDeep(field, _.cloneDeep(superField));
106
+ _.defaults(field, superField);
106
107
  } else {
107
- fields!.splice(idx++, 0, _.cloneDeep(superField));
108
+ fields!.splice(idx++, 0, superField);
108
109
  }
109
110
  });
110
111
  });
@@ -1,7 +1,7 @@
1
1
  import _ from 'lodash';
2
2
 
3
- import { getListItemsField, isListDataModel, isListField, isObjectListItems, isModelField, isObjectField, isModelListItems } from './model-utils';
4
- import { Field, FieldListItems, FieldModelProps, Model } from '../config/config-types';
3
+ import { getListFieldItems, isListDataModel, isListField, isObjectListItems, isModelField, isObjectField, isModelListItems } from './model-utils';
4
+ import { DataModel, Field, FieldList, FieldListItems, FieldModelProps, FieldObjectProps, Model } from '../config/config-types';
5
5
 
6
6
  /**
7
7
  * This function invokes the `iteratee` function for every field of the `model`.
@@ -39,24 +39,24 @@ import { Field, FieldListItems, FieldModelProps, Model } from '../config/config-
39
39
  * @param model The model to iterate fields
40
40
  * @param iteratee The callback function
41
41
  */
42
- export function iterateModelFieldsRecursively(model: Model, iteratee: (field: Field, fieldPath: string[]) => void) {
43
- function _iterateDeep({ fields, fieldPath }: { fields: Field[]; fieldPath: string[] }) {
44
- fieldPath = fieldPath.concat('fields');
42
+ export function iterateModelFieldsRecursively(model: Model, iteratee: (field: Field, modelKeyPath: string[]) => void) {
43
+ function _iterateDeep({ fields, modelKeyPath }: { fields: Field[]; modelKeyPath: string[] }) {
44
+ modelKeyPath = modelKeyPath.concat('fields');
45
45
  _.forEach(fields, (field) => {
46
46
  if (!field) {
47
47
  return;
48
48
  }
49
- const childFieldPath = fieldPath.concat(field.name);
50
- iteratee(field, childFieldPath);
49
+ const childModelKeyPath = modelKeyPath.concat(field.name);
50
+ iteratee(field, childModelKeyPath);
51
51
  if (isObjectField(field)) {
52
52
  _iterateDeep({
53
53
  fields: field.fields,
54
- fieldPath: childFieldPath
54
+ modelKeyPath: childModelKeyPath
55
55
  });
56
56
  } else if (isListField(field) && field.items && isObjectListItems(field.items)) {
57
57
  _iterateDeep({
58
58
  fields: field.items?.fields,
59
- fieldPath: childFieldPath.concat('items')
59
+ modelKeyPath: childModelKeyPath.concat('items')
60
60
  });
61
61
  }
62
62
  });
@@ -65,16 +65,64 @@ export function iterateModelFieldsRecursively(model: Model, iteratee: (field: Fi
65
65
  if (model && isListDataModel(model) && model.items && isObjectListItems(model.items)) {
66
66
  _iterateDeep({
67
67
  fields: model.items?.fields,
68
- fieldPath: ['items']
68
+ modelKeyPath: ['items']
69
69
  });
70
70
  } else {
71
71
  _iterateDeep({
72
72
  fields: model?.fields || [],
73
- fieldPath: []
73
+ modelKeyPath: []
74
74
  });
75
75
  }
76
76
  }
77
77
 
78
+ export function mapModelFieldsRecursively(model: Model, iteratee: (field: Field, modelKeyPath: string[]) => Field) {
79
+ function _mapField(field: Field, modelKeyPath: string[]): Field {
80
+ if (!field) {
81
+ return field;
82
+ }
83
+ modelKeyPath = modelKeyPath.concat(field.name);
84
+ field = iteratee(field, modelKeyPath);
85
+ if (isObjectField(field)) {
86
+ return _mapObjectField(field, modelKeyPath);
87
+ } else if (isListField(field)) {
88
+ return _mapListField(field, modelKeyPath);
89
+ } else {
90
+ return field;
91
+ }
92
+ }
93
+
94
+ function _mapObjectField<T extends FieldObjectProps | Model>(field: T, modelKeyPath: string[]): T {
95
+ const fields = field.fields;
96
+ if (!fields) {
97
+ return field;
98
+ }
99
+ modelKeyPath = modelKeyPath.concat('fields');
100
+ return {
101
+ ...field,
102
+ fields: _.map(fields, (field) => _mapField(field, modelKeyPath))
103
+ };
104
+ }
105
+
106
+ function _mapListField<T extends FieldList | (DataModel & { isList: true })>(field: T, modelKeyPath: string[]): T {
107
+ const items = field.items;
108
+ if (!items || !isObjectListItems(items)) {
109
+ return field;
110
+ }
111
+ return {
112
+ ...field,
113
+ items: _mapObjectField(items, modelKeyPath.concat('items'))
114
+ };
115
+ }
116
+
117
+ if (!model) {
118
+ return model;
119
+ } else if (isListDataModel(model)) {
120
+ return _mapListField(model, []);
121
+ } else {
122
+ return _mapObjectField(model, []);
123
+ }
124
+ }
125
+
78
126
  export function iterateObjectFieldsWithModelRecursively(
79
127
  value: any,
80
128
  model: Model,
@@ -165,7 +213,7 @@ export function iterateObjectFieldsWithModelRecursively(
165
213
  } else if (_.isArray(value)) {
166
214
  let fieldListItems: FieldListItems | null = null;
167
215
  if (field && isListField(field)) {
168
- fieldListItems = getListItemsField(field);
216
+ fieldListItems = getListFieldItems(field);
169
217
  } else if (model && isListDataModel(model)) {
170
218
  fieldListItems = model.items;
171
219
  }
@@ -287,7 +335,7 @@ export function mapObjectFieldsWithModelRecursively(
287
335
  } else if (_.isArray(value)) {
288
336
  let fieldListItems: FieldListItems | null = null;
289
337
  if (field && isListField(field)) {
290
- fieldListItems = getListItemsField(field);
338
+ fieldListItems = getListFieldItems(field);
291
339
  } else if (model && isListDataModel(model)) {
292
340
  fieldListItems = model.items;
293
341
  }
@@ -79,20 +79,20 @@ export function isEnumField(field: Field): field is FieldEnum {
79
79
  }
80
80
 
81
81
  export function isListOfObjectsField(field: Field): field is FieldListObject {
82
- return isListField(field) && isObjectListItems(getListItemsField(field));
82
+ return isListField(field) && isObjectListItems(getListFieldItems(field));
83
83
  }
84
84
 
85
85
  export function isListOfModelsField(field: Field): field is FieldListModel {
86
- return isListField(field) && isModelListItems(getListItemsField(field));
86
+ return isListField(field) && isModelListItems(getListFieldItems(field));
87
87
  }
88
88
 
89
89
  export function isListOfReferencesField(field: Field): field is FieldListReference {
90
- return isListField(field) && isReferenceListItems(getListItemsField(field));
90
+ return isListField(field) && isReferenceListItems(getListFieldItems(field));
91
91
  }
92
92
 
93
93
  export function isListOfCustomModelsField(field: Field, modelsByName?: Record<string, Model>): field is FieldList {
94
94
  // custom model field types are deprecated
95
- return isListField(field) && isCustomModelListItems(getListItemsField(field), modelsByName);
95
+ return isListField(field) && isCustomModelListItems(getListFieldItems(field), modelsByName);
96
96
  }
97
97
 
98
98
  export function isListField(field: Field): field is FieldList {
@@ -122,7 +122,7 @@ export function isCustomModelListItems(items: FieldListItems, modelsByName?: Rec
122
122
  * items field, the default field is string:
123
123
  *
124
124
  * @example
125
- * listItemField = getListItemsField({
125
+ * listItemField = getListFieldItems({
126
126
  * type: 'list',
127
127
  * name: '...',
128
128
  * items: { type: 'object', fields: [] }
@@ -134,15 +134,36 @@ export function isCustomModelListItems(items: FieldListItems, modelsByName?: Rec
134
134
  * }
135
135
  *
136
136
  * // list field without `items`
137
- * listItemField = getListItemsField({ type: 'list', name: '...' }
137
+ * listItemField = getListFieldItems({ type: 'list', name: '...' }
138
138
  * listItemField => { type: 'string' }
139
139
  *
140
140
  * @param {Object} field
141
141
  * @return {Object}
142
142
  */
143
- export function getListItemsField(field: FieldList): FieldListItems {
143
+ export function getListFieldItems(field: FieldList): FieldListItems {
144
144
  // items.type defaults to string
145
- return _.defaults(field.items, { type: 'string' });
145
+ return Object.assign({ type: 'string' }, field.items);
146
+ }
147
+
148
+ export function normalizeListField(field: FieldList): FieldList {
149
+ if (field.items?.type) {
150
+ return field;
151
+ }
152
+ return {
153
+ ...field,
154
+ items: {
155
+ type: 'string',
156
+ ...(field.items ?? {})
157
+ }
158
+ };
159
+ }
160
+
161
+ export function normalizeListFieldInPlace(field: FieldList): FieldList {
162
+ // 'items.type' of list field default to 'string', set it explicitly
163
+ if (!_.has(field, 'items.type')) {
164
+ _.set(field, 'items.type', 'string');
165
+ }
166
+ return field;
146
167
  }
147
168
 
148
169
  export function assignLabelFieldIfNeeded(modelOrField: Model | FieldObjectProps) {
@@ -176,3 +197,65 @@ export function resolveLabelFieldForModel(modelOrField: Model | FieldObjectProps
176
197
  }
177
198
  return labelField || null;
178
199
  }
200
+
201
+ export function getModelFieldForModelKeyPath(model: Model, modelKeyPath: string[]) {
202
+ function _getField(field: Field, modelKeyPath: string[]): Model | Field | FieldListItems | FieldObjectProps | null {
203
+ if (modelKeyPath.length === 0) {
204
+ return field;
205
+ } else if (isObjectField(field)) {
206
+ return _getObjectFields(field, modelKeyPath);
207
+ } else if (isListField(field)) {
208
+ return _getListItems(field, modelKeyPath);
209
+ } else {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function _getObjectFields<T extends FieldObjectProps | Model>(field: T, modelKeyPath: string[]): Model | Field | FieldListItems | FieldObjectProps | null {
215
+ if (modelKeyPath.length === 0) {
216
+ return field;
217
+ }
218
+ const key = modelKeyPath.shift();
219
+ if (key !== 'fields' || !field.fields) {
220
+ return null;
221
+ }
222
+ if (modelKeyPath.length === 0) {
223
+ return null;
224
+ }
225
+ const fieldName = modelKeyPath.shift();
226
+ const childField = _.find(field.fields, (field) => field.name === fieldName);
227
+ if (!childField) {
228
+ return null;
229
+ }
230
+ return _getField(childField, modelKeyPath);
231
+ }
232
+
233
+ function _getListItems<T extends FieldList | (DataModel & { isList: true })>(
234
+ field: T,
235
+ modelKeyPath: string[]
236
+ ): Model | Field | FieldListItems | FieldObjectProps | null {
237
+ if (modelKeyPath.length === 0) {
238
+ return field;
239
+ }
240
+ const key = modelKeyPath.shift();
241
+ if (key !== 'items' || !field.items) {
242
+ return null;
243
+ }
244
+ if (modelKeyPath.length === 0) {
245
+ return field.items;
246
+ }
247
+ if (!isObjectListItems(field.items)) {
248
+ return null;
249
+ }
250
+ return _getObjectFields(field.items, modelKeyPath);
251
+ }
252
+
253
+ modelKeyPath = modelKeyPath.slice();
254
+ if (!model) {
255
+ return null;
256
+ } else if (isListDataModel(model)) {
257
+ return _getListItems(model, modelKeyPath);
258
+ } else {
259
+ return _getObjectFields(model, modelKeyPath);
260
+ }
261
+ }