@objectstack/client 4.0.4 → 4.0.5
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/dist/index.d.mts +864 -5
- package/dist/index.d.ts +864 -5
- package/dist/index.js +1267 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1265 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +38 -13
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -645
- package/CLIENT_SERVER_INTEGRATION_TESTS.md +0 -939
- package/CLIENT_SPEC_COMPLIANCE.md +0 -361
- package/src/client.feed.test.ts +0 -273
- package/src/client.hono.test.ts +0 -169
- package/src/client.msw.test.ts +0 -223
- package/src/client.test.ts +0 -891
- package/src/index.ts +0 -1889
- package/src/query-builder.ts +0 -337
- package/src/realtime-api.ts +0 -208
- package/tests/integration/01-discovery.test.ts +0 -68
- package/tests/integration/README.md +0 -72
- package/tsconfig.json +0 -11
- package/vitest.config.ts +0 -13
- package/vitest.integration.config.ts +0 -18
package/src/client.hono.test.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import { LiteKernel } from '@objectstack/core';
|
|
3
|
-
import { ObjectQL, ObjectQLPlugin, SchemaRegistry } from '@objectstack/objectql';
|
|
4
|
-
import { InMemoryDriver } from '@objectstack/driver-memory';
|
|
5
|
-
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
|
|
6
|
-
import { ObjectStackClient } from './index';
|
|
7
|
-
|
|
8
|
-
describe('ObjectStackClient (with Hono Server)', () => {
|
|
9
|
-
let baseUrl: string;
|
|
10
|
-
let kernel: LiteKernel;
|
|
11
|
-
|
|
12
|
-
beforeAll(async () => {
|
|
13
|
-
// 1. Setup Kernel
|
|
14
|
-
kernel = new LiteKernel();
|
|
15
|
-
kernel.use(new ObjectQLPlugin());
|
|
16
|
-
|
|
17
|
-
// 2. Setup Hono Plugin
|
|
18
|
-
const honoPlugin = new HonoServerPlugin({
|
|
19
|
-
port: 0,
|
|
20
|
-
registerStandardEndpoints: true
|
|
21
|
-
});
|
|
22
|
-
kernel.use(honoPlugin);
|
|
23
|
-
|
|
24
|
-
// --- BROKER SHIM START ---
|
|
25
|
-
// HttpDispatcher requires a broker to function. We inject a simple shim.
|
|
26
|
-
(kernel as any).broker = {
|
|
27
|
-
call: async (action: string, params: any, opts: any) => {
|
|
28
|
-
const parts = action.split('.');
|
|
29
|
-
const service = parts[0];
|
|
30
|
-
const method = parts[1];
|
|
31
|
-
|
|
32
|
-
if (service === 'data') {
|
|
33
|
-
const ql = kernel.getService<any>('objectql'); // Use 'objectql' service name for clarity
|
|
34
|
-
// Delegate to protocol service when available for proper expand/populate support
|
|
35
|
-
let protocol: any;
|
|
36
|
-
try { protocol = kernel.getService<any>('protocol'); } catch { /* not registered */ }
|
|
37
|
-
if (method === 'create') {
|
|
38
|
-
const res = await ql.insert(params.object, params.data);
|
|
39
|
-
const record = { ...params.data, ...res };
|
|
40
|
-
return { object: params.object, id: record.id, record };
|
|
41
|
-
}
|
|
42
|
-
// Params from HttpDispatcher: { object, id, ...query }
|
|
43
|
-
if (method === 'get') {
|
|
44
|
-
if (protocol) {
|
|
45
|
-
return await protocol.getData({ object: params.object, id: params.id, expand: params.expand, select: params.select });
|
|
46
|
-
}
|
|
47
|
-
const record = await ql.findOne(params.object, { where: { id: params.id } });
|
|
48
|
-
return record ? { object: params.object, id: params.id, record } : null;
|
|
49
|
-
}
|
|
50
|
-
// Params from HttpDispatcher: { object, filters }
|
|
51
|
-
if (method === 'query') {
|
|
52
|
-
if (protocol) {
|
|
53
|
-
return await protocol.findData({ object: params.object, query: params.query || params.filters });
|
|
54
|
-
}
|
|
55
|
-
const records = await ql.find(params.object, { where: params.filters });
|
|
56
|
-
return { object: params.object, records, total: records.length };
|
|
57
|
-
}
|
|
58
|
-
if (method === 'find') {
|
|
59
|
-
if (protocol) {
|
|
60
|
-
return await protocol.findData({ object: params.object, query: params.query || params.filters });
|
|
61
|
-
}
|
|
62
|
-
const records = await ql.find(params.object, { where: params.filters });
|
|
63
|
-
return { object: params.object, records, total: records.length };
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (service === 'metadata') {
|
|
68
|
-
// ObjectQLPlugin registers itself as 'metadata' but doesn't implement all broker methods directly
|
|
69
|
-
// We use SchemaRegistry for lookups
|
|
70
|
-
if (method === 'getObject') {
|
|
71
|
-
return SchemaRegistry.getObject(params.objectName);
|
|
72
|
-
}
|
|
73
|
-
if (method === 'objects') {
|
|
74
|
-
return SchemaRegistry.getAllObjects();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (service === 'auth' && method === 'login') {
|
|
79
|
-
return { token: 'mock-token', user: { id: 'admin', name: 'Admin' } };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
console.warn(`[BrokerShim] Action not implemented: ${action}`);
|
|
83
|
-
throw new Error(`Action ${action} not implemented in shim`);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
// --- BROKER SHIM END ---
|
|
87
|
-
|
|
88
|
-
await kernel.bootstrap();
|
|
89
|
-
|
|
90
|
-
// 3. Setup Driver
|
|
91
|
-
const ql = kernel.getService<ObjectQL>('objectql');
|
|
92
|
-
ql.registerDriver(new InMemoryDriver(), true);
|
|
93
|
-
|
|
94
|
-
// 4. Load Metadata (Schema)
|
|
95
|
-
SchemaRegistry.registerObject({
|
|
96
|
-
name: 'customer',
|
|
97
|
-
label: 'Customer',
|
|
98
|
-
fields: {
|
|
99
|
-
name: { type: 'text', label: 'Name' },
|
|
100
|
-
email: { type: 'text', label: 'Email' }
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// 5. Get Port from Service
|
|
105
|
-
const httpServer = kernel.getService<any>('http.server');
|
|
106
|
-
const port = httpServer.getPort();
|
|
107
|
-
baseUrl = `http://localhost:${port}`;
|
|
108
|
-
|
|
109
|
-
console.log(`Test server running at ${baseUrl}`);
|
|
110
|
-
}, 30_000);
|
|
111
|
-
|
|
112
|
-
afterAll(async () => {
|
|
113
|
-
if (kernel) {
|
|
114
|
-
// Race shutdown against a hard deadline.
|
|
115
|
-
// kernel.shutdown() can hang when pino's flush callback never fires
|
|
116
|
-
// in CI (worker-thread transport timing issues), so cap the wait.
|
|
117
|
-
await Promise.race([
|
|
118
|
-
kernel.shutdown(),
|
|
119
|
-
new Promise<void>((resolve) => setTimeout(resolve, 10_000)),
|
|
120
|
-
]);
|
|
121
|
-
}
|
|
122
|
-
}, 30_000);
|
|
123
|
-
|
|
124
|
-
it('should connect to hono server and discover endpoints', async () => {
|
|
125
|
-
const client = new ObjectStackClient({ baseUrl });
|
|
126
|
-
await client.connect();
|
|
127
|
-
|
|
128
|
-
// Client should have populated discovery info
|
|
129
|
-
expect(client['discoveryInfo']).toBeDefined();
|
|
130
|
-
|
|
131
|
-
// Verify endpoints from valid discovery response
|
|
132
|
-
// Standard: /api/v1/data, /api/v1/meta, etc.
|
|
133
|
-
const endpoints = client['discoveryInfo']!.routes;
|
|
134
|
-
expect(endpoints.data).toContain('/api/v1/data');
|
|
135
|
-
expect(endpoints.metadata).toContain('/api/v1/meta');
|
|
136
|
-
expect(endpoints.auth).toContain('/api/v1/auth');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should create and retrieve data via hono', async () => {
|
|
140
|
-
const client = new ObjectStackClient({ baseUrl });
|
|
141
|
-
await client.connect();
|
|
142
|
-
|
|
143
|
-
// Create — Spec: CreateDataResponse = { object, id, record }
|
|
144
|
-
const createdResponse = await client.data.create('customer', {
|
|
145
|
-
name: 'Hono User',
|
|
146
|
-
email: 'hono@example.com'
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
expect(createdResponse.record.name).toBe('Hono User');
|
|
150
|
-
expect(createdResponse.id).toBeDefined();
|
|
151
|
-
|
|
152
|
-
// Retrieve — Spec: GetDataResponse = { object, id, record }
|
|
153
|
-
const retrievedResponse = await client.data.get('customer', createdResponse.id);
|
|
154
|
-
expect(retrievedResponse.record.name).toBe('Hono User');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should find data via hono', async () => {
|
|
158
|
-
const client = new ObjectStackClient({ baseUrl });
|
|
159
|
-
await client.connect();
|
|
160
|
-
|
|
161
|
-
// Spec: FindDataResponse = { object, records, total? }
|
|
162
|
-
const resultsResponse = await client.data.find('customer', {
|
|
163
|
-
where: { name: 'Hono User' }
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
expect(resultsResponse.records.length).toBeGreaterThan(0);
|
|
167
|
-
expect(resultsResponse.records[0].name).toBe('Hono User');
|
|
168
|
-
});
|
|
169
|
-
});
|
package/src/client.msw.test.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
2
|
-
import { setupServer } from 'msw/node';
|
|
3
|
-
import { http, HttpResponse } from 'msw';
|
|
4
|
-
import { LiteKernel } from '@objectstack/core';
|
|
5
|
-
import { ObjectQLPlugin, SchemaRegistry } from '@objectstack/objectql';
|
|
6
|
-
import { InMemoryDriver } from '@objectstack/driver-memory';
|
|
7
|
-
import { MSWPlugin } from '@objectstack/plugin-msw';
|
|
8
|
-
import { ObjectStackClient } from './index';
|
|
9
|
-
|
|
10
|
-
// 127.0.0.1 usage logic remains
|
|
11
|
-
const BASE_URL = 'http://127.0.0.1';
|
|
12
|
-
|
|
13
|
-
describe('ObjectStackClient (with MSW Plugin)', () => {
|
|
14
|
-
let kernel: LiteKernel;
|
|
15
|
-
let mswPlugin: MSWPlugin;
|
|
16
|
-
let server: any;
|
|
17
|
-
|
|
18
|
-
beforeAll(async () => {
|
|
19
|
-
// 1. Setup Kernel
|
|
20
|
-
kernel = new LiteKernel();
|
|
21
|
-
kernel.use(new ObjectQLPlugin());
|
|
22
|
-
|
|
23
|
-
// 2. Setup MSW Plugin (headless mode)
|
|
24
|
-
mswPlugin = new MSWPlugin({
|
|
25
|
-
enableBrowser: false,
|
|
26
|
-
// baseUrl: '/api/v1' // Default is /api/v1
|
|
27
|
-
});
|
|
28
|
-
kernel.use(mswPlugin);
|
|
29
|
-
|
|
30
|
-
// --- BROKER SHIM START ---
|
|
31
|
-
// HttpDispatcher requires a broker to function. We inject a simple shim.
|
|
32
|
-
(kernel as any).broker = {
|
|
33
|
-
call: async (action: string, params: any, opts: any) => {
|
|
34
|
-
const parts = action.split('.');
|
|
35
|
-
const service = parts[0];
|
|
36
|
-
const method = parts[1];
|
|
37
|
-
|
|
38
|
-
if (service === 'data') {
|
|
39
|
-
const ql = kernel.getService<any>('objectql');
|
|
40
|
-
// Delegate to protocol service when available for proper expand/populate support
|
|
41
|
-
let protocol: any;
|
|
42
|
-
try { protocol = kernel.getService<any>('protocol'); } catch { /* not registered */ }
|
|
43
|
-
if (method === 'create') {
|
|
44
|
-
const res = await ql.insert(params.object, params.data);
|
|
45
|
-
const record = { ...params.data, ...res };
|
|
46
|
-
return { object: params.object, id: record.id, record };
|
|
47
|
-
}
|
|
48
|
-
if (method === 'get') {
|
|
49
|
-
if (protocol) {
|
|
50
|
-
return await protocol.getData({ object: params.object, id: params.id, expand: params.expand, select: params.select });
|
|
51
|
-
}
|
|
52
|
-
const record = await ql.findOne(params.object, { where: { id: params.id } });
|
|
53
|
-
return record ? { object: params.object, id: params.id, record } : null;
|
|
54
|
-
}
|
|
55
|
-
if (method === 'query') {
|
|
56
|
-
if (protocol) {
|
|
57
|
-
return await protocol.findData({ object: params.object, query: params.query });
|
|
58
|
-
}
|
|
59
|
-
const queryOpts = params.query || {};
|
|
60
|
-
const records = await ql.find(params.object, { where: queryOpts.filters || queryOpts.filter || queryOpts.where });
|
|
61
|
-
return { object: params.object, records, total: records.length };
|
|
62
|
-
}
|
|
63
|
-
if (method === 'find') {
|
|
64
|
-
if (protocol) {
|
|
65
|
-
return await protocol.findData({ object: params.object, query: params.query });
|
|
66
|
-
}
|
|
67
|
-
const queryOpts = params.query || {};
|
|
68
|
-
const records = await ql.find(params.object, { where: queryOpts.filters || queryOpts.filter || queryOpts.where });
|
|
69
|
-
return { object: params.object, records, total: records.length };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (service === 'metadata') {
|
|
74
|
-
if (method === 'getObject') return SchemaRegistry.getObject(params.objectName);
|
|
75
|
-
if (method === 'objects') return SchemaRegistry.getAllObjects();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
console.warn(`[BrokerShim] Action not implemented: ${action}`);
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
// --- BROKER SHIM END ---
|
|
83
|
-
|
|
84
|
-
await kernel.bootstrap();
|
|
85
|
-
|
|
86
|
-
// 3. Setup Driver & Schema
|
|
87
|
-
const ql = kernel.getService<any>('objectql');
|
|
88
|
-
ql.registerDriver(new InMemoryDriver(), true);
|
|
89
|
-
|
|
90
|
-
SchemaRegistry.registerObject({
|
|
91
|
-
name: 'customer',
|
|
92
|
-
fields: {
|
|
93
|
-
name: { type: 'text' }
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Add some data
|
|
98
|
-
await ql.insert('customer', { id: '101', name: 'Alice' });
|
|
99
|
-
await ql.insert('customer', { id: '102', name: 'Bob' });
|
|
100
|
-
|
|
101
|
-
// 4. Get handlers and start MSW Node Server
|
|
102
|
-
const handlers = mswPlugin.getHandlers();
|
|
103
|
-
|
|
104
|
-
// Manual handlers to cover gaps in MSWPlugin generation
|
|
105
|
-
handlers.push(
|
|
106
|
-
http.get('http://127.0.0.1/api/v1', () => {
|
|
107
|
-
return HttpResponse.json({
|
|
108
|
-
name: 'ObjectOS',
|
|
109
|
-
version: '1.0.0',
|
|
110
|
-
routes: {
|
|
111
|
-
data: '/api/v1/data',
|
|
112
|
-
metadata: '/api/v1/meta',
|
|
113
|
-
auth: '/api/v1/auth'
|
|
114
|
-
},
|
|
115
|
-
capabilities: ['data', 'metadata'],
|
|
116
|
-
features: {}
|
|
117
|
-
});
|
|
118
|
-
}),
|
|
119
|
-
|
|
120
|
-
http.get('http://127.0.0.1/api/v1/meta/object/:name', async ({ params }) => {
|
|
121
|
-
try {
|
|
122
|
-
const res = await (kernel as any).broker.call('metadata.getObject', { objectName: params.name });
|
|
123
|
-
return HttpResponse.json({ success: true, data: res });
|
|
124
|
-
} catch (e: any) { return HttpResponse.json({ success: false, error: e.message }, { status: 404 }); }
|
|
125
|
-
}),
|
|
126
|
-
|
|
127
|
-
http.get('http://127.0.0.1/api/v1/data/:object', async ({ params }) => {
|
|
128
|
-
try {
|
|
129
|
-
// Simplifying: ignoring query filters for this test
|
|
130
|
-
const res = await (kernel as any).broker.call('data.find', { object: params.object, filters: {} });
|
|
131
|
-
return HttpResponse.json({ success: true, data: res });
|
|
132
|
-
} catch (e: any) { return HttpResponse.json({ success: false, error: e.message }, { status: 500 }); }
|
|
133
|
-
}),
|
|
134
|
-
|
|
135
|
-
http.post('http://127.0.0.1/api/v1/data/:object', async ({ params, request }) => {
|
|
136
|
-
try {
|
|
137
|
-
const body = await request.json();
|
|
138
|
-
const res = await (kernel as any).broker.call('data.create', { object: params.object, data: body });
|
|
139
|
-
return HttpResponse.json({ success: true, data: res }, { status: 201 });
|
|
140
|
-
} catch (e: any) { return HttpResponse.json({ success: false, error: e.message }, { status: 500 }); }
|
|
141
|
-
}),
|
|
142
|
-
|
|
143
|
-
http.get('http://127.0.0.1/api/v1/data/:object/:id', async ({ params }) => {
|
|
144
|
-
try {
|
|
145
|
-
const res = await (kernel as any).broker.call('data.get', { object: params.object, id: params.id });
|
|
146
|
-
return HttpResponse.json({ success: true, data: res });
|
|
147
|
-
} catch (e: any) { return HttpResponse.json({ success: false, error: e.message }, { status: 404 }); }
|
|
148
|
-
})
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
server = setupServer(...handlers);
|
|
152
|
-
server.listen({ onUnhandledRequest: 'error' });
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Reset handlers after each test to ensure clean state
|
|
156
|
-
afterEach(() => server.resetHandlers());
|
|
157
|
-
|
|
158
|
-
afterAll(async () => {
|
|
159
|
-
if (server) server.close();
|
|
160
|
-
if (kernel) await kernel.shutdown();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('should connect and discover endpoints', async () => {
|
|
164
|
-
// MSWPlugin configured with baseUrl: '' creates handlers at root.
|
|
165
|
-
// Client connects to /api/v1.
|
|
166
|
-
// To make them match in THIS test file where I used baseUrl: '',
|
|
167
|
-
// I should have configured MSWPlugin with baseUrl: '/api/v1' or left default.
|
|
168
|
-
|
|
169
|
-
// RE-FIXING SETUP in beforeAll (via edit).
|
|
170
|
-
// If I change MSWPlugin config to default ('/api/v1'),
|
|
171
|
-
// then Client(BASE_URL).connect() -> fetches BASE_URL/api/v1 -> matches MSW handler /api/v1.
|
|
172
|
-
|
|
173
|
-
const client = new ObjectStackClient({ baseUrl: BASE_URL });
|
|
174
|
-
await client.connect();
|
|
175
|
-
|
|
176
|
-
expect(client['discoveryInfo']).toBeDefined();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should fetch object metadata', async () => {
|
|
180
|
-
const client = new ObjectStackClient({ baseUrl: BASE_URL });
|
|
181
|
-
await client.connect();
|
|
182
|
-
|
|
183
|
-
// Spec: GetMetaItemResponse = { type, name, item }
|
|
184
|
-
const customerRes: any = await client.meta.getItem('object', 'customer');
|
|
185
|
-
expect(customerRes).toBeDefined();
|
|
186
|
-
// After unwrapResponse, we get the protocol-level response
|
|
187
|
-
// The manual handler wraps as { success, data: schema }, so unwrap yields the schema
|
|
188
|
-
const schema = customerRes.item || customerRes;
|
|
189
|
-
expect(schema.name).toBe('customer');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should find data records', async () => {
|
|
193
|
-
const client = new ObjectStackClient({ baseUrl: BASE_URL });
|
|
194
|
-
await client.connect();
|
|
195
|
-
|
|
196
|
-
// Spec: FindDataResponse = { object, records, total? }
|
|
197
|
-
const resultsRes = await client.data.find('customer');
|
|
198
|
-
expect(resultsRes.records).toBeDefined();
|
|
199
|
-
expect(resultsRes.records.length).toBe(2);
|
|
200
|
-
expect(resultsRes.records[0].name).toBe('Alice');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('should get single record', async () => {
|
|
204
|
-
const client = new ObjectStackClient({ baseUrl: BASE_URL });
|
|
205
|
-
await client.connect();
|
|
206
|
-
|
|
207
|
-
// Spec: GetDataResponse = { object, id, record }
|
|
208
|
-
const recordRes = await client.data.get('customer', '101');
|
|
209
|
-
expect(recordRes.record).toBeDefined();
|
|
210
|
-
expect(recordRes.record.name).toBe('Alice');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('should create record', async () => {
|
|
214
|
-
const client = new ObjectStackClient({ baseUrl: BASE_URL });
|
|
215
|
-
await client.connect();
|
|
216
|
-
|
|
217
|
-
// Spec: CreateDataResponse = { object, id, record }
|
|
218
|
-
const newRecordRes = await client.data.create('customer', { name: 'Charlie' });
|
|
219
|
-
expect(newRecordRes.record).toBeDefined();
|
|
220
|
-
expect(newRecordRes.record.name).toBe('Charlie');
|
|
221
|
-
expect(newRecordRes.id).toBeDefined();
|
|
222
|
-
});
|
|
223
|
-
});
|