Skip to content

feat: add client.db.count() method for counting collection documents#97

Open
Nitin-kumar-yadav1307 wants to merge 7 commits intogeturbackend:mainfrom
Nitin-kumar-yadav1307:feat/db-count-method
Open

feat: add client.db.count() method for counting collection documents#97
Nitin-kumar-yadav1307 wants to merge 7 commits intogeturbackend:mainfrom
Nitin-kumar-yadav1307:feat/db-count-method

Conversation

@Nitin-kumar-yadav1307
Copy link
Copy Markdown
Contributor

@Nitin-kumar-yadav1307 Nitin-kumar-yadav1307 commented Apr 13, 2026

What's the problem?

There was no efficient way to get the number of documents in a collection. You had to do something like this:

const all = await client.db.getAll('products');
const count = all.length; // fetches everything just to count

This is wasteful especially when you have thousands of records.


What I changed

Backend (data.controller.js + queryEngine.js)

Added a check for ?count=true query param in the controller. When present, instead of fetching all documents, it runs countDocuments() and returns { count: number }. Also added a count() method to QueryEngine so filters still work properly when counting.

SDK (database.ts)

Added a count() method to DatabaseModule. It passes count=true to the API and just returns the number back. Used Omit<QueryParams, 'count'> on the params type so nobody accidentally passes count as a filter field.

Tests (database.test.ts)

Added 3 new tests:

  • Count all documents in a collection
  • Count with filters (e.g only active items)
  • Count returns 0 when nothing matches

Usage

const total = await client.db.count('products');
const active = await client.db.count('products', { filter: { status: 'active' } });

Tests

All 22 tests passing.

Closes #94.

Summary by CodeRabbit

  • New Features

    • Count API: retrieve the total number of records in a collection without fetching items.
    • SDK: new count method to request record counts directly from client code.
  • Tests

    • Added tests validating count behavior and query construction for count requests.
  • Chores

    • SDK package version bumped.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

Warning

Rate limit exceeded

@Nitin-kumar-yadav1307 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 55 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 22 minutes and 55 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eceed723-24be-4b16-a7a7-3aaf7691256f

📥 Commits

Reviewing files that changed from the base of the PR and between 99b31f7 and 29cc38a.

📒 Files selected for processing (5)
  • apps/public-api/src/controllers/data.controller.js
  • packages/common/src/utils/queryEngine.js
  • sdks/urbackend-sdk/package.json
  • sdks/urbackend-sdk/src/modules/database.ts
  • sdks/urbackend-sdk/tests/database.test.ts
📝 Walkthrough

Walkthrough

The PR adds a backend early-return for count=true, extends QueryEngine with _buildMongoQuery(excludeCount) and count(), adds an SDK DatabaseModule.count() that requests /api/data/{collection}?count=true, and includes unit tests for the SDK count method.

Changes

Cohort / File(s) Summary
Backend controller
apps/public-api/src/controllers/data.controller.js
Added early-return branch when req.query.count === 'true' that builds/merges a Mongo filter and returns { count } instead of document list.
Query engine
packages/common/src/utils/queryEngine.js
Introduced _buildMongoQuery(excludeCount = false) to construct Mongo filters (removes pagination/sort/projection/population and count when requested) and count() to set this.query to countDocuments(...); filter() now uses _buildMongoQuery().
SDK Database API
sdks/urbackend-sdk/src/modules/database.ts
Added count(collection: string, params?: Omit<QueryParams,'count'>): Promise<number> which forces count=true in the query string, calls GET /api/data/{collection}, and returns the numeric count.
SDK tests & metadata
sdks/urbackend-sdk/tests/database.test.ts, sdks/urbackend-sdk/package.json
Added three Vitest cases stubbing fetch to validate returned counts and query-string encoding; bumped package version 0.2.20.2.7.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant SDK as DatabaseModule
    participant Backend as DataController
    participant Engine as QueryEngine
    participant DB as MongoDB

    Client->>SDK: count('products', {filter:{status:'active'}})
    SDK->>SDK: build query string (count=true & filter)
    SDK->>Backend: GET /api/data/products?count=true&status=active
    Backend->>Backend: detect count=true
    Backend->>Engine: QueryEngine(req.query).count()
    Engine->>Engine: _buildMongoQuery(excludeCount=true)
    Engine->>DB: countDocuments(mongoQuery)
    DB-->>Engine: N
    Engine-->>Backend: N
    Backend-->>SDK: { success:true, data:{count: N } }
    SDK-->>Client: Promise<number> (N)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through queries, small and spry,
Counting carrots with a watchful eye,
Backend hums, SDK sings true,
One request, one number—just for you,
A tiny hop, a tidy view ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature addition: a new client.db.count() method for counting collection documents, which is the primary objective of this PR.
Linked Issues check ✅ Passed All coding requirements from issue #94 are met: backend detects count=true and returns count; QueryEngine has count() method; SDK DatabaseModule has count() with proper typing; and Vitest tests verify functionality.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #94 objectives. Package version bump (0.2.2 → 0.2.7) is in-scope as requested by reviewer; no unrelated changes detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/public-api/src/controllers/data.controller.js (1)

150-153: ⚠️ Potential issue | 🟡 Minor

Error response exposes raw MongoDB errors to client.

The error handler returns err.message directly, which may leak internal database details. Per coding guidelines, use AppError class and avoid exposing MongoDB errors.

🛡️ Proposed fix
   } catch (err) {
     console.error(err);
-    res.status(500).json({ error: err.message });
+    res.status(500).json({ success: false, data: {}, message: 'Failed to retrieve data' });
   }

As per coding guidelines: "Use AppError class for errors — never raw throw, never expose MongoDB errors to client."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/public-api/src/controllers/data.controller.js` around lines 150 - 153,
The catch block currently returns err.message to the client (leaking DB
internals); instead log the raw error internally (console.error or your logger)
and return or forward a sanitized AppError to the middleware—e.g., replace
res.status(500).json({ error: err.message }) with creating/throwing/next-ing a
new AppError('Internal server error', 500) (or res.status(500).json({ error:
'Internal server error' })) so the client never receives raw MongoDB messages
while the original err remains logged for debugging; update the catch in the
controller handler where console.error(err) appears to use AppError and not
expose err.message.
🧹 Nitpick comments (1)
packages/common/src/utils/queryEngine.js (1)

75-101: Duplicated filter-building logic — consider extracting a shared helper.

The operator translation logic (lines 80-97) duplicates the same code from filter() (lines 12-29). Extract this into a private method to improve maintainability.

Also note: the controller attempts to call .getFilter() on the query returned by count(), but countDocuments() doesn't expose that method — this breaks RLS filter merging (see comment in data.controller.js).

♻️ Proposed refactor to extract shared logic
+    _buildMongoQuery(excludeCount = false) {
+        const queryObj = { ...this.queryString };
+        const excludedFields = ['page', 'sort', 'limit', 'fields', 'populate', 'expand'];
+        if (excludeCount) excludedFields.push('count');
+        excludedFields.forEach(el => delete queryObj[el]);
+
+        const mongoQuery = {};
+        for (const key in queryObj) {
+            if (key.endsWith('_gt')) {
+                mongoQuery[key.replace(/_gt$/, '')] = { ...mongoQuery[key.replace(/_gt$/, '')], $gt: queryObj[key] };
+            } else if (key.endsWith('_gte')) {
+                mongoQuery[key.replace(/_gte$/, '')] = { ...mongoQuery[key.replace(/_gte$/, '')], $gte: queryObj[key] };
+            } else if (key.endsWith('_lt')) {
+                mongoQuery[key.replace(/_lt$/, '')] = { ...mongoQuery[key.replace(/_lt$/, '')], $lt: queryObj[key] };
+            } else if (key.endsWith('_lte')) {
+                mongoQuery[key.replace(/_lte$/, '')] = { ...mongoQuery[key.replace(/_lte$/, '')], $lte: queryObj[key] };
+            } else {
+                mongoQuery[key] = queryObj[key];
+            }
+        }
+        return mongoQuery;
+    }
+
     filter() {
-        const queryObj = { ...this.queryString };
-        const excludedFields = ['page', 'sort', 'limit', 'fields', 'populate', 'expand'];
-        excludedFields.forEach(el => delete queryObj[el]);
-
-        const mongoQuery = {};
-        for (const key in queryObj) {
-            // ... existing logic
-        }
-        
-        this.query = this.query.find(mongoQuery);
+        this.query = this.query.find(this._buildMongoQuery());
         return this;
     }

     count() {
-        const queryObj = { ...this.queryString };
-        // ... duplicated logic
-        this.query = this.query.model.countDocuments(mongoQuery);
+        this.query = this.query.model.countDocuments(this._buildMongoQuery(true));
         return this;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/src/utils/queryEngine.js` around lines 75 - 101, Extract the
duplicated operator-translation logic used in filter() and count() into a
private helper (e.g. buildMongoQueryFromQueryString(queryString)) that returns
the mongoQuery object; replace the inlined loops in both filter() and count() to
call that helper. Also change count() to build its mongoQuery via the helper and
then set this.query to a Mongoose Query that still exposes getFilter (for
example use this.query = this.query.find(mongoQuery).countDocuments() instead of
this.query.model.countDocuments(mongoQuery)) so the controller can call
.getFilter() on the returned query.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/public-api/src/controllers/data.controller.js`:
- Around line 126-134: The count branch currently allows RLS bypass because
countQuery uses Model.countDocuments without merging baseFilter
(countEngine.count().query.getFilter?.() can be undefined); fix by extracting
the filter from countEngine.count().query (using getFilter() if available or
fallback to query.getQuery()/_conditions) into a plain object, merge it with
baseFilter into a single filter, and call Model.countDocuments(mergedFilter)
(assign to count variable after awaiting); then return the standardized response
shape { success: true, data: { count }, message: "" } instead of raw { count } —
update references to QueryEngine, countEngine.count(), countQuery,
Model.countDocuments, and baseFilter accordingly.

---

Outside diff comments:
In `@apps/public-api/src/controllers/data.controller.js`:
- Around line 150-153: The catch block currently returns err.message to the
client (leaking DB internals); instead log the raw error internally
(console.error or your logger) and return or forward a sanitized AppError to the
middleware—e.g., replace res.status(500).json({ error: err.message }) with
creating/throwing/next-ing a new AppError('Internal server error', 500) (or
res.status(500).json({ error: 'Internal server error' })) so the client never
receives raw MongoDB messages while the original err remains logged for
debugging; update the catch in the controller handler where console.error(err)
appears to use AppError and not expose err.message.

---

Nitpick comments:
In `@packages/common/src/utils/queryEngine.js`:
- Around line 75-101: Extract the duplicated operator-translation logic used in
filter() and count() into a private helper (e.g.
buildMongoQueryFromQueryString(queryString)) that returns the mongoQuery object;
replace the inlined loops in both filter() and count() to call that helper. Also
change count() to build its mongoQuery via the helper and then set this.query to
a Mongoose Query that still exposes getFilter (for example use this.query =
this.query.find(mongoQuery).countDocuments() instead of
this.query.model.countDocuments(mongoQuery)) so the controller can call
.getFilter() on the returned query.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 010149c1-1806-4da5-b8a9-b0567bd91e60

📥 Commits

Reviewing files that changed from the base of the PR and between 5e4a91e and 3fb84f3.

📒 Files selected for processing (4)
  • apps/public-api/src/controllers/data.controller.js
  • packages/common/src/utils/queryEngine.js
  • sdks/urbackend-sdk/src/modules/database.ts
  • sdks/urbackend-sdk/tests/database.test.ts

@yash-pouranik yash-pouranik self-requested a review April 13, 2026 20:07
Copy link
Copy Markdown
Collaborator

@yash-pouranik yash-pouranik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nitin-kumar-yadav1307 please update @urbackend/sdk package version from 0.2.2 -> 0.2.3

@yash-pouranik
Copy link
Copy Markdown
Collaborator

@Nitin-kumar-yadav1307
in file - /sdks/urbackend-sdk/package.json

@yash-pouranik
Copy link
Copy Markdown
Collaborator

yash-pouranik commented Apr 14, 2026

@Nitin-kumar-yadav1307 please resolve merge conflicts - also only accepts change from main. what persists from ur branch? = only the logic u have added ok? also please update version to 0.2.7

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/public-api/src/controllers/data.controller.js`:
- Around line 126-132: Exclude control params from the count path by invoking
QueryEngine.filter with the flag that omits control/query params (i.e., call
QueryEngine(...).filter(true) or otherwise ensure _buildMongoQuery(true) is
used) so `count=true` is not baked into mongoFilter; then make the merge
semantics match the list path by combining mongoFilter and baseFilter with an
$and (or use the same .and([baseFilter]) semantics) instead of a shallow
spread—produce mergedFilter as { $and: [mongoFilter, baseFilter] } when
baseFilter is non-empty before passing it to Model.countDocuments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e2542a1-a9c5-4f88-a953-2c945aa0eb8f

📥 Commits

Reviewing files that changed from the base of the PR and between 3fb84f3 and 99b31f7.

📒 Files selected for processing (3)
  • apps/public-api/src/controllers/data.controller.js
  • packages/common/src/utils/queryEngine.js
  • sdks/urbackend-sdk/package.json
✅ Files skipped from review due to trivial changes (1)
  • sdks/urbackend-sdk/package.json

@Nitin-kumar-yadav1307
Copy link
Copy Markdown
Contributor Author

@yash-pouranik have a look

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SDK/Core] Implement Native client.db.count() Support

2 participants