Skip to content

Commit ef78825

Browse files
committed
feat(adapters): enhance Hono, NestJS, and Next.js adapters with improved response handling and error management
1 parent 40ddf55 commit ef78825

4 files changed

Lines changed: 415 additions & 461 deletions

File tree

packages/adapters/hono/src/index.ts

Lines changed: 73 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Hono } from 'hono';
22
import { cors } from 'hono/cors';
3-
import { type ObjectKernel, HttpDispatcher } from '@objectstack/runtime';
3+
import { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';
44

55
export interface ObjectStackHonoOptions {
66
kernel: ObjectKernel;
@@ -14,204 +14,111 @@ export interface ObjectStackHonoOptions {
1414
export function createHonoApp(options: ObjectStackHonoOptions) {
1515
const app = new Hono();
1616
const { prefix = '/api' } = options;
17-
const kernel = options.kernel as any;
1817
const dispatcher = new HttpDispatcher(options.kernel);
1918

2019
app.use('*', cors());
2120

22-
// --- Helper for Success Response ---
23-
const success = (data: any, meta?: any) => ({
24-
success: true,
25-
data,
26-
meta,
27-
});
28-
29-
// --- Helper for Error Response ---
30-
const errorHandler = async (c: any, fn: () => Promise<any>) => {
31-
try {
32-
return await fn();
33-
} catch (err: any) {
34-
return c.json({
35-
success: false,
36-
error: {
37-
code: err.statusCode || 500,
38-
message: err.message || 'Internal Server Error',
39-
details: err.details,
40-
},
41-
}, err.statusCode || 500);
42-
}
43-
};
21+
// --- Helper for Response Normalization ---
22+
const normalizeResponse = (c: any, result: HttpDispatcherResult) => {
23+
if (result.handled) {
24+
if (result.response) {
25+
return c.json(result.response.body, result.response.status as any, result.response.headers);
26+
}
27+
if (result.result) {
28+
const res = result.result;
29+
// Redirect
30+
if (res.type === 'redirect' && res.url) {
31+
return c.redirect(res.url);
32+
}
33+
// Stream
34+
if (res.type === 'stream' && res.stream) {
35+
return c.body(res.stream, 200, res.headers);
36+
}
37+
38+
// Hono handles standard Response objects
39+
return res;
40+
}
41+
}
42+
return c.json({ success: false, error: { message: 'Not Found', code: 404 } }, 404);
43+
}
4444

4545
// --- 0. Discovery Endpoint ---
4646
app.get(prefix, (c) => {
4747
return c.json(dispatcher.getDiscoveryInfo(prefix));
4848
});
4949

50-
// --- 1. Auth (Generic Auth Handler) ---
50+
// --- 1. Auth ---
5151
app.all(`${prefix}/auth/*`, async (c) => {
52-
// subpath from /api/auth/login -> login
53-
const path = c.req.path.substring(c.req.path.indexOf('/auth/') + 6);
54-
5552
try {
56-
const result = await dispatcher.handleAuth(path, c.req.method, await c.req.parseBody().catch(() => ({})), { request: c.req.raw });
53+
// subpath from /api/auth/login -> login
54+
const path = c.req.path.substring(c.req.path.indexOf('/auth/') + 6);
55+
const body = await c.req.parseBody().catch(() => ({}));
5756

58-
if (result.handled) {
59-
if (result.response) {
60-
return c.json(result.response.body, result.response.status as any, result.response.headers);
61-
}
62-
if (result.result) {
63-
// If handler returns a response object
64-
return result.result;
65-
}
66-
}
67-
return c.json({ success: false, error: { message: 'Auth provider not configured', code: 404 } }, 404);
57+
const result = await dispatcher.handleAuth(path, c.req.method, body, { request: c.req.raw });
58+
return normalizeResponse(c, result);
6859
} catch (err: any) {
6960
return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500);
7061
}
7162
});
7263

7364
// --- 2. GraphQL ---
7465
app.post(`${prefix}/graphql`, async (c) => {
75-
return errorHandler(c, async () => {
66+
try {
7667
const body = await c.req.json();
7768
const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });
7869
return c.json(result);
79-
});
80-
});
81-
82-
// --- 2. Metadata Endpoints ---
83-
84-
// List All Objects
85-
app.get(`${prefix}/metadata`, async (c) => {
86-
return errorHandler(c, async () => {
87-
const data = await kernel.broker.call('metadata.objects', {}, { request: c.req.raw });
88-
return c.json(success(data));
89-
});
70+
} catch (err: any) {
71+
return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500);
72+
}
9073
});
9174

92-
// Get Object Metadata
93-
app.get(`${prefix}/metadata/:objectName`, async (c) => {
94-
return errorHandler(c, async () => {
95-
const { objectName } = c.req.param();
96-
const data = await kernel.broker.call('metadata.getObject', { objectName }, { request: c.req.raw });
97-
return c.json(success(data));
98-
});
75+
// --- 3. Metadata Endpoints ---
76+
app.all(`${prefix}/metadata*`, async (c) => {
77+
try {
78+
const path = c.req.path.substring(c.req.path.indexOf('/metadata') + 9);
79+
const result = await dispatcher.handleMetadata(path, { request: c.req.raw });
80+
return normalizeResponse(c, result);
81+
} catch (err: any) {
82+
return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500);
83+
}
9984
});
10085

101-
// --- 3. Data Endpoints ---
102-
103-
// List Records (Standard REST)
104-
app.get(`${prefix}/data/:objectName`, async (c) => {
105-
return errorHandler(c, async () => {
106-
const { objectName } = c.req.param();
86+
// --- 4. Data Endpoints ---
87+
app.all(`${prefix}/data*`, async (c) => {
88+
try {
89+
const path = c.req.path.substring(c.req.path.indexOf('/data') + 5);
90+
const method = c.req.method;
91+
92+
let body = {};
93+
if (method === 'POST' || method === 'PATCH') {
94+
body = await c.req.json().catch(() => ({}));
95+
}
10796
const query = c.req.query();
108-
// Basic support: pass query params as filter
109-
// In a real implementation, we might parse OData or JSON filters from query params
110-
const result = await kernel.broker.call('data.query', { object: objectName, filters: query }, { request: c.req.raw });
111-
return c.json(success(result.data, { count: result.count }));
112-
});
113-
});
11497

115-
// Query Records (POST with JSON body)
116-
app.post(`${prefix}/data/:objectName/query`, async (c) => {
117-
return errorHandler(c, async () => {
118-
const { objectName } = c.req.param();
119-
const body = await c.req.json();
120-
const result = await kernel.broker.call('data.query', { object: objectName, ...body }, { request: c.req.raw });
121-
return c.json(success(result.data, { count: result.count, limit: body.limit, skip: body.skip }));
122-
});
123-
});
124-
125-
// Get Single Record
126-
app.get(`${prefix}/data/:objectName/:id`, async (c) => {
127-
return errorHandler(c, async () => {
128-
const { objectName, id } = c.req.param();
129-
const query = c.req.query();
130-
const data = await kernel.broker.call('data.get', { object: objectName, id, ...query }, { request: c.req.raw });
131-
return c.json(success(data));
132-
});
133-
});
134-
135-
// Create Record
136-
app.post(`${prefix}/data/:objectName`, async (c) => {
137-
return errorHandler(c, async () => {
138-
const { objectName } = c.req.param();
139-
const body = await c.req.json();
140-
const data = await kernel.broker.call('data.create', { object: objectName, data: body }, { request: c.req.raw });
141-
return c.json(success(data), 201);
142-
});
143-
});
144-
145-
// Update Record
146-
app.patch(`${prefix}/data/:objectName/:id`, async (c) => {
147-
return errorHandler(c, async () => {
148-
const { objectName, id } = c.req.param();
149-
const body = await c.req.json();
150-
const data = await kernel.broker.call('data.update', { object: objectName, id, data: body }, { request: c.req.raw });
151-
return c.json(success(data));
152-
});
153-
});
154-
155-
// Delete Record
156-
app.delete(`${prefix}/data/:objectName/:id`, async (c) => {
157-
return errorHandler(c, async () => {
158-
const { objectName, id } = c.req.param();
159-
await kernel.broker.call('data.delete', { object: objectName, id }, { request: c.req.raw });
160-
return c.json(success({ id, deleted: true }));
161-
});
162-
});
163-
164-
// Batch Operations
165-
app.post(`${prefix}/data/:objectName/batch`, async (c) => {
166-
return errorHandler(c, async () => {
167-
const { objectName } = c.req.param();
168-
const { operations } = await c.req.json();
169-
const data = await kernel.broker.call('data.batch', { object: objectName, operations }, { request: c.req.raw });
170-
return c.json(success(data));
171-
});
98+
const result = await dispatcher.handleData(path, method, body, query, { request: c.req.raw });
99+
return normalizeResponse(c, result);
100+
} catch (err: any) {
101+
return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500);
102+
}
172103
});
173104

174-
// --- 4. Storage & Files ---
175-
176-
// Upload File
177-
app.post(`${prefix}/storage/upload`, async (c) => {
178-
return errorHandler(c, async () => {
179-
const storageService = (kernel as any).getService?.('file-storage') || (kernel as any).services?.['file-storage'];
180-
if (!storageService) throw { statusCode: 501, message: 'File storage not configured' };
181-
182-
const body = await c.req.parseBody();
183-
const file = body['file'];
184-
185-
if (!file) throw { statusCode: 400, message: 'No file provided' };
105+
// --- 5. Storage Endpoints ---
106+
app.all(`${prefix}/storage*`, async (c) => {
107+
try {
108+
const path = c.req.path.substring(c.req.path.indexOf('/storage') + 8);
109+
const method = c.req.method;
186110

187-
// Allow service to handle raw file object or buffer
188-
const result = await storageService.upload(file, { request: c.req.raw });
189-
return c.json(success(result));
190-
});
191-
});
192-
193-
// Get File
194-
app.get(`${prefix}/storage/file/:id`, async (c) => {
195-
return errorHandler(c, async () => {
196-
const storageService = (kernel as any).getService?.('file-storage') || (kernel as any).services?.['file-storage'];
197-
if (!storageService) throw { statusCode: 501, message: 'File storage not configured' };
111+
let file: any = undefined;
112+
if (method === 'POST' && path.includes('upload')) {
113+
const body = await c.req.parseBody();
114+
file = body['file'];
115+
}
198116

199-
const { id } = c.req.param();
200-
const result = await storageService.download(id, { request: c.req.raw });
201-
202-
// If result is a stream or blob, return it.
203-
// If result is a URL (e.g. S3 signed url), redirect.
204-
if (result.url && result.redirect) {
205-
return c.redirect(result.url);
206-
}
207-
if (result.stream) {
208-
return c.body(result.stream, 200, {
209-
'Content-Type': result.mimeType || 'application/octet-stream',
210-
'Content-Length': result.size,
211-
});
212-
}
213-
return c.json(success(result));
214-
});
117+
const result = await dispatcher.handleStorage(path, method, file, { request: c.req.raw });
118+
return normalizeResponse(c, result);
119+
} catch (err: any) {
120+
return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500);
121+
}
215122
});
216123

217124
return app;

0 commit comments

Comments
 (0)