@beignet/core 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +157 -0
- package/README.md +785 -43
- package/dist/application/index.d.ts +28 -2
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +140 -12
- package/dist/application/index.js.map +1 -1
- package/dist/client/client.d.ts +2 -2
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +136 -48
- package/dist/client/client.js.map +1 -1
- package/dist/client/error-messages.d.ts +14 -0
- package/dist/client/error-messages.d.ts.map +1 -0
- package/dist/client/error-messages.js +23 -0
- package/dist/client/error-messages.js.map +1 -0
- package/dist/client/index.d.ts +8 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +6 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +35 -5
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client-only.d.ts +8 -0
- package/dist/client-only.d.ts.map +1 -0
- package/dist/client-only.js +8 -0
- package/dist/client-only.js.map +1 -0
- package/dist/config/index.d.ts +5 -5
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/contracts/catalog-errors.d.ts +27 -0
- package/dist/contracts/catalog-errors.d.ts.map +1 -0
- package/dist/contracts/catalog-errors.js +69 -0
- package/dist/contracts/catalog-errors.js.map +1 -0
- package/dist/contracts/contract-builder.d.ts +15 -12
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +15 -41
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts +11 -8
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +13 -40
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/contract-like.d.ts +1 -1
- package/dist/contracts/contract-like.d.ts.map +1 -1
- package/dist/contracts/index.d.ts +13 -9
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +9 -5
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/openapi-meta.d.ts +48 -0
- package/dist/contracts/openapi-meta.d.ts.map +1 -1
- package/dist/contracts/openapi-meta.js +3 -0
- package/dist/contracts/openapi-meta.js.map +1 -1
- package/dist/contracts/path-template.d.ts +1 -1
- package/dist/contracts/path-template.js +2 -2
- package/dist/contracts/path-template.js.map +1 -1
- package/dist/contracts/schema-shape.d.ts +37 -0
- package/dist/contracts/schema-shape.d.ts.map +1 -0
- package/dist/contracts/schema-shape.js +61 -0
- package/dist/contracts/schema-shape.js.map +1 -0
- package/dist/contracts/success-status.d.ts +32 -0
- package/dist/contracts/success-status.d.ts.map +1 -0
- package/dist/contracts/success-status.js +18 -0
- package/dist/contracts/success-status.js.map +1 -0
- package/dist/contracts/types.d.ts +25 -5
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.js.map +1 -1
- package/dist/contracts/utils.d.ts +1 -1
- package/dist/contracts/utils.d.ts.map +1 -1
- package/dist/contracts/utils.js +1 -1
- package/dist/contracts/utils.js.map +1 -1
- package/dist/domain/events.d.ts +1 -1
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js +1 -1
- package/dist/domain/events.js.map +1 -1
- package/dist/domain/index.d.ts +3 -3
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +3 -3
- package/dist/domain/index.js.map +1 -1
- package/dist/errors/catalog.d.ts +9 -1
- package/dist/errors/catalog.d.ts.map +1 -1
- package/dist/errors/catalog.js +7 -1
- package/dist/errors/catalog.js.map +1 -1
- package/dist/errors/http.d.ts +10 -0
- package/dist/errors/http.d.ts.map +1 -1
- package/dist/errors/http.js +11 -1
- package/dist/errors/http.js.map +1 -1
- package/dist/errors/index.d.ts +4 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +4 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/errors/response.d.ts +4 -1
- package/dist/errors/response.d.ts.map +1 -1
- package/dist/errors/response.js.map +1 -1
- package/dist/events/index.d.ts +10 -12
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +10 -10
- package/dist/events/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +5 -3
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts +12 -14
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +13 -13
- package/dist/jobs/index.js.map +1 -1
- package/dist/notifications/index.d.ts +14 -16
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +14 -14
- package/dist/notifications/index.js.map +1 -1
- package/dist/openapi/index.d.ts +8 -3
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +41 -29
- package/dist/openapi/index.js.map +1 -1
- package/dist/openapi/schema-introspector.d.ts +37 -0
- package/dist/openapi/schema-introspector.d.ts.map +1 -1
- package/dist/openapi/schema-introspector.js +23 -17
- package/dist/openapi/schema-introspector.js.map +1 -1
- package/dist/outbox/index.d.ts +15 -6
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +60 -16
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/audit.d.ts +56 -10
- package/dist/ports/audit.d.ts.map +1 -1
- package/dist/ports/audit.js +71 -3
- package/dist/ports/audit.js.map +1 -1
- package/dist/ports/auth.d.ts +92 -0
- package/dist/ports/auth.d.ts.map +1 -1
- package/dist/ports/auth.js +92 -0
- package/dist/ports/auth.js.map +1 -1
- package/dist/ports/events.d.ts +2 -2
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/index.d.ts +62 -33
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +28 -34
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/policy.d.ts +32 -3
- package/dist/ports/policy.d.ts.map +1 -1
- package/dist/ports/policy.js +13 -2
- package/dist/ports/policy.js.map +1 -1
- package/dist/ports/testing.d.ts +1030 -2
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +1031 -1
- package/dist/ports/testing.js.map +1 -1
- package/dist/ports/unbound.d.ts +21 -0
- package/dist/ports/unbound.d.ts.map +1 -0
- package/dist/ports/unbound.js +57 -0
- package/dist/ports/unbound.js.map +1 -0
- package/dist/ports/unit-of-work.d.ts +1 -1
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +1 -1
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/providers/index.d.ts +3 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts +45 -4
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js +25 -6
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/providers/metadata.d.ts +39 -0
- package/dist/providers/metadata.d.ts.map +1 -0
- package/dist/providers/metadata.js +169 -0
- package/dist/providers/metadata.js.map +1 -0
- package/dist/providers/provider.d.ts +114 -9
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js +3 -20
- package/dist/providers/provider.js.map +1 -1
- package/dist/schedules/index.d.ts +94 -13
- package/dist/schedules/index.d.ts.map +1 -1
- package/dist/schedules/index.js +66 -12
- package/dist/schedules/index.js.map +1 -1
- package/dist/server/audit-context.d.ts +29 -0
- package/dist/server/audit-context.d.ts.map +1 -0
- package/dist/server/audit-context.js +44 -0
- package/dist/server/audit-context.js.map +1 -0
- package/dist/server/context.d.ts +141 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +39 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/contract-like.d.ts +1 -1
- package/dist/server/contract-like.d.ts.map +1 -1
- package/dist/server/contract-like.js +1 -1
- package/dist/server/contract-like.js.map +1 -1
- package/dist/server/health.d.ts +2 -2
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/hooks/auth.d.ts +49 -10
- package/dist/server/hooks/auth.d.ts.map +1 -1
- package/dist/server/hooks/auth.js +77 -37
- package/dist/server/hooks/auth.js.map +1 -1
- package/dist/server/hooks/cors.d.ts +1 -1
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/errors.d.ts +2 -2
- package/dist/server/hooks/errors.d.ts.map +1 -1
- package/dist/server/hooks/errors.js +2 -2
- package/dist/server/hooks/errors.js.map +1 -1
- package/dist/server/hooks/idempotency.d.ts +78 -0
- package/dist/server/hooks/idempotency.d.ts.map +1 -0
- package/dist/server/hooks/idempotency.js +154 -0
- package/dist/server/hooks/idempotency.js.map +1 -0
- package/dist/server/hooks/index.d.ts +8 -7
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js +6 -5
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/hooks/logging.d.ts +2 -2
- package/dist/server/hooks/logging.d.ts.map +1 -1
- package/dist/server/hooks/logging.js +1 -1
- package/dist/server/hooks/logging.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +25 -7
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +47 -12
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/hooks.d.ts +1 -1
- package/dist/server/hooks.d.ts.map +1 -1
- package/dist/server/hooks.js +1 -1
- package/dist/server/hooks.js.map +1 -1
- package/dist/server/http.d.ts +61 -35
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +1 -20
- package/dist/server/http.js.map +1 -1
- package/dist/server/index.d.ts +36 -12
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +24 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts +108 -0
- package/dist/server/instrumentation.d.ts.map +1 -0
- package/dist/server/instrumentation.js +297 -0
- package/dist/server/instrumentation.js.map +1 -0
- package/dist/server/openapi.d.ts +3 -3
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/openapi.js +1 -1
- package/dist/server/openapi.js.map +1 -1
- package/dist/server/providers/index.d.ts +3 -3
- package/dist/server/providers/index.d.ts.map +1 -1
- package/dist/server/providers/index.js +3 -3
- package/dist/server/providers/index.js.map +1 -1
- package/dist/server/providers/loadProviderConfig.d.ts +2 -2
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
- package/dist/server/providers/loadProviderConfig.js +2 -2
- package/dist/server/providers/loadProviderConfig.js.map +1 -1
- package/dist/server/request-context.d.ts +67 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/request-context.js +79 -0
- package/dist/server/request-context.js.map +1 -0
- package/dist/server/server-context.d.ts +38 -0
- package/dist/server/server-context.d.ts.map +1 -0
- package/dist/server/server-context.js +38 -0
- package/dist/server/server-context.js.map +1 -0
- package/dist/server/server.d.ts +105 -33
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +434 -118
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +2 -2
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -2
- package/dist/server/types.js.map +1 -1
- package/dist/server/use-case-route.d.ts +263 -0
- package/dist/server/use-case-route.d.ts.map +1 -0
- package/dist/server/use-case-route.js +77 -0
- package/dist/server/use-case-route.js.map +1 -0
- package/dist/server-only.d.ts +8 -0
- package/dist/server-only.d.ts.map +1 -0
- package/dist/server-only.js +8 -0
- package/dist/server-only.js.map +1 -0
- package/dist/tasks/index.d.ts +139 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +98 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/testing/index.d.ts +607 -5
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +426 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/index.d.ts +89 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +101 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/uploads/client.d.ts +1 -1
- package/dist/uploads/client.d.ts.map +1 -1
- package/dist/uploads/index.d.ts +2 -2
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +1 -1
- package/dist/uploads/index.js.map +1 -1
- package/package.json +24 -2
- package/src/application/index.ts +193 -10
- package/src/client/client.ts +148 -150
- package/src/client/error-messages.ts +35 -0
- package/src/client/index.ts +12 -4
- package/src/client/types.ts +44 -5
- package/src/client-only.ts +7 -0
- package/src/config/index.ts +6 -6
- package/src/contracts/catalog-errors.ts +115 -0
- package/src/contracts/contract-builder.ts +39 -76
- package/src/contracts/contract-group.ts +33 -68
- package/src/contracts/contract-like.ts +1 -1
- package/src/contracts/index.ts +24 -11
- package/src/contracts/openapi-meta.ts +55 -0
- package/src/contracts/path-template.ts +2 -2
- package/src/contracts/schema-shape.ts +75 -0
- package/src/contracts/success-status.ts +68 -0
- package/src/contracts/types.ts +32 -5
- package/src/contracts/utils.ts +5 -2
- package/src/domain/events.ts +6 -2
- package/src/domain/index.ts +3 -3
- package/src/errors/catalog.ts +9 -1
- package/src/errors/http.ts +11 -1
- package/src/errors/index.ts +4 -4
- package/src/errors/response.ts +4 -1
- package/src/events/index.ts +12 -26
- package/src/idempotency/index.ts +5 -3
- package/src/jobs/index.ts +14 -24
- package/src/notifications/index.ts +17 -27
- package/src/openapi/index.ts +73 -38
- package/src/openapi/schema-introspector.ts +68 -17
- package/src/outbox/index.ts +84 -19
- package/src/ports/audit.ts +120 -11
- package/src/ports/auth.ts +132 -0
- package/src/ports/events.ts +2 -2
- package/src/ports/index.ts +104 -35
- package/src/ports/policy.ts +50 -3
- package/src/ports/testing.ts +2220 -33
- package/src/ports/unbound.ts +64 -0
- package/src/ports/unit-of-work.ts +6 -2
- package/src/providers/index.ts +16 -3
- package/src/providers/instrumentation.ts +86 -7
- package/src/providers/metadata.ts +234 -0
- package/src/providers/provider.ts +168 -9
- package/src/schedules/index.ts +173 -23
- package/src/server/audit-context.ts +45 -0
- package/src/server/context.ts +224 -0
- package/src/server/contract-like.ts +1 -1
- package/src/server/health.ts +2 -2
- package/src/server/hooks/auth.ts +141 -51
- package/src/server/hooks/cors.ts +1 -1
- package/src/server/hooks/errors.ts +7 -4
- package/src/server/hooks/idempotency.ts +263 -0
- package/src/server/hooks/index.ts +14 -7
- package/src/server/hooks/logging.ts +3 -3
- package/src/server/hooks/rate-limit.ts +85 -17
- package/src/server/hooks.ts +1 -1
- package/src/server/http.ts +78 -51
- package/src/server/index.ts +62 -12
- package/src/server/instrumentation.ts +470 -0
- package/src/server/openapi.ts +4 -4
- package/src/server/providers/index.ts +6 -3
- package/src/server/providers/loadProviderConfig.ts +4 -4
- package/src/server/request-context.ts +116 -0
- package/src/server/server-context.ts +44 -0
- package/src/server/server.ts +886 -238
- package/src/server/types.ts +2 -2
- package/src/server/use-case-route.ts +430 -0
- package/src/server-only.ts +7 -0
- package/src/tasks/index.ts +275 -0
- package/src/testing/index.ts +1142 -6
- package/src/tracing/index.ts +176 -0
- package/src/uploads/client.ts +1 -1
- package/src/uploads/index.ts +7 -3
- package/dist/ports/mailer.d.ts +0 -6
- package/dist/ports/mailer.d.ts.map +0 -1
- package/dist/ports/mailer.js +0 -2
- package/dist/ports/mailer.js.map +0 -1
- package/dist/ports/schedules.d.ts +0 -9
- package/dist/ports/schedules.d.ts.map +0 -1
- package/dist/ports/schedules.js +0 -2
- package/dist/ports/schedules.js.map +0 -1
package/src/server/types.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from "./contract-like";
|
|
2
|
-
export * from "./http";
|
|
1
|
+
export * from "./contract-like.js";
|
|
2
|
+
export * from "./http.js";
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type {
|
|
3
|
+
HttpContractConfig,
|
|
4
|
+
InferOutput,
|
|
5
|
+
Success2xxKeys,
|
|
6
|
+
} from "../contracts/index.js";
|
|
7
|
+
import { inferSoleSuccessStatus } from "../contracts/index.js";
|
|
8
|
+
import type { ContractLike, ResolveContract } from "./contract-like.js";
|
|
9
|
+
import type {
|
|
10
|
+
AddedCtxFromHooks,
|
|
11
|
+
Handler,
|
|
12
|
+
InferBody,
|
|
13
|
+
InferHeaders,
|
|
14
|
+
InferPath,
|
|
15
|
+
InferQuery,
|
|
16
|
+
RouteHook,
|
|
17
|
+
} from "./http.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Structural shape of a finalized use case accepted by the route binder.
|
|
21
|
+
*
|
|
22
|
+
* This intentionally mirrors `UseCaseDef` from `@beignet/core/application`
|
|
23
|
+
* without importing it, so the server runtime stays decoupled from the
|
|
24
|
+
* application builder at runtime.
|
|
25
|
+
*/
|
|
26
|
+
export type AnyUseCaseLike = {
|
|
27
|
+
/**
|
|
28
|
+
* Stable use-case name, used in binder diagnostics.
|
|
29
|
+
*/
|
|
30
|
+
name: string;
|
|
31
|
+
/**
|
|
32
|
+
* Input schema declared with `.input(...)`.
|
|
33
|
+
*/
|
|
34
|
+
inputSchema: StandardSchemaV1;
|
|
35
|
+
/**
|
|
36
|
+
* Output schema declared with `.output(...)`.
|
|
37
|
+
*/
|
|
38
|
+
outputSchema: StandardSchemaV1;
|
|
39
|
+
/**
|
|
40
|
+
* Execute the use case with application context and typed input.
|
|
41
|
+
*/
|
|
42
|
+
run: (args: never) => Promise<unknown>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type UseCaseRouteCtx<UC> = UC extends {
|
|
46
|
+
run: (args: { ctx: infer Ctx; input: infer _Input }) => Promise<infer _Out>;
|
|
47
|
+
}
|
|
48
|
+
? Ctx
|
|
49
|
+
: never;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Input type accepted by a bound use case's `run(...)`.
|
|
53
|
+
*/
|
|
54
|
+
export type UseCaseRouteInput<UC> = UC extends {
|
|
55
|
+
run: (args: { ctx: infer _Ctx; input: infer Input }) => Promise<infer _Out>;
|
|
56
|
+
}
|
|
57
|
+
? Input
|
|
58
|
+
: never;
|
|
59
|
+
|
|
60
|
+
type UseCaseRouteOutput<UC> = UC extends {
|
|
61
|
+
run: (args: { ctx: infer _Ctx; input: infer _Input }) => Promise<infer Out>;
|
|
62
|
+
}
|
|
63
|
+
? Out
|
|
64
|
+
: never;
|
|
65
|
+
|
|
66
|
+
type ResponseBodyForSchema<S> = S extends null
|
|
67
|
+
? // biome-ignore lint/suspicious/noConfusingVoidType: void accepts z.void() use case outputs for null response schemas
|
|
68
|
+
void | undefined
|
|
69
|
+
: S extends StandardSchemaV1
|
|
70
|
+
? InferOutput<S>
|
|
71
|
+
: unknown;
|
|
72
|
+
|
|
73
|
+
type ResponseForStatus<
|
|
74
|
+
TResponses,
|
|
75
|
+
TStatus extends number,
|
|
76
|
+
> = TStatus extends keyof TResponses
|
|
77
|
+
? TResponses[TStatus]
|
|
78
|
+
: `${TStatus}` extends keyof TResponses
|
|
79
|
+
? TResponses[`${TStatus}`]
|
|
80
|
+
: never;
|
|
81
|
+
|
|
82
|
+
type SuccessBodyFromKeys<TResponses, K> = [K] extends [never]
|
|
83
|
+
? unknown
|
|
84
|
+
: K extends number
|
|
85
|
+
? ResponseBodyForSchema<ResponseForStatus<TResponses, K>>
|
|
86
|
+
: unknown;
|
|
87
|
+
|
|
88
|
+
type UnionToIntersection<T> = (
|
|
89
|
+
T extends unknown
|
|
90
|
+
? (value: T) => void
|
|
91
|
+
: never
|
|
92
|
+
) extends (value: infer I) => void
|
|
93
|
+
? I
|
|
94
|
+
: never;
|
|
95
|
+
|
|
96
|
+
type BinderStatusFromKeys<K> = [K] extends [never]
|
|
97
|
+
? {
|
|
98
|
+
/**
|
|
99
|
+
* Success status for the use case result. Required because the contract
|
|
100
|
+
* does not declare exactly one 2xx response.
|
|
101
|
+
*/
|
|
102
|
+
status: number;
|
|
103
|
+
}
|
|
104
|
+
: [K] extends [UnionToIntersection<K>]
|
|
105
|
+
? {
|
|
106
|
+
/**
|
|
107
|
+
* Success status for the use case result. Optional because the
|
|
108
|
+
* contract declares exactly one 2xx response.
|
|
109
|
+
*/
|
|
110
|
+
status?: K;
|
|
111
|
+
}
|
|
112
|
+
: {
|
|
113
|
+
/**
|
|
114
|
+
* Success status for the use case result. Required because the
|
|
115
|
+
* contract declares multiple 2xx responses.
|
|
116
|
+
*/
|
|
117
|
+
status: K;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `status` option for a binder route.
|
|
122
|
+
*
|
|
123
|
+
* Optional and typed to the sole declared 2xx status when the contract
|
|
124
|
+
* declares exactly one, required (typed to the union of declared 2xx
|
|
125
|
+
* statuses) otherwise.
|
|
126
|
+
*/
|
|
127
|
+
export type BinderStatusOption<C extends HttpContractConfig> =
|
|
128
|
+
BinderStatusFromKeys<Success2xxKeys<C["responses"]>>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parsed request parts passed to a binder route's `input` mapper.
|
|
132
|
+
*/
|
|
133
|
+
export type UseCaseRouteInputParts<C extends HttpContractConfig> = {
|
|
134
|
+
/**
|
|
135
|
+
* Parsed path parameters.
|
|
136
|
+
*/
|
|
137
|
+
path: InferPath<C>;
|
|
138
|
+
/**
|
|
139
|
+
* Parsed query parameters.
|
|
140
|
+
*/
|
|
141
|
+
query: InferQuery<C>;
|
|
142
|
+
/**
|
|
143
|
+
* Parsed request headers.
|
|
144
|
+
*/
|
|
145
|
+
headers: InferHeaders<C>;
|
|
146
|
+
/**
|
|
147
|
+
* Parsed request body.
|
|
148
|
+
*/
|
|
149
|
+
body: InferBody<C>;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Constraint that checks a use case against the route that binds it.
|
|
154
|
+
*
|
|
155
|
+
* Produces a readable branded mismatch object on the `useCase` property when
|
|
156
|
+
* the use case requires a context the server does not provide, or when its
|
|
157
|
+
* output does not match the contract's declared success response schema.
|
|
158
|
+
*/
|
|
159
|
+
export type UseCaseFitsRoute<Ctx, C extends HttpContractConfig, UC> = [
|
|
160
|
+
Ctx,
|
|
161
|
+
] extends [UseCaseRouteCtx<UC>]
|
|
162
|
+
? [UseCaseRouteOutput<UC>] extends [
|
|
163
|
+
SuccessBodyFromKeys<C["responses"], Success2xxKeys<C["responses"]>>,
|
|
164
|
+
]
|
|
165
|
+
? unknown
|
|
166
|
+
: {
|
|
167
|
+
"~beignetError": "useCase output does not match the contract's success response schema";
|
|
168
|
+
}
|
|
169
|
+
: {
|
|
170
|
+
"~beignetError": "useCase requires a context this server does not provide";
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
type UseCaseRouteShape<
|
|
174
|
+
HandlerCtx,
|
|
175
|
+
CLike extends ContractLike,
|
|
176
|
+
C extends HttpContractConfig,
|
|
177
|
+
UC extends AnyUseCaseLike,
|
|
178
|
+
Hooks,
|
|
179
|
+
> = {
|
|
180
|
+
/**
|
|
181
|
+
* Contract builder or plain contract config for this route.
|
|
182
|
+
*/
|
|
183
|
+
contract: CLike;
|
|
184
|
+
/**
|
|
185
|
+
* Route-scoped hooks that run after group hooks and before the use case.
|
|
186
|
+
*/
|
|
187
|
+
hooks?: Hooks;
|
|
188
|
+
/**
|
|
189
|
+
* Use case bound directly to the contract.
|
|
190
|
+
*/
|
|
191
|
+
useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC>;
|
|
192
|
+
/**
|
|
193
|
+
* Map parsed request parts to the use case input.
|
|
194
|
+
*
|
|
195
|
+
* Defaults to `defaultBinderInput`, which merges query, body, and path
|
|
196
|
+
* objects (path wins collisions) and never merges headers.
|
|
197
|
+
*/
|
|
198
|
+
input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
199
|
+
handle?: never;
|
|
200
|
+
} & BinderStatusOption<C>;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Route registration that binds a contract directly to a use case.
|
|
204
|
+
*
|
|
205
|
+
* The server synthesizes the handler: it maps parsed request parts to the use
|
|
206
|
+
* case input, runs the use case, and returns its output as the success
|
|
207
|
+
* response body. Use a full `handle` route for headers, streaming, native
|
|
208
|
+
* `Response` values, or multi-status handling.
|
|
209
|
+
*/
|
|
210
|
+
export type UseCaseRouteDef<
|
|
211
|
+
Ctx,
|
|
212
|
+
CLike extends ContractLike,
|
|
213
|
+
UC extends AnyUseCaseLike,
|
|
214
|
+
Hooks extends readonly RouteHook<Ctx, object>[] = readonly [],
|
|
215
|
+
> = UseCaseRouteShape<
|
|
216
|
+
Ctx & AddedCtxFromHooks<Hooks>,
|
|
217
|
+
CLike,
|
|
218
|
+
ResolveContract<CLike>,
|
|
219
|
+
UC,
|
|
220
|
+
Hooks
|
|
221
|
+
>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Structural check that a use case accepts the context this route provides.
|
|
225
|
+
*
|
|
226
|
+
* Enforced through `run` parameter contravariance so it applies even where
|
|
227
|
+
* contract types are erased, such as `defineRouteGroup<Ctx>({ ... })`.
|
|
228
|
+
*/
|
|
229
|
+
export type UseCaseAcceptsCtx<Ctx> = {
|
|
230
|
+
run: (args: { ctx: Ctx; input: never }) => Promise<unknown>;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Loosely typed binder route used at collection boundaries where contract and
|
|
235
|
+
* use case types are erased. The use case's context requirement is still
|
|
236
|
+
* checked against the server context.
|
|
237
|
+
*/
|
|
238
|
+
export type AnyUseCaseRouteDef<
|
|
239
|
+
Ctx,
|
|
240
|
+
CLike extends ContractLike = ContractLike,
|
|
241
|
+
Hooks extends readonly RouteHook<Ctx, object>[] = readonly RouteHook<
|
|
242
|
+
Ctx,
|
|
243
|
+
object
|
|
244
|
+
>[],
|
|
245
|
+
> = {
|
|
246
|
+
contract: CLike;
|
|
247
|
+
hooks?: Hooks;
|
|
248
|
+
useCase: AnyUseCaseLike & UseCaseAcceptsCtx<Ctx & AddedCtxFromHooks<Hooks>>;
|
|
249
|
+
// biome-ignore lint/suspicious/noExplicitAny: request part types are erased at collection boundaries
|
|
250
|
+
input?: (parts: any) => unknown;
|
|
251
|
+
status?: number;
|
|
252
|
+
handle?: never;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
type HooksOf<E> = E extends { hooks: infer H extends readonly unknown[] }
|
|
256
|
+
? H
|
|
257
|
+
: readonly [];
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Per-element binder validation applied where route tuples are inferred, such
|
|
261
|
+
* as `defineRouteGroup<Ctx>()({ ... })`, so contract/use-case mismatches are
|
|
262
|
+
* reported on the individual route literal.
|
|
263
|
+
*/
|
|
264
|
+
export type ValidatedRouteInput<Ctx, E> = E extends {
|
|
265
|
+
contract: infer CL extends ContractLike;
|
|
266
|
+
useCase: infer UC extends AnyUseCaseLike;
|
|
267
|
+
}
|
|
268
|
+
? ResolveContract<CL> extends infer C extends HttpContractConfig
|
|
269
|
+
? {
|
|
270
|
+
contract: CL;
|
|
271
|
+
hooks?: HooksOf<E>;
|
|
272
|
+
useCase: UC &
|
|
273
|
+
UseCaseFitsRoute<Ctx & AddedCtxFromHooks<HooksOf<E>>, C, UC>;
|
|
274
|
+
input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
275
|
+
handle?: never;
|
|
276
|
+
} & BinderStatusOption<C>
|
|
277
|
+
: unknown
|
|
278
|
+
: unknown;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Element-wise binder validation for a route input list.
|
|
282
|
+
*/
|
|
283
|
+
export type ValidatedRouteInputs<Ctx, R extends readonly unknown[]> = {
|
|
284
|
+
[K in keyof R]: ValidatedRouteInput<Ctx, R[K]>;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Trusted run key shared with `@beignet/core/application` via the global
|
|
289
|
+
* symbol registry, so the binder never imports the application builder at
|
|
290
|
+
* runtime.
|
|
291
|
+
*/
|
|
292
|
+
const USE_CASE_TRUSTED_RUN_KEY: unique symbol = Symbol.for(
|
|
293
|
+
"beignet.useCase.trustedRun",
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
type RuntimeUseCase = AnyUseCaseLike & {
|
|
297
|
+
run: (args: { ctx: unknown; input: unknown }) => Promise<unknown>;
|
|
298
|
+
[USE_CASE_TRUSTED_RUN_KEY]?: (args: {
|
|
299
|
+
ctx: unknown;
|
|
300
|
+
input: unknown;
|
|
301
|
+
}) => Promise<unknown>;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Loosely typed binder route definition consumed by route registration.
|
|
306
|
+
*/
|
|
307
|
+
export type RuntimeUseCaseRouteDef = {
|
|
308
|
+
useCase: RuntimeUseCase;
|
|
309
|
+
input?: (parts: {
|
|
310
|
+
path: unknown;
|
|
311
|
+
query: unknown;
|
|
312
|
+
headers: unknown;
|
|
313
|
+
body: unknown;
|
|
314
|
+
}) => unknown;
|
|
315
|
+
status?: number;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
319
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Default input mapping for binder routes.
|
|
324
|
+
*
|
|
325
|
+
* Merges parsed query, body, and path objects into one input object. Path
|
|
326
|
+
* keys win all collisions, then body keys, then query keys. Headers are never
|
|
327
|
+
* merged: parsed headers include every raw request header, so merging them
|
|
328
|
+
* would poison the use case input. Non-object bodies (text, arrays, scalars)
|
|
329
|
+
* are excluded. Routes that need headers or non-object bodies declare an
|
|
330
|
+
* explicit `input` mapper.
|
|
331
|
+
*/
|
|
332
|
+
export function defaultBinderInput(parts: {
|
|
333
|
+
path: unknown;
|
|
334
|
+
query: unknown;
|
|
335
|
+
body: unknown;
|
|
336
|
+
}): Record<string, unknown> {
|
|
337
|
+
return {
|
|
338
|
+
...(isPlainObject(parts.query) ? parts.query : {}),
|
|
339
|
+
...(isPlainObject(parts.body) ? parts.body : {}),
|
|
340
|
+
...(isPlainObject(parts.path) ? parts.path : {}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Whether a route definition is a binder route.
|
|
346
|
+
*/
|
|
347
|
+
export function isUseCaseRouteDef(route: {
|
|
348
|
+
handle?: unknown;
|
|
349
|
+
useCase?: unknown;
|
|
350
|
+
}): route is RuntimeUseCaseRouteDef {
|
|
351
|
+
return route.useCase !== undefined && route.useCase !== null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function computeTrustedInput(
|
|
355
|
+
contract: HttpContractConfig,
|
|
356
|
+
def: RuntimeUseCaseRouteDef,
|
|
357
|
+
): boolean {
|
|
358
|
+
if (def.input) return false;
|
|
359
|
+
|
|
360
|
+
const sources = [contract.pathParams, contract.query, contract.body].filter(
|
|
361
|
+
(schema) => schema !== null && schema !== undefined,
|
|
362
|
+
);
|
|
363
|
+
return sources.length === 1 && sources[0] === def.useCase.inputSchema;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function computeResponseExemption(
|
|
367
|
+
contract: HttpContractConfig,
|
|
368
|
+
def: RuntimeUseCaseRouteDef,
|
|
369
|
+
status: number,
|
|
370
|
+
): number | undefined {
|
|
371
|
+
return contract.responses[status] === def.useCase.outputSchema
|
|
372
|
+
? status
|
|
373
|
+
: undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Synthesize the route handler for a binder route at registration time.
|
|
378
|
+
*
|
|
379
|
+
* Resolves the success status, decides whether the validated request parts can
|
|
380
|
+
* skip the use case's input parse, and computes whether server-side response
|
|
381
|
+
* validation is redundant for the success status.
|
|
382
|
+
*/
|
|
383
|
+
export function createUseCaseRouteHandler<Ctx, C extends HttpContractConfig>(
|
|
384
|
+
contract: C,
|
|
385
|
+
def: RuntimeUseCaseRouteDef,
|
|
386
|
+
): {
|
|
387
|
+
handler: Handler<Ctx, C>;
|
|
388
|
+
responseValidationExemptStatus?: number;
|
|
389
|
+
} {
|
|
390
|
+
const status = def.status ?? inferSoleSuccessStatus(contract);
|
|
391
|
+
if (status === undefined) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`Route binder for contract "${contract.name}" cannot infer a success ` +
|
|
394
|
+
`status: the contract declares ${
|
|
395
|
+
Object.keys(contract.responses).length === 0
|
|
396
|
+
? "no responses"
|
|
397
|
+
: "zero or multiple 2xx responses"
|
|
398
|
+
}. Declare exactly one 2xx response or pass an explicit status.`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const mapInput = def.input ?? defaultBinderInput;
|
|
403
|
+
const trustedRun = computeTrustedInput(contract, def)
|
|
404
|
+
? def.useCase[USE_CASE_TRUSTED_RUN_KEY]
|
|
405
|
+
: undefined;
|
|
406
|
+
const run = trustedRun ?? def.useCase.run;
|
|
407
|
+
|
|
408
|
+
const handler: Handler<Ctx, C> = async ({
|
|
409
|
+
ctx,
|
|
410
|
+
path,
|
|
411
|
+
query,
|
|
412
|
+
headers,
|
|
413
|
+
body,
|
|
414
|
+
}) => ({
|
|
415
|
+
status,
|
|
416
|
+
body: await run.call(def.useCase, {
|
|
417
|
+
ctx,
|
|
418
|
+
input: mapInput({ path, query, headers, body }),
|
|
419
|
+
}),
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
handler,
|
|
424
|
+
responseValidationExemptStatus: computeResponseExemption(
|
|
425
|
+
contract,
|
|
426
|
+
def,
|
|
427
|
+
status,
|
|
428
|
+
),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Beignet lint marker for modules that must stay out of client bundles.
|
|
3
|
+
*
|
|
4
|
+
* Host frameworks may provide their own runtime-only enforcement. This marker
|
|
5
|
+
* is intentionally a no-op so Beignet can enforce intent through `beignet lint`.
|
|
6
|
+
*/
|
|
7
|
+
export const beignetServerOnly = true;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Any Standard Schema compatible validator.
|
|
5
|
+
*/
|
|
6
|
+
export type StandardSchema = StandardSchemaV1<unknown, unknown>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Value or promise of that value.
|
|
10
|
+
*/
|
|
11
|
+
export type MaybePromise<T> = T | Promise<T>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Infer the parsed output type from a Standard Schema.
|
|
15
|
+
*/
|
|
16
|
+
export type InferSchemaOutput<T extends StandardSchemaV1> =
|
|
17
|
+
StandardSchemaV1.InferOutput<T>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Operational task definition created by `defineTask(...)`.
|
|
21
|
+
*/
|
|
22
|
+
export interface TaskDef<
|
|
23
|
+
Name extends string = string,
|
|
24
|
+
Input extends StandardSchema = StandardSchema,
|
|
25
|
+
Ctx = unknown,
|
|
26
|
+
Output = unknown,
|
|
27
|
+
> {
|
|
28
|
+
/**
|
|
29
|
+
* Discriminator for task definitions.
|
|
30
|
+
*/
|
|
31
|
+
readonly kind: "task";
|
|
32
|
+
/**
|
|
33
|
+
* Stable task name used by CLIs and operational runners.
|
|
34
|
+
*/
|
|
35
|
+
readonly name: Name;
|
|
36
|
+
/**
|
|
37
|
+
* Standard Schema input validator.
|
|
38
|
+
*/
|
|
39
|
+
readonly input: Input;
|
|
40
|
+
/**
|
|
41
|
+
* Optional human-readable description for docs and tooling.
|
|
42
|
+
*/
|
|
43
|
+
readonly description?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Handle a parsed task input.
|
|
46
|
+
*/
|
|
47
|
+
handle(
|
|
48
|
+
args: TaskHandleArgs<TaskDef<Name, Input, Ctx, Output>, Ctx>,
|
|
49
|
+
): MaybePromise<Output>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Infer the parsed input type for an operational task.
|
|
54
|
+
*/
|
|
55
|
+
export type InferTaskInput<T extends TaskDef> =
|
|
56
|
+
T["input"] extends StandardSchemaV1<unknown, infer Output> ? Output : never;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Infer the result type for an operational task.
|
|
60
|
+
*/
|
|
61
|
+
export type InferTaskOutput<T extends TaskDef> =
|
|
62
|
+
T extends TaskDef<string, StandardSchema, unknown, infer Output>
|
|
63
|
+
? Awaited<Output>
|
|
64
|
+
: never;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Arguments passed to a task handler.
|
|
68
|
+
*/
|
|
69
|
+
export interface TaskHandleArgs<T extends TaskDef, Ctx> {
|
|
70
|
+
/**
|
|
71
|
+
* Task definition being handled.
|
|
72
|
+
*/
|
|
73
|
+
task: T;
|
|
74
|
+
/**
|
|
75
|
+
* Parsed task input.
|
|
76
|
+
*/
|
|
77
|
+
input: InferTaskInput<T>;
|
|
78
|
+
/**
|
|
79
|
+
* Handler context.
|
|
80
|
+
*/
|
|
81
|
+
ctx: Ctx;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Options for `defineTask(...)`.
|
|
86
|
+
*/
|
|
87
|
+
export interface DefineTaskOptions<
|
|
88
|
+
Name extends string,
|
|
89
|
+
Input extends StandardSchema,
|
|
90
|
+
Ctx,
|
|
91
|
+
Output,
|
|
92
|
+
> {
|
|
93
|
+
/**
|
|
94
|
+
* Standard Schema input validator.
|
|
95
|
+
*/
|
|
96
|
+
input: Input;
|
|
97
|
+
/**
|
|
98
|
+
* Optional human-readable description for docs and tooling.
|
|
99
|
+
*/
|
|
100
|
+
description?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Handle a parsed task input.
|
|
103
|
+
*/
|
|
104
|
+
handle(
|
|
105
|
+
args: TaskHandleArgs<TaskDef<Name, Input, Ctx, Output>, Ctx>,
|
|
106
|
+
): MaybePromise<Output>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Options for one task run.
|
|
111
|
+
*/
|
|
112
|
+
export interface RunTaskOptions<Ctx> {
|
|
113
|
+
/**
|
|
114
|
+
* Raw task input. It is parsed with the task's Standard Schema before the
|
|
115
|
+
* handler runs.
|
|
116
|
+
*/
|
|
117
|
+
input: unknown;
|
|
118
|
+
/**
|
|
119
|
+
* Handler context.
|
|
120
|
+
*/
|
|
121
|
+
ctx: Ctx;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Context-bound operational task helper factory.
|
|
126
|
+
*/
|
|
127
|
+
export interface Tasks<Ctx> {
|
|
128
|
+
/**
|
|
129
|
+
* Define a task with the bound context type.
|
|
130
|
+
*/
|
|
131
|
+
defineTask<
|
|
132
|
+
Name extends string,
|
|
133
|
+
Input extends StandardSchema,
|
|
134
|
+
Output = unknown,
|
|
135
|
+
>(
|
|
136
|
+
name: Name,
|
|
137
|
+
options: DefineTaskOptions<Name, Input, Ctx, Output>,
|
|
138
|
+
): TaskDef<Name, Input, Ctx, Output>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Error thrown when task input validation fails.
|
|
143
|
+
*/
|
|
144
|
+
export class TaskValidationError extends Error {
|
|
145
|
+
/**
|
|
146
|
+
* Raw Standard Schema validation issues.
|
|
147
|
+
*/
|
|
148
|
+
readonly issues: readonly StandardSchemaV1.Issue[];
|
|
149
|
+
|
|
150
|
+
constructor(args: {
|
|
151
|
+
name: string;
|
|
152
|
+
issues: readonly StandardSchemaV1.Issue[];
|
|
153
|
+
}) {
|
|
154
|
+
super(
|
|
155
|
+
`Task "${args.name}" input validation failed: ${formatIssues(args.issues)}`,
|
|
156
|
+
);
|
|
157
|
+
this.name = "TaskValidationError";
|
|
158
|
+
this.issues = args.issues;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatPath(path: StandardSchemaV1.Issue["path"]): string {
|
|
163
|
+
if (!path || path.length === 0) return "";
|
|
164
|
+
|
|
165
|
+
return path
|
|
166
|
+
.map((segment) => {
|
|
167
|
+
if (typeof segment === "number") return `[${segment}]`;
|
|
168
|
+
const key = String(segment);
|
|
169
|
+
return /^[A-Za-z_$][\w$]*$/.test(key)
|
|
170
|
+
? `.${key}`
|
|
171
|
+
: `[${JSON.stringify(key)}]`;
|
|
172
|
+
})
|
|
173
|
+
.join("")
|
|
174
|
+
.replace(/^\./, "");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function formatIssues(issues: readonly StandardSchemaV1.Issue[]): string {
|
|
178
|
+
return issues
|
|
179
|
+
.map((issue) => {
|
|
180
|
+
const path = formatPath(issue.path);
|
|
181
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
182
|
+
})
|
|
183
|
+
.join("; ");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function parseInput<Schema extends StandardSchemaV1>(
|
|
187
|
+
schema: Schema,
|
|
188
|
+
input: unknown,
|
|
189
|
+
options: { name: string },
|
|
190
|
+
): Promise<InferSchemaOutput<Schema>> {
|
|
191
|
+
const result = await schema["~standard"].validate(input);
|
|
192
|
+
|
|
193
|
+
if (result.issues?.length) {
|
|
194
|
+
throw new TaskValidationError({
|
|
195
|
+
name: options.name,
|
|
196
|
+
issues: result.issues,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (result as { value: InferSchemaOutput<Schema> }).value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function defineTaskImpl<
|
|
204
|
+
Name extends string,
|
|
205
|
+
Input extends StandardSchema,
|
|
206
|
+
Ctx = unknown,
|
|
207
|
+
Output = unknown,
|
|
208
|
+
>(
|
|
209
|
+
name: Name,
|
|
210
|
+
options: DefineTaskOptions<Name, Input, Ctx, Output>,
|
|
211
|
+
): TaskDef<Name, Input, Ctx, Output> {
|
|
212
|
+
return {
|
|
213
|
+
kind: "task",
|
|
214
|
+
name,
|
|
215
|
+
input: options.input,
|
|
216
|
+
description: options.description,
|
|
217
|
+
handle: options.handle as TaskDef<Name, Input, Ctx, Output>["handle"],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Validate and parse a task input with the task's Standard Schema.
|
|
223
|
+
*/
|
|
224
|
+
export async function parseTaskInput<T extends TaskDef>(
|
|
225
|
+
task: T,
|
|
226
|
+
input: unknown,
|
|
227
|
+
): Promise<InferTaskInput<T>> {
|
|
228
|
+
return (await parseInput(task.input, input, {
|
|
229
|
+
name: task.name,
|
|
230
|
+
})) as InferTaskInput<T>;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parse input and run an operational task.
|
|
235
|
+
*/
|
|
236
|
+
export async function runTask<
|
|
237
|
+
T extends TaskDef<string, StandardSchema, Ctx>,
|
|
238
|
+
Ctx,
|
|
239
|
+
>(task: T, options: RunTaskOptions<Ctx>): Promise<InferTaskOutput<T>> {
|
|
240
|
+
const parsed = await parseTaskInput(task, options.input);
|
|
241
|
+
return (await task.handle({
|
|
242
|
+
task,
|
|
243
|
+
input: parsed,
|
|
244
|
+
ctx: options.ctx,
|
|
245
|
+
})) as InferTaskOutput<T>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Define a task registry while preserving tuple inference.
|
|
250
|
+
*/
|
|
251
|
+
export function defineTasks<const Defs extends readonly TaskDef[]>(
|
|
252
|
+
tasks: Defs,
|
|
253
|
+
): Defs {
|
|
254
|
+
return tasks;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create task helper methods bound to an application context type.
|
|
259
|
+
*
|
|
260
|
+
* Call it once in `lib/tasks.ts`:
|
|
261
|
+
*
|
|
262
|
+
* ```ts
|
|
263
|
+
* export const { defineTask } = createTasks<AppContext>();
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export function createTasks<Ctx>(): Tasks<Ctx> {
|
|
267
|
+
return {
|
|
268
|
+
defineTask<Name extends string, Input extends StandardSchema, Output>(
|
|
269
|
+
name: Name,
|
|
270
|
+
options: DefineTaskOptions<Name, Input, Ctx, Output>,
|
|
271
|
+
) {
|
|
272
|
+
return defineTaskImpl<Name, Input, Ctx, Output>(name, options);
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|