@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.
- package/.DS_Store +0 -0
- package/.claude/settings.local.json +8 -0
- package/.gitignore +5 -0
- package/.idea/.gitignore +5 -0
- package/.idea/compiler.xml +6 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/jsLinters/eslint.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/prettier.xml +7 -0
- package/.idea/sdk.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/.idea/workspace.xml +257 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -3
- package/dist/client.js.map +1 -1
- package/dist/database.d.ts +0 -20
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +0 -40
- package/dist/database.js.map +1 -1
- package/dist/query-sdk/tests/setup.d.ts +16 -0
- package/dist/query-sdk/tests/setup.d.ts.map +1 -0
- package/dist/query-sdk/tests/setup.js +49 -0
- package/dist/query-sdk/tests/setup.js.map +1 -0
- package/examples/basic-usage.ts +136 -0
- package/examples/blob-upload-example.ts +140 -0
- package/examples/collection-schema-example.ts +304 -0
- package/examples/server-side-joins.ts +201 -0
- package/examples/tweet-self-joins-example.ts +352 -0
- package/package-lock.json +3823 -0
- package/package.json +1 -1
- package/skills.md +1096 -0
- package/src/.env +1 -0
- package/src/batch.d.ts +121 -0
- package/src/batch.js +205 -0
- package/src/batch.ts +257 -0
- package/src/client.ts +1856 -0
- package/src/database.d.ts +268 -0
- package/src/database.js +294 -0
- package/src/database.ts +695 -0
- package/src/index.d.ts +160 -0
- package/src/index.js +186 -0
- package/src/index.ts +253 -0
- package/src/query-sdk/ConditionBuilder.ts +103 -0
- package/src/query-sdk/FieldConditionBuilder.ts +2 -0
- package/src/query-sdk/NestedBuilders.ts +186 -0
- package/src/query-sdk/OnChainDB.ts +294 -0
- package/src/query-sdk/QueryBuilder.ts +1191 -0
- package/src/query-sdk/QueryResult.ts +375 -0
- package/src/query-sdk/README.md +866 -0
- package/src/query-sdk/SelectionBuilder.ts +94 -0
- package/src/query-sdk/adapters/HttpClientAdapter.ts +249 -0
- package/src/query-sdk/dist/ConditionBuilder.d.ts +22 -0
- package/src/query-sdk/dist/ConditionBuilder.js +90 -0
- package/src/query-sdk/dist/FieldConditionBuilder.d.ts +1 -0
- package/src/query-sdk/dist/FieldConditionBuilder.js +6 -0
- package/src/query-sdk/dist/NestedBuilders.d.ts +43 -0
- package/src/query-sdk/dist/NestedBuilders.js +144 -0
- package/src/query-sdk/dist/OnChainDB.d.ts +19 -0
- package/src/query-sdk/dist/OnChainDB.js +123 -0
- package/src/query-sdk/dist/QueryBuilder.d.ts +70 -0
- package/src/query-sdk/dist/QueryBuilder.js +295 -0
- package/src/query-sdk/dist/QueryResult.d.ts +52 -0
- package/src/query-sdk/dist/QueryResult.js +293 -0
- package/src/query-sdk/dist/SelectionBuilder.d.ts +20 -0
- package/src/query-sdk/dist/SelectionBuilder.js +80 -0
- package/src/query-sdk/dist/adapters/HttpClientAdapter.d.ts +27 -0
- package/src/query-sdk/dist/adapters/HttpClientAdapter.js +170 -0
- package/src/query-sdk/dist/index.d.ts +36 -0
- package/src/query-sdk/dist/index.js +27 -0
- package/src/query-sdk/dist/operators.d.ts +56 -0
- package/src/query-sdk/dist/operators.js +289 -0
- package/src/query-sdk/dist/tests/setup.d.ts +15 -0
- package/src/query-sdk/dist/tests/setup.js +46 -0
- package/src/query-sdk/index.ts +59 -0
- package/src/query-sdk/jest.config.js +25 -0
- package/src/query-sdk/operators.ts +335 -0
- package/src/query-sdk/package.json +46 -0
- package/src/query-sdk/tests/FieldConditionBuilder.test.ts +84 -0
- package/src/query-sdk/tests/LogicalOperator.test.ts +85 -0
- package/src/query-sdk/tests/NestedBuilders.test.ts +321 -0
- package/src/query-sdk/tests/QueryBuilder.test.ts +348 -0
- package/src/query-sdk/tests/QueryResult.test.ts +464 -0
- package/src/query-sdk/tests/aggregations.test.ts +653 -0
- package/src/query-sdk/tests/comprehensive.test.ts +279 -0
- package/src/query-sdk/tests/integration.test.ts +608 -0
- package/src/query-sdk/tests/operators.test.ts +327 -0
- package/src/query-sdk/tests/setup.ts +59 -0
- package/src/query-sdk/tests/unit.test.ts +794 -0
- package/src/query-sdk/tsconfig.json +26 -0
- package/src/query-sdk/yarn.lock +3092 -0
- package/src/types.d.ts +131 -0
- package/src/types.js +46 -0
- package/src/types.ts +534 -0
- package/src/x402/index.ts +12 -0
- package/src/x402/types.ts +250 -0
- package/src/x402/utils.ts +332 -0
- package/tsconfig.json +20 -0
- 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
|
+
});
|