Skip to content

Commit 62f4246

Browse files
authored
Merge pull request #511 from devforth/feature/AdminForth/1155/backend-validation-function-le
Feature/admin forth/1155/backend validation function le
2 parents ea82f69 + 0d9641c commit 62f4246

File tree

17 files changed

+3691
-166
lines changed

17 files changed

+3691
-166
lines changed

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

Lines changed: 75 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -677,34 +677,6 @@ export default {
677677
> `minValue` and `maxValue` checks are enforced both on frontend and backend.
678678
679679

680-
### Validation
681-
682-
In cases when column values must follow certain format, you can add `validation` to it.
683-
`validation` is an array of rules, each containing `regExp` that defines a format for a value and `message` that will be displayed in case when entered value does not pass the check.
684-
685-
```typescript title="./resources/adminuser.ts"
686-
export default {
687-
name: 'adminuser',
688-
columns: [
689-
...
690-
{
691-
name: 'email',
692-
required: true,
693-
isUnique: true,
694-
validation: [
695-
{
696-
regExp: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
697-
message: 'Email is not valid, must be in format example@test.com',
698-
},
699-
],
700-
},
701-
],
702-
},
703-
...
704-
],
705-
```
706-
707-
> `validation` checks are enforced both on frontend and backend.
708680

709681
### Input prefix and suffix
710682

@@ -967,6 +939,81 @@ When defined like this, adminforth will use value in `property_type` to figure o
967939

968940
If `beforeDatasourceRequest` or `afterDatasourceResponse` hooks are set for polymorphic foreign resource, they will be called for each resource in `polymorphicResources` array.
969941

942+
## Validation (create/edit view)
943+
In cases when column values must follow certain format, you can add `validation` to it.
944+
`validation` is an array of rules, each containing `regExp` or `validator function` that defines a format for a value and `message` that will be displayed in case when entered value does not pass the check.
945+
946+
### Frontend validation
947+
We recommend to use this validation with regExp, because it validates fields in real time
948+
949+
```typescript title="./resources/adminuser.ts"
950+
export default {
951+
name: 'adminuser',
952+
columns: [
953+
...
954+
{
955+
name: 'email',
956+
required: true,
957+
isUnique: true,
958+
//diff-add
959+
validation: [
960+
//diff-add
961+
{
962+
//diff-add
963+
regExp: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
964+
//diff-add
965+
message: 'Email is not valid, must be in format example@test.com',
966+
//diff-add
967+
},
968+
//diff-add
969+
],
970+
},
971+
],
972+
},
973+
...
974+
],
975+
```
976+
977+
### Backend validation
978+
There might be rare cases, when you want to have your own validation function. For this you can use `validator` callback:
979+
```typescript title="./resources/adminuser.ts"
980+
export default {
981+
name: 'adminuser',
982+
columns: [
983+
...
984+
{
985+
name: 'email',
986+
required: true,
987+
isUnique: true,
988+
//diff-add
989+
validation: [
990+
//diff-add
991+
{
992+
//diff-add
993+
async validator(value: any, record: any) {
994+
//diff-add
995+
if (value.endsWith("@example.com")) {
996+
//diff-add
997+
return { isValid: false, message: 'Your email can`t end with `@example.com`' };
998+
//diff-add
999+
}
1000+
//diff-add
1001+
return { isValid: true }
1002+
//diff-add
1003+
}
1004+
//diff-add
1005+
}
1006+
//diff-add
1007+
],
1008+
},
1009+
],
1010+
},
1011+
...
1012+
],
1013+
```
1014+
1015+
>Avoid using a custom validator because every time you change a field (after a failed first save attempt), it will make an API call.
1016+
9701017
## Filtering
9711018

9721019
### Filter Options

adminforth/documentation/docs/tutorial/08-Plugins/05-1-upload-api.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,4 +405,30 @@ app.post(
405405
406406
> ⚠️ Do not call `commitUrlToUpdateExistingRecord` or `commitUrlToNewRecord` from the same resource’s `beforeSave`/`afterSave` hooks, as it would create an infinite loop of updates/creates. These methods are intended to be called from your own API endpoints after the browser upload finishes.
407407
408+
## Handling Internal vs. External Links (isInternalUrl)
409+
410+
Have you ever encountered a situation where a user pastes a beautiful image directly from Google Images into the Markdown editor, but your system tries to save it as if it were a file from your own S3 bucket? Previously, this could lead to "broken" paths and corrupted data in your database.
411+
412+
### You can easily use this method in your code:
413+
Imagine you are building a custom preview page or a specialized plugin, and you need to know: Is this file something we manage in our storage, or is it just a link to an external resource?
414+
415+
```ts title="./index.ts"
416+
// Get the upload plugin instance
417+
const uploadPlugin = admin.getPluginById('my_s3_storage');
418+
419+
const urlFromEditor = "https://my-bucket.s3.eu-central-1.amazonaws.com/apartments/flat.png";
420+
const externalUrl = "https://images.unsplash.com/photo-1503376780353-7e6692767b70";
421+
422+
// Check the first link
423+
if (await uploadPlugin.isInternalUrl(urlFromEditor)) {
424+
console.log("✅ This is our file. We can extract the path and manage it in S3.");
425+
const path = uploadPlugin.getPathFromUrl(urlFromEditor);
426+
// Result: "capartmentsars/flat.png"
427+
}
428+
429+
// Check the second link
430+
if (!(await uploadPlugin.isInternalUrl(externalUrl))) {
431+
console.log("🌐 This is an external link.");
432+
}
433+
```
408434

adminforth/modules/restApi.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,9 +610,10 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
610610
let validation = null;
611611
if (col.validation) {
612612
validation = await Promise.all(
613-
col.validation.map(async (val) => {
613+
col.validation.map(async (val, index) => {
614614
return {
615615
...val,
616+
validator: inCol.validation[index].validator ? true: false,
616617
message: await tr(val.message, `resource.${resource.resourceId}`),
617618
}
618619
})
@@ -1588,7 +1589,46 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
15881589
}
15891590
}
15901591
});
1592+
server.endpoint({
1593+
method: 'POST',
1594+
path: '/validate_columns',
1595+
handler: async ({ body, adminUser, query, headers, cookies, requestUrl, response }) => {
1596+
const { resourceId, editableColumns, record } = body;
1597+
const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
1598+
if (!resource) {
1599+
return { error: `Resource '${resourceId}' not found` };
1600+
}
1601+
const validationResults = {};
1602+
const customColumnValidatorsFunctions = [];
1603+
for (const col of editableColumns) {
1604+
const columnConfig = resource.columns.find((c) => c.name === col.name);
1605+
if (columnConfig && columnConfig.validation) {
1606+
customColumnValidatorsFunctions.push(async ()=>{
1607+
for (const val of columnConfig.validation) {
1608+
if (val.validator) {
1609+
const result = await val.validator(col.value, record, this.adminforth);
1610+
if (typeof result === 'object' && result.isValid === false) {
1611+
validationResults[col.name] = {
1612+
isValid: result.isValid,
1613+
message: result.message,
1614+
}
1615+
break;
1616+
}
1617+
}
1618+
}
1619+
})
1620+
}
1621+
}
1622+
1623+
if (customColumnValidatorsFunctions.length) {
1624+
await Promise.all(customColumnValidatorsFunctions.map((fn) => fn()));
1625+
}
15911626

1627+
return {
1628+
validationResults
1629+
}
1630+
}
1631+
});
15921632
// setup endpoints for all plugins
15931633
this.adminforth.activatedPlugins.forEach((plugin) => {
15941634
plugin.setupEndpoints(server);

0 commit comments

Comments
 (0)