@hulla/api-elysia 0.0.0-stage → 2.0.1

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.
@@ -0,0 +1,29 @@
1
+ import { AdapterErrorInput } from "@hulla/api/adapters";
2
+ import { Awaitable, ServerAdapter, ServerContextInput, ServerExecutableFor } from "@hulla/api/server";
3
+ import { Contract } from "@hulla/api";
4
+ import Elysia, { AnyElysia, InferContext } from "elysia";
5
+ //#region src/index.d.ts
6
+ type ElysiaContext<App extends AnyElysia = Elysia> = Omit<InferContext<App>, 'params'> & {
7
+ readonly params: Readonly<Record<string, string>>;
8
+ };
9
+ type ElysiaAdapterContext<App extends AnyElysia> = {
10
+ readonly request: Request;
11
+ readonly elysiaContext: ElysiaContext<App>;
12
+ };
13
+ type ElysiaContextInput<ContractType extends Contract = Contract, App extends AnyElysia = Elysia> = ServerContextInput<ContractType> & ElysiaAdapterContext<App>;
14
+ type ElysiaServerErrorInput<App extends AnyElysia = Elysia> = Omit<AdapterErrorInput, 'defaultResponse' | 'hostContext' | 'request'> & ElysiaAdapterContext<App> & {
15
+ readonly defaultResponse: Response;
16
+ };
17
+ type ElysiaServerOptions<App extends AnyElysia = Elysia> = {
18
+ readonly preserveRequestBody?: boolean;
19
+ /** Optional byte limit for adapter-owned body reads. Defaults to Infinity; host limits still apply. */
20
+ readonly maxBodyBytes?: number;
21
+ readonly onError?: ((input: ElysiaServerErrorInput<App>) => Awaitable<Response | undefined | void>) | undefined;
22
+ };
23
+ type ElysiaAdapter<App extends AnyElysia = Elysia> = ServerAdapter<'elysia', ElysiaAdapterContext<App>> & {
24
+ readonly mount: <const ContractType extends Contract, const Context extends object>(implementation: ServerExecutableFor<ContractType, Context, 'elysia', ElysiaAdapterContext<App>>, options?: ElysiaServerOptions<App>) => App;
25
+ };
26
+ /** Creates an Elysia adapter bound to one caller-owned application. */
27
+ declare function elysiaAdapter<const App extends AnyElysia>(app: App, defaults?: ElysiaServerOptions<App>): ElysiaAdapter<App>;
28
+ //#endregion
29
+ export { ElysiaAdapter, ElysiaContext, ElysiaContextInput, ElysiaServerErrorInput, ElysiaServerOptions, elysiaAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,103 @@
1
+ import { bodyLimit, createAdapterRuntime, readFetchBody, toFetchResponse, writeFetchResponse } from "@hulla/api/adapters";
2
+ import { assertAdapterContext, createServerAdapter, serverContextAdapterId } from "@hulla/api/server";
3
+
4
+ //#region src/index.ts
5
+ function requestQuery(url) {
6
+ const start = url.indexOf("?");
7
+ if (start === -1) return void 0;
8
+ const hash = url.indexOf("#", start + 1);
9
+ return new URLSearchParams(url.slice(start + 1, hash === -1 ? void 0 : hash));
10
+ }
11
+ function parsedBody(context, representation) {
12
+ const value = context["body"];
13
+ switch (representation) {
14
+ case "json":
15
+ case "text": return value;
16
+ case "bytes":
17
+ if (value instanceof Uint8Array) return value;
18
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
19
+ throw new TypeError("Parsed Elysia byte request body must be ArrayBuffer or Uint8Array");
20
+ case "form-data":
21
+ if (value instanceof FormData) return value;
22
+ throw new TypeError("Parsed Elysia form data request body must be FormData");
23
+ default: throw new TypeError(`Unsupported Elysia request body representation ${representation}`);
24
+ }
25
+ }
26
+ async function readBody(nativeContext, representation, preserveRequest, limit) {
27
+ if (nativeContext.request.bodyUsed) return parsedBody(nativeContext.elysiaContext, representation);
28
+ return readFetchBody(nativeContext.request, representation, preserveRequest, limit);
29
+ }
30
+ function requestHeaders(request) {
31
+ return Object.fromEntries(request.headers.entries());
32
+ }
33
+ function replacementResponse(response) {
34
+ return {
35
+ status: response.status,
36
+ headers: {},
37
+ body: {
38
+ kind: "raw",
39
+ value: response
40
+ }
41
+ };
42
+ }
43
+ function readHeader(name) {
44
+ return this.request.headers.get(name) ?? void 0;
45
+ }
46
+ function createRouteHandler(route, usesNativeContext, includeHostContext, options) {
47
+ const limit = bodyLimit(options.maxBodyBytes);
48
+ return async (elysiaContext) => {
49
+ const request = elysiaContext.request;
50
+ const query = requestQuery(request.url);
51
+ const nativeContext = {
52
+ request,
53
+ elysiaContext
54
+ };
55
+ const input = {
56
+ request,
57
+ signal: request.signal,
58
+ preserveRequestBody: options.preserveRequestBody ?? false,
59
+ ...usesNativeContext ? { contextInput: nativeContext } : {},
60
+ params: elysiaContext.params,
61
+ ...query === void 0 ? {} : { query },
62
+ readHeaders: () => requestHeaders(request),
63
+ readHeader,
64
+ readBody: (representation, preserveRequest) => readBody(nativeContext, representation, preserveRequest, limit),
65
+ ...includeHostContext ? { hostContext: nativeContext } : {}
66
+ };
67
+ return writeFetchResponse(await route.execute(input), options.onError === void 0 ? void 0 : (error) => options.onError?.({
68
+ ...error,
69
+ ...nativeContext,
70
+ route: {
71
+ key: route.key,
72
+ method: route.method,
73
+ path: route.path
74
+ }
75
+ }));
76
+ };
77
+ }
78
+ function mountElysia(app, implementation, options) {
79
+ assertAdapterContext(implementation.context, "elysia");
80
+ const usesNativeContext = serverContextAdapterId(implementation.context) !== void 0;
81
+ const runtime = createAdapterRuntime(implementation, options.onError === void 0 ? {} : { onError: async ({ hostContext, ...input }) => {
82
+ const nativeContext = hostContext;
83
+ const replacement = await options.onError?.({
84
+ ...input,
85
+ ...nativeContext,
86
+ defaultResponse: toFetchResponse(input.defaultResponse).clone()
87
+ });
88
+ return replacement instanceof Response ? replacementResponse(replacement) : void 0;
89
+ } });
90
+ for (const route of runtime.routes) app.route(route.method, route.path, createRouteHandler(route, usesNativeContext, options.onError !== void 0, options));
91
+ return app;
92
+ }
93
+ /** Creates an Elysia adapter bound to one caller-owned application. */
94
+ function elysiaAdapter(app, defaults = {}) {
95
+ const configuredDefaults = { ...defaults };
96
+ return createServerAdapter("elysia", { mount: (implementation, options) => mountElysia(app, implementation, options === void 0 ? configuredDefaults : {
97
+ ...configuredDefaults,
98
+ ...options
99
+ }) });
100
+ }
101
+
102
+ //#endregion
103
+ export { elysiaAdapter };
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@hulla/api-elysia",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.1",
4
+ "description": "Elysia server adapter for @hulla/api implementations",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/hulladev/api.git",
9
+ "directory": "packages/elysia"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "registry": "https://registry.npmjs.org/"
25
+ },
26
+ "scripts": {
27
+ "build": "tsdown",
28
+ "clean": "rm -rf dist coverage",
29
+ "test": "vitest run",
30
+ "test:coverage": "vitest run --coverage",
31
+ "test:watch": "vitest",
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "devDependencies": {
35
+ "@hulla/api": "2.0.1",
36
+ "elysia": "^1.4.29",
37
+ "zod": "^4.4.3"
38
+ },
39
+ "peerDependencies": {
40
+ "@hulla/api": "^2.0.1",
41
+ "elysia": "^1.4.0"
42
+ }
43
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.