cdk-nuxt 2.10.5 → 2.10.8
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/README.md +1 -1
- package/lib/functions/access-logs-analysis/group-by-date/build/app/index.d.ts +2 -0
- package/lib/functions/access-logs-analysis/group-by-date/build/app/index.js +57 -0
- package/lib/functions/access-logs-analysis/group-by-date/build/app/index.js.map +1 -0
- package/lib/functions/access-logs-analysis/group-by-date/build/layer/nodejs/node_modules/.pnpm/lock.yaml +9 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/create-partition.d.ts +1 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/create-partition.js +57 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/create-partition.js.map +1 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/transform-partition.d.ts +2 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/transform-partition.js +72 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/transform-partition.js.map +1 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/types.d.ts +7 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/types.js +3 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/types.js.map +1 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/util.d.ts +9 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/util.js +44 -0
- package/lib/functions/access-logs-analysis/partitioning/build/app/util.js.map +1 -0
- package/lib/functions/access-logs-analysis/partitioning/build/layer/nodejs/node_modules/.pnpm/lock.yaml +9 -0
- package/lib/functions/assets-cleanup/build/app/index.d.ts +1 -0
- package/lib/functions/assets-cleanup/build/app/index.js +114 -0
- package/lib/functions/assets-cleanup/build/app/index.js.map +1 -0
- package/lib/functions/assets-cleanup/build/layer/nodejs/node_modules/.pnpm/lock.yaml +9 -0
- package/package.json +49 -51
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AWS CDK Nuxt Deployment Stack (Nuxt 3 & Nuxt 4)
|
|
2
2
|
|
|
3
3
|
<p>
|
|
4
|
-
<a href="https://github.com/ferdinandfrank/cdk-nuxt/actions/workflows/publish
|
|
4
|
+
<a href="https://github.com/ferdinandfrank/cdk-nuxt/actions/workflows/publish.yml"><img alt="Build" src="https://img.shields.io/github/actions/workflow/status/ferdinandfrank/cdk-nuxt/publish.yml?logo=github" /></a>
|
|
5
5
|
<a href="https://www.npmjs.com/package/cdk-nuxt"><img alt="Version" src="https://img.shields.io/npm/v/cdk-nuxt.svg" /></a>
|
|
6
6
|
<a href="https://www.npmjs.com/package/cdk-nuxt"><img alt="Downloads" src="https://img.shields.io/npm/dm/cdk-nuxt.svg"></a>
|
|
7
7
|
<a href="https://www.npmjs.com/package/cdk-nuxt"><img alt="License" src="https://img.shields.io/npm/l/cdk-nuxt.svg" /></a>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handler = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* This script moves access logs into a target folder hierarchy, structured by year, month, day, and hour (UTC).
|
|
6
|
+
* That way, the logs can easier be processed with Athena.
|
|
7
|
+
* Taken and adjusted from https://github.com/aws-samples/amazon-cloudfront-access-logs-queries.
|
|
8
|
+
* This Lambda supports access logs from both: CloudFront and S3.
|
|
9
|
+
*/
|
|
10
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
11
|
+
// without leading and trailing slashes
|
|
12
|
+
const targetFolder = process.env.TARGET_FOLDER;
|
|
13
|
+
if (!targetFolder) {
|
|
14
|
+
throw new Error('Required environment variable TARGET_FOLDER missing!');
|
|
15
|
+
}
|
|
16
|
+
if (!process.env.RAW_ACCESS_LOG_FILE_PATTERN) {
|
|
17
|
+
throw new Error('Required environment variable RAW_ACCESS_LOG_FILE_PATTERN missing!');
|
|
18
|
+
}
|
|
19
|
+
// unfortunately, CloudFront logs and S3 logs are completely different. That's why the pattern to identify
|
|
20
|
+
// unprocessed log files need to be configurable
|
|
21
|
+
const rawAccessLogFilePattern = new RegExp(process.env.RAW_ACCESS_LOG_FILE_PATTERN);
|
|
22
|
+
// matches for everything after the last slash
|
|
23
|
+
const filenamePattern = /[^/]+$/;
|
|
24
|
+
const s3Client = new client_s3_1.S3Client({ region: process.env.AWS_REGION });
|
|
25
|
+
const internalHandler = async (event) => {
|
|
26
|
+
try {
|
|
27
|
+
const pendingMoves = event.Records.map(async (record) => {
|
|
28
|
+
const bucket = record.s3.bucket.name;
|
|
29
|
+
const sourceKey = record.s3.object.key;
|
|
30
|
+
const match = rawAccessLogFilePattern.exec(sourceKey);
|
|
31
|
+
// skip other files/folder
|
|
32
|
+
if (match?.groups) {
|
|
33
|
+
const { year, month, day, hour } = match.groups;
|
|
34
|
+
const filename = filenamePattern.exec(sourceKey)[0];
|
|
35
|
+
const targetKey = `${targetFolder}/year=${year}/month=${month}/day=${day}/hour=${hour}/${filename}`;
|
|
36
|
+
const copyCommand = new client_s3_1.CopyObjectCommand({
|
|
37
|
+
Bucket: bucket,
|
|
38
|
+
Key: targetKey,
|
|
39
|
+
CopySource: bucket + '/' + sourceKey,
|
|
40
|
+
});
|
|
41
|
+
await s3Client.send(copyCommand);
|
|
42
|
+
const deleteSourceCommand = new client_s3_1.DeleteObjectCommand({
|
|
43
|
+
Bucket: bucket,
|
|
44
|
+
Key: sourceKey,
|
|
45
|
+
});
|
|
46
|
+
await s3Client.send(deleteSourceCommand);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const processed = await Promise.all(pendingMoves);
|
|
50
|
+
console.log(`successfully moved ${processed.length} files`);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error('### unexpected runtime error ###', error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
exports.handler = internalHandler;
|
|
57
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"/","sources":["index.ts"],"names":[],"mappings":";;;AAAA;;;;;GAKG;AACH,kDAAoF;AAGpF,uCAAuC;AACvC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;AAC/C,IAAI,CAAC,YAAY,EAAE,CAAC;IAClB,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;AAC1E,CAAC;AACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,CAAC;IAC7C,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;AACxF,CAAC;AACD,0GAA0G;AAC1G,gDAAgD;AAChD,MAAM,uBAAuB,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;AAEpF,8CAA8C;AAC9C,MAAM,eAAe,GAAG,QAAQ,CAAC;AAEjC,MAAM,QAAQ,GAAG,IAAI,oBAAQ,CAAC,EAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,EAAC,CAAC,CAAC;AAEhE,MAAM,eAAe,GAAG,KAAK,EAAE,KAAc,EAAE,EAAE;IAC/C,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAC,MAAM,EAAC,EAAE;YACpD,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;YACrC,MAAM,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC;YACvC,MAAM,KAAK,GAAG,uBAAuB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAEtD,0BAA0B;YAC1B,IAAI,KAAK,EAAE,MAAM,EAAE,CAAC;gBAClB,MAAM,EAAC,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAC,GAAG,KAAK,CAAC,MAAM,CAAC;gBAC9C,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,SAAS,CAAE,CAAC,CAAC,CAAC,CAAC;gBACrD,MAAM,SAAS,GAAG,GAAG,YAAY,SAAS,IAAI,UAAU,KAAK,QAAQ,GAAG,SAAS,IAAI,IAAI,QAAQ,EAAE,CAAC;gBAEpG,MAAM,WAAW,GAAG,IAAI,6BAAiB,CAAC;oBACxC,MAAM,EAAE,MAAM;oBACd,GAAG,EAAE,SAAS;oBACd,UAAU,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;iBACrC,CAAC,CAAC;gBAEH,MAAM,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;gBAEjC,MAAM,mBAAmB,GAAG,IAAI,+BAAmB,CAAC;oBAClD,MAAM,EAAE,MAAM;oBACd,GAAG,EAAE,SAAS;iBACf,CAAC,CAAC;gBAEH,MAAM,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,sBAAsB,SAAS,CAAC,MAAM,QAAQ,CAAC,CAAC;IAC9D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC,CAAC;AAEW,QAAA,OAAO,GAAG,eAAe,CAAC","sourcesContent":["/**\n * This script moves access logs into a target folder hierarchy, structured by year, month, day, and hour (UTC).\n * That way, the logs can easier be processed with Athena.\n * Taken and adjusted from https://github.com/aws-samples/amazon-cloudfront-access-logs-queries.\n * This Lambda supports access logs from both: CloudFront and S3.\n */\nimport {S3Client, CopyObjectCommand, DeleteObjectCommand} from '@aws-sdk/client-s3';\nimport type {S3Event} from 'aws-lambda';\n\n// without leading and trailing slashes\nconst targetFolder = process.env.TARGET_FOLDER;\nif (!targetFolder) {\n throw new Error('Required environment variable TARGET_FOLDER missing!');\n}\nif (!process.env.RAW_ACCESS_LOG_FILE_PATTERN) {\n throw new Error('Required environment variable RAW_ACCESS_LOG_FILE_PATTERN missing!');\n}\n// unfortunately, CloudFront logs and S3 logs are completely different. That's why the pattern to identify\n// unprocessed log files need to be configurable\nconst rawAccessLogFilePattern = new RegExp(process.env.RAW_ACCESS_LOG_FILE_PATTERN);\n\n// matches for everything after the last slash\nconst filenamePattern = /[^/]+$/;\n\nconst s3Client = new S3Client({region: process.env.AWS_REGION});\n\nconst internalHandler = async (event: S3Event) => {\n try {\n const pendingMoves = event.Records.map(async record => {\n const bucket = record.s3.bucket.name;\n const sourceKey = record.s3.object.key;\n const match = rawAccessLogFilePattern.exec(sourceKey);\n\n // skip other files/folder\n if (match?.groups) {\n const {year, month, day, hour} = match.groups;\n const filename = filenamePattern.exec(sourceKey)![0];\n const targetKey = `${targetFolder}/year=${year}/month=${month}/day=${day}/hour=${hour}/${filename}`;\n\n const copyCommand = new CopyObjectCommand({\n Bucket: bucket,\n Key: targetKey,\n CopySource: bucket + '/' + sourceKey,\n });\n\n await s3Client.send(copyCommand);\n\n const deleteSourceCommand = new DeleteObjectCommand({\n Bucket: bucket,\n Key: sourceKey,\n });\n\n await s3Client.send(deleteSourceCommand);\n }\n });\n\n const processed = await Promise.all(pendingMoves);\n console.log(`successfully moved ${processed.length} files`);\n } catch (error) {\n console.error('### unexpected runtime error ###', error);\n }\n};\n\nexport const handler = internalHandler;\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const handler: () => Promise<void>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handler = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* This script creates a partition for the upcoming hour.
|
|
6
|
+
* Taken and adjusted from https://github.com/aws-samples/amazon-cloudfront-access-logs-queries/blob/mainline/functions/createPartitions.js.
|
|
7
|
+
*/
|
|
8
|
+
// noinspection DuplicatedCode
|
|
9
|
+
const client_athena_1 = require("@aws-sdk/client-athena");
|
|
10
|
+
const util_1 = require("./util");
|
|
11
|
+
const workgroup = process.env.WORKGROUP;
|
|
12
|
+
if (!workgroup) {
|
|
13
|
+
throw new Error('Required environment variable WORKGROUP missing!');
|
|
14
|
+
}
|
|
15
|
+
const database = process.env.DATABASE;
|
|
16
|
+
if (!database) {
|
|
17
|
+
throw new Error('Required environment variable DATABASE missing!');
|
|
18
|
+
}
|
|
19
|
+
const table = process.env.TABLE;
|
|
20
|
+
if (!table) {
|
|
21
|
+
throw new Error('Required environment variable TABLE missing!');
|
|
22
|
+
}
|
|
23
|
+
const internalHandler = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const nextHour = new Date(Date.now() + 60 * 60 * 1000);
|
|
26
|
+
const year = nextHour.getUTCFullYear();
|
|
27
|
+
const month = (nextHour.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
28
|
+
const day = nextHour
|
|
29
|
+
.getUTCDate()
|
|
30
|
+
.toString()
|
|
31
|
+
.padStart(2, '0');
|
|
32
|
+
const hour = nextHour
|
|
33
|
+
.getUTCHours()
|
|
34
|
+
.toString()
|
|
35
|
+
.padStart(2, '0');
|
|
36
|
+
console.log('creating partition', { year, month, day, hour });
|
|
37
|
+
const command = new client_athena_1.StartQueryExecutionCommand({
|
|
38
|
+
QueryString: `
|
|
39
|
+
ALTER TABLE ${database}.${table}
|
|
40
|
+
ADD IF NOT EXISTS
|
|
41
|
+
PARTITION (
|
|
42
|
+
year = '${year}',
|
|
43
|
+
month = '${month}',
|
|
44
|
+
day = '${day}',
|
|
45
|
+
hour = '${hour}' );`,
|
|
46
|
+
WorkGroup: workgroup,
|
|
47
|
+
QueryExecutionContext: { Database: database },
|
|
48
|
+
});
|
|
49
|
+
await (0, util_1.executeAndAwaitResponse)(command, true);
|
|
50
|
+
console.log('partition successfully created');
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error('### unexpected runtime error ###', error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
exports.handler = internalHandler;
|
|
57
|
+
//# sourceMappingURL=create-partition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-partition.js","sourceRoot":"/","sources":["create-partition.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACH,8BAA8B;AAC9B,0DAAkE;AAClE,iCAA+C;AAE/C,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;AACxC,IAAI,CAAC,SAAS,EAAE,CAAC;IACf,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;AACtE,CAAC;AACD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;AACtC,IAAI,CAAC,QAAQ,EAAE,CAAC;IACd,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;AACrE,CAAC;AACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;AAChC,IAAI,CAAC,KAAK,EAAE,CAAC;IACX,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,eAAe,GAAG,KAAK,IAAI,EAAE;IACjC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACvE,MAAM,GAAG,GAAG,QAAQ;aACjB,UAAU,EAAE;aACZ,QAAQ,EAAE;aACV,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACpB,MAAM,IAAI,GAAG,QAAQ;aAClB,WAAW,EAAE;aACb,QAAQ,EAAE;aACV,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAEpB,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAC,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAC,CAAC,CAAC;QAE5D,MAAM,OAAO,GAAG,IAAI,0CAA0B,CAAC;YAC7C,WAAW,EAAE;sBACG,QAAQ,IAAI,KAAK;;;sBAGjB,IAAI;uBACH,KAAK;qBACP,GAAG;sBACF,IAAI,MAAM;YAC1B,SAAS,EAAE,SAAS;YACpB,qBAAqB,EAAE,EAAC,QAAQ,EAAE,QAAQ,EAAC;SAC5C,CAAC,CAAC;QAEH,MAAM,IAAA,8BAAuB,EAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC7C,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;IAChD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC,CAAC;AAEW,QAAA,OAAO,GAAG,eAAe,CAAC","sourcesContent":["/**\n * This script creates a partition for the upcoming hour.\n * Taken and adjusted from https://github.com/aws-samples/amazon-cloudfront-access-logs-queries/blob/mainline/functions/createPartitions.js.\n */\n// noinspection DuplicatedCode\nimport {StartQueryExecutionCommand} from '@aws-sdk/client-athena';\nimport {executeAndAwaitResponse} from './util';\n\nconst workgroup = process.env.WORKGROUP;\nif (!workgroup) {\n throw new Error('Required environment variable WORKGROUP missing!');\n}\nconst database = process.env.DATABASE;\nif (!database) {\n throw new Error('Required environment variable DATABASE missing!');\n}\nconst table = process.env.TABLE;\nif (!table) {\n throw new Error('Required environment variable TABLE missing!');\n}\n\nconst internalHandler = async () => {\n try {\n const nextHour = new Date(Date.now() + 60 * 60 * 1000);\n const year = nextHour.getUTCFullYear();\n const month = (nextHour.getUTCMonth() + 1).toString().padStart(2, '0');\n const day = nextHour\n .getUTCDate()\n .toString()\n .padStart(2, '0');\n const hour = nextHour\n .getUTCHours()\n .toString()\n .padStart(2, '0');\n\n console.log('creating partition', {year, month, day, hour});\n\n const command = new StartQueryExecutionCommand({\n QueryString: `\n ALTER TABLE ${database}.${table}\n ADD IF NOT EXISTS\n PARTITION (\n year = '${year}',\n month = '${month}',\n day = '${day}',\n hour = '${hour}' );`,\n WorkGroup: workgroup,\n QueryExecutionContext: {Database: database},\n });\n\n await executeAndAwaitResponse(command, true);\n console.log('partition successfully created');\n } catch (error) {\n console.error('### unexpected runtime error ###', error);\n }\n};\n\nexport const handler = internalHandler;\n"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handler = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* This script transforms the access log partition of two hours ago into the Apache parquet format.
|
|
6
|
+
* Taken and adjusted from https://github.com/aws-samples/amazon-cloudfront-access-logs-queries/blob/mainline/functions/transformPartition.js.
|
|
7
|
+
*/
|
|
8
|
+
const client_athena_1 = require("@aws-sdk/client-athena");
|
|
9
|
+
const util_1 = require("./util");
|
|
10
|
+
const workgroup = process.env.WORKGROUP;
|
|
11
|
+
if (!workgroup) {
|
|
12
|
+
throw new Error('Required environment variable WORKGROUP missing!');
|
|
13
|
+
}
|
|
14
|
+
const database = process.env.DATABASE;
|
|
15
|
+
if (!database) {
|
|
16
|
+
throw new Error('Required environment variable DATABASE missing!');
|
|
17
|
+
}
|
|
18
|
+
const sourceTable = process.env.SOURCE_TABLE;
|
|
19
|
+
if (!sourceTable) {
|
|
20
|
+
throw new Error('Required environment variable SOURCE_TABLE missing!');
|
|
21
|
+
}
|
|
22
|
+
const targetTable = process.env.TARGET_TABLE;
|
|
23
|
+
if (!targetTable) {
|
|
24
|
+
throw new Error('Required environment variable TARGET_TABLE missing!');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Reducer, that computes the column statements for the insert command. Columns are either selected by their name, or
|
|
28
|
+
* are computed by an expression specified in `columnTransformations`.
|
|
29
|
+
*/
|
|
30
|
+
const buildQueryColumns = (columnTransformations, previous, current) => {
|
|
31
|
+
const columnExpression = Object.keys(columnTransformations).includes(current)
|
|
32
|
+
? `${columnTransformations[current]} AS ${current}`
|
|
33
|
+
: current;
|
|
34
|
+
return previous.length === 0 ? columnExpression : `${previous}, ${columnExpression}`;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Entrypoint
|
|
38
|
+
*/
|
|
39
|
+
const internalHandler = async (event) => {
|
|
40
|
+
try {
|
|
41
|
+
const partitionHour = new Date(Date.now() - 120 * 60 * 1000);
|
|
42
|
+
const year = partitionHour.getUTCFullYear();
|
|
43
|
+
const month = (partitionHour.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
44
|
+
const day = partitionHour.getUTCDate().toString().padStart(2, '0');
|
|
45
|
+
const hour = partitionHour.getUTCHours().toString().padStart(2, '0');
|
|
46
|
+
console.log('transforming partition', { year, month, day, hour });
|
|
47
|
+
// apply transformations on certain columns:
|
|
48
|
+
// - obfuscate IPs
|
|
49
|
+
// - optionally filter cookies
|
|
50
|
+
const selectExpr = event.columnNames.reduce((previous, current) => buildQueryColumns(event.columnTransformations, previous, current), '');
|
|
51
|
+
const command = new client_athena_1.StartQueryExecutionCommand({
|
|
52
|
+
QueryString: `
|
|
53
|
+
INSERT INTO ${database}.${targetTable} (${event.columnNames.join(',')})
|
|
54
|
+
SELECT ${selectExpr}
|
|
55
|
+
FROM ${database}.${sourceTable}
|
|
56
|
+
WHERE year = '${year}'
|
|
57
|
+
AND month = '${month}'
|
|
58
|
+
AND day = '${day}'
|
|
59
|
+
AND hour = '${hour}';`,
|
|
60
|
+
WorkGroup: workgroup,
|
|
61
|
+
QueryExecutionContext: { Database: database },
|
|
62
|
+
});
|
|
63
|
+
if (await (0, util_1.executeAndAwaitResponse)(command, false)) {
|
|
64
|
+
console.log('successfully transformed partition', { year, month, day, hour });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('### unexpected runtime error ###', error);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
exports.handler = internalHandler;
|
|
72
|
+
//# sourceMappingURL=transform-partition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform-partition.js","sourceRoot":"/","sources":["transform-partition.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * This script transforms the access log partition of two hours ago into the Apache parquet format.\n * Taken and adjusted from https://github.com/aws-samples/amazon-cloudfront-access-logs-queries/blob/mainline/functions/transformPartition.js.\n */\nimport {StartQueryExecutionCommand} from '@aws-sdk/client-athena';\nimport {executeAndAwaitResponse} from './util';\nimport type {ColumnTransformationRules, TransformPartitionEvent} from './types';\n\nconst workgroup = process.env.WORKGROUP;\nif (!workgroup) {\n throw new Error('Required environment variable WORKGROUP missing!');\n}\nconst database = process.env.DATABASE;\nif (!database) {\n throw new Error('Required environment variable DATABASE missing!');\n}\nconst sourceTable = process.env.SOURCE_TABLE;\nif (!sourceTable) {\n throw new Error('Required environment variable SOURCE_TABLE missing!');\n}\nconst targetTable = process.env.TARGET_TABLE;\nif (!targetTable) {\n throw new Error('Required environment variable TARGET_TABLE missing!');\n}\n\n/**\n * Reducer, that computes the column statements for the insert command. Columns are either selected by their name, or\n * are computed by an expression specified in `columnTransformations`.\n */\nconst buildQueryColumns = (\n columnTransformations: ColumnTransformationRules,\n previous: string,\n current: string\n): string => {\n const columnExpression = Object.keys(columnTransformations).includes(current)\n ? `${columnTransformations[current]} AS ${current}`\n : current;\n return previous.length === 0 ? columnExpression : `${previous}, ${columnExpression}`;\n};\n\n/**\n * Entrypoint\n */\nconst internalHandler = async (event: TransformPartitionEvent) => {\n try {\n const partitionHour = new Date(Date.now() - 120 * 60 * 1000);\n const year = partitionHour.getUTCFullYear();\n const month = (partitionHour.getUTCMonth() + 1).toString().padStart(2, '0');\n const day = partitionHour.getUTCDate().toString().padStart(2, '0');\n const hour = partitionHour.getUTCHours().toString().padStart(2, '0');\n\n console.log('transforming partition', {year, month, day, hour});\n\n // apply transformations on certain columns:\n // - obfuscate IPs\n // - optionally filter cookies\n const selectExpr = event.columnNames.reduce(\n (previous, current) => buildQueryColumns(event.columnTransformations, previous, current),\n ''\n );\n\n const command = new StartQueryExecutionCommand({\n QueryString: `\n INSERT INTO ${database}.${targetTable} (${event.columnNames.join(',')})\n SELECT ${selectExpr}\n FROM ${database}.${sourceTable}\n WHERE year = '${year}'\n AND month = '${month}'\n AND day = '${day}'\n AND hour = '${hour}';`,\n WorkGroup: workgroup,\n QueryExecutionContext: {Database: database},\n });\n\n if (await executeAndAwaitResponse(command, false)) {\n console.log('successfully transformed partition', {year, month, day, hour});\n }\n } catch (error) {\n console.error('### unexpected runtime error ###', error);\n }\n};\n\nexport const handler = internalHandler;\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"/","sources":["types.ts"],"names":[],"mappings":"","sourcesContent":["export interface ColumnTransformationRules {\n readonly [columnName: string]: string | undefined;\n}\n\nexport interface TransformPartitionEvent {\n readonly columnNames: string[];\n readonly columnTransformations: ColumnTransformationRules;\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { StartQueryExecutionCommand } from '@aws-sdk/client-athena';
|
|
2
|
+
/**
|
|
3
|
+
* Executes the given command and waits up to 10s for the command to complete.
|
|
4
|
+
* @param failOnTimeout <code>true</code> in order to throw an exception,
|
|
5
|
+
* or <code>false</code> to just log the error to the console
|
|
6
|
+
* @return <code>true</code> on success or <code>false</code> if the command timed out and
|
|
7
|
+
* <code>failOnTimeout</code> was set to false
|
|
8
|
+
*/
|
|
9
|
+
export declare const executeAndAwaitResponse: (command: StartQueryExecutionCommand, failOnTimeout: boolean) => Promise<boolean>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeAndAwaitResponse = void 0;
|
|
4
|
+
const client_athena_1 = require("@aws-sdk/client-athena");
|
|
5
|
+
/**
|
|
6
|
+
* Executes the given command and waits up to 10s for the command to complete.
|
|
7
|
+
* @param failOnTimeout <code>true</code> in order to throw an exception,
|
|
8
|
+
* or <code>false</code> to just log the error to the console
|
|
9
|
+
* @return <code>true</code> on success or <code>false</code> if the command timed out and
|
|
10
|
+
* <code>failOnTimeout</code> was set to false
|
|
11
|
+
*/
|
|
12
|
+
const executeAndAwaitResponse = async (command, failOnTimeout) => {
|
|
13
|
+
console.log('executing command ', command.input.QueryString, '...');
|
|
14
|
+
const athena = new client_athena_1.AthenaClient({});
|
|
15
|
+
const response = await athena.send(command);
|
|
16
|
+
// wait up to 10s for completion
|
|
17
|
+
const checkStatus = new client_athena_1.GetQueryExecutionCommand({
|
|
18
|
+
QueryExecutionId: response.QueryExecutionId,
|
|
19
|
+
});
|
|
20
|
+
for (let i = 1; i <= 50; i++) {
|
|
21
|
+
const state = await athena.send(checkStatus);
|
|
22
|
+
switch (state.QueryExecution.Status.State) {
|
|
23
|
+
case client_athena_1.QueryExecutionState.SUCCEEDED:
|
|
24
|
+
console.log('command completed successfully');
|
|
25
|
+
return true;
|
|
26
|
+
case client_athena_1.QueryExecutionState.CANCELLED:
|
|
27
|
+
case client_athena_1.QueryExecutionState.FAILED:
|
|
28
|
+
// noinspection TypeScriptUnresolvedFunction
|
|
29
|
+
throw new Error('Command execution failed! Query:\n' + command.input.QueryString);
|
|
30
|
+
}
|
|
31
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
32
|
+
}
|
|
33
|
+
const errorMessage = 'Status of partition creation unknown - waited for 10s but command did not complete';
|
|
34
|
+
if (failOnTimeout) {
|
|
35
|
+
// noinspection TypeScriptUnresolvedFunction
|
|
36
|
+
throw new Error(errorMessage);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.error(errorMessage);
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
exports.executeAndAwaitResponse = executeAndAwaitResponse;
|
|
44
|
+
//# sourceMappingURL=util.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"/","sources":["util.ts"],"names":[],"mappings":";;;AAAA,0DAKgC;AAEhC;;;;;;GAMG;AACI,MAAM,uBAAuB,GAAG,KAAK,EAC1C,OAAmC,EACnC,aAAsB,EACJ,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAEpE,MAAM,MAAM,GAAG,IAAI,4BAAY,CAAC,EAAE,CAAC,CAAC;IACpC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE5C,gCAAgC;IAChC,MAAM,WAAW,GAAG,IAAI,wCAAwB,CAAC;QAC/C,gBAAgB,EAAE,QAAQ,CAAC,gBAAgB;KAC5C,CAAC,CAAC;IAEH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC7C,QAAQ,KAAK,CAAC,cAAe,CAAC,MAAO,CAAC,KAAK,EAAE,CAAC;YAC5C,KAAK,mCAAmB,CAAC,SAAS;gBAChC,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;gBAC9C,OAAO,IAAI,CAAC;YACd,KAAK,mCAAmB,CAAC,SAAS,CAAC;YACnC,KAAK,mCAAmB,CAAC,MAAM;gBAC7B,4CAA4C;gBAC5C,MAAM,IAAI,KAAK,CAAC,oCAAoC,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACtF,CAAC;QAED,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,MAAM,YAAY,GAAG,oFAAoF,CAAC;IAC1G,IAAI,aAAa,EAAE,CAAC;QAClB,4CAA4C;QAC5C,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC;IAChC,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AArCW,QAAA,uBAAuB,2BAqClC","sourcesContent":["import {\n AthenaClient,\n GetQueryExecutionCommand,\n QueryExecutionState,\n StartQueryExecutionCommand,\n} from '@aws-sdk/client-athena';\n\n/**\n * Executes the given command and waits up to 10s for the command to complete.\n * @param failOnTimeout <code>true</code> in order to throw an exception,\n * or <code>false</code> to just log the error to the console\n * @return <code>true</code> on success or <code>false</code> if the command timed out and\n * <code>failOnTimeout</code> was set to false\n */\nexport const executeAndAwaitResponse = async (\n command: StartQueryExecutionCommand,\n failOnTimeout: boolean\n): Promise<boolean> => {\n console.log('executing command ', command.input.QueryString, '...');\n\n const athena = new AthenaClient({});\n const response = await athena.send(command);\n\n // wait up to 10s for completion\n const checkStatus = new GetQueryExecutionCommand({\n QueryExecutionId: response.QueryExecutionId,\n });\n\n for (let i = 1; i <= 50; i++) {\n const state = await athena.send(checkStatus);\n switch (state.QueryExecution!.Status!.State) {\n case QueryExecutionState.SUCCEEDED:\n console.log('command completed successfully');\n return true;\n case QueryExecutionState.CANCELLED:\n case QueryExecutionState.FAILED:\n // noinspection TypeScriptUnresolvedFunction\n throw new Error('Command execution failed! Query:\\n' + command.input.QueryString);\n }\n\n await new Promise(resolve => setTimeout(resolve, 200));\n }\n\n const errorMessage = 'Status of partition creation unknown - waited for 10s but command did not complete';\n if (failOnTimeout) {\n // noinspection TypeScriptUnresolvedFunction\n throw new Error(errorMessage);\n } else {\n console.error(errorMessage);\n }\n return false;\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
4
|
+
const consumers_1 = require("stream/consumers");
|
|
5
|
+
const MAX_DELETE_OBJECT_KEYS = 1000;
|
|
6
|
+
const ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
|
|
7
|
+
/**
|
|
8
|
+
* Returns the current deployment revision (build timestamp).
|
|
9
|
+
*/
|
|
10
|
+
const getCurrentRevision = async (s3Client, bucketName) => {
|
|
11
|
+
const revisionFile = await s3Client.send(new client_s3_1.GetObjectCommand({
|
|
12
|
+
Bucket: bucketName,
|
|
13
|
+
Key: 'app-revision',
|
|
14
|
+
}));
|
|
15
|
+
return new Date(await (0, consumers_1.text)(revisionFile.Body));
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Filters the given assets by inspecting their revision and returns those, that are older than the specified cutoff date.
|
|
19
|
+
*/
|
|
20
|
+
const filterOutdatedAssetKeys = (metadataResults, returnOlderThan) => {
|
|
21
|
+
return metadataResults
|
|
22
|
+
.filter(assetMetadata => assetMetadata.metadata.revision
|
|
23
|
+
? new Date(assetMetadata.metadata.revision).getTime() <= returnOlderThan.getTime()
|
|
24
|
+
: false)
|
|
25
|
+
.map(filteredAssets => filteredAssets.key);
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Deletes the given assets.
|
|
29
|
+
*/
|
|
30
|
+
const deleteAssets = async (assetKeys, s3Client, bucketName) => {
|
|
31
|
+
let remainingAssetKeysToDelete = [...assetKeys];
|
|
32
|
+
const pendingDeletes = [];
|
|
33
|
+
while (remainingAssetKeysToDelete.length > 0) {
|
|
34
|
+
const curDeleteBatch = remainingAssetKeysToDelete.slice(0, MAX_DELETE_OBJECT_KEYS);
|
|
35
|
+
remainingAssetKeysToDelete = remainingAssetKeysToDelete.slice(MAX_DELETE_OBJECT_KEYS);
|
|
36
|
+
console.log('Deleting assets:', curDeleteBatch);
|
|
37
|
+
const pendingDelete = s3Client.send(new client_s3_1.DeleteObjectsCommand({
|
|
38
|
+
Bucket: bucketName,
|
|
39
|
+
Delete: {
|
|
40
|
+
Objects: curDeleteBatch.map(outdatedKey => {
|
|
41
|
+
return { Key: outdatedKey };
|
|
42
|
+
}),
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
pendingDeletes.push(pendingDelete);
|
|
46
|
+
}
|
|
47
|
+
return await Promise.all(pendingDeletes);
|
|
48
|
+
};
|
|
49
|
+
exports.handler = async (event, context) => {
|
|
50
|
+
try {
|
|
51
|
+
const client = new client_s3_1.S3Client({ region: process.env.AWS_REGION });
|
|
52
|
+
const bucketName = process.env.STATIC_ASSETS_BUCKET;
|
|
53
|
+
if (!bucketName) {
|
|
54
|
+
throw new Error("Static asset's bucket name not specified in environment!");
|
|
55
|
+
}
|
|
56
|
+
if (!process.env.OUTDATED_ASSETS_RETENTION_DAYS) {
|
|
57
|
+
throw new Error('Retain duration of static assets not specified!');
|
|
58
|
+
}
|
|
59
|
+
const retainAssetsInDays = Number.parseInt(process.env.OUTDATED_ASSETS_RETENTION_DAYS);
|
|
60
|
+
const currentRevision = await getCurrentRevision(client, bucketName);
|
|
61
|
+
const deleteOlderThan = new Date(currentRevision.getTime() - retainAssetsInDays * ONE_DAY_IN_MILLISECONDS);
|
|
62
|
+
console.log(`Starting cleanup of static assets older than ${deleteOlderThan.toISOString()}...`);
|
|
63
|
+
let assetKeysToDelete = [];
|
|
64
|
+
let lastToken = undefined;
|
|
65
|
+
do {
|
|
66
|
+
const curAssetsResult = await client.send(new client_s3_1.ListObjectsV2Command({
|
|
67
|
+
Bucket: bucketName,
|
|
68
|
+
MaxKeys: 250,
|
|
69
|
+
ContinuationToken: lastToken,
|
|
70
|
+
}));
|
|
71
|
+
// Read object metadata in blocks of 10
|
|
72
|
+
let processableAssets = [...curAssetsResult.Contents];
|
|
73
|
+
while (processableAssets.length > 0) {
|
|
74
|
+
const assetsBatch = processableAssets.slice(0, 10);
|
|
75
|
+
processableAssets = processableAssets.slice(10);
|
|
76
|
+
const pendingMetadataRequests = assetsBatch.map(asset => client.send(new client_s3_1.HeadObjectCommand({
|
|
77
|
+
Bucket: bucketName,
|
|
78
|
+
Key: asset.Key,
|
|
79
|
+
})));
|
|
80
|
+
const metadataResults = await Promise.all(pendingMetadataRequests);
|
|
81
|
+
// Assign metadata to assets
|
|
82
|
+
const metadataByAsset = metadataResults.map((metadataResult, index) => ({
|
|
83
|
+
key: assetsBatch[index].Key,
|
|
84
|
+
metadata: metadataResult.Metadata,
|
|
85
|
+
}));
|
|
86
|
+
const outdatedAssetKeys = filterOutdatedAssetKeys(metadataByAsset, deleteOlderThan);
|
|
87
|
+
assetKeysToDelete.push(...outdatedAssetKeys);
|
|
88
|
+
}
|
|
89
|
+
lastToken = curAssetsResult.NextContinuationToken;
|
|
90
|
+
} while (lastToken !== undefined);
|
|
91
|
+
if (assetKeysToDelete.length === 0) {
|
|
92
|
+
console.log('No outdated assets to delete found');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log('Deleting ' + assetKeysToDelete.length + ' assets...');
|
|
96
|
+
// Delete outdated assets (max. 1000 allowed per request)
|
|
97
|
+
const results = await deleteAssets(assetKeysToDelete, client, bucketName);
|
|
98
|
+
const failed = results.reduce((previousResult, currentResult) => {
|
|
99
|
+
const currentError = !!(currentResult.Errors && currentResult.Errors.length > 0);
|
|
100
|
+
if (currentError) {
|
|
101
|
+
console.error('Failed to delete outdated static assets', currentResult.Errors);
|
|
102
|
+
}
|
|
103
|
+
return previousResult || currentError;
|
|
104
|
+
}, false);
|
|
105
|
+
if (failed) {
|
|
106
|
+
throw new Error('Failed to delete outdated static assets');
|
|
107
|
+
}
|
|
108
|
+
console.log('Cleanup of old static assets finished');
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error('### unexpected runtime error ###', error);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"/","sources":["index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DeleteObjectsCommand,\n DeleteObjectsCommandOutput,\n GetObjectCommand, HeadObjectCommand,\n ListObjectsV2Command,\n S3Client\n} from \"@aws-sdk/client-s3\";\nimport type {ListObjectsV2CommandOutput} from \"@aws-sdk/client-s3\";\nimport {text} from 'stream/consumers';\nimport {Readable} from \"stream\";\n\ninterface AssetMetadata {\n readonly key: string;\n readonly metadata: {[key: string]: string};\n}\n\nconst MAX_DELETE_OBJECT_KEYS = 1000;\nconst ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24;\n\n/**\n * Returns the current deployment revision (build timestamp).\n */\nconst getCurrentRevision = async (s3Client: S3Client, bucketName: string): Promise<Date> => {\n const revisionFile = await s3Client.send(\n new GetObjectCommand({\n Bucket: bucketName,\n Key: 'app-revision',\n })\n );\n\n return new Date(await text(revisionFile.Body as Readable));\n};\n\n/**\n * Filters the given assets by inspecting their revision and returns those, that are older than the specified cutoff date.\n */\nconst filterOutdatedAssetKeys = (metadataResults: AssetMetadata[], returnOlderThan: Date): string[] => {\n return metadataResults\n .filter(assetMetadata =>\n assetMetadata.metadata.revision\n ? new Date(assetMetadata.metadata.revision).getTime() <= returnOlderThan.getTime()\n : false\n )\n .map(filteredAssets => filteredAssets.key);\n};\n\n/**\n * Deletes the given assets.\n */\nconst deleteAssets = async (\n assetKeys: string[],\n s3Client: S3Client,\n bucketName: string\n): Promise<DeleteObjectsCommandOutput[]> => {\n let remainingAssetKeysToDelete = [...assetKeys];\n const pendingDeletes = [];\n\n while (remainingAssetKeysToDelete.length > 0) {\n const curDeleteBatch = remainingAssetKeysToDelete.slice(0, MAX_DELETE_OBJECT_KEYS);\n remainingAssetKeysToDelete = remainingAssetKeysToDelete.slice(MAX_DELETE_OBJECT_KEYS);\n\n console.log('Deleting assets:', curDeleteBatch);\n\n const pendingDelete = s3Client.send(\n new DeleteObjectsCommand({\n Bucket: bucketName,\n Delete: {\n Objects: curDeleteBatch.map(outdatedKey => {\n return {Key: outdatedKey};\n }),\n },\n })\n );\n pendingDeletes.push(pendingDelete);\n }\n\n return await Promise.all(pendingDeletes);\n};\n\nexports.handler = async (event: any, context: any) => {\n try {\n const client = new S3Client({region: process.env.AWS_REGION});\n const bucketName = process.env.STATIC_ASSETS_BUCKET;\n if (!bucketName) {\n throw new Error(\"Static asset's bucket name not specified in environment!\");\n }\n\n if (!process.env.OUTDATED_ASSETS_RETENTION_DAYS) {\n throw new Error('Retain duration of static assets not specified!');\n }\n const retainAssetsInDays = Number.parseInt(process.env.OUTDATED_ASSETS_RETENTION_DAYS);\n const currentRevision = await getCurrentRevision(client, bucketName);\n const deleteOlderThan = new Date(currentRevision.getTime() - retainAssetsInDays * ONE_DAY_IN_MILLISECONDS);\n\n console.log(`Starting cleanup of static assets older than ${deleteOlderThan.toISOString()}...`);\n\n let assetKeysToDelete: string[] = [];\n let lastToken = undefined;\n\n do {\n const curAssetsResult: ListObjectsV2CommandOutput = await client.send(\n new ListObjectsV2Command({\n Bucket: bucketName,\n MaxKeys: 250,\n ContinuationToken: lastToken,\n })\n );\n\n // Read object metadata in blocks of 10\n let processableAssets = [...curAssetsResult.Contents!];\n\n while (processableAssets.length > 0) {\n const assetsBatch = processableAssets.slice(0, 10);\n processableAssets = processableAssets.slice(10);\n\n const pendingMetadataRequests = assetsBatch.map(asset =>\n client.send(\n new HeadObjectCommand({\n Bucket: bucketName,\n Key: asset.Key,\n })\n )\n );\n\n const metadataResults = await Promise.all(pendingMetadataRequests);\n\n // Assign metadata to assets\n const metadataByAsset: AssetMetadata[] = (metadataResults.map((metadataResult, index) => ({\n key: assetsBatch[index].Key,\n metadata: metadataResult.Metadata,\n })) as AssetMetadata[]);\n\n const outdatedAssetKeys = filterOutdatedAssetKeys(metadataByAsset, deleteOlderThan);\n assetKeysToDelete.push(...outdatedAssetKeys);\n }\n lastToken = curAssetsResult.NextContinuationToken;\n } while (lastToken !== undefined);\n\n if (assetKeysToDelete.length === 0) {\n console.log('No outdated assets to delete found');\n return;\n }\n\n console.log('Deleting ' + assetKeysToDelete.length + ' assets...');\n\n // Delete outdated assets (max. 1000 allowed per request)\n const results = await deleteAssets(assetKeysToDelete, client, bucketName);\n const failed = results.reduce((previousResult: boolean, currentResult: DeleteObjectsCommandOutput): boolean => {\n const currentError: boolean = !!(currentResult.Errors && currentResult.Errors.length > 0);\n if (currentError) {\n console.error('Failed to delete outdated static assets', currentResult.Errors);\n }\n return previousResult || currentError;\n }, false);\n\n if (failed) {\n throw new Error('Failed to delete outdated static assets');\n }\n\n console.log('Cleanup of old static assets finished');\n } catch (error) {\n console.error('### unexpected runtime error ###', error);\n }\n};"]}
|
package/package.json
CHANGED
|
@@ -1,52 +1,50 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"packageManager": "pnpm@10.15.1"
|
|
52
|
-
}
|
|
2
|
+
"name": "cdk-nuxt",
|
|
3
|
+
"version": "2.10.8",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/ferdinandfrank/cdk-nuxt.git"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.d.ts",
|
|
11
|
+
"lib",
|
|
12
|
+
"lib/functions/assets-cleanup/build/**",
|
|
13
|
+
"lib/functions/access-logs-analysis/group-by-date/build/**",
|
|
14
|
+
"lib/functions/access-logs-analysis/partitioning/build/**"
|
|
15
|
+
],
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"types": "index.d.ts",
|
|
18
|
+
"bin": {
|
|
19
|
+
"cdk-nuxt-init-server": "lib/cli/init-server.js",
|
|
20
|
+
"cdk-nuxt-init-static": "lib/cli/init-static.js",
|
|
21
|
+
"cdk-nuxt-deploy-server": "lib/cli/deploy-server.js",
|
|
22
|
+
"cdk-nuxt-destroy-server": "lib/cli/destroy-server.js",
|
|
23
|
+
"cdk-nuxt-deploy-static": "lib/cli/deploy-static.js"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^20.10",
|
|
27
|
+
"changelogen": "^0.6.2",
|
|
28
|
+
"ts-node": "^10.9.2",
|
|
29
|
+
"typescript": "^5.6.3"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@aws-cdk/aws-glue-alpha": "2.214.0-alpha.0",
|
|
33
|
+
"aws-cdk-lib": "2.214.0",
|
|
34
|
+
"constructs": "^10.4.2",
|
|
35
|
+
"shelljs": "^0.10.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"aws-cdk-lib": "^2.214.0",
|
|
39
|
+
"constructs": "^10.4.2",
|
|
40
|
+
"ts-node": "^10.9.2",
|
|
41
|
+
"typescript": "^5.6.3"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"prepack:assets-cleanup": "pnpm -C lib/functions/assets-cleanup run build && pnpm -C lib/functions/assets-cleanup run install-layer",
|
|
46
|
+
"prepack:access-logs-analysis": "pnpm -C lib/functions/access-logs-analysis/group-by-date run build && pnpm -C lib/functions/access-logs-analysis/group-by-date run install-layer && pnpm -C lib/functions/access-logs-analysis/partitioning run build && pnpm -C lib/functions/access-logs-analysis/partitioning run install-layer",
|
|
47
|
+
"release:check": "pnpm run build",
|
|
48
|
+
"release": "pnpm run release:check && pnpm changelogen --release"
|
|
49
|
+
}
|
|
50
|
+
}
|