@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/src/server/server.ts
CHANGED
|
@@ -7,70 +7,151 @@ 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,
|
|
31
53
|
HttpResponse,
|
|
32
54
|
HttpResponseLike,
|
|
33
55
|
ResolvedRoute,
|
|
56
|
+
RouteHook,
|
|
34
57
|
ServerCaughtErrorHook,
|
|
35
58
|
ServerHook,
|
|
36
59
|
ServerUnhandledErrorMapper,
|
|
37
|
-
} from "./http";
|
|
60
|
+
} from "./http.js";
|
|
61
|
+
import type { ServerInstrumentationOptions } from "./instrumentation.js";
|
|
62
|
+
import { createServerInstrumentation } from "./instrumentation.js";
|
|
38
63
|
import {
|
|
39
64
|
loadProviderConfig,
|
|
40
65
|
parseStandardSchema,
|
|
41
66
|
SchemaValidationError,
|
|
42
|
-
} 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";
|
|
43
85
|
|
|
44
86
|
/**
|
|
45
|
-
* Route registration
|
|
87
|
+
* Route registration that connects a contract to the handler implementing it.
|
|
46
88
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* `defineRoutes(...)`.
|
|
89
|
+
* Most apps keep route definitions in `features/<feature>/routes.ts` and
|
|
90
|
+
* compose them with `defineRoutes(...)`.
|
|
50
91
|
*/
|
|
51
|
-
export type
|
|
92
|
+
export type HandlerRouteDef<
|
|
93
|
+
Ctx,
|
|
94
|
+
CLike extends ContractLike = ContractLike,
|
|
95
|
+
Hooks extends readonly RouteHook<Ctx, object>[] = readonly RouteHook<
|
|
96
|
+
Ctx,
|
|
97
|
+
object
|
|
98
|
+
>[],
|
|
99
|
+
> = {
|
|
52
100
|
/**
|
|
53
101
|
* Contract builder or plain contract config for this route.
|
|
54
102
|
*/
|
|
55
103
|
contract: CLike;
|
|
104
|
+
/**
|
|
105
|
+
* Route-scoped hooks that run after group hooks and before the handler.
|
|
106
|
+
*/
|
|
107
|
+
hooks?: Hooks;
|
|
56
108
|
/**
|
|
57
109
|
* Handler that implements the contract.
|
|
58
110
|
*/
|
|
59
|
-
handle: Handler<Ctx
|
|
111
|
+
handle: Handler<Ctx & AddedCtxFromHooks<Hooks>, ResolveContract<CLike>>;
|
|
112
|
+
useCase?: never;
|
|
113
|
+
input?: never;
|
|
114
|
+
status?: never;
|
|
60
115
|
};
|
|
61
116
|
|
|
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>;
|
|
133
|
+
|
|
134
|
+
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
|
|
135
|
+
type AnyRouteDef = RouteDef<any, any>;
|
|
136
|
+
|
|
137
|
+
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
|
|
138
|
+
type PlainRouteDef<Ctx> = RouteDef<Ctx, any, readonly []>;
|
|
139
|
+
|
|
140
|
+
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
|
|
141
|
+
type AnyContractRouteDef<Ctx> = RouteDef<Ctx, any>;
|
|
142
|
+
|
|
62
143
|
const ROUTE_GROUP_KIND = "beignet.route-group";
|
|
63
144
|
|
|
64
145
|
/**
|
|
65
146
|
* Named collection of related route registrations.
|
|
66
147
|
*
|
|
67
|
-
* Route groups
|
|
68
|
-
* them before server
|
|
148
|
+
* Route groups colocate feature routes and can apply scoped route hooks to
|
|
149
|
+
* every route in the group. `defineRoutes(...)` flattens them before server
|
|
150
|
+
* registration while preserving those hooks.
|
|
69
151
|
*/
|
|
70
152
|
export type RouteGroup<
|
|
71
153
|
Ctx,
|
|
72
|
-
|
|
73
|
-
Routes extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
|
|
154
|
+
Routes extends readonly AnyRouteDef[] = readonly AnyRouteDef[],
|
|
74
155
|
> = {
|
|
75
156
|
/**
|
|
76
157
|
* Internal marker used by `defineRoutes(...)`.
|
|
@@ -80,6 +161,10 @@ export type RouteGroup<
|
|
|
80
161
|
* Human-readable group name.
|
|
81
162
|
*/
|
|
82
163
|
name: string;
|
|
164
|
+
/**
|
|
165
|
+
* Hooks applied to every route in this group.
|
|
166
|
+
*/
|
|
167
|
+
hooks?: readonly RouteHook<Ctx, object>[];
|
|
83
168
|
/**
|
|
84
169
|
* Route definitions in this group.
|
|
85
170
|
*/
|
|
@@ -87,24 +172,45 @@ export type RouteGroup<
|
|
|
87
172
|
};
|
|
88
173
|
|
|
89
174
|
type RouteInput<Ctx> =
|
|
90
|
-
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
|
|
175
|
+
| AnyContractRouteDef<Ctx>
|
|
176
|
+
| AnyRouteDef
|
|
177
|
+
| RouteGroup<Ctx, readonly AnyRouteDef[]>;
|
|
178
|
+
|
|
179
|
+
type ContextualRouteInput<Ctx> =
|
|
180
|
+
| PlainRouteDef<Ctx>
|
|
181
|
+
| RouteGroup<Ctx, readonly AnyRouteDef[]>;
|
|
182
|
+
|
|
183
|
+
type RouteGroupBuilder<Ctx> = {
|
|
184
|
+
<
|
|
185
|
+
const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
|
|
186
|
+
const R extends readonly PlainRouteDef<
|
|
187
|
+
Ctx & AddedCtxFromHooks<GroupHooks>
|
|
188
|
+
>[] = readonly PlainRouteDef<Ctx & AddedCtxFromHooks<GroupHooks>>[],
|
|
189
|
+
>(group: {
|
|
190
|
+
name: string;
|
|
191
|
+
hooks?: GroupHooks;
|
|
192
|
+
routes: R & ValidatedRouteInputs<Ctx & AddedCtxFromHooks<GroupHooks>, R>;
|
|
193
|
+
}): RouteGroup<Ctx, R>;
|
|
194
|
+
<
|
|
195
|
+
const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
|
|
196
|
+
const R extends readonly AnyRouteDef[] = readonly AnyRouteDef[],
|
|
197
|
+
>(group: {
|
|
198
|
+
name: string;
|
|
199
|
+
hooks?: GroupHooks;
|
|
200
|
+
routes: R & ValidatedRouteInputs<Ctx & AddedCtxFromHooks<GroupHooks>, R>;
|
|
201
|
+
}): RouteGroup<Ctx, R>;
|
|
202
|
+
};
|
|
94
203
|
|
|
95
204
|
type RoutesFromInput<Input> =
|
|
96
|
-
|
|
97
|
-
Input extends RouteGroup<any, infer Routes>
|
|
205
|
+
Input extends RouteGroup<infer _Ctx, infer Routes>
|
|
98
206
|
? Routes
|
|
99
|
-
:
|
|
100
|
-
Input extends RouteDef<any, any>
|
|
207
|
+
: Input extends AnyRouteDef
|
|
101
208
|
? readonly [Input]
|
|
102
209
|
: readonly [];
|
|
103
210
|
|
|
104
211
|
type FlattenRouteInputs<Inputs extends readonly unknown[]> =
|
|
105
212
|
number extends Inputs["length"]
|
|
106
|
-
?
|
|
107
|
-
readonly RouteDef<any, any>[]
|
|
213
|
+
? readonly AnyRouteDef[]
|
|
108
214
|
: Inputs extends readonly [infer First, ...infer Rest]
|
|
109
215
|
? readonly [...RoutesFromInput<First>, ...FlattenRouteInputs<Rest>]
|
|
110
216
|
: readonly [];
|
|
@@ -122,20 +228,62 @@ type ContractsFromRouteList<
|
|
|
122
228
|
: never;
|
|
123
229
|
};
|
|
124
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Define one route registration with hook-aware handler typing.
|
|
233
|
+
*
|
|
234
|
+
* Direct route objects are still supported. Use this helper when route-scoped
|
|
235
|
+
* hooks enrich `ctx` for a single handler and you want TypeScript to infer the
|
|
236
|
+
* added fields.
|
|
237
|
+
*/
|
|
238
|
+
export function defineRoute<Ctx>() {
|
|
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<
|
|
247
|
+
CLike extends ContractLike,
|
|
248
|
+
const Hooks extends readonly RouteHook<Ctx, object>[] = readonly [],
|
|
249
|
+
>(
|
|
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;
|
|
257
|
+
}
|
|
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
|
+
|
|
125
277
|
/**
|
|
126
278
|
* Options for creating a Beignet server instance.
|
|
127
279
|
*/
|
|
128
280
|
export type CreateServerOptions<
|
|
129
281
|
Ctx,
|
|
130
282
|
Ports extends AnyPorts,
|
|
283
|
+
ServiceInput = void,
|
|
131
284
|
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
|
|
132
|
-
Routes extends readonly RouteDef<
|
|
133
|
-
Providers extends readonly
|
|
134
|
-
unknown,
|
|
135
|
-
// biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
|
|
136
|
-
StandardSchemaV1<any, any>,
|
|
137
|
-
AnyPorts
|
|
138
|
-
>[] = readonly [],
|
|
285
|
+
Routes extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
|
|
286
|
+
Providers extends readonly AnyServiceProvider[] = readonly [],
|
|
139
287
|
> = {
|
|
140
288
|
/**
|
|
141
289
|
* App-owned ports available to context creation, hooks, and handlers.
|
|
@@ -159,24 +307,64 @@ export type CreateServerOptions<
|
|
|
159
307
|
providerConfig?: Record<string, unknown>;
|
|
160
308
|
|
|
161
309
|
/**
|
|
162
|
-
*
|
|
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.
|
|
163
316
|
*
|
|
164
317
|
* The `ports` argument includes app ports plus ports provided during server
|
|
165
318
|
* startup.
|
|
166
319
|
*/
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
320
|
+
context: ServerContextConfig<
|
|
321
|
+
Ctx,
|
|
322
|
+
Ports & InferProviderPorts<Providers>,
|
|
323
|
+
ServiceInput
|
|
324
|
+
>;
|
|
172
325
|
/**
|
|
173
326
|
* Server hooks that wrap every registered route.
|
|
174
327
|
*/
|
|
175
|
-
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;
|
|
176
352
|
/**
|
|
177
353
|
* Route list to register up front.
|
|
178
354
|
*/
|
|
179
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";
|
|
180
368
|
/**
|
|
181
369
|
* Global caught-error observer.
|
|
182
370
|
*/
|
|
@@ -196,7 +384,11 @@ interface RouteBuilder<Ctx, C extends HttpContractConfig> {
|
|
|
196
384
|
/**
|
|
197
385
|
* Runtime server object returned by `createServer(...)`.
|
|
198
386
|
*/
|
|
199
|
-
export interface ServerInstance<
|
|
387
|
+
export interface ServerInstance<
|
|
388
|
+
Ctx,
|
|
389
|
+
Ports extends AnyPorts = AnyPorts,
|
|
390
|
+
ServiceInput = void,
|
|
391
|
+
> {
|
|
200
392
|
/**
|
|
201
393
|
* Catch-all request handler for platform adapters.
|
|
202
394
|
*/
|
|
@@ -207,6 +399,22 @@ export interface ServerInstance<Ctx, Ports extends AnyPorts = AnyPorts> {
|
|
|
207
399
|
route: <CLike extends ContractLike>(
|
|
208
400
|
contractLike: CLike,
|
|
209
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>;
|
|
210
418
|
/**
|
|
211
419
|
* Contract configs registered through the `routes` option.
|
|
212
420
|
*/
|
|
@@ -302,6 +510,59 @@ function errorResponse(
|
|
|
302
510
|
};
|
|
303
511
|
}
|
|
304
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
|
+
|
|
305
566
|
function normalizeResponse(res: HttpResponseLike): HttpResponseLike {
|
|
306
567
|
return {
|
|
307
568
|
status: res.status,
|
|
@@ -395,6 +656,48 @@ function responseForHooks(res: HttpResponse): HttpResponseLike {
|
|
|
395
656
|
};
|
|
396
657
|
}
|
|
397
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
|
+
|
|
398
701
|
function isHttpResponseLike(value: unknown): value is HttpResponseLike {
|
|
399
702
|
return (
|
|
400
703
|
!isWebResponse(value) &&
|
|
@@ -421,6 +724,37 @@ class ResponseContractViolationError extends Error {
|
|
|
421
724
|
}
|
|
422
725
|
}
|
|
423
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
|
+
|
|
424
758
|
function getDeclaredCatalogErrorsForStatus(
|
|
425
759
|
contract: HttpContractConfig,
|
|
426
760
|
status: number,
|
|
@@ -458,10 +792,8 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
458
792
|
if (!matchingError) {
|
|
459
793
|
throw new ResponseContractViolationError({
|
|
460
794
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
461
|
-
message:
|
|
462
|
-
|
|
463
|
-
`(status ${res.status}, contract: ${contract.name})`,
|
|
464
|
-
details: {
|
|
795
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
796
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
465
797
|
issues: [
|
|
466
798
|
{
|
|
467
799
|
message:
|
|
@@ -469,7 +801,7 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
469
801
|
`Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
|
|
470
802
|
},
|
|
471
803
|
],
|
|
472
|
-
},
|
|
804
|
+
}),
|
|
473
805
|
});
|
|
474
806
|
}
|
|
475
807
|
|
|
@@ -480,10 +812,10 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
480
812
|
if (error instanceof SchemaValidationError) {
|
|
481
813
|
throw new ResponseContractViolationError({
|
|
482
814
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
483
|
-
message:
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
815
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
816
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
817
|
+
issues: error.issues,
|
|
818
|
+
}),
|
|
487
819
|
});
|
|
488
820
|
}
|
|
489
821
|
throw error;
|
|
@@ -494,6 +826,7 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
494
826
|
async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
495
827
|
contract: C,
|
|
496
828
|
res: HttpResponseLike,
|
|
829
|
+
responseValidationExemptStatus?: number,
|
|
497
830
|
): Promise<void> {
|
|
498
831
|
const statusKey = String(res.status);
|
|
499
832
|
const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
|
|
@@ -506,10 +839,9 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
506
839
|
message:
|
|
507
840
|
`Handler returned undeclared status ${res.status} for ` +
|
|
508
841
|
`${contract.method} ${contract.path} (contract: ${contract.name})`,
|
|
509
|
-
details: {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
},
|
|
842
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
843
|
+
returnedStatus: res.status,
|
|
844
|
+
}),
|
|
513
845
|
});
|
|
514
846
|
}
|
|
515
847
|
|
|
@@ -518,17 +850,15 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
518
850
|
if (res.body !== undefined && res.body !== null) {
|
|
519
851
|
throw new ResponseContractViolationError({
|
|
520
852
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
521
|
-
message:
|
|
522
|
-
|
|
523
|
-
`(status ${res.status}, contract: ${contract.name})`,
|
|
524
|
-
details: {
|
|
853
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
854
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
525
855
|
issues: [
|
|
526
856
|
{
|
|
527
857
|
message:
|
|
528
858
|
"Response body must be empty for a null response schema.",
|
|
529
859
|
},
|
|
530
860
|
],
|
|
531
|
-
},
|
|
861
|
+
}),
|
|
532
862
|
});
|
|
533
863
|
}
|
|
534
864
|
return;
|
|
@@ -536,6 +866,11 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
536
866
|
|
|
537
867
|
if (!responseSchema) return;
|
|
538
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
|
+
|
|
539
874
|
try {
|
|
540
875
|
await parseStandardSchema(responseSchema, res.body);
|
|
541
876
|
await validateCatalogErrorResponse(contract, res);
|
|
@@ -543,10 +878,10 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
543
878
|
if (error instanceof SchemaValidationError) {
|
|
544
879
|
throw new ResponseContractViolationError({
|
|
545
880
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
546
|
-
message:
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
881
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
882
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
883
|
+
issues: error.issues,
|
|
884
|
+
}),
|
|
550
885
|
});
|
|
551
886
|
}
|
|
552
887
|
throw error;
|
|
@@ -556,9 +891,14 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
556
891
|
async function finalizeResponse<C extends HttpContractConfig>(
|
|
557
892
|
contract: C,
|
|
558
893
|
res: HttpResponseLike,
|
|
894
|
+
responseValidationExemptStatus?: number,
|
|
559
895
|
): Promise<HttpResponseLike> {
|
|
560
896
|
const normalized = normalizeResponse(res);
|
|
561
|
-
await validateResponseAgainstContract(
|
|
897
|
+
await validateResponseAgainstContract(
|
|
898
|
+
contract,
|
|
899
|
+
normalized,
|
|
900
|
+
responseValidationExemptStatus,
|
|
901
|
+
);
|
|
562
902
|
return normalized;
|
|
563
903
|
}
|
|
564
904
|
|
|
@@ -618,38 +958,65 @@ async function parseBody(req: HttpRequestLike): Promise<unknown> {
|
|
|
618
958
|
}
|
|
619
959
|
}
|
|
620
960
|
|
|
621
|
-
|
|
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<
|
|
622
985
|
Ctx,
|
|
623
986
|
Ports extends AnyPorts,
|
|
624
987
|
C extends HttpContractConfig,
|
|
625
|
-
Providers extends readonly
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
StandardSchemaV1<any, any>,
|
|
629
|
-
AnyPorts
|
|
630
|
-
>[] = readonly [],
|
|
631
|
-
FinalPorts extends Ports & ProvidedPortsOfList<Providers> = Ports &
|
|
632
|
-
ProvidedPortsOfList<Providers>,
|
|
988
|
+
Providers extends readonly AnyServiceProvider[] = readonly [],
|
|
989
|
+
FinalPorts extends Ports & InferProviderPorts<Providers> = Ports &
|
|
990
|
+
InferProviderPorts<Providers>,
|
|
633
991
|
>(
|
|
634
992
|
// biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
|
|
635
|
-
options: CreateServerOptions<Ctx, Ports, any, Providers>,
|
|
993
|
+
options: CreateServerOptions<Ctx, Ports, any, any, Providers>,
|
|
636
994
|
finalPorts: FinalPorts,
|
|
637
|
-
|
|
638
|
-
|
|
995
|
+
contextRuntime: {
|
|
996
|
+
createRequestContext: (
|
|
997
|
+
req: HttpRequestLike,
|
|
998
|
+
contract: HttpContractConfig,
|
|
999
|
+
) => Promise<Ctx>;
|
|
1000
|
+
finalizeContext: (seed: ContextSeed<Ctx> | Ctx) => Ctx;
|
|
1001
|
+
},
|
|
639
1002
|
hooks: ServerHook<Ctx, FinalPorts>[],
|
|
1003
|
+
routeHooks: readonly RouteHook<unknown, object>[] = [],
|
|
640
1004
|
optionsOverrides?: {
|
|
641
1005
|
skipRoutePreparation?: boolean;
|
|
642
1006
|
},
|
|
643
1007
|
): (
|
|
1008
|
+
target: ExecutionTarget<Ctx, C>,
|
|
644
1009
|
req: HttpRequestLike,
|
|
645
1010
|
preMatchedParams?: Record<string, string>,
|
|
646
1011
|
) => Promise<HttpResponse> {
|
|
647
|
-
const
|
|
1012
|
+
const warnedNativeReplacementHooks = new WeakSet<object>();
|
|
648
1013
|
|
|
649
1014
|
return async (
|
|
1015
|
+
target: ExecutionTarget<Ctx, C>,
|
|
650
1016
|
req: HttpRequestLike,
|
|
651
1017
|
preMatchedParams?: Record<string, string>,
|
|
652
1018
|
) => {
|
|
1019
|
+
const { contract, handler: userHandler } = target;
|
|
653
1020
|
let baseCtx: Ctx | undefined;
|
|
654
1021
|
let pathValue: HandlerArgs<Ctx, C>["path"] | undefined;
|
|
655
1022
|
let queryValue: HandlerArgs<Ctx, C>["query"] | undefined;
|
|
@@ -730,6 +1097,53 @@ function buildHandler<
|
|
|
730
1097
|
};
|
|
731
1098
|
}
|
|
732
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
|
+
|
|
733
1147
|
if (currentError instanceof GateAuthorizationError) {
|
|
734
1148
|
return {
|
|
735
1149
|
ctx,
|
|
@@ -814,8 +1228,10 @@ function buildHandler<
|
|
|
814
1228
|
if (preMatchedParams) {
|
|
815
1229
|
matchedParams = preMatchedParams;
|
|
816
1230
|
} else {
|
|
817
|
-
const
|
|
1231
|
+
const compiled = target.compiled;
|
|
1232
|
+
const match = compiled ? compiled.pattern.exec(url.pathname) : null;
|
|
818
1233
|
if (
|
|
1234
|
+
!compiled ||
|
|
819
1235
|
!match ||
|
|
820
1236
|
contract.method.toUpperCase() !== req.method.toUpperCase()
|
|
821
1237
|
) {
|
|
@@ -832,15 +1248,67 @@ function buildHandler<
|
|
|
832
1248
|
}
|
|
833
1249
|
const rawHeaders = requestHeadersToRecord(req.headers);
|
|
834
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
|
+
|
|
835
1297
|
const applyTransformHooks = async (
|
|
836
1298
|
initialResult: ExecutionResult<Ctx>,
|
|
837
1299
|
allowRetry: boolean,
|
|
838
1300
|
): Promise<ExecutionResult<Ctx>> => {
|
|
839
|
-
if (isWebResponse(initialResult.response)) {
|
|
840
|
-
return initialResult;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
1301
|
try {
|
|
1302
|
+
if (isWebResponse(initialResult.response)) {
|
|
1303
|
+
return {
|
|
1304
|
+
...initialResult,
|
|
1305
|
+
response: await runNativeBeforeSend(
|
|
1306
|
+
initialResult,
|
|
1307
|
+
initialResult.response,
|
|
1308
|
+
),
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
844
1312
|
let transformed = normalizeResponse(initialResult.response);
|
|
845
1313
|
for (const hook of hooks) {
|
|
846
1314
|
if (!hook.beforeSend) continue;
|
|
@@ -917,11 +1385,10 @@ function buildHandler<
|
|
|
917
1385
|
if (optionsOverrides?.skipRoutePreparation) {
|
|
918
1386
|
let createdCtx!: Ctx;
|
|
919
1387
|
try {
|
|
920
|
-
createdCtx = await
|
|
1388
|
+
createdCtx = await contextRuntime.createRequestContext(
|
|
921
1389
|
req,
|
|
922
|
-
ports: finalPorts,
|
|
923
1390
|
contract,
|
|
924
|
-
|
|
1391
|
+
);
|
|
925
1392
|
baseCtx = createdCtx;
|
|
926
1393
|
} catch (error) {
|
|
927
1394
|
result = await resolveErrorResult(
|
|
@@ -977,26 +1444,17 @@ function buildHandler<
|
|
|
977
1444
|
try {
|
|
978
1445
|
query = await parseStandardSchema(contract.query, query);
|
|
979
1446
|
} catch (error) {
|
|
980
|
-
result =
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
: {
|
|
992
|
-
response: errorResponse(
|
|
993
|
-
422,
|
|
994
|
-
"VALIDATION_ERROR",
|
|
995
|
-
"Invalid query parameters",
|
|
996
|
-
(error as Error).message,
|
|
997
|
-
),
|
|
998
|
-
owner: "framework",
|
|
999
|
-
};
|
|
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
|
+
};
|
|
1000
1458
|
}
|
|
1001
1459
|
}
|
|
1002
1460
|
|
|
@@ -1009,26 +1467,17 @@ function buildHandler<
|
|
|
1009
1467
|
matchedParams,
|
|
1010
1468
|
);
|
|
1011
1469
|
} catch (error) {
|
|
1012
|
-
result =
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
: {
|
|
1024
|
-
response: errorResponse(
|
|
1025
|
-
422,
|
|
1026
|
-
"VALIDATION_ERROR",
|
|
1027
|
-
"Invalid path parameters",
|
|
1028
|
-
(error as Error).message,
|
|
1029
|
-
),
|
|
1030
|
-
owner: "framework",
|
|
1031
|
-
};
|
|
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
|
+
};
|
|
1032
1481
|
}
|
|
1033
1482
|
}
|
|
1034
1483
|
|
|
@@ -1039,26 +1488,17 @@ function buildHandler<
|
|
|
1039
1488
|
try {
|
|
1040
1489
|
headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
|
|
1041
1490
|
} catch (error) {
|
|
1042
|
-
result =
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
: {
|
|
1054
|
-
response: errorResponse(
|
|
1055
|
-
422,
|
|
1056
|
-
"VALIDATION_ERROR",
|
|
1057
|
-
"Invalid request headers",
|
|
1058
|
-
(error as Error).message,
|
|
1059
|
-
),
|
|
1060
|
-
owner: "framework",
|
|
1061
|
-
};
|
|
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
|
+
};
|
|
1062
1502
|
}
|
|
1063
1503
|
}
|
|
1064
1504
|
|
|
@@ -1067,9 +1507,16 @@ function buildHandler<
|
|
|
1067
1507
|
if (!result) {
|
|
1068
1508
|
try {
|
|
1069
1509
|
body = await parseBody(req);
|
|
1070
|
-
} catch {
|
|
1510
|
+
} catch (error) {
|
|
1071
1511
|
result = {
|
|
1072
|
-
response:
|
|
1512
|
+
response: requestValidationError(
|
|
1513
|
+
contract,
|
|
1514
|
+
400,
|
|
1515
|
+
"INVALID_BODY",
|
|
1516
|
+
"Malformed JSON",
|
|
1517
|
+
"body",
|
|
1518
|
+
error,
|
|
1519
|
+
),
|
|
1073
1520
|
owner: "framework",
|
|
1074
1521
|
};
|
|
1075
1522
|
}
|
|
@@ -1084,34 +1531,28 @@ function buildHandler<
|
|
|
1084
1531
|
error instanceof SchemaValidationError
|
|
1085
1532
|
) {
|
|
1086
1533
|
result = {
|
|
1087
|
-
response:
|
|
1534
|
+
response: requestValidationError(
|
|
1535
|
+
contract,
|
|
1088
1536
|
400,
|
|
1089
1537
|
"MISSING_BODY",
|
|
1090
1538
|
"Request body is required",
|
|
1539
|
+
"body",
|
|
1540
|
+
error,
|
|
1091
1541
|
),
|
|
1092
1542
|
owner: "framework",
|
|
1093
1543
|
};
|
|
1094
1544
|
} else {
|
|
1095
|
-
result =
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
: {
|
|
1107
|
-
response: errorResponse(
|
|
1108
|
-
422,
|
|
1109
|
-
"VALIDATION_ERROR",
|
|
1110
|
-
"Invalid request body",
|
|
1111
|
-
(error as Error).message,
|
|
1112
|
-
),
|
|
1113
|
-
owner: "framework",
|
|
1114
|
-
};
|
|
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
|
+
};
|
|
1115
1556
|
}
|
|
1116
1557
|
}
|
|
1117
1558
|
}
|
|
@@ -1124,11 +1565,10 @@ function buildHandler<
|
|
|
1124
1565
|
|
|
1125
1566
|
let createdCtx!: Ctx;
|
|
1126
1567
|
try {
|
|
1127
|
-
createdCtx = await
|
|
1568
|
+
createdCtx = await contextRuntime.createRequestContext(
|
|
1128
1569
|
req,
|
|
1129
|
-
ports: finalPorts,
|
|
1130
1570
|
contract,
|
|
1131
|
-
|
|
1571
|
+
);
|
|
1132
1572
|
baseCtx = createdCtx;
|
|
1133
1573
|
} catch (error) {
|
|
1134
1574
|
result = await resolveErrorResult(
|
|
@@ -1183,7 +1623,7 @@ function buildHandler<
|
|
|
1183
1623
|
break;
|
|
1184
1624
|
}
|
|
1185
1625
|
if (hookResult?.ctx !== undefined) {
|
|
1186
|
-
currentCtx = hookResult.ctx;
|
|
1626
|
+
currentCtx = contextRuntime.finalizeContext(hookResult.ctx);
|
|
1187
1627
|
}
|
|
1188
1628
|
if (hookResult?.response) {
|
|
1189
1629
|
const response = normalizeHttpResponse(hookResult.response);
|
|
@@ -1209,6 +1649,47 @@ function buildHandler<
|
|
|
1209
1649
|
}
|
|
1210
1650
|
|
|
1211
1651
|
if (!result) {
|
|
1652
|
+
for (const hook of routeHooks) {
|
|
1653
|
+
try {
|
|
1654
|
+
const additions = await hook.resolve({
|
|
1655
|
+
req,
|
|
1656
|
+
ctx: currentCtx,
|
|
1657
|
+
contract,
|
|
1658
|
+
path,
|
|
1659
|
+
query,
|
|
1660
|
+
headers,
|
|
1661
|
+
body,
|
|
1662
|
+
});
|
|
1663
|
+
if (additions && typeof additions === "object") {
|
|
1664
|
+
currentCtx = contextRuntime.finalizeContext({
|
|
1665
|
+
...(currentCtx as object),
|
|
1666
|
+
...additions,
|
|
1667
|
+
} as Ctx);
|
|
1668
|
+
}
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
result = await resolveErrorResult(
|
|
1671
|
+
error,
|
|
1672
|
+
currentCtx,
|
|
1673
|
+
pathValue,
|
|
1674
|
+
queryValue,
|
|
1675
|
+
headersValue,
|
|
1676
|
+
bodyValue,
|
|
1677
|
+
{ owner: "framework" },
|
|
1678
|
+
);
|
|
1679
|
+
break;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
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
|
+
});
|
|
1212
1693
|
try {
|
|
1213
1694
|
result = {
|
|
1214
1695
|
ctx: currentCtx,
|
|
@@ -1237,9 +1718,17 @@ function buildHandler<
|
|
|
1237
1718
|
let finalResponse = normalizeHttpResponse(result.response);
|
|
1238
1719
|
let finalError = result.error;
|
|
1239
1720
|
let finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
1240
|
-
if (
|
|
1721
|
+
if (
|
|
1722
|
+
finalOwner === "route" &&
|
|
1723
|
+
!isWebResponse(finalResponse) &&
|
|
1724
|
+
(options.validateResponses ?? true)
|
|
1725
|
+
) {
|
|
1241
1726
|
try {
|
|
1242
|
-
finalResponse = await finalizeResponse(
|
|
1727
|
+
finalResponse = await finalizeResponse(
|
|
1728
|
+
contract,
|
|
1729
|
+
finalResponse,
|
|
1730
|
+
target.responseValidationExemptStatus,
|
|
1731
|
+
);
|
|
1243
1732
|
} catch (error) {
|
|
1244
1733
|
if (error instanceof ResponseContractViolationError) {
|
|
1245
1734
|
result = await applyTransformHooks(
|
|
@@ -1312,6 +1801,55 @@ function buildHandler<
|
|
|
1312
1801
|
};
|
|
1313
1802
|
}
|
|
1314
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
|
+
|
|
1315
1853
|
/**
|
|
1316
1854
|
* Create a Beignet server instance.
|
|
1317
1855
|
*
|
|
@@ -1320,42 +1858,128 @@ function buildHandler<
|
|
|
1320
1858
|
* Use adapter packages such as `@beignet/next` to expose `server.api` to a
|
|
1321
1859
|
* specific runtime.
|
|
1322
1860
|
*
|
|
1323
|
-
* @param options - Ports, providers, routes, hooks, context
|
|
1324
|
-
* mapping hooks for the server.
|
|
1861
|
+
* @param options - Ports, providers, routes, hooks, context blueprint, and
|
|
1862
|
+
* error mapping hooks for the server.
|
|
1325
1863
|
* @returns A started server instance with final ports and a catch-all handler.
|
|
1326
1864
|
*/
|
|
1327
1865
|
export async function createServer<
|
|
1328
1866
|
Ctx,
|
|
1329
1867
|
Ports extends AnyPorts,
|
|
1868
|
+
ServiceInput = void,
|
|
1330
1869
|
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
|
|
1331
|
-
Routes extends readonly RouteDef<
|
|
1332
|
-
Providers extends readonly
|
|
1333
|
-
unknown,
|
|
1334
|
-
// biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
|
|
1335
|
-
StandardSchemaV1<any, any>,
|
|
1336
|
-
AnyPorts
|
|
1337
|
-
>[] = readonly [],
|
|
1870
|
+
Routes extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
|
|
1871
|
+
Providers extends readonly AnyServiceProvider[] = readonly [],
|
|
1338
1872
|
>(
|
|
1339
|
-
options: CreateServerOptions<Ctx, Ports, Routes, Providers>,
|
|
1340
|
-
): Promise<
|
|
1873
|
+
options: CreateServerOptions<Ctx, Ports, ServiceInput, Routes, Providers>,
|
|
1874
|
+
): Promise<
|
|
1875
|
+
ServerInstance<Ctx, Ports & InferProviderPorts<Providers>, ServiceInput>
|
|
1876
|
+
> {
|
|
1341
1877
|
type RegisteredRoute = ResolvedRoute<Ctx, HttpContractConfig> & {
|
|
1342
1878
|
compiled: CompiledPath;
|
|
1879
|
+
/**
|
|
1880
|
+
* Uppercased contract method, cached for dispatch.
|
|
1881
|
+
*/
|
|
1882
|
+
method: string;
|
|
1343
1883
|
};
|
|
1344
1884
|
const registry: RegisteredRoute[] = [];
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
>[];
|
|
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[];
|
|
1352
1891
|
const env = options.providerEnv ?? process.env;
|
|
1353
1892
|
const overrides = options.providerConfig ?? {};
|
|
1354
1893
|
const providerResults: ProviderSetupResult<AnyPorts>[] = [];
|
|
1355
1894
|
const finalPorts = { ...options.ports } as FinalPorts;
|
|
1356
|
-
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
|
+
];
|
|
1357
1904
|
const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
|
|
1358
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
|
+
|
|
1359
1983
|
let stopped = false;
|
|
1360
1984
|
const stop = async () => {
|
|
1361
1985
|
if (stopped) return;
|
|
@@ -1366,6 +1990,7 @@ export async function createServer<
|
|
|
1366
1990
|
try {
|
|
1367
1991
|
await result?.stop?.({
|
|
1368
1992
|
ports: finalPorts,
|
|
1993
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
1369
1994
|
});
|
|
1370
1995
|
} catch (err) {
|
|
1371
1996
|
errors.push(err);
|
|
@@ -1378,10 +2003,13 @@ export async function createServer<
|
|
|
1378
2003
|
|
|
1379
2004
|
const registeredPaths = new Set<string>();
|
|
1380
2005
|
const registeredShapes = new Map<string, string>();
|
|
2006
|
+
const registeredNames = new Map<string, string>();
|
|
1381
2007
|
|
|
1382
2008
|
const registerRoute = <C extends HttpContractConfig>(
|
|
1383
2009
|
contract: C,
|
|
1384
2010
|
handler: Handler<Ctx, C>,
|
|
2011
|
+
routeHooks: readonly RouteHook<unknown, object>[] = [],
|
|
2012
|
+
responseValidationExemptStatus?: number,
|
|
1385
2013
|
): void => {
|
|
1386
2014
|
if (contract.body && !methodSupportsRequestBody(contract.method)) {
|
|
1387
2015
|
throw new Error(
|
|
@@ -1403,19 +2031,46 @@ export async function createServer<
|
|
|
1403
2031
|
`Ambiguous route: ${routeKey} conflicts with ${conflictingRoute}. Dynamic parameter names are ignored during routing, so each method + path shape must be unique.`,
|
|
1404
2032
|
);
|
|
1405
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
|
+
}
|
|
1406
2055
|
registeredPaths.add(routeKey);
|
|
1407
2056
|
registeredShapes.set(shapeRouteKey, routeKey);
|
|
2057
|
+
registeredNames.set(contract.name, routeKey);
|
|
1408
2058
|
|
|
1409
2059
|
const builtHandler = buildHandler(
|
|
1410
2060
|
options,
|
|
1411
2061
|
finalPorts,
|
|
2062
|
+
contextRuntime,
|
|
1412
2063
|
contract,
|
|
1413
2064
|
handler,
|
|
1414
2065
|
hooks,
|
|
2066
|
+
routeHooks,
|
|
2067
|
+
undefined,
|
|
2068
|
+
responseValidationExemptStatus,
|
|
1415
2069
|
);
|
|
1416
2070
|
registry.push({
|
|
1417
2071
|
contract,
|
|
1418
2072
|
compiled,
|
|
2073
|
+
method: contract.method.toUpperCase(),
|
|
1419
2074
|
handler: builtHandler,
|
|
1420
2075
|
match: (method, pathname) => {
|
|
1421
2076
|
if (contract.method.toUpperCase() !== method.toUpperCase()) {
|
|
@@ -1426,7 +2081,7 @@ export async function createServer<
|
|
|
1426
2081
|
return { matched: true as const };
|
|
1427
2082
|
},
|
|
1428
2083
|
});
|
|
1429
|
-
|
|
2084
|
+
registryNeedsSort = true;
|
|
1430
2085
|
};
|
|
1431
2086
|
|
|
1432
2087
|
const createBuilder = <C extends HttpContractConfig>(
|
|
@@ -1434,7 +2089,14 @@ export async function createServer<
|
|
|
1434
2089
|
shouldRegister: boolean,
|
|
1435
2090
|
): RouteBuilder<Ctx, C> => ({
|
|
1436
2091
|
handle: (fn) => {
|
|
1437
|
-
const wrapped = buildHandler(
|
|
2092
|
+
const wrapped = buildHandler(
|
|
2093
|
+
options,
|
|
2094
|
+
finalPorts,
|
|
2095
|
+
contextRuntime,
|
|
2096
|
+
contract,
|
|
2097
|
+
fn,
|
|
2098
|
+
hooks,
|
|
2099
|
+
);
|
|
1438
2100
|
if (shouldRegister) registerRoute(contract, fn);
|
|
1439
2101
|
return wrapped;
|
|
1440
2102
|
},
|
|
@@ -1444,7 +2106,36 @@ export async function createServer<
|
|
|
1444
2106
|
try {
|
|
1445
2107
|
for (const route of options.routes) {
|
|
1446
2108
|
const contract = resolveContract(route.contract);
|
|
1447
|
-
|
|
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
|
+
}
|
|
1448
2139
|
}
|
|
1449
2140
|
} catch (error) {
|
|
1450
2141
|
try {
|
|
@@ -1465,6 +2156,7 @@ export async function createServer<
|
|
|
1465
2156
|
const result = await provider.setup({
|
|
1466
2157
|
ports: finalPorts,
|
|
1467
2158
|
config: cfg,
|
|
2159
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
1468
2160
|
});
|
|
1469
2161
|
if (result.ports) {
|
|
1470
2162
|
Object.assign(finalPorts, result.ports);
|
|
@@ -1476,8 +2168,31 @@ export async function createServer<
|
|
|
1476
2168
|
if (!result.start) continue;
|
|
1477
2169
|
await result.start({
|
|
1478
2170
|
ports: finalPorts,
|
|
2171
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
1479
2172
|
});
|
|
1480
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;
|
|
1481
2196
|
} catch (error) {
|
|
1482
2197
|
try {
|
|
1483
2198
|
await stop();
|
|
@@ -1490,38 +2205,88 @@ export async function createServer<
|
|
|
1490
2205
|
throw error;
|
|
1491
2206
|
}
|
|
1492
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
|
+
|
|
1493
2239
|
const api = async (req: HttpRequestLike) => {
|
|
2240
|
+
if (registryNeedsSort) {
|
|
2241
|
+
registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
|
|
2242
|
+
registryNeedsSort = false;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
1494
2245
|
const url = new URL(req.url);
|
|
2246
|
+
const method = req.method.toUpperCase();
|
|
1495
2247
|
|
|
2248
|
+
let pathMatchedMethods: Set<string> | undefined;
|
|
1496
2249
|
for (const entry of registry) {
|
|
1497
|
-
|
|
1498
|
-
if (
|
|
2250
|
+
if (!entry.compiled.pattern.test(url.pathname)) continue;
|
|
2251
|
+
if (entry.method === method) {
|
|
1499
2252
|
return await entry.handler(req);
|
|
1500
2253
|
}
|
|
2254
|
+
if (!pathMatchedMethods) {
|
|
2255
|
+
pathMatchedMethods = new Set();
|
|
2256
|
+
}
|
|
2257
|
+
pathMatchedMethods.add(entry.method);
|
|
1501
2258
|
}
|
|
1502
2259
|
|
|
1503
|
-
const
|
|
1504
|
-
kind: "http",
|
|
1505
|
-
name: "notFound",
|
|
1506
|
-
method: req.method.toUpperCase() as HttpContractConfig["method"],
|
|
1507
|
-
path: url.pathname || "/",
|
|
1508
|
-
pathParams: null,
|
|
1509
|
-
query: null,
|
|
1510
|
-
body: null,
|
|
1511
|
-
responses: {},
|
|
1512
|
-
metadata: {},
|
|
1513
|
-
};
|
|
2260
|
+
const pathname = url.pathname || "/";
|
|
1514
2261
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
+
}
|
|
1523
2281
|
|
|
1524
|
-
return await
|
|
2282
|
+
return await executeFallback(
|
|
2283
|
+
{
|
|
2284
|
+
contract: fallbackContract("notFound", method, pathname),
|
|
2285
|
+
handler: notFoundHandler,
|
|
2286
|
+
},
|
|
2287
|
+
req,
|
|
2288
|
+
{},
|
|
2289
|
+
);
|
|
1525
2290
|
};
|
|
1526
2291
|
|
|
1527
2292
|
return {
|
|
@@ -1530,6 +2295,8 @@ export async function createServer<
|
|
|
1530
2295
|
const contract = resolveContract(contractLike);
|
|
1531
2296
|
return createBuilder(contract, true);
|
|
1532
2297
|
},
|
|
2298
|
+
createRequestContext: (req) => createRequestContext(req),
|
|
2299
|
+
createServiceContext,
|
|
1533
2300
|
contracts,
|
|
1534
2301
|
stop,
|
|
1535
2302
|
ports: finalPorts,
|
|
@@ -1545,10 +2312,19 @@ export async function createServer<
|
|
|
1545
2312
|
* @example
|
|
1546
2313
|
* ```ts
|
|
1547
2314
|
* const routes = defineRoutes<AppContext>([
|
|
1548
|
-
* { contract: listPosts,
|
|
2315
|
+
* { contract: listPosts, useCase: listPostsUseCase },
|
|
1549
2316
|
* ]);
|
|
1550
2317
|
* ```
|
|
1551
2318
|
*/
|
|
2319
|
+
export function defineRoutes<
|
|
2320
|
+
Ctx,
|
|
2321
|
+
const R extends
|
|
2322
|
+
readonly ContextualRouteInput<Ctx>[] = readonly ContextualRouteInput<Ctx>[],
|
|
2323
|
+
>(routes: R & ValidatedRouteInputs<Ctx, R>): FlattenRouteInputs<R>;
|
|
2324
|
+
export function defineRoutes<
|
|
2325
|
+
Ctx,
|
|
2326
|
+
const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
|
|
2327
|
+
>(routes: R & ValidatedRouteInputs<Ctx, R>): FlattenRouteInputs<R>;
|
|
1552
2328
|
export function defineRoutes<
|
|
1553
2329
|
Ctx,
|
|
1554
2330
|
const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
|
|
@@ -1557,7 +2333,12 @@ export function defineRoutes<
|
|
|
1557
2333
|
|
|
1558
2334
|
for (const route of routes) {
|
|
1559
2335
|
if (isRouteGroup(route)) {
|
|
1560
|
-
|
|
2336
|
+
for (const groupRoute of route.routes) {
|
|
2337
|
+
flattened.push({
|
|
2338
|
+
...groupRoute,
|
|
2339
|
+
hooks: [...(route.hooks ?? []), ...(groupRoute.hooks ?? [])],
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
1561
2342
|
} else {
|
|
1562
2343
|
flattened.push(route);
|
|
1563
2344
|
}
|
|
@@ -1585,26 +2366,61 @@ export function contractsFromRoutes<
|
|
|
1585
2366
|
* Define a named group of related route registrations.
|
|
1586
2367
|
*
|
|
1587
2368
|
* Route groups are flattened by defineRoutes, so createServer still receives
|
|
1588
|
-
* a regular route list while app code can keep feature route wiring
|
|
2369
|
+
* a regular route list while app code can keep feature route wiring and scoped
|
|
2370
|
+
* hooks colocated.
|
|
1589
2371
|
*
|
|
1590
2372
|
* @example
|
|
1591
2373
|
* ```ts
|
|
1592
|
-
* const todoRoutes = defineRouteGroup<AppContext>({
|
|
2374
|
+
* const todoRoutes = defineRouteGroup<AppContext>()({
|
|
1593
2375
|
* name: "todos",
|
|
2376
|
+
* hooks: [auth.optional()],
|
|
1594
2377
|
* routes: [
|
|
1595
|
-
* { contract: listTodos,
|
|
2378
|
+
* { contract: listTodos, useCase: listTodosUseCase },
|
|
1596
2379
|
* ]
|
|
1597
2380
|
* });
|
|
1598
2381
|
* ```
|
|
1599
2382
|
*/
|
|
2383
|
+
export function defineRouteGroup<Ctx>(): RouteGroupBuilder<Ctx>;
|
|
1600
2384
|
export function defineRouteGroup<
|
|
1601
2385
|
Ctx,
|
|
1602
2386
|
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
|
|
1603
2387
|
const R extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
|
|
1604
|
-
>(group: {
|
|
2388
|
+
>(group: {
|
|
2389
|
+
name: string;
|
|
2390
|
+
hooks?: readonly RouteHook<Ctx, object>[];
|
|
2391
|
+
routes: R & ValidatedRouteInputs<Ctx, R>;
|
|
2392
|
+
}): RouteGroup<Ctx, R>;
|
|
2393
|
+
export function defineRouteGroup<
|
|
2394
|
+
Ctx,
|
|
2395
|
+
// biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
|
|
2396
|
+
const R extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
|
|
2397
|
+
>(group?: {
|
|
2398
|
+
name: string;
|
|
2399
|
+
hooks?: readonly RouteHook<Ctx, object>[];
|
|
2400
|
+
routes: R;
|
|
2401
|
+
}): RouteGroup<Ctx, R> | RouteGroupBuilder<Ctx> {
|
|
2402
|
+
const createGroup = <
|
|
2403
|
+
const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
|
|
2404
|
+
const GroupRoutes extends readonly AnyRouteDef[] = readonly AnyRouteDef[],
|
|
2405
|
+
>(input: {
|
|
2406
|
+
name: string;
|
|
2407
|
+
hooks?: GroupHooks;
|
|
2408
|
+
routes: GroupRoutes;
|
|
2409
|
+
}): RouteGroup<Ctx, GroupRoutes> => ({
|
|
2410
|
+
kind: ROUTE_GROUP_KIND,
|
|
2411
|
+
name: input.name,
|
|
2412
|
+
hooks: input.hooks,
|
|
2413
|
+
routes: input.routes,
|
|
2414
|
+
});
|
|
2415
|
+
|
|
2416
|
+
if (!group) {
|
|
2417
|
+
return createGroup;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
1605
2420
|
return {
|
|
1606
2421
|
kind: ROUTE_GROUP_KIND,
|
|
1607
2422
|
name: group.name,
|
|
2423
|
+
hooks: group.hooks,
|
|
1608
2424
|
routes: group.routes,
|
|
1609
2425
|
};
|
|
1610
2426
|
}
|