@uniqu/core 0.0.2 → 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 +75 -3
- package/dist/index.cjs +9 -0
- package/dist/index.d.ts +16 -4
- package/dist/index.mjs +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,8 +82,49 @@ 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 relation is a full `Uniquery` sub-query (a `WithRelation`) with its own `name`, `filter`, `controls`, and `insights`:
|
|
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
|
+
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
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{ name: 'profile', filter: {}, controls: {} },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`WithRelation` is a `Uniquery` with a required `name`:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
type WithRelation = Uniquery & { name: string }
|
|
122
|
+
```
|
|
123
|
+
|
|
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.
|
|
125
|
+
|
|
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.
|
|
127
|
+
|
|
87
128
|
## Type-Safe Filters
|
|
88
129
|
|
|
89
130
|
`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 +231,40 @@ const insights = computeInsights(query.filter, query.controls)
|
|
|
190
231
|
// 'createdAt' => Set { '$order' },
|
|
191
232
|
// 'name' => Set { '$select' },
|
|
192
233
|
// 'email' => Set { '$select' },
|
|
234
|
+
// 'posts' => Set { '$with' },
|
|
235
|
+
// }
|
|
236
|
+
```
|
|
237
|
+
|
|
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' },
|
|
193
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' } }
|
|
194
265
|
```
|
|
195
266
|
|
|
196
|
-
Use cases: field whitelisting, operator auditing, index planning.
|
|
267
|
+
Use cases: field whitelisting, operator auditing, index planning, relation validation.
|
|
197
268
|
|
|
198
269
|
### `getInsights`
|
|
199
270
|
|
|
@@ -221,8 +292,9 @@ const insights = getInsights(query)
|
|
|
221
292
|
| `ComparisonNode<T>` | Leaf node with typed field comparisons |
|
|
222
293
|
| `LogicalNode<T>` | `{ $and: ... } \| { $or: ... } \| { $not: ... }` — variants are mutually exclusive via `never` |
|
|
223
294
|
| `UniqueryControls<T>` | Pagination, sorting, projection — `$select`/`$sort` constrained to `keyof T` when typed |
|
|
224
|
-
| `Uniquery<T>` | `{ filter
|
|
225
|
-
| `
|
|
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 |
|
|
297
|
+
| `InsightOp` | `ComparisonOp \| '$select' \| '$order' \| '$with'` |
|
|
226
298
|
| `UniqueryInsights` | `Map<string, Set<InsightOp>>` |
|
|
227
299
|
|
|
228
300
|
### Functions
|
package/dist/index.cjs
CHANGED
|
@@ -65,6 +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) {
|
|
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
|
+
}
|
|
68
77
|
return insights;
|
|
69
78
|
}
|
|
70
79
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -72,18 +72,30 @@ interface UniqueryControls<T = Record<string, unknown>> {
|
|
|
72
72
|
$limit?: number;
|
|
73
73
|
$count?: boolean;
|
|
74
74
|
$select?: (keyof T & string)[] | Partial<Record<keyof T & string, 0 | 1>>;
|
|
75
|
+
/** Relations to populate alongside the query. */
|
|
76
|
+
$with?: WithRelation[];
|
|
75
77
|
/** Pass-through for unknown $-prefixed keywords. */
|
|
76
78
|
[key: `$${string}`]: unknown;
|
|
77
79
|
}
|
|
78
|
-
/**
|
|
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
|
+
*/
|
|
79
85
|
interface Uniquery<T = Record<string, unknown>> {
|
|
86
|
+
/** Relation name. Present only for nested `$with` sub-queries. */
|
|
87
|
+
name?: string;
|
|
80
88
|
filter: FilterExpr<T>;
|
|
81
89
|
controls: UniqueryControls<T>;
|
|
82
|
-
/** Pre-computed insights.
|
|
90
|
+
/** Pre-computed insights. */
|
|
83
91
|
insights?: UniqueryInsights;
|
|
84
92
|
}
|
|
93
|
+
/** A `$with` relation — a `Uniquery` with a required `name`. */
|
|
94
|
+
type WithRelation = Uniquery & {
|
|
95
|
+
name: string;
|
|
96
|
+
};
|
|
85
97
|
/** Insight operator includes comparison ops plus control-derived ops. */
|
|
86
|
-
type InsightOp = ComparisonOp | '$select' | '$order';
|
|
98
|
+
type InsightOp = ComparisonOp | '$select' | '$order' | '$with';
|
|
87
99
|
/** Map of field names to the set of operators used on that field. */
|
|
88
100
|
type UniqueryInsights = Map<string, Set<InsightOp>>;
|
|
89
101
|
|
|
@@ -126,4 +138,4 @@ declare function computeInsights(filter: FilterExpr, controls?: UniqueryControls
|
|
|
126
138
|
declare function getInsights(query: Uniquery): UniqueryInsights;
|
|
127
139
|
|
|
128
140
|
export { computeInsights, getInsights, isPrimitive, walkFilter };
|
|
129
|
-
export type { ComparisonNode, ComparisonOp, FieldOps, FieldOpsFor, FieldValue, FilterExpr, FilterVisitor, InsightOp, LogicalNode, Primitive, Uniquery, UniqueryControls, UniqueryInsights };
|
|
141
|
+
export type { ComparisonNode, ComparisonOp, FieldOps, FieldOpsFor, FieldValue, FilterExpr, FilterVisitor, InsightOp, LogicalNode, Primitive, Uniquery, UniqueryControls, UniqueryInsights, WithRelation };
|
package/dist/index.mjs
CHANGED
|
@@ -63,6 +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) {
|
|
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
|
+
}
|
|
66
75
|
return insights;
|
|
67
76
|
}
|
|
68
77
|
/**
|