import _ from 'lodash'; import * as ContentStoreTypes from '../types'; import { getContentSourceId, getDocumentFieldForLocale } from '../content-store-utils'; import { IMAGE_MODEL } from '../common/common-schema'; export function mergeObjectWithDocument({ object, document, locale, contentSourceId, contentSourceDataById, seenReferences = [], referenceBehavior, duplicatableModels, nonDuplicatableModels }: { object: Record | undefined; document: ContentStoreTypes.Document; locale?: string; contentSourceId: string; contentSourceDataById: Record; seenReferences?: string[]; referenceBehavior?: 'copyReference' | 'duplicateContents'; duplicatableModels?: string[]; nonDuplicatableModels?: string[]; }): Record { return mergeObjectWithDocumentFields({ object, documentFields: document.fields, locale, contentSourceId, contentSourceDataById, seenReferences: seenReferences.concat(document.srcObjectId), referenceBehavior, duplicatableModels, nonDuplicatableModels }); } type Context = { locale?: string; contentSourceId: string; contentSourceDataById: Record; seenReferences: string[]; referenceBehavior?: 'copyReference' | 'duplicateContents'; duplicatableModels?: string[]; nonDuplicatableModels?: string[]; }; function mergeObjectWithDocumentFields({ object, documentFields, ...context }: { object: Record | undefined; documentFields: Record; } & Context): Record { return _.reduce( documentFields, (object, documentField, fieldName) => { const value = mergeObjectWithDocumentField({ value: object[fieldName], documentField, ...context }); if (typeof value !== 'undefined') { object[fieldName] = value; } else { // make sure we keep field empty and don't use default value object[fieldName] = null; } return object; }, Object.assign({}, object) ); } function mergeObjectWithDocumentField({ value, documentField, ...context }: { value: unknown; documentField: ContentStoreTypes.DocumentField; } & Context): unknown { const locale = context.locale; switch (documentField.type) { case 'string': case 'text': case 'html': case 'slug': case 'url': case 'color': case 'boolean': case 'number': case 'date': case 'datetime': case 'enum': case 'file': case 'json': case 'style': case 'markdown': case 'richText': { // if a value was provided explicitly, use it to override the value // of the matching field in the duplicated document. if (typeof value !== 'undefined') { return value; } const localizedField = getDocumentFieldForLocale(documentField, locale); if (localizedField) { return localizedField.value; } break; } case 'image': { // if an image value was provided explicitly, use it to override the image // of the matching field in the duplicated document. if (typeof value !== 'undefined') { return value; } const localizedField = getDocumentFieldForLocale(documentField, locale); if (localizedField && !localizedField.isUnset) { if (localizedField?.sourceData) { return localizedField?.sourceData; } const localizedUrl = getDocumentFieldForLocale(localizedField.fields.url, locale); return localizedUrl?.value; } break; } case 'object': { const localizedField = getDocumentFieldForLocale(documentField, locale); if (localizedField && !localizedField.isUnset && isPlainObjectOrUndefined(value)) { return mergeObjectWithDocumentFields({ object: value, documentFields: localizedField.fields, ...context }); } break; } case 'model': { const localizedField = getDocumentFieldForLocale(documentField, locale); if (localizedField && !localizedField.isUnset && isPlainObjectOrUndefined(value)) { if (value && value.$$type !== localizedField.srcModelName) { // if the override object's $$type isn't equal to the type // of the current object in the field, then use whatever // was passed in the object. break; } return { $$type: localizedField.srcModelName, ...mergeObjectWithDocumentFields({ object: value, documentFields: localizedField.fields, ...context }) }; } break; } case 'reference': { const localizedField = getDocumentFieldForLocale(documentField, locale); if (localizedField && !localizedField.isUnset && isPlainObjectOrUndefined(value)) { if (value && value.$$ref) { // if the override object has $$ref, use it break; } if (localizedField.refType === 'asset') { // assets always duplicated by reference return { $$ref: localizedField.refId }; } else { const contentSourceData = context.contentSourceDataById[context.contentSourceId]; if (!contentSourceData) { throw new Error(`Content source data not found. Source: '${context.contentSourceId}'.`); } const document = contentSourceData.documentMap[localizedField.refId]; if (!document) { break; } const shouldDuplicateDocument = shouldDuplicate({ referenceField: localizedField, modelName: document.srcModelName, seenReferences: context.seenReferences, referenceBehavior: context.referenceBehavior, duplicatableModels: context.duplicatableModels, nonDuplicatableModels: context.nonDuplicatableModels }); if (shouldDuplicateDocument || (value && value.$$type)) { if (value && value.$$type !== document.srcModelName) { // if the override object has $$type different from // the type of the document that is currently // referenced in the field, then create and link a // new document according to the provided object break; } return { $$type: document.srcModelName, ...mergeObjectWithDocument({ object: value, document, locale: context.locale, contentSourceId: context.contentSourceId, contentSourceDataById: context.contentSourceDataById, seenReferences: context.seenReferences.concat(document.srcObjectId), referenceBehavior: context.referenceBehavior, duplicatableModels: context.duplicatableModels, nonDuplicatableModels: context.nonDuplicatableModels }) }; } else { return { $$ref: localizedField.refId }; } } } break; } case 'cross-reference': { const localizedField = getDocumentFieldForLocale(documentField, locale); if (localizedField && !localizedField.isUnset && isPlainObjectOrUndefined(value)) { if (value && value.$$ref) { // if the override object has $$ref, use it break; } if (localizedField.refType === 'asset') { // assets always duplicated by reference return { $$ref: localizedField.refId, $$refSrcType: localizedField.refSrcType, $$refProjectId: localizedField.refProjectId }; } else { const contentSourceId = getContentSourceId(localizedField.refSrcType, localizedField.refProjectId); const contentSourceData = context.contentSourceDataById[contentSourceId]; if (!contentSourceData) { throw new Error(`Content source data not found. Source: '${context.contentSourceId}'.`); } const document = contentSourceData.documentMap[localizedField.refId]; if (!document) { break; } const shouldDuplicateDocument = shouldDuplicate({ referenceField: localizedField, modelName: document.srcModelName, seenReferences: context.seenReferences, referenceBehavior: context.referenceBehavior, duplicatableModels: context.duplicatableModels, nonDuplicatableModels: context.nonDuplicatableModels }); if (shouldDuplicateDocument || (value && value.$$type)) { if ( value && (value.$$type !== document.srcModelName || value.$$refSrcType !== document.srcType || value.$$refProjectId !== document.srcProjectId) ) { // if the override object has $$type different from // the type of the document that is currently // referenced in the field, then create and link a // new document according to the provided object break; } return { $$type: document.srcModelName, $$refSrcType: document.srcType, $$refProjectId: document.srcProjectId, ...mergeObjectWithDocument({ object: value, document, locale: context.locale, contentSourceId: context.contentSourceId, contentSourceDataById: context.contentSourceDataById, seenReferences: context.seenReferences.concat(document.srcObjectId), referenceBehavior: context.referenceBehavior, duplicatableModels: context.duplicatableModels, nonDuplicatableModels: context.nonDuplicatableModels }) }; } else { return { $$ref: localizedField.refId, $$refSrcType: localizedField.refSrcType, $$refProjectId: localizedField.refProjectId }; } } } break; } case 'list': { const localizedField = getDocumentFieldForLocale(documentField, locale); if (value) { // do not try to merge provided list items with existing items // because array items can not be matched by names like fields do. break; } if (localizedField) { return (localizedField.items ?? []) .map((field) => mergeObjectWithDocumentField({ value: undefined, documentField: field, ...context }) ) .filter((value) => typeof value !== 'undefined'); // if locale passed, it may return undefined for items localized to a different locale; } break; } default: { const _exhaustiveCheck: never = documentField; return _exhaustiveCheck; } } // if the document value was not set and no merging occurred use the value // that was provided to override the field in the duplicated document return value; } function isPlainObjectOrUndefined(value: unknown): value is Record | undefined { return typeof value === 'undefined' || _.isPlainObject(value); } function shouldDuplicate({ referenceField, modelName, seenReferences, referenceBehavior, nonDuplicatableModels, duplicatableModels }: { referenceField: { refId: string; isUnset?: false }; modelName: string; seenReferences: string[]; referenceBehavior?: 'copyReference' | 'duplicateContents'; nonDuplicatableModels?: string[]; duplicatableModels?: string[]; }) { return ( !seenReferences.includes(referenceField.refId) && modelName !== IMAGE_MODEL.name && ((referenceBehavior === 'duplicateContents' && !(nonDuplicatableModels ?? []).includes(modelName)) || (referenceBehavior === 'copyReference' && (duplicatableModels ?? []).includes(modelName))) ); }