From d25e0cc41cbcb49b198988ebe24cb5cb7033972a Mon Sep 17 00:00:00 2001 From: SpaceFox1 <44732812+SpaceFox1@users.noreply.github.com> Date: Fri, 31 Oct 2025 01:51:15 -0300 Subject: [PATCH] feat: Count method --- CHANGELOG.md | 6 +++ package-lock.json | 4 +- package.json | 2 +- src/main/connection/DatabaseConnection.ts | 12 +++++- src/main/connection/MariaDBConnection.ts | 13 ++++++ src/main/interfaces/connection/index.ts | 2 +- .../interfaces/database/IDatabaseCount.ts | 5 +++ src/main/interfaces/database/index.ts | 1 + src/main/models/BaseModel.ts | 41 +++++++++++++++---- 9 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 src/main/interfaces/database/IDatabaseCount.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cb115de..cf09dd1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v1.2.1 + +- Feat: New 'Count' method that allows the counting of how many rows are in a table, how many not null columns, distinct values of a column and set a custom name for the count key in the result object. + +- Fix(types): Fix typing mistake that caused an regression forcing an object to be passed to 'Select' and 'Find', calling those methods without any argument will behave the same way as passing an empty object, this is a typing only mistake, that doesn't change how the methods behave or parse its parameters. + # v1.2.0 - Feat: 'Select' and 'Find' methods can now receive an orderBy parameter to order return the results from the database diff --git a/package-lock.json b/package-lock.json index 60651de..1468556 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "promiseorm", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "promiseorm", - "version": "1.2.0", + "version": "1.2.1", "license": "GPL-3.0", "dependencies": { "mariadb": "3.4.5" diff --git a/package.json b/package.json index 48f2934..17e54e4 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "promiseorm", - "version": "1.2.0", + "version": "1.2.1", "description": "A Typescript ORM for automatic creation and management of models and entries from simple objects", "main": "build/index.js", "files": [ diff --git a/src/main/connection/DatabaseConnection.ts b/src/main/connection/DatabaseConnection.ts index a32ff71..678a78e 100755 --- a/src/main/connection/DatabaseConnection.ts +++ b/src/main/connection/DatabaseConnection.ts @@ -1,4 +1,4 @@ -import { IDatabaseConnectionRead, IDatabaseField, IDatabaseQueryFilterExpression } from '../interfaces'; +import { IDatabaseConnectionRead, IDatabaseCount, IDatabaseField, IDatabaseQueryFilterExpression } from '../interfaces'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { DatabaseException } from '../errors'; import { BaseModel } from '../models'; @@ -112,4 +112,14 @@ export abstract class DatabaseConnection { * @abstract */ public abstract createOrUpdateTable(tableName: string, fields: Record): Promise + + /** + * Returns the count of the provided fields + * @param fields Array of objects that specify the columns to count, if the count should be distinct, and optionally to what name rename the column of the count + * @param filter The filter to apply to the records + * @returns A promise containing an object with the count of all the requested fields + * @throws [{@link DatabaseException}] + * @abstract + */ + public abstract count(database: string, { fields, filter }: { fields: IDatabaseCount[], filter?: IDatabaseQueryFilterExpression }): Promise> } diff --git a/src/main/connection/MariaDBConnection.ts b/src/main/connection/MariaDBConnection.ts index a4b160b..575503b 100755 --- a/src/main/connection/MariaDBConnection.ts +++ b/src/main/connection/MariaDBConnection.ts @@ -1,6 +1,7 @@ import { EDatabaseQueryFilterOperator, EDatabaseTypes, EMariaDBFieldTypes, IDatabaseConnectionRead, + IDatabaseCount, IDatabaseField, IDatabaseQueryFilter, IDatabaseQueryFilterExpression, IMariaDBDescribeField, @@ -197,6 +198,18 @@ export class MariaDBConnection extends DatabaseConnection { return result.affectedRows; } + /** + * @private + */ + override async count(database: string, { fields, filter }: { fields: IDatabaseCount[]; filter?: IDatabaseQueryFilterExpression; }): Promise> { + const conn = await this.getConnection(); + const counts: string[] = []; + fields.forEach((field) => counts.push(`COUNT(${field.distinct ? 'DISTINCT ' : ''}${field.key})${field.alias ? ` AS ${conn.escape(field.alias)}` : ''}`)); + const result = await conn.query(`SELECT ${counts.join(', ')} FROM ${conn.escapeId(database)}${filter ? ` WHERE ${this.filterBuilder(conn, filter)}` : ''}`); + await conn.release(); + return result[0]; + } + /** * @private */ diff --git a/src/main/interfaces/connection/index.ts b/src/main/interfaces/connection/index.ts index 6f61616..0dd1e29 100755 --- a/src/main/interfaces/connection/index.ts +++ b/src/main/interfaces/connection/index.ts @@ -1,2 +1,2 @@ -export * from './mariadb'; export * from './IDatabaseConnectionRead'; +export * from './mariadb'; diff --git a/src/main/interfaces/database/IDatabaseCount.ts b/src/main/interfaces/database/IDatabaseCount.ts new file mode 100644 index 0000000..13a805c --- /dev/null +++ b/src/main/interfaces/database/IDatabaseCount.ts @@ -0,0 +1,5 @@ +export interface IDatabaseCount { + key: string; + distinct: boolean; + alias?: string; +}; diff --git a/src/main/interfaces/database/index.ts b/src/main/interfaces/database/index.ts index 9ff0fb9..c2e7e3e 100755 --- a/src/main/interfaces/database/index.ts +++ b/src/main/interfaces/database/index.ts @@ -3,4 +3,5 @@ export * from './EDatabaseQueryFilterOperator'; export * from './IDatabaseQueryFilter'; export * from './IDatabaseOrderBy'; export * from './IDatabaseField'; +export * from './IDatabaseCount'; export * from './EDatabaseTypes'; diff --git a/src/main/models/BaseModel.ts b/src/main/models/BaseModel.ts index ad483e7..894f93d 100755 --- a/src/main/models/BaseModel.ts +++ b/src/main/models/BaseModel.ts @@ -1,4 +1,4 @@ -import { EDatabaseQueryFilterOperator, EDatabaseTypes, IDatabaseField, IDatabaseOrderBy, IDatabaseQueryFilter, IDatabaseQueryFilterExpression } from '../interfaces'; +import { EDatabaseQueryFilterOperator, EDatabaseTypes, IDatabaseCount, IDatabaseField, IDatabaseOrderBy, IDatabaseQueryFilter, IDatabaseQueryFilterExpression } from '../interfaces'; import { DatabaseConnection } from '../connection'; import { DatabaseException } from '../errors'; @@ -157,7 +157,7 @@ export class BaseModel { * @returns The data found * @throws [{@link DatabaseException}] */ - public async find(params: { query?: Record, limit?: number, orderBy?: IDatabaseOrderBy }): Promise[]> { + public async find(params?: { query?: Record, limit?: number, orderBy?: IDatabaseOrderBy }): Promise[]> { const { query, limit, orderBy } = params || {}; this.checkIsReady(); return this.connection!.read({ keys: '*', database: this.name!, @@ -205,7 +205,7 @@ export class BaseModel { * @returns The data found * @throws [{@link DatabaseException}] */ - public async select(params: { fields: string[], query?: Record, filter?: IDatabaseQueryFilterExpression, limit?: number, orderBy?: IDatabaseOrderBy }, + public async select(params: { fields: string[], filter?: IDatabaseQueryFilterExpression, limit?: number, orderBy?: IDatabaseOrderBy }, ): Promise[]> { this.checkIsReady(); if (!params) throw new DatabaseException('Missing params for \'select\' method call, if you want to retrieve all data on this table call \'find\' instead.'); @@ -246,9 +246,6 @@ export class BaseModel { * @throws [{@link DatabaseException}] */ public async update(find: Record, update: Record): Promise> { - this.checkIsReady(); - if (!update) throw new DatabaseException('Empty update!'); - this.validFieldValueCheck(update); const filter: IDatabaseQueryFilterExpression = { type: 'AND', filters: Object.keys(find).map((fieldKey) => ({ @@ -257,9 +254,7 @@ export class BaseModel { value: find[fieldKey], })), }; - const keys = Object.keys(update).filter((field) => !this.fields[field].autoIncrement); - const values = keys.map((fieldKey) => update[fieldKey] ?? null); - return this.connection!.update(this.name!, keys, values, filter); + return this.updateWhere(filter, update); } /** @@ -322,4 +317,32 @@ export class BaseModel { this.filterCheck(filter); return this.connection!.delete(this.name!, filter); } + + /** + * Counts rows in the table, call without parameters to get the total amount of rows + * + * Use the fields parameter to count the amount of rows within a specific column (without counting NULL values) + * + * Pass an object with { key: string, distinct: true } on the fields array to only count unique values for that column (Example: [{ key: 'category', distinct: true}] will return how many different categories there is on the table) + * + * Use the optional alias parameter to rename the column. Example, [{ key: 'category', distinct: true, alias: 'categoryCount'}] will return the object [{ categoryCount: 7 }]. + * + * Pass a filter to only count rows that match an specific criteria. Example, only count the rows where the column 'price' is greater than 50 will return how many items cost more than 50. + * + * Note: if fields is not provided, will return [{ count: number }] + * @param fields + */ + public async count(params?: { fields?: (IDatabaseCount | string)[], filter?: IDatabaseQueryFilterExpression }): Promise> { + const fields = params?.fields ?? undefined; + const filter = params?.filter ?? undefined; + + this.checkIsReady(); + this.filterCheck(filter); + if (fields) this.fieldsCheck(fields.map((field) => typeof field === 'string' ? field : field.key)); + + const finalFields: IDatabaseCount[] = fields?.map((field) => typeof field === 'string' ? + { key: field, distinct: false, alias: field } : { alias: field.alias ?? field.key, ...field }) ?? [{ key: '*', distinct: false, alias: 'count' }]; + + return this.connection!.count(this.name!, { fields: finalFields, filter }); + } }