@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.
- package/README.md +19 -90
- package/dist/{BaseController-Vu2yc56T.mjs → BaseController-CbKKIflT.mjs} +8 -44
- package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-BPd6NQDm.mjs} +1 -1
- package/dist/adapters/index.d.mts +3 -3
- package/dist/adapters/index.mjs +2 -2
- package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
- package/dist/audit/index.d.mts +38 -3
- package/dist/audit/index.mjs +41 -7
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +5 -5
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/cache/index.d.mts +17 -15
- package/dist/cache/index.mjs +15 -14
- package/dist/{caching-CjybdRwx.mjs → caching-CBpK_SCM.mjs} +8 -3
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -4
- package/dist/{defineResource-C__jkwvs.mjs → core-CcR01lup.mjs} +44 -12
- package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-Bp_5c_2b.mjs} +1 -1
- package/dist/{createApp-CBJUJKGP.mjs → createApp-BuvPma24.mjs} +14 -14
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DxQ6ACbt.mjs → elevation-C7hgL_aI.mjs} +2 -2
- package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-Bb49BvPD.mjs} +1 -1
- package/dist/{errorHandler-DixGcttC.d.mts → errorHandler-DRQ3EqfL.d.mts} +1 -1
- package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-CxWgpd6K.d.mts} +1 -1
- package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-DCUjuiQT.mjs} +1 -1
- package/dist/events/index.d.mts +8 -5
- package/dist/events/index.mjs +34 -17
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/{types-DZi1aYhm.d.mts → fields-Lo1VUDpt.d.mts} +121 -1
- package/dist/{filesUpload-q8oHt--L.mjs → filesUpload-t21LS-py.mjs} +2 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +7 -4
- package/dist/idempotency/index.mjs +9 -11
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-Cibkchnx.d.mts → index-8qw4y6ff.d.mts} +2 -2
- package/dist/{index-C-xjcA6F.d.mts → index-ChIw3776.d.mts} +283 -408
- package/dist/{interface-YrWsmKqE.d.mts → index-Cl0uoKd5.d.mts} +1885 -2741
- package/dist/{index-CtGKT0lf.d.mts → index-DStwgFUK.d.mts} +81 -7
- package/dist/index.d.mts +7 -8
- package/dist/index.mjs +11 -12
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/interface-D218ikEo.d.mts +77 -0
- package/dist/{memory-BFAYkf8H.mjs → memory-B5Amv9A1.mjs} +23 -8
- package/dist/{openapi-CXuTG1M9.mjs → openapi-B5F8AddX.mjs} +2 -2
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +3 -4
- package/dist/permissions/index.mjs +5 -5
- package/dist/{permissions-oNZawnkR.mjs → permissions-Dk6mshja.mjs} +315 -397
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +12 -14
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +91 -4
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-hM4WhNWY.mjs → presets-fLJVXdVn.mjs} +1 -1
- package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-BKbWjgDG.d.mts} +1 -1
- package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-DQCEfJis.mjs} +8 -8
- package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-DBqBB6AC.mjs} +1 -1
- package/dist/{redis-MXLp1oOf.d.mts → redis-DqyeggCa.d.mts} +1 -1
- package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-CakIQmwR.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BElv3xPT.mjs} +3 -3
- package/dist/scope/index.d.mts +1 -1
- package/dist/scope/index.mjs +2 -2
- package/dist/{sse-CJpt7LGI.mjs → sse-yBCgOLGu.mjs} +1 -1
- package/dist/testing/index.d.mts +6 -5
- package/dist/testing/index.mjs +8 -10
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/index.mjs +1 -31
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-CoSzA-s-.d.mts → types-Btdda02s.d.mts} +1 -1
- package/dist/{types-CunEX4UX.d.mts → types-Co8k3NyS.d.mts} +9 -9
- package/dist/types-Csi3FLfq.mjs +27 -0
- package/dist/utils/index.d.mts +207 -3
- package/dist/utils/index.mjs +3 -4
- package/dist/{utils-B7FuRr9w.mjs → utils-B2fNOD_i.mjs} +285 -2
- package/dist/{versioning-Cm8qoFDg.mjs → versioning-C2U_bLY0.mjs} +3 -5
- package/package.json +15 -18
- package/skills/arc/SKILL.md +7 -11
- package/skills/arc/references/production.md +0 -41
- package/dist/circuitBreaker-CvXkjfrW.d.mts +0 -206
- package/dist/circuitBreaker-l18oRgL5.mjs +0 -284
- package/dist/core-DNncu0xF.mjs +0 -34
- package/dist/dynamic/index.d.mts +0 -93
- package/dist/dynamic/index.mjs +0 -122
- package/dist/fields-BC7zcmI9.d.mts +0 -121
- package/dist/interface-DplgQO2e.d.mts +0 -54
- package/dist/policies/index.d.mts +0 -425
- package/dist/policies/index.mjs +0 -318
- package/dist/rpc/index.d.mts +0 -90
- package/dist/rpc/index.mjs +0 -248
- /package/dist/{EventTransport-CqZ8FyM_.d.mts → EventTransport-CUw5NNWe.d.mts} +0 -0
- /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-BNYKnrXF.mjs} +0 -0
- /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
- /package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-BBRVhjQN.mjs} +0 -0
- /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
- /package/dist/{elevation-B6S5csVA.d.mts → elevation-C5SwtkAn.d.mts} +0 -0
- /package/dist/{errors-BI8kEKsO.d.mts → errors-CCSsMpXE.d.mts} +0 -0
- /package/dist/{errors-CqWnSqM-.mjs → errors-D5c-5BJL.mjs} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
- /package/dist/{fields-CU6FlaDV.mjs → fields-bxkeltzz.mjs} +0 -0
- /package/dist/{interface-B-pe8fhj.d.mts → interface-CSbZdv_3.d.mts} +0 -0
- /package/dist/{loadResources-Bksk8ydA.mjs → loadResources-BAzJItAJ.mjs} +0 -0
- /package/dist/{logger-CDjpjySd.mjs → logger-DLg8-Ueg.mjs} +0 -0
- /package/dist/{metrics-TuOmguhi.mjs → metrics-DuhiSEZI.mjs} +0 -0
- /package/dist/{pluralize-CWP6MB39.mjs → pluralize-A0tWEl1K.mjs} +0 -0
- /package/dist/{registry-B0Wl7uVV.mjs → registry-B3lRFBWo.mjs} +0 -0
- /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
- /package/dist/{requestContext-DYtmNpm5.mjs → requestContext-xHIKedG6.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
- /package/dist/{storage-BwGQXUpd.d.mts → storage-CVk_SEn2.d.mts} +0 -0
- /package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-ZCSMJJAX.mjs} +0 -0
- /package/dist/{tracing-xqXzWeaf.d.mts → tracing-65B51Dw3.d.mts} +0 -0
- /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.
|
|
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
|
-
|
|
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
|
-
##
|
|
754
|
+
## Type imports
|
|
754
755
|
|
|
755
|
-
`
|
|
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
|
-
|
|
762
|
-
import {
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
5
|
-
import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-
|
|
6
|
-
import { t as getUserRoles } from "./types-
|
|
7
|
-
import { r as ForbiddenError } from "./errors-
|
|
8
|
-
import { t as ArcQueryParser } from "./queryParser-
|
|
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
|
-
|
|
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:
|
|
968
|
+
data: await repo.getDeleted(parsed, parsed),
|
|
1005
969
|
status: 200
|
|
1006
970
|
};
|
|
1007
971
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
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 };
|
package/dist/adapters/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-
|
|
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-
|
|
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
|
|
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)
|
|
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 };
|
package/dist/audit/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 };
|