Skip to content

Commit 18d2854

Browse files
authored
Merge pull request #438 from devforth/feature/AdminForth/1119/mongo-error
fix: enhance data type handling in MongoConnector and add support for…
2 parents 875bd25 + edaf764 commit 18d2854

File tree

5 files changed

+195
-55
lines changed

5 files changed

+195
-55
lines changed

adminforth/dataConnectors/baseConnector.ts

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { suggestIfTypo } from "../modules/utils.js";
1010
import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections } from "../types/Common.js";
1111
import { randomUUID } from "crypto";
12+
import dayjs from "dayjs";
1213

1314

1415
export default class AdminForthBaseConnector implements IAdminForthDataSourceConnectorBase {
@@ -164,9 +165,9 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
164165
return { ok: false, error: `Value for operator '${filters.operator}' should not be empty array, in filter object: ${JSON.stringify(filters) }` };
165166
}
166167
}
167-
filters.value = filters.value.map((val: any) => this.setFieldValue(fieldObj, val));
168+
filters.value = filters.value.map((val: any) => this.validateAndSetFieldValue(fieldObj, val));
168169
} else {
169-
filtersAsSingle.value = this.setFieldValue(fieldObj, filtersAsSingle.value);
170+
filtersAsSingle.value = this.validateAndSetFieldValue(fieldObj, filtersAsSingle.value);
170171
}
171172
} else if (filtersAsSingle.insecureRawSQL || filtersAsSingle.insecureRawNoSQL) {
172173
// if "insecureRawSQL" filter is insecure sql string
@@ -219,6 +220,90 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
219220
throw new Error('Method not implemented.');
220221
}
221222

223+
validateAndSetFieldValue(field: AdminForthResourceColumn, value: any): any {
224+
// Int
225+
if (field.type === AdminForthDataTypes.INTEGER) {
226+
if (value === "" || value === null) {
227+
return this.setFieldValue(field, null);
228+
}
229+
if (!Number.isFinite(value)) {
230+
throw new Error(`Value is not an integer. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`);
231+
}
232+
return this.setFieldValue(field, value);
233+
}
234+
235+
// Float
236+
if (field.type === AdminForthDataTypes.FLOAT) {
237+
if (value === "" || value === null) {
238+
return this.setFieldValue(field, null);
239+
}
240+
241+
if (typeof value !== "number" || !Number.isFinite(value)) {
242+
throw new Error(
243+
`Value is not a float. Field ${field.name} with type is ${field.type}, but got value: ${String(value)} with type ${typeof value}`
244+
);
245+
}
246+
247+
return this.setFieldValue(field, value);
248+
}
249+
250+
// Decimal
251+
if (field.type === AdminForthDataTypes.DECIMAL) {
252+
if (value === "" || value === null) {
253+
return this.setFieldValue(field, null);
254+
}
255+
if (typeof value === "string") {
256+
const string = value.trim();
257+
if (!string) {
258+
return this.setFieldValue(field, null);
259+
}
260+
if (Number.isFinite(Number(string))) {
261+
return this.setFieldValue(field, string);
262+
}
263+
throw new Error(`Value is not a decimal. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`);
264+
}
265+
266+
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}`);
267+
}
268+
269+
// Date
270+
271+
272+
// DateTime
273+
if (field.type === AdminForthDataTypes.DATETIME) {
274+
if (value === "" || value === null) {
275+
return this.setFieldValue(field, null);
276+
}
277+
if (!dayjs(value).isValid()) {
278+
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}`);
279+
}
280+
return this.setFieldValue(field, value);
281+
}
282+
283+
// Time
284+
285+
// Boolean
286+
if (field.type === AdminForthDataTypes.BOOLEAN) {
287+
if (value === "" || value === null) {
288+
return this.setFieldValue(field, null);
289+
}
290+
if (typeof value !== 'boolean') {
291+
throw new Error(`Value is not a boolean. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`);
292+
}
293+
return this.setFieldValue(field, value);
294+
}
295+
296+
// JSON
297+
298+
// String
299+
if (field.type === AdminForthDataTypes.STRING) {
300+
if (value === "" || value === null){
301+
return this.setFieldValue(field, null);
302+
}
303+
}
304+
return this.setFieldValue(field, value);
305+
}
306+
222307
getMinMaxForColumnsWithOriginalTypes({ resource, columns }: { resource: AdminForthResource; columns: AdminForthResourceColumn[]; }): Promise<{ [key: string]: { min: any; max: any; }; }> {
223308
throw new Error('Method not implemented.');
224309
}
@@ -268,7 +353,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
268353
}
269354
if (filledRecord[col.name] !== undefined) {
270355
// no sense to set value if it is not defined
271-
recordWithOriginalValues[col.name] = this.setFieldValue(col, filledRecord[col.name]);
356+
recordWithOriginalValues[col.name] = this.validateAndSetFieldValue(col, filledRecord[col.name]);
272357
}
273358
}
274359

@@ -325,7 +410,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
325410
Update record received field '${field}' (with value ${newValues[field]}), but such column not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}
326411
`);
327412
}
328-
recordWithOriginalValues[col.name] = this.setFieldValue(col, newValues[col.name]);
413+
recordWithOriginalValues[col.name] = this.validateAndSetFieldValue(col, newValues[col.name]);
329414
}
330415
const record = await this.getRecordByPrimaryKey(resource, recordId);
331416
let error: string | null = null;

adminforth/dataConnectors/mongo.ts

Lines changed: 84 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dayjs from 'dayjs';
22
import { MongoClient } from 'mongodb';
3-
import { Decimal128 } from 'bson';
3+
import { Decimal128, Double } from 'bson';
44
import { IAdminForthDataSourceConnector, IAdminForthSingleFilter, IAdminForthAndOrFilter, AdminForthResource } from '../types/Back.js';
55
import AdminForthBaseConnector from './baseConnector.js';
66

@@ -9,6 +9,24 @@ import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirection
99
const escapeRegex = (value) => {
1010
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escapes special characters
1111
};
12+
function normalizeMongoValue(v: any) {
13+
if (v == null) {
14+
return v;
15+
}
16+
if (v instanceof Decimal128) {
17+
return v.toString();
18+
}
19+
if (v instanceof Double) {
20+
return v.valueOf();
21+
}
22+
if (typeof v === "object" && v.$numberDecimal) {
23+
return String(v.$numberDecimal);
24+
}
25+
if (typeof v === "object" && v.$numberDouble) {
26+
return Number(v.$numberDouble);
27+
}
28+
return v;
29+
}
1230

1331
class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector {
1432

@@ -122,30 +140,30 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
122140
}
123141

124142
return Array.from(fieldTypes.entries()).map(([name, types]) => {
125-
const primaryKey = name === '_id';
126-
127-
const priority = ['datetime', 'date', 'integer', 'float', 'boolean', 'json', 'decimal', 'string'];
128-
129-
const matched = priority.find(t => types.has(t)) || 'string';
130-
131-
const typeMap: Record<string, string> = {
132-
string: 'STRING',
133-
integer: 'INTEGER',
134-
float: 'FLOAT',
135-
boolean: 'BOOLEAN',
136-
datetime: 'DATETIME',
137-
date: 'DATE',
138-
json: 'JSON',
139-
decimal: 'DECIMAL',
140-
};
141-
return {
142-
name,
143-
type: typeMap[matched] ?? 'STRING',
144-
...(primaryKey ? { isPrimaryKey: true } : {}),
145-
sampleValue: sampleValues.get(name),
146-
};
143+
const primaryKey = name === '_id';
144+
145+
const priority = ['datetime', 'date', 'decimal', 'integer', 'float', 'boolean', 'json', 'string'];
146+
147+
const matched = priority.find(t => types.has(t)) || 'string';
148+
149+
const typeMap: Record<string, AdminForthDataTypes> = {
150+
string: AdminForthDataTypes.STRING,
151+
integer: AdminForthDataTypes.INTEGER,
152+
float: AdminForthDataTypes.FLOAT,
153+
boolean: AdminForthDataTypes.BOOLEAN,
154+
datetime: AdminForthDataTypes.DATETIME,
155+
date: AdminForthDataTypes.DATE,
156+
json: AdminForthDataTypes.JSON,
157+
decimal: AdminForthDataTypes.DECIMAL,
158+
};
159+
return {
160+
name,
161+
type: typeMap[matched] ?? AdminForthDataTypes.STRING,
162+
...(primaryKey ? { isPrimaryKey: true } : {}),
163+
sampleValue: sampleValues.get(name),
164+
};
147165
});
148-
}
166+
}
149167

150168

151169
async discoverFields(resource) {
@@ -200,20 +218,37 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
200218

201219

202220
setFieldValue(field, value) {
203-
if (field.type == AdminForthDataTypes.DATETIME) {
204-
if (!value) {
205-
return null;
206-
}
207-
return dayjs(value).toDate();
208-
209-
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
210-
return value === null ? null : (value ? true : false);
211-
} else if (field.type == AdminForthDataTypes.DECIMAL) {
212-
if (value === null || value === undefined) {
221+
if (value === undefined) return undefined;
222+
if (value === null) return null;
223+
224+
if (field.type === AdminForthDataTypes.DATETIME) {
225+
if (value === "" || value === null) {
213226
return null;
214227
}
215-
return Decimal128.fromString(value?.toString());
228+
return dayjs(value).isValid() ? dayjs(value).toDate() : null;
216229
}
230+
231+
if (field.type === AdminForthDataTypes.INTEGER) {
232+
if (value === "" || value === null) {
233+
return null;
234+
}
235+
return Number.isFinite(value) ? Math.trunc(value) : null;
236+
}
237+
238+
if (field.type === AdminForthDataTypes.FLOAT) {
239+
if (value === "" || value === null) {
240+
return null;
241+
}
242+
return Number.isFinite(value) ? value : null;
243+
}
244+
245+
if (field.type === AdminForthDataTypes.DECIMAL) {
246+
if (value === "" || value === null) {
247+
return null;
248+
}
249+
return value.toString();
250+
}
251+
217252
return value;
218253
}
219254

@@ -251,7 +286,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
251286
return { $expr: { [mongoExprOp]: [left, right] } };
252287
}
253288
const column = resource.dataSourceColumns.find((col) => col.name === (filter as IAdminForthSingleFilter).field);
254-
if (['integer', 'decimal', 'float'].includes(column.type)) {
289+
if ([AdminForthDataTypes.INTEGER, AdminForthDataTypes.DECIMAL, AdminForthDataTypes.FLOAT].includes(column.type)) {
255290
return { [(filter as IAdminForthSingleFilter).field]: this.OperatorsMap[filter.operator](+(filter as IAdminForthSingleFilter).value) };
256291
}
257292
return { [(filter as IAdminForthSingleFilter).field]: this.OperatorsMap[filter.operator]((filter as IAdminForthSingleFilter).value) };
@@ -313,13 +348,20 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
313348
async getMinMaxForColumnsWithOriginalTypes({ resource, columns }) {
314349
const tableName = resource.table;
315350
const collection = this.client.db().collection(tableName);
316-
const result = {};
351+
const result: Record<string, { min: any; max: any }> = {};
352+
317353
for (const column of columns) {
318-
result[column] = await collection
319-
.aggregate([
320-
{ $group: { _id: null, min: { $min: `$${column}` }, max: { $max: `$${column}` } } },
321-
])
322-
.toArray();
354+
const [doc] = await collection
355+
.aggregate([
356+
{ $group: { _id: null, min: { $min: `$${column.name}` }, max: { $max: `$${column.name}` } } },
357+
{ $project: { _id: 0, min: 1, max: 1 } },
358+
])
359+
.toArray();
360+
361+
result[column.name] = {
362+
min: normalizeMongoValue(doc?.min),
363+
max: normalizeMongoValue(doc?.max),
364+
};
323365
}
324366
return result;
325367
}

adminforth/spa/src/components/ColumnValueInput.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,20 @@
8686
:readonly="(column.editReadonly && source === 'edit') || readonly"
8787
/>
8888
<Input
89-
v-else-if="['decimal', 'float'].includes(type || column.type)"
89+
v-else-if="(type || column.type) === 'decimal'"
90+
ref="input"
91+
type="number"
92+
inputmode="decimal"
93+
class="w-40"
94+
placeholder="0.0"
95+
:fullWidth="true"
96+
:prefix="column.inputPrefix"
97+
:suffix="column.inputSuffix"
98+
:modelValue="String(value)"
99+
@update:modelValue="$emit('update:modelValue', String($event))"
100+
/>
101+
<Input
102+
v-else-if="(type || column.type) === 'float'"
90103
ref="input"
91104
type="number"
92105
step="0.1"

adminforth/spa/src/components/Filters.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,24 +123,24 @@
123123
:min="getFilterMinValue(c.name)"
124124
:max="getFilterMaxValue(c.name)"
125125
:valueStart="getFilterItem({ column: c, operator: 'gte' })"
126-
@update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
126+
@update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
127127
:valueEnd="getFilterItem({ column: c, operator: 'lte' })"
128-
@update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
128+
@update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
129129
/>
130130

131131
<div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
132132
<Input
133133
type="number"
134134
aria-describedby="helper-text-explanation"
135135
:placeholder="$t('From')"
136-
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
136+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
137137
:modelValue="getFilterItem({ column: c, operator: 'gte' })"
138138
/>
139139
<Input
140140
type="number"
141141
aria-describedby="helper-text-explanation"
142142
:placeholder="$t('To')"
143-
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
143+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
144144
:modelValue="getFilterItem({ column: c, operator: 'lte' })"
145145
/>
146146
</div>

adminforth/spa/src/components/ResourceForm.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ const setCurrentValue = (key: any, value: any, index = null) => {
206206
} else if (index === currentValues.value[key].length) {
207207
currentValues.value[key].push(null);
208208
} else {
209-
if (['integer', 'float', 'decimal'].includes(col.isArray.itemType)) {
209+
if (['integer', 'float'].includes(col.isArray.itemType)) {
210210
if (value || value === 0) {
211211
currentValues.value[key][index] = +value;
212212
} else {
@@ -215,12 +215,12 @@ const setCurrentValue = (key: any, value: any, index = null) => {
215215
} else {
216216
currentValues.value[key][index] = value;
217217
}
218-
if (col?.isArray && ['text', 'richtext', 'string'].includes(col.isArray.itemType) && col.enforceLowerCase) {
218+
if (col?.isArray && ['text', 'richtext', 'string', 'decimal'].includes(col.isArray.itemType) && col.enforceLowerCase) {
219219
currentValues.value[key][index] = currentValues.value[key][index].toLowerCase();
220220
}
221221
}
222222
} else {
223-
if (col?.type && ['integer', 'float', 'decimal'].includes(col.type)) {
223+
if (col?.type && ['integer', 'float'].includes(col.type)) {
224224
if (value || value === 0) {
225225
currentValues.value[key] = +value;
226226
} else {
@@ -229,7 +229,7 @@ const setCurrentValue = (key: any, value: any, index = null) => {
229229
} else {
230230
currentValues.value[key] = value;
231231
}
232-
if (col?.type && ['text', 'richtext', 'string'].includes(col?.type) && col.enforceLowerCase) {
232+
if (col?.type && ['text', 'richtext', 'string', 'decimal'].includes(col?.type) && col.enforceLowerCase) {
233233
currentValues.value[key] = currentValues.value[key].toLowerCase();
234234
}
235235
}

0 commit comments

Comments
 (0)