@stackbit/cms-contentstack 0.1.1-staging.0

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 (72) hide show
  1. package/README.md +1 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/contentstack-api-client.d.ts +63 -0
  4. package/dist/contentstack-api-client.d.ts.map +1 -0
  5. package/dist/contentstack-api-client.js +295 -0
  6. package/dist/contentstack-api-client.js.map +1 -0
  7. package/dist/contentstack-content-poller.d.ts +46 -0
  8. package/dist/contentstack-content-poller.d.ts.map +1 -0
  9. package/dist/contentstack-content-poller.js +111 -0
  10. package/dist/contentstack-content-poller.js.map +1 -0
  11. package/dist/contentstack-content-source.d.ts +138 -0
  12. package/dist/contentstack-content-source.d.ts.map +1 -0
  13. package/dist/contentstack-content-source.js +544 -0
  14. package/dist/contentstack-content-source.js.map +1 -0
  15. package/dist/contentstack-conversion-utils.d.ts +41 -0
  16. package/dist/contentstack-conversion-utils.d.ts.map +1 -0
  17. package/dist/contentstack-conversion-utils.js +504 -0
  18. package/dist/contentstack-conversion-utils.js.map +1 -0
  19. package/dist/contentstack-entries-converter.d.ts +39 -0
  20. package/dist/contentstack-entries-converter.d.ts.map +1 -0
  21. package/dist/contentstack-entries-converter.js +333 -0
  22. package/dist/contentstack-entries-converter.js.map +1 -0
  23. package/dist/contentstack-operation-converter.d.ts +42 -0
  24. package/dist/contentstack-operation-converter.d.ts.map +1 -0
  25. package/dist/contentstack-operation-converter.js +535 -0
  26. package/dist/contentstack-operation-converter.js.map +1 -0
  27. package/dist/contentstack-schema-converter.d.ts +26 -0
  28. package/dist/contentstack-schema-converter.d.ts.map +1 -0
  29. package/dist/contentstack-schema-converter.js +379 -0
  30. package/dist/contentstack-schema-converter.js.map +1 -0
  31. package/dist/contentstack-types.d.ts +429 -0
  32. package/dist/contentstack-types.d.ts.map +1 -0
  33. package/dist/contentstack-types.js +3 -0
  34. package/dist/contentstack-types.js.map +1 -0
  35. package/dist/contentstack-utils.d.ts +31 -0
  36. package/dist/contentstack-utils.d.ts.map +1 -0
  37. package/dist/contentstack-utils.js +144 -0
  38. package/dist/contentstack-utils.js.map +1 -0
  39. package/dist/entries-converter.d.ts +10 -0
  40. package/dist/entries-converter.d.ts.map +1 -0
  41. package/dist/entries-converter.js +245 -0
  42. package/dist/entries-converter.js.map +1 -0
  43. package/dist/file-download.d.ts +2 -0
  44. package/dist/file-download.d.ts.map +1 -0
  45. package/dist/file-download.js +33 -0
  46. package/dist/file-download.js.map +1 -0
  47. package/dist/index.d.ts +4 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +14 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/schema-converter.d.ts +3 -0
  52. package/dist/schema-converter.d.ts.map +1 -0
  53. package/dist/schema-converter.js +169 -0
  54. package/dist/schema-converter.js.map +1 -0
  55. package/dist/transformation-utils.d.ts +41 -0
  56. package/dist/transformation-utils.d.ts.map +1 -0
  57. package/dist/transformation-utils.js +730 -0
  58. package/dist/transformation-utils.js.map +1 -0
  59. package/dist/types.d.ts +120 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +3 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +44 -0
  64. package/src/contentstack-api-client.ts +330 -0
  65. package/src/contentstack-content-poller.ts +157 -0
  66. package/src/contentstack-content-source.ts +687 -0
  67. package/src/contentstack-entries-converter.ts +438 -0
  68. package/src/contentstack-operation-converter.ts +703 -0
  69. package/src/contentstack-schema-converter.ts +486 -0
  70. package/src/contentstack-types.ts +527 -0
  71. package/src/contentstack-utils.ts +174 -0
  72. package/src/index.ts +3 -0
@@ -0,0 +1,703 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import _ from 'lodash';
3
+ import * as StackbitTypes from '@stackbit/types';
4
+ import type { EntryData } from '@contentstack/management/types/stack/contentType/entry';
5
+ import type { Entry, Asset } from './contentstack-types';
6
+ import type { ModelWithContext } from './contentstack-schema-converter';
7
+ import type { DocumentWithContext } from './contentstack-entries-converter';
8
+
9
+ type GetModelByName = (modelName: string) => ModelWithContext | undefined;
10
+ type GetDocumentById = (documentId: string) => DocumentWithContext | undefined;
11
+
12
+ export function createEntryFromOperationFields({
13
+ updateOperationFields,
14
+ model,
15
+ getDocumentById,
16
+ getModelByName,
17
+ logger
18
+ }: {
19
+ updateOperationFields: Record<string, StackbitTypes.UpdateOperationField>;
20
+ model: ModelWithContext;
21
+ getDocumentById: GetDocumentById;
22
+ getModelByName: GetModelByName;
23
+ logger: StackbitTypes.Logger;
24
+ }): EntryData {
25
+ return _.reduce(
26
+ updateOperationFields,
27
+ (accum: EntryData, updateOperationField, fieldName) => {
28
+ const modelField = getModelField(model, fieldName);
29
+ if (!modelField) {
30
+ return accum;
31
+ }
32
+ accum[fieldName] = convertOperationFieldValue({
33
+ updateOperationField,
34
+ modelField,
35
+ isInList: false,
36
+ rootModel: model,
37
+ entryFieldPath: [fieldName],
38
+ modelFieldPath: [fieldName],
39
+ getDocumentById,
40
+ getModelByName,
41
+ errorPrefix: 'Error occurred while creating an entry.'
42
+ });
43
+ return accum;
44
+ },
45
+ { title: uuidv4() }
46
+ );
47
+ }
48
+
49
+ export function updateEntryFromUpdateOperations({
50
+ entry,
51
+ updateOperations,
52
+ getDocumentById,
53
+ getModelByName,
54
+ logger
55
+ }: {
56
+ entry: Entry;
57
+ updateOperations: StackbitTypes.UpdateOperation[];
58
+ getDocumentById: GetDocumentById;
59
+ getModelByName: GetModelByName;
60
+ logger: StackbitTypes.Logger;
61
+ }): Entry {
62
+ const model = getModelByName(entry.content_type_uid);
63
+ if (!model) {
64
+ throw new Error(`Error occurred while updating entry ${entry.uid}, model ${entry.content_type_uid} not found.`);
65
+ }
66
+
67
+ let updatedEntry = _.reduce(
68
+ updateOperations,
69
+ (entry: Entry, updateOperation) => {
70
+ return getUpdatedEntryAtFieldPathWithOperation({
71
+ entry,
72
+ updateOperation,
73
+ model,
74
+ getDocumentById,
75
+ getModelByName,
76
+ logger
77
+ });
78
+ },
79
+ entry
80
+ );
81
+
82
+ // Create a copy of the entry and remove fields that were not updated.
83
+ const modelFieldsMap = _.keyBy(model.fields, 'name');
84
+ const updatedRootFieldNames = updateOperations.reduce((fieldNamesMap, updateOperations) => {
85
+ fieldNamesMap[updateOperations.fieldPath[0] as string] = true;
86
+ return fieldNamesMap;
87
+ }, {} as Record<string, boolean>);
88
+ // const includedMetaProps = _.transform(
89
+ // ['uid', 'stackHeaders', 'content_type_uid', 'taxonomy_uid', 'locale', '_version', 'urlPath'],
90
+ // (map, value) => {
91
+ // map[value] = true;
92
+ // },
93
+ // {} as Record<string, boolean>
94
+ // );
95
+ updatedEntry = _.reduce(
96
+ updatedEntry,
97
+ (newEntry, value, prop) => {
98
+ if (!modelFieldsMap[prop] || updatedRootFieldNames[prop]) {
99
+ newEntry[prop] = value;
100
+ }
101
+ return newEntry;
102
+ },
103
+ {} as Entry
104
+ );
105
+
106
+ return sanitizeForUpdate(updatedEntry);
107
+ }
108
+
109
+ export function getUpdatedEntryAtFieldPathWithOperation({
110
+ entry,
111
+ updateOperation,
112
+ model,
113
+ getDocumentById,
114
+ getModelByName,
115
+ logger
116
+ }: {
117
+ entry: Entry;
118
+ updateOperation: StackbitTypes.UpdateOperation;
119
+ model: ModelWithContext;
120
+ getDocumentById: GetDocumentById;
121
+ getModelByName: GetModelByName;
122
+ logger: StackbitTypes.Logger;
123
+ }): Entry {
124
+ let opMessage: string;
125
+ switch (updateOperation.opType) {
126
+ case 'set':
127
+ opMessage = 'setting a field';
128
+ break;
129
+ case 'unset':
130
+ opMessage = 'unsetting a field';
131
+ break;
132
+ case 'insert':
133
+ opMessage = 'inserting an item into a list';
134
+ break;
135
+ case 'remove':
136
+ opMessage = 'removing an item from a list';
137
+ break;
138
+ case 'reorder':
139
+ opMessage = 'reordering a list';
140
+ break;
141
+ }
142
+ const errorPrefix = `Error occurred while ${opMessage} in entry '${entry.uid}'.`;
143
+
144
+ function createError(detailedMessage: string) {
145
+ return new Error(`${errorPrefix} ${detailedMessage}`);
146
+ }
147
+
148
+ function iterateModel<Type extends Record<string, unknown>>({
149
+ value,
150
+ model,
151
+ fieldPath,
152
+ entryFieldPath,
153
+ modelFieldPath
154
+ }: {
155
+ value?: Type;
156
+ model: ModelWithContext;
157
+ fieldPath: (number | string)[];
158
+ entryFieldPath: (string | number)[];
159
+ modelFieldPath: string[];
160
+ }): Type {
161
+ if (fieldPath.length === 0) {
162
+ return getValueForOperation({
163
+ updateOperation,
164
+ value,
165
+ rootEntry: entry,
166
+ rootModel: model,
167
+ entryFieldPath,
168
+ modelFieldPath,
169
+ getDocumentById,
170
+ getModelByName,
171
+ errorPrefix,
172
+ logger
173
+ });
174
+ }
175
+ const fieldName = _.head(fieldPath)!;
176
+ const fieldPathTail = _.tail(fieldPath);
177
+ if (typeof fieldName !== 'string') {
178
+ throw createError(
179
+ `The field '${fieldName}' in field path '${entryFieldPath.concat(fieldName).join('.')}' ` +
180
+ `is not a string, 'typeof ${fieldName} === ${typeof fieldName}'.`
181
+ );
182
+ }
183
+ const modelField = getModelField(model, fieldName);
184
+ if (!modelField) {
185
+ throw createError(
186
+ `The field '${fieldName}' in field path '${entryFieldPath.concat(fieldName).join('.')}' does not exist in model '${model.name}'.`
187
+ );
188
+ }
189
+ return {
190
+ ...value,
191
+ [fieldName]: iterateField({
192
+ value: value ? value[fieldName] : undefined,
193
+ modelField,
194
+ fieldPath: fieldPathTail,
195
+ rootModel: model,
196
+ entryFieldPath: entryFieldPath.concat(fieldName),
197
+ modelFieldPath: modelFieldPath.concat(fieldName)
198
+ })
199
+ } as Type;
200
+ }
201
+
202
+ function iterateField({
203
+ value,
204
+ modelField,
205
+ fieldPath,
206
+ rootModel,
207
+ entryFieldPath,
208
+ modelFieldPath
209
+ }: {
210
+ value?: unknown;
211
+ modelField: StackbitTypes.FieldSpecificProps;
212
+ fieldPath: (number | string)[];
213
+ rootModel: ModelWithContext;
214
+ entryFieldPath: (string | number)[];
215
+ modelFieldPath: string[];
216
+ }): unknown {
217
+ if (fieldPath.length === 0) {
218
+ return getValueForOperation({
219
+ updateOperation,
220
+ value,
221
+ rootEntry: entry,
222
+ rootModel,
223
+ entryFieldPath,
224
+ modelFieldPath,
225
+ getDocumentById,
226
+ getModelByName,
227
+ errorPrefix,
228
+ logger
229
+ });
230
+ }
231
+
232
+ if (modelField.type === 'object') {
233
+ // The value must be an object to apply the operation on its nested field with name fieldName,
234
+ // otherwise the fieldPath is illegal
235
+ if (!isObject(value)) {
236
+ throw createError(`The field path '${entryFieldPath.join('.')}' points to an empty value.`);
237
+ }
238
+ const fieldName = _.head(fieldPath)!;
239
+ const fieldPathTail = _.tail(fieldPath);
240
+ if (typeof fieldName !== 'string') {
241
+ throw createError(
242
+ `The field '${fieldName}' in field path '${entryFieldPath.concat(fieldName).join('.')}' ` +
243
+ `is not a string, 'typeof ${fieldName} === ${typeof fieldName}'.`
244
+ );
245
+ }
246
+ const childModelField = getModelField(modelField, fieldName);
247
+ if (!childModelField) {
248
+ throw createError(
249
+ `The field '${fieldName}' in field path '${entryFieldPath.concat(fieldName).join('.')}' does not exist in model '${model.name}'.`
250
+ );
251
+ }
252
+ return {
253
+ ...value,
254
+ [fieldName]: iterateField({
255
+ value: value ? value[fieldName] : undefined,
256
+ modelField: childModelField,
257
+ fieldPath: fieldPathTail,
258
+ rootModel,
259
+ entryFieldPath: entryFieldPath.concat(fieldName),
260
+ modelFieldPath: modelFieldPath.concat(fieldName)
261
+ })
262
+ };
263
+ } else if (modelField.type === 'model') {
264
+ // The value must be an object to apply the operation on its nested field with name fieldName,
265
+ // otherwise the fieldPath is illegal
266
+ if (!isObject(value)) {
267
+ throw createError(`The field path '${entryFieldPath.join('.')}' points to an empty value.`);
268
+ }
269
+ const fieldPathStr = modelFieldPath.join('.');
270
+ const blockMap = rootModel.context?.blockMap?.[fieldPathStr];
271
+ if (blockMap) {
272
+ // This is a modular blocks field with keys mapping to inline (object)
273
+ // or global (model) fields mapped by model.context.blockMap[blockUID].
274
+ const blockUIDs = _.keys(value);
275
+ const blockUID = blockUIDs[0];
276
+ if (blockUIDs.length !== 1 || !blockUID) {
277
+ throw createError(`The modular block at field path '${entryFieldPath.join('.')}' has no UID.`);
278
+ }
279
+ const block = value[blockUID];
280
+ if (!isObject(block)) {
281
+ throw createError(`The field path '${entryFieldPath.join('.')}' points to an empty modular block '${blockUID}'.`);
282
+ }
283
+ const modelName = blockMap.blockIdToModelName[blockUID];
284
+ if (!modelName) {
285
+ throw createError(`Could not match model name for modular block '${blockUID}' at field path '${entryFieldPath.join('.')}'.`);
286
+ }
287
+ const nestedModel = getModelByName(modelName);
288
+ if (!nestedModel) {
289
+ throw createError(
290
+ `Could not find model named '${modelName}' for modular block '${blockUID}' ` + `at field path '${entryFieldPath.join('.')}'.`
291
+ );
292
+ }
293
+ return {
294
+ [blockUID]: iterateModel({
295
+ value: block,
296
+ fieldPath: fieldPath,
297
+ model: nestedModel,
298
+ entryFieldPath: entryFieldPath,
299
+ modelFieldPath: []
300
+ })
301
+ };
302
+ } else {
303
+ // This is as a regular global field (model) with a single model or as a CNTSTK_LINK_MODEL
304
+ if (modelField.models.length !== 1) {
305
+ throw createError(
306
+ `A model field at field path '${entryFieldPath.join('.')}' has more than one (or zero) models ${modelField.models.join('.')}.`
307
+ );
308
+ }
309
+ const modelName = modelField.models[0]!;
310
+ const nestedModel = getModelByName(modelName);
311
+ if (!nestedModel) {
312
+ throw createError(`Could not find model named '${modelName}' for object at field path '${entryFieldPath.join('.')}.'`);
313
+ }
314
+ return iterateModel({
315
+ value: value,
316
+ fieldPath: fieldPath,
317
+ model: nestedModel,
318
+ entryFieldPath: entryFieldPath,
319
+ modelFieldPath: []
320
+ });
321
+ }
322
+ } else if (modelField.type === 'list') {
323
+ // The value must be an array to apply the operation on its nested items with item index,
324
+ // otherwise the fieldPath is illegal
325
+ if (!Array.isArray(value)) {
326
+ throw createError(`The value at field path '${entryFieldPath.join('.')}' is not an array.`);
327
+ }
328
+ const fieldName = _.head(fieldPath)!;
329
+ const fieldPathTail = _.tail(fieldPath);
330
+ const index = _.toNumber(fieldName);
331
+ if (typeof index !== 'number' || _.isNaN(index)) {
332
+ throw createError(
333
+ `The field '${fieldName}' in field path '${entryFieldPath.concat(fieldName).join('.')}' ` +
334
+ `is not a string, 'typeof ${fieldName} === ${typeof fieldName}'.`
335
+ );
336
+ }
337
+ const newArray = value.slice();
338
+ newArray.splice(
339
+ index,
340
+ 1,
341
+ iterateField({
342
+ value: value[index],
343
+ fieldPath: fieldPathTail,
344
+ modelField: modelField.items,
345
+ rootModel,
346
+ entryFieldPath: entryFieldPath.concat(index),
347
+ modelFieldPath: modelFieldPath
348
+ })
349
+ );
350
+ return newArray;
351
+ }
352
+ throw createError(`The field type '${modelField.type}' at field path '${entryFieldPath.join('.')}' is not a container type (object, model, list).`);
353
+ }
354
+
355
+ return iterateModel({
356
+ value: entry,
357
+ model,
358
+ fieldPath: updateOperation.fieldPath,
359
+ entryFieldPath: [],
360
+ modelFieldPath: []
361
+ });
362
+ }
363
+
364
+ function getValueForOperation({
365
+ updateOperation,
366
+ value,
367
+ rootModel,
368
+ entryFieldPath,
369
+ modelFieldPath,
370
+ getDocumentById,
371
+ getModelByName,
372
+ errorPrefix,
373
+ logger
374
+ }: {
375
+ updateOperation: StackbitTypes.UpdateOperation;
376
+ value: unknown;
377
+ rootEntry: Entry;
378
+ rootModel: ModelWithContext;
379
+ entryFieldPath: (string | number)[];
380
+ modelFieldPath: string[];
381
+ getDocumentById: GetDocumentById;
382
+ getModelByName: GetModelByName;
383
+ errorPrefix: string;
384
+ logger: StackbitTypes.Logger;
385
+ }) {
386
+ switch (updateOperation.opType) {
387
+ case 'set':
388
+ return convertOperationFieldValue({
389
+ updateOperationField: updateOperation.field,
390
+ modelField: updateOperation.modelField,
391
+ isInList: false,
392
+ rootModel: rootModel,
393
+ entryFieldPath: entryFieldPath,
394
+ modelFieldPath: modelFieldPath,
395
+ getModelByName,
396
+ getDocumentById,
397
+ errorPrefix
398
+ });
399
+ case 'unset': {
400
+ return null;
401
+ }
402
+ case 'insert': {
403
+ if (typeof value === 'undefined') {
404
+ value = [];
405
+ }
406
+ if (!Array.isArray(value)) {
407
+ throw new Error(`${errorPrefix} The field path points to a non array value.`);
408
+ }
409
+ const newValue = convertOperationFieldValue({
410
+ updateOperationField: updateOperation.item,
411
+ modelField: updateOperation.modelField.items,
412
+ isInList: true,
413
+ rootModel: rootModel,
414
+ entryFieldPath: entryFieldPath,
415
+ modelFieldPath: modelFieldPath,
416
+ getModelByName,
417
+ getDocumentById,
418
+ errorPrefix
419
+ });
420
+ if (typeof updateOperation.index === 'undefined') {
421
+ return value.concat(newValue);
422
+ }
423
+ const newArray = value.slice();
424
+ newArray.splice(updateOperation.index, 0, newValue);
425
+ return newArray;
426
+ }
427
+ case 'remove': {
428
+ if (typeof value === 'undefined') {
429
+ value = [];
430
+ }
431
+ if (!Array.isArray(value)) {
432
+ throw new Error(`${errorPrefix} The field path points to a non array value.`);
433
+ }
434
+ const newArray = value.slice();
435
+ newArray.splice(updateOperation.index, 1);
436
+ return newArray;
437
+ }
438
+ case 'reorder': {
439
+ if (typeof value === 'undefined') {
440
+ value = [];
441
+ }
442
+ if (!Array.isArray(value)) {
443
+ throw new Error(`${errorPrefix} The field path points to a non array value.`);
444
+ }
445
+ return updateOperation.order.map((newIndex) => (value as any[])[newIndex]);
446
+ }
447
+ default: {
448
+ const _exhaustiveCheck: never = updateOperation;
449
+ return _exhaustiveCheck;
450
+ }
451
+ }
452
+ }
453
+
454
+ export function convertOperationFieldValue({
455
+ updateOperationField,
456
+ rootModel,
457
+ modelField,
458
+ isInList,
459
+ entryFieldPath,
460
+ modelFieldPath,
461
+ getDocumentById,
462
+ getModelByName,
463
+ errorPrefix
464
+ }: {
465
+ updateOperationField: StackbitTypes.UpdateOperationField;
466
+ rootModel: ModelWithContext;
467
+ modelField: StackbitTypes.FieldSpecificProps;
468
+ isInList: boolean;
469
+ entryFieldPath: (string | number)[];
470
+ modelFieldPath: string[];
471
+ getDocumentById: GetDocumentById;
472
+ getModelByName: GetModelByName;
473
+ errorPrefix: string;
474
+ }): any {
475
+ function createError(detailedMessage: string) {
476
+ return new Error(`${errorPrefix} ${detailedMessage}`);
477
+ }
478
+
479
+ switch (updateOperationField.type) {
480
+ case 'string':
481
+ case 'url':
482
+ case 'slug':
483
+ case 'text':
484
+ case 'markdown':
485
+ case 'html':
486
+ case 'color': {
487
+ // when adding item to a list, it can't be empty string or undefined,
488
+ // otherwise Contentstack will not add it to the list
489
+ if (isInList && (updateOperationField.value === '' || _.isNil(updateOperationField.value))) {
490
+ return ' ';
491
+ }
492
+ return updateOperationField.value;
493
+ }
494
+ case 'date':
495
+ case 'datetime': {
496
+ if (_.isEmpty(updateOperationField.value)) {
497
+ return new Date().toISOString();
498
+ }
499
+ return updateOperationField.value;
500
+ }
501
+ case 'enum': {
502
+ if (updateOperationField.value === '') {
503
+ return null;
504
+ }
505
+ const firstOption = (modelField as StackbitTypes.FieldEnum).options[0];
506
+ const optionValue = firstOption && isObject(firstOption) ? firstOption.value : firstOption;
507
+ const isNumberEnum = typeof optionValue === 'number';
508
+ if (isNumberEnum) {
509
+ return _.toNumber(updateOperationField.value);
510
+ } else {
511
+ return _.toString(updateOperationField.value);
512
+ }
513
+ }
514
+ case 'number': {
515
+ return updateOperationField.value || 0;
516
+ }
517
+ case 'boolean': {
518
+ return updateOperationField.value || false;
519
+ }
520
+ case 'file':
521
+ case 'image': {
522
+ return updateOperationField.value || null;
523
+ }
524
+ case 'richText':
525
+ case 'json':
526
+ case 'style':
527
+ case 'cross-reference': {
528
+ return updateOperationField.value;
529
+ }
530
+ case 'reference': {
531
+ if (updateOperationField?.refType === 'asset') {
532
+ return updateOperationField.refId;
533
+ }
534
+ const document = getDocumentById(updateOperationField?.refId);
535
+ if (!document) {
536
+ throw createError(`Could not find entry with ID '${updateOperationField?.refId}' referenced at field path ${entryFieldPath.join('.')}.`);
537
+ }
538
+ const refObject = {
539
+ uid: updateOperationField.refId,
540
+ _content_type_uid: document.modelName
541
+ };
542
+ // references fields are always stored as arrays, even if they are not marked as multiple
543
+ return isInList ? refObject : [refObject];
544
+ }
545
+ case 'object': {
546
+ if (modelField.type !== 'object') {
547
+ throw createError(
548
+ `The operation field type 'object' does not match model field type '${modelField.type}' at field path ${entryFieldPath.join('.')}.`
549
+ );
550
+ }
551
+ return _.reduce(
552
+ updateOperationField.fields,
553
+ (accum: Record<string, unknown>, nestedUpdateOperationField, fieldName) => {
554
+ const nestedModelField = getModelField(modelField, fieldName);
555
+ if (!nestedModelField) {
556
+ return accum;
557
+ }
558
+ accum[fieldName] = convertOperationFieldValue({
559
+ updateOperationField: nestedUpdateOperationField,
560
+ rootModel,
561
+ modelField: nestedModelField,
562
+ isInList: false,
563
+ entryFieldPath: entryFieldPath.concat(fieldName),
564
+ modelFieldPath: modelFieldPath.concat(fieldName),
565
+ getDocumentById,
566
+ getModelByName,
567
+ errorPrefix
568
+ });
569
+ return accum;
570
+ },
571
+ {}
572
+ );
573
+ }
574
+ case 'model': {
575
+ if (modelField.type !== 'model') {
576
+ throw createError(
577
+ `The operation field type 'model' does not match model field type '${modelField.type}' at field path ${entryFieldPath.join('.')}.`
578
+ );
579
+ }
580
+ const nestedModel = getModelByName(updateOperationField.modelName);
581
+ if (!nestedModel) {
582
+ throw createError(`Could not find model named '${updateOperationField.modelName}' for field at path '${entryFieldPath.join('.')}'.`);
583
+ }
584
+ const fieldPathStr = modelFieldPath.join('.');
585
+ const blockMap = rootModel.context?.blockMap?.[fieldPathStr];
586
+ if (blockMap) {
587
+ // this is a modular blocks field with keys mapping to inline (object) or global (model) fields
588
+ // mapped by context.blockMap[blockUID]
589
+ const blockUID = blockMap.modelNameToBlockId[updateOperationField.modelName];
590
+ if (!blockUID) {
591
+ throw createError(
592
+ `Could not match modular block UI for model '${updateOperationField.modelName}' for field path '${entryFieldPath.join('.')}'.`
593
+ );
594
+ }
595
+ return {
596
+ [blockUID]: _.reduce(
597
+ updateOperationField.fields,
598
+ (accum: Record<string, unknown>, nestedUpdateOperationField, fieldName) => {
599
+ const nestedModelField = getModelField(nestedModel, fieldName);
600
+ if (!nestedModelField) {
601
+ return accum;
602
+ }
603
+ accum[fieldName] = convertOperationFieldValue({
604
+ updateOperationField: nestedUpdateOperationField,
605
+ rootModel: nestedModel,
606
+ modelField: nestedModelField,
607
+ isInList: false,
608
+ entryFieldPath: entryFieldPath.concat([blockUID, fieldName]),
609
+ modelFieldPath: [fieldName],
610
+ getDocumentById,
611
+ getModelByName,
612
+ errorPrefix
613
+ });
614
+ return accum;
615
+ },
616
+ {}
617
+ )
618
+ };
619
+ } else {
620
+ // this is a regular global field (model) with a single model or as a CNTSTK_LINK_MODEL
621
+ return _.reduce(
622
+ updateOperationField.fields,
623
+ (accum: Record<string, unknown>, nestedUpdateOperationField, fieldName) => {
624
+ const nestedModelField = getModelField(nestedModel, fieldName);
625
+ if (!nestedModelField) {
626
+ return accum;
627
+ }
628
+ accum[fieldName] = convertOperationFieldValue({
629
+ updateOperationField: nestedUpdateOperationField,
630
+ rootModel: nestedModel,
631
+ modelField: nestedModelField,
632
+ isInList: false,
633
+ entryFieldPath: entryFieldPath.concat(fieldName),
634
+ modelFieldPath: [fieldName],
635
+ getDocumentById,
636
+ getModelByName,
637
+ errorPrefix
638
+ });
639
+ return accum;
640
+ },
641
+ {}
642
+ );
643
+ }
644
+ }
645
+ case 'list': {
646
+ if (modelField.type !== 'list') {
647
+ throw createError(
648
+ `The operation field type 'list' does not match model field type '${modelField.type}' at field path ${entryFieldPath.join('.')}.`
649
+ );
650
+ }
651
+ return _.reduce(
652
+ updateOperationField.items,
653
+ (accum: any[], listItem: StackbitTypes.UpdateOperationListFieldItem, index) => {
654
+ accum.push(
655
+ convertOperationFieldValue({
656
+ updateOperationField: listItem,
657
+ rootModel,
658
+ modelField: modelField.items,
659
+ isInList: true,
660
+ entryFieldPath: entryFieldPath.concat(index),
661
+ modelFieldPath: modelFieldPath,
662
+ getDocumentById,
663
+ getModelByName,
664
+ errorPrefix
665
+ })
666
+ );
667
+ return accum;
668
+ },
669
+ []
670
+ );
671
+ }
672
+ default: {
673
+ const _exhaustiveCheck: never = updateOperationField;
674
+ return _exhaustiveCheck;
675
+ }
676
+ }
677
+ }
678
+
679
+ function getModelField(model: ModelWithContext | StackbitTypes.FieldObjectProps, fieldName: string) {
680
+ return model.fields?.find((field) => field.name === fieldName);
681
+ }
682
+
683
+ function isObject(value: unknown): value is Record<string, unknown> {
684
+ return _.isPlainObject(value);
685
+ }
686
+
687
+ function sanitizeForUpdate(entry: Entry): Entry {
688
+ return _.cloneDeepWith(entry, (value) => {
689
+ if (isAsset(value)) {
690
+ return value.uid;
691
+ }
692
+ });
693
+ }
694
+
695
+ function isAsset(value: unknown): value is Asset {
696
+ if (!isObject(value) || !value.uid) {
697
+ return false;
698
+ }
699
+ // if all asset keys are present in the value, then it is an asset
700
+ const imageKeys = ['uid', 'created_at', 'updated_at', 'created_by', 'updated_by', 'content_type', 'file_size', 'filename', 'url'];
701
+ const objectKeys = _.keys(value);
702
+ return _.difference(imageKeys, objectKeys).length === 0;
703
+ }