Most apps require business logic. When people do stuff, other stuff needs to happen. Different people are allowed to do different things, or data needs to be transformed before it's saved, or an external API call needs to be made whenever something changes. Gadget apps define behavior like this in two ways:
- 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 every time a new user signed up to your app, you would likely add behavior to the User model. This is best represented as Model behavior, because it concerns a particular record (the user who just signed up), 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 in the left hand nav bar(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 they're created, and allows you to use as much or as little of the statechart as you like.
Statecharts really shine when you try to model your business logic as a set of states and a set of transitions among them. Statecharts are explicit about what is allowed to happen when, and about the effects of some happening should be. 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 statechart 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
If you fetch some record for this model, you can use
record.state to find which state 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 generate three mutations in this Gadget application's GraphQL API: the
createPost action, the
updatePost action, and the
deletePost action. If you want to remove one of these actions from your API, you can delete the arrow on the statechart that represents that action. If you wanted to restore this action after deleting it, you can drag a new arrow between the two states you want records to transition between.
Each statechart in Gadget can be customized to solve your specific problem. You don't need to customize it, but if you want to, you can change just about everything by adding and removing states and actions!
You can add new states to an existing statechart by clicking the Add State button. You can also move states by 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. Let's say we want a a publishing workflow where posts can be worked on in the background for a while before being published, and aren't visible to the audience until an author hits a publish button in an admin somewhere. You can do that in Gadget by adding new substates to the Post statechart. We can add two substates: 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 two states:
Created -> Draftor
Created -> Published.
- 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
- Allow finding only published or only draft posts in API calls
Your own actions like
publish action work just like
update or any other: you can run them via the API, you can grant or deny permissions on them, and you can make them run other business logic with Effects.
You can also add more than one action between your own states in either direction. If you wanted to support unpublishing already-published blog posts and move them back from
Draft, you could drag a new action arrow from the
Published state to the
Draft state. You could name this action
Unpublish, and you'd get a
unpublishPost GraphQL mutation in your API, ready to be wired up to a front end.
In the above example of publishing and unpublishing blog posts, it would be a fine strategy to model the visibility status of a Post using a Boolean field named something like
isPublished. This can be easier than using full blown states, and Gadget supports this just fine. If you had three different states a Post could be in during the authoring process, you could also use a StringOptions field to store this. Both of these solutions to the problem are altogether reasonable 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 such that states might be a good idea:
- You might want to only allow certain users to publish posts, but let a wider set create them. This would mean different permissions required for actions which change the same fields, which would be easiest to model as two different Actions.
- 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. This would be easiest to model as a Precondition instead of a more complicated code snippet Validation
- 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. This would be easiest to model using an Action Effect
All this extra business logic can become cumbersome to model when using plain old storage fields, and so we encourage you to use the state chart to express as much as you can in order to wrangle these various bits into a discoverable, maintainable system.
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 the action is running on.
- Each Action has permissions that determine whether a user or API Key can run it. Think of permissions as the answer to who can run an action. 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.
- Actions can have arbitrary preconditions that need to be met in order to run. Think of preconditions as the answer to when , or under what circumstances, an action can be 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 separate 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.
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.
Of course, you can always write your own connection effects in the Code Editor. Credentials to connected services are exposed in the
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.