Skip to content

Commit ea758cb

Browse files
committed
feat: add optional columns property to resource data request schema and implement column validation in AdminForthRestAPI
1 parent a61189c commit ea758cb

8 files changed

Lines changed: 91 additions & 23 deletions

File tree

adminforth/dataConnectors/baseConnector.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,13 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
261261
}
262262
}
263263

264-
getDataWithOriginalTypes({ resource, limit, offset, sort, filters }: {
264+
getDataWithOriginalTypes({ resource, limit, offset, sort, filters, columns }: {
265265
resource: AdminForthResource,
266266
limit: number,
267267
offset: number,
268268
sort: IAdminForthSort[],
269269
filters: IAdminForthAndOrFilter,
270+
columns?: AdminForthResourceColumn[],
270271
}): Promise<any[]> {
271272
throw new Error('Method not implemented.');
272273
}
@@ -595,13 +596,14 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
595596
throw new Error('Method not implemented.');
596597
}
597598

598-
async getData({ resource, limit, offset, sort, filters, getTotals }: {
599+
async getData({ resource, limit, offset, sort, filters, getTotals, columns }: {
599600
resource: AdminForthResource,
600601
limit: number,
601602
offset: number,
602603
sort: { field: string, direction: AdminForthSortDirections }[],
603604
filters: IAdminForthAndOrFilter,
604605
getTotals: boolean,
606+
columns?: AdminForthResourceColumn[],
605607
}): Promise<{ data: any[], total: number }> {
606608
let normalizedFilters = filters;
607609

@@ -613,7 +615,8 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
613615
normalizedFilters = filterValidation.normalizedFilters as IAdminForthAndOrFilter;
614616
}
615617

616-
const promises: Promise<any>[] = [this.getDataWithOriginalTypes({ resource, limit, offset, sort, filters: normalizedFilters })];
618+
const dataSourceColumns = columns ?? resource.dataSourceColumns;
619+
const promises: Promise<any>[] = [this.getDataWithOriginalTypes({ resource, limit, offset, sort, filters: normalizedFilters, columns: dataSourceColumns })];
617620
if (getTotals) {
618621
promises.push(this.getCount({ resource, filters }));
619622
} else {
@@ -624,7 +627,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
624627

625628
// call getFieldValue for each field
626629
data.map((record) => {
627-
for (const col of resource.dataSourceColumns) {
630+
for (const col of dataSourceColumns) {
628631
record[col.name] = this.getFieldValue(col, record[col.name]);
629632
}
630633
});

adminforth/dataConnectors/clickhouse.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -510,14 +510,15 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
510510
}));
511511
}
512512

513-
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }: {
513+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters, columns }: {
514514
resource: AdminForthResource,
515515
limit: number,
516516
offset: number,
517517
sort: { field: string, direction: AdminForthSortDirections }[],
518518
filters: IAdminForthAndOrFilter,
519+
columns?: AdminForthResourceColumn[],
519520
}): Promise<Array<{ group?: string, [key: string]: any }>> {
520-
const columns = resource.dataSourceColumns.map((col) => {
521+
const selectedColumns = (columns ?? resource.dataSourceColumns).map((col) => {
521522
// for decimal cast to string
522523
if (col.type == AdminForthDataTypes.DECIMAL) {
523524
return `toString(${col.name}) as ${col.name}`
@@ -532,7 +533,7 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
532533
const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
533534

534535

535-
const q = `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT {limit:Int} OFFSET {offset:Int}`;
536+
const q = `SELECT ${selectedColumns} FROM ${tableName} ${where} ${orderBy} LIMIT {limit:Int} OFFSET {offset:Int}`;
536537
const d = {
537538
...params,
538539
limit,

adminforth/dataConnectors/mongo.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,13 +408,14 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
408408
});
409409
}
410410

411-
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }:
411+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters, columns }:
412412
{
413413
resource: AdminForthResource,
414414
limit: number,
415415
offset: number,
416416
sort: { field: string, direction: AdminForthSortDirections }[],
417417
filters: IAdminForthAndOrFilter,
418+
columns?: Array<{ name: string }>,
418419
}
419420
): Promise<any[]> {
420421

@@ -429,7 +430,11 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
429430
return [s.field, this.SortDirectionsMap[s.direction]];
430431
});
431432

432-
const result = await collection.find(query)
433+
const projection = columns
434+
? Object.fromEntries(columns.map((col) => [col.name, 1]))
435+
: undefined;
436+
437+
const result = await collection.find(query, projection ? { projection } : undefined)
433438
.sort(sortArray)
434439
.skip(offset)
435440
.limit(limit)

adminforth/dataConnectors/mysql.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,14 +453,14 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
453453
return rows;
454454
}
455455

456-
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }): Promise<any[]> {
457-
const columns = resource.dataSourceColumns.map((col: { name: string }) => `${col.name}`).join(', ');
456+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters, columns }): Promise<any[]> {
457+
const selectedColumns = (columns ?? resource.dataSourceColumns).map((col: { name: string }) => `${col.name}`).join(', ');
458458
const tableName = resource.table;
459459

460460
const { sql: where, values: filterValues } = this.whereClauseAndValues(filters);
461461

462462
const orderBy = sort.length ? `ORDER BY ${sort.map((s: { field: string; direction: AdminForthSortDirections }) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
463-
let selectQuery = `SELECT ${columns} FROM ${tableName}`;
463+
let selectQuery = `SELECT ${selectedColumns} FROM ${tableName}`;
464464
if (where) selectQuery += ` ${where}`;
465465
if (orderBy) selectQuery += ` ${orderBy}`;
466466
if (limit) selectQuery += ` LIMIT ${limit}`;

adminforth/dataConnectors/postgres.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -428,16 +428,16 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
428428
return stmt.rows;
429429
}
430430

431-
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }): Promise<any[]> {
432-
const columns = resource.dataSourceColumns.map((col) => `"${col.name}"`).join(', ');
431+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters, columns }): Promise<any[]> {
432+
const selectedColumns = (columns ?? resource.dataSourceColumns).map((col) => `"${col.name}"`).join(', ');
433433
const tableName = resource.table;
434434

435435
const { sql: where, paramsCount, values: filterValues } = this.whereClauseAndValues(resource, filters);
436436

437437
const limitOffset = `LIMIT $${paramsCount} OFFSET $${paramsCount + 1}`;
438438
const d = [...filterValues, limit, offset];
439439
const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `"${s.field}" ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
440-
const selectQuery = `SELECT ${columns} FROM "${tableName}" ${where} ${orderBy} ${limitOffset}`;
440+
const selectQuery = `SELECT ${selectedColumns} FROM "${tableName}" ${where} ${orderBy} ${limitOffset}`;
441441
dbLogger.trace(`🪲📜 PG Q: ${selectQuery}, params: ${JSON.stringify(d)}`);
442442
const stmt = await this.client.query(selectQuery, d);
443443
const rows = stmt.rows;

adminforth/dataConnectors/sqlite.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,8 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
430430
return results.sort((a, b) => a.group.localeCompare(b.group));
431431
}
432432

433-
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }): Promise<any[]> {
434-
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
433+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters, columns }): Promise<any[]> {
434+
const selectedColumns = (columns ?? resource.dataSourceColumns).map((col) => col.name).join(', ');
435435
const tableName = resource.table;
436436

437437
const where = this.whereClause(filters);
@@ -440,7 +440,7 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
440440

441441
const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
442442

443-
const q = `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT ? OFFSET ?`;
443+
const q = `SELECT ${selectedColumns} FROM ${tableName} ${where} ${orderBy} LIMIT ? OFFSET ?`;
444444
const stmt = this.client.prepare(q);
445445
const d = [...filterValues, limit, offset];
446446

adminforth/modules/restApi.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,13 @@ const getResourceDataRequestSchema: AnySchemaObject = {
272272
},
273273
sort: commonSortSchema,
274274
filters: commonFiltersSchema,
275+
columns: {
276+
type: 'array',
277+
description: 'Optional list of resource column names to include in returned rows. Omit it to return all visible columns.',
278+
minItems: 1,
279+
uniqueItems: true,
280+
items: { type: 'string' },
281+
},
275282
},
276283
additionalProperties: true,
277284
allOf: [
@@ -1296,6 +1303,41 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
12961303
}
12971304
}
12981305
const { limit, offset, filters, sort } = body;
1306+
const selectedColumnNames = body.columns
1307+
? [...new Set(body.columns as string[])]
1308+
: undefined;
1309+
1310+
if (selectedColumnNames) {
1311+
const resourceColumnNames = new Set(resource.columns.map((col) => col.name));
1312+
const invalidColumnName = selectedColumnNames.find((columnName) => !resourceColumnNames.has(columnName));
1313+
1314+
if (invalidColumnName) {
1315+
return { error: `Column ${invalidColumnName} not found in resource ${resourceId}` };
1316+
}
1317+
}
1318+
const selectedColumnNameSet = selectedColumnNames ? new Set(selectedColumnNames) : undefined;
1319+
const selectedDataSourceColumnNameSet = selectedColumnNames
1320+
? new Set(selectedColumnNames.filter((columnName) => resource.dataSourceColumns.some((col) => col.name === columnName)))
1321+
: undefined;
1322+
1323+
if (selectedDataSourceColumnNameSet) {
1324+
for (const col of resource.columns) {
1325+
if (
1326+
selectedColumnNameSet.has(col.name) &&
1327+
col.foreignResource?.polymorphicOn
1328+
) {
1329+
selectedDataSourceColumnNameSet.add(col.foreignResource.polymorphicOn);
1330+
}
1331+
}
1332+
}
1333+
1334+
const selectedDataSourceColumns = selectedDataSourceColumnNameSet
1335+
? (
1336+
resource.dataSourceColumns.some((col) => selectedDataSourceColumnNameSet.has(col.name))
1337+
? resource.dataSourceColumns.filter((col) => selectedDataSourceColumnNameSet.has(col.name))
1338+
: resource.dataSourceColumns.filter((col) => col.primaryKey || col.name === resource.dataSourceColumns[0]?.name)
1339+
)
1340+
: undefined;
12991341

13001342
// remove virtual fields from sort if still presented after beforeDatasourceRequest hook
13011343
const sortFiltered = sort.filter((sortItem: IAdminForthSort) => {
@@ -1334,11 +1376,14 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
13341376
filters: normalizedFilters as IAdminForthAndOrFilter,
13351377
sort: sortFiltered,
13361378
getTotals: source === 'list',
1379+
columns: selectedDataSourceColumns,
13371380
});
13381381

13391382
// for foreign keys, add references
13401383
await Promise.all(
1341-
resource.columns.filter((col) => col.foreignResource).map(async (col) => {
1384+
resource.columns.filter((col) => (
1385+
col.foreignResource && (!selectedColumnNameSet || selectedColumnNameSet.has(col.name))
1386+
)).map(async (col) => {
13421387
let targetDataMap = {};
13431388

13441389
if (col.foreignResource.resourceId) {
@@ -1471,10 +1516,12 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
14711516
delete item[key];
14721517
}
14731518
}
1474-
item._label = resource.recordLabel(item);
1519+
if (!selectedColumnNameSet) {
1520+
item._label = resource.recordLabel(item);
1521+
}
14751522
}
14761523
}
1477-
if (source === 'list' && resource.options.listTableClickUrl) {
1524+
if (!selectedColumnNameSet && source === 'list' && resource.options.listTableClickUrl) {
14781525
await Promise.all(
14791526
data.data.map(async (item) => {
14801527
item._clickUrl = await resource.options.listTableClickUrl(item, adminUser, resource);
@@ -1501,6 +1548,16 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
15011548
}
15021549
}
15031550

1551+
if (selectedColumnNameSet) {
1552+
for (const item of data.data) {
1553+
for (const key of Object.keys(item)) {
1554+
if (!selectedColumnNameSet.has(key)) {
1555+
delete item[key];
1556+
}
1557+
}
1558+
}
1559+
}
1560+
15041561
return data;
15051562
},
15061563
});

adminforth/types/Back.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,12 +334,13 @@ export interface IAdminForthDataSourceConnector {
334334
*
335335
* Fields are returned from db "as is" then {@link AdminForthBaseConnector.getData} will transform each field using {@link IAdminForthDataSourceConnector.getFieldValue}
336336
*/
337-
getDataWithOriginalTypes({ resource, limit, offset, sort, filters }: {
337+
getDataWithOriginalTypes({ resource, limit, offset, sort, filters, columns }: {
338338
resource: AdminForthResource,
339339
limit: number,
340340
offset: number,
341341
sort: IAdminForthSort[],
342342
filters: IAdminForthAndOrFilter,
343+
columns?: AdminForthResourceColumn[],
343344
}): Promise<Array<any>>;
344345

345346
/**
@@ -397,13 +398,14 @@ export interface IAdminForthDataSourceConnectorBase extends IAdminForthDataSourc
397398

398399
getPrimaryKey(resource: AdminForthResource): string;
399400

400-
getData({ resource, limit, offset, sort, filters }: {
401+
getData({ resource, limit, offset, sort, filters, columns }: {
401402
resource: AdminForthResource,
402403
limit: number,
403404
offset: number,
404405
sort: IAdminForthSort[],
405406
filters: IAdminForthAndOrFilter,
406407
getTotals?: boolean,
408+
columns?: AdminForthResourceColumn[],
407409
}): Promise<{ data: Array<any>, total: number }>;
408410

409411
getRecordByPrimaryKey(resource: AdminForthResource, recordId: string): Promise<any>;

0 commit comments

Comments
 (0)