@classytic/arc 2.7.7 → 2.8.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.
Files changed (119) hide show
  1. package/README.md +11 -2
  2. package/dist/{BaseController-CpMfCXdn.mjs → BaseController-DAGGc5Xn.mjs} +76 -25
  3. package/dist/{EventTransport-C4VheKeC.d.mts → EventTransport-CLXJUzyT.d.mts} +37 -1
  4. package/dist/{ResourceRegistry-DsHiG9cL.mjs → ResourceRegistry-Dtcojmu8.mjs} +14 -2
  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-EkPaMWNM.mjs → betterAuthOpenApi-C5lDyRH2.mjs} +1 -1
  16. package/dist/cache/index.d.mts +2 -2
  17. package/dist/cache/index.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/introspect.mjs +1 -1
  21. package/dist/core/index.d.mts +2 -2
  22. package/dist/core/index.mjs +4 -3
  23. package/dist/core-CrLDuqoT.mjs +34 -0
  24. package/dist/{core-B_zEeA2b.mjs → createActionRouter-Df1BuawX.mjs} +88 -52
  25. package/dist/{createApp-D7e77m8C.mjs → createApp-p2OThysU.mjs} +10 -10
  26. package/dist/{defineResource-BW2dMCu9.mjs → defineResource-CqeUltrW.mjs} +91 -8
  27. package/dist/docs/index.d.mts +2 -2
  28. package/dist/docs/index.mjs +1 -1
  29. package/dist/dynamic/index.d.mts +2 -2
  30. package/dist/dynamic/index.mjs +1 -1
  31. package/dist/{elevation-By_p2lnn.mjs → elevation-BBGFjzIP.mjs} +1 -1
  32. package/dist/{errorHandler-CH8wk1eD.mjs → errorHandler-Cw34h_om.mjs} +1 -1
  33. package/dist/{errorHandler-pCpEtNd7.d.mts → errorHandler-DJ7OAB2V.d.mts} +1 -1
  34. package/dist/{eventPlugin-CdvUoUna.d.mts → eventPlugin-Cdjwo0Gv.d.mts} +1 -1
  35. package/dist/{eventPlugin-B6U_nCFU.mjs → eventPlugin-XijlQmlL.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.d.mts +1 -1
  40. package/dist/factory/index.d.mts +1 -1
  41. package/dist/factory/index.mjs +1 -1
  42. package/dist/hooks/index.d.mts +1 -1
  43. package/dist/hooks/index.mjs +1 -1
  44. package/dist/idempotency/index.d.mts +3 -3
  45. package/dist/idempotency/mongodb.d.mts +1 -1
  46. package/dist/idempotency/redis.d.mts +1 -1
  47. package/dist/{index-C9eYNjGR.d.mts → index-0zj73o2U.d.mts} +1 -1
  48. package/dist/{index-B0extFr4.d.mts → index-CBru2y5Y.d.mts} +3 -3
  49. package/dist/{index-BjShrzoj.d.mts → index-DadoLP51.d.mts} +48 -16
  50. package/dist/index.d.mts +8 -8
  51. package/dist/index.mjs +8 -8
  52. package/dist/integrations/event-gateway.d.mts +1 -1
  53. package/dist/integrations/event-gateway.mjs +1 -1
  54. package/dist/integrations/index.d.mts +1 -1
  55. package/dist/integrations/mcp/index.d.mts +2 -2
  56. package/dist/integrations/mcp/index.mjs +1 -1
  57. package/dist/integrations/mcp/testing.d.mts +1 -1
  58. package/dist/integrations/mcp/testing.mjs +1 -1
  59. package/dist/{interface-B91alUzq.d.mts → interface-CS6d7HiB.d.mts} +693 -110
  60. package/dist/{mongodb-Cgu9F1Nd.d.mts → mongodb-B1eVtFhw.d.mts} +1 -1
  61. package/dist/{mongodb-B7zupyck.d.mts → mongodb-NShVZDMr.d.mts} +1 -1
  62. package/dist/{openapi-D7Z7VODz.mjs → openapi-q6rNKfZy.mjs} +49 -2
  63. package/dist/org/index.d.mts +2 -2
  64. package/dist/permissions/index.d.mts +3 -3
  65. package/dist/plugins/index.d.mts +4 -4
  66. package/dist/plugins/index.mjs +9 -9
  67. package/dist/plugins/tracing-entry.d.mts +1 -1
  68. package/dist/plugins/tracing-entry.mjs +1 -1
  69. package/dist/policies/index.d.mts +1 -1
  70. package/dist/presets/index.d.mts +3 -3
  71. package/dist/presets/multiTenant.d.mts +1 -1
  72. package/dist/{queryCachePlugin-Ckl71mkc.d.mts → queryCachePlugin-BCFVXnxK.d.mts} +1 -1
  73. package/dist/{redis-3TQxm2VZ.d.mts → redis-Bunu3qWg.d.mts} +1 -1
  74. package/dist/{redis-stream-Dag5LFa9.d.mts → redis-stream-BgrYzpeq.d.mts} +1 -1
  75. package/dist/registry/index.d.mts +1 -1
  76. package/dist/registry/index.mjs +2 -2
  77. package/dist/{resourceToTools-BJkoQoUP.mjs → resourceToTools-DNNWnZtx.mjs} +194 -64
  78. package/dist/rpc/index.d.mts +1 -1
  79. package/dist/rpc/index.mjs +1 -1
  80. package/dist/scope/index.d.mts +2 -2
  81. package/dist/scope/index.mjs +1 -1
  82. package/dist/{sse-6W0hjVS_.mjs → sse-CD5Hghpu.mjs} +1 -1
  83. package/dist/testing/index.d.mts +2 -2
  84. package/dist/testing/index.mjs +1 -1
  85. package/dist/types/index.d.mts +5 -5
  86. package/dist/{types-CKB47kiu.d.mts → types-BlOuKTPw.d.mts} +9 -9
  87. package/dist/{types-B4BNthET.d.mts → types-BoaZHr-2.d.mts} +1 -1
  88. package/dist/{types-C5g2oRC7.d.mts → types-D3b7hA00.d.mts} +1 -1
  89. package/dist/utils/index.d.mts +4 -16
  90. package/dist/utils/index.mjs +5 -5
  91. package/dist/{utils-B-l6410F.mjs → utils-7sJ8X83I.mjs} +1 -13
  92. package/package.json +6 -5
  93. package/skills/arc/SKILL.md +23 -4
  94. package/skills/arc/references/integrations.md +1 -1
  95. package/skills/arc/references/mcp.md +2 -0
  96. /package/dist/{HookSystem-BNYKnrXF.mjs → HookSystem-BjFu7zf1.mjs} +0 -0
  97. /package/dist/{caching-5DtLwIqb.mjs → caching-CHH-iHs3.mjs} +0 -0
  98. /package/dist/{circuitBreaker-BBPDt-J_.d.mts → circuitBreaker-BGVoB1hD.d.mts} +0 -0
  99. /package/dist/{circuitBreaker-l18oRgL5.mjs → circuitBreaker-cmi5XDv5.mjs} +0 -0
  100. /package/dist/{elevation-D7WK0RXq.d.mts → elevation-UJO3-NvX.d.mts} +0 -0
  101. /package/dist/{errors-Cg58SLNi.mjs → errors-BF2bIOIS.mjs} +0 -0
  102. /package/dist/{errors-BS6lZvWy.d.mts → errors-BI8kEKsO.d.mts} +0 -0
  103. /package/dist/{externalPaths-iba7jD3d.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
  104. /package/dist/{fields-D4nMDqnK.d.mts → fields-DoeDgh2b.d.mts} +0 -0
  105. /package/dist/{interface-CSbZdv_3.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  106. /package/dist/{interface-CG7oRZjX.d.mts → interface-bpoLKKqx.d.mts} +0 -0
  107. /package/dist/{logger-DLg8-Ueg.mjs → logger-CDjpjySd.mjs} +0 -0
  108. /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-DuhiSEZI.mjs} +0 -0
  109. /package/dist/{mongodb-B7X7P1P8.mjs → mongodb-5Ff3w8jy.mjs} +0 -0
  110. /package/dist/{pluralize-Dckfq6US.mjs → pluralize-BneOJkpi.mjs} +0 -0
  111. /package/dist/{queryCachePlugin-CwTpR04-.mjs → queryCachePlugin-D0iIVhW_.mjs} +0 -0
  112. /package/dist/{registry-B3lRFBWo.mjs → registry-B0Wl7uVV.mjs} +0 -0
  113. /package/dist/{replyHelpers-uDUIYh7u.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
  114. /package/dist/{requestContext-xHIKedG6.mjs → requestContext-DYvHl113.mjs} +0 -0
  115. /package/dist/{schemaConverter-Y5EejTnJ.mjs → schemaConverter-OxfCshus.mjs} +0 -0
  116. /package/dist/{sessionManager-CEo9jwPI.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
  117. /package/dist/{tracing-DEqdGkr-.d.mts → tracing-xqXzWeaf.d.mts} +0 -0
  118. /package/dist/{types--D3vvfdt.d.mts → types-CN6JvmYz.d.mts} +0 -0
  119. /package/dist/{versioning-CdBbFefk.mjs → versioning-CPU_5Xfs.mjs} +0 -0
@@ -1,11 +1,11 @@
1
- import { r as RequestScope } from "./types--D3vvfdt.mjs";
2
- import { n as FieldPermissionMap } from "./fields-D4nMDqnK.mjs";
3
- import { i as UserBase, t as PermissionCheck } from "./types-B4BNthET.mjs";
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";
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';
359
+ *
360
+ * class PgRepository<TDoc> implements CrudRepository<TDoc> { … }
361
+ * ```
329
362
  *
330
- * // Your repository automatically satisfies this interface
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.
333
417
  */
334
418
  /**
335
- * Query options for read operations
419
+ * Opaque transaction session. Adapters bind this to their own type
420
+ * (Mongoose `ClientSession`, Prisma transaction client, `pg.Client`, …).
421
+ */
422
+ type RepositorySession = unknown;
423
+ /**
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.
394
664
  */
395
- getAll(params?: PaginationParams<TDoc>, options?: QueryOptions): Promise<PaginatedResult<TDoc>>;
665
+ readonly idField?: string;
396
666
  /**
397
- * Get single document by ID
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.
676
+ */
677
+ getAll(params?: PaginationParams<TDoc>, options?: QueryOptions): Promise<PaginationResult<TDoc> | TDoc[]>;
678
+ /**
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>;
699
+ /**
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.
707
+ */
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>;
407
713
  /**
408
- * Update document by ID
714
+ * Cheap existence check. Kits may return `boolean` or `{ _id }` — arc
715
+ * coerces to boolean at the call site.
409
716
  */
410
- update(id: string, data: Partial<TDoc>, options?: QueryOptions): Promise<TDoc | null>;
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[]>;
411
724
  /**
412
- * Delete document by ID
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).
413
729
  */
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.) */
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,9 +828,21 @@ 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;
469
842
  readonly disableDefaultRoutes: boolean;
470
843
  readonly disabledRoutes: CrudRouteKey[];
844
+ readonly actions?: ActionsMap;
845
+ readonly actionPermissions?: PermissionCheck;
471
846
  readonly events: Record<string, EventDefinition>;
472
847
  readonly rateLimit?: RateLimitConfig | false;
473
848
  readonly audit?: boolean | {
@@ -843,8 +1218,8 @@ interface AccessControlConfig {
843
1218
  }
844
1219
  /** Minimal repository interface for access-controlled fetch operations */
845
1220
  interface AccessControlRepository {
846
- getById(id: string, options?: unknown): Promise<unknown>;
847
- getOne?: (filter: AnyRecord, options?: unknown) => Promise<unknown>;
1221
+ getById(id: string, options?: QueryOptions): Promise<unknown>;
1222
+ getOne?: (filter: AnyRecord, options?: QueryOptions) => Promise<unknown>;
848
1223
  }
849
1224
  declare class AccessControl {
850
1225
  private readonly tenantField;
@@ -888,7 +1263,7 @@ declare class AccessControl {
888
1263
  * Replaces the duplicated pattern in get/update/delete:
889
1264
  * buildIdFilter -> getOne (or getById + checkOrgScope + checkPolicyFilters)
890
1265
  */
891
- fetchWithAccessControl<TDoc>(id: string, req: IRequestContext, repository: AccessControlRepository, queryOptions?: unknown): Promise<TDoc | null>;
1266
+ fetchWithAccessControl<TDoc>(id: string, req: IRequestContext, repository: AccessControlRepository, queryOptions?: QueryOptions): Promise<TDoc | null>;
892
1267
  /**
893
1268
  * Post-fetch access control validation for items fetched by non-ID queries
894
1269
  * (e.g., getBySlug, restore). Applies org scope, policy filters, and
@@ -1121,7 +1496,7 @@ declare class BaseController<TDoc = AnyRecord, TRepository extends RepositoryLik
1121
1496
  soft?: boolean;
1122
1497
  }>>;
1123
1498
  getBySlug(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
1124
- getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginatedResult<TDoc>>>;
1499
+ getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginationResult<TDoc>>>;
1125
1500
  restore(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
1126
1501
  getTree(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
1127
1502
  getChildren(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
@@ -1652,7 +2027,44 @@ interface ResourceConfig<TDoc = AnyRecord> {
1652
2027
  */
1653
2028
  fields?: FieldPermissionMap;
1654
2029
  middlewares?: MiddlewareConfig;
2030
+ /** @deprecated Use `routes` instead. Will error in v3. */
1655
2031
  additionalRoutes?: AdditionalRoute[];
2032
+ /**
2033
+ * Custom routes — the v2.8 way to add endpoints beyond CRUD.
2034
+ * Replaces `additionalRoutes` with cleaner naming and no `wrapHandler` boolean.
2035
+ *
2036
+ * @example
2037
+ * ```typescript
2038
+ * routes: [
2039
+ * { method: 'GET', path: '/stats', handler: 'getStats', permissions: auth() },
2040
+ * { method: 'POST', path: '/webhook', handler: webhookFn, raw: true, permissions: auth() },
2041
+ * ]
2042
+ * ```
2043
+ */
2044
+ routes?: RouteDefinition[];
2045
+ /**
2046
+ * State-transition actions → unified POST /:id/action endpoint.
2047
+ * Each action can be a bare handler or full config with permissions + schema.
2048
+ *
2049
+ * @example
2050
+ * ```typescript
2051
+ * actions: {
2052
+ * approve: async (id, data, req) => service.approve(id, req.user._id),
2053
+ * cancel: {
2054
+ * handler: async (id, data, req) => service.cancel(id, data.reason, req.user._id),
2055
+ * permissions: roles('admin'),
2056
+ * schema: { reason: { type: 'string' } },
2057
+ * },
2058
+ * },
2059
+ * actionPermissions: auth(),
2060
+ * ```
2061
+ */
2062
+ actions?: ActionsMap;
2063
+ /**
2064
+ * Fallback permission for actions without per-action permissions.
2065
+ * Only applies when `actions` is defined.
2066
+ */
2067
+ actionPermissions?: PermissionCheck;
1656
2068
  disableCrud?: boolean;
1657
2069
  disableDefaultRoutes?: boolean;
1658
2070
  disabledRoutes?: CrudRouteKey[];
@@ -1919,7 +2331,127 @@ interface AdditionalRoute {
1919
2331
  }>;
1920
2332
  isError?: boolean;
1921
2333
  }>;
2334
+ /**
2335
+ * MCP tool generation config preserved from v2.8 `routes`.
2336
+ * - `false`: skip MCP tool generation for this route
2337
+ * - `true` / omitted: auto-generate when the route goes through Arc's pipeline
2338
+ * - object: explicit description/annotations overrides
2339
+ *
2340
+ * Added in 2.8.1 — previously dropped during `routes → additionalRoutes`
2341
+ * normalization, breaking MCP opt-out and per-route annotations.
2342
+ */
2343
+ mcp?: boolean | {
2344
+ readonly description?: string;
2345
+ readonly annotations?: {
2346
+ readonly readOnlyHint?: boolean;
2347
+ readonly destructiveHint?: boolean;
2348
+ readonly idempotentHint?: boolean;
2349
+ readonly openWorldHint?: boolean;
2350
+ };
2351
+ };
2352
+ }
2353
+ /** HTTP methods for custom routes */
2354
+ type RouteMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
2355
+ /** MCP tool configuration for a route or action */
2356
+ interface RouteMcpConfig {
2357
+ /** Override auto-generated tool description */
2358
+ readonly description?: string;
2359
+ /** MCP tool annotations */
2360
+ readonly annotations?: {
2361
+ readonly readOnlyHint?: boolean;
2362
+ readonly destructiveHint?: boolean;
2363
+ readonly idempotentHint?: boolean;
2364
+ readonly openWorldHint?: boolean;
2365
+ };
2366
+ }
2367
+ /**
2368
+ * Route definition — replaces additionalRoutes.
2369
+ *
2370
+ * - `handler: 'string'` → controller method → full Arc pipeline + MCP tool
2371
+ * - `handler: function` → inline handler → full Arc pipeline + MCP tool
2372
+ * - `raw: true` → raw Fastify handler → no pipeline, no MCP by default
2373
+ */
2374
+ interface RouteDefinition {
2375
+ readonly method: RouteMethod;
2376
+ /** Path relative to resource prefix */
2377
+ readonly path: string;
2378
+ /**
2379
+ * Route handler.
2380
+ * - String: controller method name (goes through Arc pipeline)
2381
+ * - Function without `raw: true`: receives IRequestContext, returns IControllerResponse (goes through Arc pipeline)
2382
+ * - Function with `raw: true`: raw Fastify handler (request, reply)
2383
+ */
2384
+ readonly handler: string | ControllerHandler | RouteHandlerMethod | ((request: FastifyRequest<Record<string, unknown>>, reply: FastifyReply) => unknown);
2385
+ /** Permission check — REQUIRED */
2386
+ readonly permissions: PermissionCheck;
2387
+ /**
2388
+ * Raw mode — bypasses Arc pipeline. Handler receives raw Fastify request/reply.
2389
+ * Default: false (handler goes through Arc pipeline).
2390
+ */
2391
+ readonly raw?: boolean;
2392
+ /** Logical operation name (for pipeline keys, MCP tool naming). Defaults to handler name or method+path slug. */
2393
+ readonly operation?: string;
2394
+ /** OpenAPI summary */
2395
+ readonly summary?: string;
2396
+ /** OpenAPI description */
2397
+ readonly description?: string;
2398
+ /** OpenAPI tags */
2399
+ readonly tags?: string[];
2400
+ /** Route-level middleware */
2401
+ readonly preHandler?: RouteHandlerMethod[] | ((fastify: FastifyInstance) => RouteHandlerMethod[]);
2402
+ /** Pre-auth handlers (run before authentication) */
2403
+ readonly preAuth?: RouteHandlerMethod[];
2404
+ /** SSE streaming mode */
2405
+ readonly streamResponse?: boolean;
2406
+ /** Fastify route schema */
2407
+ readonly schema?: Record<string, unknown>;
2408
+ /**
2409
+ * MCP tool generation:
2410
+ * - omitted/true: auto-generate (non-raw routes only)
2411
+ * - false: skip MCP
2412
+ * - object: explicit config
2413
+ */
2414
+ readonly mcp?: boolean | RouteMcpConfig;
2415
+ /**
2416
+ * MCP handler for raw routes — parallel entry point for MCP without changing HTTP handler.
2417
+ */
2418
+ readonly mcpHandler?: (input: Record<string, unknown>) => Promise<{
2419
+ content: Array<{
2420
+ type: string;
2421
+ text: string;
2422
+ }>;
2423
+ isError?: boolean;
2424
+ }>;
1922
2425
  }
2426
+ /**
2427
+ * Action handler function for state transitions.
2428
+ * Receives the resource ID, action-specific data, and the request context.
2429
+ */
2430
+ type ActionHandlerFn = (id: string, data: Record<string, unknown>, req: RequestWithExtras) => Promise<unknown>;
2431
+ /**
2432
+ * Full action configuration with handler, permissions, and schema.
2433
+ */
2434
+ interface ActionDefinition {
2435
+ /** Action handler */
2436
+ readonly handler: ActionHandlerFn;
2437
+ /** Per-action permission check (overrides resource-level actionPermissions) */
2438
+ readonly permissions?: PermissionCheck;
2439
+ /** JSON Schema for action-specific body fields */
2440
+ readonly schema?: Record<string, Record<string, unknown>>;
2441
+ /** Description for OpenAPI docs and MCP tool */
2442
+ readonly description?: string;
2443
+ /**
2444
+ * MCP tool generation:
2445
+ * - omitted/true: auto-generate
2446
+ * - false: skip
2447
+ * - object: explicit config
2448
+ */
2449
+ readonly mcp?: boolean | RouteMcpConfig;
2450
+ }
2451
+ /** Action config: bare handler function OR full ActionDefinition */
2452
+ type ActionEntry = ActionHandlerFn | ActionDefinition;
2453
+ /** Actions configuration map */
2454
+ type ActionsMap = Record<string, ActionEntry>;
1923
2455
  interface RouteSchemaOptions {
1924
2456
  hiddenFields?: string[];
1925
2457
  readonlyFields?: string[];
@@ -1934,9 +2466,17 @@ interface RouteSchemaOptions {
1934
2466
  filterableFields?: string[];
1935
2467
  fieldRules?: Record<string, {
1936
2468
  systemManaged?: boolean;
2469
+ hidden?: boolean;
1937
2470
  immutable?: boolean;
1938
2471
  immutableAfterCreate?: boolean;
1939
- optional?: boolean;
2472
+ optional?: boolean; /** String minimum length — auto-maps to OpenAPI `minLength` and MCP tool schema */
2473
+ minLength?: number; /** String maximum length — auto-maps to OpenAPI `maxLength` and MCP tool schema */
2474
+ maxLength?: number; /** Number minimum — auto-maps to OpenAPI `minimum` and MCP tool schema */
2475
+ min?: number; /** Number maximum — auto-maps to OpenAPI `maximum` and MCP tool schema */
2476
+ max?: number; /** Regex pattern — auto-maps to OpenAPI `pattern` and MCP tool schema */
2477
+ pattern?: string; /** Allowed values — auto-maps to OpenAPI `enum` and MCP tool schema */
2478
+ enum?: ReadonlyArray<string | number>; /** Human-readable description — auto-maps to OpenAPI `description` */
2479
+ description?: string;
1940
2480
  [key: string]: unknown;
1941
2481
  }>;
1942
2482
  query?: Record<string, unknown>;
@@ -2403,6 +2943,33 @@ interface RegistryEntry extends ResourceMetadata {
2403
2943
  audit?: boolean | {
2404
2944
  operations?: ("create" | "update" | "delete")[];
2405
2945
  };
2946
+ /**
2947
+ * v2.8 declarative actions metadata — populated from `ResourceConfig.actions`.
2948
+ *
2949
+ * Consumed by OpenAPI generation (renders `POST /:id/action` with a
2950
+ * discriminated body schema) and MCP tool generation.
2951
+ *
2952
+ * Added in 2.8.1.
2953
+ */
2954
+ actions?: Array<{
2955
+ readonly name: string;
2956
+ readonly description?: string; /** Raw per-action schema (JSON Schema, Zod v4, or legacy field map) */
2957
+ readonly schema?: Record<string, unknown>; /** Per-action permission check (if different from resource-level `actionPermissions`) */
2958
+ readonly permissions?: PermissionCheck; /** MCP tool generation flag — `false` to skip, object for overrides */
2959
+ readonly mcp?: boolean | {
2960
+ readonly description?: string;
2961
+ readonly annotations?: Record<string, unknown>;
2962
+ };
2963
+ }>;
2964
+ /**
2965
+ * Resource-level fallback permission for actions without per-action
2966
+ * permissions. Used by OpenAPI to determine auth requirements and by MCP
2967
+ * as the fallback in `createActionToolHandler`.
2968
+ *
2969
+ * Added in 2.8.1 — previously not surfaced to downstream consumers,
2970
+ * causing OpenAPI to mark action endpoints as public when runtime required auth.
2971
+ */
2972
+ actionPermissions?: PermissionCheck;
2406
2973
  }
2407
2974
  interface RegistryStats {
2408
2975
  total?: number;
@@ -2460,68 +3027,84 @@ type TypedRepository<TDoc> = CrudRepository<TDoc>;
2460
3027
  //#endregion
2461
3028
  //#region src/adapters/interface.d.ts
2462
3029
  /**
2463
- * Minimal repository interface for flexible adapter compatibility.
2464
- * Any repository with these method signatures is accepted — no `as any` needed.
3030
+ * Minimal structural repository shape for flexible adapter compatibility.
2465
3031
  *
2466
- * CrudRepository<TDoc> and MongoKit Repository both satisfy this interface.
2467
- */
2468
- /**
2469
- * Minimal repository interface for flexible adapter compatibility.
2470
- * Any repository with these method signatures is accepted.
3032
+ * `RepositoryLike` is the **loose** variant of `CrudRepository<TDoc>` — it
3033
+ * uses `unknown` for document payloads so any object with the right method
3034
+ * names satisfies it without type assertions. Prefer `CrudRepository<TDoc>`
3035
+ * for kits you own; use `RepositoryLike` when wrapping third-party repos.
2471
3036
  *
2472
- * **Required** core CRUD (every resource needs these):
2473
- * getAll, getById, create, update, delete
3037
+ * Both interfaces declare the same tiered capabilities:
2474
3038
  *
2475
- * **Recommended** — used by AccessControl for compound queries:
2476
- * getOne
3039
+ * - **Required** — `getAll`, `getById`, `create`, `update`, `delete`
3040
+ * - **Recommended** — `getOne` / `getByQuery` (used by AccessControl for
3041
+ * compound filters like `idField + orgId + policy`)
3042
+ * - **Optional** — feature-detected at runtime by presets and the
3043
+ * BaseController. Declare only what your DB supports.
2477
3044
  *
2478
- * **Optional** enabled by presets, checked at runtime:
2479
- * getBySlug — slugLookup preset
2480
- * getDeleted — softDelete preset (list soft-deleted)
2481
- * restore — softDelete preset (restore soft-deleted)
2482
- * getTree — tree preset (hierarchical queries)
2483
- * getChildren — tree preset (child nodes)
2484
- * createMany — bulk preset (batch create)
2485
- * updateMany — bulk preset (batch update by filter)
2486
- * deleteMany — bulk preset (batch delete by filter)
3045
+ * See [CrudRepository](../types/repository.ts) for full prose-level docs
3046
+ * on each method and the design rationale behind the tiering.
2487
3047
  */
2488
3048
  interface RepositoryLike {
2489
- getAll(params?: unknown): Promise<unknown>;
2490
- getById(id: string, options?: unknown): Promise<unknown>;
2491
- create(data: unknown, options?: unknown): Promise<unknown>;
2492
- update(id: string, data: unknown, options?: unknown): Promise<unknown>;
2493
- delete(id: string, options?: unknown): Promise<unknown>;
2494
- /**
2495
- * The repository's native primary key field. When set, Arc's BaseController
2496
- * will pass route params through to `update()`/`delete()`/`restore()` calls
3049
+ /**
3050
+ * The repository's native primary key field. When set, arc's BaseController
3051
+ * passes route params through to `update()`/`delete()`/`restore()` calls
2497
3052
  * unchanged instead of translating them to `_id`.
2498
3053
  *
2499
- * Set this to match your `defineResource({ idField })` for repositories that
2500
- * natively look up by a custom field (e.g. MongoKit's
2501
- * `new Repository(Model, [], {}, { idField: 'id' })`). Without it, Arc will
2502
- * try to translate route ids → fetched doc's `_id` which 404s on repos that
2503
- * don't key on `_id`.
3054
+ * Match this to your `defineResource({ idField })` for repositories that
3055
+ * natively look up by a custom field (e.g. mongokit's
3056
+ * `new Repository(Model, [], {}, { idField: 'id' })`). Without it, arc
3057
+ * will try to translate route ids → fetched doc's `_id`, which 404s on
3058
+ * repos that don't key on `_id`.
2504
3059
  *
2505
- * Defaults to `'_id'` (Mongo). Repositories that always use `_id` may omit it.
3060
+ * Defaults to `'_id'` (Mongo). Kits that always use `_id` may omit it.
2506
3061
  */
2507
3062
  readonly idField?: string;
2508
- /** Find single doc by compound filter — used by AccessControl for idField + org/policy scoping.
2509
- * Without this, Arc falls back to getById + post-fetch security checks. */
2510
- getOne?(filter: Record<string, unknown>, options?: unknown): Promise<unknown>;
2511
- getBySlug?(slug: string, options?: unknown): Promise<unknown>;
2512
- getDeleted?(options?: unknown): Promise<unknown>;
2513
- restore?(id: string): Promise<unknown>;
2514
- getTree?(options?: unknown): Promise<unknown>;
2515
- getChildren?(parentId: string, options?: unknown): Promise<unknown>;
2516
- createMany?(items: unknown[], options?: unknown): Promise<unknown>;
2517
- updateMany?(filter: Record<string, unknown>, data: unknown): Promise<unknown>;
2518
- deleteMany?(filter: Record<string, unknown>): Promise<unknown>;
3063
+ getAll(params?: PaginationParams, options?: QueryOptions): Promise<unknown>;
3064
+ getById(id: string, options?: QueryOptions): Promise<unknown>;
3065
+ create(data: unknown, options?: WriteOptions): Promise<unknown>;
3066
+ update(id: string, data: unknown, options?: WriteOptions): Promise<unknown>;
3067
+ /**
3068
+ * Delete by primary key. Pass `{ mode: 'hard' }` to bypass soft-delete
3069
+ * interception (required by arc's hard-delete flow — `?hard=true` on
3070
+ * the DELETE route forwards this option).
3071
+ */
3072
+ delete(id: string, options?: DeleteOptions): Promise<unknown>;
3073
+ /**
3074
+ * Find a single doc by compound filter. Used by AccessControl for
3075
+ * `idField + org + policy` scoping. Without this, arc falls back to
3076
+ * `getById` + post-fetch security checks (slower, and 404s on custom
3077
+ * idFields that live outside the user's scope).
3078
+ */
3079
+ getOne?(filter: Record<string, unknown>, options?: QueryOptions): Promise<unknown>;
3080
+ /** Alias many kits expose alongside `getOne`. Arc checks both. */
3081
+ getByQuery?(filter: Record<string, unknown>, options?: QueryOptions): Promise<unknown>;
3082
+ count?(filter?: Record<string, unknown>, options?: QueryOptions): Promise<number>;
3083
+ exists?(filter: Record<string, unknown>, options?: QueryOptions): Promise<boolean | {
3084
+ _id: unknown;
3085
+ } | null>;
3086
+ distinct?<T = unknown>(field: string, filter?: Record<string, unknown>, options?: QueryOptions): Promise<T[]>;
3087
+ findAll?(filter?: Record<string, unknown>, options?: QueryOptions): Promise<unknown[]>;
3088
+ getOrCreate?(filter: Record<string, unknown>, data: unknown, options?: WriteOptions): Promise<unknown>;
3089
+ createMany?(items: unknown[], options?: WriteOptions): Promise<unknown[]>;
3090
+ updateMany?(filter: Record<string, unknown>, data: Record<string, unknown>, options?: WriteOptions): Promise<UpdateManyResult>;
3091
+ deleteMany?(filter: Record<string, unknown>, options?: DeleteOptions): Promise<DeleteManyResult>;
3092
+ bulkWrite?: unknown;
3093
+ restore?(id: string, options?: QueryOptions): Promise<unknown>;
3094
+ getDeleted?(params?: PaginationParams, options?: QueryOptions): Promise<PaginationResult<unknown> | unknown[]>;
3095
+ aggregate?: unknown;
3096
+ aggregatePaginate?: unknown;
3097
+ withTransaction?<T>(callback: (session: RepositorySession) => Promise<T>, options?: Record<string, unknown>): Promise<T>;
3098
+ getBySlug?(slug: string, options?: QueryOptions): Promise<unknown>;
3099
+ getTree?(options?: QueryOptions): Promise<unknown>;
3100
+ getChildren?(parentId: string, options?: QueryOptions): Promise<unknown>;
2519
3101
  [key: string]: unknown;
2520
3102
  }
2521
3103
  interface DataAdapter<TDoc = unknown> {
2522
3104
  /**
2523
- * Repository implementing CRUD operations
2524
- * Accepts CrudRepository, MongoKit Repository, or any compatible object
3105
+ * Repository implementing CRUD operations. Accepts the typed
3106
+ * `CrudRepository<TDoc>` or the loose `RepositoryLike` arc checks
3107
+ * capabilities at runtime via feature detection.
2525
3108
  */
2526
3109
  repository: CrudRepository<TDoc> | RepositoryLike;
2527
3110
  /** Adapter identifier for introspection */
@@ -2612,4 +3195,4 @@ interface ValidationResult {
2612
3195
  }
2613
3196
  type AdapterFactory<TDoc> = (config: unknown) => DataAdapter<TDoc>;
2614
3197
  //#endregion
2615
- export { RateLimitConfig as $, PipelineContext as $t, FieldRule as A, AccessControl as At, JwtContext as B, ResourceRegistry as Bt, CrudRouterOptions as C, getUserId as Ct, FastifyRequestExtras as D, QueryResolverConfig as Dt, EventsDecorator as E, QueryResolver as Et, InferDocType as F, IController as Ft, OpenApiSchemas as G, PaginatedResult as Gt, MiddlewareConfig as H, defineResource as Ht, InferResourceDoc as I, IControllerResponse as It, PopulateOption as J, Guard as Jt, OwnershipCheck as K, PaginationParams as Kt, IntrospectionData as L, IRequestContext as Lt, HealthCheck as M, ControllerHandler as Mt, HealthOptions as N, ControllerLike as Nt, FastifyWithAuth as O, BodySanitizer as Ot, InferAdapterDoc as P, FastifyHandler as Pt, QueryParserInterface as Q, PipelineConfig as Qt, IntrospectionPluginOptions as R, RouteHandler as Rt, CrudRouteKey as S, envelope as St, EventDefinition as T, BaseControllerOptions as Tt, MiddlewareHandler as U, CrudRepository as Ut, LookupOption as V, ResourceDefinition as Vt, ObjectId as W, InferDoc as Wt, PresetHook as X, NextFunction as Xt, PresetFunction as Y, Interceptor as Yt, PresetResult as Z, OperationFilter as Zt, Authenticator as _, defineHook as _n, TypedResourceConfig as _t, RelationMetadata as a, HookOperation as an, ResourceCacheConfig as at, ControllerQueryOptions as b, ValidateOptions as bt, ValidationResult as c, HookSystem as cn, ResourceHooks as ct, ApiResponse as d, afterDelete as dn, RouteHandlerMethod$1 as dt, PipelineStep as en, RegistryEntry as et, ArcDecorator as f, afterUpdate as fn, RouteSchemaOptions as ft, AuthPluginOptions as g, createHookSystem as gn, TypedRepository as gt, AuthHelpers as h, beforeUpdate as hn, TypedController as ht, FieldMetadata as i, HookHandler as in, RequestWithExtras as it, GracefulShutdownOptions as j, AccessControlConfig as jt, FastifyWithDecorators as k, BodySanitizerConfig as kt, AdditionalRoute as l, HookSystemOptions as ln, ResourceMetadata as lt, ArcRequest as m, beforeDelete as mn, TokenPair as mt, AdapterSchemaContext as n, DefineHookOptions as nn, RequestContext as nt, RepositoryLike as o, HookPhase as on, ResourceConfig as ot, ArcInternalMetadata as p, beforeCreate as pn, ServiceContext as pt, ParsedQuery as q, QueryOptions as qt, DataAdapter as r, HookContext as rn, RequestIdOptions as rt, SchemaMetadata as s, HookRegistration as sn, ResourceHookContext as st, AdapterFactory as t, Transform as tn, RegistryStats as tt, AnyRecord as u, afterCreate as un, ResourcePermissions as ut, AuthenticatorContext as v, UserLike as vt, CrudSchemas as w, BaseController as wt, CrudController as x, ValidationResult$1 as xt, ConfigError as y, UserOrganization as yt, JWTPayload as z, RegisterOptions as zt };
3198
+ 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 };