@uniqu/url 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 +15 -0
- package/dist/builder.cjs +46 -34
- package/dist/builder.mjs +47 -35
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -113,6 +113,14 @@ age>25^score>550&status=VIP
|
|
|
113
113
|
→ { $and: [{ $or: [{ age: { $gt: 25 } }, { score: { $gt: 550 } }] }, { status: 'VIP' }] }
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
Comparison fields and a logical operator in the same filter object are ANDed
|
|
117
|
+
together (see the [`@uniqu/core` README](../core/README.md#filter-expressions)),
|
|
118
|
+
so `buildUrl` emits them `&`-joined and parenthesizes an `$or` that has
|
|
119
|
+
siblings: `{ id: 101, $or: [{ s: 'a' }, { s: 'b' }] }` → `id=101&(s=a^s=b)`.
|
|
120
|
+
Conversely, a multi-part AND inside an `$or` is always parenthesized, whether
|
|
121
|
+
it is an explicit `$and` or an implicit one:
|
|
122
|
+
`{ $or: [{ $and: [{ a: 1 }, { b: 2 }] }, { c: 3 }] }` → `(a=1&b=2)^c=3`.
|
|
123
|
+
|
|
116
124
|
Adjacent AND conditions on the same field are merged when safe:
|
|
117
125
|
|
|
118
126
|
```
|
|
@@ -486,6 +494,13 @@ const parsed = parseUrl(url)
|
|
|
486
494
|
// parsed.controls.$limit → 10
|
|
487
495
|
```
|
|
488
496
|
|
|
497
|
+
Round-trips are semantic, not structural: `parseUrl` returns the canonical
|
|
498
|
+
explicit-`$and` form, with logical branches first and the merged comparison
|
|
499
|
+
fields last, so `{ id: 101, $or: [...] }` comes back as
|
|
500
|
+
`{ $and: [{ $or: [...] }, { id: 101 }] }`. Likewise an explicit `$and` inside
|
|
501
|
+
an `$or` is emitted parenthesized (`(a=1&b=2)^c=3`), which parses to the same
|
|
502
|
+
tree as the unparenthesized form.
|
|
503
|
+
|
|
489
504
|
### Bundle optimization
|
|
490
505
|
|
|
491
506
|
The builder is a separate entry point (`@uniqu/url/builder`) so that apps using only `buildUrl` don't pull in the lexer and parser code, and vice versa.
|
package/dist/builder.cjs
CHANGED
|
@@ -14,39 +14,51 @@ let _uniqu_core = require("@uniqu/core");
|
|
|
14
14
|
if (filterStr && controlStr) return filterStr + "&" + controlStr;
|
|
15
15
|
return filterStr || controlStr;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Join the non-empty parts with `&` (and) or `^` (or). `&` binds tighter than
|
|
19
|
+
* `^`, so an `or` child inside an `and` and a multi-part `and` child inside an
|
|
20
|
+
* `or` are parenthesized. A single non-empty part is returned as is — its kind
|
|
21
|
+
* is preserved so an enclosing node can still group it correctly.
|
|
22
|
+
*/ function joinParts(children, kind) {
|
|
23
|
+
const parts = children.filter((child) => child.s);
|
|
24
|
+
if (parts.length === 0) return {
|
|
25
|
+
s: "",
|
|
26
|
+
kind: "leaf"
|
|
27
|
+
};
|
|
28
|
+
if (parts.length === 1) return parts[0];
|
|
29
|
+
const wrap = kind === "and" ? "or" : "and";
|
|
30
|
+
const separator = kind === "and" ? "&" : "^";
|
|
31
|
+
return {
|
|
32
|
+
s: parts.map((child) => child.kind === wrap ? `(${child.s})` : child.s).join(separator),
|
|
33
|
+
kind
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* URL serializer as a `walkFilter` visitor. The walker owns the traversal
|
|
38
|
+
* rules (every member of a node is an implicit AND, `undefined` logical keys
|
|
39
|
+
* are skipped, a single-member node passes through unwrapped, one comparison
|
|
40
|
+
* per operator), so the serializer only decides how to spell each node.
|
|
41
|
+
*/ const urlVisitor = {
|
|
42
|
+
comparison(field, op, value) {
|
|
43
|
+
return {
|
|
44
|
+
s: op === "$eq" && value instanceof RegExp ? `${field}~=${serializeValue(value)}` : serializeComparison(field, op, value),
|
|
45
|
+
kind: "leaf"
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
and: (children) => joinParts(children, "and"),
|
|
49
|
+
or: (children) => joinParts(children, "or"),
|
|
50
|
+
not: (child) => ({
|
|
51
|
+
s: child.s ? `!(${child.s})` : "",
|
|
52
|
+
kind: "leaf"
|
|
53
|
+
})
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Serialize a filter expression. Every member of a node is an implicit AND,
|
|
57
|
+
* in key insertion order — comparison fields and logical operators may be
|
|
58
|
+
* mixed in the same object (Mongo semantics), e.g.
|
|
59
|
+
* `{ id: 101, $or: [...] }` → `id=101&(…^…)`.
|
|
60
|
+
*/ function serializeFilter(expr) {
|
|
61
|
+
return (0, _uniqu_core.walkFilter)(expr, urlVisitor)?.s ?? "";
|
|
50
62
|
}
|
|
51
63
|
function serializeComparison(field, op, value) {
|
|
52
64
|
switch (op) {
|
|
@@ -131,7 +143,7 @@ function serializeControls(controls) {
|
|
|
131
143
|
if (controls.$having) {
|
|
132
144
|
const havingStr = serializeFilter(controls.$having);
|
|
133
145
|
if (havingStr) {
|
|
134
|
-
const part = "
|
|
146
|
+
const part = havingStr.includes("&") ? `$having=(${havingStr})` : `$having=${havingStr}`;
|
|
135
147
|
result = result ? result + "&" + part : part;
|
|
136
148
|
}
|
|
137
149
|
}
|
package/dist/builder.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { walkFilter } from "@uniqu/core";
|
|
2
2
|
|
|
3
3
|
//#region packages/url/src/builder.ts
|
|
4
4
|
/**
|
|
@@ -13,39 +13,51 @@ import { isPrimitive } from "@uniqu/core";
|
|
|
13
13
|
if (filterStr && controlStr) return filterStr + "&" + controlStr;
|
|
14
14
|
return filterStr || controlStr;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Join the non-empty parts with `&` (and) or `^` (or). `&` binds tighter than
|
|
18
|
+
* `^`, so an `or` child inside an `and` and a multi-part `and` child inside an
|
|
19
|
+
* `or` are parenthesized. A single non-empty part is returned as is — its kind
|
|
20
|
+
* is preserved so an enclosing node can still group it correctly.
|
|
21
|
+
*/ function joinParts(children, kind) {
|
|
22
|
+
const parts = children.filter((child) => child.s);
|
|
23
|
+
if (parts.length === 0) return {
|
|
24
|
+
s: "",
|
|
25
|
+
kind: "leaf"
|
|
26
|
+
};
|
|
27
|
+
if (parts.length === 1) return parts[0];
|
|
28
|
+
const wrap = kind === "and" ? "or" : "and";
|
|
29
|
+
const separator = kind === "and" ? "&" : "^";
|
|
30
|
+
return {
|
|
31
|
+
s: parts.map((child) => child.kind === wrap ? `(${child.s})` : child.s).join(separator),
|
|
32
|
+
kind
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* URL serializer as a `walkFilter` visitor. The walker owns the traversal
|
|
37
|
+
* rules (every member of a node is an implicit AND, `undefined` logical keys
|
|
38
|
+
* are skipped, a single-member node passes through unwrapped, one comparison
|
|
39
|
+
* per operator), so the serializer only decides how to spell each node.
|
|
40
|
+
*/ const urlVisitor = {
|
|
41
|
+
comparison(field, op, value) {
|
|
42
|
+
return {
|
|
43
|
+
s: op === "$eq" && value instanceof RegExp ? `${field}~=${serializeValue(value)}` : serializeComparison(field, op, value),
|
|
44
|
+
kind: "leaf"
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
and: (children) => joinParts(children, "and"),
|
|
48
|
+
or: (children) => joinParts(children, "or"),
|
|
49
|
+
not: (child) => ({
|
|
50
|
+
s: child.s ? `!(${child.s})` : "",
|
|
51
|
+
kind: "leaf"
|
|
52
|
+
})
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Serialize a filter expression. Every member of a node is an implicit AND,
|
|
56
|
+
* in key insertion order — comparison fields and logical operators may be
|
|
57
|
+
* mixed in the same object (Mongo semantics), e.g.
|
|
58
|
+
* `{ id: 101, $or: [...] }` → `id=101&(…^…)`.
|
|
59
|
+
*/ function serializeFilter(expr) {
|
|
60
|
+
return walkFilter(expr, urlVisitor)?.s ?? "";
|
|
49
61
|
}
|
|
50
62
|
function serializeComparison(field, op, value) {
|
|
51
63
|
switch (op) {
|
|
@@ -130,7 +142,7 @@ function serializeControls(controls) {
|
|
|
130
142
|
if (controls.$having) {
|
|
131
143
|
const havingStr = serializeFilter(controls.$having);
|
|
132
144
|
if (havingStr) {
|
|
133
|
-
const part = "
|
|
145
|
+
const part = havingStr.includes("&") ? `$having=(${havingStr})` : `$having=${havingStr}`;
|
|
134
146
|
result = result ? result + "&" + part : part;
|
|
135
147
|
}
|
|
136
148
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -298,7 +298,7 @@ function unescapeString(str) {
|
|
|
298
298
|
const merged = [];
|
|
299
299
|
let currentMerge = {};
|
|
300
300
|
for (const node of nodes) {
|
|
301
|
-
if (
|
|
301
|
+
if (Object.keys(node).some(_uniqu_core.isLogicalKey)) {
|
|
302
302
|
merged.push(node);
|
|
303
303
|
continue;
|
|
304
304
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isPrimitive } from "@uniqu/core";
|
|
1
|
+
import { isLogicalKey, isPrimitive } from "@uniqu/core";
|
|
2
2
|
|
|
3
3
|
//#region packages/url/src/tokens.ts
|
|
4
4
|
/**
|
|
@@ -297,7 +297,7 @@ function unescapeString(str) {
|
|
|
297
297
|
const merged = [];
|
|
298
298
|
let currentMerge = {};
|
|
299
299
|
for (const node of nodes) {
|
|
300
|
-
if (
|
|
300
|
+
if (Object.keys(node).some(isLogicalKey)) {
|
|
301
301
|
merged.push(node);
|
|
302
302
|
continue;
|
|
303
303
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqu/url",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "URL query string parser producing the Uniqu canonical query format",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Artem Maltsev",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dist"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@uniqu/core": "^0.1.
|
|
48
|
+
"@uniqu/core": "^0.1.8"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"pub": "pnpm publish --access public",
|