import _ from 'lodash'; import { omitByUndefined } from '@stackbit/utils'; import * as CSITypes from '@stackbit/types'; import * as ContentStoreTypes from '../types'; import { getContentSourceId, getDocumentFieldForLocale } from '../content-store-utils'; import { ReferenceMap } from '../types'; export function mapDocumentsToApiDocuments({ documents, allowedLocales, contentSourceDataById, delegate, referenceMap }: { documents: ContentStoreTypes.Document[]; allowedLocales?: string[]; contentSourceDataById: Record; delegate: CSITypes.ConfigDelegate; referenceMap?: ReferenceMap; }): ContentStoreTypes.APIDocument[] { return documents.map((document) => documentToApiV2Object({ document, allowedLocales, contentSourceDataById, delegate, referenceMap }) ); } function documentToApiV2Object({ document, allowedLocales, contentSourceDataById, delegate, referenceMap }: { document: ContentStoreTypes.Document; allowedLocales?: string[]; contentSourceDataById: Record; delegate: CSITypes.ConfigDelegate; referenceMap?: ReferenceMap; }): ContentStoreTypes.APIDocument { const { fields, getPreview, ...rest } = document; const directReferences = referenceMap?.[document.srcType]?.[document.srcProjectId]?.[document.srcObjectId]; return omitByUndefined({ ..._.omit(rest), srcObjectLabel: getPreview({ delegate /*, locale*/ }).previewTitle, references: directReferences ? { direct: directReferences } : undefined, fields: toAPIV2Fields({ docFields: fields, delegate, allowedLocales, contentSourceDataById, contentSourceId: getContentSourceId(document.srcType, document.srcProjectId) }) }); } function toAPIV2Fields({ docFields, delegate, contentSourceDataById, contentSourceId, allowedLocales }: { docFields: Record; delegate: CSITypes.ConfigDelegate; contentSourceDataById: Record; contentSourceId: string; allowedLocales?: string[]; }): Record { return _.mapValues(docFields, (docField) => { return omitByUndefined( toAPIV2Field({ docField, delegate, contentSourceDataById, contentSourceId, allowedLocales }) ); }); } function toAPIV2Field({ docField, delegate, contentSourceDataById, contentSourceId, allowedLocales, isListItem = false }: { docField: ContentStoreTypes.DocumentField; delegate: CSITypes.ConfigDelegate; allowedLocales?: string[]; contentSourceDataById: Record; contentSourceId: string; isListItem?: boolean; }): ContentStoreTypes.DocumentFieldAPIV2 { // TODO: reduce over allowedLocales rather then existing locales - use isUnset, and add fieldActions even for missing locales switch (docField.type) { case 'string': case 'text': case 'url': case 'slug': case 'html': case 'number': case 'boolean': case 'enum': case 'date': case 'datetime': case 'color': case 'style': case 'json': case 'file': case 'markdown': case 'richText': { if (docField.localized) { const locales = _.reduce( docField.locales, (acc, cur, locale) => { acc[locale] = cur; return acc; }, {} as ContentStoreTypes.DocumentFieldLocalizedAPIV2['locales'] ); return { ...docField, locales } as ContentStoreTypes.DocumentFieldLocalizedAPIV2; } return docField; } case 'image': { if (docField.localized) { const locales = _.reduce( docField.locales, (acc, cur, locale) => { acc[locale] = { ...cur, fields: toAPIV2Fields({ docFields: cur.fields, delegate, contentSourceDataById, contentSourceId, allowedLocales }) as ContentStoreTypes.ImageFieldsAPIV2 }; return acc; }, {} as ContentStoreTypes.DocumentImageFieldLocalizedAPIV2['locales'] ); return { ...docField, locales } as ContentStoreTypes.DocumentFieldLocalizedAPIV2; } if (docField.isUnset) { return docField; } return { ...docField, fields: toAPIV2Fields({ docFields: docField.fields, delegate, contentSourceDataById, contentSourceId, allowedLocales }) as ContentStoreTypes.ImageFieldsAPIV2 }; } case 'object': { if (docField.localized) { const locales = _.reduce( docField.locales, (acc, cur) => { const { fields, getPreview, locale } = cur; acc[locale] = { ...cur, srcObjectLabel: getPreview({ delegate, locale }).previewTitle ?? (isListItem ? 'Item' : 'Object'), fields: toAPIV2Fields({ docFields: fields, delegate, contentSourceDataById, contentSourceId, allowedLocales }) }; return acc; }, {} as ContentStoreTypes.DocumentObjectFieldLocalizedAPIV2['locales'] ); return { ...docField, locales }; } // TODO: what about unset locales? what if there is an action that generates content for an empty object? if (docField.isUnset) { return docField; } const { fields, getPreview, ...rest } = docField; return { ...rest, srcObjectLabel: getPreview({ delegate }).previewTitle ?? (isListItem ? 'Item' : 'Object'), fields: toAPIV2Fields({ docFields: fields, delegate, contentSourceDataById, contentSourceId, allowedLocales }) }; } case 'model': { // TODO: merge with object if (docField.localized) { const locales = _.reduce( docField.locales, (acc, cur) => { const { fields, getPreview, locale } = cur; acc[locale] = { ...cur, srcObjectLabel: getPreview({ delegate, locale }).previewTitle, fields: toAPIV2Fields({ docFields: fields, delegate, contentSourceDataById, contentSourceId, allowedLocales }) }; return acc; }, {} as ContentStoreTypes.DocumentModelFieldLocalizedAPIV2['locales'] ); return { ...docField, locales }; } // TODO: what about unset locales? what if there is an action that generates content for an empty object? if (docField.isUnset) { return docField; } const { fields, getPreview, ...rest } = docField; return { ...rest, srcObjectLabel: getPreview({ delegate }).previewTitle, fields: toAPIV2Fields({ docFields: fields, delegate, contentSourceDataById, contentSourceId, allowedLocales }) }; } case 'reference': { if (docField.localized) { const refField = docField; const locales = _.reduce( docField.locales, (acc, cur, locale) => { acc[locale] = { ...cur, ...populateReferenceLabels({ docField: refField, contentSourceDataById, contentSourceId, locale: locale, delegate }) }; return acc; }, {} as ContentStoreTypes.DocumentReferenceFieldLocalizedAPIV2['locales'] ); return { ...docField, locales }; } return { ...docField, ...populateReferenceLabels({ docField, contentSourceDataById, contentSourceId, delegate }) }; } case 'cross-reference': { // todo: unify with reference using helper methods if (docField.localized) { const refField = docField; const locales = _.reduce( docField.locales, (acc, cur, locale) => { acc[locale] = { ...cur, ...populateReferenceLabels({ docField: refField, contentSourceDataById, contentSourceId, locale: locale, delegate }) }; return acc; }, {} as ContentStoreTypes.DocumentCrossReferenceFieldLocalizedAPIV2['locales'] ); return { ...docField, locales }; } return { ...docField, ...populateReferenceLabels({ docField, contentSourceDataById, contentSourceId, delegate }) }; } case 'list': { if (docField.localized) { const locales = _.reduce( docField.locales, (acc, cur, locale) => { acc[locale] = { ...cur, items: (cur.items ?? []).map( (field) => toAPIV2Field({ docField: field, contentSourceDataById, contentSourceId, delegate, allowedLocales, isListItem: true }) as ContentStoreTypes.DocumentListFieldItemsAPIV2 ) }; return acc; }, {} as ContentStoreTypes.DocumentListFieldLocalizedAPIV2['locales'] ); return { ...docField, locales }; } const { items, ...rest } = docField; return { ...rest, items: items.map( (field) => toAPIV2Field({ docField: field, delegate, contentSourceDataById, contentSourceId, allowedLocales, isListItem: true }) as ContentStoreTypes.DocumentListFieldItemsAPIV2 ) }; } default: { const _exhaustiveCheck: never = docField; console.error(`toLocalizedAPIField _exhaustiveCheck failed, docField.type: ${docField['type']}`); return _exhaustiveCheck; } } } function populateReferenceLabels({ docField, locale, contentSourceDataById, contentSourceId, delegate }: { docField: ContentStoreTypes.DocumentReferenceField | ContentStoreTypes.DocumentCrossReferenceField; locale?: string; contentSourceDataById: Record; contentSourceId: string; delegate: CSITypes.ConfigDelegate; }) { const refField = getDocumentFieldForLocale(docField, locale); if (!refField || refField?.isUnset) { return {}; } if (refField.type === 'reference') { if (refField.refType === 'document') { const document = contentSourceDataById[contentSourceId]?.documentMap[refField.refId]; if (document) { return { srcObjectLabel: document?.getPreview({ delegate }).previewTitle, srcModelLabel: document?.srcModelLabel, isMissing: false }; } } else if (refField.refType === 'asset') { const asset = contentSourceDataById[contentSourceId]?.assetMap[refField.refId]; if (asset) { return { srcObjectLabel: asset.srcObjectLabel, srcModelLabel: asset.srcModelLabel, isMissing: false }; } } } else if (refField?.type === 'cross-reference') { const srcId = getContentSourceId(refField.refSrcType, refField.refProjectId); if (refField.refType === 'document') { const document = contentSourceDataById[srcId]?.documentMap[refField.refId]; if (document) { return { refObjectLabel: document?.getPreview({ delegate }).previewTitle, refModelLabel: document?.srcModelLabel, refModelName: document?.srcModelName, isMissing: false }; } } else if (refField.refType === 'asset') { const asset = contentSourceDataById[srcId]?.assetMap[refField.refId]; if (asset) { return { refObjectLabel: asset.srcObjectLabel, refModelLabel: asset.srcModelLabel, refModelName: asset?.srcModelName, isMissing: false }; } } } return { isMissing: true }; }