@objectstack/client 2.0.0 → 2.0.2
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 +10 -10
- package/CHANGELOG.md +17 -0
- package/CLIENT_SERVER_INTEGRATION_TESTS.md +939 -0
- package/CLIENT_SPEC_COMPLIANCE.md +361 -0
- package/QUICK_REFERENCE.md +206 -0
- package/README.md +129 -0
- package/dist/index.d.mts +235 -27
- package/dist/index.d.ts +235 -27
- package/dist/index.js +483 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +483 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -11
- package/src/client.test.ts +563 -2
- package/src/index.ts +540 -37
- package/src/query-builder.ts +75 -0
- package/tests/integration/01-discovery.test.ts +68 -0
- package/tests/integration/README.md +72 -0
- package/vitest.integration.config.ts +18 -0
package/src/query-builder.ts
CHANGED
|
@@ -106,6 +106,46 @@ export class FilterBuilder<T = any> {
|
|
|
106
106
|
return this;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* BETWEEN filter: field BETWEEN min AND max
|
|
111
|
+
*/
|
|
112
|
+
between<K extends keyof T>(field: K, min: T[K], max: T[K]): this {
|
|
113
|
+
this.conditions.push(['and', [field as string, '>=', min], [field as string, '<=', max]] as FilterCondition);
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* CONTAINS filter: field contains value (case-insensitive LIKE %value%)
|
|
119
|
+
*/
|
|
120
|
+
contains<K extends keyof T>(field: K, value: string): this {
|
|
121
|
+
this.conditions.push([field as string, 'like', `%${value}%`]);
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* STARTS WITH filter: field starts with value (LIKE value%)
|
|
127
|
+
*/
|
|
128
|
+
startsWith<K extends keyof T>(field: K, value: string): this {
|
|
129
|
+
this.conditions.push([field as string, 'like', `${value}%`]);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* ENDS WITH filter: field ends with value (LIKE %value)
|
|
135
|
+
*/
|
|
136
|
+
endsWith<K extends keyof T>(field: K, value: string): this {
|
|
137
|
+
this.conditions.push([field as string, 'like', `%${value}`]);
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* EXISTS filter: field is not null (alias for isNotNull)
|
|
143
|
+
*/
|
|
144
|
+
exists<K extends keyof T>(field: K): this {
|
|
145
|
+
this.conditions.push([field as string, 'is_not_null', null]);
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
109
149
|
/**
|
|
110
150
|
* Build the filter condition
|
|
111
151
|
*/
|
|
@@ -220,6 +260,41 @@ export class QueryBuilder<T = any> {
|
|
|
220
260
|
return this;
|
|
221
261
|
}
|
|
222
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Expand (eager-load) a related object with an optional sub-query
|
|
265
|
+
*/
|
|
266
|
+
expand(relation: string, subQuery?: Partial<QueryAST>): this {
|
|
267
|
+
if (!this.query.expand) {
|
|
268
|
+
this.query.expand = {};
|
|
269
|
+
}
|
|
270
|
+
(this.query.expand as Record<string, any>)[relation] = subQuery || {};
|
|
271
|
+
return this;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Add full-text search
|
|
276
|
+
*/
|
|
277
|
+
search(query: string, options?: { fields?: string[]; fuzzy?: boolean }): this {
|
|
278
|
+
(this.query as any).search = { query, ...options };
|
|
279
|
+
return this;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Set cursor for keyset pagination
|
|
284
|
+
*/
|
|
285
|
+
cursor(cursor: Record<string, any>): this {
|
|
286
|
+
(this.query as any).cursor = cursor;
|
|
287
|
+
return this;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Enable SELECT DISTINCT
|
|
292
|
+
*/
|
|
293
|
+
distinct(): this {
|
|
294
|
+
(this.query as any).distinct = true;
|
|
295
|
+
return this;
|
|
296
|
+
}
|
|
297
|
+
|
|
223
298
|
/**
|
|
224
299
|
* Build the final query AST
|
|
225
300
|
*/
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Test: Discovery & Connection
|
|
3
|
+
*
|
|
4
|
+
* Tests the client's ability to discover and connect to an ObjectStack server.
|
|
5
|
+
* These tests require a running server instance.
|
|
6
|
+
*
|
|
7
|
+
* @see CLIENT_SERVER_INTEGRATION_TESTS.md for full test specification
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test, expect } from 'vitest';
|
|
11
|
+
import { ObjectStackClient } from '../../src/index';
|
|
12
|
+
|
|
13
|
+
const TEST_SERVER_URL = process.env.TEST_SERVER_URL || 'http://localhost:3000';
|
|
14
|
+
|
|
15
|
+
describe('Discovery & Connection', () => {
|
|
16
|
+
describe('TC-DISC-001: Standard Discovery via .well-known', () => {
|
|
17
|
+
test('should discover API from .well-known/objectstack', async () => {
|
|
18
|
+
const client = new ObjectStackClient({
|
|
19
|
+
baseUrl: TEST_SERVER_URL,
|
|
20
|
+
debug: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const discovery = await client.connect();
|
|
24
|
+
|
|
25
|
+
expect(discovery.version).toBeDefined();
|
|
26
|
+
expect(discovery.apiName).toBeDefined();
|
|
27
|
+
expect(discovery.capabilities).toBeDefined();
|
|
28
|
+
expect(discovery.endpoints).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('TC-DISC-002: Discovery Information', () => {
|
|
33
|
+
test('should provide valid API version information', async () => {
|
|
34
|
+
const client = new ObjectStackClient({ baseUrl: TEST_SERVER_URL });
|
|
35
|
+
const discovery = await client.connect();
|
|
36
|
+
|
|
37
|
+
// Version should be a semantic version or API version string
|
|
38
|
+
expect(discovery.version).toMatch(/^v?\d+/);
|
|
39
|
+
|
|
40
|
+
// API name should be non-empty
|
|
41
|
+
expect(discovery.apiName.length).toBeGreaterThan(0);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('TC-DISC-003: Connection Failure Handling', () => {
|
|
46
|
+
test('should throw error when server is unreachable', async () => {
|
|
47
|
+
const client = new ObjectStackClient({
|
|
48
|
+
baseUrl: 'http://localhost:9999' // Invalid port
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await expect(client.connect()).rejects.toThrow();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('TC-DISC-004: Route Resolution', () => {
|
|
56
|
+
test('should resolve API routes from discovery info', async () => {
|
|
57
|
+
const client = new ObjectStackClient({ baseUrl: TEST_SERVER_URL });
|
|
58
|
+
await client.connect();
|
|
59
|
+
|
|
60
|
+
// After connection, client should have discovery info
|
|
61
|
+
expect(client.discovery).toBeDefined();
|
|
62
|
+
expect(client.discovery?.version).toBeDefined();
|
|
63
|
+
|
|
64
|
+
// Verify that subsequent API calls can be made (routes are resolved)
|
|
65
|
+
// This implicitly tests route resolution
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Client Integration Tests
|
|
2
|
+
|
|
3
|
+
This directory contains integration tests that verify `@objectstack/client` against a live ObjectStack server.
|
|
4
|
+
|
|
5
|
+
## Running Tests
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
**Note:** Integration tests require a running ObjectStack server with test data. The server is provided by a separate repository and must be set up independently.
|
|
10
|
+
|
|
11
|
+
1. **Start a test server (external dependency):**
|
|
12
|
+
```bash
|
|
13
|
+
# In the ObjectStack server repository (separate from this package)
|
|
14
|
+
# Follow that project's documentation for test server setup
|
|
15
|
+
# Example: cd /path/to/objectstack-server && pnpm dev:test
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. **Run integration tests (from this package):**
|
|
19
|
+
```bash
|
|
20
|
+
pnpm test:integration
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Environment Variables
|
|
24
|
+
|
|
25
|
+
- `TEST_SERVER_URL` - Base URL of the test server (default: `http://localhost:3000`)
|
|
26
|
+
- `TEST_USER_EMAIL` - Test user email (default: `test@example.com`)
|
|
27
|
+
- `TEST_USER_PASSWORD` - Test user password (default: `TestPassword123!`)
|
|
28
|
+
|
|
29
|
+
## Test Structure
|
|
30
|
+
|
|
31
|
+
Tests are organized by protocol namespace:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
01-discovery.test.ts # Discovery & connection
|
|
35
|
+
02-auth.test.ts # Authentication flows
|
|
36
|
+
03-metadata.test.ts # Metadata operations
|
|
37
|
+
04-data-crud.test.ts # Basic CRUD operations
|
|
38
|
+
05-data-batch.test.ts # Batch operations
|
|
39
|
+
06-data-query.test.ts # Advanced queries
|
|
40
|
+
07-permissions.test.ts # Permission checking
|
|
41
|
+
08-workflow.test.ts # Workflow operations
|
|
42
|
+
09-realtime.test.ts # Realtime subscriptions
|
|
43
|
+
10-notifications.test.ts # Notifications
|
|
44
|
+
11-ai.test.ts # AI services
|
|
45
|
+
12-i18n.test.ts # Internationalization
|
|
46
|
+
13-analytics.test.ts # Analytics queries
|
|
47
|
+
14-packages.test.ts # Package management
|
|
48
|
+
15-views.test.ts # View management
|
|
49
|
+
16-storage.test.ts # File storage
|
|
50
|
+
17-automation.test.ts # Automation triggers
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Test Coverage Goals
|
|
54
|
+
|
|
55
|
+
- Core Services (discovery, meta, data, auth): **100%**
|
|
56
|
+
- Optional Services: **90%**
|
|
57
|
+
- Error Scenarios: **80%**
|
|
58
|
+
- Edge Cases: **70%**
|
|
59
|
+
|
|
60
|
+
## Related Documentation
|
|
61
|
+
|
|
62
|
+
- [Integration Test Specification](../../CLIENT_SERVER_INTEGRATION_TESTS.md)
|
|
63
|
+
- [Client Spec Compliance](../../CLIENT_SPEC_COMPLIANCE.md)
|
|
64
|
+
|
|
65
|
+
## CI/CD
|
|
66
|
+
|
|
67
|
+
Integration tests can be run in CI, but require:
|
|
68
|
+
- A running ObjectStack server instance (from separate repository)
|
|
69
|
+
- Test database with sample data
|
|
70
|
+
- Proper environment configuration
|
|
71
|
+
|
|
72
|
+
See `CLIENT_SERVER_INTEGRATION_TESTS.md` for example CI configuration structure.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
include: ['tests/integration/**/*.test.ts'],
|
|
6
|
+
globals: true,
|
|
7
|
+
environment: 'node',
|
|
8
|
+
testTimeout: 30000, // 30 seconds for integration tests
|
|
9
|
+
hookTimeout: 30000,
|
|
10
|
+
// Run integration tests sequentially to avoid race conditions
|
|
11
|
+
pool: 'forks',
|
|
12
|
+
poolOptions: {
|
|
13
|
+
forks: {
|
|
14
|
+
singleFork: true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|