UNPKG

@phr3nzy/rulekit

Version:

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

1,295 lines (1,276 loc) 41.2 kB
"use strict"; 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, { AdapterError: () => AdapterError, AnalysisError: () => AnalysisError, AttributeType: () => AttributeType, AttributeValidationError: () => AttributeValidationError, BaseComponentAdapter: () => BaseComponentAdapter, BaseRuleEvaluator: () => BaseRuleEvaluator, ComparisonOperators: () => ComparisonOperators, ComponentError: () => ComponentError, ComponentType: () => ComponentType, DataAnalyzer: () => DataAnalyzer, DataType: () => DataType, InterfaceOperators: () => InterfaceOperators, ProductAttributeRegistry: () => ProductAttributeRegistry, RuleConversionError: () => RuleConversionError, RuleConverter: () => RuleConverter, RuleEngine: () => RuleEngine, RuleValidationError: () => RuleValidationError, UIComponentType: () => UIComponentType, UIConditionType: () => UIConditionType, UIConfigurationError: () => UIConfigurationError, convertUIConfigurationToRules: () => convertUIConfigurationToRules, isValidAttributeValue: () => isValidAttributeValue, isValidSchemaObject: () => isValidSchemaObject, v2: () => v2, validateAttribute: () => validateAttribute, validateMatchingConfig: () => validateMatchingConfig, validateRule: () => validateRule, validateRuleSet: () => validateRuleSet, validateUIConfiguration: () => validateUIConfiguration }); module.exports = __toCommonJS(index_exports); // 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 }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { 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 });