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
11 changes: 7 additions & 4 deletions adminforth/dataConnectors/baseConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,13 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
}
}

getDataWithOriginalTypes({ resource, limit, offset, sort, filters }: {
getDataWithOriginalTypes({ resource, limit, offset, sort, filters, columns }: {
resource: AdminForthResource,
limit: number,
offset: number,
sort: IAdminForthSort[],
filters: IAdminForthAndOrFilter,
columns?: AdminForthResourceColumn[],
}): Promise<any[]> {
throw new Error('Method not implemented.');
}
Expand Down Expand Up @@ -595,13 +596,14 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
throw new Error('Method not implemented.');
}

async getData({ resource, limit, offset, sort, filters, getTotals }: {
async getData({ resource, limit, offset, sort, filters, getTotals, columns }: {
resource: AdminForthResource,
limit: number,
offset: number,
sort: { field: string, direction: AdminForthSortDirections }[],
filters: IAdminForthAndOrFilter,
getTotals: boolean,
columns?: AdminForthResourceColumn[],
}): Promise<{ data: any[], total: number }> {
let normalizedFilters = filters;

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

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

// call getFieldValue for each field
data.map((record) => {
for (const col of resource.dataSourceColumns) {
for (const col of dataSourceColumns) {
record[col.name] = this.getFieldValue(col, record[col.name]);
}
});
Expand Down
7 changes: 4 additions & 3 deletions adminforth/dataConnectors/clickhouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,14 +510,15 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
}));
}

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


const q = `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT {limit:Int} OFFSET {offset:Int}`;
const q = `SELECT ${selectedColumns} FROM ${tableName} ${where} ${orderBy} LIMIT {limit:Int} OFFSET {offset:Int}`;
const d = {
...params,
limit,
Expand Down
9 changes: 7 additions & 2 deletions adminforth/dataConnectors/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,14 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
});
}

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

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

const result = await collection.find(query)
const projection = columns
? Object.fromEntries(columns.map((col) => [col.name, 1]))
Comment thread
NoOne7135 marked this conversation as resolved.
: undefined;

const result = await collection.find(query, projection ? { projection } : undefined)
.sort(sortArray)
.skip(offset)
.limit(limit)
Expand Down
6 changes: 3 additions & 3 deletions adminforth/dataConnectors/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,14 +453,14 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
return rows;
}

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

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

const orderBy = sort.length ? `ORDER BY ${sort.map((s: { field: string; direction: AdminForthSortDirections }) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
let selectQuery = `SELECT ${columns} FROM ${tableName}`;
let selectQuery = `SELECT ${selectedColumns} FROM ${tableName}`;
if (where) selectQuery += ` ${where}`;
if (orderBy) selectQuery += ` ${orderBy}`;
if (limit) selectQuery += ` LIMIT ${limit}`;
Expand Down
6 changes: 3 additions & 3 deletions adminforth/dataConnectors/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,16 +428,16 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
return stmt.rows;
}

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

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

const limitOffset = `LIMIT $${paramsCount} OFFSET $${paramsCount + 1}`;
const d = [...filterValues, limit, offset];
const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `"${s.field}" ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
const selectQuery = `SELECT ${columns} FROM "${tableName}" ${where} ${orderBy} ${limitOffset}`;
const selectQuery = `SELECT ${selectedColumns} FROM "${tableName}" ${where} ${orderBy} ${limitOffset}`;
dbLogger.trace(`🪲📜 PG Q: ${selectQuery}, params: ${JSON.stringify(d)}`);
const stmt = await this.client.query(selectQuery, d);
const rows = stmt.rows;
Expand Down
6 changes: 3 additions & 3 deletions adminforth/dataConnectors/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,8 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
return results.sort((a, b) => a.group.localeCompare(b.group));
}

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

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

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

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

Expand Down
65 changes: 61 additions & 4 deletions adminforth/modules/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,13 @@ const getResourceDataRequestSchema: AnySchemaObject = {
},
sort: commonSortSchema,
filters: commonFiltersSchema,
columns: {
type: 'array',
description: 'Optional list of resource column names to include in returned rows. For list requests, computed row helper fields such as _label and _clickUrl are still returned when available.',
minItems: 1,
uniqueItems: true,
items: { type: 'string' },
},
},
additionalProperties: true,
allOf: [
Expand Down Expand Up @@ -1296,6 +1303,42 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
}
}
const { limit, offset, filters, sort } = body;
const selectedColumnNames = body.columns
? [...new Set(body.columns as string[])]
: undefined;

if (selectedColumnNames) {
const resourceColumnNames = new Set(resource.columns.map((col) => col.name));
const invalidColumnName = selectedColumnNames.find((columnName) => !resourceColumnNames.has(columnName));

if (invalidColumnName) {
return { error: `Column ${invalidColumnName} not found in resource ${resourceId}` };
}
Comment thread
NoOne7135 marked this conversation as resolved.
}
const selectedColumnNameSet = selectedColumnNames ? new Set(selectedColumnNames) : undefined;
const shouldAddListHelpers = source === 'list';
const selectedDataSourceColumnNameSet = selectedColumnNames
? new Set(selectedColumnNames.filter((columnName) => resource.dataSourceColumns.some((col) => col.name === columnName)))
: undefined;

if (selectedDataSourceColumnNameSet) {
for (const col of resource.columns) {
if (
selectedColumnNameSet.has(col.name) &&
col.foreignResource?.polymorphicOn
) {
selectedDataSourceColumnNameSet.add(col.foreignResource.polymorphicOn);
}
}
}

const selectedDataSourceColumns = selectedDataSourceColumnNameSet && !shouldAddListHelpers
? (
resource.dataSourceColumns.some((col) => selectedDataSourceColumnNameSet.has(col.name))
? resource.dataSourceColumns.filter((col) => selectedDataSourceColumnNameSet.has(col.name))
: resource.dataSourceColumns.filter((col) => col.primaryKey || col.name === resource.dataSourceColumns[0]?.name)
)
: undefined;

// remove virtual fields from sort if still presented after beforeDatasourceRequest hook
const sortFiltered = sort.filter((sortItem: IAdminForthSort) => {
Expand Down Expand Up @@ -1334,11 +1377,14 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
filters: normalizedFilters as IAdminForthAndOrFilter,
sort: sortFiltered,
getTotals: source === 'list',
columns: selectedDataSourceColumns,
});

// for foreign keys, add references
await Promise.all(
resource.columns.filter((col) => col.foreignResource).map(async (col) => {
resource.columns.filter((col) => (
col.foreignResource && (!selectedColumnNameSet || shouldAddListHelpers || selectedColumnNameSet.has(col.name))
)).map(async (col) => {
let targetDataMap = {};

if (col.foreignResource.resourceId) {
Expand Down Expand Up @@ -1448,7 +1494,6 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
})
);

const pkField = resource.columns.find((col) => col.primaryKey)?.name;
// remove all columns which are not defined in resources, or defined but backendOnly
{
const ctx = {
Expand All @@ -1471,10 +1516,12 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
delete item[key];
}
}
item._label = resource.recordLabel(item);
if (!selectedColumnNameSet || shouldAddListHelpers) {
item._label = resource.recordLabel(item);
}
}
}
if (source === 'list' && resource.options.listTableClickUrl) {
if (shouldAddListHelpers && resource.options.listTableClickUrl) {
await Promise.all(
data.data.map(async (item) => {
item._clickUrl = await resource.options.listTableClickUrl(item, adminUser, resource);
Expand All @@ -1501,6 +1548,16 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
}
}

if (selectedColumnNameSet) {
for (const item of data.data) {
for (const key of Object.keys(item)) {
if (!selectedColumnNameSet.has(key) && key !== '_label' && key !== '_clickUrl') {
delete item[key];
}
}
}
}

return data;
},
});
Expand Down
6 changes: 4 additions & 2 deletions adminforth/types/Back.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,12 +334,13 @@ export interface IAdminForthDataSourceConnector {
*
* Fields are returned from db "as is" then {@link AdminForthBaseConnector.getData} will transform each field using {@link IAdminForthDataSourceConnector.getFieldValue}
*/
getDataWithOriginalTypes({ resource, limit, offset, sort, filters }: {
getDataWithOriginalTypes({ resource, limit, offset, sort, filters, columns }: {
resource: AdminForthResource,
limit: number,
offset: number,
sort: IAdminForthSort[],
filters: IAdminForthAndOrFilter,
columns?: AdminForthResourceColumn[],
}): Promise<Array<any>>;

/**
Expand Down Expand Up @@ -397,13 +398,14 @@ export interface IAdminForthDataSourceConnectorBase extends IAdminForthDataSourc

getPrimaryKey(resource: AdminForthResource): string;

getData({ resource, limit, offset, sort, filters }: {
getData({ resource, limit, offset, sort, filters, columns }: {
resource: AdminForthResource,
limit: number,
offset: number,
sort: IAdminForthSort[],
filters: IAdminForthAndOrFilter,
getTotals?: boolean,
columns?: AdminForthResourceColumn[],
}): Promise<{ data: Array<any>, total: number }>;

getRecordByPrimaryKey(resource: AdminForthResource, recordId: string): Promise<any>;
Expand Down
2 changes: 2 additions & 0 deletions tests/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import cars_sl_dont_allow_delete_by_hook from './resources/cars_sl_dont_allow_de

import carsDescriptionImage from '../../dev-demo/resources/cars_description_image.js';
import passkeysResource from '../../dev-demo/resources/passkeys.js';
import polymorphicCarRefs from './resources/polymorphic_car_refs.js';

const ADMIN_BASE_URL = '';
const appFilePath = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -95,6 +96,7 @@ export const admin = new AdminForth({
cars_sl_dont_allow_delete_by_hook,

carsDescriptionImage,
polymorphicCarRefs,
passkeysResource
],
menu: [
Expand Down
5 changes: 4 additions & 1 deletion tests/application/resources/adminuser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import usersResource from "../../../dev-demo/resources/adminuser.js";
export default {
...usersResource,
plugins: [
...usersResource.plugins?.filter(p => p.className !== 'TwoFactorsAuthPlugin') || [],
...usersResource.plugins?.filter((p) => ![
'AdminForthAgentPlugin',
'TwoFactorsAuthPlugin',
].includes(p.className)) || [],
],
}
6 changes: 5 additions & 1 deletion tests/application/resources/cars_sl_allow_create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const cars_sl = carsResourseTemplate("cars_sl", "sqlite", "id");

export default {
...cars_sl,
options: {
...(cars_sl as any).options,
listTableClickUrl: async (record: any, _adminUser: any, resource: any) => `/resource/${resource.resourceId}/show/${record.id}`,
},
columns: [
...cars_sl.columns.filter(c => c.name !== "mileage" && c.name !== "photos" && c.name !== "generated_promo_picture"),
{
Expand Down Expand Up @@ -33,4 +37,4 @@ export default {
editReadonly: true,
},
],
};
};
Loading