@uniqu/url 0.0.6 → 0.1.1
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 +130 -2
- package/dist/builder.cjs +181 -0
- package/dist/builder.d.ts +12 -0
- package/dist/builder.mjs +180 -0
- package/dist/index.cjs +91 -41
- package/dist/index.mjs +91 -41
- package/package.json +19 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<img src="../../logo.svg" alt="uniqu" height="80">
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
URL query string parser
|
|
7
|
+
URL query string parser and builder for the [Uniqu](../../README.md) canonical query format. Human-readable URL syntax with full filter expressions, sorting, pagination, projection, and aggregation.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -151,11 +151,75 @@ Control keywords start with `$` and are separated from filter expressions:
|
|
|
151
151
|
| `$limit` | `$top` | `$limit=20` | `{ $limit: 20 }` |
|
|
152
152
|
| `$skip` | — | `$skip=40` | `{ $skip: 40 }` |
|
|
153
153
|
| `$count` | — | `$count` | `{ $count: true }` |
|
|
154
|
+
| `$groupBy` | — | `$groupBy=currency,region` | `{ $groupBy: ['currency', 'region'] }` |
|
|
154
155
|
| `$with` | — | `$with=posts,author` | `{ $with: [{ name: 'posts', filter: {}, controls: {} }, ...] }` |
|
|
155
156
|
| `$<custom>` | — | `$search=term` | `{ $search: 'term' }` |
|
|
156
157
|
|
|
157
158
|
Prefix a field with `-` in `$select` to exclude it. When any exclusion is present, `$select` produces an object (`{ name: 1, password: 0 }`); otherwise it produces an array (`['name', 'email']`). Prefix with `-` in `$order` for descending sort.
|
|
158
159
|
|
|
160
|
+
### Aggregate Functions in `$select`
|
|
161
|
+
|
|
162
|
+
`$select` supports aggregate function calls using `fn(field)` syntax. An optional alias can be specified with `:alias`:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
$select=sum(amount) → [{ $fn: 'sum', $field: 'amount', $as: 'sum_amount' }]
|
|
166
|
+
$select=sum(amount):total → [{ $fn: 'sum', $field: 'amount', $as: 'total' }]
|
|
167
|
+
$select=count(*) → [{ $fn: 'count', $field: '*', $as: 'count_star' }]
|
|
168
|
+
$select=sum(amount),currency → [{ $fn: 'sum', $field: 'amount', $as: 'sum_amount' }, 'currency']
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
When no alias is given, one is auto-generated as `{fn}_{field}` (with `*` becoming `star`).
|
|
172
|
+
|
|
173
|
+
Supported functions: `sum`, `count`, `avg`, `min`, `max`, plus any custom function name — consumers validate supported functions.
|
|
174
|
+
|
|
175
|
+
When aggregates are present, `$select` always uses the array form (even if `-` prefixed fields are mixed in).
|
|
176
|
+
|
|
177
|
+
### Grouping (`$groupBy`)
|
|
178
|
+
|
|
179
|
+
`$groupBy` declares which fields to group by. Comma-separated:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
$groupBy=currency → ['currency']
|
|
183
|
+
$groupBy=currency,region → ['currency', 'region']
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Aggregation Example
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
$select=sum(amount):total,count(*),currency&$groupBy=currency&$sort=-total&$limit=10
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Produces:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
{
|
|
196
|
+
controls: {
|
|
197
|
+
$select: [
|
|
198
|
+
'currency',
|
|
199
|
+
{ $fn: 'sum', $field: 'amount', $as: 'total' },
|
|
200
|
+
{ $fn: 'count', $field: '*', $as: 'count_star' },
|
|
201
|
+
],
|
|
202
|
+
$groupBy: ['currency'],
|
|
203
|
+
$sort: { total: -1 },
|
|
204
|
+
$limit: 10,
|
|
205
|
+
},
|
|
206
|
+
insights: Map {
|
|
207
|
+
'amount' => Set { 'sum' },
|
|
208
|
+
'*' => Set { 'count' },
|
|
209
|
+
'currency' => Set { '$select', '$groupBy' },
|
|
210
|
+
'total' => Set { '$order' },
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Insights track aggregate functions with bare names (`'sum'`, `'count'`), distinct from `$`-prefixed control ops (`'$select'`, `'$groupBy'`).
|
|
216
|
+
|
|
217
|
+
Aggregates and `$groupBy` work inside `$with` sub-queries too:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
$with=orders($select=sum(total):revenue&$groupBy=status)
|
|
221
|
+
```
|
|
222
|
+
|
|
159
223
|
### Relation Loading (`$with`)
|
|
160
224
|
|
|
161
225
|
`$with` declares which relations to populate alongside the primary query. Relations are comma-separated:
|
|
@@ -279,7 +343,7 @@ Uniqu parses and types the `$with` declaration. The consumer (e.g. a database ad
|
|
|
279
343
|
|
|
280
344
|
## Insights
|
|
281
345
|
|
|
282
|
-
Insights are computed **eagerly** during URL parsing — a `Map<string, Set<InsightOp>>` recording which fields are used and with which operators. This includes
|
|
346
|
+
Insights are computed **eagerly** during URL parsing — a `Map<string, Set<InsightOp>>` recording which fields are used and with which operators. This includes filter operators, control usage (`$select`, `$order`, `$groupBy`), and aggregate functions (`sum`, `avg`, etc. — bare names without `$` prefix).
|
|
283
347
|
|
|
284
348
|
For queries constructed as JSON objects (not parsed from URL), use `computeInsights()` from `@uniqu/core` for **lazy** computation.
|
|
285
349
|
|
|
@@ -340,12 +404,76 @@ Produces:
|
|
|
340
404
|
}
|
|
341
405
|
```
|
|
342
406
|
|
|
407
|
+
## URL Builder
|
|
408
|
+
|
|
409
|
+
Build URL query strings from `Uniquery` objects — the inverse of `parseUrl`. Available as a separate entry point for optimal bundle size:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import { buildUrl } from '@uniqu/url/builder'
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Usage
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
const url = buildUrl({
|
|
419
|
+
filter: { status: 'active', age: { $gte: 18 } },
|
|
420
|
+
controls: {
|
|
421
|
+
$select: ['name', 'email'],
|
|
422
|
+
$sort: { createdAt: -1 },
|
|
423
|
+
$limit: 20,
|
|
424
|
+
},
|
|
425
|
+
})
|
|
426
|
+
// → "status=active&age>=18&$select=name,email&$sort=-createdAt&$limit=20"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### `buildUrl(query: Uniquery): string`
|
|
430
|
+
|
|
431
|
+
Accepts a `Uniquery` object and returns a URL query string (without leading `?`).
|
|
432
|
+
|
|
433
|
+
All features are supported:
|
|
434
|
+
- Filter expressions (comparisons, `$and`/`$or`/`$not`, `$in`/`$nin`, `$exists`, `$regex`)
|
|
435
|
+
- Controls (`$select`, `$sort`, `$limit`, `$skip`, `$count`, `$groupBy`, `$with`)
|
|
436
|
+
- Aggregates in `$select` (`sum(amount):total`)
|
|
437
|
+
- Nested `$with` sub-queries
|
|
438
|
+
- Pass-through custom `$`-prefixed controls
|
|
439
|
+
|
|
440
|
+
### Value serialization
|
|
441
|
+
|
|
442
|
+
- Strings that look like numbers, booleans, or `null` are automatically quoted (`'25'`, `'true'`, `'null'`)
|
|
443
|
+
- Strings with special characters (`&`, `^`, `=`, spaces, quotes) are quoted and escaped
|
|
444
|
+
- Leading-zero numbers (`007`) stay as bare strings
|
|
445
|
+
- `Date` values are serialized as quoted ISO strings
|
|
446
|
+
- `RegExp` values are serialized as `/pattern/flags`
|
|
447
|
+
|
|
448
|
+
### Round-tripping
|
|
449
|
+
|
|
450
|
+
`buildUrl` produces output compatible with `parseUrl`:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
import { parseUrl } from '@uniqu/url'
|
|
454
|
+
import { buildUrl } from '@uniqu/url/builder'
|
|
455
|
+
|
|
456
|
+
const query = { filter: { age: { $gte: 18 } }, controls: { $limit: 10 } }
|
|
457
|
+
const url = buildUrl(query)
|
|
458
|
+
const parsed = parseUrl(url)
|
|
459
|
+
// parsed.filter → { age: { $gte: 18 } }
|
|
460
|
+
// parsed.controls.$limit → 10
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Bundle optimization
|
|
464
|
+
|
|
465
|
+
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.
|
|
466
|
+
|
|
343
467
|
## API Reference
|
|
344
468
|
|
|
345
469
|
### `parseUrl(raw: string): UrlQuery`
|
|
346
470
|
|
|
347
471
|
Parse a URL query string (without the leading `?`) into the uniqu format.
|
|
348
472
|
|
|
473
|
+
### `buildUrl(query: Uniquery): string`
|
|
474
|
+
|
|
475
|
+
Build a URL query string from a `Uniquery` object. Imported from `@uniqu/url/builder`.
|
|
476
|
+
|
|
349
477
|
### `UrlQuery`
|
|
350
478
|
|
|
351
479
|
```ts
|
package/dist/builder.cjs
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let _uniqu_core = require("@uniqu/core");
|
|
3
|
+
|
|
4
|
+
//#region packages/url/src/builder.ts
|
|
5
|
+
/**
|
|
6
|
+
* Build a URL query string from a Uniquery object.
|
|
7
|
+
* Produces output compatible with `parseUrl` from `@uniqu/url`.
|
|
8
|
+
*
|
|
9
|
+
* @param query - The canonical query to serialize
|
|
10
|
+
* @returns URL query string without leading "?"
|
|
11
|
+
*/ function buildUrl(query) {
|
|
12
|
+
const filterStr = query.filter ? serializeFilter(query.filter) : "";
|
|
13
|
+
const controlStr = query.controls ? serializeControls(query.controls) : "";
|
|
14
|
+
if (filterStr && controlStr) return filterStr + "&" + controlStr;
|
|
15
|
+
return filterStr || controlStr;
|
|
16
|
+
}
|
|
17
|
+
function serializeFilter(expr) {
|
|
18
|
+
if ("$and" in expr && expr.$and !== void 0) {
|
|
19
|
+
let result = "";
|
|
20
|
+
for (const child of expr.$and) {
|
|
21
|
+
const s = serializeFilter(child);
|
|
22
|
+
if (s) result = result ? result + "&" + s : s;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
if ("$or" in expr && expr.$or !== void 0) {
|
|
27
|
+
let result = "";
|
|
28
|
+
for (const child of expr.$or) {
|
|
29
|
+
const s = serializeFilter(child);
|
|
30
|
+
if (s) result = result ? result + "^" + s : s;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
if ("$not" in expr && expr.$not !== void 0) {
|
|
35
|
+
const inner = serializeFilter(expr.$not);
|
|
36
|
+
return inner ? `!(${inner})` : "";
|
|
37
|
+
}
|
|
38
|
+
let result = "";
|
|
39
|
+
for (const [field, value] of Object.entries(expr)) if ((0, _uniqu_core.isPrimitive)(value)) {
|
|
40
|
+
const part = `${field}=${serializeValue(value)}`;
|
|
41
|
+
result = result ? result + "&" + part : part;
|
|
42
|
+
} else for (const [op, opValue] of Object.entries(value)) {
|
|
43
|
+
const part = serializeComparison(field, op, opValue);
|
|
44
|
+
result = result ? result + "&" + part : part;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function serializeComparison(field, op, value) {
|
|
49
|
+
switch (op) {
|
|
50
|
+
case "$eq": return `${field}=${serializeValue(value)}`;
|
|
51
|
+
case "$ne": return `${field}!=${serializeValue(value)}`;
|
|
52
|
+
case "$gt": return `${field}>${serializeValue(value)}`;
|
|
53
|
+
case "$gte": return `${field}>=${serializeValue(value)}`;
|
|
54
|
+
case "$lt": return `${field}<${serializeValue(value)}`;
|
|
55
|
+
case "$lte": return `${field}<=${serializeValue(value)}`;
|
|
56
|
+
case "$regex": return `${field}~=${serializeValue(value)}`;
|
|
57
|
+
case "$in": {
|
|
58
|
+
let list = "";
|
|
59
|
+
for (const item of value) {
|
|
60
|
+
const s = serializeValue(item);
|
|
61
|
+
list = list ? list + "," + s : s;
|
|
62
|
+
}
|
|
63
|
+
return `${field}{${list}}`;
|
|
64
|
+
}
|
|
65
|
+
case "$nin": {
|
|
66
|
+
let list = "";
|
|
67
|
+
for (const item of value) {
|
|
68
|
+
const s = serializeValue(item);
|
|
69
|
+
list = list ? list + "," + s : s;
|
|
70
|
+
}
|
|
71
|
+
return `${field}!{${list}}`;
|
|
72
|
+
}
|
|
73
|
+
case "$exists": return value ? `$exists=${field}` : `$!exists=${field}`;
|
|
74
|
+
default: return `${field}${op}${serializeValue(value)}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function serializeValue(value) {
|
|
78
|
+
if (value === null) return "null";
|
|
79
|
+
if (value === true) return "true";
|
|
80
|
+
if (value === false) return "false";
|
|
81
|
+
if (typeof value === "number") return String(value);
|
|
82
|
+
if (value instanceof RegExp) return value.toString();
|
|
83
|
+
if (value instanceof Date) return `'${value.toISOString()}'`;
|
|
84
|
+
const str = String(value);
|
|
85
|
+
if (str === "null" || str === "true" || str === "false") return `'${str}'`;
|
|
86
|
+
if (/^\d/.test(str) && !isNaN(Number(str)) && !str.startsWith("0")) return `'${str}'`;
|
|
87
|
+
if (str.startsWith("/") && /\/[gimsuy]*$/.test(str)) return str;
|
|
88
|
+
if (/[&^=!<>~(){},\s'\\]/.test(str)) return `'${str.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
89
|
+
return str;
|
|
90
|
+
}
|
|
91
|
+
const KNOWN_CONTROL_KEYS = new Set([
|
|
92
|
+
"$select",
|
|
93
|
+
"$groupBy",
|
|
94
|
+
"$having",
|
|
95
|
+
"$sort",
|
|
96
|
+
"$limit",
|
|
97
|
+
"$skip",
|
|
98
|
+
"$count",
|
|
99
|
+
"$with"
|
|
100
|
+
]);
|
|
101
|
+
function serializeControls(controls) {
|
|
102
|
+
let result = "";
|
|
103
|
+
if (controls.$select) {
|
|
104
|
+
let seg = "";
|
|
105
|
+
if (Array.isArray(controls.$select)) for (const entry of controls.$select) {
|
|
106
|
+
let s;
|
|
107
|
+
if (typeof entry === "string") s = entry;
|
|
108
|
+
else {
|
|
109
|
+
const agg = entry;
|
|
110
|
+
s = agg.$as ? `${agg.$fn}(${agg.$field}):${agg.$as}` : `${agg.$fn}(${agg.$field})`;
|
|
111
|
+
}
|
|
112
|
+
seg = seg ? seg + "," + s : s;
|
|
113
|
+
}
|
|
114
|
+
else for (const [field, val] of Object.entries(controls.$select)) {
|
|
115
|
+
const s = val === 0 ? `-${field}` : field;
|
|
116
|
+
seg = seg ? seg + "," + s : s;
|
|
117
|
+
}
|
|
118
|
+
if (seg) result = `$select=${seg}`;
|
|
119
|
+
}
|
|
120
|
+
if (controls.$groupBy?.length) {
|
|
121
|
+
let seg = "";
|
|
122
|
+
for (const field of controls.$groupBy) seg = seg ? seg + "," + field : field;
|
|
123
|
+
const part = `$groupBy=${seg}`;
|
|
124
|
+
result = result ? result + "&" + part : part;
|
|
125
|
+
}
|
|
126
|
+
if (controls.$having) {
|
|
127
|
+
const havingStr = serializeFilter(controls.$having);
|
|
128
|
+
if (havingStr) {
|
|
129
|
+
const part = "$and" in controls.$having ? `$having=(${havingStr})` : `$having=${havingStr}`;
|
|
130
|
+
result = result ? result + "&" + part : part;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (controls.$sort) {
|
|
134
|
+
let seg = "";
|
|
135
|
+
for (const [field, dir] of Object.entries(controls.$sort)) {
|
|
136
|
+
const s = dir === -1 ? `-${field}` : field;
|
|
137
|
+
seg = seg ? seg + "," + s : s;
|
|
138
|
+
}
|
|
139
|
+
if (seg) {
|
|
140
|
+
const part = `$sort=${seg}`;
|
|
141
|
+
result = result ? result + "&" + part : part;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (controls.$limit !== void 0) {
|
|
145
|
+
const part = `$limit=${controls.$limit}`;
|
|
146
|
+
result = result ? result + "&" + part : part;
|
|
147
|
+
}
|
|
148
|
+
if (controls.$skip !== void 0) {
|
|
149
|
+
const part = `$skip=${controls.$skip}`;
|
|
150
|
+
result = result ? result + "&" + part : part;
|
|
151
|
+
}
|
|
152
|
+
if (controls.$count) result = result ? result + "&$count" : "$count";
|
|
153
|
+
if (controls.$with) {
|
|
154
|
+
let seg = "";
|
|
155
|
+
for (const entry of controls.$with) {
|
|
156
|
+
let s;
|
|
157
|
+
if (typeof entry === "string") s = entry;
|
|
158
|
+
else {
|
|
159
|
+
const rel = entry;
|
|
160
|
+
const inner = buildUrl({
|
|
161
|
+
filter: rel.filter,
|
|
162
|
+
controls: rel.controls
|
|
163
|
+
});
|
|
164
|
+
s = inner ? `${rel.name}(${inner})` : rel.name;
|
|
165
|
+
}
|
|
166
|
+
seg = seg ? seg + "," + s : s;
|
|
167
|
+
}
|
|
168
|
+
if (seg) {
|
|
169
|
+
const part = `$with=${seg}`;
|
|
170
|
+
result = result ? result + "&" + part : part;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const [key, value] of Object.entries(controls)) if (key.startsWith("$") && !KNOWN_CONTROL_KEYS.has(key)) {
|
|
174
|
+
const part = value !== void 0 && value !== "" ? `${key}=${value}` : key;
|
|
175
|
+
result = result ? result + "&" + part : part;
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
exports.buildUrl = buildUrl;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Uniquery } from '@uniqu/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a URL query string from a Uniquery object.
|
|
5
|
+
* Produces output compatible with `parseUrl` from `@uniqu/url`.
|
|
6
|
+
*
|
|
7
|
+
* @param query - The canonical query to serialize
|
|
8
|
+
* @returns URL query string without leading "?"
|
|
9
|
+
*/
|
|
10
|
+
declare function buildUrl(query: Uniquery): string;
|
|
11
|
+
|
|
12
|
+
export { buildUrl };
|
package/dist/builder.mjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { isPrimitive } from "@uniqu/core";
|
|
2
|
+
|
|
3
|
+
//#region packages/url/src/builder.ts
|
|
4
|
+
/**
|
|
5
|
+
* Build a URL query string from a Uniquery object.
|
|
6
|
+
* Produces output compatible with `parseUrl` from `@uniqu/url`.
|
|
7
|
+
*
|
|
8
|
+
* @param query - The canonical query to serialize
|
|
9
|
+
* @returns URL query string without leading "?"
|
|
10
|
+
*/ function buildUrl(query) {
|
|
11
|
+
const filterStr = query.filter ? serializeFilter(query.filter) : "";
|
|
12
|
+
const controlStr = query.controls ? serializeControls(query.controls) : "";
|
|
13
|
+
if (filterStr && controlStr) return filterStr + "&" + controlStr;
|
|
14
|
+
return filterStr || controlStr;
|
|
15
|
+
}
|
|
16
|
+
function serializeFilter(expr) {
|
|
17
|
+
if ("$and" in expr && expr.$and !== void 0) {
|
|
18
|
+
let result = "";
|
|
19
|
+
for (const child of expr.$and) {
|
|
20
|
+
const s = serializeFilter(child);
|
|
21
|
+
if (s) result = result ? result + "&" + s : s;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
if ("$or" in expr && expr.$or !== void 0) {
|
|
26
|
+
let result = "";
|
|
27
|
+
for (const child of expr.$or) {
|
|
28
|
+
const s = serializeFilter(child);
|
|
29
|
+
if (s) result = result ? result + "^" + s : s;
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
if ("$not" in expr && expr.$not !== void 0) {
|
|
34
|
+
const inner = serializeFilter(expr.$not);
|
|
35
|
+
return inner ? `!(${inner})` : "";
|
|
36
|
+
}
|
|
37
|
+
let result = "";
|
|
38
|
+
for (const [field, value] of Object.entries(expr)) if (isPrimitive(value)) {
|
|
39
|
+
const part = `${field}=${serializeValue(value)}`;
|
|
40
|
+
result = result ? result + "&" + part : part;
|
|
41
|
+
} else for (const [op, opValue] of Object.entries(value)) {
|
|
42
|
+
const part = serializeComparison(field, op, opValue);
|
|
43
|
+
result = result ? result + "&" + part : part;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function serializeComparison(field, op, value) {
|
|
48
|
+
switch (op) {
|
|
49
|
+
case "$eq": return `${field}=${serializeValue(value)}`;
|
|
50
|
+
case "$ne": return `${field}!=${serializeValue(value)}`;
|
|
51
|
+
case "$gt": return `${field}>${serializeValue(value)}`;
|
|
52
|
+
case "$gte": return `${field}>=${serializeValue(value)}`;
|
|
53
|
+
case "$lt": return `${field}<${serializeValue(value)}`;
|
|
54
|
+
case "$lte": return `${field}<=${serializeValue(value)}`;
|
|
55
|
+
case "$regex": return `${field}~=${serializeValue(value)}`;
|
|
56
|
+
case "$in": {
|
|
57
|
+
let list = "";
|
|
58
|
+
for (const item of value) {
|
|
59
|
+
const s = serializeValue(item);
|
|
60
|
+
list = list ? list + "," + s : s;
|
|
61
|
+
}
|
|
62
|
+
return `${field}{${list}}`;
|
|
63
|
+
}
|
|
64
|
+
case "$nin": {
|
|
65
|
+
let list = "";
|
|
66
|
+
for (const item of value) {
|
|
67
|
+
const s = serializeValue(item);
|
|
68
|
+
list = list ? list + "," + s : s;
|
|
69
|
+
}
|
|
70
|
+
return `${field}!{${list}}`;
|
|
71
|
+
}
|
|
72
|
+
case "$exists": return value ? `$exists=${field}` : `$!exists=${field}`;
|
|
73
|
+
default: return `${field}${op}${serializeValue(value)}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function serializeValue(value) {
|
|
77
|
+
if (value === null) return "null";
|
|
78
|
+
if (value === true) return "true";
|
|
79
|
+
if (value === false) return "false";
|
|
80
|
+
if (typeof value === "number") return String(value);
|
|
81
|
+
if (value instanceof RegExp) return value.toString();
|
|
82
|
+
if (value instanceof Date) return `'${value.toISOString()}'`;
|
|
83
|
+
const str = String(value);
|
|
84
|
+
if (str === "null" || str === "true" || str === "false") return `'${str}'`;
|
|
85
|
+
if (/^\d/.test(str) && !isNaN(Number(str)) && !str.startsWith("0")) return `'${str}'`;
|
|
86
|
+
if (str.startsWith("/") && /\/[gimsuy]*$/.test(str)) return str;
|
|
87
|
+
if (/[&^=!<>~(){},\s'\\]/.test(str)) return `'${str.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
88
|
+
return str;
|
|
89
|
+
}
|
|
90
|
+
const KNOWN_CONTROL_KEYS = new Set([
|
|
91
|
+
"$select",
|
|
92
|
+
"$groupBy",
|
|
93
|
+
"$having",
|
|
94
|
+
"$sort",
|
|
95
|
+
"$limit",
|
|
96
|
+
"$skip",
|
|
97
|
+
"$count",
|
|
98
|
+
"$with"
|
|
99
|
+
]);
|
|
100
|
+
function serializeControls(controls) {
|
|
101
|
+
let result = "";
|
|
102
|
+
if (controls.$select) {
|
|
103
|
+
let seg = "";
|
|
104
|
+
if (Array.isArray(controls.$select)) for (const entry of controls.$select) {
|
|
105
|
+
let s;
|
|
106
|
+
if (typeof entry === "string") s = entry;
|
|
107
|
+
else {
|
|
108
|
+
const agg = entry;
|
|
109
|
+
s = agg.$as ? `${agg.$fn}(${agg.$field}):${agg.$as}` : `${agg.$fn}(${agg.$field})`;
|
|
110
|
+
}
|
|
111
|
+
seg = seg ? seg + "," + s : s;
|
|
112
|
+
}
|
|
113
|
+
else for (const [field, val] of Object.entries(controls.$select)) {
|
|
114
|
+
const s = val === 0 ? `-${field}` : field;
|
|
115
|
+
seg = seg ? seg + "," + s : s;
|
|
116
|
+
}
|
|
117
|
+
if (seg) result = `$select=${seg}`;
|
|
118
|
+
}
|
|
119
|
+
if (controls.$groupBy?.length) {
|
|
120
|
+
let seg = "";
|
|
121
|
+
for (const field of controls.$groupBy) seg = seg ? seg + "," + field : field;
|
|
122
|
+
const part = `$groupBy=${seg}`;
|
|
123
|
+
result = result ? result + "&" + part : part;
|
|
124
|
+
}
|
|
125
|
+
if (controls.$having) {
|
|
126
|
+
const havingStr = serializeFilter(controls.$having);
|
|
127
|
+
if (havingStr) {
|
|
128
|
+
const part = "$and" in controls.$having ? `$having=(${havingStr})` : `$having=${havingStr}`;
|
|
129
|
+
result = result ? result + "&" + part : part;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (controls.$sort) {
|
|
133
|
+
let seg = "";
|
|
134
|
+
for (const [field, dir] of Object.entries(controls.$sort)) {
|
|
135
|
+
const s = dir === -1 ? `-${field}` : field;
|
|
136
|
+
seg = seg ? seg + "," + s : s;
|
|
137
|
+
}
|
|
138
|
+
if (seg) {
|
|
139
|
+
const part = `$sort=${seg}`;
|
|
140
|
+
result = result ? result + "&" + part : part;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (controls.$limit !== void 0) {
|
|
144
|
+
const part = `$limit=${controls.$limit}`;
|
|
145
|
+
result = result ? result + "&" + part : part;
|
|
146
|
+
}
|
|
147
|
+
if (controls.$skip !== void 0) {
|
|
148
|
+
const part = `$skip=${controls.$skip}`;
|
|
149
|
+
result = result ? result + "&" + part : part;
|
|
150
|
+
}
|
|
151
|
+
if (controls.$count) result = result ? result + "&$count" : "$count";
|
|
152
|
+
if (controls.$with) {
|
|
153
|
+
let seg = "";
|
|
154
|
+
for (const entry of controls.$with) {
|
|
155
|
+
let s;
|
|
156
|
+
if (typeof entry === "string") s = entry;
|
|
157
|
+
else {
|
|
158
|
+
const rel = entry;
|
|
159
|
+
const inner = buildUrl({
|
|
160
|
+
filter: rel.filter,
|
|
161
|
+
controls: rel.controls
|
|
162
|
+
});
|
|
163
|
+
s = inner ? `${rel.name}(${inner})` : rel.name;
|
|
164
|
+
}
|
|
165
|
+
seg = seg ? seg + "," + s : s;
|
|
166
|
+
}
|
|
167
|
+
if (seg) {
|
|
168
|
+
const part = `$with=${seg}`;
|
|
169
|
+
result = result ? result + "&" + part : part;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const [key, value] of Object.entries(controls)) if (key.startsWith("$") && !KNOWN_CONTROL_KEYS.has(key)) {
|
|
173
|
+
const part = value !== void 0 && value !== "" ? `${key}=${value}` : key;
|
|
174
|
+
result = result ? result + "&" + part : part;
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
export { buildUrl };
|
package/dist/index.cjs
CHANGED
|
@@ -289,7 +289,7 @@ var Parser = class {
|
|
|
289
289
|
}
|
|
290
290
|
};
|
|
291
291
|
function unescapeString(str) {
|
|
292
|
-
return str.replace(/(^'|'$)/gu, "");
|
|
292
|
+
return str.replace(/(^'|'$)/gu, "").replace(/\\(.)/gu, "$1");
|
|
293
293
|
}
|
|
294
294
|
/**
|
|
295
295
|
* Attempt to merge an array of simple nodes produced by `parseTerm`.
|
|
@@ -305,14 +305,15 @@ function unescapeString(str) {
|
|
|
305
305
|
for (const [key, val] of Object.entries(node)) if (key in currentMerge) {
|
|
306
306
|
const currentVal = currentMerge[key];
|
|
307
307
|
const currentOps = (0, _uniqu_core.isPrimitive)(currentVal) ? ["$eq"] : Object.keys(currentVal);
|
|
308
|
-
const otherOps = (0, _uniqu_core.isPrimitive)(val) ?
|
|
308
|
+
const otherOps = (0, _uniqu_core.isPrimitive)(val) ? new Set(["$eq"]) : new Set(Object.keys(val));
|
|
309
309
|
if (currentOps.some((op) => otherOps.has(op))) {
|
|
310
310
|
merged.push(currentMerge);
|
|
311
311
|
currentMerge = {};
|
|
312
312
|
} else {
|
|
313
|
-
|
|
314
|
-
for (const op of currentOps)
|
|
315
|
-
for (const op of
|
|
313
|
+
const m = {};
|
|
314
|
+
for (const op of currentOps) m[op] = (0, _uniqu_core.isPrimitive)(currentVal) ? currentVal : currentVal[op];
|
|
315
|
+
for (const op of otherOps) m[op] = (0, _uniqu_core.isPrimitive)(val) ? val : val[op];
|
|
316
|
+
currentMerge[key] = m;
|
|
316
317
|
}
|
|
317
318
|
} else currentMerge[key] = val;
|
|
318
319
|
}
|
|
@@ -350,9 +351,9 @@ function buildExists(fields, positive) {
|
|
|
350
351
|
let filter = {};
|
|
351
352
|
let parser;
|
|
352
353
|
if (exprParts.length) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
const parsed = parseFilterExpr(exprParts.join("&"));
|
|
355
|
+
parser = parsed.parser;
|
|
356
|
+
filter = parsed.expr;
|
|
356
357
|
} else parser = new Parser([]);
|
|
357
358
|
for (const [field, op] of controlInsights) parser.captureInsight(field, op);
|
|
358
359
|
return {
|
|
@@ -374,6 +375,15 @@ function buildExists(fields, positive) {
|
|
|
374
375
|
parts.push(str.slice(start));
|
|
375
376
|
return parts;
|
|
376
377
|
}
|
|
378
|
+
/** Lex + parse a raw filter expression string. */ function parseFilterExpr(raw) {
|
|
379
|
+
const parser = new Parser(lex(raw));
|
|
380
|
+
const expr = parser.parseExpression();
|
|
381
|
+
parser.expectEof();
|
|
382
|
+
return {
|
|
383
|
+
expr,
|
|
384
|
+
parser
|
|
385
|
+
};
|
|
386
|
+
}
|
|
377
387
|
/** Parse a single `$with` segment like `posts` or `posts($sort=-createdAt&status=active)`. */ function parseWithSegment(seg) {
|
|
378
388
|
if (!seg) return null;
|
|
379
389
|
const parenIdx = seg.indexOf("(");
|
|
@@ -403,14 +413,16 @@ function handleControls(parts) {
|
|
|
403
413
|
const controls = {};
|
|
404
414
|
const controlInsights = [];
|
|
405
415
|
for (const raw of parts) {
|
|
406
|
-
const
|
|
407
|
-
const
|
|
416
|
+
const eqIdx = raw.indexOf("=");
|
|
417
|
+
const key = eqIdx === -1 ? raw : raw.slice(0, eqIdx);
|
|
418
|
+
const value = eqIdx === -1 ? "" : raw.slice(eqIdx + 1);
|
|
408
419
|
switch (key) {
|
|
409
420
|
case "$with": {
|
|
410
421
|
var _controls;
|
|
411
422
|
if (!value) break;
|
|
412
423
|
(_controls = controls).$with ?? (_controls.$with = []);
|
|
413
|
-
const seen = new Set(
|
|
424
|
+
const seen = /* @__PURE__ */ new Set();
|
|
425
|
+
for (const r of controls.$with) seen.add(typeof r === "string" ? r : r.name);
|
|
414
426
|
for (const seg of splitTopLevel(value, ",")) {
|
|
415
427
|
const rel = parseWithSegment(seg);
|
|
416
428
|
if (!rel || seen.has(rel.name)) continue;
|
|
@@ -425,35 +437,50 @@ function handleControls(parts) {
|
|
|
425
437
|
break;
|
|
426
438
|
}
|
|
427
439
|
case "$select": {
|
|
440
|
+
const items = value.split(",");
|
|
428
441
|
let hasExclusion = false;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (!f)
|
|
432
|
-
if (f.startsWith("-"))
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
442
|
+
let hasAggregate = false;
|
|
443
|
+
for (const f of items) {
|
|
444
|
+
if (!f) continue;
|
|
445
|
+
if (f.startsWith("-")) hasExclusion = true;
|
|
446
|
+
else if (/^\w+\(/.test(f)) hasAggregate = true;
|
|
447
|
+
}
|
|
448
|
+
if (hasAggregate || !hasExclusion) {
|
|
449
|
+
const arr = Array.isArray(controls.$select) ? controls.$select : [];
|
|
450
|
+
for (const f of items) {
|
|
451
|
+
if (!f || /^\w+\(/.test(f)) continue;
|
|
452
|
+
arr.push(f);
|
|
453
|
+
controlInsights.push([f, "$select"]);
|
|
454
|
+
}
|
|
455
|
+
for (const f of items) {
|
|
456
|
+
if (!f) continue;
|
|
457
|
+
const aggMatch = /^(\w+)\((\*|[\w.]+)\)(?::([\w.]+))?$/.exec(f);
|
|
458
|
+
if (!aggMatch) continue;
|
|
459
|
+
const fn = aggMatch[1];
|
|
460
|
+
const field = aggMatch[2];
|
|
461
|
+
const alias = aggMatch[3] ?? (field === "*" ? `${fn}_star` : `${fn}_${field}`);
|
|
462
|
+
arr.push({
|
|
463
|
+
$fn: fn,
|
|
464
|
+
$field: field,
|
|
465
|
+
$as: alias
|
|
437
466
|
});
|
|
438
|
-
|
|
439
|
-
name: f,
|
|
440
|
-
include: true
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
if (hasExclusion) {
|
|
444
|
-
const obj = controls.$select ?? {};
|
|
445
|
-
for (const { name, include } of fields) {
|
|
446
|
-
obj[name] = include ? 1 : 0;
|
|
447
|
-
controlInsights.push([name, "$select"]);
|
|
467
|
+
controlInsights.push([field, fn]);
|
|
448
468
|
}
|
|
449
|
-
controls.$select =
|
|
469
|
+
controls.$select = arr;
|
|
450
470
|
} else {
|
|
451
|
-
const
|
|
452
|
-
for (const
|
|
453
|
-
|
|
454
|
-
|
|
471
|
+
const obj = controls.$select ?? {};
|
|
472
|
+
for (const f of items) {
|
|
473
|
+
if (!f) continue;
|
|
474
|
+
if (f.startsWith("-")) {
|
|
475
|
+
const name = f.slice(1);
|
|
476
|
+
obj[name] = 0;
|
|
477
|
+
controlInsights.push([name, "$select"]);
|
|
478
|
+
} else {
|
|
479
|
+
obj[f] = 1;
|
|
480
|
+
controlInsights.push([f, "$select"]);
|
|
481
|
+
}
|
|
455
482
|
}
|
|
456
|
-
controls.$select =
|
|
483
|
+
controls.$select = obj;
|
|
457
484
|
}
|
|
458
485
|
break;
|
|
459
486
|
}
|
|
@@ -461,13 +488,36 @@ function handleControls(parts) {
|
|
|
461
488
|
case "$order":
|
|
462
489
|
var _controls1;
|
|
463
490
|
(_controls1 = controls).$sort ?? (_controls1.$sort = {});
|
|
464
|
-
value.split(",")
|
|
465
|
-
if (!f)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
491
|
+
for (const f of value.split(",")) {
|
|
492
|
+
if (!f) continue;
|
|
493
|
+
if (f.startsWith("-")) {
|
|
494
|
+
const name = f.slice(1);
|
|
495
|
+
controls.$sort[name] = -1;
|
|
496
|
+
controlInsights.push([name, "$order"]);
|
|
497
|
+
} else {
|
|
498
|
+
controls.$sort[f] = 1;
|
|
499
|
+
controlInsights.push([f, "$order"]);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
470
502
|
break;
|
|
503
|
+
case "$groupBy":
|
|
504
|
+
var _controls2;
|
|
505
|
+
if (!value) break;
|
|
506
|
+
(_controls2 = controls).$groupBy ?? (_controls2.$groupBy = []);
|
|
507
|
+
for (const f of value.split(",")) {
|
|
508
|
+
if (!f) continue;
|
|
509
|
+
controls.$groupBy.push(f);
|
|
510
|
+
controlInsights.push([f, "$groupBy"]);
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
case "$having": {
|
|
514
|
+
if (!value) break;
|
|
515
|
+
const { expr, parser: hp } = parseFilterExpr(value);
|
|
516
|
+
if (controls.$having) controls.$having = { $and: [controls.$having, expr] };
|
|
517
|
+
else controls.$having = expr;
|
|
518
|
+
for (const [field] of hp.getInsights()) controlInsights.push([field, "$having"]);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
471
521
|
case "$limit":
|
|
472
522
|
case "$top":
|
|
473
523
|
controls.$limit = Number(value);
|
package/dist/index.mjs
CHANGED
|
@@ -288,7 +288,7 @@ var Parser = class {
|
|
|
288
288
|
}
|
|
289
289
|
};
|
|
290
290
|
function unescapeString(str) {
|
|
291
|
-
return str.replace(/(^'|'$)/gu, "");
|
|
291
|
+
return str.replace(/(^'|'$)/gu, "").replace(/\\(.)/gu, "$1");
|
|
292
292
|
}
|
|
293
293
|
/**
|
|
294
294
|
* Attempt to merge an array of simple nodes produced by `parseTerm`.
|
|
@@ -304,14 +304,15 @@ function unescapeString(str) {
|
|
|
304
304
|
for (const [key, val] of Object.entries(node)) if (key in currentMerge) {
|
|
305
305
|
const currentVal = currentMerge[key];
|
|
306
306
|
const currentOps = isPrimitive(currentVal) ? ["$eq"] : Object.keys(currentVal);
|
|
307
|
-
const otherOps = isPrimitive(val) ?
|
|
307
|
+
const otherOps = isPrimitive(val) ? new Set(["$eq"]) : new Set(Object.keys(val));
|
|
308
308
|
if (currentOps.some((op) => otherOps.has(op))) {
|
|
309
309
|
merged.push(currentMerge);
|
|
310
310
|
currentMerge = {};
|
|
311
311
|
} else {
|
|
312
|
-
|
|
313
|
-
for (const op of currentOps)
|
|
314
|
-
for (const op of
|
|
312
|
+
const m = {};
|
|
313
|
+
for (const op of currentOps) m[op] = isPrimitive(currentVal) ? currentVal : currentVal[op];
|
|
314
|
+
for (const op of otherOps) m[op] = isPrimitive(val) ? val : val[op];
|
|
315
|
+
currentMerge[key] = m;
|
|
315
316
|
}
|
|
316
317
|
} else currentMerge[key] = val;
|
|
317
318
|
}
|
|
@@ -349,9 +350,9 @@ function buildExists(fields, positive) {
|
|
|
349
350
|
let filter = {};
|
|
350
351
|
let parser;
|
|
351
352
|
if (exprParts.length) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
353
|
+
const parsed = parseFilterExpr(exprParts.join("&"));
|
|
354
|
+
parser = parsed.parser;
|
|
355
|
+
filter = parsed.expr;
|
|
355
356
|
} else parser = new Parser([]);
|
|
356
357
|
for (const [field, op] of controlInsights) parser.captureInsight(field, op);
|
|
357
358
|
return {
|
|
@@ -373,6 +374,15 @@ function buildExists(fields, positive) {
|
|
|
373
374
|
parts.push(str.slice(start));
|
|
374
375
|
return parts;
|
|
375
376
|
}
|
|
377
|
+
/** Lex + parse a raw filter expression string. */ function parseFilterExpr(raw) {
|
|
378
|
+
const parser = new Parser(lex(raw));
|
|
379
|
+
const expr = parser.parseExpression();
|
|
380
|
+
parser.expectEof();
|
|
381
|
+
return {
|
|
382
|
+
expr,
|
|
383
|
+
parser
|
|
384
|
+
};
|
|
385
|
+
}
|
|
376
386
|
/** Parse a single `$with` segment like `posts` or `posts($sort=-createdAt&status=active)`. */ function parseWithSegment(seg) {
|
|
377
387
|
if (!seg) return null;
|
|
378
388
|
const parenIdx = seg.indexOf("(");
|
|
@@ -402,14 +412,16 @@ function handleControls(parts) {
|
|
|
402
412
|
const controls = {};
|
|
403
413
|
const controlInsights = [];
|
|
404
414
|
for (const raw of parts) {
|
|
405
|
-
const
|
|
406
|
-
const
|
|
415
|
+
const eqIdx = raw.indexOf("=");
|
|
416
|
+
const key = eqIdx === -1 ? raw : raw.slice(0, eqIdx);
|
|
417
|
+
const value = eqIdx === -1 ? "" : raw.slice(eqIdx + 1);
|
|
407
418
|
switch (key) {
|
|
408
419
|
case "$with": {
|
|
409
420
|
var _controls;
|
|
410
421
|
if (!value) break;
|
|
411
422
|
(_controls = controls).$with ?? (_controls.$with = []);
|
|
412
|
-
const seen = new Set(
|
|
423
|
+
const seen = /* @__PURE__ */ new Set();
|
|
424
|
+
for (const r of controls.$with) seen.add(typeof r === "string" ? r : r.name);
|
|
413
425
|
for (const seg of splitTopLevel(value, ",")) {
|
|
414
426
|
const rel = parseWithSegment(seg);
|
|
415
427
|
if (!rel || seen.has(rel.name)) continue;
|
|
@@ -424,35 +436,50 @@ function handleControls(parts) {
|
|
|
424
436
|
break;
|
|
425
437
|
}
|
|
426
438
|
case "$select": {
|
|
439
|
+
const items = value.split(",");
|
|
427
440
|
let hasExclusion = false;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (!f)
|
|
431
|
-
if (f.startsWith("-"))
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
441
|
+
let hasAggregate = false;
|
|
442
|
+
for (const f of items) {
|
|
443
|
+
if (!f) continue;
|
|
444
|
+
if (f.startsWith("-")) hasExclusion = true;
|
|
445
|
+
else if (/^\w+\(/.test(f)) hasAggregate = true;
|
|
446
|
+
}
|
|
447
|
+
if (hasAggregate || !hasExclusion) {
|
|
448
|
+
const arr = Array.isArray(controls.$select) ? controls.$select : [];
|
|
449
|
+
for (const f of items) {
|
|
450
|
+
if (!f || /^\w+\(/.test(f)) continue;
|
|
451
|
+
arr.push(f);
|
|
452
|
+
controlInsights.push([f, "$select"]);
|
|
453
|
+
}
|
|
454
|
+
for (const f of items) {
|
|
455
|
+
if (!f) continue;
|
|
456
|
+
const aggMatch = /^(\w+)\((\*|[\w.]+)\)(?::([\w.]+))?$/.exec(f);
|
|
457
|
+
if (!aggMatch) continue;
|
|
458
|
+
const fn = aggMatch[1];
|
|
459
|
+
const field = aggMatch[2];
|
|
460
|
+
const alias = aggMatch[3] ?? (field === "*" ? `${fn}_star` : `${fn}_${field}`);
|
|
461
|
+
arr.push({
|
|
462
|
+
$fn: fn,
|
|
463
|
+
$field: field,
|
|
464
|
+
$as: alias
|
|
436
465
|
});
|
|
437
|
-
|
|
438
|
-
name: f,
|
|
439
|
-
include: true
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
if (hasExclusion) {
|
|
443
|
-
const obj = controls.$select ?? {};
|
|
444
|
-
for (const { name, include } of fields) {
|
|
445
|
-
obj[name] = include ? 1 : 0;
|
|
446
|
-
controlInsights.push([name, "$select"]);
|
|
466
|
+
controlInsights.push([field, fn]);
|
|
447
467
|
}
|
|
448
|
-
controls.$select =
|
|
468
|
+
controls.$select = arr;
|
|
449
469
|
} else {
|
|
450
|
-
const
|
|
451
|
-
for (const
|
|
452
|
-
|
|
453
|
-
|
|
470
|
+
const obj = controls.$select ?? {};
|
|
471
|
+
for (const f of items) {
|
|
472
|
+
if (!f) continue;
|
|
473
|
+
if (f.startsWith("-")) {
|
|
474
|
+
const name = f.slice(1);
|
|
475
|
+
obj[name] = 0;
|
|
476
|
+
controlInsights.push([name, "$select"]);
|
|
477
|
+
} else {
|
|
478
|
+
obj[f] = 1;
|
|
479
|
+
controlInsights.push([f, "$select"]);
|
|
480
|
+
}
|
|
454
481
|
}
|
|
455
|
-
controls.$select =
|
|
482
|
+
controls.$select = obj;
|
|
456
483
|
}
|
|
457
484
|
break;
|
|
458
485
|
}
|
|
@@ -460,13 +487,36 @@ function handleControls(parts) {
|
|
|
460
487
|
case "$order":
|
|
461
488
|
var _controls1;
|
|
462
489
|
(_controls1 = controls).$sort ?? (_controls1.$sort = {});
|
|
463
|
-
value.split(",")
|
|
464
|
-
if (!f)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
490
|
+
for (const f of value.split(",")) {
|
|
491
|
+
if (!f) continue;
|
|
492
|
+
if (f.startsWith("-")) {
|
|
493
|
+
const name = f.slice(1);
|
|
494
|
+
controls.$sort[name] = -1;
|
|
495
|
+
controlInsights.push([name, "$order"]);
|
|
496
|
+
} else {
|
|
497
|
+
controls.$sort[f] = 1;
|
|
498
|
+
controlInsights.push([f, "$order"]);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
469
501
|
break;
|
|
502
|
+
case "$groupBy":
|
|
503
|
+
var _controls2;
|
|
504
|
+
if (!value) break;
|
|
505
|
+
(_controls2 = controls).$groupBy ?? (_controls2.$groupBy = []);
|
|
506
|
+
for (const f of value.split(",")) {
|
|
507
|
+
if (!f) continue;
|
|
508
|
+
controls.$groupBy.push(f);
|
|
509
|
+
controlInsights.push([f, "$groupBy"]);
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
case "$having": {
|
|
513
|
+
if (!value) break;
|
|
514
|
+
const { expr, parser: hp } = parseFilterExpr(value);
|
|
515
|
+
if (controls.$having) controls.$having = { $and: [controls.$having, expr] };
|
|
516
|
+
else controls.$having = expr;
|
|
517
|
+
for (const [field] of hp.getInsights()) controlInsights.push([field, "$having"]);
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
470
520
|
case "$limit":
|
|
471
521
|
case "$top":
|
|
472
522
|
controls.$limit = Number(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqu/url",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "URL query string parser producing the Uniqu canonical query format",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Artem Maltsev",
|
|
@@ -22,13 +22,30 @@
|
|
|
22
22
|
"import": "./dist/index.mjs",
|
|
23
23
|
"require": "./dist/index.cjs"
|
|
24
24
|
},
|
|
25
|
+
"./builder": {
|
|
26
|
+
"types": "./dist/builder.d.ts",
|
|
27
|
+
"import": "./dist/builder.mjs",
|
|
28
|
+
"require": "./dist/builder.cjs"
|
|
29
|
+
},
|
|
25
30
|
"./package.json": "./package.json"
|
|
26
31
|
},
|
|
32
|
+
"build": [
|
|
33
|
+
{
|
|
34
|
+
"entries": [
|
|
35
|
+
"src/index.ts"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"entries": [
|
|
40
|
+
"src/builder.ts"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
],
|
|
27
44
|
"files": [
|
|
28
45
|
"dist"
|
|
29
46
|
],
|
|
30
47
|
"dependencies": {
|
|
31
|
-
"@uniqu/core": "^0.
|
|
48
|
+
"@uniqu/core": "^0.1.1"
|
|
32
49
|
},
|
|
33
50
|
"scripts": {
|
|
34
51
|
"pub": "pnpm publish --access public",
|