/**
* @fileoverview Schema assembly and validation utilities.
*
* Handles the final assembly of graph schema data from analyzed components,
* validation against schema definitions, and metadata generation.
*/
import { Effect } from 'effect';
import {
GraphSchemaSchema,
type GraphSchema,
type Node,
type Relationship,
type RelationshipPattern,
} from './models/index.js';
import { Errors, type GremlinQueryError } from '../errors.js';
import type { SchemaConfig } from './types.js';
/**
* Schema metadata for tracking generation performance and settings.
*/
export interface SchemaMetadata {
generated_at: string;
generation_time_ms: number;
node_count: number;
relationship_count: number;
pattern_count: number;
optimization_settings: {
sample_values_included: boolean;
max_enum_values: number;
counts_included: boolean;
enum_cardinality_threshold: number;
timeout_ms: number;
batch_size: number;
};
}
/**
* Assembles and validates the final graph schema from analyzed components.
*
* @param nodes - Analyzed vertex nodes
* @param relationships - Analyzed edge relationships
* @param patterns - Relationship patterns
* @param config - Schema generation configuration
* @param startTime - Generation start timestamp for performance metrics
* @returns Effect with validated graph schema
*/
export const assembleGraphSchema = (
nodes: Node[],
relationships: Relationship[],
patterns: RelationshipPattern[],
config: SchemaConfig,
startTime: number
): Effect.Effect<GraphSchema, GremlinQueryError> =>
Effect.gen(function* () {
// Create metadata
const metadata = createSchemaMetadata(nodes, relationships, patterns, config, startTime);
// Assemble schema data
const schemaData = {
nodes,
relationships,
relationship_patterns: patterns,
metadata,
};
yield* Effect.logDebug('Schema assembly completed', {
nodeCount: nodes.length,
relationshipCount: relationships.length,
patternCount: patterns.length,
generationTimeMs: metadata.generation_time_ms,
});
// Validate against schema
return yield* validateSchemaData(schemaData);
});
/**
* Creates schema metadata with generation statistics and configuration.
*
* @param nodes - Analyzed nodes
* @param relationships - Analyzed relationships
* @param patterns - Relationship patterns
* @param config - Configuration used for generation
* @param startTime - Start timestamp
* @returns Schema metadata object
*/
const createSchemaMetadata = (
nodes: Node[],
relationships: Relationship[],
patterns: RelationshipPattern[],
config: SchemaConfig,
startTime: number
): SchemaMetadata => ({
generated_at: new Date().toISOString(),
generation_time_ms: Date.now() - startTime,
node_count: nodes.length,
relationship_count: relationships.length,
pattern_count: patterns.length,
optimization_settings: {
sample_values_included: config.includeSampleValues,
max_enum_values: config.maxEnumValues,
counts_included: config.includeCounts,
enum_cardinality_threshold: config.enumCardinalityThreshold,
timeout_ms: config.timeoutMs || 30000,
batch_size: config.batchSize || 10,
},
});
/**
* Validates schema data against the GraphSchema specification.
*
* @param schemaData - Assembled schema data
* @returns Effect with validated GraphSchema or error
*/
const validateSchemaData = (schemaData: unknown): Effect.Effect<GraphSchema, GremlinQueryError> =>
Effect.try({
try: () => GraphSchemaSchema.parse(schemaData),
catch: (error: unknown) => {
// Use Effect logging instead of console.error for better observability
Effect.runSync(
Effect.logError('Schema validation failed', {
error: error instanceof Error ? error.message : String(error),
schemaData: safeStringify(schemaData),
})
);
return Errors.query('Schema validation failed', 'schema-validation', { error });
},
});
/**
* Safely stringifies objects for logging, handling circular references.
*
* @param obj - Object to stringify
* @returns JSON string or error representation
*/
const safeStringify = (obj: unknown): string => {
try {
return JSON.stringify(obj, null, 2);
} catch {
return `[Object: ${typeof obj}]`;
}
};
/**
* Validates node data consistency and completeness.
*
* @param nodes - Array of nodes to validate
* @returns Effect with validation result
*/
export const validateNodes = (nodes: Node[]): Effect.Effect<void, GremlinQueryError> =>
Effect.gen(function* () {
const issues: string[] = [];
nodes.forEach((node, index) => {
if (!node.labels || typeof node.labels !== 'string') {
issues.push(`Node ${index}: Invalid or missing labels`);
}
if (!Array.isArray(node.properties)) {
issues.push(`Node ${index}: Properties must be an array`);
}
node.properties?.forEach((prop, propIndex) => {
if (!prop.name || typeof prop.name !== 'string') {
issues.push(`Node ${index}, Property ${propIndex}: Invalid or missing name`);
}
if (!Array.isArray(prop.type)) {
issues.push(`Node ${index}, Property ${propIndex}: Type must be an array`);
}
});
});
if (issues.length > 0) {
return yield* Effect.fail(
Errors.query('Node validation failed', 'node-validation', { issues })
);
}
});
/**
* Validates relationship data consistency and completeness.
*
* @param relationships - Array of relationships to validate
* @returns Effect with validation result
*/
export const validateRelationships = (
relationships: Relationship[]
): Effect.Effect<void, GremlinQueryError> =>
Effect.gen(function* () {
const issues: string[] = [];
relationships.forEach((rel, index) => {
if (!rel.type || typeof rel.type !== 'string') {
issues.push(`Relationship ${index}: Invalid or missing type`);
}
if (!Array.isArray(rel.properties)) {
issues.push(`Relationship ${index}: Properties must be an array`);
}
rel.properties?.forEach((prop, propIndex) => {
if (!prop.name || typeof prop.name !== 'string') {
issues.push(`Relationship ${index}, Property ${propIndex}: Invalid or missing name`);
}
if (!Array.isArray(prop.type)) {
issues.push(`Relationship ${index}, Property ${propIndex}: Type must be an array`);
}
});
});
if (issues.length > 0) {
return yield* Effect.fail(
Errors.query('Relationship validation failed', 'relationship-validation', { issues })
);
}
});
/**
* Validates relationship patterns for consistency.
*
* @param patterns - Array of relationship patterns to validate
* @returns Effect with validation result
*/
export const validateRelationshipPatterns = (
patterns: RelationshipPattern[]
): Effect.Effect<void, GremlinQueryError> =>
Effect.gen(function* () {
const issues: string[] = [];
patterns.forEach((pattern, index) => {
if (!pattern.left_node || typeof pattern.left_node !== 'string') {
issues.push(`Pattern ${index}: Invalid or missing left_node`);
}
if (!pattern.right_node || typeof pattern.right_node !== 'string') {
issues.push(`Pattern ${index}: Invalid or missing right_node`);
}
if (!pattern.relation || typeof pattern.relation !== 'string') {
issues.push(`Pattern ${index}: Invalid or missing relation`);
}
});
if (issues.length > 0) {
return yield* Effect.fail(
Errors.query('Relationship pattern validation failed', 'pattern-validation', { issues })
);
}
});
/**
* Performs comprehensive validation of all schema components.
*
* @param nodes - Nodes to validate
* @param relationships - Relationships to validate
* @param patterns - Patterns to validate
* @returns Effect with validation result
*/
export const validateAllComponents = (
nodes: Node[],
relationships: Relationship[],
patterns: RelationshipPattern[]
): Effect.Effect<void, GremlinQueryError> =>
Effect.gen(function* () {
yield* validateNodes(nodes);
yield* validateRelationships(relationships);
yield* validateRelationshipPatterns(patterns);
yield* Effect.logInfo('All schema components validated successfully');
});