@objectstack/hono 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/hono@2.0.1 build /home/runner/work/spec/spec/packages/adapters/hono
2
+ > @objectstack/hono@2.0.2 build /home/runner/work/spec/spec/packages/adapters/hono
3
3
  > tsup --config ../../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- CJS dist/index.js 6.92 KB
14
- CJS dist/index.js.map 12.80 KB
15
- CJS ⚡️ Build success in 36ms
16
13
  ESM dist/index.mjs 5.82 KB
17
14
  ESM dist/index.mjs.map 12.76 KB
18
- ESM ⚡️ Build success in 37ms
15
+ ESM ⚡️ Build success in 39ms
16
+ CJS dist/index.js 6.92 KB
17
+ CJS dist/index.js.map 12.80 KB
18
+ CJS ⚡️ Build success in 39ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 9151ms
20
+ DTS ⚡️ Build success in 6345ms
21
21
  DTS dist/index.d.mts 1.22 KB
22
22
  DTS dist/index.d.ts 1.22 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @objectstack/hono
2
2
 
3
+ ## 2.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - @objectstack/runtime@2.0.2
8
+
3
9
  ## 2.0.1
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/hono",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,15 +12,18 @@
12
12
  }
13
13
  },
14
14
  "peerDependencies": {
15
- "hono": "^4.0.0",
16
- "@objectstack/runtime": "2.0.1"
15
+ "hono": "^4.11.9",
16
+ "@objectstack/runtime": "2.0.2"
17
17
  },
18
18
  "devDependencies": {
19
- "hono": "^4.0.0",
19
+ "hono": "^4.11.9",
20
20
  "typescript": "^5.0.0",
21
- "@objectstack/runtime": "2.0.1"
21
+ "vitest": "^4.0.18",
22
+ "@objectstack/runtime": "2.0.2"
22
23
  },
23
24
  "scripts": {
24
- "build": "tsup --config ../../../tsup.config.ts"
25
+ "build": "tsup --config ../../../tsup.config.ts",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest"
25
28
  }
26
29
  }
@@ -0,0 +1,4 @@
1
+ // Stub for @objectstack/runtime - replaced by vi.mock in tests
2
+ export const HttpDispatcher = class {};
3
+ export type ObjectKernel = any;
4
+ export type HttpDispatcherResult = any;
@@ -0,0 +1,361 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+ import { Hono } from 'hono';
5
+
6
+ // Mock dispatcher instance
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
+ handleAnalytics: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }),
14
+ handleAutomation: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }),
15
+ handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }),
16
+ handlePackages: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }),
17
+ };
18
+
19
+ vi.mock('@objectstack/runtime', () => {
20
+ return {
21
+ HttpDispatcher: function HttpDispatcher() {
22
+ return mockDispatcher;
23
+ },
24
+ };
25
+ });
26
+
27
+ import { createHonoApp, objectStackMiddleware } from './index';
28
+
29
+ const mockKernel = { name: 'test-kernel' } as any;
30
+
31
+ describe('createHonoApp', () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ });
35
+
36
+ it('should return a Hono app instance', () => {
37
+ const app = createHonoApp({ kernel: mockKernel });
38
+ expect(app).toBeInstanceOf(Hono);
39
+ });
40
+
41
+ describe('Discovery Endpoint', () => {
42
+ it('GET /api returns discovery info', async () => {
43
+ const app = createHonoApp({ kernel: mockKernel });
44
+ const res = await app.request('/api');
45
+ expect(res.status).toBe(200);
46
+ const json = await res.json();
47
+ expect(json.data).toEqual({ version: '1.0', endpoints: [] });
48
+ expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api');
49
+ });
50
+
51
+ it('uses custom prefix for discovery', async () => {
52
+ const app = createHonoApp({ kernel: mockKernel, prefix: '/v2' });
53
+ const res = await app.request('/v2');
54
+ expect(res.status).toBe(200);
55
+ expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2');
56
+ });
57
+ });
58
+
59
+ describe('Auth Endpoint', () => {
60
+ it('POST /api/auth/login calls handleAuth', async () => {
61
+ const app = createHonoApp({ kernel: mockKernel });
62
+ const res = await app.request('/api/auth/login', { method: 'POST' });
63
+ expect(res.status).toBe(200);
64
+ expect(mockDispatcher.handleAuth).toHaveBeenCalledWith(
65
+ 'login',
66
+ 'POST',
67
+ expect.anything(),
68
+ expect.objectContaining({ request: expect.any(Request) }),
69
+ );
70
+ });
71
+
72
+ it('GET /api/auth/callback calls handleAuth', async () => {
73
+ const app = createHonoApp({ kernel: mockKernel });
74
+ const res = await app.request('/api/auth/callback', { method: 'GET' });
75
+ expect(res.status).toBe(200);
76
+ expect(mockDispatcher.handleAuth).toHaveBeenCalledWith(
77
+ 'callback',
78
+ 'GET',
79
+ expect.anything(),
80
+ expect.objectContaining({ request: expect.any(Request) }),
81
+ );
82
+ });
83
+
84
+ it('returns error on handleAuth exception', async () => {
85
+ mockDispatcher.handleAuth.mockRejectedValueOnce(
86
+ Object.assign(new Error('Unauthorized'), { statusCode: 401 }),
87
+ );
88
+ const app = createHonoApp({ kernel: mockKernel });
89
+ const res = await app.request('/api/auth/login', { method: 'POST' });
90
+ expect(res.status).toBe(401);
91
+ const json = await res.json();
92
+ expect(json.success).toBe(false);
93
+ expect(json.error.message).toBe('Unauthorized');
94
+ });
95
+ });
96
+
97
+ describe('GraphQL Endpoint', () => {
98
+ it('POST /api/graphql calls handleGraphQL', async () => {
99
+ const app = createHonoApp({ kernel: mockKernel });
100
+ const body = { query: '{ objects { name } }' };
101
+ const res = await app.request('/api/graphql', {
102
+ method: 'POST',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify(body),
105
+ });
106
+ expect(res.status).toBe(200);
107
+ expect(mockDispatcher.handleGraphQL).toHaveBeenCalledWith(
108
+ body,
109
+ expect.objectContaining({ request: expect.any(Request) }),
110
+ );
111
+ });
112
+
113
+ it('returns error on handleGraphQL exception', async () => {
114
+ mockDispatcher.handleGraphQL.mockRejectedValueOnce(new Error('Parse error'));
115
+ const app = createHonoApp({ kernel: mockKernel });
116
+ const res = await app.request('/api/graphql', {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/json' },
119
+ body: JSON.stringify({ query: 'bad' }),
120
+ });
121
+ expect(res.status).toBe(500);
122
+ const json = await res.json();
123
+ expect(json.success).toBe(false);
124
+ });
125
+ });
126
+
127
+ describe('Metadata Endpoint', () => {
128
+ it('GET /api/meta/objects calls handleMetadata', async () => {
129
+ const app = createHonoApp({ kernel: mockKernel });
130
+ const res = await app.request('/api/meta/objects');
131
+ expect(res.status).toBe(200);
132
+ expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith(
133
+ '/objects',
134
+ expect.objectContaining({ request: expect.any(Request) }),
135
+ 'GET',
136
+ undefined,
137
+ expect.any(Object),
138
+ );
139
+ });
140
+
141
+ it('PUT /api/meta/objects parses JSON body', async () => {
142
+ const app = createHonoApp({ kernel: mockKernel });
143
+ const body = { name: 'test_object' };
144
+ const res = await app.request('/api/meta/objects', {
145
+ method: 'PUT',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify(body),
148
+ });
149
+ expect(res.status).toBe(200);
150
+ expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith(
151
+ '/objects',
152
+ expect.objectContaining({ request: expect.any(Request) }),
153
+ 'PUT',
154
+ body,
155
+ expect.any(Object),
156
+ );
157
+ });
158
+ });
159
+
160
+ describe('Data Endpoint', () => {
161
+ it('GET /api/data/account calls handleData', async () => {
162
+ const app = createHonoApp({ kernel: mockKernel });
163
+ const res = await app.request('/api/data/account');
164
+ expect(res.status).toBe(200);
165
+ expect(mockDispatcher.handleData).toHaveBeenCalledWith(
166
+ '/account',
167
+ 'GET',
168
+ {},
169
+ expect.any(Object),
170
+ expect.objectContaining({ request: expect.any(Request) }),
171
+ );
172
+ });
173
+
174
+ it('POST /api/data/account parses JSON body', async () => {
175
+ const app = createHonoApp({ kernel: mockKernel });
176
+ const body = { name: 'Acme' };
177
+ const res = await app.request('/api/data/account', {
178
+ method: 'POST',
179
+ headers: { 'Content-Type': 'application/json' },
180
+ body: JSON.stringify(body),
181
+ });
182
+ expect(res.status).toBe(200);
183
+ expect(mockDispatcher.handleData).toHaveBeenCalledWith(
184
+ '/account',
185
+ 'POST',
186
+ body,
187
+ expect.any(Object),
188
+ expect.objectContaining({ request: expect.any(Request) }),
189
+ );
190
+ });
191
+
192
+ it('returns 404 when result is not handled', async () => {
193
+ mockDispatcher.handleData.mockResolvedValueOnce({ handled: false });
194
+ const app = createHonoApp({ kernel: mockKernel });
195
+ const res = await app.request('/api/data/missing');
196
+ expect(res.status).toBe(404);
197
+ const json = await res.json();
198
+ expect(json.success).toBe(false);
199
+ });
200
+ });
201
+
202
+ describe('Analytics Endpoint', () => {
203
+ it('GET /api/analytics/report calls handleAnalytics', async () => {
204
+ const app = createHonoApp({ kernel: mockKernel });
205
+ const res = await app.request('/api/analytics/report');
206
+ expect(res.status).toBe(200);
207
+ expect(mockDispatcher.handleAnalytics).toHaveBeenCalled();
208
+ });
209
+
210
+ it('POST /api/analytics/report parses body', async () => {
211
+ const app = createHonoApp({ kernel: mockKernel });
212
+ const body = { metric: 'revenue' };
213
+ const res = await app.request('/api/analytics/report', {
214
+ method: 'POST',
215
+ headers: { 'Content-Type': 'application/json' },
216
+ body: JSON.stringify(body),
217
+ });
218
+ expect(res.status).toBe(200);
219
+ expect(mockDispatcher.handleAnalytics).toHaveBeenCalledWith(
220
+ '/report',
221
+ 'POST',
222
+ body,
223
+ expect.objectContaining({ request: expect.any(Request) }),
224
+ );
225
+ });
226
+ });
227
+
228
+ describe('Automation Endpoint', () => {
229
+ it('GET /api/automation/flows calls handleAutomation', async () => {
230
+ const app = createHonoApp({ kernel: mockKernel });
231
+ const res = await app.request('/api/automation/flows');
232
+ expect(res.status).toBe(200);
233
+ expect(mockDispatcher.handleAutomation).toHaveBeenCalled();
234
+ });
235
+
236
+ it('POST /api/automation/flows parses body', async () => {
237
+ const app = createHonoApp({ kernel: mockKernel });
238
+ const body = { trigger: 'on_create' };
239
+ const res = await app.request('/api/automation/flows', {
240
+ method: 'POST',
241
+ headers: { 'Content-Type': 'application/json' },
242
+ body: JSON.stringify(body),
243
+ });
244
+ expect(res.status).toBe(200);
245
+ expect(mockDispatcher.handleAutomation).toHaveBeenCalledWith(
246
+ '/flows',
247
+ 'POST',
248
+ body,
249
+ expect.objectContaining({ request: expect.any(Request) }),
250
+ );
251
+ });
252
+ });
253
+
254
+ describe('Storage Endpoint', () => {
255
+ it('GET /api/storage/files calls handleStorage', async () => {
256
+ const app = createHonoApp({ kernel: mockKernel });
257
+ const res = await app.request('/api/storage/files');
258
+ expect(res.status).toBe(200);
259
+ expect(mockDispatcher.handleStorage).toHaveBeenCalled();
260
+ });
261
+ });
262
+
263
+ describe('Packages Endpoint', () => {
264
+ it('GET /api/packages calls handlePackages', async () => {
265
+ const app = createHonoApp({ kernel: mockKernel });
266
+ const res = await app.request('/api/packages');
267
+ expect(res.status).toBe(200);
268
+ expect(mockDispatcher.handlePackages).toHaveBeenCalledWith(
269
+ '',
270
+ 'GET',
271
+ {},
272
+ expect.any(Object),
273
+ expect.objectContaining({ request: expect.any(Request) }),
274
+ );
275
+ });
276
+
277
+ it('POST /api/packages/install parses body', async () => {
278
+ const app = createHonoApp({ kernel: mockKernel });
279
+ const body = { package: 'my-plugin' };
280
+ const res = await app.request('/api/packages/install', {
281
+ method: 'POST',
282
+ headers: { 'Content-Type': 'application/json' },
283
+ body: JSON.stringify(body),
284
+ });
285
+ expect(res.status).toBe(200);
286
+ expect(mockDispatcher.handlePackages).toHaveBeenCalledWith(
287
+ '/install',
288
+ 'POST',
289
+ body,
290
+ expect.any(Object),
291
+ expect.objectContaining({ request: expect.any(Request) }),
292
+ );
293
+ });
294
+ });
295
+
296
+ describe('normalizeResponse', () => {
297
+ it('handles redirect result', async () => {
298
+ mockDispatcher.handleData.mockResolvedValueOnce({
299
+ handled: true,
300
+ result: { type: 'redirect', url: 'https://example.com' },
301
+ });
302
+ const app = createHonoApp({ kernel: mockKernel });
303
+ const res = await app.request('/api/data/redir', { redirect: 'manual' });
304
+ expect(res.status).toBe(302);
305
+ expect(res.headers.get('location')).toBe('https://example.com');
306
+ });
307
+
308
+ it('handles stream result', async () => {
309
+ const stream = new ReadableStream({
310
+ start(controller) {
311
+ controller.enqueue(new TextEncoder().encode('hello'));
312
+ controller.close();
313
+ },
314
+ });
315
+ mockDispatcher.handleData.mockResolvedValueOnce({
316
+ handled: true,
317
+ result: { type: 'stream', stream, headers: { 'Content-Type': 'text/plain' } },
318
+ });
319
+ const app = createHonoApp({ kernel: mockKernel });
320
+ const res = await app.request('/api/data/stream');
321
+ expect(res.status).toBe(200);
322
+ const text = await res.text();
323
+ expect(text).toBe('hello');
324
+ });
325
+ });
326
+ });
327
+
328
+ describe('objectStackMiddleware', () => {
329
+ it('sets kernel on context via c.set', async () => {
330
+ const app = new Hono();
331
+ const middleware = objectStackMiddleware(mockKernel);
332
+
333
+ app.use('*', middleware);
334
+ app.get('/test', (c) => {
335
+ const kernel = c.get('objectStack');
336
+ return c.json({ hasKernel: !!kernel });
337
+ });
338
+
339
+ const res = await app.request('/test');
340
+ expect(res.status).toBe(200);
341
+ const json = await res.json();
342
+ expect(json.hasKernel).toBe(true);
343
+ });
344
+
345
+ it('calls next middleware', async () => {
346
+ const app = new Hono();
347
+ const middleware = objectStackMiddleware(mockKernel);
348
+ const spy = vi.fn();
349
+
350
+ app.use('*', middleware);
351
+ app.use('*', async (_c, next) => {
352
+ spy();
353
+ await next();
354
+ });
355
+ app.get('/test', (c) => c.json({ ok: true }));
356
+
357
+ const res = await app.request('/test');
358
+ expect(res.status).toBe(200);
359
+ expect(spy).toHaveBeenCalled();
360
+ });
361
+ });
@@ -0,0 +1,16 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { defineConfig } from 'vitest/config';
4
+ import path from 'node:path';
5
+
6
+ export default defineConfig({
7
+ test: {
8
+ globals: true,
9
+ environment: 'node',
10
+ },
11
+ resolve: {
12
+ alias: {
13
+ '@objectstack/runtime': path.resolve(__dirname, 'src/__mocks__/runtime.ts'),
14
+ },
15
+ },
16
+ });