flongo 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flongoCollection.d.ts +5 -4
- package/dist/flongoCollection.js +21 -15
- package/dist/flongoQuery.d.ts +60 -8
- package/dist/flongoQuery.js +100 -21
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -92,12 +92,13 @@ export declare class FlongoCollection<T> {
|
|
|
92
92
|
*/
|
|
93
93
|
getAll(query?: FlongoQuery, pagination?: Pagination): Promise<(Entity & T)[]>;
|
|
94
94
|
/**
|
|
95
|
-
* Executes a query carrying
|
|
96
|
-
*
|
|
97
|
-
*
|
|
95
|
+
* Executes a query carrying computed sort keys (`orderByRandom` and/or
|
|
96
|
+
* `orderByExpr`) via an aggregation pipeline. For random sorts, verifies the
|
|
97
|
+
* server supports `$toHashedIndexKey` (MongoDB >= 8.0) first so callers get an
|
|
98
|
+
* actionable error rather than a raw driver failure.
|
|
98
99
|
* @private
|
|
99
100
|
*/
|
|
100
|
-
private
|
|
101
|
+
private getAllViaPipeline;
|
|
101
102
|
/**
|
|
102
103
|
* Retrieves only the `_id`s of matching documents, as strings (the same
|
|
103
104
|
* normalization `toEntity` applies). Uses a server-side projection so full
|
package/dist/flongoCollection.js
CHANGED
|
@@ -128,10 +128,11 @@ class FlongoCollection {
|
|
|
128
128
|
* @returns Promise resolving to array of documents
|
|
129
129
|
*/
|
|
130
130
|
async getAll(query, pagination) {
|
|
131
|
-
//
|
|
132
|
-
// computed per-document
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
// Seeded random and expression sorts can't be expressed with find().sort() —
|
|
132
|
+
// they sort by computed per-document values, so they route through an
|
|
133
|
+
// aggregation pipeline.
|
|
134
|
+
if (query?.needsPipeline()) {
|
|
135
|
+
return this.getAllViaPipeline(query, pagination);
|
|
135
136
|
}
|
|
136
137
|
const mongodbQuery = query?.build() ?? {};
|
|
137
138
|
const mongodbOptions = query?.buildOptions(pagination) ?? new flongoQuery_1.FlongoQuery().buildOptions(pagination);
|
|
@@ -144,13 +145,16 @@ class FlongoCollection {
|
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
/**
|
|
147
|
-
* Executes a query carrying
|
|
148
|
-
*
|
|
149
|
-
*
|
|
148
|
+
* Executes a query carrying computed sort keys (`orderByRandom` and/or
|
|
149
|
+
* `orderByExpr`) via an aggregation pipeline. For random sorts, verifies the
|
|
150
|
+
* server supports `$toHashedIndexKey` (MongoDB >= 8.0) first so callers get an
|
|
151
|
+
* actionable error rather than a raw driver failure.
|
|
150
152
|
* @private
|
|
151
153
|
*/
|
|
152
|
-
async
|
|
153
|
-
|
|
154
|
+
async getAllViaPipeline(query, pagination) {
|
|
155
|
+
if (query.hasRandomSort()) {
|
|
156
|
+
await this.assertRandomSortSupported();
|
|
157
|
+
}
|
|
154
158
|
const pipeline = query.buildPipeline(pagination);
|
|
155
159
|
const res = await this.collection.aggregate(pipeline).toArray();
|
|
156
160
|
return res.map((d) => this.toEntity(d));
|
|
@@ -181,10 +185,12 @@ class FlongoCollection {
|
|
|
181
185
|
* @returns Promise resolving to the matching document ids as strings
|
|
182
186
|
*/
|
|
183
187
|
async getIds(query, pagination) {
|
|
184
|
-
// Mirror getAll:
|
|
188
|
+
// Mirror getAll: computed sort keys must execute via the aggregation
|
|
185
189
|
// pipeline. Only the id column is materialized on either path.
|
|
186
|
-
if (query?.
|
|
187
|
-
|
|
190
|
+
if (query?.needsPipeline()) {
|
|
191
|
+
if (query.hasRandomSort()) {
|
|
192
|
+
await this.assertRandomSortSupported();
|
|
193
|
+
}
|
|
188
194
|
const pipeline = query.buildPipeline(pagination);
|
|
189
195
|
pipeline.push({ $project: { _id: 1 } });
|
|
190
196
|
const res = await this.collection.aggregate(pipeline).toArray();
|
|
@@ -237,9 +243,9 @@ class FlongoCollection {
|
|
|
237
243
|
* @returns Promise resolving to array of documents
|
|
238
244
|
*/
|
|
239
245
|
async getSome(query, pagination) {
|
|
240
|
-
// Mirror getAll:
|
|
241
|
-
if (query?.
|
|
242
|
-
return this.
|
|
246
|
+
// Mirror getAll: computed sort keys route through the aggregation path.
|
|
247
|
+
if (query?.needsPipeline()) {
|
|
248
|
+
return this.getAllViaPipeline(query, pagination);
|
|
243
249
|
}
|
|
244
250
|
const mongodbQuery = query?.build();
|
|
245
251
|
const mongodbOptions = query?.buildOptions(pagination);
|
package/dist/flongoQuery.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export declare class FlongoQuery implements ICollectionQuery {
|
|
|
40
40
|
randomSortPosition?: number;
|
|
41
41
|
/** Internal computed field used to carry the per-document shuffle hash. */
|
|
42
42
|
static readonly SHUFFLE_FIELD = "__flongoShuffle";
|
|
43
|
+
/** Prefix of internal computed fields backing expression sort keys. */
|
|
44
|
+
static readonly EXPR_FIELD_PREFIX = "__flongoSortExpr";
|
|
43
45
|
/**
|
|
44
46
|
* Primary sort field, derived from `sorts[0]`.
|
|
45
47
|
* @deprecated Retained for backward compatibility; use `sorts`.
|
|
@@ -279,6 +281,48 @@ export declare class FlongoQuery implements ICollectionQuery {
|
|
|
279
281
|
* @returns This query instance for chaining
|
|
280
282
|
*/
|
|
281
283
|
thenBy(field?: string, direction?: SortDirection): FlongoQuery;
|
|
284
|
+
/**
|
|
285
|
+
* Sets the primary sort to a computed aggregation expression, resetting any
|
|
286
|
+
* previously configured sort keys (the expression analog of `orderBy`).
|
|
287
|
+
*
|
|
288
|
+
* The expression is materialized into an internal field via `$addFields`, so a
|
|
289
|
+
* query carrying an expression sort executes via an aggregation pipeline
|
|
290
|
+
* instead of `find` (same execution path as `orderByRandom`, but with no
|
|
291
|
+
* server-version requirement). The internal field never leaks to callers.
|
|
292
|
+
*
|
|
293
|
+
* The canonical use is normalizing a field before sorting on it. MongoDB's
|
|
294
|
+
* BSON type order ranks Boolean above Null/missing, so
|
|
295
|
+
* `orderBy('featured', Descending)` puts an explicit `featured: false` *above*
|
|
296
|
+
* documents missing the field entirely. Normalizing fixes that:
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* // Pin featured docs on top; explicit false and missing tie.
|
|
300
|
+
* new FlongoQuery()
|
|
301
|
+
* .orderByExpr({ $eq: ['$featured', true] }, SortDirection.Descending)
|
|
302
|
+
* .thenBy('_id', SortDirection.Ascending);
|
|
303
|
+
*
|
|
304
|
+
* Composes with `thenBy`/`orderByRandom` exactly like a field sort key: keys
|
|
305
|
+
* apply in call order.
|
|
306
|
+
*
|
|
307
|
+
* @param expr - MongoDB aggregation expression to sort by
|
|
308
|
+
* @param direction - Sort direction (ascending or descending)
|
|
309
|
+
* @returns This query instance for chaining
|
|
310
|
+
*/
|
|
311
|
+
orderByExpr(expr: Document, direction: SortDirection): FlongoQuery;
|
|
312
|
+
/**
|
|
313
|
+
* Appends a computed aggregation expression as a tiebreaker sort key (the
|
|
314
|
+
* expression analog of `thenBy`). See `orderByExpr` for semantics.
|
|
315
|
+
*
|
|
316
|
+
* @param expr - MongoDB aggregation expression to sort by
|
|
317
|
+
* @param direction - Sort direction (ascending or descending)
|
|
318
|
+
* @returns This query instance for chaining
|
|
319
|
+
*/
|
|
320
|
+
thenByExpr(expr: Document, direction: SortDirection): FlongoQuery;
|
|
321
|
+
/**
|
|
322
|
+
* Whether this query carries any expression sort keys (`orderByExpr`/
|
|
323
|
+
* `thenByExpr`) and must therefore execute via an aggregation pipeline.
|
|
324
|
+
*/
|
|
325
|
+
hasExprSort(): boolean;
|
|
282
326
|
/**
|
|
283
327
|
* Adds a deterministic, seeded random sort. Given the same seed the produced
|
|
284
328
|
* order is identical on every call, so `$skip`/`$limit` pagination stays
|
|
@@ -316,6 +360,12 @@ export declare class FlongoQuery implements ICollectionQuery {
|
|
|
316
360
|
* pipeline (`buildPipeline`) rather than the normal `find` path.
|
|
317
361
|
*/
|
|
318
362
|
hasRandomSort(): boolean;
|
|
363
|
+
/**
|
|
364
|
+
* Whether this query must execute via an aggregation pipeline
|
|
365
|
+
* (`buildPipeline`) rather than the normal `find` path — true when it carries
|
|
366
|
+
* a random sort and/or any expression sort keys.
|
|
367
|
+
*/
|
|
368
|
+
needsPipeline(): boolean;
|
|
319
369
|
/**
|
|
320
370
|
* Adds a sub-query with AND logic
|
|
321
371
|
* @param query - Sub-query to combine with AND logic
|
|
@@ -343,20 +393,22 @@ export declare class FlongoQuery implements ICollectionQuery {
|
|
|
343
393
|
/**
|
|
344
394
|
* Builds the ordered `$sort` specification for the aggregation path, weaving
|
|
345
395
|
* the internal shuffle field in at its call position amongst the explicit
|
|
346
|
-
* sort keys
|
|
347
|
-
* a
|
|
396
|
+
* sort keys (expression keys sort by their computed internal field) and always
|
|
397
|
+
* appending `_id` as a final tiebreaker. Only used on the pipeline path.
|
|
348
398
|
* @private
|
|
349
399
|
*/
|
|
350
400
|
private buildAggregationSort;
|
|
351
401
|
/**
|
|
352
402
|
* Compiles this query into an aggregation pipeline that materializes the same
|
|
353
|
-
* results as `find` would, but ordered by
|
|
354
|
-
* (
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
403
|
+
* results as `find` would, but ordered by computed sort values — the
|
|
404
|
+
* deterministic per-document shuffle (`orderByRandom`) and/or expression sort
|
|
405
|
+
* keys (`orderByExpr`/`thenByExpr`). The `$match` stage is exactly what `find`
|
|
406
|
+
* receives today (`build()`), so all filter logic and indexes carry over
|
|
407
|
+
* unchanged; `$skip`/`$limit` are applied *after* the deterministic sort,
|
|
408
|
+
* which is what makes paging stable.
|
|
358
409
|
*
|
|
359
|
-
* **
|
|
410
|
+
* **A random sort requires MongoDB server >= 8.0** (uses `$toHashedIndexKey`);
|
|
411
|
+
* expression sorts have no version requirement.
|
|
360
412
|
*
|
|
361
413
|
* @param pagination - Optional pagination settings
|
|
362
414
|
* @returns MongoDB aggregation pipeline stages
|
package/dist/flongoQuery.js
CHANGED
|
@@ -414,6 +414,57 @@ class FlongoQuery {
|
|
|
414
414
|
}
|
|
415
415
|
return this;
|
|
416
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Sets the primary sort to a computed aggregation expression, resetting any
|
|
419
|
+
* previously configured sort keys (the expression analog of `orderBy`).
|
|
420
|
+
*
|
|
421
|
+
* The expression is materialized into an internal field via `$addFields`, so a
|
|
422
|
+
* query carrying an expression sort executes via an aggregation pipeline
|
|
423
|
+
* instead of `find` (same execution path as `orderByRandom`, but with no
|
|
424
|
+
* server-version requirement). The internal field never leaks to callers.
|
|
425
|
+
*
|
|
426
|
+
* The canonical use is normalizing a field before sorting on it. MongoDB's
|
|
427
|
+
* BSON type order ranks Boolean above Null/missing, so
|
|
428
|
+
* `orderBy('featured', Descending)` puts an explicit `featured: false` *above*
|
|
429
|
+
* documents missing the field entirely. Normalizing fixes that:
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* // Pin featured docs on top; explicit false and missing tie.
|
|
433
|
+
* new FlongoQuery()
|
|
434
|
+
* .orderByExpr({ $eq: ['$featured', true] }, SortDirection.Descending)
|
|
435
|
+
* .thenBy('_id', SortDirection.Ascending);
|
|
436
|
+
*
|
|
437
|
+
* Composes with `thenBy`/`orderByRandom` exactly like a field sort key: keys
|
|
438
|
+
* apply in call order.
|
|
439
|
+
*
|
|
440
|
+
* @param expr - MongoDB aggregation expression to sort by
|
|
441
|
+
* @param direction - Sort direction (ascending or descending)
|
|
442
|
+
* @returns This query instance for chaining
|
|
443
|
+
*/
|
|
444
|
+
orderByExpr(expr, direction) {
|
|
445
|
+
this.sorts = [];
|
|
446
|
+
return this.thenByExpr(expr, direction);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Appends a computed aggregation expression as a tiebreaker sort key (the
|
|
450
|
+
* expression analog of `thenBy`). See `orderByExpr` for semantics.
|
|
451
|
+
*
|
|
452
|
+
* @param expr - MongoDB aggregation expression to sort by
|
|
453
|
+
* @param direction - Sort direction (ascending or descending)
|
|
454
|
+
* @returns This query instance for chaining
|
|
455
|
+
*/
|
|
456
|
+
thenByExpr(expr, direction) {
|
|
457
|
+
const index = this.sorts.filter((s) => s.expr !== undefined).length;
|
|
458
|
+
this.sorts.push({ field: `${FlongoQuery.EXPR_FIELD_PREFIX}${index}`, direction, expr });
|
|
459
|
+
return this;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Whether this query carries any expression sort keys (`orderByExpr`/
|
|
463
|
+
* `thenByExpr`) and must therefore execute via an aggregation pipeline.
|
|
464
|
+
*/
|
|
465
|
+
hasExprSort() {
|
|
466
|
+
return this.sorts.some((s) => s.expr !== undefined);
|
|
467
|
+
}
|
|
417
468
|
/**
|
|
418
469
|
* Adds a deterministic, seeded random sort. Given the same seed the produced
|
|
419
470
|
* order is identical on every call, so `$skip`/`$limit` pagination stays
|
|
@@ -457,6 +508,14 @@ class FlongoQuery {
|
|
|
457
508
|
hasRandomSort() {
|
|
458
509
|
return this.randomSeed !== undefined;
|
|
459
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Whether this query must execute via an aggregation pipeline
|
|
513
|
+
* (`buildPipeline`) rather than the normal `find` path — true when it carries
|
|
514
|
+
* a random sort and/or any expression sort keys.
|
|
515
|
+
*/
|
|
516
|
+
needsPipeline() {
|
|
517
|
+
return this.hasRandomSort() || this.hasExprSort();
|
|
518
|
+
}
|
|
460
519
|
// ===========================================
|
|
461
520
|
// LOGICAL OPERATORS
|
|
462
521
|
// ===========================================
|
|
@@ -587,8 +646,10 @@ class FlongoQuery {
|
|
|
587
646
|
// Add sorting if specified. Iterate sort keys in order so MongoDB honors the
|
|
588
647
|
// insertion order of the resulting `sort` object's keys (primary first, then
|
|
589
648
|
// tiebreakers). Entries without a direction are skipped, preserving the prior
|
|
590
|
-
// behavior where a field set with no direction produced no sort.
|
|
591
|
-
|
|
649
|
+
// behavior where a field set with no direction produced no sort. Expression
|
|
650
|
+
// keys are computed fields that only exist on the pipeline path, so they are
|
|
651
|
+
// excluded here rather than sorting on a field no document has.
|
|
652
|
+
const sortKeys = this.sorts.filter((s) => s.field && s.direction && s.expr === undefined);
|
|
592
653
|
if (sortKeys.length) {
|
|
593
654
|
const sort = {};
|
|
594
655
|
for (const s of sortKeys) {
|
|
@@ -601,8 +662,8 @@ class FlongoQuery {
|
|
|
601
662
|
/**
|
|
602
663
|
* Builds the ordered `$sort` specification for the aggregation path, weaving
|
|
603
664
|
* the internal shuffle field in at its call position amongst the explicit
|
|
604
|
-
* sort keys
|
|
605
|
-
* a
|
|
665
|
+
* sort keys (expression keys sort by their computed internal field) and always
|
|
666
|
+
* appending `_id` as a final tiebreaker. Only used on the pipeline path.
|
|
606
667
|
* @private
|
|
607
668
|
*/
|
|
608
669
|
buildAggregationSort() {
|
|
@@ -632,42 +693,60 @@ class FlongoQuery {
|
|
|
632
693
|
}
|
|
633
694
|
/**
|
|
634
695
|
* Compiles this query into an aggregation pipeline that materializes the same
|
|
635
|
-
* results as `find` would, but ordered by
|
|
636
|
-
* (
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
696
|
+
* results as `find` would, but ordered by computed sort values — the
|
|
697
|
+
* deterministic per-document shuffle (`orderByRandom`) and/or expression sort
|
|
698
|
+
* keys (`orderByExpr`/`thenByExpr`). The `$match` stage is exactly what `find`
|
|
699
|
+
* receives today (`build()`), so all filter logic and indexes carry over
|
|
700
|
+
* unchanged; `$skip`/`$limit` are applied *after* the deterministic sort,
|
|
701
|
+
* which is what makes paging stable.
|
|
640
702
|
*
|
|
641
|
-
* **
|
|
703
|
+
* **A random sort requires MongoDB server >= 8.0** (uses `$toHashedIndexKey`);
|
|
704
|
+
* expression sorts have no version requirement.
|
|
642
705
|
*
|
|
643
706
|
* @param pagination - Optional pagination settings
|
|
644
707
|
* @returns MongoDB aggregation pipeline stages
|
|
645
708
|
*/
|
|
646
709
|
buildPipeline(pagination) {
|
|
647
710
|
const pipeline = [{ $match: this.build() }];
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
[FlongoQuery.SHUFFLE_FIELD]: {
|
|
654
|
-
$toHashedIndexKey: { $concat: [{ $toString: "$_id" }, ":", this.randomSeed] }
|
|
655
|
-
}
|
|
711
|
+
// Materialize every computed sort value in one $addFields stage.
|
|
712
|
+
const computed = {};
|
|
713
|
+
for (const s of this.sorts) {
|
|
714
|
+
if (s.expr !== undefined) {
|
|
715
|
+
computed[s.field] = s.expr;
|
|
656
716
|
}
|
|
657
|
-
}
|
|
717
|
+
}
|
|
718
|
+
if (this.randomSeed !== undefined) {
|
|
719
|
+
// Hash the full `_id` string mixed with the seed. Hashing the whole _id
|
|
720
|
+
// decorrelates the order from the ObjectId timestamp prefix, so newer docs
|
|
721
|
+
// aren't biased toward one end of the shuffle.
|
|
722
|
+
computed[FlongoQuery.SHUFFLE_FIELD] = {
|
|
723
|
+
$toHashedIndexKey: { $concat: [{ $toString: "$_id" }, ":", this.randomSeed] }
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
if (Object.keys(computed).length > 0) {
|
|
727
|
+
pipeline.push({ $addFields: computed });
|
|
728
|
+
}
|
|
658
729
|
pipeline.push({ $sort: this.buildAggregationSort() });
|
|
659
730
|
if (pagination) {
|
|
660
731
|
pipeline.push({ $skip: pagination.offset });
|
|
661
732
|
pipeline.push({ $limit: pagination.count });
|
|
662
733
|
}
|
|
663
|
-
// Don't leak the internal
|
|
664
|
-
|
|
734
|
+
// Don't leak the internal computed fields to callers.
|
|
735
|
+
if (Object.keys(computed).length > 0) {
|
|
736
|
+
const exclusions = {};
|
|
737
|
+
for (const field of Object.keys(computed)) {
|
|
738
|
+
exclusions[field] = 0;
|
|
739
|
+
}
|
|
740
|
+
pipeline.push({ $project: exclusions });
|
|
741
|
+
}
|
|
665
742
|
return pipeline;
|
|
666
743
|
}
|
|
667
744
|
}
|
|
668
745
|
exports.FlongoQuery = FlongoQuery;
|
|
669
746
|
/** Internal computed field used to carry the per-document shuffle hash. */
|
|
670
747
|
FlongoQuery.SHUFFLE_FIELD = "__flongoShuffle";
|
|
748
|
+
/** Prefix of internal computed fields backing expression sort keys. */
|
|
749
|
+
FlongoQuery.EXPR_FIELD_PREFIX = "__flongoSortExpr";
|
|
671
750
|
/**
|
|
672
751
|
* FlongoQueryBuilder provides a builder pattern wrapper around FlongoQuery
|
|
673
752
|
* This can be useful for more complex query construction scenarios
|
package/dist/types.d.ts
CHANGED
|
@@ -62,6 +62,12 @@ export declare class ColExpression {
|
|
|
62
62
|
export interface Sort {
|
|
63
63
|
field: string;
|
|
64
64
|
direction?: SortDirection;
|
|
65
|
+
/**
|
|
66
|
+
* Aggregation expression this sort key is computed from (set by
|
|
67
|
+
* `orderByExpr`/`thenByExpr`). When present, `field` is an internal
|
|
68
|
+
* generated name materialized via `$addFields` on the pipeline path.
|
|
69
|
+
*/
|
|
70
|
+
expr?: unknown;
|
|
65
71
|
}
|
|
66
72
|
export type ICollectionQuery = {
|
|
67
73
|
expressions: ColExpression[];
|