@uniqu/core 0.0.3 → 0.0.4

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
@@ -87,7 +87,7 @@ A `FilterExpr` is either a **comparison node** (leaf) or a **logical node** (bra
87
87
 
88
88
  ### Relation Loading (`$with`)
89
89
 
90
- `$with` declares which relations to populate alongside the primary query. Each `WithRelation` is a self-contained sub-query with its own filter, sort, pagination, projection, and nested `$with`:
90
+ `$with` declares which relations to populate alongside the primary query. Each relation is a full `Uniquery` sub-query (a `WithRelation`) with its own `name`, `filter`, `controls`, and `insights`:
91
91
 
92
92
  ```ts
93
93
  import type { Uniquery, WithRelation } from '@uniqu/core'
@@ -99,30 +99,29 @@ const query: Uniquery = {
99
99
  {
100
100
  name: 'posts',
101
101
  filter: { status: 'published' },
102
- $sort: { createdAt: -1 },
103
- $limit: 5,
104
- $select: ['title', 'body'],
105
- $with: [
106
- { name: 'comments', $limit: 10 },
107
- { name: 'author' },
108
- ],
102
+ controls: {
103
+ $sort: { createdAt: -1 },
104
+ $limit: 5,
105
+ $select: ['title', 'body'],
106
+ $with: [
107
+ { name: 'comments', filter: {}, controls: { $limit: 10 } },
108
+ { name: 'author', filter: {}, controls: {} },
109
+ ],
110
+ },
109
111
  },
110
- { name: 'profile' },
112
+ { name: 'profile', filter: {}, controls: {} },
111
113
  ],
112
114
  },
113
115
  }
114
116
  ```
115
117
 
116
- `WithRelation` extends `UniqueryControls` (minus `$count`) and adds `name` and `filter`:
118
+ `WithRelation` is a `Uniquery` with a required `name`:
117
119
 
118
120
  ```ts
119
- interface WithRelation extends Omit<UniqueryControls, '$count'> {
120
- name: string
121
- filter?: FilterExpr
122
- }
121
+ type WithRelation = Uniquery & { name: string }
123
122
  ```
124
123
 
125
- This means each relation entry supports the full set of controls `$sort`, `$skip`, `$limit`, `$select`, nested `$with`, and pass-through `$`-prefixed keys — plus an optional scoped filter. The structure is recursive: a `WithRelation` can contain `$with` entries that themselves contain `$with` entries, to any depth.
124
+ The `Uniquery` type itself has an optional `name` — when present it is a nested relation, when absent it is the root query. This means every `$with` entry is a self-contained query with its own `filter`, `controls` (including `$sort`, `$skip`, `$limit`, `$select`, nested `$with`, and pass-through keywords), and optional `insights`. The structure is recursive to any depth.
126
125
 
127
126
  Uniqu is a query parser, not an ORM. It records what was requested — the consumer (e.g. a database adapter) decides how to execute it (JOINs, subqueries, separate queries), validates relation names against its schema, and enforces depth/security limits.
128
127
 
@@ -236,7 +235,34 @@ const insights = computeInsights(query.filter, query.controls)
236
235
  // }
237
236
  ```
238
237
 
239
- Relation names from `$with` are captured with the `$with` insight operator, letting consumers validate that requested relations exist on the entity type. Only immediate (top-level) `$with` entries are captured nested relation insights belong to their parent relation's scope.
238
+ Relation names from `$with` are captured with the `$with` insight operator. Nested `$with` insights bubble up to the parent with dot-notation prefixed field names, and each relation also carries its own scoped `insights`:
239
+
240
+ ```ts
241
+ const controls: UniqueryControls = {
242
+ $with: [
243
+ {
244
+ name: 'tasks',
245
+ filter: {},
246
+ controls: {
247
+ $with: [
248
+ { name: 'comments', filter: { body: { $regex: 'Great' } }, controls: {} },
249
+ ],
250
+ },
251
+ },
252
+ ],
253
+ }
254
+
255
+ const insights = computeInsights({}, controls)
256
+ // Map {
257
+ // 'tasks' => Set { '$with' },
258
+ // 'tasks.comments' => Set { '$with' },
259
+ // 'tasks.comments.body' => Set { '$regex' },
260
+ // }
261
+
262
+ // Each relation also has its own scoped insights:
263
+ // tasks.insights => Map { 'comments' => Set { '$with' }, 'comments.body' => Set { '$regex' } }
264
+ // comments.insights => Map { 'body' => Set { '$regex' } }
265
+ ```
240
266
 
241
267
  Use cases: field whitelisting, operator auditing, index planning, relation validation.
242
268
 
@@ -266,8 +292,8 @@ const insights = getInsights(query)
266
292
  | `ComparisonNode<T>` | Leaf node with typed field comparisons |
267
293
  | `LogicalNode<T>` | `{ $and: ... } \| { $or: ... } \| { $not: ... }` — variants are mutually exclusive via `never` |
268
294
  | `UniqueryControls<T>` | Pagination, sorting, projection — `$select`/`$sort` constrained to `keyof T` when typed |
269
- | `Uniquery<T>` | `{ filter: FilterExpr<T>, controls: UniqueryControls<T>, insights?: UniqueryInsights }` |
270
- | `WithRelation` | Relation sub-query extends `UniqueryControls` (minus `$count`) with `name` and `filter` |
295
+ | `Uniquery<T>` | `{ name?, filter, controls, insights? }` — root query (no name) or nested relation (with name) |
296
+ | `WithRelation` | `Uniquery & { name: string }` a `$with` relation with a required name |
271
297
  | `InsightOp` | `ComparisonOp \| '$select' \| '$order' \| '$with'` |
272
298
  | `UniqueryInsights` | `Map<string, Set<InsightOp>>` |
273
299
 
package/dist/index.cjs CHANGED
@@ -65,7 +65,15 @@ function isPrimitive(x) {
65
65
  if (controls?.$select) if (Array.isArray(controls.$select)) for (const field of controls.$select) capture(field, "$select");
66
66
  else for (const field of Object.keys(controls.$select)) capture(field, "$select");
67
67
  if (controls?.$sort) for (const field of Object.keys(controls.$sort)) capture(field, "$order");
68
- if (controls?.$with) for (const rel of controls.$with) capture(rel.name, "$with");
68
+ if (controls?.$with) for (const rel of controls.$with) {
69
+ capture(rel.name, "$with");
70
+ const nested = rel.insights ?? computeInsights(rel.filter, rel.controls);
71
+ if (nested.size) rel.insights = nested;
72
+ for (const [field, ops] of nested) {
73
+ const prefixed = `${rel.name}.${field}`;
74
+ for (const op of ops) capture(prefixed, op);
75
+ }
76
+ }
69
77
  return insights;
70
78
  }
71
79
  /**
package/dist/index.d.ts CHANGED
@@ -65,13 +65,6 @@ type LogicalNode<T = Record<string, unknown>> = {
65
65
  $and?: never;
66
66
  $or?: never;
67
67
  };
68
- /** A relation to populate alongside the primary query. */
69
- interface WithRelation extends Omit<UniqueryControls, '$count'> {
70
- /** Relation name (e.g. "posts", "posts.author"). */
71
- name: string;
72
- /** Filter scoped to this relation. */
73
- filter?: FilterExpr;
74
- }
75
68
  /** Query controls (pagination, projection, sorting). Generic `T` constrains field names in `$select` and `$sort`. */
76
69
  interface UniqueryControls<T = Record<string, unknown>> {
77
70
  $sort?: Partial<Record<keyof T & string, 1 | -1>>;
@@ -79,18 +72,28 @@ interface UniqueryControls<T = Record<string, unknown>> {
79
72
  $limit?: number;
80
73
  $count?: boolean;
81
74
  $select?: (keyof T & string)[] | Partial<Record<keyof T & string, 0 | 1>>;
82
- /** Relations to populate alongside the primary query. */
75
+ /** Relations to populate alongside the query. */
83
76
  $with?: WithRelation[];
84
77
  /** Pass-through for unknown $-prefixed keywords. */
85
78
  [key: `$${string}`]: unknown;
86
79
  }
87
- /** Top-level query: filter tree + controls. */
80
+ /**
81
+ * Canonical query representation.
82
+ * When `name` is present this is a nested relation (sub-query inside `$with`).
83
+ * When absent it is the root query.
84
+ */
88
85
  interface Uniquery<T = Record<string, unknown>> {
86
+ /** Relation name. Present only for nested `$with` sub-queries. */
87
+ name?: string;
89
88
  filter: FilterExpr<T>;
90
89
  controls: UniqueryControls<T>;
91
- /** Pre-computed insights. When present, consumers should use `getInsights()` which trusts these instead of recomputing. */
90
+ /** Pre-computed insights. */
92
91
  insights?: UniqueryInsights;
93
92
  }
93
+ /** A `$with` relation — a `Uniquery` with a required `name`. */
94
+ type WithRelation = Uniquery & {
95
+ name: string;
96
+ };
94
97
  /** Insight operator includes comparison ops plus control-derived ops. */
95
98
  type InsightOp = ComparisonOp | '$select' | '$order' | '$with';
96
99
  /** Map of field names to the set of operators used on that field. */
package/dist/index.mjs CHANGED
@@ -63,7 +63,15 @@ function isPrimitive(x) {
63
63
  if (controls?.$select) if (Array.isArray(controls.$select)) for (const field of controls.$select) capture(field, "$select");
64
64
  else for (const field of Object.keys(controls.$select)) capture(field, "$select");
65
65
  if (controls?.$sort) for (const field of Object.keys(controls.$sort)) capture(field, "$order");
66
- if (controls?.$with) for (const rel of controls.$with) capture(rel.name, "$with");
66
+ if (controls?.$with) for (const rel of controls.$with) {
67
+ capture(rel.name, "$with");
68
+ const nested = rel.insights ?? computeInsights(rel.filter, rel.controls);
69
+ if (nested.size) rel.insights = nested;
70
+ for (const [field, ops] of nested) {
71
+ const prefixed = `${rel.name}.${field}`;
72
+ for (const op of ops) capture(prefixed, op);
73
+ }
74
+ }
67
75
  return insights;
68
76
  }
69
77
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqu/core",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Canonical query format types, tree walker, and utilities for Uniqu",
5
5
  "license": "MIT",
6
6
  "author": "Artem Maltsev",