UNPKG

scrivito

Version:

Scrivito is a professional, yet easy to use SaaS Enterprise Content Management Service, built for digital agencies and medium to large businesses. It is completely maintenance-free, cost-effective, and has unprecedented performance and security.

295 lines (248 loc) 8.34 kB
// @rewire import difference from 'lodash-es/difference'; import isEmpty from 'lodash-es/isEmpty'; import isEqual from 'lodash-es/isEqual'; import { ObjJson, WidgetJson, WidgetPoolJson } from 'scrivito_sdk/client'; import { isSystemAttribute } from 'scrivito_sdk/common'; import { REMOVE_THIS_KEY, RemoveThisKey, isRemoveThisKey, } from 'scrivito_sdk/data/remove_this_key'; // "interface extends type" is a way to circumvent "use a mapped type instead" // See https://github.com/microsoft/TypeScript/issues/24220#issuecomment-5737169810 export interface ObjJsonPatch extends _ObjJsonPatch { _widget_pool?: WidgetPoolJsonPatch; } type _ObjJsonPatch = { [attrName in Exclude<keyof ObjJson, '_widget_pool'>]?: | ObjJson[attrName] | RemoveThisKey | WidgetPoolJsonPatch; // sorry, typescript index signature requirement }; export interface WidgetPoolJsonPatch { [key: string]: WidgetJsonPatch | RemoveThisKey | null | undefined; } export interface WidgetJsonPatch extends _WidgetJsonPatch {} type _WidgetJsonPatch = { [attrName in keyof WidgetJson]?: WidgetJson[attrName] | RemoveThisKey; }; export function patchObjJson( primitiveObj: ObjJson, patch: ObjJsonPatch ): // Usually, required keys are not removed, therefore the same flavour of ObjJson is returned. // If required keys were removed, then the patch hopefully adds all required keys for a new // flavour of ObjJson. To express that in a way that typescript believes it automatically is // likely difficult / complex. ObjJson; export function patchObjJson( primitiveObj: null, patch: ObjJsonPatch ): ObjJsonPatch; export function patchObjJson( primitiveObj: undefined, patch: ObjJsonPatch ): ObjJsonPatch; export function patchObjJson( primitiveObj: ObjJson | null | undefined, patch: ObjJsonPatch ): ObjJsonPatch { if (!primitiveObj) return patch; return patchJson(primitiveObj, patch); } export function diffObjJson( fromObjJson: ObjJson | null | undefined, toObjJson: ObjJson ): ObjJsonPatch { if (!fromObjJson) return toObjJson; return diffJson(fromObjJson, toObjJson); } export function diffWidgetJson( fromWidgetJson: WidgetJson, toWidgetJson: WidgetJson ): WidgetJsonPatch { return diffJson(fromWidgetJson, toWidgetJson); } function diffMaybeWidgetJson( fromWidgetJson: WidgetJson | undefined, toWidgetJson: WidgetJson | undefined ): WidgetJsonPatch | null { if (!fromWidgetJson) return toWidgetJson || null; if (!toWidgetJson) return null; return diffJson(fromWidgetJson, toWidgetJson); } export function hasObjContentDiff( objJsonA: ObjJson, objJsonB: ObjJson ): boolean { const patch = diffObjJson(objJsonA, objJsonB); return Object.keys(patch).some(isObjContentAttributeName); } export function hasWidgetContentDiff( widgetJsonBefore: WidgetJson, widgetJsonAfter: WidgetJson ): boolean { const patch = diffWidgetJson(widgetJsonBefore, widgetJsonAfter); return Object.keys(patch).some(isContentAttributeName); } function isObjContentAttributeName(attributeName: string) { return ( isContentAttributeName(attributeName) || attributeName === '_path' || attributeName === '_permalink' ); } function isContentAttributeName(attributeName: string) { return !isSystemAttribute(attributeName) || attributeName === '_obj_class'; } function eachKeyFrom<T1, T2>( objectA: { [key: string]: T1 | undefined }, objectB: { [key: string]: T2 | undefined }, handler: ( key: string, valueInA: T1 | undefined, valueInB: T2 | undefined, isAttributeOfPatch: boolean ) => void ) { const keysOfA = Object.keys(objectA); const keysOfB = Object.keys(objectB); const keysOfAOnly = difference(keysOfA, keysOfB); keysOfAOnly.forEach((key) => { handler(key, objectA[key], objectB[key], false); }); keysOfB.forEach((key) => { handler(key, objectA[key], objectB[key], true); }); } function buildUpdatedWidgetPool( widgetPool: WidgetPoolJson, widgetPoolPatch: WidgetPoolJsonPatch | undefined ): WidgetPoolJson { if (!widgetPoolPatch || isEmpty(widgetPoolPatch)) return widgetPool; const updatedWidgetPool: WidgetPoolJson = {}; eachKeyFrom( widgetPool, widgetPoolPatch, (id, widget, widgetPatch, isKeyOfWidgetPoolPatch) => { if (isKeyOfWidgetPoolPatch) { if (isRemoveThisKey(widgetPatch)) return; if (widgetPatch) { if (widget) { updatedWidgetPool[id] = patchWidgetJson(widget, widgetPatch); } else { if (isWidgetJson(widgetPatch)) { updatedWidgetPool[id] = widgetPatch; } } } } else { updatedWidgetPool[id] = widget; } } ); return updatedWidgetPool; } function buildPatchEntry<T, Resolution>( valueA: T | undefined, valueB: T | undefined, fnHandleBoth: () => Resolution ): T | RemoveThisKey | Resolution { // Note: the return type `undefined` is actually impossible, // because `valueA` and `valueB` are not both undefined if (valueA === undefined) return valueB!; if (valueB === undefined) return REMOVE_THIS_KEY; return fnHandleBoth(); } function buildWidgetPoolPatch( widgetPoolA: WidgetPoolJson | undefined, widgetPoolB: WidgetPoolJson | undefined ): WidgetPoolJsonPatch | undefined { if (widgetPoolA === widgetPoolB) return undefined; if (!widgetPoolA) return widgetPoolB; const patch: WidgetPoolJsonPatch = {}; eachKeyFrom(widgetPoolA, widgetPoolB || {}, (widgetId, widgetA, widgetB) => { const widgetValue = buildPatchEntry(widgetA, widgetB, () => { const widgetPatch = diffMaybeWidgetJson(widgetA, widgetB); if (!isEmpty(widgetPatch)) return widgetPatch; }); if (widgetValue !== undefined) { patch[widgetId] = widgetValue; } }); return patch; } function diffJson(fromJson: ObjJson, toJson: ObjJson): ObjJsonPatch; function diffJson(fromJson: WidgetJson, toJson: WidgetJson): WidgetJsonPatch; function diffJson( fromJson: ObjJson | WidgetJson, toJson: ObjJson | WidgetJson ): ObjJsonPatch | WidgetJsonPatch { const patch: ObjJsonPatch | WidgetJsonPatch = {}; eachKeyFrom(fromJson, toJson, (attribute, valueInA, valueInB) => { if (attribute === '_widget_pool') { const widgetPoolPatch = buildWidgetPoolPatch( valueInA as WidgetPoolJson | undefined, valueInB as WidgetPoolJson | undefined ); if (!isEmpty(widgetPoolPatch)) { patch._widget_pool = widgetPoolPatch; } } else { const patchValue = buildPatchEntry(valueInA, valueInB, () => { if (!isEqual(valueInA, valueInB)) return valueInB; }); if (patchValue !== undefined) { patch[attribute] = patchValue; } } }); return patch; } function patchWidgetJson( primitiveWidget: WidgetJson, patch: WidgetJsonPatch ): WidgetJson { return patchJson(primitiveWidget, patch); } function patchJson(primitiveObj: ObjJson, patch: ObjJsonPatch): ObjJson; function patchJson( primitiveObj: WidgetJson, patch: WidgetJsonPatch ): WidgetJson; function patchJson( primitiveObj: ObjJson | WidgetJson, patch: ObjJsonPatch | WidgetJsonPatch ) { const updatedPrimitiveObj: Partial<ObjJson> | Partial<WidgetJson> = {}; eachKeyFrom( primitiveObj, patch, (attribute, objValue, patchValue, isAttributeOfPatch) => { if (attribute === '_widget_pool') { updatedPrimitiveObj._widget_pool = buildUpdatedWidgetPool( (objValue as WidgetPoolJson | null | undefined) || {}, patchValue as WidgetPoolJson | undefined ); } else if (isAttributeOfPatch) { const simplePatchValue = patchValue as Exclude< typeof patchValue, WidgetPoolJsonPatch >; if (isPatchValueToKeep(simplePatchValue)) { updatedPrimitiveObj[attribute] = simplePatchValue; } } else { updatedPrimitiveObj[attribute] = objValue; } } ); return updatedPrimitiveObj; } function isPatchValueToKeep<T>(v: T | RemoveThisKey | undefined): v is T { return v !== undefined && !isRemoveThisKey(v); } // This is a minor cheat: we assume that `widgetPatch` does not include any `RemoveThisKey` values function isWidgetJson(widgetPatch: WidgetJsonPatch): widgetPatch is WidgetJson { return typeof widgetPatch._obj_class === 'string'; }