Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 89 additions & 4 deletions adminforth/dataConnectors/baseConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.');
}
Expand Down Expand Up @@ -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]);
}
}

Expand Down Expand Up @@ -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;
Expand Down
126 changes: 84 additions & 42 deletions adminforth/dataConnectors/mongo.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 {

Expand Down Expand Up @@ -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> = {
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: 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) {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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) };
Expand Down Expand Up @@ -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<string, { min: any; max: any }> = {};

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;
}
Expand Down
15 changes: 14 additions & 1 deletion adminforth/spa/src/components/ColumnValueInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,20 @@
:readonly="(column.editReadonly && source === 'edit') || readonly"
/>
<Input
v-else-if="['decimal', 'float'].includes(type || column.type)"
v-else-if="(type || column.type) === 'decimal'"
ref="input"
type="number"
inputmode="decimal"
class="w-40"
placeholder="0.0"
:fullWidth="true"
:prefix="column.inputPrefix"
:suffix="column.inputSuffix"
:modelValue="String(value)"
@update:modelValue="$emit('update:modelValue', String($event))"
/>
<Input
v-else-if="(type || column.type) === 'float'"
ref="input"
type="number"
step="0.1"
Expand Down
8 changes: 4 additions & 4 deletions adminforth/spa/src/components/Filters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,24 +123,24 @@
:min="getFilterMinValue(c.name)"
:max="getFilterMaxValue(c.name)"
:valueStart="getFilterItem({ column: c, operator: 'gte' })"
@update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
@update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
:valueEnd="getFilterItem({ column: c, operator: 'lte' })"
@update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
@update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
/>

<div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
<Input
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' })"
/>
<Input
type="number"
aria-describedby="helper-text-explanation"
:placeholder="$t('To')"
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
:modelValue="getFilterItem({ column: c, operator: 'lte' })"
/>
</div>
Expand Down
8 changes: 4 additions & 4 deletions adminforth/spa/src/components/ResourceForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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();
}
}
Expand Down