@beignet/core 0.0.3 → 0.0.5
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 +159 -0
- package/README.md +792 -50
- 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/src/server/server.ts
CHANGED
|
@@ -7,24 +7,46 @@ import {
|
|
|
7
7
|
methodSupportsRequestBody,
|
|
8
8
|
parsePathTemplate,
|
|
9
9
|
type StandardSchema,
|
|
10
|
-
} from "../contracts";
|
|
10
|
+
} from "../contracts/index.js";
|
|
11
|
+
import {
|
|
12
|
+
comparePathParamsToTemplate,
|
|
13
|
+
formatPathParamsMismatch,
|
|
14
|
+
getObjectSchemaShape,
|
|
15
|
+
} from "../contracts/schema-shape.js";
|
|
11
16
|
import {
|
|
12
17
|
createErrorResponseBody,
|
|
18
|
+
httpErrors,
|
|
13
19
|
isAppError,
|
|
14
20
|
isErrorResponseBody,
|
|
15
21
|
toErrorResponseBody,
|
|
16
|
-
} from "../errors";
|
|
17
|
-
import
|
|
18
|
-
|
|
22
|
+
} from "../errors/index.js";
|
|
23
|
+
import {
|
|
24
|
+
IdempotencyConflictError,
|
|
25
|
+
IdempotencyInProgressError,
|
|
26
|
+
} from "../idempotency/index.js";
|
|
27
|
+
import type { AnyPorts } from "../ports/index.js";
|
|
28
|
+
import {
|
|
29
|
+
AuthUnauthorizedError,
|
|
30
|
+
GateAuthorizationError,
|
|
31
|
+
isUnboundPort,
|
|
32
|
+
TenantRequiredError,
|
|
33
|
+
} from "../ports/index.js";
|
|
19
34
|
import type {
|
|
20
|
-
|
|
35
|
+
InferProviderPorts,
|
|
21
36
|
ProviderSetupResult,
|
|
22
37
|
ServiceProvider,
|
|
23
|
-
} from "../providers";
|
|
24
|
-
import type { ContractLike, ResolveContract } from "./contract-like";
|
|
25
|
-
import { resolveContract } from "./contract-like";
|
|
26
|
-
import { getRequestIdFromContext } from "./hooks/utils";
|
|
38
|
+
} from "../providers/index.js";
|
|
27
39
|
import type {
|
|
40
|
+
ContextSeed,
|
|
41
|
+
ServerContextConfig,
|
|
42
|
+
ServiceContextInputArgs,
|
|
43
|
+
} from "./context.js";
|
|
44
|
+
import { createContextFinalizer, resolveServerContext } from "./context.js";
|
|
45
|
+
import type { ContractLike, ResolveContract } from "./contract-like.js";
|
|
46
|
+
import { resolveContract } from "./contract-like.js";
|
|
47
|
+
import { getRequestIdFromContext } from "./hooks/utils.js";
|
|
48
|
+
import type {
|
|
49
|
+
AddedCtxFromHooks,
|
|
28
50
|
Handler,
|
|
29
51
|
HandlerArgs,
|
|
30
52
|
HttpRequestLike,
|
|
@@ -35,21 +57,39 @@ import type {
|
|
|
35
57
|
ServerCaughtErrorHook,
|
|
36
58
|
ServerHook,
|
|
37
59
|
ServerUnhandledErrorMapper,
|
|
38
|
-
} from "./http";
|
|
60
|
+
} from "./http.js";
|
|
61
|
+
import type { ServerInstrumentationOptions } from "./instrumentation.js";
|
|
62
|
+
import { createServerInstrumentation } from "./instrumentation.js";
|
|
39
63
|
import {
|
|
40
64
|
loadProviderConfig,
|
|
41
65
|
parseStandardSchema,
|
|
42
66
|
SchemaValidationError,
|
|
43
|
-
} from "./providers";
|
|
67
|
+
} from "./providers/index.js";
|
|
68
|
+
import type { ActiveRequestContext } from "./request-context.js";
|
|
69
|
+
import {
|
|
70
|
+
enterActiveRequestContext,
|
|
71
|
+
readContextActor,
|
|
72
|
+
readContextTenant,
|
|
73
|
+
setActiveRequestIdentity,
|
|
74
|
+
} from "./request-context.js";
|
|
75
|
+
import type {
|
|
76
|
+
AnyUseCaseLike,
|
|
77
|
+
AnyUseCaseRouteDef,
|
|
78
|
+
UseCaseRouteDef,
|
|
79
|
+
ValidatedRouteInputs,
|
|
80
|
+
} from "./use-case-route.js";
|
|
81
|
+
import {
|
|
82
|
+
createUseCaseRouteHandler,
|
|
83
|
+
isUseCaseRouteDef,
|
|
84
|
+
} from "./use-case-route.js";
|
|
44
85
|
|
|
45
86
|
/**
|
|
46
|
-
* Route registration
|
|
87
|
+
* Route registration that connects a contract to the handler implementing it.
|
|
47
88
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `defineRoutes(...)`.
|
|
89
|
+
* Most apps keep route definitions in `features/<feature>/routes.ts` and
|
|
90
|
+
* compose them with `defineRoutes(...)`.
|
|
51
91
|
*/
|
|
52
|
-
export type
|
|
92
|
+
export type HandlerRouteDef<
|
|
53
93
|
Ctx,
|
|
54
94
|
CLike extends ContractLike = ContractLike,
|
|
55
95
|
Hooks extends readonly RouteHook<Ctx, object>[] = readonly RouteHook<
|
|
@@ -69,23 +109,27 @@ export type RouteDef<
|
|
|
69
109
|
* Handler that implements the contract.
|
|
70
110
|
*/
|
|
71
111
|
handle: Handler<Ctx & AddedCtxFromHooks<Hooks>, ResolveContract<CLike>>;
|
|
112
|
+
useCase?: never;
|
|
113
|
+
input?: never;
|
|
114
|
+
status?: never;
|
|
72
115
|
};
|
|
73
116
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Route registration for one contract.
|
|
119
|
+
*
|
|
120
|
+
* Routes either bind the contract directly to a use case (`{ contract,
|
|
121
|
+
* useCase }`) or implement a full handler (`{ contract, handle }`). The full
|
|
122
|
+
* handler form is the escape hatch for response headers, streaming, native
|
|
123
|
+
* `Response` values, and multi-status handling.
|
|
124
|
+
*/
|
|
125
|
+
export type RouteDef<
|
|
126
|
+
Ctx,
|
|
127
|
+
CLike extends ContractLike = ContractLike,
|
|
128
|
+
Hooks extends readonly RouteHook<Ctx, object>[] = readonly RouteHook<
|
|
129
|
+
Ctx,
|
|
130
|
+
object
|
|
131
|
+
>[],
|
|
132
|
+
> = HandlerRouteDef<Ctx, CLike, Hooks> | AnyUseCaseRouteDef<Ctx, CLike, Hooks>;
|
|
89
133
|
|
|
90
134
|
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
|
|
91
135
|
type AnyRouteDef = RouteDef<any, any>;
|
|
@@ -145,7 +189,7 @@ type RouteGroupBuilder<Ctx> = {
|
|
|
145
189
|
>(group: {
|
|
146
190
|
name: string;
|
|
147
191
|
hooks?: GroupHooks;
|
|
148
|
-
routes: R
|
|
192
|
+
routes: R & ValidatedRouteInputs<Ctx & AddedCtxFromHooks<GroupHooks>, R>;
|
|
149
193
|
}): RouteGroup<Ctx, R>;
|
|
150
194
|
<
|
|
151
195
|
const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
|
|
@@ -153,7 +197,7 @@ type RouteGroupBuilder<Ctx> = {
|
|
|
153
197
|
>(group: {
|
|
154
198
|
name: string;
|
|
155
199
|
hooks?: GroupHooks;
|
|
156
|
-
routes: R
|
|
200
|
+
routes: R & ValidatedRouteInputs<Ctx & AddedCtxFromHooks<GroupHooks>, R>;
|
|
157
201
|
}): RouteGroup<Ctx, R>;
|
|
158
202
|
};
|
|
159
203
|
|
|
@@ -192,28 +236,54 @@ type ContractsFromRouteList<
|
|
|
192
236
|
* added fields.
|
|
193
237
|
*/
|
|
194
238
|
export function defineRoute<Ctx>() {
|
|
195
|
-
|
|
239
|
+
function define<
|
|
240
|
+
CLike extends ContractLike,
|
|
241
|
+
UC extends AnyUseCaseLike,
|
|
242
|
+
const Hooks extends readonly RouteHook<Ctx, object>[] = readonly [],
|
|
243
|
+
>(
|
|
244
|
+
route: UseCaseRouteDef<Ctx, CLike, UC, Hooks>,
|
|
245
|
+
): UseCaseRouteDef<Ctx, CLike, UC, Hooks>;
|
|
246
|
+
function define<
|
|
196
247
|
CLike extends ContractLike,
|
|
197
248
|
const Hooks extends readonly RouteHook<Ctx, object>[] = readonly [],
|
|
198
249
|
>(
|
|
199
|
-
route:
|
|
200
|
-
):
|
|
250
|
+
route: HandlerRouteDef<Ctx, CLike, Hooks>,
|
|
251
|
+
): HandlerRouteDef<Ctx, CLike, Hooks>;
|
|
252
|
+
function define(route: unknown): unknown {
|
|
253
|
+
return route;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return define;
|
|
201
257
|
}
|
|
202
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Loosely typed provider list element.
|
|
261
|
+
*
|
|
262
|
+
* Required-port, app-context, and service-input generics are erased here so
|
|
263
|
+
* providers created with the typed `createProvider<Requires, Context,
|
|
264
|
+
* ServiceInput>()` form stay assignable to server provider lists.
|
|
265
|
+
*/
|
|
266
|
+
type AnyServiceProvider = ServiceProvider<
|
|
267
|
+
unknown,
|
|
268
|
+
// biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
|
|
269
|
+
StandardSchemaV1<any, any>,
|
|
270
|
+
AnyPorts,
|
|
271
|
+
// biome-ignore lint/suspicious/noExplicitAny: provider context types are erased at this level
|
|
272
|
+
any,
|
|
273
|
+
// biome-ignore lint/suspicious/noExplicitAny: provider service-input types are erased at this level
|
|
274
|
+
any
|
|
275
|
+
>;
|
|
276
|
+
|
|
203
277
|
/**
|
|
204
278
|
* Options for creating a Beignet server instance.
|
|
205
279
|
*/
|
|
206
280
|
export type CreateServerOptions<
|
|
207
281
|
Ctx,
|
|
208
282
|
Ports extends AnyPorts,
|
|
283
|
+
ServiceInput = void,
|
|
209
284
|
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
|
|
210
285
|
Routes extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
|
|
211
|
-
Providers extends readonly
|
|
212
|
-
unknown,
|
|
213
|
-
// biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
|
|
214
|
-
StandardSchemaV1<any, any>,
|
|
215
|
-
AnyPorts
|
|
216
|
-
>[] = readonly [],
|
|
286
|
+
Providers extends readonly AnyServiceProvider[] = readonly [],
|
|
217
287
|
> = {
|
|
218
288
|
/**
|
|
219
289
|
* App-owned ports available to context creation, hooks, and handlers.
|
|
@@ -237,24 +307,64 @@ export type CreateServerOptions<
|
|
|
237
307
|
providerConfig?: Record<string, unknown>;
|
|
238
308
|
|
|
239
309
|
/**
|
|
240
|
-
*
|
|
310
|
+
* Context blueprint for request and service contexts.
|
|
311
|
+
*
|
|
312
|
+
* Gate-less contexts may pass a plain request factory. Contexts with a
|
|
313
|
+
* `gate` property must use the blueprint form
|
|
314
|
+
* `{ gate: (ports) => ports.gate, request, service }` so the server owns
|
|
315
|
+
* gate attachment and identity changes can never go stale.
|
|
241
316
|
*
|
|
242
317
|
* The `ports` argument includes app ports plus ports provided during server
|
|
243
318
|
* startup.
|
|
244
319
|
*/
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
320
|
+
context: ServerContextConfig<
|
|
321
|
+
Ctx,
|
|
322
|
+
Ports & InferProviderPorts<Providers>,
|
|
323
|
+
ServiceInput
|
|
324
|
+
>;
|
|
250
325
|
/**
|
|
251
326
|
* Server hooks that wrap every registered route.
|
|
252
327
|
*/
|
|
253
|
-
hooks?: ServerHook<Ctx, Ports &
|
|
328
|
+
hooks?: ServerHook<Ctx, Ports & InferProviderPorts<Providers>>[];
|
|
329
|
+
/**
|
|
330
|
+
* Server-owned request instrumentation.
|
|
331
|
+
*
|
|
332
|
+
* The server resolves a request ID and W3C trace context for every request
|
|
333
|
+
* before user hooks and context creation, writes `x-request-id` and
|
|
334
|
+
* `traceparent` response headers, and records request and error events into
|
|
335
|
+
* the resolved provider instrumentation port (`ports.instrumentation`, then
|
|
336
|
+
* `ports.devtools`) when one is installed.
|
|
337
|
+
*
|
|
338
|
+
* Pass `false` to disable headers and event recording. Context factories
|
|
339
|
+
* still receive `requestId` and `trace` arguments.
|
|
340
|
+
*/
|
|
341
|
+
instrumentation?: ServerInstrumentationOptions<Ctx> | false;
|
|
342
|
+
/**
|
|
343
|
+
* Whether route-owned responses are validated against the contract's
|
|
344
|
+
* declared statuses and response schemas before they are sent.
|
|
345
|
+
*
|
|
346
|
+
* Disable this to trade response guarantees for throughput, mirroring the
|
|
347
|
+
* client-side `validateResponses` option.
|
|
348
|
+
*
|
|
349
|
+
* @default true
|
|
350
|
+
*/
|
|
351
|
+
validateResponses?: boolean;
|
|
254
352
|
/**
|
|
255
353
|
* Route list to register up front.
|
|
256
354
|
*/
|
|
257
355
|
routes?: Routes;
|
|
356
|
+
/**
|
|
357
|
+
* How to handle ports that are still unbound after all providers have
|
|
358
|
+
* started.
|
|
359
|
+
*
|
|
360
|
+
* Ports declared as `deferred` in `definePorts(...)` boot as throwing
|
|
361
|
+
* placeholders until a provider contributes them. The default `"error"`
|
|
362
|
+
* fails startup and lists the unbound port keys. Apps that bind every port
|
|
363
|
+
* directly are unaffected.
|
|
364
|
+
*
|
|
365
|
+
* @default "error"
|
|
366
|
+
*/
|
|
367
|
+
onUnboundPorts?: "error" | "warn" | "ignore";
|
|
258
368
|
/**
|
|
259
369
|
* Global caught-error observer.
|
|
260
370
|
*/
|
|
@@ -274,7 +384,11 @@ interface RouteBuilder<Ctx, C extends HttpContractConfig> {
|
|
|
274
384
|
/**
|
|
275
385
|
* Runtime server object returned by `createServer(...)`.
|
|
276
386
|
*/
|
|
277
|
-
export interface ServerInstance<
|
|
387
|
+
export interface ServerInstance<
|
|
388
|
+
Ctx,
|
|
389
|
+
Ports extends AnyPorts = AnyPorts,
|
|
390
|
+
ServiceInput = void,
|
|
391
|
+
> {
|
|
278
392
|
/**
|
|
279
393
|
* Catch-all request handler for platform adapters.
|
|
280
394
|
*/
|
|
@@ -285,6 +399,22 @@ export interface ServerInstance<Ctx, Ports extends AnyPorts = AnyPorts> {
|
|
|
285
399
|
route: <CLike extends ContractLike>(
|
|
286
400
|
contractLike: CLike,
|
|
287
401
|
) => RouteBuilder<Ctx, ResolveContract<CLike>>;
|
|
402
|
+
/**
|
|
403
|
+
* Build a fully assembled request context from a framework-neutral request.
|
|
404
|
+
*
|
|
405
|
+
* Use this for adapter entry points outside the route pipeline, such as
|
|
406
|
+
* server components or upload routes.
|
|
407
|
+
*/
|
|
408
|
+
createRequestContext: (req: HttpRequestLike) => Promise<Ctx>;
|
|
409
|
+
/**
|
|
410
|
+
* Build a fully assembled service context for schedules, outbox drains,
|
|
411
|
+
* tasks, and background work.
|
|
412
|
+
*
|
|
413
|
+
* Requires `context.service` to be declared in `createServer(...)`.
|
|
414
|
+
*/
|
|
415
|
+
createServiceContext: (
|
|
416
|
+
...args: ServiceContextInputArgs<ServiceInput>
|
|
417
|
+
) => Promise<Ctx>;
|
|
288
418
|
/**
|
|
289
419
|
* Contract configs registered through the `routes` option.
|
|
290
420
|
*/
|
|
@@ -380,6 +510,59 @@ function errorResponse(
|
|
|
380
510
|
};
|
|
381
511
|
}
|
|
382
512
|
|
|
513
|
+
type RequestValidationLocation = "query" | "path" | "headers" | "body";
|
|
514
|
+
|
|
515
|
+
function contractDiagnostics(contract: HttpContractConfig) {
|
|
516
|
+
return {
|
|
517
|
+
contract: contract.name,
|
|
518
|
+
method: contract.method,
|
|
519
|
+
path: contract.path,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function requestValidationDetails(
|
|
524
|
+
contract: HttpContractConfig,
|
|
525
|
+
location: RequestValidationLocation,
|
|
526
|
+
error?: unknown,
|
|
527
|
+
) {
|
|
528
|
+
const details = {
|
|
529
|
+
...contractDiagnostics(contract),
|
|
530
|
+
location,
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
if (error instanceof SchemaValidationError) {
|
|
534
|
+
return {
|
|
535
|
+
...details,
|
|
536
|
+
issues: error.issues,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (error instanceof Error) {
|
|
541
|
+
return {
|
|
542
|
+
...details,
|
|
543
|
+
message: error.message,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return details;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function requestValidationError(
|
|
551
|
+
contract: HttpContractConfig,
|
|
552
|
+
status: number,
|
|
553
|
+
code: string,
|
|
554
|
+
message: string,
|
|
555
|
+
location: RequestValidationLocation,
|
|
556
|
+
error?: unknown,
|
|
557
|
+
): HttpResponseLike {
|
|
558
|
+
return errorResponse(
|
|
559
|
+
status,
|
|
560
|
+
code,
|
|
561
|
+
message,
|
|
562
|
+
requestValidationDetails(contract, location, error),
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
383
566
|
function normalizeResponse(res: HttpResponseLike): HttpResponseLike {
|
|
384
567
|
return {
|
|
385
568
|
status: res.status,
|
|
@@ -473,6 +656,48 @@ function responseForHooks(res: HttpResponse): HttpResponseLike {
|
|
|
473
656
|
};
|
|
474
657
|
}
|
|
475
658
|
|
|
659
|
+
/**
|
|
660
|
+
* Merge hook-applied header changes onto a native web Response.
|
|
661
|
+
*
|
|
662
|
+
* Starts from the native response's `Headers` so `set-cookie` multiplicity is
|
|
663
|
+
* preserved, then applies headers the beforeSend chain added or changed
|
|
664
|
+
* relative to the original headers-only view. The body stream passes through
|
|
665
|
+
* untouched; status and statusText are preserved.
|
|
666
|
+
*/
|
|
667
|
+
function mergeNativeResponseHeaders(
|
|
668
|
+
nativeResponse: Response,
|
|
669
|
+
originalHeaders: Record<string, string>,
|
|
670
|
+
finalHeaders: Record<string, string>,
|
|
671
|
+
): Response {
|
|
672
|
+
const originalByLowerKey = new Map<string, string>();
|
|
673
|
+
for (const [key, value] of Object.entries(originalHeaders)) {
|
|
674
|
+
originalByLowerKey.set(key.toLowerCase(), value);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
let changed = false;
|
|
678
|
+
const merged = new Headers(nativeResponse.headers);
|
|
679
|
+
for (const [key, value] of Object.entries(finalHeaders)) {
|
|
680
|
+
const lowerKey = key.toLowerCase();
|
|
681
|
+
if (originalByLowerKey.get(lowerKey) === value) continue;
|
|
682
|
+
changed = true;
|
|
683
|
+
if (lowerKey === "set-cookie") {
|
|
684
|
+
merged.append(lowerKey, value);
|
|
685
|
+
} else {
|
|
686
|
+
merged.set(key, value);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (!changed) {
|
|
691
|
+
return nativeResponse;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return new Response(nativeResponse.body, {
|
|
695
|
+
status: nativeResponse.status,
|
|
696
|
+
statusText: nativeResponse.statusText,
|
|
697
|
+
headers: merged,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
476
701
|
function isHttpResponseLike(value: unknown): value is HttpResponseLike {
|
|
477
702
|
return (
|
|
478
703
|
!isWebResponse(value) &&
|
|
@@ -499,6 +724,37 @@ class ResponseContractViolationError extends Error {
|
|
|
499
724
|
}
|
|
500
725
|
}
|
|
501
726
|
|
|
727
|
+
function responseContractViolationMessage(
|
|
728
|
+
contract: HttpContractConfig,
|
|
729
|
+
status: number,
|
|
730
|
+
): string {
|
|
731
|
+
return (
|
|
732
|
+
`Response validation failed for ${contract.method} ${contract.path} ` +
|
|
733
|
+
`(status ${status}, contract: ${contract.name})`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function declaredResponseStatuses(contract: HttpContractConfig): number[] {
|
|
738
|
+
return Object.keys(contract.responses)
|
|
739
|
+
.map((status) => Number(status))
|
|
740
|
+
.filter((status) => Number.isFinite(status))
|
|
741
|
+
.sort((a, b) => a - b);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function responseContractViolationDetails(
|
|
745
|
+
contract: HttpContractConfig,
|
|
746
|
+
status: number,
|
|
747
|
+
details?: Record<string, unknown>,
|
|
748
|
+
) {
|
|
749
|
+
return {
|
|
750
|
+
...contractDiagnostics(contract),
|
|
751
|
+
location: "response",
|
|
752
|
+
status,
|
|
753
|
+
declaredStatuses: declaredResponseStatuses(contract),
|
|
754
|
+
...details,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
502
758
|
function getDeclaredCatalogErrorsForStatus(
|
|
503
759
|
contract: HttpContractConfig,
|
|
504
760
|
status: number,
|
|
@@ -536,10 +792,8 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
536
792
|
if (!matchingError) {
|
|
537
793
|
throw new ResponseContractViolationError({
|
|
538
794
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
539
|
-
message:
|
|
540
|
-
|
|
541
|
-
`(status ${res.status}, contract: ${contract.name})`,
|
|
542
|
-
details: {
|
|
795
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
796
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
543
797
|
issues: [
|
|
544
798
|
{
|
|
545
799
|
message:
|
|
@@ -547,7 +801,7 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
547
801
|
`Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
|
|
548
802
|
},
|
|
549
803
|
],
|
|
550
|
-
},
|
|
804
|
+
}),
|
|
551
805
|
});
|
|
552
806
|
}
|
|
553
807
|
|
|
@@ -558,10 +812,10 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
558
812
|
if (error instanceof SchemaValidationError) {
|
|
559
813
|
throw new ResponseContractViolationError({
|
|
560
814
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
561
|
-
message:
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
815
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
816
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
817
|
+
issues: error.issues,
|
|
818
|
+
}),
|
|
565
819
|
});
|
|
566
820
|
}
|
|
567
821
|
throw error;
|
|
@@ -572,6 +826,7 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
572
826
|
async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
573
827
|
contract: C,
|
|
574
828
|
res: HttpResponseLike,
|
|
829
|
+
responseValidationExemptStatus?: number,
|
|
575
830
|
): Promise<void> {
|
|
576
831
|
const statusKey = String(res.status);
|
|
577
832
|
const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
|
|
@@ -584,10 +839,9 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
584
839
|
message:
|
|
585
840
|
`Handler returned undeclared status ${res.status} for ` +
|
|
586
841
|
`${contract.method} ${contract.path} (contract: ${contract.name})`,
|
|
587
|
-
details: {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
},
|
|
842
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
843
|
+
returnedStatus: res.status,
|
|
844
|
+
}),
|
|
591
845
|
});
|
|
592
846
|
}
|
|
593
847
|
|
|
@@ -596,17 +850,15 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
596
850
|
if (res.body !== undefined && res.body !== null) {
|
|
597
851
|
throw new ResponseContractViolationError({
|
|
598
852
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
599
|
-
message:
|
|
600
|
-
|
|
601
|
-
`(status ${res.status}, contract: ${contract.name})`,
|
|
602
|
-
details: {
|
|
853
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
854
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
603
855
|
issues: [
|
|
604
856
|
{
|
|
605
857
|
message:
|
|
606
858
|
"Response body must be empty for a null response schema.",
|
|
607
859
|
},
|
|
608
860
|
],
|
|
609
|
-
},
|
|
861
|
+
}),
|
|
610
862
|
});
|
|
611
863
|
}
|
|
612
864
|
return;
|
|
@@ -614,6 +866,11 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
614
866
|
|
|
615
867
|
if (!responseSchema) return;
|
|
616
868
|
|
|
869
|
+
// Binder routes whose use case output schema is the same object as the
|
|
870
|
+
// declared success response schema skip the redundant success-status parse.
|
|
871
|
+
// Error statuses and undeclared statuses are validated unchanged.
|
|
872
|
+
if (res.status === responseValidationExemptStatus) return;
|
|
873
|
+
|
|
617
874
|
try {
|
|
618
875
|
await parseStandardSchema(responseSchema, res.body);
|
|
619
876
|
await validateCatalogErrorResponse(contract, res);
|
|
@@ -621,10 +878,10 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
621
878
|
if (error instanceof SchemaValidationError) {
|
|
622
879
|
throw new ResponseContractViolationError({
|
|
623
880
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
624
|
-
message:
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
881
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
882
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
883
|
+
issues: error.issues,
|
|
884
|
+
}),
|
|
628
885
|
});
|
|
629
886
|
}
|
|
630
887
|
throw error;
|
|
@@ -634,9 +891,14 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
634
891
|
async function finalizeResponse<C extends HttpContractConfig>(
|
|
635
892
|
contract: C,
|
|
636
893
|
res: HttpResponseLike,
|
|
894
|
+
responseValidationExemptStatus?: number,
|
|
637
895
|
): Promise<HttpResponseLike> {
|
|
638
896
|
const normalized = normalizeResponse(res);
|
|
639
|
-
await validateResponseAgainstContract(
|
|
897
|
+
await validateResponseAgainstContract(
|
|
898
|
+
contract,
|
|
899
|
+
normalized,
|
|
900
|
+
responseValidationExemptStatus,
|
|
901
|
+
);
|
|
640
902
|
return normalized;
|
|
641
903
|
}
|
|
642
904
|
|
|
@@ -696,39 +958,65 @@ async function parseBody(req: HttpRequestLike): Promise<unknown> {
|
|
|
696
958
|
}
|
|
697
959
|
}
|
|
698
960
|
|
|
699
|
-
|
|
961
|
+
type ExecutionTarget<Ctx, C extends HttpContractConfig> = {
|
|
962
|
+
contract: C;
|
|
963
|
+
/**
|
|
964
|
+
* Compiled route pattern. Only required when the executor is invoked
|
|
965
|
+
* without pre-matched params.
|
|
966
|
+
*/
|
|
967
|
+
compiled?: CompiledPath;
|
|
968
|
+
handler: Handler<Ctx, C>;
|
|
969
|
+
/**
|
|
970
|
+
* Success status whose response schema validation is skipped because the
|
|
971
|
+
* use case bound to this route already validated its output against the
|
|
972
|
+
* same schema object.
|
|
973
|
+
*/
|
|
974
|
+
responseValidationExemptStatus?: number;
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Build the per-request execution pipeline once.
|
|
979
|
+
*
|
|
980
|
+
* The returned executor takes the contract, compiled pattern, and user handler
|
|
981
|
+
* per invocation so fallback responses (404/405) can reuse a single pipeline
|
|
982
|
+
* across requests instead of rebuilding it per unmatched request.
|
|
983
|
+
*/
|
|
984
|
+
function createRequestExecutor<
|
|
700
985
|
Ctx,
|
|
701
986
|
Ports extends AnyPorts,
|
|
702
987
|
C extends HttpContractConfig,
|
|
703
|
-
Providers extends readonly
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
StandardSchemaV1<any, any>,
|
|
707
|
-
AnyPorts
|
|
708
|
-
>[] = readonly [],
|
|
709
|
-
FinalPorts extends Ports & ProvidedPortsOfList<Providers> = Ports &
|
|
710
|
-
ProvidedPortsOfList<Providers>,
|
|
988
|
+
Providers extends readonly AnyServiceProvider[] = readonly [],
|
|
989
|
+
FinalPorts extends Ports & InferProviderPorts<Providers> = Ports &
|
|
990
|
+
InferProviderPorts<Providers>,
|
|
711
991
|
>(
|
|
712
992
|
// biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
|
|
713
|
-
options: CreateServerOptions<Ctx, Ports, any, Providers>,
|
|
993
|
+
options: CreateServerOptions<Ctx, Ports, any, any, Providers>,
|
|
714
994
|
finalPorts: FinalPorts,
|
|
715
|
-
|
|
716
|
-
|
|
995
|
+
contextRuntime: {
|
|
996
|
+
createRequestContext: (
|
|
997
|
+
req: HttpRequestLike,
|
|
998
|
+
contract: HttpContractConfig,
|
|
999
|
+
) => Promise<Ctx>;
|
|
1000
|
+
finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
|
|
1001
|
+
},
|
|
717
1002
|
hooks: ServerHook<Ctx, FinalPorts>[],
|
|
718
1003
|
routeHooks: readonly RouteHook<unknown, object>[] = [],
|
|
719
1004
|
optionsOverrides?: {
|
|
720
1005
|
skipRoutePreparation?: boolean;
|
|
721
1006
|
},
|
|
722
1007
|
): (
|
|
1008
|
+
target: ExecutionTarget<Ctx, C>,
|
|
723
1009
|
req: HttpRequestLike,
|
|
724
1010
|
preMatchedParams?: Record<string, string>,
|
|
725
1011
|
) => Promise<HttpResponse> {
|
|
726
|
-
const
|
|
1012
|
+
const warnedNativeReplacementHooks = new WeakSet<object>();
|
|
727
1013
|
|
|
728
1014
|
return async (
|
|
1015
|
+
target: ExecutionTarget<Ctx, C>,
|
|
729
1016
|
req: HttpRequestLike,
|
|
730
1017
|
preMatchedParams?: Record<string, string>,
|
|
731
1018
|
) => {
|
|
1019
|
+
const { contract, handler: userHandler } = target;
|
|
732
1020
|
let baseCtx: Ctx | undefined;
|
|
733
1021
|
let pathValue: HandlerArgs<Ctx, C>["path"] | undefined;
|
|
734
1022
|
let queryValue: HandlerArgs<Ctx, C>["query"] | undefined;
|
|
@@ -809,6 +1097,53 @@ function buildHandler<
|
|
|
809
1097
|
};
|
|
810
1098
|
}
|
|
811
1099
|
|
|
1100
|
+
if (currentError instanceof TenantRequiredError) {
|
|
1101
|
+
return {
|
|
1102
|
+
ctx,
|
|
1103
|
+
response: errorResponse(
|
|
1104
|
+
currentError.status,
|
|
1105
|
+
currentError.code,
|
|
1106
|
+
currentError.message,
|
|
1107
|
+
),
|
|
1108
|
+
error: currentError,
|
|
1109
|
+
owner: "framework",
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (currentError instanceof IdempotencyConflictError) {
|
|
1114
|
+
return {
|
|
1115
|
+
ctx,
|
|
1116
|
+
response: errorResponse(
|
|
1117
|
+
httpErrors.IdempotencyConflict.status,
|
|
1118
|
+
httpErrors.IdempotencyConflict.code,
|
|
1119
|
+
currentError.message,
|
|
1120
|
+
{
|
|
1121
|
+
namespace: currentError.namespace,
|
|
1122
|
+
key: currentError.key,
|
|
1123
|
+
},
|
|
1124
|
+
),
|
|
1125
|
+
error: currentError,
|
|
1126
|
+
owner: "framework",
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (currentError instanceof IdempotencyInProgressError) {
|
|
1131
|
+
return {
|
|
1132
|
+
ctx,
|
|
1133
|
+
response: errorResponse(
|
|
1134
|
+
httpErrors.IdempotencyInProgress.status,
|
|
1135
|
+
httpErrors.IdempotencyInProgress.code,
|
|
1136
|
+
currentError.message,
|
|
1137
|
+
{
|
|
1138
|
+
namespace: currentError.namespace,
|
|
1139
|
+
key: currentError.key,
|
|
1140
|
+
},
|
|
1141
|
+
),
|
|
1142
|
+
error: currentError,
|
|
1143
|
+
owner: "framework",
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
|
|
812
1147
|
if (currentError instanceof GateAuthorizationError) {
|
|
813
1148
|
return {
|
|
814
1149
|
ctx,
|
|
@@ -893,8 +1228,10 @@ function buildHandler<
|
|
|
893
1228
|
if (preMatchedParams) {
|
|
894
1229
|
matchedParams = preMatchedParams;
|
|
895
1230
|
} else {
|
|
896
|
-
const
|
|
1231
|
+
const compiled = target.compiled;
|
|
1232
|
+
const match = compiled ? compiled.pattern.exec(url.pathname) : null;
|
|
897
1233
|
if (
|
|
1234
|
+
!compiled ||
|
|
898
1235
|
!match ||
|
|
899
1236
|
contract.method.toUpperCase() !== req.method.toUpperCase()
|
|
900
1237
|
) {
|
|
@@ -911,15 +1248,67 @@ function buildHandler<
|
|
|
911
1248
|
}
|
|
912
1249
|
const rawHeaders = requestHeadersToRecord(req.headers);
|
|
913
1250
|
|
|
1251
|
+
const runNativeBeforeSend = async (
|
|
1252
|
+
initialResult: ExecutionResult<Ctx>,
|
|
1253
|
+
nativeResponse: Response,
|
|
1254
|
+
): Promise<Response> => {
|
|
1255
|
+
const originalView = responseForHooks(nativeResponse);
|
|
1256
|
+
const originalHeaders = originalView.headers ?? {};
|
|
1257
|
+
let transformed = originalView;
|
|
1258
|
+
for (const hook of hooks) {
|
|
1259
|
+
if (!hook.beforeSend) continue;
|
|
1260
|
+
const nextResponse = await hook.beforeSend({
|
|
1261
|
+
req,
|
|
1262
|
+
ctx: initialResult.ctx,
|
|
1263
|
+
contract,
|
|
1264
|
+
path: pathValue,
|
|
1265
|
+
query: queryValue,
|
|
1266
|
+
headers: headersValue,
|
|
1267
|
+
body: bodyValue,
|
|
1268
|
+
response: transformed,
|
|
1269
|
+
error: initialResult.error,
|
|
1270
|
+
native: true,
|
|
1271
|
+
});
|
|
1272
|
+
if (nextResponse) {
|
|
1273
|
+
if (
|
|
1274
|
+
(nextResponse.status !== nativeResponse.status ||
|
|
1275
|
+
nextResponse.body !== undefined) &&
|
|
1276
|
+
!warnedNativeReplacementHooks.has(hook) &&
|
|
1277
|
+
process.env.NODE_ENV !== "production"
|
|
1278
|
+
) {
|
|
1279
|
+
warnedNativeReplacementHooks.add(hook);
|
|
1280
|
+
console.warn(
|
|
1281
|
+
`[beignet] beforeSend hook "${hook.name ?? "(anonymous)"}" returned a replacement status or body for a native Response on ${contract.method} ${contract.path}. Native responses are headers-only in beforeSend; status and body changes are ignored.`,
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
transformed = {
|
|
1285
|
+
status: nativeResponse.status,
|
|
1286
|
+
headers: nextResponse.headers,
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
return mergeNativeResponseHeaders(
|
|
1291
|
+
nativeResponse,
|
|
1292
|
+
originalHeaders,
|
|
1293
|
+
transformed.headers ?? {},
|
|
1294
|
+
);
|
|
1295
|
+
};
|
|
1296
|
+
|
|
914
1297
|
const applyTransformHooks = async (
|
|
915
1298
|
initialResult: ExecutionResult<Ctx>,
|
|
916
1299
|
allowRetry: boolean,
|
|
917
1300
|
): Promise<ExecutionResult<Ctx>> => {
|
|
918
|
-
if (isWebResponse(initialResult.response)) {
|
|
919
|
-
return initialResult;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
1301
|
try {
|
|
1302
|
+
if (isWebResponse(initialResult.response)) {
|
|
1303
|
+
return {
|
|
1304
|
+
...initialResult,
|
|
1305
|
+
response: await runNativeBeforeSend(
|
|
1306
|
+
initialResult,
|
|
1307
|
+
initialResult.response,
|
|
1308
|
+
),
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
923
1312
|
let transformed = normalizeResponse(initialResult.response);
|
|
924
1313
|
for (const hook of hooks) {
|
|
925
1314
|
if (!hook.beforeSend) continue;
|
|
@@ -996,11 +1385,10 @@ function buildHandler<
|
|
|
996
1385
|
if (optionsOverrides?.skipRoutePreparation) {
|
|
997
1386
|
let createdCtx!: Ctx;
|
|
998
1387
|
try {
|
|
999
|
-
createdCtx = await
|
|
1388
|
+
createdCtx = await contextRuntime.createRequestContext(
|
|
1000
1389
|
req,
|
|
1001
|
-
ports: finalPorts,
|
|
1002
1390
|
contract,
|
|
1003
|
-
|
|
1391
|
+
);
|
|
1004
1392
|
baseCtx = createdCtx;
|
|
1005
1393
|
} catch (error) {
|
|
1006
1394
|
result = await resolveErrorResult(
|
|
@@ -1056,26 +1444,17 @@ function buildHandler<
|
|
|
1056
1444
|
try {
|
|
1057
1445
|
query = await parseStandardSchema(contract.query, query);
|
|
1058
1446
|
} catch (error) {
|
|
1059
|
-
result =
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
: {
|
|
1071
|
-
response: errorResponse(
|
|
1072
|
-
422,
|
|
1073
|
-
"VALIDATION_ERROR",
|
|
1074
|
-
"Invalid query parameters",
|
|
1075
|
-
(error as Error).message,
|
|
1076
|
-
),
|
|
1077
|
-
owner: "framework",
|
|
1078
|
-
};
|
|
1447
|
+
result = {
|
|
1448
|
+
response: requestValidationError(
|
|
1449
|
+
contract,
|
|
1450
|
+
422,
|
|
1451
|
+
"VALIDATION_ERROR",
|
|
1452
|
+
"Invalid query parameters",
|
|
1453
|
+
"query",
|
|
1454
|
+
error,
|
|
1455
|
+
),
|
|
1456
|
+
owner: "framework",
|
|
1457
|
+
};
|
|
1079
1458
|
}
|
|
1080
1459
|
}
|
|
1081
1460
|
|
|
@@ -1088,26 +1467,17 @@ function buildHandler<
|
|
|
1088
1467
|
matchedParams,
|
|
1089
1468
|
);
|
|
1090
1469
|
} catch (error) {
|
|
1091
|
-
result =
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
: {
|
|
1103
|
-
response: errorResponse(
|
|
1104
|
-
422,
|
|
1105
|
-
"VALIDATION_ERROR",
|
|
1106
|
-
"Invalid path parameters",
|
|
1107
|
-
(error as Error).message,
|
|
1108
|
-
),
|
|
1109
|
-
owner: "framework",
|
|
1110
|
-
};
|
|
1470
|
+
result = {
|
|
1471
|
+
response: requestValidationError(
|
|
1472
|
+
contract,
|
|
1473
|
+
422,
|
|
1474
|
+
"VALIDATION_ERROR",
|
|
1475
|
+
"Invalid path parameters",
|
|
1476
|
+
"path",
|
|
1477
|
+
error,
|
|
1478
|
+
),
|
|
1479
|
+
owner: "framework",
|
|
1480
|
+
};
|
|
1111
1481
|
}
|
|
1112
1482
|
}
|
|
1113
1483
|
|
|
@@ -1118,26 +1488,17 @@ function buildHandler<
|
|
|
1118
1488
|
try {
|
|
1119
1489
|
headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
|
|
1120
1490
|
} catch (error) {
|
|
1121
|
-
result =
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
: {
|
|
1133
|
-
response: errorResponse(
|
|
1134
|
-
422,
|
|
1135
|
-
"VALIDATION_ERROR",
|
|
1136
|
-
"Invalid request headers",
|
|
1137
|
-
(error as Error).message,
|
|
1138
|
-
),
|
|
1139
|
-
owner: "framework",
|
|
1140
|
-
};
|
|
1491
|
+
result = {
|
|
1492
|
+
response: requestValidationError(
|
|
1493
|
+
contract,
|
|
1494
|
+
422,
|
|
1495
|
+
"VALIDATION_ERROR",
|
|
1496
|
+
"Invalid request headers",
|
|
1497
|
+
"headers",
|
|
1498
|
+
error,
|
|
1499
|
+
),
|
|
1500
|
+
owner: "framework",
|
|
1501
|
+
};
|
|
1141
1502
|
}
|
|
1142
1503
|
}
|
|
1143
1504
|
|
|
@@ -1146,9 +1507,16 @@ function buildHandler<
|
|
|
1146
1507
|
if (!result) {
|
|
1147
1508
|
try {
|
|
1148
1509
|
body = await parseBody(req);
|
|
1149
|
-
} catch {
|
|
1510
|
+
} catch (error) {
|
|
1150
1511
|
result = {
|
|
1151
|
-
response:
|
|
1512
|
+
response: requestValidationError(
|
|
1513
|
+
contract,
|
|
1514
|
+
400,
|
|
1515
|
+
"INVALID_BODY",
|
|
1516
|
+
"Malformed JSON",
|
|
1517
|
+
"body",
|
|
1518
|
+
error,
|
|
1519
|
+
),
|
|
1152
1520
|
owner: "framework",
|
|
1153
1521
|
};
|
|
1154
1522
|
}
|
|
@@ -1163,34 +1531,28 @@ function buildHandler<
|
|
|
1163
1531
|
error instanceof SchemaValidationError
|
|
1164
1532
|
) {
|
|
1165
1533
|
result = {
|
|
1166
|
-
response:
|
|
1534
|
+
response: requestValidationError(
|
|
1535
|
+
contract,
|
|
1167
1536
|
400,
|
|
1168
1537
|
"MISSING_BODY",
|
|
1169
1538
|
"Request body is required",
|
|
1539
|
+
"body",
|
|
1540
|
+
error,
|
|
1170
1541
|
),
|
|
1171
1542
|
owner: "framework",
|
|
1172
1543
|
};
|
|
1173
1544
|
} else {
|
|
1174
|
-
result =
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
: {
|
|
1186
|
-
response: errorResponse(
|
|
1187
|
-
422,
|
|
1188
|
-
"VALIDATION_ERROR",
|
|
1189
|
-
"Invalid request body",
|
|
1190
|
-
(error as Error).message,
|
|
1191
|
-
),
|
|
1192
|
-
owner: "framework",
|
|
1193
|
-
};
|
|
1545
|
+
result = {
|
|
1546
|
+
response: requestValidationError(
|
|
1547
|
+
contract,
|
|
1548
|
+
422,
|
|
1549
|
+
"VALIDATION_ERROR",
|
|
1550
|
+
"Invalid request body",
|
|
1551
|
+
"body",
|
|
1552
|
+
error,
|
|
1553
|
+
),
|
|
1554
|
+
owner: "framework",
|
|
1555
|
+
};
|
|
1194
1556
|
}
|
|
1195
1557
|
}
|
|
1196
1558
|
}
|
|
@@ -1203,11 +1565,10 @@ function buildHandler<
|
|
|
1203
1565
|
|
|
1204
1566
|
let createdCtx!: Ctx;
|
|
1205
1567
|
try {
|
|
1206
|
-
createdCtx = await
|
|
1568
|
+
createdCtx = await contextRuntime.createRequestContext(
|
|
1207
1569
|
req,
|
|
1208
|
-
ports: finalPorts,
|
|
1209
1570
|
contract,
|
|
1210
|
-
|
|
1571
|
+
);
|
|
1211
1572
|
baseCtx = createdCtx;
|
|
1212
1573
|
} catch (error) {
|
|
1213
1574
|
result = await resolveErrorResult(
|
|
@@ -1262,7 +1623,7 @@ function buildHandler<
|
|
|
1262
1623
|
break;
|
|
1263
1624
|
}
|
|
1264
1625
|
if (hookResult?.ctx !== undefined) {
|
|
1265
|
-
currentCtx = hookResult.ctx;
|
|
1626
|
+
currentCtx = contextRuntime.finalizeContext(hookResult.ctx);
|
|
1266
1627
|
}
|
|
1267
1628
|
if (hookResult?.response) {
|
|
1268
1629
|
const response = normalizeHttpResponse(hookResult.response);
|
|
@@ -1300,10 +1661,10 @@ function buildHandler<
|
|
|
1300
1661
|
body,
|
|
1301
1662
|
});
|
|
1302
1663
|
if (additions && typeof additions === "object") {
|
|
1303
|
-
currentCtx = {
|
|
1664
|
+
currentCtx = contextRuntime.finalizeContext({
|
|
1304
1665
|
...(currentCtx as object),
|
|
1305
1666
|
...additions,
|
|
1306
|
-
} as Ctx;
|
|
1667
|
+
} as Ctx);
|
|
1307
1668
|
}
|
|
1308
1669
|
} catch (error) {
|
|
1309
1670
|
result = await resolveErrorResult(
|
|
@@ -1321,6 +1682,14 @@ function buildHandler<
|
|
|
1321
1682
|
}
|
|
1322
1683
|
|
|
1323
1684
|
if (!result) {
|
|
1685
|
+
// Hooks may have elevated the actor or resolved a tenant.
|
|
1686
|
+
// Refresh the ambient request context so record-time
|
|
1687
|
+
// consumers such as createAmbientAuditLog see the finalized
|
|
1688
|
+
// identity.
|
|
1689
|
+
setActiveRequestIdentity({
|
|
1690
|
+
actor: readContextActor(currentCtx),
|
|
1691
|
+
tenant: readContextTenant(currentCtx),
|
|
1692
|
+
});
|
|
1324
1693
|
try {
|
|
1325
1694
|
result = {
|
|
1326
1695
|
ctx: currentCtx,
|
|
@@ -1349,9 +1718,17 @@ function buildHandler<
|
|
|
1349
1718
|
let finalResponse = normalizeHttpResponse(result.response);
|
|
1350
1719
|
let finalError = result.error;
|
|
1351
1720
|
let finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
1352
|
-
if (
|
|
1721
|
+
if (
|
|
1722
|
+
finalOwner === "route" &&
|
|
1723
|
+
!isWebResponse(finalResponse) &&
|
|
1724
|
+
(options.validateResponses ?? true)
|
|
1725
|
+
) {
|
|
1353
1726
|
try {
|
|
1354
|
-
finalResponse = await finalizeResponse(
|
|
1727
|
+
finalResponse = await finalizeResponse(
|
|
1728
|
+
contract,
|
|
1729
|
+
finalResponse,
|
|
1730
|
+
target.responseValidationExemptStatus,
|
|
1731
|
+
);
|
|
1355
1732
|
} catch (error) {
|
|
1356
1733
|
if (error instanceof ResponseContractViolationError) {
|
|
1357
1734
|
result = await applyTransformHooks(
|
|
@@ -1424,6 +1801,55 @@ function buildHandler<
|
|
|
1424
1801
|
};
|
|
1425
1802
|
}
|
|
1426
1803
|
|
|
1804
|
+
function buildHandler<
|
|
1805
|
+
Ctx,
|
|
1806
|
+
Ports extends AnyPorts,
|
|
1807
|
+
C extends HttpContractConfig,
|
|
1808
|
+
Providers extends readonly AnyServiceProvider[] = readonly [],
|
|
1809
|
+
FinalPorts extends Ports & InferProviderPorts<Providers> = Ports &
|
|
1810
|
+
InferProviderPorts<Providers>,
|
|
1811
|
+
>(
|
|
1812
|
+
// biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
|
|
1813
|
+
options: CreateServerOptions<Ctx, Ports, any, any, Providers>,
|
|
1814
|
+
finalPorts: FinalPorts,
|
|
1815
|
+
contextRuntime: {
|
|
1816
|
+
createRequestContext: (
|
|
1817
|
+
req: HttpRequestLike,
|
|
1818
|
+
contract: HttpContractConfig,
|
|
1819
|
+
) => Promise<Ctx>;
|
|
1820
|
+
finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
|
|
1821
|
+
},
|
|
1822
|
+
contract: C,
|
|
1823
|
+
userHandler: Handler<Ctx, C>,
|
|
1824
|
+
hooks: ServerHook<Ctx, FinalPorts>[],
|
|
1825
|
+
routeHooks: readonly RouteHook<unknown, object>[] = [],
|
|
1826
|
+
optionsOverrides?: {
|
|
1827
|
+
skipRoutePreparation?: boolean;
|
|
1828
|
+
},
|
|
1829
|
+
responseValidationExemptStatus?: number,
|
|
1830
|
+
): (
|
|
1831
|
+
req: HttpRequestLike,
|
|
1832
|
+
preMatchedParams?: Record<string, string>,
|
|
1833
|
+
) => Promise<HttpResponse> {
|
|
1834
|
+
const execute = createRequestExecutor<Ctx, Ports, C, Providers, FinalPorts>(
|
|
1835
|
+
options,
|
|
1836
|
+
finalPorts,
|
|
1837
|
+
contextRuntime,
|
|
1838
|
+
hooks,
|
|
1839
|
+
routeHooks,
|
|
1840
|
+
optionsOverrides,
|
|
1841
|
+
);
|
|
1842
|
+
const executionTarget: ExecutionTarget<Ctx, C> = {
|
|
1843
|
+
contract,
|
|
1844
|
+
compiled: compilePath(contract.path),
|
|
1845
|
+
handler: userHandler,
|
|
1846
|
+
responseValidationExemptStatus,
|
|
1847
|
+
};
|
|
1848
|
+
|
|
1849
|
+
return (req, preMatchedParams) =>
|
|
1850
|
+
execute(executionTarget, req, preMatchedParams);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1427
1853
|
/**
|
|
1428
1854
|
* Create a Beignet server instance.
|
|
1429
1855
|
*
|
|
@@ -1432,42 +1858,128 @@ function buildHandler<
|
|
|
1432
1858
|
* Use adapter packages such as `@beignet/next` to expose `server.api` to a
|
|
1433
1859
|
* specific runtime.
|
|
1434
1860
|
*
|
|
1435
|
-
* @param options - Ports, providers, routes, hooks, context
|
|
1436
|
-
* mapping hooks for the server.
|
|
1861
|
+
* @param options - Ports, providers, routes, hooks, context blueprint, and
|
|
1862
|
+
* error mapping hooks for the server.
|
|
1437
1863
|
* @returns A started server instance with final ports and a catch-all handler.
|
|
1438
1864
|
*/
|
|
1439
1865
|
export async function createServer<
|
|
1440
1866
|
Ctx,
|
|
1441
1867
|
Ports extends AnyPorts,
|
|
1868
|
+
ServiceInput = void,
|
|
1442
1869
|
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
|
|
1443
1870
|
Routes extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
|
|
1444
|
-
Providers extends readonly
|
|
1445
|
-
unknown,
|
|
1446
|
-
// biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
|
|
1447
|
-
StandardSchemaV1<any, any>,
|
|
1448
|
-
AnyPorts
|
|
1449
|
-
>[] = readonly [],
|
|
1871
|
+
Providers extends readonly AnyServiceProvider[] = readonly [],
|
|
1450
1872
|
>(
|
|
1451
|
-
options: CreateServerOptions<Ctx, Ports, Routes, Providers>,
|
|
1452
|
-
): Promise<
|
|
1873
|
+
options: CreateServerOptions<Ctx, Ports, ServiceInput, Routes, Providers>,
|
|
1874
|
+
): Promise<
|
|
1875
|
+
ServerInstance<Ctx, Ports & InferProviderPorts<Providers>, ServiceInput>
|
|
1876
|
+
> {
|
|
1453
1877
|
type RegisteredRoute = ResolvedRoute<Ctx, HttpContractConfig> & {
|
|
1454
1878
|
compiled: CompiledPath;
|
|
1879
|
+
/**
|
|
1880
|
+
* Uppercased contract method, cached for dispatch.
|
|
1881
|
+
*/
|
|
1882
|
+
method: string;
|
|
1455
1883
|
};
|
|
1456
1884
|
const registry: RegisteredRoute[] = [];
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
>[];
|
|
1885
|
+
// Routes can register after startup via server.route(...), so the registry
|
|
1886
|
+
// is re-sorted lazily before the next dispatch instead of on every
|
|
1887
|
+
// registration.
|
|
1888
|
+
let registryNeedsSort = false;
|
|
1889
|
+
type FinalPorts = Ports & InferProviderPorts<Providers>;
|
|
1890
|
+
const providers = (options.providers ?? []) as readonly AnyServiceProvider[];
|
|
1464
1891
|
const env = options.providerEnv ?? process.env;
|
|
1465
1892
|
const overrides = options.providerConfig ?? {};
|
|
1466
1893
|
const providerResults: ProviderSetupResult<AnyPorts>[] = [];
|
|
1467
1894
|
const finalPorts = { ...options.ports } as FinalPorts;
|
|
1468
|
-
const
|
|
1895
|
+
const instrumentation = createServerInstrumentation<Ctx>(
|
|
1896
|
+
options.instrumentation,
|
|
1897
|
+
);
|
|
1898
|
+
const hooks = [
|
|
1899
|
+
...(instrumentation.hook
|
|
1900
|
+
? [instrumentation.hook as ServerHook<Ctx, FinalPorts>]
|
|
1901
|
+
: []),
|
|
1902
|
+
...((options.hooks ?? []) as ServerHook<Ctx, FinalPorts>[]),
|
|
1903
|
+
];
|
|
1469
1904
|
const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
|
|
1470
1905
|
|
|
1906
|
+
const resolvedContext = resolveServerContext<Ctx, FinalPorts, ServiceInput>(
|
|
1907
|
+
options.context,
|
|
1908
|
+
);
|
|
1909
|
+
const finalizeContext = createContextFinalizer(
|
|
1910
|
+
resolvedContext,
|
|
1911
|
+
() => finalPorts,
|
|
1912
|
+
);
|
|
1913
|
+
const createRequestContext = async (
|
|
1914
|
+
req: HttpRequestLike,
|
|
1915
|
+
contract?: HttpContractConfig,
|
|
1916
|
+
): Promise<Ctx> => {
|
|
1917
|
+
const { requestId, trace } = instrumentation.prepareRequest(req);
|
|
1918
|
+
return finalizeContext(
|
|
1919
|
+
await resolvedContext.request({
|
|
1920
|
+
req,
|
|
1921
|
+
ports: finalPorts,
|
|
1922
|
+
contract,
|
|
1923
|
+
requestId,
|
|
1924
|
+
trace,
|
|
1925
|
+
}),
|
|
1926
|
+
);
|
|
1927
|
+
};
|
|
1928
|
+
const createServiceContext = async (
|
|
1929
|
+
...args: ServiceContextInputArgs<ServiceInput>
|
|
1930
|
+
): Promise<Ctx> => {
|
|
1931
|
+
const serviceFactory = resolvedContext.service;
|
|
1932
|
+
if (!serviceFactory) {
|
|
1933
|
+
throw new Error(
|
|
1934
|
+
"Define context.service in createServer(...) to create service contexts.",
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
const { requestId, trace } = instrumentation.createServiceCorrelation();
|
|
1939
|
+
// Enter the ambient context synchronously (before the factory awaits) so
|
|
1940
|
+
// it propagates to the caller's continuation. Identity fields are filled
|
|
1941
|
+
// onto the same object once the context is finalized, so jobs, listeners,
|
|
1942
|
+
// schedules, and tasks observe the service actor/tenant at record time.
|
|
1943
|
+
const ambient: ActiveRequestContext = {
|
|
1944
|
+
requestId,
|
|
1945
|
+
traceId: trace.traceId,
|
|
1946
|
+
spanId: trace.spanId,
|
|
1947
|
+
parentSpanId: trace.parentSpanId,
|
|
1948
|
+
traceparent: trace.traceparent,
|
|
1949
|
+
};
|
|
1950
|
+
enterActiveRequestContext(ambient);
|
|
1951
|
+
const ctx = finalizeContext(
|
|
1952
|
+
await serviceFactory({
|
|
1953
|
+
ports: finalPorts,
|
|
1954
|
+
input: args[0] as ServiceInput,
|
|
1955
|
+
requestId,
|
|
1956
|
+
trace,
|
|
1957
|
+
}),
|
|
1958
|
+
);
|
|
1959
|
+
ambient.actor = readContextActor(ctx);
|
|
1960
|
+
ambient.tenant = readContextTenant(ctx);
|
|
1961
|
+
return ctx;
|
|
1962
|
+
};
|
|
1963
|
+
const contextRuntime = {
|
|
1964
|
+
createRequestContext,
|
|
1965
|
+
finalizeContext,
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
let serviceContextsAvailable = false;
|
|
1969
|
+
const lifecycleCreateServiceContext = async (
|
|
1970
|
+
input?: unknown,
|
|
1971
|
+
): Promise<unknown> => {
|
|
1972
|
+
if (!serviceContextsAvailable) {
|
|
1973
|
+
throw new Error(
|
|
1974
|
+
"Service contexts are unavailable until providers have started.",
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
return createServiceContext(
|
|
1979
|
+
...([input] as ServiceContextInputArgs<ServiceInput>),
|
|
1980
|
+
);
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1471
1983
|
let stopped = false;
|
|
1472
1984
|
const stop = async () => {
|
|
1473
1985
|
if (stopped) return;
|
|
@@ -1478,6 +1990,7 @@ export async function createServer<
|
|
|
1478
1990
|
try {
|
|
1479
1991
|
await result?.stop?.({
|
|
1480
1992
|
ports: finalPorts,
|
|
1993
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
1481
1994
|
});
|
|
1482
1995
|
} catch (err) {
|
|
1483
1996
|
errors.push(err);
|
|
@@ -1490,11 +2003,13 @@ export async function createServer<
|
|
|
1490
2003
|
|
|
1491
2004
|
const registeredPaths = new Set<string>();
|
|
1492
2005
|
const registeredShapes = new Map<string, string>();
|
|
2006
|
+
const registeredNames = new Map<string, string>();
|
|
1493
2007
|
|
|
1494
2008
|
const registerRoute = <C extends HttpContractConfig>(
|
|
1495
2009
|
contract: C,
|
|
1496
2010
|
handler: Handler<Ctx, C>,
|
|
1497
2011
|
routeHooks: readonly RouteHook<unknown, object>[] = [],
|
|
2012
|
+
responseValidationExemptStatus?: number,
|
|
1498
2013
|
): void => {
|
|
1499
2014
|
if (contract.body && !methodSupportsRequestBody(contract.method)) {
|
|
1500
2015
|
throw new Error(
|
|
@@ -1516,20 +2031,46 @@ export async function createServer<
|
|
|
1516
2031
|
`Ambiguous route: ${routeKey} conflicts with ${conflictingRoute}. Dynamic parameter names are ignored during routing, so each method + path shape must be unique.`,
|
|
1517
2032
|
);
|
|
1518
2033
|
}
|
|
2034
|
+
const conflictingName = registeredNames.get(contract.name);
|
|
2035
|
+
if (conflictingName) {
|
|
2036
|
+
throw new Error(
|
|
2037
|
+
`Duplicate contract name: "${contract.name}" is registered for both ${conflictingName} and ${routeKey}. Contract names must be unique because typed clients, OpenAPI operations, and devtools key on them.`,
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
if (contract.pathParams) {
|
|
2041
|
+
const shape = getObjectSchemaShape(contract.pathParams);
|
|
2042
|
+
if (shape) {
|
|
2043
|
+
const { missingKeys, extraKeys } = comparePathParamsToTemplate({
|
|
2044
|
+
pathKeys: compiled.keys,
|
|
2045
|
+
shapeKeys: Object.keys(shape),
|
|
2046
|
+
});
|
|
2047
|
+
if (missingKeys.length > 0 || extraKeys.length > 0) {
|
|
2048
|
+
const details = formatPathParamsMismatch({ missingKeys, extraKeys });
|
|
2049
|
+
throw new Error(
|
|
2050
|
+
`Path parameters for contract "${contract.name}" must match "${contract.path}" (${details}). Path templates and pathParams schemas drive routing, clients, and OpenAPI together.`,
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
1519
2055
|
registeredPaths.add(routeKey);
|
|
1520
2056
|
registeredShapes.set(shapeRouteKey, routeKey);
|
|
2057
|
+
registeredNames.set(contract.name, routeKey);
|
|
1521
2058
|
|
|
1522
2059
|
const builtHandler = buildHandler(
|
|
1523
2060
|
options,
|
|
1524
2061
|
finalPorts,
|
|
2062
|
+
contextRuntime,
|
|
1525
2063
|
contract,
|
|
1526
2064
|
handler,
|
|
1527
2065
|
hooks,
|
|
1528
2066
|
routeHooks,
|
|
2067
|
+
undefined,
|
|
2068
|
+
responseValidationExemptStatus,
|
|
1529
2069
|
);
|
|
1530
2070
|
registry.push({
|
|
1531
2071
|
contract,
|
|
1532
2072
|
compiled,
|
|
2073
|
+
method: contract.method.toUpperCase(),
|
|
1533
2074
|
handler: builtHandler,
|
|
1534
2075
|
match: (method, pathname) => {
|
|
1535
2076
|
if (contract.method.toUpperCase() !== method.toUpperCase()) {
|
|
@@ -1540,7 +2081,7 @@ export async function createServer<
|
|
|
1540
2081
|
return { matched: true as const };
|
|
1541
2082
|
},
|
|
1542
2083
|
});
|
|
1543
|
-
|
|
2084
|
+
registryNeedsSort = true;
|
|
1544
2085
|
};
|
|
1545
2086
|
|
|
1546
2087
|
const createBuilder = <C extends HttpContractConfig>(
|
|
@@ -1548,7 +2089,14 @@ export async function createServer<
|
|
|
1548
2089
|
shouldRegister: boolean,
|
|
1549
2090
|
): RouteBuilder<Ctx, C> => ({
|
|
1550
2091
|
handle: (fn) => {
|
|
1551
|
-
const wrapped = buildHandler(
|
|
2092
|
+
const wrapped = buildHandler(
|
|
2093
|
+
options,
|
|
2094
|
+
finalPorts,
|
|
2095
|
+
contextRuntime,
|
|
2096
|
+
contract,
|
|
2097
|
+
fn,
|
|
2098
|
+
hooks,
|
|
2099
|
+
);
|
|
1552
2100
|
if (shouldRegister) registerRoute(contract, fn);
|
|
1553
2101
|
return wrapped;
|
|
1554
2102
|
},
|
|
@@ -1558,11 +2106,36 @@ export async function createServer<
|
|
|
1558
2106
|
try {
|
|
1559
2107
|
for (const route of options.routes) {
|
|
1560
2108
|
const contract = resolveContract(route.contract);
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
2109
|
+
const hasHandle = typeof route.handle === "function";
|
|
2110
|
+
const hasUseCase = isUseCaseRouteDef(route);
|
|
2111
|
+
|
|
2112
|
+
if (hasHandle && hasUseCase) {
|
|
2113
|
+
throw new Error(
|
|
2114
|
+
`Route for contract "${contract.name}" declares both "handle" and "useCase". Bind the contract to exactly one of them.`,
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
if (!hasHandle && !hasUseCase) {
|
|
2118
|
+
throw new Error(
|
|
2119
|
+
`Route for contract "${contract.name}" declares neither "handle" nor "useCase". Bind the contract to a use case or implement a handler.`,
|
|
2120
|
+
);
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
if (isUseCaseRouteDef(route)) {
|
|
2124
|
+
const { handler, responseValidationExemptStatus } =
|
|
2125
|
+
createUseCaseRouteHandler<Ctx, typeof contract>(contract, route);
|
|
2126
|
+
registerRoute(
|
|
2127
|
+
contract,
|
|
2128
|
+
handler,
|
|
2129
|
+
route.hooks as readonly RouteHook<unknown, object>[] | undefined,
|
|
2130
|
+
responseValidationExemptStatus,
|
|
2131
|
+
);
|
|
2132
|
+
} else {
|
|
2133
|
+
registerRoute(
|
|
2134
|
+
contract,
|
|
2135
|
+
route.handle as Handler<Ctx, typeof contract>,
|
|
2136
|
+
route.hooks as readonly RouteHook<unknown, object>[] | undefined,
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
1566
2139
|
}
|
|
1567
2140
|
} catch (error) {
|
|
1568
2141
|
try {
|
|
@@ -1583,6 +2156,7 @@ export async function createServer<
|
|
|
1583
2156
|
const result = await provider.setup({
|
|
1584
2157
|
ports: finalPorts,
|
|
1585
2158
|
config: cfg,
|
|
2159
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
1586
2160
|
});
|
|
1587
2161
|
if (result.ports) {
|
|
1588
2162
|
Object.assign(finalPorts, result.ports);
|
|
@@ -1594,8 +2168,31 @@ export async function createServer<
|
|
|
1594
2168
|
if (!result.start) continue;
|
|
1595
2169
|
await result.start({
|
|
1596
2170
|
ports: finalPorts,
|
|
2171
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
1597
2172
|
});
|
|
1598
2173
|
}
|
|
2174
|
+
|
|
2175
|
+
instrumentation.attachPorts(finalPorts);
|
|
2176
|
+
|
|
2177
|
+
const onUnboundPorts = options.onUnboundPorts ?? "error";
|
|
2178
|
+
if (onUnboundPorts !== "ignore") {
|
|
2179
|
+
const unboundKeys = Object.keys(finalPorts).filter((key) =>
|
|
2180
|
+
isUnboundPort((finalPorts as AnyPorts)[key]),
|
|
2181
|
+
);
|
|
2182
|
+
if (unboundKeys.length > 0) {
|
|
2183
|
+
const message =
|
|
2184
|
+
`Unbound ports after provider startup: ${unboundKeys.join(", ")}. ` +
|
|
2185
|
+
"Each port declared as deferred in definePorts(...) must be contributed " +
|
|
2186
|
+
"by a provider (server/providers.ts) or bound in infra/app-ports.ts. " +
|
|
2187
|
+
'Pass onUnboundPorts: "warn" or "ignore" to change this behavior.';
|
|
2188
|
+
if (onUnboundPorts === "error") {
|
|
2189
|
+
throw new Error(message);
|
|
2190
|
+
}
|
|
2191
|
+
console.warn(`[beignet] ${message}`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
serviceContextsAvailable = true;
|
|
1599
2196
|
} catch (error) {
|
|
1600
2197
|
try {
|
|
1601
2198
|
await stop();
|
|
@@ -1608,39 +2205,88 @@ export async function createServer<
|
|
|
1608
2205
|
throw error;
|
|
1609
2206
|
}
|
|
1610
2207
|
|
|
2208
|
+
// The fallback 404/405 pipeline is built once at server creation. Only the
|
|
2209
|
+
// contract surface that hooks observe (method, path, and the Allow set for
|
|
2210
|
+
// 405s) is assembled per unmatched request.
|
|
2211
|
+
const executeFallback = createRequestExecutor<
|
|
2212
|
+
Ctx,
|
|
2213
|
+
Ports,
|
|
2214
|
+
HttpContractConfig,
|
|
2215
|
+
Providers
|
|
2216
|
+
>(options, finalPorts, contextRuntime, hooks, [], {
|
|
2217
|
+
skipRoutePreparation: true,
|
|
2218
|
+
});
|
|
2219
|
+
|
|
2220
|
+
const fallbackContract = (
|
|
2221
|
+
name: string,
|
|
2222
|
+
method: string,
|
|
2223
|
+
path: string,
|
|
2224
|
+
): HttpContractConfig => ({
|
|
2225
|
+
kind: "http",
|
|
2226
|
+
name,
|
|
2227
|
+
method: method as HttpContractConfig["method"],
|
|
2228
|
+
path,
|
|
2229
|
+
pathParams: null,
|
|
2230
|
+
query: null,
|
|
2231
|
+
body: null,
|
|
2232
|
+
responses: {},
|
|
2233
|
+
metadata: {},
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
const notFoundHandler: Handler<Ctx, HttpContractConfig> = async () =>
|
|
2237
|
+
errorResponse(404, "NOT_FOUND", "Not found");
|
|
2238
|
+
|
|
1611
2239
|
const api = async (req: HttpRequestLike) => {
|
|
2240
|
+
if (registryNeedsSort) {
|
|
2241
|
+
registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
|
|
2242
|
+
registryNeedsSort = false;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
1612
2245
|
const url = new URL(req.url);
|
|
2246
|
+
const method = req.method.toUpperCase();
|
|
1613
2247
|
|
|
2248
|
+
let pathMatchedMethods: Set<string> | undefined;
|
|
1614
2249
|
for (const entry of registry) {
|
|
1615
|
-
|
|
1616
|
-
if (
|
|
2250
|
+
if (!entry.compiled.pattern.test(url.pathname)) continue;
|
|
2251
|
+
if (entry.method === method) {
|
|
1617
2252
|
return await entry.handler(req);
|
|
1618
2253
|
}
|
|
2254
|
+
if (!pathMatchedMethods) {
|
|
2255
|
+
pathMatchedMethods = new Set();
|
|
2256
|
+
}
|
|
2257
|
+
pathMatchedMethods.add(entry.method);
|
|
1619
2258
|
}
|
|
1620
2259
|
|
|
1621
|
-
const
|
|
1622
|
-
kind: "http",
|
|
1623
|
-
name: "notFound",
|
|
1624
|
-
method: req.method.toUpperCase() as HttpContractConfig["method"],
|
|
1625
|
-
path: url.pathname || "/",
|
|
1626
|
-
pathParams: null,
|
|
1627
|
-
query: null,
|
|
1628
|
-
body: null,
|
|
1629
|
-
responses: {},
|
|
1630
|
-
metadata: {},
|
|
1631
|
-
};
|
|
2260
|
+
const pathname = url.pathname || "/";
|
|
1632
2261
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2262
|
+
if (pathMatchedMethods) {
|
|
2263
|
+
const allow = [...pathMatchedMethods].sort().join(", ");
|
|
2264
|
+
|
|
2265
|
+
return await executeFallback(
|
|
2266
|
+
{
|
|
2267
|
+
contract: fallbackContract("methodNotAllowed", method, pathname),
|
|
2268
|
+
handler: async () => ({
|
|
2269
|
+
status: 405,
|
|
2270
|
+
headers: { allow },
|
|
2271
|
+
body: createErrorResponseBody({
|
|
2272
|
+
code: "METHOD_NOT_ALLOWED",
|
|
2273
|
+
message: `Method ${method} is not allowed for ${pathname}`,
|
|
2274
|
+
}),
|
|
2275
|
+
}),
|
|
2276
|
+
},
|
|
2277
|
+
req,
|
|
2278
|
+
{},
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
1642
2281
|
|
|
1643
|
-
return await
|
|
2282
|
+
return await executeFallback(
|
|
2283
|
+
{
|
|
2284
|
+
contract: fallbackContract("notFound", method, pathname),
|
|
2285
|
+
handler: notFoundHandler,
|
|
2286
|
+
},
|
|
2287
|
+
req,
|
|
2288
|
+
{},
|
|
2289
|
+
);
|
|
1644
2290
|
};
|
|
1645
2291
|
|
|
1646
2292
|
return {
|
|
@@ -1649,6 +2295,8 @@ export async function createServer<
|
|
|
1649
2295
|
const contract = resolveContract(contractLike);
|
|
1650
2296
|
return createBuilder(contract, true);
|
|
1651
2297
|
},
|
|
2298
|
+
createRequestContext: (req) => createRequestContext(req),
|
|
2299
|
+
createServiceContext,
|
|
1652
2300
|
contracts,
|
|
1653
2301
|
stop,
|
|
1654
2302
|
ports: finalPorts,
|
|
@@ -1664,7 +2312,7 @@ export async function createServer<
|
|
|
1664
2312
|
* @example
|
|
1665
2313
|
* ```ts
|
|
1666
2314
|
* const routes = defineRoutes<AppContext>([
|
|
1667
|
-
* { contract: listPosts,
|
|
2315
|
+
* { contract: listPosts, useCase: listPostsUseCase },
|
|
1668
2316
|
* ]);
|
|
1669
2317
|
* ```
|
|
1670
2318
|
*/
|
|
@@ -1672,11 +2320,11 @@ export function defineRoutes<
|
|
|
1672
2320
|
Ctx,
|
|
1673
2321
|
const R extends
|
|
1674
2322
|
readonly ContextualRouteInput<Ctx>[] = readonly ContextualRouteInput<Ctx>[],
|
|
1675
|
-
>(routes: R): FlattenRouteInputs<R>;
|
|
2323
|
+
>(routes: R & ValidatedRouteInputs<Ctx, R>): FlattenRouteInputs<R>;
|
|
1676
2324
|
export function defineRoutes<
|
|
1677
2325
|
Ctx,
|
|
1678
2326
|
const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
|
|
1679
|
-
>(routes: R): FlattenRouteInputs<R>;
|
|
2327
|
+
>(routes: R & ValidatedRouteInputs<Ctx, R>): FlattenRouteInputs<R>;
|
|
1680
2328
|
export function defineRoutes<
|
|
1681
2329
|
Ctx,
|
|
1682
2330
|
const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
|
|
@@ -1727,7 +2375,7 @@ export function contractsFromRoutes<
|
|
|
1727
2375
|
* name: "todos",
|
|
1728
2376
|
* hooks: [auth.optional()],
|
|
1729
2377
|
* routes: [
|
|
1730
|
-
* { contract: listTodos,
|
|
2378
|
+
* { contract: listTodos, useCase: listTodosUseCase },
|
|
1731
2379
|
* ]
|
|
1732
2380
|
* });
|
|
1733
2381
|
* ```
|
|
@@ -1740,7 +2388,7 @@ export function defineRouteGroup<
|
|
|
1740
2388
|
>(group: {
|
|
1741
2389
|
name: string;
|
|
1742
2390
|
hooks?: readonly RouteHook<Ctx, object>[];
|
|
1743
|
-
routes: R
|
|
2391
|
+
routes: R & ValidatedRouteInputs<Ctx, R>;
|
|
1744
2392
|
}): RouteGroup<Ctx, R>;
|
|
1745
2393
|
export function defineRouteGroup<
|
|
1746
2394
|
Ctx,
|