Skip to content

Commit 40ddf55

Browse files
committed
feat(adapters): integrate HttpDispatcher for enhanced request handling in Hono and NestJS adapters
1 parent 4ca5dfa commit 40ddf55

5 files changed

Lines changed: 217 additions & 161 deletions

File tree

packages/adapters/hono/src/index.ts

Lines changed: 23 additions & 45 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 } from '@objectstack/runtime';
3+
import { type ObjectKernel, HttpDispatcher } from '@objectstack/runtime';
44

55
export interface ObjectStackHonoOptions {
66
kernel: ObjectKernel;
@@ -15,6 +15,7 @@ export function createHonoApp(options: ObjectStackHonoOptions) {
1515
const app = new Hono();
1616
const { prefix = '/api' } = options;
1717
const kernel = options.kernel as any;
18+
const dispatcher = new HttpDispatcher(options.kernel);
1819

1920
app.use('*', cors());
2021

@@ -43,60 +44,37 @@ export function createHonoApp(options: ObjectStackHonoOptions) {
4344

4445
// --- 0. Discovery Endpoint ---
4546
app.get(prefix, (c) => {
46-
// Check capabilities based on registered services
47-
const services = (kernel as any).services || {};
48-
const hasGraphQL = !!(services['graphql'] || (kernel as any).graphql); // Kernel often has direct graphql support
49-
const hasSearch = !!services['search'];
50-
const hasWebSockets = !!services['realtime'];
51-
const hasFiles = !!(services['file-storage'] || services['storage']?.supportsFiles);
52-
53-
return c.json({
54-
name: 'ObjectOS',
55-
version: '1.0.0',
56-
environment: process.env.NODE_ENV || 'development',
57-
routes: {
58-
data: `${prefix}/data`,
59-
metadata: `${prefix}/metadata`,
60-
auth: `${prefix}/auth`,
61-
graphql: hasGraphQL ? `${prefix}/graphql` : undefined,
62-
storage: hasFiles ? `${prefix}/storage` : undefined,
63-
},
64-
features: {
65-
graphql: hasGraphQL,
66-
search: hasSearch,
67-
websockets: hasWebSockets,
68-
files: hasFiles,
69-
},
70-
});
47+
return c.json(dispatcher.getDiscoveryInfo(prefix));
7148
});
7249

7350
// --- 1. Auth (Generic Auth Handler) ---
7451
app.all(`${prefix}/auth/*`, async (c) => {
75-
// 1. Try to use generic Auth Service if available
76-
const authService = (kernel as any).getService?.('auth') || (kernel as any).services?.['auth'];
77-
if (authService && authService.handler) {
78-
return authService.handler(c.req.raw);
79-
}
80-
81-
// 2. Fallback to Legacy Auth Spec (only for login)
82-
if (c.req.path.endsWith('/login') && c.req.method === 'POST') {
83-
return errorHandler(c, async () => {
84-
const body = await c.req.json();
85-
const data = await kernel.broker.call('auth.login', body, { request: c.req.raw });
86-
return c.json(data);
87-
});
52+
// subpath from /api/auth/login -> login
53+
const path = c.req.path.substring(c.req.path.indexOf('/auth/') + 6);
54+
55+
try {
56+
const result = await dispatcher.handleAuth(path, c.req.method, await c.req.parseBody().catch(() => ({})), { request: c.req.raw });
57+
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);
68+
} catch (err: any) {
69+
return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500);
8870
}
89-
90-
return c.json({ success: false, error: { message: 'Auth provider not configured', code: 404 } }, 404);
9171
});
9272

9373
// --- 2. GraphQL ---
9474
app.post(`${prefix}/graphql`, async (c) => {
9575
return errorHandler(c, async () => {
96-
const { query, variables } = await c.req.json();
97-
const result = await kernel.graphql(query, variables, {
98-
request: c.req.raw,
99-
});
76+
const body = await c.req.json();
77+
const result = await dispatcher.handleGraphQL(body, { request: c.req.raw });
10078
return c.json(result);
10179
});
10280
});

packages/adapters/nestjs/src/index.ts

Lines changed: 51 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,22 @@ export const ConnectReq = createParamDecorator(
1414

1515
@Injectable()
1616
export class ObjectStackService {
17-
constructor(@Inject(OBJECT_KERNEL) private readonly kernel: any) {}
17+
public dispatcher: HttpDispatcher;
1818

19-
getKernel() {
20-
return this.kernel;
19+
constructor(@Inject(OBJECT_KERNEL) private readonly kernel: ObjectKernel) {
20+
this.dispatcher = new HttpDispatcher(kernel);
2121
}
2222

23-
async executeGraphQL(query: string, variables: any, request: any) {
24-
return this.kernel.graphql(query, variables, { request });
23+
getKernel() {
24+
return this.kernel;
2525
}
2626

2727
async call(action: string, params: any, request: any) {
28-
return this.kernel.broker.call(action, params, { request });
28+
const k = this.kernel as any;
29+
if (k.broker) {
30+
return k.broker.call(action, params, { request });
31+
}
32+
throw new Error('Kernel Broker not available');
2933
}
3034
}
3135

@@ -42,66 +46,57 @@ export class ObjectStackController {
4246
// --- Discovery Endpoint ---
4347
@Get()
4448
discovery() {
45-
const prefix = '/api'; // NestJS controller is mapped to 'api'
46-
const kernel = this.service.getKernel() as any;
47-
const services = kernel.services || {};
48-
const hasGraphQL = !!(services['graphql'] || kernel.graphql);
49-
// NestJS adapter doesn't expose services map directly but we have access to kernel
50-
// We can try to guess or use safe default
51-
52-
return {
53-
name: 'ObjectOS',
54-
version: '1.0.0',
55-
environment: process.env.NODE_ENV || 'development',
56-
routes: {
57-
data: `${prefix}/data`,
58-
metadata: `${prefix}/metadata`,
59-
auth: `${prefix}/auth`,
60-
graphql: hasGraphQL ? `${prefix}/graphql` : undefined,
61-
storage: `${prefix}/storage`, // NestJS implementation below
62-
},
63-
features: {
64-
graphql: hasGraphQL,
65-
search: false,
66-
websockets: false, // NestJS Gateway is separate usually
67-
files: true, // Implemented below
68-
},
69-
};
49+
return this.service.dispatcher.getDiscoveryInfo('/api');
7050
}
7151

7252
@Post('graphql')
7353
async graphql(@Body() body: any, @Req() req: any) {
74-
const { query, variables } = body;
75-
return this.service.executeGraphQL(query, variables, req);
54+
return this.service.dispatcher.handleGraphQL(body, { request: req });
7655
}
7756

7857
// Auth (Generic Auth Handler)
7958
@All('auth/*')
8059
async auth(@Req() req: any, @Res() res: any, @Body() body: any) {
81-
const kernel = this.service.getKernel() as any;
82-
const authService = kernel.getService?.('auth') || kernel.services?.['auth'];
83-
84-
if (authService && authService.handler) {
85-
const response = await authService.handler(req, res);
86-
87-
if (response instanceof Response) {
88-
res.status(response.status);
89-
response.headers.forEach((v, k) => res.setHeader(k, v));
90-
const text = await response.text();
91-
res.send(text);
92-
return;
93-
}
94-
95-
return response;
96-
}
97-
98-
// Fallback: Legacy login
99-
if (req.path.endsWith('/login') && req.method === 'POST') {
100-
const result = await this.service.call('auth.login', body, req);
101-
return res.json(result);
60+
try {
61+
const path = req.params[0] || req.url.split('/auth/')[1]?.split('?')[0] || '';
62+
63+
const result = await this.service.dispatcher.handleAuth(path, req.method, body, { request: req, response: res });
64+
65+
if (result.handled) {
66+
if (result.response) {
67+
res.status(result.response.status);
68+
if (result.response.headers) {
69+
Object.entries(result.response.headers).forEach(([k, v]) => res.setHeader(k, v));
70+
}
71+
return res.json(result.response.body);
72+
}
73+
if (result.result) {
74+
const response = result.result;
75+
// If response is a standard Response object
76+
if (response && typeof response.status === 'number' && typeof response.text === 'function') {
77+
res.status(response.status);
78+
if (response.headers && typeof response.headers.forEach === 'function') {
79+
response.headers.forEach((v: string, k: string) => res.setHeader(k, v));
80+
}
81+
const text = await response.text();
82+
res.send(text);
83+
return;
84+
}
85+
return res.status(200).json(response);
86+
}
87+
}
88+
89+
return res.status(404).json({ success: false, error: { message: 'Auth provider not configured', code: 404 } });
90+
91+
} catch (err: any) {
92+
return res.status(err.statusCode || 500).json({
93+
success: false,
94+
error: {
95+
message: err.message || 'Internal Server Error',
96+
code: err.statusCode || 500
97+
}
98+
});
10299
}
103-
104-
return res.status(404).json({ success: false, error: { message: 'Auth provider not configured', code: 404 } });
105100
}
106101

107102
// Metadata

packages/adapters/nextjs/src/index.ts

Lines changed: 21 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextRequest, NextResponse } from 'next/server';
2-
import { type ObjectKernel } from '@objectstack/runtime';
2+
import { type ObjectKernel, HttpDispatcher } from '@objectstack/runtime';
33

44
export interface NextAdapterOptions {
55
kernel: ObjectKernel;
@@ -12,85 +12,46 @@ export interface NextAdapterOptions {
1212
*/
1313
export function createRouteHandler(options: NextAdapterOptions) {
1414
const kernel = options.kernel as any;
15+
const dispatcher = new HttpDispatcher(options.kernel);
1516

1617
const success = (data: any, meta?: any) => NextResponse.json({ success: true, data, meta });
1718
const error = (msg: string, code: number = 500) => NextResponse.json({ success: false, error: { message: msg, code } }, { status: code });
1819

1920
return async function handler(req: NextRequest, { params }: { params: { objectstack: string[] } }) {
20-
const segments = params.objectstack || [];
21+
const resolvedParams = await Promise.resolve(params);
22+
const segments = resolvedParams.objectstack || [];
2123
const method = req.method;
2224

23-
// Parse path: /api/...segments...
24-
// e.g., /api/graphql
25-
// /api/metadata
26-
// /api/data/contacts/query
27-
2825
// --- 0. Discovery Endpoint ---
2926
if (segments.length === 0 && method === 'GET') {
30-
const prefix = options.prefix || '/api';
31-
const services = (kernel as any).services || {};
32-
const hasGraphQL = !!(services['graphql'] || (kernel as any).graphql);
33-
const hasSearch = !!services['search'];
34-
const hasWebSockets = !!services['realtime'];
35-
const hasFiles = !!(services['file-storage'] || services['storage']?.supportsFiles);
36-
37-
return NextResponse.json({
38-
name: 'ObjectOS',
39-
version: '1.0.0',
40-
environment: process.env.NODE_ENV || 'development',
41-
routes: {
42-
data: `${prefix}/data`,
43-
metadata: `${prefix}/metadata`,
44-
auth: `${prefix}/auth`,
45-
graphql: hasGraphQL ? `${prefix}/graphql` : undefined,
46-
storage: hasFiles ? `${prefix}/storage` : undefined,
47-
},
48-
features: {
49-
graphql: hasGraphQL,
50-
search: hasSearch,
51-
websockets: hasWebSockets,
52-
files: hasFiles,
53-
},
54-
});
27+
return NextResponse.json(dispatcher.getDiscoveryInfo(options.prefix || '/api'));
5528
}
5629

5730
// --- 1. Auth (Generic Auth Handler) ---
5831
if (segments[0] === 'auth') {
59-
const authService = (kernel as any).getService?.('auth') || (kernel as any).services?.['auth'];
60-
if (authService && authService.handler) {
61-
return authService.handler(req);
62-
}
63-
64-
// Fallback for /api/auth/login
65-
if (segments[1] === 'login' && method === 'POST') {
66-
try {
67-
// Clone request body because it might be consumed by handler above? No, we checked availability.
68-
const body = await req.json();
69-
const data = await kernel.broker.call('auth.login', body, { request: req });
70-
return NextResponse.json(data);
71-
} catch (e: any) {
72-
return error(e.message, e.statusCode || 500);
73-
}
32+
const subPath = segments.slice(1).join('/');
33+
try {
34+
const body = method === 'POST' ? await req.json().catch(() => ({})) : {};
35+
const result = await dispatcher.handleAuth(subPath, method, body, { request: req });
36+
if (result.handled) {
37+
if (result.response) {
38+
return NextResponse.json(result.response.body, { status: result.response.status });
39+
}
40+
if (result.result) return result.result;
41+
}
42+
return error('Auth provider not configured', 404);
43+
} catch (e: any) {
44+
return error(e.message, e.statusCode || 500);
7445
}
75-
return error('Auth provider not configured', 404);
7646
}
7747

7848
try {
7949
const rawRequest = req;
8050

81-
// 0. Auth
82-
if (segments[0] === 'auth') {
83-
if (segments[1] === 'login' && method === 'POST') {
84-
const body = await req.json() as any;
85-
const data = await kernel.broker.call('auth.login', body, { request: rawRequest });
86-
return NextResponse.json(data);
87-
}
88-
}
89-
9051
// 1. GraphQL
9152
if (segments[0] === 'graphql' && method === 'POST') {
92-
const body = await req.json() as any;
93-
const result = await kernel.graphql(body.query, body.variables, { request: rawRequest });
53+
const body = await req.json();
54+
const result = await dispatcher.handleGraphQL(body, { request: rawRequest });
9455
return NextResponse.json(result);
9556
}
9657

@@ -142,7 +103,7 @@ export function createRouteHandler(options: NextAdapterOptions) {
142103
if (segments.length === 2 && method === 'POST') {
143104
const body = await req.json();
144105
const data = await kernel.broker.call('data.create', { object: objectName, data: body }, { request: rawRequest });
145-
return success(data); // 201 not easy with helper, but ok
106+
return success(data);
146107
}
147108

148109
// GET /data/:objectName/:id

0 commit comments

Comments
 (0)