@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,794 @@
|
|
|
1
|
+
// Comprehensive unit tests for TypeScript SDK
|
|
2
|
+
import {
|
|
3
|
+
FieldConditionBuilder,
|
|
4
|
+
LogicalOperator,
|
|
5
|
+
ConditionBuilder,
|
|
6
|
+
NestedConditionBuilder,
|
|
7
|
+
NestedFieldConditionBuilder,
|
|
8
|
+
QueryBuilder,
|
|
9
|
+
QueryResult,
|
|
10
|
+
createQueryResult
|
|
11
|
+
} from '../index';
|
|
12
|
+
|
|
13
|
+
// Test framework helpers
|
|
14
|
+
interface TestCase {
|
|
15
|
+
name: string;
|
|
16
|
+
test: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class TestRunner {
|
|
20
|
+
private tests: TestCase[] = [];
|
|
21
|
+
private passed = 0;
|
|
22
|
+
private failed = 0;
|
|
23
|
+
|
|
24
|
+
add(name: string, test: () => void) {
|
|
25
|
+
this.tests.push({ name, test });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
run() {
|
|
29
|
+
console.log(`🧪 Running ${this.tests.length} unit tests...\n`);
|
|
30
|
+
|
|
31
|
+
for (const testCase of this.tests) {
|
|
32
|
+
try {
|
|
33
|
+
testCase.test();
|
|
34
|
+
console.log(`✅ ${testCase.name}`);
|
|
35
|
+
this.passed++;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(`❌ ${testCase.name}: ${error instanceof Error ? error.message : error}`);
|
|
38
|
+
this.failed++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`\n📊 Test Results: ${this.passed} passed, ${this.failed} failed`);
|
|
43
|
+
if (this.failed > 0) {
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Assertion helpers
|
|
50
|
+
function assertEqual<T>(actual: T, expected: T, message?: string) {
|
|
51
|
+
const actualStr = JSON.stringify(actual, null, 2);
|
|
52
|
+
const expectedStr = JSON.stringify(expected, null, 2);
|
|
53
|
+
if (actualStr !== expectedStr) {
|
|
54
|
+
throw new Error(`${message || 'Values not equal'}\nExpected: ${expectedStr}\nActual: ${actualStr}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertTrue(condition: boolean, message?: string) {
|
|
59
|
+
if (!condition) {
|
|
60
|
+
throw new Error(message || 'Condition is false');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function assertFalse(condition: boolean, message?: string) {
|
|
65
|
+
if (condition) {
|
|
66
|
+
throw new Error(message || 'Condition is true');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertThrows(fn: () => void, message?: string) {
|
|
71
|
+
try {
|
|
72
|
+
fn();
|
|
73
|
+
throw new Error(message || 'Expected function to throw');
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Expected - function threw as required
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Initialize test runner
|
|
80
|
+
const runner = new TestRunner();
|
|
81
|
+
|
|
82
|
+
// ===== FIELD CONDITION BUILDER TESTS =====
|
|
83
|
+
|
|
84
|
+
runner.add('FieldConditionBuilder - Basic operators', () => {
|
|
85
|
+
const builder = new FieldConditionBuilder('testField');
|
|
86
|
+
|
|
87
|
+
assertEqual(builder.equals('value').operator, 'is');
|
|
88
|
+
assertEqual(builder.notEquals('value').operator, 'isNot');
|
|
89
|
+
assertEqual(builder.greaterThan(10).operator, 'greaterThan');
|
|
90
|
+
assertEqual(builder.lessThan(10).operator, 'lessThan');
|
|
91
|
+
assertEqual(builder.greaterThanOrEqual(10).operator, 'greaterThanOrEqual');
|
|
92
|
+
assertEqual(builder.lessThanOrEqual(10).operator, 'lessThanOrEqual');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
runner.add('FieldConditionBuilder - String operators', () => {
|
|
96
|
+
const builder = new FieldConditionBuilder('text');
|
|
97
|
+
|
|
98
|
+
assertEqual(builder.contains('sub').operator, 'includes');
|
|
99
|
+
assertEqual(builder.startsWith('pre').operator, 'startsWith');
|
|
100
|
+
assertEqual(builder.endsWith('suf').operator, 'endsWith');
|
|
101
|
+
assertEqual(builder.regExpMatches('.*test.*').operator, 'regExpMatches');
|
|
102
|
+
assertEqual(builder.includesCaseInsensitive('TEST').operator, 'includesCaseInsensitive');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
runner.add('FieldConditionBuilder - Array operators', () => {
|
|
106
|
+
const builder = new FieldConditionBuilder('items');
|
|
107
|
+
|
|
108
|
+
const inCondition = builder.in([1, 2, 3]);
|
|
109
|
+
assertEqual(inCondition.operator, 'in');
|
|
110
|
+
assertEqual(inCondition.value, [1, 2, 3]);
|
|
111
|
+
|
|
112
|
+
const notInCondition = builder.notIn(['a', 'b']);
|
|
113
|
+
assertEqual(notInCondition.operator, 'notIn');
|
|
114
|
+
assertEqual(notInCondition.value, ['a', 'b']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
runner.add('FieldConditionBuilder - Null/existence operators', () => {
|
|
118
|
+
const builder = new FieldConditionBuilder('optional');
|
|
119
|
+
|
|
120
|
+
assertEqual(builder.exists().value, true);
|
|
121
|
+
assertEqual(builder.notExists().value, false);
|
|
122
|
+
assertEqual(builder.isNull().value, true);
|
|
123
|
+
assertEqual(builder.isNotNull().value, false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
runner.add('FieldConditionBuilder - Numeric operators', () => {
|
|
127
|
+
const builder = new FieldConditionBuilder('score');
|
|
128
|
+
|
|
129
|
+
const betweenCondition = builder.between(10, 20);
|
|
130
|
+
assertEqual(betweenCondition.operator, 'between');
|
|
131
|
+
assertEqual(betweenCondition.value, { min: 10, max: 20 });
|
|
132
|
+
|
|
133
|
+
// Test additional real operators
|
|
134
|
+
const regexCondition = builder.regExpMatches('^test.*');
|
|
135
|
+
assertEqual(regexCondition.operator, 'regExpMatches');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
runner.add('FieldConditionBuilder - String operators', () => {
|
|
139
|
+
const builder = new FieldConditionBuilder('text');
|
|
140
|
+
|
|
141
|
+
assertEqual(builder.contains('test').operator, 'includes');
|
|
142
|
+
assertEqual(builder.startsWith('prefix').operator, 'startsWith');
|
|
143
|
+
assertEqual(builder.endsWith('suffix').operator, 'endsWith');
|
|
144
|
+
assertEqual(builder.regExpMatches('^test.*').operator, 'regExpMatches');
|
|
145
|
+
assertEqual(builder.includesCaseInsensitive('Test').operator, 'includesCaseInsensitive');
|
|
146
|
+
assertEqual(builder.startsWithCaseInsensitive('Test').operator, 'startsWithCaseInsensitive');
|
|
147
|
+
assertEqual(builder.endsWithCaseInsensitive('Test').operator, 'endsWithCaseInsensitive');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
runner.add('FieldConditionBuilder - IP address operators', () => {
|
|
151
|
+
const builder = new FieldConditionBuilder('ipAddress');
|
|
152
|
+
|
|
153
|
+
assertEqual(builder.isLocalIp().operator, 'isLocalIp');
|
|
154
|
+
assertEqual(builder.isExternalIp().operator, 'isExternalIp');
|
|
155
|
+
assertEqual(builder.inCountry('US').operator, 'inCountry');
|
|
156
|
+
assertEqual(builder.cidr('192.168.1.0/24').operator, 'CIDR');
|
|
157
|
+
assertEqual(builder.b64('dGVzdA==').operator, 'b64');
|
|
158
|
+
assertEqual(builder.inDataset('malware_ips').operator, 'inDataset');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
runner.add('FieldConditionBuilder - Misc operators', () => {
|
|
162
|
+
const builder = new FieldConditionBuilder('data');
|
|
163
|
+
|
|
164
|
+
const b64Condition = builder.b64('dGVzdA==');
|
|
165
|
+
assertEqual(b64Condition.operator, 'b64');
|
|
166
|
+
assertEqual(b64Condition.value, 'dGVzdA==');
|
|
167
|
+
|
|
168
|
+
const countryCondition = builder.inCountry('US');
|
|
169
|
+
assertEqual(countryCondition.operator, 'inCountry');
|
|
170
|
+
assertEqual(countryCondition.value, 'US');
|
|
171
|
+
|
|
172
|
+
const datasetCondition = builder.inDataset('malware_ips');
|
|
173
|
+
assertEqual(datasetCondition.operator, 'inDataset');
|
|
174
|
+
assertEqual(datasetCondition.value, 'malware_ips');
|
|
175
|
+
|
|
176
|
+
const cidrCondition = builder.cidr('192.168.1.0/24');
|
|
177
|
+
assertEqual(cidrCondition.operator, 'CIDR');
|
|
178
|
+
assertEqual(cidrCondition.value, '192.168.1.0/24');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
runner.add('FieldConditionBuilder - Boolean operators', () => {
|
|
182
|
+
const builder = new FieldConditionBuilder('isActive');
|
|
183
|
+
|
|
184
|
+
const trueCondition = builder.isTrue();
|
|
185
|
+
assertEqual(trueCondition.operator, 'is');
|
|
186
|
+
assertEqual(trueCondition.value, true);
|
|
187
|
+
|
|
188
|
+
const falseCondition = builder.isFalse();
|
|
189
|
+
assertEqual(falseCondition.operator, 'is');
|
|
190
|
+
assertEqual(falseCondition.value, false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
runner.add('FieldConditionBuilder - Number operators', () => {
|
|
194
|
+
const builder = new FieldConditionBuilder('amount');
|
|
195
|
+
|
|
196
|
+
assertEqual(builder.greaterThan(100).operator, 'greaterThan');
|
|
197
|
+
assertEqual(builder.lessThan(500).operator, 'lessThan');
|
|
198
|
+
assertEqual(builder.greaterThanOrEqual(100).operator, 'greaterThanOrEqual');
|
|
199
|
+
assertEqual(builder.lessThanOrEqual(500).operator, 'lessThanOrEqual');
|
|
200
|
+
assertEqual(builder.between(100, 500).operator, 'betweenOp');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
runner.add('FieldConditionBuilder - Base operators', () => {
|
|
204
|
+
const builder = new FieldConditionBuilder('value');
|
|
205
|
+
|
|
206
|
+
assertEqual(builder.equals('test').operator, 'is');
|
|
207
|
+
assertEqual(builder.notEquals('test').operator, 'isNot');
|
|
208
|
+
assertEqual(builder.in(['a', 'b', 'c']).operator, 'in');
|
|
209
|
+
assertEqual(builder.notIn(['x', 'y', 'z']).operator, 'notIn');
|
|
210
|
+
assertEqual(builder.isNull().operator, 'isNull');
|
|
211
|
+
assertEqual(builder.isNotNull().operator, 'isNull');
|
|
212
|
+
assertEqual(builder.exists().operator, 'exists');
|
|
213
|
+
assertEqual(builder.notExists().operator, 'exists');
|
|
214
|
+
// Only test boolean convenience methods that use base operators
|
|
215
|
+
assertEqual(builder.isTrue().operator, 'is');
|
|
216
|
+
assertEqual(builder.isFalse().operator, 'is');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
runner.add('FieldConditionBuilder - Boolean convenience methods', () => {
|
|
220
|
+
const builder = new FieldConditionBuilder('flag');
|
|
221
|
+
|
|
222
|
+
const trueCondition = builder.isTrue();
|
|
223
|
+
assertEqual(trueCondition.operator, 'is');
|
|
224
|
+
assertEqual(trueCondition.value, true);
|
|
225
|
+
|
|
226
|
+
const falseCondition = builder.isFalse();
|
|
227
|
+
assertEqual(falseCondition.operator, 'is');
|
|
228
|
+
assertEqual(falseCondition.value, false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ===== LOGICAL OPERATOR TESTS =====
|
|
232
|
+
|
|
233
|
+
runner.add('LogicalOperator - Single condition', () => {
|
|
234
|
+
const condition = new FieldConditionBuilder('name').equals('John');
|
|
235
|
+
const logicalOp = LogicalOperator.Condition(condition);
|
|
236
|
+
|
|
237
|
+
assertEqual(logicalOp.type, 'condition');
|
|
238
|
+
assertEqual(logicalOp.condition?.field, 'name');
|
|
239
|
+
assertEqual(logicalOp.condition?.operator, 'is');
|
|
240
|
+
assertEqual(logicalOp.condition?.value, 'John');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
runner.add('LogicalOperator - AND operation', () => {
|
|
244
|
+
const condition1 = LogicalOperator.Condition(new FieldConditionBuilder('name').equals('John'));
|
|
245
|
+
const condition2 = LogicalOperator.Condition(new FieldConditionBuilder('age').greaterThan(25));
|
|
246
|
+
|
|
247
|
+
const andOp = LogicalOperator.And([condition1, condition2]);
|
|
248
|
+
assertEqual(andOp.type, 'and');
|
|
249
|
+
assertEqual(andOp.conditions.length, 2);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
runner.add('LogicalOperator - OR operation', () => {
|
|
253
|
+
const condition1 = LogicalOperator.Condition(new FieldConditionBuilder('status').equals('active'));
|
|
254
|
+
const condition2 = LogicalOperator.Condition(new FieldConditionBuilder('status').equals('pending'));
|
|
255
|
+
|
|
256
|
+
const orOp = LogicalOperator.Or([condition1, condition2]);
|
|
257
|
+
assertEqual(orOp.type, 'or');
|
|
258
|
+
assertEqual(orOp.conditions.length, 2);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
runner.add('LogicalOperator - NOT operation', () => {
|
|
262
|
+
const condition = LogicalOperator.Condition(new FieldConditionBuilder('deleted').isTrue());
|
|
263
|
+
const notOp = LogicalOperator.Not([condition]);
|
|
264
|
+
|
|
265
|
+
assertEqual(notOp.type, 'not');
|
|
266
|
+
assertEqual(notOp.conditions.length, 1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ===== DOT NOTATION TESTS =====
|
|
270
|
+
|
|
271
|
+
runner.add('createNestedQuery - Simple field', () => {
|
|
272
|
+
const result = createNestedQuery('name', 'is', 'John');
|
|
273
|
+
assertEqual(result, { name: { is: 'John' } });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
runner.add('createNestedQuery - Nested field', () => {
|
|
277
|
+
const result = createNestedQuery('user.profile.name', 'is', 'John');
|
|
278
|
+
const expected = {
|
|
279
|
+
user: {
|
|
280
|
+
profile: {
|
|
281
|
+
name: {
|
|
282
|
+
is: 'John'
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
assertEqual(result, expected);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
runner.add('createNestedQuery - Deep nesting', () => {
|
|
291
|
+
const result = createNestedQuery('a.b.c.d.e', 'contains', 'test');
|
|
292
|
+
const expected = {
|
|
293
|
+
a: {
|
|
294
|
+
b: {
|
|
295
|
+
c: {
|
|
296
|
+
d: {
|
|
297
|
+
e: {
|
|
298
|
+
contains: 'test'
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
assertEqual(result, expected);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
runner.add('LogicalOperator.toComposable - Dot notation integration', () => {
|
|
309
|
+
const condition = new FieldConditionBuilder('user.profile.bio').equals('Developer');
|
|
310
|
+
const logicalOp = LogicalOperator.Condition(condition);
|
|
311
|
+
const composable = logicalOp.toComposable();
|
|
312
|
+
|
|
313
|
+
const expected = {
|
|
314
|
+
user: {
|
|
315
|
+
profile: {
|
|
316
|
+
bio: {
|
|
317
|
+
is: 'Developer'
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
assertEqual(composable, expected);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ===== NESTED BUILDER TESTS =====
|
|
326
|
+
|
|
327
|
+
runner.add('NestedConditionBuilder - Basic usage', () => {
|
|
328
|
+
const nestedBuilder = new NestedConditionBuilder('user');
|
|
329
|
+
const fieldBuilder = nestedBuilder.field('name');
|
|
330
|
+
|
|
331
|
+
assertTrue(fieldBuilder instanceof NestedFieldConditionBuilder);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
runner.add('NestedFieldConditionBuilder - Path construction', () => {
|
|
335
|
+
const nestedBuilder = new NestedConditionBuilder('user');
|
|
336
|
+
const condition = nestedBuilder.field('profile').field('bio').equals('Developer');
|
|
337
|
+
|
|
338
|
+
assertEqual(condition.field, 'user.profile.bio');
|
|
339
|
+
assertEqual(condition.operator, 'is');
|
|
340
|
+
assertEqual(condition.value, 'Developer');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
runner.add('NestedFieldConditionBuilder - All operators available', () => {
|
|
344
|
+
const nestedBuilder = new NestedConditionBuilder('user');
|
|
345
|
+
const fieldBuilder = nestedBuilder.field('data');
|
|
346
|
+
|
|
347
|
+
// Test a few key operators to ensure they work
|
|
348
|
+
assertEqual(fieldBuilder.contains('test').operator, 'includes');
|
|
349
|
+
assertEqual(fieldBuilder.greaterThan(10).operator, 'greaterThan');
|
|
350
|
+
assertEqual(fieldBuilder.isNull().operator, 'isNull');
|
|
351
|
+
assertEqual(fieldBuilder.dateToday().operator, 'dateToday');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
runner.add('NestedConditionBuilder - Logical groups', () => {
|
|
355
|
+
const nestedBuilder = new NestedConditionBuilder('user');
|
|
356
|
+
|
|
357
|
+
const andGroup = nestedBuilder.andGroup(builder => [
|
|
358
|
+
LogicalOperator.Condition(builder.field('name').equals('John')),
|
|
359
|
+
LogicalOperator.Condition(builder.field('age').greaterThan(25))
|
|
360
|
+
]);
|
|
361
|
+
|
|
362
|
+
assertEqual(andGroup.type, 'and');
|
|
363
|
+
assertEqual(andGroup.conditions.length, 2);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ===== CONDITION BUILDER TESTS =====
|
|
367
|
+
|
|
368
|
+
runner.add('ConditionBuilder - Basic functionality', () => {
|
|
369
|
+
const builder = new ConditionBuilder();
|
|
370
|
+
|
|
371
|
+
assertTrue(builder.isEmpty());
|
|
372
|
+
assertEqual(builder.getConditionCount(), 0);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
runner.add('ConditionBuilder - Adding conditions', () => {
|
|
376
|
+
const builder = new ConditionBuilder();
|
|
377
|
+
const condition = LogicalOperator.Condition(new FieldConditionBuilder('name').equals('John'));
|
|
378
|
+
|
|
379
|
+
builder.addCondition(condition);
|
|
380
|
+
assertEqual(builder.getConditionCount(), 1);
|
|
381
|
+
assertFalse(builder.isEmpty());
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
runner.add('ConditionBuilder - Logical groups', () => {
|
|
385
|
+
const builder = new ConditionBuilder();
|
|
386
|
+
|
|
387
|
+
const andGroup = builder.andGroup(b => [
|
|
388
|
+
LogicalOperator.Condition(new FieldConditionBuilder('name').equals('John')),
|
|
389
|
+
LogicalOperator.Condition(new FieldConditionBuilder('age').greaterThan(25))
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
assertEqual(andGroup.type, 'and');
|
|
393
|
+
assertEqual(andGroup.conditions.length, 2);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
runner.add('ConditionBuilder - Nested conditions', () => {
|
|
397
|
+
const builder = new ConditionBuilder();
|
|
398
|
+
|
|
399
|
+
const nestedCondition = builder.nested('user', nested =>
|
|
400
|
+
LogicalOperator.Condition(nested.field('name').equals('John'))
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
assertEqual(nestedCondition.type, 'condition');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
runner.add('ConditionBuilder - Clear functionality', () => {
|
|
407
|
+
const builder = new ConditionBuilder();
|
|
408
|
+
builder.addCondition(LogicalOperator.Condition(new FieldConditionBuilder('test').equals('value')));
|
|
409
|
+
|
|
410
|
+
assertEqual(builder.getConditionCount(), 1);
|
|
411
|
+
builder.clear();
|
|
412
|
+
assertEqual(builder.getConditionCount(), 0);
|
|
413
|
+
assertTrue(builder.isEmpty());
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// ===== QUERY BUILDER TESTS =====
|
|
417
|
+
|
|
418
|
+
runner.add('QueryBuilder - Basic construction', () => {
|
|
419
|
+
const builder = new QueryBuilder();
|
|
420
|
+
assertFalse(builder.isValid()); // No conditions yet
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
runner.add('QueryBuilder - Method chaining', () => {
|
|
424
|
+
const mockClient = {
|
|
425
|
+
async post() { return { data: { records: [], total: 0 } }; }
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const builder = new QueryBuilder()
|
|
429
|
+
.withHttpClient(mockClient)
|
|
430
|
+
.withServerUrl('http://localhost:3000')
|
|
431
|
+
.collection('users')
|
|
432
|
+
.limit(10)
|
|
433
|
+
.offset(0);
|
|
434
|
+
|
|
435
|
+
const rawQuery = builder.buildRawQuery();
|
|
436
|
+
assertEqual(rawQuery.limit, 10);
|
|
437
|
+
assertEqual(rawQuery.offset, 0);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
runner.add('QueryBuilder - Where field conditions', () => {
|
|
441
|
+
const builder = new QueryBuilder();
|
|
442
|
+
const result = builder.whereField('name').equals('John');
|
|
443
|
+
|
|
444
|
+
assertTrue(result === builder); // Returns self for chaining
|
|
445
|
+
assertTrue(builder.isValid()); // Now has conditions
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
runner.add('QueryBuilder - Complex find conditions', () => {
|
|
449
|
+
const builder = new QueryBuilder();
|
|
450
|
+
|
|
451
|
+
builder.find(conditionBuilder =>
|
|
452
|
+
conditionBuilder.andGroup(b => [
|
|
453
|
+
LogicalOperator.Condition(new FieldConditionBuilder('name').equals('John')),
|
|
454
|
+
LogicalOperator.Condition(new FieldConditionBuilder('age').greaterThan(25))
|
|
455
|
+
])
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
assertTrue(builder.isValid());
|
|
459
|
+
const rawQuery = builder.buildRawQuery();
|
|
460
|
+
assertTrue(typeof rawQuery.find === 'object');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
runner.add('QueryBuilder - Selection methods', () => {
|
|
464
|
+
const builder = new QueryBuilder();
|
|
465
|
+
|
|
466
|
+
// Test selectFields
|
|
467
|
+
builder.selectFields(['name', 'email']);
|
|
468
|
+
assertTrue(builder.isValid());
|
|
469
|
+
|
|
470
|
+
// Test selectAll
|
|
471
|
+
const builder2 = new QueryBuilder();
|
|
472
|
+
builder2.selectAll();
|
|
473
|
+
assertTrue(builder2.isValid());
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
runner.add('QueryBuilder - Clone functionality', () => {
|
|
477
|
+
const original = new QueryBuilder()
|
|
478
|
+
.collection('users')
|
|
479
|
+
.limit(10)
|
|
480
|
+
.whereField('name').equals('John');
|
|
481
|
+
|
|
482
|
+
const cloned = original.clone();
|
|
483
|
+
|
|
484
|
+
// Verify they're separate instances
|
|
485
|
+
assertTrue(cloned !== original);
|
|
486
|
+
|
|
487
|
+
// Verify cloned data
|
|
488
|
+
assertEqual(cloned.buildRawQuery().limit, 10);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ===== QUERY RESULT TESTS =====
|
|
492
|
+
|
|
493
|
+
runner.add('QueryResult - Basic functionality', () => {
|
|
494
|
+
const data = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
|
|
495
|
+
const result = createQueryResult(data);
|
|
496
|
+
|
|
497
|
+
assertEqual(result.len(), 2);
|
|
498
|
+
assertEqual(result.length(), 2);
|
|
499
|
+
assertFalse(result.isEmpty());
|
|
500
|
+
assertEqual(result.first()?.name, 'John');
|
|
501
|
+
assertEqual(result.last()?.name, 'Jane');
|
|
502
|
+
assertEqual(result.get(0)?.id, 1);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
runner.add('QueryResult - Array input validation', () => {
|
|
506
|
+
// Test with single item
|
|
507
|
+
const single = createQueryResult({ id: 1, name: 'Test' } as any);
|
|
508
|
+
assertEqual(single.len(), 1);
|
|
509
|
+
|
|
510
|
+
// Test with empty array
|
|
511
|
+
const empty = createQueryResult([]);
|
|
512
|
+
assertTrue(empty.isEmpty());
|
|
513
|
+
assertEqual(empty.len(), 0);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
runner.add('QueryResult - Iteration methods', () => {
|
|
517
|
+
const data = [
|
|
518
|
+
{ id: 1, name: 'John', active: true },
|
|
519
|
+
{ id: 2, name: 'Jane', active: false },
|
|
520
|
+
{ id: 3, name: 'Bob', active: true }
|
|
521
|
+
];
|
|
522
|
+
const result = createQueryResult(data);
|
|
523
|
+
|
|
524
|
+
// Test any/some
|
|
525
|
+
assertTrue(result.any(item => item.active));
|
|
526
|
+
assertTrue(result.any(item => item.name === 'Jane'));
|
|
527
|
+
|
|
528
|
+
// Test all/every
|
|
529
|
+
assertFalse(result.all(item => item.active));
|
|
530
|
+
assertTrue(result.all(item => typeof item.id === 'number'));
|
|
531
|
+
|
|
532
|
+
// Test find
|
|
533
|
+
const found = result.find(item => item.name === 'Jane');
|
|
534
|
+
assertEqual(found?.id, 2);
|
|
535
|
+
|
|
536
|
+
// Test filter
|
|
537
|
+
const active = result.filter(item => item.active);
|
|
538
|
+
assertEqual(active.length, 2);
|
|
539
|
+
|
|
540
|
+
// Test map
|
|
541
|
+
const names = result.map(item => item.name);
|
|
542
|
+
assertEqual(names, ['John', 'Jane', 'Bob']);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
runner.add('QueryResult - Pluck methods', () => {
|
|
546
|
+
const data = [
|
|
547
|
+
{ id: 1, name: 'John', score: 95.5 },
|
|
548
|
+
{ id: 2, name: 'Jane', score: 87 },
|
|
549
|
+
{ id: 3, name: 'Bob', score: null }
|
|
550
|
+
];
|
|
551
|
+
const result = createQueryResult(data);
|
|
552
|
+
|
|
553
|
+
// Test pluck
|
|
554
|
+
const names = result.pluck('name');
|
|
555
|
+
assertEqual(names, ['John', 'Jane', 'Bob']);
|
|
556
|
+
|
|
557
|
+
// Test pluckStrings
|
|
558
|
+
const stringNames = result.pluckStrings('name');
|
|
559
|
+
assertEqual(stringNames, ['John', 'Jane', 'Bob']);
|
|
560
|
+
|
|
561
|
+
// Test pluckNumbers
|
|
562
|
+
const scores = result.pluckNumbers('score');
|
|
563
|
+
assertEqual(scores, [95.5, 87, null]);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
runner.add('QueryResult - Numeric summary', () => {
|
|
567
|
+
const data = [
|
|
568
|
+
{ score: 85 },
|
|
569
|
+
{ score: 92 },
|
|
570
|
+
{ score: 78 },
|
|
571
|
+
{ score: 95 }
|
|
572
|
+
];
|
|
573
|
+
const result = createQueryResult(data);
|
|
574
|
+
|
|
575
|
+
const summary = result.summarizeNumeric('score');
|
|
576
|
+
|
|
577
|
+
assertTrue(summary !== null);
|
|
578
|
+
assertEqual(summary!.count, 4);
|
|
579
|
+
assertEqual(summary!.sum, 350);
|
|
580
|
+
assertEqual(summary!.mean, 87.5);
|
|
581
|
+
assertEqual(summary!.min, 78);
|
|
582
|
+
assertEqual(summary!.max, 95);
|
|
583
|
+
|
|
584
|
+
// Test with no numeric data
|
|
585
|
+
const textData = [{ name: 'John' }, { name: 'Jane' }];
|
|
586
|
+
const textResult = createQueryResult(textData);
|
|
587
|
+
const textSummary = textResult.summarizeNumeric('name');
|
|
588
|
+
assertEqual(textSummary, null);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
runner.add('QueryResult - Sorting', () => {
|
|
592
|
+
const data = [
|
|
593
|
+
{ name: 'Charlie', age: 30 },
|
|
594
|
+
{ name: 'Alice', age: 25 },
|
|
595
|
+
{ name: 'Bob', age: 35 }
|
|
596
|
+
];
|
|
597
|
+
const result = createQueryResult(data);
|
|
598
|
+
|
|
599
|
+
// Test single field sort
|
|
600
|
+
const byName = result.sortBy('name');
|
|
601
|
+
assertEqual(byName[0].name, 'Alice');
|
|
602
|
+
assertEqual(byName[1].name, 'Bob');
|
|
603
|
+
assertEqual(byName[2].name, 'Charlie');
|
|
604
|
+
|
|
605
|
+
// Test descending sort
|
|
606
|
+
const byAgeDesc = result.sortBy('age', false);
|
|
607
|
+
assertEqual(byAgeDesc[0].age, 35);
|
|
608
|
+
assertEqual(byAgeDesc[2].age, 25);
|
|
609
|
+
|
|
610
|
+
// Test multi-field sort
|
|
611
|
+
const data2 = [
|
|
612
|
+
{ dept: 'B', name: 'Charlie' },
|
|
613
|
+
{ dept: 'A', name: 'Bob' },
|
|
614
|
+
{ dept: 'A', name: 'Alice' }
|
|
615
|
+
];
|
|
616
|
+
const result2 = createQueryResult(data2);
|
|
617
|
+
const sorted = result2.sortByMultiple([
|
|
618
|
+
{ field: 'dept', ascending: true },
|
|
619
|
+
{ field: 'name', ascending: true }
|
|
620
|
+
]);
|
|
621
|
+
assertEqual(sorted[0].name, 'Alice');
|
|
622
|
+
assertEqual(sorted[1].name, 'Bob');
|
|
623
|
+
assertEqual(sorted[2].name, 'Charlie');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
runner.add('QueryResult - Pagination and chunking', () => {
|
|
627
|
+
const data = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));
|
|
628
|
+
const result = createQueryResult(data);
|
|
629
|
+
|
|
630
|
+
// Test pagination
|
|
631
|
+
const page1 = result.paginate(1, 3);
|
|
632
|
+
assertEqual(page1.length, 3);
|
|
633
|
+
assertEqual(page1[0].id, 1);
|
|
634
|
+
|
|
635
|
+
const page2 = result.paginate(2, 3);
|
|
636
|
+
assertEqual(page2[0].id, 4);
|
|
637
|
+
|
|
638
|
+
// Test chunking
|
|
639
|
+
const chunks = result.chunk(3);
|
|
640
|
+
assertEqual(chunks.length, 4); // 10 items, chunks of 3 = 4 chunks
|
|
641
|
+
assertEqual(chunks[0].length, 3);
|
|
642
|
+
assertEqual(chunks[3].length, 1); // Last chunk has remainder
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
runner.add('QueryResult - Distinct and unique', () => {
|
|
646
|
+
const data = [
|
|
647
|
+
{ category: 'A', value: 1 },
|
|
648
|
+
{ category: 'B', value: 2 },
|
|
649
|
+
{ category: 'A', value: 3 },
|
|
650
|
+
{ category: 'C', value: 4 }
|
|
651
|
+
];
|
|
652
|
+
const result = createQueryResult(data);
|
|
653
|
+
|
|
654
|
+
const distinctByCategory = result.distinctBy('category');
|
|
655
|
+
assertEqual(distinctByCategory.length, 3);
|
|
656
|
+
|
|
657
|
+
// Test unique (for primitive arrays)
|
|
658
|
+
const numbers = createQueryResult([1, 2, 2, 3, 3, 4]);
|
|
659
|
+
const uniqueNumbers = numbers.unique();
|
|
660
|
+
assertEqual(uniqueNumbers.length, 4);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
runner.add('QueryResult - Statistical functions', () => {
|
|
664
|
+
const data = [
|
|
665
|
+
{ category: 'A', active: true },
|
|
666
|
+
{ category: 'B', active: false },
|
|
667
|
+
{ category: 'A', active: true },
|
|
668
|
+
{ category: 'C', active: true }
|
|
669
|
+
];
|
|
670
|
+
const result = createQueryResult(data);
|
|
671
|
+
|
|
672
|
+
// Test count with predicate
|
|
673
|
+
const activeCount = result.count(item => item.active);
|
|
674
|
+
assertEqual(activeCount, 3);
|
|
675
|
+
|
|
676
|
+
// Test countBy
|
|
677
|
+
const categoryCounts = result.countBy('category');
|
|
678
|
+
assertEqual(categoryCounts['A'], 2);
|
|
679
|
+
assertEqual(categoryCounts['B'], 1);
|
|
680
|
+
assertEqual(categoryCounts['C'], 1);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
runner.add('QueryResult - Joins', () => {
|
|
684
|
+
const users = createQueryResult([
|
|
685
|
+
{ id: 1, name: 'John', deptId: 10 },
|
|
686
|
+
{ id: 2, name: 'Jane', deptId: 20 }
|
|
687
|
+
]);
|
|
688
|
+
|
|
689
|
+
const departments = createQueryResult([
|
|
690
|
+
{ id: 10, name: 'Engineering' },
|
|
691
|
+
{ id: 20, name: 'Design' }
|
|
692
|
+
]);
|
|
693
|
+
|
|
694
|
+
// Test inner join
|
|
695
|
+
const innerJoined = users.innerJoin(departments, 'deptId', 'id');
|
|
696
|
+
assertEqual(innerJoined.length, 2);
|
|
697
|
+
assertEqual(innerJoined[0].name, 'Engineering'); // dept name
|
|
698
|
+
|
|
699
|
+
// Test left join
|
|
700
|
+
const leftJoined = users.leftJoin(departments, 'deptId', 'id');
|
|
701
|
+
assertEqual(leftJoined.length, 2);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
runner.add('QueryResult - Export functions', () => {
|
|
705
|
+
const data = [
|
|
706
|
+
{ id: 1, name: 'John', active: true },
|
|
707
|
+
{ id: 2, name: 'Jane', active: false }
|
|
708
|
+
];
|
|
709
|
+
const result = createQueryResult(data);
|
|
710
|
+
|
|
711
|
+
// Test CSV export
|
|
712
|
+
const csv = result.toCsv();
|
|
713
|
+
const lines = csv.split('\n');
|
|
714
|
+
assertEqual(lines.length, 3); // header + 2 rows
|
|
715
|
+
assertTrue(lines[0].includes('id'));
|
|
716
|
+
assertTrue(lines[0].includes('name'));
|
|
717
|
+
assertTrue(lines[0].includes('active'));
|
|
718
|
+
|
|
719
|
+
// Test JSON export
|
|
720
|
+
const json = result.toJSON();
|
|
721
|
+
const parsed = JSON.parse(json);
|
|
722
|
+
assertEqual(parsed.length, 2);
|
|
723
|
+
assertEqual(parsed[0].name, 'John');
|
|
724
|
+
|
|
725
|
+
// Test array conversion
|
|
726
|
+
const array = result.toArray();
|
|
727
|
+
assertEqual(array.length, 2);
|
|
728
|
+
assertTrue(Array.isArray(array));
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
runner.add('QueryResult - Nested field access', () => {
|
|
732
|
+
const data = [
|
|
733
|
+
{ user: { profile: { name: 'John', age: 30 } } },
|
|
734
|
+
{ user: { profile: { name: 'Jane', age: 25 } } }
|
|
735
|
+
];
|
|
736
|
+
const result = createQueryResult(data);
|
|
737
|
+
|
|
738
|
+
// Test nested pluck
|
|
739
|
+
const names = result.pluck('user.profile.name');
|
|
740
|
+
assertEqual(names, ['John', 'Jane']);
|
|
741
|
+
|
|
742
|
+
// Test nested sort
|
|
743
|
+
const sorted = result.sortBy('user.profile.age');
|
|
744
|
+
assertEqual(sorted[0].user.profile.name, 'Jane');
|
|
745
|
+
assertEqual(sorted[1].user.profile.name, 'John');
|
|
746
|
+
|
|
747
|
+
// Test nested groupBy
|
|
748
|
+
const grouped = result.groupBy('user.profile.name');
|
|
749
|
+
assertTrue('John' in grouped);
|
|
750
|
+
assertTrue('Jane' in grouped);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// ===== ERROR HANDLING AND EDGE CASES =====
|
|
754
|
+
|
|
755
|
+
runner.add('Error handling - Invalid conditions', () => {
|
|
756
|
+
assertThrows(() => {
|
|
757
|
+
const builder = new ConditionBuilder();
|
|
758
|
+
builder.buildSingle(); // Should throw - no conditions
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
runner.add('Error handling - Missing required data', () => {
|
|
763
|
+
assertThrows(() => {
|
|
764
|
+
const logicalOp = new LogicalOperator('condition' as any, [], undefined);
|
|
765
|
+
logicalOp.toComposable(); // Should throw - no condition provided
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
runner.add('Edge cases - Empty and null data', () => {
|
|
770
|
+
const empty = createQueryResult([]);
|
|
771
|
+
|
|
772
|
+
assertEqual(empty.len(), 0);
|
|
773
|
+
assertTrue(empty.isEmpty());
|
|
774
|
+
assertEqual(empty.first(), undefined);
|
|
775
|
+
assertEqual(empty.last(), undefined);
|
|
776
|
+
|
|
777
|
+
const summary = empty.summarizeNumeric('nonexistent');
|
|
778
|
+
assertEqual(summary, null);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
runner.add('Edge cases - Nested field access with missing data', () => {
|
|
782
|
+
const data = [
|
|
783
|
+
{ user: { name: 'John' } }, // Missing profile.age
|
|
784
|
+
{ user: { profile: { age: 25 } } } // Missing name
|
|
785
|
+
];
|
|
786
|
+
const result = createQueryResult(data);
|
|
787
|
+
|
|
788
|
+
const ages = result.pluck('user.profile.age');
|
|
789
|
+
assertEqual(ages[0], undefined);
|
|
790
|
+
assertEqual(ages[1], 25);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// Run all tests
|
|
794
|
+
runner.run();
|