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.
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 Shop
s, 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 owner
s 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.