trilean 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/package.json +3 -1
- package/schemas/trilean.schema.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,122 @@ await evaluatePredicate(node, { age: 21 }, resolvers);
|
|
|
49
49
|
|
|
50
50
|
See [Evaluator entry points](#evaluator-entry-points) and [Resolvers](#resolvers) for the full contract, and the [Worked example](#worked-example) for a larger tree combining boolean logic, a formula, and an aggregation.
|
|
51
51
|
|
|
52
|
+
### A nested filter for a REST API search endpoint
|
|
53
|
+
|
|
54
|
+
A search endpoint's filter criteria are exactly the kind of thing this package is for: nested boolean logic, stored as JSON, that a client can construct, a non-developer can edit via a UI, and a server evaluates per record without ever hardcoding the filter or redeploying when it changes. There is no query-string DSL to parse and no ORM query-builder to translate into — the request body already is the tree:
|
|
55
|
+
|
|
56
|
+
```http
|
|
57
|
+
POST /orders/search HTTP/1.1
|
|
58
|
+
Content-Type: application/json
|
|
59
|
+
|
|
60
|
+
{
|
|
61
|
+
"filter": {
|
|
62
|
+
"kind": "and",
|
|
63
|
+
"left": {
|
|
64
|
+
"kind": "textCompare",
|
|
65
|
+
"op": "equals",
|
|
66
|
+
"left": { "kind": "reference", "key": "status" },
|
|
67
|
+
"right": { "kind": "textLiteral", "value": "active" }
|
|
68
|
+
},
|
|
69
|
+
"right": {
|
|
70
|
+
"kind": "or",
|
|
71
|
+
"left": {
|
|
72
|
+
"kind": "compare",
|
|
73
|
+
"op": "gt",
|
|
74
|
+
"left": { "kind": "reference", "key": "orderTotal" },
|
|
75
|
+
"right": { "kind": "numberLiteral", "value": 100 }
|
|
76
|
+
},
|
|
77
|
+
"right": {
|
|
78
|
+
"kind": "memberOf",
|
|
79
|
+
"op": "in",
|
|
80
|
+
"operand": { "kind": "reference", "key": "category" },
|
|
81
|
+
"candidates": [
|
|
82
|
+
{ "kind": "textLiteral", "value": "electronics" },
|
|
83
|
+
{ "kind": "textLiteral", "value": "books" }
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`status equals "active" AND (orderTotal > 100 OR category is a preferred one)` — two levels of nesting: an `or` inside the right branch of an `and`. The server parses that body's `filter` field as a `PredicateNode` and evaluates it, unmodified, against each candidate order:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { evaluatePredicate, type PredicateNode, type Resolvers } from "trilean";
|
|
95
|
+
|
|
96
|
+
interface Order {
|
|
97
|
+
status: string;
|
|
98
|
+
orderTotal: number;
|
|
99
|
+
category: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The parsed `filter` field from the request body above.
|
|
103
|
+
const filter: PredicateNode = {
|
|
104
|
+
kind: "and",
|
|
105
|
+
left: {
|
|
106
|
+
kind: "textCompare",
|
|
107
|
+
op: "equals",
|
|
108
|
+
left: { kind: "reference", key: "status" },
|
|
109
|
+
right: { kind: "textLiteral", value: "active" },
|
|
110
|
+
},
|
|
111
|
+
right: {
|
|
112
|
+
kind: "or",
|
|
113
|
+
left: {
|
|
114
|
+
kind: "compare",
|
|
115
|
+
op: "gt",
|
|
116
|
+
left: { kind: "reference", key: "orderTotal" },
|
|
117
|
+
right: { kind: "numberLiteral", value: 100 },
|
|
118
|
+
},
|
|
119
|
+
right: {
|
|
120
|
+
kind: "memberOf",
|
|
121
|
+
op: "in",
|
|
122
|
+
operand: { kind: "reference", key: "category" },
|
|
123
|
+
candidates: [
|
|
124
|
+
{ kind: "textLiteral", value: "electronics" },
|
|
125
|
+
{ kind: "textLiteral", value: "books" },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const orderResolvers: Resolvers = {
|
|
132
|
+
async resolveValue(key, context) {
|
|
133
|
+
const order = context as Order;
|
|
134
|
+
switch (key) {
|
|
135
|
+
case "status":
|
|
136
|
+
return { found: true, value: { kind: "text", value: order.status } };
|
|
137
|
+
case "orderTotal":
|
|
138
|
+
return { found: true, value: { kind: "number", value: order.orderTotal } };
|
|
139
|
+
case "category":
|
|
140
|
+
return { found: true, value: { kind: "text", value: order.category } };
|
|
141
|
+
default:
|
|
142
|
+
return { found: false };
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
async resolveLookup() {
|
|
146
|
+
return { found: false };
|
|
147
|
+
},
|
|
148
|
+
async resolveCollection() {
|
|
149
|
+
return [];
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const orders: Order[] = [
|
|
154
|
+
{ status: "active", orderTotal: 42, category: "electronics" },
|
|
155
|
+
{ status: "active", orderTotal: 150, category: "garden" },
|
|
156
|
+
{ status: "cancelled", orderTotal: 200, category: "electronics" },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const results = await Promise.all(
|
|
160
|
+
orders.map((order) => evaluatePredicate(filter, order, orderResolvers)),
|
|
161
|
+
);
|
|
162
|
+
const matching = orders.filter((_, i) => results[i]?.status === "definite" && results[i]?.value === true);
|
|
163
|
+
// => the first two orders match; the cancelled one doesn't reach the "or" at all, since "and" absorbs on its left operand's definite false
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
See [`and`/`or`](#not-and-or), [`compare`](#compare), [`textCompare`](#textcompare), and [`memberOf`](#memberof) for the full node-kind reference.
|
|
167
|
+
|
|
52
168
|
## Build, test, and lint
|
|
53
169
|
|
|
54
170
|
```sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trilean",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Consumer-agnostic, three-valued predicate/expression evaluation trees over injected resolvers -- no assumptions about consumer data, no I/O of its own.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
"_test:smoke": "vitest run --project smoke",
|
|
69
69
|
"test:workers": "turbo run _test:workers",
|
|
70
70
|
"_test:workers": "vitest run --project workers",
|
|
71
|
+
"prepush": "turbo run _prepush",
|
|
72
|
+
"_prepush": "true",
|
|
71
73
|
"prepublishOnly": "pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run test:integration && pnpm run build && pnpm run test:smoke && publint && attw --pack",
|
|
72
74
|
"prepare": "husky",
|
|
73
75
|
"release": "semantic-release"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$defs":{"ExpressionNode":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"numberLiteral","type":"string"},"unit":{"additionalProperties":{"type":"number"},"propertyNames":{"type":"string"},"type":"object"},"value":{"type":"number"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"textLiteral","type":"string"},"value":{"type":"string"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"instantLiteral","type":"string"},"value":{"type":"string"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"durationLiteral","type":"string"},"unit":{"enum":["ms","s","min","h","d"],"type":"string"},"value":{"type":"number"}},"required":["kind","value","unit"],"type":"object"},{"additionalProperties":false,"properties":{"key":{"$ref":"#/$defs/schema0"},"kind":{"const":"reference","type":"string"},"unit":{"additionalProperties":{"type":"number"},"propertyNames":{"type":"string"},"type":"object"}},"required":["kind","key"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"arithmetic","type":"string"},"left":{"$ref":"#/$defs/ExpressionNode"},"op":{"enum":["add","subtract","multiply","divide","power","modulo"],"type":"string"},"right":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","op","left","right"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"negate","type":"string"},"operand":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","operand"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"items":{"$ref":"#/$defs/ExpressionNode"},"type":"array"},"fn":{"type":"string"},"kind":{"const":"call","type":"string"}},"required":["kind","fn","args"],"type":"object"},{"additionalProperties":false,"properties":{"keys":{"items":{"$ref":"#/$defs/ExpressionNode"},"type":"array"},"kind":{"const":"lookup","type":"string"},"table":{"$ref":"#/$defs/schema0"}},"required":["kind","table","keys"],"type":"object"},{"additionalProperties":false,"properties":{"cases":{"items":{"additionalProperties":false,"properties":{"then":{"$ref":"#/$defs/ExpressionNode"},"when":{"$ref":"#/$defs/PredicateNode"}},"required":["when","then"],"type":"object"},"type":"array"},"fallback":{"$ref":"#/$defs/ExpressionNode"},"kind":{"const":"conditional","type":"string"}},"required":["kind","cases","fallback"],"type":"object"},{"additionalProperties":false,"properties":{"collection":{"$ref":"#/$defs/schema0"},"combiner":{"oneOf":[{"additionalProperties":false,"properties":{"item":{"$ref":"#/$defs/ExpressionNode"},"mode":{"const":"max","type":"string"}},"required":["mode","item"],"type":"object"},{"additionalProperties":false,"properties":{"item":{"$ref":"#/$defs/ExpressionNode"},"mode":{"const":"min","type":"string"}},"required":["mode","item"],"type":"object"},{"additionalProperties":false,"properties":{"combine":{"$ref":"#/$defs/ExpressionNode"},"initial":{"$ref":"#/$defs/ExpressionNode"},"mode":{"const":"reduce","type":"string"}},"required":["mode","initial","combine"],"type":"object"}]},"filter":{"$ref":"#/$defs/PredicateNode"},"kind":{"const":"fold","type":"string"}},"required":["kind","collection","combiner"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"accumulator","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"delegate","type":"string"},"payload":{"$ref":"#/$defs/schema0"},"system":{"type":"string"}},"required":["kind","system","payload"],"type":"object"}]},"PredicateNode":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"not","type":"string"},"operand":{"$ref":"#/$defs/PredicateNode"}},"required":["kind","operand"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"and","type":"string"},"left":{"$ref":"#/$defs/PredicateNode"},"right":{"$ref":"#/$defs/PredicateNode"}},"required":["kind","left","right"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"or","type":"string"},"left":{"$ref":"#/$defs/PredicateNode"},"right":{"$ref":"#/$defs/PredicateNode"}},"required":["kind","left","right"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"allOf","type":"string"},"operands":{"items":{"$ref":"#/$defs/PredicateNode"},"type":"array"}},"required":["kind","operands"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"anyOf","type":"string"},"operands":{"items":{"$ref":"#/$defs/PredicateNode"},"type":"array"}},"required":["kind","operands"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"compare","type":"string"},"left":{"$ref":"#/$defs/ExpressionNode"},"op":{"enum":["gt","gte","lt","lte","eq","neq"],"type":"string"},"right":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","op","left","right"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"textCompare","type":"string"},"left":{"$ref":"#/$defs/ExpressionNode"},"op":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"right":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","op","left","right"],"type":"object"},{"additionalProperties":false,"properties":{"candidates":{"items":{"$ref":"#/$defs/ExpressionNode"},"type":"array"},"kind":{"const":"memberOf","type":"string"},"op":{"enum":["in","notIn"],"type":"string"},"operand":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","op","operand","candidates"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"exists","type":"string"},"operand":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","operand"],"type":"object"},{"additionalProperties":false,"properties":{"collection":{"$ref":"#/$defs/schema0"},"filter":{"$ref":"#/$defs/PredicateNode"},"item":{"$ref":"#/$defs/PredicateNode"},"kind":{"const":"some","type":"string"}},"required":["kind","collection","item"],"type":"object"},{"additionalProperties":false,"properties":{"collection":{"$ref":"#/$defs/schema0"},"filter":{"$ref":"#/$defs/PredicateNode"},"item":{"$ref":"#/$defs/PredicateNode"},"kind":{"const":"every","type":"string"}},"required":["kind","collection","item"],"type":"object"}]},"schema0":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"items":{"$ref":"#/$defs/schema0"},"type":"array"},{"additionalProperties":{"$ref":"#/$defs/schema0"},"propertyNames":{"type":"string"},"type":"object"}]}},"$id":"https://cdn.jsdelivr.net/npm/trilean@1.0.
|
|
1
|
+
{"$defs":{"ExpressionNode":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"numberLiteral","type":"string"},"unit":{"additionalProperties":{"type":"number"},"propertyNames":{"type":"string"},"type":"object"},"value":{"type":"number"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"textLiteral","type":"string"},"value":{"type":"string"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"instantLiteral","type":"string"},"value":{"type":"string"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"durationLiteral","type":"string"},"unit":{"enum":["ms","s","min","h","d"],"type":"string"},"value":{"type":"number"}},"required":["kind","value","unit"],"type":"object"},{"additionalProperties":false,"properties":{"key":{"$ref":"#/$defs/schema0"},"kind":{"const":"reference","type":"string"},"unit":{"additionalProperties":{"type":"number"},"propertyNames":{"type":"string"},"type":"object"}},"required":["kind","key"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"arithmetic","type":"string"},"left":{"$ref":"#/$defs/ExpressionNode"},"op":{"enum":["add","subtract","multiply","divide","power","modulo"],"type":"string"},"right":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","op","left","right"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"negate","type":"string"},"operand":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","operand"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"items":{"$ref":"#/$defs/ExpressionNode"},"type":"array"},"fn":{"type":"string"},"kind":{"const":"call","type":"string"}},"required":["kind","fn","args"],"type":"object"},{"additionalProperties":false,"properties":{"keys":{"items":{"$ref":"#/$defs/ExpressionNode"},"type":"array"},"kind":{"const":"lookup","type":"string"},"table":{"$ref":"#/$defs/schema0"}},"required":["kind","table","keys"],"type":"object"},{"additionalProperties":false,"properties":{"cases":{"items":{"additionalProperties":false,"properties":{"then":{"$ref":"#/$defs/ExpressionNode"},"when":{"$ref":"#/$defs/PredicateNode"}},"required":["when","then"],"type":"object"},"type":"array"},"fallback":{"$ref":"#/$defs/ExpressionNode"},"kind":{"const":"conditional","type":"string"}},"required":["kind","cases","fallback"],"type":"object"},{"additionalProperties":false,"properties":{"collection":{"$ref":"#/$defs/schema0"},"combiner":{"oneOf":[{"additionalProperties":false,"properties":{"item":{"$ref":"#/$defs/ExpressionNode"},"mode":{"const":"max","type":"string"}},"required":["mode","item"],"type":"object"},{"additionalProperties":false,"properties":{"item":{"$ref":"#/$defs/ExpressionNode"},"mode":{"const":"min","type":"string"}},"required":["mode","item"],"type":"object"},{"additionalProperties":false,"properties":{"combine":{"$ref":"#/$defs/ExpressionNode"},"initial":{"$ref":"#/$defs/ExpressionNode"},"mode":{"const":"reduce","type":"string"}},"required":["mode","initial","combine"],"type":"object"}]},"filter":{"$ref":"#/$defs/PredicateNode"},"kind":{"const":"fold","type":"string"}},"required":["kind","collection","combiner"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"accumulator","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"delegate","type":"string"},"payload":{"$ref":"#/$defs/schema0"},"system":{"type":"string"}},"required":["kind","system","payload"],"type":"object"}]},"PredicateNode":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"not","type":"string"},"operand":{"$ref":"#/$defs/PredicateNode"}},"required":["kind","operand"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"and","type":"string"},"left":{"$ref":"#/$defs/PredicateNode"},"right":{"$ref":"#/$defs/PredicateNode"}},"required":["kind","left","right"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"or","type":"string"},"left":{"$ref":"#/$defs/PredicateNode"},"right":{"$ref":"#/$defs/PredicateNode"}},"required":["kind","left","right"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"allOf","type":"string"},"operands":{"items":{"$ref":"#/$defs/PredicateNode"},"type":"array"}},"required":["kind","operands"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"anyOf","type":"string"},"operands":{"items":{"$ref":"#/$defs/PredicateNode"},"type":"array"}},"required":["kind","operands"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"compare","type":"string"},"left":{"$ref":"#/$defs/ExpressionNode"},"op":{"enum":["gt","gte","lt","lte","eq","neq"],"type":"string"},"right":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","op","left","right"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"textCompare","type":"string"},"left":{"$ref":"#/$defs/ExpressionNode"},"op":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"right":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","op","left","right"],"type":"object"},{"additionalProperties":false,"properties":{"candidates":{"items":{"$ref":"#/$defs/ExpressionNode"},"type":"array"},"kind":{"const":"memberOf","type":"string"},"op":{"enum":["in","notIn"],"type":"string"},"operand":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","op","operand","candidates"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"exists","type":"string"},"operand":{"$ref":"#/$defs/ExpressionNode"}},"required":["kind","operand"],"type":"object"},{"additionalProperties":false,"properties":{"collection":{"$ref":"#/$defs/schema0"},"filter":{"$ref":"#/$defs/PredicateNode"},"item":{"$ref":"#/$defs/PredicateNode"},"kind":{"const":"some","type":"string"}},"required":["kind","collection","item"],"type":"object"},{"additionalProperties":false,"properties":{"collection":{"$ref":"#/$defs/schema0"},"filter":{"$ref":"#/$defs/PredicateNode"},"item":{"$ref":"#/$defs/PredicateNode"},"kind":{"const":"every","type":"string"}},"required":["kind","collection","item"],"type":"object"}]},"schema0":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"items":{"$ref":"#/$defs/schema0"},"type":"array"},{"additionalProperties":{"$ref":"#/$defs/schema0"},"propertyNames":{"type":"string"},"type":"object"}]}},"$id":"https://cdn.jsdelivr.net/npm/trilean@1.0.8/schemas/trilean.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","oneOf":[{"$ref":"#/$defs/PredicateNode"},{"$ref":"#/$defs/ExpressionNode"}]}
|