@uniqu/core 0.1.7 → 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 CHANGED
@@ -35,7 +35,7 @@ const query: Uniquery = {
35
35
 
36
36
  ### Filter Expressions
37
37
 
38
- A `FilterExpr` is either a **comparison node** (leaf) or a **logical node** (branch):
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
- - Multi-field comparison nodes (`{ age: ..., status: ... }`) are expanded into individual `comparison` calls wrapped in `visitor.and(...)`
295
- - `$and` / `$or` nodes recurse into children and call the corresponding visitor method
296
- - `$not` nodes recurse into the single child and call `visitor.not(...)`
297
- - Empty nodes call `visitor.and([])`
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: ... }` — variants are mutually exclusive via `never` |
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
- * - Multi-field ComparisonNodes are combined via `visitor.and(...)`.
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 [field, value] of entries) if (isPrimitive(value)) results.push(visitor.comparison(field, "$eq", value));
33
- else {
34
- const ops = value;
35
- for (const [op, opValue] of Object.entries(ops)) results.push(visitor.comparison(field, op, opValue));
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
- * Each variant forbids the other logical keys via `never` to prevent
53
- * mixing comparison fields with logical operators at the type level.
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
- * - Multi-field ComparisonNodes are combined via `visitor.and(...)`.
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
- * - Multi-field ComparisonNodes are combined via `visitor.and(...)`.
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 [field, value] of entries) if (isPrimitive(value)) results.push(visitor.comparison(field, "$eq", value));
31
- else {
32
- const ops = value;
33
- for (const [op, opValue] of Object.entries(ops)) results.push(visitor.comparison(field, op, opValue));
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqu/core",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Canonical query format types, tree walker, and utilities for Uniqu",
5
5
  "license": "MIT",
6
6
  "author": "Artem Maltsev",