@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.
- package/.claude/settings.local.json +10 -2
- package/README.md +422 -355
- package/dist/batch.d.ts +1 -10
- package/dist/batch.d.ts.map +1 -1
- package/dist/batch.js +4 -26
- package/dist/batch.js.map +1 -1
- package/dist/client.d.ts +29 -43
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +198 -323
- package/dist/client.js.map +1 -1
- package/dist/database.d.ts +14 -131
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +35 -131
- package/dist/database.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -15
- package/dist/index.js.map +1 -1
- package/dist/query-sdk/ConditionBuilder.d.ts +3 -11
- package/dist/query-sdk/ConditionBuilder.d.ts.map +1 -1
- package/dist/query-sdk/ConditionBuilder.js +10 -48
- package/dist/query-sdk/ConditionBuilder.js.map +1 -1
- package/dist/query-sdk/NestedBuilders.d.ts +33 -30
- package/dist/query-sdk/NestedBuilders.d.ts.map +1 -1
- package/dist/query-sdk/NestedBuilders.js +46 -43
- package/dist/query-sdk/NestedBuilders.js.map +1 -1
- package/dist/query-sdk/QueryBuilder.d.ts +4 -2
- package/dist/query-sdk/QueryBuilder.d.ts.map +1 -1
- package/dist/query-sdk/QueryBuilder.js +47 -169
- package/dist/query-sdk/QueryBuilder.js.map +1 -1
- package/dist/query-sdk/QueryResult.d.ts +0 -38
- package/dist/query-sdk/QueryResult.d.ts.map +1 -1
- package/dist/query-sdk/QueryResult.js +1 -227
- package/dist/query-sdk/QueryResult.js.map +1 -1
- package/dist/query-sdk/index.d.ts +1 -1
- package/dist/query-sdk/index.d.ts.map +1 -1
- package/dist/query-sdk/index.js.map +1 -1
- package/dist/query-sdk/operators.d.ts +32 -28
- package/dist/query-sdk/operators.d.ts.map +1 -1
- package/dist/query-sdk/operators.js +45 -155
- package/dist/query-sdk/operators.js.map +1 -1
- package/dist/types.d.ts +153 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/jest.config.js +4 -0
- package/package.json +1 -1
- package/skills.md +0 -1
- package/src/client.ts +242 -745
- package/src/database.ts +70 -493
- package/src/index.ts +40 -193
- package/src/query-sdk/ConditionBuilder.ts +37 -89
- package/src/query-sdk/NestedBuilders.ts +90 -92
- package/src/query-sdk/QueryBuilder.ts +59 -218
- package/src/query-sdk/QueryResult.ts +4 -330
- package/src/query-sdk/README.md +214 -583
- package/src/query-sdk/index.ts +1 -1
- package/src/query-sdk/operators.ts +91 -200
- package/src/query-sdk/tests/FieldConditionBuilder.test.ts +70 -71
- package/src/query-sdk/tests/LogicalOperator.test.ts +43 -82
- package/src/query-sdk/tests/NestedBuilders.test.ts +229 -309
- package/src/query-sdk/tests/QueryBuilder.test.ts +5 -5
- package/src/query-sdk/tests/QueryResult.test.ts +41 -435
- package/src/query-sdk/tests/comprehensive.test.ts +4 -185
- package/src/tests/client-requests.test.ts +280 -0
- package/src/tests/client-validation.test.ts +80 -0
- package/src/types.ts +229 -8
- package/src/batch.ts +0 -257
- package/src/query-sdk/dist/ConditionBuilder.d.ts +0 -22
- package/src/query-sdk/dist/ConditionBuilder.js +0 -90
- package/src/query-sdk/dist/FieldConditionBuilder.d.ts +0 -1
- package/src/query-sdk/dist/FieldConditionBuilder.js +0 -6
- package/src/query-sdk/dist/NestedBuilders.d.ts +0 -43
- package/src/query-sdk/dist/NestedBuilders.js +0 -144
- package/src/query-sdk/dist/OnChainDB.d.ts +0 -19
- package/src/query-sdk/dist/OnChainDB.js +0 -123
- package/src/query-sdk/dist/QueryBuilder.d.ts +0 -70
- package/src/query-sdk/dist/QueryBuilder.js +0 -295
- package/src/query-sdk/dist/QueryResult.d.ts +0 -52
- package/src/query-sdk/dist/QueryResult.js +0 -293
- package/src/query-sdk/dist/SelectionBuilder.d.ts +0 -20
- package/src/query-sdk/dist/SelectionBuilder.js +0 -80
- package/src/query-sdk/dist/adapters/HttpClientAdapter.d.ts +0 -27
- package/src/query-sdk/dist/adapters/HttpClientAdapter.js +0 -170
- package/src/query-sdk/dist/index.d.ts +0 -36
- package/src/query-sdk/dist/index.js +0 -27
- package/src/query-sdk/dist/operators.d.ts +0 -56
- package/src/query-sdk/dist/operators.js +0 -289
- package/src/query-sdk/dist/tests/setup.d.ts +0 -15
- package/src/query-sdk/dist/tests/setup.js +0 -46
- package/src/query-sdk/jest.config.js +0 -25
- package/src/query-sdk/package.json +0 -46
- package/src/query-sdk/tests/aggregations.test.ts +0 -653
- package/src/query-sdk/tests/integration.test.ts +0 -608
- package/src/query-sdk/tests/operators.test.ts +0 -327
- package/src/query-sdk/tests/unit.test.ts +0 -794
- package/src/query-sdk/tsconfig.json +0 -26
- package/src/query-sdk/yarn.lock +0 -3092
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QueryBuilder, FieldConditionBuilder, LogicalOperator
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|