@uniqu/url 0.0.3 → 0.0.5
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 +35 -16
- package/dist/index.cjs +28 -23
- package/dist/index.d.ts +4 -2
- package/dist/index.mjs +28 -23
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ 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
|
-
| `$with` | — | `$with=posts,author` | `{ $with: [{ name: 'posts' },
|
|
154
|
+
| `$with` | — | `$with=posts,author` | `{ $with: [{ name: 'posts', filter: {}, controls: {} }, ...] }` |
|
|
155
155
|
| `$<custom>` | — | `$search=term` | `{ $search: 'term' }` |
|
|
156
156
|
|
|
157
157
|
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.
|
|
@@ -179,13 +179,12 @@ controls.$with = [
|
|
|
179
179
|
{
|
|
180
180
|
name: 'posts',
|
|
181
181
|
filter: { status: 'published' },
|
|
182
|
-
$sort: { createdAt: -1 },
|
|
183
|
-
$limit: 5,
|
|
182
|
+
controls: { $sort: { createdAt: -1 }, $limit: 5 },
|
|
184
183
|
},
|
|
185
184
|
]
|
|
186
185
|
```
|
|
187
186
|
|
|
188
|
-
All controls are supported inside parens: `$sort`, `$limit`, `$skip`, `$select`, and nested `$with`.
|
|
187
|
+
Each relation is a full `Uniquery` sub-query with its own `filter`, `controls`, and `insights`. All controls are supported inside parens: `$sort`, `$limit`, `$skip`, `$select`, `$count`, and nested `$with`.
|
|
189
188
|
|
|
190
189
|
#### Nested Relations
|
|
191
190
|
|
|
@@ -201,12 +200,22 @@ This produces a tree:
|
|
|
201
200
|
controls.$with = [
|
|
202
201
|
{
|
|
203
202
|
name: 'posts',
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
203
|
+
filter: {},
|
|
204
|
+
controls: {
|
|
205
|
+
$sort: { createdAt: -1 },
|
|
206
|
+
$limit: 5,
|
|
207
|
+
$with: [
|
|
208
|
+
{
|
|
209
|
+
name: 'comments',
|
|
210
|
+
filter: {},
|
|
211
|
+
controls: {
|
|
212
|
+
$limit: 10,
|
|
213
|
+
$with: [{ name: 'author', filter: {}, controls: {} }],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{ name: 'tags', filter: {}, controls: {} },
|
|
217
|
+
],
|
|
218
|
+
},
|
|
210
219
|
},
|
|
211
220
|
]
|
|
212
221
|
```
|
|
@@ -229,21 +238,27 @@ Produces:
|
|
|
229
238
|
{
|
|
230
239
|
name: 'posts',
|
|
231
240
|
filter: { status: 'published' },
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
241
|
+
controls: {
|
|
242
|
+
$sort: { createdAt: -1 },
|
|
243
|
+
$limit: 5,
|
|
244
|
+
$select: ['title', 'body'],
|
|
245
|
+
},
|
|
246
|
+
insights: Map { 'status' => Set { '$eq' }, ... },
|
|
235
247
|
},
|
|
236
|
-
{ name: 'author' },
|
|
248
|
+
{ name: 'author', filter: {}, controls: {} },
|
|
237
249
|
],
|
|
238
250
|
},
|
|
239
251
|
insights: Map {
|
|
240
252
|
'status' => Set { '$eq' },
|
|
241
253
|
'posts' => Set { '$with' },
|
|
254
|
+
'posts.status' => Set { '$eq' },
|
|
242
255
|
'author' => Set { '$with' },
|
|
243
256
|
},
|
|
244
257
|
}
|
|
245
258
|
```
|
|
246
259
|
|
|
260
|
+
Each `$with` relation carries its own scoped `insights`, and nested insights bubble up to the root with dot-notation prefixed field names.
|
|
261
|
+
|
|
247
262
|
#### Edge Cases
|
|
248
263
|
|
|
249
264
|
| Case | Behavior |
|
|
@@ -314,8 +329,12 @@ Produces:
|
|
|
314
329
|
$skip: 10,
|
|
315
330
|
$count: true,
|
|
316
331
|
$with: [
|
|
317
|
-
{
|
|
318
|
-
|
|
332
|
+
{
|
|
333
|
+
name: 'posts',
|
|
334
|
+
filter: { status: 'published' },
|
|
335
|
+
controls: { $sort: { date: -1 }, $limit: 5 },
|
|
336
|
+
},
|
|
337
|
+
{ name: 'profile', filter: {}, controls: {} },
|
|
319
338
|
],
|
|
320
339
|
},
|
|
321
340
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -346,7 +346,7 @@ function buildExists(fields, positive) {
|
|
|
346
346
|
if (/^\$[A-Za-z0-9_!]+/.test(p) && !p.startsWith("$exists=") && !p.startsWith("$!exists=")) controlParts.push(p);
|
|
347
347
|
else if (p.length) exprParts.push(p);
|
|
348
348
|
}
|
|
349
|
-
const { controls,
|
|
349
|
+
const { controls, controlInsights } = handleControls(controlParts);
|
|
350
350
|
let filter = {};
|
|
351
351
|
let parser;
|
|
352
352
|
if (exprParts.length) {
|
|
@@ -354,9 +354,7 @@ function buildExists(fields, positive) {
|
|
|
354
354
|
filter = parser.parseExpression();
|
|
355
355
|
parser.expectEof();
|
|
356
356
|
} else parser = new Parser([]);
|
|
357
|
-
for (const
|
|
358
|
-
for (const f of orderInsights) parser.captureInsight(f, "$order");
|
|
359
|
-
for (const f of withInsights) parser.captureInsight(f, "$with");
|
|
357
|
+
for (const [field, op] of controlInsights) parser.captureInsight(field, op);
|
|
360
358
|
return {
|
|
361
359
|
filter,
|
|
362
360
|
controls,
|
|
@@ -379,26 +377,31 @@ function buildExists(fields, positive) {
|
|
|
379
377
|
/** Parse a single `$with` segment like `posts` or `posts($sort=-createdAt&status=active)`. */ function parseWithSegment(seg) {
|
|
380
378
|
if (!seg) return null;
|
|
381
379
|
const parenIdx = seg.indexOf("(");
|
|
382
|
-
if (parenIdx === -1) return {
|
|
380
|
+
if (parenIdx === -1) return {
|
|
381
|
+
name: seg,
|
|
382
|
+
filter: {},
|
|
383
|
+
controls: {}
|
|
384
|
+
};
|
|
383
385
|
const name = seg.slice(0, parenIdx);
|
|
384
386
|
if (!name) return null;
|
|
385
387
|
const inner = seg.slice(parenIdx + 1, -1);
|
|
386
|
-
if (!inner) return {
|
|
388
|
+
if (!inner) return {
|
|
389
|
+
name,
|
|
390
|
+
filter: {},
|
|
391
|
+
controls: {}
|
|
392
|
+
};
|
|
387
393
|
const sub = parseUrl(inner);
|
|
388
|
-
const rel = {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (sub.
|
|
394
|
-
if (sub.controls.$with?.length) rel.$with = sub.controls.$with;
|
|
394
|
+
const rel = {
|
|
395
|
+
name,
|
|
396
|
+
filter: sub.filter,
|
|
397
|
+
controls: sub.controls
|
|
398
|
+
};
|
|
399
|
+
if (sub.insights.size) rel.insights = sub.insights;
|
|
395
400
|
return rel;
|
|
396
401
|
}
|
|
397
402
|
function handleControls(parts) {
|
|
398
403
|
const controls = {};
|
|
399
|
-
const
|
|
400
|
-
const orderInsights = /* @__PURE__ */ new Set();
|
|
401
|
-
const withInsights = /* @__PURE__ */ new Set();
|
|
404
|
+
const controlInsights = [];
|
|
402
405
|
for (const raw of parts) {
|
|
403
406
|
const [key, ...rest] = raw.split("=");
|
|
404
407
|
const value = rest.join("=");
|
|
@@ -413,7 +416,11 @@ function handleControls(parts) {
|
|
|
413
416
|
if (!rel || seen.has(rel.name)) continue;
|
|
414
417
|
seen.add(rel.name);
|
|
415
418
|
controls.$with.push(rel);
|
|
416
|
-
|
|
419
|
+
controlInsights.push([rel.name, "$with"]);
|
|
420
|
+
if (rel.insights) for (const [field, ops] of rel.insights) {
|
|
421
|
+
const prefixed = `${rel.name}.${field}`;
|
|
422
|
+
for (const op of ops) controlInsights.push([prefixed, op]);
|
|
423
|
+
}
|
|
417
424
|
}
|
|
418
425
|
break;
|
|
419
426
|
}
|
|
@@ -437,14 +444,14 @@ function handleControls(parts) {
|
|
|
437
444
|
const obj = controls.$select ?? {};
|
|
438
445
|
for (const { name, include } of fields) {
|
|
439
446
|
obj[name] = include ? 1 : 0;
|
|
440
|
-
|
|
447
|
+
controlInsights.push([name, "$select"]);
|
|
441
448
|
}
|
|
442
449
|
controls.$select = obj;
|
|
443
450
|
} else {
|
|
444
451
|
const arr = Array.isArray(controls.$select) ? controls.$select : [];
|
|
445
452
|
for (const { name } of fields) {
|
|
446
453
|
arr.push(name);
|
|
447
|
-
|
|
454
|
+
controlInsights.push([name, "$select"]);
|
|
448
455
|
}
|
|
449
456
|
controls.$select = arr;
|
|
450
457
|
}
|
|
@@ -456,7 +463,7 @@ function handleControls(parts) {
|
|
|
456
463
|
(_controls1 = controls).$sort ?? (_controls1.$sort = {});
|
|
457
464
|
value.split(",").forEach((f) => {
|
|
458
465
|
if (!f) return;
|
|
459
|
-
|
|
466
|
+
controlInsights.push([f.replace(/^-/, ""), "$order"]);
|
|
460
467
|
if (f.startsWith("-")) controls.$sort[f.slice(1)] = -1;
|
|
461
468
|
else controls.$sort[f] = 1;
|
|
462
469
|
});
|
|
@@ -476,9 +483,7 @@ function handleControls(parts) {
|
|
|
476
483
|
}
|
|
477
484
|
return {
|
|
478
485
|
controls,
|
|
479
|
-
|
|
480
|
-
orderInsights,
|
|
481
|
-
withInsights
|
|
486
|
+
controlInsights
|
|
482
487
|
};
|
|
483
488
|
}
|
|
484
489
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { Uniquery, UniqueryInsights } from '@uniqu/core';
|
|
1
|
+
import { Uniquery, FilterExpr, UniqueryControls, UniqueryInsights } from '@uniqu/core';
|
|
2
2
|
|
|
3
|
-
/** Result of parsing a URL query string. Narrows
|
|
3
|
+
/** Result of parsing a URL query string. Narrows optional fields to required (always produced by the parser). */
|
|
4
4
|
interface UrlQuery extends Uniquery {
|
|
5
|
+
filter: FilterExpr;
|
|
6
|
+
controls: UniqueryControls;
|
|
5
7
|
insights: UniqueryInsights;
|
|
6
8
|
}
|
|
7
9
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -345,7 +345,7 @@ function buildExists(fields, positive) {
|
|
|
345
345
|
if (/^\$[A-Za-z0-9_!]+/.test(p) && !p.startsWith("$exists=") && !p.startsWith("$!exists=")) controlParts.push(p);
|
|
346
346
|
else if (p.length) exprParts.push(p);
|
|
347
347
|
}
|
|
348
|
-
const { controls,
|
|
348
|
+
const { controls, controlInsights } = handleControls(controlParts);
|
|
349
349
|
let filter = {};
|
|
350
350
|
let parser;
|
|
351
351
|
if (exprParts.length) {
|
|
@@ -353,9 +353,7 @@ function buildExists(fields, positive) {
|
|
|
353
353
|
filter = parser.parseExpression();
|
|
354
354
|
parser.expectEof();
|
|
355
355
|
} else parser = new Parser([]);
|
|
356
|
-
for (const
|
|
357
|
-
for (const f of orderInsights) parser.captureInsight(f, "$order");
|
|
358
|
-
for (const f of withInsights) parser.captureInsight(f, "$with");
|
|
356
|
+
for (const [field, op] of controlInsights) parser.captureInsight(field, op);
|
|
359
357
|
return {
|
|
360
358
|
filter,
|
|
361
359
|
controls,
|
|
@@ -378,26 +376,31 @@ function buildExists(fields, positive) {
|
|
|
378
376
|
/** Parse a single `$with` segment like `posts` or `posts($sort=-createdAt&status=active)`. */ function parseWithSegment(seg) {
|
|
379
377
|
if (!seg) return null;
|
|
380
378
|
const parenIdx = seg.indexOf("(");
|
|
381
|
-
if (parenIdx === -1) return {
|
|
379
|
+
if (parenIdx === -1) return {
|
|
380
|
+
name: seg,
|
|
381
|
+
filter: {},
|
|
382
|
+
controls: {}
|
|
383
|
+
};
|
|
382
384
|
const name = seg.slice(0, parenIdx);
|
|
383
385
|
if (!name) return null;
|
|
384
386
|
const inner = seg.slice(parenIdx + 1, -1);
|
|
385
|
-
if (!inner) return {
|
|
387
|
+
if (!inner) return {
|
|
388
|
+
name,
|
|
389
|
+
filter: {},
|
|
390
|
+
controls: {}
|
|
391
|
+
};
|
|
386
392
|
const sub = parseUrl(inner);
|
|
387
|
-
const rel = {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (sub.
|
|
393
|
-
if (sub.controls.$with?.length) rel.$with = sub.controls.$with;
|
|
393
|
+
const rel = {
|
|
394
|
+
name,
|
|
395
|
+
filter: sub.filter,
|
|
396
|
+
controls: sub.controls
|
|
397
|
+
};
|
|
398
|
+
if (sub.insights.size) rel.insights = sub.insights;
|
|
394
399
|
return rel;
|
|
395
400
|
}
|
|
396
401
|
function handleControls(parts) {
|
|
397
402
|
const controls = {};
|
|
398
|
-
const
|
|
399
|
-
const orderInsights = /* @__PURE__ */ new Set();
|
|
400
|
-
const withInsights = /* @__PURE__ */ new Set();
|
|
403
|
+
const controlInsights = [];
|
|
401
404
|
for (const raw of parts) {
|
|
402
405
|
const [key, ...rest] = raw.split("=");
|
|
403
406
|
const value = rest.join("=");
|
|
@@ -412,7 +415,11 @@ function handleControls(parts) {
|
|
|
412
415
|
if (!rel || seen.has(rel.name)) continue;
|
|
413
416
|
seen.add(rel.name);
|
|
414
417
|
controls.$with.push(rel);
|
|
415
|
-
|
|
418
|
+
controlInsights.push([rel.name, "$with"]);
|
|
419
|
+
if (rel.insights) for (const [field, ops] of rel.insights) {
|
|
420
|
+
const prefixed = `${rel.name}.${field}`;
|
|
421
|
+
for (const op of ops) controlInsights.push([prefixed, op]);
|
|
422
|
+
}
|
|
416
423
|
}
|
|
417
424
|
break;
|
|
418
425
|
}
|
|
@@ -436,14 +443,14 @@ function handleControls(parts) {
|
|
|
436
443
|
const obj = controls.$select ?? {};
|
|
437
444
|
for (const { name, include } of fields) {
|
|
438
445
|
obj[name] = include ? 1 : 0;
|
|
439
|
-
|
|
446
|
+
controlInsights.push([name, "$select"]);
|
|
440
447
|
}
|
|
441
448
|
controls.$select = obj;
|
|
442
449
|
} else {
|
|
443
450
|
const arr = Array.isArray(controls.$select) ? controls.$select : [];
|
|
444
451
|
for (const { name } of fields) {
|
|
445
452
|
arr.push(name);
|
|
446
|
-
|
|
453
|
+
controlInsights.push([name, "$select"]);
|
|
447
454
|
}
|
|
448
455
|
controls.$select = arr;
|
|
449
456
|
}
|
|
@@ -455,7 +462,7 @@ function handleControls(parts) {
|
|
|
455
462
|
(_controls1 = controls).$sort ?? (_controls1.$sort = {});
|
|
456
463
|
value.split(",").forEach((f) => {
|
|
457
464
|
if (!f) return;
|
|
458
|
-
|
|
465
|
+
controlInsights.push([f.replace(/^-/, ""), "$order"]);
|
|
459
466
|
if (f.startsWith("-")) controls.$sort[f.slice(1)] = -1;
|
|
460
467
|
else controls.$sort[f] = 1;
|
|
461
468
|
});
|
|
@@ -475,9 +482,7 @@ function handleControls(parts) {
|
|
|
475
482
|
}
|
|
476
483
|
return {
|
|
477
484
|
controls,
|
|
478
|
-
|
|
479
|
-
orderInsights,
|
|
480
|
-
withInsights
|
|
485
|
+
controlInsights
|
|
481
486
|
};
|
|
482
487
|
}
|
|
483
488
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqu/url",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "URL query string parser producing the Uniqu canonical query format",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Artem Maltsev",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@uniqu/core": "^0.0.
|
|
31
|
+
"@uniqu/core": "^0.0.5"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"pub": "pnpm publish --access public",
|