@uniqu/core 0.0.1 → 0.0.3

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
@@ -82,8 +82,50 @@ A `FilterExpr` is either a **comparison node** (leaf) or a **logical node** (bra
82
82
  | `$limit` | `number` | Limit to N results |
83
83
  | `$count` | `boolean` | Request total count |
84
84
  | `$select` | `string[] \| Record<string, 0 \| 1>` | Field projection — array for inclusion, object for exclusion/mixed |
85
+ | `$with` | `WithRelation[]` | Relations to populate alongside the primary query |
85
86
  | `$<custom>` | `unknown` | Arbitrary pass-through keywords |
86
87
 
88
+ ### Relation Loading (`$with`)
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`:
91
+
92
+ ```ts
93
+ import type { Uniquery, WithRelation } from '@uniqu/core'
94
+
95
+ const query: Uniquery = {
96
+ filter: { status: 'active' },
97
+ controls: {
98
+ $with: [
99
+ {
100
+ name: 'posts',
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
+ ],
109
+ },
110
+ { name: 'profile' },
111
+ ],
112
+ },
113
+ }
114
+ ```
115
+
116
+ `WithRelation` extends `UniqueryControls` (minus `$count`) and adds `name` and `filter`:
117
+
118
+ ```ts
119
+ interface WithRelation extends Omit<UniqueryControls, '$count'> {
120
+ name: string
121
+ filter?: FilterExpr
122
+ }
123
+ ```
124
+
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.
126
+
127
+ 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
+
87
129
  ## Type-Safe Filters
88
130
 
89
131
  `FilterExpr<T>` accepts a generic entity type for compile-time field and value checking. Dot-notation paths are always allowed for nested access:
@@ -190,10 +232,13 @@ const insights = computeInsights(query.filter, query.controls)
190
232
  // 'createdAt' => Set { '$order' },
191
233
  // 'name' => Set { '$select' },
192
234
  // 'email' => Set { '$select' },
235
+ // 'posts' => Set { '$with' },
193
236
  // }
194
237
  ```
195
238
 
196
- Use cases: field whitelisting, operator auditing, index planning.
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.
240
+
241
+ Use cases: field whitelisting, operator auditing, index planning, relation validation.
197
242
 
198
243
  ### `getInsights`
199
244
 
@@ -222,7 +267,8 @@ const insights = getInsights(query)
222
267
  | `LogicalNode<T>` | `{ $and: ... } \| { $or: ... } \| { $not: ... }` — variants are mutually exclusive via `never` |
223
268
  | `UniqueryControls<T>` | Pagination, sorting, projection — `$select`/`$sort` constrained to `keyof T` when typed |
224
269
  | `Uniquery<T>` | `{ filter: FilterExpr<T>, controls: UniqueryControls<T>, insights?: UniqueryInsights }` |
225
- | `InsightOp` | `ComparisonOp \| '$select' \| '$order'` |
270
+ | `WithRelation` | Relation sub-query — extends `UniqueryControls` (minus `$count`) with `name` and `filter` |
271
+ | `InsightOp` | `ComparisonOp \| '$select' \| '$order' \| '$with'` |
226
272
  | `UniqueryInsights` | `Map<string, Set<InsightOp>>` |
227
273
 
228
274
  ### Functions
package/dist/index.cjs CHANGED
@@ -32,7 +32,10 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
32
32
  return results.length === 1 ? results[0] : visitor.and(results);
33
33
  }
34
34
  function isPrimitive(x) {
35
- return x === null || typeof x === "string" || typeof x === "number" || typeof x === "boolean" || x instanceof RegExp || x instanceof Date;
35
+ if (x === null || typeof x !== "object") return true;
36
+ if (x instanceof RegExp || x instanceof Date) return true;
37
+ if (!Array.isArray(x) && x.constructor !== void 0 && x.constructor !== Object) return true;
38
+ return false;
36
39
  }
37
40
 
38
41
  //#endregion
@@ -62,6 +65,7 @@ function isPrimitive(x) {
62
65
  if (controls?.$select) if (Array.isArray(controls.$select)) for (const field of controls.$select) capture(field, "$select");
63
66
  else for (const field of Object.keys(controls.$select)) capture(field, "$select");
64
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");
65
69
  return insights;
66
70
  }
67
71
  /**
package/dist/index.d.ts CHANGED
@@ -65,6 +65,13 @@ 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
+ }
68
75
  /** Query controls (pagination, projection, sorting). Generic `T` constrains field names in `$select` and `$sort`. */
69
76
  interface UniqueryControls<T = Record<string, unknown>> {
70
77
  $sort?: Partial<Record<keyof T & string, 1 | -1>>;
@@ -72,6 +79,8 @@ interface UniqueryControls<T = Record<string, unknown>> {
72
79
  $limit?: number;
73
80
  $count?: boolean;
74
81
  $select?: (keyof T & string)[] | Partial<Record<keyof T & string, 0 | 1>>;
82
+ /** Relations to populate alongside the primary query. */
83
+ $with?: WithRelation[];
75
84
  /** Pass-through for unknown $-prefixed keywords. */
76
85
  [key: `$${string}`]: unknown;
77
86
  }
@@ -83,7 +92,7 @@ interface Uniquery<T = Record<string, unknown>> {
83
92
  insights?: UniqueryInsights;
84
93
  }
85
94
  /** Insight operator includes comparison ops plus control-derived ops. */
86
- type InsightOp = ComparisonOp | '$select' | '$order';
95
+ type InsightOp = ComparisonOp | '$select' | '$order' | '$with';
87
96
  /** Map of field names to the set of operators used on that field. */
88
97
  type UniqueryInsights = Map<string, Set<InsightOp>>;
89
98
 
@@ -126,4 +135,4 @@ declare function computeInsights(filter: FilterExpr, controls?: UniqueryControls
126
135
  declare function getInsights(query: Uniquery): UniqueryInsights;
127
136
 
128
137
  export { computeInsights, getInsights, isPrimitive, walkFilter };
129
- export type { ComparisonNode, ComparisonOp, FieldOps, FieldOpsFor, FieldValue, FilterExpr, FilterVisitor, InsightOp, LogicalNode, Primitive, Uniquery, UniqueryControls, UniqueryInsights };
138
+ export type { ComparisonNode, ComparisonOp, FieldOps, FieldOpsFor, FieldValue, FilterExpr, FilterVisitor, InsightOp, LogicalNode, Primitive, Uniquery, UniqueryControls, UniqueryInsights, WithRelation };
package/dist/index.mjs CHANGED
@@ -30,7 +30,10 @@
30
30
  return results.length === 1 ? results[0] : visitor.and(results);
31
31
  }
32
32
  function isPrimitive(x) {
33
- return x === null || typeof x === "string" || typeof x === "number" || typeof x === "boolean" || x instanceof RegExp || x instanceof Date;
33
+ if (x === null || typeof x !== "object") return true;
34
+ if (x instanceof RegExp || x instanceof Date) return true;
35
+ if (!Array.isArray(x) && x.constructor !== void 0 && x.constructor !== Object) return true;
36
+ return false;
34
37
  }
35
38
 
36
39
  //#endregion
@@ -60,6 +63,7 @@ function isPrimitive(x) {
60
63
  if (controls?.$select) if (Array.isArray(controls.$select)) for (const field of controls.$select) capture(field, "$select");
61
64
  else for (const field of Object.keys(controls.$select)) capture(field, "$select");
62
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");
63
67
  return insights;
64
68
  }
65
69
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqu/core",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Canonical query format types, tree walker, and utilities for Uniqu",
5
5
  "license": "MIT",
6
6
  "author": "Artem Maltsev",