@objectstack/client 4.0.1 → 4.0.3
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.
- package/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +16 -0
- package/dist/index.d.mts +95 -9
- package/dist/index.d.ts +95 -9
- package/dist/index.js +159 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +158 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -12
- package/src/client.test.ts +4 -12
- package/src/index.ts +43 -23
- package/src/realtime-api.ts +208 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/client",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Official Client SDK for ObjectStack Protocol",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,20 +13,20 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "4.0.
|
|
17
|
-
"@objectstack/spec": "4.0.
|
|
16
|
+
"@objectstack/core": "4.0.3",
|
|
17
|
+
"@objectstack/spec": "4.0.3"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@hono/node-server": "^1.19.
|
|
21
|
-
"msw": "^2.
|
|
20
|
+
"@hono/node-server": "^1.19.14",
|
|
21
|
+
"msw": "^2.13.2",
|
|
22
22
|
"typescript": "^6.0.2",
|
|
23
|
-
"vitest": "^4.1.
|
|
24
|
-
"@objectstack/driver-memory": "4.0.
|
|
25
|
-
"@objectstack/hono": "4.0.
|
|
26
|
-
"@objectstack/objectql": "4.0.
|
|
27
|
-
"@objectstack/plugin-hono-server": "4.0.
|
|
28
|
-
"@objectstack/plugin-msw": "4.0.
|
|
29
|
-
"@objectstack/runtime": "4.0.
|
|
23
|
+
"vitest": "^4.1.4",
|
|
24
|
+
"@objectstack/driver-memory": "4.0.3",
|
|
25
|
+
"@objectstack/hono": "4.0.3",
|
|
26
|
+
"@objectstack/objectql": "4.0.3",
|
|
27
|
+
"@objectstack/plugin-hono-server": "4.0.3",
|
|
28
|
+
"@objectstack/plugin-msw": "4.0.3",
|
|
29
|
+
"@objectstack/runtime": "4.0.3"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup --config ../../tsup.config.ts",
|
package/src/client.test.ts
CHANGED
|
@@ -486,18 +486,10 @@ describe('AI namespace', () => {
|
|
|
486
486
|
expect(opts.method).toBe('POST');
|
|
487
487
|
});
|
|
488
488
|
|
|
489
|
-
it('should chat
|
|
490
|
-
const { client
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
});
|
|
494
|
-
const result = await client.ai.chat({
|
|
495
|
-
message: 'Show me customer stats',
|
|
496
|
-
conversationId: 'conv-1'
|
|
497
|
-
});
|
|
498
|
-
expect(result.conversationId).toBe('conv-1');
|
|
499
|
-
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
|
500
|
-
expect(body.message).toBe('Show me customer stats');
|
|
489
|
+
it('should not expose chat method (use Vercel AI SDK useChat directly)', () => {
|
|
490
|
+
const { client } = createMockClient({ success: true, data: {} });
|
|
491
|
+
// ai.chat was removed — consumers should use @ai-sdk/react useChat() directly
|
|
492
|
+
expect(client.ai).not.toHaveProperty('chat');
|
|
501
493
|
});
|
|
502
494
|
|
|
503
495
|
it('should get AI suggestions', async () => {
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
3
|
import { QueryAST, SortNode, AggregationNode, isFilterAST } from '@objectstack/spec/data';
|
|
4
|
-
import {
|
|
5
|
-
BatchUpdateRequest,
|
|
6
|
-
BatchUpdateResponse,
|
|
4
|
+
import {
|
|
5
|
+
BatchUpdateRequest,
|
|
6
|
+
BatchUpdateResponse,
|
|
7
7
|
UpdateManyRequest,
|
|
8
8
|
DeleteManyRequest,
|
|
9
9
|
BatchOptions,
|
|
@@ -62,8 +62,6 @@ import {
|
|
|
62
62
|
MarkAllNotificationsReadResponse,
|
|
63
63
|
AiNlqRequest,
|
|
64
64
|
AiNlqResponse,
|
|
65
|
-
AiChatRequest,
|
|
66
|
-
AiChatResponse,
|
|
67
65
|
AiSuggestRequest,
|
|
68
66
|
AiSuggestResponse,
|
|
69
67
|
AiInsightsRequest,
|
|
@@ -90,6 +88,7 @@ import {
|
|
|
90
88
|
ApiRoutes,
|
|
91
89
|
} from '@objectstack/spec/api';
|
|
92
90
|
import { Logger, createLogger } from '@objectstack/core';
|
|
91
|
+
import { RealtimeAPI } from './realtime-api';
|
|
93
92
|
|
|
94
93
|
/**
|
|
95
94
|
* Route types that the client can resolve.
|
|
@@ -230,18 +229,22 @@ export class ObjectStackClient {
|
|
|
230
229
|
private fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
231
230
|
private discoveryInfo?: DiscoveryResult;
|
|
232
231
|
private logger: Logger;
|
|
232
|
+
private realtimeAPI: RealtimeAPI;
|
|
233
233
|
|
|
234
234
|
constructor(config: ClientConfig) {
|
|
235
235
|
this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
236
236
|
this.token = config.token;
|
|
237
237
|
this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
|
|
238
|
-
|
|
238
|
+
|
|
239
239
|
// Initialize logger
|
|
240
|
-
this.logger = config.logger || createLogger({
|
|
240
|
+
this.logger = config.logger || createLogger({
|
|
241
241
|
level: config.debug ? 'debug' : 'info',
|
|
242
242
|
format: 'pretty'
|
|
243
243
|
});
|
|
244
|
-
|
|
244
|
+
|
|
245
|
+
// Initialize realtime API
|
|
246
|
+
this.realtimeAPI = new RealtimeAPI(this.baseUrl, this.token);
|
|
247
|
+
|
|
245
248
|
this.logger.debug('ObjectStack client created', { baseUrl: this.baseUrl });
|
|
246
249
|
}
|
|
247
250
|
|
|
@@ -361,10 +364,15 @@ export class ObjectStackClient {
|
|
|
361
364
|
* Get a specific metadata item by type and name
|
|
362
365
|
* @param type - Metadata type (e.g., 'object', 'plugin')
|
|
363
366
|
* @param name - Item name (snake_case identifier)
|
|
367
|
+
* @param options - Optional filters (e.g., packageId to scope by package)
|
|
364
368
|
*/
|
|
365
|
-
getItem: async (type: string, name: string) => {
|
|
369
|
+
getItem: async (type: string, name: string, options?: { packageId?: string }) => {
|
|
366
370
|
const route = this.getRoute('metadata');
|
|
367
|
-
const
|
|
371
|
+
const params = new URLSearchParams();
|
|
372
|
+
if (options?.packageId) params.set('package', options.packageId);
|
|
373
|
+
const qs = params.toString();
|
|
374
|
+
const url = `${this.baseUrl}${route}/${type}/${name}${qs ? `?${qs}` : ''}`;
|
|
375
|
+
const res = await this.fetch(url);
|
|
368
376
|
return this.unwrapResponse(res);
|
|
369
377
|
},
|
|
370
378
|
|
|
@@ -382,6 +390,19 @@ export class ObjectStackClient {
|
|
|
382
390
|
});
|
|
383
391
|
return this.unwrapResponse(res);
|
|
384
392
|
},
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Delete a metadata item
|
|
396
|
+
* @param type - Metadata type (e.g., 'object', 'plugin')
|
|
397
|
+
* @param name - Item name (snake_case identifier)
|
|
398
|
+
*/
|
|
399
|
+
deleteItem: async (type: string, name: string): Promise<{ type: string; name: string; deleted: boolean }> => {
|
|
400
|
+
const route = this.getRoute('metadata');
|
|
401
|
+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}`, {
|
|
402
|
+
method: 'DELETE',
|
|
403
|
+
});
|
|
404
|
+
return this.unwrapResponse(res);
|
|
405
|
+
},
|
|
385
406
|
|
|
386
407
|
/**
|
|
387
408
|
* Get object metadata with cache support
|
|
@@ -871,6 +892,14 @@ export class ObjectStackClient {
|
|
|
871
892
|
},
|
|
872
893
|
};
|
|
873
894
|
|
|
895
|
+
/**
|
|
896
|
+
* Event Subscription API
|
|
897
|
+
* Provides real-time event subscriptions for metadata and data changes
|
|
898
|
+
*/
|
|
899
|
+
get events() {
|
|
900
|
+
return this.realtimeAPI;
|
|
901
|
+
}
|
|
902
|
+
|
|
874
903
|
/**
|
|
875
904
|
* Permissions Services
|
|
876
905
|
*/
|
|
@@ -1209,17 +1238,7 @@ export class ObjectStackClient {
|
|
|
1209
1238
|
return this.unwrapResponse<AiNlqResponse>(res);
|
|
1210
1239
|
},
|
|
1211
1240
|
|
|
1212
|
-
|
|
1213
|
-
* Multi-turn AI chat
|
|
1214
|
-
*/
|
|
1215
|
-
chat: async (request: AiChatRequest): Promise<AiChatResponse> => {
|
|
1216
|
-
const route = this.getRoute('ai');
|
|
1217
|
-
const res = await this.fetch(`${this.baseUrl}${route}/chat`, {
|
|
1218
|
-
method: 'POST',
|
|
1219
|
-
body: JSON.stringify(request)
|
|
1220
|
-
});
|
|
1221
|
-
return this.unwrapResponse<AiChatResponse>(res);
|
|
1222
|
-
},
|
|
1241
|
+
// AI chat method removed — use Vercel AI SDK `useChat()` / `@ai-sdk/react` directly.
|
|
1223
1242
|
|
|
1224
1243
|
/**
|
|
1225
1244
|
* AI-powered field value suggestions
|
|
@@ -1783,6 +1802,9 @@ export class ObjectStackClient {
|
|
|
1783
1802
|
// Re-export type-safe query builder
|
|
1784
1803
|
export { QueryBuilder, FilterBuilder, createQuery, createFilter } from './query-builder';
|
|
1785
1804
|
|
|
1805
|
+
// Re-export realtime API types
|
|
1806
|
+
export { RealtimeAPI, RealtimeSubscriptionFilter, RealtimeEventHandler } from './realtime-api';
|
|
1807
|
+
|
|
1786
1808
|
// Re-export commonly used types from @objectstack/spec/api for convenience
|
|
1787
1809
|
export type {
|
|
1788
1810
|
BatchUpdateRequest,
|
|
@@ -1826,8 +1848,6 @@ export type {
|
|
|
1826
1848
|
ListNotificationsResponse,
|
|
1827
1849
|
AiNlqRequest,
|
|
1828
1850
|
AiNlqResponse,
|
|
1829
|
-
AiChatRequest,
|
|
1830
|
-
AiChatResponse,
|
|
1831
1851
|
AiSuggestRequest,
|
|
1832
1852
|
AiSuggestResponse,
|
|
1833
1853
|
AiInsightsRequest,
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Realtime API Module for ObjectStackClient
|
|
5
|
+
*
|
|
6
|
+
* Provides real-time event subscription capabilities using long-polling.
|
|
7
|
+
* For production WebSocket/SSE support, extend with transport adapters.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RealtimeEventPayload } from '@objectstack/spec/contracts';
|
|
11
|
+
import type { MetadataEvent, DataEvent } from '@objectstack/spec/api';
|
|
12
|
+
|
|
13
|
+
export interface RealtimeSubscriptionFilter {
|
|
14
|
+
/** Metadata/object type filter */
|
|
15
|
+
type?: string;
|
|
16
|
+
/** Package ID filter */
|
|
17
|
+
packageId?: string;
|
|
18
|
+
/** Event types to listen for */
|
|
19
|
+
eventTypes?: string[];
|
|
20
|
+
/** Record ID filter (for data events) */
|
|
21
|
+
recordId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type RealtimeEventHandler = (event: RealtimeEventPayload) => void;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Realtime API for subscribing to server events
|
|
28
|
+
*
|
|
29
|
+
* Note: Currently uses in-memory adapter. WebSocket/SSE transport planned for future.
|
|
30
|
+
*/
|
|
31
|
+
export class RealtimeAPI {
|
|
32
|
+
// @ts-expect-error - Reserved for future WebSocket/SSE implementation
|
|
33
|
+
private _baseUrl: string;
|
|
34
|
+
// @ts-expect-error - Reserved for future WebSocket/SSE implementation
|
|
35
|
+
private _token?: string;
|
|
36
|
+
private subscriptions = new Map<string, {
|
|
37
|
+
filter: RealtimeSubscriptionFilter;
|
|
38
|
+
handler: RealtimeEventHandler;
|
|
39
|
+
}>();
|
|
40
|
+
private pollInterval?: ReturnType<typeof setInterval>;
|
|
41
|
+
private eventBuffer: RealtimeEventPayload[] = [];
|
|
42
|
+
|
|
43
|
+
constructor(baseUrl: string, token?: string) {
|
|
44
|
+
this._baseUrl = baseUrl;
|
|
45
|
+
this._token = token;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to metadata events
|
|
50
|
+
* Returns an unsubscribe function
|
|
51
|
+
*/
|
|
52
|
+
subscribeMetadata(
|
|
53
|
+
type: string,
|
|
54
|
+
callback: (event: MetadataEvent) => void,
|
|
55
|
+
options?: { packageId?: string }
|
|
56
|
+
): () => void {
|
|
57
|
+
const subscriptionId = `metadata-${type}-${Date.now()}`;
|
|
58
|
+
|
|
59
|
+
this.subscriptions.set(subscriptionId, {
|
|
60
|
+
filter: {
|
|
61
|
+
type,
|
|
62
|
+
packageId: options?.packageId,
|
|
63
|
+
eventTypes: [
|
|
64
|
+
`metadata.${type}.created`,
|
|
65
|
+
`metadata.${type}.updated`,
|
|
66
|
+
`metadata.${type}.deleted`
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
handler: (event) => {
|
|
70
|
+
// Type guard and filter
|
|
71
|
+
if (event.type.startsWith('metadata.')) {
|
|
72
|
+
callback(event as any as MetadataEvent);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Start polling if not already started
|
|
78
|
+
this.startPolling();
|
|
79
|
+
|
|
80
|
+
// Return unsubscribe function
|
|
81
|
+
return () => {
|
|
82
|
+
this.subscriptions.delete(subscriptionId);
|
|
83
|
+
if (this.subscriptions.size === 0) {
|
|
84
|
+
this.stopPolling();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Subscribe to data record events
|
|
91
|
+
* Returns an unsubscribe function
|
|
92
|
+
*/
|
|
93
|
+
subscribeData(
|
|
94
|
+
object: string,
|
|
95
|
+
callback: (event: DataEvent) => void,
|
|
96
|
+
options?: { recordId?: string }
|
|
97
|
+
): () => void {
|
|
98
|
+
const subscriptionId = `data-${object}-${Date.now()}`;
|
|
99
|
+
|
|
100
|
+
this.subscriptions.set(subscriptionId, {
|
|
101
|
+
filter: {
|
|
102
|
+
type: object,
|
|
103
|
+
recordId: options?.recordId,
|
|
104
|
+
eventTypes: [
|
|
105
|
+
'data.record.created',
|
|
106
|
+
'data.record.updated',
|
|
107
|
+
'data.record.deleted'
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
handler: (event) => {
|
|
111
|
+
// Type guard and filter
|
|
112
|
+
if (event.type.startsWith('data.') && event.object === object) {
|
|
113
|
+
if (!options?.recordId || (event.payload as any)?.recordId === options.recordId) {
|
|
114
|
+
callback(event as any as DataEvent);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Start polling if not already started
|
|
121
|
+
this.startPolling();
|
|
122
|
+
|
|
123
|
+
// Return unsubscribe function
|
|
124
|
+
return () => {
|
|
125
|
+
this.subscriptions.delete(subscriptionId);
|
|
126
|
+
if (this.subscriptions.size === 0) {
|
|
127
|
+
this.stopPolling();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Emit an event to all matching subscriptions (client-side only)
|
|
134
|
+
* This is used for in-process event delivery
|
|
135
|
+
*/
|
|
136
|
+
private emitEvent(event: RealtimeEventPayload): void {
|
|
137
|
+
for (const sub of this.subscriptions.values()) {
|
|
138
|
+
// Check if event matches subscription filters
|
|
139
|
+
const matchesType = !sub.filter.type ||
|
|
140
|
+
event.type.includes(sub.filter.type) ||
|
|
141
|
+
event.object === sub.filter.type;
|
|
142
|
+
|
|
143
|
+
const matchesEventType = !sub.filter.eventTypes?.length ||
|
|
144
|
+
sub.filter.eventTypes.includes(event.type);
|
|
145
|
+
|
|
146
|
+
const matchesPackage = !sub.filter.packageId ||
|
|
147
|
+
(event.payload as any)?.packageId === sub.filter.packageId;
|
|
148
|
+
|
|
149
|
+
if (matchesType && matchesEventType && matchesPackage) {
|
|
150
|
+
try {
|
|
151
|
+
sub.handler(event);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('Error in realtime event handler:', error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Start polling for events (fallback mechanism)
|
|
161
|
+
* In production, this would be replaced with WebSocket/SSE
|
|
162
|
+
*/
|
|
163
|
+
private startPolling(): void {
|
|
164
|
+
if (this.pollInterval) return;
|
|
165
|
+
|
|
166
|
+
// For now, we rely on the in-memory adapter within the same process
|
|
167
|
+
// Events are delivered synchronously via the IRealtimeService
|
|
168
|
+
// This polling is a placeholder for future WebSocket/SSE implementation
|
|
169
|
+
|
|
170
|
+
// Poll every 2 seconds for buffered events
|
|
171
|
+
this.pollInterval = setInterval(() => {
|
|
172
|
+
// Process any buffered events
|
|
173
|
+
while (this.eventBuffer.length > 0) {
|
|
174
|
+
const event = this.eventBuffer.shift();
|
|
175
|
+
if (event) {
|
|
176
|
+
this.emitEvent(event);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}, 2000);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Stop polling for events
|
|
184
|
+
*/
|
|
185
|
+
private stopPolling(): void {
|
|
186
|
+
if (this.pollInterval) {
|
|
187
|
+
clearInterval(this.pollInterval);
|
|
188
|
+
this.pollInterval = undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Internal method to buffer events from server
|
|
194
|
+
* This would be called by WebSocket/SSE handlers in production
|
|
195
|
+
*/
|
|
196
|
+
_bufferEvent(event: RealtimeEventPayload): void {
|
|
197
|
+
this.eventBuffer.push(event);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Disconnect and clean up all subscriptions
|
|
202
|
+
*/
|
|
203
|
+
disconnect(): void {
|
|
204
|
+
this.stopPolling();
|
|
205
|
+
this.subscriptions.clear();
|
|
206
|
+
this.eventBuffer = [];
|
|
207
|
+
}
|
|
208
|
+
}
|