Skip to content

Commit

Permalink
handle more edgecases + test
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-0acf4 committed Dec 3, 2024
1 parent 3d5eb04 commit f9da645
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 81 deletions.
4 changes: 1 addition & 3 deletions src/typegate/src/runtimes/substantial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ import {
} from "./substantial/agent.ts";
import { closestWord } from "../utils.ts";
import { InternalAuth } from "../services/auth/protocols/internal.ts";
import { applyFilter, Expr } from "./substantial/filter_utils.ts";
import { applyFilter, type Expr, type ExecutionStatus } from "./substantial/filter_utils.ts";

const logger = getLogger(import.meta);

export type ExecutionStatus = "COMPLETED" | "COMPLETED_WITH_ERROR" | "ONGOING" | "UNKNOWN";

interface QueryCompletedWorkflowResult {
run_id: string;
started_at: string;
Expand Down
196 changes: 118 additions & 78 deletions src/typegate/src/runtimes/substantial/filter_utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0.
// SPDX-License-Identifier: MPL-2.0

import { ExecutionStatus } from "../substantial.ts";
import { Agent } from "./agent.ts";

export type ExecutionStatus =
| "COMPLETED"
| "COMPLETED_WITH_ERROR"
| "ONGOING"
| "UNKNOWN";

type KVHelper<T extends string[], V> = {
// should be a union type but it is not very helpful with autocomplete
[K in T[number]]?: V;
Expand Down Expand Up @@ -64,7 +69,7 @@ export async function buildSearchableItems(

const isOk = "Ok" in result;
const kind = isOk ? "Ok" : "Err";
const stoppedStatus = isOk ? "COMPLETED_WITH_ERROR" : "COMPLETED";
const stoppedStatus = isOk ? "COMPLETED" : "COMPLETED_WITH_ERROR";

searchList.push(
new SearchItem(
Expand Down Expand Up @@ -96,52 +101,64 @@ export async function applyFilter(
return searchResults;
}

function evalExpr(sResult: SearchItem, filter: Expr, path: Array<string>) {
for (const k in filter) {
const op = k as unknown as keyof Expr;
const newPath = [...path, op];
switch (op) {
// Expr
case "and":
case "or": {
const exprList = filter[op];
if (!Array.isArray(exprList)) {
// should be unreachable since filter is validated at push
throw new Error(`Fatal: array expected at ${path.join(".")}`);
}
const fn = op == "or" ? "some" : "every";
if (
!exprList[fn]((subFilter) => evalExpr(sResult, subFilter, newPath))
) {
return false;
}
break;

export function evalExpr(
sResult: SearchItem,
filter: Expr,
path: Array<string>,
) {
const keys = Object.keys(filter) as Array<keyof Expr>;
if (keys.length != 1) {
throw new Error(`Invalid expression at ${path.join(".")}`);
}
const op = keys[0];
const newPath = [...path, op];

switch (op) {
// Expr
case "and":
case "or": {
const exprList = filter[op];
if (!Array.isArray(exprList)) {
// should be unreachable since filter is validated at push
throw new Error(
`Fatal: array expected at ${path.join(".")}`,
);
}
case "not": {
if (evalExpr(sResult, filter["not"]!, newPath)) {
return false;
}
break;
const fn = op == "or" ? "some" : "every";
if (
!exprList[fn]((subFilter, index) =>
evalExpr(sResult, subFilter, [...newPath, `#${index}`])
)
) {
return false;
}
// special
case "status":
case "started_at":
case "ended_at": {
const discriminator = sResult[op];
const repr = new SearchItem(
sResult.run_id,
sResult.started_at,
sResult.ended_at,
sResult.status,
discriminator,
);
return evalTerm(repr, filter[op]!, newPath);
break;
}
case "not": {
if (evalExpr(sResult, filter["not"]!, newPath)) {
return false;
}
// Term
default: {
if (!evalTerm(sResult, filter, newPath)) {
return false;
}
break;
}
// Special
case "status":
case "started_at":
case "ended_at": {
const discriminator = sResult[op];
const repr = new SearchItem(
sResult.run_id,
null,
null,
sResult.status,
discriminator,
);
return evalTerm(repr, filter[op]!, newPath);
}
// Term
default: {
if (!evalTerm(sResult, filter, path)) {
return false;
}
}
}
Expand All @@ -151,45 +168,69 @@ function evalExpr(sResult: SearchItem, filter: Expr, path: Array<string>) {

function evalTerm(sResult: SearchItem, terms: Terms, path: Array<string>) {
const value = sResult.value;
const keys = Object.keys(terms) as Array<keyof Terms>;
if (keys.length != 1) {
throw new Error(`Invalid expression at ${path.join(".")}`);
}

for (const k in terms) {
const op = k as unknown as keyof Terms;
const term = JSON.parse(terms[op] ?? "null"); // TODO: impl generic JSON on typegate
const newPath = [...path, op];
switch (op) {
case "eq": {
if (value != term) {
return false;
}
break;
const op = keys[0];
const newPath = [...path, op];
switch (op) {
case "eq": {
// term can never compare (null at worst)
if (value === undefined) {
return false;
}
case "lt":
case "lte":
case "gt":
case "gte": {
if (!ord(value, term, op, newPath)) {
return false;
}
break;

if (!testCompare(value, toJS(terms[op]))) {
return false;
}
case "contains":
case "in": {
if (
!inclusion(value, term, op, newPath)
) {
return false;
}
break;

break;
}
case "lt":
case "lte":
case "gt":
case "gte": {
if (!ord(value, toJS(terms[op]), op, newPath)) {
return false;
}
default: {
throw new Error(`Unknown operator at ${newPath.join(".")}`);
break;
}
case "contains":
case "in": {
if (
!inclusion(value, toJS(terms[op]), op, newPath)
) {
return false;
}
break;
}
default: {
throw new Error(
`Unknown operator "${op}" at ${path.join(".")}`,
);
}
}

return true;
}

function toJS(val: string | undefined) {
// TODO: impl generic JSON on typegate
// ideally this should be an identity fn
return JSON.parse(val ?? "null");
}

function testCompare(value: unknown, testValue: unknown) {
const easy = ["number", "boolean", "string"];
if (easy.includes(typeof value)) {
return value === testValue;
}

return JSON.stringify(value) == JSON.stringify(testValue);
}

function comparable(a: unknown, b: unknown) {
return typeof a == typeof b;
}
Expand Down Expand Up @@ -229,13 +270,12 @@ function inclusion(
cp: keyof INCL,
_newPath: Array<string>,
) {
if (!comparable(l, r)) {
return false;
}

const [left, right] = cp == "in" ? [l, r] : [r, l];
if (Array.isArray(right)) {
// FIXME: does not work with [ [[1]] ].includes([[1]])
// Note: Array.prototype.includes compare inner references
const leftComp = JSON.stringify(left);
return right.some((inner) => JSON.stringify(inner) === leftComp);
} else if (typeof left == "string" && typeof right == "string") {
return right.includes(left);
} else if (
typeof left == typeof right && typeof left == "object" && left != null
Expand Down
Loading

0 comments on commit f9da645

Please sign in to comment.