Skip to content

Commit e852964

Browse files
Merge pull request #488 from objectstack-ai/copilot/update-client-according-to-spec
2 parents 080c48e + 7f003f4 commit e852964

2 files changed

Lines changed: 132 additions & 29 deletions

File tree

packages/client/src/client.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ describe('ObjectStackClient', () => {
1515
it('should make discovery request on connect', async () => {
1616
const fetchMock = vi.fn().mockResolvedValue({
1717
ok: true,
18-
json: async () => ({ routes: { data: '/api/v1/data' } })
18+
json: async () => ({
19+
version: 'v1',
20+
apiName: 'ObjectStack',
21+
capabilities: ['metadata', 'data', 'ui'],
22+
endpoints: {}
23+
})
1924
});
2025

2126
const client = new ObjectStackClient({
@@ -26,4 +31,61 @@ describe('ObjectStackClient', () => {
2631
await client.connect();
2732
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1', expect.any(Object));
2833
});
34+
35+
it('should get metadata types', async () => {
36+
const fetchMock = vi.fn().mockResolvedValue({
37+
ok: true,
38+
json: async () => ({
39+
types: ['object', 'plugin', 'view']
40+
})
41+
});
42+
43+
const client = new ObjectStackClient({
44+
baseUrl: 'http://localhost:3000',
45+
fetch: fetchMock
46+
});
47+
48+
const result = await client.meta.getTypes();
49+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta', expect.any(Object));
50+
expect(result.types).toEqual(['object', 'plugin', 'view']);
51+
});
52+
53+
it('should get metadata items by type', async () => {
54+
const fetchMock = vi.fn().mockResolvedValue({
55+
ok: true,
56+
json: async () => ({
57+
type: 'object',
58+
items: [{ name: 'customer' }, { name: 'order' }]
59+
})
60+
});
61+
62+
const client = new ObjectStackClient({
63+
baseUrl: 'http://localhost:3000',
64+
fetch: fetchMock
65+
});
66+
67+
const result = await client.meta.getItems('object');
68+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta/object', expect.any(Object));
69+
expect(result.type).toBe('object');
70+
expect(result.items).toHaveLength(2);
71+
});
72+
73+
it('should get metadata item by type and name', async () => {
74+
const fetchMock = vi.fn().mockResolvedValue({
75+
ok: true,
76+
json: async () => ({
77+
name: 'customer',
78+
fields: []
79+
})
80+
});
81+
82+
const client = new ObjectStackClient({
83+
baseUrl: 'http://localhost:3000',
84+
fetch: fetchMock
85+
});
86+
87+
const result = await client.meta.getItem('object', 'customer');
88+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta/object/customer', expect.any(Object));
89+
expect(result.name).toBe('customer');
90+
});
2991
});

packages/client/src/index.ts

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
MetadataCacheRequest,
99
MetadataCacheResponse,
1010
StandardErrorCode,
11-
ErrorCategory
11+
ErrorCategory,
12+
GetDiscoveryResponse,
13+
GetMetaTypesResponse,
14+
GetMetaItemsResponse
1215
} from '@objectstack/spec/api';
1316
import { Logger, createLogger } from '@objectstack/core';
1417

@@ -29,16 +32,11 @@ export interface ClientConfig {
2932
debug?: boolean;
3033
}
3134

32-
export interface DiscoveryResult {
33-
routes: {
34-
discovery: string;
35-
metadata: string;
36-
data: string;
37-
auth: string;
38-
ui: string;
39-
};
40-
capabilities?: Record<string, boolean>;
41-
}
35+
/**
36+
* Discovery Result
37+
* Re-export from @objectstack/spec/api for convenience
38+
*/
39+
export type DiscoveryResult = GetDiscoveryResponse;
4240

4341
export interface QueryOptions {
4442
select?: string[]; // Simplified Selection
@@ -69,7 +67,7 @@ export class ObjectStackClient {
6967
private baseUrl: string;
7068
private token?: string;
7169
private fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
72-
private routes?: DiscoveryResult['routes'];
70+
private discoveryInfo?: DiscoveryResult;
7371
private logger: Logger;
7472

7573
constructor(config: ClientConfig) {
@@ -87,21 +85,21 @@ export class ObjectStackClient {
8785
}
8886

8987
/**
90-
* Initialize the client by discovering server capabilities and routes.
88+
* Initialize the client by discovering server capabilities.
9189
*/
9290
async connect() {
9391
this.logger.debug('Connecting to ObjectStack server', { baseUrl: this.baseUrl });
9492

9593
try {
96-
// Connect to the discovery endpoint
97-
// During boot, we might not know routes, so we check convention /api/v1 first
94+
// Connect to the discovery endpoint at /api/v1
9895
const res = await this.fetch(`${this.baseUrl}/api/v1`);
9996

10097
const data = await res.json();
101-
this.routes = data.routes;
98+
this.discoveryInfo = data;
10299

103100
this.logger.info('Connected to ObjectStack server', {
104-
routes: Object.keys(data.routes || {}),
101+
version: data.version,
102+
apiName: data.apiName,
105103
capabilities: data.capabilities
106104
});
107105

@@ -116,11 +114,47 @@ export class ObjectStackClient {
116114
* Metadata Operations
117115
*/
118116
meta = {
117+
/**
118+
* Get all available metadata types
119+
* Returns types like 'object', 'plugin', 'view', etc.
120+
*/
121+
getTypes: async (): Promise<GetMetaTypesResponse> => {
122+
const route = this.getRoute('metadata');
123+
const res = await this.fetch(`${this.baseUrl}${route}`);
124+
return res.json();
125+
},
126+
127+
/**
128+
* Get all items of a specific metadata type
129+
* @param type - Metadata type name (e.g., 'object', 'plugin')
130+
*/
131+
getItems: async (type: string): Promise<GetMetaItemsResponse> => {
132+
const route = this.getRoute('metadata');
133+
const res = await this.fetch(`${this.baseUrl}${route}/${type}`);
134+
return res.json();
135+
},
136+
137+
/**
138+
* Get a specific object definition by name
139+
* @deprecated Use `getItem('object', name)` instead for consistency with spec protocol
140+
* @param name - Object name (snake_case identifier)
141+
*/
119142
getObject: async (name: string) => {
120143
const route = this.getRoute('metadata');
121144
const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`);
122145
return res.json();
123146
},
147+
148+
/**
149+
* Get a specific metadata item by type and name
150+
* @param type - Metadata type (e.g., 'object', 'plugin')
151+
* @param name - Item name (snake_case identifier)
152+
*/
153+
getItem: async (type: string, name: string) => {
154+
const route = this.getRoute('metadata');
155+
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`);
156+
return res.json();
157+
},
124158

125159
/**
126160
* Get object metadata with cache support
@@ -407,16 +441,20 @@ export class ObjectStackClient {
407441
return res;
408442
}
409443

410-
private getRoute(key: keyof DiscoveryResult['routes']): string {
411-
if (!this.routes) {
412-
// Fallback for strictness, but we allow bootstrapping
413-
this.logger.warn('Accessing route before connect()', {
414-
route: key,
415-
fallback: `/api/v1/${key}`
416-
});
417-
return `/api/v1/${key}`;
418-
}
419-
return this.routes[key] || `/api/v1/${key}`;
444+
/**
445+
* Get the conventional route path for a given API endpoint type
446+
* ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
447+
*/
448+
private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth'): string {
449+
// Use conventional ObjectStack API paths
450+
const routeMap: Record<string, string> = {
451+
data: '/api/v1/data',
452+
metadata: '/api/v1/meta',
453+
ui: '/api/v1/ui',
454+
auth: '/api/v1/auth'
455+
};
456+
457+
return routeMap[type] || `/api/v1/${type}`;
420458
}
421459
}
422460

@@ -435,5 +473,8 @@ export type {
435473
MetadataCacheRequest,
436474
MetadataCacheResponse,
437475
StandardErrorCode,
438-
ErrorCategory
476+
ErrorCategory,
477+
GetDiscoveryResponse,
478+
GetMetaTypesResponse,
479+
GetMetaItemsResponse
439480
} from '@objectstack/spec/api';

0 commit comments

Comments
 (0)