Skip to content

Commit 8e25924

Browse files
committed
feat: enhance resource data handling by updating column descriptions, adding polymorphic car references, and expanding test coverage for resource data retrieval
1 parent ea758cb commit 8e25924

5 files changed

Lines changed: 238 additions & 2 deletions

File tree

adminforth/modules/restApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ const getResourceDataRequestSchema: AnySchemaObject = {
274274
filters: commonFiltersSchema,
275275
columns: {
276276
type: 'array',
277-
description: 'Optional list of resource column names to include in returned rows. Omit it to return all visible columns.',
277+
description: 'Optional list of resource column names to include in returned rows. When set, the response is projected to exactly these resource columns and computed helper fields such as _label and _clickUrl are omitted.',
278278
minItems: 1,
279279
uniqueItems: true,
280280
items: { type: 'string' },

tests/application/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import cars_sl_dont_allow_delete_by_hook from './resources/cars_sl_dont_allow_de
2020

2121
import carsDescriptionImage from '../../dev-demo/resources/cars_description_image.js';
2222
import passkeysResource from '../../dev-demo/resources/passkeys.js';
23+
import polymorphicCarRefs from './resources/polymorphic_car_refs.js';
2324

2425
const ADMIN_BASE_URL = '';
2526
const appFilePath = fileURLToPath(import.meta.url);
@@ -95,6 +96,7 @@ export const admin = new AdminForth({
9596
cars_sl_dont_allow_delete_by_hook,
9697

9798
carsDescriptionImage,
99+
polymorphicCarRefs,
98100
passkeysResource
99101
],
100102
menu: [

tests/application/resources/adminuser.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import usersResource from "../../../dev-demo/resources/adminuser.js";
33
export default {
44
...usersResource,
55
plugins: [
6-
...usersResource.plugins?.filter(p => p.className !== 'TwoFactorsAuthPlugin') || [],
6+
...usersResource.plugins?.filter((p) => ![
7+
'AdminForthAgentPlugin',
8+
'TwoFactorsAuthPlugin',
9+
].includes(p.className)) || [],
710
],
811
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { randomUUID } from 'crypto';
2+
import { AdminForthDataTypes, type AdminForthResourceInput } from 'adminforth';
3+
4+
export default {
5+
dataSource: 'sqlite',
6+
table: 'cars_description_image',
7+
resourceId: 'polymorphic_car_refs',
8+
label: 'Polymorphic car refs',
9+
columns: [
10+
{
11+
name: 'id',
12+
primaryKey: true,
13+
required: false,
14+
fillOnCreate: () => randomUUID(),
15+
showIn: {
16+
create: false,
17+
},
18+
},
19+
{
20+
name: 'created_at',
21+
required: false,
22+
fillOnCreate: () => new Date().toISOString(),
23+
showIn: {
24+
create: false,
25+
},
26+
},
27+
{
28+
name: 'resource_id',
29+
type: AdminForthDataTypes.STRING,
30+
required: false,
31+
showIn: {
32+
create: false,
33+
edit: false,
34+
},
35+
},
36+
{
37+
name: 'record_id',
38+
type: AdminForthDataTypes.STRING,
39+
required: false,
40+
foreignResource: {
41+
polymorphicOn: 'resource_id',
42+
polymorphicResources: [
43+
{
44+
resourceId: 'cars_sl',
45+
whenValue: 'car',
46+
},
47+
],
48+
},
49+
},
50+
{
51+
name: 'image_path',
52+
required: false,
53+
},
54+
],
55+
} as AdminForthResourceInput;

tests/jest_tests/CRUD_sqlite.test.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,182 @@ describe('POST /get_resource_data', () => {
714714
});
715715
});
716716

717+
it('keeps computed helper fields when columns are not requested', async () => {
718+
const res = await agent
719+
.set('Cookie', authCookie)
720+
.post('/adminapi/v1/get_resource_data')
721+
.send({
722+
resourceId: 'cars_sl',
723+
source: 'list',
724+
limit: 1,
725+
offset: 0,
726+
sort: [],
727+
filters: [{ field: 'id', operator: 'eq', value: createdRecordId }],
728+
});
729+
730+
expect(res.status).toEqual(200);
731+
expect(res.body.error).toBeUndefined();
732+
expect(res.body.data[0]._label).toBe('🚘 Abobus amogus 🚗');
733+
});
734+
735+
it('returns exactly requested columns and omits computed helper fields', async () => {
736+
const res = await agent
737+
.set('Cookie', authCookie)
738+
.post('/adminapi/v1/get_resource_data')
739+
.send({
740+
resourceId: 'cars_sl',
741+
source: 'list',
742+
limit: 1,
743+
offset: 0,
744+
sort: [],
745+
filters: [{ field: 'id', operator: 'eq', value: createdRecordId }],
746+
columns: ['model', 'price'],
747+
});
748+
749+
expect(res.status).toEqual(200);
750+
expect(res.body.error).toBeUndefined();
751+
expect(res.body.data[0]).toEqual({
752+
model: 'Abobus amogus',
753+
price: 1234,
754+
});
755+
expect(res.body.data[0]._label).toBeUndefined();
756+
expect(res.body.data[0]._clickUrl).toBeUndefined();
757+
});
758+
759+
it('returns an error for unknown requested columns', async () => {
760+
const res = await agent
761+
.set('Cookie', authCookie)
762+
.post('/adminapi/v1/get_resource_data')
763+
.send({
764+
resourceId: 'cars_sl',
765+
source: 'list',
766+
limit: 1,
767+
offset: 0,
768+
sort: [],
769+
filters: [],
770+
columns: ['missing_column'],
771+
});
772+
773+
expect(res.status).toEqual(200);
774+
expect(res.body.error).toBe('Column missing_column not found in resource cars_sl');
775+
});
776+
777+
it('supports selecting only a virtual column without leaking internal fallback columns', async () => {
778+
const res = await agent
779+
.set('Cookie', authCookie)
780+
.post('/adminapi/v1/get_resource_data')
781+
.send({
782+
resourceId: 'adminuser',
783+
source: 'list',
784+
limit: 1,
785+
offset: 0,
786+
sort: [],
787+
filters: [{ field: 'email', operator: 'eq', value: 'adminforth' }],
788+
columns: ['password'],
789+
});
790+
791+
expect(res.status).toEqual(200);
792+
expect(res.body.error).toBeUndefined();
793+
expect(res.body.data[0]).toEqual({});
794+
});
795+
796+
it('projects requested foreign columns after reference post-processing', async () => {
797+
const adminUserRes = await agent
798+
.set('Cookie', authCookie)
799+
.post('/adminapi/v1/get_resource_data')
800+
.send({
801+
resourceId: 'adminuser',
802+
source: 'list',
803+
limit: 1,
804+
offset: 0,
805+
sort: [],
806+
filters: [{ field: 'email', operator: 'eq', value: 'adminforth' }],
807+
columns: ['id'],
808+
});
809+
810+
expect(adminUserRes.status).toEqual(200);
811+
expect(adminUserRes.body.error).toBeUndefined();
812+
const adminUserId = adminUserRes.body.data[0].id;
813+
814+
const updateRes = await agent
815+
.set('Cookie', authCookie)
816+
.post('/adminapi/v1/update_record')
817+
.send({
818+
resourceId: 'cars_sl',
819+
recordId: createdRecordId,
820+
meta: {},
821+
record: {
822+
seller_id: adminUserId,
823+
},
824+
});
825+
826+
expect(updateRes.status).toEqual(200);
827+
expect(updateRes.body.error).toBeUndefined();
828+
829+
const res = await agent
830+
.set('Cookie', authCookie)
831+
.post('/adminapi/v1/get_resource_data')
832+
.send({
833+
resourceId: 'cars_sl',
834+
source: 'list',
835+
limit: 1,
836+
offset: 0,
837+
sort: [],
838+
filters: [{ field: 'id', operator: 'eq', value: createdRecordId }],
839+
columns: ['seller_id'],
840+
});
841+
842+
expect(res.status).toEqual(200);
843+
expect(res.body.error).toBeUndefined();
844+
expect(res.body.data[0]).toEqual({
845+
seller_id: {
846+
label: '👤 adminforth',
847+
pk: adminUserId,
848+
},
849+
});
850+
});
851+
852+
it('projects polymorphic foreign columns without leaking polymorphic discriminator columns', async () => {
853+
const createRes = await agent
854+
.set('Cookie', authCookie)
855+
.post('/adminapi/v1/create_record')
856+
.send({
857+
resourceId: 'polymorphic_car_refs',
858+
record: {
859+
record_id: createdRecordId,
860+
image_path: 'test-image.png',
861+
},
862+
requiredColumnsToSkip: [],
863+
meta: {},
864+
});
865+
866+
expect(createRes.status).toEqual(200);
867+
expect(createRes.body.error).toBeUndefined();
868+
869+
const res = await agent
870+
.set('Cookie', authCookie)
871+
.post('/adminapi/v1/get_resource_data')
872+
.send({
873+
resourceId: 'polymorphic_car_refs',
874+
source: 'list',
875+
limit: 1,
876+
offset: 0,
877+
sort: [],
878+
filters: [{ field: 'id', operator: 'eq', value: createRes.body.newRecordId }],
879+
columns: ['record_id'],
880+
});
881+
882+
expect(res.status).toEqual(200);
883+
expect(res.body.error).toBeUndefined();
884+
expect(res.body.data[0]).toEqual({
885+
record_id: {
886+
label: '🚘 Abobus amogus 🚗',
887+
pk: createdRecordId,
888+
},
889+
});
890+
expect(res.body.data[0].resource_id).toBeUndefined();
891+
});
892+
717893
describe('POST /get_resource', () => {
718894
beforeAll(async () => {
719895
const res = await agent

0 commit comments

Comments
 (0)