Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/public-api/src/controllers/data.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
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." });
}
const features = new QueryEngine(Model.find(), req.query)
.filter();

Expand Down
33 changes: 33 additions & 0 deletions packages/common/src/utils/queryEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,43 @@ 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;
2 changes: 1 addition & 1 deletion sdks/urbackend-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@urbackend/sdk",
"version": "0.2.6",
"version": "0.2.7",
"description": "Official TypeScript SDK for urBackend BaaS",
"type": "module",
"files": [
Expand Down
10 changes: 10 additions & 0 deletions sdks/urbackend-sdk/src/modules/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export class DatabaseModule {
}
}

/**
* Count documents in a collection with optional filters
*/
public async count(collection: string, params: Omit<QueryParams, 'count'> = {}): Promise<number> {
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
*/
Expand Down
46 changes: 46 additions & 0 deletions sdks/urbackend-sdk/tests/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Loading