@uniqu/core 0.1.1 → 0.1.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 +38 -3
- package/dist/index.cjs +7 -1
- package/dist/index.mjs +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,7 @@ A `FilterExpr` is either a **comparison node** (leaf) or a **logical node** (bra
|
|
|
83
83
|
| `$count` | `boolean` | Request total count |
|
|
84
84
|
| `$select` | `SelectExpr<T>` | Field projection — array of strings/aggregates for inclusion, object for exclusion/mixed |
|
|
85
85
|
| `$groupBy` | `(keyof T & string)[]` | Fields to group by for aggregate queries |
|
|
86
|
+
| `$having` | `FilterExpr` | Post-aggregation filter on aliases and dimension fields |
|
|
86
87
|
| `$with` | `(WithRelation \| string)[]` | Relations to populate alongside the primary query |
|
|
87
88
|
| `$<custom>` | `unknown` | Arbitrary pass-through keywords |
|
|
88
89
|
|
|
@@ -164,6 +165,36 @@ interface AggregateExpr {
|
|
|
164
165
|
|
|
165
166
|
Known functions are `sum`, `count`, `avg`, `min`, `max` (`AggregateFn`), but `$fn` accepts any string for extensibility — consumers validate and execute supported functions.
|
|
166
167
|
|
|
168
|
+
#### Post-Aggregation Filter (`$having`)
|
|
169
|
+
|
|
170
|
+
`$having` filters groups after aggregation — the equivalent of SQL `HAVING`. It operates on aggregate result aliases and dimension fields:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const query: Uniquery = {
|
|
174
|
+
filter: { status: 'active' },
|
|
175
|
+
controls: {
|
|
176
|
+
$select: [
|
|
177
|
+
'currency',
|
|
178
|
+
{ $fn: 'sum', $field: 'amount', $as: 'total' },
|
|
179
|
+
],
|
|
180
|
+
$groupBy: ['currency'],
|
|
181
|
+
$having: { total: { $gt: 1000 } },
|
|
182
|
+
$sort: { total: -1 },
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`$having` accepts a full `FilterExpr` — logical operators (`$and`, `$or`, `$not`) and all comparison operators are supported. It is untyped (`FilterExpr` without a generic) because its fields are aggregate aliases that don't exist on the entity type `T`.
|
|
188
|
+
|
|
189
|
+
Insights track `$having` fields with the `'$having'` op:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// insights for the query above:
|
|
193
|
+
// 'total' => Set { '$having', '$order' }
|
|
194
|
+
// 'currency' => Set { '$select', '$groupBy' }
|
|
195
|
+
// 'amount' => Set { 'sum' }
|
|
196
|
+
```
|
|
197
|
+
|
|
167
198
|
Insights track aggregate usage with bare function names (not `$`-prefixed), making it easy to distinguish controls from aggregates:
|
|
168
199
|
|
|
169
200
|
```ts
|
|
@@ -341,13 +372,17 @@ const insights = getInsights(query)
|
|
|
341
372
|
| `ComparisonNode<T>` | Leaf node — keys constrained to `keyof T` when typed |
|
|
342
373
|
| `LogicalNode<T>` | `{ $and: ... } \| { $or: ... } \| { $not: ... }` — variants are mutually exclusive via `never` |
|
|
343
374
|
| `AggregateFn` | `'sum' \| 'count' \| 'avg' \| 'min' \| 'max'` |
|
|
344
|
-
| `AggregateExpr
|
|
375
|
+
| `AggregateExpr<Fn, Field, Alias>` | `{ $fn, $field, $as? }` — aggregate function call in `$select`. Generic params preserve literal types for result inference |
|
|
345
376
|
| `SelectExpr<T>` | `((keyof T & string) \| AggregateExpr)[] \| Record<keyof T & string, 0 \| 1>` |
|
|
346
|
-
| `UniqueryControls<T>` | Pagination, sorting, projection, grouping — `$select`/`$sort`/`$groupBy` constrained to `keyof T` when typed |
|
|
377
|
+
| `UniqueryControls<T>` | Pagination, sorting, projection, grouping, `$having` — `$select`/`$sort`/`$groupBy` constrained to `keyof T` when typed |
|
|
347
378
|
| `Uniquery<T>` | `{ name?, filter, controls, insights? }` — root query (no name) or nested relation (with name) |
|
|
348
379
|
| `TypedWithRelation<Nav>` | Typed `$with` entry — `keyof Nav & string` or object with typed filter/controls |
|
|
349
380
|
| `WithRelation` | Untyped `$with` relation with `{ name: string, filter?, controls?, insights? }` |
|
|
350
|
-
| `
|
|
381
|
+
| `AggregateControls<T, D, M>` | Typed aggregate controls — `$groupBy` required, `$with` forbidden, `$select` constrained to dimensions + aggregates |
|
|
382
|
+
| `AggregateQuery<T, D, M>` | Typed aggregate query — `{ filter?, controls, insights? }` with dimension/measure constraints |
|
|
383
|
+
| `AggregateResult<T, Select>` | Infer result row type from `$select` — dimensions preserve original types, aggregates → `number` (min/max preserve field type) |
|
|
384
|
+
| `ResolveAlias<A>` | Resolve the output alias of an `AggregateExpr` — uses `$as` if provided, otherwise `{fn}_{field}` |
|
|
385
|
+
| `InsightOp` | `ComparisonOp \| '$select' \| '$order' \| '$with' \| '$groupBy' \| '$having' \| AggregateFn \| string` |
|
|
351
386
|
| `UniqueryInsights` | `Map<string, Set<InsightOp>>` |
|
|
352
387
|
|
|
353
388
|
### Functions
|
package/dist/index.cjs
CHANGED
|
@@ -80,7 +80,13 @@ function isPrimitive(x) {
|
|
|
80
80
|
or() {},
|
|
81
81
|
not() {}
|
|
82
82
|
});
|
|
83
|
-
if (controls?.$sort)
|
|
83
|
+
if (controls?.$sort) {
|
|
84
|
+
let aliasToField;
|
|
85
|
+
if (Array.isArray(controls.$select)) {
|
|
86
|
+
for (const entry of controls.$select) if (typeof entry !== "string" && entry.$as) (aliasToField ?? (aliasToField = /* @__PURE__ */ new Map())).set(entry.$as, entry.$field);
|
|
87
|
+
}
|
|
88
|
+
for (const field of Object.keys(controls.$sort)) capture(aliasToField?.get(field) ?? field, "$order");
|
|
89
|
+
}
|
|
84
90
|
if (controls?.$with) for (const entry of controls.$with) {
|
|
85
91
|
if (typeof entry === "string") {
|
|
86
92
|
capture(entry, "$with");
|
package/dist/index.mjs
CHANGED
|
@@ -78,7 +78,13 @@ function isPrimitive(x) {
|
|
|
78
78
|
or() {},
|
|
79
79
|
not() {}
|
|
80
80
|
});
|
|
81
|
-
if (controls?.$sort)
|
|
81
|
+
if (controls?.$sort) {
|
|
82
|
+
let aliasToField;
|
|
83
|
+
if (Array.isArray(controls.$select)) {
|
|
84
|
+
for (const entry of controls.$select) if (typeof entry !== "string" && entry.$as) (aliasToField ?? (aliasToField = /* @__PURE__ */ new Map())).set(entry.$as, entry.$field);
|
|
85
|
+
}
|
|
86
|
+
for (const field of Object.keys(controls.$sort)) capture(aliasToField?.get(field) ?? field, "$order");
|
|
87
|
+
}
|
|
82
88
|
if (controls?.$with) for (const entry of controls.$with) {
|
|
83
89
|
if (typeof entry === "string") {
|
|
84
90
|
capture(entry, "$with");
|