Skip to content

Latest commit

 

History

History
139 lines (97 loc) · 7.16 KB

context.md

File metadata and controls

139 lines (97 loc) · 7.16 KB

A little bit of context

  1. Context
  2. React-admin ?
  3. How (GraphQL) adaptators are working?
  4. OpenCRUD you say ?
  5. Challenges limitations with Prisma

1. Context

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.

2. React-admin?

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.

3. How (GraphQL) adaptators are working?

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.

4. OpenCRUD you say ?

OpenCRUD is a GraphQL CRUD API specification for databases. It is the specification currently followed by Prisma, and GraphCMS.

5. Challenges and limitations with Prisma

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.

Challenges will mostly be on the Create/Update part

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.

Quick explanation regarding the diff to compute nodes to connect/disconnect/update:

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.

A way to override this behavior will be necessary.

(This part is still an ongoing discussion)

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  
      }  
    },  
  }  
}
});