DEV Community

Cover image for How I Automated My Weekly Newsletter using BlitzJs and CRON
John Grisham
John Grisham

Posted on • Originally published at javascript.plainenglish.io

How I Automated My Weekly Newsletter using BlitzJs and CRON

I recently discovered Blitz.js and love how quickly the framework lets you build an application but my favorite part is the command-line tool. Blitz.js has a console that can be used to run predefined queries and mutations via a node REPL environment which is great for manually running tasks from the command line, but less good when ran inside of CRON job since it will take over whatever process it’s running on.

I have a newsletter that collects news stories and I wanted to automate sending this newsletter every other week. I needed a CRON solution initiated via a bash script that wasn’t using expect commands so I started looking into the child_process module.

To support me sign up for Medium and read this tutorial there: Medium signup link


Clearing old data

Blitz.js is built upon Next.js so hosting is flexible but for me, the winner is Render.com. I’m not affiliated with them but of the hosting platforms I’ve tried, they have been the cheapest and easiest to set up. This post won’t go in-depth with how to use Render for hosting, just be aware that is the host where I set up my CRON job.

Render offers a simple CRON service. I want mine to do two things once a week.

  1. Delete records from my database that are more than 6 days old
  2. Run my newsletter auto-mail mutation if the week is odd (every other week)

My bash script will be the entry point and will initiate these processes.

echo 'Running weekly script...'
echo 'Clearing old data...'
chmod +x ./automation/clearMail.ts
ts-node ./automation/clearMail.ts
echo 'Old data cleared from db'
echo 'Checking if auto mailer should send...'
WEEK=`date +"%V"`
if [ $(($WEEK%2)) -eq 0 ];
then
    echo "Sending auto mailer..."
    chmod +x ./automation/sendAutoMail.ts
    ts-node ./automation/sendAutoMail.ts
else
    echo "No auto mail to send"
fi
echo 'Done'
Enter fullscreen mode Exit fullscreen mode

I use ts-node to make calls to other Typescript files here which will execute the core logic that I want to accomplish. Lets take a look at the first one.

// clearMail.ts
import { onExit, streamEnd } from '@rauschma/stringio'
import { spawn } from 'child_process'
import { writeToWritable } from '../app/core/utils'

async function clearMail() {
    const blitzProcess = spawn('blitz console', [], { shell: true, stdio: ['pipe', process.stdout, process.stderr] })
    blitzProcess.on('error', (err) => {
        console.error(err.message)
    })

    await writeToWritable({
        commands: ['await clearOld()', '.exit'],
        writable: blitzProcess.stdin
    })

    return await onExit(blitzProcess)
}

clearMail()
Enter fullscreen mode Exit fullscreen mode

This file will spawn a new process using the blitz console with a custom stdio configuration. This configuration is important and is the only one that worked for my solution. The problem I kept running into was that once the blitz console started it would gum up my main process and prevent me from running further commands inside the console. We are making a way for the parent process to communicate with the child process input. The parent can tell the child what to do and vice versa. As for the output and error channel, we make those default to the main process stout and sterr respectively.

I also tell the blitz process to log any errors via the process on error event handler. I’m using some utility methods to then run my clearOld mutation that will delete old records from my mail database. I’m also using the stringio package from Axel Rauschmayer for managing the console execution stream.

// writeToWritable.ts
import { streamEnd, streamWrite } from '@rauschma/stringio'
import { Writable } from 'stream'

export default async function writeToWritable({ commands, writable }: { writable: Writable; commands: string[] }) {
    for (const cmd of commands) {
        await streamWrite(writable, `${cmd}\n`)
    }

    return await streamEnd(writable)
}
Enter fullscreen mode Exit fullscreen mode

This file will write multiple commands to the new child process and then end the stream. Once we’re done we’ll exit the blitz console child process. In case you’re interested my clearOld mutation looks like this.

// clearOld.ts
import db from 'db'
import { sub } from 'date-fns'

export default async function clearOld() {
    try {
        return await db.mail.deleteMany({
            where: {
                createdAt: {
                    lt: sub(new Date(), { days: 6 })
                }
            }
        })
    } catch (err) {
        throw new Error(err.message)
    }
}
Enter fullscreen mode Exit fullscreen mode

Sending the newsletter

The next part of the bash script runs the automatic mailer file every other week. This file will get my signups from Firebase and send them a collection of news stories via the Sendgrid API. The child process setup is the same here the only difference is that I have some arguments that I’d like to be able to pass when I run the file for this I use the pirate-themed command-line arguments package Yargs.

Yargs on Github

I also have some utility methods for making a temp file with some HTML that I will then load when I send the newsletter. This HTML will serve as my template for the newsletter that I have saved in a different table. Anyway, all of that falls outside of the scope of this tutorial so here’s the base code for the auto mailer file.

// sendAutoMail.ts
import { makeTemp, writeToWritable } from '../app/core/utils'
import { autoMailer } from '../designs/designs.json'
import { hideBin } from 'yargs/helpers'
import { onExit } from '@rauschma/stringio'
import { spawn } from 'child_process'
import yargs from 'yargs/yargs'

async function sendAutoMail() {
    try {
        const argv = yargs(hideBin(process.argv)).argv
        const isTest = (argv.test as boolean) ?? false
        const testUsers = isTest ? JSON.stringify([{ email: 'test@test.com' }]) : undefined
        const subject = isTest ? 'test' : 'Your weekly echo stories'
        const html = autoMailer
        const htmlName = await makeTemp({ data: html, name: 'auto_mailer' })
        const blitzProcess = spawn('blitz console', [], { shell: true, stdio: ['pipe', process.stdout, process.stderr] })

        blitzProcess.on('error', (err) => {
            throw new Error(err.message)
        })

        blitzProcess.stdin.on('error', (err) => {
            throw new Error(err.message)
        })

        if (testUsers !== undefined) {
            await writeToWritable({
                commands: [
                    `await sendMail({ html: '${htmlName}', raw: false, mail: true, subject: '${subject}', testUsers: ${testUsers}  })`,
                    '.exit'
                ],
                writable: blitzProcess.stdin
            })
        } else {
            await writeToWritable({
                commands: [`await sendMail({ html: '${htmlName}', raw: false, mail: true, subject: '${subject}'  })`, '.exit'],
                writable: blitzProcess.stdin
            })
        }

        return await onExit(blitzProcess)
    } catch (err) {
        throw new Error(err.message)
    }
}

sendAutoMail()
Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope this has been helpful. Blitz.js is great and it’s a lot of fun to develop with but the CLI can also be used outside of generating files and folders which opens up some interesting CRON job solutions. I found the console to be simple to use and fun but lacking when trying to run commands automatically via bash. My solution is very specific to my use case and if I didn’t have such a particular goal in mind for my newsletter I would have used an existing email newsletter solution.

This guide isn’t intended to be a step-by-step solution for your CRON endeavors with the blitz console but it should give you inspiration into what you can automate out of your workflow using these technologies. If you’re like me and you need solutions outside of what other Saas providers give, you can create your automated workflows using CRON and some elbow grease. What do you think of my solution? Is there room for improvement or have I overengineered it? If you’re curious you can also check out the newsletter that I built this CRON job for here.

Echo Breaking News

Discussion (0)