mirror of
https://github.com/Warky-Devs/artemis-kit.git
synced 2025-05-19 03:37:30 +00:00
Added findObjectPath, findObjectByKeyValues
This commit is contained in:
parent
57129e46d5
commit
de50af7446
@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { getNestedValue, setNestedValue } from "./nested";
|
import { findObjectByKeyValues, findObjectPath, getNestedValue, setNestedValue } from "./nested";
|
||||||
|
|
||||||
describe("getNestedValue", () => {
|
describe("getNestedValue", () => {
|
||||||
const testObj = {
|
const testObj = {
|
||||||
@ -170,3 +170,178 @@ describe("setNestedValue", () => {
|
|||||||
expect(obj.list[0]).toBe("item");
|
expect(obj.list[0]).toBe("item");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe("findObjectPath", () => {
|
||||||
|
const testObj = {
|
||||||
|
user: {
|
||||||
|
name: "John",
|
||||||
|
id: 1,
|
||||||
|
contacts: [
|
||||||
|
{ id: 101, email: "john@example.com", phone: "123-456" },
|
||||||
|
{ id: 102, email: "john.alt@example.com", phone: "789-012" },
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
id: 201,
|
||||||
|
notifications: {
|
||||||
|
email: true,
|
||||||
|
push: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
created: "2023-01-01",
|
||||||
|
tags: ["important", "user"],
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ id: 1, name: "Item 1" },
|
||||||
|
{ id: 2, name: "Item 2" },
|
||||||
|
{ id: 1, name: "Item 3" }, // Duplicate ID
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
defaultUser: { id: 1 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test("should find simple property matches", () => {
|
||||||
|
const paths = findObjectPath(testObj, { id: 1 });
|
||||||
|
expect(paths).toContain("user");
|
||||||
|
expect(paths).toContain("items[0]");
|
||||||
|
expect(paths).toContain("items[2]");
|
||||||
|
expect(paths).toContain("config.defaultUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should find nested property matches", () => {
|
||||||
|
const paths = findObjectPath(testObj, { email: true });
|
||||||
|
expect(paths).toContain("user.settings.notifications");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should find matches in arrays", () => {
|
||||||
|
const paths = findObjectPath(testObj, { id: 101 });
|
||||||
|
expect(paths).toContain("user.contacts[0]");
|
||||||
|
|
||||||
|
const paths2 = findObjectPath(testObj, { id: 102 });
|
||||||
|
expect(paths2).toContain("user.contacts[1]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should find matches with multiple criteria", () => {
|
||||||
|
const paths = findObjectPath(testObj, { id: 1, name: "Item 1" });
|
||||||
|
expect(paths).toContain("items[0]");
|
||||||
|
expect(paths).not.toContain("items[2]"); // Has id: 1 but different name
|
||||||
|
expect(paths).not.toContain("user"); // Has id: 1 but no name property
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return empty array for no matches", () => {
|
||||||
|
const paths = findObjectPath(testObj, { id: 999 });
|
||||||
|
expect(paths).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle empty criteria", () => {
|
||||||
|
const paths = findObjectPath(testObj, {});
|
||||||
|
expect(paths).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle null or undefined input", () => {
|
||||||
|
expect(findObjectPath(null, { id: 1 })).toEqual([]);
|
||||||
|
expect(findObjectPath(undefined, { id: 1 })).toEqual([]);
|
||||||
|
expect(findObjectPath(testObj, null)).toEqual([]);
|
||||||
|
expect(findObjectPath(testObj, undefined)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should work with setNestedValue", () => {
|
||||||
|
const obj = { ...testObj }; // Clone to avoid modifying test object
|
||||||
|
const paths = findObjectPath(obj, { id: 101 });
|
||||||
|
expect(paths.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const path = paths[0];
|
||||||
|
const originalValue = getNestedValue(path, obj);
|
||||||
|
const updatedValue = { ...originalValue, email: "updated@example.com" };
|
||||||
|
|
||||||
|
setNestedValue(path, updatedValue, obj);
|
||||||
|
expect(getNestedValue(`${path}.email`, obj)).toBe("updated@example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should find path to primitive array values", () => {
|
||||||
|
const paths = findObjectPath(testObj, { 0: "important" });
|
||||||
|
expect(paths).toContain("meta.tags");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only add these tests if you're implementing the advanced version
|
||||||
|
describe("findObjectByKeyValues", () => {
|
||||||
|
const testObj = {
|
||||||
|
user: {
|
||||||
|
name: "John",
|
||||||
|
id: 1,
|
||||||
|
contacts: [
|
||||||
|
{ id: 101, email: "john@example.com", phone: "123-456" },
|
||||||
|
{ id: 102, email: "JOHN.alt@example.com", phone: "789-012" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ id: 1, name: "Item 1", status: "active" },
|
||||||
|
{ id: 2, name: "Item 2", status: "inactive" },
|
||||||
|
{ id: 3, name: "Item 3", status: "active" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test("should handle partial matches", () => {
|
||||||
|
const paths = findObjectByKeyValues(testObj, { id: 1, name: "Unknown" }, { partialMatch: true });
|
||||||
|
expect(paths).toContain("user");
|
||||||
|
expect(paths).toContain("items[0]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle maxDepth option", () => {
|
||||||
|
// Should not find matches beyond depth 1
|
||||||
|
const paths = findObjectByKeyValues(testObj, { id: 101 }, { maxDepth: 1 });
|
||||||
|
expect(paths).toEqual([]);
|
||||||
|
|
||||||
|
// Should not find matches at depth 2
|
||||||
|
const paths2 = findObjectByKeyValues(testObj, { id: 101 }, { maxDepth: 3 });
|
||||||
|
expect(paths2).toContain("user.contacts[0]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle returnFirstMatch option", () => {
|
||||||
|
// Should return only the first match
|
||||||
|
const paths = findObjectByKeyValues(testObj, { status: "active" }, { returnFirstMatch: true });
|
||||||
|
expect(paths.length).toBe(1);
|
||||||
|
expect(paths[0]).toBe("items[0]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle case insensitive matching", () => {
|
||||||
|
// Should match regardless of case
|
||||||
|
const paths = findObjectByKeyValues(testObj, { email: "john.alt@example.com" }, { caseSensitive: false });
|
||||||
|
expect(paths).toContain("user.contacts[1]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should use custom comparison function", () => {
|
||||||
|
// Custom comparator that checks if string contains substring
|
||||||
|
const customCompare = (a: any, b: any) => {
|
||||||
|
if (typeof a === 'string' && typeof b === 'string') {
|
||||||
|
return a.includes(b);
|
||||||
|
}
|
||||||
|
return a === b;
|
||||||
|
};
|
||||||
|
|
||||||
|
const paths = findObjectByKeyValues(testObj, { email: "@example" }, { customCompare });
|
||||||
|
expect(paths).toContain("user.contacts[0]");
|
||||||
|
expect(paths).toContain("user.contacts[1]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should combine multiple options", () => {
|
||||||
|
const paths = findObjectByKeyValues(
|
||||||
|
testObj,
|
||||||
|
{ id: 1, status: "active" },
|
||||||
|
{
|
||||||
|
partialMatch: true,
|
||||||
|
maxDepth: 2,
|
||||||
|
returnFirstMatch: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(paths.length).toBe(1);
|
||||||
|
// Either user or items[0] could be returned since we're stopping at first match
|
||||||
|
expect(paths[0] === "user" || paths[0] === "items[0]").toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -102,3 +102,176 @@ export function setNestedValue(
|
|||||||
current[lastKey] = value;
|
current[lastKey] = value;
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
export function findObjectByKeyValues(
|
||||||
|
obj: Record<string, any>,
|
||||||
|
criteria: Record<string, any>,
|
||||||
|
options: {
|
||||||
|
partialMatch?: boolean;
|
||||||
|
maxDepth?: number;
|
||||||
|
returnFirstMatch?: boolean;
|
||||||
|
caseSensitive?: boolean;
|
||||||
|
customCompare?: (a: any, b: any) => boolean;
|
||||||
|
} = {}
|
||||||
|
): string[] {
|
||||||
|
// Handle invalid inputs
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle empty criteria
|
||||||
|
if (!criteria || typeof criteria !== 'object' || Object.keys(criteria).length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
partialMatch = false,
|
||||||
|
maxDepth = Infinity,
|
||||||
|
returnFirstMatch = false,
|
||||||
|
caseSensitive = true,
|
||||||
|
customCompare
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const results: string[] = [];
|
||||||
|
|
||||||
|
// Helper function for comparing values
|
||||||
|
const compareValues = (a: any, b: any): boolean => {
|
||||||
|
if (customCompare) {
|
||||||
|
return customCompare(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle string comparison with case sensitivity option
|
||||||
|
if (typeof a === 'string' && typeof b === 'string' && !caseSensitive) {
|
||||||
|
return a.toLowerCase() === b.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return a === b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recursive search function
|
||||||
|
const search = (
|
||||||
|
currentObj: Record<string, any>,
|
||||||
|
currentPath: string = '',
|
||||||
|
depth: number = 0
|
||||||
|
): void => {
|
||||||
|
// Check depth first - don't process objects beyond max depth
|
||||||
|
if (depth > maxDepth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if not a valid object
|
||||||
|
if (!currentObj || typeof currentObj !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current object matches criteria
|
||||||
|
let matchCount = 0;
|
||||||
|
const criteriaKeys = Object.keys(criteria);
|
||||||
|
|
||||||
|
for (const key of criteriaKeys) {
|
||||||
|
if (key in currentObj && compareValues(currentObj[key], criteria[key])) {
|
||||||
|
matchCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = partialMatch
|
||||||
|
? matchCount > 0
|
||||||
|
: matchCount === criteriaKeys.length;
|
||||||
|
|
||||||
|
// If matched, add to results
|
||||||
|
if (isMatch && criteriaKeys.length > 0) {
|
||||||
|
results.push(currentPath);
|
||||||
|
if (returnFirstMatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively search nested objects and arrays
|
||||||
|
if (Array.isArray(currentObj)) {
|
||||||
|
for (let i = 0; i < currentObj.length; i++) {
|
||||||
|
const item = currentObj[i];
|
||||||
|
if (item && typeof item === 'object') {
|
||||||
|
const nextPath = currentPath ? `${currentPath}[${i}]` : `[${i}]`;
|
||||||
|
search(item, nextPath, depth + 1);
|
||||||
|
if (returnFirstMatch && results.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(currentObj)) {
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
const nextPath = currentPath ? `${currentPath}.${key}` : key;
|
||||||
|
search(value, nextPath, depth + 1);
|
||||||
|
if (returnFirstMatch && results.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
search(obj);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds objects in a nested structure that match specified key-value pairs
|
||||||
|
* Returns paths compatible with setNestedValue function
|
||||||
|
* @param obj - Source object to search within
|
||||||
|
* @param criteria - Object with key-value pairs to match against
|
||||||
|
* @returns Array of paths where matching objects were found
|
||||||
|
*/
|
||||||
|
export function findObjectPath(
|
||||||
|
obj: Record<string, any>,
|
||||||
|
criteria: Record<string, any>
|
||||||
|
): string[] {
|
||||||
|
const results: string[] = [];
|
||||||
|
|
||||||
|
// Handle invalid inputs
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle empty criteria - should return empty array
|
||||||
|
if (!criteria || typeof criteria !== 'object' || Object.keys(criteria).length === 0) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = (currentObj: any, path: string = '') => {
|
||||||
|
// Skip if not an object or is null
|
||||||
|
if (!currentObj || typeof currentObj !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current object matches all criteria
|
||||||
|
let matches = true;
|
||||||
|
for (const [key, value] of Object.entries(criteria)) {
|
||||||
|
if (!(key in currentObj) || currentObj[key] !== value) {
|
||||||
|
matches = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
results.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue searching in nested properties
|
||||||
|
if (Array.isArray(currentObj)) {
|
||||||
|
for (let i = 0; i < currentObj.length; i++) {
|
||||||
|
const newPath = path ? `${path}[${i}]` : `[${i}]`;
|
||||||
|
search(currentObj[i], newPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(currentObj)) {
|
||||||
|
const newPath = path ? `${path}.${key}` : key;
|
||||||
|
search(value, newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
search(obj);
|
||||||
|
return results;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user