Build a Serverless Comment System for a Jamstack Blog

This page summarizes the projects mentioned and recommended in the original post on dev.to

Our great sponsors
  • InfluxDB - Power Real-Time Data Analytics at Scale
  • WorkOS - The modern identity platform for B2B SaaS
  • SaaSHub - Software Alternatives and Reviews
  • // comment.ts import { AzureFunction, Context, HttpRequest } from "@azure/functions"; import * as querystring from "querystring"; import util = require("util"); import uuidv4 = require("uuid/v4"); import * as SendGrid from "@sendgrid/mail"; import * as simpleGit from "simple-git/promise"; import { formHelpers } from "../common/formHelpers"; import { Octokit } from "@octokit/rest"; import fs = require("fs"); import rimrafstd = require("rimraf"); import { tmpdir } from "os"; const rimraf = util.promisify(rimrafstd); const mkdir = util.promisify(fs.mkdir); const writeFile = util.promisify(fs.writeFile); const readFile = util.promisify(fs.readFile); SendGrid.setApiKey(process.env["SendGridApiKey"] as string); const httpTrigger: AzureFunction = async function ( context: Context, req: HttpRequest ): Promise { context.log("HTTP trigger function processed a request."); context.res!.headers["Content-Type"] = "application/json"; const body = querystring.parse(req.body); if ( !( body && body.comment && body.postGuid && body.authorEmail && body.authorName ) ) { context.res!.status = 400; context.res!.body = { message: "Comment invalid. Please correct errors and try again.", }; return; } //Initialize Git Repository with Simple Git // generate unique folder name for git repository const tempRepo = uuidv4(); // create empty directory to store comment file await mkdir(`${tmpdir}/${tempRepo}/comments`, { recursive: true, }); // initialize simple-git const git = simpleGit(`${tmpdir}/${tempRepo}`); // initialize git repository in tempRepo await git.init(); // set up git config await Promise.all([ git.addConfig("user.name", "GITHUB_USERNAME"), git.addConfig("user.email", "GITHUB_EMAIL"), ]); // add the private remote await git.addRemote( "private", `https://GITHUB_USERNAME:${process.env["GitHubUserPassword"]}@https://github.com/GITHUB_USERNAME/PRIVATE_REPOSITORY` ); //Checkout git branch with Simple Git // generate unique id for comment const commentId = uuidv4(); // create branch try { // fetch main branch to base of off await git.fetch("private", "main"); // use postID to see if comments already are saved for this post await git.checkout("private/main", ["--", `comments/${body.postId}.json`]); // create new branch named with commentID based off main branch await git.checkoutBranch(`${commentId}`, "private/main"); } catch (error) { // no previous comments are saved for this post await git.checkout("private/main"); await git.checkoutLocalBranch(`${commentId}`); } // Write JSON File with updated Comment data // create comment object to store as JSON in git repository const comment = { id: commentId, timestamp: new Date(new Date().toUTCString()).getTime(), authorEmail: body.authorEmail, authorName: body.authorName, bodyText: body.comment, }; // list of all comments let comments = []; // retrieve existing comments try { comments = JSON.parse( await readFile( `${tmpdir}/${tempRepo}/comments/${body.postId}.json`, "utf8" ) ); } catch (error) { //no previous comments } // add newly submitted comment comments.push(comment); // update or create new comments file with new comment included await writeFile( `${tmpdir}/${tempRepo}/comments/${body.postId}.json`, JSON.stringify(comments, null, 2), "utf8" ); // stage file modifications, commit and push await git.add(`${tmpdir}/${tempRepo}/comments/${body.postId}.json`); await git.commit(`adding comment ${commentId}`); await git.push("private", `${commentId}`); // delete temporary repository await rimraf(`${tmpdir}/${tempRepo}/`); //send notifications and create pull request const userEmail = { to: body.authorEmail, from: "YOUR_NAME@YOUR_WEBSITE", subject: "comment submitted", text: "Your comment will be visible when approved.", }; const adminEmail = { to: "ADMIN_EMAIL", from: "ADMIN_EMAIL", subject: "comment submitted", html: `from: ${body.authorName} email: ${body.authorEmail} comment: ${body.comment}`, }; await Promise.all([ SendGrid.send(userEmail), SendGrid.send(adminEmail), new Octokit({ auth: process.env["GitHubUserPassword"], }).pulls.create({ owner: "GITHUB_USERNAME", repo: "PRIVATE_REPOSITORY", title: `${commentId}`, head: `${commentId}`, base: "main", }), ]); context.res!.status = 200; context.res!.body = { message: "Success!", }; }; export default httpTrigger;

  • git-js

    A light weight interface for running git commands in any node.js application.

  • In either case of appending a comment to an existing list or committing the first one, the end result of the try catch block will be a new branch checked out with the name of the commentId that was just generated. Be sure to note the difference between checkoutBranch and checkoutLocalBranch in the Simple Git git checkout documentation.

  • InfluxDB

    Power Real-Time Data Analytics at Scale. Get real-time insights from all types of time series data with InfluxDB. Ingest, query, and analyze billions of data points in real-time with unbounded cardinality.

    InfluxDB logo
  • // comment-merge.ts import { AzureFunction, Context, HttpRequest } from "@azure/functions"; import util = require("util"); import * as querystring from "querystring"; import * as simpleGit from "simple-git/promise"; import fs = require("fs"); import { tmpdir } from "os"; import uuidv4 = require("uuid/v4"); import globstd = require("glob"); import rimrafstd = require("rimraf"); const rimraf = util.promisify(rimrafstd); const glob = util.promisify(globstd); const mkdir = util.promisify(fs.mkdir); const writeFile = util.promisify(fs.writeFile); const readFile = util.promisify(fs.readFile); const httpTrigger: AzureFunction = async function ( context: Context, req: HttpRequest ): Promise { context.log("HTTP trigger function processed a request."); context.res!.headers["Content-Type"] = "application/json"; //request content type is configured in GitHub webhook settings const payload = req.body; if ( payload.action != "closed" || payload.pull_request.base.ref != "main" || !payload.pull_request.merged_at ) { return; } // create temp repo and add remotes const tempRepo = uuidv4(); await mkdir(`${tmpdir}/${tempRepo}/comments`, { recursive: true, }); const git = simpleGit(`${tmpdir}/${tempRepo}`); await git.init(); await Promise.all([ git.addConfig("user.name", "GITHUB_USERNAME"), git.addConfig("user.email", "GITHUB_EMAIL"), ]); await Promise.all([ git.addRemote( "private", `https://GITHUB_USERNAME:${process.env["GitHubUserPassword"]}@https://github.com/GITHUB_USERNAME/PRIVATE_REPOSITORY` ), git.addRemote( "public", `https://GITHUB_USERNAME:${process.env["GitHubUserPassword"]}@https://github.com/GITHUB_USERNAME/PUBLIC_REPOSITORY` ), ]); // fetch public and integrate with latest modifications from private repo await git.fetch("public", "main"); await git.checkout("main", ["--", "comments/"]); await git.checkoutBranch("main", "main"); await git.fetch("private", "main"); await git.checkout("main", ["--", "comments/"]); // filter private data from comments // retrieve comment file paths const paths = await glob(`comments/**/*.json`, { cwd: `${tmpdir}/${tempRepo}/`, }); // wait for all paths to process asynchronously await Promise.all( paths.map(async (path) => { let pathData = []; //read JSON file with comment info pathData = JSON.parse( await readFile(`${tmpdir}/${tempRepo}/${path}`, "utf8") ); // filter out private info const publicData = pathData.map((item) => { const { authorEmail, ...store } = item; return store; }); // write file back to original with private data removed await writeFile( `${tmpdir}/${tempRepo}/${path}`, JSON.stringify(publicData, null, 2), "utf8" ); }) ); // add filtered comment file modifications, commit, and push await git.add(`${tmpdir}/${tempRepo}/comments/*.json`); await git.commit("approving comment"); await git.push("public", "main"); await rimraf(`${tmpdir}/${tempRepo}/`); context.res!.status = 200; context.res!.body = { message: "success" }; }; export default httpTrigger;

NOTE: The number of mentions on this list indicates mentions on common posts plus user suggested alternatives. Hence, a higher number means a more popular project.

Suggest a related project