Skip to content

Commit 796e474

Browse files
authored
Merge pull request #132 from devforth/polymorphic-foreign-resources
feat: add support for polymorphic foreign resources
2 parents a69deec + 9514a4e commit 796e474

File tree

12 files changed

+577
-103
lines changed

12 files changed

+577
-103
lines changed

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/modules/configValidator.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -507,14 +507,69 @@ export default class ConfigValidator implements IConfigValidator {
507507
if (col.foreignResource) {
508508

509509
if (!col.foreignResource.resourceId) {
510-
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`);
511-
}
512-
// we do || here because 'resourceId' might yet not be assigned from 'table'
513-
const resource = this.inputConfig.resources.find((r) => r.resourceId === col.foreignResource.resourceId || r.table === col.foreignResource.resourceId);
514-
if (!resource) {
515-
const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), col.foreignResource.resourceId);
516-
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}".
510+
// resourceId is absent or empty
511+
if (!col.foreignResource.polymorphicResources && !col.foreignResource.polymorphicOn) {
512+
// foreignResource is present but no specifying fields
513+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`);
514+
} else if (!col.foreignResource.polymorphicResources || !col.foreignResource.polymorphicOn) {
515+
// some polymorphic fields are present but not all
516+
if (!col.foreignResource.polymorphicResources) {
517+
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphic foreign resource requires polymorphicResources field`);
518+
} else {
519+
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphic foreign resource requires polymorphicOn field`);
520+
}
521+
} else {
522+
// correct polymorphic structure
523+
if (!col.foreignResource.polymorphicResources.length) {
524+
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicResources `);
525+
}
526+
// we do || here because 'resourceId' might yet not be assigned from 'table'
527+
col.foreignResource.polymorphicResources.forEach((polymorphicResource, polymorphicResourceIndex) => {
528+
if (!polymorphicResource.resourceId) {
529+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has polymorphic foreign resource without resourceId`);
530+
} else if (!polymorphicResource.whenValue) {
531+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has polymorphic foreign resource without whenValue`);
532+
} else {
533+
const resource = this.inputConfig.resources.find((r) => r.resourceId === polymorphicResource.resourceId || r.table === polymorphicResource.resourceId);
534+
if (!resource) {
535+
const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), polymorphicResource.resourceId);
536+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource polymorphicResource resourceId which is not in resources: "${polymorphicResource.resourceId}".
537+
${similar ? `Did you mean "${similar}" instead of "${polymorphicResource.resourceId}"?` : ''}`);
538+
}
539+
if (col.foreignResource.polymorphicResources.findIndex((pr) => pr.resourceId === polymorphicResource.resourceId) !== polymorphicResourceIndex) {
540+
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicResource resourceId should be unique`);
541+
}
542+
}
543+
544+
if (col.foreignResource.polymorphicResources.findIndex((pr) => pr.whenValue === polymorphicResource.whenValue) !== polymorphicResourceIndex) {
545+
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicResource whenValue should be unique`);
546+
}
547+
});
548+
549+
const polymorphicOnInCol = resInput.columns.find((c) => c.name === col.foreignResource.polymorphicOn);
550+
if (!polymorphicOnInCol) {
551+
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicOn links to an unknown column`);
552+
} else if (polymorphicOnInCol.type && polymorphicOnInCol.type !== AdminForthDataTypes.STRING) {
553+
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicOn links to an column that is not of type string`);
554+
} else {
555+
const polymorphicOnColShowIn = this.validateAndNormalizeShowIn(resInput, polymorphicOnInCol, errors, warnings);
556+
if (polymorphicOnColShowIn.create || polymorphicOnColShowIn.edit) {
557+
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicOn column should not be changeable manually`);
558+
}
559+
}
560+
}
561+
} else if (col.foreignResource.polymorphicResources || col.foreignResource.polymorphicOn) {
562+
// both resourceId and polymorphic fields
563+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource cannot have resourceId and be polymorphic at the same time`);
564+
} else {
565+
// non empty resourceId and no polymorphic fields
566+
// we do || here because 'resourceId' might yet not be assigned from 'table'
567+
const resource = this.inputConfig.resources.find((r) => r.resourceId === col.foreignResource.resourceId || r.table === col.foreignResource.resourceId);
568+
if (!resource) {
569+
const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), col.foreignResource.resourceId);
570+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}".
517571
${similar ? `Did you mean "${similar}" instead of "${col.foreignResource.resourceId}"?` : ''}`);
572+
}
518573
}
519574

520575
if (col.foreignResource.unsetLabel) {

0 commit comments

Comments
 (0)