standard-rule-engine
Version:
A simple rule engine that uses Standard Schema to validate facts
168 lines (166 loc) • 4.21 kB
JavaScript
// src/utils.ts
var isNotEmpty = (obj) => {
if (!obj) return false;
for (const x in obj) return true;
return false;
};
var isClass = (v) => typeof v === "function" && /^\s*class\s+/.test(v.toString()) || // Handle Object.create(null)
v.toString && // Handle import * as Sentry from '@sentry/bun'
// This also handle [object Date], [object Array]
// and FFI value like [object Prisma]
v.toString().startsWith("[object ") && v.toString() !== "[object Object]" || // If object prototype is not pure, then probably a class-like object
isNotEmpty(Object.getPrototypeOf(v));
var isObject = (item) => item && typeof item === "object" && !Array.isArray(item);
var mergeDeep = (target, source, {
skipKeys,
override = true
} = {}) => {
if (!isObject(target) || !isObject(source)) return target;
for (const [key, value] of Object.entries(source)) {
if (skipKeys?.includes(key)) continue;
if (!isObject(value) || !(key in target) || isClass(value)) {
if (override || !(key in target))
target[key] = value;
continue;
}
target[key] = mergeDeep(
target[key],
value,
{ skipKeys, override }
);
}
return target;
};
function standardValidate(schema, input) {
let result = schema["~standard"].validate(input);
if (result instanceof Promise) {
throw new Error("Facts input must be synchronous");
}
if (result.issues) {
return {
success: false,
issues: result.issues,
data: null
};
}
return {
success: true,
issues: null,
data: result.value
};
}
// src/index.ts
import clone from "clone";
var Session = class {
constructor(context, rules, helpers = {}) {
this.context = context;
this.rules = rules;
this.wrappedHelpers = Object.entries(helpers).reduce(
(acc, [key, fn]) => {
acc[key] = (...args) => {
return fn(context, ...args);
};
return acc;
},
{}
);
}
insertedFacts = [];
wrappedHelpers;
insert(facts) {
this.insertedFacts.push(facts);
return this;
}
insertMany(facts) {
this.insertedFacts.push(...facts);
return this;
}
fire() {
for (const facts of this.insertedFacts) {
Object.freeze(facts);
for (const rule of this.rules) {
if (!rule.schema) {
rule.handler(facts, {
context: this.context,
helpers: this.wrappedHelpers
});
continue;
}
const validationResult = standardValidate(rule.schema, facts);
if (!validationResult.success) {
continue;
}
rule.handler(validationResult.data, {
context: this.context,
helpers: this.wrappedHelpers
});
}
}
return this;
}
};
var Engine = class {
"~types" = {
Singleton: {}
};
initialContext = {};
rules = [];
globalSchema;
helpers = {};
schema(schema) {
this.globalSchema = schema;
return this;
}
context(nameOrContext, value) {
if (value === void 0 && typeof nameOrContext === "object") {
this.initialContext = mergeDeep(this.initialContext, nameOrContext);
} else if (typeof nameOrContext === "string") {
this.initialContext = mergeDeep(this.initialContext, {
[nameOrContext]: value
});
}
return this;
}
helper(name, fn) {
this.helpers = {
...this.helpers,
[name]: fn
};
return this;
}
rule(name, handler, meta) {
const newRule = {
name,
handler,
priority: meta?.priority ?? 1,
schema: meta?.schema ?? this.globalSchema
};
this.rules.push(newRule);
this.rules.sort((a, b) => {
if (a.priority !== b.priority) {
return a.priority - b.priority;
}
return a.name.localeCompare(b.name);
});
return this;
}
use(instance) {
this.rules = [...this.rules, ...instance.rules];
this.initialContext = mergeDeep(
this.initialContext,
instance.initialContext
);
this.helpers = mergeDeep(this.helpers, instance.helpers);
return this;
}
createSession() {
return new Session(
clone(this.initialContext),
this.rules,
this.helpers
);
}
};
export {
Engine
};