Skip to content

Commit

Permalink
feat: add dynamodb table and provider for tag and path revalidation (#…
Browse files Browse the repository at this point in the history
…145)

* feat: add dynamodb table and provider for tag and path revalidation

This diff adds the DynamoDB table and supporting data provider function to support Next.js path and tag revalition. This feature is supported by `open-next` (read more about it here: https://open-next.js.org/inner_workings/isr#tags) and is inspired by sst/sst@v2.27.0...v2.28.0

The changes include

* Adding a new DynamoDB table which stores time stamps, tags, and paths for revalidation consideration
* Adds a function (`dynamodb-provider`) which populates the DynamoDB table on deployment with the initial set of tags / paths / timestamps generated in the Next.js build phase
* **Bumps `aws-cdk-lib` to 2.95.1**, matching the SST version for access to `DynamoDB.TableV2`
* Adds a simple description to the "primary" server handler

A bit more info:

I did this because SST uses the DynamoDB `TableV2` construct and I thought it would be best to match.

I added a description because at least for me the function names end up mangled (I actually think it’d be great to clean these up, but I frankly don’t know the best practice here from IaC / AWS perspective) and it’s nice to be able to easily identify different functions, in particular the server handler, in the AWS console for debugging.

From what I can tell this is a non-breaking change, however this brings me back to a general question about how we should be managing versions and default open-next build commands… SST uses `2.2.1` as the default version of `open-next` in the build command.

* chore: bump cdk version and fix some resource names

* Using the current latest version of CDK lib
* Remove duplicate "Revalidation" from name strings for new queue and dynamodb provider functions

As far as I can tell the CDK bump hasn't had any negative impact, deployment works fine and the Next.js App Playground is deploying and seemingly working well.

* chore: update generated api docs

* chore: update open-next to use version 2.2

* chore: match casing for function descriptions to image function

* chore: update generated api doc

* * use the latest 2.x version of open-next as default build option
	* (note if somebody reads this in the future and is looking for a known working version, as of this commit I'm using `[email protected]` with success)
* some cleanup of names and removing a non-null assertion

---------

Co-authored-by: Kevin Mitchell <>
  • Loading branch information
kevin-mitchell authored Oct 24, 2023
1 parent 350933a commit e68e49f
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 34 deletions.
4 changes: 2 additions & 2 deletions .projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
gitignore: ['.idea'],
// dependency config
jsiiVersion: '~5.0.0',
cdkVersion: '2.93.0',
cdkVersion: '2.99.1',
bundledDeps: ['esbuild'] /* Runtime dependencies of this module. */,
devDeps: [
'@aws-crypto/sha256-js',
Expand Down
63 changes: 49 additions & 14 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/NextjsBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface NextjsBaseProps {

/**
* Optional value used to install NextJS node dependencies.
* @default 'npx --yes open-next@2 build'
* @default 'npx --yes open-next@^2 build'
*/
readonly buildCommand?: string;

Expand Down
11 changes: 10 additions & 1 deletion src/NextjsBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
NEXTJS_BUILD_SERVER_FN_DIR,
NEXTJS_STATIC_DIR,
NEXTJS_CACHE_DIR,
NEXTJS_BUILD_DYNAMODB_PROVIDER_FN_DIR,
} from './constants';
import { NextjsBaseProps } from './NextjsBase';
import { NextjsBucketDeployment } from './NextjsBucketDeployment';
Expand Down Expand Up @@ -51,6 +52,14 @@ export class NextjsBuild extends Construct {
this.warnIfMissing(fnPath);
return fnPath;
}
/**
* Contains function for inserting revalidation items into the table.
*/
public get nextRevalidateDynamoDBProviderFnDir(): string {
const fnPath = path.join(this.getNextBuildDir(), NEXTJS_BUILD_DYNAMODB_PROVIDER_FN_DIR);
this.warnIfMissing(fnPath);
return fnPath;
}
/**
* Static files containing client-side code.
*/
Expand Down Expand Up @@ -101,7 +110,7 @@ export class NextjsBuild extends Construct {

private build() {
const buildPath = this.props.buildPath ?? this.props.nextjsPath;
const buildCommand = this.props.buildCommand ?? 'npx open-next@2 build';
const buildCommand = this.props.buildCommand ?? 'npx open-next@^2 build';
// run build
if (!this.props.quiet) {
console.debug(`├ Running "${buildCommand}" in`, buildPath);
Expand Down
97 changes: 88 additions & 9 deletions src/NextjsRevalidation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Duration, Stack } from 'aws-cdk-lib';
import * as fs from 'fs';
import { CustomResource, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
import { AttributeType, Billing, TableV2 as Table } from 'aws-cdk-lib/aws-dynamodb';
import { AnyPrincipal, Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Code, Function as LambdaFunction, FunctionOptions } from 'aws-cdk-lib/aws-lambda';
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
import { Queue } from 'aws-cdk-lib/aws-sqs';
import { Provider } from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';
import { NextjsBaseProps } from './NextjsBase';
import { NextjsBuild } from './NextjsBuild';
Expand All @@ -27,25 +31,37 @@ export interface NextjsRevalidationProps extends NextjsBaseProps {
}

/**
* Builds the system for revalidating Next.js resources. This includes a Lambda function handler and queue system.
* Builds the system for revalidating Next.js resources. This includes a Lambda function handler and queue system as well
* as the DynamoDB table and provider function.
*
* @see {@link https://github.com/serverless-stack/open-next/blob/main/README.md?plain=1#L65}
*
*/
export class NextjsRevalidation extends Construct {
queue: Queue;
function: LambdaFunction;
table: Table;
queueFunction: LambdaFunction;
tableFunction: LambdaFunction | undefined;
private props: NextjsRevalidationProps;

constructor(scope: Construct, id: string, props: NextjsRevalidationProps) {
super(scope, id);
this.props = props;

this.queue = this.createQueue();
this.function = this.createFunction();
this.queueFunction = this.createQueueFunction();

// allow server fn to send messages to queue
props.serverFunction.lambdaFunction?.addEnvironment('REVALIDATION_QUEUE_URL', this.queue.queueUrl);
this.table = this.createRevalidationTable();
this.tableFunction = this.createRevalidationInsertFunction(this.table);

this.props.serverFunction.lambdaFunction.addEnvironment('CACHE_DYNAMO_TABLE', this.table.tableName);

if (this.props.serverFunction.lambdaFunction.role) {
this.table.grantReadWriteData(this.props.serverFunction.lambdaFunction.role);
}

this.props.serverFunction.lambdaFunction // allow server fn to send messages to queue
?.addEnvironment('REVALIDATION_QUEUE_URL', this.queue.queueUrl);
props.serverFunction.lambdaFunction?.addEnvironment('REVALIDATION_QUEUE_REGION', Stack.of(this).region);
}

Expand All @@ -72,18 +88,81 @@ export class NextjsRevalidation extends Construct {
return queue;
}

private createFunction(): LambdaFunction {
private createQueueFunction(): LambdaFunction {
const commonFnProps = getCommonFunctionProps(this);
const fn = new LambdaFunction(this, 'Fn', {
const fn = new LambdaFunction(this, 'QueueFn', {
...commonFnProps,
// open-next revalidation-function
// see: https://github.com/serverless-stack/open-next/blob/274d446ed7e940cfbe7ce05a21108f4c854ee37a/README.md?plain=1#L65
code: Code.fromAsset(this.props.nextBuild.nextRevalidateFnDir),
handler: 'index.handler',
description: 'Next.js revalidation function',
description: 'Next.js Queue Revalidation Function',
timeout: Duration.seconds(30),
});
fn.addEventSource(new SqsEventSource(this.queue, { batchSize: 5 }));
return fn;
}

private createRevalidationTable() {
return new Table(this, 'Table', {
partitionKey: { name: 'tag', type: AttributeType.STRING },
sortKey: { name: 'path', type: AttributeType.STRING },
billing: Billing.onDemand(),
globalSecondaryIndexes: [
{
indexName: 'revalidate',
partitionKey: { name: 'path', type: AttributeType.STRING },
sortKey: { name: 'revalidatedAt', type: AttributeType.NUMBER },
},
],
removalPolicy: RemovalPolicy.DESTROY,
});
}

/**
* This function will insert the initial batch of tag / path / revalidation data into the DynamoDB table during deployment.
* @see: {@link https://open-next.js.org/inner_workings/isr#tags}
*
* @param revalidationTable table to grant function access to
* @returns the revalidation insert provider function
*/
private createRevalidationInsertFunction(revalidationTable: Table) {
const dynamodbProviderPath = this.props.nextBuild.nextRevalidateDynamoDBProviderFnDir;

// note the function may not exist - it only exists if there are cache tags values defined in Next.js build meta files to be inserted
// see: https://github.com/sst/open-next/blob/c2b05e3a5f82de40da1181e11c087265983c349d/packages/open-next/src/build.ts#L426-L458
if (fs.existsSync(dynamodbProviderPath)) {
const commonFnProps = getCommonFunctionProps(this);
const insertFn = new LambdaFunction(this, 'DynamoDBProviderFn', {
...commonFnProps,
// open-next revalidation-function
// see: https://github.com/serverless-stack/open-next/blob/274d446ed7e940cfbe7ce05a21108f4c854ee37a/README.md?plain=1#L65
code: Code.fromAsset(this.props.nextBuild.nextRevalidateDynamoDBProviderFnDir),
handler: 'index.handler',
description: 'Next.js Revalidation DynamoDB Provider',
timeout: Duration.minutes(1),
environment: {
CACHE_DYNAMO_TABLE: revalidationTable.tableName,
},
});

revalidationTable.grantReadWriteData(insertFn);

const provider = new Provider(this, 'DynamoDBProvider', {
onEventHandler: insertFn,
logRetention: RetentionDays.ONE_DAY,
});

new CustomResource(this, 'DynamoDBResource', {
serviceToken: provider.serviceToken,
properties: {
version: Date.now().toString(),
},
});

return insertFn;
}

return undefined;
}
}
1 change: 1 addition & 0 deletions src/NextjsServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class NextjsServer extends Construct {
...getCommonFunctionProps(this),
code: Code.fromBucket(asset.bucket, asset.s3ObjectKey),
handler: 'index.handler',
description: 'Next.js Server Handler',
...this.props.lambda,
// `environment` needs to go after `this.props.lambda` b/c if
// `this.props.lambda.environment` is defined, it will override
Expand Down
Loading

0 comments on commit e68e49f

Please sign in to comment.