@objectstack/hono 3.0.2 → 3.0.3
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 +8 -0
- package/dist/index.d.mts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +132 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +131 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__mocks__/runtime.ts +14 -2
- package/src/hono.test.ts +343 -1
- package/src/index.ts +183 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/hono@3.0.
|
|
2
|
+
> @objectstack/hono@3.0.3 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
|
-
[
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
[
|
|
17
|
-
[
|
|
18
|
-
[
|
|
13
|
+
[32mCJS[39m [1mdist/index.js [22m[32m6.15 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m10.58 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 52ms
|
|
16
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m5.08 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m10.54 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 55ms
|
|
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 12020ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m812.00 B[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m812.00 B[39m
|
package/CHANGELOG.md
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
1
2
|
import { ObjectKernel } from '@objectstack/runtime';
|
|
2
3
|
|
|
3
4
|
interface ObjectStackHonoOptions {
|
|
@@ -8,5 +9,18 @@ interface ObjectStackHonoOptions {
|
|
|
8
9
|
* Middleware mode for existing Hono apps
|
|
9
10
|
*/
|
|
10
11
|
declare function objectStackMiddleware(kernel: ObjectKernel): (c: any, next: any) => Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Creates a full-featured Hono app with all ObjectStack route dispatchers.
|
|
14
|
+
* Provides Auth, GraphQL, Metadata, Data, and Storage routes matching
|
|
15
|
+
* Next.js/NestJS adapter completeness.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { createHonoApp } from '@objectstack/hono';
|
|
20
|
+
* const app = createHonoApp({ kernel });
|
|
21
|
+
* export default app;
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
declare function createHonoApp(options: ObjectStackHonoOptions): Hono;
|
|
11
25
|
|
|
12
|
-
export { type ObjectStackHonoOptions, objectStackMiddleware };
|
|
26
|
+
export { type ObjectStackHonoOptions, createHonoApp, objectStackMiddleware };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
1
2
|
import { ObjectKernel } from '@objectstack/runtime';
|
|
2
3
|
|
|
3
4
|
interface ObjectStackHonoOptions {
|
|
@@ -8,5 +9,18 @@ interface ObjectStackHonoOptions {
|
|
|
8
9
|
* Middleware mode for existing Hono apps
|
|
9
10
|
*/
|
|
10
11
|
declare function objectStackMiddleware(kernel: ObjectKernel): (c: any, next: any) => Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Creates a full-featured Hono app with all ObjectStack route dispatchers.
|
|
14
|
+
* Provides Auth, GraphQL, Metadata, Data, and Storage routes matching
|
|
15
|
+
* Next.js/NestJS adapter completeness.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { createHonoApp } from '@objectstack/hono';
|
|
20
|
+
* const app = createHonoApp({ kernel });
|
|
21
|
+
* export default app;
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
declare function createHonoApp(options: ObjectStackHonoOptions): Hono;
|
|
11
25
|
|
|
12
|
-
export { type ObjectStackHonoOptions, objectStackMiddleware };
|
|
26
|
+
export { type ObjectStackHonoOptions, createHonoApp, objectStackMiddleware };
|
package/dist/index.js
CHANGED
|
@@ -20,17 +20,149 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
createHonoApp: () => createHonoApp,
|
|
23
24
|
objectStackMiddleware: () => objectStackMiddleware
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var import_hono = require("hono");
|
|
28
|
+
var import_runtime = require("@objectstack/runtime");
|
|
26
29
|
function objectStackMiddleware(kernel) {
|
|
27
30
|
return async (c, next) => {
|
|
28
31
|
c.set("objectStack", kernel);
|
|
29
32
|
await next();
|
|
30
33
|
};
|
|
31
34
|
}
|
|
35
|
+
function createHonoApp(options) {
|
|
36
|
+
const app = new import_hono.Hono();
|
|
37
|
+
const prefix = options.prefix || "/api";
|
|
38
|
+
const dispatcher = new import_runtime.HttpDispatcher(options.kernel);
|
|
39
|
+
const errorJson = (c, message, code = 500) => {
|
|
40
|
+
return c.json({ success: false, error: { message, code } }, code);
|
|
41
|
+
};
|
|
42
|
+
const toResponse = (c, result) => {
|
|
43
|
+
if (result.handled) {
|
|
44
|
+
if (result.response) {
|
|
45
|
+
if (result.response.headers) {
|
|
46
|
+
Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v));
|
|
47
|
+
}
|
|
48
|
+
return c.json(result.response.body, result.response.status);
|
|
49
|
+
}
|
|
50
|
+
if (result.result) {
|
|
51
|
+
const res = result.result;
|
|
52
|
+
if (res.type === "redirect" && res.url) {
|
|
53
|
+
return c.redirect(res.url);
|
|
54
|
+
}
|
|
55
|
+
if (res.type === "stream" && res.stream) {
|
|
56
|
+
if (res.headers) {
|
|
57
|
+
Object.entries(res.headers).forEach(([k, v]) => c.header(k, v));
|
|
58
|
+
}
|
|
59
|
+
return new Response(res.stream, { status: 200 });
|
|
60
|
+
}
|
|
61
|
+
return c.json(res, 200);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return errorJson(c, "Not Found", 404);
|
|
65
|
+
};
|
|
66
|
+
app.get(`${prefix}`, (c) => {
|
|
67
|
+
return c.json({ data: dispatcher.getDiscoveryInfo(prefix) });
|
|
68
|
+
});
|
|
69
|
+
app.get("/.well-known/objectstack", (c) => {
|
|
70
|
+
return c.redirect(prefix);
|
|
71
|
+
});
|
|
72
|
+
app.all(`${prefix}/auth/*`, async (c) => {
|
|
73
|
+
try {
|
|
74
|
+
const path = c.req.path.substring(`${prefix}/auth/`.length);
|
|
75
|
+
const method = c.req.method;
|
|
76
|
+
const authService = typeof options.kernel.getService === "function" ? options.kernel.getService("auth") : null;
|
|
77
|
+
if (authService && typeof authService.handleRequest === "function") {
|
|
78
|
+
const response = await authService.handleRequest(c.req.raw);
|
|
79
|
+
return new Response(response.body, {
|
|
80
|
+
status: response.status,
|
|
81
|
+
headers: response.headers
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const body = method === "GET" || method === "HEAD" ? {} : await c.req.json().catch(() => ({}));
|
|
85
|
+
const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });
|
|
86
|
+
return toResponse(c, result);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
app.post(`${prefix}/graphql`, async (c) => {
|
|
92
|
+
try {
|
|
93
|
+
const body = await c.req.json();
|
|
94
|
+
const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });
|
|
95
|
+
return c.json(result);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
app.all(`${prefix}/meta/*`, async (c) => {
|
|
101
|
+
try {
|
|
102
|
+
const subPath = c.req.path.substring(`${prefix}/meta`.length);
|
|
103
|
+
const method = c.req.method;
|
|
104
|
+
let body = void 0;
|
|
105
|
+
if (method === "PUT" || method === "POST") {
|
|
106
|
+
body = await c.req.json().catch(() => ({}));
|
|
107
|
+
}
|
|
108
|
+
const result = await dispatcher.handleMetadata(subPath, { request: c.req.raw }, method, body);
|
|
109
|
+
return toResponse(c, result);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
app.all(`${prefix}/meta`, async (c) => {
|
|
115
|
+
try {
|
|
116
|
+
const method = c.req.method;
|
|
117
|
+
let body = void 0;
|
|
118
|
+
if (method === "PUT" || method === "POST") {
|
|
119
|
+
body = await c.req.json().catch(() => ({}));
|
|
120
|
+
}
|
|
121
|
+
const result = await dispatcher.handleMetadata("", { request: c.req.raw }, method, body);
|
|
122
|
+
return toResponse(c, result);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
app.all(`${prefix}/data/*`, async (c) => {
|
|
128
|
+
try {
|
|
129
|
+
const subPath = c.req.path.substring(`${prefix}/data`.length);
|
|
130
|
+
const method = c.req.method;
|
|
131
|
+
let body = {};
|
|
132
|
+
if (method === "POST" || method === "PATCH") {
|
|
133
|
+
body = await c.req.json().catch(() => ({}));
|
|
134
|
+
}
|
|
135
|
+
const queryParams = {};
|
|
136
|
+
const url = new URL(c.req.url);
|
|
137
|
+
url.searchParams.forEach((val, key) => {
|
|
138
|
+
queryParams[key] = val;
|
|
139
|
+
});
|
|
140
|
+
const result = await dispatcher.handleData(subPath, method, body, queryParams, { request: c.req.raw });
|
|
141
|
+
return toResponse(c, result);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
app.all(`${prefix}/storage/*`, async (c) => {
|
|
147
|
+
try {
|
|
148
|
+
const subPath = c.req.path.substring(`${prefix}/storage`.length);
|
|
149
|
+
const method = c.req.method;
|
|
150
|
+
let file = void 0;
|
|
151
|
+
if (method === "POST" && subPath === "/upload") {
|
|
152
|
+
const formData = await c.req.formData();
|
|
153
|
+
file = formData.get("file");
|
|
154
|
+
}
|
|
155
|
+
const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });
|
|
156
|
+
return toResponse(c, result);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return app;
|
|
162
|
+
}
|
|
32
163
|
// Annotate the CommonJS export names for ESM import in node:
|
|
33
164
|
0 && (module.exports = {
|
|
165
|
+
createHonoApp,
|
|
34
166
|
objectStackMiddleware
|
|
35
167
|
});
|
|
36
168
|
//# sourceMappingURL=index.js.map
|
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 { type ObjectKernel } from '@objectstack/runtime';\n\nexport interface ObjectStackHonoOptions {\n kernel: ObjectKernel;\n prefix?: string;\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAYO,SAAS,sBAAsB,QAAsB;AAC1D,SAAO,OAAO,GAAQ,SAAc;AAClC,MAAE,IAAI,eAAe,MAAM;AAC3B,UAAM,KAAK;AAAA,EACb;AACF;","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 * 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}`, (c) => {\n return c.json({ data: 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 (preferred path)\n const authService = typeof options.kernel.getService === 'function'\n ? options.kernel.getService<AuthService>('auth')\n : null;\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":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,142 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { HttpDispatcher } from "@objectstack/runtime";
|
|
2
4
|
function objectStackMiddleware(kernel) {
|
|
3
5
|
return async (c, next) => {
|
|
4
6
|
c.set("objectStack", kernel);
|
|
5
7
|
await next();
|
|
6
8
|
};
|
|
7
9
|
}
|
|
10
|
+
function createHonoApp(options) {
|
|
11
|
+
const app = new Hono();
|
|
12
|
+
const prefix = options.prefix || "/api";
|
|
13
|
+
const dispatcher = new HttpDispatcher(options.kernel);
|
|
14
|
+
const errorJson = (c, message, code = 500) => {
|
|
15
|
+
return c.json({ success: false, error: { message, code } }, code);
|
|
16
|
+
};
|
|
17
|
+
const toResponse = (c, result) => {
|
|
18
|
+
if (result.handled) {
|
|
19
|
+
if (result.response) {
|
|
20
|
+
if (result.response.headers) {
|
|
21
|
+
Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v));
|
|
22
|
+
}
|
|
23
|
+
return c.json(result.response.body, result.response.status);
|
|
24
|
+
}
|
|
25
|
+
if (result.result) {
|
|
26
|
+
const res = result.result;
|
|
27
|
+
if (res.type === "redirect" && res.url) {
|
|
28
|
+
return c.redirect(res.url);
|
|
29
|
+
}
|
|
30
|
+
if (res.type === "stream" && res.stream) {
|
|
31
|
+
if (res.headers) {
|
|
32
|
+
Object.entries(res.headers).forEach(([k, v]) => c.header(k, v));
|
|
33
|
+
}
|
|
34
|
+
return new Response(res.stream, { status: 200 });
|
|
35
|
+
}
|
|
36
|
+
return c.json(res, 200);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return errorJson(c, "Not Found", 404);
|
|
40
|
+
};
|
|
41
|
+
app.get(`${prefix}`, (c) => {
|
|
42
|
+
return c.json({ data: dispatcher.getDiscoveryInfo(prefix) });
|
|
43
|
+
});
|
|
44
|
+
app.get("/.well-known/objectstack", (c) => {
|
|
45
|
+
return c.redirect(prefix);
|
|
46
|
+
});
|
|
47
|
+
app.all(`${prefix}/auth/*`, async (c) => {
|
|
48
|
+
try {
|
|
49
|
+
const path = c.req.path.substring(`${prefix}/auth/`.length);
|
|
50
|
+
const method = c.req.method;
|
|
51
|
+
const authService = typeof options.kernel.getService === "function" ? options.kernel.getService("auth") : null;
|
|
52
|
+
if (authService && typeof authService.handleRequest === "function") {
|
|
53
|
+
const response = await authService.handleRequest(c.req.raw);
|
|
54
|
+
return new Response(response.body, {
|
|
55
|
+
status: response.status,
|
|
56
|
+
headers: response.headers
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const body = method === "GET" || method === "HEAD" ? {} : await c.req.json().catch(() => ({}));
|
|
60
|
+
const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });
|
|
61
|
+
return toResponse(c, result);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
app.post(`${prefix}/graphql`, async (c) => {
|
|
67
|
+
try {
|
|
68
|
+
const body = await c.req.json();
|
|
69
|
+
const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });
|
|
70
|
+
return c.json(result);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
app.all(`${prefix}/meta/*`, async (c) => {
|
|
76
|
+
try {
|
|
77
|
+
const subPath = c.req.path.substring(`${prefix}/meta`.length);
|
|
78
|
+
const method = c.req.method;
|
|
79
|
+
let body = void 0;
|
|
80
|
+
if (method === "PUT" || method === "POST") {
|
|
81
|
+
body = await c.req.json().catch(() => ({}));
|
|
82
|
+
}
|
|
83
|
+
const result = await dispatcher.handleMetadata(subPath, { request: c.req.raw }, method, body);
|
|
84
|
+
return toResponse(c, result);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
app.all(`${prefix}/meta`, async (c) => {
|
|
90
|
+
try {
|
|
91
|
+
const method = c.req.method;
|
|
92
|
+
let body = void 0;
|
|
93
|
+
if (method === "PUT" || method === "POST") {
|
|
94
|
+
body = await c.req.json().catch(() => ({}));
|
|
95
|
+
}
|
|
96
|
+
const result = await dispatcher.handleMetadata("", { request: c.req.raw }, method, body);
|
|
97
|
+
return toResponse(c, result);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
app.all(`${prefix}/data/*`, async (c) => {
|
|
103
|
+
try {
|
|
104
|
+
const subPath = c.req.path.substring(`${prefix}/data`.length);
|
|
105
|
+
const method = c.req.method;
|
|
106
|
+
let body = {};
|
|
107
|
+
if (method === "POST" || method === "PATCH") {
|
|
108
|
+
body = await c.req.json().catch(() => ({}));
|
|
109
|
+
}
|
|
110
|
+
const queryParams = {};
|
|
111
|
+
const url = new URL(c.req.url);
|
|
112
|
+
url.searchParams.forEach((val, key) => {
|
|
113
|
+
queryParams[key] = val;
|
|
114
|
+
});
|
|
115
|
+
const result = await dispatcher.handleData(subPath, method, body, queryParams, { request: c.req.raw });
|
|
116
|
+
return toResponse(c, result);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
app.all(`${prefix}/storage/*`, async (c) => {
|
|
122
|
+
try {
|
|
123
|
+
const subPath = c.req.path.substring(`${prefix}/storage`.length);
|
|
124
|
+
const method = c.req.method;
|
|
125
|
+
let file = void 0;
|
|
126
|
+
if (method === "POST" && subPath === "/upload") {
|
|
127
|
+
const formData = await c.req.formData();
|
|
128
|
+
file = formData.get("file");
|
|
129
|
+
}
|
|
130
|
+
const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });
|
|
131
|
+
return toResponse(c, result);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return errorJson(c, err.message || "Internal Server Error", err.statusCode || 500);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return app;
|
|
137
|
+
}
|
|
8
138
|
export {
|
|
139
|
+
createHonoApp,
|
|
9
140
|
objectStackMiddleware
|
|
10
141
|
};
|
|
11
142
|
//# sourceMappingURL=index.mjs.map
|
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 { type ObjectKernel } from '@objectstack/runtime';\n\nexport interface ObjectStackHonoOptions {\n kernel: ObjectKernel;\n prefix?: string;\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"],"mappings":";AAYO,SAAS,sBAAsB,QAAsB;AAC1D,SAAO,OAAO,GAAQ,SAAc;AAClC,MAAE,IAAI,eAAe,MAAM;AAC3B,UAAM,KAAK;AAAA,EACb;AACF;","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 * 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}`, (c) => {\n return c.json({ data: 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 (preferred path)\n const authService = typeof options.kernel.getService === 'function'\n ? options.kernel.getService<AuthService>('auth')\n : null;\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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/hono",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
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.11.9",
|
|
16
|
-
"@objectstack/runtime": "3.0.
|
|
16
|
+
"@objectstack/runtime": "3.0.3"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"hono": "^4.11.9",
|
|
20
20
|
"typescript": "^5.0.0",
|
|
21
21
|
"vitest": "^4.0.18",
|
|
22
|
-
"@objectstack/runtime": "3.0.
|
|
22
|
+
"@objectstack/runtime": "3.0.3"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup --config ../../../tsup.config.ts",
|
package/src/__mocks__/runtime.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
// Stub for @objectstack/runtime -
|
|
2
|
-
|
|
1
|
+
// Stub for @objectstack/runtime - resolved via vitest alias
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
export class HttpDispatcher {
|
|
5
|
+
getDiscoveryInfo = vi.fn().mockReturnValue({ version: '1.0', endpoints: [] });
|
|
6
|
+
handleGraphQL = vi.fn().mockResolvedValue({ data: {} });
|
|
7
|
+
handleAuth = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { ok: true } } });
|
|
8
|
+
handleMetadata = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { objects: [] } } });
|
|
9
|
+
handleData = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: { records: [] } } });
|
|
10
|
+
handleStorage = vi.fn().mockResolvedValue({ handled: true, response: { status: 200, body: {} } });
|
|
11
|
+
|
|
12
|
+
constructor(_kernel: any) {}
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export type ObjectKernel = any;
|
|
4
16
|
export type HttpDispatcherResult = any;
|
package/src/hono.test.ts
CHANGED
|
@@ -3,7 +3,25 @@
|
|
|
3
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// Mock dispatcher instance accessible across tests
|
|
7
|
+
const mockDispatcher = {
|
|
8
|
+
getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }),
|
|
9
|
+
handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }),
|
|
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
|
+
handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
vi.mock('@objectstack/runtime', () => {
|
|
17
|
+
return {
|
|
18
|
+
HttpDispatcher: function HttpDispatcher() {
|
|
19
|
+
return mockDispatcher;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
import { objectStackMiddleware, createHonoApp } from './index';
|
|
7
25
|
|
|
8
26
|
const mockKernel = { name: 'test-kernel' } as any;
|
|
9
27
|
|
|
@@ -61,3 +79,327 @@ describe('objectStackMiddleware', () => {
|
|
|
61
79
|
expect(json.name).toBe('test-kernel');
|
|
62
80
|
});
|
|
63
81
|
});
|
|
82
|
+
|
|
83
|
+
describe('createHonoApp', () => {
|
|
84
|
+
let app: Hono;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
vi.clearAllMocks();
|
|
88
|
+
app = createHonoApp({ kernel: mockKernel });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Discovery Endpoint', () => {
|
|
92
|
+
it('GET /api returns discovery info', async () => {
|
|
93
|
+
const res = await app.request('/api');
|
|
94
|
+
expect(res.status).toBe(200);
|
|
95
|
+
const json = await res.json();
|
|
96
|
+
expect(json.data).toBeDefined();
|
|
97
|
+
expect(json.data.version).toBe('1.0');
|
|
98
|
+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('uses custom prefix for discovery', async () => {
|
|
102
|
+
const customApp = createHonoApp({ kernel: mockKernel, prefix: '/v2' });
|
|
103
|
+
const res = await customApp.request('/v2');
|
|
104
|
+
expect(res.status).toBe(200);
|
|
105
|
+
const json = await res.json();
|
|
106
|
+
expect(json.data).toBeDefined();
|
|
107
|
+
expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('.well-known Endpoint', () => {
|
|
112
|
+
it('GET /.well-known/objectstack redirects to prefix', async () => {
|
|
113
|
+
const res = await app.request('/.well-known/objectstack', { redirect: 'manual' });
|
|
114
|
+
expect(res.status).toBe(302);
|
|
115
|
+
expect(res.headers.get('location')).toBe('/api');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('Auth Endpoint', () => {
|
|
120
|
+
it('POST /api/auth/login calls handleAuth', async () => {
|
|
121
|
+
const res = await app.request('/api/auth/login', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ email: 'a@b.com' }),
|
|
125
|
+
});
|
|
126
|
+
expect(res.status).toBe(200);
|
|
127
|
+
const json = await res.json();
|
|
128
|
+
expect(json.ok).toBe(true);
|
|
129
|
+
expect(mockDispatcher.handleAuth).toHaveBeenCalledWith(
|
|
130
|
+
'login',
|
|
131
|
+
'POST',
|
|
132
|
+
{ email: 'a@b.com' },
|
|
133
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('GET /api/auth/callback calls handleAuth with empty body', async () => {
|
|
138
|
+
const res = await app.request('/api/auth/callback');
|
|
139
|
+
expect(res.status).toBe(200);
|
|
140
|
+
expect(mockDispatcher.handleAuth).toHaveBeenCalledWith(
|
|
141
|
+
'callback',
|
|
142
|
+
'GET',
|
|
143
|
+
{},
|
|
144
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('returns error on handleAuth exception', async () => {
|
|
149
|
+
mockDispatcher.handleAuth.mockRejectedValueOnce(
|
|
150
|
+
Object.assign(new Error('Unauthorized'), { statusCode: 401 }),
|
|
151
|
+
);
|
|
152
|
+
const res = await app.request('/api/auth/login', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
155
|
+
body: JSON.stringify({}),
|
|
156
|
+
});
|
|
157
|
+
expect(res.status).toBe(401);
|
|
158
|
+
const json = await res.json();
|
|
159
|
+
expect(json.success).toBe(false);
|
|
160
|
+
expect(json.error.message).toBe('Unauthorized');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('Auth via AuthPlugin service', () => {
|
|
165
|
+
it('uses kernel.getService("auth") when available', async () => {
|
|
166
|
+
const mockHandleRequest = vi.fn().mockResolvedValue(
|
|
167
|
+
new Response(JSON.stringify({ user: { id: '1' } }), {
|
|
168
|
+
status: 200,
|
|
169
|
+
headers: { 'Content-Type': 'application/json' },
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
const kernelWithAuth = {
|
|
173
|
+
...mockKernel,
|
|
174
|
+
getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }),
|
|
175
|
+
};
|
|
176
|
+
const authApp = createHonoApp({ kernel: kernelWithAuth });
|
|
177
|
+
const res = await authApp.request('/api/auth/sign-in/email', {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: JSON.stringify({ email: 'a@b.com', password: 'pass' }),
|
|
181
|
+
});
|
|
182
|
+
expect(res.status).toBe(200);
|
|
183
|
+
expect(kernelWithAuth.getService).toHaveBeenCalledWith('auth');
|
|
184
|
+
expect(mockHandleRequest).toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('falls back to dispatcher when auth service is not available', async () => {
|
|
188
|
+
const kernelWithoutAuth = {
|
|
189
|
+
...mockKernel,
|
|
190
|
+
getService: vi.fn().mockReturnValue(null),
|
|
191
|
+
};
|
|
192
|
+
const authApp = createHonoApp({ kernel: kernelWithoutAuth });
|
|
193
|
+
const res = await authApp.request('/api/auth/login', {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({ email: 'a@b.com' }),
|
|
197
|
+
});
|
|
198
|
+
expect(res.status).toBe(200);
|
|
199
|
+
expect(mockDispatcher.handleAuth).toHaveBeenCalled();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('GraphQL Endpoint', () => {
|
|
204
|
+
it('POST /api/graphql calls handleGraphQL', async () => {
|
|
205
|
+
const body = { query: '{ objects { name } }' };
|
|
206
|
+
const res = await app.request('/api/graphql', {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers: { 'Content-Type': 'application/json' },
|
|
209
|
+
body: JSON.stringify(body),
|
|
210
|
+
});
|
|
211
|
+
expect(res.status).toBe(200);
|
|
212
|
+
const json = await res.json();
|
|
213
|
+
expect(json.data).toBeDefined();
|
|
214
|
+
expect(mockDispatcher.handleGraphQL).toHaveBeenCalledWith(
|
|
215
|
+
body,
|
|
216
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('returns error on handleGraphQL exception', async () => {
|
|
221
|
+
mockDispatcher.handleGraphQL.mockRejectedValueOnce(new Error('Parse error'));
|
|
222
|
+
const res = await app.request('/api/graphql', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
body: JSON.stringify({ query: 'bad' }),
|
|
226
|
+
});
|
|
227
|
+
expect(res.status).toBe(500);
|
|
228
|
+
const json = await res.json();
|
|
229
|
+
expect(json.success).toBe(false);
|
|
230
|
+
expect(json.error.message).toBe('Parse error');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('Metadata Endpoint', () => {
|
|
235
|
+
it('GET /api/meta/objects calls handleMetadata', async () => {
|
|
236
|
+
const res = await app.request('/api/meta/objects');
|
|
237
|
+
expect(res.status).toBe(200);
|
|
238
|
+
const json = await res.json();
|
|
239
|
+
expect(json.objects).toBeDefined();
|
|
240
|
+
expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith(
|
|
241
|
+
'/objects',
|
|
242
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
243
|
+
'GET',
|
|
244
|
+
undefined,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('PUT /api/meta/objects parses JSON body', async () => {
|
|
249
|
+
const body = { name: 'test_object' };
|
|
250
|
+
const res = await app.request('/api/meta/objects', {
|
|
251
|
+
method: 'PUT',
|
|
252
|
+
headers: { 'Content-Type': 'application/json' },
|
|
253
|
+
body: JSON.stringify(body),
|
|
254
|
+
});
|
|
255
|
+
expect(res.status).toBe(200);
|
|
256
|
+
expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith(
|
|
257
|
+
'/objects',
|
|
258
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
259
|
+
'PUT',
|
|
260
|
+
body,
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('GET /api/meta with no trailing path', async () => {
|
|
265
|
+
const res = await app.request('/api/meta');
|
|
266
|
+
expect(res.status).toBe(200);
|
|
267
|
+
expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith(
|
|
268
|
+
'',
|
|
269
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
270
|
+
'GET',
|
|
271
|
+
undefined,
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('Data Endpoint', () => {
|
|
277
|
+
it('GET /api/data/account calls handleData', async () => {
|
|
278
|
+
const res = await app.request('/api/data/account');
|
|
279
|
+
expect(res.status).toBe(200);
|
|
280
|
+
const json = await res.json();
|
|
281
|
+
expect(json.records).toBeDefined();
|
|
282
|
+
expect(mockDispatcher.handleData).toHaveBeenCalledWith(
|
|
283
|
+
'/account',
|
|
284
|
+
'GET',
|
|
285
|
+
{},
|
|
286
|
+
expect.any(Object),
|
|
287
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('POST /api/data/account parses JSON body', async () => {
|
|
292
|
+
const body = { name: 'Acme' };
|
|
293
|
+
const res = await app.request('/api/data/account', {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
296
|
+
body: JSON.stringify(body),
|
|
297
|
+
});
|
|
298
|
+
expect(res.status).toBe(200);
|
|
299
|
+
expect(mockDispatcher.handleData).toHaveBeenCalledWith(
|
|
300
|
+
'/account',
|
|
301
|
+
'POST',
|
|
302
|
+
body,
|
|
303
|
+
expect.any(Object),
|
|
304
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('PATCH /api/data/account parses JSON body', async () => {
|
|
309
|
+
const body = { name: 'Updated' };
|
|
310
|
+
const res = await app.request('/api/data/account', {
|
|
311
|
+
method: 'PATCH',
|
|
312
|
+
headers: { 'Content-Type': 'application/json' },
|
|
313
|
+
body: JSON.stringify(body),
|
|
314
|
+
});
|
|
315
|
+
expect(res.status).toBe(200);
|
|
316
|
+
expect(mockDispatcher.handleData).toHaveBeenCalledWith(
|
|
317
|
+
'/account',
|
|
318
|
+
'PATCH',
|
|
319
|
+
body,
|
|
320
|
+
expect.any(Object),
|
|
321
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('returns 404 when result is not handled', async () => {
|
|
326
|
+
mockDispatcher.handleData.mockResolvedValueOnce({ handled: false });
|
|
327
|
+
const res = await app.request('/api/data/missing');
|
|
328
|
+
expect(res.status).toBe(404);
|
|
329
|
+
const json = await res.json();
|
|
330
|
+
expect(json.success).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('Storage Endpoint', () => {
|
|
335
|
+
it('GET /api/storage/files calls handleStorage', async () => {
|
|
336
|
+
const res = await app.request('/api/storage/files');
|
|
337
|
+
expect(res.status).toBe(200);
|
|
338
|
+
expect(mockDispatcher.handleStorage).toHaveBeenCalledWith(
|
|
339
|
+
'/files',
|
|
340
|
+
'GET',
|
|
341
|
+
undefined,
|
|
342
|
+
expect.objectContaining({ request: expect.anything() }),
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('Error Handling', () => {
|
|
348
|
+
it('returns 500 with default message on generic error', async () => {
|
|
349
|
+
mockDispatcher.handleData.mockRejectedValueOnce(new Error());
|
|
350
|
+
const res = await app.request('/api/data/account');
|
|
351
|
+
expect(res.status).toBe(500);
|
|
352
|
+
const json = await res.json();
|
|
353
|
+
expect(json.error.message).toBe('Internal Server Error');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('uses custom statusCode from error', async () => {
|
|
357
|
+
mockDispatcher.handleData.mockRejectedValueOnce(
|
|
358
|
+
Object.assign(new Error('Forbidden'), { statusCode: 403 }),
|
|
359
|
+
);
|
|
360
|
+
const res = await app.request('/api/data/account');
|
|
361
|
+
expect(res.status).toBe(403);
|
|
362
|
+
const json = await res.json();
|
|
363
|
+
expect(json.error.message).toBe('Forbidden');
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('toResponse', () => {
|
|
368
|
+
it('handles redirect result', async () => {
|
|
369
|
+
mockDispatcher.handleData.mockResolvedValueOnce({
|
|
370
|
+
handled: true,
|
|
371
|
+
result: { type: 'redirect', url: 'https://example.com' },
|
|
372
|
+
});
|
|
373
|
+
const res = await app.request('/api/data/redir', { redirect: 'manual' });
|
|
374
|
+
expect(res.status).toBe(302);
|
|
375
|
+
expect(res.headers.get('location')).toBe('https://example.com');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('handles generic result objects with 200 status', async () => {
|
|
379
|
+
mockDispatcher.handleData.mockResolvedValueOnce({
|
|
380
|
+
handled: true,
|
|
381
|
+
result: { foo: 'bar' },
|
|
382
|
+
});
|
|
383
|
+
const res = await app.request('/api/data/custom');
|
|
384
|
+
expect(res.status).toBe(200);
|
|
385
|
+
const json = await res.json();
|
|
386
|
+
expect(json.foo).toBe('bar');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('sets custom headers from response', async () => {
|
|
390
|
+
mockDispatcher.handleData.mockResolvedValueOnce({
|
|
391
|
+
handled: true,
|
|
392
|
+
response: { status: 201, body: { id: 1 }, headers: { 'X-Custom': 'yes' } },
|
|
393
|
+
});
|
|
394
|
+
const res = await app.request('/api/data/account', {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
headers: { 'Content-Type': 'application/json' },
|
|
397
|
+
body: JSON.stringify({ name: 'test' }),
|
|
398
|
+
});
|
|
399
|
+
expect(res.status).toBe(201);
|
|
400
|
+
expect(res.headers.get('X-Custom')).toBe('yes');
|
|
401
|
+
const json = await res.json();
|
|
402
|
+
expect(json.id).toBe(1);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';
|
|
4
5
|
|
|
5
6
|
export interface ObjectStackHonoOptions {
|
|
6
7
|
kernel: ObjectKernel;
|
|
7
8
|
prefix?: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Auth service interface with handleRequest method
|
|
13
|
+
*/
|
|
14
|
+
interface AuthService {
|
|
15
|
+
handleRequest(request: Request): Promise<Response>;
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
/**
|
|
11
19
|
* Middleware mode for existing Hono apps
|
|
12
20
|
*/
|
|
@@ -16,3 +24,177 @@ export function objectStackMiddleware(kernel: ObjectKernel) {
|
|
|
16
24
|
await next();
|
|
17
25
|
};
|
|
18
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a full-featured Hono app with all ObjectStack route dispatchers.
|
|
30
|
+
* Provides Auth, GraphQL, Metadata, Data, and Storage routes matching
|
|
31
|
+
* Next.js/NestJS adapter completeness.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { createHonoApp } from '@objectstack/hono';
|
|
36
|
+
* const app = createHonoApp({ kernel });
|
|
37
|
+
* export default app;
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function createHonoApp(options: ObjectStackHonoOptions): Hono {
|
|
41
|
+
const app = new Hono();
|
|
42
|
+
const prefix = options.prefix || '/api';
|
|
43
|
+
const dispatcher = new HttpDispatcher(options.kernel);
|
|
44
|
+
|
|
45
|
+
const errorJson = (c: any, message: string, code: number = 500) => {
|
|
46
|
+
return c.json({ success: false, error: { message, code } }, code);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const toResponse = (c: any, result: HttpDispatcherResult) => {
|
|
50
|
+
if (result.handled) {
|
|
51
|
+
if (result.response) {
|
|
52
|
+
if (result.response.headers) {
|
|
53
|
+
Object.entries(result.response.headers).forEach(([k, v]) => c.header(k, v as string));
|
|
54
|
+
}
|
|
55
|
+
return c.json(result.response.body, result.response.status);
|
|
56
|
+
}
|
|
57
|
+
if (result.result) {
|
|
58
|
+
const res = result.result;
|
|
59
|
+
if (res.type === 'redirect' && res.url) {
|
|
60
|
+
return c.redirect(res.url);
|
|
61
|
+
}
|
|
62
|
+
if (res.type === 'stream' && res.stream) {
|
|
63
|
+
if (res.headers) {
|
|
64
|
+
Object.entries(res.headers).forEach(([k, v]) => c.header(k, v as string));
|
|
65
|
+
}
|
|
66
|
+
return new Response(res.stream, { status: 200 });
|
|
67
|
+
}
|
|
68
|
+
return c.json(res, 200);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return errorJson(c, 'Not Found', 404);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// --- Discovery ---
|
|
75
|
+
app.get(`${prefix}`, (c) => {
|
|
76
|
+
return c.json({ data: dispatcher.getDiscoveryInfo(prefix) });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// --- .well-known ---
|
|
80
|
+
app.get('/.well-known/objectstack', (c) => {
|
|
81
|
+
return c.redirect(prefix);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// --- Auth ---
|
|
85
|
+
app.all(`${prefix}/auth/*`, async (c) => {
|
|
86
|
+
try {
|
|
87
|
+
const path = c.req.path.substring(`${prefix}/auth/`.length);
|
|
88
|
+
const method = c.req.method;
|
|
89
|
+
|
|
90
|
+
// Try AuthPlugin service first (preferred path)
|
|
91
|
+
const authService = typeof options.kernel.getService === 'function'
|
|
92
|
+
? options.kernel.getService<AuthService>('auth')
|
|
93
|
+
: null;
|
|
94
|
+
|
|
95
|
+
if (authService && typeof authService.handleRequest === 'function') {
|
|
96
|
+
const response = await authService.handleRequest(c.req.raw);
|
|
97
|
+
return new Response(response.body, {
|
|
98
|
+
status: response.status,
|
|
99
|
+
headers: response.headers,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Fallback to legacy dispatcher
|
|
104
|
+
const body = method === 'GET' || method === 'HEAD'
|
|
105
|
+
? {}
|
|
106
|
+
: await c.req.json().catch(() => ({}));
|
|
107
|
+
const result = await dispatcher.handleAuth(path, method, body, { request: c.req.raw });
|
|
108
|
+
return toResponse(c, result);
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// --- GraphQL ---
|
|
115
|
+
app.post(`${prefix}/graphql`, async (c) => {
|
|
116
|
+
try {
|
|
117
|
+
const body = await c.req.json();
|
|
118
|
+
const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });
|
|
119
|
+
return c.json(result);
|
|
120
|
+
} catch (err: any) {
|
|
121
|
+
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// --- Metadata ---
|
|
126
|
+
app.all(`${prefix}/meta/*`, async (c) => {
|
|
127
|
+
try {
|
|
128
|
+
const subPath = c.req.path.substring(`${prefix}/meta`.length);
|
|
129
|
+
const method = c.req.method;
|
|
130
|
+
|
|
131
|
+
let body: any = undefined;
|
|
132
|
+
if (method === 'PUT' || method === 'POST') {
|
|
133
|
+
body = await c.req.json().catch(() => ({}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = await dispatcher.handleMetadata(subPath, { request: c.req.raw }, method, body);
|
|
137
|
+
return toResponse(c, result);
|
|
138
|
+
} catch (err: any) {
|
|
139
|
+
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Also handle /meta with no trailing path
|
|
144
|
+
app.all(`${prefix}/meta`, async (c) => {
|
|
145
|
+
try {
|
|
146
|
+
const method = c.req.method;
|
|
147
|
+
let body: any = undefined;
|
|
148
|
+
if (method === 'PUT' || method === 'POST') {
|
|
149
|
+
body = await c.req.json().catch(() => ({}));
|
|
150
|
+
}
|
|
151
|
+
const result = await dispatcher.handleMetadata('', { request: c.req.raw }, method, body);
|
|
152
|
+
return toResponse(c, result);
|
|
153
|
+
} catch (err: any) {
|
|
154
|
+
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- Data ---
|
|
159
|
+
app.all(`${prefix}/data/*`, async (c) => {
|
|
160
|
+
try {
|
|
161
|
+
const subPath = c.req.path.substring(`${prefix}/data`.length);
|
|
162
|
+
const method = c.req.method;
|
|
163
|
+
|
|
164
|
+
let body: any = {};
|
|
165
|
+
if (method === 'POST' || method === 'PATCH') {
|
|
166
|
+
body = await c.req.json().catch(() => ({}));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const queryParams: Record<string, any> = {};
|
|
170
|
+
const url = new URL(c.req.url);
|
|
171
|
+
url.searchParams.forEach((val, key) => { queryParams[key] = val; });
|
|
172
|
+
|
|
173
|
+
const result = await dispatcher.handleData(subPath, method, body, queryParams, { request: c.req.raw });
|
|
174
|
+
return toResponse(c, result);
|
|
175
|
+
} catch (err: any) {
|
|
176
|
+
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// --- Storage ---
|
|
181
|
+
app.all(`${prefix}/storage/*`, async (c) => {
|
|
182
|
+
try {
|
|
183
|
+
const subPath = c.req.path.substring(`${prefix}/storage`.length);
|
|
184
|
+
const method = c.req.method;
|
|
185
|
+
|
|
186
|
+
let file: any = undefined;
|
|
187
|
+
if (method === 'POST' && subPath === '/upload') {
|
|
188
|
+
const formData = await c.req.formData();
|
|
189
|
+
file = formData.get('file');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const result = await dispatcher.handleStorage(subPath, method, file, { request: c.req.raw });
|
|
193
|
+
return toResponse(c, result);
|
|
194
|
+
} catch (err: any) {
|
|
195
|
+
return errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return app;
|
|
200
|
+
}
|