@phr3nzy/rulekit
Version:
A powerful and flexible toolkit for building rule-based matching and filtering systems
1,246 lines (1,228 loc) • 39.1 kB
JavaScript
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/core/models/types.ts
var ComparisonOperators = {
eq: "eq",
ne: "ne",
gt: "gt",
gte: "gte",
lt: "lt",
lte: "lte",
in: "in",
notIn: "notIn"
};
// src/core/models/validation.ts
var validation_exports = {};
__export(validation_exports, {
RuleValidationError: () => RuleValidationError,
validateMatchingConfig: () => validateMatchingConfig,
validateRule: () => validateRule,
validateRuleSet: () => validateRuleSet
});
var RuleValidationError = class extends Error {
constructor(message) {
super(`Rule validation failed: ${message}`);
this.name = "RuleValidationError";
}
};
function isValidRuleValue(value) {
if (value === void 0 || value === null) return false;
if (Array.isArray(value)) {
return value.every((item) => typeof item === "string" || typeof item === "number");
}
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}
function validateOperator(operator, value) {
if (!isValidRuleValue(value)) {
throw new RuleValidationError(`Invalid value type for operator "${operator}"`);
}
if (["gt", "gte", "lt", "lte"].includes(operator)) {
if (typeof value !== "number") {
throw new RuleValidationError(`Operator "${operator}" requires a numeric value`);
}
}
if (["in", "notIn"].includes(operator)) {
if (!Array.isArray(value)) {
throw new RuleValidationError(`Operator "${operator}" requires an array value`);
}
}
}
function validateFilter(filter) {
const operators = Object.keys(filter);
if (operators.length === 0) {
throw new RuleValidationError("Filter must contain at least one operator");
}
for (const operator of operators) {
if (!Object.values(ComparisonOperators).includes(operator)) {
throw new RuleValidationError(`Invalid operator "${operator}"`);
}
validateOperator(operator, filter[operator]);
}
}
function validateRule(rule) {
if (typeof rule !== "object" || rule === null) {
throw new RuleValidationError("Rule must be an object");
}
const ruleObj = rule;
if ("and" in ruleObj) {
if (!Array.isArray(ruleObj.and)) {
throw new RuleValidationError("AND conditions must be an array");
}
ruleObj.and.forEach((subRule) => validateRule(subRule));
}
if ("or" in ruleObj) {
if (!Array.isArray(ruleObj.or)) {
throw new RuleValidationError("OR conditions must be an array");
}
ruleObj.or.forEach((subRule) => validateRule(subRule));
}
const filters = Object.entries(ruleObj).filter(([key]) => !["and", "or"].includes(key));
filters.forEach(([, filter]) => {
if (typeof filter !== "object" || filter === null) {
throw new RuleValidationError("Filter must be an object");
}
validateFilter(filter);
});
}
function validateRuleSet(ruleSet) {
if (typeof ruleSet !== "object" || ruleSet === null) {
throw new RuleValidationError("Rule set must be an object");
}
const { fromRules, toRules } = ruleSet;
if (!Array.isArray(fromRules) || fromRules.length === 0) {
throw new RuleValidationError("From rules must be a non-empty array");
}
if (!Array.isArray(toRules) || toRules.length === 0) {
throw new RuleValidationError("To rules must be a non-empty array");
}
fromRules.forEach((rule) => validateRule(rule));
toRules.forEach((rule) => validateRule(rule));
}
function validateMatchingConfig(config) {
if (typeof config !== "object" || config === null) {
throw new RuleValidationError("Config must be an object");
}
const { id, name, description, ruleSet, isActive, createdAt, updatedAt } = config;
if (typeof id !== "string" || id.length === 0) {
throw new RuleValidationError("Config must have a non-empty string id");
}
if (typeof name !== "string" || name.length === 0) {
throw new RuleValidationError("Config must have a non-empty string name");
}
if (description !== void 0 && (typeof description !== "string" || description.length === 0)) {
throw new RuleValidationError("Description must be a non-empty string if provided");
}
validateRuleSet(ruleSet);
if (typeof isActive !== "boolean") {
throw new RuleValidationError("isActive must be a boolean");
}
if (!(createdAt instanceof Date)) {
throw new RuleValidationError("createdAt must be a Date");
}
if (!(updatedAt instanceof Date)) {
throw new RuleValidationError("updatedAt must be a Date");
}
}
// src/core/evaluators/base-rule-evaluator.ts
var _BaseRuleEvaluator = class _BaseRuleEvaluator {
/**
* Evaluates if a single entity matches the given rule.
* Handles complex rule structures including nested AND/OR conditions.
*/
evaluateRule(entity, rule) {
if (rule.and) {
return rule.and.every((subRule) => this.evaluateRule(entity, subRule));
}
if (rule.or) {
return rule.or.some((subRule) => this.evaluateRule(entity, subRule));
}
if (rule.attributes) {
return Object.entries(rule.attributes).every(([attrKey, attrValue]) => {
const entityValue = entity.attributes[attrKey];
return this.evaluateFilter(entityValue, attrValue);
});
}
const attributeEntries = Object.entries(rule).filter(([key]) => key !== "and" && key !== "or");
if (attributeEntries.length > 0) {
return attributeEntries.every(([attrKey, attrValue]) => {
const entityValue = entity.attributes[attrKey];
return this.evaluateFilter(entityValue, attrValue);
});
}
return false;
}
/**
* Efficiently evaluates multiple entities against a single rule.
*/
evaluateRuleBatch(entities, rule) {
return entities.map((entity) => this.evaluateRule(entity, rule));
}
/**
* Evaluates if an entity matches any rule from a set of rules.
*/
evaluateRules(entity, rules) {
return rules.some((rule) => this.evaluateRule(entity, rule));
}
/**
* Clears any internal caches or state.
* Base implementation is a no-op as it maintains no state.
*/
clear() {
}
/**
* Internal method to evaluate a filter against a value.
*/
evaluateFilter(value, filter) {
if (value === void 0) return false;
const entries = Object.entries(filter);
if (Object.getOwnPropertySymbols(filter).length > 0) {
return false;
}
if (entries.length === 1) {
const [operator, targetValue] = entries[0];
if (!_BaseRuleEvaluator.validOperators.has(operator)) {
return false;
}
return this.evaluateOperator(value, operator, targetValue);
}
return entries.every(([operator, targetValue]) => {
if (!_BaseRuleEvaluator.validOperators.has(operator)) {
return false;
}
return this.evaluateOperator(value, operator, targetValue);
});
}
/**
* Internal method to evaluate a single comparison operator.
*/
evaluateOperator(value, operator, targetValue) {
if (operator === "eq") {
return Array.isArray(targetValue) ? new Set(targetValue).has(value) : value === targetValue;
}
if (typeof value === "number" && typeof targetValue === "number") {
switch (operator) {
case "gt":
return value > targetValue;
case "gte":
return value >= targetValue;
case "lt":
return value < targetValue;
case "lte":
return value <= targetValue;
}
}
if (Array.isArray(targetValue)) {
const set = new Set(targetValue);
switch (operator) {
case "ne":
return !set.has(value);
case "in":
return set.has(value);
case "notIn":
return !set.has(value);
}
}
switch (operator) {
case "ne":
return value !== targetValue;
default:
return false;
}
}
};
// Set of valid operators for O(1) lookup
_BaseRuleEvaluator.validOperators = new Set(Object.values(ComparisonOperators));
var BaseRuleEvaluator = _BaseRuleEvaluator;
// src/core/services/rule-engine.ts
var RuleEngine = class {
constructor(config) {
this.config = {
evaluator: new BaseRuleEvaluator(),
maxBatchSize: 1e3,
...config
};
this.evaluator = this.config.evaluator;
}
/**
* Finds entities that satisfy all provided 'from' rules.
*/
findMatchingFrom(entities, rules) {
const results = this.processBatch(entities, rules);
return entities.filter((_, index) => results[index]);
}
/**
* Finds target entities that can be matched with source entities based on 'to' rules.
*/
findMatchingTo(fromEntities, toRules, allEntities) {
const candidateEntities = allEntities.filter(
(entity) => !fromEntities.some((from) => from.id === entity.id)
);
const results = this.processBatch(candidateEntities, toRules);
return candidateEntities.filter((_, index) => results[index]);
}
/**
* Splits entities into optimal batch sizes based on complexity
*/
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 > 10) batchSize = Math.floor(batchSize / 2);
if (ruleComplexity > 20) batchSize = Math.floor(batchSize / 4);
return entityCount + batchSize;
}
/**
* Processes entities in optimally-sized batches
*/
processBatch(entities, rules) {
const batchSize = this.getBatchSize(entities, rules);
const results = new Array(entities.length);
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 batch = entities.slice(start, end);
const batchResults = batch.map((entity) => {
const ruleResults = rules.map((rule) => this.evaluator.evaluateRule(entity, rule));
return ruleResults.every(Boolean);
});
batchResults.forEach((result, batchIndex) => {
results[start + batchIndex] = result;
});
}
return results;
}
/**
* Processes a complete matching configuration to find both source and target entities.
*/
processConfig(config, entities) {
if (!config.isActive) {
return { fromEntities: [], toEntities: [] };
}
const fromEntities = this.findMatchingFrom(entities, config.ruleSet.fromRules);
const toEntities = this.findMatchingTo(fromEntities, config.ruleSet.toRules, entities);
return { fromEntities, toEntities };
}
};
// src/core/attributes/types.ts
var AttributeType = {
STRING: "string",
NUMBER: "number",
BOOLEAN: "boolean",
DATE: "date",
ENUM: "enum",
ARRAY: "array"
};
// src/core/attributes/validator.ts
var AttributeValidationError = class extends Error {
constructor(attributeName, message) {
super(`Validation failed for attribute "${attributeName}": ${message}`);
this.attributeName = attributeName;
this.name = "AttributeValidationError";
}
};
async function validateAttribute(name, value, definition, allAttributes) {
const { validation } = definition;
if (validation.required && (value === void 0 || value === null)) {
throw new AttributeValidationError(name, "Value is required");
}
if (validation.custom) {
const isValid = await validation.custom(value, allAttributes);
if (!isValid) {
throw new AttributeValidationError(name, "Custom validation failed");
}
}
if (value === void 0 || value === null) {
return;
}
await validateType(name, value, validation);
switch (validation.type) {
case AttributeType.STRING:
validateString(name, value, validation);
break;
case AttributeType.NUMBER:
validateNumber(name, value, validation);
break;
case AttributeType.ENUM:
validateEnum(name, value, validation);
break;
case AttributeType.ARRAY:
await validateArray(name, value, validation);
break;
}
}
async function validateType(name, value, validation) {
const expectedType = validation.type;
let isValid = false;
switch (expectedType) {
case AttributeType.STRING:
isValid = typeof value === "string";
break;
case AttributeType.NUMBER:
isValid = typeof value === "number" && !isNaN(value);
break;
case AttributeType.BOOLEAN:
isValid = typeof value === "boolean";
break;
case AttributeType.DATE:
isValid = value instanceof Date && !isNaN(value.getTime());
break;
case AttributeType.ENUM:
isValid = typeof value === "string";
break;
case AttributeType.ARRAY:
isValid = Array.isArray(value);
break;
}
if (!isValid) {
throw new AttributeValidationError(
name,
`Invalid type. Expected ${expectedType}, got ${typeof value}`
);
}
}
function validateString(name, value, validation) {
if (validation.min !== void 0 && value.length < validation.min) {
throw new AttributeValidationError(name, `String length must be at least ${validation.min}`);
}
if (validation.max !== void 0 && value.length > validation.max) {
throw new AttributeValidationError(name, `String length must be at most ${validation.max}`);
}
if (validation.pattern) {
const regex = new RegExp(validation.pattern);
if (!regex.test(value)) {
throw new AttributeValidationError(name, `Value must match pattern: ${validation.pattern}`);
}
}
}
function validateNumber(name, value, validation) {
if (validation.min !== void 0 && value < validation.min) {
throw new AttributeValidationError(name, `Value must be at least ${validation.min}`);
}
if (validation.max !== void 0 && value > validation.max) {
throw new AttributeValidationError(name, `Value must be at most ${validation.max}`);
}
}
function validateEnum(name, value, validation) {
if (!validation.enum?.includes(value)) {
throw new AttributeValidationError(
name,
`Value must be one of: ${validation.enum?.join(", ")}`
);
}
}
async function validateArray(name, value, validation) {
if (validation.min !== void 0 && value.length < validation.min) {
throw new AttributeValidationError(name, `Array must have at least ${validation.min} items`);
}
if (validation.max !== void 0 && value.length > validation.max) {
throw new AttributeValidationError(name, `Array must have at most ${validation.max} items`);
}
if (validation.arrayType) {
await Promise.all(
value.map(
(item, index) => validateType(`${name}[${index}]`, item, {
type: validation.arrayType
})
)
);
}
}
// src/core/attributes/registry.ts
var ProductAttributeRegistry = class {
constructor() {
this.registry = /* @__PURE__ */ new Map();
}
/**
* Registers a new attribute definition in the registry.
* Throws if an attribute with the same name already exists.
*
* @param {AttributeDefinition} definition - The attribute definition to register
* @throws {Error} If attribute name is already registered
*
* @remarks
* - Attribute names must be unique
* - Definition includes metadata and validation rules
* - Once registered, attributes can be used for validation
*
* @example
* ```typescript
* registry.registerAttribute({
* name: 'category',
* type: AttributeType.ENUM,
* description: 'Product category',
* validation: {
* type: AttributeType.ENUM,
* required: true,
* enum: ['electronics', 'clothing', 'books']
* }
* });
* ```
*/
registerAttribute(definition) {
if (this.registry.has(definition.name)) {
throw new Error(`Attribute "${definition.name}" is already registered`);
}
this.registry.set(definition.name, definition);
}
/**
* Retrieves an attribute definition by its name.
* Returns undefined if the attribute is not found.
*
* @param {string} name - Name of the attribute to retrieve
* @returns {AttributeDefinition | undefined} The attribute definition or undefined
*
* @example
* ```typescript
* const priceDef = registry.getAttribute('price');
* if (priceDef) {
* console.log(`Price validation: ${JSON.stringify(priceDef.validation)}`);
* }
* ```
*/
getAttribute(name) {
return this.registry.get(name);
}
/**
* Removes an attribute definition from the registry.
* Returns true if the attribute was found and removed.
*
* @param {string} name - Name of the attribute to remove
* @returns {boolean} True if attribute was removed, false if not found
*
* @example
* ```typescript
* if (registry.removeAttribute('oldField')) {
* console.log('Old field definition removed');
* }
* ```
*/
removeAttribute(name) {
return this.registry.delete(name);
}
/**
* Returns an array of all registered attribute definitions.
* Useful for inspecting or iterating over all attributes.
*
* @returns {AttributeDefinition[]} Array of all attribute definitions
*
* @example
* ```typescript
* const allAttributes = registry.getAllAttributes();
* console.log(`Registered attributes: ${allAttributes.map(a => a.name).join(', ')}`);
* ```
*/
getAllAttributes() {
return Array.from(this.registry.values());
}
/**
* Validates a set of attributes against their registered definitions.
* Performs both basic and custom validations in the correct order.
*
* @param {Record<string, unknown>} attributes - Object containing attribute values
* @throws {Error} If validation fails or unknown attributes are found
*
* @remarks
* Validation process:
* 1. Checks for required attributes
* 2. Validates known attributes
* 3. Rejects unknown attributes
* 4. Runs basic validations first
* 5. Runs custom validations last
*
* @example
* ```typescript
* try {
* await registry.validateAttributes({
* price: 1999,
* category: 'electronics',
* inStock: true
* });
* console.log('Attributes are valid');
* } catch (error) {
* console.error('Validation failed:', error.message);
* }
* ```
*/
async validateAttributes(attributes) {
const basicValidationPromises = [];
const customValidationPromises = [];
for (const [name, definition] of this.registry.entries()) {
if (definition.validation.required && !(name in attributes)) {
throw new Error(`Missing required attribute: ${name}`);
}
if (definition.validation.custom) {
const value = attributes[name];
customValidationPromises.push(validateAttribute(name, value, definition, attributes));
}
}
for (const [name, value] of Object.entries(attributes)) {
const definition = this.registry.get(name);
if (!definition) {
throw new Error(`Unknown attribute: ${name}`);
}
if (!definition.validation.custom) {
basicValidationPromises.push(validateAttribute(name, value, definition, attributes));
}
}
await Promise.all(basicValidationPromises);
await Promise.all(customValidationPromises);
}
/**
* Removes all attribute definitions from the registry.
* Useful when resetting the registry or changing attribute schemas.
*
* @example
* ```typescript
* // Clear existing definitions
* registry.clear();
*
* // Register new definitions
* registry.registerAttribute(newDefinition);
* ```
*/
clear() {
this.registry.clear();
}
};
// src/core/interface/components/types.ts
var ComponentType = {
INPUT: "input",
CHOICE: "choice",
MULTI: "multi",
RANGE: "range"
};
var ComponentError = class extends Error {
constructor(message) {
super(`Component Error: ${message}`);
this.name = "ComponentError";
}
};
// src/core/interface/components/operators.ts
var InterfaceOperators = {
...ComparisonOperators,
between: "between",
exists: "exists"
};
// src/core/interface/adapters/types.ts
var AdapterError = class extends Error {
constructor(message) {
super(`Adapter Error: ${message}`);
this.name = "AdapterError";
}
};
var BaseComponentAdapter = class {
/**
* Default validation implementation that can be extended
*/
validate(value, component) {
if (component.constraints?.required && value == null) {
return false;
}
if (typeof value === "number" && component.constraints) {
const { min, max } = component.constraints;
if (min != null && value < min) return false;
if (max != null && value > max) return false;
}
if (typeof value === "string" && component.constraints?.pattern) {
const regex = new RegExp(component.constraints.pattern);
return regex.test(value);
}
return true;
}
/**
* Helper method to ensure a component is of a specific type
*/
ensureType(component, type, methodName) {
if (component.type !== type) {
throw new AdapterError(
`${methodName} can only be used with ${type} components, but received ${component.type}`
);
}
}
};
// src/core/interface/converters/rule-converter.ts
var RuleConversionError = class extends Error {
constructor(message) {
super(`Rule Conversion Error: ${message}`);
this.name = "RuleConversionError";
}
};
var RuleConverter = class {
/**
* Convert a component's value to a rule filter
*/
convertComponentToFilter(component) {
switch (component.type) {
case ComponentType.RANGE:
return this.convertRangeComponent(component);
case ComponentType.CHOICE:
return this.convertChoiceComponent(component);
case ComponentType.MULTI:
return this.convertMultiComponent(component);
case ComponentType.INPUT:
return this.convertInputComponent(component);
default:
throw new RuleConversionError(
`Unsupported component type: ${component.type}`
);
}
}
/**
* Convert a range component to a filter
*/
convertRangeComponent(component) {
const { value, constraints } = component;
if (value === 0 || !value) {
return {
[InterfaceOperators.between]: [constraints.min, constraints.max]
};
}
return {
[InterfaceOperators.eq]: value
};
}
/**
* Convert a choice component to a filter
*/
convertChoiceComponent(component) {
const { value } = component;
if (!value) {
return {
[InterfaceOperators.in]: component.options.map((opt) => opt.value)
};
}
return {
[InterfaceOperators.eq]: value
};
}
/**
* Convert a multi-select component to a filter
*/
convertMultiComponent(component) {
const multiComponent = component;
if (!Array.isArray(multiComponent.value)) {
throw new RuleConversionError("Invalid component type for multi conversion");
}
return {
[InterfaceOperators.in]: multiComponent.value
};
}
/**
* Convert an input component to a filter
*/
convertInputComponent(component) {
if (!component.value) {
return {
[InterfaceOperators.exists]: true
};
}
return {
[InterfaceOperators.eq]: component.value
};
}
/**
* Convert a set of components to a complete rule
*/
convertComponentsToRule(components) {
const rules = components.map(({ field, component }) => ({
attributes: {
[field]: this.convertComponentToFilter(component)
}
}));
return rules.length > 1 ? { and: rules } : rules[0];
}
};
// src/core/analysis/types.ts
var DataType = {
NUMBER: "number",
STRING: "string",
BOOLEAN: "boolean",
DATE: "date"
};
var AnalysisError = class extends Error {
constructor(message) {
super(`Analysis Error: ${message}`);
this.name = "AnalysisError";
}
};
// src/core/analysis/analyzer.ts
var DEFAULT_OPTIONS = {
maxChoiceOptions: 20,
includeDetailedStats: true,
componentSuggestionRules: []
};
var DataAnalyzer = class {
constructor(options = {}) {
this.options = { ...DEFAULT_OPTIONS, ...options };
}
/**
* Analyzes an array of data objects to generate field analyses and component suggestions.
*/
analyze(data) {
if (!Array.isArray(data) || data.length === 0) {
throw new AnalysisError("Data must be a non-empty array of objects");
}
const analysis = {};
const fields = this.extractFields(data);
for (const field of fields) {
const values = data.map((item) => item[field]);
const dataType = this.detectDataType(values);
const statistics = this.calculateStatistics(values, dataType);
const suggestedComponent = this.suggestComponent(field, statistics, dataType);
analysis[field] = {
fieldName: field,
dataType,
statistics,
suggestedComponent
};
}
return analysis;
}
/**
* Extracts unique field names from the data.
*/
extractFields(data) {
const fields = /* @__PURE__ */ new Set();
data.forEach((item) => {
Object.keys(item).forEach((key) => fields.add(key));
});
return Array.from(fields);
}
/**
* Detects the data type of a field based on its values.
*/
detectDataType(values) {
const nonNullValues = values.filter((v) => v != null);
if (nonNullValues.length === 0) return DataType.STRING;
if (nonNullValues.some((v) => Array.isArray(v))) return DataType.STRING;
const types = new Set(nonNullValues.map((v) => typeof v));
if (types.has("number")) return DataType.NUMBER;
if (types.has("boolean")) return DataType.BOOLEAN;
if (nonNullValues.every((v) => !isNaN(Date.parse(String(v))))) return DataType.DATE;
return DataType.STRING;
}
/**
* Calculates statistics for a field based on its values and data type.
*/
calculateStatistics(values, dataType) {
const nonNullValues = values.filter((v) => v != null);
const statistics = {
count: values.length,
uniqueValues: new Set(values.map((v) => Array.isArray(v) ? JSON.stringify(v) : v)).size,
nullCount: values.length - nonNullValues.length
};
if (dataType === DataType.NUMBER) {
const numbers = nonNullValues.map((v) => Number(v));
const numeric = this.calculateNumericStatistics(numbers);
return { ...statistics, numeric };
}
if (dataType === DataType.STRING || dataType === DataType.BOOLEAN) {
const categorical = this.calculateCategoryStatistics(nonNullValues);
return { ...statistics, categorical };
}
return statistics;
}
/**
* Calculates numeric statistics for number fields.
*/
calculateNumericStatistics(numbers) {
const sorted = [...numbers].sort((a, b) => a - b);
const sum = numbers.reduce((a, b) => a + b, 0);
const average = sum / numbers.length;
let median;
const mid = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
median = (sorted[mid - 1] + sorted[mid]) / 2;
} else {
median = sorted[mid];
}
const squaredDiffs = numbers.map((n) => Math.pow(n - average, 2));
const variance = squaredDiffs.reduce((a, b) => a + b, 0) / (numbers.length - 1);
const standardDeviation = Math.sqrt(variance);
return {
min: Math.min(...numbers),
max: Math.max(...numbers),
average,
median,
standardDeviation
};
}
/**
* Calculates category statistics for string/boolean fields.
*/
calculateCategoryStatistics(values) {
const counts = /* @__PURE__ */ new Map();
values.forEach((value) => {
const key = Array.isArray(value) ? JSON.stringify(value) : String(value);
counts.set(key, (counts.get(key) || 0) + 1);
});
const categories = Array.from(counts.entries()).map(([value, count]) => ({
value,
count,
percentage: count / values.length * 100
}));
return { categories };
}
/**
* Suggests an appropriate component based on field analysis.
*/
suggestComponent(fieldName, statistics, dataType) {
for (const rule of this.options.componentSuggestionRules) {
if (rule.condition({ fieldName, dataType, statistics })) {
return rule.suggest({ fieldName, dataType, statistics });
}
}
const hasArrays = statistics.categorical?.categories.some((cat) => cat.value.startsWith("["));
if (hasArrays) {
return this.suggestMultiComponent(fieldName, statistics);
}
switch (dataType) {
case DataType.NUMBER:
return this.suggestNumericComponent(fieldName, statistics);
case DataType.BOOLEAN:
return this.suggestBooleanComponent(fieldName, statistics);
case DataType.DATE:
return this.suggestDateComponent(fieldName);
default:
return this.suggestStringComponent(fieldName, statistics);
}
}
/**
* Suggests a component for numeric fields.
*/
suggestNumericComponent(fieldName, statistics) {
if (!statistics.numeric) {
throw new AnalysisError("Missing numeric statistics for numeric field");
}
return {
type: ComponentType.RANGE,
identifier: fieldName,
value: 0,
constraints: {
min: statistics.numeric.min,
max: statistics.numeric.max,
step: this.calculateStep(statistics.numeric)
},
metadata: this.options.includeDetailedStats ? { statistics: statistics.numeric } : void 0
};
}
/**
* Suggests a component for boolean fields.
*/
suggestBooleanComponent(fieldName, statistics) {
return {
type: ComponentType.CHOICE,
identifier: fieldName,
value: "",
options: [
{ identifier: "true", value: "True" },
{ identifier: "false", value: "False" }
],
metadata: this.options.includeDetailedStats ? { statistics: statistics.categorical } : void 0
};
}
/**
* Suggests a component for date fields.
*/
suggestDateComponent(fieldName) {
return {
type: ComponentType.INPUT,
identifier: fieldName,
value: "",
format: "date"
};
}
/**
* Suggests a component for string fields.
*/
suggestStringComponent(fieldName, statistics) {
if (statistics.uniqueValues > this.options.maxChoiceOptions) {
return {
type: ComponentType.INPUT,
identifier: fieldName,
value: "",
format: "text"
};
}
if (!statistics.categorical) {
throw new AnalysisError("Missing categorical statistics for string field");
}
return {
type: ComponentType.CHOICE,
identifier: fieldName,
value: "",
options: statistics.categorical.categories.map((cat) => ({
identifier: cat.value,
value: cat.value,
metadata: {
count: cat.count,
percentage: cat.percentage
}
})),
metadata: this.options.includeDetailedStats ? { statistics: statistics.categorical } : void 0
};
}
/**
* Suggests a component for array fields.
*/
suggestMultiComponent(fieldName, statistics) {
if (!statistics.categorical) {
throw new AnalysisError("Missing categorical statistics for array field");
}
const allValues = /* @__PURE__ */ new Set();
statistics.categorical.categories.forEach((cat) => {
try {
const values = JSON.parse(cat.value);
if (Array.isArray(values)) {
values.forEach((v) => allValues.add(String(v)));
}
} catch {
}
});
return {
type: ComponentType.MULTI,
identifier: fieldName,
value: [],
options: Array.from(allValues).map((value) => ({
identifier: value,
value
})),
metadata: this.options.includeDetailedStats ? { statistics: statistics.categorical } : void 0
};
}
/**
* Calculates an appropriate step value for range components.
*/
calculateStep(stats) {
const range = stats.max - stats.min;
const magnitude = Math.floor(Math.log10(range));
return Math.pow(10, magnitude - 1);
}
};
// src/core/ui/types.ts
var UIConditionType = {
IS: "Is",
IS_NOT: "IsNot",
CONTAINS: "Contains",
DOES_NOT_CONTAIN: "DoesNotContain",
IN: "In"
};
var UIComponentType = {
SELECT: "select",
TEXT: "text",
OPTIONS: "options",
MULTISELECTOR: "MULTISELECTOR"
};
var UIConfigurationError = class extends Error {
constructor(message) {
super(`UI Configuration Error: ${message}`);
this.name = "UIConfigurationError";
}
};
// src/core/ui/converter.ts
function getOperator(condition) {
if (condition.type === UIComponentType.TEXT && condition.max !== void 0) {
return ComparisonOperators.lte;
}
if (condition.type === UIComponentType.TEXT && condition.min !== void 0) {
return ComparisonOperators.gte;
}
const operatorMap = {
[UIConditionType.IS]: ComparisonOperators.eq,
[UIConditionType.IS_NOT]: ComparisonOperators.ne,
[UIConditionType.CONTAINS]: ComparisonOperators.in,
[UIConditionType.DOES_NOT_CONTAIN]: ComparisonOperators.notIn,
[UIConditionType.IN]: ComparisonOperators.in
};
return operatorMap[condition.condition];
}
function convertConditionToFilter(condition, value) {
const operator = getOperator(condition);
if (!operator) {
throw new UIConfigurationError(`Unknown condition type: ${condition.condition}`);
}
if (!Array.isArray(value)) {
value = [value];
}
if (condition.type === UIComponentType.TEXT && Array.isArray(value)) {
const isPrice = condition.max !== void 0 || condition.min !== void 0;
if (isPrice && value[0] !== null) {
const num = Number(value[0]);
if (!isNaN(num)) {
value = [num];
}
}
}
if ((operator === ComparisonOperators.gt || operator === ComparisonOperators.gte || operator === ComparisonOperators.lt || operator === ComparisonOperators.lte) && Array.isArray(value) && value.length === 1) {
return {
[operator]: value[0]
};
}
return {
[operator]: value
};
}
function convertFiltersToRules(filters) {
return filters.map((filter) => {
const rules = filter.conditions.map((condition) => ({
attributes: {
[filter.name]: convertConditionToFilter(condition, condition.max ?? condition.min ?? null)
}
}));
return rules.length > 1 ? { and: rules } : rules[0];
});
}
function convertMatchingRulesToRules(rules) {
return rules.map((rule) => {
const ruleConditions = rule.conditions.map((condition) => ({
attributes: {
[rule.name]: convertConditionToFilter(condition, rule.values ?? null)
}
}));
return ruleConditions.length > 1 ? { and: ruleConditions } : ruleConditions[0];
});
}
function convertUIConfigurationToRules(config) {
const fromRules = [];
const toRules = [];
if (config.filters?.length) {
fromRules.push(...convertFiltersToRules(config.filters));
}
if (config.matchingFrom?.length) {
fromRules.push(...convertMatchingRulesToRules(config.matchingFrom));
}
if (config.matchingTo?.length) {
toRules.push(...convertMatchingRulesToRules(config.matchingTo));
}
if (config.source?.length) {
fromRules.push(...convertMatchingRulesToRules(config.source));
}
if (config.recommendations?.length) {
toRules.push(...convertMatchingRulesToRules(config.recommendations));
}
return {
fromRules,
toRules
};
}
function validateUIConfiguration(config) {
if (!config.filters?.length && !config.matchingFrom?.length && !config.matchingTo?.length && !config.source?.length && !config.recommendations?.length) {
throw new UIConfigurationError("Configuration must contain at least one rule type");
}
config.filters?.forEach((filter) => {
if (!filter.name) {
throw new UIConfigurationError("Filter must have a name");
}
if (!filter.conditions?.length) {
throw new UIConfigurationError(`Filter "${filter.name}" must have at least one condition`);
}
});
const validateMatchingRules = (rules, type) => {
rules.forEach((rule) => {
if (!rule.name) {
throw new UIConfigurationError(`${type} must have a name`);
}
if (!rule.conditions?.length) {
throw new UIConfigurationError(`${type} "${rule.name}" must have at least one condition`);
}
if (!rule.values?.length) {
throw new UIConfigurationError(`${type} "${rule.name}" must have at least one value`);
}
});
};
if (config.matchingFrom?.length) {
validateMatchingRules(config.matchingFrom, "From");
}
if (config.matchingTo?.length) {
validateMatchingRules(config.matchingTo, "To");
}
if (config.source?.length) {
validateMatchingRules(config.source, "From");
}
if (config.recommendations?.length) {
validateMatchingRules(config.recommendations, "To");
}
}
// src/v3/types/schema.ts
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/v2.ts
var v2 = {
ruleEngine: RuleEngine,
validation: validation_exports,
ComparisonOperators
};
export {
AdapterError,
AnalysisError,
AttributeType,
AttributeValidationError,
BaseComponentAdapter,
BaseRuleEvaluator,
ComparisonOperators,
ComponentError,
ComponentType,
DataAnalyzer,
DataType,
InterfaceOperators,
ProductAttributeRegistry,
RuleConversionError,
RuleConverter,
RuleEngine,
RuleValidationError,
UIComponentType,
UIConditionType,
UIConfigurationError,
convertUIConfigurationToRules,
isValidAttributeValue,
isValidSchemaObject,
v2,
validateAttribute,
validateMatchingConfig,
validateRule,
validateRuleSet,
validateUIConfiguration
};