@classytic/arc 2.8.0 → 2.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +10 -1
  2. package/dist/{BaseController-CpMfCXdn.mjs → BaseController-DAGGc5Xn.mjs} +76 -25
  3. package/dist/{EventTransport-n1KBxC_N.d.mts → EventTransport-CinyO7zQ.d.mts} +37 -1
  4. package/dist/{ResourceRegistry-BOtJuRCs.mjs → ResourceRegistry-Dq3_zBQP.mjs} +17 -5
  5. package/dist/adapters/index.d.mts +2 -2
  6. package/dist/adapters/index.mjs +1 -1
  7. package/dist/{adapters-BxGgSHjj.mjs → adapters-BBqAVvPK.mjs} +11 -0
  8. package/dist/audit/index.d.mts +1 -1
  9. package/dist/audit/index.mjs +1 -1
  10. package/dist/audit/mongodb.d.mts +1 -1
  11. package/dist/audit/mongodb.mjs +1 -1
  12. package/dist/auth/index.d.mts +4 -4
  13. package/dist/auth/index.mjs +3 -3
  14. package/dist/auth/redis-session.d.mts +1 -1
  15. package/dist/{betterAuthOpenApi-CHCIuA-p.mjs → betterAuthOpenApi-C5lDyRH2.mjs} +1 -1
  16. package/dist/cache/index.d.mts +2 -2
  17. package/dist/cli/commands/describe.mjs +1 -1
  18. package/dist/cli/commands/docs.mjs +2 -2
  19. package/dist/cli/commands/generate.mjs +1 -1
  20. package/dist/cli/commands/init.mjs +10 -10
  21. package/dist/cli/commands/introspect.mjs +3 -3
  22. package/dist/core/index.d.mts +3 -3
  23. package/dist/core/index.mjs +5 -5
  24. package/dist/{core-BfrfxNqO.mjs → core-DKSwNSXf.mjs} +1 -1
  25. package/dist/{createActionRouter-CbkIAaGh.mjs → createActionRouter-Df1BuawX.mjs} +87 -21
  26. package/dist/{createApp-Cy8eUNKQ.mjs → createApp-BOYjBgdI.mjs} +16 -7
  27. package/dist/{defineResource-CovBXvTB.mjs → defineResource-Bb_Bdhtw.mjs} +60 -33
  28. package/dist/docs/index.d.mts +2 -2
  29. package/dist/docs/index.mjs +1 -1
  30. package/dist/dynamic/index.d.mts +2 -2
  31. package/dist/dynamic/index.mjs +1 -1
  32. package/dist/{errorHandler-BeN-ERN7.d.mts → errorHandler-CdZDavNH.d.mts} +2 -2
  33. package/dist/{errorHandler-BW08lEiy.mjs → errorHandler-mzqk4cGl.mjs} +1 -1
  34. package/dist/{eventPlugin-CAOWMQS8.d.mts → eventPlugin-CVxlE6De.d.mts} +1 -1
  35. package/dist/{eventPlugin-x4jo3sG0.mjs → eventPlugin-D91S2YF4.mjs} +19 -1
  36. package/dist/events/index.d.mts +399 -28
  37. package/dist/events/index.mjs +345 -29
  38. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  39. package/dist/events/transports/redis-stream-entry.mjs +3 -1
  40. package/dist/events/transports/redis.d.mts +1 -1
  41. package/dist/factory/index.d.mts +1 -1
  42. package/dist/factory/index.mjs +2 -152
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/idempotency/index.d.mts +3 -3
  45. package/dist/idempotency/mongodb.d.mts +1 -1
  46. package/dist/idempotency/mongodb.mjs +18 -6
  47. package/dist/idempotency/redis.d.mts +1 -1
  48. package/dist/idempotency/redis.mjs +10 -1
  49. package/dist/{index-BpMhrFgn.d.mts → index-BgmMdpm8.d.mts} +1 -1
  50. package/dist/{index-CBru2y5Y.d.mts → index-CSkeivBx.d.mts} +3 -3
  51. package/dist/{index-qct60lnl.d.mts → index-CpTSDqmD.d.mts} +60 -6
  52. package/dist/index.d.mts +8 -8
  53. package/dist/index.mjs +7 -7
  54. package/dist/integrations/event-gateway.d.mts +1 -1
  55. package/dist/integrations/event-gateway.mjs +1 -1
  56. package/dist/integrations/index.d.mts +1 -1
  57. package/dist/integrations/mcp/index.d.mts +2 -2
  58. package/dist/integrations/mcp/index.mjs +1 -1
  59. package/dist/integrations/mcp/testing.d.mts +1 -1
  60. package/dist/integrations/mcp/testing.mjs +1 -1
  61. package/dist/{interface-IJqN3pXK.d.mts → interface-BVuMfeVv.d.mts} +596 -125
  62. package/dist/loadResources-Bksk8ydA.mjs +154 -0
  63. package/dist/{mongodb-B1eVtFhw.d.mts → mongodb-B8U2xaLj.d.mts} +1 -1
  64. package/dist/{mongodb-NShVZDMr.d.mts → mongodb-X7LbEjTN.d.mts} +10 -1
  65. package/dist/{openapi-AYLVjqVe.mjs → openapi-CYCuekCn.mjs} +50 -3
  66. package/dist/org/index.d.mts +2 -2
  67. package/dist/permissions/index.d.mts +3 -3
  68. package/dist/plugins/index.d.mts +5 -5
  69. package/dist/plugins/index.mjs +8 -8
  70. package/dist/plugins/tracing-entry.d.mts +1 -1
  71. package/dist/plugins/tracing-entry.mjs +1 -1
  72. package/dist/policies/index.d.mts +1 -1
  73. package/dist/presets/index.d.mts +3 -3
  74. package/dist/presets/index.mjs +1 -1
  75. package/dist/presets/multiTenant.d.mts +1 -1
  76. package/dist/{presets-BFrGvvjL.mjs → presets-C2xgzW6x.mjs} +10 -18
  77. package/dist/{queryCachePlugin-BCFVXnxK.d.mts → queryCachePlugin-CnTZZTC5.d.mts} +1 -1
  78. package/dist/{redis-stream-CF1lrKVk.d.mts → redis-stream-D54N5oXs.d.mts} +1 -1
  79. package/dist/{redis-Bunu3qWg.d.mts → redis-z3sFr1UP.d.mts} +1 -1
  80. package/dist/registry/index.d.mts +1 -1
  81. package/dist/registry/index.mjs +1 -1
  82. package/dist/{resourceToTools-C_1SMiCz.mjs → resourceToTools-O_HwWXFa.mjs} +194 -64
  83. package/dist/rpc/index.d.mts +1 -1
  84. package/dist/rpc/index.mjs +1 -1
  85. package/dist/scope/index.d.mts +2 -2
  86. package/dist/testing/index.d.mts +2 -2
  87. package/dist/testing/index.mjs +1 -1
  88. package/dist/types/index.d.mts +5 -5
  89. package/dist/{types-gUxAIZHp.d.mts → types-Bg2X42_m.d.mts} +30 -9
  90. package/dist/{types-BoaZHr-2.d.mts → types-CVC4HOKi.d.mts} +1 -1
  91. package/dist/{types-Ct0PUUSp.d.mts → types-CcG4avic.d.mts} +1 -1
  92. package/dist/utils/index.d.mts +43 -17
  93. package/dist/utils/index.mjs +5 -5
  94. package/dist/{utils-B-l6410F.mjs → utils-yYT3HDXt.mjs} +65 -13
  95. package/package.json +10 -9
  96. package/skills/arc/SKILL.md +79 -6
  97. /package/dist/{caching-CHH-iHs3.mjs → caching-CjybdRwx.mjs} +0 -0
  98. /package/dist/{circuitBreaker-BGVoB1hD.d.mts → circuitBreaker-CvXkjfrW.d.mts} +0 -0
  99. /package/dist/{circuitBreaker-l18oRgL5.mjs → circuitBreaker-cmi5XDv5.mjs} +0 -0
  100. /package/dist/{elevation-UJO3-NvX.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  101. /package/dist/{errors-Cg58SLNi.mjs → errors-BF2bIOIS.mjs} +0 -0
  102. /package/dist/{errors-BI8kEKsO.d.mts → errors-Bmn3eZT6.d.mts} +0 -0
  103. /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  104. /package/dist/{fields-DoeDgh2b.d.mts → fields-DC4So2M2.d.mts} +0 -0
  105. /package/dist/{interface-CkkWm5uR.d.mts → interface-B-pe8fhj.d.mts} +0 -0
  106. /package/dist/{interface-bpoLKKqx.d.mts → interface-DplgQO2e.d.mts} +0 -0
  107. /package/dist/{metrics-DuhiSEZI.mjs → metrics-TuOmguhi.mjs} +0 -0
  108. /package/dist/{mongodb-5Ff3w8jy.mjs → mongodb-B5O6xaW1.mjs} +0 -0
  109. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-A0tWEl1K.mjs} +0 -0
  110. /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-BLojtuvR.mjs} +0 -0
  111. /package/dist/{requestContext-xHIKedG6.mjs → requestContext-DYvHl113.mjs} +0 -0
  112. /package/dist/{schemaConverter-Y5EejTnJ.mjs → schemaConverter-OxfCshus.mjs} +0 -0
  113. /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  114. /package/dist/{sse-CD5Hghpu.mjs → sse-CJpt7LGI.mjs} +0 -0
  115. /package/dist/{tracing-xqXzWeaf.d.mts → tracing-DxjKk7eW.d.mts} +0 -0
  116. /package/dist/{types-CN6JvmYz.d.mts → types-C72d3NDn.d.mts} +0 -0
  117. /package/dist/{versioning-CPU_5Xfs.mjs → versioning-Cm8qoFDg.mjs} +0 -0
@@ -1,11 +1,11 @@
1
- import { r as RequestScope } from "./types-CN6JvmYz.mjs";
2
- import { n as FieldPermissionMap } from "./fields-DoeDgh2b.mjs";
3
- import { i as UserBase, t as PermissionCheck } from "./types-BoaZHr-2.mjs";
1
+ import { r as RequestScope } from "./types-C72d3NDn.mjs";
2
+ import { n as FieldPermissionMap } from "./fields-DC4So2M2.mjs";
3
+ import { i as UserBase, t as PermissionCheck } from "./types-CVC4HOKi.mjs";
4
4
  import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, RouteHandlerMethod, RouteHandlerMethod as RouteHandlerMethod$1 } from "fastify";
5
5
 
6
6
  //#region src/hooks/HookSystem.d.ts
7
7
  type HookPhase = "before" | "around" | "after";
8
- type HookOperation = "create" | "update" | "delete" | "read" | "list";
8
+ type HookOperation = "create" | "update" | "delete" | "restore" | "read" | "list";
9
9
  interface HookContext<T = AnyRecord> {
10
10
  resource: string;
11
11
  operation: HookOperation;
@@ -318,116 +318,479 @@ type PipelineConfig = PipelineStep[] | {
318
318
  //#endregion
319
319
  //#region src/types/repository.d.ts
320
320
  /**
321
- * Repository Interface - Database-Agnostic CRUD Operations
321
+ * Repository Interface Database-Agnostic CRUD Contract
322
322
  *
323
- * This is the standard interface that all repositories must implement.
324
- * MongoKit Repository already implements this interface.
323
+ * This is the canonical contract every arc-compatible repository follows.
324
+ * It is intentionally structural: any object matching the shape works,
325
+ * including the reference implementation at `@classytic/mongokit` and any
326
+ * future `prismakit` / `pgkit` / `sqlitekit` that mirrors it.
325
327
  *
326
- * @example
327
- * ```typescript
328
- * import type { CrudRepository } from '@classytic/arc';
328
+ * ## Design
329
+ *
330
+ * The interface is tiered so a minimal adapter can ship with five methods
331
+ * while a mature one (mongokit 3.6+) can opt into the full surface without
332
+ * type assertions:
333
+ *
334
+ * 1. **Required** — `getAll`, `getById`, `create`, `update`, `delete`.
335
+ * Every resource needs these; arc's BaseController assumes they exist.
336
+ *
337
+ * 2. **Recommended** — `getOne` / `getByQuery`. Used by AccessControl to
338
+ * enforce compound filters (idField + org scope + policy). Without them,
339
+ * arc falls back to `getById` + post-fetch checks, which is slower and
340
+ * produces wrong 404s on custom idFields.
341
+ *
342
+ * 3. **Optional capabilities** — batch ops, soft delete, aggregation,
343
+ * transactions, etc. Declared as optional so kits implement only what
344
+ * their underlying DB supports. arc feature-detects at runtime.
345
+ *
346
+ * All options/results are named types so custom kits can import and
347
+ * implement them directly:
348
+ *
349
+ * ```ts
350
+ * import type {
351
+ * CrudRepository,
352
+ * DeleteOptions,
353
+ * DeleteResult,
354
+ * PaginationResult,
355
+ * UpdateManyResult,
356
+ * BulkWriteOperation,
357
+ * BulkWriteResult,
358
+ * } from '@classytic/arc';
329
359
  *
330
- * // Your repository automatically satisfies this interface
360
+ * class PgRepository<TDoc> implements CrudRepository<TDoc> { }
361
+ * ```
362
+ *
363
+ * @example Reference implementation
364
+ * ```ts
365
+ * import type { CrudRepository } from '@classytic/arc';
331
366
  * const userRepo: CrudRepository<UserDocument> = new Repository(UserModel);
332
367
  * ```
368
+ *
369
+ * ## Contract gotchas (learned from mongokit 3.6 integration)
370
+ *
371
+ * If you build a custom kit that implements this contract, these are the
372
+ * behaviors arc's tests specifically verify. Align your kit here and
373
+ * arc's `BaseController` + presets will work out of the box:
374
+ *
375
+ * 1. **`getById` / `getOne` miss semantics** — MAY return `null` or throw a
376
+ * 404-style error whose message contains "not found". Arc handles both.
377
+ * Pick one and document it in your kit.
378
+ *
379
+ * 2. **`deleteMany` with soft-delete** — if your kit intercepts
380
+ * `deleteMany` and rewrites it to `updateMany`, the returned
381
+ * `deletedCount` may be `0` even when N docs were soft-deleted. The
382
+ * authoritative count comes from a follow-up query. Consumers shouldn't
383
+ * rely on `deletedCount` reflecting soft-delete work unless your kit
384
+ * promises it.
385
+ *
386
+ * 3. **Lifecycle hooks are shared with plugins** — never use
387
+ * `removeAllListeners(event)` to clean up test hooks. That silently
388
+ * removes soft-delete, cascade, multi-tenant, and audit plugin
389
+ * listeners too, which then makes subsequent operations misbehave
390
+ * (e.g. a soft-delete becomes a hard delete). Always use
391
+ * `.off(event, fn)` with the specific handler reference you registered.
392
+ *
393
+ * 4. **Hard-delete mode** — `delete(id, { mode: 'hard' })` and
394
+ * `deleteMany(q, { mode: 'hard' })` MUST bypass soft-delete
395
+ * interception while still running policy / multi-tenant / cascade /
396
+ * audit hooks. Kits without soft-delete should accept and ignore the
397
+ * flag.
398
+ *
399
+ * 5. **Keyset pagination auto-detection** — `getAll({ sort, limit })`
400
+ * without `page` SHOULD return a `KeysetPaginatedResult` with
401
+ * `method: "keyset"`. Kits that only offer offset pagination can return
402
+ * the legacy offset shape; arc's types still satisfy.
403
+ *
404
+ * 6. **`idField` identity** — kits that key on anything other than `"_id"`
405
+ * MUST set `readonly idField` on the repository so arc's BaseController
406
+ * passes route params straight through to `update`/`delete`/`restore`
407
+ * without translating them.
408
+ *
409
+ * 7. **`before:restore` / `after:restore` hooks** — if you implement
410
+ * `restore`, fire these hooks symmetrically with `before:delete` /
411
+ * `after:delete` so hosts can wire cascade-restore flows.
412
+ *
413
+ * See `tests/core/repository-contract-mongokit.test.ts` for a runnable
414
+ * reference against mongokit 3.6. Copy it, swap in your kit's repository,
415
+ * and make it pass — if everything's green, arc will work against your
416
+ * kit.
417
+ */
418
+ /**
419
+ * Opaque transaction session. Adapters bind this to their own type
420
+ * (Mongoose `ClientSession`, Prisma transaction client, `pg.Client`, …).
333
421
  */
422
+ type RepositorySession = unknown;
334
423
  /**
335
- * Query options for read operations
424
+ * Query options for read operations. Extended ad-hoc by adapters via the
425
+ * index signature — kit authors should namespace custom flags (e.g.
426
+ * `__pgHint`) to avoid collisions.
336
427
  */
337
428
  interface QueryOptions {
338
- /** Transaction session — adapters handle the actual type (e.g., Mongoose ClientSession) */
339
- session?: unknown;
340
- /** Field selection - include or exclude fields */
341
- select?: string | string[] | Record<string, 0 | 1>;
342
- /** Relations to populate - string, array, or Mongoose populate options */
343
- populate?: string | string[] | Record<string, unknown>;
344
- /** Return plain JS objects instead of Mongoose documents */
429
+ /** Transaction session — adapter-specific concrete type */
430
+ session?: RepositorySession;
431
+ /** Return plain objects instead of driver documents */
345
432
  lean?: boolean;
346
- /** Allow additional adapter-specific options */
433
+ /** Include soft-deleted docs in reads (honored by soft-delete plugin) */
434
+ includeDeleted?: boolean;
435
+ /** Forwarded to policy/tenant hooks */
436
+ user?: Record<string, unknown>;
437
+ /** Arc request-scoped metadata (orgId, roles, requestId, …) */
438
+ context?: Record<string, unknown>;
439
+ /**
440
+ * Adapter-specific escape hatch — `select`, `populate`, `populateOptions`,
441
+ * `readPreference`, `maxTimeMS`, and every kit's driver-specific flags
442
+ * flow through here. Arc intentionally does NOT type these concretely
443
+ * because each kit's DB shapes them differently: mongoose uses
444
+ * `PopulateOptions[]`, prisma uses `{ include: {...} }`, pgkit uses SQL
445
+ * JOIN hints, etc. Typing them as (say) `string | Record<string, unknown>`
446
+ * would REJECT the narrower shapes real kits actually expose, breaking
447
+ * structural assignability of `Repository<T> → CrudRepository<T>`.
448
+ */
347
449
  [key: string]: unknown;
348
450
  }
349
451
  /**
350
- * Pagination parameters for list operations
452
+ * Options for write operations (create/update). Superset of QueryOptions
453
+ * so callers can pass a single options object.
454
+ */
455
+ interface WriteOptions extends QueryOptions {
456
+ /** Upsert on update/replace operations */
457
+ upsert?: boolean;
458
+ }
459
+ /**
460
+ * Options for delete operations.
461
+ *
462
+ * `mode: 'hard'` opts out of the soft-delete interception when the adapter
463
+ * has a soft-delete plugin wired. Policy, cascade, audit, and cache hooks
464
+ * still fire — only the soft-delete rewrite is bypassed. Use for GDPR
465
+ * erasure or admin purge paths.
466
+ */
467
+ interface DeleteOptions extends QueryOptions {
468
+ /**
469
+ * Force physical deletion even when soft-delete is active, or force soft
470
+ * when the default would be hard. Adapters without soft-delete support
471
+ * MUST ignore this flag (it is a hint, not a contract).
472
+ */
473
+ mode?: "hard" | "soft";
474
+ }
475
+ /**
476
+ * Result of a single delete operation.
477
+ *
478
+ * Matches mongokit's shape. Adapters without soft-delete awareness can omit
479
+ * `soft` and `count`. Arc's BaseController uses the `success` flag to decide
480
+ * whether to return 200 or 404.
481
+ */
482
+ interface DeleteResult {
483
+ success: boolean;
484
+ message: string;
485
+ /** Primary key of the removed doc (string form) */
486
+ id?: string;
487
+ /** True when a soft-delete plugin intercepted the operation */
488
+ soft?: boolean;
489
+ /** For batch-variant implementations that return the delete count inline */
490
+ count?: number;
491
+ }
492
+ /**
493
+ * Result of a batch delete (`deleteMany`) — distinct from single `delete`
494
+ * because MongoDB's driver returns a different shape for batch operations.
495
+ *
496
+ * **Soft-delete gotcha** — when a soft-delete plugin intercepts
497
+ * `deleteMany` by rewriting it to `updateMany` internally (mongokit 3.6
498
+ * does this in `before:deleteMany`), the `deletedCount` returned here may
499
+ * be `0` because the underlying `Model.deleteMany` was never called. The
500
+ * affected-row count lives inside the hook's `updateMany` result and is
501
+ * not surfaced to the caller. Consumers that need the exact soft-deleted
502
+ * count should run a follow-up query (`repo.count({ deletedAt: { $ne:
503
+ * null }, ...filter })`). 3rd-party kits with soft-delete should document
504
+ * which convention they follow.
505
+ */
506
+ interface DeleteManyResult {
507
+ /** Driver-reported acknowledgement */
508
+ acknowledged?: boolean;
509
+ /**
510
+ * Number of documents removed. May be 0 when soft-delete intercepts;
511
+ * see the "Soft-delete gotcha" note above.
512
+ */
513
+ deletedCount: number;
514
+ /** True when a soft-delete plugin intercepted and did `updateMany` instead */
515
+ soft?: boolean;
516
+ }
517
+ /** Result of a bulk update operation. Matches MongoDB driver shape. */
518
+ interface UpdateManyResult {
519
+ acknowledged?: boolean;
520
+ matchedCount: number;
521
+ modifiedCount: number;
522
+ upsertedCount?: number;
523
+ upsertedId?: unknown;
524
+ }
525
+ /** Shape of a single operation passed to `bulkWrite`. */
526
+ type BulkWriteOperation<TDoc = unknown> = {
527
+ insertOne: {
528
+ document: Partial<TDoc>;
529
+ };
530
+ } | {
531
+ updateOne: {
532
+ filter: Record<string, unknown>;
533
+ update: Record<string, unknown>;
534
+ upsert?: boolean;
535
+ };
536
+ } | {
537
+ updateMany: {
538
+ filter: Record<string, unknown>;
539
+ update: Record<string, unknown>;
540
+ upsert?: boolean;
541
+ };
542
+ } | {
543
+ deleteOne: {
544
+ filter: Record<string, unknown>;
545
+ };
546
+ } | {
547
+ deleteMany: {
548
+ filter: Record<string, unknown>;
549
+ };
550
+ } | {
551
+ replaceOne: {
552
+ filter: Record<string, unknown>;
553
+ replacement: Partial<TDoc>;
554
+ upsert?: boolean;
555
+ };
556
+ };
557
+ /** Result of a heterogeneous bulk write. */
558
+ interface BulkWriteResult {
559
+ ok?: number;
560
+ insertedCount?: number;
561
+ matchedCount?: number;
562
+ modifiedCount?: number;
563
+ deletedCount?: number;
564
+ upsertedCount?: number;
565
+ insertedIds?: Record<number, unknown>;
566
+ upsertedIds?: Record<number, unknown>;
567
+ }
568
+ /**
569
+ * Pagination parameters for list operations.
570
+ *
571
+ * Supports three modes, auto-detected by the adapter:
572
+ * - **Offset** — pass `page` + `limit`.
573
+ * - **Keyset** — pass `sort` + `limit` (+ optional `after` cursor). Required
574
+ * for infinite scroll on large collections; O(1) per page.
575
+ * - **Raw** — pass neither; adapter returns all matching docs.
351
576
  */
352
577
  interface PaginationParams<TDoc = unknown> {
353
578
  /** Filter criteria */
354
579
  filters?: Partial<TDoc> & Record<string, unknown>;
355
- /** Sort specification - string ("-createdAt") or object ({ createdAt: -1 }) */
580
+ /** Sort spec string (`"-createdAt"`) or object (`{ createdAt: -1 }`) */
356
581
  sort?: string | Record<string, 1 | -1>;
357
- /** Page number (1-indexed) */
582
+ /** Page number (1-indexed) — triggers offset pagination */
358
583
  page?: number;
359
584
  /** Items per page */
360
585
  limit?: number;
361
- /** Allow additional options (select, populate, etc.) */
586
+ /** Opaque cursor from a prior `next` field — triggers keyset pagination */
587
+ after?: string;
588
+ /** Allow additional options (select, populate, search, …) */
362
589
  [key: string]: unknown;
363
590
  }
364
591
  /**
365
- * Paginated result from list operations
592
+ * Offset-based paginated result (the default shape when `page` is provided).
593
+ *
594
+ * `method` is optional so legacy adapters returning the bare `{ docs, page,
595
+ * limit, total, pages, hasNext, hasPrev }` shape still satisfy the type.
366
596
  */
367
- interface PaginatedResult<TDoc> {
368
- /** Documents for current page */
597
+ interface OffsetPaginatedResult<TDoc> {
598
+ /** Discriminator omitted or `"offset"` */
599
+ method?: "offset";
369
600
  docs: TDoc[];
370
- /** Current page number */
371
601
  page: number;
372
- /** Items per page */
373
602
  limit: number;
374
- /** Total document count */
375
603
  total: number;
376
- /** Total page count */
377
604
  pages: number;
378
- /** Has next page */
379
605
  hasNext: boolean;
380
- /** Has previous page */
381
606
  hasPrev: boolean;
382
607
  }
608
+ /**
609
+ * Keyset-based paginated result (returned when `sort` is provided without
610
+ * `page`). Ideal for infinite scroll — no `count()` query, O(1) per page.
611
+ */
612
+ interface KeysetPaginatedResult<TDoc> {
613
+ /** Discriminator — always `"keyset"` */
614
+ method: "keyset";
615
+ docs: TDoc[];
616
+ limit: number;
617
+ hasMore: boolean;
618
+ /** Opaque cursor token for the next page, or `null` at the end */
619
+ next: string | null;
620
+ }
621
+ /**
622
+ * Discriminated union of all pagination result shapes.
623
+ * Consumers narrow on the `method` discriminator.
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * const result = await repo.getAll(params);
628
+ * if (result.method === "keyset") {
629
+ * // result.next, result.hasMore
630
+ * } else {
631
+ * // result.page, result.total, result.pages
632
+ * }
633
+ * ```
634
+ */
635
+ type PaginationResult<TDoc> = OffsetPaginatedResult<TDoc> | KeysetPaginatedResult<TDoc>;
636
+ /**
637
+ * Legacy alias. Existing code typed as `PaginatedResult<TDoc>` continues
638
+ * to work unchanged — it resolves to the offset shape, which is the most
639
+ * common. New code should prefer `PaginationResult<TDoc>` for the full
640
+ * discriminated union.
641
+ */
642
+ type PaginatedResult<TDoc> = OffsetPaginatedResult<TDoc>;
383
643
  /**
384
644
  * Standard CRUD Repository Interface
385
645
  *
386
- * Defines the contract for data access operations.
387
- * All database adapters (MongoKit, Prisma, etc.) implement this interface.
646
+ * The canonical contract arc consumes. Tiered so minimal adapters only
647
+ * implement the required five methods; richer kits declare the optional
648
+ * capabilities they support.
649
+ *
650
+ * Every optional method is feature-detected at runtime by arc's
651
+ * BaseController and presets — implement only what your DB can express.
388
652
  *
389
653
  * @typeParam TDoc - The document/entity type
390
654
  */
391
655
  interface CrudRepository<TDoc> {
392
656
  /**
393
- * Get paginated list of documents
657
+ * Native primary key field. Defaults to `"_id"` (Mongo convention).
658
+ *
659
+ * Set to match `defineResource({ idField })` for kits that key on a
660
+ * custom field (e.g. `"id"`, `"uuid"`, `"slug"`). Arc's BaseController
661
+ * reads this to decide whether to pass route params straight through
662
+ * to `update`/`delete`/`restore` or to translate them via a fetched
663
+ * doc's `_id` first.
664
+ */
665
+ readonly idField?: string;
666
+ /**
667
+ * List documents with pagination. Adapter auto-selects offset vs keyset
668
+ * mode based on the presence of `page` or `after` in `params`.
669
+ *
670
+ * Return shapes (all valid under the contract):
671
+ * - `OffsetPaginatedResult<TDoc>` — when `page` is given
672
+ * - `KeysetPaginatedResult<TDoc>` — when `sort` + optional `after` are given
673
+ * - `TDoc[]` — raw array, when neither `page` nor `sort` drives pagination
674
+ *
675
+ * Arc's BaseController narrows the union before returning to clients.
394
676
  */
395
- getAll(params?: PaginationParams<TDoc>, options?: QueryOptions): Promise<PaginatedResult<TDoc>>;
677
+ getAll(params?: PaginationParams<TDoc>, options?: QueryOptions): Promise<PaginationResult<TDoc> | TDoc[]>;
396
678
  /**
397
- * Get single document by ID
679
+ * Fetch a single document by its primary key.
680
+ *
681
+ * **Miss semantics — kits may EITHER return `null` OR throw a 404-style
682
+ * error.** Arc's `BaseController` handles both: `AccessControl.fetchWith­
683
+ * AccessControl` catches errors whose message contains "not found" and
684
+ * converts them to null. 3rd-party kit authors: pick one convention and
685
+ * document it. mongokit 3.6 throws by default; pass
686
+ * `{ throwOnNotFound: false }` to get null. A SQL kit that returns null
687
+ * directly is equally valid.
398
688
  */
399
689
  getById(id: string, options?: QueryOptions): Promise<TDoc | null>;
690
+ /** Insert a single document. */
691
+ create(data: Partial<TDoc>, options?: WriteOptions): Promise<TDoc>;
692
+ /** Update a document by primary key. Returns the updated doc or null. */
693
+ update(id: string, data: Partial<TDoc>, options?: WriteOptions): Promise<TDoc | null>;
400
694
  /**
401
- * Create new document
695
+ * Delete a document by primary key. Pass `{ mode: 'hard' }` to bypass
696
+ * soft-delete interception.
402
697
  */
403
- create(data: Partial<TDoc>, options?: {
404
- session?: unknown;
405
- [key: string]: unknown;
406
- }): Promise<TDoc>;
698
+ delete(id: string, options?: DeleteOptions): Promise<DeleteResult>;
407
699
  /**
408
- * Update document by ID
700
+ * Find a single doc by a compound filter. Used by arc's AccessControl to
701
+ * combine `idField + orgId + policy` in one query. Without it, arc falls
702
+ * back to `getById` + post-fetch scope checks (slower; 404s on custom
703
+ * idFields if the doc lives outside the user's scope).
704
+ *
705
+ * Miss semantics match `getById` — kits may return null or throw. Arc
706
+ * handles both. See the note on `getById` above.
409
707
  */
410
- update(id: string, data: Partial<TDoc>, options?: QueryOptions): Promise<TDoc | null>;
708
+ getOne?(filter: Record<string, unknown>, options?: QueryOptions): Promise<TDoc | null>;
709
+ /** Alias many kits expose alongside `getOne`. Arc checks both. */
710
+ getByQuery?(filter: Record<string, unknown>, options?: QueryOptions): Promise<TDoc | null>;
711
+ /** Count matching documents. Respects soft-delete when applicable. */
712
+ count?(filter?: Record<string, unknown>, options?: QueryOptions): Promise<number>;
411
713
  /**
412
- * Delete document by ID
714
+ * Cheap existence check. Kits may return `boolean` or `{ _id }` — arc
715
+ * coerces to boolean at the call site.
413
716
  */
414
- delete(id: string, options?: {
415
- session?: unknown;
416
- [key: string]: unknown;
417
- }): Promise<{
418
- success: boolean;
419
- message: string;
420
- }>;
421
- /** Allow custom methods (getBySlug, getTree, restore, etc.) */
717
+ exists?(filter: Record<string, unknown>, options?: QueryOptions): Promise<boolean | {
718
+ _id: unknown;
719
+ } | null>;
720
+ /** Return the distinct values of a field matching the filter. */
721
+ distinct?<T = unknown>(field: string, filter?: Record<string, unknown>, options?: QueryOptions): Promise<T[]>;
722
+ /** Return all matching docs as a raw array (no pagination metadata). */
723
+ findAll?(filter?: Record<string, unknown>, options?: QueryOptions): Promise<TDoc[]>;
724
+ /**
725
+ * Atomic "find or create" — return the doc matching the filter, or
726
+ * insert `data` and return it if none exists. MAY return `null` when
727
+ * neither path produces a document (e.g. race loss + validation error
728
+ * handling — mongokit returns null in this window).
729
+ */
730
+ getOrCreate?(filter: Record<string, unknown>, data: Partial<TDoc>, options?: WriteOptions): Promise<TDoc | null>;
731
+ /** Insert multiple documents in one call. */
732
+ createMany?(items: Array<Partial<TDoc>>, options?: WriteOptions): Promise<TDoc[]>;
733
+ /**
734
+ * Update all documents matching `filter`. Should reject empty filters
735
+ * to prevent accidental mass updates (mongokit does this).
736
+ */
737
+ updateMany?(filter: Record<string, unknown>, data: Record<string, unknown>, options?: WriteOptions): Promise<UpdateManyResult>;
738
+ /**
739
+ * Delete all documents matching `filter`. Soft-deletes when a soft-delete
740
+ * plugin is wired; pass `{ mode: 'hard' }` to force physical removal.
741
+ */
742
+ deleteMany?(filter: Record<string, unknown>, options?: DeleteOptions): Promise<DeleteManyResult>;
743
+ /**
744
+ * Heterogeneous bulk write (insertOne / updateOne / deleteMany / …).
745
+ *
746
+ * Structurally typed as `unknown` because each kit uses its own operation
747
+ * shape — mongoose uses `AnyBulkWriteOperation[]`, prisma builds these
748
+ * from its client-extension API, pgkit uses SQL primitives. Arc does
749
+ * not call `bulkWrite` internally, so the exact shape is kit-specific.
750
+ * See `BulkWriteOperation<TDoc>` (exported from arc) for a reference
751
+ * shape you can use when implementing your own kit; mongokit-compatible
752
+ * callers should import its own operation types.
753
+ */
754
+ bulkWrite?: unknown;
755
+ /** Restore a soft-deleted document. Should fire `before:restore` hooks. */
756
+ restore?(id: string, options?: QueryOptions): Promise<TDoc | null>;
757
+ /** Paginated list of soft-deleted documents. */
758
+ getDeleted?(params?: PaginationParams<TDoc>, options?: QueryOptions): Promise<PaginationResult<TDoc> | TDoc[]>;
759
+ /**
760
+ * Run an aggregation pipeline.
761
+ *
762
+ * Structurally typed as `unknown` because each kit uses a different
763
+ * stage type (mongoose's `PipelineStage`, prisma's client-extension
764
+ * builders, pgkit's query-builder primitives, …). Arc does not call
765
+ * `aggregate` internally — it's a capability consumers use directly on
766
+ * the repo. Cast or re-declare at the call site using your kit's types.
767
+ */
768
+ aggregate?: unknown;
769
+ /**
770
+ * Paginated aggregation. Same kit-specificity reasoning as `aggregate`
771
+ * — structurally `unknown`, type-safe at the call site.
772
+ */
773
+ aggregatePaginate?: unknown;
774
+ /**
775
+ * Run `callback` inside a transaction. Adapters should auto-retry on
776
+ * transient transaction errors and expose a `session` the callback can
777
+ * forward to subsequent repo calls.
778
+ */
779
+ withTransaction?<T>(callback: (session: RepositorySession) => Promise<T>, options?: Record<string, unknown>): Promise<T>;
780
+ /** slugLookup preset — fetch by a business slug. */
781
+ getBySlug?(slug: string, options?: QueryOptions): Promise<TDoc | null>;
782
+ /** tree preset — return the full hierarchy. */
783
+ getTree?(options?: QueryOptions): Promise<TDoc[]>;
784
+ /** tree preset — return direct children of a node. */
785
+ getChildren?(parentId: string, options?: QueryOptions): Promise<TDoc[]>;
422
786
  [key: string]: unknown;
423
787
  }
424
788
  /**
425
- * Extract document type from a repository
789
+ * Extract document type from a repository.
426
790
  *
427
791
  * @example
428
- * ```typescript
792
+ * ```ts
429
793
  * type UserDoc = InferDoc<typeof userRepository>;
430
- * // UserDoc is now the document type of userRepository
431
794
  * ```
432
795
  */
433
796
  type InferDoc<R> = R extends CrudRepository<infer T> ? T : never;
@@ -465,7 +828,18 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
465
828
  readonly customSchemas: CrudSchemas;
466
829
  readonly permissions: ResourcePermissions;
467
830
  readonly additionalRoutes: AdditionalRoute[];
831
+ /**
832
+ * Original v2.8 `routes` declaration — retained for downstream consumers
833
+ * (OpenAPI, MCP, registry, CLI introspect). Preserves fields dropped during
834
+ * normalization to `additionalRoutes` (notably `mcp`, `description`,
835
+ * `annotations`). Undefined when the resource was defined with the legacy
836
+ * `additionalRoutes` shape.
837
+ *
838
+ * Added in 2.8.1 — the source-of-truth fix for "canonical resource manifest".
839
+ */
840
+ readonly routes?: readonly RouteDefinition[];
468
841
  readonly middlewares: MiddlewareConfig;
842
+ readonly routeGuards?: RouteHandlerMethod$1[];
469
843
  readonly disableDefaultRoutes: boolean;
470
844
  readonly disabledRoutes: CrudRouteKey[];
471
845
  readonly actions?: ActionsMap;
@@ -583,7 +957,7 @@ declare class ResourceRegistry {
583
957
  /**
584
958
  * Minimal server accessor — exposes safe, read-only server decorators.
585
959
  * Allows controller handlers to publish events, log, and audit
586
- * without switching to `wrapHandler: false`.
960
+ * without switching to `raw: true`.
587
961
  */
588
962
  interface ServerAccessor {
589
963
  /** Event bus — publish domain events from any handler */
@@ -696,7 +1070,7 @@ interface IRequestContext<TBody = unknown, TParams extends Record<string, string
696
1070
  metadata?: TMetadata;
697
1071
  /**
698
1072
  * Fastify server accessor — publish events, log, and audit
699
- * from any handler without switching to `wrapHandler: false`.
1073
+ * from any handler without switching to `raw: true`.
700
1074
  *
701
1075
  * @example
702
1076
  * ```typescript
@@ -732,7 +1106,7 @@ interface IControllerResponse<T = unknown> {
732
1106
  * Controller handler — Arc's standard pattern.
733
1107
  *
734
1108
  * Receives a request context object, returns IControllerResponse.
735
- * Use with `wrapHandler: true` in additionalRoutes.
1109
+ * Use with `raw: false` in routes.
736
1110
  *
737
1111
  * **Generic parameters:**
738
1112
  * - `TResponse` — shape of `IControllerResponse.data` (default: `unknown`)
@@ -764,12 +1138,12 @@ interface IControllerResponse<T = unknown> {
764
1138
  * return { success: true, data: product };
765
1139
  * };
766
1140
  *
767
- * additionalRoutes: [{
1141
+ * routes: [{
768
1142
  * method: 'POST',
769
1143
  * path: '/products',
770
1144
  * handler: createProduct,
771
1145
  * permissions: requireAuth(),
772
- * wrapHandler: true, // Arc wraps this into Fastify handler
1146
+ * raw: false, // Arc wraps this into Fastify handler
773
1147
  * }]
774
1148
  * ```
775
1149
  */
@@ -778,7 +1152,7 @@ type ControllerHandler<TResponse = unknown, TBody = unknown, TParams extends Rec
778
1152
  * Fastify native handler
779
1153
  *
780
1154
  * Standard Fastify request/reply pattern.
781
- * Use with `wrapHandler: false` in additionalRoutes.
1155
+ * Use with `raw: true` in routes.
782
1156
  *
783
1157
  * @example
784
1158
  * ```typescript
@@ -788,12 +1162,12 @@ type ControllerHandler<TResponse = unknown, TBody = unknown, TParams extends Rec
788
1162
  * return reply.send(file.buffer);
789
1163
  * };
790
1164
  *
791
- * additionalRoutes: [{
1165
+ * routes: [{
792
1166
  * method: 'GET',
793
1167
  * path: '/files/:id/download',
794
1168
  * handler: downloadFile,
795
1169
  * permissions: requireAuth(),
796
- * wrapHandler: false, // Use as-is, no wrapping
1170
+ * raw: true, // Use as-is, no wrapping
797
1171
  * }]
798
1172
  * ```
799
1173
  */
@@ -845,8 +1219,8 @@ interface AccessControlConfig {
845
1219
  }
846
1220
  /** Minimal repository interface for access-controlled fetch operations */
847
1221
  interface AccessControlRepository {
848
- getById(id: string, options?: unknown): Promise<unknown>;
849
- getOne?: (filter: AnyRecord, options?: unknown) => Promise<unknown>;
1222
+ getById(id: string, options?: QueryOptions): Promise<unknown>;
1223
+ getOne?: (filter: AnyRecord, options?: QueryOptions) => Promise<unknown>;
850
1224
  }
851
1225
  declare class AccessControl {
852
1226
  private readonly tenantField;
@@ -890,7 +1264,7 @@ declare class AccessControl {
890
1264
  * Replaces the duplicated pattern in get/update/delete:
891
1265
  * buildIdFilter -> getOne (or getById + checkOrgScope + checkPolicyFilters)
892
1266
  */
893
- fetchWithAccessControl<TDoc>(id: string, req: IRequestContext, repository: AccessControlRepository, queryOptions?: unknown): Promise<TDoc | null>;
1267
+ fetchWithAccessControl<TDoc>(id: string, req: IRequestContext, repository: AccessControlRepository, queryOptions?: QueryOptions): Promise<TDoc | null>;
894
1268
  /**
895
1269
  * Post-fetch access control validation for items fetched by non-ID queries
896
1270
  * (e.g., getBySlug, restore). Applies org scope, policy filters, and
@@ -1123,7 +1497,7 @@ declare class BaseController<TDoc = AnyRecord, TRepository extends RepositoryLik
1123
1497
  soft?: boolean;
1124
1498
  }>>;
1125
1499
  getBySlug(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
1126
- getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginatedResult<TDoc>>>;
1500
+ getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginationResult<TDoc>>>;
1127
1501
  restore(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
1128
1502
  getTree(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
1129
1503
  getChildren(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
@@ -1223,7 +1597,7 @@ declare module 'fastify' {
1223
1597
  /**
1224
1598
  * Typed Fastify request with Arc decorations.
1225
1599
  *
1226
- * Use this in `wrapHandler: false` handlers instead of `(req as any).user`.
1600
+ * Use this in `raw: true` handlers instead of `(req as any).user`.
1227
1601
  *
1228
1602
  * @example
1229
1603
  * ```typescript
@@ -1654,11 +2028,26 @@ interface ResourceConfig<TDoc = AnyRecord> {
1654
2028
  */
1655
2029
  fields?: FieldPermissionMap;
1656
2030
  middlewares?: MiddlewareConfig;
1657
- /** @deprecated Use `routes` instead. Will error in v3. */
1658
- additionalRoutes?: AdditionalRoute[];
1659
2031
  /**
1660
- * Custom routes the v2.8 way to add endpoints beyond CRUD.
1661
- * Replaces `additionalRoutes` with cleaner naming and no `wrapHandler` boolean.
2032
+ * PreHandler guards auto-applied to **every** route on this resource
2033
+ * (CRUD + custom `routes` + preset routes). Runs after auth/permissions,
2034
+ * before per-route `preHandler`. Use for mode gates, tenant checks,
2035
+ * feature flags — anything that applies to every endpoint.
2036
+ *
2037
+ * @example
2038
+ * ```typescript
2039
+ * defineResource({
2040
+ * routeGuards: [requireFlowMode('standard')],
2041
+ * routes: [
2042
+ * { method: 'GET', path: '/', raw: true, handler: listHandler },
2043
+ * // guard runs automatically — no per-route boilerplate
2044
+ * ],
2045
+ * });
2046
+ * ```
2047
+ */
2048
+ routeGuards?: RouteHandlerMethod[];
2049
+ /**
2050
+ * Custom routes beyond CRUD. Presets also merge their routes here.
1662
2051
  *
1663
2052
  * @example
1664
2053
  * ```typescript
@@ -1958,6 +2347,24 @@ interface AdditionalRoute {
1958
2347
  }>;
1959
2348
  isError?: boolean;
1960
2349
  }>;
2350
+ /**
2351
+ * MCP tool generation config preserved from v2.8 `routes`.
2352
+ * - `false`: skip MCP tool generation for this route
2353
+ * - `true` / omitted: auto-generate when the route goes through Arc's pipeline
2354
+ * - object: explicit description/annotations overrides
2355
+ *
2356
+ * Added in 2.8.1 — previously dropped during `routes → additionalRoutes`
2357
+ * normalization, breaking MCP opt-out and per-route annotations.
2358
+ */
2359
+ mcp?: boolean | {
2360
+ readonly description?: string;
2361
+ readonly annotations?: {
2362
+ readonly readOnlyHint?: boolean;
2363
+ readonly destructiveHint?: boolean;
2364
+ readonly idempotentHint?: boolean;
2365
+ readonly openWorldHint?: boolean;
2366
+ };
2367
+ };
1961
2368
  }
1962
2369
  /** HTTP methods for custom routes */
1963
2370
  type RouteMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
@@ -2075,9 +2482,17 @@ interface RouteSchemaOptions {
2075
2482
  filterableFields?: string[];
2076
2483
  fieldRules?: Record<string, {
2077
2484
  systemManaged?: boolean;
2485
+ hidden?: boolean;
2078
2486
  immutable?: boolean;
2079
2487
  immutableAfterCreate?: boolean;
2080
- optional?: boolean;
2488
+ optional?: boolean; /** String minimum length — auto-maps to OpenAPI `minLength` and MCP tool schema */
2489
+ minLength?: number; /** String maximum length — auto-maps to OpenAPI `maxLength` and MCP tool schema */
2490
+ maxLength?: number; /** Number minimum — auto-maps to OpenAPI `minimum` and MCP tool schema */
2491
+ min?: number; /** Number maximum — auto-maps to OpenAPI `maximum` and MCP tool schema */
2492
+ max?: number; /** Regex pattern — auto-maps to OpenAPI `pattern` and MCP tool schema */
2493
+ pattern?: string; /** Allowed values — auto-maps to OpenAPI `enum` and MCP tool schema */
2494
+ enum?: ReadonlyArray<string | number>; /** Human-readable description — auto-maps to OpenAPI `description` */
2495
+ description?: string;
2081
2496
  [key: string]: unknown;
2082
2497
  }>;
2083
2498
  query?: Record<string, unknown>;
@@ -2298,7 +2713,8 @@ interface PresetHook {
2298
2713
  }
2299
2714
  interface PresetResult {
2300
2715
  name: string;
2301
- additionalRoutes?: AdditionalRoute[] | ((permissions: ResourcePermissions) => AdditionalRoute[]);
2716
+ /** Preset routes merged into the resource's `routes` array. */
2717
+ routes?: RouteDefinition[] | ((permissions: ResourcePermissions) => RouteDefinition[]);
2302
2718
  middlewares?: MiddlewareConfig;
2303
2719
  schemaOptions?: RouteSchemaOptions;
2304
2720
  controllerOptions?: Record<string, unknown>;
@@ -2493,6 +2909,8 @@ interface CrudRouterOptions {
2493
2909
  * Set to `false` to disable rate limiting for this resource.
2494
2910
  */
2495
2911
  rateLimit?: RateLimitConfig | false;
2912
+ /** PreHandler guards applied to every route (CRUD + custom + preset). */
2913
+ routeGuards?: RouteHandlerMethod[];
2496
2914
  }
2497
2915
  interface ResourceMetadata {
2498
2916
  name: string;
@@ -2502,7 +2920,17 @@ interface ResourceMetadata {
2502
2920
  module?: string;
2503
2921
  permissions?: ResourcePermissions;
2504
2922
  presets: string[];
2505
- additionalRoutes?: AdditionalRoute[];
2923
+ customRoutes?: Array<{
2924
+ method: string;
2925
+ path: string;
2926
+ handler: string;
2927
+ operation?: string;
2928
+ summary?: string;
2929
+ description?: string;
2930
+ permissions?: PermissionCheck;
2931
+ raw?: boolean;
2932
+ schema?: Record<string, unknown>;
2933
+ }>;
2506
2934
  routes: Array<{
2507
2935
  method: string;
2508
2936
  path: string;
@@ -2544,6 +2972,33 @@ interface RegistryEntry extends ResourceMetadata {
2544
2972
  audit?: boolean | {
2545
2973
  operations?: ("create" | "update" | "delete")[];
2546
2974
  };
2975
+ /**
2976
+ * v2.8 declarative actions metadata — populated from `ResourceConfig.actions`.
2977
+ *
2978
+ * Consumed by OpenAPI generation (renders `POST /:id/action` with a
2979
+ * discriminated body schema) and MCP tool generation.
2980
+ *
2981
+ * Added in 2.8.1.
2982
+ */
2983
+ actions?: Array<{
2984
+ readonly name: string;
2985
+ readonly description?: string; /** Raw per-action schema (JSON Schema, Zod v4, or legacy field map) */
2986
+ readonly schema?: Record<string, unknown>; /** Per-action permission check (if different from resource-level `actionPermissions`) */
2987
+ readonly permissions?: PermissionCheck; /** MCP tool generation flag — `false` to skip, object for overrides */
2988
+ readonly mcp?: boolean | {
2989
+ readonly description?: string;
2990
+ readonly annotations?: Record<string, unknown>;
2991
+ };
2992
+ }>;
2993
+ /**
2994
+ * Resource-level fallback permission for actions without per-action
2995
+ * permissions. Used by OpenAPI to determine auth requirements and by MCP
2996
+ * as the fallback in `createActionToolHandler`.
2997
+ *
2998
+ * Added in 2.8.1 — previously not surfaced to downstream consumers,
2999
+ * causing OpenAPI to mark action endpoints as public when runtime required auth.
3000
+ */
3001
+ actionPermissions?: PermissionCheck;
2547
3002
  }
2548
3003
  interface RegistryStats {
2549
3004
  total?: number;
@@ -2601,68 +3056,84 @@ type TypedRepository<TDoc> = CrudRepository<TDoc>;
2601
3056
  //#endregion
2602
3057
  //#region src/adapters/interface.d.ts
2603
3058
  /**
2604
- * Minimal repository interface for flexible adapter compatibility.
2605
- * Any repository with these method signatures is accepted — no `as any` needed.
3059
+ * Minimal structural repository shape for flexible adapter compatibility.
2606
3060
  *
2607
- * CrudRepository<TDoc> and MongoKit Repository both satisfy this interface.
2608
- */
2609
- /**
2610
- * Minimal repository interface for flexible adapter compatibility.
2611
- * Any repository with these method signatures is accepted.
3061
+ * `RepositoryLike` is the **loose** variant of `CrudRepository<TDoc>` — it
3062
+ * uses `unknown` for document payloads so any object with the right method
3063
+ * names satisfies it without type assertions. Prefer `CrudRepository<TDoc>`
3064
+ * for kits you own; use `RepositoryLike` when wrapping third-party repos.
2612
3065
  *
2613
- * **Required** core CRUD (every resource needs these):
2614
- * getAll, getById, create, update, delete
3066
+ * Both interfaces declare the same tiered capabilities:
2615
3067
  *
2616
- * **Recommended** — used by AccessControl for compound queries:
2617
- * getOne
3068
+ * - **Required** — `getAll`, `getById`, `create`, `update`, `delete`
3069
+ * - **Recommended** — `getOne` / `getByQuery` (used by AccessControl for
3070
+ * compound filters like `idField + orgId + policy`)
3071
+ * - **Optional** — feature-detected at runtime by presets and the
3072
+ * BaseController. Declare only what your DB supports.
2618
3073
  *
2619
- * **Optional** enabled by presets, checked at runtime:
2620
- * getBySlug — slugLookup preset
2621
- * getDeleted — softDelete preset (list soft-deleted)
2622
- * restore — softDelete preset (restore soft-deleted)
2623
- * getTree — tree preset (hierarchical queries)
2624
- * getChildren — tree preset (child nodes)
2625
- * createMany — bulk preset (batch create)
2626
- * updateMany — bulk preset (batch update by filter)
2627
- * deleteMany — bulk preset (batch delete by filter)
3074
+ * See [CrudRepository](../types/repository.ts) for full prose-level docs
3075
+ * on each method and the design rationale behind the tiering.
2628
3076
  */
2629
3077
  interface RepositoryLike {
2630
- getAll(params?: unknown): Promise<unknown>;
2631
- getById(id: string, options?: unknown): Promise<unknown>;
2632
- create(data: unknown, options?: unknown): Promise<unknown>;
2633
- update(id: string, data: unknown, options?: unknown): Promise<unknown>;
2634
- delete(id: string, options?: unknown): Promise<unknown>;
2635
- /**
2636
- * The repository's native primary key field. When set, Arc's BaseController
2637
- * will pass route params through to `update()`/`delete()`/`restore()` calls
3078
+ /**
3079
+ * The repository's native primary key field. When set, arc's BaseController
3080
+ * passes route params through to `update()`/`delete()`/`restore()` calls
2638
3081
  * unchanged instead of translating them to `_id`.
2639
3082
  *
2640
- * Set this to match your `defineResource({ idField })` for repositories that
2641
- * natively look up by a custom field (e.g. MongoKit's
2642
- * `new Repository(Model, [], {}, { idField: 'id' })`). Without it, Arc will
2643
- * try to translate route ids → fetched doc's `_id` which 404s on repos that
2644
- * don't key on `_id`.
3083
+ * Match this to your `defineResource({ idField })` for repositories that
3084
+ * natively look up by a custom field (e.g. mongokit's
3085
+ * `new Repository(Model, [], {}, { idField: 'id' })`). Without it, arc
3086
+ * will try to translate route ids → fetched doc's `_id`, which 404s on
3087
+ * repos that don't key on `_id`.
2645
3088
  *
2646
- * Defaults to `'_id'` (Mongo). Repositories that always use `_id` may omit it.
3089
+ * Defaults to `'_id'` (Mongo). Kits that always use `_id` may omit it.
2647
3090
  */
2648
3091
  readonly idField?: string;
2649
- /** Find single doc by compound filter — used by AccessControl for idField + org/policy scoping.
2650
- * Without this, Arc falls back to getById + post-fetch security checks. */
2651
- getOne?(filter: Record<string, unknown>, options?: unknown): Promise<unknown>;
2652
- getBySlug?(slug: string, options?: unknown): Promise<unknown>;
2653
- getDeleted?(options?: unknown): Promise<unknown>;
2654
- restore?(id: string): Promise<unknown>;
2655
- getTree?(options?: unknown): Promise<unknown>;
2656
- getChildren?(parentId: string, options?: unknown): Promise<unknown>;
2657
- createMany?(items: unknown[], options?: unknown): Promise<unknown>;
2658
- updateMany?(filter: Record<string, unknown>, data: unknown): Promise<unknown>;
2659
- deleteMany?(filter: Record<string, unknown>): Promise<unknown>;
3092
+ getAll(params?: PaginationParams, options?: QueryOptions): Promise<unknown>;
3093
+ getById(id: string, options?: QueryOptions): Promise<unknown>;
3094
+ create(data: unknown, options?: WriteOptions): Promise<unknown>;
3095
+ update(id: string, data: unknown, options?: WriteOptions): Promise<unknown>;
3096
+ /**
3097
+ * Delete by primary key. Pass `{ mode: 'hard' }` to bypass soft-delete
3098
+ * interception (required by arc's hard-delete flow — `?hard=true` on
3099
+ * the DELETE route forwards this option).
3100
+ */
3101
+ delete(id: string, options?: DeleteOptions): Promise<unknown>;
3102
+ /**
3103
+ * Find a single doc by compound filter. Used by AccessControl for
3104
+ * `idField + org + policy` scoping. Without this, arc falls back to
3105
+ * `getById` + post-fetch security checks (slower, and 404s on custom
3106
+ * idFields that live outside the user's scope).
3107
+ */
3108
+ getOne?(filter: Record<string, unknown>, options?: QueryOptions): Promise<unknown>;
3109
+ /** Alias many kits expose alongside `getOne`. Arc checks both. */
3110
+ getByQuery?(filter: Record<string, unknown>, options?: QueryOptions): Promise<unknown>;
3111
+ count?(filter?: Record<string, unknown>, options?: QueryOptions): Promise<number>;
3112
+ exists?(filter: Record<string, unknown>, options?: QueryOptions): Promise<boolean | {
3113
+ _id: unknown;
3114
+ } | null>;
3115
+ distinct?<T = unknown>(field: string, filter?: Record<string, unknown>, options?: QueryOptions): Promise<T[]>;
3116
+ findAll?(filter?: Record<string, unknown>, options?: QueryOptions): Promise<unknown[]>;
3117
+ getOrCreate?(filter: Record<string, unknown>, data: unknown, options?: WriteOptions): Promise<unknown>;
3118
+ createMany?(items: unknown[], options?: WriteOptions): Promise<unknown[]>;
3119
+ updateMany?(filter: Record<string, unknown>, data: Record<string, unknown>, options?: WriteOptions): Promise<UpdateManyResult>;
3120
+ deleteMany?(filter: Record<string, unknown>, options?: DeleteOptions): Promise<DeleteManyResult>;
3121
+ bulkWrite?: unknown;
3122
+ restore?(id: string, options?: QueryOptions): Promise<unknown>;
3123
+ getDeleted?(params?: PaginationParams, options?: QueryOptions): Promise<PaginationResult<unknown> | unknown[]>;
3124
+ aggregate?: unknown;
3125
+ aggregatePaginate?: unknown;
3126
+ withTransaction?<T>(callback: (session: RepositorySession) => Promise<T>, options?: Record<string, unknown>): Promise<T>;
3127
+ getBySlug?(slug: string, options?: QueryOptions): Promise<unknown>;
3128
+ getTree?(options?: QueryOptions): Promise<unknown>;
3129
+ getChildren?(parentId: string, options?: QueryOptions): Promise<unknown>;
2660
3130
  [key: string]: unknown;
2661
3131
  }
2662
3132
  interface DataAdapter<TDoc = unknown> {
2663
3133
  /**
2664
- * Repository implementing CRUD operations
2665
- * Accepts CrudRepository, MongoKit Repository, or any compatible object
3134
+ * Repository implementing CRUD operations. Accepts the typed
3135
+ * `CrudRepository<TDoc>` or the loose `RepositoryLike` arc checks
3136
+ * capabilities at runtime via feature detection.
2666
3137
  */
2667
3138
  repository: CrudRepository<TDoc> | RepositoryLike;
2668
3139
  /** Adapter identifier for introspection */
@@ -2753,4 +3224,4 @@ interface ValidationResult {
2753
3224
  }
2754
3225
  type AdapterFactory<TDoc> = (config: unknown) => DataAdapter<TDoc>;
2755
3226
  //#endregion
2756
- export { PresetFunction as $, QueryOptions as $t, EventsDecorator as A, BaseController as At, InferResourceDoc as B, FastifyHandler as Bt, ConfigError as C, defineHook as Cn, TypedResourceConfig as Ct, CrudRouterOptions as D, ValidationResult$1 as Dt, CrudRouteKey as E, ValidateOptions as Et, GracefulShutdownOptions as F, BodySanitizerConfig as Ft, LookupOption as G, RegisterOptions as Gt, IntrospectionPluginOptions as H, IControllerResponse as Ht, HealthCheck as I, AccessControl as It, ObjectId as J, defineResource as Jt, MiddlewareConfig as K, ResourceRegistry as Kt, HealthOptions as L, AccessControlConfig as Lt, FastifyWithAuth as M, QueryResolver as Mt, FastifyWithDecorators as N, QueryResolverConfig as Nt, CrudSchemas as O, envelope as Ot, FieldRule as P, BodySanitizer as Pt, PopulateOption as Q, PaginationParams as Qt, InferAdapterDoc as R, ControllerHandler as Rt, AuthenticatorContext as S, createHookSystem as Sn, TypedRepository as St, CrudController as T, UserOrganization as Tt, JWTPayload as U, IRequestContext as Ut, IntrospectionData as V, IController as Vt, JwtContext as W, RouteHandler as Wt, OwnershipCheck as X, InferDoc as Xt, OpenApiSchemas as Y, CrudRepository as Yt, ParsedQuery as Z, PaginatedResult as Zt, ArcInternalMetadata as _, afterDelete as _n, RouteMcpConfig as _t, RelationMetadata as a, PipelineContext as an, RegistryStats as at, AuthPluginOptions as b, beforeDelete as bn, TokenPair as bt, ValidationResult as c, DefineHookOptions as cn, RequestWithExtras as ct, ActionHandlerFn as d, HookOperation as dn, ResourceHookContext as dt, Guard as en, PresetHook as et, ActionsMap as f, HookPhase as fn, ResourceHooks as ft, ArcDecorator as g, afterCreate as gn, RouteHandlerMethod$1 as gt, ApiResponse as h, HookSystemOptions as hn, RouteDefinition as ht, FieldMetadata as i, PipelineConfig as in, RegistryEntry as it, FastifyRequestExtras as j, BaseControllerOptions as jt, EventDefinition as k, getUserId as kt, ActionDefinition as l, HookContext as ln, ResourceCacheConfig as lt, AnyRecord as m, HookSystem as mn, ResourcePermissions as mt, AdapterSchemaContext as n, NextFunction as nn, QueryParserInterface as nt, RepositoryLike as o, PipelineStep as on, RequestContext as ot, AdditionalRoute as p, HookRegistration as pn, ResourceMetadata as pt, MiddlewareHandler as q, ResourceDefinition as qt, DataAdapter as r, OperationFilter as rn, RateLimitConfig as rt, SchemaMetadata as s, Transform as sn, RequestIdOptions as st, AdapterFactory as t, Interceptor as tn, PresetResult as tt, ActionEntry as u, HookHandler as un, ResourceConfig as ut, ArcRequest as v, afterUpdate as vn, RouteSchemaOptions as vt, ControllerQueryOptions as w, UserLike as wt, Authenticator as x, beforeUpdate as xn, TypedController as xt, AuthHelpers as y, beforeCreate as yn, ServiceContext as yt, InferDocType as z, ControllerLike as zt };
3227
+ export { PresetFunction as $, DeleteOptions as $t, EventsDecorator as A, beforeCreate as An, BaseController as At, InferResourceDoc as B, FastifyHandler as Bt, ConfigError as C, HookPhase as Cn, TypedResourceConfig as Ct, CrudRouterOptions as D, afterCreate as Dn, ValidationResult$1 as Dt, CrudRouteKey as E, HookSystemOptions as En, ValidateOptions as Et, GracefulShutdownOptions as F, BodySanitizerConfig as Ft, LookupOption as G, RegisterOptions as Gt, IntrospectionPluginOptions as H, IControllerResponse as Ht, HealthCheck as I, AccessControl as It, ObjectId as J, defineResource as Jt, MiddlewareConfig as K, ResourceRegistry as Kt, HealthOptions as L, AccessControlConfig as Lt, FastifyWithAuth as M, beforeUpdate as Mn, QueryResolver as Mt, FastifyWithDecorators as N, createHookSystem as Nn, QueryResolverConfig as Nt, CrudSchemas as O, afterDelete as On, envelope as Ot, FieldRule as P, defineHook as Pn, BodySanitizer as Pt, PopulateOption as Q, DeleteManyResult as Qt, InferAdapterDoc as R, ControllerHandler as Rt, AuthenticatorContext as S, HookOperation as Sn, TypedRepository as St, CrudController as T, HookSystem as Tn, UserOrganization as Tt, JWTPayload as U, IRequestContext as Ut, IntrospectionData as V, IController as Vt, JwtContext as W, RouteHandler as Wt, OwnershipCheck as X, BulkWriteResult as Xt, OpenApiSchemas as Y, BulkWriteOperation as Yt, ParsedQuery as Z, CrudRepository as Zt, ArcInternalMetadata as _, PipelineStep as _n, RouteMcpConfig as _t, RelationMetadata as a, PaginationParams as an, RegistryStats as at, AuthPluginOptions as b, HookContext as bn, TokenPair as bt, ValidationResult as c, RepositorySession as cn, RequestWithExtras as ct, ActionHandlerFn as d, Guard as dn, ResourceHookContext as dt, DeleteResult as en, PresetHook as et, ActionsMap as f, Interceptor as fn, ResourceHooks as ft, ArcDecorator as g, PipelineContext as gn, RouteHandlerMethod$1 as gt, ApiResponse as h, PipelineConfig as hn, RouteDefinition as ht, FieldMetadata as i, PaginatedResult as in, RegistryEntry as it, FastifyRequestExtras as j, beforeDelete as jn, BaseControllerOptions as jt, EventDefinition as k, afterUpdate as kn, getUserId as kt, ActionDefinition as l, UpdateManyResult as ln, ResourceCacheConfig as lt, AnyRecord as m, OperationFilter as mn, ResourcePermissions as mt, AdapterSchemaContext as n, KeysetPaginatedResult as nn, QueryParserInterface as nt, RepositoryLike as o, PaginationResult as on, RequestContext as ot, AdditionalRoute as p, NextFunction as pn, ResourceMetadata as pt, MiddlewareHandler as q, ResourceDefinition as qt, DataAdapter as r, OffsetPaginatedResult as rn, RateLimitConfig as rt, SchemaMetadata as s, QueryOptions as sn, RequestIdOptions as st, AdapterFactory as t, InferDoc as tn, PresetResult as tt, ActionEntry as u, WriteOptions as un, ResourceConfig as ut, ArcRequest as v, Transform as vn, RouteSchemaOptions as vt, ControllerQueryOptions as w, HookRegistration as wn, UserLike as wt, Authenticator as x, HookHandler as xn, TypedController as xt, AuthHelpers as y, DefineHookOptions as yn, ServiceContext as yt, InferDocType as z, ControllerLike as zt };