@stackbit/cms-core 0.1.2 → 0.1.3-alpha.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.
Files changed (48) hide show
  1. package/dist/content-source-interface.d.ts +7 -12
  2. package/dist/content-source-interface.d.ts.map +1 -1
  3. package/dist/content-source-interface.js.map +1 -1
  4. package/dist/content-store-types.d.ts +25 -24
  5. package/dist/content-store-types.d.ts.map +1 -1
  6. package/dist/content-store-utils.d.ts +9 -0
  7. package/dist/content-store-utils.d.ts.map +1 -0
  8. package/dist/content-store-utils.js +139 -0
  9. package/dist/content-store-utils.js.map +1 -0
  10. package/dist/content-store.d.ts +17 -4
  11. package/dist/content-store.d.ts.map +1 -1
  12. package/dist/content-store.js +146 -948
  13. package/dist/content-store.js.map +1 -1
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/types/search-filter.d.ts +42 -0
  19. package/dist/types/search-filter.d.ts.map +1 -0
  20. package/dist/types/search-filter.js +3 -0
  21. package/dist/types/search-filter.js.map +1 -0
  22. package/dist/utils/create-update-csi-docs.d.ts +68 -0
  23. package/dist/utils/create-update-csi-docs.d.ts.map +1 -0
  24. package/dist/utils/create-update-csi-docs.js +376 -0
  25. package/dist/utils/create-update-csi-docs.js.map +1 -0
  26. package/dist/utils/csi-to-store-docs-converter.d.ts +15 -0
  27. package/dist/utils/csi-to-store-docs-converter.d.ts.map +1 -0
  28. package/dist/utils/csi-to-store-docs-converter.js +287 -0
  29. package/dist/utils/csi-to-store-docs-converter.js.map +1 -0
  30. package/dist/utils/search-utils.d.ts +21 -0
  31. package/dist/utils/search-utils.d.ts.map +1 -0
  32. package/dist/utils/search-utils.js +323 -0
  33. package/dist/utils/search-utils.js.map +1 -0
  34. package/dist/utils/store-to-api-docs-converter.d.ts +5 -0
  35. package/dist/utils/store-to-api-docs-converter.d.ts.map +1 -0
  36. package/dist/utils/store-to-api-docs-converter.js +247 -0
  37. package/dist/utils/store-to-api-docs-converter.js.map +1 -0
  38. package/package.json +6 -5
  39. package/src/content-source-interface.ts +5 -14
  40. package/src/content-store-types.ts +23 -10
  41. package/src/content-store-utils.ts +149 -0
  42. package/src/content-store.ts +136 -1078
  43. package/src/index.ts +3 -1
  44. package/src/types/search-filter.ts +53 -0
  45. package/src/utils/create-update-csi-docs.ts +440 -0
  46. package/src/utils/csi-to-store-docs-converter.ts +365 -0
  47. package/src/utils/search-utils.ts +436 -0
  48. package/src/utils/store-to-api-docs-converter.ts +246 -0
@@ -0,0 +1,436 @@
1
+ import _ from 'lodash';
2
+ import { Model } from '@stackbit/sdk';
3
+ import { SearchFilter, SearchFilterItem } from '../types/search-filter';
4
+ import { ContentStoreTypes } from '..';
5
+ import * as ContentSourceInterface from '../content-source-interface';
6
+ import { getLocalizedFieldForLocale } from '../content-source-interface';
7
+
8
+ const META_FIELD = {
9
+ createdAt: {
10
+ type: 'date'
11
+ },
12
+ updatedAt: {
13
+ type: 'date'
14
+ }
15
+ } as const;
16
+
17
+ type Schema = Record<string, Record<string, Record<string, Model>>>;
18
+
19
+ export const searchDocuments = (data: {
20
+ query?: string;
21
+ filter?: SearchFilter;
22
+ models: Array<{
23
+ srcProjectId: string;
24
+ srcType: string;
25
+ modelName: string;
26
+ }>;
27
+ documents: ContentStoreTypes.Document[];
28
+ schema: Schema;
29
+ locale?: string;
30
+ }): {
31
+ total: number;
32
+ items: ContentStoreTypes.Document[];
33
+ } => {
34
+ const query = data.query?.toLowerCase();
35
+
36
+ const { documents, schema } = data;
37
+
38
+ let allDocuments = 0;
39
+
40
+ const matchedDocuments = documents.filter((document) => {
41
+ const isIncludedModel = _.find(data.models, {
42
+ srcType: document.srcType,
43
+ srcProjectId: document.srcProjectId,
44
+ modelName: document.srcModelName
45
+ });
46
+ if (!isIncludedModel) {
47
+ return false;
48
+ }
49
+
50
+ allDocuments += 1;
51
+
52
+ if (query) {
53
+ const matches = isDocumentMatchesPattern(document, query, data.locale);
54
+ if (!matches) {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ if (data.filter?.and) {
60
+ // only 'and' supported for now; later we can add e.g. 'or'
61
+ const matches = data.filter.and.every((filter) => {
62
+ const field = getFieldForFilter(document, filter);
63
+ return isFieldMatchesFilter({ field, filter, document, schema, locale: data.locale });
64
+ });
65
+
66
+ if (!matches) {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ return true;
72
+ });
73
+
74
+ return {
75
+ items: matchedDocuments,
76
+ total: allDocuments
77
+ };
78
+ };
79
+
80
+ const isDocumentMatchesPattern = (document: ContentStoreTypes.Document, query: string, locale?: string): boolean => {
81
+ return _.some(document.fields, (field) => {
82
+ let value;
83
+ switch (field.type) {
84
+ case 'string':
85
+ case 'slug':
86
+ case 'url':
87
+ case 'text':
88
+ case 'markdown':
89
+ case 'html':
90
+ value = getLocalizedFieldForLocale(field, locale)?.value;
91
+ break;
92
+ }
93
+
94
+ if (value) {
95
+ return value?.toString().toLowerCase().includes(query);
96
+ }
97
+
98
+ return false;
99
+ });
100
+ };
101
+
102
+ const getFieldForFilter = (document: ContentStoreTypes.Document, filter: SearchFilterItem): ContentStoreTypes.DocumentField => {
103
+ if (filter.isMeta) {
104
+ const fieldDef = META_FIELD[filter.field as keyof typeof META_FIELD];
105
+ if (!fieldDef) {
106
+ throw new Error(`Unsupported meta field ${filter.field}`);
107
+ }
108
+ return {
109
+ ...fieldDef,
110
+ value: document[filter.field as keyof typeof document]
111
+ };
112
+ } else {
113
+ const documentField = document.fields[filter.field];
114
+ if (!documentField) {
115
+ throw new Error(`Field not found ${filter.field}`);
116
+ }
117
+ return documentField;
118
+ }
119
+ };
120
+
121
+ const isFieldMatchesFilter = ({
122
+ document,
123
+ field,
124
+ filter,
125
+ locale,
126
+ schema
127
+ }: {
128
+ document: ContentStoreTypes.Document;
129
+ field: ContentStoreTypes.DocumentField;
130
+ filter: SearchFilterItem;
131
+ locale?: string;
132
+ schema: Schema;
133
+ }): boolean => {
134
+ switch (field?.type) {
135
+ case 'string':
136
+ case 'slug':
137
+ case 'url':
138
+ case 'text':
139
+ case 'markdown':
140
+ case 'html':
141
+ return isStringFieldMatches({ field, filter, locale });
142
+
143
+ case 'number':
144
+ return isNumberFieldMatches({ field, filter, locale });
145
+
146
+ case 'boolean':
147
+ return isBooleanFieldMatches({ field, filter, locale });
148
+
149
+ case 'date':
150
+ case 'datetime':
151
+ return isDateFieldMatches({ field, filter, locale });
152
+
153
+ case 'enum':
154
+ return isEnumFieldMatches({ field, filter, locale });
155
+
156
+ case 'list': {
157
+ const model = schema?.[document.srcType]?.[document.srcProjectId]?.[document.srcModelName];
158
+ if (!model) {
159
+ throw new Error(`Can't find model for the ${filter.field}`);
160
+ }
161
+ return isListFieldMatches({ field, filter, model, locale });
162
+ }
163
+
164
+ default:
165
+ throw new Error(`Unsupported filter for field ${filter.field} with type ${field?.type}`);
166
+ }
167
+ };
168
+
169
+ const isStringFieldMatches = ({
170
+ field,
171
+ filter,
172
+ locale
173
+ }: {
174
+ field: ContentStoreTypes.DocumentFieldForType<'string' | 'slug' | 'url' | 'text' | 'markdown' | 'html'>;
175
+ filter: SearchFilterItem;
176
+ locale?: string;
177
+ }) => {
178
+ // ignoring case
179
+ const fieldValue = getLocalizedFieldForLocale(field, locale)?.value?.toLowerCase();
180
+
181
+ switch (filter.operator) {
182
+ case 'is-undefined': return fieldValue === undefined;
183
+ case 'is-not-undefined': return fieldValue !== undefined;
184
+
185
+ case 'eq': // ===
186
+ case 'neq': // !==
187
+ case 'includes':
188
+ case 'not-includes': {
189
+ if (typeof filter.value !== 'string') {
190
+ throw new Error(`Filter value should be string for field ${filter.field}`);
191
+ }
192
+
193
+ const filterValue = filter.value?.toString().toLowerCase();
194
+
195
+ switch (filter.operator) {
196
+ case 'eq':
197
+ return fieldValue === filterValue;
198
+ case 'neq':
199
+ return fieldValue !== filterValue;
200
+ case 'includes':
201
+ return fieldValue.includes(filterValue);
202
+ case 'not-includes':
203
+ return !fieldValue.includes(filterValue);
204
+ }
205
+ }
206
+ }
207
+
208
+ throw new Error(`Not supported operator ${filter.operator} for field ${filter.field}`);
209
+ };
210
+
211
+ const isNumberFieldMatches = ({ field, filter, locale }: { field: ContentStoreTypes.DocumentFieldForType<'number'>; filter: SearchFilterItem; locale?: string }) => {
212
+ const fieldValue = getLocalizedFieldForLocale(field, locale)?.value;
213
+
214
+ switch (filter.operator) {
215
+ case 'is-undefined': return fieldValue === undefined;
216
+ case 'is-not-undefined': return fieldValue !== undefined;
217
+
218
+ case 'eq': // ===
219
+ case 'neq': // !==
220
+ case 'gte': // >=
221
+ case 'lte': {
222
+ // <=
223
+ if (typeof filter.value !== 'number') {
224
+ throw new Error(`Filter value should be number for field ${filter.field}`);
225
+ }
226
+
227
+ switch (filter.operator) {
228
+ case 'eq':
229
+ return fieldValue === filter.value;
230
+ case 'neq':
231
+ return fieldValue !== filter.value;
232
+ case 'gte':
233
+ return fieldValue >= filter.value;
234
+ case 'lte':
235
+ return fieldValue <= filter.value;
236
+ }
237
+ }
238
+
239
+ case 'between': {
240
+ // ..N..
241
+ if (typeof filter.startValue !== 'number' || typeof filter.endValue !== 'number') {
242
+ throw new Error(`Filter startValue and endValue should be number for field ${filter.field}`);
243
+ }
244
+
245
+ return fieldValue >= filter.startValue && fieldValue <= filter.endValue;
246
+ }
247
+ }
248
+
249
+ throw new Error(`Not supported operator ${filter.operator} for field ${filter.field}`);
250
+ };
251
+
252
+ const isBooleanFieldMatches = ({
253
+ field,
254
+ filter,
255
+ locale
256
+ }: {
257
+ field: ContentStoreTypes.DocumentFieldForType<'boolean'>;
258
+ filter: SearchFilterItem;
259
+ locale?: string;
260
+ }) => {
261
+ const fieldValue = getLocalizedFieldForLocale(field, locale)?.value;
262
+
263
+ switch (filter.operator) {
264
+ case 'is-undefined': return fieldValue === undefined;
265
+ case 'is-not-undefined': return fieldValue !== undefined;
266
+
267
+ case 'eq':
268
+ case 'neq': {
269
+ if (typeof filter.value !== 'boolean') {
270
+ throw new Error(`Filter value should be boolean for field ${filter.field}`);
271
+ }
272
+
273
+ switch (filter.operator) {
274
+ case 'eq':
275
+ return fieldValue === filter.value;
276
+ case 'neq':
277
+ return fieldValue !== filter.value;
278
+ }
279
+ }
280
+ }
281
+
282
+ throw new Error(`Not supported operator ${filter.operator} for field ${filter.field}`);
283
+ };
284
+
285
+ const parseDateValue = (value?: string): Date | undefined => {
286
+ let dateValue;
287
+ if (value?.endsWith('Z')) {
288
+ dateValue = new Date(value);
289
+ } else if (value?.match(/\d{1,4}-\d{1,2}-\d{1,2}$/)) {
290
+ // try to parse it as a date
291
+ // when use '-' in js dates constructor, it make the date with TZ offset
292
+ // 2022-10-04 => 03 Oct 2022 20:00 GMT-4
293
+ // 2022/10/04 => 04 Oct 2022 00:00 GMT-4
294
+ dateValue = new Date(value.replace(/-/g, '/'));
295
+ }
296
+ if (dateValue && !Number.isNaN(dateValue)) {
297
+ return dateValue;
298
+ }
299
+ };
300
+
301
+ const isDateFieldMatches = ({
302
+ field,
303
+ filter,
304
+ locale
305
+ }: {
306
+ field: ContentStoreTypes.DocumentFieldForType<'date' | 'datetime'>;
307
+ filter: SearchFilterItem;
308
+ locale?: string;
309
+ }) => {
310
+ const origValue = getLocalizedFieldForLocale(field, locale)?.value;
311
+ const fieldValue = parseDateValue(origValue);
312
+ if (origValue && !fieldValue) {
313
+ throw new Error(`Can't parse value ${origValue} for field ${filter.field}`);
314
+ }
315
+
316
+ switch (filter.operator) {
317
+ case 'is-undefined': return fieldValue === undefined;
318
+ case 'is-not-undefined': return fieldValue !== undefined;
319
+
320
+ case 'eq': // ===
321
+ case 'neq': // !==
322
+ case 'gte': // >=
323
+ case 'lte': {
324
+ // <=
325
+ if (typeof filter.value !== 'string') {
326
+ throw new Error(`Filter value should be in string date format for field ${filter.field}`);
327
+ }
328
+
329
+ const filterValue = parseDateValue(filter.value);
330
+
331
+ if (!filterValue) {
332
+ throw new Error(`Filter value should be in date format for field ${filter.field}`);
333
+ }
334
+
335
+ switch (filter.operator) {
336
+ case 'eq':
337
+ case 'neq': {// check if day is the same
338
+ const result = (
339
+ fieldValue?.getFullYear() === filterValue.getFullYear() &&
340
+ fieldValue?.getMonth() === filterValue.getMonth() &&
341
+ fieldValue?.getDate() === filterValue.getDate()
342
+ );
343
+ return filter.operator === 'eq' ? result : !result;
344
+ }
345
+
346
+ case 'gte':
347
+ return (fieldValue?.getTime() ?? 0) >= filterValue.getTime();
348
+ case 'lte':
349
+ return (fieldValue?.getTime() ?? 0) <= filterValue.getTime();
350
+ }
351
+ }
352
+
353
+ case 'between': {
354
+ // ..N..
355
+ if (typeof filter.startValue !== 'string' || typeof filter.endValue !== 'string') {
356
+ throw new Error(`Filter startValue and endValue should be in string date format for field ${filter.field}`);
357
+ }
358
+
359
+ const startDate = parseDateValue(filter.startValue);
360
+ const endDate = parseDateValue(filter.endValue);
361
+ if (!startDate || !endDate) {
362
+ throw new Error(`Filter startValue and endValue should be in date format for field ${filter.field}`);
363
+ }
364
+
365
+ return (fieldValue?.getTime() ?? 0) >= startDate.getTime() && (fieldValue?.getTime() ?? 0) <= endDate.getTime();
366
+ }
367
+ }
368
+
369
+ throw new Error(`Not supported operator ${filter.operator} for field ${filter.field}`);
370
+ };
371
+
372
+ const isEnumFieldMatches = ({ field, filter, locale }: { field: ContentStoreTypes.DocumentFieldForType<'enum'>; filter: SearchFilterItem; locale?: string }) => {
373
+ const fieldValue = getLocalizedFieldForLocale(field, locale)?.value;
374
+
375
+ switch (filter.operator) {
376
+ case 'is-undefined': return fieldValue === undefined;
377
+ case 'is-not-undefined': return fieldValue !== undefined;
378
+
379
+ case 'in': // one of
380
+ case 'nin': { // none of
381
+ const filterValues = filter.values as any[];
382
+
383
+ switch (filter.operator) {
384
+ case 'in':
385
+ return filterValues.includes(fieldValue);
386
+ case 'nin':
387
+ return !filterValues.includes(fieldValue);
388
+ }
389
+ }
390
+ }
391
+
392
+ throw new Error(`Not supported operator ${filter.operator} for field ${filter.field}`);
393
+ };
394
+
395
+ const isListFieldMatches = ({
396
+ field,
397
+ filter,
398
+ locale,
399
+ model
400
+ }: {
401
+ field: ContentStoreTypes.DocumentFieldForType<'list'>;
402
+ filter: SearchFilterItem;
403
+ model: Model;
404
+ locale?: string;
405
+ }) => {
406
+ const fieldModel = model?.fields?.find((field) => field.name === filter.field);
407
+ const listItemsType = fieldModel?.type === 'list' && fieldModel.items?.type;
408
+ const isPrimitiveList =
409
+ listItemsType && ['string', 'slug', 'url', 'text', 'markdown', 'boolean', 'date', 'datetime', 'number', 'enum'].includes(listItemsType);
410
+ if (!isPrimitiveList) {
411
+ throw new Error(`Unsupported filter for list field ${filter.field} with children ${listItemsType}`);
412
+ }
413
+
414
+ const fieldValue = getLocalizedFieldForLocale(field as ContentSourceInterface.DocumentFieldForType<'list'>, locale)?.items?.map((item) =>
415
+ 'value' in item ? item.value : undefined
416
+ );
417
+
418
+ switch (filter.operator) {
419
+ case 'in': // one of
420
+ case 'nin': // none of
421
+ case 'all': { // all of
422
+ const filterValues = filter.values as any[];
423
+
424
+ switch (filter.operator) {
425
+ case 'in':
426
+ return fieldValue?.some((value) => filterValues.includes(value)) ?? false;
427
+ case 'nin':
428
+ return !fieldValue?.some((value) => filterValues.includes(value));
429
+ case 'all':
430
+ return filterValues.every((value) => fieldValue?.includes(value));
431
+ }
432
+ }
433
+ }
434
+
435
+ throw new Error(`Not supported operator ${filter.operator} for field ${filter.field}`);
436
+ };
@@ -0,0 +1,246 @@
1
+ import _ from 'lodash';
2
+ import { omitByNil } from '@stackbit/utils';
3
+ import * as ContentStoreTypes from '../content-store-types';
4
+ import { getDocumentFieldForLocale } from '../content-store-utils';
5
+
6
+ export function mapDocumentsToLocalizedApiObjects(documents: ContentStoreTypes.Document[], locale?: string): ContentStoreTypes.APIDocumentObject[] {
7
+ return documents.map((document) => documentToLocalizedApiObject(document, locale));
8
+ }
9
+
10
+ function documentToLocalizedApiObject(document: ContentStoreTypes.Document, locale?: string): ContentStoreTypes.APIDocumentObject {
11
+ const { type, fields, ...rest } = document;
12
+ return {
13
+ type: 'object',
14
+ ...rest,
15
+ fields: toLocalizedAPIFields(fields, locale)
16
+ };
17
+ }
18
+
19
+ function toLocalizedAPIFields(docFields: Record<string, ContentStoreTypes.DocumentField>, locale?: string): Record<string, ContentStoreTypes.DocumentFieldAPI> {
20
+ return _.mapValues(docFields, (docField) => toLocalizedAPIField(docField, locale));
21
+ }
22
+
23
+ function toLocalizedAPIField(docField: ContentStoreTypes.DocumentField, locale?: string, isListItem = false): ContentStoreTypes.DocumentFieldAPI {
24
+ type ToBoolean<T extends boolean | undefined> = T extends true ? true : false;
25
+ function localeFields<T extends boolean | undefined>(
26
+ localized: T,
27
+ _locale: string | undefined
28
+ ): null | { localized: ToBoolean<T>; locale: string | undefined } {
29
+ return isListItem
30
+ ? null
31
+ : {
32
+ localized: !!localized as ToBoolean<T>,
33
+ locale: locale ?? _locale
34
+ };
35
+ }
36
+ switch (docField.type) {
37
+ case 'string':
38
+ case 'text':
39
+ case 'url':
40
+ case 'slug':
41
+ case 'html':
42
+ case 'number':
43
+ case 'boolean':
44
+ case 'enum':
45
+ case 'date':
46
+ case 'datetime':
47
+ case 'color':
48
+ case 'style':
49
+ case 'file':
50
+ case 'json':
51
+ case 'markdown':
52
+ case 'richText':
53
+ if (docField.localized) {
54
+ const { localized, locales, ...base } = docField;
55
+ const localeProps = locale ? locales[locale] : undefined;
56
+ return {
57
+ ...base,
58
+ ...(localeProps ?? { value: null }),
59
+ ...localeFields(localized, locale),
60
+ ...(['file', 'json', 'markdown', 'richText'].includes(docField.type) && !localeProps ? { isUnset: true } : null)
61
+ };
62
+ }
63
+ return {
64
+ ...docField,
65
+ ...localeFields(docField.localized, docField.locale)
66
+ };
67
+ case 'image':
68
+ if (docField.localized) {
69
+ const { localized, locales, ...base } = docField;
70
+ const localeProps = locale ? locales[locale] : undefined;
71
+ return {
72
+ ...base,
73
+ ...(localeProps ?? { isUnset: true }),
74
+ ...localeFields(localized, locale)
75
+ };
76
+ }
77
+ return {
78
+ ...docField,
79
+ ...localeFields(docField.localized, docField.locale)
80
+ };
81
+ case 'object':
82
+ case 'model':
83
+ if (docField.localized) {
84
+ if (docField.type === 'object') {
85
+ const { localized, locales, ...base } = docField;
86
+ const localeProps = locale ? locales[locale] : undefined;
87
+ return {
88
+ ...base,
89
+ ...(localeProps
90
+ ? {
91
+ ...localeProps,
92
+ fields: toLocalizedAPIFields(localeProps.fields, locale)
93
+ }
94
+ : { isUnset: true }),
95
+ ...localeFields(localized, locale)
96
+ };
97
+ } else {
98
+ const { localized, locales, ...base } = docField;
99
+ const localeProps = locale ? locales[locale] : undefined;
100
+ return {
101
+ ...base,
102
+ type: 'object',
103
+ ...(localeProps
104
+ ? {
105
+ ...localeProps,
106
+ fields: toLocalizedAPIFields(localeProps.fields, locale)
107
+ }
108
+ : { isUnset: true }),
109
+ ...localeFields(localized, locale)
110
+ };
111
+ }
112
+ }
113
+ return {
114
+ ...(!docField.isUnset
115
+ ? {
116
+ ...docField,
117
+ type: 'object',
118
+ fields: toLocalizedAPIFields(docField.fields, locale)
119
+ }
120
+ : {
121
+ ...docField,
122
+ type: 'object'
123
+ }),
124
+ ...localeFields(docField.localized, docField.locale)
125
+ };
126
+ case 'reference':
127
+ if (docField.localized) {
128
+ const { type, refType, localized, locales, ...base } = docField;
129
+ const localeProps = locale ? locales[locale] : undefined;
130
+ // if reference field isUnset === true, it behaves like a regular object
131
+ if (!localeProps || localeProps.isUnset) {
132
+ return {
133
+ type: 'object',
134
+ isUnset: true,
135
+ ...base,
136
+ ...localeFields(localized, locale)
137
+ };
138
+ }
139
+ return {
140
+ type: 'unresolved_reference',
141
+ refType: refType === 'asset' ? 'image' : 'object',
142
+ ...base,
143
+ ...localeProps,
144
+ ...localeFields(localized, locale)
145
+ };
146
+ }
147
+ const { type, refType, ...base } = docField;
148
+ if (base.isUnset) {
149
+ return {
150
+ type: 'object',
151
+ ...base,
152
+ ...localeFields(docField.localized, docField.locale)
153
+ };
154
+ }
155
+ return {
156
+ type: 'unresolved_reference',
157
+ refType: refType === 'asset' ? 'image' : 'object',
158
+ ...base,
159
+ ...localeFields(docField.localized, docField.locale)
160
+ };
161
+ case 'list':
162
+ if (docField.localized) {
163
+ const { localized, locales, ...base } = docField;
164
+ const localeProps = locale ? locales[locale] : undefined;
165
+ return {
166
+ ...base,
167
+ ...localeProps,
168
+ items: (localeProps?.items ?? []).map((field) => toLocalizedAPIField(field, locale, true)),
169
+ ...localeFields(localized, locale)
170
+ };
171
+ }
172
+ return {
173
+ ...docField,
174
+ ...localeFields(docField.localized, docField.locale),
175
+ items: docField.items.map((field) => toLocalizedAPIField(field, locale, true))
176
+ };
177
+ default:
178
+ const _exhaustiveCheck: never = docField;
179
+ console.error(`toLocalizedAPIField _exhaustiveCheck failed, docField.type: ${docField['type']}`);
180
+ return _exhaustiveCheck;
181
+ }
182
+ }
183
+
184
+ export function mapAssetsToLocalizedApiImages(assets: ContentStoreTypes.Asset[], locale?: string): ContentStoreTypes.APIImageObject[] {
185
+ return assets.map((asset) => assetToLocalizedApiImage(asset, locale));
186
+ }
187
+
188
+ function assetToLocalizedApiImage(asset: ContentStoreTypes.Asset, locale?: string): ContentStoreTypes.APIImageObject {
189
+ const { type, fields, ...rest } = asset;
190
+ return {
191
+ type: 'image',
192
+ ...rest,
193
+ fields: localizeAssetFields(fields, locale)
194
+ };
195
+ }
196
+
197
+ function localizeAssetFields(assetFields: ContentStoreTypes.AssetFields, locale?: string): ContentStoreTypes.AssetFieldsAPI {
198
+ const fields: ContentStoreTypes.AssetFieldsAPI = {
199
+ title: {
200
+ type: 'string' as const,
201
+ value: null as any
202
+ },
203
+ url: {
204
+ type: 'string' as const,
205
+ value: null as any
206
+ }
207
+ };
208
+ const titleFieldNonLocalized = getDocumentFieldForLocale(assetFields.title, locale);
209
+ fields.title.value = titleFieldNonLocalized?.value;
210
+ fields.title.locale = locale ?? titleFieldNonLocalized?.locale;
211
+ const assetFileField = assetFields.file;
212
+ if (assetFileField.localized) {
213
+ if (locale) {
214
+ fields.url.value = assetFileField.locales[locale]?.url ?? null;
215
+ fields.url.locale = locale;
216
+ }
217
+ } else {
218
+ fields.url.value = assetFileField.url;
219
+ fields.url.locale = assetFileField.locale;
220
+ }
221
+ return fields;
222
+ }
223
+
224
+ export function mapStoreAssetsToAPIAssets(assets: ContentStoreTypes.Asset[], locale?: string): ContentStoreTypes.APIAsset[] {
225
+ return assets.map((asset) => storeAssetToAPIAsset(asset, locale));
226
+ }
227
+
228
+ function storeAssetToAPIAsset(asset: ContentStoreTypes.Asset, locale?: string): ContentStoreTypes.APIAsset {
229
+ const assetTitleField = asset.fields.title;
230
+ const localizedTitleField = assetTitleField.localized ? assetTitleField.locales[locale!]! : assetTitleField;
231
+ const assetFileField = asset.fields.file;
232
+ const localizedFileField = assetFileField.localized ? assetFileField.locales[locale!]! : assetFileField;
233
+ return {
234
+ objectId: asset.srcObjectId,
235
+ createdAt: asset.createdAt,
236
+ url: localizedFileField.url,
237
+ ...omitByNil({
238
+ title: localizedTitleField.value,
239
+ fileName: localizedFileField.fileName,
240
+ contentType: localizedFileField.contentType,
241
+ size: localizedFileField.size,
242
+ width: localizedFileField.dimensions?.width,
243
+ height: localizedFileField.dimensions?.height
244
+ })
245
+ };
246
+ }