@objectstack/hono 3.2.7 → 3.2.9
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +21 -0
- package/dist/index.d.mts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +11 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/hono.test.ts +279 -40
- package/src/index.ts +29 -51
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/hono@3.2.
|
|
2
|
+
> @objectstack/hono@3.2.9 build /home/runner/work/spec/spec/packages/adapters/hono
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
14
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
17
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m4.33 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m9.90 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 78ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m5.41 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m9.94 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 80ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 13093ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.17 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.17 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @objectstack/hono
|
|
2
2
|
|
|
3
|
+
## 3.2.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @objectstack/runtime@3.2.9
|
|
8
|
+
|
|
9
|
+
## 3.2.8
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- @objectstack/runtime@3.2.8
|
|
14
|
+
|
|
15
|
+
## 3.2.8
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- fix: unified catch-all dispatch pattern — `createHonoApp()` now delegates all non-framework-specific routes to `HttpDispatcher.dispatch()`, automatically supporting packages, analytics, automation, i18n, ui, openapi, custom endpoints, and any future routes
|
|
20
|
+
- fix: resolves 404 errors for `/api/v1/meta` and `/api/v1/packages` after Vercel deployment
|
|
21
|
+
- Only auth (service check), storage (formData), GraphQL (raw result), and discovery (response wrapper) remain as explicit routes
|
|
22
|
+
- Added comprehensive tests for the catch-all dispatch pattern
|
|
23
|
+
|
|
3
24
|
## 3.2.7
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -11,8 +11,15 @@ interface ObjectStackHonoOptions {
|
|
|
11
11
|
declare function objectStackMiddleware(kernel: ObjectKernel): (c: any, next: any) => Promise<void>;
|
|
12
12
|
/**
|
|
13
13
|
* Creates a full-featured Hono app with all ObjectStack route dispatchers.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* Only routes that need framework-specific handling (auth service, storage
|
|
16
|
+
* formData, GraphQL raw result, discovery wrapper) are registered explicitly.
|
|
17
|
+
* All other routes (meta, data, packages, analytics, automation, i18n, ui,
|
|
18
|
+
* openapi, custom endpoints, and any future routes) are handled by a
|
|
19
|
+
* catch-all that delegates to `HttpDispatcher.dispatch()`.
|
|
20
|
+
*
|
|
21
|
+
* This means new routes added to `HttpDispatcher` automatically work in
|
|
22
|
+
* every adapter without any adapter-side code changes.
|
|
16
23
|
*
|
|
17
24
|
* @example
|
|
18
25
|
* ```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -11,8 +11,15 @@ interface ObjectStackHonoOptions {
|
|
|
11
11
|
declare function objectStackMiddleware(kernel: ObjectKernel): (c: any, next: any) => Promise<void>;
|
|
12
12
|
/**
|
|
13
13
|
* Creates a full-featured Hono app with all ObjectStack route dispatchers.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* Only routes that need framework-specific handling (auth service, storage
|
|
16
|
+
* formData, GraphQL raw result, discovery wrapper) are registered explicitly.
|
|
17
|
+
* All other routes (meta, data, packages, analytics, automation, i18n, ui,
|
|
18
|
+
* openapi, custom endpoints, and any future routes) are handled by a
|
|
19
|
+
* catch-all that delegates to `HttpDispatcher.dispatch()`.
|
|
20
|
+
*
|
|
21
|
+
* This means new routes added to `HttpDispatcher` automatically work in
|
|
22
|
+
* every adapter without any adapter-side code changes.
|
|
16
23
|
*
|
|
17
24
|
* @example
|
|
18
25
|
* ```ts
|
package/dist/index.js
CHANGED
|
@@ -106,39 +106,27 @@ function createHonoApp(options) {
|
|
|
106
106
|
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
|
-
app.all(`${prefix}/
|
|
109
|
+
app.all(`${prefix}/storage/*`, async (c) => {
|
|
110
110
|
try {
|
|
111
|
-
const subPath = c.req.path.substring(`${prefix}/
|
|
111
|
+
const subPath = c.req.path.substring(`${prefix}/storage`.length);
|
|
112
112
|
const method = c.req.method;
|
|
113
|
-
let
|
|
114
|
-
if (method === "
|
|
115
|
-
|
|
113
|
+
let file = void 0;
|
|
114
|
+
if (method === "POST" && subPath === "/upload") {
|
|
115
|
+
const formData = await c.req.formData();
|
|
116
|
+
file = formData.get("file");
|
|
116
117
|
}
|
|
117
|
-
const result = await dispatcher.
|
|
118
|
+
const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });
|
|
118
119
|
return toResponse(c, result);
|
|
119
120
|
} catch (err) {
|
|
120
121
|
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
121
122
|
}
|
|
122
123
|
});
|
|
123
|
-
app.all(`${prefix}
|
|
124
|
+
app.all(`${prefix}/*`, async (c) => {
|
|
124
125
|
try {
|
|
126
|
+
const subPath = c.req.path.substring(prefix.length);
|
|
125
127
|
const method = c.req.method;
|
|
126
128
|
let body = void 0;
|
|
127
|
-
if (method === "PUT" || method === "
|
|
128
|
-
body = await c.req.json().catch(() => ({}));
|
|
129
|
-
}
|
|
130
|
-
const result = await dispatcher.handleMetadata("", { request: c.req.raw }, method, body);
|
|
131
|
-
return toResponse(c, result);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
app.all(`${prefix}/data/*`, async (c) => {
|
|
137
|
-
try {
|
|
138
|
-
const subPath = c.req.path.substring(`${prefix}/data`.length);
|
|
139
|
-
const method = c.req.method;
|
|
140
|
-
let body = {};
|
|
141
|
-
if (method === "POST" || method === "PATCH") {
|
|
129
|
+
if (method === "POST" || method === "PUT" || method === "PATCH") {
|
|
142
130
|
body = await c.req.json().catch(() => ({}));
|
|
143
131
|
}
|
|
144
132
|
const queryParams = {};
|
|
@@ -146,22 +134,7 @@ function createHonoApp(options) {
|
|
|
146
134
|
url.searchParams.forEach((val, key) => {
|
|
147
135
|
queryParams[key] = val;
|
|
148
136
|
});
|
|
149
|
-
const result = await dispatcher.
|
|
150
|
-
return toResponse(c, result);
|
|
151
|
-
} catch (err) {
|
|
152
|
-
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
app.all(`${prefix}/storage/*`, async (c) => {
|
|
156
|
-
try {
|
|
157
|
-
const subPath = c.req.path.substring(`${prefix}/storage`.length);
|
|
158
|
-
const method = c.req.method;
|
|
159
|
-
let file = void 0;
|
|
160
|
-
if (method === "POST" && subPath === "/upload") {
|
|
161
|
-
const formData = await c.req.formData();
|
|
162
|
-
file = formData.get("file");
|
|
163
|
-
}
|
|
164
|
-
const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });
|
|
137
|
+
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
|
|
165
138
|
return toResponse(c, result);
|
|
166
139
|
} catch (err) {
|
|
167
140
|
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Hono } from 'hono';\nimport { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';\n\nexport interface ObjectStackHonoOptions {\n kernel: ObjectKernel;\n prefix?: string;\n}\n\n/**\n * Auth service interface with handleRequest method\n */\ninterface AuthService {\n handleRequest(request: Request): Promise<Response>;\n}\n\n/**\n * Middleware mode for existing Hono apps\n */\nexport function objectStackMiddleware(kernel: ObjectKernel) {\n return async (c: any, next: any) => {\n c.set('objectStack', kernel);\n await next();\n };\n}\n\n/**\n * Creates a full-featured Hono app with all ObjectStack route dispatchers.\n * Provides Auth, GraphQL, Metadata, Data, and Storage routes matching\n * Next.js/NestJS adapter completeness.\n *\n * @example\n * ```ts\n * import { createHonoApp } from '@objectstack/hono';\n * const app = createHonoApp({ kernel });\n * export default app;\n * ```\n */\nexport function createHonoApp(options: ObjectStackHonoOptions): Hono {\n const app = new Hono();\n const prefix = options.prefix || '/api';\n const dispatcher = new HttpDispatcher(options.kernel);\n\n const errorJson = (c: any, message: string, code: number = 500) => {\n return c.json({ success: false, error: { message, code } }, code);\n };\n\n const toResponse = (c: any, result: HttpDispatcherResult) => {\n if (result.handled) {\n if (result.response) {\n if (result.response.headers) {\n Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return c.json(result.response.body, result.response.status);\n }\n if (result.result) {\n const res = result.result;\n if (res.type === 'redirect' && res.url) {\n return c.redirect(res.url);\n }\n if (res.type === 'stream' && res.stream) {\n if (res.headers) {\n Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return new Response(res.stream, { status: 200 });\n }\n return c.json(res, 200);\n }\n }\n return errorJson(c, 'Not Found', 404);\n };\n\n // --- Discovery ---\n app.get(`${prefix}`, async (c) => {\n return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });\n });\n\n // --- .well-known ---\n app.get('/.well-known/objectstack', (c) => {\n return c.redirect(prefix);\n });\n\n // --- Auth ---\n app.all(`${prefix}/auth/*`, async (c) => {\n try {\n const path = c.req.path.substring(`${prefix}/auth/`.length);\n const method = c.req.method;\n\n // Try AuthPlugin service first (prefer async to support factory-based services)\n let authService: AuthService | null = null;\n try {\n if (typeof options.kernel.getServiceAsync === 'function') {\n authService = await options.kernel.getServiceAsync<AuthService>('auth');\n } else if (typeof options.kernel.getService === 'function') {\n authService = options.kernel.getService<AuthService>('auth');\n }\n } catch {\n // Service not registered — fall through to dispatcher\n authService = null;\n }\n\n if (authService && typeof authService.handleRequest === 'function') {\n const response = await authService.handleRequest(c.req.raw);\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n }\n\n // Fallback to legacy dispatcher\n const body = method === 'GET' || method === 'HEAD'\n ? {}\n : await c.req.json().catch(() => ({}));\n const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- GraphQL ---\n app.post(`${prefix}/graphql`, async (c) => {\n try {\n const body = await c.req.json();\n const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });\n return c.json(result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Metadata ---\n app.all(`${prefix}/meta/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/meta`.length);\n const method = c.req.method;\n\n let body: any = undefined;\n if (method === 'PUT' || method === 'POST') {\n body = await c.req.json().catch(() => ({}));\n }\n\n const result = await dispatcher.handleMetadata(subPath, { request: c.req.raw }, method, body);\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // Also handle /meta with no trailing path\n app.all(`${prefix}/meta`, async (c) => {\n try {\n const method = c.req.method;\n let body: any = undefined;\n if (method === 'PUT' || method === 'POST') {\n body = await c.req.json().catch(() => ({}));\n }\n const result = await dispatcher.handleMetadata('', { request: c.req.raw }, method, body);\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Data ---\n app.all(`${prefix}/data/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/data`.length);\n const method = c.req.method;\n\n let body: any = {};\n if (method === 'POST' || method === 'PATCH') {\n body = await c.req.json().catch(() => ({}));\n }\n\n const queryParams: Record<string, any> = {};\n const url = new URL(c.req.url);\n url.searchParams.forEach((val, key) => { queryParams[key] = val; });\n\n const result = await dispatcher.handleData(subPath, method, body, queryParams, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Storage ---\n app.all(`${prefix}/storage/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/storage`.length);\n const method = c.req.method;\n\n let file: any = undefined;\n if (method === 'POST' && subPath === '/upload') {\n const formData = await c.req.formData();\n file = formData.get('file');\n }\n\n const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n return app;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Hono } from 'hono';\nimport { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';\n\nexport interface ObjectStackHonoOptions {\n kernel: ObjectKernel;\n prefix?: string;\n}\n\n/**\n * Auth service interface with handleRequest method\n */\ninterface AuthService {\n handleRequest(request: Request): Promise<Response>;\n}\n\n/**\n * Middleware mode for existing Hono apps\n */\nexport function objectStackMiddleware(kernel: ObjectKernel) {\n return async (c: any, next: any) => {\n c.set('objectStack', kernel);\n await next();\n };\n}\n\n/**\n * Creates a full-featured Hono app with all ObjectStack route dispatchers.\n *\n * Only routes that need framework-specific handling (auth service, storage\n * formData, GraphQL raw result, discovery wrapper) are registered explicitly.\n * All other routes (meta, data, packages, analytics, automation, i18n, ui,\n * openapi, custom endpoints, and any future routes) are handled by a\n * catch-all that delegates to `HttpDispatcher.dispatch()`.\n *\n * This means new routes added to `HttpDispatcher` automatically work in\n * every adapter without any adapter-side code changes.\n *\n * @example\n * ```ts\n * import { createHonoApp } from '@objectstack/hono';\n * const app = createHonoApp({ kernel });\n * export default app;\n * ```\n */\nexport function createHonoApp(options: ObjectStackHonoOptions): Hono {\n const app = new Hono();\n const prefix = options.prefix || '/api';\n const dispatcher = new HttpDispatcher(options.kernel);\n\n const errorJson = (c: any, message: string, code: number = 500) => {\n return c.json({ success: false, error: { message, code } }, code);\n };\n\n const toResponse = (c: any, result: HttpDispatcherResult) => {\n if (result.handled) {\n if (result.response) {\n if (result.response.headers) {\n Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return c.json(result.response.body, result.response.status);\n }\n if (result.result) {\n const res = result.result;\n if (res.type === 'redirect' && res.url) {\n return c.redirect(res.url);\n }\n if (res.type === 'stream' && res.stream) {\n if (res.headers) {\n Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return new Response(res.stream, { status: 200 });\n }\n return c.json(res, 200);\n }\n }\n return errorJson(c, 'Not Found', 404);\n };\n\n // ─── Explicit routes (framework-specific handling required) ────────────────\n\n // --- Discovery ---\n app.get(`${prefix}`, async (c) => {\n return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });\n });\n\n // --- .well-known ---\n app.get('/.well-known/objectstack', (c) => {\n return c.redirect(prefix);\n });\n\n // --- Auth (needs auth service integration) ---\n app.all(`${prefix}/auth/*`, async (c) => {\n try {\n const path = c.req.path.substring(`${prefix}/auth/`.length);\n const method = c.req.method;\n\n // Try AuthPlugin service first (prefer async to support factory-based services)\n let authService: AuthService | null = null;\n try {\n if (typeof options.kernel.getServiceAsync === 'function') {\n authService = await options.kernel.getServiceAsync<AuthService>('auth');\n } else if (typeof options.kernel.getService === 'function') {\n authService = options.kernel.getService<AuthService>('auth');\n }\n } catch {\n // Service not registered — fall through to dispatcher\n authService = null;\n }\n\n if (authService && typeof authService.handleRequest === 'function') {\n const response = await authService.handleRequest(c.req.raw);\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n }\n\n // Fallback to legacy dispatcher\n const body = method === 'GET' || method === 'HEAD'\n ? {}\n : await c.req.json().catch(() => ({}));\n const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- GraphQL (returns raw result, not HttpDispatcherResult) ---\n app.post(`${prefix}/graphql`, async (c) => {\n try {\n const body = await c.req.json();\n const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });\n return c.json(result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Storage (needs formData parsing) ---\n app.all(`${prefix}/storage/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/storage`.length);\n const method = c.req.method;\n\n let file: any = undefined;\n if (method === 'POST' && subPath === '/upload') {\n const formData = await c.req.formData();\n file = formData.get('file');\n }\n\n const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // ─── Catch-all: delegate to dispatcher.dispatch() ─────────────────────────\n // Handles meta, data, packages, analytics, automation, i18n, ui, openapi,\n // custom API endpoints, and any future routes added to HttpDispatcher.\n app.all(`${prefix}/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(prefix.length);\n const method = c.req.method;\n\n let body: any = undefined;\n if (method === 'POST' || method === 'PUT' || method === 'PATCH') {\n body = await c.req.json().catch(() => ({}));\n }\n\n const queryParams: Record<string, any> = {};\n const url = new URL(c.req.url);\n url.searchParams.forEach((val, key) => { queryParams[key] = val; });\n\n const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n return app;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -81,39 +81,27 @@ function createHonoApp(options) {
|
|
|
81
81
|
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
82
82
|
}
|
|
83
83
|
});
|
|
84
|
-
app.all(`${prefix}/
|
|
84
|
+
app.all(`${prefix}/storage/*`, async (c) => {
|
|
85
85
|
try {
|
|
86
|
-
const subPath = c.req.path.substring(`${prefix}/
|
|
86
|
+
const subPath = c.req.path.substring(`${prefix}/storage`.length);
|
|
87
87
|
const method = c.req.method;
|
|
88
|
-
let
|
|
89
|
-
if (method === "
|
|
90
|
-
|
|
88
|
+
let file = void 0;
|
|
89
|
+
if (method === "POST" && subPath === "/upload") {
|
|
90
|
+
const formData = await c.req.formData();
|
|
91
|
+
file = formData.get("file");
|
|
91
92
|
}
|
|
92
|
-
const result = await dispatcher.
|
|
93
|
+
const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });
|
|
93
94
|
return toResponse(c, result);
|
|
94
95
|
} catch (err) {
|
|
95
96
|
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
96
97
|
}
|
|
97
98
|
});
|
|
98
|
-
app.all(`${prefix}
|
|
99
|
+
app.all(`${prefix}/*`, async (c) => {
|
|
99
100
|
try {
|
|
101
|
+
const subPath = c.req.path.substring(prefix.length);
|
|
100
102
|
const method = c.req.method;
|
|
101
103
|
let body = void 0;
|
|
102
|
-
if (method === "PUT" || method === "
|
|
103
|
-
body = await c.req.json().catch(() => ({}));
|
|
104
|
-
}
|
|
105
|
-
const result = await dispatcher.handleMetadata("", { request: c.req.raw }, method, body);
|
|
106
|
-
return toResponse(c, result);
|
|
107
|
-
} catch (err) {
|
|
108
|
-
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
app.all(`${prefix}/data/*`, async (c) => {
|
|
112
|
-
try {
|
|
113
|
-
const subPath = c.req.path.substring(`${prefix}/data`.length);
|
|
114
|
-
const method = c.req.method;
|
|
115
|
-
let body = {};
|
|
116
|
-
if (method === "POST" || method === "PATCH") {
|
|
104
|
+
if (method === "POST" || method === "PUT" || method === "PATCH") {
|
|
117
105
|
body = await c.req.json().catch(() => ({}));
|
|
118
106
|
}
|
|
119
107
|
const queryParams = {};
|
|
@@ -121,22 +109,7 @@ function createHonoApp(options) {
|
|
|
121
109
|
url.searchParams.forEach((val, key) => {
|
|
122
110
|
queryParams[key] = val;
|
|
123
111
|
});
|
|
124
|
-
const result = await dispatcher.
|
|
125
|
-
return toResponse(c, result);
|
|
126
|
-
} catch (err) {
|
|
127
|
-
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
app.all(`${prefix}/storage/*`, async (c) => {
|
|
131
|
-
try {
|
|
132
|
-
const subPath = c.req.path.substring(`${prefix}/storage`.length);
|
|
133
|
-
const method = c.req.method;
|
|
134
|
-
let file = void 0;
|
|
135
|
-
if (method === "POST" && subPath === "/upload") {
|
|
136
|
-
const formData = await c.req.formData();
|
|
137
|
-
file = formData.get("file");
|
|
138
|
-
}
|
|
139
|
-
const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });
|
|
112
|
+
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
|
|
140
113
|
return toResponse(c, result);
|
|
141
114
|
} catch (err) {
|
|
142
115
|
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Hono } from 'hono';\nimport { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';\n\nexport interface ObjectStackHonoOptions {\n kernel: ObjectKernel;\n prefix?: string;\n}\n\n/**\n * Auth service interface with handleRequest method\n */\ninterface AuthService {\n handleRequest(request: Request): Promise<Response>;\n}\n\n/**\n * Middleware mode for existing Hono apps\n */\nexport function objectStackMiddleware(kernel: ObjectKernel) {\n return async (c: any, next: any) => {\n c.set('objectStack', kernel);\n await next();\n };\n}\n\n/**\n * Creates a full-featured Hono app with all ObjectStack route dispatchers.\n * Provides Auth, GraphQL, Metadata, Data, and Storage routes matching\n * Next.js/NestJS adapter completeness.\n *\n * @example\n * ```ts\n * import { createHonoApp } from '@objectstack/hono';\n * const app = createHonoApp({ kernel });\n * export default app;\n * ```\n */\nexport function createHonoApp(options: ObjectStackHonoOptions): Hono {\n const app = new Hono();\n const prefix = options.prefix || '/api';\n const dispatcher = new HttpDispatcher(options.kernel);\n\n const errorJson = (c: any, message: string, code: number = 500) => {\n return c.json({ success: false, error: { message, code } }, code);\n };\n\n const toResponse = (c: any, result: HttpDispatcherResult) => {\n if (result.handled) {\n if (result.response) {\n if (result.response.headers) {\n Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return c.json(result.response.body, result.response.status);\n }\n if (result.result) {\n const res = result.result;\n if (res.type === 'redirect' && res.url) {\n return c.redirect(res.url);\n }\n if (res.type === 'stream' && res.stream) {\n if (res.headers) {\n Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return new Response(res.stream, { status: 200 });\n }\n return c.json(res, 200);\n }\n }\n return errorJson(c, 'Not Found', 404);\n };\n\n // --- Discovery ---\n app.get(`${prefix}`, async (c) => {\n return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });\n });\n\n // --- .well-known ---\n app.get('/.well-known/objectstack', (c) => {\n return c.redirect(prefix);\n });\n\n // --- Auth ---\n app.all(`${prefix}/auth/*`, async (c) => {\n try {\n const path = c.req.path.substring(`${prefix}/auth/`.length);\n const method = c.req.method;\n\n // Try AuthPlugin service first (prefer async to support factory-based services)\n let authService: AuthService | null = null;\n try {\n if (typeof options.kernel.getServiceAsync === 'function') {\n authService = await options.kernel.getServiceAsync<AuthService>('auth');\n } else if (typeof options.kernel.getService === 'function') {\n authService = options.kernel.getService<AuthService>('auth');\n }\n } catch {\n // Service not registered — fall through to dispatcher\n authService = null;\n }\n\n if (authService && typeof authService.handleRequest === 'function') {\n const response = await authService.handleRequest(c.req.raw);\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n }\n\n // Fallback to legacy dispatcher\n const body = method === 'GET' || method === 'HEAD'\n ? {}\n : await c.req.json().catch(() => ({}));\n const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- GraphQL ---\n app.post(`${prefix}/graphql`, async (c) => {\n try {\n const body = await c.req.json();\n const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });\n return c.json(result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Metadata ---\n app.all(`${prefix}/meta/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/meta`.length);\n const method = c.req.method;\n\n let body: any = undefined;\n if (method === 'PUT' || method === 'POST') {\n body = await c.req.json().catch(() => ({}));\n }\n\n const result = await dispatcher.handleMetadata(subPath, { request: c.req.raw }, method, body);\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // Also handle /meta with no trailing path\n app.all(`${prefix}/meta`, async (c) => {\n try {\n const method = c.req.method;\n let body: any = undefined;\n if (method === 'PUT' || method === 'POST') {\n body = await c.req.json().catch(() => ({}));\n }\n const result = await dispatcher.handleMetadata('', { request: c.req.raw }, method, body);\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Data ---\n app.all(`${prefix}/data/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/data`.length);\n const method = c.req.method;\n\n let body: any = {};\n if (method === 'POST' || method === 'PATCH') {\n body = await c.req.json().catch(() => ({}));\n }\n\n const queryParams: Record<string, any> = {};\n const url = new URL(c.req.url);\n url.searchParams.forEach((val, key) => { queryParams[key] = val; });\n\n const result = await dispatcher.handleData(subPath, method, body, queryParams, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Storage ---\n app.all(`${prefix}/storage/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/storage`.length);\n const method = c.req.method;\n\n let file: any = undefined;\n if (method === 'POST' && subPath === '/upload') {\n const formData = await c.req.formData();\n file = formData.get('file');\n }\n\n const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n return app;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Hono } from 'hono';\nimport { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';\n\nexport interface ObjectStackHonoOptions {\n kernel: ObjectKernel;\n prefix?: string;\n}\n\n/**\n * Auth service interface with handleRequest method\n */\ninterface AuthService {\n handleRequest(request: Request): Promise<Response>;\n}\n\n/**\n * Middleware mode for existing Hono apps\n */\nexport function objectStackMiddleware(kernel: ObjectKernel) {\n return async (c: any, next: any) => {\n c.set('objectStack', kernel);\n await next();\n };\n}\n\n/**\n * Creates a full-featured Hono app with all ObjectStack route dispatchers.\n *\n * Only routes that need framework-specific handling (auth service, storage\n * formData, GraphQL raw result, discovery wrapper) are registered explicitly.\n * All other routes (meta, data, packages, analytics, automation, i18n, ui,\n * openapi, custom endpoints, and any future routes) are handled by a\n * catch-all that delegates to `HttpDispatcher.dispatch()`.\n *\n * This means new routes added to `HttpDispatcher` automatically work in\n * every adapter without any adapter-side code changes.\n *\n * @example\n * ```ts\n * import { createHonoApp } from '@objectstack/hono';\n * const app = createHonoApp({ kernel });\n * export default app;\n * ```\n */\nexport function createHonoApp(options: ObjectStackHonoOptions): Hono {\n const app = new Hono();\n const prefix = options.prefix || '/api';\n const dispatcher = new HttpDispatcher(options.kernel);\n\n const errorJson = (c: any, message: string, code: number = 500) => {\n return c.json({ success: false, error: { message, code } }, code);\n };\n\n const toResponse = (c: any, result: HttpDispatcherResult) => {\n if (result.handled) {\n if (result.response) {\n if (result.response.headers) {\n Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return c.json(result.response.body, result.response.status);\n }\n if (result.result) {\n const res = result.result;\n if (res.type === 'redirect' && res.url) {\n return c.redirect(res.url);\n }\n if (res.type === 'stream' && res.stream) {\n if (res.headers) {\n Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));\n }\n return new Response(res.stream, { status: 200 });\n }\n return c.json(res, 200);\n }\n }\n return errorJson(c, 'Not Found', 404);\n };\n\n // ─── Explicit routes (framework-specific handling required) ────────────────\n\n // --- Discovery ---\n app.get(`${prefix}`, async (c) => {\n return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });\n });\n\n // --- .well-known ---\n app.get('/.well-known/objectstack', (c) => {\n return c.redirect(prefix);\n });\n\n // --- Auth (needs auth service integration) ---\n app.all(`${prefix}/auth/*`, async (c) => {\n try {\n const path = c.req.path.substring(`${prefix}/auth/`.length);\n const method = c.req.method;\n\n // Try AuthPlugin service first (prefer async to support factory-based services)\n let authService: AuthService | null = null;\n try {\n if (typeof options.kernel.getServiceAsync === 'function') {\n authService = await options.kernel.getServiceAsync<AuthService>('auth');\n } else if (typeof options.kernel.getService === 'function') {\n authService = options.kernel.getService<AuthService>('auth');\n }\n } catch {\n // Service not registered — fall through to dispatcher\n authService = null;\n }\n\n if (authService && typeof authService.handleRequest === 'function') {\n const response = await authService.handleRequest(c.req.raw);\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n }\n\n // Fallback to legacy dispatcher\n const body = method === 'GET' || method === 'HEAD'\n ? {}\n : await c.req.json().catch(() => ({}));\n const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- GraphQL (returns raw result, not HttpDispatcherResult) ---\n app.post(`${prefix}/graphql`, async (c) => {\n try {\n const body = await c.req.json();\n const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });\n return c.json(result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // --- Storage (needs formData parsing) ---\n app.all(`${prefix}/storage/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(`${prefix}/storage`.length);\n const method = c.req.method;\n\n let file: any = undefined;\n if (method === 'POST' && subPath === '/upload') {\n const formData = await c.req.formData();\n file = formData.get('file');\n }\n\n const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n // ─── Catch-all: delegate to dispatcher.dispatch() ─────────────────────────\n // Handles meta, data, packages, analytics, automation, i18n, ui, openapi,\n // custom API endpoints, and any future routes added to HttpDispatcher.\n app.all(`${prefix}/*`, async (c) => {\n try {\n const subPath = c.req.path.substring(prefix.length);\n const method = c.req.method;\n\n let body: any = undefined;\n if (method === 'POST' || method === 'PUT' || method === 'PATCH') {\n body = await c.req.json().catch(() => ({}));\n }\n\n const queryParams: Record<string, any> = {};\n const url = new URL(c.req.url);\n url.searchParams.forEach((val, key) => { queryParams[key] = val; });\n\n const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });\n return toResponse(c, result);\n } catch (err: any) {\n return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);\n }\n });\n\n return app;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/hono",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.9",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"hono": "^4.12.
|
|
16
|
-
"@objectstack/runtime": "^3.2.
|
|
15
|
+
"hono": "^4.12.8",
|
|
16
|
+
"@objectstack/runtime": "^3.2.9"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"hono": "^4.12.
|
|
19
|
+
"hono": "^4.12.8",
|
|
20
20
|
"typescript": "^5.0.0",
|
|
21
|
-
"vitest": "^4.0
|
|
22
|
-
"@objectstack/runtime": "3.2.
|
|
21
|
+
"vitest": "^4.1.0",
|
|
22
|
+
"@objectstack/runtime": "3.2.9"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup --config ../../../tsup.config.ts",
|
package/src/hono.test.ts
CHANGED
|
@@ -8,9 +8,8 @@ const mockDispatcher = {
|
|
|
8
8
|
getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }),
|
|
9
9
|
handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }),
|
|
10
10
|
handleGraphQL: vi.fn().mockResolvedValue({ data: {} }),
|
|
11
|
-
handleMetadata: vi.fn().mockResolvedValue({ handled: true, response: { body: { objects: [] }, status: 200 } }),
|
|
12
|
-
handleData: vi.fn().mockResolvedValue({ handled: true, response: { body: { records: [] }, status: 200 } }),
|
|
13
11
|
handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }),
|
|
12
|
+
dispatch: vi.fn().mockResolvedValue({ handled: true, response: { body: { success: true }, status: 200 } }),
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
vi.mock('@objectstack/runtime', () => {
|
|
@@ -268,21 +267,20 @@ describe('createHonoApp', () => {
|
|
|
268
267
|
});
|
|
269
268
|
});
|
|
270
269
|
|
|
271
|
-
describe('
|
|
272
|
-
it('GET /api/meta/objects
|
|
270
|
+
describe('Catch-all Dispatch', () => {
|
|
271
|
+
it('GET /api/meta/objects delegates to dispatch()', async () => {
|
|
273
272
|
const res = await app.request('/api/meta/objects');
|
|
274
273
|
expect(res.status).toBe(200);
|
|
275
|
-
|
|
276
|
-
expect(json.objects).toBeDefined();
|
|
277
|
-
expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith(
|
|
278
|
-
'/objects',
|
|
279
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
274
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
280
275
|
'GET',
|
|
276
|
+
'/meta/objects',
|
|
281
277
|
undefined,
|
|
278
|
+
expect.any(Object),
|
|
279
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
282
280
|
);
|
|
283
281
|
});
|
|
284
282
|
|
|
285
|
-
it('PUT /api/meta/objects parses JSON body', async () => {
|
|
283
|
+
it('PUT /api/meta/objects parses JSON body via dispatch()', async () => {
|
|
286
284
|
const body = { name: 'test_object' };
|
|
287
285
|
const res = await app.request('/api/meta/objects', {
|
|
288
286
|
method: 'PUT',
|
|
@@ -290,36 +288,46 @@ describe('createHonoApp', () => {
|
|
|
290
288
|
body: JSON.stringify(body),
|
|
291
289
|
});
|
|
292
290
|
expect(res.status).toBe(200);
|
|
293
|
-
expect(mockDispatcher.
|
|
294
|
-
'/objects',
|
|
295
|
-
expect.objectContaining({ request: expect.anything() }),
|
|
291
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
296
292
|
'PUT',
|
|
293
|
+
'/meta/objects',
|
|
297
294
|
body,
|
|
295
|
+
expect.any(Object),
|
|
296
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
298
297
|
);
|
|
299
298
|
});
|
|
300
299
|
|
|
301
300
|
it('GET /api/meta with no trailing path', async () => {
|
|
302
301
|
const res = await app.request('/api/meta');
|
|
303
302
|
expect(res.status).toBe(200);
|
|
304
|
-
expect(mockDispatcher.
|
|
305
|
-
'',
|
|
303
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
304
|
+
'GET',
|
|
305
|
+
'/meta',
|
|
306
|
+
undefined,
|
|
307
|
+
expect.any(Object),
|
|
306
308
|
expect.objectContaining({ request: expect.anything() }),
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('forwards query parameters through dispatch()', async () => {
|
|
313
|
+
const res = await app.request('/api/meta/objects?package=com.acme.crm');
|
|
314
|
+
expect(res.status).toBe(200);
|
|
315
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
307
316
|
'GET',
|
|
317
|
+
'/meta/objects',
|
|
308
318
|
undefined,
|
|
319
|
+
expect.objectContaining({ package: 'com.acme.crm' }),
|
|
320
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
309
321
|
);
|
|
310
322
|
});
|
|
311
|
-
});
|
|
312
323
|
|
|
313
|
-
|
|
314
|
-
it('GET /api/data/account calls handleData', async () => {
|
|
324
|
+
it('GET /api/data/account delegates to dispatch()', async () => {
|
|
315
325
|
const res = await app.request('/api/data/account');
|
|
316
326
|
expect(res.status).toBe(200);
|
|
317
|
-
|
|
318
|
-
expect(json.records).toBeDefined();
|
|
319
|
-
expect(mockDispatcher.handleData).toHaveBeenCalledWith(
|
|
320
|
-
'/account',
|
|
327
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
321
328
|
'GET',
|
|
322
|
-
|
|
329
|
+
'/data/account',
|
|
330
|
+
undefined,
|
|
323
331
|
expect.any(Object),
|
|
324
332
|
expect.objectContaining({ request: expect.anything() }),
|
|
325
333
|
);
|
|
@@ -333,9 +341,9 @@ describe('createHonoApp', () => {
|
|
|
333
341
|
body: JSON.stringify(body),
|
|
334
342
|
});
|
|
335
343
|
expect(res.status).toBe(200);
|
|
336
|
-
expect(mockDispatcher.
|
|
337
|
-
'/account',
|
|
344
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
338
345
|
'POST',
|
|
346
|
+
'/data/account',
|
|
339
347
|
body,
|
|
340
348
|
expect.any(Object),
|
|
341
349
|
expect.objectContaining({ request: expect.anything() }),
|
|
@@ -350,32 +358,129 @@ describe('createHonoApp', () => {
|
|
|
350
358
|
body: JSON.stringify(body),
|
|
351
359
|
});
|
|
352
360
|
expect(res.status).toBe(200);
|
|
353
|
-
expect(mockDispatcher.
|
|
354
|
-
'/account',
|
|
361
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
355
362
|
'PATCH',
|
|
363
|
+
'/data/account',
|
|
356
364
|
body,
|
|
357
365
|
expect.any(Object),
|
|
358
366
|
expect.objectContaining({ request: expect.anything() }),
|
|
359
367
|
);
|
|
360
368
|
});
|
|
361
369
|
|
|
362
|
-
it('returns 404 when result is not handled', async () => {
|
|
363
|
-
mockDispatcher.
|
|
370
|
+
it('returns 404 when dispatch result is not handled', async () => {
|
|
371
|
+
mockDispatcher.dispatch.mockResolvedValueOnce({ handled: false });
|
|
364
372
|
const res = await app.request('/api/data/missing');
|
|
365
373
|
expect(res.status).toBe(404);
|
|
366
374
|
const json = await res.json();
|
|
367
375
|
expect(json.success).toBe(false);
|
|
368
376
|
});
|
|
369
|
-
});
|
|
370
377
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
378
|
+
it('GET /api/packages delegates to dispatch()', async () => {
|
|
379
|
+
const res = await app.request('/api/packages');
|
|
380
|
+
expect(res.status).toBe(200);
|
|
381
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
382
|
+
'GET',
|
|
383
|
+
'/packages',
|
|
384
|
+
undefined,
|
|
385
|
+
expect.any(Object),
|
|
386
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('GET /api/packages/:id delegates to dispatch()', async () => {
|
|
391
|
+
const res = await app.request('/api/packages/com.acme.crm');
|
|
392
|
+
expect(res.status).toBe(200);
|
|
393
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
394
|
+
'GET',
|
|
395
|
+
'/packages/com.acme.crm',
|
|
396
|
+
undefined,
|
|
397
|
+
expect.any(Object),
|
|
398
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('POST /api/packages parses JSON body', async () => {
|
|
403
|
+
const body = { manifest: { name: 'test-pkg' } };
|
|
404
|
+
const res = await app.request('/api/packages', {
|
|
405
|
+
method: 'POST',
|
|
406
|
+
headers: { 'Content-Type': 'application/json' },
|
|
407
|
+
body: JSON.stringify(body),
|
|
408
|
+
});
|
|
409
|
+
expect(res.status).toBe(200);
|
|
410
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
411
|
+
'POST',
|
|
412
|
+
'/packages',
|
|
413
|
+
body,
|
|
414
|
+
expect.any(Object),
|
|
415
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('GET /api/packages?status=active forwards query params', async () => {
|
|
420
|
+
const res = await app.request('/api/packages?status=active');
|
|
374
421
|
expect(res.status).toBe(200);
|
|
375
|
-
expect(mockDispatcher.
|
|
376
|
-
'/files',
|
|
422
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
377
423
|
'GET',
|
|
424
|
+
'/packages',
|
|
378
425
|
undefined,
|
|
426
|
+
expect.objectContaining({ status: 'active' }),
|
|
427
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('returns error on dispatch exception', async () => {
|
|
432
|
+
mockDispatcher.dispatch.mockRejectedValueOnce(new Error('Service unavailable'));
|
|
433
|
+
const res = await app.request('/api/packages');
|
|
434
|
+
expect(res.status).toBe(500);
|
|
435
|
+
const json = await res.json();
|
|
436
|
+
expect(json.success).toBe(false);
|
|
437
|
+
expect(json.error.message).toBe('Service unavailable');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('GET /api/analytics delegates to dispatch()', async () => {
|
|
441
|
+
const res = await app.request('/api/analytics');
|
|
442
|
+
expect(res.status).toBe(200);
|
|
443
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
444
|
+
'GET',
|
|
445
|
+
'/analytics',
|
|
446
|
+
undefined,
|
|
447
|
+
expect.any(Object),
|
|
448
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('GET /api/automation delegates to dispatch()', async () => {
|
|
453
|
+
const res = await app.request('/api/automation');
|
|
454
|
+
expect(res.status).toBe(200);
|
|
455
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
456
|
+
'GET',
|
|
457
|
+
'/automation',
|
|
458
|
+
undefined,
|
|
459
|
+
expect.any(Object),
|
|
460
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('GET /api/i18n delegates to dispatch()', async () => {
|
|
465
|
+
const res = await app.request('/api/i18n');
|
|
466
|
+
expect(res.status).toBe(200);
|
|
467
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
468
|
+
'GET',
|
|
469
|
+
'/i18n',
|
|
470
|
+
undefined,
|
|
471
|
+
expect.any(Object),
|
|
472
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('GET /api/ui/view/account delegates to dispatch()', async () => {
|
|
477
|
+
const res = await app.request('/api/ui/view/account');
|
|
478
|
+
expect(res.status).toBe(200);
|
|
479
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
480
|
+
'GET',
|
|
481
|
+
'/ui/view/account',
|
|
482
|
+
undefined,
|
|
483
|
+
expect.any(Object),
|
|
379
484
|
expect.objectContaining({ request: expect.anything() }),
|
|
380
485
|
);
|
|
381
486
|
});
|
|
@@ -383,7 +488,7 @@ describe('createHonoApp', () => {
|
|
|
383
488
|
|
|
384
489
|
describe('Error Handling', () => {
|
|
385
490
|
it('returns 500 with default message on generic error', async () => {
|
|
386
|
-
mockDispatcher.
|
|
491
|
+
mockDispatcher.dispatch.mockRejectedValueOnce(new Error());
|
|
387
492
|
const res = await app.request('/api/data/account');
|
|
388
493
|
expect(res.status).toBe(500);
|
|
389
494
|
const json = await res.json();
|
|
@@ -391,7 +496,7 @@ describe('createHonoApp', () => {
|
|
|
391
496
|
});
|
|
392
497
|
|
|
393
498
|
it('uses custom statusCode from error', async () => {
|
|
394
|
-
mockDispatcher.
|
|
499
|
+
mockDispatcher.dispatch.mockRejectedValueOnce(
|
|
395
500
|
Object.assign(new Error('Forbidden'), { statusCode: 403 }),
|
|
396
501
|
);
|
|
397
502
|
const res = await app.request('/api/data/account');
|
|
@@ -403,7 +508,7 @@ describe('createHonoApp', () => {
|
|
|
403
508
|
|
|
404
509
|
describe('toResponse', () => {
|
|
405
510
|
it('handles redirect result', async () => {
|
|
406
|
-
mockDispatcher.
|
|
511
|
+
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
407
512
|
handled: true,
|
|
408
513
|
result: { type: 'redirect', url: 'https://example.com' },
|
|
409
514
|
});
|
|
@@ -413,7 +518,7 @@ describe('createHonoApp', () => {
|
|
|
413
518
|
});
|
|
414
519
|
|
|
415
520
|
it('handles generic result objects with 200 status', async () => {
|
|
416
|
-
mockDispatcher.
|
|
521
|
+
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
417
522
|
handled: true,
|
|
418
523
|
result: { foo: 'bar' },
|
|
419
524
|
});
|
|
@@ -424,7 +529,7 @@ describe('createHonoApp', () => {
|
|
|
424
529
|
});
|
|
425
530
|
|
|
426
531
|
it('sets custom headers from response', async () => {
|
|
427
|
-
mockDispatcher.
|
|
532
|
+
mockDispatcher.dispatch.mockResolvedValueOnce({
|
|
428
533
|
handled: true,
|
|
429
534
|
response: { status: 201, body: { id: 1 }, headers: { 'X-Custom': 'yes' } },
|
|
430
535
|
});
|
|
@@ -439,4 +544,138 @@ describe('createHonoApp', () => {
|
|
|
439
544
|
expect(json.id).toBe(1);
|
|
440
545
|
});
|
|
441
546
|
});
|
|
547
|
+
|
|
548
|
+
describe('Vercel Delegation Pattern (api/index.ts → inner.fetch)', () => {
|
|
549
|
+
/**
|
|
550
|
+
* Helper: creates the same outer→inner delegation pattern used by
|
|
551
|
+
* `apps/studio/api/index.ts`. The outer Hono app delegates all
|
|
552
|
+
* requests to the inner ObjectStack Hono app via `inner.fetch()`.
|
|
553
|
+
*/
|
|
554
|
+
function createVercelApp() {
|
|
555
|
+
const innerApp = createHonoApp({ kernel: mockKernel, prefix: '/api/v1' });
|
|
556
|
+
const outerApp = new Hono();
|
|
557
|
+
outerApp.all('*', async (c) => {
|
|
558
|
+
return innerApp.fetch(c.req.raw);
|
|
559
|
+
});
|
|
560
|
+
return outerApp;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
it('works when an outer Hono app delegates via inner.fetch(c.req.raw)', async () => {
|
|
564
|
+
const outerApp = createVercelApp();
|
|
565
|
+
|
|
566
|
+
const res = await outerApp.request('/api/v1/meta');
|
|
567
|
+
expect(res.status).toBe(200);
|
|
568
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
569
|
+
'GET',
|
|
570
|
+
'/meta',
|
|
571
|
+
undefined,
|
|
572
|
+
expect.any(Object),
|
|
573
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('routes /api/v1/packages through outer→inner delegation', async () => {
|
|
578
|
+
const outerApp = createVercelApp();
|
|
579
|
+
|
|
580
|
+
const res = await outerApp.request('/api/v1/packages');
|
|
581
|
+
expect(res.status).toBe(200);
|
|
582
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
583
|
+
'GET',
|
|
584
|
+
'/packages',
|
|
585
|
+
undefined,
|
|
586
|
+
expect.any(Object),
|
|
587
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('routes /api/v1 discovery through outer→inner delegation', async () => {
|
|
592
|
+
const outerApp = createVercelApp();
|
|
593
|
+
|
|
594
|
+
const res = await outerApp.request('/api/v1');
|
|
595
|
+
expect(res.status).toBe(200);
|
|
596
|
+
const json = await res.json();
|
|
597
|
+
expect(json.data).toBeDefined();
|
|
598
|
+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('routes /api/v1/data/account through outer→inner delegation', async () => {
|
|
602
|
+
const outerApp = createVercelApp();
|
|
603
|
+
|
|
604
|
+
const res = await outerApp.request('/api/v1/data/account');
|
|
605
|
+
expect(res.status).toBe(200);
|
|
606
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
607
|
+
'GET',
|
|
608
|
+
'/data/account',
|
|
609
|
+
undefined,
|
|
610
|
+
expect.any(Object),
|
|
611
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('returns 500 with error details when inner app throws', async () => {
|
|
616
|
+
const outerApp = new Hono();
|
|
617
|
+
|
|
618
|
+
outerApp.all('*', async (c) => {
|
|
619
|
+
try {
|
|
620
|
+
// Simulate a kernel boot failure
|
|
621
|
+
throw new Error('Kernel boot failed');
|
|
622
|
+
} catch (err: any) {
|
|
623
|
+
return c.json(
|
|
624
|
+
{ success: false, error: { message: err.message, code: 500 } },
|
|
625
|
+
500,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const res = await outerApp.request('/api/v1/meta');
|
|
631
|
+
expect(res.status).toBe(500);
|
|
632
|
+
const json = await res.json();
|
|
633
|
+
expect(json.success).toBe(false);
|
|
634
|
+
expect(json.error.message).toBe('Kernel boot failed');
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
describe('Vercel deployment endpoint smoke tests', () => {
|
|
639
|
+
/**
|
|
640
|
+
* These tests validate that the two key deployment-health endpoints
|
|
641
|
+
* `/api/v1/meta` and `/api/v1/packages` return 200 OK when routed
|
|
642
|
+
* through the Vercel adapter pattern (outer Hono → inner ObjectStack Hono).
|
|
643
|
+
*/
|
|
644
|
+
let outerApp: Hono;
|
|
645
|
+
|
|
646
|
+
beforeEach(() => {
|
|
647
|
+
vi.clearAllMocks();
|
|
648
|
+
const innerApp = createHonoApp({ kernel: mockKernel, prefix: '/api/v1' });
|
|
649
|
+
outerApp = new Hono();
|
|
650
|
+
outerApp.all('*', async (c) => innerApp.fetch(c.req.raw));
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('GET /api/v1/meta returns 200 OK', async () => {
|
|
654
|
+
const res = await outerApp.request('/api/v1/meta');
|
|
655
|
+
expect(res.status).toBe(200);
|
|
656
|
+
const json = await res.json();
|
|
657
|
+
expect(json.success).toBe(true);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('GET /api/v1/meta/object returns 200 OK', async () => {
|
|
661
|
+
const res = await outerApp.request('/api/v1/meta/object');
|
|
662
|
+
expect(res.status).toBe(200);
|
|
663
|
+
const json = await res.json();
|
|
664
|
+
expect(json.success).toBe(true);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('GET /api/v1/packages returns 200 OK', async () => {
|
|
668
|
+
const res = await outerApp.request('/api/v1/packages');
|
|
669
|
+
expect(res.status).toBe(200);
|
|
670
|
+
const json = await res.json();
|
|
671
|
+
expect(json.success).toBe(true);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('GET /api/v1/packages/:id returns 200 OK', async () => {
|
|
675
|
+
const res = await outerApp.request('/api/v1/packages/com.acme.crm');
|
|
676
|
+
expect(res.status).toBe(200);
|
|
677
|
+
const json = await res.json();
|
|
678
|
+
expect(json.success).toBe(true);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
442
681
|
});
|
package/src/index.ts
CHANGED
|
@@ -27,8 +27,15 @@ export function objectStackMiddleware(kernel: ObjectKernel) {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Creates a full-featured Hono app with all ObjectStack route dispatchers.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
*
|
|
31
|
+
* Only routes that need framework-specific handling (auth service, storage
|
|
32
|
+
* formData, GraphQL raw result, discovery wrapper) are registered explicitly.
|
|
33
|
+
* All other routes (meta, data, packages, analytics, automation, i18n, ui,
|
|
34
|
+
* openapi, custom endpoints, and any future routes) are handled by a
|
|
35
|
+
* catch-all that delegates to `HttpDispatcher.dispatch()`.
|
|
36
|
+
*
|
|
37
|
+
* This means new routes added to `HttpDispatcher` automatically work in
|
|
38
|
+
* every adapter without any adapter-side code changes.
|
|
32
39
|
*
|
|
33
40
|
* @example
|
|
34
41
|
* ```ts
|
|
@@ -71,6 +78,8 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
|
|
|
71
78
|
return errorJson(c, 'Not Found', 404);
|
|
72
79
|
};
|
|
73
80
|
|
|
81
|
+
// ─── Explicit routes (framework-specific handling required) ────────────────
|
|
82
|
+
|
|
74
83
|
// --- Discovery ---
|
|
75
84
|
app.get(`${prefix}`, async (c) => {
|
|
76
85
|
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
@@ -81,7 +90,7 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
|
|
|
81
90
|
return c.redirect(prefix);
|
|
82
91
|
});
|
|
83
92
|
|
|
84
|
-
// --- Auth ---
|
|
93
|
+
// --- Auth (needs auth service integration) ---
|
|
85
94
|
app.all(`${prefix}/auth/*`, async (c) => {
|
|
86
95
|
try {
|
|
87
96
|
const path = c.req.path.substring(`${prefix}/auth/`.length);
|
|
@@ -119,7 +128,7 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
|
|
|
119
128
|
}
|
|
120
129
|
});
|
|
121
130
|
|
|
122
|
-
// --- GraphQL ---
|
|
131
|
+
// --- GraphQL (returns raw result, not HttpDispatcherResult) ---
|
|
123
132
|
app.post(`${prefix}/graphql`, async (c) => {
|
|
124
133
|
try {
|
|
125
134
|
const body = await c.req.json();
|
|
@@ -130,47 +139,35 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
|
|
|
130
139
|
}
|
|
131
140
|
});
|
|
132
141
|
|
|
133
|
-
// ---
|
|
134
|
-
app.all(`${prefix}/
|
|
142
|
+
// --- Storage (needs formData parsing) ---
|
|
143
|
+
app.all(`${prefix}/storage/*`, async (c) => {
|
|
135
144
|
try {
|
|
136
|
-
const subPath = c.req.path.substring(`${prefix}/
|
|
145
|
+
const subPath = c.req.path.substring(`${prefix}/storage`.length);
|
|
137
146
|
const method = c.req.method;
|
|
138
147
|
|
|
139
|
-
let
|
|
140
|
-
if (method === '
|
|
141
|
-
|
|
148
|
+
let file: any = undefined;
|
|
149
|
+
if (method === 'POST' && subPath === '/upload') {
|
|
150
|
+
const formData = await c.req.formData();
|
|
151
|
+
file = formData.get('file');
|
|
142
152
|
}
|
|
143
153
|
|
|
144
|
-
const result = await dispatcher.
|
|
145
|
-
return toResponse(c, result);
|
|
146
|
-
} catch (err: any) {
|
|
147
|
-
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Also handle /meta with no trailing path
|
|
152
|
-
app.all(`${prefix}/meta`, async (c) => {
|
|
153
|
-
try {
|
|
154
|
-
const method = c.req.method;
|
|
155
|
-
let body: any = undefined;
|
|
156
|
-
if (method === 'PUT' || method === 'POST') {
|
|
157
|
-
body = await c.req.json().catch(() => ({}));
|
|
158
|
-
}
|
|
159
|
-
const result = await dispatcher.handleMetadata('', { request: c.req.raw }, method, body);
|
|
154
|
+
const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });
|
|
160
155
|
return toResponse(c, result);
|
|
161
156
|
} catch (err: any) {
|
|
162
157
|
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|
|
163
158
|
}
|
|
164
159
|
});
|
|
165
160
|
|
|
166
|
-
//
|
|
167
|
-
|
|
161
|
+
// ─── Catch-all: delegate to dispatcher.dispatch() ─────────────────────────
|
|
162
|
+
// Handles meta, data, packages, analytics, automation, i18n, ui, openapi,
|
|
163
|
+
// custom API endpoints, and any future routes added to HttpDispatcher.
|
|
164
|
+
app.all(`${prefix}/*`, async (c) => {
|
|
168
165
|
try {
|
|
169
|
-
const subPath = c.req.path.substring(
|
|
166
|
+
const subPath = c.req.path.substring(prefix.length);
|
|
170
167
|
const method = c.req.method;
|
|
171
168
|
|
|
172
|
-
let body: any =
|
|
173
|
-
if (method === 'POST' || method === 'PATCH') {
|
|
169
|
+
let body: any = undefined;
|
|
170
|
+
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
|
174
171
|
body = await c.req.json().catch(() => ({}));
|
|
175
172
|
}
|
|
176
173
|
|
|
@@ -178,26 +175,7 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
|
|
|
178
175
|
const url = new URL(c.req.url);
|
|
179
176
|
url.searchParams.forEach((val, key) => { queryParams[key] = val; });
|
|
180
177
|
|
|
181
|
-
const result = await dispatcher.
|
|
182
|
-
return toResponse(c, result);
|
|
183
|
-
} catch (err: any) {
|
|
184
|
-
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// --- Storage ---
|
|
189
|
-
app.all(`${prefix}/storage/*`, async (c) => {
|
|
190
|
-
try {
|
|
191
|
-
const subPath = c.req.path.substring(`${prefix}/storage`.length);
|
|
192
|
-
const method = c.req.method;
|
|
193
|
-
|
|
194
|
-
let file: any = undefined;
|
|
195
|
-
if (method === 'POST' && subPath === '/upload') {
|
|
196
|
-
const formData = await c.req.formData();
|
|
197
|
-
file = formData.get('file');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });
|
|
178
|
+
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
|
|
201
179
|
return toResponse(c, result);
|
|
202
180
|
} catch (err: any) {
|
|
203
181
|
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|