@supacloud/elysia 0.1.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 ADDED
@@ -0,0 +1,101 @@
1
+ # @supacloud/elysia
2
+
3
+ Elysia runtime adapter for SupaCloud applications: it adapts the plain-data
4
+ `CompiledModule` produced by `@supacloud/compiler` into Elysia plugins and
5
+ manages the `application` / `request` scopes at runtime.
6
+
7
+ This package does **not** depend on the compiler. The contract below is
8
+ declared and re-exported locally; any object matching it works.
9
+
10
+ ## Compiled module contract
11
+
12
+ ```ts
13
+ interface CompiledRoute {
14
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
15
+ path: string;
16
+ handler: string; // method name on the controller instance
17
+ body?: unknown; // TypeBox schema; validation enabled only when present
18
+ params?: unknown;
19
+ query?: unknown;
20
+ response?: unknown;
21
+ }
22
+
23
+ interface CompiledController {
24
+ path: string; // controller prefix, e.g. "/cases"
25
+ serviceKey: string; // key on services or the request scope
26
+ scope: "application" | "request" | "job";
27
+ routes: CompiledRoute[];
28
+ }
29
+
30
+ interface CompiledModule {
31
+ name: string;
32
+ createServices(
33
+ deps: Record<string, unknown>,
34
+ imported: Record<string, Record<string, unknown>>,
35
+ ): Record<string, unknown>;
36
+ createRequestScope?(
37
+ services: Record<string, unknown>,
38
+ ctx: unknown,
39
+ ): Record<string, unknown>;
40
+ controllers: CompiledController[];
41
+ }
42
+ ```
43
+
44
+ Controller methods are invoked as
45
+ `controller[handler]({ body, params, query, request, scope })`; non-`Response`
46
+ return values are serialized to JSON by Elysia. Validation failures use
47
+ Elysia's default behavior (422).
48
+
49
+ ## Usage
50
+
51
+ ```ts
52
+ import { createApplication } from "@supacloud/elysia";
53
+ import AuditModule from "./.generated/audit.module";
54
+ import CaseModule from "./.generated/case.module";
55
+
56
+ const app = createApplication({
57
+ name: "case-service",
58
+ modules: [AuditModule, CaseModule], // topological import order
59
+ deps: { db: createDbClient() }, // platform deps, passed to createServices
60
+ requestContext: (request) => ({
61
+ requestId: request.headers.get("x-request-id") ?? crypto.randomUUID(),
62
+ request,
63
+ }),
64
+ });
65
+
66
+ export default app;
67
+ ```
68
+
69
+ - Each module is instantiated in order: `createServices(deps, imported)`
70
+ receives platform `deps` plus the services of all previously created
71
+ modules, keyed by module name.
72
+ - Each module becomes a named Elysia plugin (`supacloud:<module.name>`) that
73
+ decorates the context with `services`.
74
+ - When a module defines `createRequestScope`, a **fresh** request scope is
75
+ resolved per request and exposed on the `scope` context key — never reused
76
+ across requests. Request-scoped controller instances are looked up on
77
+ `scope`; application-scoped instances on `services`.
78
+ - Full route paths are `controller.path + route.path` (slashes normalized);
79
+ schema options (`body` / `params` / `query` / `response`) are passed to
80
+ Elysia only when the corresponding field exists.
81
+
82
+ Lower-level building blocks:
83
+
84
+ - `createModulePlugin(compiled, services, ctxFactory?)` — adapt a single
85
+ module into an Elysia plugin (useful for tests and embedding).
86
+ - `createTestApp(options)` — semantic alias of `createApplication`.
87
+ - `testRequest(app, path, init?)` (from `@supacloud/elysia` source
88
+ `src/testing.ts`) — in-process `app.handle(new Request(...))` helper.
89
+
90
+ ## Edge runtime integration
91
+
92
+ SupaCloud's edge runtime (`@supacloud/edge-runtime`) supports Elysia as a
93
+ first-class function framework. Default-export the Elysia instance from your
94
+ function entrypoint and set the activation manifest to:
95
+
96
+ ```json
97
+ { "framework": "elysia" }
98
+ ```
99
+
100
+ The runtime then routes requests through `app.handle(request)` instead of a
101
+ plain `fetch` handler.
@@ -0,0 +1,55 @@
1
+ import { Elysia } from "elysia";
2
+ export interface CompiledRoute {
3
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
4
+ path: string;
5
+ /** Method name on the controller instance. */
6
+ handler: string;
7
+ /** TypeBox schema; validation is enabled only when the field is present. */
8
+ body?: unknown;
9
+ params?: unknown;
10
+ query?: unknown;
11
+ response?: unknown;
12
+ }
13
+ export interface CompiledController {
14
+ /** Controller path prefix, e.g. "/cases". */
15
+ path: string;
16
+ /** Key of the controller instance on `services` or the request scope. */
17
+ serviceKey: string;
18
+ scope: "application" | "request" | "job";
19
+ routes: CompiledRoute[];
20
+ }
21
+ export interface CompiledModule {
22
+ name: string;
23
+ createServices(deps: Record<string, unknown>, imported: Record<string, Record<string, unknown>>): Record<string, unknown>;
24
+ createRequestScope?(services: Record<string, unknown>, ctx: unknown): Record<string, unknown>;
25
+ controllers: CompiledController[];
26
+ }
27
+ export type RequestContextFactory = (request: Request) => unknown | Promise<unknown>;
28
+ export interface ApplicationOptions {
29
+ name?: string;
30
+ /** Modules in topological import order. */
31
+ modules: CompiledModule[];
32
+ /** Platform-level dependencies (db client etc.), passed to createServices. */
33
+ deps?: Record<string, unknown>;
34
+ /** Builds the per-request context object. Defaults to { requestId, request }. */
35
+ requestContext?: RequestContextFactory;
36
+ }
37
+ /**
38
+ * Adapt a single compiled module into an Elysia plugin.
39
+ *
40
+ * The plugin decorates the context with `services`; when the module defines
41
+ * `createRequestScope`, a fresh request scope is resolved per request and
42
+ * exposed on the `scope` context key. Request-scoped controller instances are
43
+ * looked up on `scope`, everything else on `services`.
44
+ */
45
+ export declare function createModulePlugin(compiled: CompiledModule, services: Record<string, unknown>, ctxFactory?: RequestContextFactory): Elysia;
46
+ /**
47
+ * Create the root Elysia application from compiled modules.
48
+ *
49
+ * Modules are instantiated in the given (topological) order: each module's
50
+ * `createServices` receives `deps` plus the services of all previously
51
+ * created modules, keyed by module name.
52
+ */
53
+ export declare function createApplication(options: ApplicationOptions): Elysia;
54
+ /** Semantic alias of createApplication for readable tests. */
55
+ export declare function createTestApp(options: ApplicationOptions): Elysia;
package/dist/index.js ADDED
@@ -0,0 +1,85 @@
1
+ // src/index.ts
2
+ import { Elysia } from "elysia";
3
+ var defaultRequestContext = (request) => ({
4
+ requestId: crypto.randomUUID(),
5
+ request
6
+ });
7
+ function joinPaths(prefix, path) {
8
+ const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
9
+ return joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
10
+ }
11
+ function createModulePlugin(compiled, services, ctxFactory = defaultRequestContext) {
12
+ const plugin = new Elysia({ name: `supacloud:${compiled.name}` }).decorate("services", services);
13
+ if (compiled.createRequestScope) {
14
+ const createRequestScope = compiled.createRequestScope;
15
+ plugin.resolve(async ({ request }) => ({
16
+ scope: createRequestScope(services, await ctxFactory(request))
17
+ }));
18
+ }
19
+ for (const controller of compiled.controllers) {
20
+ for (const route of controller.routes) {
21
+ const path = joinPaths(controller.path, route.path);
22
+ const schema = {};
23
+ if (route.body !== undefined)
24
+ schema.body = route.body;
25
+ if (route.params !== undefined)
26
+ schema.params = route.params;
27
+ if (route.query !== undefined)
28
+ schema.query = route.query;
29
+ if (route.response !== undefined)
30
+ schema.response = route.response;
31
+ const handler = async (ctx) => {
32
+ const source = controller.scope === "request" ? ctx.scope : services;
33
+ const instance = source?.[controller.serviceKey];
34
+ const method = instance?.[route.handler];
35
+ if (typeof method !== "function") {
36
+ throw new Error(`supacloud: controller "${controller.serviceKey}" has no handler "${route.handler}" in scope "${controller.scope}"`);
37
+ }
38
+ return method.call(instance, {
39
+ body: ctx.body,
40
+ params: ctx.params,
41
+ query: ctx.query,
42
+ request: ctx.request,
43
+ scope: ctx.scope
44
+ });
45
+ };
46
+ switch (route.method) {
47
+ case "GET":
48
+ plugin.get(path, handler, schema);
49
+ break;
50
+ case "POST":
51
+ plugin.post(path, handler, schema);
52
+ break;
53
+ case "PUT":
54
+ plugin.put(path, handler, schema);
55
+ break;
56
+ case "PATCH":
57
+ plugin.patch(path, handler, schema);
58
+ break;
59
+ case "DELETE":
60
+ plugin.delete(path, handler, schema);
61
+ break;
62
+ }
63
+ }
64
+ }
65
+ return plugin;
66
+ }
67
+ function createApplication(options) {
68
+ const app = new Elysia({ name: options.name ?? "supacloud:app" });
69
+ const ctxFactory = options.requestContext ?? defaultRequestContext;
70
+ const imported = {};
71
+ for (const module of options.modules) {
72
+ const services = module.createServices(options.deps ?? {}, imported);
73
+ imported[module.name] = services;
74
+ app.use(createModulePlugin(module, services, ctxFactory));
75
+ }
76
+ return app;
77
+ }
78
+ function createTestApp(options) {
79
+ return createApplication(options);
80
+ }
81
+ export {
82
+ createApplication,
83
+ createModulePlugin,
84
+ createTestApp
85
+ };
@@ -0,0 +1,3 @@
1
+ import type { Elysia } from "elysia";
2
+ /** Issue an in-process request against an Elysia app. */
3
+ export declare function testRequest(app: Elysia, path: string, init?: RequestInit): Promise<Response>;
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@supacloud/elysia",
3
+ "version": "0.1.0",
4
+ "description": "Elysia runtime adapter for SupaCloud compiled modules: application/request scopes, route registration and validation",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "bun run clean && bun run build:js && bun run build:types",
22
+ "build:js": "bun build src/index.ts --outdir dist --target node --external elysia",
23
+ "build:types": "tsc -p tsconfig.json --emitDeclarationOnly",
24
+ "clean": "rm -rf dist",
25
+ "prepublishOnly": "bun run build",
26
+ "test": "bun test",
27
+ "typecheck": "tsc -p tsconfig.json --noEmit",
28
+ "typecheck:test": "tsc -p tsconfig.test.json --noEmit"
29
+ },
30
+ "keywords": [
31
+ "supacloud",
32
+ "elysia",
33
+ "adapter",
34
+ "dependency-injection",
35
+ "module"
36
+ ],
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/vibeunion/supacloud.git",
41
+ "directory": "packages/elysia"
42
+ },
43
+ "peerDependencies": {
44
+ "elysia": "^1.4.30"
45
+ },
46
+ "devDependencies": {
47
+ "@types/bun": "^1.4.0",
48
+ "elysia": "^1.4.30",
49
+ "typescript": "^7.0.2"
50
+ }
51
+ }