Skip to content

Commit 733f582

Browse files
authored
Merge pull request #985 from objectstack-ai/copilot/fix-discovery-api-routing-issues
2 parents 9cac368 + b9e8c25 commit 733f582

19 files changed

Lines changed: 156 additions & 54 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/client": minor
4+
"@objectstack/runtime": patch
5+
"@objectstack/express": patch
6+
"@objectstack/fastify": patch
7+
"@objectstack/hono": patch
8+
"@objectstack/nestjs": patch
9+
"@objectstack/nextjs": patch
10+
"@objectstack/nuxt": patch
11+
"@objectstack/sveltekit": patch
12+
---
13+
14+
Fix discovery API endpoint routing and protocol consistency.
15+
16+
**Discovery route standardization:**
17+
- All adapters (Express, Fastify, Hono, NestJS, Next.js, Nuxt, SvelteKit) now mount the discovery endpoint at `{prefix}/discovery` instead of `{prefix}` root.
18+
- `.well-known/objectstack` redirects now point to `{prefix}/discovery`.
19+
- Client `connect()` fallback URL changed from `/api/v1` to `/api/v1/discovery`.
20+
- Runtime dispatcher handles both `/discovery` (standard) and `/` (legacy) for backward compatibility.
21+
22+
**Schema & route alignment:**
23+
- Added `storage` (service: `file-storage`) and `feed` (service: `data`) routes to `DEFAULT_DISPATCHER_ROUTES`.
24+
- Added `feed` and `discovery` fields to `ApiRoutesSchema`.
25+
- Unified `GetDiscoveryResponseSchema` with `DiscoverySchema` as single source of truth.
26+
- Client `getRoute('feed')` fallback updated from `/api/v1/data` to `/api/v1/feed`.
27+
28+
**Type safety:**
29+
- Extracted `ApiRouteType` from `ApiRoutes` keys for type-safe client route resolution.
30+
- Removed `as any` type casting in client route access.

packages/adapters/express/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function createExpressRouter(options: ExpressAdapterOptions): Router {
7878
// ─── Explicit routes (framework-specific handling required) ────────────────
7979

8080
// --- Discovery ---
81-
router.get('/', async (_req: Request, res: Response) => {
81+
router.get('/discovery', async (_req: Request, res: Response) => {
8282
res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
8383
});
8484

packages/adapters/fastify/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ export async function objectStackPlugin(fastify: FastifyInstance, options: Fasti
7676
// ─── Explicit routes (framework-specific handling required) ────────────────
7777

7878
// --- Discovery ---
79-
fastify.get(`${prefix}`, async (_request: FastifyRequest, reply: FastifyReply) => {
79+
fastify.get(`${prefix}/discovery`, async (_request: FastifyRequest, reply: FastifyReply) => {
8080
return reply.send({ data: await dispatcher.getDiscoveryInfo(prefix) });
8181
});
8282

8383
// --- .well-known ---
8484
fastify.get('/.well-known/objectstack', async (_request: FastifyRequest, reply: FastifyReply) => {
85-
return reply.redirect(prefix);
85+
return reply.redirect(`${prefix}/discovery`);
8686
});
8787

8888
// --- Auth (needs auth service integration) ---

packages/adapters/hono/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,13 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
8181
// ─── Explicit routes (framework-specific handling required) ────────────────
8282

8383
// --- Discovery ---
84-
app.get(`${prefix}`, async (c) => {
84+
app.get(`${prefix}/discovery`, async (c) => {
8585
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
8686
});
8787

8888
// --- .well-known ---
8989
app.get('/.well-known/objectstack', (c) => {
90-
return c.redirect(prefix);
90+
return c.redirect(`${prefix}/discovery`);
9191
});
9292

9393
// --- Auth (needs auth service integration) ---

packages/adapters/nestjs/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class ObjectStackController {
9494
}
9595

9696
// --- Discovery Endpoint ---
97-
@Get()
97+
@Get('discovery')
9898
async discovery() {
9999
return { data: await this.service.dispatcher.getDiscoveryInfo('/api') };
100100
}
@@ -241,7 +241,7 @@ export class ObjectStackController {
241241
export class DiscoveryController {
242242
@Get('objectstack')
243243
discover(@Res() res: any) {
244-
return res.redirect('/api');
244+
return res.redirect('/api/discovery');
245245
}
246246
}
247247

packages/adapters/nestjs/src/nestjs.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,12 +442,12 @@ describe('ObjectStackController', () => {
442442
});
443443

444444
describe('DiscoveryController', () => {
445-
it('redirects to /api', () => {
445+
it('redirects to /api/discovery', () => {
446446
const controller = new DiscoveryController();
447447
const res = createMockRes();
448448

449449
controller.discover(res);
450450

451-
expect(res._redirectUrl).toBe('/api');
451+
expect(res._redirectUrl).toBe('/api/discovery');
452452
});
453453
});

packages/adapters/nextjs/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function createRouteHandler(options: NextAdapterOptions) {
6262
const method = req.method;
6363

6464
// --- 0. Discovery Endpoint ---
65-
if (segments.length === 0 && method === 'GET') {
65+
if (segments.length === 1 && segments[0] === 'discovery' && method === 'GET') {
6666
return NextResponse.json({ data: await dispatcher.getDiscoveryInfo(options.prefix || '/api') });
6767
}
6868

@@ -152,7 +152,7 @@ export function createDiscoveryHandler(options: NextAdapterOptions) {
152152
return async function discoveryHandler(req: NextRequest) {
153153
const apiPath = options.prefix || '/api';
154154
const url = new URL(req.url);
155-
const targetUrl = new URL(apiPath, url.origin);
155+
const targetUrl = new URL(`${apiPath}/discovery`, url.origin);
156156
return NextResponse.redirect(targetUrl);
157157
}
158158
}

packages/adapters/nuxt/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function createH3Router(options: NuxtAdapterOptions): Router {
8989

9090
// --- Discovery ---
9191
router.get(
92-
`${prefix}`,
92+
`${prefix}/discovery`,
9393
defineEventHandler(async () => {
9494
return { data: await dispatcher.getDiscoveryInfo(prefix) };
9595
}),
@@ -99,7 +99,7 @@ export function createH3Router(options: NuxtAdapterOptions): Router {
9999
router.get(
100100
'/.well-known/objectstack',
101101
defineEventHandler((event) => {
102-
return sendRedirect(event, prefix);
102+
return sendRedirect(event, `${prefix}/discovery`);
103103
}),
104104
);
105105

packages/adapters/sveltekit/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export function createRequestHandler(options: SvelteKitAdapterOptions) {
9999
const segments = path.split('/').filter(Boolean);
100100

101101
// --- Discovery ---
102-
if (segments.length === 0 && method === 'GET') {
102+
if (segments.length === 1 && segments[0] === 'discovery' && method === 'GET') {
103103
return new Response(JSON.stringify({ data: await dispatcher.getDiscoveryInfo(prefix) }), {
104104
status: 200,
105105
headers: { 'Content-Type': 'application/json' },

packages/client/src/index.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,17 @@ import {
8787
SubscribeResponse,
8888
UnsubscribeResponse,
8989
WellKnownCapabilities,
90+
ApiRoutes,
9091
} from '@objectstack/spec/api';
9192
import { Logger, createLogger } from '@objectstack/core';
9293

94+
/**
95+
* Route types that the client can resolve.
96+
* Covers all keys from `ApiRoutes` (the discovery schema) plus
97+
* client-specific virtual routes (`views`, `permissions`).
98+
*/
99+
export type ApiRouteType = keyof ApiRoutes | 'views' | 'permissions';
100+
93101
export interface ClientConfig {
94102
baseUrl: string;
95103
token?: string;
@@ -238,10 +246,10 @@ export class ObjectStackClient {
238246
this.logger.debug('Standard discovery probe failed', { error: (e as Error).message });
239247
}
240248

241-
// 2. Fallback to Legacy/Direct Path /api/v1
249+
// 2. Fallback to Protocol-standard Discovery Path /api/v1/discovery
242250
if (!data) {
243-
const fallbackUrl = `${this.baseUrl}/api/v1`;
244-
this.logger.debug('Falling back to legacy discovery', { url: fallbackUrl });
251+
const fallbackUrl = `${this.baseUrl}/api/v1/discovery`;
252+
this.logger.debug('Falling back to standard discovery endpoint', { url: fallbackUrl });
245253
const res = await this.fetchImpl(fallbackUrl);
246254
if (!res.ok) {
247255
throw new Error(`Failed to connect to ${fallbackUrl}: ${res.statusText}`);
@@ -1677,16 +1685,20 @@ export class ObjectStackClient {
16771685
* Get the conventional route path for a given API endpoint type
16781686
* ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
16791687
*/
1680-
private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth' | 'analytics' | 'storage' | 'automation' | 'packages' | 'permissions' | 'realtime' | 'workflow' | 'views' | 'notifications' | 'ai' | 'i18n' | 'feed'): string {
1681-
// 1. Use discovered routes if available
1682-
if (this.discoveryInfo?.routes && (this.discoveryInfo.routes as any)[type]) {
1683-
return (this.discoveryInfo.routes as any)[type];
1688+
private getRoute(type: ApiRouteType): string {
1689+
// 1. Use discovered routes if available (only for ApiRoutes keys, not client-specific keys)
1690+
const routes = this.discoveryInfo?.routes;
1691+
if (routes) {
1692+
const key = type as keyof ApiRoutes;
1693+
const discovered = routes[key];
1694+
if (discovered) return discovered;
16841695
}
16851696

1686-
// 2. Fallback to conventions
1687-
const routeMap: Record<string, string> = {
1697+
// 2. Fallback to conventions (covers all ApiRoutes keys + client-specific virtual routes)
1698+
const routeMap: Record<ApiRouteType, string> = {
16881699
data: '/api/v1/data',
16891700
metadata: '/api/v1/meta',
1701+
discovery: '/api/v1/discovery',
16901702
ui: '/api/v1/ui',
16911703
auth: '/api/v1/auth',
16921704
analytics: '/api/v1/analytics',
@@ -1700,7 +1712,8 @@ export class ObjectStackClient {
17001712
notifications: '/api/v1/notifications',
17011713
ai: '/api/v1/ai',
17021714
i18n: '/api/v1/i18n',
1703-
feed: '/api/v1/data',
1715+
feed: '/api/v1/feed',
1716+
graphql: '/graphql',
17041717
};
17051718

17061719
return routeMap[type] || `/api/v1/${type}`;

0 commit comments

Comments
 (0)