import { isStringKeyedObject } from "@phoenix/typeUtils";
/**
* Removes null, undefined, empty strings, empty objects, and empty arrays from an object.
* Returns undefined if the resulting object would be empty.
*
* Used to build compact GraphQL mutation inputs that only include defined values.
*
* @returns A partial version of the input object with empty values removed,
* or undefined if all values were empty.
*
* @remarks
* The return type is `Partial<T> | undefined` which provides better type safety
* than `any` while still being compatible with GraphQL mutation inputs.
* The GraphQL schema enforces required fields at the server layer, and
* Zod validation ensures required fields are present before submission.
*/
export function compressObject<T extends Record<string, unknown>>(
obj: T
): Partial<T> | undefined {
const entries = Object.entries(obj).filter(([, value]) => {
if (value === null || value === undefined) return false;
if (value === "") return false;
if (typeof value === "object" && !Array.isArray(value)) {
return Object.keys(value).length > 0;
}
if (Array.isArray(value)) return value.length > 0;
return true;
});
if (entries.length === 0) return undefined;
return Object.fromEntries(entries) as Partial<T>;
}
/**
* Get a value from an object using a dot-notation path.
*
* TODO: Replace this with a JSON path-based utility (e.g., using jsonpath-plus
* or similar library) to support full JSON path syntax like:
* - $.input.query
* - $.metadata.tags[0]
* - $..nested.field
*
* @param obj - The object to retrieve the value from
* @param path - Dot-notation path (e.g., "input", "input.query")
* @returns The value at the path, or undefined if not found
*/
export function getValueAtPath(obj: unknown, path: string): unknown {
if (!path || !isStringKeyedObject(obj)) {
return obj;
}
const segments = path.split(".");
let current: unknown = obj;
for (const segment of segments) {
if (!isStringKeyedObject(current)) {
return undefined;
}
current = current[segment];
}
return current;
}
/**
* Recursively extracts all paths from an object.
*
* For nested objects, generates dot-notation paths.
* For arrays, generates indexed paths like "items[0]".
*
* @param obj - The object to extract paths from
* @param prefix - Optional prefix for the current path level
* @param maxDepth - Maximum depth to traverse (default: 10)
* @returns An array of all paths in the object
*
* @example
* extractPathsFromObject({ user: { name: "Alice", tags: ["a", "b"] } })
* // Returns: ["user", "user.name", "user.tags", "user.tags[0]", "user.tags[1]"]
*/
export function extractPathsFromObject(
obj: unknown,
prefix = "",
maxDepth = 10
): string[] {
if (maxDepth <= 0) {
return prefix ? [prefix] : [];
}
const paths: string[] = [];
if (Array.isArray(obj)) {
// For arrays, add paths for each element
obj.forEach((item, index) => {
const arrayPath = `${prefix}[${index}]`;
paths.push(arrayPath);
if (isStringKeyedObject(item) || Array.isArray(item)) {
paths.push(...extractPathsFromObject(item, arrayPath, maxDepth - 1));
}
});
} else if (isStringKeyedObject(obj)) {
// For objects, add paths for each key
for (const key of Object.keys(obj)) {
const currentPath = prefix ? `${prefix}.${key}` : key;
paths.push(currentPath);
const value = obj[key];
if (isStringKeyedObject(value) || Array.isArray(value)) {
paths.push(...extractPathsFromObject(value, currentPath, maxDepth - 1));
}
}
}
return paths;
}
/**
* Extracts all unique paths from multiple dataset examples.
*
* @param examples - Array of dataset examples to extract paths from
* @param templateVariablesPath - Optional path prefix that scopes the variables.
* When set (e.g., "input"), paths are extracted relative to that prefix.
* When null/empty, paths are extracted from the full context (input, reference, metadata).
* @param maxExamples - Maximum number of examples to process (to limit computation)
* @returns A deduplicated array of all paths found across examples
*/
export function extractPathsFromDatasetExamples(
examples: Array<{
input: unknown;
taskOutput?: unknown;
metadata: unknown;
reference: unknown;
}>,
templateVariablesPath: string | null | undefined,
maxExamples = 10
): string[] {
const allPaths = new Set<string>();
// Process only up to maxExamples to limit computation
const examplesToProcess = examples.slice(0, maxExamples);
for (const example of examplesToProcess) {
// Build the template variables context matching the backend mapping
// when processing template contexts in the playground and evaluators:
// - reference is the output field of an example
// - output is the task output field, this does not exist on a dataset example
const templateContext: Record<string, unknown> = {
input: example.input,
reference: example.reference,
metadata: example.metadata,
...(example.taskOutput != null ? { output: example.taskOutput } : {}),
};
// Determine the target object based on templateVariablesPath
let targetObject: unknown;
if (templateVariablesPath) {
// Get the scoped object (e.g., if path is "input", get templateContext.input)
targetObject = getValueAtPath(templateContext, templateVariablesPath);
} else {
// No path prefix - use full context
targetObject = templateContext;
}
if (targetObject != null) {
const paths = extractPathsFromObject(targetObject);
for (const path of paths) {
allPaths.add(path);
}
}
}
// Sort paths alphabetically for consistent ordering
return Array.from(allPaths).sort();
}