@onchaindb/sdk 0.4.5 → 1.0.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.
Files changed (97) hide show
  1. package/.claude/settings.local.json +10 -2
  2. package/README.md +422 -355
  3. package/dist/batch.d.ts +1 -10
  4. package/dist/batch.d.ts.map +1 -1
  5. package/dist/batch.js +4 -26
  6. package/dist/batch.js.map +1 -1
  7. package/dist/client.d.ts +29 -43
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +198 -323
  10. package/dist/client.js.map +1 -1
  11. package/dist/database.d.ts +14 -131
  12. package/dist/database.d.ts.map +1 -1
  13. package/dist/database.js +35 -131
  14. package/dist/database.js.map +1 -1
  15. package/dist/index.d.ts +6 -9
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -15
  18. package/dist/index.js.map +1 -1
  19. package/dist/query-sdk/ConditionBuilder.d.ts +3 -11
  20. package/dist/query-sdk/ConditionBuilder.d.ts.map +1 -1
  21. package/dist/query-sdk/ConditionBuilder.js +10 -48
  22. package/dist/query-sdk/ConditionBuilder.js.map +1 -1
  23. package/dist/query-sdk/NestedBuilders.d.ts +33 -30
  24. package/dist/query-sdk/NestedBuilders.d.ts.map +1 -1
  25. package/dist/query-sdk/NestedBuilders.js +46 -43
  26. package/dist/query-sdk/NestedBuilders.js.map +1 -1
  27. package/dist/query-sdk/QueryBuilder.d.ts +4 -2
  28. package/dist/query-sdk/QueryBuilder.d.ts.map +1 -1
  29. package/dist/query-sdk/QueryBuilder.js +47 -169
  30. package/dist/query-sdk/QueryBuilder.js.map +1 -1
  31. package/dist/query-sdk/QueryResult.d.ts +0 -38
  32. package/dist/query-sdk/QueryResult.d.ts.map +1 -1
  33. package/dist/query-sdk/QueryResult.js +1 -227
  34. package/dist/query-sdk/QueryResult.js.map +1 -1
  35. package/dist/query-sdk/index.d.ts +1 -1
  36. package/dist/query-sdk/index.d.ts.map +1 -1
  37. package/dist/query-sdk/index.js.map +1 -1
  38. package/dist/query-sdk/operators.d.ts +32 -28
  39. package/dist/query-sdk/operators.d.ts.map +1 -1
  40. package/dist/query-sdk/operators.js +45 -155
  41. package/dist/query-sdk/operators.js.map +1 -1
  42. package/dist/types.d.ts +153 -1
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/jest.config.js +4 -0
  46. package/package.json +1 -1
  47. package/skills.md +0 -1
  48. package/src/client.ts +242 -745
  49. package/src/database.ts +70 -493
  50. package/src/index.ts +40 -193
  51. package/src/query-sdk/ConditionBuilder.ts +37 -89
  52. package/src/query-sdk/NestedBuilders.ts +90 -92
  53. package/src/query-sdk/QueryBuilder.ts +59 -218
  54. package/src/query-sdk/QueryResult.ts +4 -330
  55. package/src/query-sdk/README.md +214 -583
  56. package/src/query-sdk/index.ts +1 -1
  57. package/src/query-sdk/operators.ts +91 -200
  58. package/src/query-sdk/tests/FieldConditionBuilder.test.ts +70 -71
  59. package/src/query-sdk/tests/LogicalOperator.test.ts +43 -82
  60. package/src/query-sdk/tests/NestedBuilders.test.ts +229 -309
  61. package/src/query-sdk/tests/QueryBuilder.test.ts +5 -5
  62. package/src/query-sdk/tests/QueryResult.test.ts +41 -435
  63. package/src/query-sdk/tests/comprehensive.test.ts +4 -185
  64. package/src/tests/client-requests.test.ts +280 -0
  65. package/src/tests/client-validation.test.ts +80 -0
  66. package/src/types.ts +229 -8
  67. package/src/batch.ts +0 -257
  68. package/src/query-sdk/dist/ConditionBuilder.d.ts +0 -22
  69. package/src/query-sdk/dist/ConditionBuilder.js +0 -90
  70. package/src/query-sdk/dist/FieldConditionBuilder.d.ts +0 -1
  71. package/src/query-sdk/dist/FieldConditionBuilder.js +0 -6
  72. package/src/query-sdk/dist/NestedBuilders.d.ts +0 -43
  73. package/src/query-sdk/dist/NestedBuilders.js +0 -144
  74. package/src/query-sdk/dist/OnChainDB.d.ts +0 -19
  75. package/src/query-sdk/dist/OnChainDB.js +0 -123
  76. package/src/query-sdk/dist/QueryBuilder.d.ts +0 -70
  77. package/src/query-sdk/dist/QueryBuilder.js +0 -295
  78. package/src/query-sdk/dist/QueryResult.d.ts +0 -52
  79. package/src/query-sdk/dist/QueryResult.js +0 -293
  80. package/src/query-sdk/dist/SelectionBuilder.d.ts +0 -20
  81. package/src/query-sdk/dist/SelectionBuilder.js +0 -80
  82. package/src/query-sdk/dist/adapters/HttpClientAdapter.d.ts +0 -27
  83. package/src/query-sdk/dist/adapters/HttpClientAdapter.js +0 -170
  84. package/src/query-sdk/dist/index.d.ts +0 -36
  85. package/src/query-sdk/dist/index.js +0 -27
  86. package/src/query-sdk/dist/operators.d.ts +0 -56
  87. package/src/query-sdk/dist/operators.js +0 -289
  88. package/src/query-sdk/dist/tests/setup.d.ts +0 -15
  89. package/src/query-sdk/dist/tests/setup.js +0 -46
  90. package/src/query-sdk/jest.config.js +0 -25
  91. package/src/query-sdk/package.json +0 -46
  92. package/src/query-sdk/tests/aggregations.test.ts +0 -653
  93. package/src/query-sdk/tests/integration.test.ts +0 -608
  94. package/src/query-sdk/tests/operators.test.ts +0 -327
  95. package/src/query-sdk/tests/unit.test.ts +0 -794
  96. package/src/query-sdk/tsconfig.json +0 -26
  97. package/src/query-sdk/yarn.lock +0 -3092
@@ -1,4 +1,4 @@
1
- import { QueryBuilder, FieldConditionBuilder, LogicalOperator, createQueryResult } from '../index';
1
+ import { QueryBuilder, FieldConditionBuilder, LogicalOperator } from '../index';
2
2
  import { MockHttpClient, createMockResponse } from './setup';
3
3
 
4
4
  describe('Comprehensive SDK Integration Tests', () => {
@@ -34,13 +34,12 @@ describe('Comprehensive SDK Integration Tests', () => {
34
34
  });
35
35
 
36
36
  expect(rawQuery.limit).toBe(10);
37
- expect(rawQuery.select).toEqual({ fields: ['id', 'name'] });
37
+ expect(rawQuery.select).toEqual({ id: true, name: true });
38
38
  });
39
39
 
40
40
  test('should handle deep nesting scenarios', () => {
41
41
  const deepCondition = new FieldConditionBuilder('user.profile.settings.preferences.theme').equals('dark');
42
- const logicalOp = LogicalOperator.Condition(deepCondition);
43
- const composable = logicalOp.toComposable();
42
+ const composable = deepCondition.toComposable();
44
43
 
45
44
  const expected = {
46
45
  user: {
@@ -79,90 +78,7 @@ describe('Comprehensive SDK Integration Tests', () => {
79
78
  });
80
79
  });
81
80
 
82
- describe('QueryResult advanced operations', () => {
83
- test('should perform complex join operations', () => {
84
- const users = createQueryResult([
85
- { id: 1, name: 'John', departmentId: 1 },
86
- { id: 2, name: 'Jane', departmentId: 2 }
87
- ]);
88
-
89
- const departments = createQueryResult([
90
- { id: 1, name: 'Engineering' },
91
- { id: 2, name: 'Design' }
92
- ]);
93
-
94
- const joined = users.innerJoin(departments, 'departmentId', 'id');
95
- expect(joined.length).toBe(2);
96
- expect(joined[0].name).toBe('Engineering'); // Department name merged
97
- expect(joined[1].name).toBe('Design');
98
- });
99
-
100
- test('should export to CSV with proper formatting', () => {
101
- const users = createQueryResult([
102
- { id: 1, name: 'John', age: 30, score: 95.5 },
103
- { id: 2, name: 'Jane', age: 25, score: 87.2 },
104
- { id: 3, name: 'Bob', age: 35, score: 92.8 }
105
- ]);
106
-
107
- const csv = users.toCsv();
108
- const lines = csv.split('\n');
109
-
110
- expect(lines.length).toBe(4); // header + 3 data rows
111
- expect(lines[0]).toContain('id');
112
- expect(lines[0]).toContain('name');
113
- expect(lines[0]).toContain('age');
114
- expect(lines[0]).toContain('score');
115
- });
116
-
117
- test('should provide comprehensive numeric summaries', () => {
118
- const testData = [
119
- { id: 1, name: 'John', age: 30, score: 95.5 },
120
- { id: 2, name: 'Jane', age: 25, score: 87.2 },
121
- { id: 3, name: 'Bob', age: 35, score: 92.8 }
122
- ];
123
-
124
- const result = createQueryResult(testData);
125
- const scoreSummary = result.summarizeNumeric('score');
126
-
127
- expect(scoreSummary).not.toBeNull();
128
- expect(scoreSummary!.count).toBe(3);
129
- expect(scoreSummary!.mean).toBeCloseTo((95.5 + 87.2 + 92.8) / 3, 2);
130
- expect(scoreSummary!.min).toBe(87.2);
131
- expect(scoreSummary!.max).toBe(95.5);
132
- });
133
-
134
- test('should handle sorting and filtering', () => {
135
- const testData = [
136
- { id: 1, name: 'John', age: 30 },
137
- { id: 2, name: 'Jane', age: 25 },
138
- { id: 3, name: 'Bob', age: 35 }
139
- ];
140
-
141
- const result = createQueryResult(testData);
142
-
143
- const sortedByAge = result.sortBy('age');
144
- expect(sortedByAge[0].age).toBe(25);
145
- expect(sortedByAge[2].age).toBe(35);
146
-
147
- const filtered = result.filter(item => item.age > 30);
148
- expect(filtered.length).toBe(1);
149
- expect(filtered[0].name).toBe('Bob');
150
-
151
- const grouped = result.groupBy('age');
152
- expect(Object.keys(grouped).length).toBe(3);
153
- });
154
- });
155
-
156
81
  describe('Error handling and validation', () => {
157
- test('should handle empty QueryResult operations gracefully', () => {
158
- const emptyResult = createQueryResult([]);
159
-
160
- expect(emptyResult.isEmpty()).toBe(true);
161
- expect(emptyResult.first()).toBeUndefined();
162
- expect(emptyResult.len()).toBe(0);
163
- expect(emptyResult.summarizeNumeric('field')).toBeNull();
164
- });
165
-
166
82
  test('should validate QueryBuilder state', () => {
167
83
  const emptyBuilder = new QueryBuilder();
168
84
  expect(emptyBuilder.isValid()).toBe(false);
@@ -174,106 +90,9 @@ describe('Comprehensive SDK Integration Tests', () => {
174
90
 
175
91
  test('should handle invalid field references', () => {
176
92
  expect(() => {
177
- const condition = new FieldConditionBuilder('').equals('test');
178
- LogicalOperator.Condition(condition).toComposable();
93
+ new FieldConditionBuilder('').equals('test').toComposable();
179
94
  }).not.toThrow();
180
95
  });
181
96
  });
182
97
 
183
- describe('All operators comprehensive coverage', () => {
184
- test('should support all field condition operators', () => {
185
- const builder = new FieldConditionBuilder('test_field');
186
-
187
- // Only test operators that actually exist in the Rust implementation
188
- const operators = [
189
- 'equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterThanOrEqual', 'lessThanOrEqual',
190
- 'contains', 'startsWith', 'endsWith', 'in', 'notIn', 'exists', 'notExists',
191
- 'isNull', 'isNotNull', 'regExpMatches', 'includesCaseInsensitive',
192
- 'startsWithCaseInsensitive', 'endsWithCaseInsensitive', 'between',
193
- 'isLocalIp', 'isExternalIp', 'b64', 'inDataset', 'inCountry', 'cidr',
194
- 'isTrue', 'isFalse'
195
- ];
196
-
197
- operators.forEach(op => {
198
- expect(typeof (builder as any)[op]).toBe('function');
199
- });
200
- });
201
-
202
- test('should create correct conditions for advanced operators', () => {
203
- const builder = new FieldConditionBuilder('test_field');
204
-
205
- // String operators
206
- const regexCondition = builder.regExpMatches('^test.*');
207
- expect(regexCondition.operator).toBe('regExpMatches');
208
-
209
- const caseInsensitiveCondition = builder.includesCaseInsensitive('Test');
210
- expect(caseInsensitiveCondition.operator).toBe('includesCaseInsensitive');
211
-
212
- // IP operators
213
- const ipBuilder = new FieldConditionBuilder('ip_address');
214
- const ipLocalCondition = ipBuilder.isLocalIp();
215
- expect(ipLocalCondition.operator).toBe('isLocalIp');
216
-
217
- const ipExternalCondition = ipBuilder.isExternalIp();
218
- expect(ipExternalCondition.operator).toBe('isExternalIp');
219
-
220
- // Misc operators
221
- const b64Condition = builder.b64('dGVzdA==');
222
- expect(b64Condition.operator).toBe('b64');
223
-
224
- const countryCondition = builder.inCountry('US');
225
- expect(countryCondition.operator).toBe('inCountry');
226
-
227
- const cidrCondition = builder.cidr('192.168.1.0/24');
228
- expect(cidrCondition.operator).toBe('CIDR');
229
-
230
- const datasetCondition = builder.inDataset('malware_ips');
231
- expect(datasetCondition.operator).toBe('inDataset');
232
-
233
- const betweenCondition = builder.between(5, 15);
234
- expect(betweenCondition.operator).toBe('betweenOp');
235
- });
236
- });
237
-
238
- describe('Performance and memory scenarios', () => {
239
- test('should handle large datasets efficiently', () => {
240
- const largeDataset = Array.from({ length: 1000 }, (_, i) => ({
241
- id: i,
242
- name: `User${i}`,
243
- value: Math.random() * 100
244
- }));
245
-
246
- const result = createQueryResult(largeDataset);
247
- expect(result.len()).toBe(1000);
248
-
249
- const filtered = result.filter(item => item.value > 50);
250
- expect(filtered.length).toBeGreaterThan(0);
251
-
252
- const summary = result.summarizeNumeric('value');
253
- expect(summary).not.toBeNull();
254
- expect(summary!.count).toBe(1000);
255
- });
256
-
257
- test('should handle complex nested structures', () => {
258
- const complexData = Array.from({ length: 100 }, (_, i) => ({
259
- id: i,
260
- user: {
261
- profile: {
262
- details: {
263
- settings: {
264
- preferences: {
265
- theme: i % 2 === 0 ? 'dark' : 'light'
266
- }
267
- }
268
- }
269
- }
270
- }
271
- }));
272
-
273
- const result = createQueryResult(complexData);
274
- const themes = result.pluck('user.profile.details.settings.preferences.theme');
275
- expect(themes.filter(theme => theme === 'dark')).toHaveLength(50);
276
- expect(themes.filter(theme => theme === 'light')).toHaveLength(50);
277
- });
278
- });
279
98
  });
@@ -0,0 +1,280 @@
1
+ import axios from 'axios';
2
+ import { OnChainDBClient } from '../client';
3
+ import { CreateQueryRequest } from '../types';
4
+
5
+ // Spy on axios.create so we can capture the instance handed to the client
6
+ let mockHttp: {
7
+ post: jest.Mock;
8
+ get: jest.Mock;
9
+ delete: jest.Mock;
10
+ patch: jest.Mock;
11
+ interceptors: { request: { use: jest.Mock }; response: { use: jest.Mock } };
12
+ };
13
+ let createSpy: jest.SpyInstance;
14
+ let client: OnChainDBClient;
15
+
16
+ beforeEach(() => {
17
+ mockHttp = {
18
+ post: jest.fn().mockResolvedValue({ data: {} }),
19
+ get: jest.fn().mockResolvedValue({ data: {} }),
20
+ delete: jest.fn().mockResolvedValue({ data: {} }),
21
+ patch: jest.fn().mockResolvedValue({ data: {} }),
22
+ interceptors: {
23
+ request: { use: jest.fn() },
24
+ response: { use: jest.fn() },
25
+ },
26
+ };
27
+ createSpy = jest.spyOn(axios, 'create').mockReturnValue(mockHttp as any);
28
+ client = new OnChainDBClient({
29
+ endpoint: 'http://localhost:9092',
30
+ appId: 'test_app',
31
+ appKey: 'key123',
32
+ });
33
+ });
34
+
35
+ afterEach(() => {
36
+ createSpy.mockRestore();
37
+ jest.clearAllMocks();
38
+ });
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Predefined Queries
42
+ // ---------------------------------------------------------------------------
43
+
44
+ describe('createQuery()', () => {
45
+ test('POSTs the full CreateQueryRequest body (not just name+query)', async () => {
46
+ const req: CreateQueryRequest = {
47
+ name: 'active_orders',
48
+ source_collection: 'orders',
49
+ base_query: { find: { status: { $eq: 'active' } } },
50
+ parameters: [
51
+ { name: 'status', field_path: 'status', required: true },
52
+ { name: 'min_amount', field_path: 'amount.$gte', default: 0 },
53
+ ],
54
+ description: 'Active orders query',
55
+ };
56
+
57
+ mockHttp.post.mockResolvedValue({ data: { success: true, message: 'ok', query_name: 'active_orders' } });
58
+ await client.createQuery(req);
59
+
60
+ expect(mockHttp.post).toHaveBeenCalledWith('/apps/test_app/queries', req);
61
+ });
62
+
63
+ test('uses the correct URL with appId', async () => {
64
+ mockHttp.post.mockResolvedValue({ data: { success: true, message: 'ok', query_name: 'q' } });
65
+ await client.createQuery({ name: 'q', source_collection: 'col', base_query: {} });
66
+
67
+ const [url] = mockHttp.post.mock.calls[0];
68
+ expect(url).toBe('/apps/test_app/queries');
69
+ });
70
+ });
71
+
72
+ describe('executeQuery()', () => {
73
+ test('passes params as axios params object, not as path string', async () => {
74
+ mockHttp.get.mockResolvedValue({ data: { success: true, query_name: 'q', data: [], count: 0, query_time_ms: 1 } });
75
+ await client.executeQuery('active_orders', { market: 'BTC', status: 'open' });
76
+
77
+ const [url, config] = mockHttp.get.mock.calls[0];
78
+ expect(url).toBe('/api/queries/test_app/active_orders/data');
79
+ expect(config.params).toMatchObject({ market: 'BTC', status: 'open' });
80
+ expect(url).not.toContain('?'); // no manual query string
81
+ });
82
+
83
+ test('includes v param when version is provided', async () => {
84
+ mockHttp.get.mockResolvedValue({ data: { success: true, query_name: 'q', data: [], count: 0, query_time_ms: 1 } });
85
+ await client.executeQuery('active_orders', { market: 'ETH' }, 2);
86
+
87
+ const [, config] = mockHttp.get.mock.calls[0];
88
+ expect(config.params).toMatchObject({ market: 'ETH', v: 2 });
89
+ });
90
+
91
+ test('works without params or version', async () => {
92
+ mockHttp.get.mockResolvedValue({ data: { success: true, query_name: 'q', data: [], count: 0, query_time_ms: 1 } });
93
+ await client.executeQuery('active_orders');
94
+
95
+ const [url, config] = mockHttp.get.mock.calls[0];
96
+ expect(url).toBe('/api/queries/test_app/active_orders/data');
97
+ expect(config.params).toEqual({}); // no params, no v
98
+ });
99
+ });
100
+
101
+ describe('listQueries()', () => {
102
+ test('unwraps response.data.queries array', async () => {
103
+ const definitions = [
104
+ { name: 'q1', source_collection: 'orders', base_query: {}, created_at: '2024-01-01' },
105
+ { name: 'q2', source_collection: 'users', base_query: {}, created_at: '2024-01-02' },
106
+ ];
107
+ mockHttp.get.mockResolvedValue({ data: { queries: definitions } });
108
+
109
+ const result = await client.listQueries();
110
+ expect(result).toEqual(definitions);
111
+ });
112
+
113
+ test('falls back to raw data when no queries wrapper', async () => {
114
+ const definitions = [{ name: 'q1', source_collection: 'col', base_query: {}, created_at: '' }];
115
+ mockHttp.get.mockResolvedValue({ data: definitions });
116
+
117
+ const result = await client.listQueries();
118
+ expect(result).toEqual(definitions);
119
+ });
120
+ });
121
+
122
+ describe('deleteQuery()', () => {
123
+ test('sends DELETE to the correct URL', async () => {
124
+ mockHttp.delete.mockResolvedValue({ data: { success: true } });
125
+ await client.deleteQuery('my_query');
126
+
127
+ expect(mockHttp.delete).toHaveBeenCalledWith('/apps/test_app/queries/my_query');
128
+ });
129
+ });
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Retention
133
+ // ---------------------------------------------------------------------------
134
+
135
+ describe('getRetention()', () => {
136
+ test('GETs the correct URL', async () => {
137
+ mockHttp.get.mockResolvedValue({
138
+ data: { collection: 'orders', monthly_cost_per_kb: 0.001, status: 'permanent' },
139
+ });
140
+ await client.getRetention('orders');
141
+
142
+ expect(mockHttp.get).toHaveBeenCalledWith('/api/apps/test_app/collections/orders/retention');
143
+ });
144
+ });
145
+
146
+ describe('setRetention()', () => {
147
+ test('POSTs to the correct URL with retention_days in the body', async () => {
148
+ mockHttp.post.mockResolvedValue({
149
+ data: { message: 'updated', config: { collection: 'orders', monthly_cost_per_kb: 0.001, status: 'active' } },
150
+ });
151
+ await client.setRetention('orders', { retention_days: 90 });
152
+
153
+ expect(mockHttp.post).toHaveBeenCalledWith(
154
+ '/api/apps/test_app/collections/orders/retention',
155
+ { retention_days: 90 }
156
+ );
157
+ });
158
+
159
+ test('can set null to remove retention (permanent storage)', async () => {
160
+ mockHttp.post.mockResolvedValue({ data: { message: 'updated', config: {} } });
161
+ await client.setRetention('orders', { retention_days: null });
162
+
163
+ const [, body] = mockHttp.post.mock.calls[0];
164
+ expect(body.retention_days).toBeNull();
165
+ });
166
+ });
167
+
168
+ describe('getRetentionCost()', () => {
169
+ test('GETs the correct URL', async () => {
170
+ mockHttp.get.mockResolvedValue({
171
+ data: { app_id: 'test_app', collections: [], total_monthly_cost_tia: 0, projected_next_month_cost_tia: 0 },
172
+ });
173
+ await client.getRetentionCost();
174
+
175
+ expect(mockHttp.get).toHaveBeenCalledWith('/api/apps/test_app/retention/cost');
176
+ });
177
+ });
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Collection management
181
+ // ---------------------------------------------------------------------------
182
+
183
+ describe('deleteCollection()', () => {
184
+ test('sends DELETE to the correct URL', async () => {
185
+ mockHttp.delete.mockResolvedValue({ data: { success: true } });
186
+ await client.deleteCollection('old_col');
187
+
188
+ expect(mockHttp.delete).toHaveBeenCalledWith('/api/apps/test_app/collections/old_col');
189
+ });
190
+ });
191
+
192
+ describe('updateCollection()', () => {
193
+ test('sends PATCH to the correct URL with the update body', async () => {
194
+ mockHttp.patch.mockResolvedValue({ data: { success: true } });
195
+ await client.updateCollection('users', { public: true, description: 'Public users' });
196
+
197
+ expect(mockHttp.patch).toHaveBeenCalledWith(
198
+ '/api/apps/test_app/collections/users',
199
+ { public: true, description: 'Public users' }
200
+ );
201
+ });
202
+ });
203
+
204
+ describe('getCollectionInfo()', () => {
205
+ test('GETs using the collectionId (index-based ID, not name)', async () => {
206
+ mockHttp.get.mockResolvedValue({
207
+ data: {
208
+ id: 'test_app_0', name: 'users', namespace: 'ns', primary_column: 'id',
209
+ status: 'active', collection_type: 'local', document_count: 100,
210
+ size_kb: 50, created_at: '', last_modified: '',
211
+ },
212
+ });
213
+ await client.getCollectionInfo('test_app_0');
214
+
215
+ expect(mockHttp.get).toHaveBeenCalledWith('/api/apps/test_app/collections/test_app_0');
216
+ });
217
+ });
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // syncCollection
221
+ // ---------------------------------------------------------------------------
222
+
223
+ describe('syncCollection()', () => {
224
+ test('POSTs one index per indexed field to the correct URL', async () => {
225
+ mockHttp.post.mockResolvedValue({ data: {} });
226
+
227
+ const result = await client.syncCollection({
228
+ name: 'products',
229
+ fields: {
230
+ title: { type: 'string', index: true },
231
+ price: { type: 'number', index: true },
232
+ active: { type: 'boolean', index: false }, // not indexed
233
+ },
234
+ useBaseFields: false,
235
+ });
236
+
237
+ // Only 'title' and 'price' are indexed
238
+ const indexCalls = mockHttp.post.mock.calls.filter(([url]) =>
239
+ url === '/api/apps/test_app/indexes'
240
+ );
241
+ expect(indexCalls).toHaveLength(2);
242
+ const fieldNames = indexCalls.map(([, body]) => body.field_name).sort();
243
+ expect(fieldNames).toEqual(['price', 'title']);
244
+ });
245
+
246
+ test('includes sharding PATCH when schema has sharding config', async () => {
247
+ mockHttp.post.mockResolvedValue({ data: {} });
248
+ mockHttp.patch.mockResolvedValue({ data: {} });
249
+
250
+ const result = await client.syncCollection({
251
+ name: 'snapshots',
252
+ fields: { market: { type: 'string', index: true } },
253
+ useBaseFields: false,
254
+ sharding: {
255
+ keys: [{ field: 'market', type: 'discrete' }],
256
+ enforce_in_queries: true,
257
+ },
258
+ });
259
+
260
+ expect(mockHttp.patch).toHaveBeenCalledWith(
261
+ '/api/apps/test_app/collections/snapshots/sharding',
262
+ expect.objectContaining({ keys: [{ field: 'market', type: 'discrete' }] })
263
+ );
264
+ expect(result.sharding_configured).toBe(true);
265
+ });
266
+
267
+ test('marks index errors in result without throwing', async () => {
268
+ mockHttp.post.mockRejectedValue(new Error('index creation failed'));
269
+
270
+ const result = await client.syncCollection({
271
+ name: 'col',
272
+ fields: { email: { type: 'string', index: true } },
273
+ useBaseFields: false,
274
+ });
275
+
276
+ expect(result.success).toBe(false);
277
+ expect(result.errors!.length).toBeGreaterThan(0);
278
+ expect(result.errors![0]).toContain('email');
279
+ });
280
+ });
@@ -0,0 +1,80 @@
1
+ import { OnChainDBClient } from '../client';
2
+ import { ValidationError } from '../types';
3
+
4
+ // These tests cover pure validation logic that throws before any HTTP is made.
5
+ // No axios mock required — the errors fire synchronously before any await.
6
+
7
+ const clientWithApp = new OnChainDBClient({
8
+ endpoint: 'http://localhost:9092',
9
+ appId: 'test_app',
10
+ });
11
+
12
+ const clientNoApp = new OnChainDBClient({
13
+ endpoint: 'http://localhost:9092',
14
+ });
15
+
16
+ describe('OnChainDBClient — store() validation', () => {
17
+ test('throws when data is empty array', async () => {
18
+ await expect(
19
+ clientWithApp.store({ collection: 'col', data: [] })
20
+ ).rejects.toThrow(ValidationError);
21
+ });
22
+
23
+ test('throws when data is not an array', async () => {
24
+ await expect(
25
+ clientWithApp.store({ collection: 'col', data: 'not-an-array' as any })
26
+ ).rejects.toThrow(ValidationError);
27
+ });
28
+
29
+ test('throws when data is missing entirely', async () => {
30
+ await expect(
31
+ clientWithApp.store({ collection: 'col' } as any)
32
+ ).rejects.toThrow(ValidationError);
33
+ });
34
+
35
+ test('throws when neither root nor collection is provided', async () => {
36
+ await expect(
37
+ clientWithApp.store({ data: [{ id: '1' }] })
38
+ ).rejects.toThrow(ValidationError);
39
+ });
40
+
41
+ test('throws when data item is not an object', async () => {
42
+ await expect(
43
+ clientWithApp.store({ collection: 'col', data: ['not-an-object'] as any })
44
+ ).rejects.toThrow(ValidationError);
45
+ });
46
+
47
+ test('throws when total data size exceeds 5MB', async () => {
48
+ const bigRecord = { id: '1', payload: 'x'.repeat(6 * 1024 * 1024) };
49
+ await expect(
50
+ clientWithApp.store({ collection: 'col', data: [bigRecord] })
51
+ ).rejects.toThrow(ValidationError);
52
+ });
53
+ });
54
+
55
+ describe('OnChainDBClient — appId-required methods throw ValidationError when appId is missing', () => {
56
+ const cases: Array<[string, () => Promise<any>]> = [
57
+ ['setupSharding()', () => clientNoApp.setupSharding('col', { keys: [], enforce_in_queries: false })],
58
+ ['syncCollection()', () => clientNoApp.syncCollection({ name: 'col', fields: {} })],
59
+ ['deleteCollection()', () => clientNoApp.deleteCollection('col')],
60
+ ['updateCollection()', () => clientNoApp.updateCollection('col', {})],
61
+ ['getCollectionInfo()', () => clientNoApp.getCollectionInfo('col_0')],
62
+ ['getRetention()', () => clientNoApp.getRetention('col')],
63
+ ['setRetention()', () => clientNoApp.setRetention('col', { retention_days: 30 })],
64
+ ['getRetentionCost()', () => clientNoApp.getRetentionCost()],
65
+ ['listQueries()', () => clientNoApp.listQueries()],
66
+ ['createQuery()', () => clientNoApp.createQuery({ name: 'q', source_collection: 'col', base_query: {} })],
67
+ ['getQuery()', () => clientNoApp.getQuery('q')],
68
+ ['deleteQuery()', () => clientNoApp.deleteQuery('q')],
69
+ ['executeQuery()', () => clientNoApp.executeQuery('q')],
70
+ // database() tested separately below — it throws synchronously
71
+ ];
72
+
73
+ test.each(cases)('%s', async (_name, fn) => {
74
+ await expect(fn()).rejects.toThrow(ValidationError);
75
+ });
76
+
77
+ test('database()', () => {
78
+ expect(() => clientNoApp.database()).toThrow(ValidationError);
79
+ });
80
+ });