@classytic/arc 2.7.1 → 2.7.7
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 +14 -2
- package/dist/{HookSystem-D7lfx--K.mjs → HookSystem-BNYKnrXF.mjs} +3 -2
- package/dist/adapters/index.d.mts +2 -2
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +1 -1
- package/dist/audit/mongodb.d.mts +1 -1
- package/dist/audit/mongodb.mjs +1 -1
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +2 -2
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-CCw3YX0g.mjs → betterAuthOpenApi-EkPaMWNM.mjs} +1 -1
- package/dist/cache/index.d.mts +2 -2
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/{core-BWekSEju.mjs → core-B_zEeA2b.mjs} +1 -1
- package/dist/{createApp-B_nvKNAQ.mjs → createApp-D7e77m8C.mjs} +18 -7
- package/dist/{defineResource-DZzyl4a4.mjs → defineResource-BW2dMCu9.mjs} +1 -6
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/dynamic/index.d.mts +2 -2
- package/dist/dynamic/index.mjs +1 -1
- package/dist/{errorHandler-DXUttWEO.mjs → errorHandler-CH8wk1eD.mjs} +16 -1
- package/dist/{errorHandler-COa51ho_.d.mts → errorHandler-pCpEtNd7.d.mts} +46 -2
- package/dist/{eventPlugin-DsaNNXzZ.mjs → eventPlugin-B6U_nCFU.mjs} +3 -2
- package/dist/{eventPlugin-BgLxJkIB.d.mts → eventPlugin-CdvUoUna.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +1 -1
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +7 -6
- 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/mongodb.d.mts +1 -1
- package/dist/idempotency/mongodb.mjs +1 -3
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-BYpRGXif.d.mts → index-B0extFr4.d.mts} +3 -3
- package/dist/{index-KXM8_JmQ.d.mts → index-BjShrzoj.d.mts} +3 -3
- package/dist/{index-StgFaQKD.d.mts → index-C9eYNjGR.d.mts} +1 -1
- package/dist/index.d.mts +8 -7
- package/dist/index.mjs +3 -3
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +8 -5
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +39 -7
- package/dist/integrations/streamline.mjs +106 -4
- package/dist/integrations/webhooks.d.mts +58 -1
- package/dist/integrations/webhooks.mjs +78 -7
- package/dist/integrations/websocket.d.mts +7 -1
- package/dist/integrations/websocket.mjs +7 -1
- package/dist/{interface-Dwzqt4mn.d.mts → interface-B91alUzq.d.mts} +4 -4
- package/dist/{mongodb-Bq90j-Uj.d.mts → mongodb-B7zupyck.d.mts} +1 -1
- package/dist/{mongodb-DdyYlIXg.d.mts → mongodb-Cgu9F1Nd.d.mts} +1 -1
- package/dist/{openapi-C5UhIeWu.mjs → openapi-D7Z7VODz.mjs} +1 -1
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +3 -3
- package/dist/plugins/index.d.mts +52 -5
- package/dist/plugins/index.mjs +6 -5
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/policies/index.d.mts +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/{queryCachePlugin-Bw8XyJpX.d.mts → queryCachePlugin-Ckl71mkc.d.mts} +1 -1
- package/dist/{redis-CyCntzTO.d.mts → redis-3TQxm2VZ.d.mts} +1 -1
- package/dist/{redis-stream-We_Ucl9-.d.mts → redis-stream-Dag5LFa9.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/replyHelpers-uDUIYh7u.mjs +40 -0
- package/dist/{resourceToTools-CkVSSzKg.mjs → resourceToTools-BJkoQoUP.mjs} +11 -5
- package/dist/rpc/index.d.mts +1 -1
- package/dist/{schemaConverter-0TyONAwM.mjs → schemaConverter-Y5EejTnJ.mjs} +1 -4
- package/dist/scope/index.d.mts +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-DPsC0taJ.d.mts → types-B4BNthET.d.mts} +1 -1
- package/dist/{types-ClmkMDK1.d.mts → types-C5g2oRC7.d.mts} +18 -2
- package/dist/{types-D0qf0Mf4.d.mts → types-CKB47kiu.d.mts} +48 -9
- package/dist/utils/index.d.mts +3 -3
- package/dist/utils/index.mjs +1 -1
- package/package.json +9 -5
- package/skills/arc/SKILL.md +62 -1
- package/skills/arc/references/integrations.md +32 -7
- package/skills/arc/references/mcp.md +31 -7
- package/skills/arc/references/production.md +69 -0
- /package/dist/{EventTransport-CUpRK_Lg.d.mts → EventTransport-C4VheKeC.d.mts} +0 -0
- /package/dist/{circuitBreaker-DwxrljLB.d.mts → circuitBreaker-BBPDt-J_.d.mts} +0 -0
- /package/dist/{elevation-Dm-HTBCt.d.mts → elevation-D7WK0RXq.d.mts} +0 -0
- /package/dist/{errors-CCSsMpXE.d.mts → errors-BS6lZvWy.d.mts} +0 -0
- /package/dist/{externalPaths-Dg7OLsKo.d.mts → externalPaths-iba7jD3d.d.mts} +0 -0
- /package/dist/{fields-CYuLMJPD.d.mts → fields-D4nMDqnK.d.mts} +0 -0
- /package/dist/{interface-CnluRL4_.d.mts → interface-CG7oRZjX.d.mts} +0 -0
- /package/dist/{interface-B9rHWPxD.d.mts → interface-CSbZdv_3.d.mts} +0 -0
- /package/dist/{mongodb-mlgxkYI3.mjs → mongodb-B7X7P1P8.mjs} +0 -0
- /package/dist/{pluralize-COpOVar8.mjs → pluralize-Dckfq6US.mjs} +0 -0
- /package/dist/{sessionManager-IW4sbIea.d.mts → sessionManager-CEo9jwPI.d.mts} +0 -0
- /package/dist/{sse-Bp3dabF1.mjs → sse-6W0hjVS_.mjs} +0 -0
- /package/dist/{tracing-65B51Dw3.d.mts → tracing-DEqdGkr-.d.mts} +0 -0
- /package/dist/{types-CNEbix8T.d.mts → types--D3vvfdt.d.mts} +0 -0
- /package/dist/{versioning-aUUVziBY.mjs → versioning-CdBbFefk.mjs} +0 -0
package/skills/arc/SKILL.md
CHANGED
|
@@ -8,7 +8,7 @@ description: |
|
|
|
8
8
|
Triggers: arc, fastify resource, defineResource, createApp, BaseController, arc preset,
|
|
9
9
|
arc auth, arc events, arc jobs, arc websocket, arc mcp, arc plugin, arc testing, arc cli,
|
|
10
10
|
arc permissions, arc hooks, arc pipeline, arc factory, arc cache, arc QueryCache.
|
|
11
|
-
version: 2.7.
|
|
11
|
+
version: 2.7.3
|
|
12
12
|
license: MIT
|
|
13
13
|
metadata:
|
|
14
14
|
author: Classytic
|
|
@@ -748,14 +748,26 @@ Connect Claude CLI: `claude mcp add --transport http my-api http://localhost:300
|
|
|
748
748
|
**Auth** — three modes, user chooses: `false` | `getAuth()` (Better Auth OAuth 2.1) | custom function:
|
|
749
749
|
|
|
750
750
|
```typescript
|
|
751
|
+
// Human user auth
|
|
751
752
|
auth: async (headers) => {
|
|
752
753
|
if (headers['x-api-key'] !== process.env.MCP_KEY) return null;
|
|
753
754
|
return { userId: 'bot', organizationId: 'org-1', roles: ['admin'] };
|
|
754
755
|
},
|
|
756
|
+
|
|
757
|
+
// Service account / machine-to-machine (produces kind: "service" scope)
|
|
758
|
+
auth: async (headers) => ({
|
|
759
|
+
clientId: 'ingestion-pipeline',
|
|
760
|
+
organizationId: 'org-1',
|
|
761
|
+
scopes: ['read:products', 'write:events'],
|
|
762
|
+
}),
|
|
755
763
|
```
|
|
756
764
|
|
|
765
|
+
`auth: false` → `ctx.user` is `null`, `scope.kind` is `"public"`. Permission guards like `!!ctx.user` correctly block anonymous callers.
|
|
766
|
+
|
|
757
767
|
**Guards** for custom tools: `guard(requireAuth, requireOrg, requireRole('admin'), handler)`
|
|
758
768
|
|
|
769
|
+
**Service scope**: When `clientId` is set in auth result, MCP produces `kind: "service"` RequestScope — works with `requireServiceScope()`, `getClientId()`, `getServiceScopes()`. No synthetic userId needed for machine principals.
|
|
770
|
+
|
|
759
771
|
**Multi-tenancy**: `organizationId` from auth flows into BaseController org-scoping automatically.
|
|
760
772
|
|
|
761
773
|
**Permission filters**: `PermissionResult.filters` from resource permissions flow into MCP tools — same as REST. Define once, works everywhere:
|
|
@@ -881,6 +893,55 @@ additionalRoutes: [{ preAuth: [(req) => { req.headers.authorization = `Bearer ${
|
|
|
881
893
|
additionalRoutes: [{ streamResponse: true, handler: async (req, reply) => reply.send(stream) }]
|
|
882
894
|
```
|
|
883
895
|
|
|
896
|
+
## DX Helpers (v2.7.3)
|
|
897
|
+
|
|
898
|
+
**Reply helpers** — consistent response envelopes (opt-in via `createApp({ replyHelpers: true })`):
|
|
899
|
+
|
|
900
|
+
```typescript
|
|
901
|
+
return reply.ok({ name: 'MacBook' }); // → 200 { success: true, data: {...} }
|
|
902
|
+
return reply.ok(product, 201); // → 201 { success: true, data: {...} }
|
|
903
|
+
return reply.fail('Not found', 404); // → 404 { success: false, error: '...' }
|
|
904
|
+
return reply.fail(['err1', 'err2'], 422); // → 422 { success: false, errors: [...] }
|
|
905
|
+
return reply.paginated({ docs, total, page, limit });
|
|
906
|
+
return reply.stream(csvReadable, { contentType: 'text/csv', filename: 'export.csv' });
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
**Error mappers** — class-based domain error → HTTP response (in `errorHandler` options):
|
|
910
|
+
|
|
911
|
+
```typescript
|
|
912
|
+
const app = await createApp({
|
|
913
|
+
errorHandler: {
|
|
914
|
+
errorMappers: [{
|
|
915
|
+
type: AccountingError,
|
|
916
|
+
toResponse: (err) => ({ status: err.status, code: err.code, message: err.message }),
|
|
917
|
+
}],
|
|
918
|
+
},
|
|
919
|
+
});
|
|
920
|
+
// Handlers just throw — Arc catches and maps automatically
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
**BigInt serialization** — opt-in via `createApp({ serializeBigInt: true })`. Converts BigInt → Number in all JSON responses.
|
|
924
|
+
|
|
925
|
+
**Multipart body middleware** — opt-in file upload for CRUD routes:
|
|
926
|
+
|
|
927
|
+
```typescript
|
|
928
|
+
import { multipartBody } from '@classytic/arc/middleware';
|
|
929
|
+
|
|
930
|
+
defineResource({
|
|
931
|
+
name: 'product',
|
|
932
|
+
adapter,
|
|
933
|
+
middlewares: { create: [multipartBody({ allowedMimeTypes: ['image/png', 'image/jpeg'], maxFileSize: 5 * 1024 * 1024 })] },
|
|
934
|
+
hooks: {
|
|
935
|
+
'before:create': async (data) => {
|
|
936
|
+
if (data._files?.image) { data.imageUrl = await uploadToS3(data._files.image); delete data._files; }
|
|
937
|
+
return data;
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
});
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
`multipartBody()` is a no-op for JSON requests — safe to always add.
|
|
944
|
+
|
|
884
945
|
## Subpath Imports
|
|
885
946
|
|
|
886
947
|
```typescript
|
|
@@ -297,7 +297,7 @@ All optional, gracefully degrade:
|
|
|
297
297
|
import { webhookPlugin } from '@classytic/arc/integrations/webhooks';
|
|
298
298
|
```
|
|
299
299
|
|
|
300
|
-
Fastify plugin that auto-dispatches Arc events to customer webhook endpoints with HMAC-SHA256 signing, delivery logging, and pluggable persistence.
|
|
300
|
+
Fastify plugin that auto-dispatches Arc events to customer webhook endpoints with HMAC-SHA256 signing, delivery logging, bounded concurrency, and pluggable persistence.
|
|
301
301
|
|
|
302
302
|
### Setup
|
|
303
303
|
|
|
@@ -309,6 +309,7 @@ await fastify.register(webhookPlugin, {
|
|
|
309
309
|
store: myMongoWebhookStore, // implements WebhookStore { getAll, save, remove }
|
|
310
310
|
timeout: 5000, // delivery timeout (default: 10000ms)
|
|
311
311
|
maxLogEntries: 500, // ring buffer cap (default: 1000)
|
|
312
|
+
concurrency: 10, // max parallel deliveries per event (default: 5)
|
|
312
313
|
});
|
|
313
314
|
```
|
|
314
315
|
|
|
@@ -338,21 +339,45 @@ await app.events.publish('order.created', { orderId: '123' });
|
|
|
338
339
|
// Body: { type, payload, meta }
|
|
339
340
|
```
|
|
340
341
|
|
|
341
|
-
|
|
342
|
+
Deliveries run with bounded concurrency (default: 5) — one slow endpoint won't block the rest. Set `concurrency: 1` for sequential delivery.
|
|
342
343
|
|
|
343
|
-
|
|
344
|
+
### HMAC Signing & Verification
|
|
345
|
+
|
|
346
|
+
**Outbound** — every delivery is signed with the subscription's secret:
|
|
344
347
|
|
|
345
348
|
```
|
|
346
349
|
x-webhook-signature: sha256=a1b2c3...
|
|
347
350
|
```
|
|
348
351
|
|
|
349
|
-
|
|
352
|
+
**Inbound** — verify with `verifySignature()` (timing-safe, never throws):
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import { verifySignature } from '@classytic/arc/integrations/webhooks';
|
|
356
|
+
|
|
357
|
+
fastify.post('/webhooks/incoming', async (req, reply) => {
|
|
358
|
+
const sig = req.headers['x-webhook-signature'] as string;
|
|
359
|
+
if (!verifySignature(req.rawBody, secret, sig)) {
|
|
360
|
+
return reply.status(401).send({ error: 'Invalid signature' });
|
|
361
|
+
}
|
|
362
|
+
// handle event via req.headers['x-webhook-event']
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Accepts `string | Buffer` body, `string | undefined` signature. Configurable for non-Arc senders:
|
|
367
|
+
|
|
350
368
|
```typescript
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
369
|
+
// GitHub (same prefix, same algorithm — works with defaults)
|
|
370
|
+
verifySignature(body, secret, req.headers['x-hub-signature-256']);
|
|
371
|
+
|
|
372
|
+
// Custom algorithm / bare hex
|
|
373
|
+
verifySignature(body, secret, req.headers['x-custom-sig'], {
|
|
374
|
+
prefix: '', // bare hex, no prefix
|
|
375
|
+
algorithm: 'sha512', // non-default algorithm
|
|
376
|
+
});
|
|
354
377
|
```
|
|
355
378
|
|
|
379
|
+
**Note:** `req.rawBody` requires `fastify-raw-body` — JSON re-serialization breaks HMAC since field ordering differs.
|
|
380
|
+
|
|
356
381
|
### Delivery Log
|
|
357
382
|
|
|
358
383
|
```typescript
|
|
@@ -139,7 +139,7 @@ Arc doesn't enforce an auth strategy. You choose what fits.
|
|
|
139
139
|
await app.register(mcpPlugin, { resources, auth: false });
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
All tools open.
|
|
142
|
+
All tools open. `ctx.user` is `null`, `scope.kind` is `"public"`. Permission guards like `!!ctx.user` correctly block — anonymous callers cannot bypass auth checks.
|
|
143
143
|
|
|
144
144
|
### 2. Better Auth OAuth 2.1 (production SaaS)
|
|
145
145
|
|
|
@@ -175,13 +175,27 @@ type McpAuthResolver = (headers: Record<string, string | undefined>) =>
|
|
|
175
175
|
Promise<McpAuthResult | null> | McpAuthResult | null;
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
Return `
|
|
178
|
+
Return `McpAuthResult` to allow. Return `null` to reject (401).
|
|
179
|
+
|
|
180
|
+
**`McpAuthResult` fields:**
|
|
181
|
+
- `userId?` — human user ID (optional for machine principals)
|
|
182
|
+
- `organizationId?` — org scope
|
|
183
|
+
- `roles?` / `orgRoles?` — user roles
|
|
184
|
+
- `clientId?` — set this to produce `kind: "service"` scope (machine-to-machine)
|
|
185
|
+
- `scopes?` — OAuth scopes for service accounts
|
|
179
186
|
|
|
180
187
|
```typescript
|
|
181
|
-
// API key
|
|
188
|
+
// Human user — API key
|
|
182
189
|
auth: async (headers) => {
|
|
183
190
|
if (headers['x-api-key'] !== process.env.MCP_API_KEY) return null;
|
|
184
|
-
return { userId: '
|
|
191
|
+
return { userId: 'alice', organizationId: 'org-123', roles: ['admin'] };
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// Machine principal — service account (no userId needed)
|
|
195
|
+
auth: async (headers) => {
|
|
196
|
+
const key = headers['x-service-key'];
|
|
197
|
+
if (key !== process.env.SVC_KEY) return null;
|
|
198
|
+
return { clientId: 'ingestion-pipeline', organizationId: 'org-123', scopes: ['write:events'] };
|
|
185
199
|
},
|
|
186
200
|
|
|
187
201
|
// Gateway-validated JWT (token already verified upstream)
|
|
@@ -191,9 +205,6 @@ auth: async (headers) => {
|
|
|
191
205
|
return userId ? { userId, organizationId: orgId } : null;
|
|
192
206
|
},
|
|
193
207
|
|
|
194
|
-
// Static org (trusted internal network)
|
|
195
|
-
auth: async () => ({ userId: 'internal', organizationId: 'org-main' }),
|
|
196
|
-
|
|
197
208
|
// Bearer token with custom validation
|
|
198
209
|
auth: async (headers) => {
|
|
199
210
|
const token = headers['authorization']?.replace('Bearer ', '');
|
|
@@ -203,6 +214,19 @@ auth: async (headers) => {
|
|
|
203
214
|
},
|
|
204
215
|
```
|
|
205
216
|
|
|
217
|
+
### Service Scope (machine-to-machine)
|
|
218
|
+
|
|
219
|
+
When `clientId` is present in the auth result, Arc produces `kind: "service"` RequestScope:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
auth resolver returns { clientId: 'pipeline-v2', organizationId: 'org-a', scopes: ['read:all'] }
|
|
223
|
+
→ buildRequestContext sets _scope: { kind: 'service', clientId: 'pipeline-v2', organizationId: 'org-a', scopes: ['read:all'] }
|
|
224
|
+
→ ctx.user is null (machine principals don't masquerade as users)
|
|
225
|
+
→ isService(scope), getClientId(scope), getServiceScopes(scope) all work
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
When `userId` is present (without `clientId`), Arc produces `kind: "member"` or `kind: "authenticated"` as before.
|
|
229
|
+
|
|
206
230
|
### Multi-Tenancy
|
|
207
231
|
|
|
208
232
|
The `organizationId` from auth flows into BaseController's org-scoping automatically:
|
|
@@ -267,6 +267,75 @@ import { errorHandlerPlugin } from '@classytic/arc/plugins';
|
|
|
267
267
|
// Auto-registered by createApp()
|
|
268
268
|
```
|
|
269
269
|
|
|
270
|
+
### Error Mappers (v2.7.3)
|
|
271
|
+
|
|
272
|
+
Class-based domain error → HTTP response mapping. Register once, handlers just throw:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
class AccountingError extends Error {
|
|
276
|
+
constructor(message: string, public status: number, public code: string) { super(message); }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const app = await createApp({
|
|
280
|
+
errorHandler: {
|
|
281
|
+
errorMappers: [{
|
|
282
|
+
type: AccountingError,
|
|
283
|
+
toResponse: (err) => ({ status: err.status, code: err.code, message: err.message }),
|
|
284
|
+
}],
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
// Now handlers just throw — Arc catches and maps automatically
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Mappers are checked via `instanceof` before all other error classification. Multiple mappers supported — first match wins.
|
|
291
|
+
|
|
292
|
+
## Reply Helpers (v2.7.3)
|
|
293
|
+
|
|
294
|
+
Opt-in response envelope decorators: `createApp({ replyHelpers: true })`
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
return reply.ok({ name: 'MacBook' }); // → 200 { success: true, data: {...} }
|
|
298
|
+
return reply.ok(product, 201); // → 201 { success: true, data: {...} }
|
|
299
|
+
return reply.fail('Not found', 404); // → 404 { success: false, error: '...' }
|
|
300
|
+
return reply.fail(['err1', 'err2'], 422); // → 422 { success: false, errors: [...] }
|
|
301
|
+
return reply.paginated({ docs, total, page, limit });
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Streaming Responses
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
// CSV export
|
|
308
|
+
return reply.stream(csvReadableStream, { contentType: 'text/csv', filename: 'export.csv' });
|
|
309
|
+
// PDF download
|
|
310
|
+
return reply.stream(pdfBuffer, { contentType: 'application/pdf', filename: 'report.pdf' });
|
|
311
|
+
// Raw stream (Fastify native — works without reply helpers too)
|
|
312
|
+
return reply.header('content-type', 'text/csv').send(readableStream);
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### BigInt Serialization
|
|
316
|
+
|
|
317
|
+
Opt-in: `createApp({ serializeBigInt: true })` — auto-converts BigInt → Number in all JSON responses.
|
|
318
|
+
|
|
319
|
+
## Multipart Body Middleware (v2.7.3)
|
|
320
|
+
|
|
321
|
+
Opt-in file upload support for CRUD routes — parses multipart form fields into `req.body`, attaches files to `req.body._files`:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
import { multipartBody } from '@classytic/arc/middleware';
|
|
325
|
+
|
|
326
|
+
defineResource({
|
|
327
|
+
middlewares: { create: [multipartBody({ allowedMimeTypes: ['image/png'], maxFileSize: 5_000_000 })] },
|
|
328
|
+
hooks: {
|
|
329
|
+
'before:create': async (data) => {
|
|
330
|
+
if (data._files?.image) { data.imageUrl = await uploadToS3(data._files.image); delete data._files; }
|
|
331
|
+
return data;
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
No-op for JSON requests — safe to always add. Options: `maxFileSize`, `maxFiles`, `allowedMimeTypes`, `filesKey`.
|
|
338
|
+
|
|
270
339
|
## Migrations
|
|
271
340
|
|
|
272
341
|
```typescript
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|