Plugin API version 4 updates plugins to run on a newer version of Node, which removes support for the Fibers extension. Fibers is what let the previous server-side actions API return results directly instead of as promises. Because it's gone, some API functions change in a way that isn't backwards compatible.
If your plugin uses any server-side actions on plugin API version 3 or below, you'll need to update it manually to work with version 4. In most cases, the changes are straightforward.
This article explains what the updated plugin API looks like and how to bring older plugins up to date.
What's the difference between the Fibers extension and promises?
What's the difference between the Fibers extension and promises?
Fibers and promises are two different approaches to handling asynchronous operations, meaning code that has to wait for something (like a network request) to finish.
The Fibers extension (old) let you write asynchronous code in a more synchronous-looking way. It could pause and resume your code at certain points, making asynchronous tasks appear to run in a normal top-to-bottom flow. This made some code easier to read.
Promises (new) are a native JavaScript feature for managing asynchronous operations. A promise represents the eventual result of an asynchronous task, or its failure. Promises are the standard, widely used approach, and they let you chain operations, handle errors, and structure your code more predictably.
What's changed
The functions below now return a promise for their original value. If a server-side action in your plugin uses any of them, that action will break when you update, until you adjust it.
How to update
Work through your server-side actions and apply the following changes:
1. Update context.request calls
For any top-level function that calls context.request, add async to the function and await before the call, then change context.request to context.v3.request. Consider switching to node-fetch, which has a more modern, standardized API.
2. Update context.async calls
For any top-level function that calls context.async, add async to the function and await before the call, then change context.async to context.v3.async. Consider switching to util.promisify, which is a more modern approach.
3. Update .get on Bubble things
For any top-level function that calls .get on a Bubble thing, add async to the function and await before .get.
4. Update .get on Bubble lists
For any top-level function that calls .get on a Bubble list, add async to the function and await before .get.
5. Update .length on Bubble lists
For any top-level function that calls .length on a Bubble list, add async to the function and await before .length.
6. Switch to version 4
Switch your Bubble plugin API version to version 4 in the Dependencies dropdown of the Shared tab.
Examples
Here are a few examples of how to update the run_server function of your plugin's server-side actions. Each shows the version 3 code first, then the version 4 equivalent.
context.request
Version 3 and below:
function(properties, context) {
// Send a request to the cat API synchronously
const response = context.request({
url: 'https://api.thecatapi.com/v1/images/search',
json: true
})
const cat_image = response.body[0].url
return { cat_image }
}
Version 4:
async function(properties, context) {
// Send a request to the cat API asynchronously, with promises
const response = await context.v3.request({
url: 'https://api.thecatapi.com/v1/images/search',
json: true
})
const cat_image = response.body[0].url
return { cat_image }
}
You can also switch to node-fetch, which is globally available, instead of context.request:
async function(properties, context) {
// Use the node-fetch module to make an HTTP request
const response = await fetch('https://api.thecatapi.com/v1/images/search')
const body = await response.json()
const cat_image = body[0].url
return { cat_image }
}
context.async
Version 3 and below:
In the old API, plugin authors often used context.async to work with modern libraries that already used promises. For example, using the weathered npm package to fetch real-time weather warnings:
function(properties, context) {
const { Client } = require('weathered')
const client = new Client()
const region = properties.region || 'NY'
const alerts = context.async((cb) => {
client.getAlerts(true, { region })
.then((result) => cb(null, result))
.catch((error) => cb(error))
})
const weather_alerts = alerts.features.map(f => f.properties.description)
return { weather_alerts }
}
Version 4:
In the version 4 API, which is already promise-based, you can await the result directly:
async function(properties, context) {
const { Client } = require('weathered')
const client = new Client()
const region = properties.region || 'NY'
const alerts = await client.getAlerts(true, { region })
const weather_alerts = alerts.features.map(f => f.properties.description)
return { weather_alerts }
}
Version 3 and below:
If the code you're waiting on is callback-based rather than promise-based, you'll still need a wrapper. Here's a (somewhat contrived) example using Node's callback-based fs.stat to inspect the filesystem of the lambda your action runs on:
function(properties, context) {
const fs = require('node:fs')
const { inspect } = require('node:util')
const stats = context.async((cb) => {
fs.stat('/', cb)
})
return { stats: inspect(stats) }
}
Version 4:
Instead of context.async, wrap fs.stat with Node's built-in promisify utility so it returns a promise, then await it:
async function(properties, context) {
const fs = require('node:fs')
const { inspect, promisify } = require('node:util')
const stats = await promisify(fs.stat)('/')
return { stats: inspect(stats) }
}
.length()
Version 3 and below:
function(properties, context) {
let list_length = properties.my_list.length()
return { list_length }
}
Version 4:
async function(properties, context) {
let list_length = await properties.my_list.length()
return { list_length }
}
What's new in version 4
Alongside the breaking changes, version 4 adds some new capabilities:
getAll()on a Bubble thing. Returns a promise for an object containing all of the thing's fields and their values.idfield on a Bubble thing. A field that contains the thing's unique ID.isBubbleThingandisBubbleListon context. Check whether a JavaScript value is a Bubble thing or a Bubble list. These replace the oldersingle_apiandlist_apifields, which still work but are no longer encouraged.Async iteration on lists. Bubble lists now implement the
AsyncIterableinterface, so you can loop over them withfor (await ... of ...)syntax.getThingByIdandgetThingsByIdon context. Previously usable but undocumented, these are now officially supported and return promises. They replace the old globalget_object_from_idandget_objects_from_idsfunctions.
Full type reference
export type Primitive = string | number | boolean | Date | null | undefined
/** Any value that can be passed as a property to an action or stored in the DB. */
export type BubbleValue = Primitive | Primitive[] | BubbleThing | BubbleList
/** Fields present on any Bubble data type. */
export interface ThingFields {
// These fields are guaranteed to be present on any database Thing.
'Slug'?: string
'Created Date': Date
'Modified Date': Date
// Other fields might also be present, depending on the data type.
[_: string]: BubbleValue
}
/** Additional fields present on the User data type. */
export interface UserFields extends ThingFields {
'email': string
'logged_in': boolean
}
/** An object representing a single Thing from the Bubble app's database. */
export interface BubbleThing {
/** Returns the names of the fields on the Thing. */
listProperties(): string[]
// CHANGED - now returns a promise
/** Returns the value stored in a particular field of this Thing. */
get(propertyName: string): Promise
// NEW
/** Returns an object with all the thing's fields and their values. */
getAll(): Promise>
// NEW
/** Field that contains the unique ID of this Thing. */
readonly id: string
// for historical interest - maybe de-document these
single_api: true
list_api: false
}
/**
* An object representing a list of Things from the Bubble app's database.
*
* As lists may be quite large, the data contained in the list isn't all
* loaded up front. Therefore, methods for accessing data within the list
* are generally asynchronous.
*/
export interface BubbleList {
// CHANGED - now returns a promise
/** The number of items in this BubbleList. */
length(): Promise
// CHANGED - now returns a promise
/** Fetch a portion of a BubbleList as an array. */
get(start: number, length: number): Promise
// NEW
/** Allows you to use a BubbleList with the `for (await x of list)` syntax */
[Symbol.asyncIterator](): AsyncIterator
// for historical interest - maybe de-document these
single_api: false
list_api: true
}
/** An object containing utility functions available to server-side actions. */
export interface Context {
/** The current user who initiated the workflow that's running this action. */
currentUser: BubbleThing
/** The timezone the workflow is running in. */
userTimezone: string
/** An object containing any keys set for this plugin in the app. */
keys: Record
// NEW
/** Check if a JS value is a BubbleThing object. */
isBubbleThing(x: unknown): boolean
// NEW
/** Check if a JS value is a BubbleList object. */
isBubbleList(x: unknown): boolean
// NEW - was secret global before
/** Fetch a BubbleThing object for a given unique ID. */
getThingById(id: string): Promise
// NEW - was secret global before
/**
* Fetch the corresponding BubbleThing objects for each ID in an array.
* Results will be returned in the same order as requested, with `null`
* in place of any IDs with no corresponding Thing.
*/
getThingsById(ids: string[]): Promise>
// NEW/CHANGED - these used to be top-level properties of the context
v3: ContextDeprecatedV3
}
/** Context functions that are no longer recommended, but are provided for backwards compatibility. */
export interface ContextDeprecatedV3 {
// CHANGED - now returns a promise
/**
* See documentation for the `request` library.
*
* @deprecated We recommend you use node-fetch instead.
*/
request(...args: unknown[]): Promise
// CHANGED - now returns a promise
/**
* Takes a node-style asynchronous function which expects to be passed a callback,
* and turns it into a promise.
*
* @deprecated We recommend you use node's built-in `util.promisify`.
*/
'async': (fn: (callback: (err: unknown, res?: T) => void) => void) => Promise
}
declare global {
/** Exposes node-fetch to plugins. */
function fetch(...args: unknown[]): Promise
}
Changelog
context.requestis being deprecated (we plan to eventually stop supporting it), moved tocontext.v3.request, and now returns a promise. We recommend using the globally availablenode-fetchinstead.context.asyncis being deprecated, moved tocontext.v3.async, and now returns a promise. We recommend usingpromisifyfrom Node's built-inutilmodule instead.The
.getmethod on Bubble things now returns a promise.The
.getmethod on Bubble lists now returns a promise.The
.lengthmethod on Bubble lists now returns a promise.New
getAll()method on Bubble things: returns a promise for an object with all the thing's fields.New
idfield on Bubble things: the thing's ID.Bubble lists now implement the
AsyncIterableinterface, so you can loop over them withfor (await ... of ...)syntax.The
single_apiandlist_apifields are still present but no longer encouraged, and will lose official documentation. Use the newisBubbleThingandisBubbleListcontext methods instead.New official support for
getThingByIdandgetThingsById, added to the context object for consistency with the rest of the API. Both return promises.
