From ab3ade3890e3fedfe3569ac0ae425bdc1e5f355a Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Fri, 2 Jan 2026 15:24:44 +0200 Subject: [PATCH 1/9] fix: enhance data type handling in MongoConnector and add support for Double type --- adminforth/dataConnectors/mongo.ts | 113 +++++++++++++++++------------ 1 file changed, 67 insertions(+), 46 deletions(-) diff --git a/adminforth/dataConnectors/mongo.ts b/adminforth/dataConnectors/mongo.ts index 6e1186172..7c58b0765 100644 --- a/adminforth/dataConnectors/mongo.ts +++ b/adminforth/dataConnectors/mongo.ts @@ -1,6 +1,6 @@ import dayjs from 'dayjs'; import { MongoClient } from 'mongodb'; -import { Decimal128 } from 'bson'; +import { Decimal128, Double } from 'bson'; import { IAdminForthDataSourceConnector, IAdminForthSingleFilter, IAdminForthAndOrFilter, AdminForthResource } from '../types/Back.js'; import AdminForthBaseConnector from './baseConnector.js'; @@ -122,30 +122,30 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS } return Array.from(fieldTypes.entries()).map(([name, types]) => { - const primaryKey = name === '_id'; - - const priority = ['datetime', 'date', 'integer', 'float', 'boolean', 'json', 'decimal', 'string']; - - const matched = priority.find(t => types.has(t)) || 'string'; - - const typeMap: Record = { - string: 'STRING', - integer: 'INTEGER', - float: 'FLOAT', - boolean: 'BOOLEAN', - datetime: 'DATETIME', - date: 'DATE', - json: 'JSON', - decimal: 'DECIMAL', - }; - return { - name, - type: typeMap[matched] ?? 'STRING', - ...(primaryKey ? { isPrimaryKey: true } : {}), - sampleValue: sampleValues.get(name), - }; + const primaryKey = name === '_id'; + + const priority = ['datetime','date','decimal','integer','float','boolean','json','string']; + + const matched = priority.find(t => types.has(t)) || 'string'; + + const typeMap: Record = { + string: AdminForthDataTypes.STRING, + integer: AdminForthDataTypes.INTEGER, + float: AdminForthDataTypes.FLOAT, + boolean: AdminForthDataTypes.BOOLEAN, + datetime: AdminForthDataTypes.DATETIME, + date: AdminForthDataTypes.DATE, + json: AdminForthDataTypes.JSON, + decimal: AdminForthDataTypes.DECIMAL, + }; + return { + name, + type: typeMap[matched] ?? AdminForthDataTypes.STRING, + ...(primaryKey ? { isPrimaryKey: true } : {}), + sampleValue: sampleValues.get(name), + }; }); - } + } async discoverFields(resource) { @@ -200,19 +200,34 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS setFieldValue(field, value) { - if (field.type == AdminForthDataTypes.DATETIME) { - if (!value) { - return null; - } - return dayjs(value).toDate(); - - } else if (field.type == AdminForthDataTypes.BOOLEAN) { - return value === null ? null : (value ? true : false); - } else if (field.type == AdminForthDataTypes.DECIMAL) { - if (value === null || value === undefined) { - return null; - } - return Decimal128.fromString(value?.toString()); + if (value === undefined) return undefined; + if (value === null || value === "") return null; + + if (field.type === AdminForthDataTypes.DATETIME) { + return dayjs(value).isValid() ? dayjs(value).toDate() : null; + } + + if (field.type === AdminForthDataTypes.DATE) { + const d = dayjs(value); + return d.isValid() ? d.startOf("day").toDate() : null; + } + + if (field.type === AdminForthDataTypes.BOOLEAN) { + return value === null ? null : !!value; + } + + if (field.type === AdminForthDataTypes.INTEGER) { + const n = typeof value === "number" ? value : Number(String(value).replace(",", ".")); + return Number.isFinite(n) ? Math.trunc(n) : null; + } + + if (field.type === AdminForthDataTypes.FLOAT) { + const n = typeof value === "number" ? value : Number(String(value).replace(",", ".")); + return Number.isFinite(n) ? new Double(n) : null; + } + + if (field.type === AdminForthDataTypes.DECIMAL) { + return Decimal128.fromString(value.toString()); } return value; } @@ -251,7 +266,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS return { $expr: { [mongoExprOp]: [left, right] } }; } const column = resource.dataSourceColumns.find((col) => col.name === (filter as IAdminForthSingleFilter).field); - if (['integer', 'decimal', 'float'].includes(column.type)) { + if ([AdminForthDataTypes.INTEGER, AdminForthDataTypes.DECIMAL, AdminForthDataTypes.FLOAT].includes(column.type)) { return { [(filter as IAdminForthSingleFilter).field]: this.OperatorsMap[filter.operator](+(filter as IAdminForthSingleFilter).value) }; } return { [(filter as IAdminForthSingleFilter).field]: this.OperatorsMap[filter.operator]((filter as IAdminForthSingleFilter).value) }; @@ -315,9 +330,9 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS const collection = this.client.db().collection(tableName); const result = {}; for (const column of columns) { - result[column] = await collection + result[column.name] = await collection .aggregate([ - { $group: { _id: null, min: { $min: `$${column}` }, max: { $max: `$${column}` } } }, + { $group: { _id: null, min: { $min: `$${column.name}` }, max: { $max: `$${column.name}` } } }, ]) .toArray(); } @@ -325,12 +340,12 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS } async createRecordOriginalValues({ resource, record }): Promise { - const tableName = resource.table; - const collection = this.client.db().collection(tableName); - const columns = Object.keys(record); + const collection = this.client.db().collection(resource.table); + const colsByName = new Map(resource.dataSourceColumns.map((c) => [c.name, c])); const newRecord = {}; - for (const colName of columns) { - newRecord[colName] = record[colName]; + for (const [key, raw] of Object.entries(record)) { + const col = colsByName.get(key); + newRecord[key] = col ? this.setFieldValue(col, raw) : raw; } const ret = await collection.insertOne(newRecord); return ret.insertedId; @@ -338,7 +353,13 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS async updateRecordOriginalValues({ resource, recordId, newValues }) { const collection = this.client.db().collection(resource.table); - await collection.updateOne({ [this.getPrimaryKey(resource)]: recordId }, { $set: newValues }); + const colsByName = new Map(resource.dataSourceColumns.map((c) => [c.name, c])); + const updatedValues = {}; + for (const [key, raw] of Object.entries(newValues)) { + const col = colsByName.get(key); + updatedValues[key] = col ? this.setFieldValue(col, raw) : raw; + } + await collection.updateOne({ [this.getPrimaryKey(resource)]: recordId }, { $set: updatedValues }); } async deleteRecord({ resource, recordId }): Promise { From d2de4dec91bf750445b915d1ecadae3ab14903e4 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Fri, 2 Jan 2026 15:49:38 +0200 Subject: [PATCH 2/9] fix: improve null and empty string handling in setFieldValue method --- adminforth/dataConnectors/mongo.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/adminforth/dataConnectors/mongo.ts b/adminforth/dataConnectors/mongo.ts index 7c58b0765..9f5104631 100644 --- a/adminforth/dataConnectors/mongo.ts +++ b/adminforth/dataConnectors/mongo.ts @@ -124,7 +124,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS return Array.from(fieldTypes.entries()).map(([name, types]) => { const primaryKey = name === '_id'; - const priority = ['datetime','date','decimal','integer','float','boolean','json','string']; + const priority = ['datetime', 'date', 'decimal', 'integer', 'float', 'boolean', 'json', 'string']; const matched = priority.find(t => types.has(t)) || 'string'; @@ -201,34 +201,41 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS setFieldValue(field, value) { if (value === undefined) return undefined; - if (value === null || value === "") return null; + if (value === null) return null; if (field.type === AdminForthDataTypes.DATETIME) { + if (value === "" || value === null) return null; return dayjs(value).isValid() ? dayjs(value).toDate() : null; } if (field.type === AdminForthDataTypes.DATE) { + if (value === "" || value === null) return null; const d = dayjs(value); return d.isValid() ? d.startOf("day").toDate() : null; } if (field.type === AdminForthDataTypes.BOOLEAN) { - return value === null ? null : !!value; + if (value === "" || value === null) return null; + return !!value; } if (field.type === AdminForthDataTypes.INTEGER) { + if (value === "" || value === null) return null; const n = typeof value === "number" ? value : Number(String(value).replace(",", ".")); return Number.isFinite(n) ? Math.trunc(n) : null; } if (field.type === AdminForthDataTypes.FLOAT) { - const n = typeof value === "number" ? value : Number(String(value).replace(",", ".")); - return Number.isFinite(n) ? new Double(n) : null; + if (value === "" || value === null) return null; + const n = typeof value === "number" ? value : Number(String(value).replace(",", ".")); + return Number.isFinite(n) ? new Double(n) : null; } if (field.type === AdminForthDataTypes.DECIMAL) { + if (value === "" || value === null) return null; return Decimal128.fromString(value.toString()); } + return value; } @@ -266,7 +273,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS return { $expr: { [mongoExprOp]: [left, right] } }; } const column = resource.dataSourceColumns.find((col) => col.name === (filter as IAdminForthSingleFilter).field); - if ([AdminForthDataTypes.INTEGER, AdminForthDataTypes.DECIMAL, AdminForthDataTypes.FLOAT].includes(column.type)) { + if (column && [AdminForthDataTypes.INTEGER, AdminForthDataTypes.DECIMAL, AdminForthDataTypes.FLOAT].includes(column.type)) { return { [(filter as IAdminForthSingleFilter).field]: this.OperatorsMap[filter.operator](+(filter as IAdminForthSingleFilter).value) }; } return { [(filter as IAdminForthSingleFilter).field]: this.OperatorsMap[filter.operator]((filter as IAdminForthSingleFilter).value) }; From 2c449a46fc2a0775906bf6bb58b29f6c0bd63387 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Mon, 5 Jan 2026 12:29:50 +0200 Subject: [PATCH 3/9] fix: implement validateAndSetFieldValue method for improved data validation in AdminForthBaseConnector and update MongoConnector to utilize it --- adminforth/dataConnectors/baseConnector.ts | 100 ++++++++++++++++++++- adminforth/dataConnectors/mongo.ts | 60 ++++++------- 2 files changed, 123 insertions(+), 37 deletions(-) diff --git a/adminforth/dataConnectors/baseConnector.ts b/adminforth/dataConnectors/baseConnector.ts index c8e587a67..391f3181c 100644 --- a/adminforth/dataConnectors/baseConnector.ts +++ b/adminforth/dataConnectors/baseConnector.ts @@ -9,6 +9,7 @@ import { import { suggestIfTypo } from "../modules/utils.js"; import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections } from "../types/Common.js"; import { randomUUID } from "crypto"; +import dayjs from "dayjs"; export default class AdminForthBaseConnector implements IAdminForthDataSourceConnectorBase { @@ -164,9 +165,9 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon return { ok: false, error: `Value for operator '${filters.operator}' should not be empty array, in filter object: ${JSON.stringify(filters) }` }; } } - filters.value = filters.value.map((val: any) => this.setFieldValue(fieldObj, val)); + filters.value = filters.value.map((val: any) => this.validateAndSetFieldValue(fieldObj, val)); } else { - filtersAsSingle.value = this.setFieldValue(fieldObj, filtersAsSingle.value); + filtersAsSingle.value = this.validateAndSetFieldValue(fieldObj, filtersAsSingle.value); } } else if (filtersAsSingle.insecureRawSQL || filtersAsSingle.insecureRawNoSQL) { // if "insecureRawSQL" filter is insecure sql string @@ -219,6 +220,97 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon throw new Error('Method not implemented.'); } + validateAndSetFieldValue(field: AdminForthResourceColumn, value: any): any { + // Int + if (field.type === AdminForthDataTypes.INTEGER) { + if (value === "" || value === null) return this.setFieldValue(field, null); + if (!Number.isFinite(value) || Math.trunc(value) !== value) { + throw new Error(`Value is not an integer. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`); + } + return this.setFieldValue(field, Math.trunc(value)); + } + + // Float + if (field.type === AdminForthDataTypes.FLOAT) { + if (value === "" || value === null) return this.setFieldValue(field, null); + const n = + typeof value === "number" + ? value + : (typeof value === "object" && value !== null ? (value as any).valueOf() : NaN); + + if (typeof n !== "number" || !Number.isFinite(n)) { + throw new Error( + `Value is not a float. Field ${field.name} with type is ${field.type}, but got value: ${String(value)} with type ${typeof value}` + ); + } + + return this.setFieldValue(field, n); + } + + // Decimal + if (field.type === AdminForthDataTypes.DECIMAL) { + if (value === "" || value === null) return this.setFieldValue(field, null); + + if (typeof value === "number") { + if (!Number.isFinite(value)) { + throw new Error(`Value is not a decimal. Field ${field.name} got: ${value} (number)`); + } + return this.setFieldValue(field, value); + } + + if (typeof value === "string") { + const s = value.trim(); + if (!s) return this.setFieldValue(field, null); + if (Number.isFinite(Number(s))) return this.setFieldValue(field, s); + throw new Error(`Value is not a decimal. Field ${field.name} got: ${value} (string)`); + } + + if (typeof value === "object" && value) { + const v: any = value; + if (typeof v.toString !== "function") { + throw new Error(`Decimal object has no toString(). Field ${field.name} got: ${String(value)}`); + } + const s = v.toString().trim(); + if (!s) return this.setFieldValue(field, null); + if (Number.isFinite(Number(s))) return this.setFieldValue(field, s); + throw new Error(`Value is not a decimal. Field ${field.name} got: ${s} (object->string)`); + } + + throw new Error(`Value is not a decimal. Field ${field.name} got: ${String(value)} (${typeof value})`); + } + + // Date + + + // DateTime + if (field.type === AdminForthDataTypes.DATETIME) { + if (value === "" || value === null) return this.setFieldValue(field, null); + if (!dayjs(value).isValid()) { + throw new Error(`Value is not a valid datetime. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`); + } + return this.setFieldValue(field, dayjs(value).toISOString()); + } + + // Time + + // Boolean + if (field.type === AdminForthDataTypes.BOOLEAN) { + if (value === "" || value === null) return this.setFieldValue(field, null); + if (typeof value !== 'boolean') { + throw new Error(`Value is not a boolean. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`); + } + return this.setFieldValue(field, value); + } + + // JSON + + // String + if (field.type === AdminForthDataTypes.STRING) { + if (value === "" || value === null) return this.setFieldValue(field, null); + } + return this.setFieldValue(field, value); + } + getMinMaxForColumnsWithOriginalTypes({ resource, columns }: { resource: AdminForthResource; columns: AdminForthResourceColumn[]; }): Promise<{ [key: string]: { min: any; max: any; }; }> { throw new Error('Method not implemented.'); } @@ -268,7 +360,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon } if (filledRecord[col.name] !== undefined) { // no sense to set value if it is not defined - recordWithOriginalValues[col.name] = this.setFieldValue(col, filledRecord[col.name]); + recordWithOriginalValues[col.name] = this.validateAndSetFieldValue(col, filledRecord[col.name]); } } @@ -325,7 +417,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon Update record received field '${field}' (with value ${newValues[field]}), but such column not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''} `); } - recordWithOriginalValues[col.name] = this.setFieldValue(col, newValues[col.name]); + recordWithOriginalValues[col.name] = this.validateAndSetFieldValue(col, newValues[col.name]); } const record = await this.getRecordByPrimaryKey(resource, recordId); let error: string | null = null; diff --git a/adminforth/dataConnectors/mongo.ts b/adminforth/dataConnectors/mongo.ts index 9f5104631..d8470adc7 100644 --- a/adminforth/dataConnectors/mongo.ts +++ b/adminforth/dataConnectors/mongo.ts @@ -9,6 +9,12 @@ import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirection const escapeRegex = (value) => { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escapes special characters }; +function normalizeMongoValue(v: any) { + if (v == null) return v; + if (v instanceof Decimal128) return v.toString(); + if (typeof v === "object" && v.$numberDecimal) return String(v.$numberDecimal); + return v; +} class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector { @@ -208,27 +214,14 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS return dayjs(value).isValid() ? dayjs(value).toDate() : null; } - if (field.type === AdminForthDataTypes.DATE) { - if (value === "" || value === null) return null; - const d = dayjs(value); - return d.isValid() ? d.startOf("day").toDate() : null; - } - - if (field.type === AdminForthDataTypes.BOOLEAN) { - if (value === "" || value === null) return null; - return !!value; - } - if (field.type === AdminForthDataTypes.INTEGER) { if (value === "" || value === null) return null; - const n = typeof value === "number" ? value : Number(String(value).replace(",", ".")); - return Number.isFinite(n) ? Math.trunc(n) : null; + return Number.isFinite(value) ? Math.trunc(value) : null; } if (field.type === AdminForthDataTypes.FLOAT) { if (value === "" || value === null) return null; - const n = typeof value === "number" ? value : Number(String(value).replace(",", ".")); - return Number.isFinite(n) ? new Double(n) : null; + return Number.isFinite(value) ? new Double(value) : null; } if (field.type === AdminForthDataTypes.DECIMAL) { @@ -335,24 +328,31 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS async getMinMaxForColumnsWithOriginalTypes({ resource, columns }) { const tableName = resource.table; const collection = this.client.db().collection(tableName); - const result = {}; + const result: Record = {}; + for (const column of columns) { - result[column.name] = await collection - .aggregate([ - { $group: { _id: null, min: { $min: `$${column.name}` }, max: { $max: `$${column.name}` } } }, - ]) - .toArray(); + const [doc] = await collection + .aggregate([ + { $group: { _id: null, min: { $min: `$${column.name}` }, max: { $max: `$${column.name}` } } }, + { $project: { _id: 0, min: 1, max: 1 } }, + ]) + .toArray(); + + result[column.name] = { + min: normalizeMongoValue(doc?.min), + max: normalizeMongoValue(doc?.max), + }; } return result; } async createRecordOriginalValues({ resource, record }): Promise { - const collection = this.client.db().collection(resource.table); - const colsByName = new Map(resource.dataSourceColumns.map((c) => [c.name, c])); + const tableName = resource.table; + const collection = this.client.db().collection(tableName); + const columns = Object.keys(record); const newRecord = {}; - for (const [key, raw] of Object.entries(record)) { - const col = colsByName.get(key); - newRecord[key] = col ? this.setFieldValue(col, raw) : raw; + for (const colName of columns) { + newRecord[colName] = record[colName]; } const ret = await collection.insertOne(newRecord); return ret.insertedId; @@ -360,13 +360,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS async updateRecordOriginalValues({ resource, recordId, newValues }) { const collection = this.client.db().collection(resource.table); - const colsByName = new Map(resource.dataSourceColumns.map((c) => [c.name, c])); - const updatedValues = {}; - for (const [key, raw] of Object.entries(newValues)) { - const col = colsByName.get(key); - updatedValues[key] = col ? this.setFieldValue(col, raw) : raw; - } - await collection.updateOne({ [this.getPrimaryKey(resource)]: recordId }, { $set: updatedValues }); + await collection.updateOne({ [this.getPrimaryKey(resource)]: recordId }, { $set: newValues }); } async deleteRecord({ resource, recordId }): Promise { From 651159c52ee8aaba89f2829b9dd4464e7fed3320 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Mon, 5 Jan 2026 12:35:16 +0200 Subject: [PATCH 4/9] fix: improve float and decimal value handling in setFieldValue method --- adminforth/dataConnectors/baseConnector.ts | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/adminforth/dataConnectors/baseConnector.ts b/adminforth/dataConnectors/baseConnector.ts index 391f3181c..0f5868416 100644 --- a/adminforth/dataConnectors/baseConnector.ts +++ b/adminforth/dataConnectors/baseConnector.ts @@ -233,18 +233,18 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon // Float if (field.type === AdminForthDataTypes.FLOAT) { if (value === "" || value === null) return this.setFieldValue(field, null); - const n = + const number = typeof value === "number" ? value : (typeof value === "object" && value !== null ? (value as any).valueOf() : NaN); - if (typeof n !== "number" || !Number.isFinite(n)) { + if (typeof number !== "number" || !Number.isFinite(number)) { throw new Error( `Value is not a float. Field ${field.name} with type is ${field.type}, but got value: ${String(value)} with type ${typeof value}` ); } - return this.setFieldValue(field, n); + return this.setFieldValue(field, number); } // Decimal @@ -259,21 +259,20 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon } if (typeof value === "string") { - const s = value.trim(); - if (!s) return this.setFieldValue(field, null); - if (Number.isFinite(Number(s))) return this.setFieldValue(field, s); + const string = value.trim(); + if (!string) return this.setFieldValue(field, null); + if (Number.isFinite(Number(string))) return this.setFieldValue(field, string); throw new Error(`Value is not a decimal. Field ${field.name} got: ${value} (string)`); } if (typeof value === "object" && value) { - const v: any = value; - if (typeof v.toString !== "function") { + if (typeof value.toString !== "function") { throw new Error(`Decimal object has no toString(). Field ${field.name} got: ${String(value)}`); } - const s = v.toString().trim(); - if (!s) return this.setFieldValue(field, null); - if (Number.isFinite(Number(s))) return this.setFieldValue(field, s); - throw new Error(`Value is not a decimal. Field ${field.name} got: ${s} (object->string)`); + const string = value.toString().trim(); + if (!string) return this.setFieldValue(field, null); + if (Number.isFinite(Number(string))) return this.setFieldValue(field, string); + throw new Error(`Value is not a decimal. Field ${field.name} got: ${string} (object->string)`); } throw new Error(`Value is not a decimal. Field ${field.name} got: ${String(value)} (${typeof value})`); From 30c1fbdb0c2326539aa58790237bf606d5225491 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Mon, 5 Jan 2026 12:40:10 +0200 Subject: [PATCH 5/9] fix: enhance float value handling in setFieldValue method and simplify column type check in MongoConnector --- adminforth/dataConnectors/baseConnector.ts | 12 ++++++++---- adminforth/dataConnectors/mongo.ts | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/adminforth/dataConnectors/baseConnector.ts b/adminforth/dataConnectors/baseConnector.ts index 0f5868416..8acb56021 100644 --- a/adminforth/dataConnectors/baseConnector.ts +++ b/adminforth/dataConnectors/baseConnector.ts @@ -233,10 +233,14 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon // Float if (field.type === AdminForthDataTypes.FLOAT) { if (value === "" || value === null) return this.setFieldValue(field, null); - const number = - typeof value === "number" - ? value - : (typeof value === "object" && value !== null ? (value as any).valueOf() : NaN); + let number: any; + if (typeof value === "number") { + number = value; + } else if (typeof value === "object" && value !== null) { + number = (value as any).valueOf(); + } else { + number = NaN; + } if (typeof number !== "number" || !Number.isFinite(number)) { throw new Error( diff --git a/adminforth/dataConnectors/mongo.ts b/adminforth/dataConnectors/mongo.ts index d8470adc7..095d95e37 100644 --- a/adminforth/dataConnectors/mongo.ts +++ b/adminforth/dataConnectors/mongo.ts @@ -266,7 +266,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS return { $expr: { [mongoExprOp]: [left, right] } }; } const column = resource.dataSourceColumns.find((col) => col.name === (filter as IAdminForthSingleFilter).field); - if (column && [AdminForthDataTypes.INTEGER, AdminForthDataTypes.DECIMAL, AdminForthDataTypes.FLOAT].includes(column.type)) { + if ([AdminForthDataTypes.INTEGER, AdminForthDataTypes.DECIMAL, AdminForthDataTypes.FLOAT].includes(column.type)) { return { [(filter as IAdminForthSingleFilter).field]: this.OperatorsMap[filter.operator](+(filter as IAdminForthSingleFilter).value) }; } return { [(filter as IAdminForthSingleFilter).field]: this.OperatorsMap[filter.operator]((filter as IAdminForthSingleFilter).value) }; From ef7d9cefa8fc04021ccc45c257a595977cf5ee85 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Mon, 5 Jan 2026 12:54:06 +0200 Subject: [PATCH 6/9] fix: improve object type handling in normalizeMongoValue and setFieldValue methods --- adminforth/dataConnectors/baseConnector.ts | 4 ++-- adminforth/dataConnectors/mongo.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/adminforth/dataConnectors/baseConnector.ts b/adminforth/dataConnectors/baseConnector.ts index 8acb56021..5d1a25dee 100644 --- a/adminforth/dataConnectors/baseConnector.ts +++ b/adminforth/dataConnectors/baseConnector.ts @@ -236,7 +236,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon let number: any; if (typeof value === "number") { number = value; - } else if (typeof value === "object" && value !== null) { + } else if (typeof value === "object") { number = (value as any).valueOf(); } else { number = NaN; @@ -269,7 +269,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon throw new Error(`Value is not a decimal. Field ${field.name} got: ${value} (string)`); } - if (typeof value === "object" && value) { + if (typeof value === "object") { if (typeof value.toString !== "function") { throw new Error(`Decimal object has no toString(). Field ${field.name} got: ${String(value)}`); } diff --git a/adminforth/dataConnectors/mongo.ts b/adminforth/dataConnectors/mongo.ts index 095d95e37..83c57e114 100644 --- a/adminforth/dataConnectors/mongo.ts +++ b/adminforth/dataConnectors/mongo.ts @@ -12,7 +12,9 @@ const escapeRegex = (value) => { function normalizeMongoValue(v: any) { if (v == null) return v; if (v instanceof Decimal128) return v.toString(); + if (v instanceof Double) return v.valueOf(); if (typeof v === "object" && v.$numberDecimal) return String(v.$numberDecimal); + if (typeof v === "object" && v.$numberDouble) return Number(v.$numberDouble); return v; } From 46a96260f1414d701b9bed82470dc11d7cdd4436 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Mon, 5 Jan 2026 16:13:03 +0200 Subject: [PATCH 7/9] refactor: simplify value handling for integer, float, and decimal types across data connectors and components --- adminforth/dataConnectors/baseConnector.ts | 38 +++---------------- adminforth/dataConnectors/mongo.ts | 4 +- .../spa/src/components/ColumnValueInput.vue | 15 +++++++- adminforth/spa/src/components/Filters.vue | 8 ++-- .../spa/src/components/ResourceForm.vue | 8 ++-- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/adminforth/dataConnectors/baseConnector.ts b/adminforth/dataConnectors/baseConnector.ts index 5d1a25dee..958a0c860 100644 --- a/adminforth/dataConnectors/baseConnector.ts +++ b/adminforth/dataConnectors/baseConnector.ts @@ -224,62 +224,36 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon // Int if (field.type === AdminForthDataTypes.INTEGER) { if (value === "" || value === null) return this.setFieldValue(field, null); - if (!Number.isFinite(value) || Math.trunc(value) !== value) { + if (!Number.isFinite(value)) { throw new Error(`Value is not an integer. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`); } - return this.setFieldValue(field, Math.trunc(value)); + return this.setFieldValue(field, value); } // Float if (field.type === AdminForthDataTypes.FLOAT) { if (value === "" || value === null) return this.setFieldValue(field, null); - let number: any; - if (typeof value === "number") { - number = value; - } else if (typeof value === "object") { - number = (value as any).valueOf(); - } else { - number = NaN; - } - if (typeof number !== "number" || !Number.isFinite(number)) { + if (typeof value !== "number" || !Number.isFinite(value)) { throw new Error( `Value is not a float. Field ${field.name} with type is ${field.type}, but got value: ${String(value)} with type ${typeof value}` ); } - return this.setFieldValue(field, number); + return this.setFieldValue(field, value); } // Decimal if (field.type === AdminForthDataTypes.DECIMAL) { if (value === "" || value === null) return this.setFieldValue(field, null); - - if (typeof value === "number") { - if (!Number.isFinite(value)) { - throw new Error(`Value is not a decimal. Field ${field.name} got: ${value} (number)`); - } - return this.setFieldValue(field, value); - } - if (typeof value === "string") { const string = value.trim(); if (!string) return this.setFieldValue(field, null); if (Number.isFinite(Number(string))) return this.setFieldValue(field, string); - throw new Error(`Value is not a decimal. Field ${field.name} got: ${value} (string)`); - } - - if (typeof value === "object") { - if (typeof value.toString !== "function") { - throw new Error(`Decimal object has no toString(). Field ${field.name} got: ${String(value)}`); - } - const string = value.toString().trim(); - if (!string) return this.setFieldValue(field, null); - if (Number.isFinite(Number(string))) return this.setFieldValue(field, string); - throw new Error(`Value is not a decimal. Field ${field.name} got: ${string} (object->string)`); + throw new Error(`Value is not a decimal. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`); } - throw new Error(`Value is not a decimal. Field ${field.name} got: ${String(value)} (${typeof value})`); + throw new Error(`Value is not a decimal. Field ${field.name} with type is ${field.type}, but got value: ${String(value)} with type ${typeof value}`); } // Date diff --git a/adminforth/dataConnectors/mongo.ts b/adminforth/dataConnectors/mongo.ts index 83c57e114..cea1b9222 100644 --- a/adminforth/dataConnectors/mongo.ts +++ b/adminforth/dataConnectors/mongo.ts @@ -223,12 +223,12 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS if (field.type === AdminForthDataTypes.FLOAT) { if (value === "" || value === null) return null; - return Number.isFinite(value) ? new Double(value) : null; + return Number.isFinite(value) ? value : null; } if (field.type === AdminForthDataTypes.DECIMAL) { if (value === "" || value === null) return null; - return Decimal128.fromString(value.toString()); + return value.toString(); } return value; diff --git a/adminforth/spa/src/components/ColumnValueInput.vue b/adminforth/spa/src/components/ColumnValueInput.vue index 012f5bbae..452438839 100644 --- a/adminforth/spa/src/components/ColumnValueInput.vue +++ b/adminforth/spa/src/components/ColumnValueInput.vue @@ -86,7 +86,20 @@ :readonly="(column.editReadonly && source === 'edit') || readonly" /> +
@@ -133,14 +133,14 @@ type="number" aria-describedby="helper-text-explanation" :placeholder="$t('From')" - @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })" + @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })" :modelValue="getFilterItem({ column: c, operator: 'gte' })" />
diff --git a/adminforth/spa/src/components/ResourceForm.vue b/adminforth/spa/src/components/ResourceForm.vue index 94bd005d0..91cbbafd8 100644 --- a/adminforth/spa/src/components/ResourceForm.vue +++ b/adminforth/spa/src/components/ResourceForm.vue @@ -206,7 +206,7 @@ const setCurrentValue = (key: any, value: any, index = null) => { } else if (index === currentValues.value[key].length) { currentValues.value[key].push(null); } else { - if (['integer', 'float', 'decimal'].includes(col.isArray.itemType)) { + if (['integer', 'float'].includes(col.isArray.itemType)) { if (value || value === 0) { currentValues.value[key][index] = +value; } else { @@ -215,12 +215,12 @@ const setCurrentValue = (key: any, value: any, index = null) => { } else { currentValues.value[key][index] = value; } - if (col?.isArray && ['text', 'richtext', 'string'].includes(col.isArray.itemType) && col.enforceLowerCase) { + if (col?.isArray && ['text', 'richtext', 'string', 'decimal'].includes(col.isArray.itemType) && col.enforceLowerCase) { currentValues.value[key][index] = currentValues.value[key][index].toLowerCase(); } } } else { - if (col?.type && ['integer', 'float', 'decimal'].includes(col.type)) { + if (col?.type && ['integer', 'float'].includes(col.type)) { if (value || value === 0) { currentValues.value[key] = +value; } else { @@ -229,7 +229,7 @@ const setCurrentValue = (key: any, value: any, index = null) => { } else { currentValues.value[key] = value; } - if (col?.type && ['text', 'richtext', 'string'].includes(col?.type) && col.enforceLowerCase) { + if (col?.type && ['text', 'richtext', 'string', 'decimal'].includes(col?.type) && col.enforceLowerCase) { currentValues.value[key] = currentValues.value[key].toLowerCase(); } } From 454161fae39c65a1a26cda74da9eec59bc0ae6c3 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Mon, 5 Jan 2026 16:16:50 +0200 Subject: [PATCH 8/9] fix: update datetime value handling in setFieldValue method to retain original value --- adminforth/dataConnectors/baseConnector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adminforth/dataConnectors/baseConnector.ts b/adminforth/dataConnectors/baseConnector.ts index 958a0c860..77ab2d4fe 100644 --- a/adminforth/dataConnectors/baseConnector.ts +++ b/adminforth/dataConnectors/baseConnector.ts @@ -265,7 +265,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon if (!dayjs(value).isValid()) { throw new Error(`Value is not a valid datetime. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`); } - return this.setFieldValue(field, dayjs(value).toISOString()); + return this.setFieldValue(field, value); } // Time From edaf7645ddce3d48f348ea802926278d465901c9 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Mon, 5 Jan 2026 17:17:15 +0200 Subject: [PATCH 9/9] refactor: improve null value handling in validateAndSetFieldValue and normalizeMongoValue methods --- adminforth/dataConnectors/baseConnector.ts | 32 ++++++++++++++----- adminforth/dataConnectors/mongo.ts | 36 ++++++++++++++++------ 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/adminforth/dataConnectors/baseConnector.ts b/adminforth/dataConnectors/baseConnector.ts index 77ab2d4fe..7faddd4c7 100644 --- a/adminforth/dataConnectors/baseConnector.ts +++ b/adminforth/dataConnectors/baseConnector.ts @@ -223,7 +223,9 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon validateAndSetFieldValue(field: AdminForthResourceColumn, value: any): any { // Int if (field.type === AdminForthDataTypes.INTEGER) { - if (value === "" || value === null) return this.setFieldValue(field, null); + if (value === "" || value === null) { + return this.setFieldValue(field, null); + } if (!Number.isFinite(value)) { throw new Error(`Value is not an integer. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`); } @@ -232,7 +234,9 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon // Float if (field.type === AdminForthDataTypes.FLOAT) { - if (value === "" || value === null) return this.setFieldValue(field, null); + if (value === "" || value === null) { + return this.setFieldValue(field, null); + } if (typeof value !== "number" || !Number.isFinite(value)) { throw new Error( @@ -245,11 +249,17 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon // Decimal if (field.type === AdminForthDataTypes.DECIMAL) { - if (value === "" || value === null) return this.setFieldValue(field, null); + if (value === "" || value === null) { + return this.setFieldValue(field, null); + } if (typeof value === "string") { const string = value.trim(); - if (!string) return this.setFieldValue(field, null); - if (Number.isFinite(Number(string))) return this.setFieldValue(field, string); + if (!string) { + return this.setFieldValue(field, null); + } + if (Number.isFinite(Number(string))) { + return this.setFieldValue(field, string); + } throw new Error(`Value is not a decimal. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`); } @@ -261,7 +271,9 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon // DateTime if (field.type === AdminForthDataTypes.DATETIME) { - if (value === "" || value === null) return this.setFieldValue(field, null); + if (value === "" || value === null) { + return this.setFieldValue(field, null); + } if (!dayjs(value).isValid()) { throw new Error(`Value is not a valid datetime. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`); } @@ -272,7 +284,9 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon // Boolean if (field.type === AdminForthDataTypes.BOOLEAN) { - if (value === "" || value === null) return this.setFieldValue(field, null); + if (value === "" || value === null) { + return this.setFieldValue(field, null); + } if (typeof value !== 'boolean') { throw new Error(`Value is not a boolean. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`); } @@ -283,7 +297,9 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon // String if (field.type === AdminForthDataTypes.STRING) { - if (value === "" || value === null) return this.setFieldValue(field, null); + if (value === "" || value === null){ + return this.setFieldValue(field, null); + } } return this.setFieldValue(field, value); } diff --git a/adminforth/dataConnectors/mongo.ts b/adminforth/dataConnectors/mongo.ts index cea1b9222..7899f0333 100644 --- a/adminforth/dataConnectors/mongo.ts +++ b/adminforth/dataConnectors/mongo.ts @@ -10,11 +10,21 @@ const escapeRegex = (value) => { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escapes special characters }; function normalizeMongoValue(v: any) { - if (v == null) return v; - if (v instanceof Decimal128) return v.toString(); - if (v instanceof Double) return v.valueOf(); - if (typeof v === "object" && v.$numberDecimal) return String(v.$numberDecimal); - if (typeof v === "object" && v.$numberDouble) return Number(v.$numberDouble); + if (v == null) { + return v; + } + if (v instanceof Decimal128) { + return v.toString(); + } + if (v instanceof Double) { + return v.valueOf(); + } + if (typeof v === "object" && v.$numberDecimal) { + return String(v.$numberDecimal); + } + if (typeof v === "object" && v.$numberDouble) { + return Number(v.$numberDouble); + } return v; } @@ -212,22 +222,30 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS if (value === null) return null; if (field.type === AdminForthDataTypes.DATETIME) { - if (value === "" || value === null) return null; + if (value === "" || value === null) { + return null; + } return dayjs(value).isValid() ? dayjs(value).toDate() : null; } if (field.type === AdminForthDataTypes.INTEGER) { - if (value === "" || value === null) return null; + if (value === "" || value === null) { + return null; + } return Number.isFinite(value) ? Math.trunc(value) : null; } if (field.type === AdminForthDataTypes.FLOAT) { - if (value === "" || value === null) return null; + if (value === "" || value === null) { + return null; + } return Number.isFinite(value) ? value : null; } if (field.type === AdminForthDataTypes.DECIMAL) { - if (value === "" || value === null) return null; + if (value === "" || value === null) { + return null; + } return value.toString(); }