Skip to content

Commit 80a250f

Browse files
committed
refactor: streamline filter handling and improve ID conversion in Mongo connector
1 parent be497f3 commit 80a250f

File tree

2 files changed

+106
-108
lines changed

2 files changed

+106
-108
lines changed

adminforth/dataConnectors/baseConnector.ts

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {
22
AdminForthResource, IAdminForthDataSourceConnectorBase,
33
AdminForthResourceColumn,
4-
IAdminForthSort, IAdminForthSingleFilter, IAdminForthAndOrFilter
4+
IAdminForthSort, IAdminForthSingleFilter, IAdminForthAndOrFilter,
5+
Filters
56
} from "../types/Back.js";
67

78

@@ -39,37 +40,30 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
3940
limit: 1,
4041
offset: 0,
4142
sort: [],
42-
filters: {
43-
operator: AdminForthFilterOperators.AND,
44-
subFilters: [{
45-
field: this.getPrimaryKey(resource),
46-
operator: AdminForthFilterOperators.EQ,
47-
value: this.setFieldValue(resource.dataSourceColumns.find((col) => col.name === this.getPrimaryKey(resource)), id),
48-
}],
49-
},
43+
filters: Filters.AND(Filters.EQ(this.getPrimaryKey(resource), id))
5044
});
5145
return data.length > 0 ? data[0] : null;
5246
}
5347

5448
validateAndNormalizeInputFilters(filter: IAdminForthSingleFilter | IAdminForthAndOrFilter | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter> | undefined): IAdminForthAndOrFilter {
5549
if (!filter) {
5650
// if no filter, return empty "and" filter
57-
return { operator: AdminForthFilterOperators.AND, subFilters: [] };
51+
return Filters.AND();
5852
}
5953
if (typeof filter !== 'object') {
6054
throw new Error(`Filter should be an array or an object`);
6155
}
6256
if (Array.isArray(filter)) {
6357
// if filter is an array, combine them using "and" operator
64-
return { operator: AdminForthFilterOperators.AND, subFilters: filter };
58+
return Filters.AND(...filter);
6559
}
6660
if ((filter as IAdminForthAndOrFilter).subFilters) {
6761
// if filter is already AndOr filter - return as is
6862
return filter as IAdminForthAndOrFilter;
6963
}
7064

7165
// by default, assume filter is Single filter, turn it into AndOr filter
72-
return { operator: AdminForthFilterOperators.AND, subFilters: [filter] };
66+
return Filters.AND(filter);
7367
}
7468

7569
validateAndNormalizeFilters(filters: IAdminForthSingleFilter | IAdminForthAndOrFilter | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>, resource: AdminForthResource): { ok: boolean, error: string } {
@@ -89,12 +83,11 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
8983
const column = resource.dataSourceColumns.find((col) => col.name == (f as IAdminForthSingleFilter).field);
9084
// 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`);
9185
if (column.isArray?.enabled && (column.enum || column.foreignResource)) {
92-
filters[fIndex] = {
93-
operator: AdminForthFilterOperators.OR,
94-
subFilters: f.value.map((v: any) => {
95-
return { field: column.name, operator: AdminForthFilterOperators.LIKE, value: v };
96-
}),
97-
};
86+
filters[fIndex] = Filters.OR(
87+
...f.value.map((v: any) => {
88+
return Filters.LIKE(column.name, v);
89+
})
90+
);
9891
}
9992
}
10093

@@ -325,13 +318,10 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
325318
const primaryKeyField = this.getPrimaryKey(resource);
326319
const existingRecord = await this.getData({
327320
resource,
328-
filters: {
329-
operator: AdminForthFilterOperators.AND,
330-
subFilters: [
331-
{ field: column.name, operator: AdminForthFilterOperators.EQ, value: this.setFieldValue(column, value) },
332-
...(record ? [{ field: primaryKeyField, operator: AdminForthFilterOperators.NE as AdminForthFilterOperators.NE, value: record[primaryKeyField] }] : [])
333-
]
334-
},
321+
filters: Filters.AND(
322+
Filters.EQ(column.name, value),
323+
...(record ? [Filters.NEQ(primaryKeyField, record[primaryKeyField])] : [])
324+
),
335325
limit: 1,
336326
sort: [],
337327
offset: 0,

adminforth/dataConnectors/mongo.ts

Lines changed: 91 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,39 @@
11
import dayjs from 'dayjs';
2-
import { MongoClient } from 'mongodb';
3-
import { Decimal128, Double, ObjectId } from 'bson';
4-
import { IAdminForthDataSourceConnector, IAdminForthSingleFilter, IAdminForthAndOrFilter, AdminForthResource } from '../types/Back.js';
2+
import { MongoClient, BSON, ObjectId, Decimal128, Double, UUID } from 'mongodb';
3+
import { IAdminForthDataSourceConnector, IAdminForthSingleFilter, IAdminForthAndOrFilter, AdminForthResource, Filters } from '../types/Back.js';
54
import AdminForthBaseConnector from './baseConnector.js';
6-
75
import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, } from '../types/Common.js';
86

7+
function idToString(v: any) {
8+
if (v == null) return null;
9+
if (typeof v === "string" || typeof v === "number" || typeof v === "bigint") return String(v);
10+
11+
const s = BSON.EJSON.serialize(v);
12+
if (s && typeof s === "object") {
13+
if ("$oid" in s) {
14+
return String(s.$oid);
15+
}
16+
if ("$uuid" in s) {
17+
return String(s.$uuid);
18+
}
19+
return String(v);
20+
}
21+
}
22+
23+
const extractSimplePkEq = (filters: IAdminForthAndOrFilter, pk: string): string | null => {
24+
if (!filters?.subFilters?.length) return null;
25+
if (filters.operator !== AdminForthFilterOperators.AND) return null;
26+
if (filters.subFilters.length !== 1) return null;
27+
28+
const f: any = filters.subFilters[0];
29+
if (!f?.field) return null;
30+
if (f.field !== pk) return null;
31+
if (f.operator !== AdminForthFilterOperators.EQ) return null;
32+
if (typeof f.value !== "string") return null;
33+
34+
return f.value;
35+
}
36+
937
const escapeRegex = (value) => {
1038
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escapes special characters
1139
};
@@ -184,85 +212,44 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
184212
}
185213

186214
getFieldValue(field, value) {
187-
if (field.type == AdminForthDataTypes.DATETIME) {
188-
if (!value) {
189-
return null;
190-
}
191-
return dayjs(Date.parse(value)).toISOString();
192-
193-
} else if (field.type == AdminForthDataTypes.DATE) {
194-
if (!value) {
195-
return null;
196-
}
197-
return dayjs(Date.parse(value)).toISOString().split('T')[0];
198-
199-
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
200-
return value === null ? null : !!value;
201-
} else if (field.type == AdminForthDataTypes.DECIMAL) {
202-
if (value === null || value === undefined) {
203-
return null;
204-
}
205-
return value?.toString();
206-
} else if (field.name === '_id' && !field.fillOnCreate) {
207-
// value is supposed to be an ObjectId or string representing it
208-
if (typeof value === 'object') {
209-
return value?.toString();
210-
}
215+
if (field.type === AdminForthDataTypes.DATETIME) {
216+
return value ? dayjs(Date.parse(value)).toISOString() : null;
217+
}
218+
if (field.type === AdminForthDataTypes.DATE) {
219+
return value ? dayjs(Date.parse(value)).toISOString().split("T")[0] : null;
220+
}
221+
if (field.type === AdminForthDataTypes.BOOLEAN) {
222+
return value === null ? null : !!value;
223+
}
224+
if (field.type === AdminForthDataTypes.DECIMAL) {
225+
return value === null || value === undefined ? null : value.toString();
226+
}
227+
if (field.name === '_id') {
228+
return idToString(value);
211229
}
212-
213230
return value;
214231
}
215232

216233

217234
setFieldValue(field, value) {
218-
if (value === undefined) return undefined;
219-
if (value === null) return null;
220-
235+
if (value === undefined) {
236+
return undefined;
237+
}
238+
if (value === null || value === '') {
239+
return null;
240+
}
221241
if (field.type === AdminForthDataTypes.DATETIME) {
222-
if (value === "" || value === null) {
223-
return null;
224-
}
225242
return dayjs(value).isValid() ? dayjs(value).toDate() : null;
226243
}
227-
228244
if (field.type === AdminForthDataTypes.INTEGER) {
229-
if (value === "" || value === null) {
230-
return null;
231-
}
232245
return Number.isFinite(value) ? Math.trunc(value) : null;
233246
}
234-
235247
if (field.type === AdminForthDataTypes.FLOAT) {
236-
if (value === "" || value === null) {
237-
return null;
238-
}
239248
return Number.isFinite(value) ? value : null;
240249
}
241-
242250
if (field.type === AdminForthDataTypes.DECIMAL) {
243-
if (value === "" || value === null) {
244-
return null;
245-
}
246251
return value.toString();
247252
}
248-
249-
if (field.name === '_id' && !field.fillOnCreate) {
250-
// value is supposed to be an ObjectId
251-
if (!ObjectId.isValid(value)) {
252-
return null;
253-
}
254-
if (typeof value === 'string' || typeof value === 'number') {
255-
// if string or number - turn it into ObjectId
256-
return new ObjectId(value);
257-
} else if (typeof value === 'object') {
258-
// assume it is a correct ObjectId
259-
return value;
260-
}
261-
262-
// unsupported type for ObjectId
263-
return null;
264-
}
265-
266253
return value;
267254
}
268255

@@ -313,34 +300,53 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
313300
.map((f) => this.getFilterQuery(resource, f)));
314301
}
315302

316-
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }:
317-
{
318-
resource: AdminForthResource,
319-
limit: number,
320-
offset: number,
321-
sort: { field: string, direction: AdminForthSortDirections }[],
303+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }:
304+
{
305+
resource: AdminForthResource,
306+
limit: number,
307+
offset: number,
308+
sort: { field: string, direction: AdminForthSortDirections }[],
322309
filters: IAdminForthAndOrFilter,
323310
}
324311
): Promise<any[]> {
325312

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

329317

330-
const collection = this.client.db().collection(tableName);
318+
const pk = this.getPrimaryKey(resource);
319+
const pkValue = extractSimplePkEq(filters, pk);
320+
321+
if (pkValue !== null) {
322+
let res = await collection.find({ [pk]: pkValue }).limit(1).toArray();
323+
if (res.length) {
324+
return res;
325+
}
326+
try {
327+
res = await collection.find({ [pk]: new UUID(pkValue) }).limit(1).toArray();
328+
if (res.length) {
329+
return res;
330+
}
331+
} catch {}
332+
try {
333+
res = await collection.find({ [pk]: new ObjectId(pkValue) }).limit(1).toArray();
334+
if (res.length) {
335+
return res;
336+
}
337+
} catch {}
338+
return [];
339+
}
340+
331341
const query = filters.subFilters.length ? this.getFilterQuery(resource, filters) : {};
332342

333-
const sortArray: any[] = sort.map((s) => {
334-
return [s.field, this.SortDirectionsMap[s.direction]];
335-
});
343+
const sortArray: any[] = sort.map((s) => [s.field, this.SortDirectionsMap[s.direction]]);
336344

337-
const result = await collection.find(query)
345+
return await collection.find(query)
338346
.sort(sortArray)
339347
.skip(offset)
340348
.limit(limit)
341349
.toArray();
342-
343-
return result
344350
}
345351

346352
async getCount({ resource, filters }: {
@@ -393,15 +399,17 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
393399
}
394400

395401
async updateRecordOriginalValues({ resource, recordId, newValues }) {
396-
const collection = this.client.db().collection(resource.table);
397-
const primaryKeyColumn = resource.dataSourceColumns.find((col) => col.name === this.getPrimaryKey(resource));
398-
await collection.updateOne({ [primaryKeyColumn.name]: this.setFieldValue(primaryKeyColumn, recordId) }, { $set: newValues });
402+
const collection = this.client.db().collection(resource.table);
403+
const pk = this.getPrimaryKey(resource);
404+
const rows = await this.getDataWithOriginalTypes({resource, limit: 1, offset: 0, sort: [], filters: Filters.AND(Filters.EQ(pk, recordId))});
405+
await collection.updateOne({ [pk]: rows[0][pk] || recordId }, { $set: newValues });
399406
}
400407

401408
async deleteRecord({ resource, recordId }): Promise<boolean> {
402409
const collection = this.client.db().collection(resource.table);
403-
const primaryKeyColumn = resource.dataSourceColumns.find((col) => col.name === this.getPrimaryKey(resource));
404-
const res = await collection.deleteOne({ [primaryKeyColumn.name]: this.setFieldValue(primaryKeyColumn, recordId) });
410+
const pk = this.getPrimaryKey(resource);
411+
const rows = await this.getDataWithOriginalTypes({resource, limit: 1, offset: 0, sort: [], filters: Filters.AND(Filters.EQ(pk, recordId))});
412+
const res = await collection.deleteOne({ [pk]: rows[0][pk] });
405413
return res.deletedCount > 0;
406414
}
407415

0 commit comments

Comments
 (0)