@uniqu/core 0.1.6 → 0.1.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 +29 -6
- package/dist/index.cjs +20 -25
- package/dist/index.d.ts +14 -4
- package/dist/index.mjs +20 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ const query: Uniquery = {
|
|
|
35
35
|
|
|
36
36
|
### Filter Expressions
|
|
37
37
|
|
|
38
|
-
A `FilterExpr` is
|
|
38
|
+
A `FilterExpr` is a **comparison node** (leaf), a **logical node** (branch), or a mix of both in one object (see [below](#mixing-comparison-fields-with-logical-operators)):
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
41
|
// Comparison — one or more field conditions
|
|
@@ -54,6 +54,27 @@ A `FilterExpr` is either a **comparison node** (leaf) or a **logical node** (bra
|
|
|
54
54
|
{ $not: { status: 'DELETED' } }
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
#### Mixing comparison fields with logical operators
|
|
58
|
+
|
|
59
|
+
Comparison fields and logical operators may appear in the **same** object. All
|
|
60
|
+
members of an object are combined with an implicit **AND**, in key order
|
|
61
|
+
(MongoDB semantics):
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// Both the field conditions and the $or branch apply
|
|
65
|
+
{ id: 101, nextRefreshAt: { $lte: now }, $or: [{ status: 'a' }, { status: 'b' }] }
|
|
66
|
+
|
|
67
|
+
// …is equivalent to
|
|
68
|
+
{ $and: [
|
|
69
|
+
{ id: 101, nextRefreshAt: { $lte: now } },
|
|
70
|
+
{ $or: [{ status: 'a' }, { status: 'b' }] },
|
|
71
|
+
]}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The same holds for `$and` and `$not` members, and for several logical keys in
|
|
75
|
+
one object — each is simply another AND member: `{ a: 1, $and: [...], $or: [...] }`
|
|
76
|
+
means `a = 1` AND the `$and` branch AND the `$or` branch.
|
|
77
|
+
|
|
57
78
|
### Comparison Operators
|
|
58
79
|
|
|
59
80
|
| Operator | Description | Value Type |
|
|
@@ -290,11 +311,13 @@ interface FilterVisitor<R> {
|
|
|
290
311
|
|
|
291
312
|
### Walker Behavior
|
|
292
313
|
|
|
314
|
+
- Every member of a node is ANDed, in key order — comparison fields and logical keys alike. A field with several operators (`{ age: { $gte: 18, $lte: 30 } }`) contributes one `comparison` call per operator
|
|
293
315
|
- Bare primitive values (`{ name: 'John' }`) are normalized to `comparison(field, '$eq', value)` calls
|
|
294
|
-
-
|
|
295
|
-
- `$and` / `$or`
|
|
296
|
-
-
|
|
297
|
-
-
|
|
316
|
+
- A single-member node returns its result unwrapped: `and()` is not called for `{ a: 1 }` or a lone `{ $or: [...] }`, but it is called with one child for `{ $and: [x] }`
|
|
317
|
+
- `$and` / `$or` recurse into their children and call `and(...)` / `or(...)`; `$not` recurses into its single child and calls `not(...)`
|
|
318
|
+
- Children are visited before their parent (depth-first, post-order)
|
|
319
|
+
- A logical key whose value is `undefined` is skipped
|
|
320
|
+
- An empty node calls `and([])`
|
|
298
321
|
|
|
299
322
|
## Lazy Insights
|
|
300
323
|
|
|
@@ -370,7 +393,7 @@ const insights = getInsights(query)
|
|
|
370
393
|
| `FieldValue` | `Primitive \| FieldOps` |
|
|
371
394
|
| `FilterExpr<T>` | `ComparisonNode<T> \| LogicalNode<T>` |
|
|
372
395
|
| `ComparisonNode<T>` | Leaf node — keys constrained to `keyof T` when typed |
|
|
373
|
-
| `LogicalNode<T>` | `{ $and: ... } \| { $or: ... } \| { $not: ... }` —
|
|
396
|
+
| `LogicalNode<T>` | `{ $and: ... } \| { $or: ... } \| { $not: ... }` — at most one logical key per object at the type level (the others are `never`); comparison fields may sit alongside it, and the runtime ANDs several logical keys |
|
|
374
397
|
| `AggregateFn` | `'sum' \| 'count' \| 'avg' \| 'min' \| 'max'` |
|
|
375
398
|
| `AggregateExpr<Fn, Field, Alias>` | `{ $fn, $field, $as? }` — aggregate function call in `$select`. Generic params preserve literal types for result inference |
|
|
376
399
|
| `SelectExpr<T>` | `((keyof T & string) \| AggregateExpr)[] \| Record<keyof T & string, 0 \| 1>` |
|
package/dist/index.cjs
CHANGED
|
@@ -6,36 +6,30 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
6
6
|
* Returns the fully assembled result from the visitor.
|
|
7
7
|
*
|
|
8
8
|
* - Bare primitive values are normalized to `comparison(field, '$eq', value)`.
|
|
9
|
-
* -
|
|
9
|
+
* - Every member of an object is combined with implicit AND, in key insertion
|
|
10
|
+
* order — comparison fields and logical operators may be mixed freely
|
|
11
|
+
* (Mongo semantics): `{ id: 101, $or: [...] }` is equivalent to
|
|
12
|
+
* `{ $and: [{ id: 101 }, { $or: [...] }] }`. A field with several operators
|
|
13
|
+
* contributes one `comparison` per operator.
|
|
14
|
+
* - A node that yields a single result returns it unwrapped (no surrounding
|
|
15
|
+
* `and`); an empty node yields `visitor.and([])`.
|
|
10
16
|
*/ function walkFilter(expr, visitor) {
|
|
11
17
|
if (!expr) return void 0;
|
|
12
|
-
if ("$and" in expr && expr.$and !== void 0) {
|
|
13
|
-
const andArr = expr.$and;
|
|
14
|
-
const children = [];
|
|
15
|
-
for (const child of andArr) children.push(walkFilter(child, visitor));
|
|
16
|
-
return visitor.and(children);
|
|
17
|
-
}
|
|
18
|
-
if ("$or" in expr && expr.$or !== void 0) {
|
|
19
|
-
const orArr = expr.$or;
|
|
20
|
-
const children = [];
|
|
21
|
-
for (const child of orArr) children.push(walkFilter(child, visitor));
|
|
22
|
-
return visitor.or(children);
|
|
23
|
-
}
|
|
24
|
-
if ("$not" in expr && expr.$not !== void 0) {
|
|
25
|
-
const child = walkFilter(expr.$not, visitor);
|
|
26
|
-
return visitor.not(child);
|
|
27
|
-
}
|
|
28
|
-
const node = expr;
|
|
29
|
-
const entries = Object.entries(node);
|
|
30
|
-
if (entries.length === 0) return visitor.and([]);
|
|
31
18
|
const results = [];
|
|
32
|
-
for (const [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
19
|
+
for (const [key, value] of Object.entries(expr)) if (isLogicalKey(key)) {
|
|
20
|
+
if (value !== void 0) results.push(walkLogical(key, value, visitor));
|
|
21
|
+
} else if (isPrimitive(value)) results.push(visitor.comparison(key, "$eq", value));
|
|
22
|
+
else for (const [op, opValue] of Object.entries(value)) results.push(visitor.comparison(key, op, opValue));
|
|
37
23
|
return results.length === 1 ? results[0] : visitor.and(results);
|
|
38
24
|
}
|
|
25
|
+
function walkLogical(key, value, visitor) {
|
|
26
|
+
if (key === "$not") return visitor.not(walkFilter(value, visitor));
|
|
27
|
+
const children = value.map((child) => walkFilter(child, visitor));
|
|
28
|
+
return key === "$and" ? visitor.and(children) : visitor.or(children);
|
|
29
|
+
}
|
|
30
|
+
/** Type guard for the logical keys a filter node may carry. */ function isLogicalKey(key) {
|
|
31
|
+
return key === "$and" || key === "$or" || key === "$not";
|
|
32
|
+
}
|
|
39
33
|
function isPrimitive(x) {
|
|
40
34
|
if (x === null || typeof x !== "object") return true;
|
|
41
35
|
if (x instanceof RegExp || x instanceof Date) return true;
|
|
@@ -112,5 +106,6 @@ function isPrimitive(x) {
|
|
|
112
106
|
//#endregion
|
|
113
107
|
exports.computeInsights = computeInsights;
|
|
114
108
|
exports.getInsights = getInsights;
|
|
109
|
+
exports.isLogicalKey = isLogicalKey;
|
|
115
110
|
exports.isPrimitive = isPrimitive;
|
|
116
111
|
exports.walkFilter = walkFilter;
|
package/dist/index.d.ts
CHANGED
|
@@ -49,8 +49,10 @@ type ComparisonNode<T = Record<string, unknown>> = {
|
|
|
49
49
|
};
|
|
50
50
|
/**
|
|
51
51
|
* Branch node: logical combination of child expressions.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* The `never` members allow at most one logical key per object at the type
|
|
53
|
+
* level (`{ $and, $or }` is rejected); comparison fields may still sit
|
|
54
|
+
* alongside it (`{ id: 1, $or: [...] }`). At runtime every member of a node
|
|
55
|
+
* is ANDed, so several logical keys in one object are accepted and combined.
|
|
54
56
|
*/
|
|
55
57
|
type LogicalNode<T = Record<string, unknown>> = {
|
|
56
58
|
$and: FilterExpr<T>[];
|
|
@@ -209,9 +211,17 @@ interface FilterVisitor<R> {
|
|
|
209
211
|
* Returns the fully assembled result from the visitor.
|
|
210
212
|
*
|
|
211
213
|
* - Bare primitive values are normalized to `comparison(field, '$eq', value)`.
|
|
212
|
-
* -
|
|
214
|
+
* - Every member of an object is combined with implicit AND, in key insertion
|
|
215
|
+
* order — comparison fields and logical operators may be mixed freely
|
|
216
|
+
* (Mongo semantics): `{ id: 101, $or: [...] }` is equivalent to
|
|
217
|
+
* `{ $and: [{ id: 101 }, { $or: [...] }] }`. A field with several operators
|
|
218
|
+
* contributes one `comparison` per operator.
|
|
219
|
+
* - A node that yields a single result returns it unwrapped (no surrounding
|
|
220
|
+
* `and`); an empty node yields `visitor.and([])`.
|
|
213
221
|
*/
|
|
214
222
|
declare function walkFilter<R>(expr: FilterExpr | undefined, visitor: FilterVisitor<R>): R | undefined;
|
|
223
|
+
/** Type guard for the logical keys a filter node may carry. */
|
|
224
|
+
declare function isLogicalKey(key: string): key is '$and' | '$or' | '$not';
|
|
215
225
|
declare function isPrimitive(x: unknown): x is Primitive;
|
|
216
226
|
|
|
217
227
|
/**
|
|
@@ -226,5 +236,5 @@ declare function computeInsights(filter?: FilterExpr, controls?: UniqueryControl
|
|
|
226
236
|
*/
|
|
227
237
|
declare function getInsights(query: Uniquery): UniqueryInsights;
|
|
228
238
|
|
|
229
|
-
export { computeInsights, getInsights, isPrimitive, walkFilter };
|
|
239
|
+
export { computeInsights, getInsights, isLogicalKey, isPrimitive, walkFilter };
|
|
230
240
|
export type { AggregateControls, AggregateExpr, AggregateFn, AggregateQuery, AggregateResult, ComparisonNode, ComparisonOp, FieldOps, FieldOpsFor, FieldValue, FilterExpr, FilterVisitor, InsightOp, LogicalNode, NavTarget, Primitive, ResolveAlias, SelectExpr, TypedWithRelation, Uniquery, UniqueryControls, UniqueryInsights, WithRelation };
|
package/dist/index.mjs
CHANGED
|
@@ -4,36 +4,30 @@
|
|
|
4
4
|
* Returns the fully assembled result from the visitor.
|
|
5
5
|
*
|
|
6
6
|
* - Bare primitive values are normalized to `comparison(field, '$eq', value)`.
|
|
7
|
-
* -
|
|
7
|
+
* - Every member of an object is combined with implicit AND, in key insertion
|
|
8
|
+
* order — comparison fields and logical operators may be mixed freely
|
|
9
|
+
* (Mongo semantics): `{ id: 101, $or: [...] }` is equivalent to
|
|
10
|
+
* `{ $and: [{ id: 101 }, { $or: [...] }] }`. A field with several operators
|
|
11
|
+
* contributes one `comparison` per operator.
|
|
12
|
+
* - A node that yields a single result returns it unwrapped (no surrounding
|
|
13
|
+
* `and`); an empty node yields `visitor.and([])`.
|
|
8
14
|
*/ function walkFilter(expr, visitor) {
|
|
9
15
|
if (!expr) return void 0;
|
|
10
|
-
if ("$and" in expr && expr.$and !== void 0) {
|
|
11
|
-
const andArr = expr.$and;
|
|
12
|
-
const children = [];
|
|
13
|
-
for (const child of andArr) children.push(walkFilter(child, visitor));
|
|
14
|
-
return visitor.and(children);
|
|
15
|
-
}
|
|
16
|
-
if ("$or" in expr && expr.$or !== void 0) {
|
|
17
|
-
const orArr = expr.$or;
|
|
18
|
-
const children = [];
|
|
19
|
-
for (const child of orArr) children.push(walkFilter(child, visitor));
|
|
20
|
-
return visitor.or(children);
|
|
21
|
-
}
|
|
22
|
-
if ("$not" in expr && expr.$not !== void 0) {
|
|
23
|
-
const child = walkFilter(expr.$not, visitor);
|
|
24
|
-
return visitor.not(child);
|
|
25
|
-
}
|
|
26
|
-
const node = expr;
|
|
27
|
-
const entries = Object.entries(node);
|
|
28
|
-
if (entries.length === 0) return visitor.and([]);
|
|
29
16
|
const results = [];
|
|
30
|
-
for (const [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
17
|
+
for (const [key, value] of Object.entries(expr)) if (isLogicalKey(key)) {
|
|
18
|
+
if (value !== void 0) results.push(walkLogical(key, value, visitor));
|
|
19
|
+
} else if (isPrimitive(value)) results.push(visitor.comparison(key, "$eq", value));
|
|
20
|
+
else for (const [op, opValue] of Object.entries(value)) results.push(visitor.comparison(key, op, opValue));
|
|
35
21
|
return results.length === 1 ? results[0] : visitor.and(results);
|
|
36
22
|
}
|
|
23
|
+
function walkLogical(key, value, visitor) {
|
|
24
|
+
if (key === "$not") return visitor.not(walkFilter(value, visitor));
|
|
25
|
+
const children = value.map((child) => walkFilter(child, visitor));
|
|
26
|
+
return key === "$and" ? visitor.and(children) : visitor.or(children);
|
|
27
|
+
}
|
|
28
|
+
/** Type guard for the logical keys a filter node may carry. */ function isLogicalKey(key) {
|
|
29
|
+
return key === "$and" || key === "$or" || key === "$not";
|
|
30
|
+
}
|
|
37
31
|
function isPrimitive(x) {
|
|
38
32
|
if (x === null || typeof x !== "object") return true;
|
|
39
33
|
if (x instanceof RegExp || x instanceof Date) return true;
|
|
@@ -108,4 +102,4 @@ function isPrimitive(x) {
|
|
|
108
102
|
}
|
|
109
103
|
|
|
110
104
|
//#endregion
|
|
111
|
-
export { computeInsights, getInsights, isPrimitive, walkFilter };
|
|
105
|
+
export { computeInsights, getInsights, isLogicalKey, isPrimitive, walkFilter };
|