Skip to content

Commit 2c449a4

Browse files
committed
fix: implement validateAndSetFieldValue method for improved data validation in AdminForthBaseConnector and update MongoConnector to utilize it
1 parent d2de4de commit 2c449a4

File tree

2 files changed

+123
-37
lines changed

2 files changed

+123
-37
lines changed

adminforth/dataConnectors/baseConnector.ts

Lines changed: 96 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,97 @@ 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) return this.setFieldValue(field, null);
227+
if (!Number.isFinite(value) || Math.trunc(value) !== value) {
228+
throw new Error(`Value is not an integer. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`);
229+
}
230+
return this.setFieldValue(field, Math.trunc(value));
231+
}
232+
233+
// Float
234+
if (field.type === AdminForthDataTypes.FLOAT) {
235+
if (value === "" || value === null) return this.setFieldValue(field, null);
236+
const n =
237+
typeof value === "number"
238+
? value
239+
: (typeof value === "object" && value !== null ? (value as any).valueOf() : NaN);
240+
241+
if (typeof n !== "number" || !Number.isFinite(n)) {
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, n);
248+
}
249+
250+
// Decimal
251+
if (field.type === AdminForthDataTypes.DECIMAL) {
252+
if (value === "" || value === null) return this.setFieldValue(field, null);
253+
254+
if (typeof value === "number") {
255+
if (!Number.isFinite(value)) {
256+
throw new Error(`Value is not a decimal. Field ${field.name} got: ${value} (number)`);
257+
}
258+
return this.setFieldValue(field, value);
259+
}
260+
261+
if (typeof value === "string") {
262+
const s = value.trim();
263+
if (!s) return this.setFieldValue(field, null);
264+
if (Number.isFinite(Number(s))) return this.setFieldValue(field, s);
265+
throw new Error(`Value is not a decimal. Field ${field.name} got: ${value} (string)`);
266+
}
267+
268+
if (typeof value === "object" && value) {
269+
const v: any = value;
270+
if (typeof v.toString !== "function") {
271+
throw new Error(`Decimal object has no toString(). Field ${field.name} got: ${String(value)}`);
272+
}
273+
const s = v.toString().trim();
274+
if (!s) return this.setFieldValue(field, null);
275+
if (Number.isFinite(Number(s))) return this.setFieldValue(field, s);
276+
throw new Error(`Value is not a decimal. Field ${field.name} got: ${s} (object->string)`);
277+
}
278+
279+
throw new Error(`Value is not a decimal. Field ${field.name} got: ${String(value)} (${typeof value})`);
280+
}
281+
282+
// Date
283+
284+
285+
// DateTime
286+
if (field.type === AdminForthDataTypes.DATETIME) {
287+
if (value === "" || value === null) return this.setFieldValue(field, null);
288+
if (!dayjs(value).isValid()) {
289+
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}`);
290+
}
291+
return this.setFieldValue(field, dayjs(value).toISOString());
292+
}
293+
294+
// Time
295+
296+
// Boolean
297+
if (field.type === AdminForthDataTypes.BOOLEAN) {
298+
if (value === "" || value === null) return this.setFieldValue(field, null);
299+
if (typeof value !== 'boolean') {
300+
throw new Error(`Value is not a boolean. Field ${field.name} with type is ${field.type}, but got value: ${value} with type ${typeof value}`);
301+
}
302+
return this.setFieldValue(field, value);
303+
}
304+
305+
// JSON
306+
307+
// String
308+
if (field.type === AdminForthDataTypes.STRING) {
309+
if (value === "" || value === null) return this.setFieldValue(field, null);
310+
}
311+
return this.setFieldValue(field, value);
312+
}
313+
222314
getMinMaxForColumnsWithOriginalTypes({ resource, columns }: { resource: AdminForthResource; columns: AdminForthResourceColumn[]; }): Promise<{ [key: string]: { min: any; max: any; }; }> {
223315
throw new Error('Method not implemented.');
224316
}
@@ -268,7 +360,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
268360
}
269361
if (filledRecord[col.name] !== undefined) {
270362
// no sense to set value if it is not defined
271-
recordWithOriginalValues[col.name] = this.setFieldValue(col, filledRecord[col.name]);
363+
recordWithOriginalValues[col.name] = this.validateAndSetFieldValue(col, filledRecord[col.name]);
272364
}
273365
}
274366

@@ -325,7 +417,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
325417
Update record received field '${field}' (with value ${newValues[field]}), but such column not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}
326418
`);
327419
}
328-
recordWithOriginalValues[col.name] = this.setFieldValue(col, newValues[col.name]);
420+
recordWithOriginalValues[col.name] = this.validateAndSetFieldValue(col, newValues[col.name]);
329421
}
330422
const record = await this.getRecordByPrimaryKey(resource, recordId);
331423
let error: string | null = null;

adminforth/dataConnectors/mongo.ts

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ 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) return v;
14+
if (v instanceof Decimal128) return v.toString();
15+
if (typeof v === "object" && v.$numberDecimal) return String(v.$numberDecimal);
16+
return v;
17+
}
1218

1319
class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector {
1420

@@ -208,27 +214,14 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
208214
return dayjs(value).isValid() ? dayjs(value).toDate() : null;
209215
}
210216

211-
if (field.type === AdminForthDataTypes.DATE) {
212-
if (value === "" || value === null) return null;
213-
const d = dayjs(value);
214-
return d.isValid() ? d.startOf("day").toDate() : null;
215-
}
216-
217-
if (field.type === AdminForthDataTypes.BOOLEAN) {
218-
if (value === "" || value === null) return null;
219-
return !!value;
220-
}
221-
222217
if (field.type === AdminForthDataTypes.INTEGER) {
223218
if (value === "" || value === null) return null;
224-
const n = typeof value === "number" ? value : Number(String(value).replace(",", "."));
225-
return Number.isFinite(n) ? Math.trunc(n) : null;
219+
return Number.isFinite(value) ? Math.trunc(value) : null;
226220
}
227221

228222
if (field.type === AdminForthDataTypes.FLOAT) {
229223
if (value === "" || value === null) return null;
230-
const n = typeof value === "number" ? value : Number(String(value).replace(",", "."));
231-
return Number.isFinite(n) ? new Double(n) : null;
224+
return Number.isFinite(value) ? new Double(value) : null;
232225
}
233226

234227
if (field.type === AdminForthDataTypes.DECIMAL) {
@@ -335,38 +328,39 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
335328
async getMinMaxForColumnsWithOriginalTypes({ resource, columns }) {
336329
const tableName = resource.table;
337330
const collection = this.client.db().collection(tableName);
338-
const result = {};
331+
const result: Record<string, { min: any; max: any }> = {};
332+
339333
for (const column of columns) {
340-
result[column.name] = await collection
341-
.aggregate([
342-
{ $group: { _id: null, min: { $min: `$${column.name}` }, max: { $max: `$${column.name}` } } },
343-
])
344-
.toArray();
334+
const [doc] = await collection
335+
.aggregate([
336+
{ $group: { _id: null, min: { $min: `$${column.name}` }, max: { $max: `$${column.name}` } } },
337+
{ $project: { _id: 0, min: 1, max: 1 } },
338+
])
339+
.toArray();
340+
341+
result[column.name] = {
342+
min: normalizeMongoValue(doc?.min),
343+
max: normalizeMongoValue(doc?.max),
344+
};
345345
}
346346
return result;
347347
}
348348

349349
async createRecordOriginalValues({ resource, record }): Promise<string> {
350-
const collection = this.client.db().collection(resource.table);
351-
const colsByName = new Map(resource.dataSourceColumns.map((c) => [c.name, c]));
350+
const tableName = resource.table;
351+
const collection = this.client.db().collection(tableName);
352+
const columns = Object.keys(record);
352353
const newRecord = {};
353-
for (const [key, raw] of Object.entries(record)) {
354-
const col = colsByName.get(key);
355-
newRecord[key] = col ? this.setFieldValue(col, raw) : raw;
354+
for (const colName of columns) {
355+
newRecord[colName] = record[colName];
356356
}
357357
const ret = await collection.insertOne(newRecord);
358358
return ret.insertedId;
359359
}
360360

361361
async updateRecordOriginalValues({ resource, recordId, newValues }) {
362362
const collection = this.client.db().collection(resource.table);
363-
const colsByName = new Map(resource.dataSourceColumns.map((c) => [c.name, c]));
364-
const updatedValues = {};
365-
for (const [key, raw] of Object.entries(newValues)) {
366-
const col = colsByName.get(key);
367-
updatedValues[key] = col ? this.setFieldValue(col, raw) : raw;
368-
}
369-
await collection.updateOne({ [this.getPrimaryKey(resource)]: recordId }, { $set: updatedValues });
363+
await collection.updateOne({ [this.getPrimaryKey(resource)]: recordId }, { $set: newValues });
370364
}
371365

372366
async deleteRecord({ resource, recordId }): Promise<boolean> {

0 commit comments

Comments
 (0)