/**
* @fileoverview Gremlin result parsing with type-safe transformations.
*
* Handles the complex task of converting raw Gremlin driver results into standardized,
* serializable objects. Manages various result formats from different Gremlin
* implementations and normalizes them for MCP consumption.
*/
import { z } from 'zod';
import { GremlinResultItemSchema } from '../gremlin/models/index.js';
import { calculateResultMetadata } from './result-metadata.js';
import { Effect } from 'effect';
import { Errors } from '../errors.js';
/**
* Type guard for objects with specific constructor names.
*
* @param obj - Object to check
* @param name - Expected constructor name
* @returns True if object has the specified constructor name
*
* Critical for identifying native Gremlin driver types (Vertex, Edge, etc.)
* which need special handling during result transformation.
*/
function hasConstructorName(
obj: unknown,
name: 'Vertex' | 'Edge' | 'Path' | 'Property' | 'VertexProperty'
): boolean {
return (
obj !== null &&
typeof obj === 'object' &&
'constructor' in obj &&
typeof obj.constructor === 'function' &&
obj.constructor.name === name
);
}
/**
* Type guard and interface for raw vertex objects
*/
interface RawVertex {
id: unknown;
label: string;
properties?: unknown;
}
function isRawVertex(obj: unknown): obj is RawVertex {
return (
obj !== null &&
typeof obj === 'object' &&
'id' in obj &&
'label' in obj &&
typeof obj.label === 'string'
);
}
/**
* Type guard and interface for raw edge objects
*/
interface RawEdge {
id: unknown;
label: string;
inV?: unknown;
outV?: unknown;
properties?: unknown;
}
function isRawEdge(obj: unknown): obj is RawEdge {
return (
obj !== null &&
typeof obj === 'object' &&
'id' in obj &&
'label' in obj &&
typeof obj.label === 'string'
);
}
/**
* Type guard and interface for raw path objects
*/
interface RawPath {
labels?: unknown;
objects?: unknown;
}
function isRawPath(obj: unknown): obj is RawPath {
return obj !== null && typeof obj === 'object';
}
/**
* Type guard and interface for raw property objects
*/
interface RawProperty {
key?: string;
label?: string;
value: unknown;
}
function isRawProperty(obj: unknown): obj is RawProperty {
return obj !== null && typeof obj === 'object' && 'value' in obj;
}
/**
* Zod schema with preprocessing for raw Gremlin results.
*
* @description Transforms native Gremlin driver types into standardized objects before validation.
*
* Preprocessing pipeline:
* 1. Detects and transforms native driver types (Vertex, Edge, Path, Property)
* 2. Converts ES6 Maps to plain objects
* 3. Adds type discriminators for schema validation
* 4. Handles various result formats from different Gremlin implementations
*
* Critical for cross-driver compatibility - different Gremlin servers return
* results in slightly different formats.
*/
const GremlinPreprocessedResultSchema = z.preprocess((arg: unknown) => {
// Pass through primitives and null/undefined, which Zod can handle directly.
if (arg === null || typeof arg !== 'object') {
return arg;
}
// Handle native Gremlin structure types by checking their constructor name
// and transforming them into plain objects with a 'type' discriminator.
if (hasConstructorName(arg, 'Vertex') && isRawVertex(arg)) {
return { ...arg, type: 'vertex' };
}
if (hasConstructorName(arg, 'Edge') && isRawEdge(arg)) {
return { ...arg, type: 'edge' };
}
if (hasConstructorName(arg, 'Path') && isRawPath(arg)) {
return {
labels: Array.isArray(arg.labels) ? arg.labels.map(String) : [],
objects: arg.objects,
type: 'path',
};
}
if (
(hasConstructorName(arg, 'Property') || hasConstructorName(arg, 'VertexProperty')) &&
isRawProperty(arg)
) {
return {
key: arg.key || arg.label || '',
value: arg.value,
type: 'property',
};
}
// Convert ES6 Maps to plain objects so Zod can parse them.
if (arg instanceof Map) {
return Object.fromEntries(arg.entries());
}
// If a generic object looks like a vertex or edge (e.g., from a different driver
// or a simple JSON response), add the 'type' discriminator to help Zod parse it.
if ('id' in arg && 'label' in arg) {
if ('properties' in arg && !('inV' in arg) && !('outV' in arg) && isRawVertex(arg)) {
return { ...arg, type: 'vertex' };
}
if ('inV' in arg && 'outV' in arg && isRawEdge(arg)) {
return { ...arg, type: 'edge' };
}
}
// Let arrays and other plain objects pass through for Zod to handle.
return arg;
}, GremlinResultItemSchema);
/**
* Parses a single raw Gremlin result into a typed, serializable object.
*
* @param rawResult - Raw result item from Gremlin query
* @returns Validated and typed result item
*
* Uses the preprocessing schema to handle driver-specific transformations.
*/
export const parseGremlinResultItem = (rawResult: unknown) =>
Effect.try({
try: () => GremlinPreprocessedResultSchema.parse(rawResult),
catch: error => Errors.parse('Failed to parse Gremlin result item', rawResult, error),
});
/**
* Parses an array of raw Gremlin results into typed objects.
*/
export const parseGremlinResults = (rawResults: unknown[]) =>
Effect.all(rawResults.map(parseGremlinResultItem));
/**
* Enhanced result parser with comprehensive metadata generation.
*
* @param rawResults - Array of raw Gremlin query results
* @returns Parsed results with detailed type and structure metadata
*
* Provides additional context about result composition, useful for
* understanding query output patterns and debugging.
*/
export const parseGremlinResultsWithMetadata = (rawResults: unknown[]) =>
Effect.gen(function* () {
const results = yield* parseGremlinResults(rawResults);
const metadata = calculateResultMetadata(results);
return { results, metadata };
});