Skip to content
Open
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
46 changes: 23 additions & 23 deletions adminforth/dataConnectors/baseConnector.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
AdminForthResource, IAdminForthDataSourceConnectorBase,
AdminForthResourceColumn,
IAdminForthSort, IAdminForthSingleFilter, IAdminForthAndOrFilter
IAdminForthSort, IAdminForthSingleFilter, IAdminForthAndOrFilter,
Filters
} from "../types/Back.js";


Expand Down Expand Up @@ -39,30 +40,30 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
limit: 1,
offset: 0,
sort: [],
filters: { operator: AdminForthFilterOperators.AND, subFilters: [{ field: this.getPrimaryKey(resource), operator: AdminForthFilterOperators.EQ, value: id }]},
filters: Filters.AND(Filters.EQ(this.getPrimaryKey(resource), id))
});
return data.length > 0 ? data[0] : null;
}

validateAndNormalizeInputFilters(filter: IAdminForthSingleFilter | IAdminForthAndOrFilter | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter> | undefined): IAdminForthAndOrFilter {
if (!filter) {
// if no filter, return empty "and" filter
return { operator: AdminForthFilterOperators.AND, subFilters: [] };
return Filters.AND();
}
if (typeof filter !== 'object') {
throw new Error(`Filter should be an array or an object`);
}
if (Array.isArray(filter)) {
// if filter is an array, combine them using "and" operator
return { operator: AdminForthFilterOperators.AND, subFilters: filter };
return Filters.AND(...filter);
}
if ((filter as IAdminForthAndOrFilter).subFilters) {
// if filter is already AndOr filter - return as is
return filter as IAdminForthAndOrFilter;
}

// by default, assume filter is Single filter, turn it into AndOr filter
return { operator: AdminForthFilterOperators.AND, subFilters: [filter] };
return Filters.AND(filter);
}

validateAndNormalizeFilters(filters: IAdminForthSingleFilter | IAdminForthAndOrFilter | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>, resource: AdminForthResource): { ok: boolean, error: string } {
Expand All @@ -82,12 +83,11 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
const column = resource.dataSourceColumns.find((col) => col.name == (f as IAdminForthSingleFilter).field);
// console.log(`\n~~~ column: ${JSON.stringify(column, null, 2)}\n~~~ resource.columns: ${JSON.stringify(resource.dataSourceColumns, null, 2)}\n~~~ filter: ${JSON.stringify(f, null, 2)}\n`);
if (column.isArray?.enabled && (column.enum || column.foreignResource)) {
filters[fIndex] = {
operator: AdminForthFilterOperators.OR,
subFilters: f.value.map((v: any) => {
return { field: column.name, operator: AdminForthFilterOperators.LIKE, value: v };
}),
};
filters[fIndex] = Filters.OR(
...f.value.map((v: any) => {
return Filters.LIKE(column.name, v);
})
);
}
}

Expand Down Expand Up @@ -318,13 +318,10 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
const primaryKeyField = this.getPrimaryKey(resource);
const existingRecord = await this.getData({
resource,
filters: {
operator: AdminForthFilterOperators.AND,
subFilters: [
{ field: column.name, operator: AdminForthFilterOperators.EQ, value },
...(record ? [{ field: primaryKeyField, operator: AdminForthFilterOperators.NE as AdminForthFilterOperators.NE, value: record[primaryKeyField] }] : [])
]
},
filters: Filters.AND(
Filters.EQ(column.name, value),
...(record ? [Filters.NEQ(primaryKeyField, record[primaryKeyField])] : [])
),
limit: 1,
sort: [],
offset: 0,
Expand Down Expand Up @@ -489,11 +486,14 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon

getRecordByPrimaryKey(resource: AdminForthResource, recordId: string): Promise<any> {
return this.getRecordByPrimaryKeyWithOriginalTypes(resource, recordId).then((record) => {
const newRecord = {};
for (const col of resource.dataSourceColumns) {
newRecord[col.name] = this.getFieldValue(col, record[col.name]);
}
return newRecord;
if (!record) {
return null;
}
const newRecord = {};
for (const col of resource.dataSourceColumns) {
newRecord[col.name] = this.getFieldValue(col, record[col.name]);
}
return newRecord;
});
}
async getAllTables(): Promise<string[]> {
Expand Down
170 changes: 101 additions & 69 deletions adminforth/dataConnectors/mongo.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import dayjs from 'dayjs';
import { MongoClient } from 'mongodb';
import { Decimal128, Double } from 'bson';
import { IAdminForthDataSourceConnector, IAdminForthSingleFilter, IAdminForthAndOrFilter, AdminForthResource } from '../types/Back.js';
import { MongoClient, BSON, ObjectId, Decimal128, Double, UUID } from 'mongodb';
import { IAdminForthDataSourceConnector, IAdminForthSingleFilter, IAdminForthAndOrFilter, AdminForthResource, Filters } from '../types/Back.js';
import AdminForthBaseConnector from './baseConnector.js';

import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, } from '../types/Common.js';

const UUID36 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const HEX24 = /^[0-9a-f]{24}$/i; // 24-hex (Mongo ObjectId)

function idToString(v: any) {
if (v == null) return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

== is very implicit, better use !v maybe or

=== null || v === undefined return v

if (typeof v === "string" || typeof v === "number" || typeof v === "bigint") return String(v);

const s = BSON.EJSON.serialize(v);
if (s && typeof s === "object") {
if ("$oid" in s) {
return String(s.$oid);
}
if ("$uuid" in s) {
return String(s.$uuid);
}
}
return String(v);
}

const extractSimplePkEq = (f: any, pk: string): string | null => {
while (f?.subFilters?.length === 1) f = f.subFilters[0];
return (f?.operator === AdminForthFilterOperators.EQ && f?.field === pk && f.value != null && typeof f.value !== "object")
? String(f.value)
: null;
};

const escapeRegex = (value) => {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escapes special characters
};

function normalizeMongoValue(v: any) {
if (v == null) {
return v;
Expand All @@ -29,7 +54,13 @@ function normalizeMongoValue(v: any) {
}

class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector {

private pkCandidates(pkValue: any): any[] {
if (pkValue == null || typeof pkValue !== "string") return [pkValue];
const candidates: any[] = [pkValue];
try { candidates.push(new UUID(pkValue)); } catch(err) { console.error(`Failed to create UUID from ${pkValue}: ${err.message}`); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NoOne7135 console.error? so if user uses own string ids like "asdfa1231" we will say him "Error"?

try { candidates.push(new ObjectId(pkValue)); } catch(err) { console.error(`Failed to create ObjectId from ${pkValue}: ${err.message}`); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NoOne7135 are you sure logging is needed in both catch blocks? I suppose if there is uuid it might be not object id right?

return candidates;
}
async setupClient(url): Promise<void> {
this.client = new MongoClient(url);
(async () => {
Expand Down Expand Up @@ -183,72 +214,45 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
}, {});
}

getPrimaryKey(resource) {
for (const col of resource.dataSourceColumns) {
if (col.primaryKey) {
return col.name;
}
}
}

getFieldValue(field, value) {
if (field.type == AdminForthDataTypes.DATETIME) {
if (!value) {
return null;
}
return dayjs(Date.parse(value)).toISOString();

} else if (field.type == AdminForthDataTypes.DATE) {
if (!value) {
return null;
}
return dayjs(Date.parse(value)).toISOString().split('T')[0];

} else if (field.type == AdminForthDataTypes.BOOLEAN) {
return value === null ? null : !!value;
} else if (field.type == AdminForthDataTypes.DECIMAL) {
if (value === null || value === undefined) {
return null;
}
return value?.toString();
if (field.type === AdminForthDataTypes.DATETIME) {
return value ? dayjs(Date.parse(value)).toISOString() : null;
}
if (field.type === AdminForthDataTypes.DATE) {
return value ? dayjs(Date.parse(value)).toISOString().split("T")[0] : null;
}
if (field.type === AdminForthDataTypes.BOOLEAN) {
return value === null ? null : !!value;
}
if (field.type === AdminForthDataTypes.DECIMAL) {
return value === null || value === undefined ? null : value.toString();
}
if (field.name === '_id') {
return idToString(value);
}

return value;
}


setFieldValue(field, value) {
if (value === undefined) return undefined;
if (value === null) return null;

if (value === undefined) {
return undefined;
}
if (value === null || value === '') {
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.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 @@ -299,34 +303,54 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
.map((f) => this.getFilterQuery(resource, f)));
}

async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }:
{
resource: AdminForthResource,
limit: number,
offset: number,
sort: { field: string, direction: AdminForthSortDirections }[],
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }:
{
resource: AdminForthResource,
limit: number,
offset: number,
sort: { field: string, direction: AdminForthSortDirections }[],
filters: IAdminForthAndOrFilter,
}
): Promise<any[]> {

// const columns = resource.dataSourceColumns.filter(c=> !c.virtual).map((col) => col.name).join(', ');
const tableName = resource.table;
const collection = this.client.db().collection(tableName);


const collection = this.client.db().collection(tableName);
const pk = this.getPrimaryKey(resource);
const pkValue = extractSimplePkEq(filters, pk);

if (pkValue !== null) {
let res = await collection.find({ [pk]: pkValue }).limit(1).toArray();
if (res.length) {
return res;
}
if (UUID36.test(pkValue)) {
res = await collection.find({ [pk]: new UUID(pkValue) }).limit(1).toArray();
}
if (res.length) {
return res;
}
if (HEX24.test(pkValue)) {
res = await collection.find({ [pk]: new ObjectId(pkValue) }).limit(1).toArray();
}
if (res.length) {
return res;
}

return [];
}

const query = filters.subFilters.length ? this.getFilterQuery(resource, filters) : {};

const sortArray: any[] = sort.map((s) => {
return [s.field, this.SortDirectionsMap[s.direction]];
});
const sortArray: any[] = sort.map((s) => [s.field, this.SortDirectionsMap[s.direction]]);

const result = await collection.find(query)
return await collection.find(query)
.sort(sortArray)
.skip(offset)
.limit(limit)
.toArray();

return result
}

async getCount({ resource, filters }: {
Expand Down Expand Up @@ -380,14 +404,22 @@ 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 pk = this.getPrimaryKey(resource);
for (const id of this.pkCandidates(recordId)) {
const res = await collection.updateOne({ [pk]: id }, { $set: newValues });
if (res.matchedCount > 0) return;
}
throw new Error(`Record with id ${recordId} not found in resource ${resource.name}`);
}

async deleteRecord({ resource, recordId }): Promise<boolean> {
const primaryKey = this.getPrimaryKey(resource);
const collection = this.client.db().collection(resource.table);
const res = await collection.deleteOne({ [primaryKey]: recordId });
return res.deletedCount > 0;
const pk = this.getPrimaryKey(resource);
for (const id of this.pkCandidates(recordId)) {
const res = await collection.deleteOne({ [pk]: id });
if (res.deletedCount > 0) return true;
}
return false;
}

async close() {
Expand Down
4 changes: 2 additions & 2 deletions dev-demo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import cars_MyS_resource from './resources/cars_MyS.js';
import cars_PG_resource from './resources/cars_PG.js';
import cars_Mongo_resource from './resources/cars_mongo.js';
import cars_Ch_resource from './resources/cars_Ch.js';

import { ObjectId } from 'mongodb';
import auditLogsResource from "./resources/auditLogs.js"
import { FICTIONAL_CAR_BRANDS, FICTIONAL_CAR_MODELS_BY_BRAND, ENGINE_TYPES, BODY_TYPES } from './custom/cars_data.js';
import passkeysResource from './resources/passkeys.js';
Expand Down Expand Up @@ -216,7 +216,7 @@ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
for (let i = 0; i < 100; i++) {
const engine_type = ENGINE_TYPES[Math.floor(Math.random() * ENGINE_TYPES.length)].value;
await admin.resource('cars_mongo').create({
_id: `${i}`,
_id: new ObjectId(),
model: `${FICTIONAL_CAR_BRANDS[Math.floor(Math.random() * FICTIONAL_CAR_BRANDS.length)]} ${FICTIONAL_CAR_MODELS_BY_BRAND[FICTIONAL_CAR_BRANDS[Math.floor(Math.random() * FICTIONAL_CAR_BRANDS.length)]][Math.floor(Math.random() * 4)]}`,
price: Decimal(Math.random() * 10000).toFixed(2),
engine_type: engine_type,
Expand Down
2 changes: 1 addition & 1 deletion dev-demo/resources/carsResourseTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function carsResourseTemplate(resourceId: string, dataSource: str
inputSuffix: 'USD',
allowMinMaxQuery: true,
editingNote: 'Price is in USD',
type: AdminForthDataTypes.FLOAT,
type: AdminForthDataTypes.DECIMAL,
required: true,
},
{
Expand Down