@beignet/core 0.0.3 → 0.0.4
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/CHANGELOG.md +157 -0
- package/README.md +785 -43
- package/dist/application/index.d.ts +28 -2
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +140 -12
- package/dist/application/index.js.map +1 -1
- package/dist/client/client.d.ts +2 -2
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +136 -48
- package/dist/client/client.js.map +1 -1
- package/dist/client/error-messages.d.ts +14 -0
- package/dist/client/error-messages.d.ts.map +1 -0
- package/dist/client/error-messages.js +23 -0
- package/dist/client/error-messages.js.map +1 -0
- package/dist/client/index.d.ts +8 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +6 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +35 -5
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client-only.d.ts +8 -0
- package/dist/client-only.d.ts.map +1 -0
- package/dist/client-only.js +8 -0
- package/dist/client-only.js.map +1 -0
- package/dist/config/index.d.ts +5 -5
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/contracts/catalog-errors.d.ts +27 -0
- package/dist/contracts/catalog-errors.d.ts.map +1 -0
- package/dist/contracts/catalog-errors.js +69 -0
- package/dist/contracts/catalog-errors.js.map +1 -0
- package/dist/contracts/contract-builder.d.ts +15 -12
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +15 -41
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts +11 -8
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +13 -40
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/contract-like.d.ts +1 -1
- package/dist/contracts/contract-like.d.ts.map +1 -1
- package/dist/contracts/index.d.ts +13 -9
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +9 -5
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/openapi-meta.d.ts +48 -0
- package/dist/contracts/openapi-meta.d.ts.map +1 -1
- package/dist/contracts/openapi-meta.js +3 -0
- package/dist/contracts/openapi-meta.js.map +1 -1
- package/dist/contracts/path-template.d.ts +1 -1
- package/dist/contracts/path-template.js +2 -2
- package/dist/contracts/path-template.js.map +1 -1
- package/dist/contracts/schema-shape.d.ts +37 -0
- package/dist/contracts/schema-shape.d.ts.map +1 -0
- package/dist/contracts/schema-shape.js +61 -0
- package/dist/contracts/schema-shape.js.map +1 -0
- package/dist/contracts/success-status.d.ts +32 -0
- package/dist/contracts/success-status.d.ts.map +1 -0
- package/dist/contracts/success-status.js +18 -0
- package/dist/contracts/success-status.js.map +1 -0
- package/dist/contracts/types.d.ts +25 -5
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.js.map +1 -1
- package/dist/contracts/utils.d.ts +1 -1
- package/dist/contracts/utils.d.ts.map +1 -1
- package/dist/contracts/utils.js +1 -1
- package/dist/contracts/utils.js.map +1 -1
- package/dist/domain/events.d.ts +1 -1
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js +1 -1
- package/dist/domain/events.js.map +1 -1
- package/dist/domain/index.d.ts +3 -3
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +3 -3
- package/dist/domain/index.js.map +1 -1
- package/dist/errors/catalog.d.ts +9 -1
- package/dist/errors/catalog.d.ts.map +1 -1
- package/dist/errors/catalog.js +7 -1
- package/dist/errors/catalog.js.map +1 -1
- package/dist/errors/http.d.ts +10 -0
- package/dist/errors/http.d.ts.map +1 -1
- package/dist/errors/http.js +11 -1
- package/dist/errors/http.js.map +1 -1
- package/dist/errors/index.d.ts +4 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +4 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/errors/response.d.ts +4 -1
- package/dist/errors/response.d.ts.map +1 -1
- package/dist/errors/response.js.map +1 -1
- package/dist/events/index.d.ts +10 -12
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +10 -10
- package/dist/events/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +5 -3
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts +12 -14
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +13 -13
- package/dist/jobs/index.js.map +1 -1
- package/dist/notifications/index.d.ts +14 -16
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +14 -14
- package/dist/notifications/index.js.map +1 -1
- package/dist/openapi/index.d.ts +8 -3
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +41 -29
- package/dist/openapi/index.js.map +1 -1
- package/dist/openapi/schema-introspector.d.ts +37 -0
- package/dist/openapi/schema-introspector.d.ts.map +1 -1
- package/dist/openapi/schema-introspector.js +23 -17
- package/dist/openapi/schema-introspector.js.map +1 -1
- package/dist/outbox/index.d.ts +15 -6
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +60 -16
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/audit.d.ts +56 -10
- package/dist/ports/audit.d.ts.map +1 -1
- package/dist/ports/audit.js +71 -3
- package/dist/ports/audit.js.map +1 -1
- package/dist/ports/auth.d.ts +92 -0
- package/dist/ports/auth.d.ts.map +1 -1
- package/dist/ports/auth.js +92 -0
- package/dist/ports/auth.js.map +1 -1
- package/dist/ports/events.d.ts +2 -2
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/index.d.ts +62 -33
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +28 -34
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/policy.d.ts +32 -3
- package/dist/ports/policy.d.ts.map +1 -1
- package/dist/ports/policy.js +13 -2
- package/dist/ports/policy.js.map +1 -1
- package/dist/ports/testing.d.ts +1030 -2
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +1031 -1
- package/dist/ports/testing.js.map +1 -1
- package/dist/ports/unbound.d.ts +21 -0
- package/dist/ports/unbound.d.ts.map +1 -0
- package/dist/ports/unbound.js +57 -0
- package/dist/ports/unbound.js.map +1 -0
- package/dist/ports/unit-of-work.d.ts +1 -1
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +1 -1
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/providers/index.d.ts +3 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts +45 -4
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js +25 -6
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/providers/metadata.d.ts +39 -0
- package/dist/providers/metadata.d.ts.map +1 -0
- package/dist/providers/metadata.js +169 -0
- package/dist/providers/metadata.js.map +1 -0
- package/dist/providers/provider.d.ts +114 -9
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js +3 -20
- package/dist/providers/provider.js.map +1 -1
- package/dist/schedules/index.d.ts +94 -13
- package/dist/schedules/index.d.ts.map +1 -1
- package/dist/schedules/index.js +66 -12
- package/dist/schedules/index.js.map +1 -1
- package/dist/server/audit-context.d.ts +29 -0
- package/dist/server/audit-context.d.ts.map +1 -0
- package/dist/server/audit-context.js +44 -0
- package/dist/server/audit-context.js.map +1 -0
- package/dist/server/context.d.ts +141 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +39 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/contract-like.d.ts +1 -1
- package/dist/server/contract-like.d.ts.map +1 -1
- package/dist/server/contract-like.js +1 -1
- package/dist/server/contract-like.js.map +1 -1
- package/dist/server/health.d.ts +2 -2
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/hooks/auth.d.ts +49 -10
- package/dist/server/hooks/auth.d.ts.map +1 -1
- package/dist/server/hooks/auth.js +77 -37
- package/dist/server/hooks/auth.js.map +1 -1
- package/dist/server/hooks/cors.d.ts +1 -1
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/errors.d.ts +2 -2
- package/dist/server/hooks/errors.d.ts.map +1 -1
- package/dist/server/hooks/errors.js +2 -2
- package/dist/server/hooks/errors.js.map +1 -1
- package/dist/server/hooks/idempotency.d.ts +78 -0
- package/dist/server/hooks/idempotency.d.ts.map +1 -0
- package/dist/server/hooks/idempotency.js +154 -0
- package/dist/server/hooks/idempotency.js.map +1 -0
- package/dist/server/hooks/index.d.ts +8 -7
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js +6 -5
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/hooks/logging.d.ts +2 -2
- package/dist/server/hooks/logging.d.ts.map +1 -1
- package/dist/server/hooks/logging.js +1 -1
- package/dist/server/hooks/logging.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +25 -7
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +47 -12
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/hooks.d.ts +1 -1
- package/dist/server/hooks.d.ts.map +1 -1
- package/dist/server/hooks.js +1 -1
- package/dist/server/hooks.js.map +1 -1
- package/dist/server/http.d.ts +61 -35
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +1 -20
- package/dist/server/http.js.map +1 -1
- package/dist/server/index.d.ts +36 -12
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +24 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts +108 -0
- package/dist/server/instrumentation.d.ts.map +1 -0
- package/dist/server/instrumentation.js +297 -0
- package/dist/server/instrumentation.js.map +1 -0
- package/dist/server/openapi.d.ts +3 -3
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/openapi.js +1 -1
- package/dist/server/openapi.js.map +1 -1
- package/dist/server/providers/index.d.ts +3 -3
- package/dist/server/providers/index.d.ts.map +1 -1
- package/dist/server/providers/index.js +3 -3
- package/dist/server/providers/index.js.map +1 -1
- package/dist/server/providers/loadProviderConfig.d.ts +2 -2
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
- package/dist/server/providers/loadProviderConfig.js +2 -2
- package/dist/server/providers/loadProviderConfig.js.map +1 -1
- package/dist/server/request-context.d.ts +67 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/request-context.js +79 -0
- package/dist/server/request-context.js.map +1 -0
- package/dist/server/server-context.d.ts +38 -0
- package/dist/server/server-context.d.ts.map +1 -0
- package/dist/server/server-context.js +38 -0
- package/dist/server/server-context.js.map +1 -0
- package/dist/server/server.d.ts +105 -33
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +434 -118
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +2 -2
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -2
- package/dist/server/types.js.map +1 -1
- package/dist/server/use-case-route.d.ts +263 -0
- package/dist/server/use-case-route.d.ts.map +1 -0
- package/dist/server/use-case-route.js +77 -0
- package/dist/server/use-case-route.js.map +1 -0
- package/dist/server-only.d.ts +8 -0
- package/dist/server-only.d.ts.map +1 -0
- package/dist/server-only.js +8 -0
- package/dist/server-only.js.map +1 -0
- package/dist/tasks/index.d.ts +139 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +98 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/testing/index.d.ts +607 -5
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +426 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/index.d.ts +89 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +101 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/uploads/client.d.ts +1 -1
- package/dist/uploads/client.d.ts.map +1 -1
- package/dist/uploads/index.d.ts +2 -2
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +1 -1
- package/dist/uploads/index.js.map +1 -1
- package/package.json +24 -2
- package/src/application/index.ts +193 -10
- package/src/client/client.ts +148 -150
- package/src/client/error-messages.ts +35 -0
- package/src/client/index.ts +12 -4
- package/src/client/types.ts +44 -5
- package/src/client-only.ts +7 -0
- package/src/config/index.ts +6 -6
- package/src/contracts/catalog-errors.ts +115 -0
- package/src/contracts/contract-builder.ts +39 -76
- package/src/contracts/contract-group.ts +33 -68
- package/src/contracts/contract-like.ts +1 -1
- package/src/contracts/index.ts +24 -11
- package/src/contracts/openapi-meta.ts +55 -0
- package/src/contracts/path-template.ts +2 -2
- package/src/contracts/schema-shape.ts +75 -0
- package/src/contracts/success-status.ts +68 -0
- package/src/contracts/types.ts +32 -5
- package/src/contracts/utils.ts +5 -2
- package/src/domain/events.ts +6 -2
- package/src/domain/index.ts +3 -3
- package/src/errors/catalog.ts +9 -1
- package/src/errors/http.ts +11 -1
- package/src/errors/index.ts +4 -4
- package/src/errors/response.ts +4 -1
- package/src/events/index.ts +12 -26
- package/src/idempotency/index.ts +5 -3
- package/src/jobs/index.ts +14 -24
- package/src/notifications/index.ts +17 -27
- package/src/openapi/index.ts +73 -38
- package/src/openapi/schema-introspector.ts +68 -17
- package/src/outbox/index.ts +84 -19
- package/src/ports/audit.ts +120 -11
- package/src/ports/auth.ts +132 -0
- package/src/ports/events.ts +2 -2
- package/src/ports/index.ts +104 -35
- package/src/ports/policy.ts +50 -3
- package/src/ports/testing.ts +2220 -33
- package/src/ports/unbound.ts +64 -0
- package/src/ports/unit-of-work.ts +6 -2
- package/src/providers/index.ts +16 -3
- package/src/providers/instrumentation.ts +86 -7
- package/src/providers/metadata.ts +234 -0
- package/src/providers/provider.ts +168 -9
- package/src/schedules/index.ts +173 -23
- package/src/server/audit-context.ts +45 -0
- package/src/server/context.ts +224 -0
- package/src/server/contract-like.ts +1 -1
- package/src/server/health.ts +2 -2
- package/src/server/hooks/auth.ts +141 -51
- package/src/server/hooks/cors.ts +1 -1
- package/src/server/hooks/errors.ts +7 -4
- package/src/server/hooks/idempotency.ts +263 -0
- package/src/server/hooks/index.ts +14 -7
- package/src/server/hooks/logging.ts +3 -3
- package/src/server/hooks/rate-limit.ts +85 -17
- package/src/server/hooks.ts +1 -1
- package/src/server/http.ts +78 -51
- package/src/server/index.ts +62 -12
- package/src/server/instrumentation.ts +470 -0
- package/src/server/openapi.ts +4 -4
- package/src/server/providers/index.ts +6 -3
- package/src/server/providers/loadProviderConfig.ts +4 -4
- package/src/server/request-context.ts +116 -0
- package/src/server/server-context.ts +44 -0
- package/src/server/server.ts +886 -238
- package/src/server/types.ts +2 -2
- package/src/server/use-case-route.ts +430 -0
- package/src/server-only.ts +7 -0
- package/src/tasks/index.ts +275 -0
- package/src/testing/index.ts +1142 -6
- package/src/tracing/index.ts +176 -0
- package/src/uploads/client.ts +1 -1
- package/src/uploads/index.ts +7 -3
- package/dist/ports/mailer.d.ts +0 -6
- package/dist/ports/mailer.d.ts.map +0 -1
- package/dist/ports/mailer.js +0 -2
- package/dist/ports/mailer.js.map +0 -1
- package/dist/ports/schedules.d.ts +0 -9
- package/dist/ports/schedules.d.ts.map +0 -1
- package/dist/ports/schedules.js +0 -2
- package/dist/ports/schedules.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Core framework primitives for Beignet
|
|
4
4
|
|
|
5
|
+
> [!CAUTION]
|
|
6
|
+
> Beignet is experimental alpha software. The `0.0.x` package line is for early
|
|
7
|
+
> evaluation, and APIs may change between releases while the framework settles.
|
|
8
|
+
|
|
5
9
|
This package provides Beignet's framework primitives: contracts, server runtime,
|
|
6
10
|
typed client, use cases, ports, domain helpers, app errors, config, events,
|
|
7
11
|
idempotency, outbox, mail, notifications, schedules, uploads, pagination helpers,
|
|
@@ -35,6 +39,7 @@ name the framework area they depend on.
|
|
|
35
39
|
| --- | --- |
|
|
36
40
|
| `@beignet/core/application` | Use case builder and test helpers |
|
|
37
41
|
| `@beignet/core/client` | Typed HTTP client |
|
|
42
|
+
| `@beignet/core/client-only` | Static lint marker for modules intended for client-side imports |
|
|
38
43
|
| `@beignet/core/config` | Environment config validation |
|
|
39
44
|
| `@beignet/core/contracts` | HTTP contract builders, types, path helpers, and contract metadata |
|
|
40
45
|
| `@beignet/core/domain` | Entities, value objects, and domain events |
|
|
@@ -50,12 +55,180 @@ name the framework area they depend on.
|
|
|
50
55
|
| `@beignet/core/ports` | App-facing ports, auth, audit, policies, cache, storage, logging, and redaction |
|
|
51
56
|
| `@beignet/core/ports/testing` | Port and policy test helpers |
|
|
52
57
|
| `@beignet/core/providers` | Provider lifecycle and instrumentation primitives |
|
|
53
|
-
| `@beignet/core/schedules` |
|
|
58
|
+
| `@beignet/core/schedules` | Schedule primitives |
|
|
54
59
|
| `@beignet/core/server` | Framework-agnostic server runtime and hook helpers |
|
|
55
|
-
| `@beignet/core/
|
|
60
|
+
| `@beignet/core/server-only` | Static lint marker for modules that must stay out of client bundles |
|
|
61
|
+
| `@beignet/core/tasks` | Operational task definitions and inline task execution |
|
|
62
|
+
| `@beignet/core/testing` | Test context factories, memory port fixtures, provider install helper, factories, seeds, and database harnesses |
|
|
63
|
+
| `@beignet/core/tracing` | Dependency-free W3C trace context primitives |
|
|
56
64
|
| `@beignet/core/uploads` | Upload definitions, router, signer port, and test signer |
|
|
57
65
|
| `@beignet/core/uploads/client` | Browser upload client for server and direct uploads |
|
|
58
66
|
|
|
67
|
+
## Durable failure language
|
|
68
|
+
|
|
69
|
+
Jobs, outbox delivery, and schedule runners use the same terms:
|
|
70
|
+
|
|
71
|
+
- `attempt` is the one-based execution or delivery attempt currently being
|
|
72
|
+
handled.
|
|
73
|
+
- `attempts` in a retry policy is the maximum total attempts, including the
|
|
74
|
+
first try.
|
|
75
|
+
- `backoff` is the delay before the next retry.
|
|
76
|
+
- `terminal failure` means the work should not be retried automatically.
|
|
77
|
+
- `dead letter` is a durable terminal delivery state, currently owned by the
|
|
78
|
+
outbox.
|
|
79
|
+
|
|
80
|
+
Schedules do not own retry policies. They can carry provider attempt metadata
|
|
81
|
+
through `ScheduleRunContext.attempt`, then dispatch jobs or outbox messages when
|
|
82
|
+
the work needs Beignet-managed retry and dead-letter behavior.
|
|
83
|
+
|
|
84
|
+
Outbox drains emit first-class provider instrumentation for delivered, retried,
|
|
85
|
+
and dead-lettered messages when you pass a devtools or instrumentation port to
|
|
86
|
+
`drainOutbox(...)`. Pass `instrumentationContext` when the worker has request or
|
|
87
|
+
trace IDs that should connect the drain to devtools rows.
|
|
88
|
+
|
|
89
|
+
## Provider-contributed ports
|
|
90
|
+
|
|
91
|
+
Apps bind app-owned ports directly and defer the rest to providers with the
|
|
92
|
+
curried `definePorts<AppPorts>()({ bound, deferred })` form. Deferred keys boot
|
|
93
|
+
as throwing placeholders, and `createServer(...)` fails startup with the
|
|
94
|
+
unbound key list unless `onUnboundPorts` is set to `"warn"` or `"ignore"`.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { definePorts } from "@beignet/core/ports";
|
|
98
|
+
import type { AppPorts } from "@/ports";
|
|
99
|
+
|
|
100
|
+
export const appPorts = definePorts<AppPorts>()({
|
|
101
|
+
bound: { gate },
|
|
102
|
+
deferred: ["db", "logger", "mailer", "storage"],
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use `InferProviderPorts` with an `as const` provider list to type the runtime
|
|
107
|
+
ports without casts:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import type { InferProviderPorts } from "@beignet/core/providers";
|
|
111
|
+
import type { AppPorts } from "@/ports";
|
|
112
|
+
import type { providers } from "@/server/providers";
|
|
113
|
+
|
|
114
|
+
export type AppRuntimePorts = AppPorts & InferProviderPorts<typeof providers>;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
App-local providers can declare required ports, app context, and
|
|
118
|
+
service-context input through the curried `createProvider()` form. `setup`
|
|
119
|
+
then receives typed `ports` and a `createServiceContext` factory that returns
|
|
120
|
+
the app context:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { createProvider } from "@beignet/core/providers";
|
|
124
|
+
|
|
125
|
+
export const appDatabaseProvider = createProvider<
|
|
126
|
+
{ db: DbPort<typeof schema>; devtools?: DevtoolsPort },
|
|
127
|
+
AppContext,
|
|
128
|
+
AppServiceContextInput
|
|
129
|
+
>()({
|
|
130
|
+
name: "app-database",
|
|
131
|
+
async setup({ ports, createServiceContext }) {
|
|
132
|
+
const repositories = createRepositories(ports.db.db);
|
|
133
|
+
return { ports: repositories };
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Lifecycle hooks returned from `setup` should close over setup locals; a
|
|
139
|
+
`start(ctx)` hook with an unannotated parameter keeps TypeScript from inferring
|
|
140
|
+
the provided ports from the returned `ports` object.
|
|
141
|
+
|
|
142
|
+
## Provider metadata
|
|
143
|
+
|
|
144
|
+
Reusable provider packages should declare static metadata in `package.json`
|
|
145
|
+
under `beignet.provider`. That manifest metadata is package-owned and
|
|
146
|
+
side-effect-free, so CLI diagnostics can inspect installed provider packages
|
|
147
|
+
without importing provider implementation code.
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"beignet": {
|
|
152
|
+
"provider": {
|
|
153
|
+
"displayName": "Cache provider",
|
|
154
|
+
"ports": ["cache"],
|
|
155
|
+
"appPorts": [{ "name": "cache", "type": "CachePort" }],
|
|
156
|
+
"env": ["CACHE_URL", "CACHE_REGION"],
|
|
157
|
+
"requiredEnv": ["CACHE_URL"],
|
|
158
|
+
"registration": {
|
|
159
|
+
"required": true,
|
|
160
|
+
"tokens": ["cacheProvider", "createCacheProvider"]
|
|
161
|
+
},
|
|
162
|
+
"watchers": ["cache"]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`env` lists all variables the provider may read. `requiredEnv` is the subset
|
|
169
|
+
that `beignet doctor --strict` should require in app config.
|
|
170
|
+
|
|
171
|
+
`registration.required: true` marks providers that apps must register in
|
|
172
|
+
`server/providers.ts`; doctor reports a missing registration as a warning,
|
|
173
|
+
which fails `beignet doctor --strict`. Optional-by-design providers such as
|
|
174
|
+
`@beignet/devtools` can declare `registration.severity: "hint"` instead, so an
|
|
175
|
+
installed-but-unregistered package is reported as an informational hint that
|
|
176
|
+
never fails doctor, even in strict mode. Use
|
|
177
|
+
`parseProviderPackageMetadata(...)` to validate manifest metadata before
|
|
178
|
+
publishing a provider package.
|
|
179
|
+
|
|
180
|
+
Provider objects can also declare optional runtime-inert metadata for app-local
|
|
181
|
+
tooling and documentation. It does not change runtime setup; it describes the
|
|
182
|
+
package, contributed ports, required prior ports, env vars, and devtools
|
|
183
|
+
watchers owned by the provider.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { createProvider } from "@beignet/core/providers";
|
|
187
|
+
|
|
188
|
+
export const cacheProvider = createProvider({
|
|
189
|
+
name: "cache",
|
|
190
|
+
metadata: {
|
|
191
|
+
packageName: "@acme/beignet-provider-cache",
|
|
192
|
+
ports: ["cache"],
|
|
193
|
+
env: ["CACHE_URL"],
|
|
194
|
+
watchers: ["cache"],
|
|
195
|
+
},
|
|
196
|
+
setup() {
|
|
197
|
+
return { ports: { cache: createCachePort() } };
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Tasks
|
|
203
|
+
|
|
204
|
+
Use `@beignet/core/tasks` for app-owned operational entrypoints such as
|
|
205
|
+
backfills, maintenance work, and one-off repair scripts. Tasks are not HTTP
|
|
206
|
+
routes and are not background jobs; they are explicit functions a CLI or worker
|
|
207
|
+
can run with parsed input and an application context. Run them with
|
|
208
|
+
`runTask(...)` or `beignet task run`, and collect them with `defineTasks(...)`.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { createTasks } from "@beignet/core/tasks";
|
|
212
|
+
import { z } from "zod";
|
|
213
|
+
import type { AppContext } from "@/app-context";
|
|
214
|
+
|
|
215
|
+
const { defineTask } = createTasks<AppContext>();
|
|
216
|
+
|
|
217
|
+
export const backfillSearchTask = defineTask("posts.backfill-search", {
|
|
218
|
+
input: z.object({
|
|
219
|
+
dryRun: z.boolean().default(true),
|
|
220
|
+
}),
|
|
221
|
+
async handle({ input, ctx }) {
|
|
222
|
+
ctx.ports.logger.info("Backfill started", {
|
|
223
|
+
dryRun: input.dryRun,
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Feature-owned task files should usually call use cases, repositories, or
|
|
230
|
+
ports rather than hiding business rules inside a script.
|
|
231
|
+
|
|
59
232
|
## Key concepts
|
|
60
233
|
|
|
61
234
|
### Contract
|
|
@@ -77,10 +250,10 @@ A **contract group** allows you to share configuration across related endpoints,
|
|
|
77
250
|
|
|
78
251
|
```ts
|
|
79
252
|
import { z } from "zod";
|
|
80
|
-
import {
|
|
253
|
+
import { defineContractGroup } from "@beignet/core/contracts";
|
|
81
254
|
|
|
82
255
|
// Create a contract group for related endpoints
|
|
83
|
-
const todos =
|
|
256
|
+
const todos = defineContractGroup()
|
|
84
257
|
.namespace("todos")
|
|
85
258
|
.prefix("/api/todos")
|
|
86
259
|
.meta({ auth: "required" })
|
|
@@ -132,6 +305,22 @@ Clients and OpenAPI generation infer required path argument keys from literal
|
|
|
132
305
|
path templates. Use `.pathParams(...)` when you want runtime validation,
|
|
133
306
|
coercion, richer OpenAPI schemas, or parameter descriptions.
|
|
134
307
|
|
|
308
|
+
`createServer(...)` enforces registration-time guarantees: each method + path
|
|
309
|
+
may only be registered once, contract names must be unique across the route
|
|
310
|
+
registry because typed clients, OpenAPI operations, and devtools key on them,
|
|
311
|
+
and an introspectable `.pathParams(...)` object schema must declare exactly the
|
|
312
|
+
`:param` keys from the path template. Mismatches fail server startup with the
|
|
313
|
+
contract name and path. At dispatch time, a request that matches a registered
|
|
314
|
+
path with an unregistered method receives a framework-owned `405
|
|
315
|
+
METHOD_NOT_ALLOWED` response with an `Allow` header listing the registered
|
|
316
|
+
methods; `HEAD` is intentionally not served by `GET` handlers.
|
|
317
|
+
|
|
318
|
+
Contract path templates intentionally support concrete segments and
|
|
319
|
+
single-segment params such as `:id` and `[id]`. Framework or platform
|
|
320
|
+
catch-all route files can expose a central Beignet handler, but individual
|
|
321
|
+
contracts should stay on explicit paths; catch-all contract patterns such as
|
|
322
|
+
`/files/[...path]` are rejected.
|
|
323
|
+
|
|
135
324
|
Use `.headers(...)` for request headers that are part of the endpoint contract. Declare header keys in lowercase; server and client runtime matching is case-insensitive.
|
|
136
325
|
|
|
137
326
|
Request bodies are supported for `POST`, `PUT`, and `PATCH` contracts only.
|
|
@@ -139,10 +328,10 @@ Request bodies are supported for `POST`, `PUT`, and `PATCH` contracts only.
|
|
|
139
328
|
If you do not pass `name`, Beignet generates one from the HTTP method and full path:
|
|
140
329
|
|
|
141
330
|
```ts
|
|
142
|
-
|
|
331
|
+
defineContract({ method: "GET", path: "/users/:id" }).name;
|
|
143
332
|
// "getUsersById"
|
|
144
333
|
|
|
145
|
-
|
|
334
|
+
defineContract({ method: "POST", path: "/api/todos" }).name;
|
|
146
335
|
// "createTodos"
|
|
147
336
|
```
|
|
148
337
|
|
|
@@ -153,7 +342,7 @@ Auto-generated names ignore a leading `/api` segment, include path parameters as
|
|
|
153
342
|
Use `.prefix(...)` on a contract group to compose shared URL path segments without repeating them on every route:
|
|
154
343
|
|
|
155
344
|
```ts
|
|
156
|
-
const api =
|
|
345
|
+
const api = defineContractGroup().prefix("/api/v1");
|
|
157
346
|
|
|
158
347
|
const todos = api
|
|
159
348
|
.namespace("todos")
|
|
@@ -170,21 +359,198 @@ Prefixes compose immutably and normalize boundary slashes. `namespace()` control
|
|
|
170
359
|
resource identity for contract names, OpenAPI tags, and client cache grouping;
|
|
171
360
|
`prefix()` only controls URL paths.
|
|
172
361
|
|
|
362
|
+
### Test app fixtures
|
|
363
|
+
|
|
364
|
+
Use `@beignet/core/testing` to build app contexts and common memory ports
|
|
365
|
+
without hand-rolling audit, event, job, mail, notification, outbox, storage,
|
|
366
|
+
idempotency, logger, clock, and UOW setup in every test:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { createUseCaseTester } from "@beignet/core/application";
|
|
370
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
371
|
+
import {
|
|
372
|
+
createTestTenant,
|
|
373
|
+
createTestUserActor,
|
|
374
|
+
} from "@beignet/core/ports/testing";
|
|
375
|
+
|
|
376
|
+
const fixture = createTestPorts<AppContext["ports"]>({
|
|
377
|
+
base: appPorts,
|
|
378
|
+
overrides: {
|
|
379
|
+
gate: appPorts.gate,
|
|
380
|
+
posts: { findById: async (id) => postRecord(id) },
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
const createContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
384
|
+
ports: fixture.ports,
|
|
385
|
+
actor: createTestUserActor("user_test"),
|
|
386
|
+
auth: { user: { id: "user_test" } },
|
|
387
|
+
tenant: createTestTenant("tenant_example"),
|
|
388
|
+
});
|
|
389
|
+
const tester = createUseCaseTester<AppContext>(createContext);
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The returned fixture exposes captured side effects such as `events`,
|
|
393
|
+
`dispatchedJobs`, `audit.entries`, `mailer.deliveries`,
|
|
394
|
+
`notifications.deliveries`, `outbox.messages`, and memory storage for
|
|
395
|
+
assertions.
|
|
396
|
+
|
|
397
|
+
`overrides` is typed as `TestPortsOverrides<Ports>`, which accepts typed
|
|
398
|
+
partial ports without casts. The partial rule is one level deep: an
|
|
399
|
+
object-valued port may supply only the members the test needs, and any missing
|
|
400
|
+
member becomes a named throwing function (`Test port "posts.update" was called
|
|
401
|
+
but not provided.`). Function-valued ports, class instances, and other exotic
|
|
402
|
+
objects are supplied whole — nested config objects are not partial.
|
|
403
|
+
|
|
404
|
+
The default `audit` port is wrapped with `createAmbientAuditLog(...)`, so
|
|
405
|
+
entries recorded inside an active request context inherit actor, tenant,
|
|
406
|
+
request ID, and trace ID exactly like production. `fixture.audit` still
|
|
407
|
+
exposes the underlying memory port for `entries` assertions.
|
|
408
|
+
|
|
409
|
+
#### One-call test contexts
|
|
410
|
+
|
|
411
|
+
Use `createTestContext(...)` when a job, listener, schedule, notification, or
|
|
412
|
+
task test needs a full app context instead of a repeated factory:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
import { createTestContext } from "@beignet/core/testing";
|
|
416
|
+
|
|
417
|
+
const makeContext = createTestContext<AppContext>();
|
|
418
|
+
|
|
419
|
+
it("audits handled jobs", async () => {
|
|
420
|
+
using fixture = makeContext({
|
|
421
|
+
ports: { issues: { findById: async (id) => issueRecord(id) } },
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
await IndexIssueJob.handle({ job: IndexIssueJob, payload, ctx: fixture.ctx });
|
|
425
|
+
|
|
426
|
+
expect(fixture.audit.entries).toMatchObject([
|
|
427
|
+
{ action: "jobs.issues.index", requestId: "test-request" },
|
|
428
|
+
]);
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
The fixture assembles `ctx` with actor (default
|
|
433
|
+
`createTestSystemActor("test-system")`), tenant, request ID, trace ID, `auth`,
|
|
434
|
+
ports, and a live bound `ctx.gate`. It also enters the ambient request context
|
|
435
|
+
so ambient enrichment (such as the default audit port) behaves like the
|
|
436
|
+
server; `using` (or an explicit `dispose()`) clears it:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
let fixture: ReturnType<ReturnType<typeof createTestContext<AppContext>>>;
|
|
440
|
+
|
|
441
|
+
afterEach(() => {
|
|
442
|
+
fixture.dispose();
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Pass `ambient: false` to skip ambient entry. Reading an app port that is
|
|
447
|
+
neither a kit default nor supplied throws a named error
|
|
448
|
+
(`App port "tweets" is not bound in this test context.`), so partial port
|
|
449
|
+
wiring fails on use instead of failing silently.
|
|
450
|
+
|
|
451
|
+
#### Transactional domain events
|
|
452
|
+
|
|
453
|
+
When a use case records domain events through a buffered recorder on the
|
|
454
|
+
transaction ports, pass `transaction.outbox: true` to flush `tx.events` to
|
|
455
|
+
`ports.eventBus` after commit and clear it after rollback:
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
import { createDomainEventRecorder } from "@beignet/core/ports";
|
|
459
|
+
|
|
460
|
+
const fixture = createTestPorts<AppContext["ports"], AppTransactionPorts>({
|
|
461
|
+
transaction: {
|
|
462
|
+
ports: (ports) => ({ ...ports, events: createDomainEventRecorder() }),
|
|
463
|
+
outbox: true,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`transaction.outbox` requires `transaction.ports` to include an `events`
|
|
469
|
+
recorder such as `createDomainEventRecorder()` or
|
|
470
|
+
`createOutboxEventRecorder(...)`; the kit throws a named error otherwise.
|
|
471
|
+
|
|
472
|
+
#### Sharing the server context blueprint
|
|
473
|
+
|
|
474
|
+
Declare the `context` blueprint once with `defineServerContext(...)` from
|
|
475
|
+
`@beignet/core/server` and keep it in a canonical `server/context.ts` file.
|
|
476
|
+
The same value round-trips through `createServer(...)` adapters and
|
|
477
|
+
`createTestApp(...)` with full inference:
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
// server/context.ts
|
|
481
|
+
import { defineServerContext } from "@beignet/core/server";
|
|
482
|
+
|
|
483
|
+
export const appContext = defineServerContext<AppContext, AppPorts>()({
|
|
484
|
+
gate: (ports) => ports.gate,
|
|
485
|
+
request: async ({ req, ports, requestId, trace }) => ({
|
|
486
|
+
actor: await resolveActor(req),
|
|
487
|
+
auth: null,
|
|
488
|
+
requestId,
|
|
489
|
+
...trace,
|
|
490
|
+
ports,
|
|
491
|
+
}),
|
|
492
|
+
service: ({ ports, requestId, trace }) => ({
|
|
493
|
+
actor: createServiceActor("app-service"),
|
|
494
|
+
auth: null,
|
|
495
|
+
requestId,
|
|
496
|
+
...trace,
|
|
497
|
+
ports,
|
|
498
|
+
}),
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
// server/index.ts
|
|
504
|
+
const server = await createNextServer({ ports, routes, context: appContext });
|
|
505
|
+
|
|
506
|
+
// features/<feature>/tests/routes.test.ts
|
|
507
|
+
const app = await createTestApp({ ports, routes, context: appContext });
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Testing providers
|
|
511
|
+
|
|
512
|
+
Use `installProviderForTest(...)` to run provider setup against test ports
|
|
513
|
+
without hand-rolling setup, port merge, and lifecycle plumbing:
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
import { installProviderForTest } from "@beignet/core/testing";
|
|
517
|
+
|
|
518
|
+
const { ports, result, start, stop } = await installProviderForTest(
|
|
519
|
+
redisProvider,
|
|
520
|
+
{
|
|
521
|
+
ports: { devtools },
|
|
522
|
+
config: { URL: "redis://localhost:6379" },
|
|
523
|
+
},
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const cache = ports.cache as CachePort;
|
|
527
|
+
await cache.set("posts:list", "[]");
|
|
528
|
+
|
|
529
|
+
await stop();
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
`ports` contains the base ports merged with provider-contributed ports, and
|
|
533
|
+
`result` exposes the raw setup result for lifecycle-hook assertions. `config`
|
|
534
|
+
is passed to setup as-is, matching server startup where config is validated
|
|
535
|
+
before setup runs. Pass `createServiceContext` when the provider builds
|
|
536
|
+
service contexts from runtime entrypoints.
|
|
537
|
+
|
|
173
538
|
### Test factories and seeds
|
|
174
539
|
|
|
175
|
-
Use
|
|
176
|
-
|
|
177
|
-
|
|
540
|
+
Use the same subpath to keep feature tests and demo seed data port-based.
|
|
541
|
+
Factories build app-owned records, and optional `persist` functions write
|
|
542
|
+
through the context you pass in:
|
|
178
543
|
|
|
179
544
|
```ts
|
|
180
545
|
import {
|
|
181
|
-
|
|
546
|
+
createDatabaseTestHarness,
|
|
547
|
+
createFactory,
|
|
182
548
|
defineSeed,
|
|
183
549
|
resetFactories,
|
|
184
550
|
runSeeds,
|
|
185
551
|
} from "@beignet/core/testing";
|
|
186
552
|
|
|
187
|
-
const postFactory =
|
|
553
|
+
const postFactory = createFactory("post", {
|
|
188
554
|
defaults: ({ sequence }) => ({
|
|
189
555
|
title: `Post ${sequence}`,
|
|
190
556
|
content: "Created in a test.",
|
|
@@ -207,9 +573,173 @@ export function resetPostFactories() {
|
|
|
207
573
|
}
|
|
208
574
|
```
|
|
209
575
|
|
|
576
|
+
For repository and persistence tests, compose the app-owned database fixture
|
|
577
|
+
with the same factories and seeds:
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
const databaseHarness = createDatabaseTestHarness({
|
|
581
|
+
create: createTestDatabase,
|
|
582
|
+
ctx: (database) => ({ ports: database.ports }),
|
|
583
|
+
reset: (database) => database.reset(),
|
|
584
|
+
close: (database) => database.close(),
|
|
585
|
+
factories: [postFactory],
|
|
586
|
+
seeds: [demoPostsSeed],
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
afterEach(async () => {
|
|
590
|
+
await databaseHarness.cleanup();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const { ctx } = await databaseHarness.setup({ seed: true });
|
|
594
|
+
const post = await postFactory.create(ctx, { title: "Database conventions" });
|
|
595
|
+
```
|
|
596
|
+
|
|
210
597
|
Keep factories and seeds app-owned. They should not import database clients,
|
|
211
598
|
ORM table objects, or provider SDKs directly.
|
|
212
599
|
|
|
600
|
+
### Port testing helpers
|
|
601
|
+
|
|
602
|
+
Use `@beignet/core/ports/testing` when tests need stable actor, tenant,
|
|
603
|
+
authorization, or audit assertions:
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
import {
|
|
607
|
+
assertAuditEntry,
|
|
608
|
+
createPolicyTester,
|
|
609
|
+
createTestActivityContext,
|
|
610
|
+
createTestTenant,
|
|
611
|
+
createTestUserActor,
|
|
612
|
+
} from "@beignet/core/ports/testing";
|
|
613
|
+
|
|
614
|
+
const activity = createTestActivityContext({
|
|
615
|
+
actor: createTestUserActor("user_1", { role: "admin" }),
|
|
616
|
+
tenant: createTestTenant("tenant_1"),
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const tester = createPolicyTester({ policies: [postPolicy] });
|
|
620
|
+
await tester.assertMatrix([
|
|
621
|
+
{
|
|
622
|
+
name: "admin can publish",
|
|
623
|
+
ctx: activity,
|
|
624
|
+
ability: "posts.publish",
|
|
625
|
+
subject: post,
|
|
626
|
+
expected: "allow",
|
|
627
|
+
},
|
|
628
|
+
]);
|
|
629
|
+
|
|
630
|
+
assertAuditEntry(audit.entries, {
|
|
631
|
+
action: "posts.publish",
|
|
632
|
+
actorId: "user_1",
|
|
633
|
+
tenantId: "tenant_1",
|
|
634
|
+
resourceType: "post",
|
|
635
|
+
resourceId: post.id,
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
`createTestImpersonatedUserActor(...)` is available for tests where an admin or
|
|
640
|
+
support actor is acting as another user and audit metadata should record the
|
|
641
|
+
impersonator ID.
|
|
642
|
+
|
|
643
|
+
The same subpath includes assertion helpers for common provider-backed test
|
|
644
|
+
adapters:
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
import {
|
|
648
|
+
assertDispatchedJob,
|
|
649
|
+
assertIdempotencyCompleted,
|
|
650
|
+
assertMailDelivery,
|
|
651
|
+
assertNotificationDelivery,
|
|
652
|
+
assertOutboxDelivered,
|
|
653
|
+
assertOutboxDrainResult,
|
|
654
|
+
assertOutboxPending,
|
|
655
|
+
assertProviderInstrumentationEvent,
|
|
656
|
+
assertRecordedEvent,
|
|
657
|
+
assertStorageObject,
|
|
658
|
+
createRecordingEventBus,
|
|
659
|
+
createRecordingJobDispatcher,
|
|
660
|
+
createRecordingProviderInstrumentation,
|
|
661
|
+
} from "@beignet/core/ports/testing";
|
|
662
|
+
import { drainOutbox } from "@beignet/core/outbox";
|
|
663
|
+
import { createProviderInstrumentation } from "@beignet/core/providers";
|
|
664
|
+
|
|
665
|
+
const { bus, events } = createRecordingEventBus();
|
|
666
|
+
const { jobs, dispatchedJobs } = createRecordingJobDispatcher();
|
|
667
|
+
|
|
668
|
+
await bus.publish(PostPublished, { postId: post.id });
|
|
669
|
+
await jobs.dispatch(LogPostPublishedJob, { postId: post.id });
|
|
670
|
+
|
|
671
|
+
assertRecordedEvent(events, {
|
|
672
|
+
name: "posts.published",
|
|
673
|
+
payload: { postId: post.id },
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
assertDispatchedJob(dispatchedJobs, {
|
|
677
|
+
name: "posts.log-published",
|
|
678
|
+
payload: { postId: post.id },
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
assertNotificationDelivery(notifications.deliveries, {
|
|
682
|
+
notificationName: "posts.published",
|
|
683
|
+
channels: ["email"],
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
assertMailDelivery(mailer.deliveries, {
|
|
687
|
+
subject: "Post published",
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
await assertStorageObject(storage, {
|
|
691
|
+
key: "posts/post_1/attachment.txt",
|
|
692
|
+
text: "hello",
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
assertIdempotencyCompleted(fixture.idempotency, {
|
|
696
|
+
namespace: "posts.create",
|
|
697
|
+
key: "idem_1",
|
|
698
|
+
result: { id: post.id },
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
assertOutboxPending(outbox, {
|
|
702
|
+
kind: "event",
|
|
703
|
+
name: "posts.published",
|
|
704
|
+
payload: { postId: post.id },
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const result = await drainOutbox({ outbox, registry, eventBus, jobs });
|
|
708
|
+
|
|
709
|
+
assertOutboxDrainResult(result, {
|
|
710
|
+
claimed: 1,
|
|
711
|
+
delivered: 1,
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
assertOutboxDelivered(outbox.messages, {
|
|
715
|
+
kind: "event",
|
|
716
|
+
name: "posts.published",
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const { instrumentation, events: providerEvents } =
|
|
720
|
+
createRecordingProviderInstrumentation();
|
|
721
|
+
const providerInstrumentation = createProviderInstrumentation(instrumentation, {
|
|
722
|
+
providerName: "redis",
|
|
723
|
+
watcher: "providers",
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
providerInstrumentation.custom({
|
|
727
|
+
name: "cache.get",
|
|
728
|
+
details: { key: "posts:list", hit: true },
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
assertProviderInstrumentationEvent(providerEvents, {
|
|
732
|
+
type: "custom",
|
|
733
|
+
name: "cache.get",
|
|
734
|
+
providerName: "redis",
|
|
735
|
+
details: { hit: true },
|
|
736
|
+
});
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
`createProviderInstrumentation(...)` adds `details.providerName` to custom and
|
|
740
|
+
typed provider events, so tests and devtools can group provider work
|
|
741
|
+
consistently.
|
|
742
|
+
|
|
213
743
|
### Pagination
|
|
214
744
|
|
|
215
745
|
Use `@beignet/core/pagination` to keep list use cases and repository ports
|
|
@@ -233,10 +763,65 @@ return ctx.ports.posts.findMany({
|
|
|
233
763
|
Beignet's convention is `items` for list contents and `page` for pagination
|
|
234
764
|
metadata. Keep filters and sort options app-owned plain objects.
|
|
235
765
|
|
|
766
|
+
### Rate limiting
|
|
767
|
+
|
|
768
|
+
Use `createRateLimitHooks(...)` from `@beignet/core/server` to enforce
|
|
769
|
+
`contract.metadata.rateLimit` at the HTTP boundary:
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
import { createRateLimitHooks } from "@beignet/core/server";
|
|
773
|
+
|
|
774
|
+
const server = await createServer<AppContext, AppPorts>({
|
|
775
|
+
ports: appPorts,
|
|
776
|
+
hooks: [createRateLimitHooks<AppContext>()],
|
|
777
|
+
// ...
|
|
778
|
+
});
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
- `global` and `ip` scopes run in `onRequest` before parsing and context
|
|
782
|
+
creation; `user` scope runs in `beforeHandle` once `ctx.actor` exists.
|
|
783
|
+
- `ip` buckets default to the last `x-forwarded-for` entry, the address
|
|
784
|
+
appended by the platform's trusted proxy. Use the `ipSource` option to
|
|
785
|
+
switch to `"x-forwarded-for-first"` behind a header-normalizing edge, or
|
|
786
|
+
pass a function for platform headers such as `cf-connecting-ip`.
|
|
787
|
+
- Denials throw the framework `429 Too Many Requests` catalog error with
|
|
788
|
+
`scope`, `retryAfterSeconds`, and `resetAt` details. The bucket key is
|
|
789
|
+
never included in the client-visible response.
|
|
790
|
+
- Each denial emits a `rateLimit.denied` instrumentation event carrying the
|
|
791
|
+
key, scope, limit, and window when the app ports include an
|
|
792
|
+
`instrumentation` or `devtools` sink, so operators keep bucket visibility.
|
|
793
|
+
|
|
236
794
|
### Idempotency
|
|
237
795
|
|
|
238
|
-
Use `@beignet/core/
|
|
239
|
-
|
|
796
|
+
Use `createIdempotencyHooks(...)` from `@beignet/core/server` to enforce
|
|
797
|
+
`contract.metadata.idempotency` at the HTTP boundary, mirroring
|
|
798
|
+
`createRateLimitHooks(...)`:
|
|
799
|
+
|
|
800
|
+
```ts
|
|
801
|
+
import { createIdempotencyHooks } from "@beignet/core/server";
|
|
802
|
+
|
|
803
|
+
const server = await createServer<AppContext, AppPorts>({
|
|
804
|
+
ports: appPorts,
|
|
805
|
+
hooks: [createIdempotencyHooks<AppContext>()],
|
|
806
|
+
// ...
|
|
807
|
+
});
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
The hook reads the key from the metadata header (default `idempotency-key`),
|
|
811
|
+
reserves it through `ctx.ports.idempotency` after request parsing, replays
|
|
812
|
+
completed matching responses with an `idempotency-replayed: true` header, and
|
|
813
|
+
maps in-progress and conflicting keys to framework-owned `409` responses using
|
|
814
|
+
the `httpErrors.IdempotencyInProgress` and `httpErrors.IdempotencyConflict`
|
|
815
|
+
catalog entries.
|
|
816
|
+
|
|
817
|
+
Typed clients read the same metadata: `createClient(...)` endpoints attach a
|
|
818
|
+
generated UUID to the metadata header on every call (injected before request
|
|
819
|
+
header validation, so header schemas pass), and the header becomes optional in
|
|
820
|
+
call types. Pass `idempotencyKey` as a call option for retry-with-same-key
|
|
821
|
+
flows; an explicit `headers` value always wins over generation.
|
|
822
|
+
|
|
823
|
+
Use `runIdempotently(...)` from `@beignet/core/idempotency` when a non-HTTP
|
|
824
|
+
command, webhook, or job may be retried and must not perform duplicate work:
|
|
240
825
|
|
|
241
826
|
```ts
|
|
242
827
|
import {
|
|
@@ -245,17 +830,17 @@ import {
|
|
|
245
830
|
} from "@beignet/core/idempotency";
|
|
246
831
|
|
|
247
832
|
const result = await runIdempotently(ctx.ports.idempotency, {
|
|
248
|
-
namespace: "todos.
|
|
249
|
-
key: input.
|
|
833
|
+
namespace: "todos.import",
|
|
834
|
+
key: input.importId,
|
|
250
835
|
scope: {
|
|
251
836
|
tenantId: ctx.tenant?.id,
|
|
252
837
|
actorId: ctx.actor?.id,
|
|
253
838
|
},
|
|
254
839
|
fingerprint: await createIdempotencyFingerprint(input, {
|
|
255
|
-
omit: ["
|
|
840
|
+
omit: ["importId"],
|
|
256
841
|
}),
|
|
257
842
|
ttlSec: 60 * 60 * 24,
|
|
258
|
-
run: () => ctx.ports.uow.transaction((tx) => tx.todos.
|
|
843
|
+
run: () => ctx.ports.uow.transaction((tx) => tx.todos.importBatch(input)),
|
|
259
844
|
});
|
|
260
845
|
```
|
|
261
846
|
|
|
@@ -268,9 +853,11 @@ const idempotency = createMemoryIdempotencyStore();
|
|
|
268
853
|
```
|
|
269
854
|
|
|
270
855
|
Production apps should back `IdempotencyPort` with atomic SQL or Redis storage.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
856
|
+
The Drizzle/libSQL path can use `createDrizzleTursoIdempotencyPort(...)` from
|
|
857
|
+
`@beignet/provider-drizzle-turso`. For high-integrity workflows, prefer exposing
|
|
858
|
+
a transaction-scoped `tx.idempotency` port from the app Unit of Work so
|
|
859
|
+
reservation, business writes, audit records, domain-event records, and
|
|
860
|
+
idempotency completion commit together.
|
|
274
861
|
|
|
275
862
|
### Outbox
|
|
276
863
|
|
|
@@ -317,6 +904,33 @@ await drainOutbox({
|
|
|
317
904
|
The outbox is at-least-once delivery. Use idempotent listeners or jobs when a
|
|
318
905
|
duplicate delivery would be harmful.
|
|
319
906
|
|
|
907
|
+
### Schedules
|
|
908
|
+
|
|
909
|
+
Use `@beignet/core/schedules` to define typed schedules and run them
|
|
910
|
+
inline from cron routes, workers, scripts, and tests. Pass a
|
|
911
|
+
devtools-compatible sink as `instrumentation` and the inline runner records
|
|
912
|
+
`schedule` devtools events (`started`, `completed`, `failed`) for each run:
|
|
913
|
+
|
|
914
|
+
```ts
|
|
915
|
+
import { createInlineScheduleRunner } from "@beignet/core/schedules";
|
|
916
|
+
|
|
917
|
+
const runner = createInlineScheduleRunner<AppContext>({
|
|
918
|
+
ctx,
|
|
919
|
+
instrumentation: ctx.ports.devtools,
|
|
920
|
+
instrumentationContext: {
|
|
921
|
+
requestId: ctx.requestId,
|
|
922
|
+
traceId: ctx.traceId,
|
|
923
|
+
},
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
await runner.run(SendDailyDigestSchedule, { source: "vercel-cron" });
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
`instrumentationContext` attaches request correlation fields to recorded
|
|
930
|
+
events. Recording failures are isolated from schedule execution and reported
|
|
931
|
+
to `onHookError` when provided. Handler failures still reject `runner.run(...)`
|
|
932
|
+
after `onError` runs so trigger hosts can retry.
|
|
933
|
+
|
|
320
934
|
### Contract metadata and route hooks
|
|
321
935
|
|
|
322
936
|
Use metadata to describe cross-cutting concerns for OpenAPI, clients, docs, and
|
|
@@ -343,41 +957,159 @@ const sendMessage = messages
|
|
|
343
957
|
});
|
|
344
958
|
```
|
|
345
959
|
|
|
346
|
-
|
|
960
|
+
The built-in server hooks enforce `rateLimit` and `idempotency` metadata:
|
|
961
|
+
install `createRateLimitHooks(...)` and `createIdempotencyHooks(...)` where the
|
|
962
|
+
server is composed. Use route hooks for runtime enforcement of route-specific
|
|
963
|
+
policy where the route is wired:
|
|
347
964
|
|
|
348
965
|
```ts
|
|
349
|
-
import {
|
|
350
|
-
|
|
351
|
-
defineRoute,
|
|
352
|
-
defineRouteGroup,
|
|
353
|
-
} from "@beignet/core/server";
|
|
354
|
-
|
|
355
|
-
const auth = createAuthHooks<AppContext, { user: CurrentUser }>({
|
|
356
|
-
resolve: async ({ ctx, req }) => {
|
|
357
|
-
const session = await ctx.ports.auth.getSession(req);
|
|
966
|
+
import { createAuthHooks, defineRouteGroup } from "@beignet/core/server";
|
|
967
|
+
import type { AppContext } from "@/app-context";
|
|
358
968
|
|
|
359
|
-
|
|
969
|
+
const auth = createAuthHooks<AppContext>()({
|
|
970
|
+
resolve: ({ ctx }) => {
|
|
971
|
+
return ctx.auth ? { user: ctx.auth.user } : null;
|
|
360
972
|
},
|
|
361
973
|
});
|
|
362
974
|
|
|
363
|
-
const route = defineRoute<AppContext>();
|
|
364
|
-
|
|
365
975
|
export const messageRoutes = defineRouteGroup<AppContext>()({
|
|
366
976
|
name: "messages",
|
|
367
977
|
routes: [
|
|
368
|
-
|
|
978
|
+
{
|
|
369
979
|
contract: sendMessage,
|
|
370
980
|
hooks: [auth.required()],
|
|
371
|
-
|
|
372
|
-
|
|
981
|
+
useCase: sendMessageUseCase,
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
});
|
|
985
|
+
```
|
|
373
986
|
|
|
374
|
-
|
|
375
|
-
|
|
987
|
+
Ordinary app routes bind `{ contract, useCase }`. The response status is
|
|
988
|
+
inferred when the contract declares exactly one `2xx` response (otherwise
|
|
989
|
+
`status` is required and typed to the declared keys), and the use case input
|
|
990
|
+
defaults to the merged request parts via `defaultBinderInput` — query lowest,
|
|
991
|
+
then body, then path; headers are never merged and need an explicit
|
|
992
|
+
`input: (parts) => ...` mapper. When the use case `.input(...)` schema is the
|
|
993
|
+
contract's sole request schema by reference, the server skips the use case's
|
|
994
|
+
input re-parse — one schema, one parse.
|
|
995
|
+
|
|
996
|
+
Use `{ contract, handle }` as the escape hatch for response headers,
|
|
997
|
+
streaming, and multi-status responses. `defineRoute<AppContext>()` remains
|
|
998
|
+
available for full handlers that read hook-added `ctx` fields.
|
|
999
|
+
|
|
1000
|
+
When credentials live in request headers, declare a `headers` schema on the
|
|
1001
|
+
auth hooks. The hook validates the raw lowercase request header record before
|
|
1002
|
+
`resolve` runs, so `resolve` receives typed header values; on `required()`
|
|
1003
|
+
routes a schema failure returns a framework-owned `401`:
|
|
1004
|
+
|
|
1005
|
+
```ts
|
|
1006
|
+
const writerAuth = createAuthHooks<AppContext>()({
|
|
1007
|
+
name: "writer",
|
|
1008
|
+
headers: writerHeadersSchema,
|
|
1009
|
+
resolve: ({ headers }) => ({
|
|
1010
|
+
actor: createUserActor(headers["x-user-id"]),
|
|
1011
|
+
}),
|
|
1012
|
+
});
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
### HTTP adapter boundary
|
|
1016
|
+
|
|
1017
|
+
`@beignet/core/server` is framework-neutral. It owns route matching, hooks,
|
|
1018
|
+
request validation, response validation, error mapping, and provider lifecycle.
|
|
1019
|
+
Adapters own the platform edge only:
|
|
1020
|
+
|
|
1021
|
+
- Convert the native request into `HttpRequestLike`
|
|
1022
|
+
- Call `server.api(...)` or a single route handler
|
|
1023
|
+
- Convert `HttpResponse` back into the native response type
|
|
1024
|
+
|
|
1025
|
+
The public adapter contract is `HttpAdapter<NativeRequest, NativeResponse>`.
|
|
1026
|
+
Use it when building a runtime package beyond the first-party `@beignet/web`
|
|
1027
|
+
and `@beignet/next` adapters.
|
|
1028
|
+
|
|
1029
|
+
### Request instrumentation and tracing
|
|
1030
|
+
|
|
1031
|
+
`createServer(...)` owns request instrumentation. For every request it
|
|
1032
|
+
resolves a request ID (from `x-request-id`, or generated) and a W3C trace
|
|
1033
|
+
context (from `traceparent`, or generated) before user hooks and context
|
|
1034
|
+
creation, passes them to context factories as `requestId` and `trace`, writes
|
|
1035
|
+
both response headers, and records `request`/`error` events into the provider
|
|
1036
|
+
instrumentation port resolved from final ports (`ports.instrumentation`, then
|
|
1037
|
+
`ports.devtools`). Without an installed sink, headers are still written and
|
|
1038
|
+
events are a no-op.
|
|
1039
|
+
|
|
1040
|
+
```ts
|
|
1041
|
+
const server = await createServer({
|
|
1042
|
+
ports,
|
|
1043
|
+
providers,
|
|
1044
|
+
// Defaults shown. Pass `instrumentation: false` to disable entirely.
|
|
1045
|
+
instrumentation: {
|
|
1046
|
+
requestIdHeader: "x-request-id",
|
|
1047
|
+
traceContextHeader: "traceparent",
|
|
1048
|
+
ignorePaths: ["/api/devtools"],
|
|
1049
|
+
},
|
|
1050
|
+
context: {
|
|
1051
|
+
request: ({ ports, requestId, trace }) => ({
|
|
1052
|
+
requestId,
|
|
1053
|
+
...trace,
|
|
1054
|
+
ports,
|
|
376
1055
|
}),
|
|
377
|
-
|
|
1056
|
+
},
|
|
378
1057
|
});
|
|
379
1058
|
```
|
|
380
1059
|
|
|
1060
|
+
Service contexts created with `server.createServiceContext(...)` receive fresh
|
|
1061
|
+
`requestId` and `trace` values per call. Context values win: when a factory
|
|
1062
|
+
sets its own `requestId`, headers and recorded events use it.
|
|
1063
|
+
|
|
1064
|
+
Trace primitives live in `@beignet/core/tracing` (`TraceContext`,
|
|
1065
|
+
`createTraceContext`, `createChildTraceContext`, `parseTraceparent`,
|
|
1066
|
+
`createTraceparent`, `createTraceId`, `createSpanId`). The module is
|
|
1067
|
+
dependency-free so app context types can be imported from client bundles.
|
|
1068
|
+
|
|
1069
|
+
Use cases created with `createUseCase(...)` are instrumented by default. Each
|
|
1070
|
+
run resolves the instrumentation port from `ctx.ports`, creates a child span
|
|
1071
|
+
from the context's trace fields, and records `usecase` lifecycle events plus
|
|
1072
|
+
correlated `error` events for failures. Pass `instrumentation: false` to opt
|
|
1073
|
+
out.
|
|
1074
|
+
|
|
1075
|
+
`createInstrumentedAuditLog({ audit, instrumentation })` from
|
|
1076
|
+
`@beignet/core/ports` writes durable audit entries first and mirrors sanitized
|
|
1077
|
+
audit activity into the resolved instrumentation sink.
|
|
1078
|
+
|
|
1079
|
+
`createAmbientAuditLog(audit)` from `@beignet/core/server` fills missing
|
|
1080
|
+
`actor`, `tenant`, `requestId`, and `traceId` fields from the ambient request
|
|
1081
|
+
context at record time. The server keeps that context current for requests
|
|
1082
|
+
(including identity elevated by route hooks) and for service contexts created
|
|
1083
|
+
with `server.createServiceContext(...)`, so jobs, listeners, schedules, and
|
|
1084
|
+
tasks are covered. Because enrichment happens at record time, the wrapper also
|
|
1085
|
+
works for audit ports rebuilt per transaction inside a unit of work — wrap
|
|
1086
|
+
both the top-level port and the per-transaction rebuild:
|
|
1087
|
+
|
|
1088
|
+
```ts
|
|
1089
|
+
import { createAmbientAuditLog } from "@beignet/core/server";
|
|
1090
|
+
|
|
1091
|
+
const audit = createAmbientAuditLog(
|
|
1092
|
+
createInstrumentedAuditLog({ audit: durableAudit, instrumentation: ports }),
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
await audit.record({
|
|
1096
|
+
action: "posts.publish",
|
|
1097
|
+
resource: { type: "post", id: post.id },
|
|
1098
|
+
});
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
Entry-provided fields always win; on runtimes without `AsyncLocalStorage`
|
|
1102
|
+
the wrapper passes entries through unchanged, and entries without an actor
|
|
1103
|
+
normalize to an anonymous actor.
|
|
1104
|
+
|
|
1105
|
+
Route-owned response validation can be disabled with
|
|
1106
|
+
`validateResponses: false` on `createServer(...)`, mirroring the client option
|
|
1107
|
+
of the same name. Binder routes whose use case `.output(...)` schema is the
|
|
1108
|
+
declared success response schema by reference already skip the redundant
|
|
1109
|
+
success-status parse; if profiling justifies disabling validation entirely,
|
|
1110
|
+
drive `validateResponses` from an environment flag so development and CI keep
|
|
1111
|
+
it on.
|
|
1112
|
+
|
|
381
1113
|
### OpenAPI metadata
|
|
382
1114
|
|
|
383
1115
|
Add OpenAPI-specific metadata for documentation using the `.openapi()` method:
|
|
@@ -401,6 +1133,12 @@ export const getTodo = todos
|
|
|
401
1133
|
});
|
|
402
1134
|
```
|
|
403
1135
|
|
|
1136
|
+
Use `requestBody`, `responses`, and `parameters` overrides when an operation
|
|
1137
|
+
needs non-JSON media such as multipart uploads, binary downloads, event streams,
|
|
1138
|
+
or cookie parameters. `contractsToOpenAPI(...)` accepts `schemaConverters` for
|
|
1139
|
+
non-Zod Standard Schema libraries; custom converters run before Beignet's
|
|
1140
|
+
default Zod converter.
|
|
1141
|
+
|
|
404
1142
|
### Schema introspection
|
|
405
1143
|
|
|
406
1144
|
Contracts expose their schemas for runtime introspection:
|
|
@@ -418,12 +1156,12 @@ getTodo.metadata; // { auth: "required", ... }
|
|
|
418
1156
|
|
|
419
1157
|
## API reference
|
|
420
1158
|
|
|
421
|
-
### `
|
|
1159
|
+
### `defineContractGroup()`
|
|
422
1160
|
|
|
423
1161
|
Creates a new contract group for defining related endpoints.
|
|
424
1162
|
|
|
425
1163
|
```ts
|
|
426
|
-
const group =
|
|
1164
|
+
const group = defineContractGroup()
|
|
427
1165
|
.namespace("myNamespace") // Optional resource namespace
|
|
428
1166
|
.prefix("/api/v1") // Optional URL path prefix
|
|
429
1167
|
.meta({ auth: "required" }) // Shared metadata
|
|
@@ -433,6 +1171,10 @@ const group = createContractGroup()
|
|
|
433
1171
|
});
|
|
434
1172
|
```
|
|
435
1173
|
|
|
1174
|
+
Shared catalog errors merge with route-level `.errors(...)` declarations, so
|
|
1175
|
+
each contract carries the union of group and route errors. Later declarations
|
|
1176
|
+
win when the same catalog key is declared twice.
|
|
1177
|
+
|
|
436
1178
|
Any non-empty response map is treated as a response contract. Include
|
|
437
1179
|
successful statuses such as `200` or `201` alongside custom error statuses; use
|
|
438
1180
|
`responses: {}` only when you want to skip response validation. Prefer
|
|
@@ -453,7 +1195,7 @@ standard error envelope.
|
|
|
453
1195
|
| `.headers(schema)` | Define request header schema |
|
|
454
1196
|
| `.body(schema)` | Define request body schema |
|
|
455
1197
|
| `.responses({ ... })` | Define or merge response schemas by status code |
|
|
456
|
-
| `.errors({ ... })` | Declare route-owned catalog errors using Beignet's standard error envelope |
|
|
1198
|
+
| `.errors({ ... })` | Declare route-owned catalog errors using Beignet's standard error envelope; merges with group and earlier declarations |
|
|
457
1199
|
| `.meta(metadata)` | Add custom metadata |
|
|
458
1200
|
| `.openapi(options)` | Add OpenAPI metadata (summary, tags, etc.) |
|
|
459
1201
|
|