@supacloud/elysia 0.8.0 → 0.9.0
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/README.md +45 -3
- package/dist/index.d.ts +7 -0
- package/dist/index.js +40 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,8 @@ Runtime adapter that turns `@supacloud/compiler` output into a production-ready
|
|
|
26
26
|
performed.
|
|
27
27
|
- **Public error mapping**: transforms framework / application errors via
|
|
28
28
|
`errorMapper` with standard `ApplicationError` envelope support, preserving
|
|
29
|
-
|
|
29
|
+
HTTP 422 for request validation and HTTP 500 / `RESPONSE_VALIDATION_ERROR`
|
|
30
|
+
for invalid handler output, without exposing payloads or schema internals.
|
|
30
31
|
|
|
31
32
|
## Installation
|
|
32
33
|
|
|
@@ -83,8 +84,16 @@ sandbox.reset();
|
|
|
83
84
|
|
|
84
85
|
Route `body`, `params`, `query`, and `response` schemas are enforced by
|
|
85
86
|
Elysia before and after the handler. Invalid input returns the standard `422`
|
|
86
|
-
validation response; invalid handler output
|
|
87
|
-
|
|
87
|
+
validation response; invalid structured handler output returns HTTP 500 with
|
|
88
|
+
`RESPONSE_VALIDATION_ERROR`. A response validation failure can occur **after a
|
|
89
|
+
command has committed**; it does not imply rollback and must not trigger a blind
|
|
90
|
+
write retry. Confirm the outcome using the application's durable receipt or
|
|
91
|
+
read-back protocol. A custom `errorMapper` can override this public envelope.
|
|
92
|
+
|
|
93
|
+
Native `Response` objects are passed through by Elysia, including JSON responses.
|
|
94
|
+
Use `validatedJsonResponse` to opt into validation when constructing a native
|
|
95
|
+
JSON response. Otherwise handlers must validate their JSON payload themselves.
|
|
96
|
+
The adapter does not consume or parse binary/streaming responses.
|
|
88
97
|
|
|
89
98
|
Jobs are executed explicitly with `executeJob(compiledModule, services, job,
|
|
90
99
|
input, requestContext)`. The asynchronous compiler-generated job scope is
|
|
@@ -93,6 +102,39 @@ fails partway through.
|
|
|
93
102
|
|
|
94
103
|
## API
|
|
95
104
|
|
|
105
|
+
### `validatedJsonResponse(validate, value, init?): Response`
|
|
106
|
+
|
|
107
|
+
Constructs a native JSON response after a synchronous, caller-owned type guard
|
|
108
|
+
validates the actual serialized JSON snapshot. The value type is inferred from
|
|
109
|
+
the guard; compatible extra fields are preserved. For a TypeBox contract, the
|
|
110
|
+
guard can delegate to `Value.Check(schema, value)` or a compiled validator.
|
|
111
|
+
No additional schema dependency is required by the adapter.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { validatedJsonResponse } from "@supacloud/elysia";
|
|
115
|
+
import { isReportReceipt } from "./contracts";
|
|
116
|
+
|
|
117
|
+
return validatedJsonResponse(isReportReceipt, receipt, {
|
|
118
|
+
status: 201,
|
|
119
|
+
headers: { "x-request-id": requestId },
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The helper serializes once, validates that wire snapshot, and sends those same
|
|
124
|
+
bytes. Validation cannot mutate the outgoing body; it is not a transform or
|
|
125
|
+
coercion hook. Guards must be synchronous and side-effect free. Serialization
|
|
126
|
+
failures and invalid receipts throw a sanitized `ApplicationError` with HTTP 500
|
|
127
|
+
and `RESPONSE_VALIDATION_ERROR`, without retaining payloads or validator causes.
|
|
128
|
+
The application's `errorMapper` can map this to its outcome-confirmation
|
|
129
|
+
protocol. The helper never retries a command or implies rollback.
|
|
130
|
+
|
|
131
|
+
`init` uses native `Response` options. The helper explicitly rejects null-body
|
|
132
|
+
statuses 204, 205 and 304, including on runtimes that otherwise accept a body
|
|
133
|
+
with those statuses. The default content type is
|
|
134
|
+
`application/json`, and explicitly supplied headers are preserved. This helper
|
|
135
|
+
is for bounded JSON payloads, not files or streams. Existing native `Response`
|
|
136
|
+
passthrough is unchanged.
|
|
137
|
+
|
|
96
138
|
### `createApplication(options: ApplicationOptions): Elysia`
|
|
97
139
|
|
|
98
140
|
Creates the root Elysia application from compiled modules.
|
package/dist/index.d.ts
CHANGED
|
@@ -144,6 +144,13 @@ export declare class ApplicationError extends Error implements PublicApplication
|
|
|
144
144
|
readonly details?: unknown;
|
|
145
145
|
constructor(message: string, options?: ApplicationErrorOptions);
|
|
146
146
|
}
|
|
147
|
+
export type JsonResponseValidator<T> = (value: unknown) => value is T;
|
|
148
|
+
/**
|
|
149
|
+
* Validate the serialized JSON snapshot before creating a native Response.
|
|
150
|
+
* Validators must be synchronous and side-effect free; response failures do not
|
|
151
|
+
* imply that application writes were rolled back.
|
|
152
|
+
*/
|
|
153
|
+
export declare function validatedJsonResponse<T>(validate: JsonResponseValidator<T>, value: NoInfer<T>, init?: ResponseInit): Response;
|
|
147
154
|
export interface ErrorContext {
|
|
148
155
|
request: Request;
|
|
149
156
|
requestContext: unknown;
|
package/dist/index.js
CHANGED
|
@@ -405,6 +405,28 @@ class ApplicationError extends Error {
|
|
|
405
405
|
this.details = options.details;
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
|
+
function validatedJsonResponse(validate, value, init) {
|
|
409
|
+
if (init?.status === 204 || init?.status === 205 || init?.status === 304) {
|
|
410
|
+
throw new TypeError("JSON responses cannot use a null-body status");
|
|
411
|
+
}
|
|
412
|
+
let body;
|
|
413
|
+
try {
|
|
414
|
+
const serialized = JSON.stringify(value);
|
|
415
|
+
if (serialized === undefined || validate(JSON.parse(serialized)) !== true) {
|
|
416
|
+
throw new Error("Invalid JSON response");
|
|
417
|
+
}
|
|
418
|
+
body = serialized;
|
|
419
|
+
} catch {
|
|
420
|
+
throw new ApplicationError("Response validation failed", {
|
|
421
|
+
status: 500,
|
|
422
|
+
code: "RESPONSE_VALIDATION_ERROR"
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
const headers = new Headers(init?.headers);
|
|
426
|
+
if (!headers.has("content-type"))
|
|
427
|
+
headers.set("content-type", "application/json");
|
|
428
|
+
return new Response(body, { ...init, headers });
|
|
429
|
+
}
|
|
408
430
|
function safeHeaderValue(value, maxLength) {
|
|
409
431
|
if (!value || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
410
432
|
return;
|
|
@@ -572,6 +594,15 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
572
594
|
requestContexts.set(request, requestContext);
|
|
573
595
|
return { requestContext };
|
|
574
596
|
});
|
|
597
|
+
plugin.onError(async ({ code, error, request }) => {
|
|
598
|
+
const context = {
|
|
599
|
+
request,
|
|
600
|
+
requestContext: requestContexts.get(request),
|
|
601
|
+
frameworkCode: code
|
|
602
|
+
};
|
|
603
|
+
const mapped = await options.errorMapper?.(error, context);
|
|
604
|
+
return mapped ?? defaultErrorResponse(error, code);
|
|
605
|
+
});
|
|
575
606
|
for (const controller of compiled.controllers) {
|
|
576
607
|
for (const route of controller.routes) {
|
|
577
608
|
const path = joinPaths(controller.path, route.path);
|
|
@@ -683,15 +714,6 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
683
714
|
}
|
|
684
715
|
}
|
|
685
716
|
}
|
|
686
|
-
plugin.onError({ as: "global" }, async ({ code, error, request }) => {
|
|
687
|
-
const context = {
|
|
688
|
-
request,
|
|
689
|
-
requestContext: requestContexts.get(request),
|
|
690
|
-
frameworkCode: code
|
|
691
|
-
};
|
|
692
|
-
const mapped = await options.errorMapper?.(error, context);
|
|
693
|
-
return mapped ?? defaultErrorResponse(error, code);
|
|
694
|
-
});
|
|
695
717
|
return plugin;
|
|
696
718
|
}
|
|
697
719
|
async function executeJob(compiled, services, job, input, requestContext, imported = {}, executor) {
|
|
@@ -738,6 +760,13 @@ function defaultErrorResponse(error, frameworkCode) {
|
|
|
738
760
|
}, { status: error.status });
|
|
739
761
|
}
|
|
740
762
|
if (frameworkCode === "VALIDATION") {
|
|
763
|
+
if (isRecord(error) && error.type === "response") {
|
|
764
|
+
return Response.json({
|
|
765
|
+
ok: false,
|
|
766
|
+
code: "RESPONSE_VALIDATION_ERROR",
|
|
767
|
+
message: "Response validation failed"
|
|
768
|
+
}, { status: 500 });
|
|
769
|
+
}
|
|
741
770
|
return Response.json({
|
|
742
771
|
ok: false,
|
|
743
772
|
code: "VALIDATION_ERROR",
|
|
@@ -800,5 +829,6 @@ export {
|
|
|
800
829
|
defaultErrorResponse,
|
|
801
830
|
executeJob,
|
|
802
831
|
requireIdempotencyKey,
|
|
803
|
-
requireTrustedIdentity
|
|
832
|
+
requireTrustedIdentity,
|
|
833
|
+
validatedJsonResponse
|
|
804
834
|
};
|
package/package.json
CHANGED