@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
JavaScript
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