Skip to content

Commit 3e908da

Browse files
authored
Merge pull request #859 from objectstack-ai/copilot/fix-async-service-bug
2 parents c8f1183 + 77086c2 commit 3e908da

13 files changed

Lines changed: 329 additions & 53 deletions

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ This strategy ensures rapid iteration while maintaining a clear path to producti
123123
| `z.unknown()` in extensibility fields | ✅ Justified | `properties`, `children`, `context`, `options`, `body` — inherently dynamic extensibility points |
124124
| DashboardWidget discriminated union by type | 🔴 | Planned — chart/metric/pivot subtypes with type-specific required fields |
125125
| CI lint rule rejecting new `z.any()` | 🔴 | Planned — eslint or custom lint rule to block `z.any()` additions |
126-
| Dispatcher async `getService` bug fix || All `getService`/`getObjectQLService` calls in `http-dispatcher.ts` now properly `await` async service factories. Covers `handleAnalytics`, `handleAuth`, `handleStorage`, `handleAutomation`, `handleMetadata`, `handleUi`, `handlePackages`. 20 new tests for async/sync/error scenarios. |
126+
| Dispatcher async `getService` bug fix || All `getService`/`getObjectQLService` calls in `http-dispatcher.ts` now properly `await` async service factories. Covers `handleAnalytics`, `handleAuth`, `handleStorage`, `handleAutomation`, `handleMetadata`, `handleUi`, `handlePackages`. All 7 framework adapters (Express, Fastify, Hono, Next.js, SvelteKit, NestJS, Nuxt) updated to use `getServiceAsync()` for auth service resolution. |
127127

128128
---
129129

packages/adapters/express/src/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,18 @@ export function createExpressRouter(options: ExpressAdapterOptions): Router {
8181
const path = (req.params as any).path;
8282
const method = req.method;
8383

84-
// Try AuthPlugin service first
85-
const authService = typeof options.kernel.getService === 'function'
86-
? options.kernel.getService<AuthService>('auth')
87-
: null;
84+
// Try AuthPlugin service first (prefer async to support factory-based services)
85+
let authService: AuthService | null = null;
86+
try {
87+
if (typeof options.kernel.getServiceAsync === 'function') {
88+
authService = await options.kernel.getServiceAsync<AuthService>('auth');
89+
} else if (typeof options.kernel.getService === 'function') {
90+
authService = options.kernel.getService<AuthService>('auth');
91+
}
92+
} catch {
93+
// Service not registered — fall through to dispatcher
94+
authService = null;
95+
}
8896

8997
if (authService && typeof authService.handleRequest === 'function') {
9098
const protocol = req.protocol || 'http';

packages/adapters/fastify/src/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,18 @@ export async function objectStackPlugin(fastify: FastifyInstance, options: Fasti
8484
const path = request.url.substring(`${prefix}/auth/`.length).split('?')[0];
8585
const method = request.method;
8686

87-
// Try AuthPlugin service first
88-
const authService = typeof options.kernel.getService === 'function'
89-
? options.kernel.getService<AuthService>('auth')
90-
: null;
87+
// Try AuthPlugin service first (prefer async to support factory-based services)
88+
let authService: AuthService | null = null;
89+
try {
90+
if (typeof options.kernel.getServiceAsync === 'function') {
91+
authService = await options.kernel.getServiceAsync<AuthService>('auth');
92+
} else if (typeof options.kernel.getService === 'function') {
93+
authService = options.kernel.getService<AuthService>('auth');
94+
}
95+
} catch {
96+
// Service not registered — fall through to dispatcher
97+
authService = null;
98+
}
9199

92100
if (authService && typeof authService.handleRequest === 'function') {
93101
const protocol = request.protocol || 'http';

packages/adapters/hono/src/hono.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,43 @@ describe('createHonoApp', () => {
198198
expect(res.status).toBe(200);
199199
expect(mockDispatcher.handleAuth).toHaveBeenCalled();
200200
});
201+
202+
it('uses kernel.getServiceAsync("auth") when available (async factory)', async () => {
203+
const mockHandleRequest = vi.fn().mockResolvedValue(
204+
new Response(JSON.stringify({ user: { id: '2' } }), {
205+
status: 200,
206+
headers: { 'Content-Type': 'application/json' },
207+
}),
208+
);
209+
const kernelWithAsyncAuth = {
210+
...mockKernel,
211+
getServiceAsync: vi.fn().mockResolvedValue({ handleRequest: mockHandleRequest }),
212+
};
213+
const authApp = createHonoApp({ kernel: kernelWithAsyncAuth });
214+
const res = await authApp.request('/api/auth/sign-in/email', {
215+
method: 'POST',
216+
headers: { 'Content-Type': 'application/json' },
217+
body: JSON.stringify({ email: 'a@b.com', password: 'pass' }),
218+
});
219+
expect(res.status).toBe(200);
220+
expect(kernelWithAsyncAuth.getServiceAsync).toHaveBeenCalledWith('auth');
221+
expect(mockHandleRequest).toHaveBeenCalled();
222+
});
223+
224+
it('falls back to dispatcher when getServiceAsync throws (async factory error)', async () => {
225+
const kernelWithFailingAsync = {
226+
...mockKernel,
227+
getServiceAsync: vi.fn().mockRejectedValue(new Error("Service 'auth' not found")),
228+
};
229+
const authApp = createHonoApp({ kernel: kernelWithFailingAsync });
230+
const res = await authApp.request('/api/auth/login', {
231+
method: 'POST',
232+
headers: { 'Content-Type': 'application/json' },
233+
body: JSON.stringify({ email: 'a@b.com' }),
234+
});
235+
expect(res.status).toBe(200);
236+
expect(mockDispatcher.handleAuth).toHaveBeenCalled();
237+
});
201238
});
202239

203240
describe('GraphQL Endpoint', () => {

packages/adapters/hono/src/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,18 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
8787
const path = c.req.path.substring(`${prefix}/auth/`.length);
8888
const method = c.req.method;
8989

90-
// Try AuthPlugin service first (preferred path)
91-
const authService = typeof options.kernel.getService === 'function'
92-
? options.kernel.getService<AuthService>('auth')
93-
: null;
90+
// Try AuthPlugin service first (prefer async to support factory-based services)
91+
let authService: AuthService | null = null;
92+
try {
93+
if (typeof options.kernel.getServiceAsync === 'function') {
94+
authService = await options.kernel.getServiceAsync<AuthService>('auth');
95+
} else if (typeof options.kernel.getService === 'function') {
96+
authService = options.kernel.getService<AuthService>('auth');
97+
}
98+
} catch {
99+
// Service not registered — fall through to dispatcher
100+
authService = null;
101+
}
94102

95103
if (authService && typeof authService.handleRequest === 'function') {
96104
const response = await authService.handleRequest(c.req.raw);

packages/adapters/nestjs/src/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,19 @@ export class ObjectStackController {
113113
@All('auth/*')
114114
async auth(@Req() req: any, @Res() res: any, @Body() body: any) {
115115
try {
116-
// Try AuthPlugin service first (preferred path)
116+
// Try AuthPlugin service first (prefer async to support factory-based services)
117117
const kernel = this.service.getKernel();
118-
const authService = typeof kernel.getService === 'function'
119-
? kernel.getService<AuthService>('auth')
120-
: null;
118+
let authService: AuthService | null = null;
119+
try {
120+
if (typeof kernel.getServiceAsync === 'function') {
121+
authService = await kernel.getServiceAsync<AuthService>('auth');
122+
} else if (typeof kernel.getService === 'function') {
123+
authService = kernel.getService<AuthService>('auth');
124+
}
125+
} catch {
126+
// Service not registered — fall through to legacy dispatcher
127+
authService = null;
128+
}
121129

122130
if (authService && typeof authService.handleRequest === 'function') {
123131
// Construct a Web standard Request from the Express/Fastify request

packages/adapters/nextjs/src/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,18 @@ export function createRouteHandler(options: NextAdapterOptions) {
6868

6969
// --- 1. Auth ---
7070
if (segments[0] === 'auth') {
71-
// Try AuthPlugin service first (preferred path)
72-
const authService = typeof options.kernel.getService === 'function'
73-
? options.kernel.getService<AuthService>('auth')
74-
: null;
71+
// Try AuthPlugin service first (prefer async to support factory-based services)
72+
let authService: AuthService | null = null;
73+
try {
74+
if (typeof options.kernel.getServiceAsync === 'function') {
75+
authService = await options.kernel.getServiceAsync<AuthService>('auth');
76+
} else if (typeof options.kernel.getService === 'function') {
77+
authService = options.kernel.getService<AuthService>('auth');
78+
}
79+
} catch {
80+
// Service not registered — fall through to dispatcher
81+
authService = null;
82+
}
7583

7684
if (authService && typeof authService.handleRequest === 'function') {
7785
const response = await authService.handleRequest(req);

packages/adapters/nextjs/src/nextjs.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,37 @@ describe('createRouteHandler', () => {
213213
expect(res.status).toBe(500);
214214
expect(res.body.success).toBe(false);
215215
});
216+
217+
it('uses kernel.getServiceAsync("auth") when available (async factory)', async () => {
218+
const mockHandleRequest = vi.fn().mockResolvedValue(
219+
new Response(JSON.stringify({ user: { id: '2' } }), {
220+
status: 200,
221+
headers: { 'Content-Type': 'application/json' },
222+
}),
223+
);
224+
const kernelWithAsyncAuth = {
225+
...mockKernel,
226+
getServiceAsync: vi.fn().mockResolvedValue({ handleRequest: mockHandleRequest }),
227+
};
228+
const handler = createRouteHandler({ kernel: kernelWithAsyncAuth });
229+
const req = makeReq('http://localhost/api/auth/sign-in/email', 'POST', { email: 'a@b.com', password: 'pass' });
230+
const res = await handler(req, { params: { objectstack: ['auth', 'sign-in', 'email'] } });
231+
expect(res.status).toBe(200);
232+
expect(kernelWithAsyncAuth.getServiceAsync).toHaveBeenCalledWith('auth');
233+
expect(mockHandleRequest).toHaveBeenCalled();
234+
});
235+
236+
it('falls back to dispatcher when getServiceAsync throws', async () => {
237+
const kernelWithFailingAsync = {
238+
...mockKernel,
239+
getServiceAsync: vi.fn().mockRejectedValue(new Error("Service 'auth' not found")),
240+
};
241+
const handler = createRouteHandler({ kernel: kernelWithFailingAsync });
242+
const req = makeReq('http://localhost/api/auth/login', 'POST', { email: 'a@b.com' });
243+
const res = await handler(req, { params: { objectstack: ['auth', 'login'] } });
244+
expect(res.status).toBe(200);
245+
expect(mockDispatcher.handleAuth).toHaveBeenCalled();
246+
});
216247
});
217248

218249
describe('GraphQL Endpoint', () => {

packages/adapters/nuxt/src/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,18 @@ export function createH3Router(options: NuxtAdapterOptions): Router {
104104
const path = urlPath.substring(`${prefix}/auth/`.length).split('?')[0];
105105
const method = event.method;
106106

107-
// Try AuthPlugin service first
108-
const authService = typeof options.kernel.getService === 'function'
109-
? options.kernel.getService<AuthService>('auth')
110-
: null;
107+
// Try AuthPlugin service first (prefer async to support factory-based services)
108+
let authService: AuthService | null = null;
109+
try {
110+
if (typeof options.kernel.getServiceAsync === 'function') {
111+
authService = await options.kernel.getServiceAsync<AuthService>('auth');
112+
} else if (typeof options.kernel.getService === 'function') {
113+
authService = options.kernel.getService<AuthService>('auth');
114+
}
115+
} catch {
116+
// Service not registered — fall through to dispatcher
117+
authService = null;
118+
}
111119

112120
if (authService && typeof authService.handleRequest === 'function') {
113121
const host = event.node.req.headers.host || 'localhost';

packages/adapters/sveltekit/src/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,18 @@ export function createRequestHandler(options: SvelteKitAdapterOptions) {
108108
if (segments[0] === 'auth') {
109109
const subPath = segments.slice(1).join('/');
110110

111-
// Try AuthPlugin service first
112-
const authService = typeof options.kernel.getService === 'function'
113-
? options.kernel.getService<AuthService>('auth')
114-
: null;
111+
// Try AuthPlugin service first (prefer async to support factory-based services)
112+
let authService: AuthService | null = null;
113+
try {
114+
if (typeof options.kernel.getServiceAsync === 'function') {
115+
authService = await options.kernel.getServiceAsync<AuthService>('auth');
116+
} else if (typeof options.kernel.getService === 'function') {
117+
authService = options.kernel.getService<AuthService>('auth');
118+
}
119+
} catch {
120+
// Service not registered — fall through to dispatcher
121+
authService = null;
122+
}
115123

116124
if (authService && typeof authService.handleRequest === 'function') {
117125
return await authService.handleRequest(request);

0 commit comments

Comments
 (0)