diff --git a/packages/dataprovider/README.md b/packages/dataprovider/README.md index 90ad204..52aa0db 100644 --- a/packages/dataprovider/README.md +++ b/packages/dataprovider/README.md @@ -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 - -``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_ @@ -140,7 +138,11 @@ export const CityList = (props) => ( - + @@ -160,7 +162,7 @@ export const UserList = (props) => ( @@ -182,7 +184,7 @@ export const UserEdit = (props) => ( ( ); ``` +### Sorting by relations + +``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), @@ -342,7 +348,7 @@ buildGraphQLProvider({ fragment: { many: { type: "document", - mode: "extend" + mode: "extend" // <--- doc: gql` fragment OneUserWithTwitter on User { userSocialMedia { diff --git a/packages/dataprovider/src/buildQuery.ts b/packages/dataprovider/src/buildQuery.ts index 3a9e080..7c7b3f0 100644 --- a/packages/dataprovider/src/buildQuery.ts +++ b/packages/dataprovider/src/buildQuery.ts @@ -5,7 +5,6 @@ import { IntrospectionResult } from "./constants/interfaces"; import getResponseParser from "./getResponseParser"; import { FetchType, - isDeprecatedDocumentNodeFragment, isOneAndManyFragment, OurOptions, ResourceFragment, @@ -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); diff --git a/packages/dataprovider/src/buildVariables/buildData.ts b/packages/dataprovider/src/buildVariables/buildData.ts index 927ea68..f60765c 100644 --- a/packages/dataprovider/src/buildVariables/buildData.ts +++ b/packages/dataprovider/src/buildVariables/buildData.ts @@ -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", @@ -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" && @@ -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) || @@ -395,7 +402,7 @@ export const buildData = ( } const newVaue = buildNewInputValue( - params.data[key], + fieldData, previousFieldData, field.name, fieldType, diff --git a/packages/dataprovider/src/buildWhere.ts b/packages/dataprovider/src/buildWhere.ts index 60c7a5d..0f86668 100644 --- a/packages/dataprovider/src/buildWhere.ts +++ b/packages/dataprovider/src/buildWhere.ts @@ -12,6 +12,7 @@ import { Resource, } from "./constants/interfaces"; import { OurOptions } from "./types"; +import { sanitizeKey } from "./utils/sanitizeData"; const getStringFilter = ( key: string, @@ -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, @@ -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, diff --git a/packages/dataprovider/src/getResponseParser.test.ts b/packages/dataprovider/src/getResponseParser.test.ts index a3abfd8..b3daabe 100644 --- a/packages/dataprovider/src/getResponseParser.test.ts +++ b/packages/dataprovider/src/getResponseParser.test.ts @@ -1,4 +1,3 @@ -import { TypeKind } from "graphql"; import { GET_LIST, GET_MANY, @@ -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, @@ -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, @@ -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 } }, @@ -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, @@ -141,11 +128,7 @@ const testSingleTypes = (type: FetchType) => { data: { id: "user1", firstName: "firstName1", - roles: [ - { - id: "admin", - }, - ], + roles: [{ id: "admin" }], }, }, }; @@ -158,7 +141,8 @@ const testSingleTypes = (type: FetchType) => { data: { id: "user1", firstName: "firstName1", - roles: ["admin"], + roles: [{ id: "admin" }], + roles_ids: ["admin"], }, }); }); diff --git a/packages/dataprovider/src/getResponseParser.ts b/packages/dataprovider/src/getResponseParser.ts index 0fdfe72..68c5792 100644 --- a/packages/dataprovider/src/getResponseParser.ts +++ b/packages/dataprovider/src/getResponseParser.ts @@ -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("_")) { @@ -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, }; } @@ -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 = () => { diff --git a/packages/dataprovider/src/utils/sanitizeData.ts b/packages/dataprovider/src/utils/sanitizeData.ts new file mode 100644 index 0000000..a02675c --- /dev/null +++ b/packages/dataprovider/src/utils/sanitizeData.ts @@ -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]]; + }, []), + ); +}; diff --git a/tsconfig-test.json b/tsconfig-test.json index b4d9ee0..68075c8 100644 --- a/tsconfig-test.json +++ b/tsconfig-test.json @@ -3,8 +3,8 @@ "compilerOptions": { "module": "commonjs", - "target": "es2018", - "lib": ["es2018", "esnext.asynciterable"], + "target": "es2019", + "lib": ["es2019", "esnext.asynciterable"], "emitDecoratorMetadata": true, "experimentalDecorators": true }