Skip to content

Instantly share code, notes, and snippets.

@sykire
Created May 16, 2023 17:41
Show Gist options
  • Save sykire/0e8649baf06ffe422ed8d6ce24f8e608 to your computer and use it in GitHub Desktop.
Save sykire/0e8649baf06ffe422ed8d6ce24f8e608 to your computer and use it in GitHub Desktop.
export const DELETE = Symbol('DELETE');
/**
* Traverse an object and apply a transformation function to each value.
* The transformation function is called with the value, the key and the key path.
*
* The transformation function can return a new value or the same value.
* If the transformation function returns a new value, the value in the object is replaced with the new value.
*
* The transformation function can return an object.
* If the transformation function returns an object, the object is traversed as well.
*
* Warning: The transformation could loop infinitely if it always returns an object.
*
* @param obj { object } - the object to traverse
* @param transform { (value: unknown, key: string, keyPath: string) => unknown } - a function that transforms the value, the key and the key path
*/
export async function traverse(obj, transform) {
/**
* @type { object[] }
*/
const objectStack = [obj];
/**
* @type { string[] }
*/
const keysStack = []
while (objectStack.length > 0) {
const obj = objectStack.pop();
if (obj) {
for (const key in obj) {
keysStack.push(key);
const keyPath = keysStack.join('.');
const value = obj[key];
const transformed = transform(value, key, keyPath);
if (transformed === DELETE) {
delete obj[key];
keysStack.pop();
continue;
}
if (transformed !== value) {
obj[key] = transformed;
}
if (typeof transformed === 'object' && transformed !== null) {
objectStack.push(transformed);
} else {
keysStack.pop();
}
}
}
}
}
/**
* Traverse an object and apply a transformation function to each value.
* The transformation function is called with the value, the key and the key path.
*
* The transformation function can return a new value or the same value.
* If the transformation function returns a new value, the value in the object is replaced with the new value.
*
* The transformation function can return an object.
* If the transformation function returns an object, the object is traversed as well.
*
* The transformation function can return a promise.
* If the transformation function returns a promise, the promise is awaited and the resolved value is used.
*
* Warning: The transformation could loop infinitely if it always returns an object.
*
* @param obj { object } - the object to traverse
* @param transform { (value: unknown, key: string, keyPath: string) => unknown | Promise<unknown> } - a function that transforms the value, the key and the key path
* @returns
**/
export async function traverseAsync(obj, transform) {
/**
* @type { object[] }
*/
const objectStack = [obj];
/**
* @type { string[] }
*/
const keysStack = []
while (objectStack.length > 0) {
const obj = objectStack.pop();
if (obj) {
for (const key in obj) {
keysStack.push(key);
const keyPath = keysStack.join('.');
const value = obj[key];
const awaitedValue = await value;
const transformed = await transform(awaitedValue, key, keyPath);
if (transformed === DELETE) {
delete obj[key];
keysStack.pop();
continue;
}
if (value !== transformed) {
obj[key] = transformed;
}
if (typeof transformed === 'object' && transformed !== null) {
objectStack.push(transformed);
} else {
keysStack.pop();
}
}
}
}
}
const obj = { a: 1, b: { c: [{ a: 2 }, { d: 20 }] } }
traverse(obj, (value, key, keyPath) => {
if (keyPath === 'a') {
return DELETE;
}
return value
})
obj
const asyncObj = { a: 1, b: { c: Promise.resolve([{ a: 2 }]) } }
traverseAsync(asyncObj, (value, key, keyPath) => {
if (keyPath === 'a') {
return DELETE;
}
return value
}).then(() => {
asyncObj
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment