Skip to content

Commit

Permalink
feat: references are now added as separate fields with suffix (#111)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `ReferenceArrayField/Input` or `Referenceinput/Field` now require you to add a suffix to the `source` property:
`_id` or `_ids` (array).

E.g.

consider a relation between User and UserRole. A User has many UserRoles.

new:

```tsx
<ReferenceArrayField
    label="Roles"
    source="roles_ids" // <-- suffix _ids because it is an array field
    reference="UserRole"
>....

```

before:

```tsx
<ReferenceArrayField
    label="Roles"
    source="roles" // <--- old approach
    reference="UserRole"
>....

```

*Reasoning*

There was some sanitizing going on to support using the parent field name (e.g. "roles") directly,
without adding a suffix for convenience.

However there were some problems and inconsitency:

- if you use a custom fragment for a resource ("ResourceView") this sanitizing was already disabled
- mixing custom fragments with the default behavior lead to cache inconsistencies
  • Loading branch information
macrozone authored May 29, 2022
1 parent 316b497 commit 5fc2d39
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 90 deletions.
22 changes: 14 additions & 8 deletions packages/dataprovider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,9 @@ const UserFilter = (props) => (
If you have relations, you can use `ReferenceArrayField/Input` or `Referenceinput/Field`. Make sure that the reference Model is also compatible (by calling `addCrudResolvers("MyReferenceModel")` from `@ra-data-prisma/backend` on your backend).
### Sorting by relations
`<List />`s can be sorted by relations. [Enable it in the backend](../backend#enable-sort-by-relation)
Make sure to add the suffix `_id` or `_ids` (if its an array field) to the `source` property.
#### some examples:
#### Examples:
_show a list of cities with the country_
Expand All @@ -140,7 +138,11 @@ export const CityList = (props) => (
<Datagrid>
<TextField source="id" />
<TextField source="name" />
<ReferenceField label="Country" source="country" reference="Country">
<ReferenceField
label="Country"
source="country_id" // <-- suffix _id
reference="Country"
>
<TextField source="name" />
</ReferenceField>
<EditButton />
Expand All @@ -160,7 +162,7 @@ export const UserList = (props) => (
<ReferenceArrayField
alwaysOn
label="Roles"
source="roles"
source="roles_ids" // <-- suffix _ids because it is an array field
reference="UserRole"
>
<SingleFieldList>
Expand All @@ -182,7 +184,7 @@ export const UserEdit = (props) => (
<TextInput source="userName" />
<ReferenceArrayInput
label="Roles"
source="roles"
source="roles_ids" // <-- suffix _ids because it is an array field
reference="UserRole"
allowEmpty
fullWidth
Expand All @@ -194,6 +196,10 @@ export const UserEdit = (props) => (
);
```
### Sorting by relations
`<List />`s can be sorted by relations. [Enable it in the backend](../backend#enable-sort-by-relation)
### Customize fetching & virtual Resources
react-admin has [no mechanism to tell the dataprovider which fields are requested for any resources](https://github.com/marmelab/react-admin/issues/4751),
Expand Down Expand Up @@ -342,7 +348,7 @@ buildGraphQLProvider({
fragment: {
many: {
type: "document",
mode: "extend"
mode: "extend" // <---
doc: gql`
fragment OneUserWithTwitter on User {
userSocialMedia {
Expand Down
8 changes: 0 additions & 8 deletions packages/dataprovider/src/buildQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { IntrospectionResult } from "./constants/interfaces";
import getResponseParser from "./getResponseParser";
import {
FetchType,
isDeprecatedDocumentNodeFragment,
isOneAndManyFragment,
OurOptions,
ResourceFragment,
Expand Down Expand Up @@ -81,13 +80,6 @@ export const buildQueryFactory = (
resourceViewFragment,
);
const parseResponse = getResponseParser(introspectionResults, {
shouldSanitizeLinkedResources: !(
// don't sanitze on real fragments
(
resourceViewFragment &&
isDeprecatedDocumentNodeFragment(resourceViewFragment)
)
),
queryDialect: options.queryDialect,
})(aorFetchType, resource);

Expand Down
19 changes: 13 additions & 6 deletions packages/dataprovider/src/buildVariables/buildData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import isObject from "lodash/isObject";
import { IntrospectionResult } from "../constants/interfaces";
import exhaust from "../utils/exhaust";
import getFinalType from "../utils/getFinalType";
import { sanitizeData } from "../utils/sanitizeData";

enum ModifiersParams {
connect = "connect",
Expand Down Expand Up @@ -145,7 +146,12 @@ const buildNewInputValue = (
(i) => i.name === ModifiersParams.delete,
);

if (setModifier && !connectModifier && !disconnectModifier && !deleteModifier) {
if (
setModifier &&
!connectModifier &&
!disconnectModifier &&
!deleteModifier
) {
// if its a date, convert it to a date
if (
setModifier.type.kind === "SCALAR" &&
Expand Down Expand Up @@ -378,14 +384,15 @@ export const buildData = (
if (!inputType) {
return {};
}
const data = sanitizeData(params.data);
const previousData =
"previousData" in params ? sanitizeData(params.previousData) : null;
return inputType.inputFields.reduce((acc, field) => {
const key = field.name;
const fieldType =
field.type.kind === "NON_NULL" ? field.type.ofType : field.type;
const fieldData = params.data[key];
//console.log(key, fieldData, fieldType);
const previousFieldData =
(params as UpdateParams)?.previousData?.[key] ?? null;
const fieldData = data[key];
const previousFieldData = previousData?.[key] ?? null;
// TODO in case the content of the array has changed but not the array itself?
if (
isEqual(fieldData, previousFieldData) ||
Expand All @@ -395,7 +402,7 @@ export const buildData = (
}

const newVaue = buildNewInputValue(
params.data[key],
fieldData,
previousFieldData,
field.name,
fieldType,
Expand Down
13 changes: 9 additions & 4 deletions packages/dataprovider/src/buildWhere.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Resource,
} from "./constants/interfaces";
import { OurOptions } from "./types";
import { sanitizeKey } from "./utils/sanitizeData";

const getStringFilter = (
key: string,
Expand Down Expand Up @@ -491,11 +492,13 @@ const buildWhereWithType = (
const hasAnd = whereType.inputFields.some((i) => i.name === "AND");
const where = hasAnd
? Object.keys(filter ?? {}).reduce(
(acc, key) => {
(acc, keyRaw) => {
const value = filter[keyRaw];
const key = sanitizeKey(keyRaw);
// defaults to AND
const filters = getFilters(
key,
filter[key],
value,
whereType,

introspectionResults,
Expand All @@ -506,10 +509,12 @@ const buildWhereWithType = (
},
{ AND: [] },
)
: Object.keys(filter ?? {}).reduce((acc, key) => {
: Object.keys(filter ?? {}).reduce((acc, keyRaw) => {
const value = filter[keyRaw];
const key = sanitizeKey(keyRaw);
const filters = getFilters(
key,
filter[key],
value,
whereType,

introspectionResults,
Expand Down
46 changes: 15 additions & 31 deletions packages/dataprovider/src/getResponseParser.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { TypeKind } from "graphql";
import {
GET_LIST,
GET_MANY,
Expand Down Expand Up @@ -31,20 +30,12 @@ const testListTypes = (type: FetchType) => {
{
id: "user1",
firstName: "firstName1",
roles: [
{
id: "admin",
},
],
roles: [{ id: "admin" }],
},
{
id: "post2",
firstName: "firstName2",
roles: [
{
id: "admin",
},
],
roles: [{ id: "admin" }],
},
],
total: 100,
Expand All @@ -61,12 +52,14 @@ const testListTypes = (type: FetchType) => {
{
id: "user1",
firstName: "firstName1",
roles: ["admin"],
roles: [{ id: "admin" }],
roles_ids: ["admin"],
},
{
id: "post2",
firstName: "firstName2",
roles: ["admin"],
roles: [{ id: "admin" }],
roles_ids: ["admin"],
},
],
total: 100,
Expand All @@ -86,20 +79,12 @@ const testListTypes = (type: FetchType) => {
{
id: "user1",
firstName: "firstName1",
roles: [
{
id: "admin",
},
],
roles: [{ id: "admin" }],
},
{
id: "post2",
firstName: "firstName2",
roles: [
{
id: "admin",
},
],
roles: [{ id: "admin" }],
},
],
total: { _count: { _all: 100 } },
Expand All @@ -116,12 +101,14 @@ const testListTypes = (type: FetchType) => {
{
id: "user1",
firstName: "firstName1",
roles: ["admin"],
roles: [{ id: "admin" }],
roles_ids: ["admin"],
},
{
id: "post2",
firstName: "firstName2",
roles: ["admin"],
roles: [{ id: "admin" }],
roles_ids: ["admin"],
},
],
total: 100,
Expand All @@ -141,11 +128,7 @@ const testSingleTypes = (type: FetchType) => {
data: {
id: "user1",
firstName: "firstName1",
roles: [
{
id: "admin",
},
],
roles: [{ id: "admin" }],
},
},
};
Expand All @@ -158,7 +141,8 @@ const testSingleTypes = (type: FetchType) => {
data: {
id: "user1",
firstName: "firstName1",
roles: ["admin"],
roles: [{ id: "admin" }],
roles_ids: ["admin"],
},
});
});
Expand Down
49 changes: 18 additions & 31 deletions packages/dataprovider/src/getResponseParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import { IntrospectionResult, Resource } from "./constants/interfaces";
import { FetchType, QueryDialect } from "./types";

const sanitizeResource =
(
introspectionResults: IntrospectionResult,
resource: Resource,
shouldSanitizeLinkedResources: boolean = true,
) =>
(introspectionResults: IntrospectionResult, resource: Resource) =>
(data: { [key: string]: any } = {}): any => {
return Object.keys(data).reduce((acc, key) => {
if (key.startsWith("_")) {
Expand All @@ -24,25 +20,23 @@ const sanitizeResource =
if (type.kind !== TypeKind.OBJECT) {
return { ...acc, [field.name]: data[field.name] };
}

// FIXME: We might have to handle linked types which are not resources but will have to be careful about endless circular dependencies
const linkedResource = introspectionResults.resources.find(
(r) => r.type.name === type.name,
);

if (shouldSanitizeLinkedResources && linkedResource) {
const linkedResourceData = data[field.name];

if (Array.isArray(linkedResourceData)) {
return {
...acc,
[field.name]: data[field.name].map((obj) => obj.id),
};
}

// if the field contains an array of object with ids, we add a field field_ids to the data
if (
Array.isArray(data[field.name]) &&
data[field.name]?.every((c) => c.id)
) {
return {
...acc,
[field.name]: data[field.name],
[`${field.name}_ids`]: data[field.name].map((c) => c.id),
};
}
// similarly if its an object with id
if (data[field.name]?.id) {
return {
...acc,
[field.name]: data[field.name]?.id,
[field.name]: data[field.name],
[`${field.name}_id`]: data[field.name].id,
};
}

Expand All @@ -52,18 +46,11 @@ const sanitizeResource =

export default (
introspectionResults: IntrospectionResult,
{
shouldSanitizeLinkedResources = true,
queryDialect,
}: { shouldSanitizeLinkedResources?: boolean; queryDialect: QueryDialect },
{ queryDialect }: { queryDialect: QueryDialect },
) =>
(aorFetchType: FetchType, resource: Resource) =>
(response: { [key: string]: any }) => {
const sanitize = sanitizeResource(
introspectionResults,
resource,
shouldSanitizeLinkedResources,
);
const sanitize = sanitizeResource(introspectionResults, resource);
const data = response.data;

const getTotal = () => {
Expand Down
25 changes: 25 additions & 0 deletions packages/dataprovider/src/utils/sanitizeData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const sanitizeKey = (key: string) => {
if (key.endsWith("_ids")) {
return key.substring(0, key.lastIndexOf("_ids"));
}
if (key.endsWith("_id")) {
return key.substring(0, key.lastIndexOf("_id"));
}
return key;
};
/**
* Due to some implementation details in react-admin, we have to add copies with suffixed keys of certain field data.
* This function sanitizes these keys:
* - suffix _id: a string reference
* - suffix _ids: an array of ids referencing
* @param data data
* @returns data where the suffixes got removed and the original data is overwritten with the suffixed version
*/
export const sanitizeData = (data: { [key: string]: any }) => {
return Object.fromEntries(
Object.entries(data).reduce((acc, [keyRaw, value]) => {
const key = sanitizeKey(keyRaw);
return [...acc, [key, value]];
}, []),
);
};
4 changes: 2 additions & 2 deletions tsconfig-test.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

"compilerOptions": {
"module": "commonjs",
"target": "es2018",
"lib": ["es2018", "esnext.asynciterable"],
"target": "es2019",
"lib": ["es2019", "esnext.asynciterable"],
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
Expand Down

0 comments on commit 5fc2d39

Please sign in to comment.