- Context
- React-admin ?
- How (GraphQL) adaptators are working?
- OpenCRUD you say ?
- Challenges limitations with Prisma
As powerful as prisma can be, building backoffices can be painful mostly due to the redundancy of the tasks.
As Prisma architecture suggest, it should be put behind a handcrafted GraphQL server, and be used as a bridge between the database and the actual API that will eventually be consumed. This implies that most of the CRUD mutations will inevitably have to be duplicated.
On top of that, these duplicated resolvers will need additional logic to compute which fields needs to be disconnected, which needs to be connect, created, or updated, which kill all the benefits from having an auto-generated CRUD GraphQL API.
There may be one solution though: As Prisma builds a generic GraphQL API following conventions, it means there's a door for automation. If we used Prisma directly to handle all those redundant CRUD tasks, and made use of the conventions followed by Prisma, maybe we could automate the whole process.
A powerful library to build backoffices on top of any backend
As the github package says, react-admin
is "A frontend Framework for building admin applications running in the browser on top of REST/GraphQL APIs, using ES6, React and Material Design".
react-admin
uses an adaptator approach, making it theoretically working with any kind of API that follows a predictable convention (which is the case of Prisma).
react-admin
speaks a dialect that abstract most CRUD operations (CREATE
, UPDATE
, GET_MANY
, GET_ONE
, GET_LIST
etc...). It is then the responsability of the adaptator to convert this dialect into requests that can be understood by our backend. Below is an example following GraphCool's grammar.
In fact, there's already an adaptator working with GraphCool's conventions. The only remaining job is to update this adaptator to follow Prisma's conventions.
Most GraphQL adaptators are based on a low level adaptator called ra-data-graphql made by react-admin
creators.
In a nutshell, its job is to run an introspection query on your GraphQL api, pass it to your adaptator along with the type of query that is made (CREATE
, UPDATE
, GET_MANY
, GET_ONE
, GET_LIST
, DELETE
etc...).
It is then the job of the prisma adaptator to build the GraphQL query that matches Prisma's conventions, and to provide a function that will parse the response of that query in a way that react-admin
can understand.
Once the query and the function is passed back to ra-data-graphql
, the actual HTTP requests is sent (using ApolloClient) to your GraphQL API, then the response is parsed with the provided function and that parsed response is given to ra-core
, the core of react-admin. That's it.
ra-core
=> ra-data-graphql
=> ra-data-opencrud
=> ra-data-graphql
=> ra-core
.
OpenCRUD is a GraphQL CRUD API specification for databases. It is the specification currently followed by Prisma, and GraphCMS.
As querying data with Prisma is as straightforward as with any other GraphQL API, there won't be big challenges here.
When relevant, react-admin
passes some parameters to the adaptator (pagination, sorting, and filtering). As Prisma already handles pagination, sorting and filtering, converting them to Prisma types should be easy.
react-admin
also expects a total
field when fetching several items to properly handle pagination. Prisma is also able to provide that data using <Type>Connection
fields.
But that's where all the redundancy explained above will be done once and for all.
Thanksfully, when updating a resource, react-admin
not only provides to the adaptator the updated data, but also the previous data. This will allow to compute fields that will have to be created / connected / disconnected / updated / deleted (by processing a diff like I've done here for example).
One drawback is that we will have to make an opiniated choice as how updates and creations treats references.
Here is my proposal regarding this default behavior:
CREATE
: When creating a resource, references should only be connected.
UPDATE
: When updating a resource, references should only be connected/disconnected/updated using the computations shown on the link above.
Given data
(the updated data) and previousData
(the data before the updates)
Nodes to connect
are: Nodes ids that are in data
but not in previousData
Nodes to disconnect
are: Nodes ids that are no longer in data
but in previousData
Nodes to update
are: Nodes ids that are both present in data
and previousData
.
Note: If the nodes to update haven't changed, we could still put them in an update
object to let it be idem-potent.
We might want for example to create
/delete
instead of connect
/disconnect
.
Here's a data-structure that could describe our needs, configurable by resource
and by fields
of that resource.
// exported from prisma adaptator
// Prisma mutation types
export const CONNECT = 'connect';
export const DISCONNECT = 'disconnect';
export const CREATE = 'create';
export const DELETE = 'delete';
export const UPDATE = 'update';
// Mutation "actions"
export const NEW = 'new';
export const REMOVED = 'removed';
export const UPDATED = 'updated';
// What mutations options would look like according to the default behavior described above
const defaultMutationOptions = {
resourceName: {
field1: {
UPDATE: {
[NEW]: CONNECT, //Connect the node when added
[REMOVED]: DISCONNECT, //Disconnect the node when removed
[UPDATED]: UPDATE //Update the node
},
CREATE: {
[NEW]: CONNECT
}
},
field2: { ... },
},
resourceName2: { ... }
};
buildPrismaDataProvider({
clientOptions: { uri: 'localhost' },
introspectionOptions: { ... },
// overidden mutationOptions
mutationOptions: {
Product: {
prices: {
UPDATE: {
[NEW]: CREATE, //Create the node when added
[REMOVED]: DELETE, //Delete the node when added
[UPDATED]: UPDATE //Update the node
},
CREATE: {
[NEW]: CONNECT
}
},
}
}
});