@objectstack/hono 4.0.1 → 4.0.2
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 +9 -9
- package/CHANGELOG.md +7 -0
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/hono.test.ts +269 -0
- package/src/index.ts +32 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/hono@4.0.
|
|
2
|
+
> @objectstack/hono@4.0.2 build /home/runner/work/framework/framework/packages/adapters/hono
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
7
|
[34mCLI[39m tsup v8.5.1
|
|
8
|
-
[34mCLI[39m Using tsup config: /home/runner/work/
|
|
8
|
+
[34mCLI[39m Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
|
|
9
9
|
[34mCLI[39m Target: es2020
|
|
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[32m5.37 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m11.82 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 76ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m6.44 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m11.87 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 76ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 13532ms
|
|
21
21
|
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.17 KB[39m
|
|
22
22
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.17 KB[39m
|
package/CHANGELOG.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -52,6 +52,31 @@ function createHonoApp(options) {
|
|
|
52
52
|
if (res.type === "redirect" && res.url) {
|
|
53
53
|
return c.redirect(res.url);
|
|
54
54
|
}
|
|
55
|
+
if (res.type === "stream" && res.events) {
|
|
56
|
+
const headers = {
|
|
57
|
+
"Content-Type": res.contentType || "text/event-stream",
|
|
58
|
+
"Cache-Control": "no-cache",
|
|
59
|
+
"Connection": "keep-alive",
|
|
60
|
+
...res.headers || {}
|
|
61
|
+
};
|
|
62
|
+
const stream = new ReadableStream({
|
|
63
|
+
async start(controller) {
|
|
64
|
+
try {
|
|
65
|
+
const encoder = new TextEncoder();
|
|
66
|
+
for await (const event of res.events) {
|
|
67
|
+
const chunk = res.vercelDataStream ? typeof event === "string" ? event : JSON.stringify(event) + "\n" : `data: ${JSON.stringify(event)}
|
|
68
|
+
|
|
69
|
+
`;
|
|
70
|
+
controller.enqueue(encoder.encode(chunk));
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
} finally {
|
|
74
|
+
controller.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return new Response(stream, { status: 200, headers });
|
|
79
|
+
}
|
|
55
80
|
if (res.type === "stream" && res.stream) {
|
|
56
81
|
if (res.headers) {
|
|
57
82
|
Object.entries(res.headers).forEach(([k, v]) => c.header(k, v));
|
|
@@ -66,6 +91,9 @@ function createHonoApp(options) {
|
|
|
66
91
|
app.get(prefix, async (c) => {
|
|
67
92
|
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
68
93
|
});
|
|
94
|
+
app.get(`${prefix}/discovery`, async (c) => {
|
|
95
|
+
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
96
|
+
});
|
|
69
97
|
app.get("/.well-known/objectstack", (c) => {
|
|
70
98
|
return c.redirect(prefix);
|
|
71
99
|
});
|
|
@@ -134,7 +162,7 @@ function createHonoApp(options) {
|
|
|
134
162
|
url.searchParams.forEach((val, key) => {
|
|
135
163
|
queryParams[key] = val;
|
|
136
164
|
});
|
|
137
|
-
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
|
|
165
|
+
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix);
|
|
138
166
|
return toResponse(c, result);
|
|
139
167
|
} catch (err) {
|
|
140
168
|
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 *\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":[]}
|
|
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.events) {\n // SSE / Vercel Data Stream streaming response\n const headers: Record<string, string> = {\n 'Content-Type': res.contentType || 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n ...(res.headers || {}),\n };\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const encoder = new TextEncoder();\n for await (const event of res.events) {\n const chunk = res.vercelDataStream\n ? (typeof event === 'string' ? event : JSON.stringify(event) + '\\n')\n : `data: ${JSON.stringify(event)}\\n\\n`;\n controller.enqueue(encoder.encode(chunk));\n }\n } catch (err) {\n // Stream error — close gracefully\n } finally {\n controller.close();\n }\n },\n });\n return new Response(stream, { status: 200, headers });\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 app.get(`${prefix}/discovery`, 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 }, prefix);\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,kBAAqB;AACrB,qBAAwE;AAiBjE,SAAS,sBAAsB,QAAsB;AAC1D,SAAO,OAAO,GAAQ,SAAc;AAClC,MAAE,IAAI,eAAe,MAAM;AAC3B,UAAM,KAAK;AAAA,EACb;AACF;AAqBO,SAAS,cAAc,SAAuC;AACnE,QAAM,MAAM,IAAI,iBAAK;AACrB,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,aAAa,IAAI,8BAAe,QAAQ,MAAM;AAEpD,QAAM,YAAY,CAAC,GAAQ,SAAiB,OAAe,QAAQ;AACjE,WAAO,EAAE,KAAK,EAAE,SAAS,OAAO,OAAO,EAAE,SAAS,KAAK,EAAE,GAAG,IAAI;AAAA,EAClE;AAEA,QAAM,aAAa,CAAC,GAAQ,WAAiC;AAC3D,QAAI,OAAO,SAAS;AAClB,UAAI,OAAO,UAAU;AACnB,YAAI,OAAO,SAAS,SAAS;AAC3B,iBAAO,QAAQ,OAAO,SAAS,OAAO,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,CAAW,CAAC;AAAA,QACtF;AACA,eAAO,EAAE,KAAK,OAAO,SAAS,MAAM,OAAO,SAAS,MAAM;AAAA,MAC5D;AACA,UAAI,OAAO,QAAQ;AACjB,cAAM,MAAM,OAAO;AACnB,YAAI,IAAI,SAAS,cAAc,IAAI,KAAK;AACtC,iBAAO,EAAE,SAAS,IAAI,GAAG;AAAA,QAC3B;AACA,YAAI,IAAI,SAAS,YAAY,IAAI,QAAQ;AAEvC,gBAAM,UAAkC;AAAA,YACtC,gBAAgB,IAAI,eAAe;AAAA,YACnC,iBAAiB;AAAA,YACjB,cAAc;AAAA,YACd,GAAI,IAAI,WAAW,CAAC;AAAA,UACtB;AACA,gBAAM,SAAS,IAAI,eAAe;AAAA,YAChC,MAAM,MAAM,YAAY;AACtB,kBAAI;AACF,sBAAM,UAAU,IAAI,YAAY;AAChC,iCAAiB,SAAS,IAAI,QAAQ;AACpC,wBAAM,QAAQ,IAAI,mBACb,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK,IAAI,OAC7D,SAAS,KAAK,UAAU,KAAK,CAAC;AAAA;AAAA;AAClC,6BAAW,QAAQ,QAAQ,OAAO,KAAK,CAAC;AAAA,gBAC1C;AAAA,cACF,SAAS,KAAK;AAAA,cAEd,UAAE;AACA,2BAAW,MAAM;AAAA,cACnB;AAAA,YACF;AAAA,UACF,CAAC;AACD,iBAAO,IAAI,SAAS,QAAQ,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,QACtD;AACA,YAAI,IAAI,SAAS,YAAY,IAAI,QAAQ;AACvC,cAAI,IAAI,SAAS;AACf,mBAAO,QAAQ,IAAI,OAAO,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,CAAW,CAAC;AAAA,UAC1E;AACA,iBAAO,IAAI,SAAS,IAAI,QAAQ,EAAE,QAAQ,IAAI,CAAC;AAAA,QACjD;AACA,eAAO,EAAE,KAAK,KAAK,GAAG;AAAA,MACxB;AAAA,IACF;AACA,WAAO,UAAU,GAAG,aAAa,GAAG;AAAA,EACtC;AAKA,MAAI,IAAI,QAAQ,OAAO,MAAM;AAC3B,WAAO,EAAE,KAAK,EAAE,MAAM,MAAM,WAAW,iBAAiB,MAAM,EAAE,CAAC;AAAA,EACnE,CAAC;AAED,MAAI,IAAI,GAAG,MAAM,cAAc,OAAO,MAAM;AAC1C,WAAO,EAAE,KAAK,EAAE,MAAM,MAAM,WAAW,iBAAiB,MAAM,EAAE,CAAC;AAAA,EACnE,CAAC;AAGD,MAAI,IAAI,4BAA4B,CAAC,MAAM;AACzC,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B,CAAC;AAGD,MAAI,IAAI,GAAG,MAAM,WAAW,OAAO,MAAM;AACvC,QAAI;AACF,YAAM,OAAO,EAAE,IAAI,KAAK,UAAU,GAAG,MAAM,SAAS,MAAM;AAC1D,YAAM,SAAS,EAAE,IAAI;AAGrB,UAAI,cAAkC;AACtC,UAAI;AACF,YAAI,OAAO,QAAQ,OAAO,oBAAoB,YAAY;AACxD,wBAAc,MAAM,QAAQ,OAAO,gBAA6B,MAAM;AAAA,QACxE,WAAW,OAAO,QAAQ,OAAO,eAAe,YAAY;AAC1D,wBAAc,QAAQ,OAAO,WAAwB,MAAM;AAAA,QAC7D;AAAA,MACF,QAAQ;AAEN,sBAAc;AAAA,MAChB;AAEA,UAAI,eAAe,OAAO,YAAY,kBAAkB,YAAY;AAClE,cAAM,WAAW,MAAM,YAAY,cAAc,EAAE,IAAI,GAAG;AAC1D,eAAO,IAAI,SAAS,SAAS,MAAM;AAAA,UACjC,QAAQ,SAAS;AAAA,UACjB,SAAS,SAAS;AAAA,QACpB,CAAC;AAAA,MACH;AAGA,YAAM,OAAO,WAAW,SAAS,WAAW,SACxC,CAAC,IACD,MAAM,EAAE,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACvC,YAAM,SAAS,MAAM,WAAW,WAAW,MAAM,QAAQ,MAAM,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC;AACrF,aAAO,WAAW,GAAG,MAAM;AAAA,IAC7B,SAAS,KAAU;AACjB,aAAO,UAAU,GAAG,IAAI,WAAW,yBAAyB,IAAI,cAAc,GAAG;AAAA,IACnF;AAAA,EACF,CAAC;AAGD,MAAI,KAAK,GAAG,MAAM,YAAY,OAAO,MAAM;AACzC,QAAI;AACF,YAAM,OAAO,MAAM,EAAE,IAAI,KAAK;AAC9B,YAAM,SAAS,MAAM,WAAW,cAAc,MAAM,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC;AAC1E,aAAO,EAAE,KAAK,MAAM;AAAA,IACtB,SAAS,KAAU;AACjB,aAAO,UAAU,GAAG,IAAI,WAAW,yBAAyB,IAAI,cAAc,GAAG;AAAA,IACnF;AAAA,EACF,CAAC;AAGD,MAAI,IAAI,GAAG,MAAM,cAAc,OAAO,MAAM;AAC1C,QAAI;AACF,YAAM,UAAU,EAAE,IAAI,KAAK,UAAU,GAAG,MAAM,WAAW,MAAM;AAC/D,YAAM,SAAS,EAAE,IAAI;AAErB,UAAI,OAAY;AAChB,UAAI,WAAW,UAAU,YAAY,WAAW;AAC9C,cAAM,WAAW,MAAM,EAAE,IAAI,SAAS;AACtC,eAAO,SAAS,IAAI,MAAM;AAAA,MAC5B;AAEA,YAAM,SAAS,MAAM,WAAW,cAAc,SAAS,QAAQ,MAAM,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC;AAC3F,aAAO,WAAW,GAAG,MAAM;AAAA,IAC7B,SAAS,KAAU;AACjB,aAAO,UAAU,GAAG,IAAI,WAAW,yBAAyB,IAAI,cAAc,GAAG;AAAA,IACnF;AAAA,EACF,CAAC;AAKD,MAAI,IAAI,GAAG,MAAM,MAAM,OAAO,MAAM;AAClC,QAAI;AACF,YAAM,UAAU,EAAE,IAAI,KAAK,UAAU,OAAO,MAAM;AAClD,YAAM,SAAS,EAAE,IAAI;AAErB,UAAI,OAAY;AAChB,UAAI,WAAW,UAAU,WAAW,SAAS,WAAW,SAAS;AAC/D,eAAO,MAAM,EAAE,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAAA,MAC5C;AAEA,YAAM,cAAmC,CAAC;AAC1C,YAAM,MAAM,IAAI,IAAI,EAAE,IAAI,GAAG;AAC7B,UAAI,aAAa,QAAQ,CAAC,KAAK,QAAQ;AAAE,oBAAY,GAAG,IAAI;AAAA,MAAK,CAAC;AAElE,YAAM,SAAS,MAAM,WAAW,SAAS,QAAQ,SAAS,MAAM,aAAa,EAAE,SAAS,EAAE,IAAI,IAAI,GAAG,MAAM;AAC3G,aAAO,WAAW,GAAG,MAAM;AAAA,IAC7B,SAAS,KAAU;AACjB,aAAO,UAAU,GAAG,IAAI,WAAW,yBAAyB,IAAI,cAAc,GAAG;AAAA,IACnF;AAAA,EACF,CAAC;AAED,SAAO;AACT;","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -27,6 +27,31 @@ function createHonoApp(options) {
|
|
|
27
27
|
if (res.type === "redirect" && res.url) {
|
|
28
28
|
return c.redirect(res.url);
|
|
29
29
|
}
|
|
30
|
+
if (res.type === "stream" && res.events) {
|
|
31
|
+
const headers = {
|
|
32
|
+
"Content-Type": res.contentType || "text/event-stream",
|
|
33
|
+
"Cache-Control": "no-cache",
|
|
34
|
+
"Connection": "keep-alive",
|
|
35
|
+
...res.headers || {}
|
|
36
|
+
};
|
|
37
|
+
const stream = new ReadableStream({
|
|
38
|
+
async start(controller) {
|
|
39
|
+
try {
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
for await (const event of res.events) {
|
|
42
|
+
const chunk = res.vercelDataStream ? typeof event === "string" ? event : JSON.stringify(event) + "\n" : `data: ${JSON.stringify(event)}
|
|
43
|
+
|
|
44
|
+
`;
|
|
45
|
+
controller.enqueue(encoder.encode(chunk));
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
} finally {
|
|
49
|
+
controller.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return new Response(stream, { status: 200, headers });
|
|
54
|
+
}
|
|
30
55
|
if (res.type === "stream" && res.stream) {
|
|
31
56
|
if (res.headers) {
|
|
32
57
|
Object.entries(res.headers).forEach(([k, v]) => c.header(k, v));
|
|
@@ -41,6 +66,9 @@ function createHonoApp(options) {
|
|
|
41
66
|
app.get(prefix, async (c) => {
|
|
42
67
|
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
43
68
|
});
|
|
69
|
+
app.get(`${prefix}/discovery`, async (c) => {
|
|
70
|
+
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
71
|
+
});
|
|
44
72
|
app.get("/.well-known/objectstack", (c) => {
|
|
45
73
|
return c.redirect(prefix);
|
|
46
74
|
});
|
|
@@ -109,7 +137,7 @@ function createHonoApp(options) {
|
|
|
109
137
|
url.searchParams.forEach((val, key) => {
|
|
110
138
|
queryParams[key] = val;
|
|
111
139
|
});
|
|
112
|
-
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
|
|
140
|
+
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix);
|
|
113
141
|
return toResponse(c, result);
|
|
114
142
|
} catch (err) {
|
|
115
143
|
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 *\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":[]}
|
|
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.events) {\n // SSE / Vercel Data Stream streaming response\n const headers: Record<string, string> = {\n 'Content-Type': res.contentType || 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n ...(res.headers || {}),\n };\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const encoder = new TextEncoder();\n for await (const event of res.events) {\n const chunk = res.vercelDataStream\n ? (typeof event === 'string' ? event : JSON.stringify(event) + '\\n')\n : `data: ${JSON.stringify(event)}\\n\\n`;\n controller.enqueue(encoder.encode(chunk));\n }\n } catch (err) {\n // Stream error — close gracefully\n } finally {\n controller.close();\n }\n },\n });\n return new Response(stream, { status: 200, headers });\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 app.get(`${prefix}/discovery`, 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 }, prefix);\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": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
15
|
"hono": "^4.12.8",
|
|
16
|
-
"@objectstack/runtime": "^4.0.
|
|
16
|
+
"@objectstack/runtime": "^4.0.2"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"hono": "^4.12.
|
|
19
|
+
"hono": "^4.12.10",
|
|
20
20
|
"typescript": "^6.0.2",
|
|
21
21
|
"vitest": "^4.1.2",
|
|
22
|
-
"@objectstack/runtime": "4.0.
|
|
22
|
+
"@objectstack/runtime": "4.0.2"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup --config ../../../tsup.config.ts",
|
package/src/hono.test.ts
CHANGED
|
@@ -97,6 +97,14 @@ describe('createHonoApp', () => {
|
|
|
97
97
|
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
it('GET /api/discovery returns discovery info with correct prefix', async () => {
|
|
101
|
+
const res = await app.request('/api/discovery');
|
|
102
|
+
expect(res.status).toBe(200);
|
|
103
|
+
const json = await res.json();
|
|
104
|
+
expect(json.data).toBeDefined();
|
|
105
|
+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
|
|
106
|
+
});
|
|
107
|
+
|
|
100
108
|
it('uses custom prefix for discovery', async () => {
|
|
101
109
|
const customApp = createHonoApp({ kernel: mockKernel, prefix: '/v2' });
|
|
102
110
|
const res = await customApp.request('/v2');
|
|
@@ -105,6 +113,15 @@ describe('createHonoApp', () => {
|
|
|
105
113
|
expect(json.data).toBeDefined();
|
|
106
114
|
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
|
|
107
115
|
});
|
|
116
|
+
|
|
117
|
+
it('uses custom prefix for /discovery route', async () => {
|
|
118
|
+
const customApp = createHonoApp({ kernel: mockKernel, prefix: '/v2' });
|
|
119
|
+
const res = await customApp.request('/v2/discovery');
|
|
120
|
+
expect(res.status).toBe(200);
|
|
121
|
+
const json = await res.json();
|
|
122
|
+
expect(json.data).toBeDefined();
|
|
123
|
+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
|
|
124
|
+
});
|
|
108
125
|
});
|
|
109
126
|
|
|
110
127
|
describe('.well-known Endpoint', () => {
|
|
@@ -277,6 +294,7 @@ describe('createHonoApp', () => {
|
|
|
277
294
|
undefined,
|
|
278
295
|
expect.any(Object),
|
|
279
296
|
expect.objectContaining({ request: expect.anything() }),
|
|
297
|
+
'/api',
|
|
280
298
|
);
|
|
281
299
|
});
|
|
282
300
|
|
|
@@ -294,6 +312,7 @@ describe('createHonoApp', () => {
|
|
|
294
312
|
body,
|
|
295
313
|
expect.any(Object),
|
|
296
314
|
expect.objectContaining({ request: expect.anything() }),
|
|
315
|
+
'/api',
|
|
297
316
|
);
|
|
298
317
|
});
|
|
299
318
|
|
|
@@ -306,6 +325,7 @@ describe('createHonoApp', () => {
|
|
|
306
325
|
undefined,
|
|
307
326
|
expect.any(Object),
|
|
308
327
|
expect.objectContaining({ request: expect.anything() }),
|
|
328
|
+
'/api',
|
|
309
329
|
);
|
|
310
330
|
});
|
|
311
331
|
|
|
@@ -318,6 +338,7 @@ describe('createHonoApp', () => {
|
|
|
318
338
|
undefined,
|
|
319
339
|
expect.objectContaining({ package: 'com.acme.crm' }),
|
|
320
340
|
expect.objectContaining({ request: expect.anything() }),
|
|
341
|
+
'/api',
|
|
321
342
|
);
|
|
322
343
|
});
|
|
323
344
|
|
|
@@ -330,6 +351,7 @@ describe('createHonoApp', () => {
|
|
|
330
351
|
undefined,
|
|
331
352
|
expect.any(Object),
|
|
332
353
|
expect.objectContaining({ request: expect.anything() }),
|
|
354
|
+
'/api',
|
|
333
355
|
);
|
|
334
356
|
});
|
|
335
357
|
|
|
@@ -347,6 +369,7 @@ describe('createHonoApp', () => {
|
|
|
347
369
|
body,
|
|
348
370
|
expect.any(Object),
|
|
349
371
|
expect.objectContaining({ request: expect.anything() }),
|
|
372
|
+
'/api',
|
|
350
373
|
);
|
|
351
374
|
});
|
|
352
375
|
|
|
@@ -364,6 +387,7 @@ describe('createHonoApp', () => {
|
|
|
364
387
|
body,
|
|
365
388
|
expect.any(Object),
|
|
366
389
|
expect.objectContaining({ request: expect.anything() }),
|
|
390
|
+
'/api',
|
|
367
391
|
);
|
|
368
392
|
});
|
|
369
393
|
|
|
@@ -384,6 +408,7 @@ describe('createHonoApp', () => {
|
|
|
384
408
|
undefined,
|
|
385
409
|
expect.any(Object),
|
|
386
410
|
expect.objectContaining({ request: expect.anything() }),
|
|
411
|
+
'/api',
|
|
387
412
|
);
|
|
388
413
|
});
|
|
389
414
|
|
|
@@ -396,6 +421,7 @@ describe('createHonoApp', () => {
|
|
|
396
421
|
undefined,
|
|
397
422
|
expect.any(Object),
|
|
398
423
|
expect.objectContaining({ request: expect.anything() }),
|
|
424
|
+
'/api',
|
|
399
425
|
);
|
|
400
426
|
});
|
|
401
427
|
|
|
@@ -413,6 +439,7 @@ describe('createHonoApp', () => {
|
|
|
413
439
|
body,
|
|
414
440
|
expect.any(Object),
|
|
415
441
|
expect.objectContaining({ request: expect.anything() }),
|
|
442
|
+
'/api',
|
|
416
443
|
);
|
|
417
444
|
});
|
|
418
445
|
|
|
@@ -425,6 +452,7 @@ describe('createHonoApp', () => {
|
|
|
425
452
|
undefined,
|
|
426
453
|
expect.objectContaining({ status: 'active' }),
|
|
427
454
|
expect.objectContaining({ request: expect.anything() }),
|
|
455
|
+
'/api',
|
|
428
456
|
);
|
|
429
457
|
});
|
|
430
458
|
|
|
@@ -446,6 +474,7 @@ describe('createHonoApp', () => {
|
|
|
446
474
|
undefined,
|
|
447
475
|
expect.any(Object),
|
|
448
476
|
expect.objectContaining({ request: expect.anything() }),
|
|
477
|
+
'/api',
|
|
449
478
|
);
|
|
450
479
|
});
|
|
451
480
|
|
|
@@ -458,6 +487,7 @@ describe('createHonoApp', () => {
|
|
|
458
487
|
undefined,
|
|
459
488
|
expect.any(Object),
|
|
460
489
|
expect.objectContaining({ request: expect.anything() }),
|
|
490
|
+
'/api',
|
|
461
491
|
);
|
|
462
492
|
});
|
|
463
493
|
|
|
@@ -470,6 +500,7 @@ describe('createHonoApp', () => {
|
|
|
470
500
|
undefined,
|
|
471
501
|
expect.any(Object),
|
|
472
502
|
expect.objectContaining({ request: expect.anything() }),
|
|
503
|
+
'/api',
|
|
473
504
|
);
|
|
474
505
|
});
|
|
475
506
|
|
|
@@ -482,6 +513,7 @@ describe('createHonoApp', () => {
|
|
|
482
513
|
undefined,
|
|
483
514
|
expect.any(Object),
|
|
484
515
|
expect.objectContaining({ request: expect.anything() }),
|
|
516
|
+
'/api',
|
|
485
517
|
);
|
|
486
518
|
});
|
|
487
519
|
});
|
|
@@ -571,6 +603,7 @@ describe('createHonoApp', () => {
|
|
|
571
603
|
undefined,
|
|
572
604
|
expect.any(Object),
|
|
573
605
|
expect.objectContaining({ request: expect.anything() }),
|
|
606
|
+
'/api/v1',
|
|
574
607
|
);
|
|
575
608
|
});
|
|
576
609
|
|
|
@@ -585,6 +618,7 @@ describe('createHonoApp', () => {
|
|
|
585
618
|
undefined,
|
|
586
619
|
expect.any(Object),
|
|
587
620
|
expect.objectContaining({ request: expect.anything() }),
|
|
621
|
+
'/api/v1',
|
|
588
622
|
);
|
|
589
623
|
});
|
|
590
624
|
|
|
@@ -598,6 +632,16 @@ describe('createHonoApp', () => {
|
|
|
598
632
|
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
|
|
599
633
|
});
|
|
600
634
|
|
|
635
|
+
it('routes /api/v1/discovery through outer→inner delegation with correct prefix', async () => {
|
|
636
|
+
const outerApp = createVercelApp();
|
|
637
|
+
|
|
638
|
+
const res = await outerApp.request('/api/v1/discovery');
|
|
639
|
+
expect(res.status).toBe(200);
|
|
640
|
+
const json = await res.json();
|
|
641
|
+
expect(json.data).toBeDefined();
|
|
642
|
+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api/v1');
|
|
643
|
+
});
|
|
644
|
+
|
|
601
645
|
it('routes /api/v1/data/account through outer→inner delegation', async () => {
|
|
602
646
|
const outerApp = createVercelApp();
|
|
603
647
|
|
|
@@ -609,6 +653,84 @@ describe('createHonoApp', () => {
|
|
|
609
653
|
undefined,
|
|
610
654
|
expect.any(Object),
|
|
611
655
|
expect.objectContaining({ request: expect.anything() }),
|
|
656
|
+
'/api/v1',
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('POST /api/v1/data/account parses JSON body through outer→inner delegation', async () => {
|
|
661
|
+
const outerApp = createVercelApp();
|
|
662
|
+
const body = { name: 'Acme' };
|
|
663
|
+
|
|
664
|
+
const res = await outerApp.request('/api/v1/data/account', {
|
|
665
|
+
method: 'POST',
|
|
666
|
+
headers: { 'Content-Type': 'application/json' },
|
|
667
|
+
body: JSON.stringify(body),
|
|
668
|
+
});
|
|
669
|
+
expect(res.status).toBe(200);
|
|
670
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
671
|
+
'POST',
|
|
672
|
+
'/data/account',
|
|
673
|
+
body,
|
|
674
|
+
expect.any(Object),
|
|
675
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
676
|
+
'/api/v1',
|
|
677
|
+
);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('PUT /api/v1/data/account parses JSON body through outer→inner delegation', async () => {
|
|
681
|
+
const outerApp = createVercelApp();
|
|
682
|
+
const body = { name: 'Updated' };
|
|
683
|
+
|
|
684
|
+
const res = await outerApp.request('/api/v1/data/account', {
|
|
685
|
+
method: 'PUT',
|
|
686
|
+
headers: { 'Content-Type': 'application/json' },
|
|
687
|
+
body: JSON.stringify(body),
|
|
688
|
+
});
|
|
689
|
+
expect(res.status).toBe(200);
|
|
690
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
691
|
+
'PUT',
|
|
692
|
+
'/data/account',
|
|
693
|
+
body,
|
|
694
|
+
expect.any(Object),
|
|
695
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
696
|
+
'/api/v1',
|
|
697
|
+
);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('PATCH /api/v1/data/account parses JSON body through outer→inner delegation', async () => {
|
|
701
|
+
const outerApp = createVercelApp();
|
|
702
|
+
const body = { name: 'Patched' };
|
|
703
|
+
|
|
704
|
+
const res = await outerApp.request('/api/v1/data/account', {
|
|
705
|
+
method: 'PATCH',
|
|
706
|
+
headers: { 'Content-Type': 'application/json' },
|
|
707
|
+
body: JSON.stringify(body),
|
|
708
|
+
});
|
|
709
|
+
expect(res.status).toBe(200);
|
|
710
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
711
|
+
'PATCH',
|
|
712
|
+
'/data/account',
|
|
713
|
+
body,
|
|
714
|
+
expect.any(Object),
|
|
715
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
716
|
+
'/api/v1',
|
|
717
|
+
);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it('DELETE /api/v1/data/account routes through outer→inner delegation', async () => {
|
|
721
|
+
const outerApp = createVercelApp();
|
|
722
|
+
|
|
723
|
+
const res = await outerApp.request('/api/v1/data/account', {
|
|
724
|
+
method: 'DELETE',
|
|
725
|
+
});
|
|
726
|
+
expect(res.status).toBe(200);
|
|
727
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
728
|
+
'DELETE',
|
|
729
|
+
'/data/account',
|
|
730
|
+
undefined,
|
|
731
|
+
expect.any(Object),
|
|
732
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
733
|
+
'/api/v1',
|
|
612
734
|
);
|
|
613
735
|
});
|
|
614
736
|
|
|
@@ -635,6 +757,153 @@ describe('createHonoApp', () => {
|
|
|
635
757
|
});
|
|
636
758
|
});
|
|
637
759
|
|
|
760
|
+
describe('Body-safe Vercel delegation (buffered body forwarding)', () => {
|
|
761
|
+
/**
|
|
762
|
+
* Validates the body-safe delegation pattern used in
|
|
763
|
+
* `apps/studio/server/index.ts` where the outer handler buffers
|
|
764
|
+
* POST/PUT/PATCH bodies and creates a fresh `Request` for the inner app.
|
|
765
|
+
* This avoids @hono/node-server's lazy body materialisation which can
|
|
766
|
+
* hang on Vercel when the IncomingMessage stream state has changed.
|
|
767
|
+
*/
|
|
768
|
+
function createBodySafeVercelApp() {
|
|
769
|
+
const innerApp = createHonoApp({ kernel: mockKernel, prefix: '/api/v1' });
|
|
770
|
+
const outerApp = new Hono();
|
|
771
|
+
|
|
772
|
+
outerApp.all('*', async (c) => {
|
|
773
|
+
const method = c.req.method;
|
|
774
|
+
|
|
775
|
+
// GET/HEAD have no body — pass through directly
|
|
776
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
777
|
+
return innerApp.fetch(c.req.raw);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Buffer body and create a fresh Request
|
|
781
|
+
const rawReq = c.req.raw;
|
|
782
|
+
const body = await rawReq.arrayBuffer();
|
|
783
|
+
const forwarded = new Request(rawReq.url, {
|
|
784
|
+
method,
|
|
785
|
+
headers: rawReq.headers,
|
|
786
|
+
body,
|
|
787
|
+
});
|
|
788
|
+
return innerApp.fetch(forwarded);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
return outerApp;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
it('GET requests work without body buffering', async () => {
|
|
795
|
+
const outerApp = createBodySafeVercelApp();
|
|
796
|
+
|
|
797
|
+
const res = await outerApp.request('/api/v1/data/account');
|
|
798
|
+
expect(res.status).toBe(200);
|
|
799
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
800
|
+
'GET',
|
|
801
|
+
'/data/account',
|
|
802
|
+
undefined,
|
|
803
|
+
expect.any(Object),
|
|
804
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
805
|
+
'/api/v1',
|
|
806
|
+
);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('POST body is forwarded correctly via buffered delegation', async () => {
|
|
810
|
+
const outerApp = createBodySafeVercelApp();
|
|
811
|
+
const body = { name: 'Acme Corp' };
|
|
812
|
+
|
|
813
|
+
const res = await outerApp.request('/api/v1/data/account', {
|
|
814
|
+
method: 'POST',
|
|
815
|
+
headers: { 'Content-Type': 'application/json' },
|
|
816
|
+
body: JSON.stringify(body),
|
|
817
|
+
});
|
|
818
|
+
expect(res.status).toBe(200);
|
|
819
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
820
|
+
'POST',
|
|
821
|
+
'/data/account',
|
|
822
|
+
body,
|
|
823
|
+
expect.any(Object),
|
|
824
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
825
|
+
'/api/v1',
|
|
826
|
+
);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('PUT body is forwarded correctly via buffered delegation', async () => {
|
|
830
|
+
const outerApp = createBodySafeVercelApp();
|
|
831
|
+
const body = { name: 'Updated Corp' };
|
|
832
|
+
|
|
833
|
+
const res = await outerApp.request('/api/v1/data/account', {
|
|
834
|
+
method: 'PUT',
|
|
835
|
+
headers: { 'Content-Type': 'application/json' },
|
|
836
|
+
body: JSON.stringify(body),
|
|
837
|
+
});
|
|
838
|
+
expect(res.status).toBe(200);
|
|
839
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
840
|
+
'PUT',
|
|
841
|
+
'/data/account',
|
|
842
|
+
body,
|
|
843
|
+
expect.any(Object),
|
|
844
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
845
|
+
'/api/v1',
|
|
846
|
+
);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
it('PATCH body is forwarded correctly via buffered delegation', async () => {
|
|
850
|
+
const outerApp = createBodySafeVercelApp();
|
|
851
|
+
const body = { status: 'active' };
|
|
852
|
+
|
|
853
|
+
const res = await outerApp.request('/api/v1/data/account', {
|
|
854
|
+
method: 'PATCH',
|
|
855
|
+
headers: { 'Content-Type': 'application/json' },
|
|
856
|
+
body: JSON.stringify(body),
|
|
857
|
+
});
|
|
858
|
+
expect(res.status).toBe(200);
|
|
859
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
860
|
+
'PATCH',
|
|
861
|
+
'/data/account',
|
|
862
|
+
body,
|
|
863
|
+
expect.any(Object),
|
|
864
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
865
|
+
'/api/v1',
|
|
866
|
+
);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it('DELETE without body works via buffered delegation', async () => {
|
|
870
|
+
const outerApp = createBodySafeVercelApp();
|
|
871
|
+
|
|
872
|
+
const res = await outerApp.request('/api/v1/data/account', {
|
|
873
|
+
method: 'DELETE',
|
|
874
|
+
});
|
|
875
|
+
expect(res.status).toBe(200);
|
|
876
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
877
|
+
'DELETE',
|
|
878
|
+
'/data/account',
|
|
879
|
+
undefined,
|
|
880
|
+
expect.any(Object),
|
|
881
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
882
|
+
'/api/v1',
|
|
883
|
+
);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('POST with empty body defaults to {} via buffered delegation', async () => {
|
|
887
|
+
const outerApp = createBodySafeVercelApp();
|
|
888
|
+
|
|
889
|
+
const res = await outerApp.request('/api/v1/data/account', {
|
|
890
|
+
method: 'POST',
|
|
891
|
+
headers: { 'Content-Type': 'application/json' },
|
|
892
|
+
body: '',
|
|
893
|
+
});
|
|
894
|
+
expect(res.status).toBe(200);
|
|
895
|
+
// Empty body falls back to {} via .catch(() => ({})) in the adapter
|
|
896
|
+
expect(mockDispatcher.dispatch).toHaveBeenCalledWith(
|
|
897
|
+
'POST',
|
|
898
|
+
'/data/account',
|
|
899
|
+
{},
|
|
900
|
+
expect.any(Object),
|
|
901
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
902
|
+
'/api/v1',
|
|
903
|
+
);
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
638
907
|
describe('Vercel deployment endpoint smoke tests', () => {
|
|
639
908
|
/**
|
|
640
909
|
* These tests validate that the two key deployment-health endpoints
|
package/src/index.ts
CHANGED
|
@@ -66,6 +66,33 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
|
|
|
66
66
|
if (res.type === 'redirect' && res.url) {
|
|
67
67
|
return c.redirect(res.url);
|
|
68
68
|
}
|
|
69
|
+
if (res.type === 'stream' && res.events) {
|
|
70
|
+
// SSE / Vercel Data Stream streaming response
|
|
71
|
+
const headers: Record<string, string> = {
|
|
72
|
+
'Content-Type': res.contentType || 'text/event-stream',
|
|
73
|
+
'Cache-Control': 'no-cache',
|
|
74
|
+
'Connection': 'keep-alive',
|
|
75
|
+
...(res.headers || {}),
|
|
76
|
+
};
|
|
77
|
+
const stream = new ReadableStream({
|
|
78
|
+
async start(controller) {
|
|
79
|
+
try {
|
|
80
|
+
const encoder = new TextEncoder();
|
|
81
|
+
for await (const event of res.events) {
|
|
82
|
+
const chunk = res.vercelDataStream
|
|
83
|
+
? (typeof event === 'string' ? event : JSON.stringify(event) + '\n')
|
|
84
|
+
: `data: ${JSON.stringify(event)}\n\n`;
|
|
85
|
+
controller.enqueue(encoder.encode(chunk));
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// Stream error — close gracefully
|
|
89
|
+
} finally {
|
|
90
|
+
controller.close();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
return new Response(stream, { status: 200, headers });
|
|
95
|
+
}
|
|
69
96
|
if (res.type === 'stream' && res.stream) {
|
|
70
97
|
if (res.headers) {
|
|
71
98
|
Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));
|
|
@@ -85,6 +112,10 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
|
|
|
85
112
|
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
86
113
|
});
|
|
87
114
|
|
|
115
|
+
app.get(`${prefix}/discovery`, async (c) => {
|
|
116
|
+
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
117
|
+
});
|
|
118
|
+
|
|
88
119
|
// --- .well-known ---
|
|
89
120
|
app.get('/.well-known/objectstack', (c) => {
|
|
90
121
|
return c.redirect(prefix);
|
|
@@ -175,7 +206,7 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
|
|
|
175
206
|
const url = new URL(c.req.url);
|
|
176
207
|
url.searchParams.forEach((val, key) => { queryParams[key] = val; });
|
|
177
208
|
|
|
178
|
-
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw });
|
|
209
|
+
const result = await dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix);
|
|
179
210
|
return toResponse(c, result);
|
|
180
211
|
} catch (err: any) {
|
|
181
212
|
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|