Skip to content

Commit 249ed30

Browse files
authored
Merge branch 'next' into custom-actions
2 parents 47ecc47 + 238aea6 commit 249ed30

File tree

30 files changed

+910
-177
lines changed

30 files changed

+910
-177
lines changed

adminforth/commands/createApp/templates/index.ts.hbs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ if (import.meta.url === `file://${process.argv[1]}`) {
7777
});
7878

7979
admin.express.listen(port, () => {
80-
console.log(`Example app listening at http://localhost:${port}`);
8180
console.log(`\n⚡ AdminForth is available at http://localhost:${port}${ADMIN_BASE_URL}\n`);
8281
});
8382
}

adminforth/dataConnectors/clickhouse.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirection
77

88
class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector {
99

10-
dbName: string;
11-
url: string;
10+
dbName: string;
11+
url: string;
1212

1313
async setupClient(url): Promise<void> {
1414
this.dbName = new URL(url).pathname.replace('/', '');
@@ -217,7 +217,13 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
217217
sort: { field: string, direction: AdminForthSortDirections }[],
218218
filters: { field: string, operator: AdminForthFilterOperators, value: any }[],
219219
}): Promise<any[]> {
220-
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
220+
const columns = resource.dataSourceColumns.map((col) => {
221+
// for decimal cast to string
222+
if (col.type == AdminForthDataTypes.DECIMAL) {
223+
return `toString(${col.name}) as ${col.name}`
224+
}
225+
return col.name;
226+
}).join(', ');
221227
const tableName = resource.table;
222228

223229
const where = this.whereClause(resource, filters);

adminforth/dataConnectors/mysql.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
88

99
async setupClient(url): Promise<void> {
1010
try {
11-
this.client = await mysql.createConnection(url);
11+
this.client = mysql.createPool({
12+
uri: url,
13+
waitForConnections: true,
14+
connectionLimit: 10, // Adjust based on your needs
15+
queueLimit: 0
16+
});
1217
} catch (e) {
1318
console.error(`Failed to connect to MySQL: ${e}`);
1419
}
@@ -33,7 +38,7 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
3338
};
3439

3540
async discoverFields(resource) {
36-
const [results] = await this.client.query("SHOW COLUMNS FROM " + resource.table);
41+
const [results] = await this.client.execute("SHOW COLUMNS FROM " + resource.table);
3742
const fieldTypes = {};
3843
results.forEach((row) => {
3944
const field: any = {};
@@ -231,7 +236,7 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
231236
if (process.env.HEAVY_DEBUG_QUERY) {
232237
console.log('🪲📜 MySQL Q:', q, 'values:', filterValues);
233238
}
234-
const [results] = await this.client.query(q, filterValues);
239+
const [results] = await this.client.execute(q, filterValues);
235240
return +results[0]["COUNT(*)"];
236241
}
237242

@@ -243,7 +248,7 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
243248
if (process.env.HEAVY_DEBUG_QUERY) {
244249
console.log('🪲📜 MySQL Q:', q);
245250
}
246-
const [results] = await this.client.query(q);
251+
const [results] = await this.client.execute(q);
247252
const { min, max } = results[0];
248253
result[col.name] = {
249254
min, max,

adminforth/documentation/docs/tutorial/03-Customization/05-limitingAccess.md

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Before we start it is worth to mention that callbacks or scalars defined in `all
99

1010
As you can see allowedAction callbacks are called in parallel in async manner. However it is important to keep them fast and not to make any slow operations in them, to keep UI responsive.
1111

12-
## Statically disable some action
12+
## Statically disable some action on resource
1313

1414
You can use `options.allowedActions` on resource to limit access to the resource actions (list, show, create, edit, delete).
1515

@@ -33,7 +33,43 @@ If you want to disable deletion of the resource records for all users:
3333
}
3434
```
3535

36-
## Disable some action based on logged in user record or role
36+
## Disable full access to resource based on logged in user record or role
37+
38+
If you want to disable all actions to the resource for all users except users with role `superadmin`:
39+
40+
```ts title="./resources/adminuser.ts"
41+
{
42+
...
43+
resourceId: 'adminuser',
44+
...
45+
//diff-add
46+
options: {
47+
//diff-add
48+
allowedActions: {
49+
//diff-add
50+
all: async ({ adminUser }: { adminUser: AdminUser }): Promise<boolean> => {
51+
//diff-add
52+
return adminUser.dbUser.role === 'superadmin';
53+
//diff-add
54+
}
55+
//diff-add
56+
}
57+
//diff-add
58+
}
59+
}
60+
```
61+
62+
> ☝️ This will not hide link to the resource in the menu, you should separately use [menuItem.visible](/docs/tutorial/Customization/menuConfiguration/#visibility-of-menu-items) to hide it.
63+
64+
65+
66+
> ☝️ instead of reading role from user you can check permission using complex ACL/RBAC models with permissions stored in the database.
67+
> However we recommend you to keep in mind that allowedActions callback is called on every request related to resource, so it should be fast.
68+
> So try to minimize requests to database as much as possible.
69+
70+
71+
72+
## Disable only some action based on logged in user record or role
3773

3874
If you want to disable deletion of apartments for all users apart from users with role `superadmin`:
3975

@@ -61,11 +97,7 @@ import type { AdminUser } from 'adminforth';
6197
}
6298
```
6399

64-
> ☝️ instead of reading role from user you can check permission using complex ACL/RBAC models with permissions stored in the database.
65-
> However we recommend you to keep in mind that allowedActions callback is called on every request related to resource, so it should be fast.
66-
> So try to minimize requests to database as much as possible.
67-
68-
## Reuse the same callback for multiple actions
100+
### Reuse the same callback for multiple actions
69101

70102
Let's disable creating and editing of new users for all users apart from users with role `superadmin`, and at the same time disable deletion for all users:
71103

@@ -80,6 +112,8 @@ async function canModifyUsers({ adminUser }: { adminUser: AdminUser }): Promise<
80112
//diff-add
81113
}
82114

115+
...
116+
83117
{
84118
...
85119
resourceId: 'adminuser',
@@ -97,6 +131,7 @@ async function canModifyUsers({ adminUser }: { adminUser: AdminUser }): Promise<
97131
}
98132
```
99133

134+
100135
## Customizing the access control based on resource values
101136

102137
In more advanced cases you might need to check access based on record value.

adminforth/documentation/docs/tutorial/03-Customization/10-menuConfiguration.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,32 @@ E.g. create group "Blog" with Items who link to resource "posts" and "categories
5353

5454
If it is rare Group you can make it `open: false` so it would not take extra space in menu, but admin users will be able to open it by clicking on the group name.
5555

56+
## Visibility of menu items
57+
58+
You might want to hide some menu items from the menu for some users.
59+
60+
To do it use `visible` field in the menu item configuration:
61+
62+
```ts title='./index.ts'
63+
{
64+
...
65+
menu: [
66+
{
67+
label: 'Categories',
68+
icon: 'flowbite:folder-duplicate-outline',
69+
resourceId: 'categories',
70+
//diff-add
71+
visible: adminUser => adminUser.dbUser.role === 'admin'
72+
},
73+
],
74+
...
75+
}
76+
```
77+
78+
> 👆 Please note that this will just hide menu item for non `admin` users, but resource pages will still be available by direct
79+
> URLs. To limit access, you should also use [allowedActions](/docs/tutorial/Customization/limitingAccess/#disable-full-access-to-resource-based-on-logged-in-user-record-or-role) field in the resource configuration in addition to this.
80+
81+
5682
## Gap
5783

5884
You can put one or several gaps between menu items:

adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,9 @@ This way, when creating or editing a record you will be able to choose value for
539539

540540
When foreign resource column is not required, selector will have an 'Unset' option that will set field to `null`. You can change label for this option using `unsetLabel`, like so:
541541

542-
```typescript title="./resources/adminuser.ts"
542+
```typescript title="./resources/apartments.ts"
543543
export default {
544-
name: 'adminuser',
544+
name: 'apartments',
545545
columns: [
546546
...
547547
{
@@ -558,6 +558,96 @@ export default {
558558
],
559559
```
560560

561+
### Polymorphic foreign resources
562+
563+
Sometimes it is needed for one column to be a foreign key for multiple tables. For example, given the following schema:
564+
565+
```prisma title="./schema.prisma"
566+
...
567+
model apartments {
568+
id String @id
569+
created_at DateTime?
570+
title String
571+
square_meter Float?
572+
price Decimal
573+
number_of_rooms Int?
574+
realtor_id String?
575+
}
576+
577+
model houses {
578+
id String @id
579+
created_at DateTime?
580+
title String
581+
house_square_meter Float?
582+
land_square_meter Float?
583+
price Decimal
584+
realtor_id String?
585+
}
586+
587+
model sold_property {
588+
id String @id
589+
created_at DateTime?
590+
title String
591+
property_id String
592+
realtor_id String?
593+
}
594+
595+
```
596+
597+
Here, in `sold_property` table, column `property_id` can be a foreign key for both `apartments` and `houses` tables. If schema is set like this, the is no way to tell to what table exactly `property_id` links to. Also, if defined like usual, adminforth will link to only one of them. To make sure that `property_id` works as intended we need add one more column to `sold_property` and change the way foreign resource is defined in adminforth resource config.
598+
599+
```prisma title="./schema.prisma"
600+
...
601+
602+
model sold_property {
603+
id String @id
604+
created_at DateTime?
605+
title String
606+
//diff-add
607+
property_type String
608+
property_id String
609+
realtor_id String?
610+
}
611+
612+
```
613+
614+
`property_type` column will be used to store what table id in `property_id` refers to. And in adminforth config for `sold_property` table, when describing `property_id` column, foreign resource field should be defined as follows:
615+
616+
```typescript title="./resources/sold_property.ts"
617+
export default {
618+
name: 'sold_property',
619+
columns: [
620+
...
621+
{
622+
name: "property_type",
623+
showIn: { create: false, edit: false },
624+
},
625+
{
626+
name: "property_id",
627+
foreignResource: {
628+
polymorphicResources: [
629+
{
630+
resourceId: 'apartments',
631+
whenValue: 'apartment',
632+
},
633+
{
634+
resourceId: 'houses',
635+
whenValue: 'house',
636+
},
637+
],
638+
polymorphicOn: 'property_type',
639+
},
640+
},
641+
],
642+
},
643+
...
644+
],
645+
```
646+
647+
When defined like this, adminforth will use value in `property_type` to figure out to what table does id in `property_id` refers to and properly link them. When creating or editing a record, adminforth will figure out to what table new `property_id` links to and fill `property_type` on its own using corresponding `whenValue`. Note, that `whenValue` does not have to be the same as `resourceId`, it can be any string as long as they do not repeat withing `polymorphicResources` array. Also, since `whenValue` is a string, column designated as `polymorphicOn` must also be string. Another thing to note is that, `polymorphicOn` column (`property_type` in our case) must not be editable by user, so it must include both `create` and `edit` as `false` in `showIn` value. Even though, `polymorphicOn` column is no editable, it can be beneficial to set is as an enumerator. This will have two benefits: first, columns value displayed in table and show page can be changed to a desired one and second, when filtering on this column, user will only able to choose values provided for him.
648+
649+
If `beforeDatasourceRequest` or `afterDatasourceResponse` hooks are set for polymorphic foreign resource, they will be called for each resource in `polymorphicResources` array.
650+
561651
## Filtering
562652

563653
### Filter Options

adminforth/documentation/docs/tutorial/05-Plugins/06-text-complete.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ export const admin = new AdminForth({
3737
...
3838
//diff-add
3939
new TextCompletePlugin({
40-
//diff-add
41-
openAiApiKey: process.env.OPENAI_API_KEY as string,
4240
//diff-add
4341
fieldName: 'title',
4442
//diff-add

adminforth/documentation/docs/tutorial/05-Plugins/09-open-signup.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export default {
103103
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
104104
//diff-add
105105
}),
106+
//diff-add
106107
},
107108
}),
108109
...

adminforth/index.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -207,29 +207,6 @@ class AdminForth implements IAdminForth {
207207
return plugins[0] as T;
208208
}
209209

210-
validateFieldGroups(fieldGroups: {
211-
groupName: string;
212-
columns: string[];
213-
}[], fieldTypes: {
214-
[key: string]: AdminForthResourceColumn;
215-
}): void {
216-
if (!fieldGroups) return;
217-
const allColumns = Object.keys(fieldTypes);
218-
219-
fieldGroups.forEach((group) => {
220-
group.columns.forEach((col) => {
221-
if (!allColumns.includes(col)) {
222-
const similar = suggestIfTypo(allColumns, col);
223-
throw new Error(
224-
`Group '${group.groupName}' has an unknown column '${col}'. ${
225-
similar ? `Did you mean '${similar}'?` : ''
226-
}`
227-
);
228-
}
229-
});
230-
});
231-
}
232-
233210
validateRecordValues(resource: AdminForthResource, record: any): any {
234211
// check if record with validation is valid
235212
for (const column of resource.columns.filter((col) => col.name in record && col.validation)) {
@@ -341,12 +318,6 @@ class AdminForth implements IAdminForth {
341318
// first find discovered values, but allow override
342319
res.columns[i] = { ...fieldTypes[col.name], ...col };
343320
});
344-
345-
this.validateFieldGroups(res.options.fieldGroups, fieldTypes);
346-
this.validateFieldGroups(res.options.showFieldGroups, fieldTypes);
347-
this.validateFieldGroups(res.options.createFieldGroups, fieldTypes);
348-
this.validateFieldGroups(res.options.editFieldGroups, fieldTypes);
349-
350321

351322
// check if primaryKey column is present
352323
if (!res.columns.some((col) => col.primaryKey)) {

adminforth/modules/codeInjector.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,14 @@ class CodeInjector implements ICodeInjector {
363363
}
364364
});
365365
}
366+
367+
if (resource.options?.actions) {
368+
resource.options.actions.forEach((action) => {
369+
if (action.icon) {
370+
icons.push(action.icon);
371+
}
372+
});
373+
}
366374
});
367375

368376
const uniqueIcons = Array.from(new Set(icons));

0 commit comments

Comments
 (0)