@uniqu/core 0.1.0 → 0.1.1

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/dist/index.cjs CHANGED
@@ -72,6 +72,14 @@ function isPrimitive(x) {
72
72
  else capture(entry.$field, entry.$fn);
73
73
  else for (const field of Object.keys(controls.$select)) capture(field, "$select");
74
74
  if (controls?.$groupBy) for (const field of controls.$groupBy) capture(field, "$groupBy");
75
+ if (controls?.$having) walkFilter(controls.$having, {
76
+ comparison(field) {
77
+ capture(field, "$having");
78
+ },
79
+ and() {},
80
+ or() {},
81
+ not() {}
82
+ });
75
83
  if (controls?.$sort) for (const field of Object.keys(controls.$sort)) capture(field, "$order");
76
84
  if (controls?.$with) for (const entry of controls.$with) {
77
85
  if (typeof entry === "string") {
package/dist/index.d.ts CHANGED
@@ -67,14 +67,14 @@ type LogicalNode<T = Record<string, unknown>> = {
67
67
  };
68
68
  /** Known aggregate function names. Consumers may support additional functions via the (string & {}) escape hatch. */
69
69
  type AggregateFn = 'sum' | 'count' | 'avg' | 'min' | 'max';
70
- /** A single aggregate function call within $select. */
71
- interface AggregateExpr {
70
+ /** A single aggregate function call within $select. Generic params preserve literal types for result inference. */
71
+ interface AggregateExpr<Fn extends string = AggregateFn | (string & {}), Field extends string = string, Alias extends string = string> {
72
72
  /** Function name (sum, count, avg, min, max, or custom). */
73
- $fn: AggregateFn | (string & {});
73
+ $fn: Fn;
74
74
  /** Field to aggregate. '*' for count(*). */
75
- $field: string;
75
+ $field: Field;
76
76
  /** Alias for the result. Auto-generated by URL parser if omitted. */
77
- $as?: string;
77
+ $as?: Alias;
78
78
  }
79
79
  /**
80
80
  * Projection definition.
@@ -92,6 +92,8 @@ interface UniqueryControls<T = Record<string, unknown>, Nav extends Record<strin
92
92
  $select?: SelectExpr<T>;
93
93
  /** Fields to group by for aggregate queries. */
94
94
  $groupBy?: (keyof T & string)[];
95
+ /** Post-aggregation filter. Operates on aggregate aliases and dimension fields. */
96
+ $having?: FilterExpr;
95
97
  /** Relations to populate alongside the query. */
96
98
  $with?: TypedWithRelation<Nav>[];
97
99
  /** Pass-through for unknown $-prefixed keywords. */
@@ -145,9 +147,46 @@ type WithRelation = {
145
147
  * Insight operator includes comparison ops, control ops ($-prefixed),
146
148
  * and aggregate function names (bare, e.g. 'sum', 'avg').
147
149
  */
148
- type InsightOp = ComparisonOp | '$select' | '$order' | '$with' | '$groupBy' | AggregateFn | (string & {});
150
+ type InsightOp = ComparisonOp | '$select' | '$order' | '$with' | '$groupBy' | '$having' | AggregateFn | (string & {});
149
151
  /** Map of field names to the set of operators used on that field. */
150
152
  type UniqueryInsights = Map<string, Set<InsightOp>>;
153
+ /** Aggregate query controls. Separate from UniqueryControls: $groupBy is required, $with is forbidden. */
154
+ interface AggregateControls<T = Record<string, unknown>, D extends keyof T & string = keyof T & string, M extends keyof T & string = keyof T & string> {
155
+ $groupBy: D[];
156
+ $select?: (D | AggregateExpr<AggregateFn, M | '*'>)[];
157
+ $having?: FilterExpr;
158
+ $sort?: Record<string, 1 | -1>;
159
+ $skip?: number;
160
+ $limit?: number;
161
+ $count?: boolean;
162
+ [key: `$${string}`]: unknown;
163
+ }
164
+ /** Aggregate query — no name (can't nest), no Nav (no $with). */
165
+ interface AggregateQuery<T = Record<string, unknown>, D extends keyof T & string = keyof T & string, M extends keyof T & string = keyof T & string> {
166
+ filter?: FilterExpr<T>;
167
+ controls: AggregateControls<T, D, M>;
168
+ insights?: UniqueryInsights;
169
+ }
170
+ /** Resolve the output alias of an AggregateExpr. Uses $as if provided, otherwise generates {fn}_{field}. */
171
+ type ResolveAlias<A extends AggregateExpr> = A extends {
172
+ $as: infer Alias extends string;
173
+ } ? Alias : A extends {
174
+ $fn: infer Fn extends string;
175
+ $field: infer F extends string;
176
+ } ? `${Fn}_${F}` : string;
177
+ /**
178
+ * Infer the result row type from an aggregate query's $select.
179
+ * Dimension fields preserve their original type from T.
180
+ * Aggregate expressions: min/max preserve original type, others → number.
181
+ */
182
+ type AggregateResult<T, Select extends readonly (string | AggregateExpr)[]> = {
183
+ [K in Extract<Select[number], string> & keyof T]: T[K];
184
+ } & {
185
+ [A in Extract<Select[number], AggregateExpr> as ResolveAlias<A>]: A extends {
186
+ $fn: 'min' | 'max';
187
+ $field: infer F extends keyof T & string;
188
+ } ? T[F] : number;
189
+ };
151
190
 
152
191
  /**
153
192
  * Visitor callbacks for controlling how filter nodes are processed.
@@ -188,4 +227,4 @@ declare function computeInsights(filter?: FilterExpr, controls?: UniqueryControl
188
227
  declare function getInsights(query: Uniquery): UniqueryInsights;
189
228
 
190
229
  export { computeInsights, getInsights, isPrimitive, walkFilter };
191
- export type { AggregateExpr, AggregateFn, ComparisonNode, ComparisonOp, FieldOps, FieldOpsFor, FieldValue, FilterExpr, FilterVisitor, InsightOp, LogicalNode, NavTarget, Primitive, SelectExpr, TypedWithRelation, Uniquery, UniqueryControls, UniqueryInsights, WithRelation };
230
+ export type { AggregateControls, AggregateExpr, AggregateFn, AggregateQuery, AggregateResult, ComparisonNode, ComparisonOp, FieldOps, FieldOpsFor, FieldValue, FilterExpr, FilterVisitor, InsightOp, LogicalNode, NavTarget, Primitive, ResolveAlias, SelectExpr, TypedWithRelation, Uniquery, UniqueryControls, UniqueryInsights, WithRelation };
package/dist/index.mjs CHANGED
@@ -70,6 +70,14 @@ function isPrimitive(x) {
70
70
  else capture(entry.$field, entry.$fn);
71
71
  else for (const field of Object.keys(controls.$select)) capture(field, "$select");
72
72
  if (controls?.$groupBy) for (const field of controls.$groupBy) capture(field, "$groupBy");
73
+ if (controls?.$having) walkFilter(controls.$having, {
74
+ comparison(field) {
75
+ capture(field, "$having");
76
+ },
77
+ and() {},
78
+ or() {},
79
+ not() {}
80
+ });
73
81
  if (controls?.$sort) for (const field of Object.keys(controls.$sort)) capture(field, "$order");
74
82
  if (controls?.$with) for (const entry of controls.$with) {
75
83
  if (typeof entry === "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqu/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Canonical query format types, tree walker, and utilities for Uniqu",
5
5
  "license": "MIT",
6
6
  "author": "Artem Maltsev",