@phr3nzy/rulekit
Version:
A powerful and flexible toolkit for building rule-based matching and filtering systems
212 lines (207 loc) • 7.29 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
AttributeType: () => AttributeType,
ComparisonOperators: () => ComparisonOperators,
RuleEngine: () => RuleEngine,
isValidAttributeValue: () => isValidAttributeValue,
isValidSchemaObject: () => isValidSchemaObject
});
module.exports = __toCommonJS(index_exports);
// 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]);
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
AttributeType,
ComparisonOperators,
RuleEngine,
isValidAttributeValue,
isValidSchemaObject
});