UNPKG

@phr3nzy/rulekit

Version:

A powerful and flexible toolkit for building rule-based matching and filtering systems

181 lines (178 loc) 6.07 kB
// src/attributes/types.ts var AttributeType = { STRING: "string", NUMBER: "number", BOOLEAN: "boolean", DATE: "date", ENUM: "enum", ARRAY: "array" }; // src/types/schema.ts var ComparisonOperators = { eq: "eq", ne: "ne", gt: "gt", gte: "gte", lt: "lt", lte: "lte", in: "in", notIn: "notIn" }; function isValidAttributeValue(value, type, arrayType) { if (value === null || value === void 0) return false; switch (type) { case AttributeType.STRING: return typeof value === "string"; case AttributeType.NUMBER: return typeof value === "number" && !isNaN(value); case AttributeType.BOOLEAN: return typeof value === "boolean"; case AttributeType.DATE: return value instanceof Date && !isNaN(value.getTime()); case AttributeType.ENUM: return typeof value === "string"; case AttributeType.ARRAY: return Array.isArray(value) && (!arrayType || value.every((item) => isValidAttributeValue(item, arrayType))); default: return false; } } function isValidSchemaObject(obj, schema) { if (!obj || typeof obj !== "object") return false; const attributes = obj; if (typeof attributes.__validated !== "boolean") return false; return Object.entries(schema).every(([key, def]) => { if (!(key in attributes)) return !def.validation.required; return isValidAttributeValue( attributes[key], def.type, def.type === AttributeType.ARRAY ? def.validation.arrayType : void 0 ); }); } // src/engine/rule-engine.ts var RuleEngine = class { constructor(schema, config) { this.schema = schema; this.config = { maxBatchSize: 1e3, ...config }; } /** * Evaluates a single rule against an entity's attributes (assumes pre-validation) */ evaluateRule(attributes, rule) { if (rule.and?.length) { return rule.and.every((subRule) => this.evaluateRule(attributes, subRule)); } if (rule.or?.length) { return rule.or.some((subRule) => this.evaluateRule(attributes, subRule)); } if (rule.attributes) { return Object.entries(rule.attributes).every(([key, conditions]) => { const value = attributes[key]; return Object.entries(conditions).every(([op, expected]) => { switch (op) { case "eq": return value === expected; case "ne": return value !== expected; case "gt": return typeof value === "number" && value > expected; case "gte": return typeof value === "number" && value >= expected; case "lt": return typeof value === "number" && value < expected; case "lte": return typeof value === "number" && value <= expected; case "in": return Array.isArray(expected) && (Array.isArray(value) ? value.some((v) => expected.includes(v)) : expected.includes(value)); case "notIn": return Array.isArray(expected) && (Array.isArray(value) ? !value.some((v) => expected.includes(v)) : !expected.includes(value)); default: return false; } }); }); } return true; } /** * Splits entities into optimal batch sizes based on complexity heuristic */ getBatchSize(entities, rules) { const entityCount = entities.length; let batchSize = this.config.maxBatchSize; const ruleComplexity = rules.reduce((complexity, rule) => { const countConditions = (r) => { let count = 0; if (r.and) count += r.and.reduce((sum, subRule) => sum + countConditions(subRule), 0); if (r.or) count += r.or.reduce((sum, subRule) => sum + countConditions(subRule), 0); if (r.attributes) count += Object.keys(r.attributes).length; return count || 1; }; return complexity + countConditions(rule); }, 0); if (ruleComplexity > 20) { batchSize = Math.max(1, Math.floor(batchSize / 4)); } else if (ruleComplexity > 10) { batchSize = Math.max(1, Math.floor(batchSize / 2)); } return Math.min(entityCount, batchSize); } /** * Processes entities in optimally-sized batches, applying rules with validation and short-circuiting */ processBatch(entities, rules) { if (rules.length === 0) { return new Array(entities.length).fill(true); } const batchSize = this.getBatchSize(entities, rules); const results = new Array(entities.length).fill(false); const batches = Math.ceil(entities.length / batchSize); for (let i = 0; i < batches; i++) { const start = i * batchSize; const end = Math.min(start + batchSize, entities.length); const batchEntities = entities.slice(start, end); batchEntities.forEach((entity, batchIndex) => { const overallIndex = start + batchIndex; if (!isValidSchemaObject(entity.attributes, this.schema)) { return; } let allRulesPass = true; for (const rule of rules) { if (!this.evaluateRule(entity.attributes, rule)) { allRulesPass = false; break; } } results[overallIndex] = allRulesPass; }); } return results; } /** * Finds entities that satisfy all provided 'from' rules */ findMatchingFrom(entities, rules) { const matchResults = this.processBatch(entities, rules); return entities.filter((_, index) => matchResults[index]); } /** * Finds target entities that can be matched with source entities based on 'to' rules */ findMatchingTo(fromEntities, toRules, allEntities) { const fromEntityIds = new Set(fromEntities.map((e) => e.id)); const candidateEntities = allEntities.filter((entity) => !fromEntityIds.has(entity.id)); const matchResults = this.processBatch(candidateEntities, toRules); return candidateEntities.filter((_, index) => matchResults[index]); } }; export { AttributeType, ComparisonOperators, RuleEngine, isValidAttributeValue, isValidSchemaObject };