@onchaindb/sdk 0.4.5 → 2.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 (117) 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 +31 -46
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +222 -357
  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 +10 -13
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +4 -18
  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/{src/query-sdk/dist/OnChainDB.d.ts → dist/query-sdk/OnDB.d.ts} +10 -2
  28. package/dist/query-sdk/OnDB.d.ts.map +1 -0
  29. package/{src/query-sdk/dist/OnChainDB.js → dist/query-sdk/OnDB.js} +86 -18
  30. package/dist/query-sdk/OnDB.js.map +1 -0
  31. package/dist/query-sdk/QueryBuilder.d.ts +4 -2
  32. package/dist/query-sdk/QueryBuilder.d.ts.map +1 -1
  33. package/dist/query-sdk/QueryBuilder.js +47 -169
  34. package/dist/query-sdk/QueryBuilder.js.map +1 -1
  35. package/dist/query-sdk/QueryResult.d.ts +0 -38
  36. package/dist/query-sdk/QueryResult.d.ts.map +1 -1
  37. package/dist/query-sdk/QueryResult.js +1 -227
  38. package/dist/query-sdk/QueryResult.js.map +1 -1
  39. package/dist/query-sdk/index.d.ts +2 -2
  40. package/dist/query-sdk/index.d.ts.map +1 -1
  41. package/dist/query-sdk/index.js +3 -3
  42. package/dist/query-sdk/index.js.map +1 -1
  43. package/dist/query-sdk/operators.d.ts +32 -28
  44. package/dist/query-sdk/operators.d.ts.map +1 -1
  45. package/dist/query-sdk/operators.js +45 -155
  46. package/dist/query-sdk/operators.js.map +1 -1
  47. package/dist/types.d.ts +159 -36
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/types.js +8 -8
  50. package/dist/types.js.map +1 -1
  51. package/dist/x402/types.d.ts +1 -1
  52. package/dist/x402/types.d.ts.map +1 -1
  53. package/dist/x402/utils.js +2 -2
  54. package/dist/x402/utils.js.map +1 -1
  55. package/jest.config.js +4 -0
  56. package/package.json +1 -1
  57. package/skills.md +0 -1
  58. package/src/batch.d.ts +3 -3
  59. package/src/batch.js +1 -1
  60. package/src/client.ts +287 -823
  61. package/src/database.d.ts +1 -1
  62. package/src/database.js +4 -4
  63. package/src/database.ts +71 -494
  64. package/src/index.d.ts +18 -18
  65. package/src/index.js +16 -16
  66. package/src/index.ts +44 -198
  67. package/src/query-sdk/ConditionBuilder.ts +37 -89
  68. package/src/query-sdk/NestedBuilders.ts +90 -92
  69. package/src/query-sdk/{OnChainDB.ts → OnDB.ts} +1 -1
  70. package/src/query-sdk/QueryBuilder.ts +59 -218
  71. package/src/query-sdk/QueryResult.ts +4 -330
  72. package/src/query-sdk/README.md +218 -587
  73. package/src/query-sdk/index.ts +2 -2
  74. package/src/query-sdk/operators.ts +91 -200
  75. package/src/query-sdk/tests/FieldConditionBuilder.test.ts +70 -71
  76. package/src/query-sdk/tests/LogicalOperator.test.ts +43 -82
  77. package/src/query-sdk/tests/NestedBuilders.test.ts +229 -309
  78. package/src/query-sdk/tests/QueryBuilder.test.ts +5 -5
  79. package/src/query-sdk/tests/QueryResult.test.ts +41 -435
  80. package/src/query-sdk/tests/comprehensive.test.ts +4 -185
  81. package/src/tests/client-requests.test.ts +280 -0
  82. package/src/tests/client-validation.test.ts +80 -0
  83. package/src/types.d.ts +6 -6
  84. package/src/types.js +8 -8
  85. package/src/types.ts +239 -54
  86. package/src/x402/types.ts +3 -3
  87. package/src/x402/utils.ts +3 -3
  88. package/examples/blob-upload-example.ts +0 -140
  89. package/src/batch.ts +0 -257
  90. package/src/query-sdk/dist/ConditionBuilder.d.ts +0 -22
  91. package/src/query-sdk/dist/ConditionBuilder.js +0 -90
  92. package/src/query-sdk/dist/FieldConditionBuilder.d.ts +0 -1
  93. package/src/query-sdk/dist/FieldConditionBuilder.js +0 -6
  94. package/src/query-sdk/dist/NestedBuilders.d.ts +0 -43
  95. package/src/query-sdk/dist/NestedBuilders.js +0 -144
  96. package/src/query-sdk/dist/QueryBuilder.d.ts +0 -70
  97. package/src/query-sdk/dist/QueryBuilder.js +0 -295
  98. package/src/query-sdk/dist/QueryResult.d.ts +0 -52
  99. package/src/query-sdk/dist/QueryResult.js +0 -293
  100. package/src/query-sdk/dist/SelectionBuilder.d.ts +0 -20
  101. package/src/query-sdk/dist/SelectionBuilder.js +0 -80
  102. package/src/query-sdk/dist/adapters/HttpClientAdapter.d.ts +0 -27
  103. package/src/query-sdk/dist/adapters/HttpClientAdapter.js +0 -170
  104. package/src/query-sdk/dist/index.d.ts +0 -36
  105. package/src/query-sdk/dist/index.js +0 -27
  106. package/src/query-sdk/dist/operators.d.ts +0 -56
  107. package/src/query-sdk/dist/operators.js +0 -289
  108. package/src/query-sdk/dist/tests/setup.d.ts +0 -15
  109. package/src/query-sdk/dist/tests/setup.js +0 -46
  110. package/src/query-sdk/jest.config.js +0 -25
  111. package/src/query-sdk/package.json +0 -46
  112. package/src/query-sdk/tests/aggregations.test.ts +0 -653
  113. package/src/query-sdk/tests/integration.test.ts +0 -608
  114. package/src/query-sdk/tests/operators.test.ts +0 -327
  115. package/src/query-sdk/tests/unit.test.ts +0 -794
  116. package/src/query-sdk/tsconfig.json +0 -26
  117. 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 { OnDBClient } 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: OnDBClient;
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 OnDBClient({
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 { OnDBClient } 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 OnDBClient({
8
+ endpoint: 'http://localhost:9092',
9
+ appId: 'test_app',
10
+ });
11
+
12
+ const clientNoApp = new OnDBClient({
13
+ endpoint: 'http://localhost:9092',
14
+ });
15
+
16
+ describe('OnDBClient — 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('OnDBClient — 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
+ });
package/src/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { QueryValue } from "./query-sdk";
2
- export interface OnChainDBConfig {
2
+ export interface OnDBConfig {
3
3
  endpoint: string;
4
4
  apiKey?: string;
5
5
  appId?: string;
@@ -68,25 +68,25 @@ export interface TransactionEvents {
68
68
  'transaction:failed': (tx: TransactionStatus) => void;
69
69
  'error': (error: Error) => void;
70
70
  }
71
- export declare class OnChainDBError extends Error {
71
+ export declare class OnDBError extends Error {
72
72
  code: string;
73
73
  statusCode?: number | undefined;
74
74
  details?: any | undefined;
75
75
  constructor(message: string, code: string, statusCode?: number | undefined, details?: any | undefined);
76
76
  }
77
- export declare class TransactionError extends OnChainDBError {
77
+ export declare class TransactionError extends OnDBError {
78
78
  transactionId: string;
79
79
  constructor(message: string, transactionId: string, details?: any);
80
80
  }
81
- export declare class ValidationError extends OnChainDBError {
81
+ export declare class ValidationError extends OnDBError {
82
82
  constructor(message: string, details?: any);
83
83
  }
84
- export declare class PaymentRequiredError extends OnChainDBError {
84
+ export declare class PaymentRequiredError extends OnDBError {
85
85
  requiredAmount: number;
86
86
  providedAmount?: number | undefined;
87
87
  constructor(message: string, requiredAmount: number, providedAmount?: number | undefined, details?: any);
88
88
  }
89
- export declare class PaymentVerificationError extends OnChainDBError {
89
+ export declare class PaymentVerificationError extends OnDBError {
90
90
  txHash: string;
91
91
  constructor(message: string, txHash: string, details?: any);
92
92
  }
package/src/types.js CHANGED
@@ -1,18 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PaymentVerificationError = exports.PaymentRequiredError = exports.ValidationError = exports.TransactionError = exports.OnChainDBError = void 0;
3
+ exports.PaymentVerificationError = exports.PaymentRequiredError = exports.ValidationError = exports.TransactionError = exports.OnDBError = void 0;
4
4
  // Error Types
5
- class OnChainDBError extends Error {
5
+ class OnDBError extends Error {
6
6
  constructor(message, code, statusCode, details) {
7
7
  super(message);
8
8
  this.code = code;
9
9
  this.statusCode = statusCode;
10
10
  this.details = details;
11
- this.name = 'OnChainDBError';
11
+ this.name = 'OnDBError';
12
12
  }
13
13
  }
14
- exports.OnChainDBError = OnChainDBError;
15
- class TransactionError extends OnChainDBError {
14
+ exports.OnDBError = OnDBError;
15
+ class TransactionError extends OnDBError {
16
16
  constructor(message, transactionId, details) {
17
17
  super(message, 'TRANSACTION_ERROR', undefined, details);
18
18
  this.transactionId = transactionId;
@@ -20,14 +20,14 @@ class TransactionError extends OnChainDBError {
20
20
  }
21
21
  }
22
22
  exports.TransactionError = TransactionError;
23
- class ValidationError extends OnChainDBError {
23
+ class ValidationError extends OnDBError {
24
24
  constructor(message, details) {
25
25
  super(message, 'VALIDATION_ERROR', 400, details);
26
26
  this.name = 'ValidationError';
27
27
  }
28
28
  }
29
29
  exports.ValidationError = ValidationError;
30
- class PaymentRequiredError extends OnChainDBError {
30
+ class PaymentRequiredError extends OnDBError {
31
31
  constructor(message, requiredAmount, providedAmount, details) {
32
32
  super(message, 'PAYMENT_REQUIRED', 402, details);
33
33
  this.requiredAmount = requiredAmount;
@@ -36,7 +36,7 @@ class PaymentRequiredError extends OnChainDBError {
36
36
  }
37
37
  }
38
38
  exports.PaymentRequiredError = PaymentRequiredError;
39
- class PaymentVerificationError extends OnChainDBError {
39
+ class PaymentVerificationError extends OnDBError {
40
40
  constructor(message, txHash, details) {
41
41
  super(message, 'PAYMENT_VERIFICATION_FAILED', 402, details);
42
42
  this.txHash = txHash;