Skip to content

Commit a74ae1a

Browse files
committed
feat: implement backend-only column visibility logic in REST API
1 parent 533c5db commit a74ae1a

File tree

2 files changed

+126
-19
lines changed

2 files changed

+126
-19
lines changed

adminforth/modules/restApi.ts

Lines changed: 112 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
IAdminForthSort,
1212
HttpExtra,
1313
IAdminForthAndOrFilter,
14+
BackendOnlyInput,
1415
} from "../types/Back.js";
1516

1617
import { ADMINFORTH_VERSION, listify, md5hash, getLoginPromptHTML } from './utils.js';
@@ -23,6 +24,46 @@ import { ActionCheckSource, AdminForthConfigMenuItem, AdminForthDataTypes, Admin
2324
ShowInResolved} from "../types/Common.js";
2425
import { filtersTools } from "../modules/filtersTools.js";
2526

27+
async function resolveBoolOrFn(
28+
val: BackendOnlyInput | undefined,
29+
ctx: {
30+
adminUser: AdminUser;
31+
resource: AdminForthResource;
32+
meta: any;
33+
source: ActionCheckSource;
34+
adminforth: IAdminForth;
35+
}
36+
): Promise<boolean> {
37+
if (typeof val === 'function') {
38+
return !!(await (val as any)(ctx));
39+
}
40+
return !!val;
41+
}
42+
43+
async function isBackendOnly(
44+
col: AdminForthResource['columns'][number],
45+
ctx: {
46+
adminUser: AdminUser;
47+
resource: AdminForthResource;
48+
meta: any;
49+
source: ActionCheckSource;
50+
adminforth: IAdminForth;
51+
}
52+
): Promise<boolean> {
53+
return await resolveBoolOrFn(col.backendOnly as BackendOnlyInput, ctx);
54+
}
55+
56+
async function isShown(
57+
col: AdminForthResource['columns'][number],
58+
page: 'list' | 'show' | 'edit' | 'create' | 'filter',
59+
ctx: Parameters<typeof isBackendOnly>[1]
60+
): Promise<boolean> {
61+
const s = (col.showIn as any) || {};
62+
if (s[page] !== undefined) return await resolveBoolOrFn(s[page], ctx);
63+
if (s.all !== undefined) return await resolveBoolOrFn(s.all, ctx);
64+
return true;
65+
}
66+
2667
export async function interpretResource(
2768
adminUser: AdminUser,
2869
resource: AdminForthResource,
@@ -363,12 +404,22 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
363404

364405
// strip all backendOnly fields or not described in adminForth fields from dbUser
365406
// (when user defines column and does not set backendOnly, we assume it is not backendOnly)
366-
Object.keys(adminUser.dbUser).forEach((key) => {
367-
const col = userResource.columns.find((col) => col.name === key);
368-
if (!col || col.backendOnly) {
369-
delete adminUser.dbUser[key];
407+
{
408+
const ctx = {
409+
adminUser,
410+
resource: userResource,
411+
meta: {},
412+
source: ActionCheckSource.ShowRequest,
413+
adminforth: this.adminforth,
414+
};
415+
for (const key of Object.keys(adminUser.dbUser)) {
416+
const col = userResource.columns.find((c) => c.name === key);
417+
const bo = col ? await isBackendOnly(col, ctx) : true;
418+
if (!col || bo) {
419+
delete adminUser.dbUser[key];
420+
}
370421
}
371-
})
422+
}
372423

373424
return {
374425
user: userData,
@@ -803,14 +854,30 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
803854

804855
const pkField = resource.columns.find((col) => col.primaryKey)?.name;
805856
// remove all columns which are not defined in resources, or defined but backendOnly
806-
data.data.forEach((item) => {
807-
Object.keys(item).forEach((key) => {
808-
if (!resource.columns.find((col) => col.name === key) || resource.columns.find((col) => col.name === key && col.backendOnly)) {
809-
delete item[key];
857+
{
858+
const ctx = {
859+
adminUser,
860+
resource,
861+
meta,
862+
source: {
863+
show: ActionCheckSource.ShowRequest,
864+
list: ActionCheckSource.ListRequest,
865+
edit: ActionCheckSource.EditLoadRequest,
866+
}[source],
867+
adminforth: this.adminforth,
868+
};
869+
870+
for (const item of data.data) {
871+
for (const key of Object.keys(item)) {
872+
const col = resource.columns.find((c) => c.name === key);
873+
const bo = col ? await isBackendOnly(col, ctx) : true;
874+
if (!col || bo) {
875+
delete item[key];
876+
}
810877
}
811-
})
812-
item._label = resource.recordLabel(item);
813-
});
878+
item._label = resource.recordLabel(item);
879+
}
880+
}
814881
if (source === 'list' && resource.options.listTableClickUrl) {
815882
await Promise.all(
816883
data.data.map(async (item) => {
@@ -1076,11 +1143,30 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
10761143
}
10771144
}
10781145

1146+
const ctxCreate = {
1147+
adminUser,
1148+
resource,
1149+
meta: { requestBody: body },
1150+
source: ActionCheckSource.CreateRequest,
1151+
adminforth: this.adminforth,
1152+
};
1153+
1154+
for (const column of resource.columns) {
1155+
if ((column.required as { create?: boolean })?.create) {
1156+
const shown = await isShown(column, 'create', ctxCreate);
1157+
if (shown && record[column.name] === undefined) {
1158+
return { error: `Column '${column.name}' is required`, ok: false };
1159+
}
1160+
}
1161+
}
1162+
10791163
for (const column of resource.columns) {
10801164
const fieldName = column.name;
10811165
if (fieldName in record) {
1082-
if (!column.showIn?.create || column.backendOnly) {
1083-
return { error: `Field "${fieldName}" cannot be modified as it is restricted from creation (showIn.create is false, please set it to true)`, ok: false };
1166+
const shown = await isShown(column, 'create', ctxCreate);
1167+
const bo = await isBackendOnly(column, ctxCreate);
1168+
if (!shown || bo) {
1169+
return { error: `Field "${fieldName}" cannot be modified as it is restricted from creation (backendOnly or showIn.create is false, please set it to true)`, ok: false };
10841170
}
10851171
}
10861172
}
@@ -1172,15 +1258,24 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
11721258
return { error: allowedError };
11731259
}
11741260

1261+
const ctxEdit = {
1262+
adminUser,
1263+
resource,
1264+
meta: { requestBody: body, newRecord: record, oldRecord, pk: recordId },
1265+
source: ActionCheckSource.EditRequest,
1266+
adminforth: this.adminforth,
1267+
};
1268+
11751269
for (const column of resource.columns) {
11761270
const fieldName = column.name;
11771271
if (fieldName in record) {
1178-
if (!column.showIn?.edit || column.editReadonly || column.backendOnly) {
1179-
return { error: `Field "${fieldName}" cannot be modified as it is restricted from editing (showIn.edit is false, please set it to true)`, ok: false };
1272+
const shown = await isShown(column, 'edit', ctxEdit);
1273+
const bo = await isBackendOnly(column, ctxEdit);
1274+
if (!shown || column.editReadonly || bo) {
1275+
return { error: `Field "${fieldName}" cannot be modified as it is restricted from editing (backendOnly or showIn.edit is false, please set it to true)`, ok: false };
11801276
}
11811277
}
11821278
}
1183-
11841279
// for polymorphic foreign resources, we need to find out the value for polymorphicOn column
11851280
for (const column of resource.columns) {
11861281
if (column.foreignResource?.polymorphicOn && record[column.name] === null) {

adminforth/types/Back.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,15 +1503,27 @@ export type ShowInInput = ShowInModernInput | ShowInLegacyInput;
15031503
export type ShowIn = {
15041504
[key in AdminForthResourcePages]: AllowedActionValue
15051505
}
1506+
export type BackendOnlyInput =
1507+
| boolean
1508+
| ((p: {
1509+
adminUser: AdminUser;
1510+
resource: AdminForthResource;
1511+
meta: any;
1512+
source: ActionCheckSource;
1513+
adminforth: IAdminForth;
1514+
}) => boolean | Promise<boolean>);
1515+
15061516

1507-
export interface AdminForthResourceColumnInput extends Omit<AdminForthResourceColumnInputCommon, 'showIn'> {
1517+
export interface AdminForthResourceColumnInput extends Omit<AdminForthResourceColumnInputCommon, 'showIn' | 'backendOnly'> {
15081518
showIn?: ShowInInput,
15091519
foreignResource?: AdminForthForeignResource,
1520+
backendOnly?: BackendOnlyInput;
15101521
}
15111522

1512-
export interface AdminForthResourceColumn extends Omit<AdminForthResourceColumnCommon, 'showIn'> {
1523+
export interface AdminForthResourceColumn extends Omit<AdminForthResourceColumnCommon, 'showIn' | 'backendOnly'> {
15131524
showIn?: ShowIn,
15141525
foreignResource?: AdminForthForeignResource,
1526+
backendOnly?: BackendOnlyInput;
15151527
}
15161528

15171529
export interface IWebSocketClient {

0 commit comments

Comments
 (0)