@stackbit/cms-sanity 0.2.44-staging.2 → 0.2.45-develop.2

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.
@@ -1,54 +1,276 @@
1
1
  import _ from 'lodash';
2
- import { Field, FieldGroupItem, Model } from '@stackbit/types';
3
- import * as ContentSourceTypes from '@stackbit/types';
4
- import { deepMap, omitByNil } from '@stackbit/utils';
2
+ import * as StackbitTypes from '@stackbit/types';
3
+ import type * as SanityTypes from '@sanity/types';
4
+ import { deepMap, omitByNil, omitByUndefined } from '@stackbit/utils';
5
5
  import { resolveLabelFieldForModel } from './utils';
6
6
 
7
- // sanity cloudinary plugin adds these models, we are replacing them to our internal image model, so we removing them from schema response
7
+ // Sanity's cloudinary and other 3rd party DRM plugins add the following models.
8
+ // These models are removed when converting the schema and the fields referencing
9
+ // these models are replaced with our regular "image" field with a "source" property
10
+ // matching to the name of the supported DRM.
8
11
  const thirdPartyImageModels = ['cloudinary.asset', 'cloudinary.assetDerived', 'bynder.asset', 'aprimo.asset', 'aprimo.cdnasset'];
9
- const skipModels = [...thirdPartyImageModels, 'media.tag'];
12
+ const skipModels = [...thirdPartyImageModels, 'media.tag', 'slug', 'markdown', 'json', 'color', 'hslaColor', 'hsvaColor', 'rgbaColor'];
13
+ const internationalizedArrayPrefix = 'internationalizedArray';
10
14
 
11
- export function convertSchema(schema: any): { models: Model[] } {
12
- // sanity schema allow arrays to be at the root level, we don't.
13
- // map all root arrays, then pass them to rest of the model and reference them as if they were inline
14
- // hope there is no cyclic references from array objects from within array objects
15
- const filteredModels = schema.models.filter((model: any) => !model.name.startsWith('sanity.') && !skipModels.includes(model.name));
16
- const [arrayModels, nonArrayModels] = _.partition(filteredModels, { type: 'array' });
17
- const arrayModelsByName = _.keyBy(arrayModels, 'name');
18
- const models = nonArrayModels.map((model) => {
19
- return mapObjectModel(model, arrayModelsByName);
15
+ export type SchemaContext = null;
16
+
17
+ export type ModelWithContext = StackbitTypes.Model<ModelContext>;
18
+ export type ModelContext = {
19
+ localizedFieldsModelMap?: LocalizedFieldsModelMap;
20
+ fieldAliasMap?: FieldAliasMap;
21
+ } | null;
22
+
23
+ /**
24
+ * Maps model fields to the Internationalized Array types
25
+ * The key is the field path of the field within the model.
26
+ */
27
+ export type LocalizedFieldsModelMap = Record<
28
+ string,
29
+ {
30
+ arrayModelName: string;
31
+ arrayValueModelName: string;
32
+ }
33
+ >;
34
+
35
+ export type FieldAliasMap = Record<
36
+ string,
37
+ {
38
+ origTypeName: string;
39
+ resolvedTypeName: string;
40
+ }[]
41
+ >;
42
+
43
+ /**
44
+ * Converts Sanity schema to Netlify Create Schema.
45
+ *
46
+ * @param schema Schema as received from sanity-schema-converter.js
47
+ * @param defaultLocale The default locale specified in the SanityContentSource constructor.
48
+ */
49
+ export function convertSchema({
50
+ schema,
51
+ logger,
52
+ defaultLocale
53
+ }: {
54
+ schema: { models: SanityTypes.SchemaTypeDefinition[] };
55
+ logger: StackbitTypes.Logger;
56
+ defaultLocale?: string;
57
+ }): {
58
+ models: ModelWithContext[];
59
+ locales: StackbitTypes.Locale[];
60
+ } {
61
+ // First, skip all Sanity internal models with names starting with "sanity."
62
+ // and models defined in skipModels.
63
+ const filteredModels = schema.models.filter((model) => {
64
+ return !model.name.startsWith('sanity.') && !skipModels.includes(model.name);
65
+ });
66
+
67
+ // Split models into three groups:
68
+ // 1. Regular "document" and "objets" models that map to Netlify Create's
69
+ // "data" and "object" models respectively.
70
+ // 2. Models with names starting with "internationalizedArray...". These are
71
+ // special models produced by Sanity's internationalized-array plugin
72
+ // https://www.sanity.io/plugins/internationalized-array
73
+ // 3. Type alias models, which are aliases to regular fields like "array",
74
+ // "string", etc.
75
+ const {
76
+ i18nModels = [],
77
+ documentAndObjectModels = [],
78
+ typeAliasModels = []
79
+ } = _.groupBy(filteredModels, (model) => {
80
+ if (model.name.startsWith(internationalizedArrayPrefix)) {
81
+ return 'i18nModels';
82
+ } else if (['document', 'object'].includes(model.type)) {
83
+ return 'documentAndObjectModels';
84
+ } else {
85
+ return 'typeAliasModels';
86
+ }
87
+ }) as unknown as {
88
+ i18nModels: SanityTypes.TypeAliasDefinition<string, SanityTypes.IntrinsicTypeName | undefined>[];
89
+ documentAndObjectModels: (SanityTypes.DocumentDefinition | SanityTypes.ObjectDefinition)[];
90
+ typeAliasModels: SanityTypes.TypeAliasDefinition<string, SanityTypes.IntrinsicTypeName | undefined>[];
91
+ };
92
+
93
+ // Get all locale codes from Internationalized Array models
94
+ const locales = getLocalesFromInternationalizedArrays(i18nModels, defaultLocale);
95
+ // Create map of Internationalized Array
96
+ const { i18nArrayModelMap, i18nValueModelMap } = getLocalizedModelMap(i18nModels);
97
+
98
+ const typeAliasMap = _.keyBy(typeAliasModels, 'name');
99
+
100
+ const models = documentAndObjectModels.map((model): StackbitTypes.DataModel<ModelContext> | StackbitTypes.ObjectModel<ModelContext> => {
101
+ const localizedFieldsModelMap = {};
102
+ const fieldAliasMap = {};
103
+ try {
104
+ const stackbitModel = mapObjectModel({
105
+ model,
106
+ modelFieldPath: [],
107
+ typeAliasMap,
108
+ i18nArrayModelMap,
109
+ localizedFieldsModelMap,
110
+ fieldAliasMap,
111
+ seenAliases: []
112
+ });
113
+ let context: ModelContext = null;
114
+ if (!_.isEmpty(localizedFieldsModelMap) || !_.isEmpty(fieldAliasMap)) {
115
+ context = {
116
+ ...(_.isEmpty(localizedFieldsModelMap) ? null : { localizedFieldsModelMap }),
117
+ ...(_.isEmpty(fieldAliasMap) ? null : { fieldAliasMap })
118
+ };
119
+ }
120
+ return {
121
+ ...stackbitModel,
122
+ context
123
+ };
124
+ } catch (error: any) {
125
+ logger.error(`Error converting model '${model.name}'. ${error.message}`);
126
+ throw error;
127
+ }
20
128
  });
21
- return { models };
129
+ return {
130
+ models,
131
+ locales
132
+ };
22
133
  }
23
134
 
24
- function mapObjectModel(model: any, arrayModelsByName: ContentSourceTypes.ModelMap): Model {
25
- // model name can be null for nested objects
135
+ type CommonProps = {
136
+ modelFieldPath: string[];
137
+ typeAliasMap: Record<string, any>;
138
+ i18nArrayModelMap: Record<string, any>;
139
+ localizedFieldsModelMap: LocalizedFieldsModelMap;
140
+ fieldAliasMap: FieldAliasMap;
141
+ seenAliases: string[];
142
+ };
143
+ type SanityObjectFieldOrArrayItem = SanityIntrinsicFieldOrArrayItem<'object'>;
144
+ type MapObjectModelOptions<Type extends SanityTypes.DocumentDefinition | SanityTypes.ObjectDefinition | SanityObjectFieldOrArrayItem> = {
145
+ model: Type;
146
+ } & CommonProps;
147
+ function mapObjectModel(
148
+ options: MapObjectModelOptions<SanityTypes.DocumentDefinition | SanityTypes.ObjectDefinition>
149
+ ): StackbitTypes.DataModel | StackbitTypes.ObjectModel;
150
+ function mapObjectModel(options: MapObjectModelOptions<SanityObjectFieldOrArrayItem>): StackbitTypes.FieldObject;
151
+ function mapObjectModel({
152
+ model,
153
+ ...rest
154
+ }: MapObjectModelOptions<SanityTypes.DocumentDefinition | SanityTypes.ObjectDefinition | SanityObjectFieldOrArrayItem>):
155
+ | StackbitTypes.DataModel
156
+ | StackbitTypes.ObjectModel
157
+ | StackbitTypes.FieldObject {
26
158
  const modelName = _.get(model, 'name', null);
27
159
  const modelLabel = _.get(model, 'title', modelName ? _.startCase(modelName) : null);
28
- const sanityFieldGroups: FieldGroupItem[] = _.get(model, 'groups', []).map((group: any) => ({ name: group.name, label: group.title }));
160
+ const sanityFieldGroups: StackbitTypes.FieldGroupItem[] = _.get(model, 'groups', []).map((group: any) => ({ name: group.name, label: group.title }));
29
161
  const fields = _.get(model, 'fields', []);
30
- const mappedFields = mapObjectFields(fields, arrayModelsByName);
162
+ const mappedFields = mapObjectFields({
163
+ fields,
164
+ ...rest
165
+ });
31
166
 
32
167
  return omitByNil({
168
+ // TODO: ensure type aliases work for documents
33
169
  type: getNormalizedModelType(model),
34
170
  name: modelName,
35
171
  label: modelLabel,
36
172
  labelField: resolveLabelFieldForModel(model, 'preview.select.title', mappedFields),
37
173
  fieldGroups: sanityFieldGroups,
38
174
  fields: mappedFields
39
- }) as Model;
175
+ }) as StackbitTypes.DataModel | StackbitTypes.ObjectModel | StackbitTypes.FieldObject;
40
176
  }
41
177
 
42
- function mapObjectFields(fields: any[], arrayModelsByName: ContentSourceTypes.ModelMap): Field[] {
178
+ function mapObjectFields({ fields, modelFieldPath, ...rest }: { fields: SanityTypes.FieldDefinition[] } & CommonProps): StackbitTypes.Field[] {
43
179
  return _.map(fields, (field) => {
44
- return mapField(field, arrayModelsByName);
180
+ return mapField({
181
+ field,
182
+ modelFieldPath: modelFieldPath.concat(field.name),
183
+ ...rest
184
+ });
45
185
  });
46
186
  }
47
187
 
48
- function mapField(field: any, arrayModelsByName: ContentSourceTypes.ModelMap): Field {
49
- const type = _.get(field, 'type');
188
+ /**
189
+ * Maps Sanity FieldDefinition or ArrayOfType to the {@link StackbitTypes.Field}
190
+ * or the {@link StackbitTypes.FieldListItems} respectively.
191
+ *
192
+ * The `mapField()` can be called for object fields or array items.
193
+ * When called for array items, the 'name' property is optional and the 'hidden'
194
+ * attribute is not present.
195
+ */
196
+ function mapField({
197
+ field,
198
+ modelFieldPath,
199
+ typeAliasMap,
200
+ i18nArrayModelMap,
201
+ localizedFieldsModelMap,
202
+ fieldAliasMap,
203
+ seenAliases
204
+ }: { field: SanityTypes.FieldDefinition | SanityTypes.ArrayOfType } & CommonProps): StackbitTypes.Field {
205
+ let type = _.get(field, 'type');
50
206
  const name = _.get(field, 'name');
51
207
  const label = _.get(field, 'title', name ? _.startCase(name) : undefined);
208
+ const modelFieldPathStr = modelFieldPath.join('.');
209
+
210
+ let localized: true | undefined;
211
+ // TODO: can the Internationalized Array type have an alias?
212
+ // e.g.: localizedString an alias to internationalizedArrayString?
213
+ // { name: 'localizedString', type: 'internationalizedArrayString' }
214
+ if (type in i18nArrayModelMap) {
215
+ // The localization model map can reference a field definition, or a type alias.
216
+ // i18nArrayModelMap['internationalizedArrayString'] => { valueModelName: 'internationalizedArrayStringValue', valueField: { type: 'string', name: 'value' } }
217
+ // i18nArrayModelMap['internationalizedArrayBoolean'] => { valueModelName: 'internationalizedArrayBooleanValue', valueField: { type: 'boolean', name: 'value' } }
218
+ // i18nArrayModelMap['internationalizedArrayInlineReference'] => { valueModelName: 'internationalizedArrayInlineReferenceValue', valueField: { type: 'reference', to: [...], name: 'value' } }
219
+ // i18nArrayModelMap['internationalizedArrayCustomTypeAlias'] => { valueModelName: 'internationalizedArrayCustomTypeAliasValue', valueField: { type: 'customTypeAlias', name: 'value' } }
220
+ // etc.
221
+ if (seenAliases.includes(type)) {
222
+ throw new Error(`Circular Array aliases are not supported, the Array alias '${type}' is recursively referenced in field '${modelFieldPathStr}'.`);
223
+ }
224
+ seenAliases = seenAliases.concat(type);
225
+ localized = true;
226
+ field = {
227
+ ...i18nArrayModelMap[type].valueField,
228
+ ...field,
229
+ type: i18nArrayModelMap[type].valueField.type
230
+ };
231
+ localizedFieldsModelMap[modelFieldPathStr] = {
232
+ arrayModelName: type,
233
+ arrayValueModelName: i18nArrayModelMap[type].valueModelName
234
+ };
235
+ type = field.type;
236
+ }
237
+
238
+ const visitedTypes: string[] = [];
239
+ let addedAlias = false;
240
+ while (type in typeAliasMap) {
241
+ if (seenAliases.includes(type)) {
242
+ throw new Error(`Circular Array aliases not supported, the Array alias ${type} is recursively referenced in field ${modelFieldPathStr}.`);
243
+ }
244
+ seenAliases = seenAliases.concat(type);
245
+ // In Sanity, the properties of the field, override the properties of
246
+ // the alias. However, the final field type is the type of the alias.
247
+ if (visitedTypes.includes(type)) {
248
+ throw new Error(`Circular type alias detected in field ${modelFieldPathStr}: ${visitedTypes.join(' => ')} => ${type}.`);
249
+ }
250
+ visitedTypes.push(type);
251
+ field = {
252
+ ...typeAliasMap[type],
253
+ ...field,
254
+ type: typeAliasMap[type].type
255
+ };
256
+ if (!fieldAliasMap[modelFieldPathStr]) {
257
+ fieldAliasMap[modelFieldPathStr] = [];
258
+ }
259
+ const fieldAliases = fieldAliasMap[modelFieldPathStr]!;
260
+ // Non list fields should have only one alias entry.
261
+ // List fields, can have multiple aliases per list item type.
262
+ if (!addedAlias) {
263
+ addedAlias = true;
264
+ fieldAliases.push({
265
+ origTypeName: type,
266
+ resolvedTypeName: field.type
267
+ });
268
+ } else {
269
+ fieldAliases[fieldAliases.length - 1]!.resolvedTypeName = field.type;
270
+ }
271
+ type = field.type;
272
+ }
273
+
52
274
  const description = _.get(field, 'description');
53
275
  const readOnly = _.get(field, 'readOnly');
54
276
  const isRequired = _.get(field, 'validation.isRequired');
@@ -75,24 +297,30 @@ function mapField(field: any, arrayModelsByName: ContentSourceTypes.ModelMap): F
75
297
  }
76
298
  }
77
299
 
78
- const extra = convertField(field, arrayModelsByName);
300
+ const extra = convertField({
301
+ field,
302
+ modelFieldPath,
303
+ typeAliasMap,
304
+ i18nArrayModelMap,
305
+ localizedFieldsModelMap,
306
+ fieldAliasMap,
307
+ seenAliases
308
+ });
79
309
 
80
310
  return _.assign(
81
- _.omitBy(
82
- {
83
- type: null,
84
- name: name,
85
- label: label,
86
- description: description,
87
- group: group,
88
- required: isRequired || undefined,
89
- default: defaultValue,
90
- const: constValue,
91
- readOnly: readOnly,
92
- hidden: hidden
93
- },
94
- _.isUndefined
95
- ),
311
+ omitByUndefined({
312
+ type: null,
313
+ name: name,
314
+ label: label,
315
+ description: description,
316
+ group: group,
317
+ required: isRequired || undefined,
318
+ default: defaultValue,
319
+ const: constValue,
320
+ readOnly: readOnly,
321
+ hidden: hidden,
322
+ localized: localized
323
+ }),
96
324
  extra
97
325
  );
98
326
  }
@@ -127,12 +355,12 @@ function convertDefaultValue(defaultValue: any) {
127
355
  );
128
356
  }
129
357
 
130
- function convertField(field: any, arrayModelsByName: ContentSourceTypes.ModelMap) {
358
+ function convertField({ field, ...rest }: { field: SanityTypes.FieldDefinition | SanityTypes.ArrayOfType } & CommonProps): StackbitTypes.FieldSpecificProps {
131
359
  const type = _.get(field, 'type');
132
360
  if (!_.has(fieldConverterMap, type)) {
133
- return fieldConverterMap.model(field, arrayModelsByName);
361
+ return fieldConverterMap.model({ field: field as SanityAliasFieldOrArrayItemDefinition<'model'>, ...rest });
134
362
  }
135
- return _.get(fieldConverterMap, type)(field, arrayModelsByName);
363
+ return _.get(fieldConverterMap, type)({ field, ...rest });
136
364
  }
137
365
 
138
366
  function getEnumOptions(field: any) {
@@ -149,8 +377,20 @@ function getEnumOptions(field: any) {
149
377
  }
150
378
  }
151
379
 
152
- const fieldConverterMap = {
153
- string: (field: any) => {
380
+ type SanityFieldTypes = Exclude<SanityTypes.IntrinsicTypeName, 'document'>;
381
+ type AliasFieldTypes = 'model' | 'color' | 'markdown' | 'json' | 'cloudinary.asset' | 'bynder.asset' | 'aprimo.cdnasset';
382
+ type SanityIntrinsicFieldOrArrayItem<Type extends SanityFieldTypes> =
383
+ | (SanityTypes.InlineFieldDefinition[Type] & SanityTypes.FieldDefinitionBase)
384
+ | SanityTypes.IntrinsicArrayOfDefinition[Type];
385
+ type SanityAliasFieldOrArrayItemDefinition<Type extends string> =
386
+ | (SanityTypes.TypeAliasDefinition<Type, SanityTypes.IntrinsicTypeName | undefined> & SanityTypes.FieldDefinitionBase)
387
+ | SanityTypes.ArrayOfEntry<SanityTypes.TypeAliasDefinition<Type, SanityTypes.IntrinsicTypeName | undefined>>;
388
+ const fieldConverterMap: {
389
+ [Type in SanityFieldTypes]: (options: { field: SanityIntrinsicFieldOrArrayItem<Type> } & CommonProps) => StackbitTypes.FieldSpecificProps;
390
+ } & {
391
+ [Type in AliasFieldTypes]: (options: { field: SanityAliasFieldOrArrayItemDefinition<Type> } & CommonProps) => StackbitTypes.FieldSpecificProps;
392
+ } = {
393
+ string: ({ field }) => {
154
394
  const options = getEnumOptions(field);
155
395
  if (options) {
156
396
  return { type: 'enum', options: options };
@@ -167,6 +407,9 @@ const fieldConverterMap = {
167
407
  text: () => {
168
408
  return { type: 'text' };
169
409
  },
410
+ email: () => {
411
+ return { type: 'string' };
412
+ },
170
413
  color: () => {
171
414
  return { type: 'color' };
172
415
  },
@@ -176,21 +419,15 @@ const fieldConverterMap = {
176
419
  block: () => {
177
420
  return { type: 'richText' };
178
421
  },
179
- span: () => {
180
- // TODO: implement when we will handle parsing of rich text
181
- },
182
- number: (field: any) => {
422
+ number: ({ field }) => {
183
423
  const validation = _.get(field, 'validation');
184
424
  const isInteger = _.get(validation, 'isInteger');
185
- return _.omitBy(
186
- {
187
- type: 'number',
188
- subtype: isInteger ? 'int' : 'float',
189
- min: _.get(validation, 'min'),
190
- max: _.get(validation, 'max')
191
- },
192
- _.isNil
193
- );
425
+ return omitByNil({
426
+ type: 'number',
427
+ subtype: isInteger ? 'int' : 'float',
428
+ min: _.get(validation, 'min'),
429
+ max: _.get(validation, 'max')
430
+ });
194
431
  },
195
432
  boolean: () => {
196
433
  return { type: 'boolean' };
@@ -219,26 +456,43 @@ const fieldConverterMap = {
219
456
  return { type: 'image', source: 'aprimo' };
220
457
  },
221
458
  geopoint: () => {
222
- return { type: 'geopoint' };
459
+ return { type: 'model', models: ['geopoint'] };
223
460
  },
224
- reference: (field: any) => {
461
+ reference: ({ field }) => {
225
462
  const toItems = _.castArray(_.get(field, 'to', []));
226
463
  return {
227
464
  type: 'reference',
228
465
  models: _.map(toItems, (item) => item.type)
229
466
  };
230
467
  },
468
+ crossDatasetReference: ({ field }) => {
469
+ // TODO: implement cross-reference
470
+ // Sanity crossDatasetReference fields can reference between datasets
471
+ // of the same project. But Stackbit cross-reference fields cannot
472
+ // differentiate environments of the same project as the object in the
473
+ // models array only contains the srcType and srcProjectId
474
+ return {
475
+ type: 'cross-reference',
476
+ models: []
477
+ // models: field.to.map((toItem) => {
478
+ // return {
479
+ // modelName: toItem.type,
480
+ // srcType: 'sanity',
481
+ // srcProjectId: '...', // the projectId should be the same as the current one
482
+ // // Stackbit cross-references do not have a way to specify different environment
483
+ // srcEnvironment: field.dataset
484
+ // }
485
+ // })
486
+ };
487
+ },
231
488
  json: () => {
232
489
  return { type: 'json' };
233
490
  },
234
- object: (field: any, arrayModelsByName: ContentSourceTypes.ModelMap) => {
235
- return mapObjectModel(field, arrayModelsByName);
491
+ object: ({ field, ...rest }) => {
492
+ return mapObjectModel({ model: field, ...rest }) as StackbitTypes.FieldObjectProps;
236
493
  },
237
- model: (field: any, arrayModelsByName: ContentSourceTypes.ModelMap) => {
494
+ model: ({ field }) => {
238
495
  const type = _.get(field, 'type');
239
- if (_.has(arrayModelsByName, type)) {
240
- return fieldConverterMap.array(arrayModelsByName[type], arrayModelsByName);
241
- }
242
496
  if (thirdPartyImageModels.includes(type)) {
243
497
  const fn = (fieldConverterMap[type as keyof typeof fieldConverterMap] as any) ?? fieldConverterMap.image;
244
498
  return fn();
@@ -281,7 +535,7 @@ const fieldConverterMap = {
281
535
  * }]
282
536
  * }
283
537
  */
284
- array: (field: any, arrayModelsByName: ContentSourceTypes.ModelMap) => {
538
+ array: ({ field, ...rest }) => {
285
539
  const options = getEnumOptions(field);
286
540
  if (options) {
287
541
  return {
@@ -298,13 +552,38 @@ const fieldConverterMap = {
298
552
  return { type: 'richText' };
299
553
  }
300
554
  const items = _.map(ofItems, (item, index) => {
301
- return mapField(item, arrayModelsByName);
555
+ const { modelFieldPath, ...props } = rest;
556
+ const listModelFieldPath = modelFieldPath.concat('items');
557
+ const modelFieldPathStr = listModelFieldPath.join('.');
558
+ // Sanity array fields may consist of anonymous 'object' with names.
559
+ // When creating such objects, the '_type' should be set to their
560
+ // name to identify them among other 'object' types.
561
+ if (item && 'name' in item && item.name && 'type' in item && item.type === 'object') {
562
+ if (!props.fieldAliasMap[modelFieldPathStr]) {
563
+ props.fieldAliasMap[modelFieldPathStr] = [];
564
+ }
565
+ props.fieldAliasMap[modelFieldPathStr]!.push({
566
+ origTypeName: item.name,
567
+ resolvedTypeName: 'object'
568
+ });
569
+ }
570
+ return mapField({
571
+ field: item,
572
+ modelFieldPath: listModelFieldPath,
573
+ ...props
574
+ });
302
575
  });
303
576
  let modelNames: string[] = [];
304
577
  let referenceModelName: string[] = [];
305
578
  const consolidatedItems = [];
306
579
  _.forEach(items, (item) => {
307
580
  const type = _.get(item, 'type');
581
+ // If the converted items have only two properties
582
+ // - type: 'model' and models: [...],
583
+ // - type: 'reference' and models: [...]
584
+ // Then, consolidate all their models under the same 'model' or
585
+ // 'reference' item. Otherwise, if the field has additional properties
586
+ // like, "name", "label", etc., then add it as a separate items.
308
587
  if (type === 'model' && _.has(item, 'models') && _.size(item) === 2) {
309
588
  modelNames = modelNames.concat(_.get(item, 'models'));
310
589
  } else if (type === 'reference' && _.has(item, 'models') && _.size(item) === 2) {
@@ -329,12 +608,12 @@ const fieldConverterMap = {
329
608
  return {
330
609
  type: 'list',
331
610
  items: _.head(consolidatedItems)
332
- };
611
+ } as StackbitTypes.FieldList;
333
612
  }
334
613
  return {
335
614
  type: 'list',
336
615
  items: consolidatedItems
337
- };
616
+ } as unknown as StackbitTypes.FieldList;
338
617
  }
339
618
  };
340
619
 
@@ -342,3 +621,78 @@ function getNormalizedModelType(sanityModel: any) {
342
621
  const modelType = _.get(sanityModel, 'type');
343
622
  return modelType === 'document' ? 'data' : 'object';
344
623
  }
624
+
625
+ function getLocalesFromInternationalizedArrays(internationalizedArrayModels: any, defaultLocale?: string): StackbitTypes.Locale[] {
626
+ const localeMap = internationalizedArrayModels
627
+ .filter((model: any) => model.type === 'array')
628
+ .reduce((localeMap: Record<string, string>, model: any) => {
629
+ if (model?.options?.languages && Array.isArray(model?.options?.languages)) {
630
+ return model?.options?.languages.reduce((localeMap: Record<string, string>, locale: { id: string; title: string }) => {
631
+ if (!(locale.id in localeMap)) {
632
+ localeMap[locale.id] = locale.title;
633
+ }
634
+ return localeMap;
635
+ }, localeMap);
636
+ }
637
+ return localeMap;
638
+ }, {});
639
+ let defaultLocaleFound = false;
640
+ const locales = Object.entries(localeMap).map(([localeId, localeTitle]) => {
641
+ const locale: StackbitTypes.Locale = {
642
+ code: localeId
643
+ };
644
+ if (defaultLocale && localeId === defaultLocale) {
645
+ defaultLocaleFound = true;
646
+ locale.default = true;
647
+ }
648
+ return locale;
649
+ });
650
+ if (!defaultLocaleFound && locales.length > 0) {
651
+ locales[0]!.default = true;
652
+ }
653
+ return locales;
654
+ }
655
+
656
+ function getLocalizedModelMap(i18nModels: any[]): {
657
+ i18nArrayModelMap: Record<
658
+ string,
659
+ {
660
+ valueModelName: string;
661
+ valueField: any;
662
+ }
663
+ >;
664
+ i18nValueModelMap: Record<string, any>;
665
+ } {
666
+ const [i18nArrayModels, i18nObjectModels] = _.partition(i18nModels, { type: 'array' });
667
+ const i18nObjectModelsByName = _.keyBy(i18nObjectModels, 'name');
668
+ return _.reduce(
669
+ i18nArrayModels,
670
+ (accum, i18nArrayModel) => {
671
+ if (Array.isArray(i18nArrayModel.of) && i18nArrayModel.of.length === 1) {
672
+ const localizedArrayOfItem = i18nArrayModel.of[0];
673
+ if (localizedArrayOfItem && localizedArrayOfItem.type in i18nObjectModelsByName) {
674
+ const valueModelName = localizedArrayOfItem.type;
675
+ const localizedArrayValueModel = i18nObjectModelsByName[valueModelName];
676
+ const valueField = localizedArrayValueModel.fields[0];
677
+ // valueField.name === 'value'
678
+ accum.i18nArrayModelMap[i18nArrayModel.name] = {
679
+ valueModelName: valueModelName,
680
+ valueField: valueField
681
+ };
682
+ accum.i18nValueModelMap[valueModelName] = valueField;
683
+ }
684
+ }
685
+ return accum;
686
+ },
687
+ {
688
+ i18nArrayModelMap: {} as Record<
689
+ string,
690
+ {
691
+ valueModelName: string;
692
+ valueField: any;
693
+ }
694
+ >,
695
+ i18nValueModelMap: {} as Record<string, any>
696
+ }
697
+ );
698
+ }
@@ -3,7 +3,7 @@ import * as path from 'path';
3
3
  import _ from 'lodash';
4
4
  import fse from 'fs-extra';
5
5
  import { deepMap } from '@stackbit/utils';
6
- import * as ContentSourceTypes from '@stackbit/types';
6
+ import * as StackbitTypes from '@stackbit/types';
7
7
 
8
8
  export async function fetchSchemaLegacy({ studioPath }: { studioPath: string }) {
9
9
  const getSanitySchema = require('@sanity/core/lib/actions/graphql/getSanitySchema');
@@ -176,8 +176,8 @@ export interface FetchSchemaOptions {
176
176
  studioPath: string;
177
177
  repoPath: string;
178
178
  nodePath?: string;
179
- spawnRunner?: ContentSourceTypes.UserCommandSpawner;
180
- logger?: ContentSourceTypes.Logger;
179
+ spawnRunner?: StackbitTypes.UserCommandSpawner;
180
+ logger?: StackbitTypes.Logger;
181
181
  }
182
182
 
183
183
  export function spawnFetchSchema({ studioPath, nodePath, repoPath, spawnRunner, logger }: FetchSchemaOptions): Promise<any> {