@stackbit/cms-sanity 0.2.45-develop.1 → 0.2.45-staging.1
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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/sanity-content-source.d.ts +21 -10
- package/dist/sanity-content-source.d.ts.map +1 -1
- package/dist/sanity-content-source.js +74 -242
- package/dist/sanity-content-source.js.map +1 -1
- package/dist/sanity-document-converter.d.ts +11 -9
- package/dist/sanity-document-converter.d.ts.map +1 -1
- package/dist/sanity-document-converter.js +262 -205
- package/dist/sanity-document-converter.js.map +1 -1
- package/dist/sanity-operation-converter.d.ts +60 -0
- package/dist/sanity-operation-converter.d.ts.map +1 -0
- package/dist/sanity-operation-converter.js +664 -0
- package/dist/sanity-operation-converter.js.map +1 -0
- package/dist/sanity-schema-converter.d.ts +35 -3
- package/dist/sanity-schema-converter.d.ts.map +1 -1
- package/dist/sanity-schema-converter.js +290 -43
- package/dist/sanity-schema-converter.js.map +1 -1
- package/dist/sanity-schema-fetcher.d.ts +3 -3
- package/dist/sanity-schema-fetcher.d.ts.map +1 -1
- package/dist/utils.d.ts +53 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +93 -1
- package/dist/utils.js.map +1 -1
- package/package.json +6 -5
- package/src/index.ts +1 -1
- package/src/sanity-content-source.ts +109 -317
- package/src/sanity-document-converter.ts +332 -231
- package/src/sanity-operation-converter.ts +785 -0
- package/src/sanity-schema-converter.ts +424 -70
- package/src/sanity-schema-fetcher.ts +3 -3
- package/src/utils.ts +98 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { v4 as uuid } from 'uuid';
|
|
3
|
+
import tinycolor from 'tinycolor2';
|
|
4
|
+
import type { PatchOperations, SanityDocument } from '@sanity/client';
|
|
5
|
+
import type * as StackbitTypes from '@stackbit/types';
|
|
6
|
+
import type { ModelContext, ModelWithContext } from './sanity-schema-converter';
|
|
7
|
+
import { getItemTypeForListItem, isLocalizedModelField, getSanityAliasFieldType, resolvedFieldType } from './utils';
|
|
8
|
+
import type { GetModelByName } from './sanity-document-converter';
|
|
9
|
+
|
|
10
|
+
export function convertUpdateOperation({
|
|
11
|
+
operation,
|
|
12
|
+
...rest
|
|
13
|
+
}: {
|
|
14
|
+
operation: StackbitTypes.UpdateOperation;
|
|
15
|
+
sanityDocument: SanityDocument;
|
|
16
|
+
getModelByName: GetModelByName;
|
|
17
|
+
model: ModelWithContext;
|
|
18
|
+
}): PatchOperations {
|
|
19
|
+
switch (operation.opType) {
|
|
20
|
+
case 'set':
|
|
21
|
+
return Operations.set({ operation, ...rest });
|
|
22
|
+
case 'unset':
|
|
23
|
+
return Operations.unset({ operation, ...rest });
|
|
24
|
+
case 'insert':
|
|
25
|
+
return Operations.insert({ operation, ...rest });
|
|
26
|
+
case 'remove':
|
|
27
|
+
return Operations.remove({ operation, ...rest });
|
|
28
|
+
case 'reorder':
|
|
29
|
+
return Operations.reorder({ operation, ...rest });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Operations: {
|
|
34
|
+
[Type in StackbitTypes.UpdateOperation as Type['opType']]: ({
|
|
35
|
+
sanityDocument,
|
|
36
|
+
operation,
|
|
37
|
+
getModelByName
|
|
38
|
+
}: {
|
|
39
|
+
sanityDocument: SanityDocument;
|
|
40
|
+
operation: Type;
|
|
41
|
+
getModelByName: GetModelByName;
|
|
42
|
+
model: StackbitTypes.Model<ModelContext>;
|
|
43
|
+
}) => PatchOperations;
|
|
44
|
+
} = {
|
|
45
|
+
set: ({ operation, sanityDocument, getModelByName, model }) => {
|
|
46
|
+
const { field, fieldPath, modelField, locale } = operation;
|
|
47
|
+
const { patchFieldPath, modelFieldPath, localizedLeaf, isInList } = getPatchPathAndModelFieldPaths({
|
|
48
|
+
fieldPath,
|
|
49
|
+
sanityDocument,
|
|
50
|
+
getModelByName,
|
|
51
|
+
addValueToI18NLeafs: true,
|
|
52
|
+
allowUndefinedI18NLeafArrays: true,
|
|
53
|
+
locale
|
|
54
|
+
});
|
|
55
|
+
let value = mapUpdateOperationFieldToSanityValue({
|
|
56
|
+
updateOperationField: field,
|
|
57
|
+
getModelByName,
|
|
58
|
+
modelField,
|
|
59
|
+
rootModel: model,
|
|
60
|
+
modelFieldPath,
|
|
61
|
+
locale,
|
|
62
|
+
isInList
|
|
63
|
+
});
|
|
64
|
+
if (isLocalizedModelField(modelField) && localizedLeaf !== 'value') {
|
|
65
|
+
value = localizedValue({
|
|
66
|
+
value,
|
|
67
|
+
model,
|
|
68
|
+
modelFieldPath,
|
|
69
|
+
locale
|
|
70
|
+
});
|
|
71
|
+
if (localizedLeaf === 'newItem') {
|
|
72
|
+
return {
|
|
73
|
+
insert: {
|
|
74
|
+
after: `${patchFieldPath}[-1]`,
|
|
75
|
+
items: [value]
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
} else if (localizedLeaf === 'undefinedArray') {
|
|
79
|
+
return { set: { [patchFieldPath]: [value] } };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { set: { [patchFieldPath]: value } };
|
|
83
|
+
},
|
|
84
|
+
unset: ({ operation, sanityDocument, getModelByName }) => {
|
|
85
|
+
const { fieldPath, locale } = operation;
|
|
86
|
+
const { patchFieldPath } = getPatchPathAndModelFieldPaths({
|
|
87
|
+
fieldPath,
|
|
88
|
+
sanityDocument,
|
|
89
|
+
getModelByName,
|
|
90
|
+
locale
|
|
91
|
+
});
|
|
92
|
+
return { unset: [patchFieldPath] };
|
|
93
|
+
},
|
|
94
|
+
insert: ({ operation, sanityDocument, getModelByName, model }) => {
|
|
95
|
+
const { item, fieldPath, modelField, index, locale } = operation;
|
|
96
|
+
const listItemModelField = (modelField as StackbitTypes.FieldList).items ?? { type: 'string' };
|
|
97
|
+
const { patchFieldPath, modelFieldPath, currentValue, localizedLeaf } = getPatchPathAndModelFieldPaths({
|
|
98
|
+
fieldPath,
|
|
99
|
+
sanityDocument,
|
|
100
|
+
getModelByName,
|
|
101
|
+
addValueToI18NLeafs: true,
|
|
102
|
+
allowUndefinedI18NLeafArrays: true,
|
|
103
|
+
locale
|
|
104
|
+
});
|
|
105
|
+
let value = mapUpdateOperationFieldToSanityValue({
|
|
106
|
+
updateOperationField: item,
|
|
107
|
+
getModelByName,
|
|
108
|
+
modelField: listItemModelField,
|
|
109
|
+
rootModel: model,
|
|
110
|
+
modelFieldPath: modelFieldPath.concat('items'),
|
|
111
|
+
locale,
|
|
112
|
+
isInList: true
|
|
113
|
+
});
|
|
114
|
+
// In the case of a localized array field, the field will contain an
|
|
115
|
+
// array of objects with localized arrays:
|
|
116
|
+
// [
|
|
117
|
+
// {
|
|
118
|
+
// _key: 'en',
|
|
119
|
+
// _type: 'internationalizedArrayLocalizedArray',
|
|
120
|
+
// value: ['en value 1', 'en value 2', 'en value 3']
|
|
121
|
+
// },
|
|
122
|
+
// {
|
|
123
|
+
// _key: 'es',
|
|
124
|
+
// _type: 'internationalizedArrayLocalizedArray',
|
|
125
|
+
// value: ['es value 1', 'es value 2', 'es value 3']
|
|
126
|
+
// }
|
|
127
|
+
// ]
|
|
128
|
+
if (isLocalizedModelField(modelField) && localizedLeaf !== 'value') {
|
|
129
|
+
// When there is no array for a given locale, create a new localized
|
|
130
|
+
// array with a single value and insert it into the localized array:
|
|
131
|
+
// {
|
|
132
|
+
// _key: 'es',
|
|
133
|
+
// _type: 'internationalizedArrayLocalizedArray',
|
|
134
|
+
// value: [value]
|
|
135
|
+
// }
|
|
136
|
+
value = localizedValue({
|
|
137
|
+
value: [value],
|
|
138
|
+
model,
|
|
139
|
+
modelFieldPath,
|
|
140
|
+
locale
|
|
141
|
+
});
|
|
142
|
+
if (localizedLeaf === 'newItem') {
|
|
143
|
+
return {
|
|
144
|
+
insert: {
|
|
145
|
+
after: `${patchFieldPath}[-1]`,
|
|
146
|
+
items: [value]
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
} else if (localizedLeaf === 'undefinedArray') {
|
|
150
|
+
return { set: { [patchFieldPath]: [value] } };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!currentValue) {
|
|
154
|
+
return { set: { [patchFieldPath]: [value] } };
|
|
155
|
+
} else if (_.isNil(index) || index >= currentValue.length) {
|
|
156
|
+
return {
|
|
157
|
+
insert: {
|
|
158
|
+
after: `${patchFieldPath}[-1]`,
|
|
159
|
+
items: [value]
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
insert: {
|
|
165
|
+
before: `${patchFieldPath}[${index}]`,
|
|
166
|
+
items: [value]
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
remove: ({ sanityDocument, operation, getModelByName }) => {
|
|
171
|
+
const { fieldPath, index, locale } = operation;
|
|
172
|
+
const { patchFieldPath } = getPatchPathAndModelFieldPaths({
|
|
173
|
+
fieldPath: fieldPath.concat(index),
|
|
174
|
+
sanityDocument,
|
|
175
|
+
getModelByName,
|
|
176
|
+
locale
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
unset: [patchFieldPath]
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
reorder: ({ sanityDocument, operation, getModelByName }) => {
|
|
183
|
+
const { fieldPath, order, locale } = operation;
|
|
184
|
+
const { patchFieldPath, currentValue } = getPatchPathAndModelFieldPaths({
|
|
185
|
+
fieldPath,
|
|
186
|
+
sanityDocument,
|
|
187
|
+
getModelByName,
|
|
188
|
+
addValueToI18NLeafs: true,
|
|
189
|
+
locale
|
|
190
|
+
});
|
|
191
|
+
const newEntryArr = order.map((newIndex) => currentValue[newIndex]);
|
|
192
|
+
return { set: { [patchFieldPath]: newEntryArr } };
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export function mapUpdateOperationFieldToSanityValue({
|
|
197
|
+
updateOperationField,
|
|
198
|
+
getModelByName,
|
|
199
|
+
modelField,
|
|
200
|
+
rootModel,
|
|
201
|
+
modelFieldPath,
|
|
202
|
+
locale,
|
|
203
|
+
isInList
|
|
204
|
+
}: {
|
|
205
|
+
updateOperationField: StackbitTypes.UpdateOperationField;
|
|
206
|
+
getModelByName: GetModelByName;
|
|
207
|
+
modelField: StackbitTypes.FieldSpecificProps;
|
|
208
|
+
rootModel: StackbitTypes.Model<ModelContext>;
|
|
209
|
+
modelFieldPath: string[];
|
|
210
|
+
locale: string | undefined;
|
|
211
|
+
isInList?: boolean;
|
|
212
|
+
}): any {
|
|
213
|
+
switch (updateOperationField.type) {
|
|
214
|
+
case 'string':
|
|
215
|
+
case 'url':
|
|
216
|
+
case 'text':
|
|
217
|
+
case 'markdown':
|
|
218
|
+
case 'html':
|
|
219
|
+
case 'boolean':
|
|
220
|
+
case 'date':
|
|
221
|
+
case 'datetime':
|
|
222
|
+
case 'enum':
|
|
223
|
+
case 'style':
|
|
224
|
+
case 'json':
|
|
225
|
+
case 'richText':
|
|
226
|
+
case 'file': {
|
|
227
|
+
return updateOperationField.value;
|
|
228
|
+
}
|
|
229
|
+
case 'number': {
|
|
230
|
+
return Number(updateOperationField.value);
|
|
231
|
+
}
|
|
232
|
+
case 'slug': {
|
|
233
|
+
return {
|
|
234
|
+
_type: getSanityAliasFieldType({
|
|
235
|
+
resolvedType: 'slug',
|
|
236
|
+
model: rootModel,
|
|
237
|
+
modelFieldPath
|
|
238
|
+
}),
|
|
239
|
+
current: updateOperationField.value
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
case 'color': {
|
|
243
|
+
const color = tinycolor(updateOperationField.value);
|
|
244
|
+
return {
|
|
245
|
+
_type: getSanityAliasFieldType({
|
|
246
|
+
resolvedType: 'color',
|
|
247
|
+
model: rootModel,
|
|
248
|
+
modelFieldPath
|
|
249
|
+
}),
|
|
250
|
+
hex: color.toHexString(),
|
|
251
|
+
alpha: color.getAlpha(),
|
|
252
|
+
hsl: {
|
|
253
|
+
_type: 'hslaColor',
|
|
254
|
+
...color.toHsl()
|
|
255
|
+
},
|
|
256
|
+
hsv: {
|
|
257
|
+
_type: 'hsvaColor',
|
|
258
|
+
...color.toHsv()
|
|
259
|
+
},
|
|
260
|
+
rgb: {
|
|
261
|
+
_type: 'rgbaColor',
|
|
262
|
+
...color.toRgb()
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
case 'image': {
|
|
267
|
+
const value = updateOperationField?.value;
|
|
268
|
+
if (modelField.type === 'image') {
|
|
269
|
+
if (modelField.source === 'cloudinary' || modelField.source === 'aprimo') {
|
|
270
|
+
const type = modelField.source === 'cloudinary' ? 'cloudinary.asset' : 'aprimo.cdnasset';
|
|
271
|
+
return addKeyIfInList(
|
|
272
|
+
{
|
|
273
|
+
_type: type,
|
|
274
|
+
...value
|
|
275
|
+
},
|
|
276
|
+
isInList
|
|
277
|
+
);
|
|
278
|
+
} else if (modelField.source === 'bynder') {
|
|
279
|
+
let imageValue = value;
|
|
280
|
+
if (imageValue?.__typename) {
|
|
281
|
+
imageValue = _.omitBy(
|
|
282
|
+
{
|
|
283
|
+
id: value.id,
|
|
284
|
+
name: value.name,
|
|
285
|
+
databaseId: value.databaseId,
|
|
286
|
+
type: value.type,
|
|
287
|
+
previewUrl: value.type === 'VIDEO' ? value.previewUrls[0] : value.files.webImage.url,
|
|
288
|
+
previewImg: value.files.webImage.url,
|
|
289
|
+
datUrl: value.files.transformBaseUrl?.url,
|
|
290
|
+
videoUrl: value.type === 'VIDEO' ? value.files.original?.url : null,
|
|
291
|
+
description: value.description,
|
|
292
|
+
aspectRatio: value.height / value.width
|
|
293
|
+
},
|
|
294
|
+
_.isUndefined
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return addKeyIfInList(
|
|
298
|
+
{
|
|
299
|
+
_type: 'bynder.asset',
|
|
300
|
+
...imageValue
|
|
301
|
+
},
|
|
302
|
+
isInList
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// TODO: there is a bug right now because documentField is inferred from the model which is an "image", not reference
|
|
307
|
+
return addKeyIfInList(linkForAssetId(value), isInList);
|
|
308
|
+
}
|
|
309
|
+
case 'object': {
|
|
310
|
+
if (modelField.type !== 'object') {
|
|
311
|
+
throw new Error(`Operation field type 'object' does not match model field type '${modelField.type}'.`);
|
|
312
|
+
}
|
|
313
|
+
// Sanity array fields may consist of anonymous 'object' with names.
|
|
314
|
+
// When creating such objects, the '_type' should be set to their
|
|
315
|
+
// name to identify them among other 'object' types.
|
|
316
|
+
const fieldAlias = rootModel.context?.fieldAliasMap?.[modelFieldPath.join('.')] ?? [];
|
|
317
|
+
const typeName = fieldAlias?.find((alias) => alias.resolvedTypeName === 'object')?.origTypeName;
|
|
318
|
+
const object = addKeyIfInList(
|
|
319
|
+
{
|
|
320
|
+
...(typeName ? { _type: typeName } : null)
|
|
321
|
+
},
|
|
322
|
+
isInList
|
|
323
|
+
);
|
|
324
|
+
return _.reduce(
|
|
325
|
+
updateOperationField.fields,
|
|
326
|
+
(result, childUpdateOperationField, fieldName) => {
|
|
327
|
+
const childModelField = _.find(modelField.fields, (field) => field.name === fieldName);
|
|
328
|
+
if (!childModelField) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`No model field found for field '${fieldName}' in model '${rootModel.name}' at field path ${modelFieldPath.join('.')}.`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const childModelFieldPath = modelFieldPath.concat(fieldName);
|
|
334
|
+
const value = mapUpdateOperationFieldToSanityValue({
|
|
335
|
+
updateOperationField: childUpdateOperationField,
|
|
336
|
+
getModelByName,
|
|
337
|
+
modelField: childModelField,
|
|
338
|
+
rootModel,
|
|
339
|
+
modelFieldPath: childModelFieldPath,
|
|
340
|
+
locale
|
|
341
|
+
});
|
|
342
|
+
if (isLocalizedModelField(childModelField)) {
|
|
343
|
+
_.set(result, fieldName, [
|
|
344
|
+
localizedValue({
|
|
345
|
+
value,
|
|
346
|
+
model: rootModel,
|
|
347
|
+
modelFieldPath: childModelFieldPath,
|
|
348
|
+
locale
|
|
349
|
+
})
|
|
350
|
+
]);
|
|
351
|
+
} else {
|
|
352
|
+
_.set(result, fieldName, value);
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
},
|
|
356
|
+
object
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
case 'model': {
|
|
360
|
+
if (modelField.type !== 'model') {
|
|
361
|
+
throw new Error(`Operation field type 'model' does not match model field type '${modelField.type}'.`);
|
|
362
|
+
}
|
|
363
|
+
const modelName = updateOperationField.modelName;
|
|
364
|
+
const childModel = getModelByName(modelName);
|
|
365
|
+
if (!childModel) {
|
|
366
|
+
throw new Error(`No model '${modelName}' was found for field at '${modelFieldPath.join('.')}' in model '${rootModel.name}'.`);
|
|
367
|
+
}
|
|
368
|
+
const object = addKeyIfInList(
|
|
369
|
+
{
|
|
370
|
+
_type: getSanityAliasFieldType({
|
|
371
|
+
resolvedType: modelName,
|
|
372
|
+
model: rootModel,
|
|
373
|
+
modelFieldPath
|
|
374
|
+
})
|
|
375
|
+
},
|
|
376
|
+
isInList
|
|
377
|
+
);
|
|
378
|
+
return _.reduce(
|
|
379
|
+
updateOperationField.fields,
|
|
380
|
+
(result, childUpdateOperationField, fieldName) => {
|
|
381
|
+
const childModelField = _.find(childModel?.fields, (field) => field.name === fieldName);
|
|
382
|
+
if (!childModelField) {
|
|
383
|
+
throw new Error(`No model field found for field '${fieldName}' in model '${childModel.name}'.`);
|
|
384
|
+
}
|
|
385
|
+
const childModelFieldPath = [fieldName];
|
|
386
|
+
const value = mapUpdateOperationFieldToSanityValue({
|
|
387
|
+
updateOperationField: childUpdateOperationField,
|
|
388
|
+
getModelByName,
|
|
389
|
+
modelField: childModelField,
|
|
390
|
+
rootModel: childModel,
|
|
391
|
+
modelFieldPath: childModelFieldPath,
|
|
392
|
+
locale
|
|
393
|
+
});
|
|
394
|
+
if (isLocalizedModelField(childModelField)) {
|
|
395
|
+
_.set(result, fieldName, [
|
|
396
|
+
localizedValue({
|
|
397
|
+
value,
|
|
398
|
+
model: rootModel,
|
|
399
|
+
modelFieldPath: childModelFieldPath,
|
|
400
|
+
locale
|
|
401
|
+
})
|
|
402
|
+
]);
|
|
403
|
+
} else {
|
|
404
|
+
_.set(result, fieldName, value);
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
},
|
|
408
|
+
object
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
case 'reference': {
|
|
412
|
+
const value =
|
|
413
|
+
updateOperationField.refType === 'document'
|
|
414
|
+
? {
|
|
415
|
+
_ref: updateOperationField.refId,
|
|
416
|
+
_type: getSanityAliasFieldType({
|
|
417
|
+
resolvedType: 'reference',
|
|
418
|
+
model: rootModel,
|
|
419
|
+
modelFieldPath
|
|
420
|
+
}),
|
|
421
|
+
// TODO: this is a bug!
|
|
422
|
+
// The _weak is not always `true`. Lookup the referenced document
|
|
423
|
+
// by id, and the original model field's `weak` value.
|
|
424
|
+
// If the referenced document was published (status === 'published'
|
|
425
|
+
// or status === 'modified'), then the _weak value should be set to
|
|
426
|
+
// the `weak` value defined in Sanity model field's.
|
|
427
|
+
// Otherwise, if the document was not published (status === 'added'),
|
|
428
|
+
// then the _weak value should be set to `true` and the
|
|
429
|
+
// _strengthenOnPublish.weak should be set to the `weak` value
|
|
430
|
+
// defined in the model field's.
|
|
431
|
+
_weak: true,
|
|
432
|
+
// TODO: Lookup the referenced document by id, and the original model
|
|
433
|
+
// field's `weak` value. If the referenced document was never published
|
|
434
|
+
// (status === 'added'), then add the _strengthenOnPublish object and
|
|
435
|
+
// set its `weak` property to the model field's `weak` value.
|
|
436
|
+
// When publishing objects with reference fields having the
|
|
437
|
+
// _strengthenOnPublish object, update the field's _weak with that of
|
|
438
|
+
// _strengthenOnPublish.weak.
|
|
439
|
+
// For more info: https://www.sanity.io/blog/obvious-features-aren-t-obviously-made#2c38c9f38060
|
|
440
|
+
_strengthenOnPublish: {
|
|
441
|
+
// type: <type for referenced item>,
|
|
442
|
+
// weak: <the _weak value of the original field>
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
: linkForAssetId(updateOperationField.refId);
|
|
446
|
+
return addKeyIfInList(value, isInList);
|
|
447
|
+
}
|
|
448
|
+
case 'cross-reference': {
|
|
449
|
+
throw new Error('Sanity crossDatasetReference fields not supported.');
|
|
450
|
+
}
|
|
451
|
+
case 'list': {
|
|
452
|
+
if (modelField.type !== 'list') {
|
|
453
|
+
throw new Error(`Operation field type 'list' does not match model field type '${modelField.type}'.`);
|
|
454
|
+
}
|
|
455
|
+
return updateOperationField.items.map((item, index) => {
|
|
456
|
+
let listItemModelField = modelField.items;
|
|
457
|
+
if (_.isArray(modelField.items)) {
|
|
458
|
+
const itemModel = (modelField.items as StackbitTypes.FieldListItems[]).find((listItemsModel) => listItemsModel.type === item.type);
|
|
459
|
+
if (!itemModel) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`No list item model found for item type '${item.type}' in model '${rootModel.name}' at field path ${modelFieldPath.join('.')}.`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
listItemModelField = itemModel;
|
|
465
|
+
}
|
|
466
|
+
return mapUpdateOperationFieldToSanityValue({
|
|
467
|
+
updateOperationField: item,
|
|
468
|
+
getModelByName,
|
|
469
|
+
modelField: listItemModelField,
|
|
470
|
+
rootModel,
|
|
471
|
+
modelFieldPath: modelFieldPath.concat('items'),
|
|
472
|
+
locale,
|
|
473
|
+
isInList: true
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
default: {
|
|
478
|
+
const _exhaustiveCheck: never = updateOperationField;
|
|
479
|
+
return _exhaustiveCheck;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function addKeyIfInList(object: Record<string, any>, isInList?: boolean) {
|
|
485
|
+
if (isInList) {
|
|
486
|
+
_.set(object, '_key', uuid());
|
|
487
|
+
}
|
|
488
|
+
return object;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* In Sanity, a localized field is represented by an array of objects containing the localized field values.
|
|
493
|
+
* Each object has three properties:
|
|
494
|
+
* - `_key` holds the field's locale
|
|
495
|
+
* - `_type` holds the type of the localized value
|
|
496
|
+
* - `value` holds the localized value.
|
|
497
|
+
*
|
|
498
|
+
* Note: the `_type` is not the regular type such as `string` or `object`,
|
|
499
|
+
* but a special localized type generated by the Sanity Internationalized Array plugin.
|
|
500
|
+
* The map between the localized fields and these types is stored in the model's context.
|
|
501
|
+
* title: [
|
|
502
|
+
* {
|
|
503
|
+
* _key: 'us',
|
|
504
|
+
* _type: 'internationalizedArrayStringValue',
|
|
505
|
+
* value: 'hello',
|
|
506
|
+
* }, {
|
|
507
|
+
* _key: 'es',
|
|
508
|
+
* _type: 'internationalizedArrayStringValue',
|
|
509
|
+
* value: 'hola',
|
|
510
|
+
* }
|
|
511
|
+
* ]
|
|
512
|
+
*/
|
|
513
|
+
export function localizedValue({ value, model, modelFieldPath, locale }: { value: any; model: ModelWithContext; modelFieldPath: string[]; locale?: string }) {
|
|
514
|
+
const localizedFieldModelNameMap = model.context?.localizedFieldsModelMap?.[modelFieldPath.join('.')];
|
|
515
|
+
if (!localizedFieldModelNameMap) {
|
|
516
|
+
throw Error(`Internationalized array model for localized field at path ${modelFieldPath.join('.')} of model ${model.name} not found.`);
|
|
517
|
+
}
|
|
518
|
+
if (!locale) {
|
|
519
|
+
throw Error(`No locale provided for localized field at path ${modelFieldPath.join('.')} of model ${model.name}.`);
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
_key: locale,
|
|
523
|
+
_type: localizedFieldModelNameMap.arrayValueModelName,
|
|
524
|
+
value: value
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function linkForAssetId(assetId?: string): any {
|
|
529
|
+
return {
|
|
530
|
+
_type: 'image',
|
|
531
|
+
asset: {
|
|
532
|
+
_ref: assetId,
|
|
533
|
+
_type: 'reference'
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Receives a `fieldPath` of a target field in a `sanityDocument` and returns
|
|
540
|
+
* an object with following properties:
|
|
541
|
+
*
|
|
542
|
+
* - `model`: The closest ancestor model of the target field.
|
|
543
|
+
* - `modelFieldPath`: The field path of the target field from the `model`.
|
|
544
|
+
* The model's field path doesn't identify a specific document field,
|
|
545
|
+
* therefore it does not include list indexes.
|
|
546
|
+
* - `patchFieldPath`: A Sanity specific field path for patching fields.
|
|
547
|
+
* For array items, the field path will include [_key="..."] when possible.
|
|
548
|
+
* For localized fields, which represented by arrays in Sanity, the path
|
|
549
|
+
* will point to the localized array item using the `_key`:
|
|
550
|
+
* `sections[_key=="es"].value[3].title`
|
|
551
|
+
* - `localizedLeaf`: A string specifying the leaf value in the `patchFieldPath`
|
|
552
|
+
* when the target field is localized:
|
|
553
|
+
* - `value`: The `patchFieldPath` points to the localized value:
|
|
554
|
+
* `sections[_key=="es"].value
|
|
555
|
+
* - `newItem`: The `patchFieldPath` points to the existing localized array,
|
|
556
|
+
* which does not include an item for the provided locale.
|
|
557
|
+
* Returned only when `allowUndefinedI18NLeafArrays` set to `true`.
|
|
558
|
+
* - `undefinedArray`: The `patchFieldPath` points to undefined localized array.
|
|
559
|
+
* Returned only when `allowUndefinedI18NLeafArrays` set to `true`.
|
|
560
|
+
* - `currentValue`: The value of the target field. If the target field is
|
|
561
|
+
* localized but doesn't have a value for the provided locale,
|
|
562
|
+
* the `currentValue` will be undefined.
|
|
563
|
+
*/
|
|
564
|
+
function getPatchPathAndModelFieldPaths({
|
|
565
|
+
fieldPath,
|
|
566
|
+
sanityDocument,
|
|
567
|
+
getModelByName,
|
|
568
|
+
addValueToI18NLeafs,
|
|
569
|
+
allowUndefinedI18NLeafArrays,
|
|
570
|
+
locale
|
|
571
|
+
}: {
|
|
572
|
+
fieldPath: StackbitTypes.FieldPath;
|
|
573
|
+
sanityDocument: SanityDocument;
|
|
574
|
+
getModelByName: (modelName: string) => ModelWithContext | undefined;
|
|
575
|
+
addValueToI18NLeafs?: boolean;
|
|
576
|
+
allowUndefinedI18NLeafArrays?: boolean;
|
|
577
|
+
locale?: string;
|
|
578
|
+
}): {
|
|
579
|
+
model: ModelWithContext;
|
|
580
|
+
modelFieldPath: string[];
|
|
581
|
+
patchFieldPath: string;
|
|
582
|
+
localizedLeaf?: 'value' | 'newItem' | 'undefinedArray';
|
|
583
|
+
currentValue: any;
|
|
584
|
+
isInList: boolean;
|
|
585
|
+
} {
|
|
586
|
+
function iterateObject({
|
|
587
|
+
object,
|
|
588
|
+
model,
|
|
589
|
+
rootModel,
|
|
590
|
+
modelFieldPath,
|
|
591
|
+
fieldPath,
|
|
592
|
+
first = false
|
|
593
|
+
}: {
|
|
594
|
+
object: Record<string, any>;
|
|
595
|
+
model: StackbitTypes.Model | StackbitTypes.FieldObjectProps;
|
|
596
|
+
rootModel: ModelWithContext;
|
|
597
|
+
modelFieldPath: string[];
|
|
598
|
+
fieldPath: StackbitTypes.FieldPath;
|
|
599
|
+
first?: boolean;
|
|
600
|
+
}): {
|
|
601
|
+
model: ModelWithContext;
|
|
602
|
+
modelFieldPath: string[];
|
|
603
|
+
patchFieldPath: string;
|
|
604
|
+
localizedLeaf?: 'value' | 'newItem' | 'undefinedArray';
|
|
605
|
+
currentValue: any;
|
|
606
|
+
isInList: boolean;
|
|
607
|
+
} {
|
|
608
|
+
const [fieldName, ...fieldPathTail] = fieldPath as [string, ...StackbitTypes.FieldPath];
|
|
609
|
+
if (typeof fieldName === 'undefined') {
|
|
610
|
+
throw new Error('The fieldPath cannot be empty.');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const modelField = (model.fields ?? []).find((field) => field.name === fieldName);
|
|
614
|
+
if (!modelField) {
|
|
615
|
+
throw new Error(`Model field for field '${fieldName}' not found.`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let patchFieldPath = '';
|
|
619
|
+
if (/\W/.test(fieldName)) {
|
|
620
|
+
// field name is a string with non-alphanumeric characters
|
|
621
|
+
patchFieldPath += `['${fieldName}']`;
|
|
622
|
+
} else {
|
|
623
|
+
if (!first) {
|
|
624
|
+
patchFieldPath += '.';
|
|
625
|
+
}
|
|
626
|
+
patchFieldPath += fieldName;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let value = object[fieldName];
|
|
630
|
+
let localizedLeaf: 'value' | 'newItem' | 'undefinedArray' | undefined;
|
|
631
|
+
if (modelField?.localized) {
|
|
632
|
+
if (Array.isArray(value)) {
|
|
633
|
+
const localizedItem = value.find((item) => item._key === locale);
|
|
634
|
+
if (localizedItem) {
|
|
635
|
+
patchFieldPath += `[_key=="${locale}"]`;
|
|
636
|
+
if (fieldPathTail.length === 0) {
|
|
637
|
+
localizedLeaf = 'value';
|
|
638
|
+
if (addValueToI18NLeafs) {
|
|
639
|
+
patchFieldPath += '.value';
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
patchFieldPath += '.value';
|
|
643
|
+
}
|
|
644
|
+
value = localizedItem.value;
|
|
645
|
+
} else if (fieldPathTail.length === 0 && allowUndefinedI18NLeafArrays) {
|
|
646
|
+
localizedLeaf = 'newItem';
|
|
647
|
+
value = undefined;
|
|
648
|
+
} else {
|
|
649
|
+
throw new Error(`The localized field '${fieldName}' has no value for locale '${locale}'.`);
|
|
650
|
+
}
|
|
651
|
+
} else if (fieldPathTail.length === 0 && allowUndefinedI18NLeafArrays) {
|
|
652
|
+
localizedLeaf = 'undefinedArray';
|
|
653
|
+
value = undefined;
|
|
654
|
+
} else {
|
|
655
|
+
throw new Error(`The localized field '${fieldName}' has no localization array defined for locale '${locale}'.`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const result = iterateField({
|
|
660
|
+
value,
|
|
661
|
+
modelField,
|
|
662
|
+
fieldPath: fieldPathTail,
|
|
663
|
+
rootModel,
|
|
664
|
+
modelFieldPath: modelFieldPath.concat(fieldName),
|
|
665
|
+
isInList: false
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
model: result.model,
|
|
670
|
+
modelFieldPath: result.modelFieldPath,
|
|
671
|
+
patchFieldPath: patchFieldPath + result.patchFieldPath,
|
|
672
|
+
localizedLeaf: localizedLeaf ?? result.localizedLeaf,
|
|
673
|
+
currentValue: result.currentValue,
|
|
674
|
+
isInList: result.isInList
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function iterateField({
|
|
679
|
+
value,
|
|
680
|
+
modelField,
|
|
681
|
+
fieldPath,
|
|
682
|
+
rootModel,
|
|
683
|
+
modelFieldPath,
|
|
684
|
+
isInList
|
|
685
|
+
}: {
|
|
686
|
+
value: any;
|
|
687
|
+
modelField: StackbitTypes.FieldSpecificProps;
|
|
688
|
+
fieldPath: StackbitTypes.FieldPath;
|
|
689
|
+
rootModel: ModelWithContext;
|
|
690
|
+
modelFieldPath: string[];
|
|
691
|
+
isInList: boolean;
|
|
692
|
+
}): {
|
|
693
|
+
model: ModelWithContext;
|
|
694
|
+
modelFieldPath: string[];
|
|
695
|
+
patchFieldPath: string;
|
|
696
|
+
localizedLeaf?: 'value' | 'newItem' | 'undefinedArray';
|
|
697
|
+
currentValue: any;
|
|
698
|
+
isInList: boolean;
|
|
699
|
+
} {
|
|
700
|
+
if (fieldPath.length === 0) {
|
|
701
|
+
return {
|
|
702
|
+
patchFieldPath: '',
|
|
703
|
+
currentValue: value,
|
|
704
|
+
model: rootModel,
|
|
705
|
+
modelFieldPath,
|
|
706
|
+
isInList: isInList
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (typeof value === 'undefined') {
|
|
710
|
+
throw new Error(`Field path has more items [${fieldPath.join('.')}], but value is undefined.`);
|
|
711
|
+
}
|
|
712
|
+
if (modelField?.type === 'object') {
|
|
713
|
+
return iterateObject({
|
|
714
|
+
object: value,
|
|
715
|
+
model: modelField,
|
|
716
|
+
rootModel,
|
|
717
|
+
modelFieldPath,
|
|
718
|
+
fieldPath
|
|
719
|
+
});
|
|
720
|
+
} else if (modelField?.type === 'model') {
|
|
721
|
+
const modelName = resolvedFieldType({
|
|
722
|
+
sanityFieldType: value._type,
|
|
723
|
+
model: rootModel,
|
|
724
|
+
modelFieldPath
|
|
725
|
+
});
|
|
726
|
+
const model = getModelByName(modelName);
|
|
727
|
+
if (!model) {
|
|
728
|
+
throw new Error(`Model '${modelName}' not found.`);
|
|
729
|
+
}
|
|
730
|
+
return iterateObject({
|
|
731
|
+
object: value,
|
|
732
|
+
model,
|
|
733
|
+
rootModel: model,
|
|
734
|
+
modelFieldPath: [],
|
|
735
|
+
fieldPath
|
|
736
|
+
});
|
|
737
|
+
} else if (modelField?.type === 'list') {
|
|
738
|
+
const [itemIndex, ...fieldPathTail] = fieldPath as [number, ...StackbitTypes.FieldPath];
|
|
739
|
+
const listItem = value[itemIndex];
|
|
740
|
+
const itemModel = getItemTypeForListItem(listItem, modelField);
|
|
741
|
+
if (!itemModel) {
|
|
742
|
+
throw new Error('Could not resolve type of a list item.');
|
|
743
|
+
}
|
|
744
|
+
const result = iterateField({
|
|
745
|
+
value: listItem,
|
|
746
|
+
modelField: itemModel,
|
|
747
|
+
rootModel,
|
|
748
|
+
modelFieldPath: modelFieldPath.concat('items'),
|
|
749
|
+
fieldPath: fieldPathTail,
|
|
750
|
+
isInList: true
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// try to use Sanity _key as explicit accessor
|
|
754
|
+
const key = listItem?._key;
|
|
755
|
+
const patchFieldPath = key ? `[_key=="${key}"]` : `[${Number(itemIndex)}]`;
|
|
756
|
+
|
|
757
|
+
return {
|
|
758
|
+
// model fieldPath doesn't include array indexes.
|
|
759
|
+
// return fieldPath with field names only, no list indexes
|
|
760
|
+
model: result.model,
|
|
761
|
+
modelFieldPath: result.modelFieldPath,
|
|
762
|
+
// fieldPath for Sanity patches should always include array indexes.
|
|
763
|
+
patchFieldPath: patchFieldPath + result.patchFieldPath,
|
|
764
|
+
currentValue: result.currentValue,
|
|
765
|
+
isInList: result.isInList
|
|
766
|
+
};
|
|
767
|
+
} else {
|
|
768
|
+
throw new Error(`Field path has more items [${fieldPath.join('.')}], but no objects/arrays left to iterate.`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const modelName = sanityDocument._type;
|
|
773
|
+
const model = getModelByName(modelName);
|
|
774
|
+
if (!model) {
|
|
775
|
+
throw new Error(`Model '${modelName}' not found.`);
|
|
776
|
+
}
|
|
777
|
+
return iterateObject({
|
|
778
|
+
object: sanityDocument,
|
|
779
|
+
model,
|
|
780
|
+
rootModel: model,
|
|
781
|
+
modelFieldPath: [],
|
|
782
|
+
fieldPath,
|
|
783
|
+
first: true
|
|
784
|
+
});
|
|
785
|
+
}
|