@classytic/arc 2.11.3 → 2.13.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/README.md +27 -18
- package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
- package/dist/EventTransport-CT_52aWU.d.mts +34 -0
- package/dist/EventTransport-DLWoUMHy.mjs +103 -0
- package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
- package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/audit.d.mts +199 -0
- package/dist/auth/audit.mjs +288 -0
- package/dist/auth/index.d.mts +5 -5
- package/dist/auth/index.mjs +117 -191
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
- package/dist/buildHandler-olo-gt94.mjs +610 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/describe.d.mts +89 -13
- package/dist/cli/commands/describe.mjs +56 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +147 -48
- package/dist/cli/commands/init.d.mts +13 -0
- package/dist/cli/commands/init.mjs +237 -112
- package/dist/cli/commands/introspect.mjs +8 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/core-D72ia0EH.mjs +1399 -0
- package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
- package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
- package/dist/{createApp-BFxtdKy6.mjs → createApp-XX2-N0Yd.mjs} +31 -27
- package/dist/defineEvent-D5h7EvAx.mjs +188 -0
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
- package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
- package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
- package/dist/errors-j4aJm1Wg.mjs +184 -0
- package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-CaKTYkYM.mjs} +35 -137
- package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-qXpqTebY.d.mts} +57 -7
- package/dist/events/index.d.mts +164 -5
- package/dist/events/index.mjs +133 -209
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +204 -31
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-C8Y0XLAu.d.mts → fields-COhcH3fk.d.mts} +23 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +1 -20
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-BYCqHCVu.d.mts → index-BTqLEvhu.d.mts} +164 -4
- package/dist/{index-6u4_Gg6G.d.mts → index-BtW7qYwa.d.mts} +661 -281
- package/dist/{index-BdXnTPRj.d.mts → index-Ds61mrJE.d.mts} +50 -4
- package/dist/{index-DdQ3O9Pg.d.mts → index-Dz5IKsrE.d.mts} +360 -219
- package/dist/index.d.mts +6 -7
- package/dist/index.mjs +9 -10
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- 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/integrations/streamline.d.mts +60 -11
- package/dist/integrations/streamline.mjs +75 -85
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +2 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +2 -2
- package/dist/migrations/index.d.mts +23 -3
- package/dist/migrations/index.mjs +0 -7
- package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
- package/dist/{openapi-BGUn7Ki1.mjs → openapi-CiOMVW1p.mjs} +143 -13
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
- package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +18 -33
- package/dist/plugins/index.mjs +33 -13
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +5 -5
- package/dist/presets/filesUpload.mjs +6 -9
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -2
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +6 -8
- package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
- package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
- package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
- package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
- package/dist/redis-stream-D6HzR1Z_.d.mts +232 -0
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
- package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-C5coh64w.mjs} +224 -71
- package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
- package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
- package/dist/schemas/index.d.mts +100 -30
- package/dist/schemas/index.mjs +86 -29
- package/dist/scim/index.d.mts +264 -0
- package/dist/scim/index.mjs +963 -0
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +4 -4
- package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
- package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -8
- package/dist/testing/index.mjs +16 -24
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-BH7dEGvU.d.mts → types-BvqwCCSx.d.mts} +77 -29
- package/dist/{types-tgR4Pt8F.d.mts → types-CTYvcwHe.d.mts} +195 -1
- package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
- package/dist/{types-9beEMe25.d.mts → types-DQHFc8PM.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +5 -5
- package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
- package/dist/{versioning-M9lNLhO8.d.mts → versioning-DTTvc80y.d.mts} +1 -1
- package/package.json +24 -34
- package/skills/arc/SKILL.md +521 -785
- package/skills/arc/references/agent-auth.md +238 -0
- package/skills/arc/references/api-reference.md +187 -0
- package/skills/arc/references/auth.md +354 -7
- package/skills/arc/references/enterprise-auth.md +94 -0
- package/skills/arc/references/events.md +8 -6
- package/skills/arc/references/mcp.md +2 -2
- package/skills/arc/references/multi-tenancy.md +11 -2
- package/skills/arc/references/production.md +10 -9
- package/skills/arc/references/scim.md +247 -0
- package/skills/arc/references/testing.md +1 -1
- package/skills/arc-code-review/SKILL.md +141 -0
- package/skills/arc-code-review/references/anti-patterns.md +911 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
- package/skills/arc-code-review/references/migration-recipes.md +700 -0
- package/skills/arc-code-review/references/mongokit-migration.md +386 -0
- package/skills/arc-code-review/references/scaffolding.md +230 -0
- package/skills/arc-code-review/references/severity.md +127 -0
- package/dist/EventTransport-CfVEGaEl.d.mts +0 -293
- package/dist/adapters/index.d.mts +0 -3
- package/dist/adapters/index.mjs +0 -2
- package/dist/adapters-D0tT2Tyo.mjs +0 -949
- package/dist/auth/mongoose.d.mts +0 -191
- package/dist/auth/mongoose.mjs +0 -73
- package/dist/core-DnUsRpuX.mjs +0 -1049
- package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
- package/dist/errorHandler-Co3lnVmJ.d.mts +0 -114
- package/dist/errors-D5c-5BJL.mjs +0 -232
- package/dist/index-BbMrcvGp.d.mts +0 -362
- package/dist/redis-stream-CM8TXTix.d.mts +0 -110
- /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
- /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
- /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
- /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
- /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
- /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
- /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
- /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
- /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
- /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{pluralize-BneOJkpi.mjs → pluralize-DQgqgifU.mjs} +0 -0
- /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
- /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
- /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
- /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
- /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
- /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
- /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# Arc Cheatsheet — what arc provides, in one page
|
|
2
|
+
|
|
3
|
+
A condensed map of arc's capabilities so you can spot, during audit, what the team is *missing* vs hand-rolling. For deep API, see the existing `skills/arc/SKILL.md` and its `references/`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Boot order (FIXED — don't reorder)
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
1. Arc core (security, auth, events)
|
|
11
|
+
2. plugins() ← user infra (DB, docs, webhooks)
|
|
12
|
+
3. bootstrap[] ← domain init (engines, singletons)
|
|
13
|
+
4. resources factory ← if async: resolved here, after bootstrap
|
|
14
|
+
5. resources[] ← register each resource
|
|
15
|
+
6. afterResources() ← post-registration wiring
|
|
16
|
+
7. onReady / onClose ← Fastify lifecycle hooks
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
When auditing: top-level `await ensureCatalogEngine()` in a `*.resource.ts` file = lifecycle violation. Use `resources: async (fastify) => [...]`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## `createApp()` essentials
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const app = await createApp({
|
|
27
|
+
preset: 'production', // production | development | testing | edge
|
|
28
|
+
runtime: 'memory', // memory (default) | distributed
|
|
29
|
+
auth: { type: 'jwt', jwt: { secret } }, // | 'betterAuth' | 'custom' | false
|
|
30
|
+
resources: [resource1, resource2], // OR async (fastify) => [...]
|
|
31
|
+
arcPlugins: { events: true, queryCache: false, sse: false, caching: true },
|
|
32
|
+
stores: { events: ..., queryCache: ..., idempotency: ... }, // distributed only
|
|
33
|
+
cors: { origin: [...], credentials: true },
|
|
34
|
+
helmet: true, rateLimit: { max: 100 },
|
|
35
|
+
resourcePrefix: '/api/v1',
|
|
36
|
+
bootstrap: [async () => { ... }],
|
|
37
|
+
afterResources: async (app) => { ... },
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## `defineResource()` — full surface
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
defineResource({
|
|
47
|
+
name: 'product', // required
|
|
48
|
+
adapter: createMongooseAdapter({ model, repository, schemaGenerator? }), // required
|
|
49
|
+
controller?: new MyController(), // optional — auto-built if omitted
|
|
50
|
+
permissions: { // required
|
|
51
|
+
list, get, create, update, delete: PermissionCheck,
|
|
52
|
+
},
|
|
53
|
+
presets?: ['softDelete', { name: 'multiTenant', tenantField: 'orgId' }, ...],
|
|
54
|
+
schemaOptions?: {
|
|
55
|
+
fieldRules: { [field]: FieldRuleEntry },
|
|
56
|
+
query: { allowedPopulate, allowedLookups, filterableFields },
|
|
57
|
+
},
|
|
58
|
+
routes?: [{ method, path, handler, permissions, raw?, mcp?, summary? }],
|
|
59
|
+
actions?: { [name]: { handler, permissions?, schema?, mcp?, description? } },
|
|
60
|
+
actionPermissions?: PermissionCheck,
|
|
61
|
+
hooks?: { beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete },
|
|
62
|
+
events?: { created: {}, updated: {}, deleted: {}, [custom]: { description?, schema? } },
|
|
63
|
+
cache?: { staleTime, gcTime, tags, invalidateOn?, list?, byId? },
|
|
64
|
+
routeGuards?: [preHandlerFn],
|
|
65
|
+
middlewares?: { create: [multipartBody(...)], ... },
|
|
66
|
+
pipe?: { create: [guard(...), transform(...), intercept(...)] },
|
|
67
|
+
rateLimit?: { max, timeWindow },
|
|
68
|
+
tenantField?: string | false,
|
|
69
|
+
idField?: string,
|
|
70
|
+
prefix?: string, skipGlobalPrefix?: boolean,
|
|
71
|
+
queryParser?: QueryParserInterface,
|
|
72
|
+
onFieldWriteDenied?: 'reject' | 'strip',
|
|
73
|
+
audit?: boolean | { operations: [...] },
|
|
74
|
+
displayName?: string, module?: string,
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `FieldRuleEntry` flags
|
|
79
|
+
|
|
80
|
+
| Flag | Effect |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `systemManaged` | Strip from body, drop from `required[]`. Framework stamps the value. |
|
|
83
|
+
| `preserveForElevated` | Elevated admins keep the field on ingest (cross-tenant writes). |
|
|
84
|
+
| `immutable` / `immutableAfterCreate` | Omit from update body. |
|
|
85
|
+
| `optional` | Strip from `required[]` without touching `properties`. |
|
|
86
|
+
| `nullable` | Widen JSON-Schema `type` to include null. |
|
|
87
|
+
| `hidden` | Block from response projection + OpenAPI. |
|
|
88
|
+
| `minLength`/`maxLength`/`min`/`max`/`pattern`/`enum` | Map to AJV + OpenAPI. |
|
|
89
|
+
| `description` | OpenAPI `description`. |
|
|
90
|
+
|
|
91
|
+
Mongoose model-level constraints take precedence; `fieldRules` supplements.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Permissions — combinators
|
|
96
|
+
|
|
97
|
+
From `@classytic/arc`:
|
|
98
|
+
```typescript
|
|
99
|
+
allowPublic() // always grant
|
|
100
|
+
requireAuth() // any authenticated user
|
|
101
|
+
requireRoles(['admin', 'editor'])// platform OR org roles
|
|
102
|
+
requireOwnership('userId') // row-level: filters → { userId: scope.userId }
|
|
103
|
+
requireOrgMembership() // member | service | elevated
|
|
104
|
+
requireOrgRole(['admin']) // human-only role within org
|
|
105
|
+
requireTeamMembership()
|
|
106
|
+
requireServiceScope('jobs:bulk-write') // OAuth-style API-key scopes
|
|
107
|
+
requireScopeContext('branchId') // app-defined dimensions
|
|
108
|
+
requireOrgInScope(targetId) // parent-child org hierarchy
|
|
109
|
+
allOf(check1, check2, ...)
|
|
110
|
+
anyOf(check1, check2, ...)
|
|
111
|
+
not(check)
|
|
112
|
+
when(condition, ifTrue, ifFalse)
|
|
113
|
+
denyAll()
|
|
114
|
+
createDynamicPermissionMatrix({ resolveRolePermissions, cacheStore })
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Convenience bundles: `publicRead()`, `publicReadAdminWrite()`, `adminOnly()`, `ownerWithAdminBypass()`.
|
|
118
|
+
|
|
119
|
+
`PermissionCheck` returns `boolean | { granted, reason?, filters?, scope? }`. `filters` flow into the repo query (row-level ABAC). `scope` stamps attributes downstream.
|
|
120
|
+
|
|
121
|
+
### Field-level
|
|
122
|
+
```typescript
|
|
123
|
+
import { fields } from '@classytic/arc';
|
|
124
|
+
fieldRules: {
|
|
125
|
+
password: fields.hidden(),
|
|
126
|
+
salary: fields.visibleTo(['admin', 'hr']),
|
|
127
|
+
role: fields.writableBy(['admin']),
|
|
128
|
+
email: fields.redactFor(['viewer'], '***'),
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## RequestScope — five kinds
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
type RequestScope =
|
|
138
|
+
| { kind: 'public' }
|
|
139
|
+
| { kind: 'authenticated'; userId?; userRoles? }
|
|
140
|
+
| { kind: 'member'; userId?; userRoles; organizationId; orgRoles; teamId?; context?; ancestorOrgIds? }
|
|
141
|
+
| { kind: 'service'; clientId; organizationId; scopes?; context?; ancestorOrgIds? }
|
|
142
|
+
| { kind: 'elevated'; userId?; organizationId?; elevatedBy; context?; ancestorOrgIds? };
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Always read via accessors from `@classytic/arc/scope`:**
|
|
146
|
+
```typescript
|
|
147
|
+
isPublic, isAuthenticated, isMember, isService, isElevated, hasOrgAccess,
|
|
148
|
+
getUserId, getUserRoles, getOrgId, getOrgRoles, getTeamId, getClientId,
|
|
149
|
+
getServiceScopes, getScopeContext, getScopeContextMap,
|
|
150
|
+
getAncestorOrgIds, isOrgInScope, getRequestScope,
|
|
151
|
+
requireUserId, requireClientId, // throw 401 (UnauthorizedError) if absent
|
|
152
|
+
requireOrgId, requireTeamId, // throw 403 (OrgRequiredError) if absent
|
|
153
|
+
createTenantKeyGenerator,
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Helper | `member` | `service` | `elevated` |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| `requireOrgMembership()` | ✅ | ✅ | ✅ |
|
|
159
|
+
| `requireOrgRole(roles)` | role match | ❌ deny | ✅ bypass |
|
|
160
|
+
| `requireServiceScope(scopes)` | ❌ | scope match | ✅ bypass |
|
|
161
|
+
| `requireScopeContext(...)` | key match | key match | ✅ bypass |
|
|
162
|
+
| `requireTeamMembership()` | `teamId` set | n/a | ✅ bypass |
|
|
163
|
+
| `requireOrgInScope(target)` | target in chain | target in chain | ✅ bypass |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Presets
|
|
168
|
+
|
|
169
|
+
| Preset | Routes added | Config |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `softDelete` | `GET /deleted`, `POST /:id/restore` | `{ deletedField }` |
|
|
172
|
+
| `slugLookup` | `GET /slug/:slug` | `{ slugField }` |
|
|
173
|
+
| `tree` | `GET /tree`, `GET /:parent/children` | `{ parentField }` |
|
|
174
|
+
| `ownedByUser` | none (middleware) | `{ ownerField }` |
|
|
175
|
+
| `multiTenant` | none (middleware) | `{ tenantField }` OR `{ tenantFields: TenantFieldSpec[] }` |
|
|
176
|
+
| `audited` | none (middleware) | — |
|
|
177
|
+
| `bulk` | `POST/PATCH/DELETE /bulk` | `{ operations?, maxCreateItems? }` |
|
|
178
|
+
| `filesUpload` | `POST /upload`, `GET /:id`, `DELETE /:id` | `{ storage, sanitizeFilename?, allowedMimeTypes?, maxFileSize? }` |
|
|
179
|
+
| `search` | `POST /search`, `/search-similar`, `/embed` | `{ repository?, search?, similar?, embed?, routes? }` |
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
presets: ['softDelete', { name: 'multiTenant', tenantField: 'organizationId' }]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Hooks
|
|
188
|
+
|
|
189
|
+
Inline (per-resource):
|
|
190
|
+
```typescript
|
|
191
|
+
hooks: {
|
|
192
|
+
beforeCreate: async (ctx) => { /* ctx.data, ctx.user, ctx.meta */ },
|
|
193
|
+
afterCreate: async (ctx) => { ... },
|
|
194
|
+
beforeUpdate: async (ctx) => { /* ctx.meta.existing has the pre-image */ },
|
|
195
|
+
afterUpdate: async (ctx) => { ... },
|
|
196
|
+
beforeDelete: async (ctx) => { ... },
|
|
197
|
+
afterDelete: async (ctx) => { ... },
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
App-level (cross-resource):
|
|
202
|
+
```typescript
|
|
203
|
+
import { createHookSystem, beforeCreate, afterUpdate } from '@classytic/arc/hooks';
|
|
204
|
+
|
|
205
|
+
const hooks = createHookSystem();
|
|
206
|
+
beforeCreate(hooks, 'product', async (ctx) => { ctx.data.slug = slugify(ctx.data.name); });
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Pipeline (for finer control):
|
|
210
|
+
```typescript
|
|
211
|
+
import { guard, transform, intercept } from '@classytic/arc/pipeline';
|
|
212
|
+
|
|
213
|
+
pipe: {
|
|
214
|
+
create: [
|
|
215
|
+
guard('verified', async (ctx) => ctx.user?.verified === true),
|
|
216
|
+
transform('inject', async (ctx) => { ctx.body.createdBy = ctx.user._id; }),
|
|
217
|
+
],
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Events
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
events: { created: {}, updated: {}, deleted: {}, custom: { description, schema } }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
CRUD events auto-emit. Custom: `await req.fastify.events.publish(eventType, payload)`. Subscribe: `app.events.subscribe('order.*', handler)`.
|
|
230
|
+
|
|
231
|
+
**Transports:** `MemoryEventTransport` · `RedisEventTransport` (pub/sub fire-and-forget) · `RedisStreamTransport` (durable, at-least-once, consumer groups, DLQ) · `EventOutbox` (transactional outbox).
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Cache (QueryCache)
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
cache: {
|
|
239
|
+
staleTime: 30, gcTime: 300, tags: ['catalog'],
|
|
240
|
+
invalidateOn: { 'category.*': ['catalog'] },
|
|
241
|
+
list: { staleTime: 60 },
|
|
242
|
+
byId: { staleTime: 10 },
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
Modes: `memory` (default) | `distributed` (`stores.queryCache: RedisCacheStore`). Response: `x-cache: HIT | STALE | MISS`.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## CLI
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
arc init my-api --mongokit --jwt --ts # scaffold (also: --custom, --better-auth, --multi)
|
|
253
|
+
arc generate resource product # generate resource files
|
|
254
|
+
arc generate resource product --mcp # + MCP tools file
|
|
255
|
+
arc generate mcp analytics # standalone MCP tools file
|
|
256
|
+
arc docs ./openapi.json --entry ./dist/index.js # emit OpenAPI
|
|
257
|
+
arc introspect --entry ./dist/index.js # list registered resources
|
|
258
|
+
arc describe product # detail a resource's routes/actions/permissions
|
|
259
|
+
arc doctor # diagnose env
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`.arcrc`: project config used by `arc generate`. Set `"mcp": true` to always emit `.mcp.ts` alongside resources.
|
|
263
|
+
|
|
264
|
+
Generated layout:
|
|
265
|
+
```
|
|
266
|
+
src/resources/{name}/
|
|
267
|
+
{name}.model.ts # Mongoose schema (with --mongokit)
|
|
268
|
+
{name}.repository.ts # Repository class (mongokit Repository)
|
|
269
|
+
{name}.resource.ts # defineResource() config
|
|
270
|
+
{name}.mcp.ts # (optional) custom MCP tools
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Naming: kebab input (`org-profile`) → PascalCase class (`OrgProfile`), camelCase var (`orgProfile`), kebab files.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## MCP
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
import { mcpPlugin } from '@classytic/arc/mcp';
|
|
281
|
+
|
|
282
|
+
await app.register(mcpPlugin, {
|
|
283
|
+
resources: [productResource, orderResource],
|
|
284
|
+
auth: false, // | getAuth() | custom function
|
|
285
|
+
exclude: ['credential'],
|
|
286
|
+
overrides: { product: { operations: ['list', 'get'] } },
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Stateful (server-initiated messages)
|
|
290
|
+
await app.register(mcpPlugin, { resources, stateful: true, sessionTtlMs: 600000 });
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Auto-generates 5 CRUD tools per resource + custom routes + actions. Permissions and field rules carry through. Connect via `claude mcp add --transport http my-api http://localhost:3000/mcp`.
|
|
294
|
+
|
|
295
|
+
Custom tools alongside resources: co-locate `order.mcp.ts`, wire via `extraTools: [...]`. AI SDK bridge: `buildMcpToolsFromBridges([bridge])`.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Adapters
|
|
300
|
+
|
|
301
|
+
In arc 2.12, every kit-specific adapter ships from its kit's `/adapter` subpath. Arc has zero kit-bound adapters. The cross-framework contract lives in `@classytic/repo-core/adapter`.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// Mongoose — from mongokit
|
|
305
|
+
import { createMongooseAdapter } from '@classytic/mongokit/adapter';
|
|
306
|
+
import { buildCrudSchemasFromModel, Repository } from '@classytic/mongokit';
|
|
307
|
+
|
|
308
|
+
const adapter = createMongooseAdapter({
|
|
309
|
+
model: ProductModel,
|
|
310
|
+
repository: new Repository(ProductModel),
|
|
311
|
+
schemaGenerator: buildCrudSchemasFromModel,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Drizzle — from sqlitekit
|
|
315
|
+
import { createDrizzleAdapter } from '@classytic/sqlitekit/adapter';
|
|
316
|
+
import { buildCrudSchemasFromTable } from '@classytic/sqlitekit';
|
|
317
|
+
|
|
318
|
+
// Prisma — from prismakit
|
|
319
|
+
import { createPrismaAdapter } from '@classytic/prismakit/adapter';
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Custom adapter: implement `DataAdapter` / `MinimalRepo<TDoc>` from `@classytic/repo-core/adapter` (5-method floor). Any kit (mongokit, sqlitekit, prismakit, future pgkit, custom) plugs in identically. `RepositoryLike<TDoc> = MinimalRepo<TDoc> & Partial<StandardRepo<TDoc>>` — arc feature-detects optional methods at call sites. Arc re-exports `RepositoryLike`; the rest of the contract types come from `@classytic/repo-core/adapter` directly.
|
|
323
|
+
|
|
324
|
+
| Plugin | Required methods on repo |
|
|
325
|
+
|---|---|
|
|
326
|
+
| `auditPlugin` | `create`, `findAll` |
|
|
327
|
+
| `idempotencyPlugin` | `getOne`, `deleteMany`, `findOneAndUpdate` |
|
|
328
|
+
| `EventOutbox` | `create`, `getOne`, `findAll`, `deleteMany`, `findOneAndUpdate` |
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Plugins (`@classytic/arc/plugins`)
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import {
|
|
336
|
+
healthPlugin, gracefulShutdownPlugin, ssePlugin,
|
|
337
|
+
metricsPlugin, versioningPlugin,
|
|
338
|
+
} from '@classytic/arc/plugins';
|
|
339
|
+
import { tracingPlugin } from '@classytic/arc/plugins/tracing';
|
|
340
|
+
import { auditPlugin } from '@classytic/arc/audit';
|
|
341
|
+
import { idempotencyPlugin } from '@classytic/arc/idempotency';
|
|
342
|
+
import { jobsPlugin } from '@classytic/arc/integrations/jobs';
|
|
343
|
+
import { websocketPlugin } from '@classytic/arc/integrations/websocket';
|
|
344
|
+
import { eventGatewayPlugin } from '@classytic/arc/integrations/event-gateway';
|
|
345
|
+
import { webhookPlugin } from '@classytic/arc/integrations/webhooks';
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Subpath imports (tree-shakeable)
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { defineResource, BaseController, allowPublic, requireRoles } from '@classytic/arc';
|
|
354
|
+
import { createApp, loadResources } from '@classytic/arc/factory';
|
|
355
|
+
import { MemoryCacheStore, RedisCacheStore, QueryCache } from '@classytic/arc/cache';
|
|
356
|
+
import { createBetterAuthAdapter } from '@classytic/arc/auth';
|
|
357
|
+
import { eventPlugin, EventOutbox } from '@classytic/arc/events';
|
|
358
|
+
import { RedisEventTransport } from '@classytic/arc/events/redis';
|
|
359
|
+
import { mcpPlugin, defineTool } from '@classytic/arc/mcp';
|
|
360
|
+
import { bulkPreset, multiTenantPreset } from '@classytic/arc/presets';
|
|
361
|
+
import { isMember, getUserId, getOrgId, hasOrgAccess } from '@classytic/arc/scope';
|
|
362
|
+
import { createTestApp, expectArc } from '@classytic/arc/testing';
|
|
363
|
+
import { multipartBody } from '@classytic/arc/middleware';
|
|
364
|
+
import { defineGuard, withCompensation, CircuitBreaker, createStateMachine } from '@classytic/arc/utils';
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Audit signal: a project importing only from the `@classytic/arc` root barrel is probably under-using subpath features (caching, scope accessors, presets, MCP, testing harness).
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Non-negotiable conventions (mirror in client projects)
|
|
372
|
+
|
|
373
|
+
1. No `console.log` in `src/` (except `cli/`) — use logger.
|
|
374
|
+
2. No `mongoose`/`drizzle-orm`/`@prisma/client` imports anywhere in the host outside the host's adapter wiring file. Every kit-specific adapter factory (`createMongooseAdapter` / `createDrizzleAdapter` / `createPrismaAdapter`) MUST come from the kit's `/adapter` subpath, never from `@classytic/arc` — the `@classytic/arc/adapters` subpath was removed in arc 2.12.
|
|
375
|
+
3. No `any` — use `unknown`. No `@ts-ignore` — fix the type.
|
|
376
|
+
4. No default exports in `src/` (knip enforces in arc; recommend in clients).
|
|
377
|
+
5. Always read `request.user` via guard or use `@classytic/arc/scope` accessors.
|
|
378
|
+
6. Always use `req.rawBody` for `verifySignature(...)`, never parsed body.
|
|
379
|
+
7. Set headers in `onRequest` or `preSerialization`, never `onSend`.
|
|
380
|
+
8. `request.user: Record<string, unknown> | undefined` — required property, NOT optional.
|