Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -539,9 +539,9 @@ This way, when creating or editing a record you will be able to choose value for

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:

```typescript title="./resources/adminuser.ts"
```typescript title="./resources/apartments.ts"
export default {
name: 'adminuser',
name: 'apartments',
columns: [
...
{
Expand All @@ -558,6 +558,96 @@ export default {
],
```

### Polymorphic foreign resources

Sometimes it is needed for one column to be a foreign key for multiple tables. For example, given the following schema:

```prisma title="./schema.prisma"
...
model apartments {
id String @id
created_at DateTime?
title String
square_meter Float?
price Decimal
number_of_rooms Int?
realtor_id String?
}

model houses {
id String @id
created_at DateTime?
title String
house_square_meter Float?
land_square_meter Float?
price Decimal
realtor_id String?
}

model sold_property {
id String @id
created_at DateTime?
title String
property_id String
realtor_id String?
}

```

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.

```prisma title="./schema.prisma"
...

model sold_property {
id String @id
created_at DateTime?
title String
//diff-add
property_type String
property_id String
realtor_id String?
}

```

`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:

```typescript title="./resources/sold_property.ts"
export default {
name: 'sold_property',
columns: [
...
{
name: "property_type",
showIn: { create: false, edit: false },
},
{
name: "property_id",
foreignResource: {
polymorphicResources: [
{
resourceId: 'apartments',
whenValue: 'apartment',
},
{
resourceId: 'houses',
whenValue: 'house',
},
],
polymorphicOn: 'property_type',
},
},
],
},
...
],
```

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.

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

## Filtering

### Filter Options
Expand Down
69 changes: 62 additions & 7 deletions adminforth/modules/configValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,14 +507,69 @@ export default class ConfigValidator implements IConfigValidator {
if (col.foreignResource) {

if (!col.foreignResource.resourceId) {
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`);
}
// we do || here because 'resourceId' might yet not be assigned from 'table'
const resource = this.inputConfig.resources.find((r) => r.resourceId === col.foreignResource.resourceId || r.table === col.foreignResource.resourceId);
if (!resource) {
const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), col.foreignResource.resourceId);
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}".
// resourceId is absent or empty
if (!col.foreignResource.polymorphicResources && !col.foreignResource.polymorphicOn) {
// foreignResource is present but no specifying fields
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`);
} else if (!col.foreignResource.polymorphicResources || !col.foreignResource.polymorphicOn) {
// some polymorphic fields are present but not all
if (!col.foreignResource.polymorphicResources) {
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphic foreign resource requires polymorphicResources field`);
} else {
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphic foreign resource requires polymorphicOn field`);
}
} else {
// correct polymorphic structure
if (!col.foreignResource.polymorphicResources.length) {
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicResources `);
}
// we do || here because 'resourceId' might yet not be assigned from 'table'
col.foreignResource.polymorphicResources.forEach((polymorphicResource, polymorphicResourceIndex) => {
if (!polymorphicResource.resourceId) {
errors.push(`Resource "${res.resourceId}" column "${col.name}" has polymorphic foreign resource without resourceId`);
} else if (!polymorphicResource.whenValue) {
errors.push(`Resource "${res.resourceId}" column "${col.name}" has polymorphic foreign resource without whenValue`);
} else {
const resource = this.inputConfig.resources.find((r) => r.resourceId === polymorphicResource.resourceId || r.table === polymorphicResource.resourceId);
if (!resource) {
const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), polymorphicResource.resourceId);
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource polymorphicResource resourceId which is not in resources: "${polymorphicResource.resourceId}".
${similar ? `Did you mean "${similar}" instead of "${polymorphicResource.resourceId}"?` : ''}`);
}
if (col.foreignResource.polymorphicResources.findIndex((pr) => pr.resourceId === polymorphicResource.resourceId) !== polymorphicResourceIndex) {
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicResource resourceId should be unique`);
}
}

if (col.foreignResource.polymorphicResources.findIndex((pr) => pr.whenValue === polymorphicResource.whenValue) !== polymorphicResourceIndex) {
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicResource whenValue should be unique`);
}
});

const polymorphicOnInCol = resInput.columns.find((c) => c.name === col.foreignResource.polymorphicOn);
if (!polymorphicOnInCol) {
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicOn links to an unknown column`);
} else if (polymorphicOnInCol.type && polymorphicOnInCol.type !== AdminForthDataTypes.STRING) {
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicOn links to an column that is not of type string`);
} else {
const polymorphicOnColShowIn = this.validateAndNormalizeShowIn(resInput, polymorphicOnInCol, errors, warnings);
if (polymorphicOnColShowIn.create || polymorphicOnColShowIn.edit) {
errors.push(`Resource "${res.resourceId}" column "${col.name}" polymorphicOn column should not be changeable manually`);
}
}
}
} else if (col.foreignResource.polymorphicResources || col.foreignResource.polymorphicOn) {
// both resourceId and polymorphic fields
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource cannot have resourceId and be polymorphic at the same time`);
} else {
// non empty resourceId and no polymorphic fields
// we do || here because 'resourceId' might yet not be assigned from 'table'
const resource = this.inputConfig.resources.find((r) => r.resourceId === col.foreignResource.resourceId || r.table === col.foreignResource.resourceId);
if (!resource) {
const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), col.foreignResource.resourceId);
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}".
${similar ? `Did you mean "${similar}" instead of "${col.foreignResource.resourceId}"?` : ''}`);
}
}

if (col.foreignResource.unsetLabel) {
Expand Down
Loading