import { isArray, isEmptyObject, isMap, isPlainObject, isPrimitive, isSet, } from './is.js'; import { escapeKey, stringifyPath } from './pathstringifier.js'; import { isInstanceOfRegisteredClass, transformValue, untransformValue, } from './transformer.js'; import { includes, forEach } from './util.js'; import { parsePath } from './pathstringifier.js'; import { getDeep, setDeep } from './accessDeep.js'; function traverse(tree, walker, origin = []) { if (!tree) { return; } if (!isArray(tree)) { forEach(tree, (subtree, key) => traverse(subtree, walker, [...origin, ...parsePath(key)])); return; } const [nodeValue, children] = tree; if (children) { forEach(children, (child, key) => { traverse(child, walker, [...origin, ...parsePath(key)]); }); } walker(nodeValue, origin); } export function applyValueAnnotations(plain, annotations, superJson) { traverse(annotations, (type, path) => { plain = setDeep(plain, path, v => untransformValue(v, type, superJson)); }); return plain; } export function applyReferentialEqualityAnnotations(plain, annotations) { function apply(identicalPaths, path) { const object = getDeep(plain, parsePath(path)); identicalPaths.map(parsePath).forEach(identicalObjectPath => { plain = setDeep(plain, identicalObjectPath, () => object); }); } if (isArray(annotations)) { const [root, other] = annotations; root.forEach(identicalPath => { plain = setDeep(plain, parsePath(identicalPath), () => plain); }); if (other) { forEach(other, apply); } } else { forEach(annotations, apply); } return plain; } const isDeep = (object, superJson) => isPlainObject(object) || isArray(object) || isMap(object) || isSet(object) || isInstanceOfRegisteredClass(object, superJson); function addIdentity(object, path, identities) { const existingSet = identities.get(object); if (existingSet) { existingSet.push(path); } else { identities.set(object, [path]); } } export function generateReferentialEqualityAnnotations(identitites, dedupe) { const result = {}; let rootEqualityPaths = undefined; identitites.forEach(paths => { if (paths.length <= 1) { return; } // if we're not deduping, all of these objects continue existing. // putting the shortest path first makes it easier to parse for humans // if we're deduping though, only the first entry will still exist, so we can't do this optimisation. if (!dedupe) { paths = paths .map(path => path.map(String)) .sort((a, b) => a.length - b.length); } const [representativePath, ...identicalPaths] = paths; if (representativePath.length === 0) { rootEqualityPaths = identicalPaths.map(stringifyPath); } else { result[stringifyPath(representativePath)] = identicalPaths.map(stringifyPath); } }); if (rootEqualityPaths) { if (isEmptyObject(result)) { return [rootEqualityPaths]; } else { return [rootEqualityPaths, result]; } } else { return isEmptyObject(result) ? undefined : result; } } export const walker = (object, identities, superJson, dedupe, path = [], objectsInThisPath = [], seenObjects = new Map()) => { const primitive = isPrimitive(object); if (!primitive) { addIdentity(object, path, identities); const seen = seenObjects.get(object); if (seen) { // short-circuit result if we've seen this object before return dedupe ? { transformedValue: null, } : seen; } } if (!isDeep(object, superJson)) { const transformed = transformValue(object, superJson); const result = transformed ? { transformedValue: transformed.value, annotations: [transformed.type], } : { transformedValue: object, }; if (!primitive) { seenObjects.set(object, result); } return result; } if (includes(objectsInThisPath, object)) { // prevent circular references return { transformedValue: null, }; } const transformationResult = transformValue(object, superJson); const transformed = transformationResult?.value ?? object; const transformedValue = isArray(transformed) ? [] : {}; const innerAnnotations = {}; forEach(transformed, (value, index) => { if (index === '__proto__' || index === 'constructor' || index === 'prototype') { throw new Error(`Detected property ${index}. This is a prototype pollution risk, please remove it from your object.`); } const recursiveResult = walker(value, identities, superJson, dedupe, [...path, index], [...objectsInThisPath, object], seenObjects); transformedValue[index] = recursiveResult.transformedValue; if (isArray(recursiveResult.annotations)) { innerAnnotations[index] = recursiveResult.annotations; } else if (isPlainObject(recursiveResult.annotations)) { forEach(recursiveResult.annotations, (tree, key) => { innerAnnotations[escapeKey(index) + '.' + key] = tree; }); } }); const result = isEmptyObject(innerAnnotations) ? { transformedValue, annotations: !!transformationResult ? [transformationResult.type] : undefined, } : { transformedValue, annotations: !!transformationResult ? [transformationResult.type, innerAnnotations] : innerAnnotations, }; if (!primitive) { seenObjects.set(object, result); } return result; }; //# sourceMappingURL=plainer.js.map