flongo 2.1.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/README.md +46 -0
- package/dist/flongoCollection.d.ts +31 -4
- package/dist/flongoCollection.js +61 -12
- package/dist/flongoQuery.d.ts +60 -8
- package/dist/flongoQuery.js +115 -33
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,6 +168,32 @@ const topRated = await products.aggregate([
|
|
|
168
168
|
]);
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
+
#### Id projection & semi-joins (`getIds`)
|
|
172
|
+
|
|
173
|
+
When you only need to know **which** documents match — not their contents —
|
|
174
|
+
`getIds(query, pagination?)` returns matching `_id`s as strings. It uses a
|
|
175
|
+
server-side projection, so full documents never leave the server. This is the
|
|
176
|
+
natural fit for a semi-join: "of these N candidate ids, which pass this filter?"
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// Of these candidate stayIds, which are publicly visible?
|
|
180
|
+
const visibleIds = await stays.getIds(
|
|
181
|
+
new FlongoQuery()
|
|
182
|
+
.where('_id').in(candidateStayIds)
|
|
183
|
+
.and('status').eq('Accepted')
|
|
184
|
+
.and('enable').eq(true)
|
|
185
|
+
);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`getIds` honors the same query and pagination semantics as `getAll`, including
|
|
189
|
+
sort clauses — ordered ids are useful for id-first pagination.
|
|
190
|
+
|
|
191
|
+
> **Note (2.2.0):** inline `_id` clauses now compose with other field clauses,
|
|
192
|
+
> as the example above relies on. Previously an `_id` clause caused every other
|
|
193
|
+
> clause in the same clause list to be silently dropped, and filters had to be
|
|
194
|
+
> attached via `.andQuery(new FlongoQuery().where('_id').in(ids))` — that
|
|
195
|
+
> pattern still works unchanged.
|
|
196
|
+
|
|
171
197
|
### Collection Operations
|
|
172
198
|
|
|
173
199
|
```typescript
|
|
@@ -184,6 +210,7 @@ const user = await collection.create({
|
|
|
184
210
|
// Read
|
|
185
211
|
const user = await collection.get('user123');
|
|
186
212
|
const users = await collection.getAll(query);
|
|
213
|
+
const ids = await collection.getIds(query); // matching _ids only (server-side projection)
|
|
187
214
|
const first = await collection.getFirst(query);
|
|
188
215
|
const count = await collection.count(query);
|
|
189
216
|
const exists = await collection.exists(query);
|
|
@@ -502,6 +529,25 @@ const users = await collection.getAll(
|
|
|
502
529
|
);
|
|
503
530
|
```
|
|
504
531
|
|
|
532
|
+
## Changelog
|
|
533
|
+
|
|
534
|
+
### 2.2.0
|
|
535
|
+
|
|
536
|
+
- **`FlongoCollection.getIds(query?, pagination?)`** — returns matching `_id`s
|
|
537
|
+
as strings via a server-side projection (`{_id: 1}` on `find`, `$project` on
|
|
538
|
+
the aggregation path), so full documents never cross the wire. Honors the
|
|
539
|
+
same query, sort, and pagination semantics as `getAll`.
|
|
540
|
+
- **Inline `_id` clauses now compose in `build()`.** Previously an `_id`
|
|
541
|
+
expression in the main clause list short-circuited the build and silently
|
|
542
|
+
discarded every other clause; combining `_id` with other filters required the
|
|
543
|
+
`.andQuery(new FlongoQuery().where('_id').in(ids))` workaround. Now `_id`
|
|
544
|
+
composes like any other field (string → ObjectId conversion is preserved for
|
|
545
|
+
both single values and `$in` arrays). Queries with **only** an `_id` clause
|
|
546
|
+
build identical output to before, and the `andQuery` pattern still works.
|
|
547
|
+
*Behavior note:* if a query combined `_id` with other clauses **and** relied
|
|
548
|
+
on those other clauses being ignored, it now returns only documents matching
|
|
549
|
+
all clauses — i.e., what the query literally says.
|
|
550
|
+
|
|
505
551
|
## License
|
|
506
552
|
|
|
507
553
|
MIT
|
|
@@ -92,12 +92,39 @@ 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;
|
|
102
|
+
/**
|
|
103
|
+
* Retrieves only the `_id`s of matching documents, as strings (the same
|
|
104
|
+
* normalization `toEntity` applies). Uses a server-side projection so full
|
|
105
|
+
* documents never leave the server — ideal for semi-joins ("which of these N
|
|
106
|
+
* ids match this filter?") and other membership checks where hydrating whole
|
|
107
|
+
* documents is wasted work.
|
|
108
|
+
*
|
|
109
|
+
* Honors the same query and pagination semantics as `getAll`, including sort
|
|
110
|
+
* clauses (ordered ids are useful for id-first pagination).
|
|
111
|
+
*
|
|
112
|
+
* Example usage:
|
|
113
|
+
* ```typescript
|
|
114
|
+
* // Semi-join: of these candidate stayIds, which are publicly visible?
|
|
115
|
+
* const visibleIds = await stays.getIds(
|
|
116
|
+
* new FlongoQuery()
|
|
117
|
+
* .where('_id').in(candidateStayIds)
|
|
118
|
+
* .and('status').eq('Accepted')
|
|
119
|
+
* .and('enable').eq(true)
|
|
120
|
+
* );
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* @param query - Optional FlongoQuery for filtering
|
|
124
|
+
* @param pagination - Optional pagination settings
|
|
125
|
+
* @returns Promise resolving to the matching document ids as strings
|
|
126
|
+
*/
|
|
127
|
+
getIds(query?: FlongoQuery, pagination?: Pagination): Promise<string[]>;
|
|
101
128
|
/**
|
|
102
129
|
* Runs a raw aggregation pipeline against this collection and returns the
|
|
103
130
|
* documents with their `_id` normalized to a string (Entity form). A low-level
|
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,17 +145,65 @@ 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));
|
|
157
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Retrieves only the `_id`s of matching documents, as strings (the same
|
|
164
|
+
* normalization `toEntity` applies). Uses a server-side projection so full
|
|
165
|
+
* documents never leave the server — ideal for semi-joins ("which of these N
|
|
166
|
+
* ids match this filter?") and other membership checks where hydrating whole
|
|
167
|
+
* documents is wasted work.
|
|
168
|
+
*
|
|
169
|
+
* Honors the same query and pagination semantics as `getAll`, including sort
|
|
170
|
+
* clauses (ordered ids are useful for id-first pagination).
|
|
171
|
+
*
|
|
172
|
+
* Example usage:
|
|
173
|
+
* ```typescript
|
|
174
|
+
* // Semi-join: of these candidate stayIds, which are publicly visible?
|
|
175
|
+
* const visibleIds = await stays.getIds(
|
|
176
|
+
* new FlongoQuery()
|
|
177
|
+
* .where('_id').in(candidateStayIds)
|
|
178
|
+
* .and('status').eq('Accepted')
|
|
179
|
+
* .and('enable').eq(true)
|
|
180
|
+
* );
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* @param query - Optional FlongoQuery for filtering
|
|
184
|
+
* @param pagination - Optional pagination settings
|
|
185
|
+
* @returns Promise resolving to the matching document ids as strings
|
|
186
|
+
*/
|
|
187
|
+
async getIds(query, pagination) {
|
|
188
|
+
// Mirror getAll: computed sort keys must execute via the aggregation
|
|
189
|
+
// pipeline. Only the id column is materialized on either path.
|
|
190
|
+
if (query?.needsPipeline()) {
|
|
191
|
+
if (query.hasRandomSort()) {
|
|
192
|
+
await this.assertRandomSortSupported();
|
|
193
|
+
}
|
|
194
|
+
const pipeline = query.buildPipeline(pagination);
|
|
195
|
+
pipeline.push({ $project: { _id: 1 } });
|
|
196
|
+
const res = await this.collection.aggregate(pipeline).toArray();
|
|
197
|
+
return res.map((d) => String(d._id));
|
|
198
|
+
}
|
|
199
|
+
const mongodbQuery = query?.build() ?? {};
|
|
200
|
+
const mongodbOptions = {
|
|
201
|
+
...(query?.buildOptions(pagination) ?? new flongoQuery_1.FlongoQuery().buildOptions(pagination)),
|
|
202
|
+
projection: { _id: 1 }
|
|
203
|
+
};
|
|
204
|
+
const res = await this.collection.find(mongodbQuery, mongodbOptions).toArray();
|
|
205
|
+
return res.map((d) => String(d._id));
|
|
206
|
+
}
|
|
158
207
|
/**
|
|
159
208
|
* Runs a raw aggregation pipeline against this collection and returns the
|
|
160
209
|
* documents with their `_id` normalized to a string (Entity form). A low-level
|
|
@@ -194,9 +243,9 @@ class FlongoCollection {
|
|
|
194
243
|
* @returns Promise resolving to array of documents
|
|
195
244
|
*/
|
|
196
245
|
async getSome(query, pagination) {
|
|
197
|
-
// Mirror getAll:
|
|
198
|
-
if (query?.
|
|
199
|
-
return this.
|
|
246
|
+
// Mirror getAll: computed sort keys route through the aggregation path.
|
|
247
|
+
if (query?.needsPipeline()) {
|
|
248
|
+
return this.getAllViaPipeline(query, pagination);
|
|
200
249
|
}
|
|
201
250
|
const mongodbQuery = query?.build();
|
|
202
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
|
// ===========================================
|
|
@@ -492,25 +551,24 @@ class FlongoQuery {
|
|
|
492
551
|
// Process main expressions
|
|
493
552
|
if (this.expressions) {
|
|
494
553
|
for (const expression of this.expressions) {
|
|
495
|
-
//
|
|
554
|
+
// Build query object for this clause
|
|
555
|
+
let fieldValue;
|
|
496
556
|
if (expression.key === "_id") {
|
|
557
|
+
// _id keeps its string -> ObjectId conversion but composes with the
|
|
558
|
+
// other clauses like any field (it used to short-circuit the build,
|
|
559
|
+
// silently discarding every co-existing clause).
|
|
497
560
|
if (Array.isArray(expression.val)) {
|
|
498
561
|
// Multiple IDs: {_id: {$in: [ObjectId(...), ObjectId(...)]}}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
[expression.op ?? "$in"]: expression.val.map((element) => new mongodb_1.ObjectId(element))
|
|
502
|
-
}
|
|
562
|
+
fieldValue = {
|
|
563
|
+
[expression.op ?? "$in"]: expression.val.map((element) => new mongodb_1.ObjectId(element))
|
|
503
564
|
};
|
|
504
565
|
}
|
|
505
566
|
else {
|
|
506
567
|
// Single ID: {_id: ObjectId(...)}
|
|
507
|
-
|
|
568
|
+
fieldValue = new mongodb_1.ObjectId(expression.val);
|
|
508
569
|
}
|
|
509
|
-
break; // _id queries are typically exclusive
|
|
510
570
|
}
|
|
511
|
-
|
|
512
|
-
let fieldValue;
|
|
513
|
-
if (!expression.op) {
|
|
571
|
+
else if (!expression.op) {
|
|
514
572
|
// Direct value assignment for simple equality (arrContains)
|
|
515
573
|
fieldValue = expression.val;
|
|
516
574
|
}
|
|
@@ -528,10 +586,14 @@ class FlongoQuery {
|
|
|
528
586
|
// Standard operator
|
|
529
587
|
fieldValue = { [expression.op]: expression.val };
|
|
530
588
|
}
|
|
531
|
-
// Merge operators for the same field (e.g., range queries)
|
|
589
|
+
// Merge operators for the same field (e.g., range queries).
|
|
590
|
+
// ObjectId instances are direct values, not operator maps — spreading
|
|
591
|
+
// one would destroy it, so they always assign.
|
|
532
592
|
if (mongodbQuery[expression.key] &&
|
|
533
593
|
typeof mongodbQuery[expression.key] === "object" &&
|
|
534
|
-
|
|
594
|
+
!(mongodbQuery[expression.key] instanceof mongodb_1.ObjectId) &&
|
|
595
|
+
typeof fieldValue === "object" &&
|
|
596
|
+
!(fieldValue instanceof mongodb_1.ObjectId)) {
|
|
535
597
|
mongodbQuery[expression.key] = { ...mongodbQuery[expression.key], ...fieldValue };
|
|
536
598
|
}
|
|
537
599
|
else {
|
|
@@ -584,8 +646,10 @@ class FlongoQuery {
|
|
|
584
646
|
// Add sorting if specified. Iterate sort keys in order so MongoDB honors the
|
|
585
647
|
// insertion order of the resulting `sort` object's keys (primary first, then
|
|
586
648
|
// tiebreakers). Entries without a direction are skipped, preserving the prior
|
|
587
|
-
// behavior where a field set with no direction produced no sort.
|
|
588
|
-
|
|
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);
|
|
589
653
|
if (sortKeys.length) {
|
|
590
654
|
const sort = {};
|
|
591
655
|
for (const s of sortKeys) {
|
|
@@ -598,8 +662,8 @@ class FlongoQuery {
|
|
|
598
662
|
/**
|
|
599
663
|
* Builds the ordered `$sort` specification for the aggregation path, weaving
|
|
600
664
|
* the internal shuffle field in at its call position amongst the explicit
|
|
601
|
-
* sort keys
|
|
602
|
-
* 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.
|
|
603
667
|
* @private
|
|
604
668
|
*/
|
|
605
669
|
buildAggregationSort() {
|
|
@@ -629,42 +693,60 @@ class FlongoQuery {
|
|
|
629
693
|
}
|
|
630
694
|
/**
|
|
631
695
|
* Compiles this query into an aggregation pipeline that materializes the same
|
|
632
|
-
* results as `find` would, but ordered by
|
|
633
|
-
* (
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
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.
|
|
637
702
|
*
|
|
638
|
-
* **
|
|
703
|
+
* **A random sort requires MongoDB server >= 8.0** (uses `$toHashedIndexKey`);
|
|
704
|
+
* expression sorts have no version requirement.
|
|
639
705
|
*
|
|
640
706
|
* @param pagination - Optional pagination settings
|
|
641
707
|
* @returns MongoDB aggregation pipeline stages
|
|
642
708
|
*/
|
|
643
709
|
buildPipeline(pagination) {
|
|
644
710
|
const pipeline = [{ $match: this.build() }];
|
|
645
|
-
//
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
[FlongoQuery.SHUFFLE_FIELD]: {
|
|
651
|
-
$toHashedIndexKey: { $concat: [{ $toString: "$_id" }, ":", this.randomSeed] }
|
|
652
|
-
}
|
|
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;
|
|
653
716
|
}
|
|
654
|
-
}
|
|
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
|
+
}
|
|
655
729
|
pipeline.push({ $sort: this.buildAggregationSort() });
|
|
656
730
|
if (pagination) {
|
|
657
731
|
pipeline.push({ $skip: pagination.offset });
|
|
658
732
|
pipeline.push({ $limit: pagination.count });
|
|
659
733
|
}
|
|
660
|
-
// Don't leak the internal
|
|
661
|
-
|
|
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
|
+
}
|
|
662
742
|
return pipeline;
|
|
663
743
|
}
|
|
664
744
|
}
|
|
665
745
|
exports.FlongoQuery = FlongoQuery;
|
|
666
746
|
/** Internal computed field used to carry the per-document shuffle hash. */
|
|
667
747
|
FlongoQuery.SHUFFLE_FIELD = "__flongoShuffle";
|
|
748
|
+
/** Prefix of internal computed fields backing expression sort keys. */
|
|
749
|
+
FlongoQuery.EXPR_FIELD_PREFIX = "__flongoSortExpr";
|
|
668
750
|
/**
|
|
669
751
|
* FlongoQueryBuilder provides a builder pattern wrapper around FlongoQuery
|
|
670
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[];
|