From d0e3602b3bd5667bb5d22f0b3611c100daf79950 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Yadav Date: Tue, 14 Apr 2026 00:55:12 +0530 Subject: [PATCH 1/7] feat: implement native client.db.count() support --- .../src/controllers/data.controller.js | 10 ++++ packages/common/src/utils/queryEngine.js | 5 -- sdks/urbackend-sdk/src/modules/database.ts | 10 ++++ sdks/urbackend-sdk/tests/database.test.ts | 46 +++++++++++++++++++ 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/apps/public-api/src/controllers/data.controller.js b/apps/public-api/src/controllers/data.controller.js index a0ee9e1e..0fb91e69 100644 --- a/apps/public-api/src/controllers/data.controller.js +++ b/apps/public-api/src/controllers/data.controller.js @@ -122,6 +122,16 @@ module.exports.getAllData = async (req, res) => { ); const baseFilter = req.rlsFilter && typeof req.rlsFilter === 'object' ? req.rlsFilter : {}; + // Handle count=true query parameter +if (req.query.count === 'true') { + const countEngine = new QueryEngine(Model.find(), req.query); + let countQuery = countEngine.count().query; + if (Object.keys(baseFilter).length > 0) { + countQuery = Model.countDocuments({ ...countQuery.getFilter?.() ?? {}, ...baseFilter }); + } + const count = await countQuery; + return res.json({ count }); +} const features = new QueryEngine(Model.find(), req.query) .filter(); diff --git a/packages/common/src/utils/queryEngine.js b/packages/common/src/utils/queryEngine.js index a5234578..e0658816 100644 --- a/packages/common/src/utils/queryEngine.js +++ b/packages/common/src/utils/queryEngine.js @@ -71,11 +71,6 @@ class QueryEngine { }); return this; } - - async count() { - // Clone the query to avoid affecting the original query's skip/limit - return await this.query.model.countDocuments(this.query.getQuery()); - } } module.exports = QueryEngine; diff --git a/sdks/urbackend-sdk/src/modules/database.ts b/sdks/urbackend-sdk/src/modules/database.ts index b3720810..8ecf72d6 100644 --- a/sdks/urbackend-sdk/src/modules/database.ts +++ b/sdks/urbackend-sdk/src/modules/database.ts @@ -22,6 +22,16 @@ export class DatabaseModule { } } + /** + * Count documents in a collection with optional filters + */ +public async count(collection: string, params: Omit = {}): Promise { + const queryString = this.buildQueryString({ ...params, count: 'true' }); + const path = `/api/data/${collection}${queryString}`; + const result = await this.client.request<{ count: number }>('GET', path); + return result.count; +} + /** * Fetch a single document by its ID */ diff --git a/sdks/urbackend-sdk/tests/database.test.ts b/sdks/urbackend-sdk/tests/database.test.ts index ddc749d5..8372dfae 100644 --- a/sdks/urbackend-sdk/tests/database.test.ts +++ b/sdks/urbackend-sdk/tests/database.test.ts @@ -132,3 +132,49 @@ test('delete returns { deleted: true } and handles token', async () => { }), ); }); + +test('count returns total number of documents in a collection', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + headers: new Headers({ 'content-type': 'application/json' }), + json: () => Promise.resolve({ success: true, data: { count: 42 } }), + }); + vi.stubGlobal('fetch', fetchMock); + + const result = await client.db.count('products'); + + expect(result).toBe(42); + const url = fetchMock.mock.calls[0][0] as string; + const searchParams = new URL(url).searchParams; + expect(searchParams.get('count')).toBe('true'); +}); + +test('count with filters builds correct query string', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + headers: new Headers({ 'content-type': 'application/json' }), + json: () => Promise.resolve({ success: true, data: { count: 7 } }), + }); + vi.stubGlobal('fetch', fetchMock); + + const result = await client.db.count('products', { filter: { status: 'active' } }); + + expect(result).toBe(7); + const url = fetchMock.mock.calls[0][0] as string; + const searchParams = new URL(url).searchParams; + expect(searchParams.get('count')).toBe('true'); + expect(searchParams.get('status')).toBe('active'); +}); + +test('count returns 0 when no documents match', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + headers: new Headers({ 'content-type': 'application/json' }), + json: () => Promise.resolve({ success: true, data: { count: 0 } }), + }); + vi.stubGlobal('fetch', fetchMock); + + const result = await client.db.count('products', { filter: { status: 'deleted' } }); + + expect(result).toBe(0); +}); From 699d13d71f8dc9159f045fb4188003c7eef8166b Mon Sep 17 00:00:00 2001 From: Nitin Kumar Yadav Date: Tue, 14 Apr 2026 01:11:47 +0530 Subject: [PATCH 2/7] fix: resolve RLS bypass and fix response format in count endpoint --- apps/public-api/src/controllers/data.controller.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/public-api/src/controllers/data.controller.js b/apps/public-api/src/controllers/data.controller.js index 0fb91e69..9d002047 100644 --- a/apps/public-api/src/controllers/data.controller.js +++ b/apps/public-api/src/controllers/data.controller.js @@ -125,12 +125,12 @@ module.exports.getAllData = async (req, res) => { // Handle count=true query parameter if (req.query.count === 'true') { const countEngine = new QueryEngine(Model.find(), req.query); - let countQuery = countEngine.count().query; - if (Object.keys(baseFilter).length > 0) { - countQuery = Model.countDocuments({ ...countQuery.getFilter?.() ?? {}, ...baseFilter }); - } - const count = await countQuery; - return res.json({ count }); + const mongoFilter = countEngine.filter().query.getFilter(); + const mergedFilter = Object.keys(baseFilter).length > 0 + ? { ...mongoFilter, ...baseFilter } + : mongoFilter; + const count = await Model.countDocuments(mergedFilter); + return res.status(200).json({ success: true, data: { count }, message: "Count fetched successfully." }); } const features = new QueryEngine(Model.find(), req.query) .filter(); From 9985466cda33e02c91eb10056e032342c4ebf0fb Mon Sep 17 00:00:00 2001 From: Nitin Kumar Yadav Date: Tue, 14 Apr 2026 01:24:21 +0530 Subject: [PATCH 3/7] refactor: extract shared _buildMongoQuery helper in QueryEngine --- packages/common/src/utils/queryEngine.js | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/common/src/utils/queryEngine.js b/packages/common/src/utils/queryEngine.js index e0658816..ade5d025 100644 --- a/packages/common/src/utils/queryEngine.js +++ b/packages/common/src/utils/queryEngine.js @@ -71,6 +71,44 @@ class QueryEngine { }); return this; } + +<<<<<<< HEAD + async count() { + // Clone the query to avoid affecting the original query's skip/limit + return await this.query.model.countDocuments(this.query.getQuery()); + } +======= + _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')) { + const field = key.replace(/_gt$/, ''); + mongoQuery[field] = { ...mongoQuery[field], $gt: queryObj[key] }; + } else if (key.endsWith('_gte')) { + const field = key.replace(/_gte$/, ''); + mongoQuery[field] = { ...mongoQuery[field], $gte: queryObj[key] }; + } else if (key.endsWith('_lt')) { + const field = key.replace(/_lt$/, ''); + mongoQuery[field] = { ...mongoQuery[field], $lt: queryObj[key] }; + } else if (key.endsWith('_lte')) { + const field = key.replace(/_lte$/, ''); + mongoQuery[field] = { ...mongoQuery[field], $lte: queryObj[key] }; + } else { + mongoQuery[key] = queryObj[key]; + } + } + return mongoQuery; +} + +count() { + this.query = this.query.model.countDocuments(this._buildMongoQuery(true)); + return this; +} +>>>>>>> 3fb84f3e (feat: implement native client.db.count() support) } module.exports = QueryEngine; From 885bb79060ad14dd0e50cdb2a44e0df50332854c Mon Sep 17 00:00:00 2001 From: Nitin Kumar Yadav Date: Tue, 14 Apr 2026 01:57:24 +0530 Subject: [PATCH 4/7] chore: bump sdk version to 0.2.3 --- sdks/urbackend-sdk/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdks/urbackend-sdk/package.json b/sdks/urbackend-sdk/package.json index 4827573c..0879f3b2 100644 --- a/sdks/urbackend-sdk/package.json +++ b/sdks/urbackend-sdk/package.json @@ -1,6 +1,10 @@ { "name": "@urbackend/sdk", +<<<<<<< HEAD "version": "0.2.6", +======= + "version": "0.2.3", +>>>>>>> 806126af (chore: bump sdk version to 0.2.3) "description": "Official TypeScript SDK for urBackend BaaS", "type": "module", "files": [ From d150496b1d9561753f0931883203c0edcf951842 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Yadav Date: Wed, 15 Apr 2026 10:36:36 +0530 Subject: [PATCH 5/7] chore: bump sdk version to 0.2.7 --- sdks/urbackend-sdk/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdks/urbackend-sdk/package.json b/sdks/urbackend-sdk/package.json index 0879f3b2..1d49c086 100644 --- a/sdks/urbackend-sdk/package.json +++ b/sdks/urbackend-sdk/package.json @@ -1,10 +1,14 @@ { "name": "@urbackend/sdk", +<<<<<<< HEAD <<<<<<< HEAD "version": "0.2.6", ======= "version": "0.2.3", >>>>>>> 806126af (chore: bump sdk version to 0.2.3) +======= + "version": "0.2.7", +>>>>>>> 99b31f75 (chore: bump sdk version to 0.2.7) "description": "Official TypeScript SDK for urBackend BaaS", "type": "module", "files": [ From 8a0a5b8a5807c269b7490e6c3f859c5d07f4c55e Mon Sep 17 00:00:00 2001 From: Nitin Kumar Yadav Date: Wed, 15 Apr 2026 10:50:32 +0530 Subject: [PATCH 6/7] fix: count param leaking into mongo filter --- apps/public-api/src/controllers/data.controller.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/public-api/src/controllers/data.controller.js b/apps/public-api/src/controllers/data.controller.js index 9d002047..fca72264 100644 --- a/apps/public-api/src/controllers/data.controller.js +++ b/apps/public-api/src/controllers/data.controller.js @@ -125,10 +125,10 @@ module.exports.getAllData = async (req, res) => { // Handle count=true query parameter if (req.query.count === 'true') { const countEngine = new QueryEngine(Model.find(), req.query); - const mongoFilter = countEngine.filter().query.getFilter(); - const mergedFilter = Object.keys(baseFilter).length > 0 - ? { ...mongoFilter, ...baseFilter } - : mongoFilter; +const mongoFilter = countEngine._buildMongoQuery(true); +const mergedFilter = Object.keys(baseFilter).length > 0 + ? { $and: [mongoFilter, baseFilter] } + : mongoFilter; const count = await Model.countDocuments(mergedFilter); return res.status(200).json({ success: true, data: { count }, message: "Count fetched successfully." }); } From 29cc38a6801eea1a5e4ad3b730f68a6bfac40405 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Yadav Date: Wed, 15 Apr 2026 11:13:42 +0530 Subject: [PATCH 7/7] chore: bump sdk version to 0.2.7 --- sdks/urbackend-sdk/package.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/sdks/urbackend-sdk/package.json b/sdks/urbackend-sdk/package.json index 1d49c086..00f6619e 100644 --- a/sdks/urbackend-sdk/package.json +++ b/sdks/urbackend-sdk/package.json @@ -1,14 +1,6 @@ { "name": "@urbackend/sdk", -<<<<<<< HEAD -<<<<<<< HEAD - "version": "0.2.6", -======= - "version": "0.2.3", ->>>>>>> 806126af (chore: bump sdk version to 0.2.3) -======= "version": "0.2.7", ->>>>>>> 99b31f75 (chore: bump sdk version to 0.2.7) "description": "Official TypeScript SDK for urBackend BaaS", "type": "module", "files": [