@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.
@@ -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
- });
@@ -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
- });