@paperbits/common
Version:
Paperbits common components.
297 lines (240 loc) • 9.61 kB
text/typescript
export function isObject(item: any): boolean {
return item !== undefined && (item && typeof item === "object" && !Array.isArray(item));
}
export function getObjectAt<T>(path: string, source: object, delimiter: string = "/"): T {
const segments = path.split(delimiter);
let segmentObject = source;
for (const segment of segments) {
segmentObject = segmentObject[segment];
if (segmentObject === null || segmentObject === undefined) {
return <any>segmentObject;
}
}
return <any>segmentObject;
}
/**
* Applies changes to a target and returns reverse changeset that can be used to rollback changes.
* @param target {object} Object to which changes are being applied.
* @param changeset {object} Object that needs to be merged into targt object.
* @param removeNulls {boolean} Indicates that properties with "null" values should be removed.
* @returns {object} Reverse changeset.
*/
export function mergeDeep(target: Object, changeset: Object, removeNulls: boolean = false): Object {
const reverseChangeset = {};
if (isObject(target) && isObject(changeset)) {
Object.keys(changeset).forEach(key => {
const sourceProperty = changeset[key];
if (isObject(sourceProperty)) {
if (target[key] !== undefined && target[key] !== null) {
reverseChangeset[key] = mergeDeep(target[key], sourceProperty, removeNulls);
}
else { // if not present in target, safely assign whole source.
if (target[key] !== sourceProperty) {
reverseChangeset[key] = target[key] || null;
target[key] = sourceProperty;
if (removeNulls) {
if (target[key] === null || target[key] === undefined) {
delete target[key];
}
}
}
}
}
else { // if value or array, just assign as is.
if (target[key] !== sourceProperty) {
reverseChangeset[key] = target[key] || null;
target[key] = sourceProperty;
if (removeNulls) {
if (target[key] === null || target[key] === undefined) {
delete target[key];
}
}
}
}
});
}
return reverseChangeset;
}
export function mergeDeepAt(path: string, target: any, source: any, removeNulls: boolean = false): void {
if (Array.isArray(source)) {
setValueWithCompensation(path, target, source);
}
else {
const updatingObject = setStructure(path, target);
mergeDeep(updatingObject, source, removeNulls);
}
}
export function setStructure(path: string, target: object, delimiter: string = "/"): object {
const segments = path.split(delimiter);
let segmentObject = target;
segments.forEach(segment => {
if (!segmentObject[segment]) {
segmentObject[segment] = {};
}
segmentObject = segmentObject[segment];
});
return segmentObject;
}
export interface CleanupOptions {
/**
* If the node property value is null, the property gets removed from the node.
*/
removeNulls?: boolean;
/**
* If all child properties of the node are null, the node itself becomes null.
*/
collapseNulls?: boolean;
/**
* If the node property is an empty string, the property gets removed from the node.
*/
removeEmptyString?: boolean;
}
/**
* Removes all properties with `undefined` value from object.
* @param source {object} An object that needs to be cleaned.
*/
export function cleanupObject(source: object, options?: CleanupOptions): void {
if (options?.removeNulls && options?.collapseNulls) {
throw new Error(`Cannot remove and collapse nulls at the same time.`);
}
if (!(source instanceof Object)) {
return;
}
Object.keys(source).forEach(key => {
const childNode = source[key];
if (Array.isArray(childNode)) {
childNode.forEach(x => cleanupObject(x, options));
if (childNode.length === 0) {
source[key] = null;
if (options?.removeNulls) {
delete source[key];
}
}
}
else if (childNode instanceof Object) {
if (childNode === source) {
console.log(`Ciruclar reference: `, source);
throw new Error(`Ciruclar reference detected. See the message above.`);
}
cleanupObject(childNode, options);
if (Object.keys(childNode).length === 0) {
delete source[key];
}
else if (options?.collapseNulls) {
const hasNonNullProps = Object.values(childNode).some(x => x !== null);
if (!hasNonNullProps) { // collapse empty nodes
source[key] = null;
}
}
}
else if (childNode === undefined || (options?.removeNulls && childNode === null) || (options?.removeEmptyString && childNode === "")) {
delete source[key];
}
});
}
/**
* Sets value in an object tree and returns compensation tree.
* @param path Object tree path.
* @param target Object tree root.
* @param value
*/
export function setValueWithCompensation(path: string, target: object, value: any): object {
const original: any = clone(target);
const compensation = getObjectAt(path, original);
setValue(path, target, value);
return <any>compensation;
}
/**
* Sets value in an object tree.
* @param path Object tree path.
* @param target Object tree root.
* @param value
*/
export function setValue(path: string, target: object, value: any): void {
const segments = path.split("/");
let segmentObject = target;
for (let i = 0; i < segments.length - 1; i++) {
const segment = segments[i];
if (!segmentObject[segment]) {
segmentObject[segment] = {};
}
segmentObject = segmentObject[segment];
}
segmentObject[segments[segments.length - 1]] = value;
}
export function deleteNodeAt(path: string, target: object): void {
const segments = path.split("/");
let segmentObject = target;
for (let i = 0; i < segments.length - 1; i++) {
const segment = segments[i];
if (!segmentObject[segment]) {
segmentObject[segment] = {};
}
segmentObject = segmentObject[segment];
}
delete segmentObject[segments[segments.length - 1]];
// TODO: Try to delete entire trail, if empty
}
export function clone<T>(obj: Object): T {
return <T>JSON.parse(JSON.stringify(obj));
}
export function findObjects(source: object, predicate: (node: object) => boolean): object[] {
let result = [];
const keys = Object.keys(source);
for (const key of keys) {
const node = source[key];
if (predicate(node)) {
result.push(node);
}
else {
const childResult = findObjects(node, predicate);
result = result.concat(childResult);
}
}
return result;
}
export function isEmpty(source: object): boolean {
return !source || Object.keys(source).length === 0;
}
export function deepFreeze(obj: object): void {
const propNames = Object.getOwnPropertyNames(obj);
for (const name of propNames) {
const value = obj[name];
if (value && typeof value === "object") {
this.deepFreeze(value);
}
}
Object.freeze(obj);
}
export function generateChangeset(target: Object, source: Object, collapseEmptyNodes: boolean = false): Object {
const changeset = {};
if (!isObject(target) || !isObject(source)) {
return changeset;
}
Object.keys(source).forEach(key => {
const sourceProperty = source[key];
const targetProperty = target[key];
if (isObject(sourceProperty)) {
if (targetProperty !== undefined && targetProperty !== null) {
changeset[key] = generateChangeset(targetProperty, sourceProperty);
if (collapseEmptyNodes && !Object.values(changeset[key]).some(x => x !== null)) { // collapse empty nodes empty nodes
changeset[key] = null;
}
}
else if (targetProperty !== sourceProperty && sourceProperty !== undefined) { // if not present in target, safely assign whole source.
changeset[key] = sourceProperty;
}
}
else if (targetProperty !== sourceProperty && sourceProperty !== undefined) { // if value or array, just assign as is.
changeset[key] = sourceProperty || null;
}
});
Object.keys(target).forEach(key => {
const targetProperty = target[key];
const sourceProperty = source[key];
if (targetProperty !== undefined && (sourceProperty === undefined || sourceProperty === null)) {
changeset[key] = null;
}
});
return changeset;
}