@skedulo/eql
v0.8.0
Published
A TypeScript parser for Skedulo's Entity Query Language (EQL). This library provides tools for parsing and evaluating EQL filter expressions.
Maintainers
Keywords
Readme
@skedulo/eql
A TypeScript parser for Skedulo's Entity Query Language (EQL). This library provides tools for parsing and evaluating EQL filter expressions.
Installation
npm install @skedulo/eql
# or
yarn add @skedulo/eqlUsage
You can use this library in several ways:
1. Parse and evaluate a filter expression in one step:
import { evaluateFilter } from "@skedulo/eql";
const event = {
Current: { JobStatus: "Ready" },
Previous: { JobStatus: "Dispatched" },
};
const result = evaluateFilter("Current.JobStatus == 'Ready'", event);
console.log(result); // true2. Validate a filter without a schema:
validateFilter returns whether an expression is safe to pass to
evaluateFilter. It returns false for unparseable input and for expressions
that parse but cannot be evaluated (e.g. a bare banana with no operator).
import { validateFilter, evaluateFilter } from "@skedulo/eql";
if (validateFilter(expr)) {
return evaluateFilter(expr, event);
}
validateFilter("Current.JobStatus == 'Ready'"); // true
validateFilter("banana"); // false3. Parse and evaluate separately:
import { parseFilter, evaluateExpression } from "@skedulo/eql";
// Parse the filter expression into an AST
const ast = parseFilter("Current.JobStatus == 'Ready'");
// Later, evaluate it against an event
const result = evaluateExpression(ast, event);4. Validate a filter expression against a schema:
Schema validation is intentionally stricter than the evaluator — it mirrors elasticserver's EQL type checker (elasticorm), so it catches filters that the evaluator would silently tolerate (e.g. Description == 1 evaluates to false, but fails schema validation). For record-change filters, nest the entity schema under Current/Previous object fields.
import { validateFilterAgainstSchema, FieldSchema } from "@skedulo/eql";
const jobSchema: FieldSchema = {
UID: { type: "string", required: true },
JobStatus: { type: "string", allowedValues: ["Pending", "InProgress", "Complete"] },
Start: { type: "date" },
Duration: { type: "duration" },
Count: { type: "integer" },
Tags: { type: "list", elementType: "string" },
Region: {
type: "object",
fields: {
Name: { type: "string" },
},
},
};
const result = validateFilterAgainstSchema("Region.Name == 'Sydney' AND Start > 2024-01-01", jobSchema);
console.log(result); // { valid: true, errors: [] }
const result2 = validateFilterAgainstSchema("Foo == 'bar' AND Start LIKE '%test%'", jobSchema);
console.log(result2);
// {
// valid: false,
// errors: [
// { type: "unknown_field", path: "Foo", message: "Unknown field 'Foo'" },
// { type: "type_mismatch", path: "Start", message: "LIKE requires a string field, got date" },
// ]
// }The validator checks:
- Field existence — unknown fields and dot-path traversal
- Type/operator compatibility — e.g.,
LIKErequires a string field,INCLUDESrequires a list field;date/time,duration/time, anddatetimevsdate/timeare incompatible - Numeric types —
integervsdecimal; only an integer literal widens todecimal/duration(one way), matching elasticorm's Int→Decimal coercion requiredfields — reject anullcomparison (null_comparison)allowedValues— reject a value outside the field's allowed set (disallowed_value)- List element types — validates
INlist elements andINCLUDES/EXCLUDESvalues match the field's element type
5. Convert AST back to string (round-trip conversion):
import { parseFilter, stringify } from "@skedulo/eql";
// Parse a query into an AST
const ast = parseFilter("Current.JobStatus == 'Ready' AND priority > 5");
// Convert the AST back to a string
const queryString = stringify(ast);
console.log(queryString); // "Current.JobStatus == 'Ready' AND priority > 5"This is useful for:
- Query transformation and optimization
- Normalizing query format (e.g., quote styles)
- Building query builders or editors
- Serializing parsed queries for storage
6. Use the types for your own implementations:
import { Expression, Event } from "@skedulo/eql";
function customEvaluator(expr: Expression, event: Event) {
// Your custom evaluation logic
}Supported Operations
- Comparison operators:
==,!=,<,<=,>,>= - String pattern matching:
LIKE,NOTLIKE(with%and_wildcards) - List membership:
IN,NOTIN - List containment:
INCLUDES,EXCLUDES - Logical operators:
AND,OR - Parentheses for grouping
Examples
// Basic equality
"Current.JobStatus == 'Ready'";
// Comparison between paths
"Current.JobStatus != Previous.JobStatus";
// Pattern matching
"Current.Description LIKE '%urgent%'";
// List membership
"Current.Status IN ['Open', 'InProgress']";
// Complex expressions
"(Current.Status == 'Open' OR Current.Status == 'InProgress') AND Current.Priority == 'High'";AST to String Conversion
The stringify function converts parsed AST expressions back to EQL query strings. This enables round-trip conversion and query manipulation:
Basic Usage
import { parseFilter, stringify } from "@skedulo/eql";
const ast = parseFilter("operation == 'INSERT'");
const queryString = stringify(ast);
// Result: "operation == 'INSERT'"Quote Style
String literals must use single quotes. Double-quoted strings are rejected by the parser:
parseFilter("operation == 'INSERT'"); // OK
parseFilter('operation == "INSERT"'); // throws FilterParseErrorComplex Expressions
The stringifier handles complex nested expressions with proper parenthesization:
const complexQuery = "(operation == 'UPDATE' OR operation == 'INSERT') AND Current.status != Previous.status";
const ast = parseFilter(complexQuery);
const regenerated = stringify(ast);
// Result: exact same string with proper parentheses preservedSupported Features
- ✅ All comparison operators (
==,!=,<,<=,>,>=,LIKE,NOTLIKE,IN,NOTIN) - ✅ Logical operators (
AND,OR) with proper parenthesization - ✅ All literal types (strings, numbers, booleans, null, dates, datetimes, times, durations)
- ✅ List literals with mixed types
- ✅ Nested path expressions (e.g.,
Current.job.status) - ✅ Quote normalization and escaping
Releasing
- Bump the version in
package.jsonand merge tomainvia PR - Create a GitHub Release with tag
v<version>(e.g.v0.5.4) targetingmain - The npm publish happens automatically via CI
