@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 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' }, { name: 'author' }] }` |
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
- $sort: { createdAt: -1 },
205
- $limit: 5,
206
- $with: [
207
- { name: 'comments', $limit: 10, $with: [{ name: 'author' }] },
208
- { name: 'tags' },
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
- $sort: { createdAt: -1 },
233
- $limit: 5,
234
- $select: ['title', 'body'],
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
- { name: 'posts', filter: { status: 'published' }, $sort: { date: -1 }, $limit: 5 },
318
- { name: 'profile' },
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, selectInsights, orderInsights, withInsights } = handleControls(controlParts);
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 f of selectInsights) parser.captureInsight(f, "$select");
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 { name: seg };
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 { name };
388
+ if (!inner) return {
389
+ name,
390
+ filter: {},
391
+ controls: {}
392
+ };
387
393
  const sub = parseUrl(inner);
388
- const rel = { name };
389
- if (Object.keys(sub.filter).length) rel.filter = sub.filter;
390
- if (sub.controls.$sort) rel.$sort = sub.controls.$sort;
391
- if (sub.controls.$skip != null) rel.$skip = sub.controls.$skip;
392
- if (sub.controls.$limit != null) rel.$limit = sub.controls.$limit;
393
- if (sub.controls.$select) rel.$select = sub.controls.$select;
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 selectInsights = /* @__PURE__ */ new Set();
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
- withInsights.add(rel.name);
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
- selectInsights.add(name);
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
- selectInsights.add(name);
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
- orderInsights.add(f.replace(/^-/, ""));
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
- selectInsights,
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 `Uniquery.insights` from optional to required (eagerly computed during parsing). */
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, selectInsights, orderInsights, withInsights } = handleControls(controlParts);
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 f of selectInsights) parser.captureInsight(f, "$select");
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 { name: seg };
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 { name };
387
+ if (!inner) return {
388
+ name,
389
+ filter: {},
390
+ controls: {}
391
+ };
386
392
  const sub = parseUrl(inner);
387
- const rel = { name };
388
- if (Object.keys(sub.filter).length) rel.filter = sub.filter;
389
- if (sub.controls.$sort) rel.$sort = sub.controls.$sort;
390
- if (sub.controls.$skip != null) rel.$skip = sub.controls.$skip;
391
- if (sub.controls.$limit != null) rel.$limit = sub.controls.$limit;
392
- if (sub.controls.$select) rel.$select = sub.controls.$select;
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 selectInsights = /* @__PURE__ */ new Set();
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
- withInsights.add(rel.name);
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
- selectInsights.add(name);
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
- selectInsights.add(name);
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
- orderInsights.add(f.replace(/^-/, ""));
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
- selectInsights,
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",
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.3"
31
+ "@uniqu/core": "^0.0.5"
32
32
  },
33
33
  "scripts": {
34
34
  "pub": "pnpm publish --access public",