@opennextjs/aws 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/adapters/binary.d.ts +1 -0
- package/dist/adapters/binary.js +63 -0
- package/dist/adapters/cache.d.ts +96 -0
- package/dist/adapters/cache.js +278 -0
- package/dist/adapters/config/index.d.ts +27 -0
- package/dist/adapters/config/index.js +21 -0
- package/dist/adapters/config/util.d.ts +28 -0
- package/dist/adapters/config/util.js +84 -0
- package/dist/adapters/dynamo-provider.d.ts +8 -0
- package/dist/adapters/dynamo-provider.js +42 -0
- package/dist/adapters/edge-adapter.d.ts +8 -0
- package/dist/adapters/edge-adapter.js +54 -0
- package/dist/adapters/image-optimization-adapter.d.ts +4 -0
- package/dist/adapters/image-optimization-adapter.js +193 -0
- package/dist/adapters/logger.d.ts +13 -0
- package/dist/adapters/logger.js +51 -0
- package/dist/adapters/middleware.d.ts +5 -0
- package/dist/adapters/middleware.js +84 -0
- package/dist/adapters/plugins/14.1/util.d.ts +1 -0
- package/dist/adapters/plugins/14.1/util.js +29 -0
- package/dist/adapters/plugins/image-optimization/image-optimization.d.ts +11 -0
- package/dist/adapters/plugins/image-optimization/image-optimization.js +14 -0
- package/dist/adapters/plugins/image-optimization/image-optimization.replacement.d.ts +11 -0
- package/dist/adapters/plugins/image-optimization/image-optimization.replacement.js +22 -0
- package/dist/adapters/plugins/without-routing/requestHandler.d.ts +2 -0
- package/dist/adapters/plugins/without-routing/requestHandler.js +10 -0
- package/dist/adapters/revalidate.d.ts +9 -0
- package/dist/adapters/revalidate.js +71 -0
- package/dist/adapters/server-adapter.d.ts +4 -0
- package/dist/adapters/server-adapter.js +27 -0
- package/dist/adapters/util.d.ts +10 -0
- package/dist/adapters/util.js +28 -0
- package/dist/adapters/warmer-function.d.ts +12 -0
- package/dist/adapters/warmer-function.js +80 -0
- package/dist/build/bundleNextServer.d.ts +1 -0
- package/dist/build/bundleNextServer.js +87 -0
- package/dist/build/compileConfig.d.ts +3 -0
- package/dist/build/compileConfig.js +64 -0
- package/dist/build/copyTracedFiles.d.ts +1 -0
- package/dist/build/copyTracedFiles.js +207 -0
- package/dist/build/createServerBundle.d.ts +3 -0
- package/dist/build/createServerBundle.js +228 -0
- package/dist/build/edge/createEdgeBundle.d.ts +20 -0
- package/dist/build/edge/createEdgeBundle.js +149 -0
- package/dist/build/generateOutput.d.ts +2 -0
- package/dist/build/generateOutput.js +239 -0
- package/dist/build/helper.d.ts +33 -0
- package/dist/build/helper.js +193 -0
- package/dist/build/patch/patchedAsyncStorage.d.ts +5 -0
- package/dist/build/patch/patchedAsyncStorage.js +16 -0
- package/dist/build/validateConfig.d.ts +2 -0
- package/dist/build/validateConfig.js +76 -0
- package/dist/build.d.ts +7 -0
- package/dist/build.js +533 -0
- package/dist/cache/incremental/s3-lite.d.ts +3 -0
- package/dist/cache/incremental/s3-lite.js +72 -0
- package/dist/cache/incremental/s3.d.ts +3 -0
- package/dist/cache/incremental/s3.js +44 -0
- package/dist/cache/incremental/types.d.ts +34 -0
- package/dist/cache/incremental/types.js +1 -0
- package/dist/cache/next-types.d.ts +62 -0
- package/dist/cache/next-types.js +1 -0
- package/dist/cache/tag/constants.d.ts +2 -0
- package/dist/cache/tag/constants.js +17 -0
- package/dist/cache/tag/dynamodb-lite.d.ts +3 -0
- package/dist/cache/tag/dynamodb-lite.js +174 -0
- package/dist/cache/tag/dynamodb.d.ts +3 -0
- package/dist/cache/tag/dynamodb.js +129 -0
- package/dist/cache/tag/types.d.ts +11 -0
- package/dist/cache/tag/types.js +1 -0
- package/dist/converters/aws-apigw-v1.d.ts +3 -0
- package/dist/converters/aws-apigw-v1.js +89 -0
- package/dist/converters/aws-apigw-v2.d.ts +3 -0
- package/dist/converters/aws-apigw-v2.js +99 -0
- package/dist/converters/aws-cloudfront.d.ts +3 -0
- package/dist/converters/aws-cloudfront.js +170 -0
- package/dist/converters/dummy.d.ts +7 -0
- package/dist/converters/dummy.js +16 -0
- package/dist/converters/edge.d.ts +6 -0
- package/dist/converters/edge.js +88 -0
- package/dist/converters/node.d.ts +3 -0
- package/dist/converters/node.js +45 -0
- package/dist/converters/sqs-revalidate.d.ts +4 -0
- package/dist/converters/sqs-revalidate.js +22 -0
- package/dist/converters/utils.d.ts +1 -0
- package/dist/converters/utils.js +9 -0
- package/dist/core/createGenericHandler.d.ts +11 -0
- package/dist/core/createGenericHandler.js +16 -0
- package/dist/core/createMainHandler.d.ts +18 -0
- package/dist/core/createMainHandler.js +24 -0
- package/dist/core/edgeFunctionHandler.d.ts +54 -0
- package/dist/core/edgeFunctionHandler.js +21 -0
- package/dist/core/patchAsyncStorage.d.ts +1 -0
- package/dist/core/patchAsyncStorage.js +16 -0
- package/dist/core/requestHandler.d.ts +3 -0
- package/dist/core/requestHandler.js +162 -0
- package/dist/core/require-hooks.d.ts +3 -0
- package/dist/core/require-hooks.js +102 -0
- package/dist/core/resolve.d.ts +31 -0
- package/dist/core/resolve.js +81 -0
- package/dist/core/routing/cacheInterceptor.d.ts +2 -0
- package/dist/core/routing/cacheInterceptor.js +162 -0
- package/dist/core/routing/i18n/accept-header.d.ts +1 -0
- package/dist/core/routing/i18n/accept-header.js +101 -0
- package/dist/core/routing/i18n/index.d.ts +2 -0
- package/dist/core/routing/i18n/index.js +36 -0
- package/dist/core/routing/matcher.d.ts +25 -0
- package/dist/core/routing/matcher.js +294 -0
- package/dist/core/routing/middleware.d.ts +7 -0
- package/dist/core/routing/middleware.js +167 -0
- package/dist/core/routing/util.d.ts +105 -0
- package/dist/core/routing/util.js +415 -0
- package/dist/core/routingHandler.d.ts +8 -0
- package/dist/core/routingHandler.js +127 -0
- package/dist/core/util.d.ts +5 -0
- package/dist/core/util.js +84 -0
- package/dist/helpers/withCloudflare.d.ts +65 -0
- package/dist/helpers/withCloudflare.js +58 -0
- package/dist/helpers/withSST.d.ts +54 -0
- package/dist/helpers/withSST.js +28 -0
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +4 -0
- package/dist/http/openNextResponse.d.ts +78 -0
- package/dist/http/openNextResponse.js +281 -0
- package/dist/http/request.d.ts +12 -0
- package/dist/http/request.js +35 -0
- package/dist/http/util.d.ts +4 -0
- package/dist/http/util.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +37 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +13 -0
- package/dist/minimize-js.d.ts +1 -0
- package/dist/minimize-js.js +101 -0
- package/dist/overrides/imageLoader/host.d.ts +3 -0
- package/dist/overrides/imageLoader/host.js +29 -0
- package/dist/overrides/imageLoader/s3.d.ts +3 -0
- package/dist/overrides/imageLoader/s3.js +33 -0
- package/dist/plugins/edge.d.ts +15 -0
- package/dist/plugins/edge.js +157 -0
- package/dist/plugins/replacement.d.ts +42 -0
- package/dist/plugins/replacement.js +74 -0
- package/dist/plugins/resolve.d.ts +18 -0
- package/dist/plugins/resolve.js +45 -0
- package/dist/queue/sqs-lite.d.ts +3 -0
- package/dist/queue/sqs-lite.js +52 -0
- package/dist/queue/sqs.d.ts +3 -0
- package/dist/queue/sqs.js +20 -0
- package/dist/queue/types.d.ts +12 -0
- package/dist/queue/types.js +1 -0
- package/dist/types/aws-lambda.d.ts +22 -0
- package/dist/types/aws-lambda.js +1 -0
- package/dist/types/next-types.d.ts +155 -0
- package/dist/types/next-types.js +2 -0
- package/dist/types/open-next.d.ts +303 -0
- package/dist/types/open-next.js +1 -0
- package/dist/utils/error.d.ts +23 -0
- package/dist/utils/error.js +32 -0
- package/dist/utils/fetch.d.ts +2 -0
- package/dist/utils/fetch.js +23 -0
- package/dist/utils/promise.d.ts +18 -0
- package/dist/utils/promise.js +48 -0
- package/dist/utils/stream.d.ts +5 -0
- package/dist/utils/stream.js +30 -0
- package/dist/wrappers/aws-lambda-streaming.d.ts +7 -0
- package/dist/wrappers/aws-lambda-streaming.js +88 -0
- package/dist/wrappers/aws-lambda.d.ts +7 -0
- package/dist/wrappers/aws-lambda.js +39 -0
- package/dist/wrappers/cloudflare.d.ts +11 -0
- package/dist/wrappers/cloudflare.js +20 -0
- package/dist/wrappers/node.d.ts +7 -0
- package/dist/wrappers/node.js +57 -0
- package/package.json +66 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Meta } from "../next-types";
|
|
2
|
+
export type S3CachedFile = {
|
|
3
|
+
type: "redirect";
|
|
4
|
+
props?: Object;
|
|
5
|
+
meta?: Meta;
|
|
6
|
+
} | {
|
|
7
|
+
type: "page";
|
|
8
|
+
html: string;
|
|
9
|
+
json: Object;
|
|
10
|
+
meta?: Meta;
|
|
11
|
+
} | {
|
|
12
|
+
type: "app";
|
|
13
|
+
html: string;
|
|
14
|
+
rsc: string;
|
|
15
|
+
meta?: Meta;
|
|
16
|
+
} | {
|
|
17
|
+
type: "route";
|
|
18
|
+
body: string;
|
|
19
|
+
meta?: Meta;
|
|
20
|
+
};
|
|
21
|
+
export type S3FetchCache = Object;
|
|
22
|
+
export type WithLastModified<T> = {
|
|
23
|
+
lastModified?: number;
|
|
24
|
+
value?: T;
|
|
25
|
+
};
|
|
26
|
+
export type CacheValue<IsFetch extends boolean> = (IsFetch extends true ? S3FetchCache : S3CachedFile) & {
|
|
27
|
+
revalidate?: number | false;
|
|
28
|
+
};
|
|
29
|
+
export type IncrementalCache = {
|
|
30
|
+
get<IsFetch extends boolean = false>(key: string, isFetch?: IsFetch): Promise<WithLastModified<CacheValue<IsFetch>>>;
|
|
31
|
+
set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
|
|
32
|
+
delete(key: string): Promise<void>;
|
|
33
|
+
name: string;
|
|
34
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
interface CachedFetchValue {
|
|
3
|
+
kind: "FETCH";
|
|
4
|
+
data: {
|
|
5
|
+
headers: {
|
|
6
|
+
[k: string]: string;
|
|
7
|
+
};
|
|
8
|
+
body: string;
|
|
9
|
+
url: string;
|
|
10
|
+
status?: number;
|
|
11
|
+
tags?: string[];
|
|
12
|
+
};
|
|
13
|
+
revalidate: number;
|
|
14
|
+
}
|
|
15
|
+
interface CachedRedirectValue {
|
|
16
|
+
kind: "REDIRECT";
|
|
17
|
+
props: Object;
|
|
18
|
+
}
|
|
19
|
+
interface CachedRouteValue {
|
|
20
|
+
kind: "ROUTE";
|
|
21
|
+
body: Buffer;
|
|
22
|
+
status: number;
|
|
23
|
+
headers: Record<string, undefined | string | string[]>;
|
|
24
|
+
}
|
|
25
|
+
interface CachedImageValue {
|
|
26
|
+
kind: "IMAGE";
|
|
27
|
+
etag: string;
|
|
28
|
+
buffer: Buffer;
|
|
29
|
+
extension: string;
|
|
30
|
+
isMiss?: boolean;
|
|
31
|
+
isStale?: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface IncrementalCachedPageValue {
|
|
34
|
+
kind: "PAGE";
|
|
35
|
+
html: string;
|
|
36
|
+
pageData: Object;
|
|
37
|
+
status?: number;
|
|
38
|
+
headers?: Record<string, undefined | string>;
|
|
39
|
+
}
|
|
40
|
+
type IncrementalCacheValue = CachedRedirectValue | IncrementalCachedPageValue | CachedImageValue | CachedFetchValue | CachedRouteValue;
|
|
41
|
+
export interface CacheHandlerContext {
|
|
42
|
+
fs?: never;
|
|
43
|
+
dev?: boolean;
|
|
44
|
+
flushToDisk?: boolean;
|
|
45
|
+
serverDistDir?: string;
|
|
46
|
+
maxMemoryCacheSize?: number;
|
|
47
|
+
_appDir: boolean;
|
|
48
|
+
_requestHeaders: never;
|
|
49
|
+
fetchCacheKeyPrefix?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface CacheHandlerValue {
|
|
52
|
+
lastModified?: number;
|
|
53
|
+
age?: number;
|
|
54
|
+
cacheState?: string;
|
|
55
|
+
value: IncrementalCacheValue | null;
|
|
56
|
+
}
|
|
57
|
+
export type Extension = "cache" | "fetch";
|
|
58
|
+
export interface Meta {
|
|
59
|
+
status?: number;
|
|
60
|
+
headers?: Record<string, undefined | string | string[]>;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT = 25;
|
|
2
|
+
/**
|
|
3
|
+
* Sending to dynamo X commands at a time, using about X * 25 write units per batch to not overwhelm DDB
|
|
4
|
+
* and give production plenty of room to work with. With DDB Response times, you can expect about 10 batches per second.
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_DYNAMO_BATCH_WRITE_COMMAND_CONCURRENCY = 4;
|
|
7
|
+
export const getDynamoBatchWriteCommandConcurrency = () => {
|
|
8
|
+
const dynamoBatchWriteCommandConcurrencyFromEnv = process.env.DYNAMO_BATCH_WRITE_COMMAND_CONCURRENCY;
|
|
9
|
+
const parsedDynamoBatchWriteCommandConcurrencyFromEnv = dynamoBatchWriteCommandConcurrencyFromEnv
|
|
10
|
+
? parseInt(dynamoBatchWriteCommandConcurrencyFromEnv)
|
|
11
|
+
: undefined;
|
|
12
|
+
if (parsedDynamoBatchWriteCommandConcurrencyFromEnv &&
|
|
13
|
+
!isNaN(parsedDynamoBatchWriteCommandConcurrencyFromEnv)) {
|
|
14
|
+
return parsedDynamoBatchWriteCommandConcurrencyFromEnv;
|
|
15
|
+
}
|
|
16
|
+
return DEFAULT_DYNAMO_BATCH_WRITE_COMMAND_CONCURRENCY;
|
|
17
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
import { AwsClient } from "aws4fetch";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { RecoverableError } from "../../utils/error";
|
|
5
|
+
import { customFetchClient } from "../../utils/fetch";
|
|
6
|
+
import { debug, error } from "../../adapters/logger";
|
|
7
|
+
import { chunk, parseNumberFromEnv } from "../../adapters/util";
|
|
8
|
+
import { getDynamoBatchWriteCommandConcurrency, MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, } from "./constants";
|
|
9
|
+
let awsClient = null;
|
|
10
|
+
const getAwsClient = () => {
|
|
11
|
+
const { CACHE_BUCKET_REGION } = process.env;
|
|
12
|
+
if (awsClient) {
|
|
13
|
+
return awsClient;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
awsClient = new AwsClient({
|
|
17
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
18
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
19
|
+
sessionToken: process.env.AWS_SESSION_TOKEN,
|
|
20
|
+
region: CACHE_BUCKET_REGION,
|
|
21
|
+
retries: parseNumberFromEnv(process.env.AWS_SDK_S3_MAX_ATTEMPTS),
|
|
22
|
+
});
|
|
23
|
+
return awsClient;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const awsFetch = (body, type = "query") => {
|
|
27
|
+
const { CACHE_BUCKET_REGION } = process.env;
|
|
28
|
+
const client = getAwsClient();
|
|
29
|
+
return customFetchClient(client)(`https://dynamodb.${CACHE_BUCKET_REGION}.amazonaws.com`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/x-amz-json-1.0",
|
|
33
|
+
"X-Amz-Target": `DynamoDB_20120810.${type === "query" ? "Query" : "BatchWriteItem"}`,
|
|
34
|
+
},
|
|
35
|
+
body,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
function buildDynamoKey(key) {
|
|
39
|
+
const { NEXT_BUILD_ID } = process.env;
|
|
40
|
+
// FIXME: We should probably use something else than path.join here
|
|
41
|
+
// this could transform some fetch cache key into a valid path
|
|
42
|
+
return path.posix.join(NEXT_BUILD_ID ?? "", key);
|
|
43
|
+
}
|
|
44
|
+
function buildDynamoObject(path, tags, revalidatedAt) {
|
|
45
|
+
return {
|
|
46
|
+
path: { S: buildDynamoKey(path) },
|
|
47
|
+
tag: { S: buildDynamoKey(tags) },
|
|
48
|
+
revalidatedAt: { N: `${revalidatedAt ?? Date.now()}` },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const tagCache = {
|
|
52
|
+
async getByPath(path) {
|
|
53
|
+
try {
|
|
54
|
+
if (globalThis.disableDynamoDBCache)
|
|
55
|
+
return [];
|
|
56
|
+
const { CACHE_DYNAMO_TABLE, NEXT_BUILD_ID } = process.env;
|
|
57
|
+
const result = await awsFetch(JSON.stringify({
|
|
58
|
+
TableName: CACHE_DYNAMO_TABLE,
|
|
59
|
+
IndexName: "revalidate",
|
|
60
|
+
KeyConditionExpression: "#key = :key",
|
|
61
|
+
ExpressionAttributeNames: {
|
|
62
|
+
"#key": "path",
|
|
63
|
+
},
|
|
64
|
+
ExpressionAttributeValues: {
|
|
65
|
+
":key": { S: buildDynamoKey(path) },
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
if (result.status !== 200) {
|
|
69
|
+
throw new RecoverableError(`Failed to get tags by path: ${result.status}`);
|
|
70
|
+
}
|
|
71
|
+
const { Items } = (await result.json());
|
|
72
|
+
const tags = Items?.map((item) => item.tag.S ?? "") ?? [];
|
|
73
|
+
debug("tags for path", path, tags);
|
|
74
|
+
// We need to remove the buildId from the path
|
|
75
|
+
return tags.map((tag) => tag.replace(`${NEXT_BUILD_ID}/`, ""));
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
error("Failed to get tags by path", e);
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
async getByTag(tag) {
|
|
83
|
+
try {
|
|
84
|
+
if (globalThis.disableDynamoDBCache)
|
|
85
|
+
return [];
|
|
86
|
+
const { CACHE_DYNAMO_TABLE, NEXT_BUILD_ID } = process.env;
|
|
87
|
+
const result = await awsFetch(JSON.stringify({
|
|
88
|
+
TableName: CACHE_DYNAMO_TABLE,
|
|
89
|
+
KeyConditionExpression: "#tag = :tag",
|
|
90
|
+
ExpressionAttributeNames: {
|
|
91
|
+
"#tag": "tag",
|
|
92
|
+
},
|
|
93
|
+
ExpressionAttributeValues: {
|
|
94
|
+
":tag": { S: buildDynamoKey(tag) },
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
if (result.status !== 200) {
|
|
98
|
+
throw new RecoverableError(`Failed to get by tag: ${result.status}`);
|
|
99
|
+
}
|
|
100
|
+
const { Items } = (await result.json());
|
|
101
|
+
return (
|
|
102
|
+
// We need to remove the buildId from the path
|
|
103
|
+
Items?.map(({ path: { S: key } }) => key?.replace(`${NEXT_BUILD_ID}/`, "") ?? "") ?? []);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
error("Failed to get by tag", e);
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
async getLastModified(key, lastModified) {
|
|
111
|
+
try {
|
|
112
|
+
if (globalThis.disableDynamoDBCache)
|
|
113
|
+
return lastModified ?? Date.now();
|
|
114
|
+
const { CACHE_DYNAMO_TABLE } = process.env;
|
|
115
|
+
const result = await awsFetch(JSON.stringify({
|
|
116
|
+
TableName: CACHE_DYNAMO_TABLE,
|
|
117
|
+
IndexName: "revalidate",
|
|
118
|
+
KeyConditionExpression: "#key = :key AND #revalidatedAt > :lastModified",
|
|
119
|
+
ExpressionAttributeNames: {
|
|
120
|
+
"#key": "path",
|
|
121
|
+
"#revalidatedAt": "revalidatedAt",
|
|
122
|
+
},
|
|
123
|
+
ExpressionAttributeValues: {
|
|
124
|
+
":key": { S: buildDynamoKey(key) },
|
|
125
|
+
":lastModified": { N: String(lastModified ?? 0) },
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
if (result.status !== 200) {
|
|
129
|
+
throw new RecoverableError(`Failed to get last modified: ${result.status}`);
|
|
130
|
+
}
|
|
131
|
+
const revalidatedTags = (await result.json()).Items ?? [];
|
|
132
|
+
debug("revalidatedTags", revalidatedTags);
|
|
133
|
+
// If we have revalidated tags we return -1 to force revalidation
|
|
134
|
+
return revalidatedTags.length > 0 ? -1 : lastModified ?? Date.now();
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
error("Failed to get revalidated tags", e);
|
|
138
|
+
return lastModified ?? Date.now();
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async writeTags(tags) {
|
|
142
|
+
try {
|
|
143
|
+
const { CACHE_DYNAMO_TABLE } = process.env;
|
|
144
|
+
if (globalThis.disableDynamoDBCache)
|
|
145
|
+
return;
|
|
146
|
+
const dataChunks = chunk(tags, MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT).map((Items) => ({
|
|
147
|
+
RequestItems: {
|
|
148
|
+
[CACHE_DYNAMO_TABLE ?? ""]: Items.map((Item) => ({
|
|
149
|
+
PutRequest: {
|
|
150
|
+
Item: {
|
|
151
|
+
...buildDynamoObject(Item.path, Item.tag, Item.revalidatedAt),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
})),
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
const toInsert = chunk(dataChunks, getDynamoBatchWriteCommandConcurrency());
|
|
158
|
+
for (const paramsChunk of toInsert) {
|
|
159
|
+
await Promise.all(paramsChunk.map(async (params) => {
|
|
160
|
+
const response = await awsFetch(JSON.stringify(params), "batchWrite");
|
|
161
|
+
if (response.status !== 200) {
|
|
162
|
+
throw new RecoverableError(`Failed to batch write dynamo item: ${response.status}`);
|
|
163
|
+
}
|
|
164
|
+
return response;
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
error("Failed to batch write dynamo item", e);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
name: "dynamoDb",
|
|
173
|
+
};
|
|
174
|
+
export default tagCache;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { BatchWriteItemCommand, DynamoDBClient, QueryCommand, } from "@aws-sdk/client-dynamodb";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { awsLogger, debug, error } from "../../adapters/logger";
|
|
4
|
+
import { chunk, parseNumberFromEnv } from "../../adapters/util";
|
|
5
|
+
import { getDynamoBatchWriteCommandConcurrency, MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, } from "./constants";
|
|
6
|
+
const { CACHE_BUCKET_REGION, CACHE_DYNAMO_TABLE, NEXT_BUILD_ID } = process.env;
|
|
7
|
+
function parseDynamoClientConfigFromEnv() {
|
|
8
|
+
return {
|
|
9
|
+
region: CACHE_BUCKET_REGION,
|
|
10
|
+
logger: awsLogger,
|
|
11
|
+
maxAttempts: parseNumberFromEnv(process.env.AWS_SDK_DYNAMODB_MAX_ATTEMPTS),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const dynamoClient = new DynamoDBClient(parseDynamoClientConfigFromEnv());
|
|
15
|
+
function buildDynamoKey(key) {
|
|
16
|
+
// FIXME: We should probably use something else than path.join here
|
|
17
|
+
// this could transform some fetch cache key into a valid path
|
|
18
|
+
return path.posix.join(NEXT_BUILD_ID ?? "", key);
|
|
19
|
+
}
|
|
20
|
+
function buildDynamoObject(path, tags, revalidatedAt) {
|
|
21
|
+
return {
|
|
22
|
+
path: { S: buildDynamoKey(path) },
|
|
23
|
+
tag: { S: buildDynamoKey(tags) },
|
|
24
|
+
revalidatedAt: { N: `${revalidatedAt ?? Date.now()}` },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const tagCache = {
|
|
28
|
+
async getByPath(path) {
|
|
29
|
+
try {
|
|
30
|
+
if (globalThis.disableDynamoDBCache)
|
|
31
|
+
return [];
|
|
32
|
+
const result = await dynamoClient.send(new QueryCommand({
|
|
33
|
+
TableName: CACHE_DYNAMO_TABLE,
|
|
34
|
+
IndexName: "revalidate",
|
|
35
|
+
KeyConditionExpression: "#key = :key",
|
|
36
|
+
ExpressionAttributeNames: {
|
|
37
|
+
"#key": "path",
|
|
38
|
+
},
|
|
39
|
+
ExpressionAttributeValues: {
|
|
40
|
+
":key": { S: buildDynamoKey(path) },
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
const tags = result.Items?.map((item) => item.tag.S ?? "") ?? [];
|
|
44
|
+
debug("tags for path", path, tags);
|
|
45
|
+
// We need to remove the buildId from the path
|
|
46
|
+
return tags.map((tag) => tag.replace(`${NEXT_BUILD_ID}/`, ""));
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
error("Failed to get tags by path", e);
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
async getByTag(tag) {
|
|
54
|
+
try {
|
|
55
|
+
if (globalThis.disableDynamoDBCache)
|
|
56
|
+
return [];
|
|
57
|
+
const { Items } = await dynamoClient.send(new QueryCommand({
|
|
58
|
+
TableName: CACHE_DYNAMO_TABLE,
|
|
59
|
+
KeyConditionExpression: "#tag = :tag",
|
|
60
|
+
ExpressionAttributeNames: {
|
|
61
|
+
"#tag": "tag",
|
|
62
|
+
},
|
|
63
|
+
ExpressionAttributeValues: {
|
|
64
|
+
":tag": { S: buildDynamoKey(tag) },
|
|
65
|
+
},
|
|
66
|
+
}));
|
|
67
|
+
return (
|
|
68
|
+
// We need to remove the buildId from the path
|
|
69
|
+
Items?.map(({ path: { S: key } }) => key?.replace(`${NEXT_BUILD_ID}/`, "") ?? "") ?? []);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
error("Failed to get by tag", e);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async getLastModified(key, lastModified) {
|
|
77
|
+
try {
|
|
78
|
+
if (globalThis.disableDynamoDBCache)
|
|
79
|
+
return lastModified ?? Date.now();
|
|
80
|
+
const result = await dynamoClient.send(new QueryCommand({
|
|
81
|
+
TableName: CACHE_DYNAMO_TABLE,
|
|
82
|
+
IndexName: "revalidate",
|
|
83
|
+
KeyConditionExpression: "#key = :key AND #revalidatedAt > :lastModified",
|
|
84
|
+
ExpressionAttributeNames: {
|
|
85
|
+
"#key": "path",
|
|
86
|
+
"#revalidatedAt": "revalidatedAt",
|
|
87
|
+
},
|
|
88
|
+
ExpressionAttributeValues: {
|
|
89
|
+
":key": { S: buildDynamoKey(key) },
|
|
90
|
+
":lastModified": { N: String(lastModified ?? 0) },
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
const revalidatedTags = result.Items ?? [];
|
|
94
|
+
debug("revalidatedTags", revalidatedTags);
|
|
95
|
+
// If we have revalidated tags we return -1 to force revalidation
|
|
96
|
+
return revalidatedTags.length > 0 ? -1 : lastModified ?? Date.now();
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
error("Failed to get revalidated tags", e);
|
|
100
|
+
return lastModified ?? Date.now();
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async writeTags(tags) {
|
|
104
|
+
try {
|
|
105
|
+
if (globalThis.disableDynamoDBCache)
|
|
106
|
+
return;
|
|
107
|
+
const dataChunks = chunk(tags, MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT).map((Items) => ({
|
|
108
|
+
RequestItems: {
|
|
109
|
+
[CACHE_DYNAMO_TABLE ?? ""]: Items.map((Item) => ({
|
|
110
|
+
PutRequest: {
|
|
111
|
+
Item: {
|
|
112
|
+
...buildDynamoObject(Item.path, Item.tag, Item.revalidatedAt),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
})),
|
|
116
|
+
},
|
|
117
|
+
}));
|
|
118
|
+
const toInsert = chunk(dataChunks, getDynamoBatchWriteCommandConcurrency());
|
|
119
|
+
for (const paramsChunk of toInsert) {
|
|
120
|
+
await Promise.all(paramsChunk.map(async (params) => dynamoClient.send(new BatchWriteItemCommand(params))));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
error("Failed to batch write dynamo item", e);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
name: "dynamoDb",
|
|
128
|
+
};
|
|
129
|
+
export default tagCache;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type TagCache = {
|
|
2
|
+
getByTag(tag: string): Promise<string[]>;
|
|
3
|
+
getByPath(path: string): Promise<string[]>;
|
|
4
|
+
getLastModified(path: string, lastModified?: number): Promise<number>;
|
|
5
|
+
writeTags(tags: {
|
|
6
|
+
tag: string;
|
|
7
|
+
path: string;
|
|
8
|
+
revalidatedAt?: number;
|
|
9
|
+
}[]): Promise<void>;
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { fromReadableStream } from "../utils/stream";
|
|
2
|
+
import { debug } from "../adapters/logger";
|
|
3
|
+
import { removeUndefinedFromQuery } from "./utils";
|
|
4
|
+
function normalizeAPIGatewayProxyEventHeaders(event) {
|
|
5
|
+
event.multiValueHeaders;
|
|
6
|
+
const headers = {};
|
|
7
|
+
for (const [key, values] of Object.entries(event.multiValueHeaders || {})) {
|
|
8
|
+
if (values) {
|
|
9
|
+
headers[key.toLowerCase()] = values.join(",");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
for (const [key, value] of Object.entries(event.headers || {})) {
|
|
13
|
+
if (value) {
|
|
14
|
+
headers[key.toLowerCase()] = value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return headers;
|
|
18
|
+
}
|
|
19
|
+
function normalizeAPIGatewayProxyEventQueryParams(event) {
|
|
20
|
+
// Note that the same query string values are returned in both
|
|
21
|
+
// "multiValueQueryStringParameters" and "queryStringParameters".
|
|
22
|
+
// We only need to use one of them.
|
|
23
|
+
// For example:
|
|
24
|
+
// "?name=foo" appears in the event object as
|
|
25
|
+
// {
|
|
26
|
+
// ...
|
|
27
|
+
// queryStringParameters: { name: 'foo' },
|
|
28
|
+
// multiValueQueryStringParameters: { name: [ 'foo' ] },
|
|
29
|
+
// ...
|
|
30
|
+
// }
|
|
31
|
+
const params = new URLSearchParams();
|
|
32
|
+
for (const [key, value] of Object.entries(event.multiValueQueryStringParameters || {})) {
|
|
33
|
+
if (value !== undefined) {
|
|
34
|
+
for (const v of value) {
|
|
35
|
+
params.append(key, v);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const value = params.toString();
|
|
40
|
+
return value ? `?${value}` : "";
|
|
41
|
+
}
|
|
42
|
+
async function convertFromAPIGatewayProxyEvent(event) {
|
|
43
|
+
const { path, body, httpMethod, requestContext, isBase64Encoded } = event;
|
|
44
|
+
return {
|
|
45
|
+
type: "core",
|
|
46
|
+
method: httpMethod,
|
|
47
|
+
rawPath: path,
|
|
48
|
+
url: path + normalizeAPIGatewayProxyEventQueryParams(event),
|
|
49
|
+
body: Buffer.from(body ?? "", isBase64Encoded ? "base64" : "utf8"),
|
|
50
|
+
headers: normalizeAPIGatewayProxyEventHeaders(event),
|
|
51
|
+
remoteAddress: requestContext.identity.sourceIp,
|
|
52
|
+
query: removeUndefinedFromQuery(event.multiValueQueryStringParameters ?? {}),
|
|
53
|
+
cookies: event.multiValueHeaders?.cookie?.reduce((acc, cur) => {
|
|
54
|
+
const [key, value] = cur.split("=");
|
|
55
|
+
return { ...acc, [key]: value };
|
|
56
|
+
}, {}) ?? {},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function convertToApiGatewayProxyResult(result) {
|
|
60
|
+
const headers = {};
|
|
61
|
+
const multiValueHeaders = {};
|
|
62
|
+
Object.entries(result.headers).forEach(([key, value]) => {
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
multiValueHeaders[key] = value;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (value === null) {
|
|
68
|
+
headers[key] = "";
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
headers[key] = value;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const body = await fromReadableStream(result.body, result.isBase64Encoded);
|
|
75
|
+
const response = {
|
|
76
|
+
statusCode: result.statusCode,
|
|
77
|
+
headers,
|
|
78
|
+
body,
|
|
79
|
+
isBase64Encoded: result.isBase64Encoded,
|
|
80
|
+
multiValueHeaders,
|
|
81
|
+
};
|
|
82
|
+
debug(response);
|
|
83
|
+
return response;
|
|
84
|
+
}
|
|
85
|
+
export default {
|
|
86
|
+
convertFrom: convertFromAPIGatewayProxyEvent,
|
|
87
|
+
convertTo: convertToApiGatewayProxyResult,
|
|
88
|
+
name: "aws-apigw-v1",
|
|
89
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { parseCookies } from "../http/util";
|
|
2
|
+
import { fromReadableStream } from "../utils/stream";
|
|
3
|
+
import { debug } from "../adapters/logger";
|
|
4
|
+
import { convertToQuery } from "../core/routing/util";
|
|
5
|
+
import { removeUndefinedFromQuery } from "./utils";
|
|
6
|
+
// Not sure which one is reallly needed as this is not documented anywhere but server actions redirect are not working without this, it causes a 500 error from cloudfront itself with a 'x-amzErrortype: InternalFailure' header
|
|
7
|
+
const CloudFrontBlacklistedHeaders = [
|
|
8
|
+
"connection",
|
|
9
|
+
"expect",
|
|
10
|
+
"keep-alive",
|
|
11
|
+
"proxy-authenticate",
|
|
12
|
+
"proxy-authorization",
|
|
13
|
+
"proxy-connection",
|
|
14
|
+
"trailer",
|
|
15
|
+
"upgrade",
|
|
16
|
+
"x-accel-buffering",
|
|
17
|
+
"x-accel-charset",
|
|
18
|
+
"x-accel-limit-rate",
|
|
19
|
+
"x-accel-redirect",
|
|
20
|
+
/x-amz-cf-(.*)/,
|
|
21
|
+
/x-amzn-(.*)/,
|
|
22
|
+
/x-edge-(.*)/,
|
|
23
|
+
"x-cache",
|
|
24
|
+
"x-forwarded-proto",
|
|
25
|
+
"x-real-ip",
|
|
26
|
+
"set-cookie",
|
|
27
|
+
"age",
|
|
28
|
+
"via",
|
|
29
|
+
];
|
|
30
|
+
function normalizeAPIGatewayProxyEventV2Body(event) {
|
|
31
|
+
const { body, isBase64Encoded } = event;
|
|
32
|
+
if (Buffer.isBuffer(body)) {
|
|
33
|
+
return body;
|
|
34
|
+
}
|
|
35
|
+
else if (typeof body === "string") {
|
|
36
|
+
return Buffer.from(body, isBase64Encoded ? "base64" : "utf8");
|
|
37
|
+
}
|
|
38
|
+
else if (typeof body === "object") {
|
|
39
|
+
return Buffer.from(JSON.stringify(body));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return Buffer.from("", "utf8");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function normalizeAPIGatewayProxyEventV2Headers(event) {
|
|
46
|
+
const { headers: rawHeaders, cookies } = event;
|
|
47
|
+
const headers = {};
|
|
48
|
+
if (Array.isArray(cookies)) {
|
|
49
|
+
headers["cookie"] = cookies.join("; ");
|
|
50
|
+
}
|
|
51
|
+
for (const [key, value] of Object.entries(rawHeaders || {})) {
|
|
52
|
+
headers[key.toLowerCase()] = value;
|
|
53
|
+
}
|
|
54
|
+
return headers;
|
|
55
|
+
}
|
|
56
|
+
async function convertFromAPIGatewayProxyEventV2(event) {
|
|
57
|
+
const { rawPath, rawQueryString, requestContext } = event;
|
|
58
|
+
return {
|
|
59
|
+
type: "core",
|
|
60
|
+
method: requestContext.http.method,
|
|
61
|
+
rawPath,
|
|
62
|
+
url: rawPath + (rawQueryString ? `?${rawQueryString}` : ""),
|
|
63
|
+
body: normalizeAPIGatewayProxyEventV2Body(event),
|
|
64
|
+
headers: normalizeAPIGatewayProxyEventV2Headers(event),
|
|
65
|
+
remoteAddress: requestContext.http.sourceIp,
|
|
66
|
+
query: removeUndefinedFromQuery(convertToQuery(rawQueryString)),
|
|
67
|
+
cookies: event.cookies?.reduce((acc, cur) => {
|
|
68
|
+
const [key, value] = cur.split("=");
|
|
69
|
+
return { ...acc, [key]: value };
|
|
70
|
+
}, {}) ?? {},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function convertToApiGatewayProxyResultV2(result) {
|
|
74
|
+
const headers = {};
|
|
75
|
+
Object.entries(result.headers)
|
|
76
|
+
.filter(([key]) => !CloudFrontBlacklistedHeaders.some((header) => typeof header === "string" ? header === key : header.test(key)))
|
|
77
|
+
.forEach(([key, value]) => {
|
|
78
|
+
if (value === null || value === undefined) {
|
|
79
|
+
headers[key] = "";
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
headers[key] = Array.isArray(value) ? value.join(", ") : `${value}`;
|
|
83
|
+
});
|
|
84
|
+
const body = await fromReadableStream(result.body, result.isBase64Encoded);
|
|
85
|
+
const response = {
|
|
86
|
+
statusCode: result.statusCode,
|
|
87
|
+
headers,
|
|
88
|
+
cookies: parseCookies(result.headers["set-cookie"]),
|
|
89
|
+
body,
|
|
90
|
+
isBase64Encoded: result.isBase64Encoded,
|
|
91
|
+
};
|
|
92
|
+
debug(response);
|
|
93
|
+
return response;
|
|
94
|
+
}
|
|
95
|
+
export default {
|
|
96
|
+
convertFrom: convertFromAPIGatewayProxyEventV2,
|
|
97
|
+
convertTo: convertToApiGatewayProxyResultV2,
|
|
98
|
+
name: "aws-apigw-v2",
|
|
99
|
+
};
|