diff --git a/adminforth/dataConnectors/baseConnector.ts b/adminforth/dataConnectors/baseConnector.ts index c8e587a67..7faddd4c7 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,90 @@ 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)) { + 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, value); + } + + // Float + if (field.type === AdminForthDataTypes.FLOAT) { + if (value === "" || value === null) { + return this.setFieldValue(field, null); + } + + 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, value); + } + + // Decimal + if (field.type === AdminForthDataTypes.DECIMAL) { + 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); + } + 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} with type is ${field.type}, but got value: ${String(value)} with type ${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, value); + } + + // 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 +353,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 +410,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 6e1186172..7899f0333 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'; @@ -9,6 +9,24 @@ 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 (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; +} class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector { @@ -122,30 +140,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,20 +218,37 @@ 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) { + if (value === undefined) return undefined; + if (value === null) return null; + + if (field.type === AdminForthDataTypes.DATETIME) { + if (value === "" || value === null) { return null; } - return Decimal128.fromString(value?.toString()); + return dayjs(value).isValid() ? dayjs(value).toDate() : null; } + + if (field.type === AdminForthDataTypes.INTEGER) { + 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; + } + return Number.isFinite(value) ? value : null; + } + + if (field.type === AdminForthDataTypes.DECIMAL) { + if (value === "" || value === null) { + return null; + } + return value.toString(); + } + return value; } @@ -251,7 +286,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) }; @@ -313,13 +348,20 @@ 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] = await collection - .aggregate([ - { $group: { _id: null, min: { $min: `$${column}` }, max: { $max: `$${column}` } } }, - ]) - .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; } 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(); } }