Reindex
TeamBlogDocs
By

Build a GraphQL shopping cart API with Reindex

Reindex is a great service for quickly prototyping and building a GraphQL API for your app. But it also simplifies moving from a prototype to a production system and scaling your app. Authorization, an essential feature for secure production apps, determines which resources each user can access and perform operations on. We’ve built a flexible permission system into Reindex for implementing authorization.

In this tutorial, we’ll explore the Reindex permissions by building a shopping cart API with sophisticated access controls. We’ll then have a GraphQL API with data storage and authentication on which we can build a client-side shopping app (with React Native and Relay, for example).

The Data Model of The Shopping Cart

We’ll start by sketching the data model. It comprises all the data types used to create the app and will be the basis of the app’s schema.

  • Shop: Represents a shop, which has an owner, staff, and lists of products and orders.

  • User: Represents any person using the app, including customers, shop owners and staff.

  • Order: Belongs to a user and a shop, and has a bunch of line items.

  • Line Item: Has a quantity of some product in the order.

  • Product: Belongs to a shop and can be included in line items.

Below is a chart of this data model.

A diagram of the data model

Defining the Schema

Now we can express the data model as a Reindex schema. The schema is simply a machine readable representation of the data model, which Reindex uses to create a GraphQL API for your app. Let’s begin with the Shop and User types.

Shop and User

[
  {
    name: 'Shop',
    kind: 'OBJECT',
    interfaces: ['Node'],
    fields: [
      { name: 'id', type: 'ID', nonNull: true, unique: true },
      { name: 'owner', type: 'User', reverseName: 'ownShops' },
      { name: 'staff', type: 'Connection', ofType: 'User',
        reverseName: 'staffShops' },
    ],
  },
  {
    name: 'User',
    kind: 'OBJECT',
    interfaces: ['Node'],
    fields: [
      { name: 'id', type: 'ID', nonNull: true, unique: true },
      { name: 'ownShops', type: 'Connection', ofType: 'Shop',
        reverseName: 'owner' },
      { name: 'staffShops', type: 'Connection', ofType: 'Shop',
        reverseName: 'staff' },
    ],
  }
]

Both types implement the Node interface, so they must have an id field. owner and ownShops form a one-to-many relationship between a User and several Shops, and staff and staffShops form a many-to-many relationship.

Order, LineItem, and Product

The types of orders, line items, and products can be defined similarly:

[
  // ...
  {
    name: 'Order',
    kind: 'OBJECT',
    interfaces: ['Node'],
    fields: [
      { name: 'id', type: 'ID', nonNull: true, unique: true },
      { name: 'status', type: 'String' },
      { name: 'customer', type: 'Connection', ofType: 'User',
        reverseName: 'orders' },
      { name: 'shop', type: 'Shop', reverseName: 'orders' },
      { name: 'lineItems', type: 'Connection', ofType: 'LineItem',
        reverseName: 'order' },
    ],
  },
  {
    name: 'LineItem',
    kind: 'OBJECT',
    interfaces: ['Node'],
    fields: [
      { name: 'id', type: 'ID', nonNull: true, unique: true },
      { name: 'order', type: 'Order', reverseName: 'lineItems' },
      { name: 'product', type: 'Product', reverseName: 'lineItems' },
      { name: 'quantity', type: 'Int' },
    ],
  },
  {
    name: 'Product',
    kind: 'OBJECT',
    interfaces: ['Node'],
    fields: [
      { name: 'id', type: 'ID', nonNull: true, unique: true },
      { name: 'name', type: 'String' },
      { name: 'shop', type: 'Shop', reverseName: 'products' },
      { name: 'lineItems', type: 'Connection', ofType: 'LineItem',
        reverseName: 'product' },
    ],
  },
]

Finally, we must add the reverse fields, which refer to these types in Shop:

{ name: 'orders', type: 'Connection', ofType: 'Order',
  reverseName: 'shop' },
{ name: 'products', type: 'Connection', ofType: 'Product',
  reverseName: 'shop' },

as well as the orders field in User:

{ name: 'orders', type: 'Connection', ofType: 'Order',
  reverseName: 'customer' },

We’ll save this schema in a file named ReindexSchema.json, then push it to Reindex with the CLI. (If you don’t have a Reindex app yet, you can get one here for free.)

reindex login
reindex schema-push

When you run the schema-push command, the Reindex service reads the data model you’ve defined in the schema file and creates a GraphQL API for it, including paginated lists and mutations for updating the data. You get a fully functional Relay-compatible GraphQL API that stores data in a hosted MongoDB database.

We can now open the app in GraphiQL (an in-browser GraphQL IDE) with the reindex graphiql command, play with the API, and make some queries.

We haven’t yet defined any of the permissions that will allow all users to access the data. Let’s add those permissions next.

Permissions

The permissions of Reindex correspond to the graph-like structure of the data stored in Reindex: if a type has a field that contains a User type or is otherwise related to a user (even if through a long chain of relationships), you can grant permissions to the related user.

For example, an order is related to a user, not only through the customer field, but also indirectly, through the owner and staff fields of the shop associated with the order. All these relationships can be the basis of permissions. For example, to allow the shop owners to update their shops’ orders, we can create and add to the Order type the following permission:

{
  name: 'Order',
  // ...
  permissions: [
    { grantee: 'USER', userPath: ['shop', 'owner'], update: true },
  ],
}

The chain of field names, or userPath, can be as long as necessary, as long as it begins with a field of the type we’re defining a permission for and ends with a User or UserConnection field. This is quite a powerful feature. Specifying permissions through chains of fields allows us to model fairly complex permissions without writing complex code or maintaining separate access control lists or roles—it’s all declarative and all defined in terms of the data.

Permissions for All Users

Everyone should be able to see the shops and their products, so we’ll start by adding this permission to both Product and Shop:

{ grantee: 'EVERYONE', read: true }

We also want to allow authenticated users to see other users’ public information, so we’ll add this permission to User:

{
  grantee: 'AUTHENTICATED',
  read: true,
}

Shop

A shop should only be created, updated, and deleted by its owner. Adding to Shop a permission that refers to the owner field and grants these rights creates the restrictions:

{
  grantee: 'USER',
  userPath: ['owner'],
  create: true,
  update: true,
  delete: true
}

Permissions and Relationships

We also want to allow users to add staff to their own shops. To add a relationship between nodes, the user must have access to the fields on both sides of the relationship. Therefore, we must add a permission to User that allows other authenticated users to update the staffShops field:

{
  grantee: 'AUTHENTICATED',
  update: true,
  permittedFields: ['staffShops'],
}

Now, when a user wants to add or remove an employee, Reindex verifies that the user has a permission to update the staff field of Shop and staffShops field of User. This ensures that users can only add staff to their own shop. The permission we added above will make the staff field available to users who are adding themselves as owners and not attempting to create a shop on someone else’s account. staffShops permission is granted to authenticated users.

The list of a shop’s orders must be updated when a new order is placed, so we’ll add this to the Shop permissions:

{
  grantee: 'AUTHENTICATED',
  update: true,
  permittedFields: ['orders'],
},

Authenticated users need a permission to update Product.lineItems so that the user who is creating a line item can add a product to it:

{
  grantee: 'AUTHENTICATED',
  update: true,
  permittedFields: ['lineItems'],
},

Permissions for Product

We want to allow shop owners to create, update, and delete products that belong to their shops (i.e., products with a value of shop that corresponds to their shops). Nothing new here, let’s simply add this to the permissions of Shop:

{
  grantee: 'USER',
  userPath: ['shop', 'owner'],
  create: true,
  update: true,
  delete: true,
}

Permissions for Order and LineItem

Finally, let’s define the permissions for Order and LineItem.

We want the shop owner to be able to change the orders and line items. Customers should be able to create orders and see their own orders. In addition, the staff should be able to see the orders and update order status.

Let’s add these privileges. Here are the permissions to be added to Order:

// The shop owner can read, create and update orders.
{
  grantee: 'USER',
  userPath: ['shop', 'owner'],
  create: true,
  read: true,
  update: true,
},
// The shop staff can read the orders of the shop.
{
  grantee: 'USER',
  userPath: ['shop', 'staff'],
  read: true,
},
// The shop staff can update the status of an order.
{
  grantee: 'USER',
  userPath: ['shop', 'staff'],
  update: true,
  permittedFields: ['status'],
},
// The customer can create orders and read their own orders.
{
  grantee: 'USER',
  userPath: ['customer'],
  create: true,
  read: true,
},

The permissions for LineItem are similar:

// The shop owner can create, read and update line items.
{
  grantee: 'USER',
  userPath: ['order', 'shop', 'owner'],
  create: true,
  read: true,
  update: true,
},
// The shop staff can read the line items of the orders of the shop.
{
  grantee: 'USER',
  userPath: ['order', 'shop', 'staff'],
  read: true,
},
// The customer can create and read line items in their own orders.
{
  grantee: 'USER',
  userPath: ['order', 'customer'],
  create: true,
  read: true,
},

That’s all. We can now add all the permissions to the schema and push the schema to Reindex again to make our API secure.

reindex schema-push

You can find the final schema here: shopping cart schema with permissions .

Conclusion

Hopefully, I’ve given you a taste of what Reindex permissions are capable of. In this tutorial, we used the shopping cart as an example, but these simple permissions allow you to create sophisticated access control rules for any data model. Automatic permission checks occur when you read or update the data using the GraphQL API of Reindex; you don’t have to write any complicated code to perform them.

We would love to hear what you’re building and whether you found this tutorial useful! Say hello: hello@reindex.io.

Discuss this post on Hacker News.

Written by
CEO & Co-founder

Reindex Blog