@onchaindb/sdk 2.0.1 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +5 -1
- package/.gitignore +1 -0
- package/README.md +0 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +0 -5
- package/dist/client.js.map +1 -1
- package/dist/database.d.ts +46 -8
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +32 -9
- package/dist/database.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/query-sdk/NestedBuilders.d.ts +3 -2
- package/dist/query-sdk/NestedBuilders.d.ts.map +1 -1
- package/dist/query-sdk/NestedBuilders.js +7 -4
- package/dist/query-sdk/NestedBuilders.js.map +1 -1
- package/dist/query-sdk/QueryBuilder.d.ts +17 -15
- package/dist/query-sdk/QueryBuilder.d.ts.map +1 -1
- package/dist/query-sdk/QueryBuilder.js +126 -190
- package/dist/query-sdk/QueryBuilder.js.map +1 -1
- package/dist/query-sdk/index.d.ts +24 -1
- package/dist/query-sdk/index.d.ts.map +1 -1
- package/dist/query-sdk/index.js.map +1 -1
- package/dist/query-sdk/operators.d.ts +3 -2
- package/dist/query-sdk/operators.d.ts.map +1 -1
- package/dist/query-sdk/operators.js +7 -4
- package/dist/query-sdk/operators.js.map +1 -1
- package/dist/types.d.ts +17 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/skills.md +5 -5
- package/src/client.ts +0 -6
- package/src/database.ts +159 -26
- package/src/index.ts +23 -10
- package/src/query-sdk/NestedBuilders.ts +14 -4
- package/src/query-sdk/QueryBuilder.ts +212 -235
- package/src/query-sdk/index.ts +19 -1
- package/src/query-sdk/operators.ts +15 -4
- package/src/query-sdk/tests/FieldConditionBuilder.test.ts +4 -2
- package/src/query-sdk/tests/NestedBuilders.test.ts +4 -3
- package/src/types.ts +26 -17
- package/.DS_Store +0 -0
|
@@ -3,7 +3,7 @@ import {QueryValue} from './index';
|
|
|
3
3
|
import {FieldConditionBuilder, LogicalOperator} from './operators';
|
|
4
4
|
import {SelectionBuilder} from './SelectionBuilder';
|
|
5
5
|
import {ConditionBuilder} from './ConditionBuilder';
|
|
6
|
-
import {FieldMap, HttpClient} from "./index";
|
|
6
|
+
import {FieldMap, HttpClient, AggregateSpec} from "./index";
|
|
7
7
|
import {AxiosError} from "axios";
|
|
8
8
|
import {
|
|
9
9
|
X402PaymentRequiredResponse,
|
|
@@ -22,13 +22,6 @@ export interface ServerJoinConfig {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// DEPRECATED: Client-side join configuration (kept for backward compatibility)
|
|
26
|
-
export interface JoinConfig {
|
|
27
|
-
parentCollection: string;
|
|
28
|
-
parentField: string;
|
|
29
|
-
childField: string;
|
|
30
|
-
alias: string;
|
|
31
|
-
}
|
|
32
25
|
|
|
33
26
|
// Main query builder providing fluent API for building queries
|
|
34
27
|
export class QueryBuilder {
|
|
@@ -37,15 +30,16 @@ export class QueryBuilder {
|
|
|
37
30
|
private fieldMap?: FieldMap;
|
|
38
31
|
private limitValue?: number;
|
|
39
32
|
private offsetValue?: number;
|
|
40
|
-
private
|
|
33
|
+
private _sortField?: string;
|
|
41
34
|
private sortDirection?: string;
|
|
42
35
|
private includeHistoryValue?: boolean;
|
|
43
36
|
private httpClient?: HttpClient;
|
|
44
37
|
private serverUrl?: string;
|
|
45
38
|
private collectionName: string | undefined;
|
|
46
39
|
private app: string | undefined;
|
|
47
|
-
private serverJoinConfigs: ServerJoinConfig[] = [];
|
|
48
|
-
private
|
|
40
|
+
private serverJoinConfigs: ServerJoinConfig[] = [];
|
|
41
|
+
private _groupBy?: string[];
|
|
42
|
+
private _aggregate?: AggregateSpec;
|
|
49
43
|
|
|
50
44
|
constructor(httpClient?: HttpClient, serverUrl?: string, app?: string) {
|
|
51
45
|
this.httpClient = httpClient;
|
|
@@ -115,18 +109,6 @@ export class QueryBuilder {
|
|
|
115
109
|
return new JoinBuilder(this, alias, model, true); // many = true
|
|
116
110
|
}
|
|
117
111
|
|
|
118
|
-
// DEPRECATED: Client-side join (uses Promise.all to fetch related data)
|
|
119
|
-
// Use serverJoin(), joinWith(), joinOne(), or joinMany() instead for better performance
|
|
120
|
-
join(parentCollection: string, parentField: string, childField: string, alias: string): QueryBuilder {
|
|
121
|
-
console.warn('QueryBuilder.join() is deprecated. Use serverJoin(), joinOne(), or joinMany() for server-side JOINs.');
|
|
122
|
-
this.joinConfigs.push({
|
|
123
|
-
parentCollection,
|
|
124
|
-
parentField,
|
|
125
|
-
childField,
|
|
126
|
-
alias
|
|
127
|
-
});
|
|
128
|
-
return this;
|
|
129
|
-
}
|
|
130
112
|
|
|
131
113
|
// Build selections using a builder function
|
|
132
114
|
select(builderFn: (builder: SelectionBuilder) => SelectionBuilder): QueryBuilder {
|
|
@@ -162,29 +144,31 @@ export class QueryBuilder {
|
|
|
162
144
|
return this;
|
|
163
145
|
}
|
|
164
146
|
|
|
147
|
+
/** Alias for limit(). */
|
|
148
|
+
take(n: number): QueryBuilder { return this.limit(n); }
|
|
149
|
+
|
|
165
150
|
// Set offset for results
|
|
166
151
|
offset(offset: number): QueryBuilder {
|
|
167
152
|
this.offsetValue = offset;
|
|
168
153
|
return this;
|
|
169
154
|
}
|
|
170
155
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
this.sortBy = fields;
|
|
174
|
-
switch (direction) {
|
|
175
|
-
case "ASC":
|
|
176
|
-
this.sortDirection = "ascending";
|
|
177
|
-
break;
|
|
178
|
-
case "DESC":
|
|
179
|
-
this.sortDirection = "descending";
|
|
180
|
-
break;
|
|
181
|
-
default:
|
|
182
|
-
this.sortDirection = "ascending";
|
|
183
|
-
}
|
|
156
|
+
/** Alias for offset(). */
|
|
157
|
+
skip(n: number): QueryBuilder { return this.offset(n); }
|
|
184
158
|
|
|
159
|
+
// Set sort field and direction (accepts "asc"/"desc" or "ASC"/"DESC")
|
|
160
|
+
sortBy(field: string, direction?: "asc" | "desc" | "ASC" | "DESC"): QueryBuilder {
|
|
161
|
+
this._sortField = field;
|
|
162
|
+
const dir = direction?.toLowerCase();
|
|
163
|
+
this.sortDirection = dir === "desc" ? "descending" : "ascending";
|
|
185
164
|
return this;
|
|
186
165
|
}
|
|
187
166
|
|
|
167
|
+
// Alias kept for backward compatibility
|
|
168
|
+
orderBy(field: string, direction?: "ASC" | "DESC"): QueryBuilder {
|
|
169
|
+
return this.sortBy(field, direction);
|
|
170
|
+
}
|
|
171
|
+
|
|
188
172
|
// Include all versions of records in query results (default: false, returns only latest versions)
|
|
189
173
|
// When true, returns all historical versions of each record
|
|
190
174
|
// When false or not called (default), returns only the most recent version of each record
|
|
@@ -202,18 +186,6 @@ export class QueryBuilder {
|
|
|
202
186
|
throw new Error('Server URL is required for query execution');
|
|
203
187
|
}
|
|
204
188
|
|
|
205
|
-
// Server-side JOINs are embedded in the query structure and handled by the backend
|
|
206
|
-
if (this.serverJoinConfigs.length > 0) {
|
|
207
|
-
return this.executeSimpleQuery<T>();
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// DEPRECATED: Client-side joins using Promise.all
|
|
211
|
-
if (this.joinConfigs.length > 0) {
|
|
212
|
-
console.warn('Using deprecated client-side JOINs. Consider migrating to serverJoin() for better performance.');
|
|
213
|
-
return this.executeWithJoins<T>();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// No joins, execute as a normal query
|
|
217
189
|
return this.executeSimpleQuery<T>();
|
|
218
190
|
}
|
|
219
191
|
|
|
@@ -277,8 +249,12 @@ export class QueryBuilder {
|
|
|
277
249
|
* ```
|
|
278
250
|
*/
|
|
279
251
|
async count(): Promise<number> {
|
|
280
|
-
const
|
|
281
|
-
|
|
252
|
+
const clone = this.clone();
|
|
253
|
+
clone._setSelections({});
|
|
254
|
+
clone._setAggregate({ _count: { '$count': '*' } });
|
|
255
|
+
const response = await clone.execute();
|
|
256
|
+
const first = response.records?.[0] as any;
|
|
257
|
+
return first?._count ?? 0;
|
|
282
258
|
}
|
|
283
259
|
|
|
284
260
|
/**
|
|
@@ -296,13 +272,12 @@ export class QueryBuilder {
|
|
|
296
272
|
* ```
|
|
297
273
|
*/
|
|
298
274
|
async sumBy(field: string): Promise<number> {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}, 0);
|
|
275
|
+
const clone = this.clone();
|
|
276
|
+
clone._setSelections({});
|
|
277
|
+
clone._setAggregate({ _result: { '$sum': field } });
|
|
278
|
+
const response = await clone.execute();
|
|
279
|
+
const first = response.records?.[0] as any;
|
|
280
|
+
return first?._result ?? 0;
|
|
306
281
|
}
|
|
307
282
|
|
|
308
283
|
/**
|
|
@@ -320,15 +295,12 @@ export class QueryBuilder {
|
|
|
320
295
|
* ```
|
|
321
296
|
*/
|
|
322
297
|
async avgBy(field: string): Promise<number> {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}, 0);
|
|
330
|
-
|
|
331
|
-
return sum / response.records.length;
|
|
298
|
+
const clone = this.clone();
|
|
299
|
+
clone._setSelections({});
|
|
300
|
+
clone._setAggregate({ _result: { '$avg': field } });
|
|
301
|
+
const response = await clone.execute();
|
|
302
|
+
const first = response.records?.[0] as any;
|
|
303
|
+
return first?._result ?? 0;
|
|
332
304
|
}
|
|
333
305
|
|
|
334
306
|
/**
|
|
@@ -345,14 +317,12 @@ export class QueryBuilder {
|
|
|
345
317
|
* ```
|
|
346
318
|
*/
|
|
347
319
|
async maxBy<T = any>(field: string): Promise<T | null> {
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return value > max ? value : max;
|
|
355
|
-
}, null);
|
|
320
|
+
const clone = this.clone();
|
|
321
|
+
clone._setSelections({});
|
|
322
|
+
clone._setAggregate({ _result: { '$max': field } });
|
|
323
|
+
const response = await clone.execute();
|
|
324
|
+
const first = response.records?.[0] as any;
|
|
325
|
+
return first?._result ?? null;
|
|
356
326
|
}
|
|
357
327
|
|
|
358
328
|
/**
|
|
@@ -369,14 +339,12 @@ export class QueryBuilder {
|
|
|
369
339
|
* ```
|
|
370
340
|
*/
|
|
371
341
|
async minBy<T = any>(field: string): Promise<T | null> {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return value < min ? value : min;
|
|
379
|
-
}, null);
|
|
342
|
+
const clone = this.clone();
|
|
343
|
+
clone._setSelections({});
|
|
344
|
+
clone._setAggregate({ _result: { '$min': field } });
|
|
345
|
+
const response = await clone.execute();
|
|
346
|
+
const first = response.records?.[0] as any;
|
|
347
|
+
return first?._result ?? null;
|
|
380
348
|
}
|
|
381
349
|
|
|
382
350
|
/**
|
|
@@ -421,8 +389,46 @@ export class QueryBuilder {
|
|
|
421
389
|
* ```
|
|
422
390
|
*/
|
|
423
391
|
async countDistinct(field: string): Promise<number> {
|
|
424
|
-
const
|
|
425
|
-
|
|
392
|
+
const clone = this.clone();
|
|
393
|
+
clone._setSelections({});
|
|
394
|
+
clone._setAggregate({ _result: { '$countDistinct': field } });
|
|
395
|
+
const response = await clone.execute();
|
|
396
|
+
const first = response.records?.[0] as any;
|
|
397
|
+
return first?._result ?? 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Run multiple aggregations in a single HTTP round-trip.
|
|
402
|
+
*
|
|
403
|
+
* Each key in `spec` becomes an output alias in the returned record.
|
|
404
|
+
* Supported operators: `$count`, `$sum`, `$avg`, `$max`, `$min`, `$countDistinct`.
|
|
405
|
+
* Use `'*'` as the field value for `$count`.
|
|
406
|
+
*
|
|
407
|
+
* @param spec - Map of output alias to `{ '$op': 'field' }` descriptor
|
|
408
|
+
* @returns Promise resolving to a single record containing all requested aggregation results
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```typescript
|
|
412
|
+
* const stats = await db.queryBuilder()
|
|
413
|
+
* .collection('posts')
|
|
414
|
+
* .whereField('category').equals('technology')
|
|
415
|
+
* .runAggregate({
|
|
416
|
+
* total: { '$count': '*' },
|
|
417
|
+
* totalLikes: { '$sum': 'likes' },
|
|
418
|
+
* avgLikes: { '$avg': 'likes' },
|
|
419
|
+
* maxLikes: { '$max': 'likes' },
|
|
420
|
+
* minLikes: { '$min': 'likes' },
|
|
421
|
+
* uniqueAuthors: { '$countDistinct': 'author_id' }
|
|
422
|
+
* });
|
|
423
|
+
* // Returns: { total: 4, totalLikes: 203, avgLikes: 50.75, maxLikes: 67, minLikes: 38, uniqueAuthors: 2 }
|
|
424
|
+
* ```
|
|
425
|
+
*/
|
|
426
|
+
async runAggregate(spec: AggregateSpec): Promise<Record<string, any>> {
|
|
427
|
+
const clone = this.clone();
|
|
428
|
+
clone._setSelections({});
|
|
429
|
+
clone._setAggregate(spec);
|
|
430
|
+
const response = await clone.execute();
|
|
431
|
+
return (response.records?.[0] as any) ?? {};
|
|
426
432
|
}
|
|
427
433
|
|
|
428
434
|
/**
|
|
@@ -510,7 +516,7 @@ export class QueryBuilder {
|
|
|
510
516
|
...queryValue,
|
|
511
517
|
limit: this.limitValue,
|
|
512
518
|
offset: this.offsetValue,
|
|
513
|
-
sortBy: this.
|
|
519
|
+
sortBy: this._sortField,
|
|
514
520
|
sortDirection: this.sortDirection,
|
|
515
521
|
root: `${this.app}::${this.collectionName}`
|
|
516
522
|
};
|
|
@@ -528,7 +534,7 @@ export class QueryBuilder {
|
|
|
528
534
|
...this.buildQueryValue(),
|
|
529
535
|
limit: this.limitValue,
|
|
530
536
|
offset: this.offsetValue,
|
|
531
|
-
sortBy: this.
|
|
537
|
+
sortBy: this._sortField,
|
|
532
538
|
sortDirection: this.sortDirection,
|
|
533
539
|
};
|
|
534
540
|
}
|
|
@@ -541,12 +547,13 @@ export class QueryBuilder {
|
|
|
541
547
|
cloned.fieldMap = this.fieldMap ? {...this.fieldMap} : undefined;
|
|
542
548
|
cloned.limitValue = this.limitValue;
|
|
543
549
|
cloned.offsetValue = this.offsetValue;
|
|
544
|
-
cloned.
|
|
550
|
+
cloned._sortField = this._sortField;
|
|
545
551
|
cloned.sortDirection = this.sortDirection;
|
|
546
552
|
cloned.includeHistoryValue = this.includeHistoryValue;
|
|
547
553
|
cloned.collectionName = this.collectionName;
|
|
548
|
-
cloned.serverJoinConfigs = [...this.serverJoinConfigs];
|
|
549
|
-
cloned.
|
|
554
|
+
cloned.serverJoinConfigs = [...this.serverJoinConfigs];
|
|
555
|
+
cloned._groupBy = this._groupBy ? [...this._groupBy] : undefined;
|
|
556
|
+
cloned._aggregate = this._aggregate ? {...this._aggregate} : undefined;
|
|
550
557
|
return cloned;
|
|
551
558
|
}
|
|
552
559
|
|
|
@@ -555,6 +562,22 @@ export class QueryBuilder {
|
|
|
555
562
|
this.serverJoinConfigs.push(config);
|
|
556
563
|
}
|
|
557
564
|
|
|
565
|
+
// Internal helpers used by aggregation methods and GroupByQueryBuilder after cloning
|
|
566
|
+
_setSelections(select: SelectionMap): this {
|
|
567
|
+
this.selections = select;
|
|
568
|
+
return this;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
_setGroupBy(fields: string[]): this {
|
|
572
|
+
this._groupBy = fields;
|
|
573
|
+
return this;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
_setAggregate(agg: AggregateSpec): this {
|
|
577
|
+
this._aggregate = agg;
|
|
578
|
+
return this;
|
|
579
|
+
}
|
|
580
|
+
|
|
558
581
|
// Execute a simple query without joins
|
|
559
582
|
private async executeSimpleQuery<T extends Record<string, any>>(): Promise<QueryResponse<T>> {
|
|
560
583
|
const request = this.getQueryRequest();
|
|
@@ -601,73 +624,6 @@ export class QueryBuilder {
|
|
|
601
624
|
}
|
|
602
625
|
}
|
|
603
626
|
|
|
604
|
-
// Execute query with joins using parallel queries
|
|
605
|
-
private async executeWithJoins<T extends Record<string, any>>(): Promise<QueryResponse<T>> {
|
|
606
|
-
// First, get the main collection records
|
|
607
|
-
const mainQuery = this.clone();
|
|
608
|
-
mainQuery.joinConfigs = []; // Remove joins for main query
|
|
609
|
-
const mainResults = await mainQuery.executeSimpleQuery();
|
|
610
|
-
|
|
611
|
-
if (mainResults.records.length === 0) {
|
|
612
|
-
return mainResults as QueryResponse<T>;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// For each join, execute parallel queries and merge results
|
|
616
|
-
const joinResults: Record<string, any[]> = {};
|
|
617
|
-
|
|
618
|
-
for (const joinConfig of this.joinConfigs) {
|
|
619
|
-
// Get all unique values from the main results for the join key
|
|
620
|
-
const joinKeys = [...new Set(
|
|
621
|
-
mainResults.records
|
|
622
|
-
.map(record => record[joinConfig.childField])
|
|
623
|
-
.filter(key => key !== null && key !== undefined)
|
|
624
|
-
)];
|
|
625
|
-
|
|
626
|
-
if (joinKeys.length > 0) {
|
|
627
|
-
// Query the joined collection
|
|
628
|
-
const joinQuery = new QueryBuilder(this.httpClient, this.serverUrl, this.app);
|
|
629
|
-
const joinResponse = await joinQuery
|
|
630
|
-
.collection(joinConfig.parentCollection)
|
|
631
|
-
.whereField(joinConfig.parentField).in(joinKeys)
|
|
632
|
-
.selectAll()
|
|
633
|
-
.executeSimpleQuery();
|
|
634
|
-
|
|
635
|
-
joinResults[joinConfig.alias] = joinResponse.records;
|
|
636
|
-
} else {
|
|
637
|
-
joinResults[joinConfig.alias] = [];
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// Merge join results with main results
|
|
642
|
-
const mergedRecords = mainResults.records.map(mainRecord => {
|
|
643
|
-
const merged: any = {...mainRecord};
|
|
644
|
-
|
|
645
|
-
for (const joinConfig of this.joinConfigs) {
|
|
646
|
-
const joinKey = mainRecord[joinConfig.childField];
|
|
647
|
-
const relatedRecords = joinResults[joinConfig.alias].filter(
|
|
648
|
-
joinRecord => joinRecord[joinConfig.parentField] === joinKey
|
|
649
|
-
);
|
|
650
|
-
|
|
651
|
-
// For one-to-many relationships, return array; for many-to-one, return single object
|
|
652
|
-
if (joinConfig.parentCollection === this.collectionName) {
|
|
653
|
-
// Self-referential join (like replies to tweets)
|
|
654
|
-
merged[joinConfig.alias] = relatedRecords;
|
|
655
|
-
} else {
|
|
656
|
-
// Cross-collection join
|
|
657
|
-
merged[joinConfig.alias] = relatedRecords.length > 0 ? relatedRecords[0] : null;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
return merged;
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
return {
|
|
665
|
-
records: mergedRecords,
|
|
666
|
-
limit: mainResults.limit,
|
|
667
|
-
page: mainResults.page,
|
|
668
|
-
total: mainResults.total
|
|
669
|
-
} as QueryResponse<T>;
|
|
670
|
-
}
|
|
671
627
|
|
|
672
628
|
// Build the query value object for HTTP requests
|
|
673
629
|
private buildQueryValue(): QueryValue {
|
|
@@ -704,6 +660,21 @@ export class QueryBuilder {
|
|
|
704
660
|
queryValue.include_history = this.includeHistoryValue;
|
|
705
661
|
}
|
|
706
662
|
|
|
663
|
+
// Send fieldMap to Scepter so it can rename output fields server-side
|
|
664
|
+
if (this.fieldMap && Object.keys(this.fieldMap).length > 0) {
|
|
665
|
+
queryValue.field_map = this.fieldMap;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Send group_by as an array so Scepter performs server-side grouping
|
|
669
|
+
if (this._groupBy && this._groupBy.length > 0) {
|
|
670
|
+
queryValue.group_by = this._groupBy;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Send aggregate separately — never embed in select
|
|
674
|
+
if (this._aggregate && Object.keys(this._aggregate).length > 0) {
|
|
675
|
+
queryValue.aggregate = this._aggregate;
|
|
676
|
+
}
|
|
677
|
+
|
|
707
678
|
return queryValue;
|
|
708
679
|
}
|
|
709
680
|
}
|
|
@@ -743,9 +714,14 @@ export class WhereClause {
|
|
|
743
714
|
isLocalIp(): QueryBuilder { return this.set(new FieldConditionBuilder(this.fieldName).isLocalIp()); }
|
|
744
715
|
isExternalIp(): QueryBuilder { return this.set(new FieldConditionBuilder(this.fieldName).isExternalIp()); }
|
|
745
716
|
b64(value: string): QueryBuilder { return this.set(new FieldConditionBuilder(this.fieldName).b64(value)); }
|
|
746
|
-
|
|
717
|
+
/** Case-sensitive membership check (use .in() for case-insensitive). */
|
|
718
|
+
inDataset(values: string[]): QueryBuilder { return this.set(new FieldConditionBuilder(this.fieldName).inDataset(values)); }
|
|
747
719
|
inCountry(countryCode: string): QueryBuilder { return this.set(new FieldConditionBuilder(this.fieldName).inCountry(countryCode)); }
|
|
748
|
-
|
|
720
|
+
/** Check if IP falls within CIDR range(s). Always uses "$cidr" alias to avoid Scepter stack overflow. */
|
|
721
|
+
cidr(ranges: string | string[]): QueryBuilder { return this.set(new FieldConditionBuilder(this.fieldName).cidr(ranges)); }
|
|
722
|
+
/** Matches if the field contains ANY of the given keywords (case-insensitive substring). */
|
|
723
|
+
keywords(keywords: string[]): QueryBuilder { return this.set(new FieldConditionBuilder(this.fieldName).keywords(keywords)); }
|
|
724
|
+
/** Exclusive range: matches records where min < field < max (both bounds excluded). */
|
|
749
725
|
between(min: any, max: any): QueryBuilder { return this.set(new FieldConditionBuilder(this.fieldName).between(min, max)); }
|
|
750
726
|
isTrue(): QueryBuilder { return this.set(new FieldConditionBuilder(this.fieldName).isTrue()); }
|
|
751
727
|
isFalse(): QueryBuilder { return this.set(new FieldConditionBuilder(this.fieldName).isFalse()); }
|
|
@@ -914,119 +890,120 @@ export class GroupByQueryBuilder {
|
|
|
914
890
|
) {}
|
|
915
891
|
|
|
916
892
|
/**
|
|
917
|
-
* Count records in each group
|
|
893
|
+
* Count records in each group.
|
|
894
|
+
* Executes a server-side groupBy + $count aggregation.
|
|
918
895
|
* @returns Promise resolving to map of group key -> count
|
|
919
896
|
*/
|
|
920
897
|
async count(): Promise<Record<string, number>> {
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
groups[key] = (groups[key] || 0) + 1;
|
|
928
|
-
}
|
|
929
|
-
return groups;
|
|
898
|
+
const clone = this.queryBuilder.clone();
|
|
899
|
+
clone._setGroupBy([this.groupByField]);
|
|
900
|
+
clone._setSelections({});
|
|
901
|
+
clone._setAggregate({ _count: { '$count': '*' } });
|
|
902
|
+
const response = await clone.execute();
|
|
903
|
+
return this.buildGroupMap<number>(response.records, '_count');
|
|
930
904
|
}
|
|
931
905
|
|
|
932
906
|
/**
|
|
933
|
-
* Sum a numeric field within each group
|
|
907
|
+
* Sum a numeric field within each group.
|
|
908
|
+
* Executes a server-side groupBy + $sum aggregation.
|
|
934
909
|
* @param field - The field name to sum
|
|
935
910
|
* @returns Promise resolving to map of group key -> sum
|
|
936
911
|
*/
|
|
937
912
|
async sumBy(field: string): Promise<Record<string, number>> {
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const value = (record as any)[field];
|
|
945
|
-
groups[key] = (groups[key] || 0) + (typeof value === 'number' ? value : 0);
|
|
946
|
-
}
|
|
947
|
-
return groups;
|
|
913
|
+
const clone = this.queryBuilder.clone();
|
|
914
|
+
clone._setGroupBy([this.groupByField]);
|
|
915
|
+
clone._setSelections({});
|
|
916
|
+
clone._setAggregate({ _result: { '$sum': field } });
|
|
917
|
+
const response = await clone.execute();
|
|
918
|
+
return this.buildGroupMap<number>(response.records, '_result');
|
|
948
919
|
}
|
|
949
920
|
|
|
950
921
|
/**
|
|
951
|
-
* Calculate average of a numeric field within each group
|
|
922
|
+
* Calculate average of a numeric field within each group.
|
|
923
|
+
* Executes a server-side groupBy + $avg aggregation.
|
|
952
924
|
* @param field - The field name to average
|
|
953
925
|
* @returns Promise resolving to map of group key -> average
|
|
954
926
|
*/
|
|
955
927
|
async avgBy(field: string): Promise<Record<string, number>> {
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
const value = (record as any)[field];
|
|
963
|
-
if (!groups[key]) {
|
|
964
|
-
groups[key] = { sum: 0, count: 0 };
|
|
965
|
-
}
|
|
966
|
-
groups[key].sum += typeof value === 'number' ? value : 0;
|
|
967
|
-
groups[key].count += 1;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
const result: Record<string, number> = {};
|
|
971
|
-
for (const [key, { sum, count }] of Object.entries(groups)) {
|
|
972
|
-
result[key] = count > 0 ? sum / count : 0;
|
|
973
|
-
}
|
|
974
|
-
return result;
|
|
928
|
+
const clone = this.queryBuilder.clone();
|
|
929
|
+
clone._setGroupBy([this.groupByField]);
|
|
930
|
+
clone._setSelections({});
|
|
931
|
+
clone._setAggregate({ _result: { '$avg': field } });
|
|
932
|
+
const response = await clone.execute();
|
|
933
|
+
return this.buildGroupMap<number>(response.records, '_result');
|
|
975
934
|
}
|
|
976
935
|
|
|
977
936
|
/**
|
|
978
|
-
* Find maximum value of a field within each group
|
|
937
|
+
* Find maximum value of a field within each group.
|
|
938
|
+
* Executes a server-side groupBy + $max aggregation.
|
|
979
939
|
* @param field - The field name to find maximum
|
|
980
940
|
* @returns Promise resolving to map of group key -> max value
|
|
981
941
|
*/
|
|
982
942
|
async maxBy<T = any>(field: string): Promise<Record<string, T>> {
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
const value = (record as any)[field] as T;
|
|
990
|
-
if (!(key in groups) || value > (groups[key] as T)) {
|
|
991
|
-
groups[key] = value;
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
return groups;
|
|
943
|
+
const clone = this.queryBuilder.clone();
|
|
944
|
+
clone._setGroupBy([this.groupByField]);
|
|
945
|
+
clone._setSelections({});
|
|
946
|
+
clone._setAggregate({ _result: { '$max': field } });
|
|
947
|
+
const response = await clone.execute();
|
|
948
|
+
return this.buildGroupMap<T>(response.records, '_result');
|
|
995
949
|
}
|
|
996
950
|
|
|
997
951
|
/**
|
|
998
|
-
* Find minimum value of a field within each group
|
|
952
|
+
* Find minimum value of a field within each group.
|
|
953
|
+
* Executes a server-side groupBy + $min aggregation.
|
|
999
954
|
* @param field - The field name to find minimum
|
|
1000
955
|
* @returns Promise resolving to map of group key -> min value
|
|
1001
956
|
*/
|
|
1002
957
|
async minBy<T = any>(field: string): Promise<Record<string, T>> {
|
|
1003
|
-
const
|
|
1004
|
-
|
|
958
|
+
const clone = this.queryBuilder.clone();
|
|
959
|
+
clone._setGroupBy([this.groupByField]);
|
|
960
|
+
clone._setSelections({});
|
|
961
|
+
clone._setAggregate({ _result: { '$min': field } });
|
|
962
|
+
const response = await clone.execute();
|
|
963
|
+
return this.buildGroupMap<T>(response.records, '_result');
|
|
964
|
+
}
|
|
1005
965
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
966
|
+
/**
|
|
967
|
+
* Run multiple aggregations per group in a single HTTP round-trip.
|
|
968
|
+
*
|
|
969
|
+
* Each key in `spec` becomes an output alias on each returned record (alongside the group field).
|
|
970
|
+
* Supported operators: `$count`, `$sum`, `$avg`, `$max`, `$min`, `$countDistinct`.
|
|
971
|
+
*
|
|
972
|
+
* @param spec - Map of output alias to `{ '$op': 'field' }` descriptor
|
|
973
|
+
* @returns Promise resolving to an array of records, each containing the group field and all aggregation results
|
|
974
|
+
*
|
|
975
|
+
* @example
|
|
976
|
+
* ```typescript
|
|
977
|
+
* const rows = await db.queryBuilder()
|
|
978
|
+
* .collection('orders')
|
|
979
|
+
* .groupBy('category')
|
|
980
|
+
* .run({
|
|
981
|
+
* total: { '$count': '*' },
|
|
982
|
+
* revenue: { '$sum': 'amount' },
|
|
983
|
+
* avgOrder: { '$avg': 'amount' }
|
|
984
|
+
* });
|
|
985
|
+
* // Returns: [{ category: 'electronics', total: 12, revenue: 5000, avgOrder: 416.67 }, ...]
|
|
986
|
+
* ```
|
|
987
|
+
*/
|
|
988
|
+
async run(spec: AggregateSpec): Promise<Record<string, any>[]> {
|
|
989
|
+
const clone = this.queryBuilder.clone();
|
|
990
|
+
clone._setGroupBy([this.groupByField]);
|
|
991
|
+
clone._setSelections({});
|
|
992
|
+
clone._setAggregate(spec);
|
|
993
|
+
const response = await clone.execute();
|
|
994
|
+
return (response.records as any[]) ?? [];
|
|
1015
995
|
}
|
|
1016
996
|
|
|
1017
997
|
/**
|
|
1018
|
-
*
|
|
998
|
+
* Build a Record<groupKey, aggValue> from the records returned by Scepter.
|
|
999
|
+
* Each record contains the group field and the aggregated value under aggKey.
|
|
1019
1000
|
*/
|
|
1020
|
-
private
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
for (const part of parts) {
|
|
1026
|
-
current = current?.[part];
|
|
1027
|
-
}
|
|
1028
|
-
return String(current ?? 'null');
|
|
1001
|
+
private buildGroupMap<T>(records: any[], aggKey: string): Record<string, T> {
|
|
1002
|
+
const result: Record<string, T> = {};
|
|
1003
|
+
for (const record of records ?? []) {
|
|
1004
|
+
const key = String(record[this.groupByField] ?? 'null');
|
|
1005
|
+
result[key] = record[aggKey];
|
|
1029
1006
|
}
|
|
1030
|
-
return
|
|
1007
|
+
return result;
|
|
1031
1008
|
}
|
|
1032
1009
|
}
|
package/src/query-sdk/index.ts
CHANGED
|
@@ -8,6 +8,12 @@ export interface QueryValue {
|
|
|
8
8
|
find: any;
|
|
9
9
|
select: any;
|
|
10
10
|
include_history?: boolean;
|
|
11
|
+
/** Maps logical field names to one or more physical source fields. Values are arrays. */
|
|
12
|
+
field_map?: Record<string, string[]>;
|
|
13
|
+
/** Group-by fields for server-side aggregation. Pass as an array, e.g. ["category"]. */
|
|
14
|
+
group_by?: string[];
|
|
15
|
+
/** Server-side aggregation operations. Keys are output aliases, values are typed AggregateOp objects. */
|
|
16
|
+
aggregate?: AggregateSpec;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export type QueryRequest = QueryValue & {
|
|
@@ -24,7 +30,19 @@ export interface QueryResponse<T = any> {
|
|
|
24
30
|
|
|
25
31
|
// Selection types
|
|
26
32
|
export type SelectionMap = Record<string, any>;
|
|
27
|
-
|
|
33
|
+
/** Maps logical field names to one or more physical source fields. */
|
|
34
|
+
export type FieldMap = Record<string, string[]>;
|
|
35
|
+
|
|
36
|
+
// Aggregation operation types — mirrors Rust's AggregateOp untagged enum (query_input.rs)
|
|
37
|
+
export type AggCountOp = { '$count': string }; // '*' counts all records; or a field name
|
|
38
|
+
export type AggSumOp = { '$sum': string };
|
|
39
|
+
export type AggAvgOp = { '$avg': string };
|
|
40
|
+
export type AggMaxOp = { '$max': string };
|
|
41
|
+
export type AggMinOp = { '$min': string };
|
|
42
|
+
export type AggCountDistinctOp = { '$countDistinct': string };
|
|
43
|
+
export type AggregateOp = AggCountOp | AggSumOp | AggAvgOp | AggMaxOp | AggMinOp | AggCountDistinctOp;
|
|
44
|
+
/** Maps output alias names to aggregation operations. */
|
|
45
|
+
export type AggregateSpec = Record<string, AggregateOp>;
|
|
28
46
|
|
|
29
47
|
// Operator values - expanded to match Rust SDK
|
|
30
48
|
export type Val = string | number | boolean | null | Val[] | Date | RegExp;
|