@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 +101 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +85 -0
- package/dist/testing.d.ts +3 -0
- package/package.json +51 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|
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
|
+
}
|