Skip to content

Latest commit

 

History

History
635 lines (525 loc) · 17.5 KB

README.md

File metadata and controls

635 lines (525 loc) · 17.5 KB

@ra-data-prisma/dataprovider

Data provider for react admin

Usage

yarn add @ra-data-prisma/dataprovider

Make sure your backend API is compatible by using the other package in this repo backend. Alternativly, you can connect to a backend that was created using typegraphql-prisma. See Chapter typegraphql-prisma

Add the dataprovider to your react-admin app:

import React, { Component } from "react"

import { Admin, Resource, Datagrid, TextField, Login } from "react-admin"
import { useDataProvider } from "@ra-data-prisma/dataprovider"
import { UserList, UserEdit, UserCreate } from "./resources/user"

import useAuthProvider from "./useAuthProvider"


const AdminApp = () => {
  const dataProvider = useDataProvider({
    clientOptions: { uri: "/graphql" }
    aliasPrefix: "admin" // 👈 set this, if you use a aliasPrefix on your backend as well (recommended)
    filters: {} // custom filters
    queryDialect: "nexus-prisma" // customize query dialect, defaults to nexus-prisma,
  })
  const authProvider = useAuthProvider()

  if (!dataProvider) {
    return <div>Loading</div>
  }

  return (
    <Admin
      dataProvider={dataProvider}
      authProvider={authProvider}
    >
      <Resource
        name="User"
        list={UserList}
        edit={UserEdit}
        create={UserCreate}
      />
    </Admin>
  )
}

export default AdminApp

Features

aliasPrefix

Set aliasPrefix if you have set it on the backend as well (see backend ).

Search & Filtering

this dataprovider supports all filtering and searching and adds some convenience to it:

  • intelligent handling of strings and ints: you don't have to specify equals or contains, we do that for you. The filter can just be {firstname: "albert"} and we create the correct graphql query for that
  • extended react-admin input fields: you can directly use comparison operators on NumberInputs, TextInputs and DateInputs. For example, implementing "Created since" filter (DateInput on a field createdAt) would become
    <DateInput label="Created since" source="createdAt_gt" />
    • available comparisons (default comparison is the one which would be used if omitted):
      • ints, floats and datetimes - gt, gte, lt, lte, equals (default = equals)
      • strings - gt, gte, lt, lte, equals, contains, startsWith, endsWith (default = contains)
  • case insensitive: If your Prisma version supports it (>= 2.8.0), we automatically query strings as case insensitive. If your Prisma version doesn't support it, we emulate it with query for multiple variations of the search term: as-is, fully lowercase, first letter uppercase. This does work in many cases (searching for terms and names), but fails in some.
  • q query. q is a convention in react-admin for general search. We implement this client side. A query on q will search all string fields and int fields on a resource. It additionaly splits multiple search terms and does an AND search
  • if you need more sophisticated search, you can use normal nexus-prisma graphql queries. You can even mix it with q and the intelligent short notation

Custom filters

if you have a complex query, you can define custom filters:

 const dataProvider = useDataProvider({
    clientOptions: { uri: "/graphql" }

    filters: {
      onlyMillenials: (value?: boolean) =>
        value === true
          ? {
              AND: [
                {
                  yearOfBirth: {
                    gte: 1981,
                  },
                },
                {
                  yearOfBirth: {
                    lte: 1996,
                  },
                },
              ],
            }
          : undefined,
    },
  })

Then you can use that in a react-admin filter:

const UserFilter = (props) => (
  <Filter {...props}>
    <TextInput label="Search" source="q" alwaysOn />
    <BooleanInput
      label="Show only millenials"
      source="onlyMillenials"
      alwaysOn
    />
  </Filter>
);

Notice if you want to omit the filter, return null or undefined.

Relations

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).

Make sure to add the suffix _id or _ids (if its an array field) to the source property.

Examples:

show a list of cities with the country

export const CityList = (props) => (
  <List {...props}>
    <Datagrid>
      <TextField source="id" />
      <TextField source="name" />
      <ReferenceField
        label="Country"
        source="country_id" // <-- suffix _id
        reference="Country"
      >
        <TextField source="name" />
      </ReferenceField>
      <EditButton />
    </Datagrid>
  </List>
);

show all user roles in the user list

export const UserList = (props) => (
  <List {...props}>
    <Datagrid>
      <TextField source="id" />
      <TextField source="username" />
      <ReferenceArrayField
        alwaysOn
        label="Roles"
        source="roles_ids" // <-- suffix _ids because it is an array field
        reference="UserRole"
      >
        <SingleFieldList>
          <ChipField source="name" />
        </SingleFieldList>
      </ReferenceArrayField>
      <EditButton />
    </Datagrid>
  </List>
);

edit the roles for a user

export const UserEdit = (props) => (
  <Edit title={<UserTitle />} {...props} undoable={false}>
    <SimpleForm variant="outlined">
      <TextInput source="userName" />
      <ReferenceArrayInput
        label="Roles"
        source="roles_ids" // <-- suffix _ids because it is an array field
        reference="UserRole"
        allowEmpty
        fullWidth
      >
        <SelectArrayInput optionText="id" />
      </ReferenceArrayInput>
    </SimpleForm>
  </Edit>
);

Sorting by relations

<List />s can be sorted by relations. Enable it in the backend

Customize fetching & virtual Resources

react-admin has no mechanism to tell the dataprovider which fields are requested for any resources, we therefore load all fields for a resource. If a field points to an existing Resource, we only fetch the id of that resource.

But sometimes you need to customize this behaviour, e.g.:

  • you want to load a nested resource with all properties for easier export
  • you have large/slow resolvers in some resources and don't want to load these for performance reasons

We therefore provide a way to customize the loaded field-set by defining fragments, blacklists and whitelists.

Additionaly you can use that to create "virtual resources" that show other fields.

Basic usage

using one fragment for both one and many:

buildGraphQLProvider({
  clientOptions: { uri: "/api/graphql" } as any,
  resourceViews: {
    <local resource name>: {
      resource: <backend resource name>,
      fragment: <fragment to use>
    },
  },
});

You can also specify different fragments for one and many (or ommit one of those):

buildGraphQLProvider({
  clientOptions: { uri: "/api/graphql" } as any,
  resourceViews: {
    <local resource name>: {
      resource: <backend resource name>,
      fragment: {
        one: <fragment for one>
        many: <fragment for many>
      }
    },
  },
});
  • <backend resource name>: the name of an existing Resource that is defined on the backend
  • <local resource name>: if you use the same name as <backend resource name>, you basically customize that Resource. You can chose a different name to create a "virtual" resource.
  • <fragment for ...>: Specifies which fields are loaded when fetching one or many records. See below for available fragment types
  • if you only specify one or many, it will fall back to the default behavior if either would be used.

Gotcha: when using different fragments for one and many, the detail view might be loaded with certain fields being undefined. See this discussion

Fragment type blacklist / whitelist

Use this if you want to exclude certain fields (blacklist) or only include certain fields:

// whitelist
buildGraphQLProvider({
  clientOptions: { uri: "/api/graphql" } as any,
  resourceViews: {
    Users: {
      resource: "Users",
      fragment: {
        many: {
          type: "whitelist",
          fields: ["id", "firstName", "lastName"],
        },
      },
    },
  },
});
// blacklist
buildGraphQLProvider({
  clientOptions: { uri: "/api/graphql" } as any,
  resourceViews: {
    Users: {
      resource: "Users",
      fragment: {
        many: {
          type: "blacklist",
          fields: ["roles", "avatarImage"],
        },
      },
    },
  },
});

Fragment type "document"

You can use graphql Fragments (DocumentNode) to presicely select fields. This is more verbose than using blacklists / whitelists, but enables you to deeply select fields. Additionaly your IDE can typecheck the fragment (e.g. when using the apollo extension in vscode).

By default, the fragment replaces the default fields.

import gql from "graphql-tag";

buildGraphQLProvider({
  clientOptions: { uri: "/api/graphql" } as any,
  resourceViews: {
    Users: {
      resource: "Users",
      fragment: {
        many: {
          type: "document",
          // mode: "replace" <--- that is the default
          doc: gql`
            fragment OneUserWithTwitter on User {
              id
              firstName
              lastName
              userSocialMedia {
                twitter
              }
            }
          `,
        },
      },
    },
  },
});

If you want to extend the default fields with a fragment, use mode: "extend" instead:

import gql from "graphql-tag";

buildGraphQLProvider({
  clientOptions: { uri: "/api/graphql" } as any,
  resourceViews: {
    Users: {
      resource: "Users",
      fragment: {
        many: {
          type: "document",
          mode: "extend" // <---
          doc: gql`
            fragment OneUserWithTwitter on User {
              userSocialMedia {
                twitter
              }
            }
          `,
        },
      },
    },
  },
});

this will fetch all default fields plus the fields of the fragment. If the fragment declares a field with the same name as a default field, the fragment's field replaces the default one.

Virtual Resources

You can use a different name for a resource, that does not exist on the backend:

// real world example
buildGraphQLProvider({
  clientOptions: { uri: "/api/graphql" } as any,
  resourceViews: {
    ParticipantsToInvoice: {
      resource: "ChallengeParticipation",
      fragment: {
        one: {
          type: "document",
          doc: gql`
            fragment OneBilling on ChallengeParticipation {
              challenge {
                title
              }
              user {
                email
                firstname
                lastname
                school {
                  name
                  address
                  city {
                    name
                    zipCode
                    canton {
                      id
                    }
                  }
                }
              }
              teamsCount
              teams {
                name
              }
            }
          `,
        },
        many: {
          type: "document",
          doc: gql`
            fragment ManyBillings on ChallengeParticipation {
              challenge {
                title
              }
              user {
                email
                firstname
                lastname
                school {
                  name
                  address
                }
              }
              teams {
                name
              }
            }
          `,
        },
      },
    },
  },
});

Now you have a new virtual resource ParticipantsToInvoice that can be used to display a List or for one record. (notice: update/create/delete is currently not specified, so use it read-only) and it will have exactly this data.

There are two ways you can use this new virtual resource. If you want to use it with React-Admin's query hooks (useQuery, useGetList, useGetOne), you need to add this as a new <Resource>:

<Admin>
  // ...
  <Resource name="ParticipantsToInvoice" />
</Admin>

These hooks rely on Redux store and will throw an error if the resource isn't defined.

However, if you directly use data provider calls, you can use it with defined <Resource> but also without as it directly calls data provider.

const dataProvider = useDataProvider(options);
const { data } = await dataProvider.getList("ParticipantsToInvoice", {
  pagination: { page: 1, perPage: 10 },
  sort: { field: "id", order: "ASC" },
  filter: {},
});

Customize input data on create / update (experimental)

You can alter the data sent to create / update mutations.

E.g. Consider having a field <TextField source="userEmail" />. userEmail does not exist on the Resource, but you want to use this field to customize the data sent to the backend:

const dataProvider = useDataProvider({
  customizeInputData: {
    MyResource: {
      create: (data, params) => ({
        ...data,
        user: {
          connectOrCreate: {
            create: {
              email: params.userEmail,
            },
            where: {
              email: params.userEmail,
            },
          },
        },
      }),
      update: // ...
    },
  },
});

Use without introspection

the dataprovider uses introspection in order to create the right queries for your backend. But if you bundle the IntrospectionSchema or graphql schema, you can use it directly.

use the introspection schema

you can download a introspection schema (in the json format) with:

npx apollo schema:download --endpoint=http://localhost:3000 ./src/graphql-schema.json

and then:

import schema from "../graphql-schema.json";

useDataProvider({
 // ...
 introspection: {
   schema: schema.__schema
 }
})

use with graphql file:

this assumes that you can load and bundle the graphql file (e.g. with raw-loader):

import { buildSchema, introspectionFromSchema } from "graphql";


useDataProvider({
 // ...
 introspection: {
   schema: introspectionFromSchema(
      buildSchema(
        require("!!raw-loader!../schema.graphql").default
      )
    ).__schema
 }
})

Usage with typegraphql-prisma

(beta)

You can use the dataprovider to connect to a backend that was created using https://www.npmjs.com/package/typegraphql-prisma. It has a slightly different dialect. Pass the following option to the dataprovider:

const dataProvider = useDataProvider({
    clientOptions: { uri: "/graphql" }
    queryDialect: "typegraphql" // 👈
})

override mutation or query operation names due to prisma versions breaking changes

You can override operation names depending on the version of typegraphql-prisma you are using:

import {
  CREATE,
  UPDATE,
  DELETE,
  GET_LIST,
  GET_MANY,
  GET_MANY_REFERENCE,
  GET_ONE,
} from "react-admin";
import { Options } from "@ra-data-prisma/dataprovider";
import camelCase from "lodash/camelCase";
import pluralize from "pluralize";

const options: Options = {
  queryDialect: "typegraphql",
  mutationOperationNames: {
    typegraphql: {
      [CREATE]: (resource) => `createOne${resource.name}`,
      [UPDATE]: (resource) => `updateOne${resource.name}`,
      [DELETE]: (resource) => `deleteOne${resource.name}`,
    },
  },
  queryOperationNames: {
    typegraphql: {
      [GET_LIST]: (resource) => `${pluralize(camelCase(resource.name))}`,
      [GET_MANY]: (resource) => `${pluralize(camelCase(resource.name))}`,
      [GET_MANY_REFERENCE]: (resource) =>
        `${pluralize(camelCase(resource.name))}`,
      [GET_ONE]: (resource) => `${camelCase(resource.name)}`,
    },
  },
};

If you are using an alias prefix, be sure to include it in your custom operation names:

import { CREATE, UPDATE, DELETE } from "react-admin";
import { Options, makePrefixedFullName } from "@ra-data-prisma/dataprovider";

const aliasPrefix = "admin";

const prefix = (s: string) => makePrefixedFullName(s, aliasPrefix);

const options: Options = {
  aliasPrefix,
  queryDialect: "typegraphql",
  mutationOperationNames: {
    typegraphql: {
      [CREATE]: (resource) => prefix(`createOne${resource.name}`),
      [UPDATE]: (resource) => prefix(`updateOne${resource.name}`),
      [DELETE]: (resource) => prefix(`deleteOne${resource.name}`),
    },
  },
  queryOperationNames: {
    typegraphql: {
      [GET_LIST]: (resource) =>
        prefix(`${pluralize(camelCase(resource.name))}`),
      [GET_MANY]: (resource) =>
        prefix(`${pluralize(camelCase(resource.name))}`),
      [GET_MANY_REFERENCE]: (resource) =>
        prefix(`${pluralize(camelCase(resource.name))}`),
      [GET_ONE]: (resource) => prefix(`${camelCase(resource.name)}`),
    },
  },
};

Usage with pothos-prisma

(beta)

You can use the dataprovider to connect to a backend that was created using https://www.npmjs.com/package/@pothos/plugin-prisma-utils. It has slightly different OrderBy values. Pass the following option to the dataprovider:

const dataProvider = useDataProvider({
    clientOptions: { uri: "/graphql" }
    queryDialect: "pothos-prisma" // 👈
})