Skip to content

Commit e013a25

Browse files
committed
feat(adapters): add Hono, NestJS, and Next.js adapters with documentation and configuration
1 parent 6d6de01 commit e013a25

14 files changed

Lines changed: 1265 additions & 0 deletions

File tree

packages/adapters/hono/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# @objectstack/hono
2+
3+
The official Hono adapter for ObjectStack.
4+
5+
## Features
6+
- Lightweight & Fast
7+
- Edge Compatible (Cloudflare Workers, Deno, Bun)
8+
- Built-in CORS
9+
10+
## Usage
11+
12+
```typescript
13+
import { createHonoApp } from '@objectstack/hono';
14+
import { kernel } from './my-kernel';
15+
16+
const app = createHonoApp({ kernel });
17+
18+
export default app;
19+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "@objectstack/hono",
3+
"version": "0.1.0",
4+
"main": "dist/index.js",
5+
"types": "dist/index.d.ts",
6+
"scripts": {
7+
"build": "tsc"
8+
},
9+
"dependencies": {
10+
"@objectstack/runtime": "workspace:*",
11+
"hono": "^4.0.0"
12+
}
13+
}
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { Hono } from 'hono';
2+
import { cors } from 'hono/cors';
3+
import { type ObjectKernel } from '@objectstack/runtime';
4+
5+
export interface ObjectStackHonoOptions {
6+
kernel: ObjectKernel;
7+
prefix?: string;
8+
}
9+
10+
/**
11+
* Creates a Hono application tailored for ObjectStack
12+
* Fully compliant with @objectstack/spec
13+
*/
14+
export function createHonoApp(options: ObjectStackHonoOptions) {
15+
const app = new Hono();
16+
const { prefix = '/api' } = options;
17+
const kernel = options.kernel as any;
18+
19+
app.use('*', cors());
20+
21+
// --- Helper for Success Response ---
22+
const success = (data: any, meta?: any) => ({
23+
success: true,
24+
data,
25+
meta,
26+
});
27+
28+
// --- Helper for Error Response ---
29+
const errorHandler = async (c: any, fn: () => Promise<any>) => {
30+
try {
31+
return await fn();
32+
} catch (err: any) {
33+
return c.json({
34+
success: false,
35+
error: {
36+
code: err.statusCode || 500,
37+
message: err.message || 'Internal Server Error',
38+
details: err.details,
39+
},
40+
}, err.statusCode || 500);
41+
}
42+
};
43+
44+
// --- 0. Discovery Endpoint ---
45+
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+
});
71+
});
72+
73+
// --- 1. Auth (Generic Auth Handler) ---
74+
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+
});
88+
}
89+
90+
return c.json({ success: false, error: { message: 'Auth provider not configured', code: 404 } }, 404);
91+
});
92+
93+
// --- 2. GraphQL ---
94+
app.post(`${prefix}/graphql`, async (c) => {
95+
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+
});
100+
return c.json(result);
101+
});
102+
});
103+
104+
// --- 2. Metadata Endpoints ---
105+
106+
// List All Objects
107+
app.get(`${prefix}/metadata`, async (c) => {
108+
return errorHandler(c, async () => {
109+
const data = await kernel.broker.call('metadata.objects', {}, { request: c.req.raw });
110+
return c.json(success(data));
111+
});
112+
});
113+
114+
// Get Object Metadata
115+
app.get(`${prefix}/metadata/:objectName`, async (c) => {
116+
return errorHandler(c, async () => {
117+
const { objectName } = c.req.param();
118+
const data = await kernel.broker.call('metadata.getObject', { objectName }, { request: c.req.raw });
119+
return c.json(success(data));
120+
});
121+
});
122+
123+
// --- 3. Data Endpoints ---
124+
125+
// List Records (Standard REST)
126+
app.get(`${prefix}/data/:objectName`, async (c) => {
127+
return errorHandler(c, async () => {
128+
const { objectName } = c.req.param();
129+
const query = c.req.query();
130+
// Basic support: pass query params as filter
131+
// In a real implementation, we might parse OData or JSON filters from query params
132+
const result = await kernel.broker.call('data.query', { object: objectName, filters: query }, { request: c.req.raw });
133+
return c.json(success(result.data, { count: result.count }));
134+
});
135+
});
136+
137+
// Query Records (POST with JSON body)
138+
app.post(`${prefix}/data/:objectName/query`, async (c) => {
139+
return errorHandler(c, async () => {
140+
const { objectName } = c.req.param();
141+
const body = await c.req.json();
142+
const result = await kernel.broker.call('data.query', { object: objectName, ...body }, { request: c.req.raw });
143+
return c.json(success(result.data, { count: result.count, limit: body.limit, skip: body.skip }));
144+
});
145+
});
146+
147+
// Get Single Record
148+
app.get(`${prefix}/data/:objectName/:id`, async (c) => {
149+
return errorHandler(c, async () => {
150+
const { objectName, id } = c.req.param();
151+
const query = c.req.query();
152+
const data = await kernel.broker.call('data.get', { object: objectName, id, ...query }, { request: c.req.raw });
153+
return c.json(success(data));
154+
});
155+
});
156+
157+
// Create Record
158+
app.post(`${prefix}/data/:objectName`, async (c) => {
159+
return errorHandler(c, async () => {
160+
const { objectName } = c.req.param();
161+
const body = await c.req.json();
162+
const data = await kernel.broker.call('data.create', { object: objectName, data: body }, { request: c.req.raw });
163+
return c.json(success(data), 201);
164+
});
165+
});
166+
167+
// Update Record
168+
app.patch(`${prefix}/data/:objectName/:id`, async (c) => {
169+
return errorHandler(c, async () => {
170+
const { objectName, id } = c.req.param();
171+
const body = await c.req.json();
172+
const data = await kernel.broker.call('data.update', { object: objectName, id, data: body }, { request: c.req.raw });
173+
return c.json(success(data));
174+
});
175+
});
176+
177+
// Delete Record
178+
app.delete(`${prefix}/data/:objectName/:id`, async (c) => {
179+
return errorHandler(c, async () => {
180+
const { objectName, id } = c.req.param();
181+
await kernel.broker.call('data.delete', { object: objectName, id }, { request: c.req.raw });
182+
return c.json(success({ id, deleted: true }));
183+
});
184+
});
185+
186+
// Batch Operations
187+
app.post(`${prefix}/data/:objectName/batch`, async (c) => {
188+
return errorHandler(c, async () => {
189+
const { objectName } = c.req.param();
190+
const { operations } = await c.req.json();
191+
const data = await kernel.broker.call('data.batch', { object: objectName, operations }, { request: c.req.raw });
192+
return c.json(success(data));
193+
});
194+
});
195+
196+
// --- 4. Storage & Files ---
197+
198+
// Upload File
199+
app.post(`${prefix}/storage/upload`, async (c) => {
200+
return errorHandler(c, async () => {
201+
const storageService = (kernel as any).getService?.('file-storage') || (kernel as any).services?.['file-storage'];
202+
if (!storageService) throw { statusCode: 501, message: 'File storage not configured' };
203+
204+
const body = await c.req.parseBody();
205+
const file = body['file'];
206+
207+
if (!file) throw { statusCode: 400, message: 'No file provided' };
208+
209+
// Allow service to handle raw file object or buffer
210+
const result = await storageService.upload(file, { request: c.req.raw });
211+
return c.json(success(result));
212+
});
213+
});
214+
215+
// Get File
216+
app.get(`${prefix}/storage/file/:id`, async (c) => {
217+
return errorHandler(c, async () => {
218+
const storageService = (kernel as any).getService?.('file-storage') || (kernel as any).services?.['file-storage'];
219+
if (!storageService) throw { statusCode: 501, message: 'File storage not configured' };
220+
221+
const { id } = c.req.param();
222+
const result = await storageService.download(id, { request: c.req.raw });
223+
224+
// If result is a stream or blob, return it.
225+
// If result is a URL (e.g. S3 signed url), redirect.
226+
if (result.url && result.redirect) {
227+
return c.redirect(result.url);
228+
}
229+
if (result.stream) {
230+
return c.body(result.stream, 200, {
231+
'Content-Type': result.mimeType || 'application/octet-stream',
232+
'Content-Length': result.size,
233+
});
234+
}
235+
return c.json(success(result));
236+
});
237+
});
238+
239+
return app;
240+
}
241+
242+
/**
243+
* Middleware mode for existing Hono apps
244+
*/
245+
export function objectStackMiddleware(kernel: ObjectKernel) {
246+
return async (c: any, next: any) => {
247+
c.set('objectStack', kernel);
248+
await next();
249+
};
250+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"outDir": "./dist",
5+
"rootDir": "./src",
6+
"module": "NodeNext",
7+
"moduleResolution": "NodeNext",
8+
"esModuleInterop": true,
9+
"skipLibCheck": true
10+
},
11+
"include": ["src/**/*"]
12+
}

packages/adapters/nestjs/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# @objectstack/nestjs
2+
3+
The official NestJS integration for ObjectStack.
4+
5+
## Features
6+
- `ObjectStackModule`: Global module for DI.
7+
- `ObjectStackService`: Injectable wrapper for the Kernel.
8+
- Compatible with all NestJS features (Guards, Interceptors, Pipes).
9+
10+
## Usage
11+
12+
```typescript
13+
import { Module } from '@nestjs/common';
14+
import { ObjectStackModule } from '@objectstack/nestjs';
15+
import { kernel } from './kernel';
16+
17+
@Module({
18+
imports: [
19+
ObjectStackModule.forRoot(kernel)
20+
]
21+
})
22+
export class AppModule {}
23+
```
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "@objectstack/nestjs",
3+
"version": "0.1.0",
4+
"main": "dist/index.js",
5+
"types": "dist/index.d.ts",
6+
"scripts": {
7+
"build": "tsc"
8+
},
9+
"dependencies": {
10+
"@nestjs/common": "^10.0.0",
11+
"@nestjs/core": "^10.0.0",
12+
"@objectstack/runtime": "workspace:*"
13+
}
14+
}

0 commit comments

Comments
 (0)