Actions are a unique feature of Joystick, designed to help you organize complex, multi-step code into a single, fault-tolerant function. For example, when a new user signs up for your app, the "action" being taken might be called signup()
which consists of a few discrete "steps:"
- Create a user account.
- Create a customer account in your payment system.
- Seed the database with some example data.
- Fire off a welcome email.
- Track the signup in your analytics.
While we can certainly write code that runs each of these steps independently, it can be helpful to call them together, with the end result of all of the steps being completed successfully being identified as a successful "signup."
Conversely, if any of those steps fail, we can say that the "signup" failed. We may have gotten a user account and some seed data, but the payments API was down and so now there's no payment data connected to the user.
When this happens, ideally, we can treat the entire action as a failure and alert the user, suggesting they try again.
This is where Joystick's built-in action
library comes in to play.
Example Usage
An action works by creating a function composed of multiple steps that can be called together. Inside of the action, steps can reference each other's inputs and outputs, streamlining the process of completing some "action." This is helpful not only for maintenance purposes and system stability, but also for things like testing your app.
In addition to being able to call steps together in sequence, an action can also include input validation (to confirm all of the data you need to complete the action is present with the correct data types) as well as the option to "abort" the action if any one of the steps fails.
Consider the following example action signup()
:
/api/signup.js
import joystick, { accounts, email, origin } from "@joystick.js/node";
import Stripe from 'stripe';
import analytics from 'analytics-provider';
import seed_new_user_data from '../lib/seed_new_user_data.js';
const stripe = Stripe(joystick?.settings?.private?.stripe?.secret_key);
const signup = joystick.action(
"signup",
{
input: {
email_address: {
type: 'string',
required: true,
},
password: {
type: 'string',
required: true,
},
name: {
type: 'string',
required: true,
},
},
steps: {
create_user: {
run: (email_address = '', password = '', name = '') => {
return accounts.signup({
email_address,
password,
metadata: {
name,
}
});
},
on_error: (exception, action) => {
action.abort(exception.message);
},
},
create_customer_on_stripe: {
run: (name = '', email_address = '', user_id = '') => {
return stripe.customers.create({
name,
email_address,
metadata: {
user_id,
}
});
},
on_error: (exception, action) => {
action.abort(exception.message);
},
},
add_customer_to_user: {
run: (user_id = '', stripe_customer_id = '') => {
return process.databases.mongodb.collection('users').updateOne(
{ _id: user_id },
{
$set: {
stripe_customer_id,
},
}
);
},
on_error: (exception, action) => {
action.abort(exception.message);
},
},
seed_example_data: {
run: (user_id = '') => {
seed_new_user_data(user_id);
},
on_error: (exception, action) => {
action.abort(exception.message);
},
},
send_welcome_email: {
run: (email_address = '', name = '') => {
return email.send({
to: email_address,
from: 'customers@app.com',
subject: 'Welcome!',
template: 'welcome',
props: {
name,
onboarding_url: `${origin}/setup`,
},
});
},
},
track_signup: {
run: (user_id = '') => {
return analytics.track({ event: 'signup', user_id });
},
},
},
run: async (input = {}, steps = {}, action = {}) => {
const user = await steps.create_user(input?.email_address, input?.password, input?.name);
const customer_on_stripe = await steps.create_customer_on_stripe(input?.name, input?.email_address);
await steps.add_customer_to_user(user?._id, customer_on_stripe?.id);
await steps.seed_example_data(user?._id);
await steps.send_welcome_email(input?.email_address, input?.name);
return user?._id;
},
},
{
log_errors: true,
}
);
export default signup;
Calling an action
If we look above, we've exported the return value of calling action()
above. What we expect to get in return is a function that we can call, passing the expected input (relative to the input
schema we defined on the action):
/api/users/setters.js
import joystick from '@joystick.js/node';
import signup from '../signup.js';
const setters = {
create_user: {
input: {
email_address: {
type: 'string',
required: true,
},
password: {
type: 'string',
required: true,
},
name: {
type: 'string',
required: true,
},
},
},
set: (input = {}) => {
return signup(input);
},
};
export default setters;
API Reference
action()
Function API
action(action_name: string, action_definition: object, action_options: object) => function;
Parameters
- action_name string required
- The name for the action. Used internally by the action when reporting errors.
- action_definition object required
-
The definition for the action.
- input object
- An object defining an input validation schema to validate the input before calling the action's run() function.
- steps object required
-
An object containing individual steps as objects with a run() function and optional on_error()/on_success() methods.
- [step_name] object required
-
An object defining a single step.
- run function required
- The function for this step. Receives arguments passed from the action's run() function.
- on_error function
- A function called if the step throws an error. Receives the error and the action instance.
- on_success function
- A function called if the step succeeds. Receives the return value from the step and the action instance.
- run function required
- The main function for the action, responsible for calling steps in sequence.
- action_options object
-
Additional options for configuring the behavior of the action.
- log_errors boolean
- Set whether or not the action should log errors to the server console. Defaults to false.