UNPKG

@pulzar/core

Version:

Next-generation Node.js framework for ultra-fast web applications with zero-reflection DI, GraphQL, WebSockets, events, and edge runtime support

481 lines 17 kB
import { EventError, } from "../types"; import { logger } from "../../utils/logger"; export class RedisSchemaRegistry { redis; config; connected = false; zodModule; constructor(config = {}) { this.config = { host: config.host || "localhost", port: config.port || 6379, password: config.password || "", db: config.db || 1, // Different DB from DLQ keyPrefix: config.keyPrefix || "pulzar:schema", enableVersioning: config.enableVersioning !== false, maxVersionsPerSubject: config.maxVersionsPerSubject || 10, }; } /** * Initialize Redis connection */ async connect() { if (this.connected) { return; } try { const Redis = await this.importRedis(); if (!Redis) { throw new EventError("Redis package not installed. Run: npm install ioredis", "REDIS_NOT_INSTALLED"); } this.redis = new Redis({ host: this.config.host, port: this.config.port, password: this.config.password || undefined, db: this.config.db, retryDelayOnFailover: 100, maxRetriesPerRequest: 3, lazyConnect: true, }); await this.redis.connect(); this.connected = true; // Try to load Zod for validation this.zodModule = await this.importZod(); logger.info("Redis Schema Registry connected", { host: this.config.host, port: this.config.port, db: this.config.db, zodAvailable: !!this.zodModule, }); } catch (error) { logger.error("Failed to connect to Redis Schema Registry", { error }); throw new EventError(`Redis Schema Registry connection failed: ${error.message}`, "CONNECTION_FAILED", undefined, error); } } /** * Disconnect from Redis */ async disconnect() { if (this.redis && this.connected) { await this.redis.quit(); this.connected = false; logger.info("Redis Schema Registry disconnected"); } } /** * Register a new schema */ async register(schema) { if (!this.connected) { throw new EventError("Redis Schema Registry not connected", "NOT_CONNECTED"); } try { // Validate schema format this.validateSchemaFormat(schema); // Check compatibility if versioning is enabled if (this.config.enableVersioning) { const isCompatible = await this.isCompatible(schema.subject, schema); if (!isCompatible) { throw new EventError(`Schema is not compatible with existing versions for subject: ${schema.subject}`, "SCHEMA_INCOMPATIBLE"); } } const schemaKey = this.getSchemaKey(schema.subject, schema.version); const subjectKey = this.getSubjectKey(schema.subject); // Store schema const pipeline = this.redis.pipeline(); // Store full schema pipeline.hset(schemaKey, this.serializeSchema(schema)); // Add to subject versions list pipeline.zadd(subjectKey, Date.now(), schema.version); // Maintain version limit if (this.config.maxVersionsPerSubject > 0) { pipeline.zremrangebyrank(subjectKey, 0, -(this.config.maxVersionsPerSubject + 1)); } // Update registry stats pipeline.hincrby(this.getStatsKey(), "totalSchemas", 1); pipeline.sadd(this.getSubjectsKey(), schema.subject); await pipeline.exec(); logger.info("Schema registered", { subject: schema.subject, version: schema.version, type: schema.type, }); } catch (error) { logger.error("Failed to register schema", { subject: schema.subject, version: schema.version, error, }); throw error; } } /** * Get schema by subject and version */ async get(subject, version) { if (!this.connected) { throw new EventError("Redis Schema Registry not connected", "NOT_CONNECTED"); } try { let targetVersion = version; // If no version specified, get latest if (!targetVersion) { const versions = await this.redis.zrevrange(this.getSubjectKey(subject), 0, 0); if (!versions || versions.length === 0) { return null; } targetVersion = versions[0]; } if (!targetVersion) { return null; } const schemaData = await this.redis.hgetall(this.getSchemaKey(subject, targetVersion)); if (!schemaData || Object.keys(schemaData).length === 0) { return null; } return this.deserializeSchema(schemaData); } catch (error) { logger.error("Failed to get schema", { subject, version, error }); throw new EventError(`Schema get failed: ${error.message}`, "SCHEMA_GET_FAILED", undefined, error); } } /** * List all schemas */ async list() { if (!this.connected) { throw new EventError("Redis Schema Registry not connected", "NOT_CONNECTED"); } try { const subjects = await this.redis.smembers(this.getSubjectsKey()); const schemas = []; for (const subject of subjects) { // Get all versions for this subject const versions = await this.redis.zrevrange(this.getSubjectKey(subject), 0, -1); for (const version of versions) { const schema = await this.get(subject, version); if (schema) { schemas.push(schema); } } } return schemas; } catch (error) { logger.error("Failed to list schemas", { error }); throw new EventError(`Schema list failed: ${error.message}`, "SCHEMA_LIST_FAILED", undefined, error); } } /** * Validate data against schema */ async validate(subject, data) { if (!this.connected) { throw new EventError("Redis Schema Registry not connected", "NOT_CONNECTED"); } try { const schema = await this.get(subject); if (!schema) { return { valid: false, errors: [ { path: "", message: `No schema found for subject: ${subject}`, code: "SCHEMA_NOT_FOUND", value: subject, }, ], }; } return await this.validateWithSchema(data, schema); } catch (error) { logger.error("Failed to validate data", { subject, error }); return { valid: false, errors: [ { path: "", message: `Validation failed: ${error.message}`, code: "VALIDATION_ERROR", value: data, }, ], }; } } /** * Check schema compatibility */ async isCompatible(subject, newSchema) { if (!this.connected) { throw new EventError("Redis Schema Registry not connected", "NOT_CONNECTED"); } try { const existingSchema = await this.get(subject); if (!existingSchema) { return true; // No existing schema, so it's compatible } // Check compatibility based on schema type and compatibility setting return this.checkCompatibility(existingSchema, newSchema); } catch (error) { logger.error("Failed to check schema compatibility", { subject, newVersion: newSchema.version, error, }); return false; } } /** * Validate data with specific schema */ async validateWithSchema(data, schema) { try { // Use custom validator if provided if (schema.validator) { return await schema.validator.validate(data); } // Handle different schema types switch (schema.type) { case "zod": return this.validateWithZod(data, schema); case "json-schema": return this.validateWithJsonSchema(data, schema); case "avro": return this.validateWithAvro(data, schema); case "protobuf": return this.validateWithProtobuf(data, schema); default: throw new Error(`Unsupported schema type: ${schema.type}`); } } catch (error) { return { valid: false, errors: [ { path: "", message: error.message, code: "VALIDATION_ERROR", value: data, }, ], }; } } /** * Validate with Zod schema */ validateWithZod(data, schema) { if (!this.zodModule) { throw new Error("Zod package not available"); } try { const zodSchema = this.zodModule.z(schema.schema); const result = zodSchema.safeParse(data); if (result.success) { return { valid: true }; } const errors = result.error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message, code: issue.code, value: issue.input, })); return { valid: false, errors }; } catch (error) { throw new Error(`Zod validation failed: ${error.message}`); } } /** * Validate with JSON Schema */ validateWithJsonSchema(data, schema) { // For now, just basic validation // In production, use a proper JSON Schema library like Ajv try { // Basic JSON serialization check JSON.stringify(data); return { valid: true }; } catch (error) { return { valid: false, errors: [ { path: "", message: `JSON validation failed: ${error.message}`, code: "JSON_INVALID", value: data, }, ], }; } } /** * Validate with Avro schema */ validateWithAvro(data, schema) { // Placeholder for Avro validation // In production, use avsc or similar library logger.warn("Avro validation not implemented, returning valid"); return { valid: true }; } /** * Validate with Protobuf schema */ validateWithProtobuf(data, schema) { // Placeholder for Protobuf validation // In production, use protobufjs or similar library logger.warn("Protobuf validation not implemented, returning valid"); return { valid: true }; } /** * Check compatibility between schemas */ checkCompatibility(existing, newSchema) { // Basic compatibility rules if (existing.type !== newSchema.type) { return false; // Different schema types are incompatible } if (!newSchema.compatibility) { return true; // No compatibility requirement } switch (newSchema.compatibility) { case "backward": // New schema can read data written with old schema return this.isBackwardCompatible(existing, newSchema); case "forward": // Old schema can read data written with new schema return this.isForwardCompatible(existing, newSchema); case "full": // Both backward and forward compatible return (this.isBackwardCompatible(existing, newSchema) && this.isForwardCompatible(existing, newSchema)); case "none": return true; // No compatibility checking default: return false; } } /** * Check backward compatibility */ isBackwardCompatible(existing, newSchema) { // Simplified backward compatibility check // In production, implement proper schema evolution rules if (existing.type === "json-schema" && newSchema.type === "json-schema") { // For JSON Schema, check if required fields weren't removed const existingRequired = existing.schema?.required || []; const newRequired = newSchema.schema?.required || []; // All previously required fields must still be required return existingRequired.every((field) => newRequired.includes(field)); } // For other types, assume compatible for now return true; } /** * Check forward compatibility */ isForwardCompatible(existing, newSchema) { // Simplified forward compatibility check // In production, implement proper schema evolution rules if (existing.type === "json-schema" && newSchema.type === "json-schema") { // For JSON Schema, check if new required fields weren't added const existingRequired = existing.schema?.required || []; const newRequired = newSchema.schema?.required || []; // No new required fields should be added return !newRequired.some((field) => !existingRequired.includes(field)); } // For other types, assume compatible for now return true; } /** * Validate schema format */ validateSchemaFormat(schema) { if (!schema.name || !schema.version || !schema.subject) { throw new EventError("Schema must have name, version, and subject", "INVALID_SCHEMA_FORMAT"); } if (!["json-schema", "avro", "protobuf", "zod"].includes(schema.type)) { throw new EventError(`Unsupported schema type: ${schema.type}`, "UNSUPPORTED_SCHEMA_TYPE"); } if (!schema.schema) { throw new EventError("Schema definition is required", "MISSING_SCHEMA_DEFINITION"); } } /** * Serialize schema for Redis storage */ serializeSchema(schema) { return { name: schema.name, version: schema.version, subject: schema.subject, type: schema.type, schema: JSON.stringify(schema.schema), compatibility: schema.compatibility || "none", }; } /** * Deserialize schema from Redis */ deserializeSchema(data) { return { name: data.name || "", version: data.version || "", subject: data.subject || "", type: data.type || "json-schema", schema: JSON.parse(data.schema || "{}"), compatibility: data.compatibility || "none", }; } /** * Generate Redis keys */ getSchemaKey(subject, version) { return `${this.config.keyPrefix}:schema:${subject}:${version}`; } getSubjectKey(subject) { return `${this.config.keyPrefix}:subject:${subject}`; } getSubjectsKey() { return `${this.config.keyPrefix}:subjects`; } getStatsKey() { return `${this.config.keyPrefix}:stats`; } /** * Try to import Redis package */ async importRedis() { try { return (await import("ioredis")).default; } catch (error) { logger.warn("Redis package not available", { error }); return null; } } /** * Try to import Zod package */ async importZod() { try { return await import("zod"); } catch (error) { logger.debug("Zod package not available", { error }); return null; } } } export default RedisSchemaRegistry; //# sourceMappingURL=redis-schema-registry.js.map