diff --git a/.projen/deps.json b/.projen/deps.json index db054760..c0215ba5 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -49,7 +49,7 @@ }, { "name": "aws-cdk-lib", - "version": "2.93.0", + "version": "2.99.1", "type": "build" }, { @@ -173,7 +173,7 @@ }, { "name": "aws-cdk-lib", - "version": "^2.93.0", + "version": "^2.99.1", "type": "peer" }, { diff --git a/.projenrc.ts b/.projenrc.ts index 358411ce..94a9344e 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -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', diff --git a/API.md b/API.md index 40963fd7..3e63de57 100644 --- a/API.md +++ b/API.md @@ -602,6 +602,7 @@ Any object. | node | constructs.Node | The tree node. | | nextCacheDir | string | Cache directory for generated data. | | nextImageFnDir | string | Contains function for processessing image requests. | +| nextRevalidateDynamoDBProviderFnDir | string | Contains function for inserting revalidation items into the table. | | nextRevalidateFnDir | string | Contains function for processing items from revalidation queue. | | nextServerFnDir | string | Contains server code and dependencies. | | nextStaticDir | string | Static files containing client-side code. | @@ -647,6 +648,18 @@ Should be arm64. --- +##### `nextRevalidateDynamoDBProviderFnDir`Required + +```typescript +public readonly nextRevalidateDynamoDBProviderFnDir: string; +``` + +- *Type:* string + +Contains function for inserting revalidation items into the table. + +--- + ##### `nextRevalidateFnDir`Required ```typescript @@ -2075,7 +2088,7 @@ The tree node. ### NextjsRevalidation -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. > [{@link https://github.com/serverless-stack/open-next/blob/main/README.md?plain=1#L65}]({@link https://github.com/serverless-stack/open-next/blob/main/README.md?plain=1#L65}) @@ -2160,8 +2173,10 @@ Any object. | **Name** | **Type** | **Description** | | --- | --- | --- | | node | constructs.Node | The tree node. | -| function | aws-cdk-lib.aws_lambda.Function | *No description.* | | queue | aws-cdk-lib.aws_sqs.Queue | *No description.* | +| queueFunction | aws-cdk-lib.aws_lambda.Function | *No description.* | +| table | aws-cdk-lib.aws_dynamodb.TableV2 | *No description.* | +| tableFunction | aws-cdk-lib.aws_lambda.Function | *No description.* | --- @@ -2177,23 +2192,43 @@ The tree node. --- -##### `function`Required +##### `queue`Required ```typescript -public readonly function: Function; +public readonly queue: Queue; +``` + +- *Type:* aws-cdk-lib.aws_sqs.Queue + +--- + +##### `queueFunction`Required + +```typescript +public readonly queueFunction: Function; ``` - *Type:* aws-cdk-lib.aws_lambda.Function --- -##### `queue`Required +##### `table`Required ```typescript -public readonly queue: Queue; +public readonly table: TableV2; ``` -- *Type:* aws-cdk-lib.aws_sqs.Queue +- *Type:* aws-cdk-lib.aws_dynamodb.TableV2 + +--- + +##### `tableFunction`Optional + +```typescript +public readonly tableFunction: Function; +``` + +- *Type:* aws-cdk-lib.aws_lambda.Function --- @@ -2596,7 +2631,7 @@ public readonly buildCommand: string; ``` - *Type:* string -- *Default:* 'npx --yes open-next@2 build' +- *Default:* 'npx --yes open-next@^2 build' Optional value used to install NextJS node dependencies. @@ -2885,7 +2920,7 @@ public readonly buildCommand: string; ``` - *Type:* string -- *Default:* 'npx --yes open-next@2 build' +- *Default:* 'npx --yes open-next@^2 build' Optional value used to install NextJS node dependencies. @@ -3217,7 +3252,7 @@ public readonly buildCommand: string; ``` - *Type:* string -- *Default:* 'npx --yes open-next@2 build' +- *Default:* 'npx --yes open-next@^2 build' Optional value used to install NextJS node dependencies. @@ -3674,7 +3709,7 @@ public readonly buildCommand: string; ``` - *Type:* string -- *Default:* 'npx --yes open-next@2 build' +- *Default:* 'npx --yes open-next@^2 build' Optional value used to install NextJS node dependencies. @@ -3949,7 +3984,7 @@ public readonly buildCommand: string; ``` - *Type:* string -- *Default:* 'npx --yes open-next@2 build' +- *Default:* 'npx --yes open-next@^2 build' Optional value used to install NextJS node dependencies. @@ -4174,7 +4209,7 @@ public readonly buildCommand: string; ``` - *Type:* string -- *Default:* 'npx --yes open-next@2 build' +- *Default:* 'npx --yes open-next@^2 build' Optional value used to install NextJS node dependencies. @@ -4363,7 +4398,7 @@ public readonly buildCommand: string; ``` - *Type:* string -- *Default:* 'npx --yes open-next@2 build' +- *Default:* 'npx --yes open-next@^2 build' Optional value used to install NextJS node dependencies. diff --git a/package.json b/package.json index b8645da7..1385f36d 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@types/node": "^18", "@typescript-eslint/eslint-plugin": "^5", "@typescript-eslint/parser": "^5", - "aws-cdk-lib": "2.93.0", + "aws-cdk-lib": "2.99.1", "aws-lambda": "^1.0.7", "constructs": "10.0.5", "esbuild": "^0.19.2", @@ -81,7 +81,7 @@ "undici": "^5.23.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.93.0", + "aws-cdk-lib": "^2.99.1", "constructs": "^10.0.5" }, "dependencies": { diff --git a/src/NextjsBase.ts b/src/NextjsBase.ts index d8bdaff9..e8369d00 100644 --- a/src/NextjsBase.ts +++ b/src/NextjsBase.ts @@ -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; diff --git a/src/NextjsBuild.ts b/src/NextjsBuild.ts index af20d7e1..a982d822 100644 --- a/src/NextjsBuild.ts +++ b/src/NextjsBuild.ts @@ -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'; @@ -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. */ @@ -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); diff --git a/src/NextjsRevalidation.ts b/src/NextjsRevalidation.ts index 48271f57..82db7252 100644 --- a/src/NextjsRevalidation.ts +++ b/src/NextjsRevalidation.ts @@ -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'; @@ -27,14 +31,17 @@ 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) { @@ -42,10 +49,19 @@ export class NextjsRevalidation extends Construct { 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); } @@ -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; + } } diff --git a/src/NextjsServer.ts b/src/NextjsServer.ts index 86fcbf27..81efc3ae 100644 --- a/src/NextjsServer.ts +++ b/src/NextjsServer.ts @@ -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 diff --git a/src/constants.ts b/src/constants.ts index 9e5b6d4e..18e0856e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,5 +9,6 @@ export const NEXTJS_STATIC_DIR = 'assets'; export const NEXTJS_BUILD_DIR = '.open-next'; export const NEXTJS_CACHE_DIR = 'cache'; export const NEXTJS_BUILD_REVALIDATE_FN_DIR = 'revalidation-function'; +export const NEXTJS_BUILD_DYNAMODB_PROVIDER_FN_DIR = 'dynamodb-provider'; export const NEXTJS_BUILD_IMAGE_FN_DIR = 'image-optimization-function'; export const NEXTJS_BUILD_SERVER_FN_DIR = 'server-function'; diff --git a/yarn.lock b/yarn.lock index 4d3684d8..38f3fbef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2515,10 +2515,10 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -aws-cdk-lib@2.93.0: - version "2.93.0" - resolved "https://registry.yarnpkg.com/aws-cdk-lib/-/aws-cdk-lib-2.93.0.tgz#545bc0072bc0f2e27cb0fecb0c9e54de29b10731" - integrity sha512-kKbcKkts272Ju5xjGKI3pXTOpiJxW4OQbDF8Vmw/NIkkuJLo8GlRCFfeOfoN/hilvlYQgENA67GCgSWccbvu7w== +aws-cdk-lib@2.99.1: + version "2.99.1" + resolved "https://registry.yarnpkg.com/aws-cdk-lib/-/aws-cdk-lib-2.99.1.tgz#e2cd3e091fa9c65ca2835ad9a4bb6564e6b42189" + integrity sha512-mUhuT2JTy3VyX9o9IOSuy7UYDimFHGnmpASwTb4rD10Hksb1dTqqN2BsXU5kogHakYevBD3vwYc87rOVso4M7Q== dependencies: "@aws-cdk/asset-awscli-v1" "^2.2.200" "@aws-cdk/asset-kubectl-v20" "^2.1.2"