@ruhisfi/rql
Version:
`RQL` (Ruhis Query Language) is a powerful library designed to simplify the process of filtering, sorting, and aggregating large amounts of data. With RQL, you can effortlessly extract valuable insights from complex datasets, making data analysis and mani
1,391 lines (1,374 loc) • 45 kB
JavaScript
// src/statement/AbstractStatement.ts
var AbstractStatement = class {
};
// src/dynamicField.ts
var dynamicField_default = (field, data) => {
const fieldPath = field.split(".");
for (const path of fieldPath) {
if (data == null || !Object.prototype.hasOwnProperty.call(data, path)) {
data = null;
break;
}
data = data[path];
}
return data;
};
// src/functionalField.ts
import ipRangeCheck from "ip-range-check";
import { createHash } from "crypto";
var functionalField_default = (field, data) => {
const match = field.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*)\)$/);
if (!match) {
return null;
}
const functionName = match[1];
const rawArgs = match[2].split(new RegExp("(?<!\\\\),")).map((arg) => arg.trim()).filter((arg) => arg.length > 0).map((arg) => {
let trimmedArg = arg.trim();
if (trimmedArg.startsWith('"') && trimmedArg.endsWith('"')) {
trimmedArg = trimmedArg.slice(1, -1);
} else if (trimmedArg.startsWith("'") && trimmedArg.endsWith("'")) {
trimmedArg = trimmedArg.slice(1, -1);
}
return trimmedArg;
});
const args = match[2].split(new RegExp("(?<!\\\\),")).map((arg) => arg.trim()).filter((arg) => arg.length > 0).map((arg) => {
let trimmedArg = arg.trim();
if (trimmedArg.startsWith('"') && trimmedArg.endsWith('"')) {
trimmedArg = trimmedArg.slice(1, -1);
} else if (trimmedArg.startsWith("'") && trimmedArg.endsWith("'")) {
trimmedArg = trimmedArg.slice(1, -1);
}
const df = dynamicField_default(trimmedArg, data);
if (df !== null)
return df;
return trimmedArg;
});
switch (functionName) {
case "add": {
if (args.length !== 2) {
return null;
}
const [num1, num2] = args.map((arg) => parseFloat(arg));
if (isNaN(num1) || isNaN(num2)) {
return null;
}
return num1 + num2;
}
case "ago": {
if (args.length !== 1) {
return null;
}
const [dateString] = args;
const parsed = parseRelativeTime(dateString, "past");
if (!parsed) {
return null;
}
return parsed;
}
case "base64_decode": {
if (args.length !== 1) {
return null;
}
const [str] = args;
return Buffer.from(str, "base64").toString("utf-8");
}
case "base64_encode": {
if (args.length !== 1) {
return null;
}
const [str] = args;
return Buffer.from(str).toString("base64");
}
case "ceil": {
if (args.length !== 1) {
return null;
}
const [num] = args.map((arg) => parseFloat(arg));
if (isNaN(num)) {
return null;
}
return Math.ceil(num);
}
case "coalesce": {
if (args.length < 1) {
return null;
}
for (const arg of rawArgs) {
if (dynamicField_default(arg, data) !== null) {
return dynamicField_default(arg, data);
}
}
return null;
}
case "extract_url_host": {
if (args.length !== 1) {
return null;
}
const [url] = args;
try {
const parsedUrl = new URL(
!url.match(/^.{1,10}?:\/\//) ? "http://" + url : url
);
return parsedUrl.hostname;
} catch (err) {
return null;
}
}
case "floor": {
if (args.length !== 1) {
return null;
}
const [num] = args.map((arg) => parseFloat(arg));
if (isNaN(num)) {
return null;
}
return Math.floor(num);
}
case "fnv1a": {
if (args.length !== 1) {
return null;
}
const [str] = args;
let hash = 2166136261;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash = Math.imul(hash, 16777619);
}
return hash >>> 0;
}
case "future": {
if (args.length !== 1) {
return null;
}
const [dateString] = args;
const parsed = parseRelativeTime(dateString, "future");
if (!parsed) {
return null;
}
return parsed;
}
case "geo_distance": {
if (args.length !== 4) {
return null;
}
const [lat1, lon1, lat2, lon2] = args.map((arg) => parseFloat(arg));
if (isNaN(lat1) || isNaN(lon1) || isNaN(lat2) || isNaN(lon2)) {
return null;
}
const R = 6371;
const toRadians = (d) => d * Math.PI / 180;
const dLat = toRadians(lat2 - lat1);
const dLon = toRadians(lon2 - lon1);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
case "geo_in_polygon": {
if (args.length < 3) {
return null;
}
const [lat, lon] = args;
const latNum = parseFloat(lat);
const lonNum = parseFloat(lon);
let polygon = args.slice(2).join(",").replace(/\\,/g, ",");
if (polygon.startsWith("'") && polygon.endsWith("'") || polygon.startsWith('"') && polygon.endsWith('"')) {
polygon = polygon.slice(1, -1);
}
if (isNaN(latNum) || isNaN(lonNum)) {
return null;
}
let parsedPolygon;
try {
parsedPolygon = JSON.parse(polygon);
} catch (err) {
return null;
}
if (!parsedPolygon || parsedPolygon.type !== "Polygon" || !Array.isArray(parsedPolygon.coordinates) || parsedPolygon.coordinates.length === 0) {
return null;
}
const coordinates = parsedPolygon.coordinates[0];
if (!Array.isArray(coordinates) || coordinates.length < 3) {
return null;
}
let inside = false;
for (let i = 0, j = coordinates.length - 1; i < coordinates.length; j = i++) {
const [xi, yi] = coordinates[i];
const [xj, yj] = coordinates[j];
const intersect = yi > latNum !== yj > latNum && lonNum < (xj - xi) * (latNum - yi) / (yj - yi) + xi;
if (intersect)
inside = !inside;
}
return inside;
}
case "get": {
if (rawArgs.length !== 1) {
return null;
}
const [str] = rawArgs;
if (dynamicField_default(str, data) !== null) {
return dynamicField_default(str, data);
}
return null;
}
case "get_array": {
if (args.length !== 2) {
return null;
}
const [arr, index] = args;
if (!arr || !Array.isArray(arr)) {
return null;
}
const idx = parseInt(index, 10);
if (isNaN(idx)) {
return null;
}
if (idx < 0) {
return arr[arr.length + idx];
}
return arr[idx];
}
case "incidr": {
if (args.length !== 2) {
return null;
}
return ipRangeCheck(args[0], args[1]);
}
case "json_parse": {
if (args.length !== 1) {
return null;
}
const [jsonString] = args;
try {
return JSON.parse(jsonString);
} catch (err) {
return null;
}
}
case "json_stringify": {
if (args.length !== 1) {
return null;
}
const [jsonObject] = args;
return JSON.stringify(jsonObject);
}
case "length": {
if (args.length !== 1) {
return null;
}
const [str] = args;
if (!str || typeof str !== "string" && !Array.isArray(str)) {
return null;
}
return str.length;
}
case "lowercase": {
if (args.length !== 1) {
return null;
}
const [str] = args;
return str.toLowerCase();
}
case "md5": {
if (args.length !== 1) {
return null;
}
const [str] = args;
const hash = createHash("md5");
hash.update(str);
return hash.digest("hex");
}
case "multiply": {
if (args.length !== 2) {
return null;
}
const [num1, num2] = args.map((arg) => parseFloat(arg));
if (isNaN(num1) || isNaN(num2)) {
return null;
}
return num1 * num2;
}
case "now": {
return /* @__PURE__ */ new Date();
}
case "round": {
if (args.length !== 1) {
return null;
}
const [num] = args.map((arg) => parseFloat(arg));
if (isNaN(num)) {
return null;
}
return Math.round(num);
}
case "sha1": {
if (args.length !== 1) {
return null;
}
const [str] = args;
const hash = createHash("sha1");
hash.update(str);
return hash.digest("hex");
}
case "sha256": {
if (args.length !== 1) {
return null;
}
const [str] = args;
const hash = createHash("sha256");
hash.update(str);
return hash.digest("hex");
}
case "split": {
if (args.length !== 2) {
return null;
}
const [str, delimiter] = args;
const result = str.split(delimiter.replace("\\,", ","));
return result.length > 1 ? result : null;
}
case "substring": {
if (args.length !== 3) {
return null;
}
const [str, startArg, endArg] = args;
const start = parseInt(startArg, 10);
const end = parseInt(endArg, 10);
if (isNaN(start) || isNaN(end)) {
return null;
}
if (typeof str !== "string") {
return null;
}
return str.substring(start, end);
}
case "subtract": {
if (args.length !== 2) {
return null;
}
const [num1, num2] = args.map((arg) => parseFloat(arg));
if (isNaN(num1) || isNaN(num2)) {
return null;
}
return num1 - num2;
}
case "to_date": {
if (args.length !== 1) {
return null;
}
const [value] = args;
return new Date(value);
}
case "to_number": {
if (args.length !== 1) {
return null;
}
const [value] = args;
const result = Number(value);
if (isNaN(result)) {
return null;
}
return result;
}
case "to_string": {
if (args.length !== 1) {
return null;
}
const [value] = args;
return value.toString();
}
case "trim": {
if (args.length !== 1) {
return null;
}
const [value] = args;
if (!value || typeof value !== "string" && !Array.isArray(value)) {
return null;
}
if (Array.isArray(value)) {
return value.map((val) => val.trim());
} else {
return value.trim();
}
}
case "uppercase": {
if (args.length !== 1) {
return null;
}
const [str] = args;
return str.toUpperCase();
}
default:
throw new Error(`Unknown function: ${functionName}.`);
}
};
var parseRelativeTime = (timeString, direction) => {
const relativeDateRegex = /^(\d+)([dhms])$/;
const relativeDateMatch = timeString.toString().trim().match(relativeDateRegex);
if (relativeDateMatch) {
const now = /* @__PURE__ */ new Date();
const relativeDate = new Date(now);
const dateVal = parseInt(relativeDateMatch[1], 10);
const unit = relativeDateMatch[2];
const sign = direction === "past" ? -1 : 1;
switch (unit) {
case "d":
relativeDate.setDate(relativeDate.getDate() + sign * dateVal);
break;
case "h":
relativeDate.setHours(relativeDate.getHours() + sign * dateVal);
break;
case "m":
relativeDate.setMinutes(relativeDate.getMinutes() + sign * dateVal);
break;
case "s":
relativeDate.setSeconds(relativeDate.getSeconds() + sign * dateVal);
break;
default:
throw new Error(`Invalid relative date unit: '${unit}'`);
}
return relativeDate;
}
return null;
};
// src/statement/AlterStatement.ts
var AlterStatement = class extends AbstractStatement {
execute(_query, statement, data) {
if (!statement.alter) {
throw new Error("Alter statement must have alters");
}
const alter = statement.alter;
try {
data.map((row) => {
row[alter.field] = functionalField_default(
alter.func + "(" + alter.rawParameters + ")",
row
);
});
} catch (e) {
throw new Error(
`Invalid alter statement: '${alter.func}' with parameters '${alter.parameters.join(", ")}'`
);
}
return data;
}
parse(query, statement) {
const pattern = /^(?:alter)?\s*([a-zA-Z_]\w*)\s*=\s*([a-zA-Z_]\w*)\((.*?)\)$/;
const match = statement.match(pattern);
if (!match) {
throw new Error("Invalid expression format");
}
const [, variableName, functionName, parameters] = match;
query.statements.push({
type: "alter",
alter: {
field: variableName,
func: functionName,
parameters: parameters.split(new RegExp("(?<!\\\\),")).map((param) => param.trim()),
rawParameters: parameters
}
});
}
};
// src/statement/DedupStatement.ts
var DedupStatement = class extends AbstractStatement {
execute(_query, statement, data) {
if (!statement.dedup) {
throw new Error("Dedup statement must have dedup");
}
const { fields, sortBy, sortDirection } = statement.dedup;
const dedupedResults = /* @__PURE__ */ new Map();
for (const row of data) {
const compositeKeyParts = fields.map((field) => {
const fieldValue = dynamicField_default(field, row);
return fieldValue !== null && fieldValue !== void 0 ? fieldValue.toString() : "";
});
if (!compositeKeyParts.includes("")) {
const compositeKey = compositeKeyParts.join("|");
if (!dedupedResults.has(compositeKey) || sortBy && sortDirection) {
const existingRow = dedupedResults.get(compositeKey);
if (!existingRow || sortDirection === "desc" && dynamicField_default(sortBy || "", row) > dynamicField_default(sortBy || "", existingRow)) {
dedupedResults.set(compositeKey, row);
}
}
}
}
data = Array.from(dedupedResults.values());
return data;
}
parse(query, statement) {
statement = statement.substring(5).trim();
const stmtParts = statement.split(" ");
let fields = [];
let sortBy = void 0;
let sortDirection = void 0;
const byIndex = stmtParts.indexOf("by");
if (byIndex !== -1) {
fields = stmtParts.slice(0, byIndex).map((field) => field.replace(/,$/, ""));
if (stmtParts.length > byIndex + 2) {
sortBy = stmtParts[byIndex + 1];
sortDirection = stmtParts[byIndex + 2].toLowerCase();
if (sortDirection !== "asc" && sortDirection !== "desc") {
throw new Error(`Invalid dedup direction: '${sortDirection}'`);
}
} else {
throw new Error("sortBy and sortDirection expected after 'by'");
}
} else {
fields = stmtParts;
}
if (fields.length === 0) {
throw new Error("No fields specified for dedup");
}
fields = fields.flatMap(
(entry) => entry.split(",").map((f) => f.trim().replace(/,$/, "")).filter((f) => f !== "")
);
query.statements.push({
type: "dedup",
dedup: {
fields,
sortBy,
sortDirection
}
});
}
};
// src/statement/FieldsStatement.ts
var FieldsStatement = class extends AbstractStatement {
execute(_query, statement, data) {
data = data.map((row) => {
if (!statement.fields) {
throw new Error("Fields statement must have fields");
}
const result = {};
for (const field of statement.fields) {
const { name, alias } = field;
result[alias || name] = row[name];
}
return result;
});
return data;
}
parse(query, statement) {
statement = statement.substring(6).trim();
const fields = statement.split(",");
const fieldsStat = fields.map((s) => {
const [name, alias] = s.trim().split(/\s+as\s+/i);
if (alias) {
return { name, alias: alias || name };
}
return { name };
});
query.statements.push({ type: "fields", fields: fieldsStat });
}
};
// src/statement/FilterStatement.ts
import ipRangeCheck2 from "ip-range-check";
// src/statement/ConfigStatement.ts
var ConfigStatement = class extends AbstractStatement {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
execute(query, _statement, _data) {
let caseSensitive = true;
let grouping = null;
if (query.config && query.config.length > 0) {
for (const config of query.config) {
if (config.key === "case_sensitive") {
caseSensitive = config.value === "true";
} else if (config.key === "grouping") {
grouping = config.value;
}
}
}
return { caseSensitive, grouping };
}
parse(query, statement) {
const config = statement.substring(6).trim();
const parts = config.match(
/(['"][^'"]+['"]|\S+)\s*(=)\s*(['"][^'"]+['"]|\S+)/i
);
if (!parts) {
throw new Error(`Invalid config statement: '${statement}'`);
}
const key = parts[1].replace(/^['"]|['"]$/g, "");
const value = parts[3].replace(/^['"]|['"]$/g, "");
if (!key || !value) {
throw new Error(`Invalid config statement: '${statement}'`);
}
query.config.push({
key: key.trim(),
value: value.trim()
});
}
};
// src/statement/FilterStatement.ts
var FilterStatement = class extends AbstractStatement {
execute(query, statement, data) {
const { caseSensitive } = new ConfigStatement().execute(
query,
statement,
data
);
data = data.filter((row) => {
let filterResult = false;
if (!statement.filter) {
throw new Error("Filter statement is missing filter object");
}
for (const block of statement.filter.blocks) {
let blockResult = true;
for (const expression of block.expressions) {
const { field, operator } = expression;
let { value } = expression;
let rowValue = dynamicField_default(field, row);
if (rowValue === null && field !== null && field.match(/^[a-zA-Z_]\w*\(([^()]*|[^()]*\([^()]*\))*\)$/)) {
rowValue = functionalField_default(field, row);
}
if (rowValue === null && operator !== "notIn" && value.toString().toLowerCase() !== "null" && value.toString().toLowerCase() !== "undefined") {
blockResult = false;
break;
}
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
if (typeof rowValue === "object" && rowValue instanceof Date) {
if (typeof value === "string" && new Date(value).toString() !== "Invalid Date") {
value = new Date(value);
}
} else if (isoRegex.test(rowValue)) {
rowValue = new Date(rowValue);
if (typeof value === "string" && new Date(value).toString() !== "Invalid Date") {
value = new Date(value);
}
}
if (value !== null && typeof value === "string" && value.match(/^[a-zA-Z_]\w*\(([^()]*|[^()]*\([^()]*\))*\)$/)) {
value = functionalField_default(value, row);
}
const relativeDateRegex = /^(-)?(\d+)([dhms])/;
const relativeDateMatch = value.toString().trim().match(relativeDateRegex);
if (relativeDateMatch) {
const now = /* @__PURE__ */ new Date();
const relativeDate = new Date(now);
const sign = relativeDateMatch[1] === "-" ? -1 : 1;
const dateVal = parseInt(relativeDateMatch[2], 10);
const unit = relativeDateMatch[3];
switch (unit) {
case "d":
relativeDate.setDate(relativeDate.getDate() + sign * dateVal);
break;
case "h":
relativeDate.setHours(relativeDate.getHours() + sign * dateVal);
break;
case "m":
relativeDate.setMinutes(
relativeDate.getMinutes() + sign * dateVal
);
break;
case "s":
relativeDate.setSeconds(
relativeDate.getSeconds() + sign * dateVal
);
break;
default:
throw new Error(`Invalid relative date unit: '${unit}'`);
}
value = relativeDate;
}
operatorSwitch:
switch (operator) {
case "equals": {
if (value.toString().toLowerCase() === "null" || value.toString().toLowerCase() === "undefined") {
blockResult = rowValue === null || rowValue === void 0;
break operatorSwitch;
}
if (!caseSensitive) {
blockResult = rowValue === value || rowValue.toString().toLowerCase() === value.toString().toLowerCase();
break;
}
blockResult = rowValue === value || rowValue.toString() === value.toString();
break;
}
case "notEquals": {
if (value.toString().toLowerCase() === "null" || value.toString().toLowerCase() === "undefined") {
blockResult = rowValue !== null && rowValue !== void 0;
break operatorSwitch;
}
if (!caseSensitive) {
blockResult = rowValue !== value && rowValue.toString().toLowerCase() !== value.toString().toLowerCase();
break;
}
blockResult = rowValue !== value && rowValue.toString() !== value.toString();
break;
}
case "contains":
if (!caseSensitive) {
blockResult = rowValue.includes(value) || rowValue.toString().toLowerCase().includes(value.toString().toLowerCase());
break;
}
blockResult = rowValue.includes(value) || rowValue.toString().includes(value.toString());
break;
case "notContains":
if (!caseSensitive) {
blockResult = !rowValue.includes(value) && !rowValue.toString().toLowerCase().includes(value.toString().toLowerCase());
break;
}
blockResult = !rowValue.includes(value) || !rowValue.toString().includes(value.toString());
break;
case "lessThan":
blockResult = rowValue < value;
break;
case "greaterThan":
blockResult = rowValue > value;
break;
case "lessThanOrEquals":
blockResult = rowValue <= value;
break;
case "greaterThanOrEquals":
blockResult = rowValue >= value;
break;
case "matches":
try {
const regexParts = value.toString().match(/^\/(.+)\/([a-z]*)$/);
if (regexParts) {
const pattern = regexParts[1];
const flags = regexParts[2];
const regex2 = new RegExp(pattern, flags);
blockResult = regex2.test(rowValue.toString());
break;
}
const regex = new RegExp(value.toString());
blockResult = regex.test(rowValue.toString());
} catch (e) {
throw new Error(`Invalid regex pattern: '${value}'`);
}
break;
case "incidr":
blockResult = ipRangeCheck2(rowValue, value.toString());
break;
case "notIncidr":
blockResult = !ipRangeCheck2(rowValue, value.toString());
break;
case "in":
blockResult = value.includes(`${rowValue}`);
break;
case "notIn":
blockResult = !value.includes(`${rowValue}`);
break;
default:
throw new Error(`Invalid operator: '${operator}'`);
}
if (!blockResult) {
break;
}
}
if (blockResult) {
filterResult = true;
break;
}
}
return filterResult;
});
return data;
}
parse(query, statement) {
const filter = statement.substring(6).trim();
const parsedFilter = this.parseFilter(filter);
query.statements.push({ type: "filter", filter: parsedFilter });
}
parseFilter(filter) {
const blocks = filter.split(" or ").map((s) => s.trim());
const filters = blocks.map((b) => this.parseFilterBlock(b));
return { blocks: filters };
}
parseFilterBlock(block) {
const expressions = block.split(" and ").map((s) => s.trim());
const filters = expressions.map((e) => this.parseFilterExpression(e));
return { expressions: filters };
}
parseFilterExpression(expression) {
const operators = [
// Make sure that any string operators have a space after them to avoid accidental matching with field names
" matches ",
" not contains ",
" contains ",
" not incidr ",
" incidr ",
" not in ",
" in ",
"~=",
"!=",
"<=",
">=",
"<",
">",
"="
// This should be last to avoid premature matching
];
const operatorsRegexParts = operators.map(
(op) => (
// Escape special regex characters and sort by length to prioritize matching longer operators first
op.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")
)
).sort((a, b) => b.length - a.length);
const regex = new RegExp(
`(.+?)\\s*(${operatorsRegexParts.join("|")})\\s*(.+)`
);
const match = expression.match(regex);
if (!match) {
throw new Error(`Invalid filter expression: '${expression}'`);
}
const field = match[1].trim();
const operator = match[2].trim();
const value = match[3].trim().replace(/^['"](.*)['"]$/, "$1");
switch (operator) {
case "=":
return {
field,
operator: "equals",
value: this.parseFilterValue(value)
};
case "!=":
return {
field,
operator: "notEquals",
value: this.parseFilterValue(value)
};
case "~=":
case "matches":
return {
field,
operator: "matches",
value: this.parseFilterValue(value)
};
case "contains":
return {
field,
operator: "contains",
value: this.parseFilterValue(value)
};
case "not contains":
return {
field,
operator: "notContains",
value: this.parseFilterValue(value)
};
case "<":
return {
field,
operator: "lessThan",
value: this.parseFilterValue(value)
};
case ">":
return {
field,
operator: "greaterThan",
value: this.parseFilterValue(value)
};
case "<=":
return {
field,
operator: "lessThanOrEquals",
value: this.parseFilterValue(value)
};
case ">=":
return {
field,
operator: "greaterThanOrEquals",
value: this.parseFilterValue(value)
};
case "incidr":
return {
field,
operator: "incidr",
value: this.parseFilterValue(value)
};
case "not incidr":
return {
field,
operator: "notIncidr",
value: this.parseFilterValue(value)
};
case "in":
return {
field,
operator: "in",
value: this.parseListFilterValue(value)
};
case "not in":
return {
field,
operator: "notIn",
value: this.parseListFilterValue(value)
};
default:
throw new Error(`Invalid filter expression: '${expression}'`);
}
}
parseFilterValue(value) {
const v4 = new RegExp(
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
);
if (value.toString().match(v4)) {
return value.toString();
} else if (value === "true" || value === "false") {
return value === "true";
} else if (!isNaN(Number(value))) {
return Number(value);
} else if (value === "date()" || value === "now()") {
return /* @__PURE__ */ new Date();
} else {
return value;
}
}
parseListFilterValue(value) {
let parsedValue = this.parseFilterValue(value);
if (typeof parsedValue !== "string") {
throw new Error(`Unsupported filter value: '${value}'`);
}
if (!parsedValue.startsWith("(") || !parsedValue.endsWith(")")) {
throw new Error(`Unsupported filter value: '${value}'`);
}
parsedValue = parsedValue.substring(1, parsedValue.length - 1);
const parsedList = parsedValue.split(",").map((v) => v.trim().replace(/^['"](.*)['"]$/, "$1"));
return parsedList;
}
};
// src/statement/LimitStatement.ts
var LimitStatement = class extends AbstractStatement {
execute(_query, statement, data) {
if (statement.limit === void 0 || statement.limit === null) {
throw new Error("Limit statement must have limit");
}
data = data.slice(0, statement.limit);
return data;
}
parse(query, statement) {
const parts = statement.split(" ");
if (parts.length !== 2) {
throw new Error(`Invalid limit statement: '${statement}'`);
}
const limit = Number(parts[1]);
if (isNaN(limit)) {
throw new Error(`Invalid limit statement: '${statement}'`);
}
query.statements.push({ type: "limit", limit });
}
};
// src/statement/SearchStatement.ts
var SearchStatement = class extends AbstractStatement {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
execute(query, statement, data) {
const searchKey = statement.search;
const { caseSensitive } = new ConfigStatement().execute(
query,
statement,
data
);
if (!searchKey) {
return data;
}
if (caseSensitive) {
return data.filter(
(row) => Object.values(row).some(
(value) => typeof value === "string" ? value.includes(searchKey) : value.toString().includes(searchKey)
)
);
}
return data.filter(
(row) => Object.values(row).some(
(value) => typeof value === "string" ? value.toLowerCase().includes(searchKey.toLowerCase()) : value.toString().includes(searchKey)
)
);
}
parse(query, statement) {
statement = statement.substring(6).trim();
query.statements.push({
type: "search",
search: statement.replace(/^['"](.*)['"]$/, "$1")
});
}
};
// src/dynamicSort.ts
var dynamicSort_default = (properties) => {
return (a, b) => {
for (let i = 0; i < properties.length; i += 1) {
const sortOrder = properties[i][0] === "-" ? -1 : 1;
const property = sortOrder === -1 ? properties[i].slice(1) : properties[i];
const propertyPath = property.split(".");
let propertyValueA = a;
let propertyValueB = b;
let j = 0;
while (propertyValueA && propertyValueB && j < propertyPath.length) {
propertyValueA = propertyValueA[propertyPath[j]];
propertyValueB = propertyValueB[propertyPath[j]];
j += 1;
}
if (propertyValueA < propertyValueB) {
return -1 * sortOrder;
}
if (propertyValueA > propertyValueB) {
return 1 * sortOrder;
}
}
return 0;
};
};
// src/statement/SortStatement.ts
var SortStatement = class extends AbstractStatement {
execute(_query, statement, data) {
if (!statement.sort) {
throw new Error("Sort statement must have sort");
}
const sorts = statement.sort.map((s) => {
if (s.direction === "desc") {
return `-${s.field}`;
}
return s.field;
});
data = data.sort(dynamicSort_default(sorts));
return data;
}
parse(query, statement) {
let sorts = statement.split(",");
sorts[0] = sorts[0].replace("sort", "").trim();
sorts = sorts.map((s) => s.trim());
const stat = { type: "sort", sort: [] };
for (const sort of sorts) {
const stmtParts = sort.trim().split(" ");
if (stmtParts.length === 1 || stmtParts.length === 2) {
const field = stmtParts[0];
const direction = stmtParts.length === 2 ? stmtParts[1].toLowerCase() : "asc";
if (direction === "asc" || direction === "desc") {
stat.sort.push({
field,
direction
});
} else {
throw new Error(`Invalid sort direction: '${direction}'`);
}
} else {
throw new Error(`Invalid sort statement: '${sort}'`);
}
}
query.statements.push(stat);
}
};
// src/statement/CompStatement.ts
var CompStatement = class extends AbstractStatement {
execute(query, statement, data) {
const groupedData = {};
const { grouping } = new ConfigStatement().execute(query, statement, data);
const groupingFields = statement.groupingFields || (grouping ? [grouping] : []);
if (groupingFields.length > 0) {
for (const row of data) {
const keyParts = [];
let hasNullValue = false;
for (const field of groupingFields) {
const value = dynamicField_default(field, row);
if (value === null || value === void 0) {
hasNullValue = true;
break;
}
keyParts.push(String(value));
}
if (hasNullValue) {
continue;
}
const groupKey = keyParts.join("|||");
if (!groupedData[groupKey]) {
groupedData[groupKey] = [];
}
groupedData[groupKey].push(row);
}
} else {
groupedData["_all"] = data;
}
const results = [];
const compStatement = statement.comp;
if (!compStatement) {
throw new Error("Comp statement must have comp");
}
for (const [groupKey, groupData] of Object.entries(groupedData)) {
const statsRow = {};
if (groupingFields.length > 0) {
const keyParts = groupKey.split("|||");
for (let i = 0; i < groupingFields.length; i++) {
const fieldName = groupingFields[i].replace(/\./g, "_");
statsRow[fieldName] = keyParts[i];
}
}
for (const comp of compStatement) {
if (!comp) {
throw new Error("Comp statement must have comp");
}
if (comp.function === "count" && !comp.field) {
statsRow[comp.returnField] = groupData.length;
continue;
}
const values = groupData.map((row) => dynamicField_default(comp.field, row)).filter((value) => value !== null && value !== void 0);
switch (comp.function) {
case "count":
statsRow[comp.returnField] = values.length;
break;
case "count_distinct":
statsRow[comp.returnField] = new Set(values).size;
break;
case "min":
statsRow[comp.returnField] = Math.min(...values);
break;
case "max":
statsRow[comp.returnField] = Math.max(...values);
break;
case "avg":
statsRow[comp.returnField] = values.reduce((a, b) => a + b, 0) / values.length;
break;
case "sum":
statsRow[comp.returnField] = values.reduce((a, b) => a + b, 0);
break;
case "median":
values.sort((a, b) => a - b);
const middle = Math.floor(values.length / 2);
statsRow[comp.returnField] = values.length % 2 !== 0 ? values[middle] : (values[middle - 1] + values[middle]) / 2;
break;
case "earliest":
statsRow[comp.returnField] = new Date(
Math.min(
...values.map((isoString) => new Date(isoString).getTime())
)
);
break;
case "latest":
statsRow[comp.returnField] = new Date(
Math.max(
...values.map((isoString) => new Date(isoString).getTime())
)
);
break;
case "first":
statsRow[comp.returnField] = groupData.length > 0 ? dynamicField_default(comp.field, groupData[0]) : null;
break;
case "last":
statsRow[comp.returnField] = groupData.length > 0 ? dynamicField_default(
comp.field,
groupData[groupData.length - 1]
) : null;
break;
case "to_array":
statsRow[comp.returnField] = Array.from(new Set(values));
break;
default:
throw new Error(`Invalid comp function: '${comp.function}'`);
}
}
results.push(statsRow);
}
return results;
}
parse(query, statement) {
let groupingFields = [];
let statementWithoutBy = statement;
let aliasSuffix = "";
const byMatch = statement.match(/\s+by\s+(.+)$/i);
if (byMatch) {
const byClause = byMatch[1].trim();
const byWithAliasMatch = byClause.match(/^(.+?)\s+as\s+(\w+)$/i);
if (byWithAliasMatch) {
groupingFields = [byWithAliasMatch[1].trim()];
aliasSuffix = ` as ${byWithAliasMatch[2]}`;
statementWithoutBy = statement.substring(0, statement.indexOf(" by "));
} else {
groupingFields = byClause.split(",").map((f) => f.trim());
statementWithoutBy = statement.substring(0, statement.indexOf(" by "));
}
}
const individualStatements = statementWithoutBy.split(",").map((s) => s.trim());
individualStatements[0] = individualStatements[0].substring(4).trim();
const baseStatement = {
type: "comp",
comp: [],
groupingFields: groupingFields.length > 0 ? groupingFields : void 0
};
for (let i = 0; i < individualStatements.length; i++) {
let individualStatement = individualStatements[i];
if (i === 0 && aliasSuffix && !individualStatement.includes(" as ")) {
individualStatement += aliasSuffix;
}
const functionCallMatch = individualStatement.match(
/^(\w+)\s*\(([^)]*)\)\s*(?:as\s+(\w+))?$/i
);
if (functionCallMatch) {
const func = functionCallMatch[1].trim();
const field = functionCallMatch[2].trim() || "";
const returnField = functionCallMatch[3] || (field ? `${field}_${func}` : func).replace(/\./g, "_");
baseStatement.comp.push({
function: func,
field,
returnField
});
} else {
const parts = individualStatement.split(" ");
if (parts.length < 1) {
throw new Error(`Invalid comp statement: '${individualStatement}'`);
}
const func = parts[0].trim();
let field = "";
let returnField = "";
if (parts.length > 1 && parts[1] !== "as") {
field = parts[1].trim();
}
const asIndex = parts.indexOf("as");
if (asIndex !== -1) {
if (asIndex + 1 >= parts.length) {
throw new Error(`Invalid comp statement: '${individualStatement}'`);
}
returnField = parts[asIndex + 1].trim();
if (asIndex + 2 < parts.length) {
throw new Error(`Invalid comp statement: '${individualStatement}'`);
}
} else {
if (parts.length > 2) {
throw new Error(`Invalid comp statement: '${individualStatement}'`);
}
}
if (!returnField) {
returnField = field ? `${field}_${func}`.replace(/\./g, "_") : func;
}
baseStatement.comp.push({
function: func,
field,
returnField
});
}
}
query.statements.push(baseStatement);
}
};
// src/QueryExecutor.ts
var QueryExecutor = class {
/**
* Executes the provided query on the given data array.
*
* The data array consists of objects with key-value pairs representing the data.
*
* If any field or operator is not found in a row of data, an error will be thrown.
*
* @param {Query} query The query object containing fields, alters, filters, sort, and limit properties.
* @param {Array} data The data to be queried, as an array of objects.
* @returns {Array} The result of the query execution, as an array of objects.
* @throws {Error} If any field in the query is not found in the data, or an invalid operator is used.
* @public
* @static
*/
static executeQuery(query, data) {
let results = data;
for (const statement of query.statements) {
switch (statement.type) {
case "dataset":
case "config":
break;
case "filter":
results = new FilterStatement().execute(query, statement, results);
break;
case "fields":
results = new FieldsStatement().execute(query, statement, results);
break;
case "sort":
results = new SortStatement().execute(query, statement, results);
break;
case "limit":
results = new LimitStatement().execute(query, statement, results);
break;
case "dedup":
results = new DedupStatement().execute(query, statement, results);
break;
case "alter":
results = new AlterStatement().execute(query, statement, results);
break;
case "comp":
results = new CompStatement().execute(query, statement, results);
break;
case "search":
results = new SearchStatement().execute(query, statement, results);
break;
default:
throw new Error(`Invalid statement type: '${statement.type}'`);
}
}
return results;
}
};
// src/statement/DatasetStatement.ts
var DatasetStatement = class extends AbstractStatement {
execute(_query, _statement, _data) {
throw new Error("Method not implemented.");
}
parse(query, statement) {
if (statement.split("=").length !== 2) {
throw new Error(`Invalid dataset statement: '${statement}'`);
}
query.dataset = statement.split("=")[1].trim();
}
};
// src/QueryParser.ts
var QueryParser = class {
/**
* Parses the provided query string into a Query object.
*
* Example query string:
* `dataset = myDataset | filter price > 100 | sort price desc, name asc | fields name, price as cost, description | limit 10`
*
* The returned Query object has properties matching the parsed elements of the query string. If any part of the query string cannot be parsed correctly, an error will be thrown.
*
* @param {string} queryString The query string to be parsed.
* @param {QueryParsingOptions} options Options for parsing the query string.
* @returns {Query} The parsed query as a Query object.
* @throws {Error} If any part of the query string cannot be parsed, or if no dataset is specified.
* @public
* @static
*/
static parseQuery(queryString, options = { strictDataset: true }) {
const query = {
dataset: "",
config: [],
statements: []
};
const queryLines = queryString.split("\n");
let constructedQuery = "";
queryLines.map((l) => {
if (l.startsWith("#") || l.startsWith("//")) {
return;
}
constructedQuery += l;
});
const queryParts = constructedQuery.split("|");
for (const part of queryParts) {
const statement = part.trim();
if (statement.startsWith("dataset")) {
new DatasetStatement().parse(query, statement);
} else if (statement.startsWith("filter")) {
new FilterStatement().parse(query, statement);
} else if (statement.startsWith("fields")) {
new FieldsStatement().parse(query, statement);
} else if (statement.startsWith("sort")) {
new SortStatement().parse(query, statement);
} else if (statement.startsWith("limit")) {
new LimitStatement().parse(query, statement);
} else if (statement.startsWith("dedup")) {
new DedupStatement().parse(query, statement);
} else if (statement.startsWith("alter")) {
new AlterStatement().parse(query, statement);
} else if (statement.startsWith("config")) {
new ConfigStatement().parse(query, statement);
} else if (statement.startsWith("comp")) {
new CompStatement().parse(query, statement);
} else if (statement.startsWith("search")) {
new SearchStatement().parse(query, statement);
} else {
throw new Error(`Invalid statement: '${statement}'`);
}
}
if (!query.dataset && options.strictDataset) {
throw new Error("No dataset specified");
}
return query;
}
};
export {
QueryExecutor,
QueryParser
};