Actions

Actions are the things that "do" stuff. They're defined in the Gadget Editor, where developers can create Model Actions or Global Actions. Actions are most often exposed as GraphQL mutations in the generated GraphQL API. Most actions apply to a specific model and create, update, or delete records for that Model, along with running any other effects defined for that action. Gadget also supports Global Actions, which aren't associated with one particular model, but still have GraphQL mutations and run effects like any other.

How are actions executed?

By default, the Run Effects of Actions execute inside of a database transaction. This means that any error thrown from a Run Effect will roll back any database changes made by other Run Effects in the list. Gadget runs the Success Effects list if the Run Effects succeed. Gadget only returns errors in the GraphQL mutation describing Run Effect failures.

Gadget can only reverse database changes when a transaction fails. It cannot, and does not, reverse the impact of your code effects on other parts of your application or third-party systems.

Nested Actions

Every action supports calling actions on associated models in its input. This is useful, for example, to create a model and some associated models without having to make API calls for each individual record.

For example, suppose you're modeling a blog where each post has many comments. If we wanted to create a blog post with some comments, it would look something like this with nested actions.

1await api.post.create({
2 title: "My First Blog Post",
3 author: {
4 _link: "1",
5 },
6 body: "some interesting content",
7 comments: [
8 {
9 create: {
10 body: "first comment!",
11 author: {
12 _link: "2",
13 },
14 },
15 },
16 {
17 create: {
18 body: "another comment",
19 author: {
20 _link: "3",
21 },
22 },
23 },
24 ],
25});
1mutation CreatePost($post: CreatePostInput) {
2 createPost(post: $post) {
3 success
4 errors {
5 message
6 }
7 post {
8 id
9 }
10 }
11}
Variables
json
1{
2 "post": {
3 "title": "My First Blog Post",
4 "author": { "_link": "1" },
5 "body": "some interesting content",
6 "comments": [
7 { "create": { "body": "first comment!", "author": { "_link": "2" } } },
8 { "create": { "body": "another comment", "author": { "_link": "3" } } }
9 ]
10 }
11}

Note that there's no need for a { _link: "…" } parameter to link the blog post and comments.

How are nested actions executed?

When you invoke an action with an input that has nested actions, Gadget will execute the actions in the right order to feed the right values for relationship fields of subsequent inner actions. In the above example, Gadget would create the blog post first and get its ID value to then send into the comment create actions.

When a group of nested actions is executed, all the constituent actions' Run Effects are run together within one database transaction, and then all the actions' Success Effects are executed second if all the Run Effects complete. This means that Gadget runs all the Run Effects for the root action and nested actions first, then commits the transaction, then runs Success Effects.

If any actions throw an error during the Run Effects, like say if a record is invalid or a code snippet throws an error, all the actions will be aborted, and no Success Effects for any action will run.

The rule of thumb is: an effect that fails on any action rolls back changes from all other actions.

Converge Actions

Gadget supports a special nested action called _converge that creates, updates, or deletes records in the database as needed to match an incoming specified list. Often when updating a parent object and list of child objects at the same time, it is cumbersome to exactly specify the individual operations you want, and instead, it's easier to just specify the new list. _converge figures out the individual operations that take you from the current state in the database to the new specified list. This is most frequently used on forms that edit a parent and child records at the same time so you can just send the new list of child records specified in the form, instead of tracking which things have been added, removed, and changed.

The _converge action will process creates, updates, and deletes using actions with the api identifiers create, update, and delete by default, so any effects added to those actions will run as Gadget converges the record list. You can override which actions Gadget uses to implement the converge using the actions property, see Invoking custom actions to converge for more details.

Run a converge call by specifying _converge as the action name in a nested action input, and passing a new list of records in the values property like so:

json
1{
2 "_converge": {
3 "values": [
4 // will update record with id 1
5 { "id": "1", "name": "first" },
6 // will create a new record
7 { "name": "second" }
8 // other records not present in this list will be deleted
9 ]
10 }
11}

For example, suppose you're editing a blog post where each post has many images. If you wanted to build a client side form where users could remove some of the images that exist already, and then add a couple new ones, you can use _converge to create, update, and delete the images as needed to get to the new list. Without _converge, you'd have to manually track the image ids that have been deleted or updated to build the nested actions data format yourself.

Instead, we can converge the list of child images for the post to the newly specified list.

1await api.post.update("123", {
2 title: "Updated Blog Post",
3 body: "some interesting content",
4 images: [
5 {
6 _converge: {
7 values: [
8 {
9 caption: "Mountains",
10 url: "https://example.com/mountains.jpg",
11 },
12 {
13 id: "10",
14 caption: "Oceans",
15 url: "https://example.com/oceans.jpg",
16 },
17 ],
18 },
19 },
20 ],
21});
1mutation UpdatePost($id: GadgetID!, $post: CreatePostInput) {
2 updatePost(id: $id, post: $post) {
3 success
4 errors {
5 message
6 }
7 post {
8 id
9 }
10 }
11}
Variables
json
1{
2 "post": {
3 "title": "Updated Blog Post",
4 "body": "some interesting content",
5 "images": [
6 {
7 "_converge": {
8 "values": [
9 { "caption": "Mountains", "url": "https://example.com/mountains.jpg" },
10 {
11 "id": "10",
12 "caption": "Oceans",
13 "url": "https://example.com/oceans.jpg"
14 }
15 ]
16 }
17 }
18 ]
19 }
20}

The _converge call above will create a new image with the caption "Mountains" using the Image.create action, and update an existing image with id "10" to have the caption "Oceans" using the Image.update action. It will also delete any images in the database which aren't specified in this list using Image.delete, so if there was previously an image with id "9" labelled "Skies", it'd be deleted.

Invoking custom actions to converge

The _converge action will process creates, updates, and deletes using actions with the api identifiers create, update, and delete by default. You can override which actions Gadget uses to implement the converge using the actions property of the passed object like so:

json
1{
2 "_converge": {
3 "values": [
4 // ...
5 ],
6 "actions": {
7 // will create any new records using the publicCreate action of the child model
8 "create": "publicCreate",
9
10 // will update any existing records using the specialUpdate action of the child model
11 "update": "specialUpdate"
12 }
13 }
14}

The actions override list is optional and partial, so you can just specify the actions you want to override, or just omit the actions property entirely.

Global Actions

Gadget supports Global Actions, which are sets of effects that run outside the context of one particular model or one particular record. Each Global Action has a corresponding GraphQL mutation that will run the run effects for the action and return the result. If an error is thrown during processing, the error will be returned and the Success Effects will be skipped. Otherwise, Gadget will run the Success Effects and return the result.