@onchaindb/sdk 0.4.0 → 0.4.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.
Files changed (98) hide show
  1. package/.DS_Store +0 -0
  2. package/.claude/settings.local.json +8 -0
  3. package/.gitignore +5 -0
  4. package/.idea/.gitignore +5 -0
  5. package/.idea/compiler.xml +6 -0
  6. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  7. package/.idea/jsLinters/eslint.xml +6 -0
  8. package/.idea/modules.xml +8 -0
  9. package/.idea/prettier.xml +7 -0
  10. package/.idea/sdk.iml +12 -0
  11. package/.idea/vcs.xml +6 -0
  12. package/.idea/workspace.xml +257 -0
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +11 -3
  15. package/dist/client.js.map +1 -1
  16. package/dist/database.d.ts +0 -20
  17. package/dist/database.d.ts.map +1 -1
  18. package/dist/database.js +0 -40
  19. package/dist/database.js.map +1 -1
  20. package/dist/query-sdk/tests/setup.d.ts +16 -0
  21. package/dist/query-sdk/tests/setup.d.ts.map +1 -0
  22. package/dist/query-sdk/tests/setup.js +49 -0
  23. package/dist/query-sdk/tests/setup.js.map +1 -0
  24. package/examples/basic-usage.ts +136 -0
  25. package/examples/blob-upload-example.ts +140 -0
  26. package/examples/collection-schema-example.ts +304 -0
  27. package/examples/server-side-joins.ts +201 -0
  28. package/examples/tweet-self-joins-example.ts +352 -0
  29. package/package-lock.json +3823 -0
  30. package/package.json +1 -1
  31. package/skills.md +1096 -0
  32. package/src/.env +1 -0
  33. package/src/batch.d.ts +121 -0
  34. package/src/batch.js +205 -0
  35. package/src/batch.ts +257 -0
  36. package/src/client.ts +1856 -0
  37. package/src/database.d.ts +268 -0
  38. package/src/database.js +294 -0
  39. package/src/database.ts +695 -0
  40. package/src/index.d.ts +160 -0
  41. package/src/index.js +186 -0
  42. package/src/index.ts +253 -0
  43. package/src/query-sdk/ConditionBuilder.ts +103 -0
  44. package/src/query-sdk/FieldConditionBuilder.ts +2 -0
  45. package/src/query-sdk/NestedBuilders.ts +186 -0
  46. package/src/query-sdk/OnChainDB.ts +294 -0
  47. package/src/query-sdk/QueryBuilder.ts +1191 -0
  48. package/src/query-sdk/QueryResult.ts +375 -0
  49. package/src/query-sdk/README.md +866 -0
  50. package/src/query-sdk/SelectionBuilder.ts +94 -0
  51. package/src/query-sdk/adapters/HttpClientAdapter.ts +249 -0
  52. package/src/query-sdk/dist/ConditionBuilder.d.ts +22 -0
  53. package/src/query-sdk/dist/ConditionBuilder.js +90 -0
  54. package/src/query-sdk/dist/FieldConditionBuilder.d.ts +1 -0
  55. package/src/query-sdk/dist/FieldConditionBuilder.js +6 -0
  56. package/src/query-sdk/dist/NestedBuilders.d.ts +43 -0
  57. package/src/query-sdk/dist/NestedBuilders.js +144 -0
  58. package/src/query-sdk/dist/OnChainDB.d.ts +19 -0
  59. package/src/query-sdk/dist/OnChainDB.js +123 -0
  60. package/src/query-sdk/dist/QueryBuilder.d.ts +70 -0
  61. package/src/query-sdk/dist/QueryBuilder.js +295 -0
  62. package/src/query-sdk/dist/QueryResult.d.ts +52 -0
  63. package/src/query-sdk/dist/QueryResult.js +293 -0
  64. package/src/query-sdk/dist/SelectionBuilder.d.ts +20 -0
  65. package/src/query-sdk/dist/SelectionBuilder.js +80 -0
  66. package/src/query-sdk/dist/adapters/HttpClientAdapter.d.ts +27 -0
  67. package/src/query-sdk/dist/adapters/HttpClientAdapter.js +170 -0
  68. package/src/query-sdk/dist/index.d.ts +36 -0
  69. package/src/query-sdk/dist/index.js +27 -0
  70. package/src/query-sdk/dist/operators.d.ts +56 -0
  71. package/src/query-sdk/dist/operators.js +289 -0
  72. package/src/query-sdk/dist/tests/setup.d.ts +15 -0
  73. package/src/query-sdk/dist/tests/setup.js +46 -0
  74. package/src/query-sdk/index.ts +59 -0
  75. package/src/query-sdk/jest.config.js +25 -0
  76. package/src/query-sdk/operators.ts +335 -0
  77. package/src/query-sdk/package.json +46 -0
  78. package/src/query-sdk/tests/FieldConditionBuilder.test.ts +84 -0
  79. package/src/query-sdk/tests/LogicalOperator.test.ts +85 -0
  80. package/src/query-sdk/tests/NestedBuilders.test.ts +321 -0
  81. package/src/query-sdk/tests/QueryBuilder.test.ts +348 -0
  82. package/src/query-sdk/tests/QueryResult.test.ts +464 -0
  83. package/src/query-sdk/tests/aggregations.test.ts +653 -0
  84. package/src/query-sdk/tests/comprehensive.test.ts +279 -0
  85. package/src/query-sdk/tests/integration.test.ts +608 -0
  86. package/src/query-sdk/tests/operators.test.ts +327 -0
  87. package/src/query-sdk/tests/setup.ts +59 -0
  88. package/src/query-sdk/tests/unit.test.ts +794 -0
  89. package/src/query-sdk/tsconfig.json +26 -0
  90. package/src/query-sdk/yarn.lock +3092 -0
  91. package/src/types.d.ts +131 -0
  92. package/src/types.js +46 -0
  93. package/src/types.ts +534 -0
  94. package/src/x402/index.ts +12 -0
  95. package/src/x402/types.ts +250 -0
  96. package/src/x402/utils.ts +332 -0
  97. package/tsconfig.json +20 -0
  98. package/yarn.lock +2309 -0
@@ -0,0 +1,321 @@
1
+ import { NestedConditionBuilder, NestedFieldConditionBuilder } from '../NestedBuilders';
2
+ import { LogicalOperator } from '../operators';
3
+
4
+ describe('NestedConditionBuilder', () => {
5
+ let nestedBuilder: NestedConditionBuilder;
6
+
7
+ beforeEach(() => {
8
+ nestedBuilder = new NestedConditionBuilder('user');
9
+ });
10
+
11
+ describe('Basic nested building', () => {
12
+ test('should create nested field condition builder', () => {
13
+ const fieldBuilder = nestedBuilder.field('name');
14
+ expect(fieldBuilder).toBeInstanceOf(NestedFieldConditionBuilder);
15
+ });
16
+
17
+ test('should create condition with correct path', () => {
18
+ const condition = nestedBuilder.field('name').equals('John');
19
+ expect(condition.field).toBe('user.name');
20
+ expect(condition.operator).toBe('is');
21
+ expect(condition.value).toBe('John');
22
+ });
23
+
24
+ test('should handle multi-level nesting', () => {
25
+ const condition = new NestedFieldConditionBuilder(['user', 'profile', 'bio']).contains('developer');
26
+ expect(condition.field).toBe('user.profile.bio');
27
+ expect(condition.operator).toBe('includes');
28
+ expect(condition.value).toBe('developer');
29
+ });
30
+ });
31
+
32
+ describe('Logical groups', () => {
33
+ test('should create AND groups', () => {
34
+ const andGroup = nestedBuilder.andGroup(builder => [
35
+ LogicalOperator.Condition(builder.field('name').equals('John')),
36
+ LogicalOperator.Condition(builder.field('age').greaterThan(25))
37
+ ]);
38
+
39
+ expect(andGroup.type).toBe('and');
40
+ expect(andGroup.conditions).toHaveLength(2);
41
+ });
42
+
43
+ test('should create OR groups', () => {
44
+ const orGroup = nestedBuilder.orGroup(builder => [
45
+ LogicalOperator.Condition(builder.field('status').equals('active')),
46
+ LogicalOperator.Condition(builder.field('status').equals('pending'))
47
+ ]);
48
+
49
+ expect(orGroup.type).toBe('or');
50
+ expect(orGroup.conditions).toHaveLength(2);
51
+ });
52
+
53
+ test('should create NOT groups', () => {
54
+ const notGroup = nestedBuilder.notGroup(builder => [
55
+ LogicalOperator.Condition(builder.field('deleted').isTrue())
56
+ ]);
57
+
58
+ expect(notGroup.type).toBe('not');
59
+ expect(notGroup.conditions).toHaveLength(1);
60
+ });
61
+ });
62
+
63
+ describe('Nested structure creation', () => {
64
+ test('should create nested callback structure', () => {
65
+ const nested = nestedBuilder.nested('profile', nested =>
66
+ LogicalOperator.Condition(nested.field('bio').equals('Developer'))
67
+ );
68
+
69
+ expect(nested.type).toBe('condition');
70
+ });
71
+ });
72
+ });
73
+
74
+ describe('NestedFieldConditionBuilder', () => {
75
+ let fieldBuilder: NestedFieldConditionBuilder;
76
+
77
+ beforeEach(() => {
78
+ fieldBuilder = new NestedFieldConditionBuilder(['user', 'profile', 'details']);
79
+ });
80
+
81
+ describe('Path construction', () => {
82
+ test('should construct correct field path', () => {
83
+ const condition = fieldBuilder.equals('test');
84
+ expect(condition.field).toBe('user.profile.details');
85
+ });
86
+
87
+ test('should work with single-level path', () => {
88
+ const singleLevel = new NestedFieldConditionBuilder(['name']);
89
+ const condition = singleLevel.equals('John');
90
+ expect(condition.field).toBe('name');
91
+ });
92
+
93
+ test('should work with deep nesting', () => {
94
+ const deepNesting = new NestedFieldConditionBuilder(['a', 'b', 'c', 'd', 'e']);
95
+ const condition = deepNesting.contains('value');
96
+ expect(condition.field).toBe('a.b.c.d.e');
97
+ });
98
+ });
99
+
100
+ describe('Basic operators', () => {
101
+ test('equals should create correct condition', () => {
102
+ const condition = fieldBuilder.equals('value');
103
+ expect(condition.field).toBe('user.profile.details');
104
+ expect(condition.operator).toBe('is');
105
+ expect(condition.value).toBe('value');
106
+ });
107
+
108
+ test('notEquals should create correct condition', () => {
109
+ const condition = fieldBuilder.notEquals('value');
110
+ expect(condition.operator).toBe('isNot');
111
+ expect(condition.value).toBe('value');
112
+ });
113
+
114
+ test('comparison operators should work correctly', () => {
115
+ expect(fieldBuilder.greaterThan(10).operator).toBe('greaterThan');
116
+ expect(fieldBuilder.greaterThanOrEqual(10).operator).toBe('greaterThanOrEqual');
117
+ expect(fieldBuilder.lessThan(10).operator).toBe('lessThan');
118
+ expect(fieldBuilder.lessThanOrEqual(10).operator).toBe('lessThanOrEqual');
119
+ });
120
+ });
121
+
122
+ describe('String operators', () => {
123
+ test('string operations should work correctly', () => {
124
+ expect(fieldBuilder.contains('test').operator).toBe('includes');
125
+ expect(fieldBuilder.startsWith('pre').operator).toBe('startsWith');
126
+ expect(fieldBuilder.endsWith('suf').operator).toBe('endsWith');
127
+ expect(fieldBuilder.regExpMatches('.*').operator).toBe('regExpMatches');
128
+ expect(fieldBuilder.includesCaseInsensitive('TEST').operator).toBe('includesCaseInsensitive');
129
+ });
130
+
131
+ test('wildcard operators should work correctly', () => {
132
+ expect(fieldBuilder.wildcard('*.txt').operator).toBe('wildcard');
133
+ expect(fieldBuilder.glob('**/*.js').operator).toBe('glob');
134
+ });
135
+ });
136
+
137
+ describe('Array operators', () => {
138
+ test('list operations should work correctly', () => {
139
+ const inCondition = fieldBuilder.in([1, 2, 3]);
140
+ expect(inCondition.operator).toBe('in');
141
+ expect(inCondition.value).toEqual([1, 2, 3]);
142
+
143
+ const notInCondition = fieldBuilder.notIn(['a', 'b']);
144
+ expect(notInCondition.operator).toBe('notIn');
145
+ expect(notInCondition.value).toEqual(['a', 'b']);
146
+ });
147
+
148
+ test('base operators should work correctly', () => {
149
+ expect(fieldBuilder.equals('value').operator).toBe('is');
150
+ expect(fieldBuilder.notEquals('value').operator).toBe('isNot');
151
+ expect(fieldBuilder.isNull().operator).toBe('isNull');
152
+ expect(fieldBuilder.isNotNull().operator).toBe('isNull');
153
+ });
154
+ });
155
+
156
+ describe('Existence operators', () => {
157
+ test('existence checks should work correctly', () => {
158
+ expect(fieldBuilder.exists().operator).toBe('exists');
159
+ expect(fieldBuilder.exists().value).toBe(true);
160
+
161
+ expect(fieldBuilder.notExists().operator).toBe('exists');
162
+ expect(fieldBuilder.notExists().value).toBe(false);
163
+ });
164
+
165
+ test('null checks should work correctly', () => {
166
+ expect(fieldBuilder.isNull().operator).toBe('isNull');
167
+ expect(fieldBuilder.isNull().value).toBe(true);
168
+
169
+ expect(fieldBuilder.isNotNull().operator).toBe('isNull');
170
+ expect(fieldBuilder.isNotNull().value).toBe(false);
171
+ });
172
+
173
+ test('empty checks should work correctly', () => {
174
+ expect(fieldBuilder.isEmpty().operator).toBe('isEmpty');
175
+ expect(fieldBuilder.isEmpty().value).toBe(true);
176
+
177
+ expect(fieldBuilder.isNotEmpty().operator).toBe('isEmpty');
178
+ expect(fieldBuilder.isNotEmpty().value).toBe(false);
179
+ });
180
+ });
181
+
182
+ describe('Numeric operators', () => {
183
+ test('number operations should work correctly', () => {
184
+ const betweenCondition = fieldBuilder.between(10, 20);
185
+ expect(betweenCondition.operator).toBe('betweenOp');
186
+ expect(betweenCondition.value).toEqual({ from: 10, to: 20 });
187
+
188
+ expect(fieldBuilder.greaterThan(100).operator).toBe('greaterThan');
189
+ expect(fieldBuilder.lessThan(500).operator).toBe('lessThan');
190
+ expect(fieldBuilder.greaterThanOrEqual(100).operator).toBe('greaterThanOrEqual');
191
+ expect(fieldBuilder.lessThanOrEqual(500).operator).toBe('lessThanOrEqual');
192
+ });
193
+ });
194
+
195
+ describe('Misc operators', () => {
196
+ test('misc operators should work correctly', () => {
197
+ expect(fieldBuilder.b64('dGVzdA==').operator).toBe('b64');
198
+ expect(fieldBuilder.inCountry('US').operator).toBe('inCountry');
199
+ expect(fieldBuilder.inDataset('malware_ips').operator).toBe('inDataset');
200
+ expect(fieldBuilder.cidr('192.168.1.0/24').operator).toBe('CIDR');
201
+ });
202
+ });
203
+
204
+ describe('Boolean operators', () => {
205
+ test('boolean checks should work correctly', () => {
206
+ const trueCondition = fieldBuilder.isTrue();
207
+ expect(trueCondition.operator).toBe('is');
208
+ expect(trueCondition.value).toBe(true);
209
+
210
+ const falseCondition = fieldBuilder.isFalse();
211
+ expect(falseCondition.operator).toBe('is');
212
+ expect(falseCondition.value).toBe(false);
213
+ });
214
+ });
215
+
216
+ describe('Type checking operators', () => {
217
+ test('type validation operators should work correctly', () => {
218
+ expect(fieldBuilder.isString().operator).toBe('isString');
219
+ expect(fieldBuilder.isNumber().operator).toBe('isNumber');
220
+ expect(fieldBuilder.isBoolean().operator).toBe('isBoolean');
221
+ expect(fieldBuilder.isArray().operator).toBe('isArray');
222
+ expect(fieldBuilder.isObject().operator).toBe('isObject');
223
+ });
224
+ });
225
+
226
+ describe('String operators', () => {
227
+ test('string operators should work correctly', () => {
228
+ expect(fieldBuilder.startsWith('prefix').operator).toBe('startsWith');
229
+ expect(fieldBuilder.endsWith('suffix').operator).toBe('endsWith');
230
+ expect(fieldBuilder.contains('substring').operator).toBe('includes');
231
+ expect(fieldBuilder.regExpMatches('^test.*').operator).toBe('regExpMatches');
232
+ expect(fieldBuilder.includesCaseInsensitive('Test').operator).toBe('includesCaseInsensitive');
233
+ });
234
+ });
235
+
236
+ describe('IP address operators', () => {
237
+ test('IP address operations should work correctly', () => {
238
+ expect(fieldBuilder.isLocalIp().operator).toBe('isLocalIp');
239
+ expect(fieldBuilder.isExternalIp().operator).toBe('isExternalIp');
240
+ expect(fieldBuilder.inCountry('US').operator).toBe('inCountry');
241
+ expect(fieldBuilder.cidr('192.168.1.0/24').operator).toBe('CIDR');
242
+ expect(fieldBuilder.b64('dGVzdA==').operator).toBe('b64');
243
+ expect(fieldBuilder.inDataset('malware_ips').operator).toBe('inDataset');
244
+ });
245
+ });
246
+
247
+ describe('Number operators', () => {
248
+ test('number operations should work correctly', () => {
249
+ expect(fieldBuilder.greaterThan(100).operator).toBe('greaterThan');
250
+ expect(fieldBuilder.lessThan(500).operator).toBe('lessThan');
251
+ expect(fieldBuilder.greaterThanOrEqual(100).operator).toBe('greaterThanOrEqual');
252
+ expect(fieldBuilder.lessThanOrEqual(500).operator).toBe('lessThanOrEqual');
253
+ expect(fieldBuilder.between(100, 500).operator).toBe('betweenOp');
254
+ });
255
+ });
256
+
257
+ describe('Complex scenarios', () => {
258
+ test('should work with very deep nesting', () => {
259
+ const deepPath = ['level1', 'level2', 'level3', 'level4', 'level5', 'finalField'];
260
+ const deepBuilder = new NestedFieldConditionBuilder(deepPath);
261
+ const condition = deepBuilder.equals('deep_value');
262
+
263
+ expect(condition.field).toBe('level1.level2.level3.level4.level5.finalField');
264
+ expect(condition.value).toBe('deep_value');
265
+ });
266
+
267
+ test('should handle special characters in field names', () => {
268
+ const specialPath = ['user-profile', 'bio_text', 'data@field'];
269
+ const specialBuilder = new NestedFieldConditionBuilder(specialPath);
270
+ const condition = specialBuilder.contains('text');
271
+
272
+ expect(condition.field).toBe('user-profile.bio_text.data@field');
273
+ });
274
+
275
+ test('should work with all operator types in nested context', () => {
276
+ const nestedBuilder = new NestedConditionBuilder('order');
277
+ const itemsBuilder = nestedBuilder.field('items');
278
+
279
+ // Test that we can chain multiple field levels
280
+ const priceCondition = itemsBuilder.field('price').greaterThan(100);
281
+ expect(priceCondition.field).toBe('order.items.price');
282
+
283
+ const categoryCondition = itemsBuilder.field('category').equals('electronics');
284
+ expect(categoryCondition.field).toBe('order.items.category');
285
+
286
+ const stockCondition = itemsBuilder.field('stock').exists();
287
+ expect(stockCondition.field).toBe('order.items.stock');
288
+ });
289
+ });
290
+ });
291
+
292
+ describe('Integration with LogicalOperator', () => {
293
+ test('should work correctly with LogicalOperator.Condition', () => {
294
+ const nestedBuilder = new NestedConditionBuilder('user');
295
+ const condition = new NestedFieldConditionBuilder(['user', 'profile', 'name']).equals('John');
296
+ const logicalOp = LogicalOperator.Condition(condition);
297
+
298
+ expect(logicalOp.type).toBe('condition');
299
+ expect(logicalOp.condition?.field).toBe('user.profile.name');
300
+ });
301
+
302
+ test('should work in complex logical structures', () => {
303
+ const nestedBuilder = new NestedConditionBuilder('user');
304
+
305
+ const complexLogic = LogicalOperator.And([
306
+ LogicalOperator.Condition(new NestedFieldConditionBuilder(['user', 'profile', 'name']).equals('John')),
307
+ LogicalOperator.Or([
308
+ LogicalOperator.Condition(nestedBuilder.field('status').equals('active')),
309
+ LogicalOperator.Condition(nestedBuilder.field('status').equals('pending'))
310
+ ]),
311
+ LogicalOperator.Not([
312
+ LogicalOperator.Condition(new NestedFieldConditionBuilder(['user', 'flags', 'deleted']).isTrue())
313
+ ])
314
+ ]);
315
+
316
+ expect(complexLogic.type).toBe('and');
317
+ expect(complexLogic.conditions).toHaveLength(3);
318
+ expect(complexLogic.conditions[1].type).toBe('or');
319
+ expect(complexLogic.conditions[2].type).toBe('not');
320
+ });
321
+ });
@@ -0,0 +1,348 @@
1
+ import { QueryBuilder, FieldConditionBuilder, LogicalOperator } from '../index';
2
+ import { MockHttpClient, createMockResponse } from './setup';
3
+
4
+ describe('QueryBuilder', () => {
5
+ let mockClient: MockHttpClient;
6
+ let queryBuilder: QueryBuilder;
7
+
8
+ const serverUrl = 'http://localhost:3000';
9
+ const app = 'testApp';
10
+
11
+ beforeEach(() => {
12
+ mockClient = new MockHttpClient();
13
+ queryBuilder = new QueryBuilder(mockClient, serverUrl, app);
14
+ });
15
+
16
+ afterEach(() => {
17
+ mockClient.clear();
18
+ });
19
+
20
+ describe('Basic query building', () => {
21
+ test('should create query builder with HTTP client', () => {
22
+ expect(queryBuilder).toBeInstanceOf(QueryBuilder);
23
+ });
24
+
25
+ test('should build valid raw query', () => {
26
+ const rawQuery = queryBuilder
27
+ .collection('users')
28
+ .whereField('name')
29
+ .equals('John')
30
+ .buildRawQuery();
31
+
32
+ expect(rawQuery).toMatchObject({
33
+ find: {
34
+ name: { is: 'John' }
35
+ }
36
+ });
37
+ });
38
+
39
+ test('should validate query correctly', () => {
40
+ expect(queryBuilder.isValid()).toBe(false);
41
+
42
+ queryBuilder.whereField('name').equals('John');
43
+ expect(queryBuilder.isValid()).toBe(true);
44
+ });
45
+ });
46
+
47
+ describe('Method chaining', () => {
48
+ test('should support fluent API', () => {
49
+ const result = queryBuilder
50
+ .collection('users')
51
+ .whereField('name').equals('John')
52
+ .limit(10)
53
+ .offset(0);
54
+
55
+ expect(result).toBe(queryBuilder);
56
+ });
57
+
58
+ test('should build complex query with chaining', () => {
59
+ const rawQuery = queryBuilder
60
+ .collection('users')
61
+ .whereField('active').equals(true)
62
+ .whereField('age').greaterThan(18)
63
+ .selectFields(['id', 'name', 'email'])
64
+ .limit(50)
65
+ .offset(100)
66
+ .orderBy('name')
67
+ .buildRawQuery();
68
+
69
+ expect(rawQuery.limit).toBe(50);
70
+ expect(rawQuery.offset).toBe(100);
71
+ expect(rawQuery.sortBy).toBe('name');
72
+ });
73
+ });
74
+
75
+ describe('Complex find conditions', () => {
76
+ test('should build AND conditions', () => {
77
+ const rawQuery = queryBuilder
78
+ .collection('users')
79
+ .find(builder =>
80
+ LogicalOperator.And([
81
+ LogicalOperator.Condition(new FieldConditionBuilder('name').equals('John')),
82
+ LogicalOperator.Condition(new FieldConditionBuilder('age').greaterThan(25))
83
+ ])
84
+ )
85
+ .buildRawQuery();
86
+
87
+ expect(rawQuery.find).toEqual({
88
+ and: [
89
+ { name: { is: 'John' } },
90
+ { age: { greaterThan: 25 } }
91
+ ]
92
+ });
93
+ });
94
+
95
+ test('should build OR conditions', () => {
96
+ const rawQuery = queryBuilder
97
+ .collection('users')
98
+ .find(builder =>
99
+ LogicalOperator.Or([
100
+ LogicalOperator.Condition(new FieldConditionBuilder('role').equals('admin')),
101
+ LogicalOperator.Condition(new FieldConditionBuilder('role').equals('moderator'))
102
+ ])
103
+ )
104
+ .buildRawQuery();
105
+
106
+ expect(rawQuery.find).toEqual({
107
+ or: [
108
+ { role: { is: 'admin' } },
109
+ { role: { is: 'moderator' } }
110
+ ]
111
+ });
112
+ });
113
+
114
+ test('should build NOT conditions', () => {
115
+ const rawQuery = queryBuilder
116
+ .collection('users')
117
+ .find(builder =>
118
+ LogicalOperator.Not([
119
+ LogicalOperator.Condition(new FieldConditionBuilder('status').equals('banned'))
120
+ ])
121
+ )
122
+ .buildRawQuery();
123
+
124
+ expect(rawQuery.find).toEqual({
125
+ not: [
126
+ { status: { is: 'banned' } }
127
+ ]
128
+ });
129
+ });
130
+ });
131
+
132
+ describe('Selection methods', () => {
133
+ test('should select specific fields', () => {
134
+ queryBuilder.selectFields(['id', 'name', 'email']);
135
+ expect(queryBuilder.isValid()).toBe(true);
136
+ });
137
+
138
+ test('should select all fields', () => {
139
+ queryBuilder.selectAll();
140
+ expect(queryBuilder.isValid()).toBe(true);
141
+ });
142
+ });
143
+
144
+ describe('Query execution', () => {
145
+ test('should execute query successfully', async () => {
146
+ const mockData = [
147
+ { id: 1, name: 'John', email: 'john@example.com' },
148
+ { id: 2, name: 'Jane', email: 'jane@example.com' }
149
+ ];
150
+
151
+ mockClient.setResponse('/list', createMockResponse(mockData));
152
+
153
+ const response = await queryBuilder
154
+ .collection('users')
155
+ .whereField('active')
156
+ .equals(true)
157
+ .execute();
158
+
159
+ expect(response.records).toEqual(mockData);
160
+ expect(response.total).toBe(2);
161
+ });
162
+
163
+ test('should throw error when HTTP client is missing', async () => {
164
+ const builderWithoutClient = new QueryBuilder();
165
+
166
+ await expect(
167
+ builderWithoutClient
168
+ .collection('users')
169
+ .whereField('name')
170
+ .equals('John')
171
+ .execute()
172
+ ).rejects.toThrow('HTTP client is required');
173
+ });
174
+
175
+ test('should throw error when server URL is missing', async () => {
176
+ const builderWithoutUrl = new QueryBuilder(mockClient);
177
+
178
+ await expect(
179
+ builderWithoutUrl
180
+ .collection('users')
181
+ .whereField('name')
182
+ .equals('John')
183
+ .execute()
184
+ ).rejects.toThrow('Server URL is required');
185
+ });
186
+ });
187
+
188
+ describe('Query cloning', () => {
189
+ test('should clone query builder', () => {
190
+ const original = queryBuilder
191
+ .collection('users')
192
+ .whereField('name').equals('John')
193
+ .limit(10);
194
+
195
+ const cloned = original.clone();
196
+
197
+ expect(cloned).not.toBe(original);
198
+ expect(cloned.buildRawQuery().limit).toBe(10);
199
+ });
200
+
201
+ test('should create independent clones', () => {
202
+ const original = queryBuilder
203
+ .collection('users')
204
+ .whereField('name').equals('John');
205
+
206
+ const cloned = original.clone();
207
+ cloned.whereField('age').greaterThan(25);
208
+
209
+ const originalQuery = original.buildRawQuery();
210
+ const clonedQuery = cloned.buildRawQuery();
211
+
212
+ // Original should not be affected by clone modifications
213
+ expect(originalQuery).not.toEqual(clonedQuery);
214
+ });
215
+ });
216
+
217
+ describe('WhereClause', () => {
218
+ test('should build conditions via WhereClause', () => {
219
+ const rawQuery = queryBuilder
220
+ .collection('users')
221
+ .whereField('age')
222
+ .between(18, 65)
223
+ .buildRawQuery();
224
+
225
+ expect(rawQuery.find).toEqual({
226
+ age: { betweenOp: { from: 18, to: 65 } }
227
+ });
228
+ });
229
+
230
+ test('should support all operators in WhereClause', () => {
231
+ // Test a sampling of operators
232
+ const tests = [
233
+ { method: 'contains', value: 'test', expected: { includes: 'test' } },
234
+ { method: 'startsWith', value: 'pre', expected: { startsWith: 'pre' } },
235
+ { method: 'endsWith', value: 'suf', expected: { endsWith: 'suf' } },
236
+ { method: 'in', value: [1, 2, 3], expected: { in: [1, 2, 3] } },
237
+ { method: 'exists', value: undefined, expected: { exists: true } },
238
+ { method: 'isNull', value: undefined, expected: { isNull: true } }
239
+ ];
240
+
241
+ tests.forEach(({ method, value, expected }) => {
242
+ const builder = new QueryBuilder(mockClient, serverUrl, app)
243
+ .collection('test');
244
+
245
+ const whereClause = builder.whereField('field');
246
+
247
+ if (value !== undefined) {
248
+ (whereClause as any)[method](value);
249
+ } else {
250
+ (whereClause as any)[method]();
251
+ }
252
+
253
+ const rawQuery = builder.buildRawQuery();
254
+ expect(rawQuery.find).toEqual({
255
+ field: expected
256
+ });
257
+ });
258
+ });
259
+ });
260
+
261
+ describe('Nested field queries', () => {
262
+ test('should handle dot notation in whereField', () => {
263
+ const rawQuery = queryBuilder
264
+ .collection('users')
265
+ .whereField('user.profile.name')
266
+ .equals('John')
267
+ .buildRawQuery();
268
+
269
+ expect(rawQuery.find).toEqual({
270
+ user: {
271
+ profile: {
272
+ name: {
273
+ is: 'John'
274
+ }
275
+ }
276
+ }
277
+ });
278
+ });
279
+
280
+ test('should handle deeply nested fields', () => {
281
+ const rawQuery = queryBuilder
282
+ .collection('data')
283
+ .whereField('a.b.c.d.e')
284
+ .contains('value')
285
+ .buildRawQuery();
286
+
287
+ expect(rawQuery.find).toEqual({
288
+ a: {
289
+ b: {
290
+ c: {
291
+ d: {
292
+ e: {
293
+ includes: 'value'
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+ });
300
+ });
301
+ });
302
+
303
+ describe('HTTP client integration', () => {
304
+ test('should use correct endpoint and request format', async () => {
305
+ const mockData = [{ id: 1, name: 'Test' }];
306
+ mockClient.setResponse('/list', createMockResponse(mockData));
307
+
308
+ // Spy on the HTTP client to verify the request
309
+ const postSpy = jest.spyOn(mockClient, 'post');
310
+
311
+ await queryBuilder
312
+ .collection('users')
313
+ .whereField('name')
314
+ .equals('Test')
315
+ .limit(10)
316
+ .execute();
317
+
318
+ expect(postSpy).toHaveBeenCalledWith(
319
+ `${serverUrl}/list`,
320
+ expect.objectContaining({
321
+ find: { name: { is: 'Test' } },
322
+ limit: 10,
323
+ root: `${app}::users`
324
+ }),
325
+ { 'Content-Type': 'application/json' }
326
+ );
327
+ });
328
+
329
+ test('should handle HTTP errors gracefully', async () => {
330
+ // Mock HTTP client that throws an error
331
+ const errorClient = {
332
+ async post() {
333
+ throw new Error('Network error');
334
+ }
335
+ };
336
+
337
+ const errorBuilder = new QueryBuilder(errorClient, serverUrl, app);
338
+
339
+ await expect(
340
+ errorBuilder
341
+ .collection('users')
342
+ .whereField('name')
343
+ .equals('Test')
344
+ .execute()
345
+ ).rejects.toThrow('Query execution failed: Network error');
346
+ });
347
+ });
348
+ });