flongo 2.1.0 → 2.2.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 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
@@ -98,6 +98,32 @@ export declare class FlongoCollection<T> {
98
98
  * @private
99
99
  */
100
100
  private getAllRandom;
101
+ /**
102
+ * Retrieves only the `_id`s of matching documents, as strings (the same
103
+ * normalization `toEntity` applies). Uses a server-side projection so full
104
+ * documents never leave the server — ideal for semi-joins ("which of these N
105
+ * ids match this filter?") and other membership checks where hydrating whole
106
+ * documents is wasted work.
107
+ *
108
+ * Honors the same query and pagination semantics as `getAll`, including sort
109
+ * clauses (ordered ids are useful for id-first pagination).
110
+ *
111
+ * Example usage:
112
+ * ```typescript
113
+ * // Semi-join: of these candidate stayIds, which are publicly visible?
114
+ * const visibleIds = await stays.getIds(
115
+ * new FlongoQuery()
116
+ * .where('_id').in(candidateStayIds)
117
+ * .and('status').eq('Accepted')
118
+ * .and('enable').eq(true)
119
+ * );
120
+ * ```
121
+ *
122
+ * @param query - Optional FlongoQuery for filtering
123
+ * @param pagination - Optional pagination settings
124
+ * @returns Promise resolving to the matching document ids as strings
125
+ */
126
+ getIds(query?: FlongoQuery, pagination?: Pagination): Promise<string[]>;
101
127
  /**
102
128
  * Runs a raw aggregation pipeline against this collection and returns the
103
129
  * documents with their `_id` normalized to a string (Entity form). A low-level
@@ -155,6 +155,49 @@ class FlongoCollection {
155
155
  const res = await this.collection.aggregate(pipeline).toArray();
156
156
  return res.map((d) => this.toEntity(d));
157
157
  }
158
+ /**
159
+ * Retrieves only the `_id`s of matching documents, as strings (the same
160
+ * normalization `toEntity` applies). Uses a server-side projection so full
161
+ * documents never leave the server — ideal for semi-joins ("which of these N
162
+ * ids match this filter?") and other membership checks where hydrating whole
163
+ * documents is wasted work.
164
+ *
165
+ * Honors the same query and pagination semantics as `getAll`, including sort
166
+ * clauses (ordered ids are useful for id-first pagination).
167
+ *
168
+ * Example usage:
169
+ * ```typescript
170
+ * // Semi-join: of these candidate stayIds, which are publicly visible?
171
+ * const visibleIds = await stays.getIds(
172
+ * new FlongoQuery()
173
+ * .where('_id').in(candidateStayIds)
174
+ * .and('status').eq('Accepted')
175
+ * .and('enable').eq(true)
176
+ * );
177
+ * ```
178
+ *
179
+ * @param query - Optional FlongoQuery for filtering
180
+ * @param pagination - Optional pagination settings
181
+ * @returns Promise resolving to the matching document ids as strings
182
+ */
183
+ async getIds(query, pagination) {
184
+ // Mirror getAll: a seeded random sort must execute via the aggregation
185
+ // pipeline. Only the id column is materialized on either path.
186
+ if (query?.hasRandomSort()) {
187
+ await this.assertRandomSortSupported();
188
+ const pipeline = query.buildPipeline(pagination);
189
+ pipeline.push({ $project: { _id: 1 } });
190
+ const res = await this.collection.aggregate(pipeline).toArray();
191
+ return res.map((d) => String(d._id));
192
+ }
193
+ const mongodbQuery = query?.build() ?? {};
194
+ const mongodbOptions = {
195
+ ...(query?.buildOptions(pagination) ?? new flongoQuery_1.FlongoQuery().buildOptions(pagination)),
196
+ projection: { _id: 1 }
197
+ };
198
+ const res = await this.collection.find(mongodbQuery, mongodbOptions).toArray();
199
+ return res.map((d) => String(d._id));
200
+ }
158
201
  /**
159
202
  * Runs a raw aggregation pipeline against this collection and returns the
160
203
  * documents with their `_id` normalized to a string (Entity form). A low-level
@@ -492,25 +492,24 @@ class FlongoQuery {
492
492
  // Process main expressions
493
493
  if (this.expressions) {
494
494
  for (const expression of this.expressions) {
495
- // Special handling for _id field - convert strings to ObjectIds
495
+ // Build query object for this clause
496
+ let fieldValue;
496
497
  if (expression.key === "_id") {
498
+ // _id keeps its string -> ObjectId conversion but composes with the
499
+ // other clauses like any field (it used to short-circuit the build,
500
+ // silently discarding every co-existing clause).
497
501
  if (Array.isArray(expression.val)) {
498
502
  // Multiple IDs: {_id: {$in: [ObjectId(...), ObjectId(...)]}}
499
- mongodbQuery = {
500
- _id: {
501
- [expression.op ?? "$in"]: expression.val.map((element) => new mongodb_1.ObjectId(element))
502
- }
503
+ fieldValue = {
504
+ [expression.op ?? "$in"]: expression.val.map((element) => new mongodb_1.ObjectId(element))
503
505
  };
504
506
  }
505
507
  else {
506
508
  // Single ID: {_id: ObjectId(...)}
507
- mongodbQuery = { _id: new mongodb_1.ObjectId(expression.val) };
509
+ fieldValue = new mongodb_1.ObjectId(expression.val);
508
510
  }
509
- break; // _id queries are typically exclusive
510
511
  }
511
- // Build query object for non-_id fields
512
- let fieldValue;
513
- if (!expression.op) {
512
+ else if (!expression.op) {
514
513
  // Direct value assignment for simple equality (arrContains)
515
514
  fieldValue = expression.val;
516
515
  }
@@ -528,10 +527,14 @@ class FlongoQuery {
528
527
  // Standard operator
529
528
  fieldValue = { [expression.op]: expression.val };
530
529
  }
531
- // Merge operators for the same field (e.g., range queries)
530
+ // Merge operators for the same field (e.g., range queries).
531
+ // ObjectId instances are direct values, not operator maps — spreading
532
+ // one would destroy it, so they always assign.
532
533
  if (mongodbQuery[expression.key] &&
533
534
  typeof mongodbQuery[expression.key] === "object" &&
534
- typeof fieldValue === "object") {
535
+ !(mongodbQuery[expression.key] instanceof mongodb_1.ObjectId) &&
536
+ typeof fieldValue === "object" &&
537
+ !(fieldValue instanceof mongodb_1.ObjectId)) {
535
538
  mongodbQuery[expression.key] = { ...mongodbQuery[expression.key], ...fieldValue };
536
539
  }
537
540
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flongo",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Firestore-like fluent query API for MongoDB",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",