@classytic/arc 2.14.2 → 2.15.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 +44 -0
- package/dist/{BaseController-Dv60tU83.mjs → BaseController-dx3m2J8V.mjs} +102 -2
- package/dist/auth/index.d.mts +1 -1
- package/dist/{buildHandler-jSZ6Fdvi.mjs → buildHandler-CcFOpJLh.mjs} +2 -19
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +2 -2
- package/dist/{core-D29kkRL5.mjs → core-CvmOqEms.mjs} +70 -13
- package/dist/{createAggregationRouter-DhR-Ofiz.mjs → createAggregationRouter-B0bPDf5b.mjs} +7 -5
- package/dist/{createApp-BarYhXCZ.mjs → createApp-PFegs47-.mjs} +64 -4
- package/dist/docs/index.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/{index-D1-Kp_dP.d.mts → index-BstGxcc3.d.mts} +1 -1
- package/dist/{index-Dwc0orNd.d.mts → index-BswOSJCE.d.mts} +88 -17
- package/dist/{index-Bt0F3nJj.d.mts → index-bRjYu21O.d.mts} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/middleware/index.d.mts +1 -1
- package/dist/org/index.d.mts +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +2 -35
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -1
- package/dist/presets/search.d.mts +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/{resourceToTools-BM686jB4.mjs → resourceToTools-tFYUNmM0.mjs} +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.d.mts +1 -1
- package/dist/{types-C6ONJ_Z2.d.mts → types-BQsjgQzS.d.mts} +1 -1
- package/dist/{types-NGtx3uxV.d.mts → types-DrBaUwyV.d.mts} +40 -4
- package/dist/utils/index.d.mts +1 -1
- package/dist/{versioning-DTTvc80y.d.mts → versioning-hmkPcDlX.d.mts} +34 -1
- package/package.json +5 -5
- package/skills/arc/SKILL.md +77 -0
- package/skills/arc-code-review/references/anti-patterns.md +44 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.3",
|
|
4
4
|
"description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
},
|
|
247
247
|
"peerDependencies": {
|
|
248
248
|
"@classytic/primitives": ">=0.4.0",
|
|
249
|
-
"@classytic/repo-core": ">=0.4.
|
|
249
|
+
"@classytic/repo-core": ">=0.4.1",
|
|
250
250
|
"@classytic/streamline": ">=2.3.0",
|
|
251
251
|
"@fastify/cors": ">=11.0.0",
|
|
252
252
|
"@fastify/helmet": ">=13.0.0",
|
|
@@ -357,10 +357,10 @@
|
|
|
357
357
|
"@better-auth/mongo-adapter": "^1.6.9",
|
|
358
358
|
"@biomejs/biome": "^2.4.11",
|
|
359
359
|
"@classytic/dev-tools": "^0.2.0",
|
|
360
|
-
"@classytic/mongokit": "^3.13.
|
|
360
|
+
"@classytic/mongokit": "^3.13.2",
|
|
361
361
|
"@classytic/primitives": "^0.4.0",
|
|
362
|
-
"@classytic/repo-core": "^0.4.
|
|
363
|
-
"@classytic/sqlitekit": "^0.3.
|
|
362
|
+
"@classytic/repo-core": "^0.4.1",
|
|
363
|
+
"@classytic/sqlitekit": "^0.3.1",
|
|
364
364
|
"@classytic/streamline": "^2.3.0",
|
|
365
365
|
"@fastify/cors": "^11.2.0",
|
|
366
366
|
"@fastify/helmet": "^13.0.2",
|
package/skills/arc/SKILL.md
CHANGED
|
@@ -639,6 +639,83 @@ defineResource({
|
|
|
639
639
|
|
|
640
640
|
MCP auto-derives `filterableFields` from `queryParser`.
|
|
641
641
|
|
|
642
|
+
## Aggregations — dashboards in declarative form
|
|
643
|
+
|
|
644
|
+
Add `aggregations: { … }` to a resource and Arc registers `GET
|
|
645
|
+
/{prefix}/aggregations/:name` per entry. Each runs a portable `$match
|
|
646
|
+
→ $group → $project → $sort → $limit` pipeline against the kit's
|
|
647
|
+
`repo.aggregate(req, options)` — same shape across mongokit /
|
|
648
|
+
sqlitekit / prismakit, so dashboards work unchanged across backends.
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
import { defineResource, defineAggregation } from '@classytic/arc';
|
|
652
|
+
|
|
653
|
+
defineResource({
|
|
654
|
+
name: 'transaction',
|
|
655
|
+
adapter,
|
|
656
|
+
presets: [multiTenantPreset({ tenantField: 'organizationId' })],
|
|
657
|
+
permissions: { list: canViewRevenue() },
|
|
658
|
+
|
|
659
|
+
aggregations: {
|
|
660
|
+
byPaymentMethod: defineAggregation({
|
|
661
|
+
groupBy: 'method',
|
|
662
|
+
measures: { total: 'sum:amount', count: 'count' },
|
|
663
|
+
sort: { total: -1 },
|
|
664
|
+
cache: { staleTime: 60, swr: true, tags: ['revenue'] },
|
|
665
|
+
permissions: canViewRevenue(),
|
|
666
|
+
}),
|
|
667
|
+
byFlow: defineAggregation({
|
|
668
|
+
groupBy: 'flow',
|
|
669
|
+
measures: { total: 'sum:amount', count: 'count' },
|
|
670
|
+
cache: { staleTime: 60, swr: true, tags: ['revenue'] },
|
|
671
|
+
permissions: canViewRevenue(),
|
|
672
|
+
}),
|
|
673
|
+
byDay: defineAggregation({
|
|
674
|
+
dateBuckets: { day: { field: 'createdAt', interval: 'day' } },
|
|
675
|
+
groupBy: 'flow',
|
|
676
|
+
measures: { total: 'sum:amount', count: 'count' },
|
|
677
|
+
sort: { day: 1 },
|
|
678
|
+
requireDateRange: { field: 'createdAt', maxRangeDays: 365 },
|
|
679
|
+
cache: { staleTime: 60, swr: true, tags: ['revenue'] },
|
|
680
|
+
permissions: canViewRevenue(),
|
|
681
|
+
}),
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
**Tenant scope flows through the second arg, NOT the filter.** Arc is
|
|
687
|
+
DB-agnostic — type-coercion (string → ObjectId for mongokit
|
|
688
|
+
`fieldType: 'objectId'`, UUID/text for sqlitekit, etc.) belongs to the
|
|
689
|
+
kit. Arc threads `tenantOptions` to `repo.aggregate(req, options)`;
|
|
690
|
+
the kit's multi-tenant plugin reads `context.organizationId`, casts
|
|
691
|
+
correctly, and merges into the request. Authors never inject the
|
|
692
|
+
tenant key into `aggReq.filter` themselves.
|
|
693
|
+
|
|
694
|
+
**Caller filters via query string compose with `groupBy` / measures:**
|
|
695
|
+
|
|
696
|
+
```
|
|
697
|
+
GET /api/transactions/aggregations/byPaymentMethod?status=verified
|
|
698
|
+
GET /api/transactions/aggregations/byDay?createdAt[gte]=2026-01-01&createdAt[lt]=2026-02-01
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
**Safety guards on the declaration:**
|
|
702
|
+
- `requireDateRange: { field, maxRangeDays }` — bounded range mandatory; kills accidental all-time scans
|
|
703
|
+
- `requireFilters: ['orgId']` — mandatory scope keys
|
|
704
|
+
- `maxGroups: 1000` — post-execution row cap; 422 on overflow
|
|
705
|
+
|
|
706
|
+
**Cache invalidation:** writes through resource CRUD bump the
|
|
707
|
+
matching tag. Aggregations cached with the same `tags` invalidate
|
|
708
|
+
together. SWR mode serves stale immediately while revalidating in
|
|
709
|
+
background.
|
|
710
|
+
|
|
711
|
+
**MCP auto-export:** every aggregation surfaces as an MCP tool
|
|
712
|
+
named `{resource}_aggregations_{name}` with the same permission gate
|
|
713
|
+
and filter validation as the HTTP route.
|
|
714
|
+
|
|
715
|
+
For backends without `repo.aggregate` (custom adapters), declare a
|
|
716
|
+
`materialized` hook on the aggregation — Arc routes through it
|
|
717
|
+
instead of the kit and returns the same `{ rows }` envelope.
|
|
718
|
+
|
|
642
719
|
## QueryCache
|
|
643
720
|
|
|
644
721
|
TanStack Query-style server cache, stale-while-revalidate, auto-invalidation on mutations.
|
|
@@ -894,6 +894,49 @@ Then check whether the hook body sets headers (`reply.header(`, `reply.headers[`
|
|
|
894
894
|
|
|
895
895
|
---
|
|
896
896
|
|
|
897
|
+
## §33. Hand-rolled aggregation routes when `aggregations: { … }` would do 🟠
|
|
898
|
+
|
|
899
|
+
**Detection:**
|
|
900
|
+
```
|
|
901
|
+
pattern: "Model\\.aggregate\\(\\["
|
|
902
|
+
output_mode: content
|
|
903
|
+
```
|
|
904
|
+
Also: `\\.aggregate\\(\\s*\\[`, `\\$group\\b`, `\\$match\\b` inside resource handler files. Plus inline mongoose pipeline imports inside route handlers (vs. inside the kit's repository).
|
|
905
|
+
|
|
906
|
+
**Anti-pattern:**
|
|
907
|
+
```typescript
|
|
908
|
+
// In a custom GET /aggregate/byPaymentMethod route:
|
|
909
|
+
const orgId = getOrgId(getScope(req));
|
|
910
|
+
const rows = await Transaction.aggregate([
|
|
911
|
+
{ $match: { organizationId: new mongoose.Types.ObjectId(orgId), status: 'verified' } },
|
|
912
|
+
{ $group: { _id: '$method', total: { $sum: '$amount' }, count: { $sum: 1 } } },
|
|
913
|
+
{ $sort: { total: -1 } },
|
|
914
|
+
]);
|
|
915
|
+
return reply.send({ rows });
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
**Why high:** loses every cross-cutting feature arc's aggregation router gives for free — per-aggregation permissions, OpenAPI schema, MCP tool export, SWR cache + tag invalidation, `requireFilters` / `requireDateRange` safety guards, `maxGroups` cap, multi-tenant scope without manual `ObjectId` casting. The hand-rolled version also drifts: every new bucket needs a new route, no central declaration, no unified shape across kits.
|
|
919
|
+
|
|
920
|
+
**Fix:** declare `aggregations: { name: defineAggregation({ groupBy, measures, sort, cache, permissions }) }` on the resource. Arc registers `GET /:prefix/aggregations/:name` per entry, threads `tenantOptions` to `repo.aggregate(req, options)`, and the kit's multi-tenant plugin handles type-coercion. Type-coercion (string → ObjectId / UUID / text) is **the kit's responsibility, not the framework's** — never inject the tenant key into `aggReq.filter` from arc-host code.
|
|
921
|
+
|
|
922
|
+
```typescript
|
|
923
|
+
import { defineAggregation } from '@classytic/arc';
|
|
924
|
+
|
|
925
|
+
aggregations: {
|
|
926
|
+
byPaymentMethod: defineAggregation({
|
|
927
|
+
groupBy: 'method',
|
|
928
|
+
measures: { total: 'sum:amount', count: 'count' },
|
|
929
|
+
sort: { total: -1 },
|
|
930
|
+
cache: { staleTime: 60, swr: true, tags: ['revenue'] },
|
|
931
|
+
permissions: canViewRevenue(),
|
|
932
|
+
}),
|
|
933
|
+
}
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
For backends without `repo.aggregate`, declare a `materialized` hook on the aggregation — the router calls it instead of the kit and keeps the same `{ rows }` envelope, permission gate, and cache contract.
|
|
937
|
+
|
|
938
|
+
---
|
|
939
|
+
|
|
897
940
|
## Detection checklist (run-order)
|
|
898
941
|
|
|
899
942
|
Run sweeps in this order — early hits often invalidate later context:
|
|
@@ -909,3 +952,4 @@ Run sweeps in this order — early hits often invalidate later context:
|
|
|
909
952
|
9. §14, §18, §19 — preset adoption + custom controller wiring
|
|
910
953
|
10. §11, §12, §13, §29, §30, §31 — style and edges
|
|
911
954
|
11. §32a–§32g — canonical-contract drift (pagination / events / tenant / errors / adapter imports), missing `requireOrgId` accessors, missing `schemaGenerator`, kit-specific adapter imports from arc (3.0 break)
|
|
955
|
+
12. §33 — hand-rolled `Model.aggregate([...])` in resource routes vs declarative `aggregations: { … }`
|
|
@@ -246,6 +246,33 @@ Modes: `memory` (default) | `distributed` (`stores.queryCache: RedisCacheStore`)
|
|
|
246
246
|
|
|
247
247
|
---
|
|
248
248
|
|
|
249
|
+
## Aggregations (declarative dashboards)
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { defineAggregation } from '@classytic/arc';
|
|
253
|
+
|
|
254
|
+
aggregations: {
|
|
255
|
+
byMethod: defineAggregation({
|
|
256
|
+
groupBy: 'method',
|
|
257
|
+
measures: { total: 'sum:amount', count: 'count' },
|
|
258
|
+
sort: { total: -1 },
|
|
259
|
+
cache: { staleTime: 60, swr: true, tags: ['revenue'] },
|
|
260
|
+
permissions: canViewRevenue(),
|
|
261
|
+
}),
|
|
262
|
+
byDay: defineAggregation({
|
|
263
|
+
dateBuckets: { day: { field: 'createdAt', interval: 'day' } },
|
|
264
|
+
groupBy: 'flow',
|
|
265
|
+
measures: { total: 'sum:amount' },
|
|
266
|
+
requireDateRange: { field: 'createdAt', maxRangeDays: 365 },
|
|
267
|
+
permissions: canViewRevenue(),
|
|
268
|
+
}),
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Registers `GET /:prefix/aggregations/:name` per entry. Same permissions, OpenAPI, MCP tool, cache + tag invalidation as CRUD. Tenant flows via the kit's multi-tenant plugin (string → ObjectId casting handled by the kit, **not** by hand). Caller filters via query string (`?status=verified&createdAt[gte]=...`) compose with the declaration. Safety: `requireFilters`, `requireDateRange`, `maxGroups`. **Anti-pattern:** custom routes calling `Model.aggregate([...])` directly — see anti-patterns §33.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
249
276
|
## CLI
|
|
250
277
|
|
|
251
278
|
```bash
|