Composability
Since Interval actions and pages are just async functions, sharing logic between actions is as simple as writing shared functions.
- TypeScript
- JavaScript
- Python Experimental
import { Action } from "@interval/sdk";
export default new Action(async (io, ctx) => {
//
});
To extract and share logic from an action that uses io
and/or ctx
, your shared functions can use the io
and ctx
objects exported from the Interval SDK:
import { Action, io, ctx } from "@interval/sdk";
export default new Action(async () => {
//
});
Both objects are identical whether you access them as exports from the SDK or as arguments passed to your action handler functions. Which form you use is matter of stylistic preference.
const { Action } = require("@interval/sdk");
export default new Action(async (io, ctx) => {
//
});
To extract and share logic from an action that uses io
and/or ctx
, your shared functions can use the io
and ctx
objects exported from the Interval SDK:
const { Action, io, ctx } = require("@interval/sdk");
export default new Action(async () => {
//
});
Both objects are identical whether you access them as exports from the SDK or as arguments passed to your action handler functions. Which form you use is matter of stylistic preference.
@interval.action
async def hello_world(io: IO, ctx: ActionContext):
# action logic here
pass
To extract and share logic from an action that uses io
and/or ctx
, your shared functions can use the io
and ctx
objects exported from the Interval SDK:
from interval_sdk import Interval, io_var, ctx_var
These imports are just Python Context Variables, but otherwise behave the same as the io
and ctx
objects passed to your action handler functions.
ctx_var
may be cast to ActionContext
or PageContext
depending on where they are used (or you can import and use action_ctx_var
and page_ctx_var
directly).
warning
Trying to access io
or ctx
outside of an action will cause an error to be thrown. If you write shared functions that reference io
or ctx
, only call those functions within an action.
Example: lookup user function
Imagine you have multiple actions that require the ability for person running the action to lookup a user from your database.
Our app might look something like this:
- TypeScript
- JavaScript
- Python Experimental
import { Action } from "@interval/sdk";
import db from "../db";
export default new Action(async (io, ctx) => {
let user: User | null = null;
if (ctx.params.id) {
const foundUser = await db.user.lookupById(ctx.params.id);
if (foundUser) {
user = foundUser;
}
}
if (!user) {
user = await io.search("Select a user", {
onSearch: async query => {
return user.find(query);
},
renderResult: user => ({
label: [user.firstName, user.lastName].join(" "),
description: user.email,
}),
});
}
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to refund a user
});
import { Action } from "@interval/sdk";
import db from "../db";
export default new Action(async (io, ctx) => {
let user: User | null = null;
if (ctx.params.id) {
const foundUser = await db.user.lookupById(ctx.params.id);
if (foundUser) {
user = foundUser;
}
}
if (!user) {
user = await io.search("Select a user", {
onSearch: async query => {
return user.find(query);
},
renderResult: user => ({
label: [user.firstName, user.lastName].join(" "),
description: user.email,
}),
});
}
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to edit a user
});
The above code is clearly very repetitive. We can extract our user lookup logic into its own function:
import { io, ctx } from "@interval/sdk";
import db from "../db";
export async function lookupOrSearchForUser() {
let user: User | null = null;
if (ctx.params.id) {
const foundUser = await db.user.lookupById(ctx.params.id);
if (foundUser) {
user = foundUser;
}
}
if (!user) {
user = await io.search("Select a user", {
onSearch: async query => {
return user.find(query);
},
renderResult: user => ({
label: [user.firstName, user.lastName].join(" "),
description: user.email,
}),
});
}
return user;
}
This lookupOrSearchForUser
function can now be used in multiple actions:
import { Action } from "@interval/sdk";
import { lookupOrSearchForUser } from "../utils/user";
export default new Action(async () => {
const user = lookupOrSearchForUser();
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to refund a user
});
import { Action } from "@interval/sdk";
import { lookupOrSearchForUser } from "../utils/user";
export default new Action(async () => {
const user = lookupOrSearchForUser();
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to edit a user
});
const { Action } = require("@interval/sdk");
const db = require("../db");
module.exports = new Action(async (io, ctx) => {
let user: User | null = null;
if (ctx.params.id) {
const foundUser = await db.user.lookupById(ctx.params.id);
if (foundUser) {
user = foundUser;
}
}
if (!user) {
user = await io.search("Select a user", {
onSearch: async query => {
return user.find(query);
},
renderResult: user => ({
label: [user.firstName, user.lastName].join(" "),
description: user.email,
}),
});
}
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to refund a user
});
const { Action } = require("@interval/sdk");
const db = require("../db");
module.exports = new Action(async (io, ctx) => {
let user: User | null = null;
if (ctx.params.id) {
const foundUser = await db.user.lookupById(ctx.params.id);
if (foundUser) {
user = foundUser;
}
}
if (!user) {
user = await io.search("Select a user", {
onSearch: async query => {
return user.find(query);
},
renderResult: user => ({
label: [user.firstName, user.lastName].join(" "),
description: user.email,
}),
});
}
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to edit a user
});
The above code is clearly very repetitive. We can extract our user lookup logic into its own function:
const { io, ctx } = require("@interval/sdk");
const db = require("../db");
module.exports = async function lookupOrSearchForUser() {
let user: User | null = null;
if (ctx.params.id) {
const foundUser = await db.user.lookupById(ctx.params.id);
if (foundUser) {
user = foundUser;
}
}
if (!user) {
user = await io.search("Select a user", {
onSearch: async query => {
return user.find(query);
},
renderResult: user => ({
label: [user.firstName, user.lastName].join(" "),
description: user.email,
}),
});
}
return user;
};
This lookupOrSearchForUser
function can now be used in multiple actions:
const { Action } = require("@interval/sdk");
const { lookupOrSearchForUser } = require("../utils/user");
export default new Action(async () => {
const user = lookupOrSearchForUser();
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to refund a user
});
const { Action } = require("@interval/sdk");
const { lookupOrSearchForUser } = require("../utils/user");
module.exports = new Action(async () => {
const user = lookupOrSearchForUser();
// By this line, we have a guaranteed non-null value for our user variable.
// ... logic to edit a user
});
import os
from interval_sdk import Interval, IO, ActionContext
import my_db
interval = Interval(
os.environ["INTERVAL_API_KEY"],
)
@interval.action
async def refund_user(io: IO, ctx: ActionContext):
ctx = ctx_var.get()
if ctx.params.id:
user = await my_db.user.lookup_by_id(ctx.params.id)
if not user:
async def on_search(query: str):
my_db.user.find(query)
user = await io.search(
"Select a user",
on_search=on_search,
render_result=lambda user: {
"label": f"{user['firstName']} {user['lastName']}",
"description": user["email"],
},
)
# By this line, we have a guaranteed not null value for our user variable.
# ... logic to refund a user
@interval.action
async def apply_credit(io: IO, ctx: ActionContext):
ctx = ctx_var.get()
if ctx.params.id:
user = await my_db.user.lookup_by_id(ctx.params.id)
if not user:
async def on_search(query: str):
my_db.user.find(query)
user = await io.search(
"Select a user",
on_search=on_search,
render_result=lambda user: {
"label": f"{user['firstName']} {user['lastName']}",
"description": user["email"],
},
)
# By this line, we have a guaranteed not null value for our user variable.
# ... logic to credit a user
interval.listen()
The above code is clearly very repetitive. We can extract our user lookup logic into its own function:
import os
from interval_sdk import Interval, IO
from interval_sdk.internal_rpc_schema import ActionContext
import my_db
interval = Interval(
os.environ["INTERVAL_API_KEY"],
)
async def lookup_or_search_for_user():
ctx = ctx_var.get()
if ctx.params.id:
user = await my_db.user.lookup_by_id(ctx.params.id)
if not user:
async def on_search(query: str):
my_db.user.find(query)
user = await io.search(
"Select a user",
on_search=on_search,
render_result=lambda user: {
"label": f"{user['firstName']} {user['lastName']}",
"description": user["email"],
},
)
return user
@interval.action
async def refund_user(io: IO, ctx: ActionContext):
user = await lookup_or_search_for_user()
# By this line, we have a guaranteed not null value for our user variable.
# ... logic to refund a user
@interval.action
async def apply_credit(io: IO, ctx: ActionContext):
user = await lookup_or_search_for_user()
# By this line, we have a guaranteed not null value for our user variable.
# ... logic to credit a user
interval.listen()
Returning an I/O Promise from a helper function
info
In short, I/O Promises cannot be returned from async
functions.
This currently only applies to TypeScript and JavaScript.
Returning an I/O method call directly from a helper function is a great way to share custom behavior for commonly used methods, allowing them to be used both on their own and in groups.
import { Action, io } from "@interval/sdk";
function customNumberInput(label) {
return io.input.number(label, {
decimals: 3,
min: 0,
max: 10,
});
}
export default new Action(() => {
const num1 = await customNumberInput("First");
const [num2, num3] = await io.group([
customNumberInput("Second"),
customNumberInput("Third"),
]);
});
However, because I/O Promises are lazy and are executed immediately when
await
ed, returning an I/O method call from an async
function will not
work as expected in an io.group.
JavaScript will automatically resolve the promise at
the end of the function body and return the result, which will cause the I/O
method call to be rendered individually.
See MDN for more information about async
functions.
There are two ways to work around this. The simplest is to remove the async
keyword from the helper function, and pass in any data that requires an
asynchronous function call.
Another option is to wrap the return value in a data structure that prevents JavaScript from immediately resolving the I/O Promise. This can be as simple as an array, an object, or a custom class wrapper.
Note that the I/O Promise will need to be unwrapped at the call site before being used.
- Object (JavaScript example)
- Class (TypeScript example)
import { Action, io } "@interval/sdk";
async function nameHelper() {
return {inner: io.input.text('Enter a name')};
}
export default new Action(async () => {
const name = await (await nameHelper()).inner;
const [nameFromGroup] = await io.group([
(await nameHelper()).inner
]);
});
import { Action, io } "@interval/sdk";
export class Box<Inner> {
inner: Inner;
constructor(inner: Inner) {
this.inner = inner;
}
unwrap() {
return this.inner;
}
}
async function nameHelper() {
return new Box(io.input.text('Enter a name'));
}
export default new Action(async () => {
const name = await (await nameHelper()).unwrap();
const [nameFromGroup] = await io.group([
(await nameHelper()).unwrap()
]);
});