Plugin API v4 for async function

First, I wish to say the switch from context.async to the use of Promises is fantastic.

In theory, it means I can mock up my code in Node JS, then make very few changes to reformat it for a plugin.

However, I have a plugin for which I am reformatting to use the await, then, error, syntax, for which something critical is not working, which indicates to me there is something about the await, then, catch syntax I’m not understanding.

Below is the full code of my plugin.
`
async function(properties, context) {

/* References

Return an object: http://forum.bubble.io/t/output-an-object-for-server-side-client-side/59419

Customized error handling in Bubble: https://youtu.be/AITGodtyVyw

Axios error handling: https://www.npmjs.com/package/axios#handling-errors

*/

const axios = require('axios')

const cheerio = require('cheerio')

var webpageUrl  = properties.webpage

var linkUrl     = properties.link

let objectToReturn = {

    _p_link_href: "",

    _p_link_rel: "",

    _p_link_text: "",

    _p_returnStatus: null,

    _p_returnStatusText: "",

    _p_errorMessage: ""

}

let webpageHtml = ""

let linkv1 = linkUrl

let linkv2 = ""

let regex1 = /\/$/

let regex2 = /$/

if (linkUrl.match(regex1)) {

    linkv2 = linkUrl.replace(regex1, "")

}

else if (linkUrl.match(regex2)) {

    linkv2 = linkUrl.replace(regex2, "/")

}

await axios.get(webpageUrl)

    .then((response) => {

        objectToReturn._p_returnStatus = response.status

        objectToReturn._p_returnStatusText = response.statusText

        webpageHtml = response.data

    })

    .catch((error) => {

        objectToReturn._p_returnStatus = error.response.status

        objectToReturn._p_returnStatusText = error.response.statusText

        objectToReturn._p_errorMessage = error.message

    })

    .finally(() => {

        const $ = cheerio.load(webpageHtml)

        const webpageLinks = $("a")

        webpageLinks.each(function () {

            if (($(this).attr('href') === linkv1) || ($(this).attr('href') === linkv2)) {

                objectToReturn._p_href = $(this).attr('href')

                objectToReturn._p_rel = $(this).attr('rel')

                objectToReturn._p_text = $(this).text()

            }

        })

        return { "object": objectToReturn }

    })

}
`

The problem is the code in the finally section seems to never execute. If I move the return statement so it’s after the finally section, I can see that the code in the then section definitely did execute as the two variables that are set in that code section are set.

However, here is my NodeJS mock up in which I use console.log statements to see what variables are set when, I can see that in this NodeJS mock up, the code in the finally section DOES execute.
`
/*

*/

let webpageUrl = “https://organicgrowth.biz/link-building/you-do-not-want-backlinks-to-your-landing-pages/

let linkUrl = “https://www.quoleady.com/product-led-content/

console.log(“CustomPlugin:verifyLink:Execution starting”)

console.log("CustomPlugin:verifyLink:webpageUlr: ", webpageUrl)

console.log("CustomPlugin:verifyLink:linkUrl: ", linkUrl)

const cheerio = require(“cheerio”)

const axios = require(“axios”)

let objectToReturn = {

_p_href: "",

_p_rel: "",

_p_text: "",

_p_returnStatus: null,

_p_returnStatusText: "",

_p_errorMessage: ""

}

let webpageHtml = “”

const outputfile = “outputfile.htm”

const fs = require(“fs”)

function write2outputfile (data2write) {

fs.writeFile(outputfile, data2write, function(err) {

    if (err) {

        console.log("fs err: ", err)

    }

}

)

}

const verifyTheLink = async function verifyLink(webpageUrl, linkUrl) {

let linkv1 = linkUrl

let linkv2 = ""

let regex1 = /\/$/

let regex2 = /$/

if (linkUrl.match(regex1)) {

    linkv2 = linkUrl.replace(regex1, "")

}

else if (linkUrl.match(regex2)) {

    linkv2 = linkUrl.replace(regex2, "/")

}

console.log("")

console.log("CustomPlugin:verifyLink:linkv1 =", linkv1)

console.log("CustomPlugin:verifyLink:linkv2 =", linkv2)

let temp = axios.get(webpageUrl)

.then ((response) => {

    objectToReturn._p_returnStatus = response.status

    objectToReturn._p_returnStatusText = response.statusText

    webpageHtml = response.data

    console.log("\nCustomPlugin:verifyLink:objectToReturn:then: ", objectToReturn)

})

.catch((error) => {

    console.log("CustomPlugin:verifyLink:catch:error.response.status:     ", error.response.status)

    console.log("CustomPlugin:verifyLink:catch:error.response.statusText: ", error.response.statusText)

    console.log("CustomPlugin:verifyLink:error.message:                   ", error.message)

    objectToReturn._p_returnStatus = error.response.status

    objectToReturn._p_returnStatusText = error.response.statusText

    objectToReturn._p_errorMessage = error.message

    console.log("\nCustomPlugin:verifyLink:objectToReturn:catch: ", objectToReturn)

})

.finally(() => {

    console.log("\nCustomPlugin:verifyLink:objectToReturn:finally1: ", objectToReturn)

    const $ = cheerio.load(webpageHtml)

    write2outputfile(webpageHtml)

    const webpageLinks = $("a")

    webpageLinks.each(function () {

        if (($(this).attr('href') === linkv1) || ($(this).attr('href') === linkv2)) {

            objectToReturn._p_href = $(this).attr('href')

            objectToReturn._p_rel = $(this).attr('rel')

            objectToReturn._p_text = $(this).text()

        }

    })

    console.log("\nCustomPlugin:verifyLink:objectToReturn:finally2: ", objectToReturn)

})

console.log("\nCustomPlugin:verifyLink:objectToReturn:first: ", objectToReturn)

}

verifyTheLink(webpageUrl, linkUrl)

console.log(“CustomPlugin:verifyLink:Execution stopping”)
`

Does anyone know what I’m doing wrong?

You can do this entirely with the NodeJS fetch command, assuming the web page to retrieve is passed in properties.url. This function will return three distinct lists hrefs, rels, and texts. You will have to modify the code to return the Bubble _p_ objects you are working with:

// Load
const cheerio = require("cheerio");

// Setup
const url = properties.url;
const options = {
    method: "GET",
    headers: { "Accept": "text/html" }
};

// Dispatch and chain the promise
const response = fetch(url, options)
.then((response) => { return response.text(); })
.then((page) => { return cheerio.load(page); })
.then((dom) => { return dom("a"); })
.then(
    (links) => {
        return {
            hrefs: links.map((link) => { return link.attr("href"); }),
            rels: links.map((link) => { return link.attr("rel"); }),
            texts: links.map((link) => { return link.attr("text"); })
        };
    }
);

// Output the promise tail
return response;

You can make some small performance improvements by bypassing the text() buffer and loading the body stream straight into cheerio. That will take a little bit of fussing to make sure the character encoding and stream datatype lineup.

Thank you. I will check this out.

From MDN

The finally() method of Promise instances schedules a function to be called when the promise is settled (either fulfilled or rejected)

what you return from the finally callback does not modify the settled value of the Promise. The code in the finally block belongs to the first then.
Also the plugin function needs to return something while your first code is not returning anything.
Consider choosing only one between then/catch and async/await because mixing them brings confusion in the code. In the axios documentation you can find both examples.

Progress was made, in as much as I’m now using fetch, but I have the same situation where my NodeJS mock up does what I expect, but the plugin code I derive from it doesn’t.

I’m wondering if you can take a look again and offer advise.

In order to get the title tag, I was not able to use the exact code example you provided as weirdly, with that code I saw it in some links but not in others, so for the actual link details I reverted to the code I had that has been working.

But that code merely extracts the link details from the array of links.

Here is the NodeJS mock up code.

/*
https://dev.to/ramonak/javascript-how-to-access-the-return-value-of-a-promise-object-1bck
https://www.npmjs.com/package/axios#handling-errors
*/

let webpageUrl  = "https://organicgrowth.biz/link-building/you-do-not-want-backlinks-to-your-landing-pages/"
let linkUrl     = "https://www.quoleady.com/product-led-content/"

console.log("CustomPlugin:verifyLink:Execution starting")
console.log("CustomPlugin:verifyLink:webpageUlr: ", webpageUrl)
console.log("CustomPlugin:verifyLink:linkUrl:    ", linkUrl)

const cheerio = require("cheerio")

let objectToReturn = {
    _p_href: "",
    _p_rel: "",
    _p_text: "",
    _p_returnStatus: null,
    _p_returnStatusText: "",
    _p_errorMessage: ""
}

const options = {
    method: "GET",
    headers: { "Accept": "text/html"}
}

const outputfile = "file_output.htm"
const domfile    = "file_dom.txt"
const fs = require("fs")
function write2file (data2write, file2create) {
    fs.writeFile(file2create, data2write, function(err) {
        if (err) {
            console.log("fs err: ", err)
        }
    }
    )
}

const verifyTheLink = async function verifyLink(webpageUrl, linkUrl) {

    let linkv1 = linkUrl
    let linkv2 = ""
    let regex1 = /\/$/
    let regex2 = /$/

    if (linkUrl.match(regex1)) {
        linkv2 = linkUrl.replace(regex1, "")
    }
    else if (linkUrl.match(regex2)) {
        linkv2 = linkUrl.replace(regex2, "/")
    }

    console.log("")
    console.log("CustomPlugin:verifyLink:linkv1 =", linkv1)
    console.log("CustomPlugin:verifyLink:linkv2 =", linkv2)

    const response = fetch (webpageUrl, options)
    .then((response) => {
        objectToReturn._p_returnStatus     = response.status
        objectToReturn._p_returnStatusText = response.statusText
        console.log("\nCustomPlugin:verifyLink:then(response):objectToReturn: ", objectToReturn)
        return response.text()
    })
    .then((page)     => {
        console.log("\nCustomPlugin:verifyLink:then(page)1:objectToReturn: ", objectToReturn)

        write2file(page, outputfile)

        const $ = cheerio.load(page)
        const webpageLinks = $("a")
        webpageLinks.each(function () {
            if (($(this).attr('href') === linkv1) || ($(this).attr('href') === linkv2)) {
                objectToReturn._p_href = $(this).attr('href')
                objectToReturn._p_rel = $(this).attr('rel')
                objectToReturn._p_text = $(this).text()
            }
        })
        console.log("\nCustomPlugin:verifyLink:then(page)2:objectToReturn: ", objectToReturn)

        return objectToReturn
    })
    .then (
        (object) => {
            console.log("CustomPlugin:verifyLink:then(object): ", object)           
            return {
                object
            }
        }
    )
}

verifyTheLink(webpageUrl, linkUrl)
console.log("CustomPlugin:verifyLink:Execution stopping")

And here is the plugin code I derived from it.

async function(properties, context) {

    /* References
    Return an object: http://forum.bubble.io/t/output-an-object-for-server-side-client-side/59419
    Customized error handling in Bubble: https://youtu.be/AITGodtyVyw
    Axios error handling: https://www.npmjs.com/package/axios#handling-errors
    */

    const cheerio = require('cheerio')

    var webpageUrl  = properties.webpage
    var linkUrl     = properties.link

    let objectToReturn = {
        _p_link_href: "",
        _p_link_rel: "",
        _p_link_text: "",
        _p_returnStatus: null,
        _p_returnStatusText: "",
        _p_errorMessage: ""
    }

    const options = {
        method: "GET",
        headers: { "Accept": "text/html" }
    }

    let linkv1 = linkUrl
    let linkv2 = ""
    let regex1 = /\/$/
    let regex2 = /$/

    if (linkUrl.match(regex1)) {
        linkv2 = linkUrl.replace(regex1, "")
    }
    else if (linkUrl.match(regex2)) {
        linkv2 = linkUrl.replace(regex2, "/")
    }

    const response = await fetch(webpageUrl, options)
        .then((response) => {
            objectToReturn._p_returnStatus = response.status
            objectToReturn._p_returnStatusText = response.statusText
            console.log("\nCustomPlugin:verifyLink:then(response):objectToReturn: ", objectToReturn)
            return response.text()
        })
        .then((page) => {
            console.log("\nCustomPlugin:verifyLink:then(page)1:objectToReturn: ", objectToReturn)
            const $ = cheerio.load(page)
            const webpageLinks = $("a")
            webpageLinks.each(function () {
                if (($(this).attr('href') === linkv1) || ($(this).attr('href') === linkv2)) {
                    objectToReturn._p_href = $(this).attr('href')
                    objectToReturn._p_rel = $(this).attr('rel')
                    objectToReturn._p_text = $(this).text()
                }
            })
            console.log("\nCustomPlugin:verifyLink:then(page)2:objectToReturn: ", objectToReturn)

            return objectToReturn
        })
        .then(
            (object) => {
                console.log("CustomPlugin:verifyLink:then(object): ", object)
                return {
                    object
                }
            }
        )
}

your plugin code does not return anything. You may want to return response

Thanks. That of course is a problem. I had “return { object }” which of course is syntactically incorrect.

I just changed the plugin code to

async function(properties, context) {

    /* References
    Return an object: http://forum.bubble.io/t/output-an-object-for-server-side-client-side/59419
    Customized error handling in Bubble: https://youtu.be/AITGodtyVyw
    Axios error handling: https://www.npmjs.com/package/axios#handling-errors
    */

    const cheerio = require('cheerio')

    var webpageUrl  = properties.webpage
    var linkUrl     = properties.link

    let objectToReturn = {
        _p_link_href: "",
        _p_link_rel: "",
        _p_link_text: "",
        _p_returnStatus: null,
        _p_returnStatusText: "",
        _p_errorMessage: ""
    }

    const options = {
        method: "GET",
        headers: { "Accept": "text/html" }
    }

    let linkv1 = linkUrl
    let linkv2 = ""
    let regex1 = /\/$/
    let regex2 = /$/

    if (linkUrl.match(regex1)) {
        linkv2 = linkUrl.replace(regex1, "")
    }
    else if (linkUrl.match(regex2)) {
        linkv2 = linkUrl.replace(regex2, "/")
    }

    const linkDetails = await fetch(webpageUrl, options)
        .then((response) => {
            objectToReturn._p_returnStatus = response.status
            objectToReturn._p_returnStatusText = response.statusText
            console.log("\nCustomPlugin:verifyLink:then(response):objectToReturn: ", objectToReturn)
            return response.text()
        })
        .then((page) => {
            console.log("\nCustomPlugin:verifyLink:then(page)1:objectToReturn: ", objectToReturn)
            const $ = cheerio.load(page)
            const webpageLinks = $("a")
            webpageLinks.each(function () {
                if (($(this).attr('href') === linkv1) || ($(this).attr('href') === linkv2)) {
                    objectToReturn._p_href = $(this).attr('href')
                    objectToReturn._p_rel = $(this).attr('rel')
                    objectToReturn._p_text = $(this).text()
                }
            })
            console.log("\nCustomPlugin:verifyLink:then(page)2:objectToReturn: ", objectToReturn)
            return objectToReturn
        })
        .then((link) => {
            console.log("CustomPlugin:verifyLink:then(link): ", link)
            return link
        })
    return { "object": linkDetails }
}

So the name of what I’m returning more accurate describes what it is, and I return it.

However, while I am now returning a value, it’s devoid of the link detail I collect in the “then((page) =>” section.

you have a big mosunderstanding of promises and async functions. You may want to read more about that on MDN.

In your last example you are returning an object with a property with a value that is a Promise. Bubble expects that you return an object or a Promise that resolves to an object. It’s not meant to handle Promises inside the returned object.

you want something like this:

async function(properties, context) {
    return myAsyncFunction().then((something) => {
        return {
            object: something
        }
    })
}

or like this

async function(properties, context) {
    const something = await myAsyncFunction()
    return {
        object: something
    }
}

I readily admit that promises and async functions still seem mysterious to me, even after reading quite a bit about them.

I was previously attempting to return the object I stuffed with data, and it produces a similar symptom, where the object is returned, but the link detail I stuff into it in the “.then((page) =>” section is missing.

This is what it was:
return { “object”: objectToReturn }

I missed that you are awaiting the fetch, apologies for that.
I still think that you should choose one syntax between async/await and then/catch instead of using both of them.

According to this documentation which I’m trying to follow, I understand I have to put “await” immediately in front of the fetch.

You’ll see it if you search for “Using node-fetch instead of context.request”.

https://manual.bubble.io/account-and-marketplace/building-plugins/updating-to-plugin-api-v4

it’s

const response = await fetch(url, options)
// do your checks on status etc
const html = await response.text()
// do your stuff with cheerio
return {} // the object with the data you want to return
1 Like

Be careful trying to use node-fetch to anyone who is reading this. Latest version of node fetch needs to be imported, not required. You must downgrade to version 2 (if I remember correctly) in order to use.

1 Like

I think I get it now.

Thank you.

1 Like

to ease debugging add a string return value to the plugin and return the stringified object in that, so you can check if it is a problem of interpreting the object as a bubble thing or if it’s missing the data entirely.
Also, every string has an end of string, /$/ will match any string, so the condition is always true

1 Like

You can use node-fetch v3 in bubble ssa. It was already possible to dynamically import it before the new api.
Have a look here
It’s as easy as const { default: nodeFetch } = await import("node-fetch");

Thanks for the info. That’s hacky AF tho. Glad there’s a solution!

that’s standard dynamic imports as per official specifications

Maybe cause I’m not very familiar with node, but normally you have to go inside package json and change something there right? It’s been a while

from nodejs docs

Dynamic import() is supported in both CommonJS and ES modules. In CommonJS modules it can be used to load ES modules

bubble ssa is a CommonJS module. The only thing that you need to specify in the package.json is the version of node-fetch you want to use.

What you are referring to is specifying if your code is ES module or CommonJS using the field type in package.json, but that has nothing to do with bubble ssa because you can’t choose it.

1 Like