@classytic/arc 2.9.1 → 2.10.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 (139) hide show
  1. package/README.md +19 -90
  2. package/dist/{BaseController-Vu2yc56T.mjs → BaseController-CbKKIflT.mjs} +8 -44
  3. package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-BPd6NQDm.mjs} +1 -1
  4. package/dist/adapters/index.d.mts +3 -3
  5. package/dist/adapters/index.mjs +2 -2
  6. package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
  7. package/dist/audit/index.d.mts +38 -3
  8. package/dist/audit/index.mjs +41 -7
  9. package/dist/auth/index.d.mts +4 -4
  10. package/dist/auth/index.mjs +5 -5
  11. package/dist/auth/redis-session.d.mts +1 -1
  12. package/dist/cache/index.d.mts +17 -15
  13. package/dist/cache/index.mjs +15 -14
  14. package/dist/{caching-CjybdRwx.mjs → caching-CBpK_SCM.mjs} +8 -3
  15. package/dist/cli/commands/describe.mjs +1 -1
  16. package/dist/cli/commands/docs.mjs +2 -2
  17. package/dist/cli/commands/generate.mjs +1 -1
  18. package/dist/cli/commands/init.mjs +1 -1
  19. package/dist/cli/commands/introspect.mjs +1 -1
  20. package/dist/core/index.d.mts +2 -2
  21. package/dist/core/index.mjs +3 -4
  22. package/dist/{defineResource-C__jkwvs.mjs → core-CcR01lup.mjs} +44 -12
  23. package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-Bp_5c_2b.mjs} +1 -1
  24. package/dist/{createApp-CBJUJKGP.mjs → createApp-BuvPma24.mjs} +14 -14
  25. package/dist/docs/index.d.mts +2 -2
  26. package/dist/docs/index.mjs +2 -2
  27. package/dist/{elevation-DxQ6ACbt.mjs → elevation-C7hgL_aI.mjs} +2 -2
  28. package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-Bb49BvPD.mjs} +1 -1
  29. package/dist/{errorHandler-DixGcttC.d.mts → errorHandler-DRQ3EqfL.d.mts} +1 -1
  30. package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-CxWgpd6K.d.mts} +1 -1
  31. package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-DCUjuiQT.mjs} +1 -1
  32. package/dist/events/index.d.mts +8 -5
  33. package/dist/events/index.mjs +34 -17
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis.d.mts +1 -1
  36. package/dist/factory/index.d.mts +1 -1
  37. package/dist/factory/index.mjs +2 -2
  38. package/dist/{types-DZi1aYhm.d.mts → fields-Lo1VUDpt.d.mts} +121 -1
  39. package/dist/{filesUpload-q8oHt--L.mjs → filesUpload-t21LS-py.mjs} +2 -2
  40. package/dist/hooks/index.d.mts +1 -1
  41. package/dist/hooks/index.mjs +1 -1
  42. package/dist/idempotency/index.d.mts +7 -4
  43. package/dist/idempotency/index.mjs +9 -11
  44. package/dist/idempotency/redis.d.mts +1 -1
  45. package/dist/{index-Cibkchnx.d.mts → index-8qw4y6ff.d.mts} +2 -2
  46. package/dist/{index-C-xjcA6F.d.mts → index-ChIw3776.d.mts} +283 -408
  47. package/dist/{interface-YrWsmKqE.d.mts → index-Cl0uoKd5.d.mts} +1885 -2741
  48. package/dist/{index-CtGKT0lf.d.mts → index-DStwgFUK.d.mts} +81 -7
  49. package/dist/index.d.mts +7 -8
  50. package/dist/index.mjs +11 -12
  51. package/dist/integrations/event-gateway.d.mts +1 -1
  52. package/dist/integrations/event-gateway.mjs +1 -1
  53. package/dist/integrations/index.d.mts +1 -1
  54. package/dist/integrations/mcp/index.d.mts +2 -2
  55. package/dist/integrations/mcp/index.mjs +1 -1
  56. package/dist/integrations/mcp/testing.d.mts +1 -1
  57. package/dist/integrations/mcp/testing.mjs +1 -1
  58. package/dist/interface-D218ikEo.d.mts +77 -0
  59. package/dist/{memory-BFAYkf8H.mjs → memory-B5Amv9A1.mjs} +23 -8
  60. package/dist/{openapi-CXuTG1M9.mjs → openapi-B5F8AddX.mjs} +2 -2
  61. package/dist/org/index.d.mts +2 -2
  62. package/dist/permissions/index.d.mts +3 -4
  63. package/dist/permissions/index.mjs +5 -5
  64. package/dist/{permissions-oNZawnkR.mjs → permissions-Dk6mshja.mjs} +315 -397
  65. package/dist/plugins/index.d.mts +4 -4
  66. package/dist/plugins/index.mjs +12 -14
  67. package/dist/plugins/response-cache.mjs +1 -1
  68. package/dist/plugins/tracing-entry.d.mts +1 -1
  69. package/dist/plugins/tracing-entry.mjs +1 -1
  70. package/dist/presets/filesUpload.d.mts +3 -3
  71. package/dist/presets/filesUpload.mjs +1 -1
  72. package/dist/presets/index.d.mts +1 -1
  73. package/dist/presets/index.mjs +2 -2
  74. package/dist/presets/multiTenant.d.mts +1 -1
  75. package/dist/presets/multiTenant.mjs +1 -1
  76. package/dist/presets/search.d.mts +91 -4
  77. package/dist/presets/search.mjs +1 -1
  78. package/dist/{presets-hM4WhNWY.mjs → presets-fLJVXdVn.mjs} +1 -1
  79. package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-BKbWjgDG.d.mts} +1 -1
  80. package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-DQCEfJis.mjs} +8 -8
  81. package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-DBqBB6AC.mjs} +1 -1
  82. package/dist/{redis-MXLp1oOf.d.mts → redis-DqyeggCa.d.mts} +1 -1
  83. package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-CakIQmwR.d.mts} +1 -1
  84. package/dist/registry/index.d.mts +1 -1
  85. package/dist/registry/index.mjs +2 -2
  86. package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BElv3xPT.mjs} +3 -3
  87. package/dist/scope/index.d.mts +1 -1
  88. package/dist/scope/index.mjs +2 -2
  89. package/dist/{sse-CJpt7LGI.mjs → sse-yBCgOLGu.mjs} +1 -1
  90. package/dist/testing/index.d.mts +6 -5
  91. package/dist/testing/index.mjs +8 -10
  92. package/dist/testing/storageContract.d.mts +1 -1
  93. package/dist/types/index.d.mts +4 -4
  94. package/dist/types/index.mjs +1 -31
  95. package/dist/types/storage.d.mts +1 -1
  96. package/dist/{types-CoSzA-s-.d.mts → types-Btdda02s.d.mts} +1 -1
  97. package/dist/{types-CunEX4UX.d.mts → types-Co8k3NyS.d.mts} +9 -9
  98. package/dist/types-Csi3FLfq.mjs +27 -0
  99. package/dist/utils/index.d.mts +207 -3
  100. package/dist/utils/index.mjs +3 -4
  101. package/dist/{utils-B7FuRr9w.mjs → utils-B2fNOD_i.mjs} +285 -2
  102. package/dist/{versioning-Cm8qoFDg.mjs → versioning-C2U_bLY0.mjs} +3 -5
  103. package/package.json +15 -18
  104. package/skills/arc/SKILL.md +7 -11
  105. package/skills/arc/references/production.md +0 -41
  106. package/dist/circuitBreaker-CvXkjfrW.d.mts +0 -206
  107. package/dist/circuitBreaker-l18oRgL5.mjs +0 -284
  108. package/dist/core-DNncu0xF.mjs +0 -34
  109. package/dist/dynamic/index.d.mts +0 -93
  110. package/dist/dynamic/index.mjs +0 -122
  111. package/dist/fields-BC7zcmI9.d.mts +0 -121
  112. package/dist/interface-DplgQO2e.d.mts +0 -54
  113. package/dist/policies/index.d.mts +0 -425
  114. package/dist/policies/index.mjs +0 -318
  115. package/dist/rpc/index.d.mts +0 -90
  116. package/dist/rpc/index.mjs +0 -248
  117. /package/dist/{EventTransport-CqZ8FyM_.d.mts → EventTransport-CUw5NNWe.d.mts} +0 -0
  118. /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-BNYKnrXF.mjs} +0 -0
  119. /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
  120. /package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-BBRVhjQN.mjs} +0 -0
  121. /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
  122. /package/dist/{elevation-B6S5csVA.d.mts → elevation-C5SwtkAn.d.mts} +0 -0
  123. /package/dist/{errors-BI8kEKsO.d.mts → errors-CCSsMpXE.d.mts} +0 -0
  124. /package/dist/{errors-CqWnSqM-.mjs → errors-D5c-5BJL.mjs} +0 -0
  125. /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
  126. /package/dist/{fields-CU6FlaDV.mjs → fields-bxkeltzz.mjs} +0 -0
  127. /package/dist/{interface-B-pe8fhj.d.mts → interface-CSbZdv_3.d.mts} +0 -0
  128. /package/dist/{loadResources-Bksk8ydA.mjs → loadResources-BAzJItAJ.mjs} +0 -0
  129. /package/dist/{logger-CDjpjySd.mjs → logger-DLg8-Ueg.mjs} +0 -0
  130. /package/dist/{metrics-TuOmguhi.mjs → metrics-DuhiSEZI.mjs} +0 -0
  131. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-A0tWEl1K.mjs} +0 -0
  132. /package/dist/{registry-B0Wl7uVV.mjs → registry-B3lRFBWo.mjs} +0 -0
  133. /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
  134. /package/dist/{requestContext-DYtmNpm5.mjs → requestContext-xHIKedG6.mjs} +0 -0
  135. /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
  136. /package/dist/{storage-BwGQXUpd.d.mts → storage-CVk_SEn2.d.mts} +0 -0
  137. /package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-ZCSMJJAX.mjs} +0 -0
  138. /package/dist/{tracing-xqXzWeaf.d.mts → tracing-65B51Dw3.d.mts} +0 -0
  139. /package/dist/{types-ZUu_h0jp.mjs → types-DV9WDfeg.mjs} +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Database-agnostic resource framework for Fastify. Define resources, get CRUD routes, permissions, presets, caching, events, OpenAPI, and MCP tools — without boilerplate.
4
4
 
5
- **v2.9** | Fastify 5+ | Node.js 22+ | ESM only
5
+ **v2.10** | Fastify 5+ | Node.js 22+ | ESM only
6
6
 
7
7
  ## Install
8
8
 
@@ -65,12 +65,14 @@ const app = await createApp({
65
65
  Clean DX without growing exclude lists:
66
66
 
67
67
  ```typescript
68
- import { Repository } from '@classytic/mongokit';
68
+ import { Repository, methodRegistryPlugin, batchOperationsPlugin } from '@classytic/mongokit';
69
69
 
70
70
  // app.ts — pass any RepositoryLike (mongokit / prismakit / custom)
71
71
  await fastify.register(auditPlugin, {
72
72
  autoAudit: { perResource: true },
73
- repository: new Repository(AuditModel), // or omit for in-memory dev
73
+ // batchOperationsPlugin enables deleteMany, required for purgeOlderThan()
74
+ repository: new Repository(AuditModel, [methodRegistryPlugin(), batchOperationsPlugin()]),
75
+ // or omit `repository` for in-memory dev
74
76
  });
75
77
 
76
78
  // order.resource.ts — opt in
@@ -737,7 +739,6 @@ Arc sets `"sideEffects": false` in [package.json](package.json), so modern bundl
737
739
  | `@classytic/arc/presets` | Preset functions + interfaces |
738
740
  | `@classytic/arc/audit` | Audit trail |
739
741
  | `@classytic/arc/idempotency` | Idempotency |
740
- | `@classytic/arc/policies` | Policy engine |
741
742
  | `@classytic/arc/schemas` | TypeBox helpers |
742
743
  | `@classytic/arc/utils` | Errors, circuit breaker, state machine, query parser |
743
744
  | `@classytic/arc/testing` | Test utilities, mocks, in-memory DB |
@@ -750,101 +751,29 @@ Arc sets `"sideEffects": false` in [package.json](package.json), so modern bundl
750
751
  | `@classytic/arc/docs` | OpenAPI generation |
751
752
  | `@classytic/arc/cli` | CLI commands (programmatic) |
752
753
 
753
- ## v2.9.1 Highlights
754
+ ## Type imports
754
755
 
755
- `auditPlugin`, `idempotencyPlugin`, and `EventOutbox` take a
756
- `repository: RepositoryLike` option — pass any `Repository` (mongokit /
757
- prismakit / your own kit). Arc calls `create` / `getOne` / `findAll` /
758
- `deleteMany` / `findOneAndUpdate` on it directly:
756
+ Arc owns framework types (`IController`, `IRequestContext`, `ResourceConfig`, `RepositoryLike`, `PaginationResult`). The repository contract lives in `@classytic/repo-core` — import those types directly:
759
757
 
760
758
  ```typescript
761
- import { Repository } from '@classytic/mongokit';
762
- import { auditPlugin } from '@classytic/arc/audit';
763
- import { idempotencyPlugin } from '@classytic/arc/idempotency';
764
- import { EventOutbox } from '@classytic/arc/events';
765
-
766
- await fastify.register(auditPlugin, { repository: new Repository(AuditModel) });
767
- await fastify.register(idempotencyPlugin, {
768
- repository: new Repository(IdempotencyModel, [
769
- methodRegistryPlugin(), batchOperationsPlugin(), mongoOperationsPlugin(),
770
- ]),
771
- });
772
- new EventOutbox({ repository: new Repository(OutboxModel), transport });
773
- ```
774
-
775
- Memory + Redis stores unchanged via `store` / `customStores`. Requires
776
- `@classytic/mongokit ≥3.8.0`.
777
-
778
- ## v2.9 Highlights
779
-
780
- **Security defaults (breaking):**
781
- - **Field-write perms reject by default** — requests with non-writable fields return 403 with denied-field list. Opt into legacy silent-strip via `defineResource({ onFieldWriteDenied: 'strip' })`.
782
- - **multiTenant preset injects org on UPDATE** — body-supplied `organizationId` is overwritten with caller's scope (prior: CREATE only; a member could hop their own doc to another tenant).
783
- - **Elevation always emits `arc.scope.elevated`** — privilege elevation can no longer be silently un-audited. Subscribe via `fastify.events.subscribe('arc.scope.elevated', …)`. `onElevation` callback still supported.
784
- - **`verifySignature` throws on parsed body** — prevents the common `req.body` vs `req.rawBody` footgun that looks like a wrong secret.
785
- - **Upload `sanitizeFilename` policy** — rejects path separators, NUL, `.`/`..`, >255 chars by default. Flexible: pass `false` / `'*'` / custom function to override.
786
- - **Idempotency `namespace`** — optional key folded into fingerprint for shared-store deployments (prod + canary on one Redis).
759
+ // Arc framework types
760
+ import type { IRequestContext, RepositoryLike, PaginationResult } from '@classytic/arc';
787
761
 
788
- **Event contract v2 (additive):**
789
- - `EventMeta` gets `schemaVersion`, `causationId`, `partitionKey`, `source`, `idempotencyKey`, `aggregate: { type, id }`
790
- - `createChildEvent(parent, type, payload)` auto-chains causation + inherits correlation / userId / organizationId / source / idempotencyKey (aggregate stays explicit)
791
- - `DeadLetteredEvent<T>` type + optional `transport.deadLetter()` for first-class DLQ
792
- - `withRetry({ transport })` auto-routes exhausted events to `transport.deadLetter()` — no custom `$deadLetter` plumbing
793
- - Downstream packages narrow `aggregate.type` to a closed DDD union via interface extension
794
-
795
- **Outbox v2 (additive):**
796
- - `EventOutbox.store()` auto-maps `event.meta.idempotencyKey` → `OutboxWriteOptions.dedupeKey` (caller's explicit `dedupeKey` still wins)
797
- - `new EventOutbox({ failurePolicy: ({ attempts, error }) => ({ retryAt?, deadLetter? }) })` — centralises retry + DLQ escalation, no more hand-rolled `exponentialBackoff` at every failure site
798
- - `outbox.getDeadLettered(limit)` returns typed `DeadLetteredEvent[]` — same shape `withRetry` produces, closes the loop between retry DLQ and outbox DLQ state
799
- - `RelayResult.deadLettered` — per-batch DLQ transition count for dashboards
800
- - Durable outbox via any `RepositoryLike` — `new EventOutbox({ repository, transport })` (2.9.1); multi-worker claim, TTL purge, dedupe, and session-threaded writes come from the repository's backing kit
801
-
802
- **Removed (no replacement kept):**
803
- - `createActionRouter`, `buildActionBodySchema` — use `defineResource({ actions })`
804
- - `ResourceConfig.onRegister` — use `actions` or resource `hooks`
805
- - `PluginResourceResult.additionalRoutes` → `routes: RouteDefinition[]`
806
- - `PolicyContext` / `PolicyResult` `any` fields → `unknown` (tighten downstream narrowing)
807
-
808
- ## v2.8.4 Highlights
809
-
810
- - **MCP ↔ AI SDK bridge** — expose AI SDK `tool()` definitions over MCP without duplicating code. `bridgeToMcp(bridge)` adapts any AI SDK tool into an MCP tool with automatic auth, guard delegation, and `{ error } → isError` envelope translation. `buildMcpToolsFromBridges(bridges, { include, exclude })` registers a whole catalog at once with per-environment filtering.
811
-
812
- ```typescript
813
- import { bridgeToMcp, buildMcpToolsFromBridges, type McpBridge } from '@classytic/arc/mcp';
814
-
815
- export const triggerJobBridge: McpBridge = {
816
- name: 'trigger_job',
817
- description: 'Start a job.',
818
- inputSchema: { phase: z.enum(['investigate', 'fix']) },
819
- annotations: { destructiveHint: true },
820
- buildTool: (ctx) => buildTriggerJobTool(getUserId(ctx) ?? ''),
821
- };
822
-
823
- await app.register(mcpPlugin, {
824
- resources,
825
- extraTools: buildMcpToolsFromBridges([triggerJobBridge]),
826
- });
762
+ // Repository contract (repo-core is the single source of truth)
763
+ import type { StandardRepo, WriteOptions, QueryOptions } from '@classytic/repo-core/repository';
764
+ import type { OffsetPaginationResult } from '@classytic/repo-core/pagination';
827
765
  ```
828
766
 
829
- ## v2.8.1 Highlights
767
+ > Arc 2.10 dropped the legacy `CrudRepository`, `PaginatedResult`, and pass-through `WriteOptions`/`QueryOptions` re-exports. See [CHANGELOG.md](CHANGELOG.md#210) for the migration table.
830
768
 
831
- - **Per-action discriminated validation** — `actions` schemas now enforce required fields via a `oneOf` body schema; missing inputs are rejected at the HTTP layer by AJV (no more silent bypass)
832
- - **Actions in OpenAPI** — `POST /:id/action` endpoint auto-generated from `ResourceDefinition.actions`, with per-action descriptions and the same discriminated body schema as the runtime router
833
- - **Route/action metadata preserved** — `mcp: false`, `description`, `annotations` no longer dropped during `routes → additionalRoutes` normalization
834
- - **Canonical source retained** — `ResourceDefinition.routes` and `ResourceDefinition.actions` now kept as declared, so OpenAPI/MCP/registry can read the original shape
835
- - **Outbox hardening** — expanded `OutboxStore` contract (`claimPending`, `fail`, write options, dedupe, visibleAt), ownership-mismatch throws, onError reporting, safe multi-worker relay
836
- - **`slugLookup` fallback** — works with MongoKit's default Repository (no custom `getBySlug` needed)
769
+ ## v2.10 Highlights
837
770
 
838
- ## v2.8.0 Highlights
771
+ - **Clean-break on repo-core types** — `CrudRepository` / `PaginatedResult` / pass-through repo options removed from arc's public surface. Import `StandardRepo`, `OffsetPaginationResult`, etc. directly from `@classytic/repo-core`. See [CHANGELOG.md](CHANGELOG.md) for the rewrite table.
772
+ - **Outbox bugfix** — `repositoryAsOutboxStore.fail()` now passes `updatePipeline: true` to `findOneAndUpdate`, so retry / DLQ transitions work on mongokit ≥3.10.
773
+ - **Plugin requirements documented** — audit / outbox / idempotency require mongokit's `methodRegistryPlugin` + `batchOperationsPlugin` for `deleteMany`; README snippets + production-ops docs now show the correct chain.
774
+ - **Removed** — `@classytic/arc/policies` (use `permissions/`), `@classytic/arc/rpc`, `@classytic/arc/dynamic` (use `factory/loadResources`).
839
775
 
840
- - **MCP Integration** expose resources as AI agent tools (stateless by default, service scope, multi-tenancy)
841
- - **Reply Helpers** — `reply.ok()`, `reply.fail()`, `reply.paginated()`, `reply.stream()` (opt-in)
842
- - **Error Mappers** — class-based `instanceof` domain error → HTTP response mapping
843
- - **Multipart Body** — `multipartBody()` middleware for file upload in CRUD routes
844
- - **Service Scope** — `kind: "service"` RequestScope for machine-to-machine auth (MCP + WebSocket)
845
- - **BigInt Serialization** — `serializeBigInt: true` auto-converts BigInt → Number
846
- - **Event WAL** — skips internal `arc.*` events to prevent startup timeout with durable stores
847
- - **Security** — `auth: false` produces null `ctx.user` (prevents anonymous bypass of `!!ctx.user` guards)
776
+ See [CHANGELOG.md](CHANGELOG.md) for the full v2.9 / v2.8 / v2.7 history.
848
777
 
849
778
  ## License
850
779
 
@@ -1,20 +1,12 @@
1
- import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-Cxde4rpC.mjs";
1
+ import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-BhY1OHoH.mjs";
2
2
  import { _ as isElevated, n as PUBLIC_SCOPE, o as getOrgId, v as isMember } from "./types-AOD8fxIw.mjs";
3
3
  import { t as buildQueryKey } from "./keys-qcD-TVJl.mjs";
4
- import { getUserId } from "./types/index.mjs";
5
- import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-CU6FlaDV.mjs";
6
- import { t as getUserRoles } from "./types-ZUu_h0jp.mjs";
7
- import { r as ForbiddenError } from "./errors-CqWnSqM-.mjs";
8
- import { t as ArcQueryParser } from "./queryParser-Cs-6SHQK.mjs";
4
+ import { n as getUserId } from "./types-Csi3FLfq.mjs";
5
+ import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-bxkeltzz.mjs";
6
+ import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
7
+ import { r as ForbiddenError } from "./errors-D5c-5BJL.mjs";
8
+ import { t as ArcQueryParser } from "./queryParser-DBqBB6AC.mjs";
9
9
  //#region src/core/AccessControl.ts
10
- /**
11
- * AccessControl - Composable access control logic extracted from BaseController.
12
- *
13
- * Handles ID filtering, policy filter checking, org/tenant scope validation,
14
- * ownership verification, and fetch-with-access-control patterns.
15
- *
16
- * Designed to be used standalone or composed into controllers.
17
- */
18
10
  var AccessControl = class AccessControl {
19
11
  tenantField;
20
12
  idField;
@@ -664,20 +656,10 @@ var BaseController = class {
664
656
  async executeListQuery(options, req) {
665
657
  const hooks = this.getHooks(req);
666
658
  const repoGetAll = async () => this.repository.getAll(options);
667
- const result = hooks && this.resourceName ? await hooks.executeAround(this.resourceName, "list", options, repoGetAll, {
659
+ return hooks && this.resourceName ? await hooks.executeAround(this.resourceName, "list", options, repoGetAll, {
668
660
  user: req.user,
669
661
  context: this.meta(req)
670
662
  }) : await repoGetAll();
671
- if (Array.isArray(result)) return {
672
- docs: result,
673
- page: 1,
674
- limit: result.length,
675
- total: result.length,
676
- pages: 1,
677
- hasNext: false,
678
- hasPrev: false
679
- };
680
- return result;
681
663
  }
682
664
  async get(req) {
683
665
  const id = req.params.id;
@@ -981,27 +963,9 @@ var BaseController = class {
981
963
  status: 501
982
964
  };
983
965
  const parsed = this.queryResolver.resolve(req, this.meta(req));
984
- const result = await repo.getDeleted(parsed, parsed);
985
- if (Array.isArray(result)) {
986
- const docs = result;
987
- return {
988
- success: true,
989
- data: {
990
- method: "offset",
991
- docs,
992
- page: 1,
993
- limit: docs.length,
994
- total: docs.length,
995
- pages: 1,
996
- hasNext: false,
997
- hasPrev: false
998
- },
999
- status: 200
1000
- };
1001
- }
1002
966
  return {
1003
967
  success: true,
1004
- data: result,
968
+ data: await repo.getDeleted(parsed, parsed),
1005
969
  status: 200
1006
970
  };
1007
971
  }
@@ -1,4 +1,4 @@
1
- import { t as CRUD_OPERATIONS } from "./constants-Cxde4rpC.mjs";
1
+ import { t as CRUD_OPERATIONS } from "./constants-BhY1OHoH.mjs";
2
2
  //#region src/registry/ResourceRegistry.ts
3
3
  /**
4
4
  * Resource Registry
@@ -1,3 +1,3 @@
1
- import { a as RelationMetadata, c as ValidationResult, i as FieldMetadata, o as RepositoryLike, r as DataAdapter, s as SchemaMetadata, t as AdapterFactory } from "../interface-YrWsmKqE.mjs";
2
- import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter } from "../index-CtGKT0lf.mjs";
3
- export { AdapterFactory, DataAdapter, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, createMongooseAdapter, createPrismaAdapter };
1
+ import { At as SchemaMetadata, Dt as FieldMetadata, Et as DataAdapter, Ot as RelationMetadata, jt as ValidationResult, kt as RepositoryLike, wt as AdapterFactory } from "../index-Cl0uoKd5.mjs";
2
+ import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, d as DrizzleAdapterOptions, f as createDrizzleAdapter, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter, u as DrizzleAdapter } from "../index-DStwgFUK.mjs";
3
+ export { AdapterFactory, DataAdapter, DrizzleAdapter, DrizzleAdapterOptions, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
@@ -1,2 +1,2 @@
1
- import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-BBqAVvPK.mjs";
2
- export { MongooseAdapter, PrismaAdapter, PrismaQueryParser, createMongooseAdapter, createPrismaAdapter };
1
+ import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, o as DrizzleAdapter, r as createPrismaAdapter, s as createDrizzleAdapter, t as PrismaAdapter } from "../adapters-BXY4i-hw.mjs";
2
+ export { DrizzleAdapter, MongooseAdapter, PrismaAdapter, PrismaQueryParser, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
@@ -1,4 +1,44 @@
1
- import { h as SYSTEM_FIELDS, m as RESERVED_QUERY_PARAMS } from "./constants-Cxde4rpC.mjs";
1
+ import { h as SYSTEM_FIELDS, m as RESERVED_QUERY_PARAMS } from "./constants-BhY1OHoH.mjs";
2
+ //#region src/adapters/field-rule-helpers.ts
3
+ /**
4
+ * Merge constraint-style `fieldRules` into an `OpenApiSchemas` bag in place.
5
+ *
6
+ * Operates on the three schema slots that carry property maps — `createBody`,
7
+ * `updateBody`, `response`. `listQuery` and `params` are skipped (their
8
+ * constraint vocabulary is owned by the kit's query parser).
9
+ *
10
+ * Existing constraints on a property always win — the merge only fills in
11
+ * gaps. Adapters that already walk `fieldRules` during base-schema assembly
12
+ * can call this helper for free (the checks are no-ops when constraints
13
+ * already exist).
14
+ */
15
+ function mergeFieldRuleConstraints(schemas, schemaOptions) {
16
+ if (!schemas || typeof schemas !== "object") return;
17
+ const rules = schemaOptions?.fieldRules;
18
+ if (!rules || Object.keys(rules).length === 0) return;
19
+ for (const slot of [
20
+ "createBody",
21
+ "updateBody",
22
+ "response"
23
+ ]) {
24
+ const slotSchema = schemas[slot];
25
+ if (!slotSchema || typeof slotSchema !== "object") continue;
26
+ const properties = slotSchema.properties;
27
+ if (!properties) continue;
28
+ for (const [field, rule] of Object.entries(rules)) {
29
+ const prop = properties[field];
30
+ if (!prop || typeof prop !== "object") continue;
31
+ if (rule.minLength != null && prop.minLength == null) prop.minLength = rule.minLength;
32
+ if (rule.maxLength != null && prop.maxLength == null) prop.maxLength = rule.maxLength;
33
+ if (rule.min != null && prop.minimum == null) prop.minimum = rule.min;
34
+ if (rule.max != null && prop.maximum == null) prop.maximum = rule.max;
35
+ if (rule.pattern != null && prop.pattern == null) prop.pattern = rule.pattern;
36
+ if (rule.enum != null && prop.enum == null) prop.enum = rule.enum;
37
+ if (rule.description != null && prop.description == null) prop.description = rule.description;
38
+ }
39
+ }
40
+ }
41
+ //#endregion
2
42
  //#region src/adapters/types.ts
3
43
  /**
4
44
  * Check if value is a Mongoose model
@@ -13,6 +53,168 @@ function isRepository(value) {
13
53
  return typeof value === "object" && value !== null && "getAll" in value && "getById" in value && "create" in value && "update" in value && "delete" in value;
14
54
  }
15
55
  //#endregion
56
+ //#region src/adapters/drizzle.ts
57
+ const DRIZZLE_COLUMNS_SYMBOL = Symbol.for("drizzle:Columns");
58
+ function getColumns(table) {
59
+ const cols = table[DRIZZLE_COLUMNS_SYMBOL];
60
+ if (!cols || typeof cols !== "object") return {};
61
+ return cols;
62
+ }
63
+ function columnToJsonSchema(column) {
64
+ const { dataType, columnType, enumValues, length } = column;
65
+ if (dataType === "date") return {
66
+ type: "string",
67
+ format: "date-time"
68
+ };
69
+ if (dataType === "boolean") return { type: "boolean" };
70
+ if (dataType === "json") return {
71
+ type: "object",
72
+ additionalProperties: true
73
+ };
74
+ if (dataType === "buffer") return {
75
+ type: "string",
76
+ contentEncoding: "base64"
77
+ };
78
+ if (dataType === "number" || dataType === "bigint") return { type: columnType === "SQLiteInteger" ? "integer" : "number" };
79
+ if (dataType === "string") {
80
+ const result = { type: "string" };
81
+ if (Array.isArray(enumValues) && enumValues.length > 0) result.enum = [...enumValues];
82
+ if (typeof length === "number" && length > 0) result.maxLength = length;
83
+ return result;
84
+ }
85
+ return {};
86
+ }
87
+ function columnToFieldMetadata(column) {
88
+ const { dataType, enumValues } = column;
89
+ const meta = {
90
+ type: (dataType && {
91
+ number: "number",
92
+ bigint: "number",
93
+ string: "string",
94
+ date: "date",
95
+ boolean: "boolean",
96
+ json: "object",
97
+ buffer: "object"
98
+ }[dataType]) ?? (enumValues?.length ? "enum" : "object"),
99
+ required: !!column.notNull && !column.hasDefault
100
+ };
101
+ if (enumValues?.length) meta.enum = [...enumValues];
102
+ if (typeof column.length === "number") meta.maxLength = column.length;
103
+ return meta;
104
+ }
105
+ var DrizzleAdapter = class {
106
+ type = "drizzle";
107
+ name;
108
+ table;
109
+ repository;
110
+ schemaGenerator;
111
+ constructor(options) {
112
+ if (!options.table || typeof options.table !== "object") throw new TypeError("DrizzleAdapter: Invalid table. Expected a Drizzle table created with sqliteTable / pgTable / mysqlTable.");
113
+ if (!isRepository(options.repository)) throw new TypeError("DrizzleAdapter: Invalid repository. Expected an object implementing MinimalRepo (getAll / getById / create / update / delete).");
114
+ this.table = options.table;
115
+ this.repository = options.repository;
116
+ this.schemaGenerator = options.schemaGenerator;
117
+ this.name = options.name ?? "DrizzleAdapter";
118
+ }
119
+ /**
120
+ * Introspect Drizzle columns into arc's schema metadata shape.
121
+ */
122
+ getSchemaMetadata() {
123
+ const columns = getColumns(this.table);
124
+ const fields = {};
125
+ const indexes = [];
126
+ for (const [name, column] of Object.entries(columns)) {
127
+ fields[name] = columnToFieldMetadata(column);
128
+ if (column.primary) indexes.push({
129
+ fields: [name],
130
+ unique: true
131
+ });
132
+ }
133
+ return {
134
+ name: this.name,
135
+ fields,
136
+ ...indexes.length > 0 ? { indexes } : {}
137
+ };
138
+ }
139
+ /**
140
+ * Generate OpenAPI schemas. Delegates to the user-provided
141
+ * `schemaGenerator` when available (strongly recommended — that's where
142
+ * field rules, omit lists, and param-type narrowing live). The built-in
143
+ * fallback emits a permissive entity + CRUD body shape so routes still
144
+ * register when no generator is provided.
145
+ *
146
+ * After the kit generator runs, arc merges constraint-style field rules
147
+ * (`minLength`, `maxLength`, `min`, `max`, `pattern`, `enum`, `description`)
148
+ * into the resulting property schemas so sqlitekit / pgkit behave
149
+ * identically to mongoose here — rule-driven AJV constraints apply
150
+ * regardless of backend.
151
+ */
152
+ generateSchemas(schemaOptions, context) {
153
+ try {
154
+ if (this.schemaGenerator) {
155
+ const generated = this.schemaGenerator(this.table, schemaOptions, context);
156
+ mergeFieldRuleConstraints(generated, schemaOptions);
157
+ return generated;
158
+ }
159
+ const columns = getColumns(this.table);
160
+ if (Object.keys(columns).length === 0) return null;
161
+ const entityProperties = {};
162
+ const inputProperties = {};
163
+ const inputRequired = [];
164
+ const updateProperties = {};
165
+ const fieldRules = schemaOptions?.fieldRules ?? {};
166
+ const readonlySet = new Set(schemaOptions?.readonlyFields ?? []);
167
+ const optionalSet = new Set(schemaOptions?.optionalFields ?? []);
168
+ const blocked = new Set([
169
+ ...Object.entries(fieldRules).filter(([, rules]) => rules.systemManaged || rules.hidden).map(([field]) => field),
170
+ ...schemaOptions?.excludeFields ?? [],
171
+ ...schemaOptions?.hiddenFields ?? []
172
+ ]);
173
+ for (const [fieldName, column] of Object.entries(columns)) {
174
+ const schema = columnToJsonSchema(column);
175
+ entityProperties[fieldName] = schema;
176
+ if (blocked.has(fieldName)) continue;
177
+ if (column.primary && column.columnType === "SQLiteInteger") continue;
178
+ if (!readonlySet.has(fieldName)) {
179
+ inputProperties[fieldName] = schema;
180
+ if (!!column.notNull && !column.hasDefault && !optionalSet.has(fieldName)) inputRequired.push(fieldName);
181
+ updateProperties[fieldName] = schema;
182
+ }
183
+ }
184
+ return {
185
+ createBody: {
186
+ type: "object",
187
+ properties: inputProperties,
188
+ required: inputRequired.length > 0 ? inputRequired : void 0,
189
+ additionalProperties: true
190
+ },
191
+ updateBody: {
192
+ type: "object",
193
+ properties: updateProperties,
194
+ additionalProperties: true
195
+ },
196
+ response: {
197
+ type: "object",
198
+ properties: entityProperties,
199
+ additionalProperties: true
200
+ }
201
+ };
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+ async healthCheck() {
207
+ return typeof this.repository.getAll === "function";
208
+ }
209
+ };
210
+ /**
211
+ * Factory — preferred construction style for symmetry with
212
+ * `createMongooseAdapter` / `createPrismaAdapter`.
213
+ */
214
+ function createDrizzleAdapter(options) {
215
+ return new DrizzleAdapter(options);
216
+ }
217
+ //#endregion
16
218
  //#region src/adapters/mongoose.ts
17
219
  /**
18
220
  * Mongoose data adapter with proper type safety
@@ -27,7 +229,7 @@ var MongooseAdapter = class {
27
229
  schemaGenerator;
28
230
  constructor(options) {
29
231
  if (!isMongooseModel(options.model)) throw new TypeError("MongooseAdapter: Invalid model. Expected Mongoose Model instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
30
- if (!isRepository(options.repository)) throw new TypeError("MongooseAdapter: Invalid repository. Expected CrudRepository instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
232
+ if (!isRepository(options.repository)) throw new TypeError("MongooseAdapter: Invalid repository. Expected StandardRepo instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
31
233
  this.model = options.model;
32
234
  this.repository = options.repository;
33
235
  this.schemaGenerator = options.schemaGenerator;
@@ -73,7 +275,11 @@ var MongooseAdapter = class {
73
275
  */
74
276
  generateSchemas(schemaOptions, context) {
75
277
  try {
76
- if (this.schemaGenerator) return this.schemaGenerator(this.model, schemaOptions, context);
278
+ if (this.schemaGenerator) {
279
+ const generated = this.schemaGenerator(this.model, schemaOptions, context);
280
+ mergeFieldRuleConstraints(generated, schemaOptions);
281
+ return generated;
282
+ }
77
283
  const paths = this.model.schema.paths;
78
284
  const properties = {};
79
285
  const required = [];
@@ -242,43 +448,6 @@ function createMongooseAdapter(modelOrOptions, repository) {
242
448
  //#endregion
243
449
  //#region src/adapters/prisma.ts
244
450
  /**
245
- * Prisma Adapter - PostgreSQL/MySQL/SQLite Implementation
246
- *
247
- * @experimental This adapter is implemented but has no integration tests yet.
248
- * Use in production at your own risk. The Mongoose adapter is the recommended
249
- * and battle-tested path.
250
- *
251
- * Bridges Prisma Client with Arc's DataAdapter interface.
252
- * Supports Prisma 5+ with all database providers.
253
- *
254
- * Implemented features:
255
- * - Schema generation (OpenAPI docs from DMMF)
256
- * - Health checks (database connectivity)
257
- * - Query parsing (URL params → Prisma where/orderBy)
258
- * - Policy filter translation
259
- * - Soft delete preset support
260
- *
261
- * Known gaps:
262
- * - No integration test coverage
263
- * - Multi-tenant isolation relies on caller-provided policyFilters (no auto-enforcement)
264
- *
265
- * @example
266
- * ```typescript
267
- * import { PrismaClient, Prisma } from '@prisma/client';
268
- * import { createPrismaAdapter, PrismaQueryParser } from '@classytic/arc/adapters';
269
- *
270
- * const prisma = new PrismaClient();
271
- *
272
- * const userAdapter = createPrismaAdapter({
273
- * client: prisma,
274
- * modelName: 'user',
275
- * repository: new UserRepository(prisma),
276
- * dmmf: Prisma.dmmf, // For schema generation
277
- * queryParser: new PrismaQueryParser(), // Optional: custom parser
278
- * });
279
- * ```
280
- */
281
- /**
282
451
  * Prisma Query Parser - Converts URL parameters to Prisma query format
283
452
  *
284
453
  * Translates Arc's query format to Prisma's where/orderBy/take/skip structure.
@@ -723,4 +892,4 @@ function createPrismaAdapter(options) {
723
892
  return new PrismaAdapter(options);
724
893
  }
725
894
  //#endregion
726
- export { createMongooseAdapter as a, MongooseAdapter as i, PrismaQueryParser as n, createPrismaAdapter as r, PrismaAdapter as t };
895
+ export { createMongooseAdapter as a, MongooseAdapter as i, PrismaQueryParser as n, DrizzleAdapter as o, createPrismaAdapter as r, createDrizzleAdapter as s, PrismaAdapter as t };
@@ -1,5 +1,5 @@
1
- import { o as RepositoryLike } from "../interface-YrWsmKqE.mjs";
2
- import { i as UserBase } from "../types-DZi1aYhm.mjs";
1
+ import { d as UserBase } from "../fields-Lo1VUDpt.mjs";
2
+ import { kt as RepositoryLike } from "../index-Cl0uoKd5.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
5
5
  //#region src/audit/stores/interface.d.ts
@@ -59,6 +59,15 @@ interface AuditStore {
59
59
  log(entry: AuditEntry): Promise<void>;
60
60
  /** Query audit logs (optional - not all stores support querying) */
61
61
  query?(options: AuditQueryOptions): Promise<AuditEntry[]>;
62
+ /**
63
+ * Purge entries older than `cutoff`, return count deleted. Optional —
64
+ * stores that don't support deletion (append-only emitters like Kafka,
65
+ * S3 archivers) simply omit this method and are skipped by
66
+ * `fastify.audit.purge(...)`. Mongo-backed repositories can also rely
67
+ * on a server-side TTL index instead of calling this; the method is
68
+ * the DB-agnostic escape hatch.
69
+ */
70
+ purgeOlderThan?(cutoff: Date): Promise<number>;
62
71
  /** Close/cleanup (optional) */
63
72
  close?(): Promise<void>;
64
73
  }
@@ -104,6 +113,22 @@ interface AuditPluginOptions {
104
113
  * to every store.
105
114
  */
106
115
  customStores?: AuditStore[];
116
+ /**
117
+ * Retention policy — optional. Entries older than `maxAgeMs` are purged
118
+ * on a timer (`purgeIntervalMs`, default 24h). Stores that implement
119
+ * `purgeOlderThan` participate; append-only stores are skipped.
120
+ *
121
+ * Apps on MongoDB can instead declare a TTL index on the audit
122
+ * collection's `timestamp` field — server-side TTL is cheaper than a
123
+ * periodic delete. Both approaches coexist: `fastify.audit.purge(...)`
124
+ * is always available for manual / cron-driven purges.
125
+ *
126
+ * Set `purgeIntervalMs: 0` to skip the timer (manual purge only).
127
+ */
128
+ retention?: {
129
+ /** Max entry age in ms. Entries with `timestamp < now - maxAgeMs` are purged. */maxAgeMs: number; /** Interval between purges in ms. Default 86_400_000 (24h). 0 disables the timer. */
130
+ purgeIntervalMs?: number;
131
+ };
107
132
  /**
108
133
  * Automatically audit CRUD operations via the hook system (default: true when enabled).
109
134
  * When enabled, create/update/delete operations are auto-logged without manual calls.
@@ -167,10 +192,19 @@ interface AuditLogger {
167
192
  custom: (resource: string, documentId: string, action: string, data?: Record<string, unknown>, context?: AuditContext) => Promise<void>;
168
193
  /** Query audit logs (if stores support it) */
169
194
  query: (options: AuditQueryOptions) => Promise<AuditEntry[]>;
195
+ /**
196
+ * Purge audit entries older than `cutoff` across every registered store.
197
+ * Returns the total number of entries deleted. Stores that don't support
198
+ * deletion (append-only emitters) are skipped silently.
199
+ */
200
+ purge: (cutoff: Date) => Promise<number>;
170
201
  }
171
202
  declare const auditPlugin: FastifyPluginAsync<AuditPluginOptions>;
172
203
  declare const _default: FastifyPluginAsync<AuditPluginOptions>;
173
204
  //#endregion
205
+ //#region src/audit/repository-audit-adapter.d.ts
206
+ declare function repositoryAsAuditStore(repository: RepositoryLike): AuditStore;
207
+ //#endregion
174
208
  //#region src/audit/stores/memory.d.ts
175
209
  interface MemoryAuditStoreOptions {
176
210
  /** Maximum entries to keep (default: 1000) */
@@ -183,6 +217,7 @@ declare class MemoryAuditStore implements AuditStore {
183
217
  constructor(options?: MemoryAuditStoreOptions);
184
218
  log(entry: AuditEntry): Promise<void>;
185
219
  query(options?: AuditQueryOptions): Promise<AuditEntry[]>;
220
+ purgeOlderThan(cutoff: Date): Promise<number>;
186
221
  close(): Promise<void>;
187
222
  /** Get all entries (for testing) */
188
223
  getAll(): AuditEntry[];
@@ -190,4 +225,4 @@ declare class MemoryAuditStore implements AuditStore {
190
225
  clear(): void;
191
226
  }
192
227
  //#endregion
193
- export { type AuditAction, type AuditContext, type AuditEntry, type AuditLogger, type AuditPluginOptions, type AuditQueryOptions, type AuditStore, type AuditStoreOptions, MemoryAuditStore, type MemoryAuditStoreOptions, _default as auditPlugin, auditPlugin as auditPluginFn, createAuditEntry };
228
+ export { type AuditAction, type AuditContext, type AuditEntry, type AuditLogger, type AuditPluginOptions, type AuditQueryOptions, type AuditStore, type AuditStoreOptions, MemoryAuditStore, type MemoryAuditStoreOptions, _default as auditPlugin, auditPlugin as auditPluginFn, createAuditEntry, repositoryAsAuditStore };