@plumbus/core 0.6.7 → 0.6.8

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.
@@ -107,6 +107,31 @@ const total = await ctx.data.User.count({ active: true }, filters);
107
107
 
108
108
  `findMany` applies `limit`/`offset`/`orderBy`; `count` returns the matching row count over the same filters (so page totals stay correct). `search` uses case-insensitive `ILIKE` (OR across columns), `in` is SQL `IN`, `notEq` is `<>`. Multi-column sort: `orderBy: [{ column: "name", dir: "asc" }, { column: "createdAt", dir: "desc" }]`. Tenant isolation and soft-delete filters are applied automatically.
109
109
 
110
+ ### Aggregates (SUM / GROUP BY / DISTINCT)
111
+
112
+ To total, average, or group **in the database** — instead of loading rows and reducing in memory — use `aggregate`. Filtering is identical to `findMany`/`count` (the `query` equality arg plus `dateFilters`/`search`/`in`/`notEq`), and tenant scoping, soft-delete, and encrypted-field guards all apply.
113
+
114
+ ```ts
115
+ // Grand total over a filtered scope — one row, even over zero matches:
116
+ const [totals] = await ctx.data.ProjectCostLedger.aggregate(
117
+ { projectId },
118
+ { dateFilters: { recordedAt: { gte: monthStart } }, sum: "cost", count: true },
119
+ );
120
+ totals.sum_cost; // number (empty scope = 0); totals.count // number
121
+
122
+ // GROUP BY with per-group aggregates, ordered by an aggregate, limited:
123
+ const byProvider = await ctx.data.ProjectCostLedger.aggregate(
124
+ {},
125
+ { groupBy: "provider", sum: "cost", count: true, orderBy: "sum_cost", orderDir: "desc", limit: 10 },
126
+ );
127
+ // → [{ provider: "openai", sum_cost: 41.2, count: 318 }, ...]
128
+
129
+ // COUNT(DISTINCT), min/max/avg also supported:
130
+ await ctx.data.WorkflowStep.aggregate({}, { countDistinct: "projectId" }); // [{ countDistinct_projectId: 7 }]
131
+ ```
132
+
133
+ Result rows (`AggregateRow`) key each `groupBy` column by its value, plus `count` (when `count: true`), `sum_<col>`, `avg_<col>`, `min_<col>`, `max_<col>`, and `countDistinct_<col>` per requested column. `sum_*`, `count`, and `countDistinct_*` are numbers (an empty `SUM` is `0`, not `null`); `avg_*`/`min_*`/`max_*` are `null` over an empty set. Without `groupBy` you always get exactly one grand-total row; with `groupBy`, one row per group that has matching records. `orderBy` may reference a group column or an aggregate alias (e.g. `"sum_cost"`); `limit` is clamped to 1–1000. Requesting neither a group column nor an aggregate throws. Aggregating an `encrypted: true` field is rejected. Add a matching composite index (e.g. `indexes: [["projectId", "recordedAt"]]`) so the aggregate runs as an index scan.
134
+
110
135
  Repositories automatically:
111
136
  - Inject `tenantId` from `ctx.auth.tenantId` (if `tenantScoped: true`)
112
137
  - Record audit events for mutations
@@ -64,6 +64,8 @@ Entity fields with `classification: "sensitive"` or `"highly_sensitive"`:
64
64
  - Trigger governance warnings if used in AI prompts
65
65
  - Can be flagged for encryption at rest (`encrypted: true` + `PLUMBUS_ENCRYPTION_KEY`)
66
66
 
67
+ When encryption is enabled, repositories reject `findMany`/`aggregate` filters and aggregate functions on encrypted string columns (ciphertext is not queryable at the SQL layer). See [entities.md](./entities.md#aggregates-sum--group-by--distinct).
68
+
67
69
  **AI prompt scanning** is separate and **opt-in**: configure `aiProviders.security` (or `AI_SECURITY_*` env) to warn/redact/block classified fields in `ctx.ai.*` inputs. Without that config, classification alone does not scan prompts. See `ai.md` § Security.
68
70
 
69
71
  ### Edit Zones
@@ -47,6 +47,35 @@ expect(page1.data.items).not.toEqual(page2.data.items);
47
47
  expect(page1.data.total).toBeGreaterThan(page1.data.limit);
48
48
  ```
49
49
 
50
+ ### Aggregate / dashboard capabilities
51
+
52
+ When testing capabilities that call `aggregate()` (totals, grouped rollups, `COUNT(DISTINCT)`), use `createTestContext()` or `createInMemoryRepository()` — both mirror production aggregate semantics without a database:
53
+
54
+ - Assert **grand totals** return a single row (even over an empty filter scope, where `SUM` is `0`)
55
+ - Assert **GROUP BY** returns one row per group with the expected `sum_*` / `count` / `countDistinct_*` keys
56
+ - Assert **filters** (`query`, `dateFilters`, `search`, `in`, `notEq`) narrow the scope the same way as `findMany`/`count`
57
+ - Assert **empty scope** for grouped queries returns no rows; ungrouped empty scope returns `[{ sum_*: 0, count: 0 }]`
58
+
59
+ ```ts
60
+ const ctx = createTestContext({
61
+ data: {
62
+ Ledger: [
63
+ { id: "1", provider: "openai", cost: 1.5 },
64
+ { id: "2", provider: "openai", cost: 2.5 },
65
+ { id: "3", provider: "anthropic", cost: 3 },
66
+ ],
67
+ },
68
+ });
69
+
70
+ const rows = await ctx.data.Ledger.aggregate(
71
+ { provider: "openai" },
72
+ { sum: "cost", count: true },
73
+ );
74
+ expect(rows).toEqual([{ sum_cost: 4, count: 2 }]);
75
+ ```
76
+
77
+ See [entities.md](./entities.md#aggregates-sum--group-by--distinct) for result shape and guards.
78
+
50
79
  ### `simulateFlow`
51
80
 
52
81
  Simulate flow execution and inspect step history:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumbus/core",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Plumbus framework core — types, SDK, runtime, CLI, test utilities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -95,9 +95,9 @@
95
95
  "@vitest/browser": "^3.0.0",
96
96
  "esbuild": "^0.25.12",
97
97
  "typescript": "^5.7.0",
98
- "@plumbus/api": "0.1.2",
98
+ "@plumbus/voice": "0.3.0",
99
99
  "@plumbus/mcp": "0.5.1",
100
- "@plumbus/voice": "0.3.0"
100
+ "@plumbus/api": "0.1.2"
101
101
  },
102
102
  "scripts": {
103
103
  "build": "tsc -b",