Alright, I know I said in the previous post that the application was finished, but that's the thing with software, it's not really finished until you decommission it.

Being based in the United Kingdom means that American Pay-Per-Views don't start until late at night: a 7 p.m. Eastern start is midnight for us. What I'd like is for the application to automatically 'lock' an event just before the event begins its broadcast so that nobody can edit their predictions during an event.

AWS Amplify supports creating scheduled lambda functions through the command line. Run amplify add function with the following options:

  • Capability: Lambda function (serverless function)
  • NodeJS
  • Function Template: Hello World
  • Configure advanced settings? Yes
  • Do you want to access other resources in this project from your Lambda function? Yes
  • CRUD permissions for storage
  • Do you want to invoke this function on a recurring schedule? Yes
  • At which interval should the function be invoked? Hourly
  • Enter the rate in hours: 1
  • Choose no for everything else.

This will create a new directory for you lambda function within the amplify/backend/function direcrtory, alongside any other applications.

The Functionality

I opted to run the Lambda function hourly because I didn't want to assume that the event starts at a particular time of the day. This will be the case when AEW returns to Wembley in August and I certainly won't be at my computer desk!

The Lambda function itself queries for all upcoming events. It then compares their start time against the current timestamp (in milliseconds from the Unix Epoch). If an event is due to start in one hour or less from now, the event state is changed from upcoming to broadcast.

import { getUpcomingEvents, updateEventState } from './services.js';

const headers = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*"
};
const ONE_HOUR = 1000 * 60 * 60;

export async function handler() {
  try {
    const now = Date.now();
    const events = await getUpcomingEvents();

    for (let event of events) {
      if (Number(event.date) - now <= ONE_HOUR) {
        await updateEventState(event.PK, event.SK);
      }
    }

    return {
      statusCode: 200,
      headers,
    };
  } catch (error) {
    return {
      statusCode: 500,
      headers,
      body: error
    };
  }
};

The DynamoDB queries are separated into a services.js file, for separation of concerns:

import { DynamoDBClient, QueryCommand, UpdateItemCommand } from '@aws-sdk/client-dynamodb';
import { unmarshall } from '@aws-sdk/util-dynamodb';

const PICKEM_TABLE = process.env.STORAGE_PICKEM_NAME;

const client = new DynamoDBClient({ region: process.env.TABLE_REGION });

const db = {
  query: async function(params) {
    const { Items } = await client.send(new QueryCommand(params));
    return Items.map(unmarshall);
  },
  update: async function (params) {
    await client.send(new UpdateItemCommand(params));
  }
};

export async function getUpcomingEvents() {
  const params = {
    TableName: PICKEM_TABLE,
    ScanIndexForward: false,
    ExpressionAttributeNames: {
      "#pk": "PK",
      "#state": "state"
    },
    ExpressionAttributeValues: {
      ':pk': { 'S': 'EVENT' },
      ':s': { 'S': 'upcoming' }
    },
    KeyConditionExpression: '#pk=:pk',
    FilterExpression: "#state = :s"
  };

  const items = await db.query(params);
  return items;
}

export async function updateEventState(pk, sk) {
  const params = {
    TableName: PICKEM_TABLE,
    Key: {
      "PK": { "S": pk },
      "SK": { "S": sk },
    },
    ExpressionAttributeNames: {
      "#s": "state",
    },
    ExpressionAttributeValues: {
      ":s": { "S": "broadcast" }
    },
    UpdateExpression: 'SET #s=:s'
  };
  await db.update(params);
}

And Now

I'm not going to make the same mistake of saying that the application is finished, no not this time. I am impressed at the simplicity of Amplify, especially regarding setting up a dedicated piece of standalone functionality with minimal application changes.