Behavior

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.

Statecharts

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.

States and Actions

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.

Visual explanation of what is a state and what is an action in a state diagram

The Normal CRUD

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:

The default state chart for models in Gadget

Just by looking at the default statechart, we can infer quite a bit, namely:

  • All records in this model are either in a Created state or Deleted state
  • There are three possible actions on this model's records: Create, Update and Delete
  • The Create action generates and saves a record that is in the Created state
  • The Update action maybe has some side effects, but doesn't move the record out of the Created state. This is also called a self-transition.
  • The Delete action transitions a record from the Created state to the Deleted state, and has the side effect of actually deleting the record from the database
  • There is no way to move a record back from the Deleted state to the Created state 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 Created state.

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.

Beyond CRUD

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 title and 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 Draft to Published:

A state chart representing a blog post in Gadget

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 -> Draft or 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 publishPost GraphQL mutation that transitions a post from Draft to Published
  • Allow finding only published or only draft posts in API calls

Your own actions like publish action work just like create, 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 Published into 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.

Substates versus String Options or Booleans

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.

What are Actions?

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.

Anatomy of an action

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 Publish action that checks the current date to ensure that it is not a weekend. Preconditions are written as JavaScript functions.
  • Actions have effects, which manipulate the record or do anything else as a result of the action. Think of effects as the answer to what happens once an action has decided to run. Effects are the logic that make up the bulk of an application's utility. Gadget has built in effects to manipulate the database or trigger other actions, and you can also write your own effects in JavaScript.

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:

Gadget executes every action by checking the permissions of the user or API Key running the action, and then runs the preconditions. If the preconditions pass, Gadget then runs the effects.

Action Preconditions

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.

An action's preconditions are checked immediately upon an API call. If any of the action's preconditions are not met, the API will return an error instead of executing the action. Each precondition is written as a JavaScript function that runs on Gadget's platform. To learn more about coding preconditions, please refer to Extending with Code.

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.

Action Effects

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.

Action Transactions

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

Run Effects are for the main body of business logic that an action should run. Most database operations belong in Run Effects.

Success and Failure 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.

For reference, if we were writing out this procedure in plain old JavaScript code, it'd look something like this:

JavaScript
1try {
2 runEffects();
3} catch (error) {
4 failureEffects();
5 throw error; // abort the whole action
6}
7
8successEffects();

Success Effects

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

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.

Handling errors in the client

Errors thrown during Run Effects, Success Effects, or Failure Effects are sent back from your application's API as errors in the Action Result format. If you're making GraphQL requests with the Gadget API client, they'll be thrown as JavaScript 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.

Effect library

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
  • Run Code: runs a JavaScript function to do anything else. To learn more about authoring Run Code effects, refer to Extending with Code.
  • 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.

Default Database Effects

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.

Run Code Effects

Gadget supports running arbitrary JavaScript code in Run, Success, and Failure effects. Add a Run Code effect to an action, and your JavaScript function will be executed within the transaction along with the other effects in the list. For more information on custom code effects, refer to Extending with Code.

Connection Effects

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.

Built-in Connection effect selection

Of course, you can always write your own connection effects in the Code Editor. Credentials to connected services are exposed in the context.config object.

Global Actions

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 model or record object in the contexts for their action executions.