@objectstack/client 4.0.4 → 4.1.0
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 +979 -5
- package/dist/index.d.ts +979 -5
- package/dist/index.js +1419 -42
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1417 -41
- 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
|
@@ -1,939 +0,0 @@
|
|
|
1
|
-
# @objectstack/client - Server Integration Test Specification
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This document defines comprehensive integration tests for validating `@objectstack/client` against a live ObjectStack server implementation. These tests verify that the client SDK correctly communicates with the server across all API namespaces.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Test Environment Setup
|
|
10
|
-
|
|
11
|
-
### Prerequisites
|
|
12
|
-
|
|
13
|
-
1. **Server Requirements:**
|
|
14
|
-
- ObjectStack server instance running
|
|
15
|
-
- Test database (SQLite/Postgres) with sample data
|
|
16
|
-
- All core services enabled (metadata, data, auth)
|
|
17
|
-
- Optional services enabled (workflow, ai, realtime, etc.)
|
|
18
|
-
|
|
19
|
-
2. **Client Configuration:**
|
|
20
|
-
```typescript
|
|
21
|
-
const testConfig: ClientConfig = {
|
|
22
|
-
baseUrl: process.env.TEST_SERVER_URL || 'http://localhost:3000',
|
|
23
|
-
token: undefined, // Will be set after login
|
|
24
|
-
debug: true,
|
|
25
|
-
logger: createLogger({ level: 'debug' })
|
|
26
|
-
};
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
3. **Test Data:**
|
|
30
|
-
- Sample objects: `test_contact`, `test_project`, `test_task`
|
|
31
|
-
- Sample users: test@example.com (admin), user@example.com (standard)
|
|
32
|
-
- Sample packages: `@test/sample-plugin`
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Test Suite Structure
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
packages/client/tests/integration/
|
|
40
|
-
├── 01-discovery.test.ts # Discovery & connection
|
|
41
|
-
├── 02-auth.test.ts # Authentication flows
|
|
42
|
-
├── 03-metadata.test.ts # Metadata operations
|
|
43
|
-
├── 04-data-crud.test.ts # Basic CRUD operations
|
|
44
|
-
├── 05-data-batch.test.ts # Batch operations
|
|
45
|
-
├── 06-data-query.test.ts # Advanced queries
|
|
46
|
-
├── 07-permissions.test.ts # Permission checking
|
|
47
|
-
├── 08-workflow.test.ts # Workflow operations
|
|
48
|
-
├── 09-realtime.test.ts # Realtime subscriptions
|
|
49
|
-
├── 10-notifications.test.ts # Notifications
|
|
50
|
-
├── 11-ai.test.ts # AI services
|
|
51
|
-
├── 12-i18n.test.ts # Internationalization
|
|
52
|
-
├── 13-analytics.test.ts # Analytics queries
|
|
53
|
-
├── 14-packages.test.ts # Package management
|
|
54
|
-
├── 15-views.test.ts # View management
|
|
55
|
-
├── 16-storage.test.ts # File storage
|
|
56
|
-
├── 17-automation.test.ts # Automation triggers
|
|
57
|
-
└── helpers/
|
|
58
|
-
├── test-server.ts # Mock/stub server helpers
|
|
59
|
-
├── test-data.ts # Test data generators
|
|
60
|
-
└── assertions.ts # Custom assertions
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## Test Cases
|
|
66
|
-
|
|
67
|
-
### 1. Discovery & Connection (`01-discovery.test.ts`)
|
|
68
|
-
|
|
69
|
-
#### TC-DISC-001: Standard Discovery via .well-known
|
|
70
|
-
```typescript
|
|
71
|
-
describe('Discovery via .well-known', () => {
|
|
72
|
-
test('should discover API from .well-known/objectstack', async () => {
|
|
73
|
-
const client = new ObjectStackClient({
|
|
74
|
-
baseUrl: 'http://localhost:3000'
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const discovery = await client.connect();
|
|
78
|
-
|
|
79
|
-
expect(discovery.version).toBe('v1');
|
|
80
|
-
expect(discovery.apiName).toBe('ObjectStack');
|
|
81
|
-
expect(discovery.capabilities).toBeDefined();
|
|
82
|
-
expect(discovery.endpoints).toBeDefined();
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
#### TC-DISC-002: Fallback Discovery via /api/v1
|
|
88
|
-
```typescript
|
|
89
|
-
test('should fallback to /api/v1 when .well-known unavailable', async () => {
|
|
90
|
-
// Mock .well-known to return 404
|
|
91
|
-
mockServer.get('/.well-known/objectstack').reply(404);
|
|
92
|
-
mockServer.get('/api/v1').reply(200, {
|
|
93
|
-
version: 'v1',
|
|
94
|
-
apiName: 'ObjectStack'
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const client = new ObjectStackClient({ baseUrl: mockServerUrl });
|
|
98
|
-
const discovery = await client.connect();
|
|
99
|
-
|
|
100
|
-
expect(discovery.version).toBe('v1');
|
|
101
|
-
});
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
#### TC-DISC-003: Connection Failure Handling
|
|
105
|
-
```typescript
|
|
106
|
-
test('should throw error when both discovery methods fail', async () => {
|
|
107
|
-
mockServer.get('/.well-known/objectstack').reply(404);
|
|
108
|
-
mockServer.get('/api/v1').reply(503);
|
|
109
|
-
|
|
110
|
-
const client = new ObjectStackClient({ baseUrl: mockServerUrl });
|
|
111
|
-
|
|
112
|
-
await expect(client.connect()).rejects.toThrow(/Failed to connect/);
|
|
113
|
-
});
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
### 2. Authentication (`02-auth.test.ts`)
|
|
119
|
-
|
|
120
|
-
#### TC-AUTH-001: Email/Password Login
|
|
121
|
-
```typescript
|
|
122
|
-
test('should login with email and password', async () => {
|
|
123
|
-
const client = new ObjectStackClient({ baseUrl: testServerUrl });
|
|
124
|
-
|
|
125
|
-
const session = await client.auth.login({
|
|
126
|
-
method: 'email',
|
|
127
|
-
email: 'test@example.com',
|
|
128
|
-
password: 'TestPassword123!'
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
expect(session.token).toBeDefined();
|
|
132
|
-
expect(session.user).toBeDefined();
|
|
133
|
-
expect(session.user.email).toBe('test@example.com');
|
|
134
|
-
expect(session.expiresAt).toBeDefined();
|
|
135
|
-
});
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
#### TC-AUTH-002: Registration
|
|
139
|
-
```typescript
|
|
140
|
-
test('should register new user account', async () => {
|
|
141
|
-
const client = new ObjectStackClient({ baseUrl: testServerUrl });
|
|
142
|
-
|
|
143
|
-
const session = await client.auth.register({
|
|
144
|
-
email: 'newuser@example.com',
|
|
145
|
-
password: 'SecurePass123!',
|
|
146
|
-
firstName: 'New',
|
|
147
|
-
lastName: 'User'
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
expect(session.token).toBeDefined();
|
|
151
|
-
expect(session.user.email).toBe('newuser@example.com');
|
|
152
|
-
});
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
#### TC-AUTH-003: Token Refresh
|
|
156
|
-
```typescript
|
|
157
|
-
test('should refresh expired token', async () => {
|
|
158
|
-
const client = new ObjectStackClient({
|
|
159
|
-
baseUrl: testServerUrl,
|
|
160
|
-
token: expiredToken
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const newSession = await client.auth.refreshToken({
|
|
164
|
-
refreshToken: validRefreshToken
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
expect(newSession.token).not.toBe(expiredToken);
|
|
168
|
-
expect(newSession.expiresAt).toBeGreaterThan(Date.now());
|
|
169
|
-
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
#### TC-AUTH-004: Get Current User
|
|
173
|
-
```typescript
|
|
174
|
-
test('should get current authenticated user', async () => {
|
|
175
|
-
const client = new ObjectStackClient({
|
|
176
|
-
baseUrl: testServerUrl,
|
|
177
|
-
token: validToken
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const user = await client.auth.me();
|
|
181
|
-
|
|
182
|
-
expect(user.id).toBeDefined();
|
|
183
|
-
expect(user.email).toBe('test@example.com');
|
|
184
|
-
expect(user.roles).toContain('admin');
|
|
185
|
-
});
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
#### TC-AUTH-005: Logout
|
|
189
|
-
```typescript
|
|
190
|
-
test('should logout and invalidate session', async () => {
|
|
191
|
-
const client = new ObjectStackClient({
|
|
192
|
-
baseUrl: testServerUrl,
|
|
193
|
-
token: validToken
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
await client.auth.logout();
|
|
197
|
-
|
|
198
|
-
// Subsequent requests should fail with 401
|
|
199
|
-
await expect(client.auth.me()).rejects.toThrow(/Unauthorized/);
|
|
200
|
-
});
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
---
|
|
204
|
-
|
|
205
|
-
### 3. Metadata Operations (`03-metadata.test.ts`)
|
|
206
|
-
|
|
207
|
-
#### TC-META-001: Get Metadata Types
|
|
208
|
-
```typescript
|
|
209
|
-
test('should retrieve all metadata types', async () => {
|
|
210
|
-
const client = await createAuthenticatedClient();
|
|
211
|
-
|
|
212
|
-
const types = await client.meta.getTypes();
|
|
213
|
-
|
|
214
|
-
expect(types.types).toContain('object');
|
|
215
|
-
expect(types.types).toContain('plugin');
|
|
216
|
-
expect(types.types).toContain('view');
|
|
217
|
-
expect(types.types).toContain('workflow');
|
|
218
|
-
});
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
#### TC-META-002: Get Items of Type
|
|
222
|
-
```typescript
|
|
223
|
-
test('should retrieve all objects', async () => {
|
|
224
|
-
const client = await createAuthenticatedClient();
|
|
225
|
-
|
|
226
|
-
const objects = await client.meta.getItems('object');
|
|
227
|
-
|
|
228
|
-
expect(objects.items).toBeDefined();
|
|
229
|
-
expect(objects.items.length).toBeGreaterThan(0);
|
|
230
|
-
expect(objects.items[0].name).toBeDefined();
|
|
231
|
-
expect(objects.items[0].label).toBeDefined();
|
|
232
|
-
});
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
#### TC-META-003: Get Specific Object Definition
|
|
236
|
-
```typescript
|
|
237
|
-
test('should retrieve object definition by name', async () => {
|
|
238
|
-
const client = await createAuthenticatedClient();
|
|
239
|
-
|
|
240
|
-
const contactObject = await client.meta.getItem('object', 'test_contact');
|
|
241
|
-
|
|
242
|
-
expect(contactObject.name).toBe('test_contact');
|
|
243
|
-
expect(contactObject.label).toBe('Contact');
|
|
244
|
-
expect(contactObject.fields).toBeDefined();
|
|
245
|
-
expect(contactObject.fields.first_name).toBeDefined();
|
|
246
|
-
expect(contactObject.fields.first_name.type).toBe('text');
|
|
247
|
-
});
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
#### TC-META-004: Save Object Definition
|
|
251
|
-
```typescript
|
|
252
|
-
test('should create/update object definition', async () => {
|
|
253
|
-
const client = await createAuthenticatedClient();
|
|
254
|
-
|
|
255
|
-
const newObject = {
|
|
256
|
-
name: 'test_dynamic',
|
|
257
|
-
label: 'Dynamic Test',
|
|
258
|
-
fields: {
|
|
259
|
-
name: { type: 'text', label: 'Name', required: true },
|
|
260
|
-
status: { type: 'select', label: 'Status', options: ['active', 'inactive'] }
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const saved = await client.meta.saveItem('object', 'test_dynamic', newObject);
|
|
265
|
-
|
|
266
|
-
expect(saved.name).toBe('test_dynamic');
|
|
267
|
-
expect(saved.fields.name).toBeDefined();
|
|
268
|
-
});
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
#### TC-META-005: Metadata Caching with ETag
|
|
272
|
-
```typescript
|
|
273
|
-
test('should support ETag-based caching', async () => {
|
|
274
|
-
const client = await createAuthenticatedClient();
|
|
275
|
-
|
|
276
|
-
// First request
|
|
277
|
-
const first = await client.meta.getCached('test_contact');
|
|
278
|
-
expect(first.data).toBeDefined();
|
|
279
|
-
expect(first.etag).toBeDefined();
|
|
280
|
-
expect(first.notModified).toBe(false);
|
|
281
|
-
|
|
282
|
-
// Second request with ETag
|
|
283
|
-
const second = await client.meta.getCached('test_contact', {
|
|
284
|
-
ifNoneMatch: `"${first.etag!.value}"`
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
expect(second.notModified).toBe(true);
|
|
288
|
-
expect(second.data).toBeUndefined();
|
|
289
|
-
});
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
---
|
|
293
|
-
|
|
294
|
-
### 4. Data CRUD Operations (`04-data-crud.test.ts`)
|
|
295
|
-
|
|
296
|
-
#### TC-DATA-001: Create Record
|
|
297
|
-
```typescript
|
|
298
|
-
test('should create new record', async () => {
|
|
299
|
-
const client = await createAuthenticatedClient();
|
|
300
|
-
|
|
301
|
-
const contact = await client.data.create('test_contact', {
|
|
302
|
-
first_name: 'John',
|
|
303
|
-
last_name: 'Doe',
|
|
304
|
-
email: 'john.doe@example.com',
|
|
305
|
-
phone: '+1234567890'
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
expect(contact.id).toBeDefined();
|
|
309
|
-
expect(contact.first_name).toBe('John');
|
|
310
|
-
expect(contact.created_at).toBeDefined();
|
|
311
|
-
});
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
#### TC-DATA-002: Get Record by ID
|
|
315
|
-
```typescript
|
|
316
|
-
test('should retrieve record by ID', async () => {
|
|
317
|
-
const client = await createAuthenticatedClient();
|
|
318
|
-
const created = await client.data.create('test_contact', testContactData);
|
|
319
|
-
|
|
320
|
-
const retrieved = await client.data.get('test_contact', created.id);
|
|
321
|
-
|
|
322
|
-
expect(retrieved.id).toBe(created.id);
|
|
323
|
-
expect(retrieved.first_name).toBe(testContactData.first_name);
|
|
324
|
-
});
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
#### TC-DATA-003: Update Record
|
|
328
|
-
```typescript
|
|
329
|
-
test('should update existing record', async () => {
|
|
330
|
-
const client = await createAuthenticatedClient();
|
|
331
|
-
const contact = await client.data.create('test_contact', testContactData);
|
|
332
|
-
|
|
333
|
-
const updated = await client.data.update('test_contact', contact.id, {
|
|
334
|
-
phone: '+9876543210',
|
|
335
|
-
notes: 'Updated via test'
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
expect(updated.id).toBe(contact.id);
|
|
339
|
-
expect(updated.phone).toBe('+9876543210');
|
|
340
|
-
expect(updated.notes).toBe('Updated via test');
|
|
341
|
-
expect(updated.first_name).toBe(testContactData.first_name); // Unchanged
|
|
342
|
-
});
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
#### TC-DATA-004: Delete Record
|
|
346
|
-
```typescript
|
|
347
|
-
test('should delete record', async () => {
|
|
348
|
-
const client = await createAuthenticatedClient();
|
|
349
|
-
const contact = await client.data.create('test_contact', testContactData);
|
|
350
|
-
|
|
351
|
-
await client.data.delete('test_contact', contact.id);
|
|
352
|
-
|
|
353
|
-
await expect(
|
|
354
|
-
client.data.get('test_contact', contact.id)
|
|
355
|
-
).rejects.toThrow(/Not Found|404/);
|
|
356
|
-
});
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
#### TC-DATA-005: Find Records with Filters
|
|
360
|
-
```typescript
|
|
361
|
-
test('should find records with filters', async () => {
|
|
362
|
-
const client = await createAuthenticatedClient();
|
|
363
|
-
|
|
364
|
-
// Create test data
|
|
365
|
-
await client.data.create('test_contact', { first_name: 'Alice', status: 'active' });
|
|
366
|
-
await client.data.create('test_contact', { first_name: 'Bob', status: 'inactive' });
|
|
367
|
-
await client.data.create('test_contact', { first_name: 'Charlie', status: 'active' });
|
|
368
|
-
|
|
369
|
-
const results = await client.data.find('test_contact', {
|
|
370
|
-
filters: { status: 'active' },
|
|
371
|
-
sort: 'first_name',
|
|
372
|
-
top: 10
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
expect(results.data.length).toBe(2);
|
|
376
|
-
expect(results.data[0].first_name).toBe('Alice');
|
|
377
|
-
expect(results.data[1].first_name).toBe('Charlie');
|
|
378
|
-
expect(results.total).toBeGreaterThanOrEqual(2);
|
|
379
|
-
});
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
#### TC-DATA-006: Pagination
|
|
383
|
-
```typescript
|
|
384
|
-
test('should support pagination', async () => {
|
|
385
|
-
const client = await createAuthenticatedClient();
|
|
386
|
-
|
|
387
|
-
// Create 25 test contacts
|
|
388
|
-
for (let i = 0; i < 25; i++) {
|
|
389
|
-
await client.data.create('test_contact', {
|
|
390
|
-
first_name: `Contact${i}`,
|
|
391
|
-
email: `contact${i}@example.com`
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Page 1
|
|
396
|
-
const page1 = await client.data.find('test_contact', {
|
|
397
|
-
top: 10,
|
|
398
|
-
skip: 0,
|
|
399
|
-
sort: 'first_name'
|
|
400
|
-
});
|
|
401
|
-
expect(page1.data.length).toBe(10);
|
|
402
|
-
expect(page1.hasMore).toBe(true);
|
|
403
|
-
|
|
404
|
-
// Page 2
|
|
405
|
-
const page2 = await client.data.find('test_contact', {
|
|
406
|
-
top: 10,
|
|
407
|
-
skip: 10,
|
|
408
|
-
sort: 'first_name'
|
|
409
|
-
});
|
|
410
|
-
expect(page2.data.length).toBe(10);
|
|
411
|
-
expect(page2.data[0].first_name).not.toBe(page1.data[0].first_name);
|
|
412
|
-
});
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
---
|
|
416
|
-
|
|
417
|
-
### 5. Batch Operations (`05-data-batch.test.ts`)
|
|
418
|
-
|
|
419
|
-
#### TC-BATCH-001: Create Many Records
|
|
420
|
-
```typescript
|
|
421
|
-
test('should create multiple records', async () => {
|
|
422
|
-
const client = await createAuthenticatedClient();
|
|
423
|
-
|
|
424
|
-
const contacts = [
|
|
425
|
-
{ first_name: 'Alice', email: 'alice@example.com' },
|
|
426
|
-
{ first_name: 'Bob', email: 'bob@example.com' },
|
|
427
|
-
{ first_name: 'Charlie', email: 'charlie@example.com' }
|
|
428
|
-
];
|
|
429
|
-
|
|
430
|
-
const created = await client.data.createMany('test_contact', contacts);
|
|
431
|
-
|
|
432
|
-
expect(created.length).toBe(3);
|
|
433
|
-
expect(created[0].id).toBeDefined();
|
|
434
|
-
expect(created[0].first_name).toBe('Alice');
|
|
435
|
-
});
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
#### TC-BATCH-002: Update Many Records
|
|
439
|
-
```typescript
|
|
440
|
-
test('should update multiple records', async () => {
|
|
441
|
-
const client = await createAuthenticatedClient();
|
|
442
|
-
|
|
443
|
-
// Create test records
|
|
444
|
-
const c1 = await client.data.create('test_contact', { first_name: 'Test1' });
|
|
445
|
-
const c2 = await client.data.create('test_contact', { first_name: 'Test2' });
|
|
446
|
-
|
|
447
|
-
const result = await client.data.updateMany('test_contact', [
|
|
448
|
-
{ id: c1.id, data: { status: 'updated' } },
|
|
449
|
-
{ id: c2.id, data: { status: 'updated' } }
|
|
450
|
-
]);
|
|
451
|
-
|
|
452
|
-
expect(result.success).toBe(true);
|
|
453
|
-
expect(result.successCount).toBe(2);
|
|
454
|
-
expect(result.failedCount).toBe(0);
|
|
455
|
-
});
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
#### TC-BATCH-003: Delete Many Records
|
|
459
|
-
```typescript
|
|
460
|
-
test('should delete multiple records', async () => {
|
|
461
|
-
const client = await createAuthenticatedClient();
|
|
462
|
-
|
|
463
|
-
const c1 = await client.data.create('test_contact', { first_name: 'Delete1' });
|
|
464
|
-
const c2 = await client.data.create('test_contact', { first_name: 'Delete2' });
|
|
465
|
-
|
|
466
|
-
const result = await client.data.deleteMany('test_contact', [c1.id, c2.id]);
|
|
467
|
-
|
|
468
|
-
expect(result.success).toBe(true);
|
|
469
|
-
expect(result.successCount).toBe(2);
|
|
470
|
-
|
|
471
|
-
await expect(client.data.get('test_contact', c1.id)).rejects.toThrow();
|
|
472
|
-
await expect(client.data.get('test_contact', c2.id)).rejects.toThrow();
|
|
473
|
-
});
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
#### TC-BATCH-004: Mixed Batch Operations
|
|
477
|
-
```typescript
|
|
478
|
-
test('should execute mixed batch operations', async () => {
|
|
479
|
-
const client = await createAuthenticatedClient();
|
|
480
|
-
|
|
481
|
-
const existing = await client.data.create('test_contact', { first_name: 'Existing' });
|
|
482
|
-
|
|
483
|
-
const batchRequest: BatchUpdateRequest = {
|
|
484
|
-
operations: [
|
|
485
|
-
{ action: 'create', data: { first_name: 'New1' } },
|
|
486
|
-
{ action: 'update', id: existing.id, data: { first_name: 'Updated' } },
|
|
487
|
-
{ action: 'create', data: { first_name: 'New2' } }
|
|
488
|
-
],
|
|
489
|
-
options: {
|
|
490
|
-
continueOnError: true,
|
|
491
|
-
returnData: true
|
|
492
|
-
}
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
const result = await client.data.batch('test_contact', batchRequest);
|
|
496
|
-
|
|
497
|
-
expect(result.success).toBe(true);
|
|
498
|
-
expect(result.successCount).toBe(3);
|
|
499
|
-
expect(result.results).toHaveLength(3);
|
|
500
|
-
});
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
#### TC-BATCH-005: Transaction Rollback on Error
|
|
504
|
-
```typescript
|
|
505
|
-
test('should rollback batch on error when continueOnError=false', async () => {
|
|
506
|
-
const client = await createAuthenticatedClient();
|
|
507
|
-
|
|
508
|
-
const batchRequest: BatchUpdateRequest = {
|
|
509
|
-
operations: [
|
|
510
|
-
{ action: 'create', data: { first_name: 'Valid1' } },
|
|
511
|
-
{ action: 'update', id: 'invalid-id', data: { first_name: 'Invalid' } }, // This will fail
|
|
512
|
-
{ action: 'create', data: { first_name: 'Valid2' } }
|
|
513
|
-
],
|
|
514
|
-
options: {
|
|
515
|
-
continueOnError: false,
|
|
516
|
-
transactional: true
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
await expect(
|
|
521
|
-
client.data.batch('test_contact', batchRequest)
|
|
522
|
-
).rejects.toThrow();
|
|
523
|
-
|
|
524
|
-
// Verify no records were created (rolled back)
|
|
525
|
-
const all = await client.data.find('test_contact', {
|
|
526
|
-
filters: { first_name: ['Valid1', 'Valid2'] }
|
|
527
|
-
});
|
|
528
|
-
expect(all.data.length).toBe(0);
|
|
529
|
-
});
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
---
|
|
533
|
-
|
|
534
|
-
### 6. Advanced Queries (`06-data-query.test.ts`)
|
|
535
|
-
|
|
536
|
-
#### TC-QUERY-001: ObjectQL AST Query
|
|
537
|
-
```typescript
|
|
538
|
-
test('should execute ObjectQL AST query', async () => {
|
|
539
|
-
const client = await createAuthenticatedClient();
|
|
540
|
-
|
|
541
|
-
const query: Partial<QueryAST> = {
|
|
542
|
-
object: 'test_contact',
|
|
543
|
-
filter: {
|
|
544
|
-
and: [
|
|
545
|
-
{ field: 'status', operator: 'eq', value: 'active' },
|
|
546
|
-
{ field: 'created_at', operator: 'gte', value: '2024-01-01' }
|
|
547
|
-
]
|
|
548
|
-
},
|
|
549
|
-
sort: [
|
|
550
|
-
{ field: 'last_name', direction: 'asc' },
|
|
551
|
-
{ field: 'first_name', direction: 'asc' }
|
|
552
|
-
],
|
|
553
|
-
pagination: { limit: 20, offset: 0 }
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
const results = await client.data.query('test_contact', query);
|
|
557
|
-
|
|
558
|
-
expect(results.data).toBeDefined();
|
|
559
|
-
expect(results.total).toBeGreaterThanOrEqual(0);
|
|
560
|
-
});
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
#### TC-QUERY-002: Query with Joins/Lookups
|
|
564
|
-
```typescript
|
|
565
|
-
test('should query with lookup field expansion', async () => {
|
|
566
|
-
const client = await createAuthenticatedClient();
|
|
567
|
-
|
|
568
|
-
// Create related data
|
|
569
|
-
const project = await client.data.create('test_project', { name: 'Test Project' });
|
|
570
|
-
const task = await client.data.create('test_task', {
|
|
571
|
-
title: 'Test Task',
|
|
572
|
-
project_id: project.id
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
const query: Partial<QueryAST> = {
|
|
576
|
-
object: 'test_task',
|
|
577
|
-
expand: ['project_id'], // Expand the lookup field
|
|
578
|
-
filter: { field: 'id', operator: 'eq', value: task.id }
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
const results = await client.data.query('test_task', query);
|
|
582
|
-
|
|
583
|
-
expect(results.data[0].project_id).toBeDefined();
|
|
584
|
-
expect(results.data[0].project_id.name).toBe('Test Project');
|
|
585
|
-
});
|
|
586
|
-
```
|
|
587
|
-
|
|
588
|
-
#### TC-QUERY-003: Aggregation Query
|
|
589
|
-
```typescript
|
|
590
|
-
test('should execute aggregation query', async () => {
|
|
591
|
-
const client = await createAuthenticatedClient();
|
|
592
|
-
|
|
593
|
-
const query: Partial<QueryAST> = {
|
|
594
|
-
object: 'test_contact',
|
|
595
|
-
aggregations: [
|
|
596
|
-
{ function: 'count', alias: 'total_contacts' },
|
|
597
|
-
{ function: 'count', field: 'status', alias: 'contacts_with_status' }
|
|
598
|
-
],
|
|
599
|
-
groupBy: ['status']
|
|
600
|
-
};
|
|
601
|
-
|
|
602
|
-
const results = await client.data.query('test_contact', query);
|
|
603
|
-
|
|
604
|
-
expect(results.aggregations).toBeDefined();
|
|
605
|
-
expect(results.aggregations!.total_contacts).toBeGreaterThan(0);
|
|
606
|
-
});
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
---
|
|
610
|
-
|
|
611
|
-
### 7. Permissions (`07-permissions.test.ts`)
|
|
612
|
-
|
|
613
|
-
#### TC-PERM-001: Check Create Permission
|
|
614
|
-
```typescript
|
|
615
|
-
test('should check if user can create records', async () => {
|
|
616
|
-
const client = await createAuthenticatedClient();
|
|
617
|
-
|
|
618
|
-
const result = await client.permissions.check({
|
|
619
|
-
object: 'test_contact',
|
|
620
|
-
action: 'create'
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
expect(result.allowed).toBe(true);
|
|
624
|
-
expect(result.deniedFields).toBeUndefined();
|
|
625
|
-
});
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
#### TC-PERM-002: Get Object Permissions
|
|
629
|
-
```typescript
|
|
630
|
-
test('should retrieve object-level permissions', async () => {
|
|
631
|
-
const client = await createAuthenticatedClient();
|
|
632
|
-
|
|
633
|
-
const perms = await client.permissions.getObjectPermissions('test_contact');
|
|
634
|
-
|
|
635
|
-
expect(perms.object).toBe('test_contact');
|
|
636
|
-
expect(perms.permissions).toBeDefined();
|
|
637
|
-
expect(perms.fieldPermissions).toBeDefined();
|
|
638
|
-
});
|
|
639
|
-
```
|
|
640
|
-
|
|
641
|
-
#### TC-PERM-003: Get Effective Permissions
|
|
642
|
-
```typescript
|
|
643
|
-
test('should get effective permissions for current user', async () => {
|
|
644
|
-
const client = await createAuthenticatedClient();
|
|
645
|
-
|
|
646
|
-
const effective = await client.permissions.getEffectivePermissions('test_contact');
|
|
647
|
-
|
|
648
|
-
expect(effective.canCreate).toBeDefined();
|
|
649
|
-
expect(effective.canRead).toBeDefined();
|
|
650
|
-
expect(effective.canEdit).toBeDefined();
|
|
651
|
-
expect(effective.canDelete).toBeDefined();
|
|
652
|
-
expect(effective.fields).toBeDefined();
|
|
653
|
-
});
|
|
654
|
-
```
|
|
655
|
-
|
|
656
|
-
---
|
|
657
|
-
|
|
658
|
-
### 8. Workflow (`08-workflow.test.ts`)
|
|
659
|
-
|
|
660
|
-
#### TC-WF-001: Get Workflow Configuration
|
|
661
|
-
```typescript
|
|
662
|
-
test('should retrieve workflow rules for object', async () => {
|
|
663
|
-
const client = await createAuthenticatedClient();
|
|
664
|
-
|
|
665
|
-
const config = await client.workflow.getConfig('test_approval');
|
|
666
|
-
|
|
667
|
-
expect(config.object).toBe('test_approval');
|
|
668
|
-
expect(config.states).toBeDefined();
|
|
669
|
-
expect(config.transitions).toBeDefined();
|
|
670
|
-
});
|
|
671
|
-
```
|
|
672
|
-
|
|
673
|
-
#### TC-WF-002: Get Workflow State
|
|
674
|
-
```typescript
|
|
675
|
-
test('should get current workflow state and available transitions', async () => {
|
|
676
|
-
const client = await createAuthenticatedClient();
|
|
677
|
-
|
|
678
|
-
const record = await client.data.create('test_approval', {
|
|
679
|
-
title: 'Test Approval',
|
|
680
|
-
status: 'draft'
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
const state = await client.workflow.getState('test_approval', record.id);
|
|
684
|
-
|
|
685
|
-
expect(state.currentState).toBe('draft');
|
|
686
|
-
expect(state.availableTransitions).toContain('submit');
|
|
687
|
-
});
|
|
688
|
-
```
|
|
689
|
-
|
|
690
|
-
#### TC-WF-003: Execute Workflow Transition
|
|
691
|
-
```typescript
|
|
692
|
-
test('should execute workflow state transition', async () => {
|
|
693
|
-
const client = await createAuthenticatedClient();
|
|
694
|
-
|
|
695
|
-
const record = await client.data.create('test_approval', {
|
|
696
|
-
title: 'Test',
|
|
697
|
-
status: 'draft'
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
const result = await client.workflow.transition({
|
|
701
|
-
object: 'test_approval',
|
|
702
|
-
recordId: record.id,
|
|
703
|
-
transition: 'submit',
|
|
704
|
-
comment: 'Submitting for approval'
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
expect(result.success).toBe(true);
|
|
708
|
-
expect(result.newState).toBe('pending');
|
|
709
|
-
});
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
#### TC-WF-004: Approve Workflow
|
|
713
|
-
```typescript
|
|
714
|
-
test('should approve workflow transition', async () => {
|
|
715
|
-
const client = await createAuthenticatedClient();
|
|
716
|
-
|
|
717
|
-
const result = await client.workflow.approve({
|
|
718
|
-
object: 'test_approval',
|
|
719
|
-
recordId: testRecordId,
|
|
720
|
-
comment: 'Approved by manager'
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
expect(result.success).toBe(true);
|
|
724
|
-
expect(result.newState).toBe('approved');
|
|
725
|
-
});
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
#### TC-WF-005: Reject Workflow
|
|
729
|
-
```typescript
|
|
730
|
-
test('should reject workflow transition', async () => {
|
|
731
|
-
const client = await createAuthenticatedClient();
|
|
732
|
-
|
|
733
|
-
const result = await client.workflow.reject({
|
|
734
|
-
object: 'test_approval',
|
|
735
|
-
recordId: testRecordId,
|
|
736
|
-
reason: 'Insufficient documentation',
|
|
737
|
-
comment: 'Please provide more details'
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
expect(result.success).toBe(true);
|
|
741
|
-
expect(result.newState).toBe('rejected');
|
|
742
|
-
});
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
---
|
|
746
|
-
|
|
747
|
-
### 9-17. Additional Test Categories
|
|
748
|
-
|
|
749
|
-
*(Similar detailed test cases for remaining namespaces: Realtime, Notifications, AI, i18n, Analytics, Packages, Views, Storage, Automation)*
|
|
750
|
-
|
|
751
|
-
---
|
|
752
|
-
|
|
753
|
-
## Test Utilities
|
|
754
|
-
|
|
755
|
-
### Mock Server Setup
|
|
756
|
-
|
|
757
|
-
```typescript
|
|
758
|
-
// packages/client/tests/integration/helpers/test-server.ts
|
|
759
|
-
|
|
760
|
-
import { setupServer } from 'msw/node';
|
|
761
|
-
import { rest } from 'msw';
|
|
762
|
-
|
|
763
|
-
export function createMockServer() {
|
|
764
|
-
return setupServer(
|
|
765
|
-
// Discovery
|
|
766
|
-
rest.get('/.well-known/objectstack', (req, res, ctx) => {
|
|
767
|
-
return res(ctx.json({
|
|
768
|
-
version: 'v1',
|
|
769
|
-
apiName: 'ObjectStack Test Server',
|
|
770
|
-
capabilities: ['metadata', 'data', 'auth'],
|
|
771
|
-
endpoints: { /* ... */ }
|
|
772
|
-
}));
|
|
773
|
-
}),
|
|
774
|
-
|
|
775
|
-
// Auth
|
|
776
|
-
rest.post('/api/v1/auth/login', (req, res, ctx) => {
|
|
777
|
-
return res(ctx.json({
|
|
778
|
-
success: true,
|
|
779
|
-
data: {
|
|
780
|
-
token: 'mock-jwt-token',
|
|
781
|
-
user: { id: '1', email: 'test@example.com' },
|
|
782
|
-
expiresAt: Date.now() + 3600000
|
|
783
|
-
}
|
|
784
|
-
}));
|
|
785
|
-
}),
|
|
786
|
-
|
|
787
|
-
// Add more handlers...
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
```
|
|
791
|
-
|
|
792
|
-
### Test Data Generators
|
|
793
|
-
|
|
794
|
-
```typescript
|
|
795
|
-
// packages/client/tests/integration/helpers/test-data.ts
|
|
796
|
-
|
|
797
|
-
export const generateContact = (overrides = {}) => ({
|
|
798
|
-
first_name: faker.person.firstName(),
|
|
799
|
-
last_name: faker.person.lastName(),
|
|
800
|
-
email: faker.internet.email(),
|
|
801
|
-
phone: faker.phone.number(),
|
|
802
|
-
...overrides
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
export const generateProject = (overrides = {}) => ({
|
|
806
|
-
name: faker.commerce.productName(),
|
|
807
|
-
description: faker.lorem.paragraph(),
|
|
808
|
-
status: 'active',
|
|
809
|
-
...overrides
|
|
810
|
-
});
|
|
811
|
-
```
|
|
812
|
-
|
|
813
|
-
### Custom Assertions
|
|
814
|
-
|
|
815
|
-
```typescript
|
|
816
|
-
// packages/client/tests/integration/helpers/assertions.ts
|
|
817
|
-
|
|
818
|
-
export function expectValidId(id: string) {
|
|
819
|
-
expect(id).toBeDefined();
|
|
820
|
-
expect(typeof id).toBe('string');
|
|
821
|
-
expect(id.length).toBeGreaterThan(0);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
export function expectValidTimestamp(timestamp: string) {
|
|
825
|
-
expect(timestamp).toBeDefined();
|
|
826
|
-
expect(new Date(timestamp).getTime()).toBeGreaterThan(0);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
export function expectValidResponse<T>(response: any): asserts response is T {
|
|
830
|
-
expect(response).toBeDefined();
|
|
831
|
-
expect(typeof response).toBe('object');
|
|
832
|
-
}
|
|
833
|
-
```
|
|
834
|
-
|
|
835
|
-
---
|
|
836
|
-
|
|
837
|
-
## Running Tests
|
|
838
|
-
|
|
839
|
-
### Local Development
|
|
840
|
-
|
|
841
|
-
**Note:** Integration tests require a running ObjectStack server. The server is provided by a separate repository/package and is not included in this spec repository.
|
|
842
|
-
|
|
843
|
-
```bash
|
|
844
|
-
# Start test server (in the ObjectStack server repository)
|
|
845
|
-
# Follow the server project's documentation for setup
|
|
846
|
-
# Example: cd /path/to/objectstack-server && pnpm dev:test
|
|
847
|
-
|
|
848
|
-
# Run integration tests (in this repository)
|
|
849
|
-
cd packages/client
|
|
850
|
-
pnpm test:integration
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
### CI/CD Pipeline
|
|
854
|
-
|
|
855
|
-
**Note:** The workflow file referenced below is an example. Actual CI implementation will require setting up the test server infrastructure separately.
|
|
856
|
-
|
|
857
|
-
```yaml
|
|
858
|
-
# Example: .github/workflows/client-integration-tests.yml
|
|
859
|
-
# This workflow would need to be created and configured with proper server setup
|
|
860
|
-
name: Client Integration Tests
|
|
861
|
-
|
|
862
|
-
on: [push, pull_request]
|
|
863
|
-
|
|
864
|
-
jobs:
|
|
865
|
-
test:
|
|
866
|
-
runs-on: ubuntu-latest
|
|
867
|
-
|
|
868
|
-
services:
|
|
869
|
-
postgres:
|
|
870
|
-
image: postgres:15
|
|
871
|
-
env:
|
|
872
|
-
POSTGRES_PASSWORD: test
|
|
873
|
-
options: >-
|
|
874
|
-
--health-cmd pg_isready
|
|
875
|
-
--health-interval 10s
|
|
876
|
-
|
|
877
|
-
steps:
|
|
878
|
-
- uses: actions/checkout@v3
|
|
879
|
-
|
|
880
|
-
- name: Setup Node
|
|
881
|
-
uses: actions/setup-node@v3
|
|
882
|
-
with:
|
|
883
|
-
node-version: 20
|
|
884
|
-
|
|
885
|
-
- name: Install dependencies
|
|
886
|
-
run: pnpm install
|
|
887
|
-
|
|
888
|
-
- name: Build spec
|
|
889
|
-
run: pnpm --filter @objectstack/spec build
|
|
890
|
-
|
|
891
|
-
# Note: Server setup would require additional configuration
|
|
892
|
-
# This is a placeholder showing the expected structure
|
|
893
|
-
- name: Start test server
|
|
894
|
-
run: |
|
|
895
|
-
# Server startup logic would go here
|
|
896
|
-
# This depends on the ObjectStack server implementation
|
|
897
|
-
echo "Server setup required"
|
|
898
|
-
env:
|
|
899
|
-
DATABASE_URL: postgresql://postgres:test@localhost:5432/test
|
|
900
|
-
|
|
901
|
-
- name: Run integration tests
|
|
902
|
-
run: pnpm --filter @objectstack/client test:integration
|
|
903
|
-
```
|
|
904
|
-
|
|
905
|
-
---
|
|
906
|
-
|
|
907
|
-
## Test Coverage Goals
|
|
908
|
-
|
|
909
|
-
| Category | Target Coverage | Priority |
|
|
910
|
-
|----------|----------------|----------|
|
|
911
|
-
| Core Services (discovery, meta, data, auth) | 100% | Critical |
|
|
912
|
-
| Optional Services | 90% | High |
|
|
913
|
-
| Error Scenarios | 80% | High |
|
|
914
|
-
| Edge Cases | 70% | Medium |
|
|
915
|
-
|
|
916
|
-
---
|
|
917
|
-
|
|
918
|
-
## Success Criteria
|
|
919
|
-
|
|
920
|
-
- ✅ All 17 test suites pass
|
|
921
|
-
- ✅ 90%+ code coverage on client SDK
|
|
922
|
-
- ✅ Zero protocol compliance violations
|
|
923
|
-
- ✅ All request/response schemas validated
|
|
924
|
-
- ✅ Authentication flow complete
|
|
925
|
-
- ✅ Error handling verified
|
|
926
|
-
- ✅ Performance benchmarks met
|
|
927
|
-
|
|
928
|
-
---
|
|
929
|
-
|
|
930
|
-
## Related Documentation
|
|
931
|
-
|
|
932
|
-
- [Client Spec Compliance Matrix](./CLIENT_SPEC_COMPLIANCE.md)
|
|
933
|
-
- [Client README](./README.md)
|
|
934
|
-
- [Spec Protocol Map](../spec/PROTOCOL_MAP.md)
|
|
935
|
-
|
|
936
|
-
---
|
|
937
|
-
|
|
938
|
-
**Last Updated:** 2026-02-09
|
|
939
|
-
**Status:** 📝 Specification Complete - Ready for Implementation
|