@classytic/arc 1.1.0 → 2.1.2
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 +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
- package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
- package/dist/HookSystem-BsGV-j2l.mjs +405 -0
- package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +82 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +276 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-C3T5DTUx.mjs +141 -0
- package/dist/audited-C3T5DTUx.mjs.map +1 -0
- package/dist/auth/index.d.mts +189 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +1102 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth/redis-session.d.mts +44 -0
- package/dist/auth/redis-session.d.mts.map +1 -0
- package/dist/auth/redis-session.mjs +76 -0
- package/dist/auth/redis-session.mjs.map +1 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
- package/dist/cache/index.d.mts +146 -0
- package/dist/cache/index.d.mts.map +1 -0
- package/dist/cache/index.mjs +92 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/caching-Bl28lYsR.mjs +94 -0
- package/dist/caching-Bl28lYsR.mjs.map +1 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
- package/dist/cli/commands/describe.d.mts +19 -0
- package/dist/cli/commands/describe.d.mts.map +1 -0
- package/dist/cli/commands/describe.mjs +239 -0
- package/dist/cli/commands/describe.mjs.map +1 -0
- package/dist/cli/commands/docs.d.mts +14 -0
- package/dist/cli/commands/docs.d.mts.map +1 -0
- package/dist/cli/commands/docs.mjs +53 -0
- package/dist/cli/commands/docs.mjs.map +1 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
- package/dist/cli/commands/generate.d.mts.map +1 -0
- package/dist/cli/commands/generate.mjs +358 -0
- package/dist/cli/commands/generate.mjs.map +1 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
- package/dist/cli/commands/init.d.mts.map +1 -0
- package/dist/cli/commands/{init.js → init.mjs} +807 -616
- package/dist/cli/commands/init.mjs.map +1 -0
- package/dist/cli/commands/introspect.d.mts +11 -0
- package/dist/cli/commands/introspect.d.mts.map +1 -0
- package/dist/cli/commands/introspect.mjs +76 -0
- package/dist/cli/commands/introspect.mjs.map +1 -0
- package/dist/cli/index.d.mts +17 -0
- package/dist/cli/index.d.mts.map +1 -0
- package/dist/cli/index.mjs +157 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/constants-DdXFXQtN.mjs +85 -0
- package/dist/constants-DdXFXQtN.mjs.map +1 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-CUgNqegw.mjs +560 -0
- package/dist/createApp-CUgNqegw.mjs.map +1 -0
- package/dist/defineResource-k0_BDn8v.mjs +2197 -0
- package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
- package/dist/discovery/index.d.mts +47 -0
- package/dist/discovery/index.d.mts.map +1 -0
- package/dist/discovery/index.mjs +110 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/docs/index.d.mts +163 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +73 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/elevation-BRy3yFWT.mjs +113 -0
- package/dist/elevation-BRy3yFWT.mjs.map +1 -0
- package/dist/elevation-B_2dRLVP.d.mts +88 -0
- package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
- package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
- package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
- package/dist/errorHandler-C1okiriz.mjs +109 -0
- package/dist/errorHandler-C1okiriz.mjs.map +1 -0
- package/dist/errors-B9bZok84.mjs +212 -0
- package/dist/errors-B9bZok84.mjs.map +1 -0
- package/dist/errors-ChKiFz62.d.mts +125 -0
- package/dist/errors-ChKiFz62.d.mts.map +1 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
- package/dist/eventPlugin-DGR_B2on.mjs +230 -0
- package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
- package/dist/events/index.d.mts +54 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +52 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +178 -0
- package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
- package/dist/events/transports/redis.d.mts +77 -0
- package/dist/events/transports/redis.d.mts.map +1 -0
- package/dist/events/transports/redis.mjs +125 -0
- package/dist/events/transports/redis.mjs.map +1 -0
- package/dist/externalPaths-DlINfKbP.d.mts +51 -0
- package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
- package/dist/factory/index.d.mts +64 -0
- package/dist/factory/index.d.mts.map +1 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
- package/dist/fields-DyaDVX4J.d.mts +110 -0
- package/dist/fields-DyaDVX4J.d.mts.map +1 -0
- package/dist/fields-iagOozy0.mjs +115 -0
- package/dist/fields-iagOozy0.mjs.map +1 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +97 -0
- package/dist/idempotency/index.d.mts.map +1 -0
- package/dist/idempotency/index.mjs +320 -0
- package/dist/idempotency/index.mjs.map +1 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +115 -0
- package/dist/idempotency/mongodb.mjs.map +1 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +104 -0
- package/dist/idempotency/redis.mjs.map +1 -0
- package/dist/index.d.mts +261 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +105 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations/event-gateway.d.mts +47 -0
- package/dist/integrations/event-gateway.d.mts.map +1 -0
- package/dist/integrations/event-gateway.mjs +44 -0
- package/dist/integrations/event-gateway.mjs.map +1 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +104 -0
- package/dist/integrations/jobs.d.mts.map +1 -0
- package/dist/integrations/jobs.mjs +124 -0
- package/dist/integrations/jobs.mjs.map +1 -0
- package/dist/integrations/streamline.d.mts +61 -0
- package/dist/integrations/streamline.d.mts.map +1 -0
- package/dist/integrations/streamline.mjs +126 -0
- package/dist/integrations/streamline.mjs.map +1 -0
- package/dist/integrations/websocket.d.mts +83 -0
- package/dist/integrations/websocket.d.mts.map +1 -0
- package/dist/integrations/websocket.mjs +289 -0
- package/dist/integrations/websocket.mjs.map +1 -0
- package/dist/interface-B01JvPVc.d.mts +78 -0
- package/dist/interface-B01JvPVc.d.mts.map +1 -0
- package/dist/interface-CZe8IkMf.d.mts +55 -0
- package/dist/interface-CZe8IkMf.d.mts.map +1 -0
- package/dist/interface-Ch8HU9uM.d.mts +1098 -0
- package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
- package/dist/keys-BqNejWup.mjs +43 -0
- package/dist/keys-BqNejWup.mjs.map +1 -0
- package/dist/logger-Df2O2WsW.mjs +79 -0
- package/dist/logger-Df2O2WsW.mjs.map +1 -0
- package/dist/memory-cQgelFOj.mjs +144 -0
- package/dist/memory-cQgelFOj.mjs.map +1 -0
- package/dist/migrations/index.d.mts +157 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +261 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/mongodb-BfJVlUJH.mjs +94 -0
- package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
- package/dist/mongodb-CGzRbfAK.d.mts +119 -0
- package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
- package/dist/mongodb-JN-9JA7K.d.mts +72 -0
- package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
- package/dist/openapi-G3Cw7XuM.mjs +524 -0
- package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
- package/dist/org/index.d.mts +69 -0
- package/dist/org/index.d.mts.map +1 -0
- package/dist/org/index.mjs +514 -0
- package/dist/org/index.mjs.map +1 -0
- package/dist/org/types.d.mts +83 -0
- package/dist/org/types.d.mts.map +1 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +279 -0
- package/dist/permissions/index.d.mts.map +1 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/permissions/index.mjs.map +1 -0
- package/dist/plugins/index.d.mts +173 -0
- package/dist/plugins/index.d.mts.map +1 -0
- package/dist/plugins/index.mjs +523 -0
- package/dist/plugins/index.mjs.map +1 -0
- package/dist/plugins/response-cache.d.mts +88 -0
- package/dist/plugins/response-cache.d.mts.map +1 -0
- package/dist/plugins/response-cache.mjs +284 -0
- package/dist/plugins/response-cache.mjs.map +1 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +186 -0
- package/dist/plugins/tracing-entry.mjs.map +1 -0
- package/dist/pluralize-CEweyOEm.mjs +87 -0
- package/dist/pluralize-CEweyOEm.mjs.map +1 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -169
- package/dist/policies/index.d.mts.map +1 -0
- package/dist/policies/index.mjs +322 -0
- package/dist/policies/index.mjs.map +1 -0
- package/dist/presets/{index.d.ts → index.d.mts} +63 -131
- package/dist/presets/index.d.mts.map +1 -0
- package/dist/presets/index.mjs +144 -0
- package/dist/presets/index.mjs.map +1 -0
- package/dist/presets/multiTenant.d.mts +25 -0
- package/dist/presets/multiTenant.d.mts.map +1 -0
- package/dist/presets/multiTenant.mjs +114 -0
- package/dist/presets/multiTenant.mjs.map +1 -0
- package/dist/presets-BITljm96.mjs +120 -0
- package/dist/presets-BITljm96.mjs.map +1 -0
- package/dist/presets-DzSMwlKj.d.mts +58 -0
- package/dist/presets-DzSMwlKj.d.mts.map +1 -0
- package/dist/prisma-DJbMt3yf.mjs +628 -0
- package/dist/prisma-DJbMt3yf.mjs.map +1 -0
- package/dist/prisma-Dg9GoVdj.d.mts +275 -0
- package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
- package/dist/redis-D-JAeLtm.d.mts +50 -0
- package/dist/redis-D-JAeLtm.d.mts.map +1 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
- package/dist/registry/index.d.mts +12 -0
- package/dist/registry/index.d.mts.map +1 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-QQD6ROJc.mjs +56 -0
- package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
- package/dist/schemaConverter-BwrmWroW.mjs +99 -0
- package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
- package/dist/schemas/index.d.mts +64 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +83 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/scope/index.d.mts +22 -0
- package/dist/scope/index.d.mts.map +1 -0
- package/dist/scope/index.mjs +66 -0
- package/dist/scope/index.mjs.map +1 -0
- package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
- package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
- package/dist/sse-B3c3_yZp.mjs +124 -0
- package/dist/sse-B3c3_yZp.mjs.map +1 -0
- package/dist/testing/index.d.mts +908 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +1977 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/tracing-Cc7vVQPp.d.mts +71 -0
- package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
- package/dist/typeGuards-DhMNLuvU.mjs +10 -0
- package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
- package/dist/types/index.d.mts +947 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +15 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-Beqn1Un7.mjs +39 -0
- package/dist/types-Beqn1Un7.mjs.map +1 -0
- package/dist/types-CIgB7UUl.d.mts +446 -0
- package/dist/types-CIgB7UUl.d.mts.map +1 -0
- package/dist/types-aYB4V7uN.d.mts +87 -0
- package/dist/types-aYB4V7uN.d.mts.map +1 -0
- package/dist/utils/index.d.mts +748 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
package/README.md
CHANGED
|
@@ -1,162 +1,45 @@
|
|
|
1
1
|
# @classytic/arc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Database-agnostic resource framework for Fastify. Define resources, get CRUD routes, permissions, presets, caching, events, and OpenAPI — without boilerplate.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Requires:** Fastify 5+ | Node.js 22+ | ESM only
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **MongoDB** → `npm install @classytic/mongokit`
|
|
9
|
-
- **PostgreSQL/MySQL/SQLite** → `@classytic/prismakit` (coming soon)
|
|
10
|
-
|
|
11
|
-
> **⚠️ ESM Only**: Arc requires Node.js 18+ with ES modules (`"type": "module"` in package.json). CommonJS is not supported. [Migration guide →](https://nodejs.org/api/esm.html)
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Why Arc?
|
|
16
|
-
|
|
17
|
-
Building REST APIs in Node.js often means making hundreds of small decisions: How do I structure routes? Where does validation go? How do I handle soft deletes consistently? What about multi-tenant isolation?
|
|
18
|
-
|
|
19
|
-
**Arc gives you conventions so you can focus on your domain, not boilerplate.**
|
|
20
|
-
|
|
21
|
-
| Without Arc | With Arc |
|
|
22
|
-
|-------------|----------|
|
|
23
|
-
| Write CRUD routes for every model | `defineResource()` generates them |
|
|
24
|
-
| Manually wire controllers to routes | Convention-based auto-wiring |
|
|
25
|
-
| Copy-paste soft delete logic | `presets: ['softDelete']` |
|
|
26
|
-
| Manually filter by tenant on every query | `presets: ['multiTenant']` auto-filters |
|
|
27
|
-
| Hand-roll OpenAPI specs | Auto-generated from resources |
|
|
28
|
-
|
|
29
|
-
**Arc is opinionated where it matters, flexible where you need it.**
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Installation
|
|
7
|
+
## Install
|
|
34
8
|
|
|
35
9
|
```bash
|
|
36
|
-
|
|
37
|
-
npm install @classytic/
|
|
38
|
-
|
|
39
|
-
# Choose your database kit:
|
|
40
|
-
npm install @classytic/mongokit # MongoDB/Mongoose
|
|
41
|
-
# npm install @classytic/prismakit # PostgreSQL/MySQL/SQLite (coming soon)
|
|
10
|
+
npm install @classytic/arc fastify
|
|
11
|
+
npm install @classytic/mongokit mongoose # MongoDB adapter
|
|
42
12
|
```
|
|
43
13
|
|
|
44
|
-
### Optional Dependencies
|
|
45
|
-
|
|
46
|
-
Arc's security and utility plugins are opt-in via peer dependencies. Install only what you need:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
# Security plugins (recommended for production)
|
|
50
|
-
npm install @fastify/helmet @fastify/cors @fastify/rate-limit
|
|
51
|
-
|
|
52
|
-
# Performance plugins
|
|
53
|
-
npm install @fastify/under-pressure
|
|
54
|
-
|
|
55
|
-
# Utility plugins
|
|
56
|
-
npm install @fastify/sensible @fastify/multipart fastify-raw-body
|
|
57
|
-
|
|
58
|
-
# Development logging
|
|
59
|
-
npm install pino-pretty
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Or disable plugins you don't need:
|
|
63
|
-
```typescript
|
|
64
|
-
createApp({
|
|
65
|
-
helmet: false, // Disable if not needed
|
|
66
|
-
rateLimit: false, // Disable if not needed
|
|
67
|
-
// ...
|
|
68
|
-
})
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Key Features
|
|
72
|
-
|
|
73
|
-
- **Resource-First Architecture** — Define your API as resources with `defineResource()`, not scattered route handlers
|
|
74
|
-
- **Presets System** — Composable behaviors like `softDelete`, `slugLookup`, `tree`, `ownedByUser`, `multiTenant`
|
|
75
|
-
- **Auto-Generated OpenAPI** — Documentation that stays in sync with your code
|
|
76
|
-
- **Database-Agnostic Core** — Works with any database via adapters. MongoDB/Mongoose optimized out of the box, extensible to Prisma, Drizzle, TypeORM, etc.
|
|
77
|
-
- **Production Defaults** — Helmet, CORS, rate limiting enabled by default
|
|
78
|
-
- **CLI Tooling** — `arc generate resource` scaffolds new resources instantly
|
|
79
|
-
- **Environment Presets** — Development, production, and testing configs built-in
|
|
80
|
-
- **Type-Safe Presets** — TypeScript interfaces ensure controller methods match preset requirements
|
|
81
|
-
- **Ultra-Fast Testing** — In-memory MongoDB support for 10x faster tests
|
|
82
|
-
|
|
83
14
|
## Quick Start
|
|
84
15
|
|
|
85
|
-
### Using ArcFactory (Recommended)
|
|
86
|
-
|
|
87
16
|
```typescript
|
|
88
17
|
import mongoose from 'mongoose';
|
|
89
18
|
import { createApp } from '@classytic/arc/factory';
|
|
90
|
-
import { productResource } from './resources/product.js';
|
|
91
|
-
import config from './config/index.js';
|
|
92
19
|
|
|
93
|
-
|
|
94
|
-
await mongoose.connect(config.db.uri);
|
|
20
|
+
await mongoose.connect(process.env.DB_URI);
|
|
95
21
|
|
|
96
|
-
// 2. Create Arc app
|
|
97
22
|
const app = await createApp({
|
|
98
|
-
preset: 'production',
|
|
99
|
-
auth: { jwt: { secret:
|
|
100
|
-
cors: { origin:
|
|
101
|
-
|
|
102
|
-
// Opt-out security (all enabled by default)
|
|
103
|
-
helmet: true, // Set false to disable
|
|
104
|
-
rateLimit: true, // Set false to disable
|
|
105
|
-
underPressure: true, // Set false to disable
|
|
23
|
+
preset: 'production',
|
|
24
|
+
auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
|
|
25
|
+
cors: { origin: process.env.ALLOWED_ORIGINS?.split(',') },
|
|
106
26
|
});
|
|
107
27
|
|
|
108
|
-
// 3. Register your resources
|
|
109
28
|
await app.register(productResource.toPlugin());
|
|
110
|
-
|
|
111
29
|
await app.listen({ port: 8040, host: '0.0.0.0' });
|
|
112
30
|
```
|
|
113
31
|
|
|
114
|
-
|
|
32
|
+
## defineResource
|
|
115
33
|
|
|
116
|
-
|
|
34
|
+
Single API for a full REST resource with routes, permissions, and behaviors:
|
|
117
35
|
|
|
118
36
|
```typescript
|
|
119
|
-
import
|
|
120
|
-
|
|
121
|
-
// Connect to multiple databases
|
|
122
|
-
const primaryDb = await mongoose.connect(process.env.PRIMARY_DB);
|
|
123
|
-
const analyticsDb = mongoose.createConnection(process.env.ANALYTICS_DB);
|
|
124
|
-
|
|
125
|
-
// Each resource uses its own adapter
|
|
126
|
-
const orderResource = defineResource({
|
|
127
|
-
name: 'order',
|
|
128
|
-
adapter: createMongooseAdapter({ model: OrderModel, repository: orderRepo }),
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const analyticsResource = defineResource({
|
|
132
|
-
name: 'analytics',
|
|
133
|
-
adapter: createMongooseAdapter({ model: AnalyticsModel, repository: analyticsRepo }),
|
|
134
|
-
});
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Manual Setup
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
import Fastify from 'fastify';
|
|
141
|
-
import mongoose from 'mongoose';
|
|
142
|
-
import { defineResource, createMongooseAdapter } from '@classytic/arc';
|
|
143
|
-
|
|
144
|
-
// Connect your database
|
|
145
|
-
await mongoose.connect('mongodb://localhost:27017/myapp');
|
|
146
|
-
|
|
147
|
-
const fastify = Fastify();
|
|
148
|
-
|
|
149
|
-
// Define and register resources
|
|
150
|
-
import { allowPublic, requireRoles } from '@classytic/arc';
|
|
37
|
+
import { defineResource, createMongooseAdapter, allowPublic, requireRoles } from '@classytic/arc';
|
|
151
38
|
|
|
152
39
|
const productResource = defineResource({
|
|
153
40
|
name: 'product',
|
|
154
|
-
adapter: createMongooseAdapter({
|
|
155
|
-
|
|
156
|
-
repository: productRepository,
|
|
157
|
-
}),
|
|
158
|
-
controller: productController, // optional; auto-created if omitted
|
|
159
|
-
presets: ['softDelete', 'slugLookup'],
|
|
41
|
+
adapter: createMongooseAdapter({ model: ProductModel, repository: productRepo }),
|
|
42
|
+
presets: ['softDelete', 'slugLookup', { name: 'multiTenant', tenantField: 'orgId' }],
|
|
160
43
|
permissions: {
|
|
161
44
|
list: allowPublic(),
|
|
162
45
|
get: allowPublic(),
|
|
@@ -164,766 +47,336 @@ const productResource = defineResource({
|
|
|
164
47
|
update: requireRoles(['admin']),
|
|
165
48
|
delete: requireRoles(['admin']),
|
|
166
49
|
},
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
await fastify.register(productResource.toPlugin());
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## Core Concepts
|
|
173
|
-
|
|
174
|
-
### Authentication
|
|
175
|
-
|
|
176
|
-
Arc provides **optional** built-in JWT authentication. You can:
|
|
177
|
-
|
|
178
|
-
1. **Use Arc's JWT auth** (default) - Simple, production-ready
|
|
179
|
-
2. **Replace with OAuth** - Google, Facebook, GitHub, etc.
|
|
180
|
-
3. **Use Passport.js** - 500+ authentication strategies
|
|
181
|
-
4. **Create custom auth** - Full control over authentication logic
|
|
182
|
-
5. **Mix multiple strategies** - JWT + API keys + OAuth
|
|
183
|
-
|
|
184
|
-
**Arc's auth is NOT mandatory.** Disable it and use any Fastify auth plugin:
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
import { createApp } from '@classytic/arc';
|
|
188
|
-
|
|
189
|
-
// Disable Arc's JWT auth
|
|
190
|
-
const app = await createApp({
|
|
191
|
-
auth: false, // Use your own auth strategy
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// Use @fastify/oauth2 for Google login
|
|
195
|
-
await app.register(require('@fastify/oauth2'), {
|
|
196
|
-
name: 'googleOAuth',
|
|
197
|
-
credentials: {
|
|
198
|
-
client: {
|
|
199
|
-
id: process.env.GOOGLE_CLIENT_ID,
|
|
200
|
-
secret: process.env.GOOGLE_CLIENT_SECRET,
|
|
201
|
-
},
|
|
202
|
-
auth: {
|
|
203
|
-
authorizeHost: 'https://accounts.google.com',
|
|
204
|
-
authorizePath: '/o/oauth2/v2/auth',
|
|
205
|
-
tokenHost: 'https://www.googleapis.com',
|
|
206
|
-
tokenPath: '/oauth2/v4/token',
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
startRedirectPath: '/auth/google',
|
|
210
|
-
callbackUri: 'http://localhost:8080/auth/google/callback',
|
|
211
|
-
scope: ['profile', 'email'],
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// OAuth callback - issue JWT
|
|
215
|
-
app.get('/auth/google/callback', async (request, reply) => {
|
|
216
|
-
const { token } = await app.googleOAuth.getAccessTokenFromAuthorizationCodeFlow(request);
|
|
217
|
-
|
|
218
|
-
// Fetch user info from Google
|
|
219
|
-
const userInfo = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
|
220
|
-
headers: { Authorization: `Bearer ${token.access_token}` },
|
|
221
|
-
}).then(r => r.json());
|
|
222
|
-
|
|
223
|
-
// Create user in your database
|
|
224
|
-
const user = await User.findOneAndUpdate(
|
|
225
|
-
{ email: userInfo.email },
|
|
226
|
-
{ email: userInfo.email, name: userInfo.name, googleId: userInfo.id },
|
|
227
|
-
{ upsert: true, new: true }
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
// Issue JWT using Arc's auth (or use sessions/cookies)
|
|
231
|
-
const jwtToken = app.jwt.sign({ _id: user._id, email: user.email });
|
|
232
|
-
|
|
233
|
-
return reply.send({ token: jwtToken, user });
|
|
234
|
-
});
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**See [examples/custom-auth-providers.ts](examples/custom-auth-providers.ts) for:**
|
|
238
|
-
- OAuth (Google, Facebook)
|
|
239
|
-
- Passport.js integration
|
|
240
|
-
- Custom authentication strategies
|
|
241
|
-
- SAML/SSO for enterprise
|
|
242
|
-
- Hybrid auth (JWT + API keys)
|
|
243
|
-
|
|
244
|
-
### Resources
|
|
245
|
-
|
|
246
|
-
A resource encapsulates model, repository, controller, and routes:
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
import { defineResource, createMongooseAdapter, allowPublic, requireRoles } from '@classytic/arc';
|
|
250
|
-
|
|
251
|
-
export default defineResource({
|
|
252
|
-
name: 'product',
|
|
253
|
-
adapter: createMongooseAdapter({
|
|
254
|
-
model: ProductModel,
|
|
255
|
-
repository: productRepository,
|
|
256
|
-
}),
|
|
257
|
-
controller: productController,
|
|
258
|
-
|
|
259
|
-
// Presets add common functionality
|
|
260
|
-
presets: [
|
|
261
|
-
'softDelete', // deletedAt field, restore endpoint
|
|
262
|
-
'slugLookup', // GET /products/:slug
|
|
263
|
-
'ownedByUser', // createdBy ownership checks
|
|
264
|
-
'multiTenant', // organizationId isolation
|
|
265
|
-
'tree', // Hierarchical data support
|
|
266
|
-
],
|
|
267
|
-
|
|
268
|
-
// Permission functions (NOT string arrays)
|
|
269
|
-
permissions: {
|
|
270
|
-
list: allowPublic(), // Public
|
|
271
|
-
get: allowPublic(), // Public
|
|
272
|
-
create: requireRoles(['admin', 'editor']), // Restricted
|
|
273
|
-
update: requireRoles(['admin', 'editor']),
|
|
274
|
-
delete: requireRoles(['admin']),
|
|
275
|
-
},
|
|
276
|
-
|
|
277
|
-
// Custom routes beyond CRUD
|
|
50
|
+
cache: { staleTime: 30, gcTime: 300, tags: ['catalog'] }, // QueryCache (opt-in)
|
|
278
51
|
additionalRoutes: [
|
|
279
|
-
{
|
|
280
|
-
method: 'GET',
|
|
281
|
-
path: '/featured',
|
|
282
|
-
handler: 'getFeatured', // Controller method name
|
|
283
|
-
permissions: allowPublic(), // Permission function
|
|
284
|
-
wrapHandler: true, // Arc context pattern (IRequestContext)
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
method: 'GET',
|
|
288
|
-
path: '/:id/download',
|
|
289
|
-
handler: 'downloadFile', // Fastify native handler
|
|
290
|
-
permissions: requireAuth(),
|
|
291
|
-
wrapHandler: false, // Native Fastify (request, reply)
|
|
292
|
-
},
|
|
52
|
+
{ method: 'GET', path: '/featured', handler: 'getFeatured', permissions: allowPublic(), wrapHandler: true },
|
|
293
53
|
],
|
|
294
54
|
});
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### Controllers
|
|
298
|
-
|
|
299
|
-
Extend BaseController for built-in security and CRUD:
|
|
300
|
-
|
|
301
|
-
```typescript
|
|
302
|
-
import { BaseController } from '@classytic/arc';
|
|
303
|
-
import type { IRequestContext, IControllerResponse } from '@classytic/arc';
|
|
304
|
-
import type { ISoftDeleteController, ISlugLookupController } from '@classytic/arc/presets';
|
|
305
|
-
|
|
306
|
-
// Type-safe controller with preset interfaces
|
|
307
|
-
class ProductController
|
|
308
|
-
extends BaseController<Product>
|
|
309
|
-
implements ISoftDeleteController<Product>, ISlugLookupController<Product>
|
|
310
|
-
{
|
|
311
|
-
constructor() {
|
|
312
|
-
super(productRepository);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Custom method - Arc context pattern
|
|
316
|
-
async getFeatured(req: IRequestContext): Promise<IControllerResponse> {
|
|
317
|
-
const { organizationId } = req;
|
|
318
|
-
|
|
319
|
-
const products = await this.repository.findAll({
|
|
320
|
-
filter: { isFeatured: true, organizationId },
|
|
321
|
-
});
|
|
322
55
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// Preset methods
|
|
327
|
-
async getBySlug(req: IRequestContext): Promise<IControllerResponse> {
|
|
328
|
-
const { slug } = req.params;
|
|
329
|
-
const product = await this.repository.getBySlug(slug);
|
|
330
|
-
|
|
331
|
-
if (!product) {
|
|
332
|
-
return { success: false, error: 'Product not found', status: 404 };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return { success: true, data: product };
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
**Preset Type Interfaces:** Arc exports TypeScript interfaces for each preset that requires controller methods:
|
|
341
|
-
|
|
342
|
-
- `ISoftDeleteController` - requires `getDeleted()` and `restore()`
|
|
343
|
-
- `ISlugLookupController` - requires `getBySlug()`
|
|
344
|
-
- `ITreeController` - requires `getTree()` and `getChildren()`
|
|
345
|
-
|
|
346
|
-
**Note:** Presets like `multiTenant`, `ownedByUser`, and `audited` don't require controller methods—they work via middleware.
|
|
347
|
-
|
|
348
|
-
### Request Context API
|
|
349
|
-
|
|
350
|
-
Controller methods receive `req: IRequestContext`:
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
interface IRequestContext {
|
|
354
|
-
params: Record<string, string>; // Route params: /users/:id
|
|
355
|
-
query: Record<string, unknown>; // Query string: ?page=1
|
|
356
|
-
body: unknown; // Request body
|
|
357
|
-
user: UserBase | null; // Authenticated user
|
|
358
|
-
headers: Record<string, string | undefined>; // Request headers
|
|
359
|
-
organizationId?: string; // Multi-tenant org ID
|
|
360
|
-
metadata?: Record<string, unknown>; // Custom data, _policyFilters, middleware context
|
|
361
|
-
}
|
|
56
|
+
// Auto-generates: GET /, GET /:id, POST /, PATCH /:id, DELETE /:id
|
|
57
|
+
// Plus preset routes: GET /deleted, POST /:id/restore, GET /slug/:slug
|
|
362
58
|
```
|
|
363
59
|
|
|
364
|
-
|
|
365
|
-
- `req.metadata` - Custom data from hooks, policies, or middleware
|
|
366
|
-
- `req.organizationId` - Set by `multiTenant` preset or org scope plugin
|
|
367
|
-
- `req.user` - Set by auth plugin, preserves original auth structure
|
|
368
|
-
|
|
369
|
-
### TypeScript Strict Mode
|
|
60
|
+
## Authentication
|
|
370
61
|
|
|
371
|
-
|
|
62
|
+
Auth uses a discriminated union — pick a `type`:
|
|
372
63
|
|
|
373
64
|
```typescript
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
interface Product {
|
|
378
|
-
_id: string;
|
|
379
|
-
name: string;
|
|
380
|
-
slug: string;
|
|
381
|
-
price: number;
|
|
382
|
-
deletedAt?: Date;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
class ProductController
|
|
386
|
-
extends BaseController<Product>
|
|
387
|
-
implements ISoftDeleteController<Product>, ISlugLookupController<Product>
|
|
388
|
-
{
|
|
389
|
-
async getBySlug(req: IRequestContext): Promise<IControllerResponse<Product>> {
|
|
390
|
-
const { slug } = req.params;
|
|
391
|
-
const product = await this.repository.getBySlug(slug);
|
|
65
|
+
// Arc JWT
|
|
66
|
+
auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET, expiresIn: '15m' } }
|
|
392
67
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
68
|
+
// Better Auth (recommended for SaaS with orgs)
|
|
69
|
+
import { createBetterAuthAdapter } from '@classytic/arc/auth';
|
|
70
|
+
auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth, orgContext: true }) }
|
|
396
71
|
|
|
397
|
-
|
|
398
|
-
|
|
72
|
+
// Custom plugin
|
|
73
|
+
auth: { type: 'custom', plugin: myAuthPlugin }
|
|
399
74
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return { success: true, data: products };
|
|
403
|
-
}
|
|
75
|
+
// Custom function
|
|
76
|
+
auth: { type: 'authenticator', authenticate: async (req, reply) => { ... } }
|
|
404
77
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const product = await this.repository.restore(id);
|
|
408
|
-
return { success: true, data: product };
|
|
409
|
-
}
|
|
410
|
-
}
|
|
78
|
+
// Disabled
|
|
79
|
+
auth: false
|
|
411
80
|
```
|
|
412
81
|
|
|
413
|
-
**
|
|
414
|
-
- Compile-time type checking
|
|
415
|
-
- IntelliSense autocomplete
|
|
416
|
-
- Safe refactoring
|
|
82
|
+
**Decorates:** `app.authenticate`, `app.optionalAuthenticate`, `app.authorize`
|
|
417
83
|
|
|
418
|
-
|
|
84
|
+
## Permissions
|
|
419
85
|
|
|
420
|
-
|
|
86
|
+
Function-based, composable:
|
|
421
87
|
|
|
422
|
-
**MongoDB with MongoKit:**
|
|
423
88
|
```typescript
|
|
424
|
-
import {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
89
|
+
import {
|
|
90
|
+
allowPublic, requireAuth, requireRoles, requireOwnership,
|
|
91
|
+
requireOrgMembership, requireOrgRole, allOf, anyOf, denyAll,
|
|
92
|
+
createDynamicPermissionMatrix,
|
|
93
|
+
} from '@classytic/arc';
|
|
94
|
+
|
|
95
|
+
permissions: {
|
|
96
|
+
list: allowPublic(),
|
|
97
|
+
get: requireAuth(),
|
|
98
|
+
create: requireRoles(['admin', 'editor']),
|
|
99
|
+
update: anyOf(requireOwnership('userId'), requireRoles(['admin'])),
|
|
100
|
+
delete: allOf(requireAuth(), requireRoles(['admin'])),
|
|
434
101
|
}
|
|
435
102
|
```
|
|
436
103
|
|
|
437
|
-
**
|
|
438
|
-
```typescript
|
|
439
|
-
import { PrismaRepository } from '@classytic/prismakit';
|
|
440
|
-
|
|
441
|
-
class ProductRepository extends PrismaRepository {
|
|
442
|
-
// Same interface, different database
|
|
443
|
-
}
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
## CLI Commands
|
|
447
|
-
|
|
448
|
-
```bash
|
|
449
|
-
# Generate resource scaffold
|
|
450
|
-
arc generate resource product --module catalog --presets softDelete,slugLookup
|
|
451
|
-
|
|
452
|
-
# Show all registered resources (loads from entry file)
|
|
453
|
-
arc introspect --entry ./src/index.js
|
|
454
|
-
|
|
455
|
-
# Export OpenAPI spec (loads from entry file)
|
|
456
|
-
arc docs ./docs/openapi.json --entry ./src/index.js
|
|
457
|
-
|
|
458
|
-
# Note: --entry flag loads your resource definitions into the registry
|
|
459
|
-
# Point it to the file that imports all your resources
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
## Environment Presets
|
|
463
|
-
|
|
464
|
-
### Production
|
|
465
|
-
- Info-level logging
|
|
466
|
-
- Strict CORS (must configure origin)
|
|
467
|
-
- Rate limiting: **100 req/min/IP** (configurable via `rateLimit.max` option)
|
|
468
|
-
- Helmet with CSP
|
|
469
|
-
- Health monitoring (under-pressure)
|
|
470
|
-
- All security plugins enabled
|
|
471
|
-
|
|
472
|
-
> **💡 Tip**: Default rate limit (100 req/min) may be conservative for high-traffic APIs. Adjust via:
|
|
473
|
-
> ```typescript
|
|
474
|
-
> createApp({ rateLimit: { max: 300, timeWindow: '1 minute' } })
|
|
475
|
-
> ```
|
|
476
|
-
|
|
477
|
-
> **Note**: Compression is not included due to known Fastify 5 stream issues. Use a reverse proxy (Nginx, Caddy) or CDN for response compression.
|
|
478
|
-
|
|
479
|
-
### Development
|
|
480
|
-
- Debug logging
|
|
481
|
-
- Permissive CORS
|
|
482
|
-
- Rate limiting: 1000 req/min (development-friendly)
|
|
483
|
-
- Relaxed security
|
|
484
|
-
|
|
485
|
-
### Testing
|
|
486
|
-
- Silent logging
|
|
487
|
-
- No CORS restrictions
|
|
488
|
-
- Rate limiting: disabled (test performance)
|
|
489
|
-
- Minimal security overhead
|
|
490
|
-
|
|
491
|
-
## Serverless Deployment
|
|
492
|
-
|
|
493
|
-
### AWS Lambda
|
|
494
|
-
|
|
495
|
-
```typescript
|
|
496
|
-
import { createLambdaHandler } from './index.factory.js';
|
|
497
|
-
|
|
498
|
-
export const handler = await createLambdaHandler();
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
### Google Cloud Run
|
|
104
|
+
**Field-level permissions:**
|
|
502
105
|
|
|
503
106
|
```typescript
|
|
504
|
-
import {
|
|
505
|
-
import { createServer } from 'http';
|
|
506
|
-
|
|
507
|
-
createServer(cloudRunHandler).listen(process.env.PORT || 8080);
|
|
508
|
-
```
|
|
107
|
+
import { fields } from '@classytic/arc';
|
|
509
108
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
109
|
+
fields: {
|
|
110
|
+
password: fields.hidden(),
|
|
111
|
+
salary: fields.visibleTo(['admin', 'hr']),
|
|
112
|
+
role: fields.writableBy(['admin']),
|
|
113
|
+
email: fields.redactFor(['viewer'], '***'),
|
|
114
|
+
}
|
|
516
115
|
```
|
|
517
116
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
### Test App Creation with In-Memory MongoDB
|
|
521
|
-
|
|
522
|
-
Arc's testing utilities now include **in-memory MongoDB by default** for 10x faster tests.
|
|
117
|
+
**Dynamic ACL (DB-managed):**
|
|
523
118
|
|
|
524
119
|
```typescript
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
describe('API Tests', () => {
|
|
529
|
-
let testApp: TestAppResult;
|
|
530
|
-
|
|
531
|
-
beforeAll(async () => {
|
|
532
|
-
// Creates app + starts in-memory MongoDB automatically
|
|
533
|
-
testApp = await createTestApp({
|
|
534
|
-
auth: { jwt: { secret: 'test-secret-32-chars-minimum-len' } },
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
// Connect your models to the in-memory DB
|
|
538
|
-
await mongoose.connect(testApp.mongoUri);
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
afterAll(async () => {
|
|
542
|
-
// Cleans up DB and closes app
|
|
543
|
-
await testApp.close();
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
test('GET /products', async () => {
|
|
547
|
-
const response = await testApp.app.inject({
|
|
548
|
-
method: 'GET',
|
|
549
|
-
url: '/products',
|
|
550
|
-
});
|
|
551
|
-
expect(response.statusCode).toBe(200);
|
|
552
|
-
});
|
|
120
|
+
const acl = createDynamicPermissionMatrix({
|
|
121
|
+
resolveRolePermissions: async (ctx) => aclService.getRoleMatrix(orgId),
|
|
122
|
+
cacheStore: new RedisCacheStore({ client: redis, prefix: 'acl:' }),
|
|
553
123
|
});
|
|
554
|
-
```
|
|
555
124
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
**Using External MongoDB:**
|
|
563
|
-
|
|
564
|
-
```typescript
|
|
565
|
-
const testApp = await createTestApp({
|
|
566
|
-
auth: { jwt: { secret: 'test-secret-32-chars-minimum-len' } },
|
|
567
|
-
useInMemoryDb: false,
|
|
568
|
-
mongoUri: 'mongodb://localhost:27017/test-db',
|
|
569
|
-
});
|
|
125
|
+
permissions: {
|
|
126
|
+
list: acl.canAction('product', 'read'),
|
|
127
|
+
create: acl.canAction('product', 'create'),
|
|
128
|
+
}
|
|
570
129
|
```
|
|
571
130
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
### Mock Factories
|
|
575
|
-
|
|
576
|
-
```typescript
|
|
577
|
-
import { createMockRepository, createDataFactory } from '@classytic/arc/testing';
|
|
578
|
-
|
|
579
|
-
// Mock repository
|
|
580
|
-
const mockRepo = createMockRepository({
|
|
581
|
-
findById: jest.fn().mockResolvedValue({ _id: '123', name: 'Test' }),
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// Data factory
|
|
585
|
-
const productFactory = createDataFactory({
|
|
586
|
-
name: (i) => `Product ${i}`,
|
|
587
|
-
price: (i) => 100 + i * 10,
|
|
588
|
-
isActive: () => true,
|
|
589
|
-
});
|
|
131
|
+
## Presets
|
|
590
132
|
|
|
591
|
-
|
|
592
|
-
```
|
|
133
|
+
Composable resource behaviors:
|
|
593
134
|
|
|
594
|
-
|
|
135
|
+
| Preset | Effect | Config |
|
|
136
|
+
|--------|--------|--------|
|
|
137
|
+
| `softDelete` | `GET /deleted`, `POST /:id/restore`, `deletedAt` field | `{ deletedField }` |
|
|
138
|
+
| `slugLookup` | `GET /slug/:slug` | `{ slugField }` |
|
|
139
|
+
| `tree` | `GET /tree`, `GET /:parent/children` | `{ parentField }` |
|
|
140
|
+
| `ownedByUser` | Auto-checks `createdBy` on update/delete | `{ ownerField }` |
|
|
141
|
+
| `multiTenant` | Auto-filters all queries by tenant | `{ tenantField }` |
|
|
142
|
+
| `audited` | Sets `createdBy`/`updatedBy` from user | — |
|
|
595
143
|
|
|
596
144
|
```typescript
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
describe('Product Repository', () => {
|
|
600
|
-
withTestDb((db) => {
|
|
601
|
-
it('should create product', async () => {
|
|
602
|
-
const product = await Product.create({ name: 'Test' });
|
|
603
|
-
expect(product.name).toBe('Test');
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
});
|
|
145
|
+
presets: ['softDelete', { name: 'multiTenant', tenantField: 'organizationId' }]
|
|
607
146
|
```
|
|
608
147
|
|
|
609
|
-
##
|
|
148
|
+
## QueryCache
|
|
610
149
|
|
|
611
|
-
|
|
612
|
-
import { createStateMachine } from '@classytic/arc/utils';
|
|
613
|
-
|
|
614
|
-
const orderStateMachine = createStateMachine('order', {
|
|
615
|
-
submit: {
|
|
616
|
-
from: ['draft'],
|
|
617
|
-
to: 'pending',
|
|
618
|
-
guard: ({ data }) => data.items.length > 0,
|
|
619
|
-
after: async ({ from, to, data }) => {
|
|
620
|
-
await sendNotification(data.userId, 'Order submitted');
|
|
621
|
-
},
|
|
622
|
-
},
|
|
623
|
-
approve: {
|
|
624
|
-
from: ['pending'],
|
|
625
|
-
to: 'approved',
|
|
626
|
-
},
|
|
627
|
-
ship: {
|
|
628
|
-
from: ['approved'],
|
|
629
|
-
to: 'shipped',
|
|
630
|
-
},
|
|
631
|
-
cancel: {
|
|
632
|
-
from: ['draft', 'pending'],
|
|
633
|
-
to: 'cancelled',
|
|
634
|
-
},
|
|
635
|
-
}, { trackHistory: true });
|
|
636
|
-
|
|
637
|
-
// Usage
|
|
638
|
-
orderStateMachine.can('submit', 'draft'); // true
|
|
639
|
-
orderStateMachine.assert('submit', 'draft'); // throws if invalid
|
|
640
|
-
orderStateMachine.getAvailableActions('pending'); // ['approve', 'cancel']
|
|
641
|
-
orderStateMachine.getHistory(); // Array of transitions
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
## Hooks System
|
|
150
|
+
TanStack Query-inspired server cache with stale-while-revalidate and auto-invalidation:
|
|
645
151
|
|
|
646
152
|
```typescript
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
//
|
|
650
|
-
hookRegistry.register('product', 'beforeCreate', async (context) => {
|
|
651
|
-
context.data.slug = slugify(context.data.name);
|
|
153
|
+
// Enable globally
|
|
154
|
+
const app = await createApp({
|
|
155
|
+
arcPlugins: { queryCache: true }, // Memory store by default
|
|
652
156
|
});
|
|
653
157
|
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
```typescript
|
|
664
|
-
import { definePolicy } from '@classytic/arc/policies';
|
|
665
|
-
|
|
666
|
-
const ownedByUserPolicy = definePolicy({
|
|
667
|
-
name: 'ownedByUser',
|
|
668
|
-
apply: async (query, req) => {
|
|
669
|
-
if (!req.user) throw new Error('Unauthorized');
|
|
670
|
-
query.filter.createdBy = req.user._id;
|
|
671
|
-
return query;
|
|
158
|
+
// Per-resource config
|
|
159
|
+
defineResource({
|
|
160
|
+
name: 'product',
|
|
161
|
+
cache: {
|
|
162
|
+
staleTime: 30, // seconds fresh
|
|
163
|
+
gcTime: 300, // seconds stale data kept (SWR window)
|
|
164
|
+
tags: ['catalog'],
|
|
165
|
+
invalidateOn: { 'category.*': ['catalog'] }, // cross-resource
|
|
672
166
|
},
|
|
673
167
|
});
|
|
674
|
-
|
|
675
|
-
// Apply in resource
|
|
676
|
-
export default defineResource({
|
|
677
|
-
name: 'document',
|
|
678
|
-
policies: [ownedByUserPolicy],
|
|
679
|
-
// ...
|
|
680
|
-
});
|
|
681
168
|
```
|
|
682
169
|
|
|
683
|
-
|
|
170
|
+
**How it works:**
|
|
171
|
+
- `GET` requests: cached with `x-cache: HIT | STALE | MISS` header
|
|
172
|
+
- `POST/PATCH/DELETE`: auto-bumps resource version, invalidating all cached queries
|
|
173
|
+
- Cross-resource: category mutation bumps `catalog` tag, invalidates product cache
|
|
174
|
+
- Multi-tenant safe: cache keys scoped by userId + orgId
|
|
684
175
|
|
|
685
|
-
|
|
686
|
-
import { eventPlugin } from '@classytic/arc/events';
|
|
176
|
+
**Runtime modes:**
|
|
687
177
|
|
|
688
|
-
|
|
178
|
+
| Mode | Store | Config |
|
|
179
|
+
|------|-------|--------|
|
|
180
|
+
| `memory` (default) | `MemoryCacheStore` (50 MiB budget) | Zero config |
|
|
181
|
+
| `distributed` | `RedisCacheStore` | `stores: { queryCache: new RedisCacheStore({ client: redis }) }` |
|
|
689
182
|
|
|
690
|
-
|
|
691
|
-
await fastify.events.publish('order.created', { orderId: '123', userId: '456' });
|
|
183
|
+
## BaseController
|
|
692
184
|
|
|
693
|
-
|
|
694
|
-
const unsubscribe = await fastify.events.subscribe('order.created', async (event) => {
|
|
695
|
-
await sendConfirmationEmail(event.payload.userId);
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
// Unsubscribe
|
|
699
|
-
unsubscribe();
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
## Introspection
|
|
185
|
+
Override only what you need:
|
|
703
186
|
|
|
704
187
|
```typescript
|
|
705
|
-
import {
|
|
706
|
-
|
|
707
|
-
// Get all resources
|
|
708
|
-
const resources = resourceRegistry.getAll();
|
|
188
|
+
import { BaseController } from '@classytic/arc';
|
|
189
|
+
import type { IRequestContext, IControllerResponse } from '@classytic/arc';
|
|
709
190
|
|
|
710
|
-
|
|
711
|
-
|
|
191
|
+
class ProductController extends BaseController<Product> {
|
|
192
|
+
constructor() { super(productRepo); }
|
|
712
193
|
|
|
713
|
-
|
|
714
|
-
const
|
|
715
|
-
|
|
194
|
+
async getFeatured(req: IRequestContext): Promise<IControllerResponse> {
|
|
195
|
+
const products = await this.repository.getAll({ filters: { isFeatured: true } });
|
|
196
|
+
return { success: true, data: products };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
716
199
|
```
|
|
717
200
|
|
|
718
|
-
##
|
|
719
|
-
|
|
720
|
-
### OpenTelemetry Distributed Tracing
|
|
721
|
-
|
|
722
|
-
```typescript
|
|
723
|
-
import { tracingPlugin } from '@classytic/arc/plugins';
|
|
724
|
-
|
|
725
|
-
await fastify.register(tracingPlugin, {
|
|
726
|
-
serviceName: 'my-api',
|
|
727
|
-
exporterUrl: 'http://localhost:4318/v1/traces',
|
|
728
|
-
sampleRate: 0.1, // Trace 10% of requests
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
// Custom spans
|
|
732
|
-
import { createSpan } from '@classytic/arc/plugins';
|
|
733
|
-
|
|
734
|
-
return createSpan(req, 'expensiveOperation', async (span) => {
|
|
735
|
-
span.setAttribute('userId', req.user._id);
|
|
736
|
-
return await processData();
|
|
737
|
-
});
|
|
738
|
-
```
|
|
201
|
+
## Events
|
|
739
202
|
|
|
740
|
-
|
|
203
|
+
Domain event pub/sub with pluggable transports. The factory auto-registers `eventPlugin` — no manual setup needed:
|
|
741
204
|
|
|
742
205
|
```typescript
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
await
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
{
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
critical: true,
|
|
752
|
-
},
|
|
753
|
-
{
|
|
754
|
-
name: 'redis',
|
|
755
|
-
check: async () => redisClient.ping() === 'PONG',
|
|
756
|
-
critical: true,
|
|
206
|
+
// createApp() registers eventPlugin automatically (default: MemoryEventTransport)
|
|
207
|
+
// Transport is sourced from stores.events if provided
|
|
208
|
+
const app = await createApp({
|
|
209
|
+
stores: { events: new RedisEventTransport(redis) }, // optional, defaults to memory
|
|
210
|
+
arcPlugins: {
|
|
211
|
+
events: { // event plugin config (default: true)
|
|
212
|
+
logEvents: true,
|
|
213
|
+
retry: { maxRetries: 3, backoffMs: 1000 },
|
|
757
214
|
},
|
|
758
|
-
|
|
215
|
+
},
|
|
759
216
|
});
|
|
760
217
|
|
|
761
|
-
|
|
218
|
+
await app.events.publish('order.created', { orderId: '123' });
|
|
219
|
+
await app.events.subscribe('order.*', async (event) => { ... });
|
|
762
220
|
```
|
|
763
221
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
```typescript
|
|
767
|
-
import { CircuitBreaker } from '@classytic/arc/utils';
|
|
768
|
-
|
|
769
|
-
const stripeBreaker = new CircuitBreaker(
|
|
770
|
-
async (amount) => stripe.charges.create({ amount }),
|
|
771
|
-
{
|
|
772
|
-
failureThreshold: 5,
|
|
773
|
-
resetTimeout: 30000,
|
|
774
|
-
fallback: async (amount) => queuePayment(amount),
|
|
775
|
-
}
|
|
776
|
-
);
|
|
777
|
-
|
|
778
|
-
const charge = await stripeBreaker.call(1000);
|
|
779
|
-
```
|
|
222
|
+
CRUD events (`product.created`, `product.updated`, `product.deleted`) emit automatically.
|
|
780
223
|
|
|
781
|
-
|
|
224
|
+
## Factory — createApp()
|
|
782
225
|
|
|
783
226
|
```typescript
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
)
|
|
227
|
+
const app = await createApp({
|
|
228
|
+
preset: 'production', // production | development | testing | edge
|
|
229
|
+
runtime: 'memory', // memory (default) | distributed (requires Redis)
|
|
230
|
+
auth: { type: 'jwt', jwt: { secret } },
|
|
231
|
+
cors: { origin: ['https://myapp.com'] },
|
|
232
|
+
helmet: true, // false to disable
|
|
233
|
+
rateLimit: { max: 100 }, // false to disable
|
|
234
|
+
arcPlugins: {
|
|
235
|
+
events: true, // event plugin (default: true, false to disable)
|
|
236
|
+
emitEvents: true, // CRUD event emission (default: true)
|
|
237
|
+
queryCache: true, // server cache (default: false)
|
|
238
|
+
sse: true, // server-sent events (default: false)
|
|
239
|
+
caching: true, // ETag + Cache-Control (default: false)
|
|
794
240
|
},
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
);
|
|
241
|
+
stores: { // required when runtime: 'distributed'
|
|
242
|
+
events: new RedisEventTransport({ client: redis }),
|
|
243
|
+
cache: new RedisCacheStore({ client: redis }),
|
|
244
|
+
queryCache: new RedisCacheStore({ client: redis, prefix: 'arc:qc:' }),
|
|
800
245
|
},
|
|
801
246
|
});
|
|
802
|
-
|
|
803
|
-
const runner = new MigrationRunner(mongoose.connection.db);
|
|
804
|
-
await runner.up([productV2]);
|
|
805
247
|
```
|
|
806
248
|
|
|
807
|
-
**
|
|
808
|
-
|
|
809
|
-
## Battle-Tested Deployments
|
|
249
|
+
**Arc plugins defaults:**
|
|
810
250
|
|
|
811
|
-
|
|
251
|
+
| Plugin | Default | Status |
|
|
252
|
+
|--------|---------|--------|
|
|
253
|
+
| `events` | `true` | opt-out — registers `eventPlugin` (provides `fastify.events`) |
|
|
254
|
+
| `emitEvents` | `true` | opt-out — CRUD operations emit domain events |
|
|
255
|
+
| `requestId` | `true` | opt-out |
|
|
256
|
+
| `health` | `true` | opt-out |
|
|
257
|
+
| `gracefulShutdown` | `true` | opt-out |
|
|
258
|
+
| `caching` | `false` | opt-in — ETag + Cache-Control headers |
|
|
259
|
+
| `queryCache` | `false` | opt-in — TanStack Query-inspired server cache |
|
|
260
|
+
| `sse` | `false` | opt-in — Server-Sent Events streaming |
|
|
812
261
|
|
|
813
|
-
|
|
262
|
+
| Preset | Logging | Rate Limit | Security |
|
|
263
|
+
|--------|---------|------------|----------|
|
|
264
|
+
| production | info | 100/min | full |
|
|
265
|
+
| development | debug | 1000/min | relaxed |
|
|
266
|
+
| testing | silent | disabled | minimal |
|
|
267
|
+
| edge | warn | disabled | none (API GW handles) |
|
|
814
268
|
|
|
815
|
-
|
|
816
|
-
|-------------|--------|-------|
|
|
817
|
-
| Docker | ✅ Tested | Use Node 18+ Alpine images |
|
|
818
|
-
| Kubernetes | ✅ Tested | Health checks + graceful shutdown built-in |
|
|
819
|
-
| AWS Lambda | ✅ Tested | Use `@fastify/aws-lambda` adapter |
|
|
820
|
-
| Google Cloud Run | ✅ Tested | Auto-scales, health checks work OOTB |
|
|
821
|
-
| Vercel Serverless | ✅ Tested | Use serverless functions adapter |
|
|
822
|
-
| Bare Metal / VPS | ✅ Tested | PM2 or systemd recommended |
|
|
823
|
-
| Railway / Render | ✅ Tested | Works with zero config |
|
|
269
|
+
## Real-Time
|
|
824
270
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
Before deploying to production:
|
|
271
|
+
SSE and WebSocket with fail-closed auth (throws at registration if auth missing):
|
|
828
272
|
|
|
829
273
|
```typescript
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
// 1. Validate environment variables at startup
|
|
833
|
-
validateEnv({
|
|
834
|
-
JWT_SECRET: { required: true, min: 32 },
|
|
835
|
-
DATABASE_URL: { required: true },
|
|
836
|
-
NODE_ENV: { required: true, values: ['production', 'staging'] },
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
// 2. Use production environment preset
|
|
274
|
+
// SSE — via factory
|
|
840
275
|
const app = await createApp({
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
// 3. Configure CORS properly (never use origin: true)
|
|
844
|
-
cors: {
|
|
845
|
-
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
|
|
846
|
-
credentials: true,
|
|
847
|
-
},
|
|
848
|
-
|
|
849
|
-
// 4. Adjust rate limits for your traffic
|
|
850
|
-
rateLimit: {
|
|
851
|
-
max: 300, // Requests per window
|
|
852
|
-
timeWindow: '1 minute',
|
|
853
|
-
ban: 10, // Ban after 10 violations
|
|
854
|
-
},
|
|
855
|
-
|
|
856
|
-
// 5. Enable health checks
|
|
857
|
-
healthCheck: true,
|
|
276
|
+
arcPlugins: { sse: { path: '/events', requireAuth: true, orgScoped: true } },
|
|
277
|
+
});
|
|
858
278
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
279
|
+
// WebSocket — separate plugin
|
|
280
|
+
import { websocketPlugin } from '@classytic/arc/integrations/websocket';
|
|
281
|
+
await app.register(websocketPlugin, {
|
|
282
|
+
auth: true, // fail-closed: throws if authenticate not registered
|
|
283
|
+
resources: ['product', 'order'],
|
|
284
|
+
roomPolicy: (client, room) => ['product', 'order'].includes(room),
|
|
285
|
+
maxMessageBytes: 16384, // 16KB message size cap
|
|
286
|
+
maxSubscriptionsPerClient: 100, // prevent resource exhaustion
|
|
864
287
|
});
|
|
865
288
|
|
|
866
|
-
//
|
|
867
|
-
|
|
868
|
-
|
|
289
|
+
// EventGateway — unified SSE + WebSocket with shared config
|
|
290
|
+
import { eventGatewayPlugin } from '@classytic/arc/integrations/event-gateway';
|
|
291
|
+
await app.register(eventGatewayPlugin, {
|
|
292
|
+
auth: true, orgScoped: true,
|
|
293
|
+
roomPolicy: (client, room) => allowedRooms.includes(room),
|
|
294
|
+
sse: { path: '/api/events', patterns: ['order.*'] },
|
|
295
|
+
ws: { path: '/ws', resources: ['product', 'order'] },
|
|
296
|
+
});
|
|
869
297
|
```
|
|
870
298
|
|
|
871
|
-
|
|
299
|
+
## Integrations
|
|
872
300
|
|
|
873
|
-
|
|
301
|
+
All separate subpath imports — only loaded when used:
|
|
874
302
|
|
|
875
303
|
```typescript
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
mongodb: {
|
|
879
|
-
primary: process.env.MONGODB_PRIMARY,
|
|
880
|
-
replicas: process.env.MONGODB_REPLICAS?.split(','),
|
|
881
|
-
readPreference: 'nearest',
|
|
882
|
-
},
|
|
304
|
+
// Job Queue (BullMQ)
|
|
305
|
+
import { jobsPlugin, defineJob } from '@classytic/arc/integrations/jobs';
|
|
883
306
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
enabled: true,
|
|
887
|
-
serviceName: `api-${process.env.REGION}`,
|
|
888
|
-
exporter: 'zipkin',
|
|
889
|
-
},
|
|
890
|
-
});
|
|
891
|
-
```
|
|
307
|
+
// WebSocket (room-based, CRUD auto-broadcast)
|
|
308
|
+
import { websocketPlugin } from '@classytic/arc/integrations/websocket';
|
|
892
309
|
|
|
893
|
-
|
|
310
|
+
// EventGateway (unified SSE + WebSocket)
|
|
311
|
+
import { eventGatewayPlugin } from '@classytic/arc/integrations/event-gateway';
|
|
894
312
|
|
|
895
|
-
|
|
313
|
+
// Streamline Workflows
|
|
314
|
+
import { streamlinePlugin } from '@classytic/arc/integrations/streamline';
|
|
896
315
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
- **Memory**: ~50MB base + ~0.5MB per 1000 requests
|
|
900
|
-
- **Connections**: Handles 10,000+ concurrent connections
|
|
901
|
-
- **Database**: Tested with 1M+ documents, sub-10ms queries with proper indexes
|
|
316
|
+
// Audit Trail
|
|
317
|
+
import { auditPlugin } from '@classytic/arc/audit';
|
|
902
318
|
|
|
903
|
-
|
|
319
|
+
// Idempotency (exactly-once mutations)
|
|
320
|
+
import { idempotencyPlugin } from '@classytic/arc/idempotency';
|
|
904
321
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
2. **Enable Memory Monitoring** - Detect leaks early in production
|
|
909
|
-
3. **Use Testing Preset** - Minimal overhead for test suites
|
|
910
|
-
4. **Apply Indexes** - Always index query fields in models
|
|
911
|
-
5. **Use Lean Queries** - Repository returns plain objects by default
|
|
912
|
-
6. **Rate Limiting** - Protect endpoints from abuse
|
|
913
|
-
7. **Validate Early** - Use environment validator at startup
|
|
914
|
-
8. **Distributed Tracing** - Track requests across services (5ms overhead)
|
|
915
|
-
9. **Circuit Breakers** - Prevent cascading failures (<1ms overhead)
|
|
916
|
-
10. **Health Checks** - K8s-compatible liveness/readiness probes
|
|
322
|
+
// OpenTelemetry Tracing
|
|
323
|
+
import { tracingPlugin } from '@classytic/arc/plugins/tracing';
|
|
324
|
+
```
|
|
917
325
|
|
|
918
|
-
##
|
|
326
|
+
## CLI
|
|
919
327
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
328
|
+
```bash
|
|
329
|
+
arc init my-api --mongokit --better-auth --ts # Scaffold project
|
|
330
|
+
arc generate resource product # Generate resource files
|
|
331
|
+
arc docs ./openapi.json --entry ./dist/index.js # Export OpenAPI
|
|
332
|
+
arc introspect --entry ./dist/index.js # Show resources
|
|
333
|
+
arc doctor # Health check
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Subpath Imports
|
|
337
|
+
|
|
338
|
+
| Import | Purpose |
|
|
339
|
+
|--------|---------|
|
|
340
|
+
| `@classytic/arc` | Core: `defineResource`, `BaseController`, permissions, errors |
|
|
341
|
+
| `@classytic/arc/factory` | `createApp()`, presets |
|
|
342
|
+
| `@classytic/arc/cache` | `MemoryCacheStore`, `RedisCacheStore`, `QueryCache` |
|
|
343
|
+
| `@classytic/arc/auth` | Auth plugin, Better Auth adapter, session manager |
|
|
344
|
+
| `@classytic/arc/events` | Event plugin, memory transport |
|
|
345
|
+
| `@classytic/arc/events/redis` | Redis event transport |
|
|
346
|
+
| `@classytic/arc/events/redis-stream` | Redis Streams transport |
|
|
347
|
+
| `@classytic/arc/plugins` | Health, graceful shutdown, request ID, SSE, caching |
|
|
348
|
+
| `@classytic/arc/plugins/tracing` | OpenTelemetry |
|
|
349
|
+
| `@classytic/arc/permissions` | All permission functions |
|
|
350
|
+
| `@classytic/arc/scope` | Request scope helpers (`isMember`, `isElevated`, `getOrgId`) |
|
|
351
|
+
| `@classytic/arc/org` | Organization module |
|
|
352
|
+
| `@classytic/arc/hooks` | Lifecycle hooks |
|
|
353
|
+
| `@classytic/arc/presets` | Preset functions + interfaces |
|
|
354
|
+
| `@classytic/arc/audit` | Audit trail |
|
|
355
|
+
| `@classytic/arc/idempotency` | Idempotency |
|
|
356
|
+
| `@classytic/arc/policies` | Policy engine |
|
|
357
|
+
| `@classytic/arc/schemas` | TypeBox helpers |
|
|
358
|
+
| `@classytic/arc/utils` | Errors, circuit breaker, state machine, query parser |
|
|
359
|
+
| `@classytic/arc/testing` | Test utilities, mocks, in-memory DB |
|
|
360
|
+
| `@classytic/arc/migrations` | Schema migrations |
|
|
361
|
+
| `@classytic/arc/integrations/jobs` | BullMQ job queue |
|
|
362
|
+
| `@classytic/arc/integrations/websocket` | WebSocket |
|
|
363
|
+
| `@classytic/arc/integrations/event-gateway` | Unified SSE + WebSocket gateway |
|
|
364
|
+
| `@classytic/arc/integrations/streamline` | Workflow orchestration |
|
|
365
|
+
| `@classytic/arc/docs` | OpenAPI generation |
|
|
366
|
+
| `@classytic/arc/cli` | CLI commands |
|
|
367
|
+
|
|
368
|
+
## Documentation
|
|
369
|
+
|
|
370
|
+
- [Setup](docs/getting-started/setup.md) — Project setup
|
|
371
|
+
- [Core Concepts](docs/getting-started/core.md) — Resources, controllers, adapters
|
|
372
|
+
- [Authentication](docs/getting-started/auth.md) — JWT, Better Auth, custom auth
|
|
373
|
+
- [Permissions](docs/getting-started/permissions.md) — RBAC, ABAC, field-level
|
|
374
|
+
- [Presets](docs/getting-started/presets.md) — softDelete, multiTenant, tree, etc.
|
|
375
|
+
- [Organizations](docs/getting-started/org.md) — Multi-tenant SaaS
|
|
376
|
+
- [Factory](docs/production-ops/factory.md) — createApp() and environment presets
|
|
377
|
+
- [Events](docs/production-ops/events.md) — Domain events and transports
|
|
378
|
+
- [Plugins](docs/production-ops/plugins.md) — Health, caching, SSE, tracing
|
|
379
|
+
- [Hooks](docs/framework-extension/hooks.md) — Lifecycle hooks
|
|
927
380
|
|
|
928
381
|
## License
|
|
929
382
|
|