@beignet/core 0.0.2 → 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 +173 -0
- package/README.md +821 -30
- 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 +148 -16
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +174 -14
- 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 +18 -4
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +104 -4
- 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 +46 -5
- 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 +89 -65
- package/dist/server/hooks/auth.d.ts.map +1 -1
- package/dist/server/hooks/auth.js +84 -55
- 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 +84 -6
- package/dist/server/http.d.ts.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 +148 -35
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +482 -145
- 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 +611 -5
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +434 -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 +278 -0
- package/dist/uploads/client.d.ts.map +1 -0
- package/dist/uploads/client.js +428 -0
- package/dist/uploads/client.js.map +1 -0
- package/dist/uploads/index.d.ts +361 -0
- package/dist/uploads/index.d.ts.map +1 -0
- package/dist/uploads/index.js +543 -0
- package/dist/uploads/index.js.map +1 -0
- package/package.json +34 -3
- 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 +340 -29
- 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 +151 -6
- 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 +93 -8
- 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 +175 -158
- 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 +15 -12
- 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 +112 -6
- package/src/server/index.ts +63 -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 +1045 -229
- 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 +1153 -6
- package/src/tracing/index.ts +176 -0
- package/src/uploads/client.ts +861 -0
- package/src/uploads/index.ts +1071 -0
- 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,9 +2,13 @@
|
|
|
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
|
-
idempotency, outbox, mail, notifications, schedules, pagination helpers,
|
|
11
|
+
idempotency, outbox, mail, notifications, schedules, uploads, pagination helpers,
|
|
8
12
|
testing helpers, and OpenAPI generation.
|
|
9
13
|
|
|
10
14
|
## Installation
|
|
@@ -35,13 +39,14 @@ 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 |
|
|
41
46
|
| `@beignet/core/errors` | Error catalogs and response helpers |
|
|
42
47
|
| `@beignet/core/events` | Events and listeners |
|
|
43
48
|
| `@beignet/core/idempotency` | Retry-safe command, webhook, and job primitives |
|
|
44
|
-
| `@beignet/core/jobs` | Job definitions and inline job dispatch |
|
|
49
|
+
| `@beignet/core/jobs` | Job definitions, retry policies, and inline job dispatch |
|
|
45
50
|
| `@beignet/core/mail` | Mail port and memory mailer |
|
|
46
51
|
| `@beignet/core/notifications` | Notification definitions, dispatchers, mail channels, and test adapters |
|
|
47
52
|
| `@beignet/core/openapi` | OpenAPI generation |
|
|
@@ -50,9 +55,179 @@ 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 |
|
|
64
|
+
| `@beignet/core/uploads` | Upload definitions, router, signer port, and test signer |
|
|
65
|
+
| `@beignet/core/uploads/client` | Browser upload client for server and direct uploads |
|
|
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.
|
|
56
231
|
|
|
57
232
|
## Key concepts
|
|
58
233
|
|
|
@@ -67,7 +242,7 @@ A **contract** is the single source of truth for an API endpoint. It describes:
|
|
|
67
242
|
|
|
68
243
|
### Contract group
|
|
69
244
|
|
|
70
|
-
A **contract group** allows you to share configuration across related endpoints, such as a common namespace,
|
|
245
|
+
A **contract group** allows you to share configuration across related endpoints, such as a common namespace, route metadata, headers, and shared response schemas.
|
|
71
246
|
|
|
72
247
|
## Usage
|
|
73
248
|
|
|
@@ -75,10 +250,10 @@ A **contract group** allows you to share configuration across related endpoints,
|
|
|
75
250
|
|
|
76
251
|
```ts
|
|
77
252
|
import { z } from "zod";
|
|
78
|
-
import {
|
|
253
|
+
import { defineContractGroup } from "@beignet/core/contracts";
|
|
79
254
|
|
|
80
255
|
// Create a contract group for related endpoints
|
|
81
|
-
const todos =
|
|
256
|
+
const todos = defineContractGroup()
|
|
82
257
|
.namespace("todos")
|
|
83
258
|
.prefix("/api/todos")
|
|
84
259
|
.meta({ auth: "required" })
|
|
@@ -130,6 +305,22 @@ Clients and OpenAPI generation infer required path argument keys from literal
|
|
|
130
305
|
path templates. Use `.pathParams(...)` when you want runtime validation,
|
|
131
306
|
coercion, richer OpenAPI schemas, or parameter descriptions.
|
|
132
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
|
+
|
|
133
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.
|
|
134
325
|
|
|
135
326
|
Request bodies are supported for `POST`, `PUT`, and `PATCH` contracts only.
|
|
@@ -137,10 +328,10 @@ Request bodies are supported for `POST`, `PUT`, and `PATCH` contracts only.
|
|
|
137
328
|
If you do not pass `name`, Beignet generates one from the HTTP method and full path:
|
|
138
329
|
|
|
139
330
|
```ts
|
|
140
|
-
|
|
331
|
+
defineContract({ method: "GET", path: "/users/:id" }).name;
|
|
141
332
|
// "getUsersById"
|
|
142
333
|
|
|
143
|
-
|
|
334
|
+
defineContract({ method: "POST", path: "/api/todos" }).name;
|
|
144
335
|
// "createTodos"
|
|
145
336
|
```
|
|
146
337
|
|
|
@@ -151,7 +342,7 @@ Auto-generated names ignore a leading `/api` segment, include path parameters as
|
|
|
151
342
|
Use `.prefix(...)` on a contract group to compose shared URL path segments without repeating them on every route:
|
|
152
343
|
|
|
153
344
|
```ts
|
|
154
|
-
const api =
|
|
345
|
+
const api = defineContractGroup().prefix("/api/v1");
|
|
155
346
|
|
|
156
347
|
const todos = api
|
|
157
348
|
.namespace("todos")
|
|
@@ -168,16 +359,198 @@ Prefixes compose immutably and normalize boundary slashes. `namespace()` control
|
|
|
168
359
|
resource identity for contract names, OpenAPI tags, and client cache grouping;
|
|
169
360
|
`prefix()` only controls URL paths.
|
|
170
361
|
|
|
171
|
-
### Test
|
|
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
|
+
```
|
|
172
445
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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:
|
|
176
456
|
|
|
177
457
|
```ts
|
|
178
|
-
import {
|
|
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
|
|
179
511
|
|
|
180
|
-
|
|
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
|
+
|
|
538
|
+
### Test factories and seeds
|
|
539
|
+
|
|
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:
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
import {
|
|
546
|
+
createDatabaseTestHarness,
|
|
547
|
+
createFactory,
|
|
548
|
+
defineSeed,
|
|
549
|
+
resetFactories,
|
|
550
|
+
runSeeds,
|
|
551
|
+
} from "@beignet/core/testing";
|
|
552
|
+
|
|
553
|
+
const postFactory = createFactory("post", {
|
|
181
554
|
defaults: ({ sequence }) => ({
|
|
182
555
|
title: `Post ${sequence}`,
|
|
183
556
|
content: "Created in a test.",
|
|
@@ -194,11 +567,179 @@ const demoPostsSeed = defineSeed("demo-posts", {
|
|
|
194
567
|
export async function seedDemoPosts(ctx: AppContext) {
|
|
195
568
|
await runSeeds({ ctx, seeds: [demoPostsSeed] });
|
|
196
569
|
}
|
|
570
|
+
|
|
571
|
+
export function resetPostFactories() {
|
|
572
|
+
resetFactories(postFactory);
|
|
573
|
+
}
|
|
574
|
+
```
|
|
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" });
|
|
197
595
|
```
|
|
198
596
|
|
|
199
597
|
Keep factories and seeds app-owned. They should not import database clients,
|
|
200
598
|
ORM table objects, or provider SDKs directly.
|
|
201
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
|
+
|
|
202
743
|
### Pagination
|
|
203
744
|
|
|
204
745
|
Use `@beignet/core/pagination` to keep list use cases and repository ports
|
|
@@ -222,10 +763,65 @@ return ctx.ports.posts.findMany({
|
|
|
222
763
|
Beignet's convention is `items` for list contents and `page` for pagination
|
|
223
764
|
metadata. Keep filters and sort options app-owned plain objects.
|
|
224
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
|
+
|
|
225
794
|
### Idempotency
|
|
226
795
|
|
|
227
|
-
Use `@beignet/core/
|
|
228
|
-
|
|
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:
|
|
229
825
|
|
|
230
826
|
```ts
|
|
231
827
|
import {
|
|
@@ -234,17 +830,17 @@ import {
|
|
|
234
830
|
} from "@beignet/core/idempotency";
|
|
235
831
|
|
|
236
832
|
const result = await runIdempotently(ctx.ports.idempotency, {
|
|
237
|
-
namespace: "todos.
|
|
238
|
-
key: input.
|
|
833
|
+
namespace: "todos.import",
|
|
834
|
+
key: input.importId,
|
|
239
835
|
scope: {
|
|
240
836
|
tenantId: ctx.tenant?.id,
|
|
241
837
|
actorId: ctx.actor?.id,
|
|
242
838
|
},
|
|
243
839
|
fingerprint: await createIdempotencyFingerprint(input, {
|
|
244
|
-
omit: ["
|
|
840
|
+
omit: ["importId"],
|
|
245
841
|
}),
|
|
246
842
|
ttlSec: 60 * 60 * 24,
|
|
247
|
-
run: () => ctx.ports.uow.transaction((tx) => tx.todos.
|
|
843
|
+
run: () => ctx.ports.uow.transaction((tx) => tx.todos.importBatch(input)),
|
|
248
844
|
});
|
|
249
845
|
```
|
|
250
846
|
|
|
@@ -257,9 +853,11 @@ const idempotency = createMemoryIdempotencyStore();
|
|
|
257
853
|
```
|
|
258
854
|
|
|
259
855
|
Production apps should back `IdempotencyPort` with atomic SQL or Redis storage.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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.
|
|
263
861
|
|
|
264
862
|
### Outbox
|
|
265
863
|
|
|
@@ -306,9 +904,37 @@ await drainOutbox({
|
|
|
306
904
|
The outbox is at-least-once delivery. Use idempotent listeners or jobs when a
|
|
307
905
|
duplicate delivery would be harmful.
|
|
308
906
|
|
|
309
|
-
###
|
|
907
|
+
### Schedules
|
|
310
908
|
|
|
311
|
-
Use
|
|
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
|
+
|
|
934
|
+
### Contract metadata and route hooks
|
|
935
|
+
|
|
936
|
+
Use metadata to describe cross-cutting concerns for OpenAPI, clients, docs, and
|
|
937
|
+
app conventions:
|
|
312
938
|
|
|
313
939
|
```ts
|
|
314
940
|
const sendMessage = messages
|
|
@@ -331,6 +957,159 @@ const sendMessage = messages
|
|
|
331
957
|
});
|
|
332
958
|
```
|
|
333
959
|
|
|
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:
|
|
964
|
+
|
|
965
|
+
```ts
|
|
966
|
+
import { createAuthHooks, defineRouteGroup } from "@beignet/core/server";
|
|
967
|
+
import type { AppContext } from "@/app-context";
|
|
968
|
+
|
|
969
|
+
const auth = createAuthHooks<AppContext>()({
|
|
970
|
+
resolve: ({ ctx }) => {
|
|
971
|
+
return ctx.auth ? { user: ctx.auth.user } : null;
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
export const messageRoutes = defineRouteGroup<AppContext>()({
|
|
976
|
+
name: "messages",
|
|
977
|
+
routes: [
|
|
978
|
+
{
|
|
979
|
+
contract: sendMessage,
|
|
980
|
+
hooks: [auth.required()],
|
|
981
|
+
useCase: sendMessageUseCase,
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
});
|
|
985
|
+
```
|
|
986
|
+
|
|
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,
|
|
1055
|
+
}),
|
|
1056
|
+
},
|
|
1057
|
+
});
|
|
1058
|
+
```
|
|
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
|
+
|
|
334
1113
|
### OpenAPI metadata
|
|
335
1114
|
|
|
336
1115
|
Add OpenAPI-specific metadata for documentation using the `.openapi()` method:
|
|
@@ -354,6 +1133,12 @@ export const getTodo = todos
|
|
|
354
1133
|
});
|
|
355
1134
|
```
|
|
356
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
|
+
|
|
357
1142
|
### Schema introspection
|
|
358
1143
|
|
|
359
1144
|
Contracts expose their schemas for runtime introspection:
|
|
@@ -371,12 +1156,12 @@ getTodo.metadata; // { auth: "required", ... }
|
|
|
371
1156
|
|
|
372
1157
|
## API reference
|
|
373
1158
|
|
|
374
|
-
### `
|
|
1159
|
+
### `defineContractGroup()`
|
|
375
1160
|
|
|
376
1161
|
Creates a new contract group for defining related endpoints.
|
|
377
1162
|
|
|
378
1163
|
```ts
|
|
379
|
-
const group =
|
|
1164
|
+
const group = defineContractGroup()
|
|
380
1165
|
.namespace("myNamespace") // Optional resource namespace
|
|
381
1166
|
.prefix("/api/v1") // Optional URL path prefix
|
|
382
1167
|
.meta({ auth: "required" }) // Shared metadata
|
|
@@ -386,6 +1171,10 @@ const group = createContractGroup()
|
|
|
386
1171
|
});
|
|
387
1172
|
```
|
|
388
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
|
+
|
|
389
1178
|
Any non-empty response map is treated as a response contract. Include
|
|
390
1179
|
successful statuses such as `200` or `201` alongside custom error statuses; use
|
|
391
1180
|
`responses: {}` only when you want to skip response validation. Prefer
|
|
@@ -406,7 +1195,7 @@ standard error envelope.
|
|
|
406
1195
|
| `.headers(schema)` | Define request header schema |
|
|
407
1196
|
| `.body(schema)` | Define request body schema |
|
|
408
1197
|
| `.responses({ ... })` | Define or merge response schemas by status code |
|
|
409
|
-
| `.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 |
|
|
410
1199
|
| `.meta(metadata)` | Add custom metadata |
|
|
411
1200
|
| `.openapi(options)` | Add OpenAPI metadata (summary, tags, etc.) |
|
|
412
1201
|
|
|
@@ -422,9 +1211,11 @@ OpenAPI generation currently requires Zod schemas, even though core contracts ca
|
|
|
422
1211
|
|
|
423
1212
|
## Related packages
|
|
424
1213
|
|
|
1214
|
+
- [`@beignet/web`](https://beignet.dev/server) - Web Fetch server adapter
|
|
425
1215
|
- [`@beignet/next`](https://beignet.dev/server) - Next.js server adapter
|
|
426
1216
|
- [`@beignet/react-query`](https://beignet.dev/react-query) - TanStack Query integration
|
|
427
1217
|
- [`@beignet/react-hook-form`](https://beignet.dev/react-hook-form) - React Hook Form integration
|
|
1218
|
+
- [`@beignet/react-uploads`](https://beignet.dev/react-uploads) - React upload state and progress hooks
|
|
428
1219
|
- [`@beignet/nuqs`](https://beignet.dev/nuqs) - URL query state integration with nuqs
|
|
429
1220
|
- [`@beignet/devtools`](https://beignet.dev/devtools) - Local request, provider, and audit timeline
|
|
430
1221
|
|