Setting Up Store for CRUD operations
Learn how to build a Store supporting queries and mutations.
Introduction
In this two-part guide, we’ll build a Store to handle CRUD (Create, Read, Update, Delete) operations for the Trails app. This first part focuses on defining a flexible data model and the operations required to accommodate complex queries and mutations.
Prerequisites:
- Complete the Quickstart.
- Have an understanding of the core Store concepts.
The code for this example is available in the Trails repository.
Defining the Data Model
Creating a Generalized Model
Interface
To create a flexible and reusable Store
, we’ll start by defining a generalized Model
interface. This interface represents any data type we wish to store and forms the foundation of our data model.
The Model
interfaces uses type parameters K
, P
, and E
to represent the Key
, Properties
, and Edges
of the model, respectively. This design allows us to define models with varying structures while maintaining type safety.
Implementing the Model
Interface in Post
With the Model
interface in place, we can now refactor our Post
class to implement this interface. This enables us to define operations on different aspects of a Post
, such as its key, properties, and edges.
Implementing the Model
interface in Post
allows us to define operations on various Post
models. For instance:
- When creating a new
Post
, we have only theProperties
(noKey
orEdges
). - When updating a
Post
, we have both theKey
and theProperties
. - When fetching a feed of
Post
s, we have a list ofPost.Node
. - When fetching a single
Post
, we have aPost.Composite
.
This refactored Post
class now aligns with the Model
interface, allowing us to leverage the type hierarchy for different operations.
Defining Operations
Creating the Operation
Sealed Class
To represent different types of operations (queries and mutations), we’ll define a generalized Operation
sealed class. This class serves as the base for all specific operation types in our Store
.
The Operation
class is parameterized with the types K
, P
, E
, and N
, corresponding to the Model
components. The Operation
class has two subclasses, Query
and Mutation
. Query
represents read operations, while Mutation
represents write operations.
Defining Create
Operations
Under the Mutation
class, we can define the Create
operations that allow us to insert new data into the Store
.
In the Create
operations, we have InsertOne
and InsertMany
, which accept Properties
or a list of Properties
as input. Notice that we use Nothing
for types we don’t have yet, such as the Key
for new entries.
Defining Read
Operations
Let’s define a DataSources
sealed class to represent the different sources of data we may want to query.
The DataSources
class allows us to specify whether to fetch data from memory, disk, or remote sources.
Providing a companion object with common configurations improves readability and reduces the likelihood of errors.
Next, we can define the Query
operations within the Operation
class. Each query
operation specifies the DataSources
to use.
The Query
operations include:
FindOne
andFindMany
for fetching specific items by key(s).FindAll
for fetching all items.ObserveOne
andObserveMany
for observing changes to specific items.
Defining Update
Operations
Under the Mutation
class, we can define the Update
operations for modifying existing data.
The Update
operations include:
UpdateOne
andUpdateMany
for updating existing items.UpsertOne
andUpsertMany
for inserting or updating an item.
Defining Delete
Operations
Finally, we can define Delete
operations under the Mutation
class.
The Delete
operations include:
DeleteOne
andDeleteMany
for deleting specific items by key(s).DeleteAll
for deleting all items.
These operations require the Key
of the items to be deleted.
Defining Output Types
Because our operations can return either a single Model
or a collection of Model
s, we need to define an output type that can represent both cases.
We’ll define an Output
sealed class to represent the results of our operations.
The Output
class has two subclasses:
Single
, which wraps a singleModel
item.Collection
, which wraps a list ofModel
items.
This design allows our Store
to handle both single-item and multiple-item results in a type-safe manner.
Conclusion
With all the components in place, we can now redefine our PostStore
to use the new Operation
and Output
types.
Creating type aliases for PostOperation
, Output
, and PostStore
simplifies our code and improves readability.
By defining flexible data model and operation classes, we’ve established a foundation for handling complex query and mutation operations. In the next part of this guide, we’ll modify our implementations of Fetcher
, SourceOfTruth
, Updater
, and Bookkeeper
to support these operations.