Most apps require business logic. Different people are allowed to do different things, and when they do stuff, other stuff needs to happen. To do things like changing the database, transforming incoming data, or making API calls to other systems, Gadget developers must add behaviors to their application. Gadget provides two different ways to add behavior:
- via a specific model behaviors, which run in the context of specific records
- via Global Actions, which run outside the context of any particular record
For example, if you wanted to implement logic that sent an email to your boss everytime a new user signed up to your app, you would likely add smarts to the User model. This is best represented as Model behavior, because it concerns a particular record, and Gadget can help with loading and saving that particular record as it is changed. Conversely, a weekly email that gets sent to your whole company notifying you of all that week's signups might be a Global Action. This behavior doesn't read or write any data to or from a particular central record, and so it's most natural to model it as a Global Action, because it's outside any one record's lifecycle.
Model behaviors can be found by clicking on the behavior icon next to the model's name (displayed on hover) or via a link on the model's Storage page.
Model behaviors are expressed using a statechart. Statecharts are a visual way of representing complex behaviors. Statecharts have been around since the 80s, and while they are robust and flexible, they are infrequently used as they can be challenging to setup. Fortunately, Gadget sets up a statechart for all of your models as soon as your create them, and allows you to use as much or as little of the statechart as you desire in your modeling.
Statecharts really shine when you try to model your business logic as a set of states and transitions. Statecharts are explicit about what is allowed to happen when, and about what you can expect to happen as a result of stimulating your data in some way. They're also easy to read and understand if you're coming back to a statechart you haven't worked with in a while.
Gadget generates a statechart for each model you create, and allows you to define exactly what's possible within that model by editing the statechat directly in the Gadget editor.
Each rectangular box in the statechart represents a different state that a record can be in. Each arrow in the statechart represents the actions that can be taken on your model. States describe the system's current view of the world, and actions are processes that can be run to change this view. Records start their lives in a particular states, and then Actions change that state when users trigger them. The current state for each record is exposed on that record's RecordState field, and each action is exposed as a separate GraphQL mutation in your public API.
Let's look at an example statechart for a blog's Post model. Each model starts off with a set of default behaviors from Gadget which allow creating, updating, and deleting records. Just by looking at the default statechart, we can infer quite a bit, namely:
- All records in this model are either in a
- There are three possible actions on this model's records:
Createaction generates and saves a record that is in the
Updateaction maybe has some side effects, but doesn't move the record out of the
Createdstate. This is also called a self-transition.
Deleteaction transitions a record from the
Createdstate to the
Deletedstate, and has the side effect of actually deleting the record from the database
- There is no way to move a record back from the
Deletedstate to the
Createdstate for this model
When you have a record for a model using this state chart, you can use
record.state to inspect the state that the record is currently in. Since we don't have any other states in this state chart,
record.state will always be
created, which is the API Identifier for the
A model with this statechart will add three new mutations to this Gadget application's GraphQL API: the
createPost action, the
updatePost action, and the
deletePost action. If you wanted to remove one of these actions from your API, you can delete the arrow on the statechart that represents it. If you wanted to restore this action after deleting it, you could drag a new arrow between the two states you want records to transition between.
Each statechart in Gadget can be customized to whatever degree is necessary to solve your problem. You don't need to customize at all, but if you want to, you can change everything about the state chart by adding and removing states and actions!
You can add new states to an existing statechart by clicking the Add State button, or dragging states around to put them in new contexts. You can add new actions by dragging new action arrows from the action arrow handles beside each state. For each new action arrow you create, a new mutation and function will be added to your public API.
For example, let's say we're building the backend of a blog, and that we have a Post model storing fields like
body. If you wanted this blog to have a publishing workflow where posts aren't made immediately visible to the audience, and instead only publish posts once you're ready, you can do that in Gadget by adding new substates to the Post statechart. We can add two substates to Post, one representing
Draft posts and one representing
Published posts, and then create actions to transition from
By adding these states and actions, Gadget now knows quite a bit more about your application, without you having to write any code at all. From the statechart, Gadget will:
- Store post records in one of three states:
created -> draft,
Created -> publishedand
- Allow draft posts to be published, but not allow published posts to be moved back to drafts
- Allow draft or published posts to be deleted
- Add a
publishPostGraphQL mutation that transitions a post from
With this statechart approach to behavior modeling, you can teach the platform a lot without doing much work. This blog's API will support finding only the published posts for displaying to a frontend, or all the posts for displaying on a backend. The API's
publishPost mutation will know how to update the Post record's
state field and store it, and optionally perform any other business logic you need it to.
If you wanted to support unpublishing already published blog posts to move them back from
Draft, you could drag a new action arrow from the
Published state to the
Draft state. Like all actions, this new action would get a dedicated GraphQL mutation with whatever name you prefer.
In the above example of publishing and unpublishing blog posts, it would be a perfectly acceptable strategy to model the publish status of a Post using a Boolean field. Even if you had three different states a Post could be in during the process of it's creation, you could still use a StringOptions field to store this information in Gadget. Both of these solutions to the problem are altogether reasonable and supported by Gadget, and using states to model business logic is not mandatory.
That said, even for a simple example like publishing and unpublishing blog posts, there's often more to the story. You might want to only allow certain users to publish posts, but let a wider set create them. You might want to allow posts to be created without titles because naming stuff is hard, but have the title field become required as the post gets published. You might want to send the post's data off to a hosted search service when it gets published so it doesn't appear in search results before publishing.
All this extra business logic can become cumbersome to model when using plain old storage fields. The statechart offers you an expressive and fast way to describe this same behavior, so we recommend using it when you can. To implement enhanced permissions, you can change the permissions of the
Publish action to be different than the
Create action. To implement preconditional title validation, you can add a Precondition to the
Publish action that checks for the required state. To syndicate post data off to your search system of choice, you can use an Effect on the
Publish action for an easy spot to run code when the thing you care about happens.
Actions are the units of work your application can perform. When a user (or other system) interacts with your application, they do so by calling actions. Each action, either from a model's statechart or a Global Action, is exposed as a unique mutation in your application's GraphQL API.
Every model action in Gadget shares a few properties:
- Actions automatically load the record they are being run on from the database. You don't have to manually fetch the record that is being operated on.
- Actions have permissions that determine whether a user or API Key should be allowed to run the desired behavior. These permissions are built into the Gadget platform and configured by you. When an action is called, Gadget verifies that the caller has permission to run that action. For more information on roles and permissions, see the Access Control guide. Think of permissions as the answer to who can run an action.
- Actions can have arbitrary preconditions that need to be met in order to run. This allows expressing business logic beyond just the state a record is in to prevent action execution. For example, to prevent a blog from being able to publish posts on weekends, you would create a precondition on the
Global Actions share all these same properties, but since they don't operate in the context of a record, they don't load any data automatically.
Actions all follow the same pattern of execution:
Action preconditions are individual chunks of code that answer the question: "can this action be run right now?". Preconditions can inspect fields on the record the action is attempting to run on, or pull other things from the context, like the currently logged in user.
Preconditions are separate from Effects so that your API can answer the question "which actions can be run right now", without having to actually try to run the action. This is useful if you want to enable or disable buttons in a UI depending on if the action is currently available, or if you want to hide functionality from users who don't have permission to access them.
Actions have effects that do the useful things, once it has been established that an action should run. Effects can change the database, the requester's session, make outgoing API calls, and really do whatever they want! An action's effects are broken into three separate lists that run in three seperate transactions: Run Effects, Success Effects, and Failure Effects.
The Run Effects for an action are atomic, which means they all run inside a single database transaction. This means that all the changes made to the database by all the run effects will either commit together, or revert together. This is a convenient way to program -- if something downstream from one effect hits an error condition, the prior effects don't need to have the smarts to undo what they have done so far. Instead, the Gadget database can just undo anything that was underway, and restore a consistent state. Without this, only part of your changes might take effect (which creates lots of bugs) or your code must try to revert some of the changes manually (which is error prone and unpleasant to code).
Most of the time, the run effects will succeed, and the transaction wrapping them will commit. When the Run Effects are successful, Gadget will then execute the Success Effects. If one of the Run Effects fails, and the transaction is rolled back, Gadget will execute the Failure Effects for the action instead.
Run Effects are for the main body of business logic that an action should run. Most database operations belong in Run Effects.
Gadget supports two secondary effect lists that run after the Run Effects: the Success Effects and the Failure Effects. Success Effects run when the Run Effects succeed, and Failure Effects run when any of the Run Effects throw an error. If the Run Effects fail, the Success Effects are never run. If the Run Effects succeed, the Failure Effects are never run.
Success Effects allow you to run effects after an action's main changes have been successfully committed. This is most often useful for sending data to other systems, like a hosted search service, payments API, or other third party. Another frequent use case for Success Effects is sending emails or push notifications, because you generally only want to show those external signs of success if an action really did succeed.
It's generally important to ensure that the changes you are making to your local Gadget database have been committed successfully and saved to disk before informing those other services that changes have been made. If third party service API calls are made in the Run Effects, there is still a chance that a different Run Effect might fail, and cause the transaction to be rolled back. If this happens, then the third party service may have been sent data that doesn't exist in the database, which can be a major bug.
If the Success Effects themselves fail, the error is reported to the API caller. Notably, the Failure Effects are not triggered, as the action has successfully run and committed changes by the time the Success Effects start running.
Failure Effects allow you to run effects after an action's Run Effects hit an error condition and roll back. This is most often useful for recovering from unexpected failures. Run Effects are free to use try/catch statements and handle their own errors, but sometimes, unexpected errors occur, and it is convenient to run some other chain of effects as a result.
If the Failure Effects themselves fail, the error will be reported to the API caller, but it's not possible to run further Failure Effects. For this reason, we recommend making your Failure Effects as robust as possible to avoid failed Failure Effect executions.
Errors thrown during Run Effects, Success Effects, or Failure Effects are sent back from your application's API as
Error objects that you can catch. If you're making GraphQL requests with a different client, you will need to inspect the result from your GraphQL mutation to see if there are any
errors in the result JSON. For more information, consult the API Reference.
For debugging, errors are also logged when they occur. These logs are visible in the Gadget Log Viewer.
Action effects come in different forms. Gadget supports a handful of different effect types:
- Create Record: instructs Gadget to create a new record for the current model
- Update Record: instructs Gadget to update a passed in record for the current model
- Delete Record: instructs Gadget to delete a passed in record for the current model
- Connection Effects: Connection effects are a quick way to access simple, common effects from third party APIs. At the moment, Gadget offers a built-in connection to Twilio and SendGrid. You can enable or disable a connection from the Connections page, accessible from the main navigation menu.
Gadget has some built in database effects that make it easy to persist a model. By default, Gadget installs a Create Record effect to each model's Create action, an Update Record effect to the Update action, and a Delete Record effect to the Delete action. You can remove default effects if you like, though that may mean your actions no longer actually accomplish what their names suggest they do. You can re-add these actions without any special configuration by clicking the Add Effect button for the action you'd like to add an effect to.
Note: Gadget's database effects are implemented using the Internal API. These effects are what are used to implement higher level Actions, so it makes sense that they use the low level functionality to avoid accidental infinite recursion.
Gadget offers a handful of pre-built connections to third party APIs that are commonly used by developers. Today, this list is limited to Twilio and SendGrid. Connections can be enabled or disabled from the connections screen in the navigation menu. Once a connection is established, Gadget will expose the connection's contributed effects as new options in the Action configuration panel. Connection effects are a fast way to complete common operations without building integrations.
Global Actions run operations that are not tied to a model or particular record. For example, sending a weekly newsletter to all your users at the same time is best modeled as a Global Action, because it's not manipulating any single record. Given that Global Actions are not tied to a record, they don't belong to a statechart, and are expressed entirely within the Global Action configuration page.
Global Actions can still fetch and manipulate record data, but Gadget doesn't automatically load a record for the Global Action to work on.
Global Actions support Run Effects, Success Effects, and Failure Effects the same way that Model Actions do, and share the same transactional semantics. Global Actions won't have a
record object in the contexts for their action executions.