@xube/kit-aws 0.0.109 → 0.0.111
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/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/resources/athena/client.d.ts +4 -0
- package/dist/resources/athena/client.d.ts.map +1 -0
- package/dist/resources/athena/client.js +13 -0
- package/dist/resources/athena/get.d.ts +21 -0
- package/dist/resources/athena/get.d.ts.map +1 -0
- package/dist/resources/athena/get.js +150 -0
- package/dist/resources/firehose/client.d.ts +4 -0
- package/dist/resources/firehose/client.d.ts.map +1 -0
- package/dist/resources/firehose/client.js +13 -0
- package/dist/resources/kinesis/client.d.ts +4 -0
- package/dist/resources/kinesis/client.d.ts.map +1 -0
- package/dist/resources/kinesis/client.js +13 -0
- package/dist/resources/kinesis/put.d.ts +3 -0
- package/dist/resources/kinesis/put.d.ts.map +1 -0
- package/dist/resources/kinesis/put.js +12 -0
- package/package.json +9 -6
- package/src/index.ts +8 -0
- package/src/resources/athena/client.ts +11 -0
- package/src/resources/athena/get.ts +239 -0
- package/src/resources/firehose/client.ts +11 -0
- package/src/resources/kinesis/client.ts +11 -0
- package/src/resources/kinesis/put.ts +18 -0
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,9 @@ export * from "./resources/dynamodb/put";
|
|
|
6
6
|
export * from "./resources/dynamodb/query";
|
|
7
7
|
export * from "./resources/dynamodb/transform";
|
|
8
8
|
export * from "./resources/parameter-store/get";
|
|
9
|
+
export * from "./resources/athena/client";
|
|
10
|
+
export * from "./resources/athena/get";
|
|
11
|
+
export * from "./resources/kinesis/client";
|
|
12
|
+
export * from "./resources/kinesis/put";
|
|
13
|
+
export * from "./resources/firehose/client";
|
|
9
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAE7C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAE/C,cAAc,iCAAiC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAE7C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAE/C,cAAc,iCAAiC,CAAC;AAEhD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AAExC,cAAc,6BAA6B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -22,3 +22,8 @@ __exportStar(require("./resources/dynamodb/put"), exports);
|
|
|
22
22
|
__exportStar(require("./resources/dynamodb/query"), exports);
|
|
23
23
|
__exportStar(require("./resources/dynamodb/transform"), exports);
|
|
24
24
|
__exportStar(require("./resources/parameter-store/get"), exports);
|
|
25
|
+
__exportStar(require("./resources/athena/client"), exports);
|
|
26
|
+
__exportStar(require("./resources/athena/get"), exports);
|
|
27
|
+
__exportStar(require("./resources/kinesis/client"), exports);
|
|
28
|
+
__exportStar(require("./resources/kinesis/put"), exports);
|
|
29
|
+
__exportStar(require("./resources/firehose/client"), exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/resources/athena/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE1E,eAAO,MAAM,qBAAqB,QAAO,kBAEvC,CAAC;AAIH,eAAO,MAAM,eAAe,oBAE3B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getAthenaClient = exports.getAthenaClientConfig = void 0;
|
|
4
|
+
const client_athena_1 = require("@aws-sdk/client-athena");
|
|
5
|
+
const getAthenaClientConfig = () => ({
|
|
6
|
+
region: process.env.AWS_REGION,
|
|
7
|
+
});
|
|
8
|
+
exports.getAthenaClientConfig = getAthenaClientConfig;
|
|
9
|
+
const athenaClient = new client_athena_1.AthenaClient((0, exports.getAthenaClientConfig)());
|
|
10
|
+
const getAthenaClient = () => {
|
|
11
|
+
return athenaClient;
|
|
12
|
+
};
|
|
13
|
+
exports.getAthenaClient = getAthenaClient;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AthenaClient, GetQueryResultsCommandOutput, ResultSet } from "@aws-sdk/client-athena";
|
|
2
|
+
import { APIGatewayProxyResult } from "aws-lambda";
|
|
3
|
+
import { XubeLog } from "@xube/kit-log";
|
|
4
|
+
export declare const waitForQueryCompletion: (athena: AthenaClient, queryExecutionId: string, log?: XubeLog, maxWaitTimeMs?: number, pollIntervalMs?: number) => Promise<boolean>;
|
|
5
|
+
export declare const transformAthenaResults: (resultSet: ResultSet | undefined, isNextPage: boolean) => Record<string, any>[];
|
|
6
|
+
interface PaginationParams {
|
|
7
|
+
isNextPage: boolean;
|
|
8
|
+
queryExecutionId?: string;
|
|
9
|
+
nextToken?: string;
|
|
10
|
+
}
|
|
11
|
+
type FetchDataRequest = {
|
|
12
|
+
from: number;
|
|
13
|
+
to?: number;
|
|
14
|
+
nextToken?: string;
|
|
15
|
+
queryExecutionId?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const handlePagination: (request: FetchDataRequest) => Promise<string>;
|
|
18
|
+
export declare const fetchQueryResults: (queryExecutionId: string, paginationParams: PaginationParams, limit: number) => Promise<GetQueryResultsCommandOutput>;
|
|
19
|
+
export declare const formatResponse: (resultsResponse: GetQueryResultsCommandOutput, queryExecutionId: string, paginationParams: PaginationParams) => APIGatewayProxyResult;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=get.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../src/resources/athena/get.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAGZ,4BAA4B,EAC5B,SAAS,EAEV,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAIxC,eAAO,MAAM,sBAAsB,WACzB,YAAY,oBACF,MAAM,QACnB,OAAO,kBACG,MAAM,mBACL,MAAM,KACrB,OAAO,CAAC,OAAO,CAgDjB,CAAC;AAEF,eAAO,MAAM,sBAAsB,cACtB,SAAS,GAAG,SAAS,cACpB,OAAO,KAClB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAsBrB,CAAC;AAEF,UAAU,gBAAgB;IACxB,UAAU,EAAE,OAAO,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,KAAK,gBAAgB,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AA+DF,eAAO,MAAM,gBAAgB,YAClB,gBAAgB,KACxB,OAAO,CAAC,MAAM,CAahB,CAAC;AAEF,eAAO,MAAM,iBAAiB,qBACV,MAAM,oBACN,gBAAgB,SAC3B,MAAM,KACZ,OAAO,CAAC,4BAA4B,CAiBtC,CAAC;AAEF,eAAO,MAAM,cAAc,oBACR,4BAA4B,oBAC3B,MAAM,oBACN,gBAAgB,KACjC,qBAuBF,CAAC"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatResponse = exports.fetchQueryResults = exports.handlePagination = exports.transformAthenaResults = exports.waitForQueryCompletion = void 0;
|
|
4
|
+
const client_athena_1 = require("@aws-sdk/client-athena");
|
|
5
|
+
const kit_constants_1 = require("@xube/kit-constants");
|
|
6
|
+
const kit_log_1 = require("@xube/kit-log");
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const client_1 = require("./client");
|
|
9
|
+
const waitForQueryCompletion = async (athena, queryExecutionId, log = kit_log_1.XubeLog.getInstance(), maxWaitTimeMs = 300000, pollIntervalMs = 500) => {
|
|
10
|
+
const startTime = Date.now();
|
|
11
|
+
let attempts = 0;
|
|
12
|
+
const maxAttempts = Math.ceil(maxWaitTimeMs / pollIntervalMs);
|
|
13
|
+
while (attempts < maxAttempts) {
|
|
14
|
+
attempts++;
|
|
15
|
+
const queryExecution = await athena.send(new client_athena_1.GetQueryExecutionCommand({
|
|
16
|
+
QueryExecutionId: queryExecutionId,
|
|
17
|
+
}));
|
|
18
|
+
const state = queryExecution.QueryExecution?.Status?.State;
|
|
19
|
+
if (state === "SUCCEEDED") {
|
|
20
|
+
log.info(`Query completed successfully after ${attempts} attempts (${Date.now() - startTime}ms)`);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (state === "FAILED" || state === "CANCELLED") {
|
|
24
|
+
log.error(`Query failed with state: ${state} after ${attempts} attempts (${Date.now() - startTime}ms)`);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (Date.now() - startTime >= maxWaitTimeMs) {
|
|
28
|
+
log.error(`Query timeout: exceeded maximum wait time of ${maxWaitTimeMs}ms after ${attempts} attempts`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
32
|
+
}
|
|
33
|
+
log.error(`Query timeout: exceeded maximum attempts (${maxAttempts}) after ${Date.now() - startTime}ms`);
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
exports.waitForQueryCompletion = waitForQueryCompletion;
|
|
37
|
+
const transformAthenaResults = (resultSet, isNextPage) => {
|
|
38
|
+
if (!resultSet?.Rows || resultSet.Rows.length < 2) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const headers = resultSet.ResultSetMetadata?.ColumnInfo?.map((col) => col.Name ?? "") ?? [];
|
|
42
|
+
return resultSet.Rows.slice(isNextPage ? 0 : 1).map((row) => {
|
|
43
|
+
const normalizedRow = {};
|
|
44
|
+
row.Data.forEach((col, index) => {
|
|
45
|
+
const header = headers?.[index];
|
|
46
|
+
if (header) {
|
|
47
|
+
try {
|
|
48
|
+
normalizedRow[header] = JSON.parse(col.VarCharValue);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
normalizedRow[header] = col.VarCharValue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return normalizedRow;
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
exports.transformAthenaResults = transformAthenaResults;
|
|
59
|
+
const GLUE_DATABASE_NAME = process.env.ATHENA_GLUE_DATABASE ?? "";
|
|
60
|
+
const GLUE_TABLE_NAME = process.env.ATHENA_GLUE_TABLE ?? "";
|
|
61
|
+
const S3_BUCKET_NAME = process.env.ATHENA_RESULTS_BUCKET ?? "";
|
|
62
|
+
const S3_BUCKET_ATHENA_RESULTS_PREFIX = process.env.ATHENA_RESULTS_PREFIX ?? "athena-results";
|
|
63
|
+
const buildQuery = (request) => {
|
|
64
|
+
const databaseName = GLUE_DATABASE_NAME;
|
|
65
|
+
const tableName = GLUE_TABLE_NAME;
|
|
66
|
+
const { from, to } = request;
|
|
67
|
+
if (to) {
|
|
68
|
+
return `
|
|
69
|
+
SELECT * FROM "${databaseName}"."${tableName}"
|
|
70
|
+
WHERE "s" BETWEEN ${from} AND ${to}
|
|
71
|
+
ORDER BY "s" DESC
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
return `
|
|
76
|
+
SELECT * FROM "${databaseName}"."${tableName}"
|
|
77
|
+
WHERE "s" >= ${from}
|
|
78
|
+
ORDER BY "s" DESC
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const executeNewQuery = async (query) => {
|
|
83
|
+
const athenaClient = (0, client_1.getAthenaClient)();
|
|
84
|
+
const databaseName = GLUE_DATABASE_NAME;
|
|
85
|
+
const queryExecution = await athenaClient.send(new client_athena_1.StartQueryExecutionCommand({
|
|
86
|
+
QueryString: query,
|
|
87
|
+
QueryExecutionContext: {
|
|
88
|
+
Database: databaseName,
|
|
89
|
+
},
|
|
90
|
+
WorkGroup: "primary",
|
|
91
|
+
ResultConfiguration: {
|
|
92
|
+
OutputLocation: `s3://${S3_BUCKET_NAME}/${S3_BUCKET_ATHENA_RESULTS_PREFIX}`,
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
if (!queryExecution.QueryExecutionId) {
|
|
96
|
+
throw new Error("No QueryExecutionId returned");
|
|
97
|
+
}
|
|
98
|
+
const queryCompleted = await (0, exports.waitForQueryCompletion)(athenaClient, queryExecution.QueryExecutionId);
|
|
99
|
+
if (!queryCompleted) {
|
|
100
|
+
throw new Error(`Query execution failed`);
|
|
101
|
+
}
|
|
102
|
+
return queryExecution.QueryExecutionId;
|
|
103
|
+
};
|
|
104
|
+
const handlePagination = async (request) => {
|
|
105
|
+
const paginationParams = {
|
|
106
|
+
isNextPage: !!request.nextToken && !!request.queryExecutionId,
|
|
107
|
+
queryExecutionId: request.queryExecutionId,
|
|
108
|
+
nextToken: request.nextToken,
|
|
109
|
+
};
|
|
110
|
+
if (paginationParams.isNextPage) {
|
|
111
|
+
return request.queryExecutionId;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const query = buildQuery(request);
|
|
115
|
+
return await executeNewQuery(query);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
exports.handlePagination = handlePagination;
|
|
119
|
+
const fetchQueryResults = async (queryExecutionId, paginationParams, limit) => {
|
|
120
|
+
const athenaClient = (0, client_1.getAthenaClient)();
|
|
121
|
+
return await athenaClient.send(new client_athena_1.GetQueryResultsCommand(paginationParams.isNextPage
|
|
122
|
+
? {
|
|
123
|
+
QueryExecutionId: paginationParams.queryExecutionId,
|
|
124
|
+
NextToken: decodeURIComponent(paginationParams.nextToken),
|
|
125
|
+
MaxResults: limit,
|
|
126
|
+
}
|
|
127
|
+
: {
|
|
128
|
+
QueryExecutionId: queryExecutionId,
|
|
129
|
+
MaxResults: limit,
|
|
130
|
+
}));
|
|
131
|
+
};
|
|
132
|
+
exports.fetchQueryResults = fetchQueryResults;
|
|
133
|
+
const formatResponse = (resultsResponse, queryExecutionId, paginationParams) => {
|
|
134
|
+
const DataListSchema = zod_1.z.array(zod_1.z.record(zod_1.z.string(), zod_1.z.any()));
|
|
135
|
+
const data = DataListSchema.parse((0, exports.transformAthenaResults)(resultsResponse.ResultSet, paginationParams.isNextPage));
|
|
136
|
+
const hasNextPage = !!resultsResponse.NextToken;
|
|
137
|
+
return {
|
|
138
|
+
statusCode: kit_constants_1.StatusCode.OK,
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
nextToken: hasNextPage ? resultsResponse.NextToken : undefined,
|
|
141
|
+
queryExecutionId: hasNextPage
|
|
142
|
+
? paginationParams.isNextPage
|
|
143
|
+
? paginationParams.queryExecutionId
|
|
144
|
+
: queryExecutionId
|
|
145
|
+
: undefined,
|
|
146
|
+
data,
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
exports.formatResponse = formatResponse;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/resources/firehose/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhF,eAAO,MAAM,uBAAuB,QAAO,oBAEzC,CAAC;AAIH,eAAO,MAAM,iBAAiB,sBAE7B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getFirehoseClient = exports.getFirehoseClientConfig = void 0;
|
|
4
|
+
const client_firehose_1 = require("@aws-sdk/client-firehose");
|
|
5
|
+
const getFirehoseClientConfig = () => ({
|
|
6
|
+
region: process.env.AWS_REGION,
|
|
7
|
+
});
|
|
8
|
+
exports.getFirehoseClientConfig = getFirehoseClientConfig;
|
|
9
|
+
const firehoseClient = new client_firehose_1.FirehoseClient((0, exports.getFirehoseClientConfig)());
|
|
10
|
+
const getFirehoseClient = () => {
|
|
11
|
+
return firehoseClient;
|
|
12
|
+
};
|
|
13
|
+
exports.getFirehoseClient = getFirehoseClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/resources/kinesis/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE7E,eAAO,MAAM,sBAAsB,QAAO,mBAExC,CAAC;AAIH,eAAO,MAAM,gBAAgB,qBAE5B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getKinesisClient = exports.getKinesisClientConfig = void 0;
|
|
4
|
+
const client_kinesis_1 = require("@aws-sdk/client-kinesis");
|
|
5
|
+
const getKinesisClientConfig = () => ({
|
|
6
|
+
region: process.env.AWS_REGION,
|
|
7
|
+
});
|
|
8
|
+
exports.getKinesisClientConfig = getKinesisClientConfig;
|
|
9
|
+
const kinesisClient = new client_kinesis_1.KinesisClient((0, exports.getKinesisClientConfig)());
|
|
10
|
+
const getKinesisClient = () => {
|
|
11
|
+
return kinesisClient;
|
|
12
|
+
};
|
|
13
|
+
exports.getKinesisClient = getKinesisClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"put.d.ts","sourceRoot":"","sources":["../../../src/resources/kinesis/put.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAEb,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAEjC,eAAO,MAAM,UAAU,eACT,MAAM,WACT,sBAAsB,EAAE,WACxB,aAAa,kBAQvB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.putRecords = void 0;
|
|
4
|
+
const client_kinesis_1 = require("@aws-sdk/client-kinesis");
|
|
5
|
+
const putRecords = async (streamName, records, kinesis) => {
|
|
6
|
+
const command = new client_kinesis_1.PutRecordsCommand({
|
|
7
|
+
StreamName: streamName,
|
|
8
|
+
Records: records,
|
|
9
|
+
});
|
|
10
|
+
await kinesis.send(command);
|
|
11
|
+
};
|
|
12
|
+
exports.putRecords = putRecords;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xube/kit-aws",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.111",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,18 +18,21 @@
|
|
|
18
18
|
"homepage": "https://github.com/XubeLtd/dev-kit#readme",
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/aws-lambda": "^8.10.119",
|
|
21
|
-
"@xube/kit-build": "^0.0.
|
|
21
|
+
"@xube/kit-build": "^0.0.111"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@aws-sdk/client-athena": "^3.656.0",
|
|
24
25
|
"@aws-sdk/client-cognito-identity-provider": "^3.654.0",
|
|
25
26
|
"@aws-sdk/client-dynamodb": "^3.656.0",
|
|
27
|
+
"@aws-sdk/client-firehose": "^3.656.0",
|
|
28
|
+
"@aws-sdk/client-kinesis": "^3.656.0",
|
|
26
29
|
"@aws-sdk/client-ssm": "^3.654.0",
|
|
27
30
|
"@aws-sdk/lib-dynamodb": "^3.656.0",
|
|
28
31
|
"@aws-sdk/util-dynamodb": "^3.656.0",
|
|
29
|
-
"@xube/kit-aws-schema": "^0.0.
|
|
30
|
-
"@xube/kit-log": "^0.0.
|
|
31
|
-
"@xube/kit-request": "^0.0.
|
|
32
|
-
"@xube/kit-schema": "^0.0.
|
|
32
|
+
"@xube/kit-aws-schema": "^0.0.111",
|
|
33
|
+
"@xube/kit-log": "^0.0.111",
|
|
34
|
+
"@xube/kit-request": "^0.0.111",
|
|
35
|
+
"@xube/kit-schema": "^0.0.111",
|
|
33
36
|
"zod": "^3.23.8"
|
|
34
37
|
}
|
|
35
38
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,3 +8,11 @@ export * from "./resources/dynamodb/query";
|
|
|
8
8
|
export * from "./resources/dynamodb/transform";
|
|
9
9
|
|
|
10
10
|
export * from "./resources/parameter-store/get";
|
|
11
|
+
|
|
12
|
+
export * from "./resources/athena/client";
|
|
13
|
+
export * from "./resources/athena/get";
|
|
14
|
+
|
|
15
|
+
export * from "./resources/kinesis/client";
|
|
16
|
+
export * from "./resources/kinesis/put";
|
|
17
|
+
|
|
18
|
+
export * from "./resources/firehose/client";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AthenaClient, AthenaClientConfig } from "@aws-sdk/client-athena";
|
|
2
|
+
|
|
3
|
+
export const getAthenaClientConfig = (): AthenaClientConfig => ({
|
|
4
|
+
region: process.env.AWS_REGION,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const athenaClient = new AthenaClient(getAthenaClientConfig());
|
|
8
|
+
|
|
9
|
+
export const getAthenaClient = () => {
|
|
10
|
+
return athenaClient;
|
|
11
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AthenaClient,
|
|
3
|
+
GetQueryExecutionCommand,
|
|
4
|
+
GetQueryResultsCommand,
|
|
5
|
+
GetQueryResultsCommandOutput,
|
|
6
|
+
ResultSet,
|
|
7
|
+
StartQueryExecutionCommand,
|
|
8
|
+
} from "@aws-sdk/client-athena";
|
|
9
|
+
import { APIGatewayProxyResult } from "aws-lambda";
|
|
10
|
+
import { StatusCode } from "@xube/kit-constants";
|
|
11
|
+
import { XubeLog } from "@xube/kit-log";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { getAthenaClient } from "./client";
|
|
14
|
+
|
|
15
|
+
export const waitForQueryCompletion = async (
|
|
16
|
+
athena: AthenaClient,
|
|
17
|
+
queryExecutionId: string,
|
|
18
|
+
log: XubeLog = XubeLog.getInstance(),
|
|
19
|
+
maxWaitTimeMs: number = 300000,
|
|
20
|
+
pollIntervalMs: number = 500
|
|
21
|
+
): Promise<boolean> => {
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
let attempts = 0;
|
|
24
|
+
const maxAttempts = Math.ceil(maxWaitTimeMs / pollIntervalMs);
|
|
25
|
+
|
|
26
|
+
while (attempts < maxAttempts) {
|
|
27
|
+
attempts++;
|
|
28
|
+
|
|
29
|
+
const queryExecution = await athena.send(
|
|
30
|
+
new GetQueryExecutionCommand({
|
|
31
|
+
QueryExecutionId: queryExecutionId,
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const state = queryExecution.QueryExecution?.Status?.State;
|
|
36
|
+
if (state === "SUCCEEDED") {
|
|
37
|
+
log.info(
|
|
38
|
+
`Query completed successfully after ${attempts} attempts (${
|
|
39
|
+
Date.now() - startTime
|
|
40
|
+
}ms)`
|
|
41
|
+
);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (state === "FAILED" || state === "CANCELLED") {
|
|
45
|
+
log.error(
|
|
46
|
+
`Query failed with state: ${state} after ${attempts} attempts (${
|
|
47
|
+
Date.now() - startTime
|
|
48
|
+
}ms)`
|
|
49
|
+
);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Date.now() - startTime >= maxWaitTimeMs) {
|
|
54
|
+
log.error(
|
|
55
|
+
`Query timeout: exceeded maximum wait time of ${maxWaitTimeMs}ms after ${attempts} attempts`
|
|
56
|
+
);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
log.error(
|
|
64
|
+
`Query timeout: exceeded maximum attempts (${maxAttempts}) after ${
|
|
65
|
+
Date.now() - startTime
|
|
66
|
+
}ms`
|
|
67
|
+
);
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const transformAthenaResults = (
|
|
72
|
+
resultSet: ResultSet | undefined,
|
|
73
|
+
isNextPage: boolean
|
|
74
|
+
): Record<string, any>[] => {
|
|
75
|
+
if (!resultSet?.Rows || resultSet.Rows.length < 2) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const headers =
|
|
80
|
+
resultSet.ResultSetMetadata?.ColumnInfo?.map((col) => col.Name ?? "") ?? [];
|
|
81
|
+
|
|
82
|
+
return resultSet.Rows.slice(isNextPage ? 0 : 1).map((row: any) => {
|
|
83
|
+
const normalizedRow: Record<string, any> = {};
|
|
84
|
+
row.Data.forEach((col: any, index: number) => {
|
|
85
|
+
const header = headers?.[index];
|
|
86
|
+
if (header) {
|
|
87
|
+
try {
|
|
88
|
+
normalizedRow[header] = JSON.parse(col.VarCharValue);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
normalizedRow[header] = col.VarCharValue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return normalizedRow;
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
interface PaginationParams {
|
|
99
|
+
isNextPage: boolean;
|
|
100
|
+
queryExecutionId?: string;
|
|
101
|
+
nextToken?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type FetchDataRequest = {
|
|
105
|
+
from: number;
|
|
106
|
+
to?: number;
|
|
107
|
+
nextToken?: string;
|
|
108
|
+
queryExecutionId?: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const GLUE_DATABASE_NAME = process.env.ATHENA_GLUE_DATABASE ?? "";
|
|
112
|
+
const GLUE_TABLE_NAME = process.env.ATHENA_GLUE_TABLE ?? "";
|
|
113
|
+
const S3_BUCKET_NAME = process.env.ATHENA_RESULTS_BUCKET ?? "";
|
|
114
|
+
const S3_BUCKET_ATHENA_RESULTS_PREFIX =
|
|
115
|
+
process.env.ATHENA_RESULTS_PREFIX ?? "athena-results";
|
|
116
|
+
|
|
117
|
+
const buildQuery = (request: FetchDataRequest): string => {
|
|
118
|
+
const databaseName = GLUE_DATABASE_NAME;
|
|
119
|
+
const tableName = GLUE_TABLE_NAME;
|
|
120
|
+
|
|
121
|
+
const { from, to } = request;
|
|
122
|
+
|
|
123
|
+
if (to) {
|
|
124
|
+
return `
|
|
125
|
+
SELECT * FROM "${databaseName}"."${tableName}"
|
|
126
|
+
WHERE "s" BETWEEN ${from} AND ${to}
|
|
127
|
+
ORDER BY "s" DESC
|
|
128
|
+
`;
|
|
129
|
+
} else {
|
|
130
|
+
return `
|
|
131
|
+
SELECT * FROM "${databaseName}"."${tableName}"
|
|
132
|
+
WHERE "s" >= ${from}
|
|
133
|
+
ORDER BY "s" DESC
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const executeNewQuery = async (query: string): Promise<string> => {
|
|
139
|
+
const athenaClient = getAthenaClient();
|
|
140
|
+
|
|
141
|
+
const databaseName = GLUE_DATABASE_NAME;
|
|
142
|
+
|
|
143
|
+
const queryExecution = await athenaClient.send(
|
|
144
|
+
new StartQueryExecutionCommand({
|
|
145
|
+
QueryString: query,
|
|
146
|
+
QueryExecutionContext: {
|
|
147
|
+
Database: databaseName,
|
|
148
|
+
},
|
|
149
|
+
WorkGroup: "primary",
|
|
150
|
+
ResultConfiguration: {
|
|
151
|
+
OutputLocation: `s3://${S3_BUCKET_NAME}/${S3_BUCKET_ATHENA_RESULTS_PREFIX}`,
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (!queryExecution.QueryExecutionId) {
|
|
157
|
+
throw new Error("No QueryExecutionId returned");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const queryCompleted = await waitForQueryCompletion(
|
|
161
|
+
athenaClient,
|
|
162
|
+
queryExecution.QueryExecutionId
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (!queryCompleted) {
|
|
166
|
+
throw new Error(`Query execution failed`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return queryExecution.QueryExecutionId;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const handlePagination = async (
|
|
173
|
+
request: FetchDataRequest
|
|
174
|
+
): Promise<string> => {
|
|
175
|
+
const paginationParams: PaginationParams = {
|
|
176
|
+
isNextPage: !!request.nextToken && !!request.queryExecutionId,
|
|
177
|
+
queryExecutionId: request.queryExecutionId,
|
|
178
|
+
nextToken: request.nextToken,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (paginationParams.isNextPage) {
|
|
182
|
+
return request.queryExecutionId!;
|
|
183
|
+
} else {
|
|
184
|
+
const query = buildQuery(request);
|
|
185
|
+
return await executeNewQuery(query);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const fetchQueryResults = async (
|
|
190
|
+
queryExecutionId: string,
|
|
191
|
+
paginationParams: PaginationParams,
|
|
192
|
+
limit: number
|
|
193
|
+
): Promise<GetQueryResultsCommandOutput> => {
|
|
194
|
+
const athenaClient = getAthenaClient();
|
|
195
|
+
|
|
196
|
+
return await athenaClient.send(
|
|
197
|
+
new GetQueryResultsCommand(
|
|
198
|
+
paginationParams.isNextPage
|
|
199
|
+
? {
|
|
200
|
+
QueryExecutionId: paginationParams.queryExecutionId!,
|
|
201
|
+
NextToken: decodeURIComponent(paginationParams.nextToken!),
|
|
202
|
+
MaxResults: limit,
|
|
203
|
+
}
|
|
204
|
+
: {
|
|
205
|
+
QueryExecutionId: queryExecutionId,
|
|
206
|
+
MaxResults: limit,
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const formatResponse = (
|
|
213
|
+
resultsResponse: GetQueryResultsCommandOutput,
|
|
214
|
+
queryExecutionId: string,
|
|
215
|
+
paginationParams: PaginationParams
|
|
216
|
+
): APIGatewayProxyResult => {
|
|
217
|
+
const DataListSchema = z.array(z.record(z.string(), z.any()));
|
|
218
|
+
const data = DataListSchema.parse(
|
|
219
|
+
transformAthenaResults(
|
|
220
|
+
resultsResponse.ResultSet,
|
|
221
|
+
paginationParams.isNextPage
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const hasNextPage = !!resultsResponse.NextToken;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
statusCode: StatusCode.OK,
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
nextToken: hasNextPage ? resultsResponse.NextToken : undefined,
|
|
231
|
+
queryExecutionId: hasNextPage
|
|
232
|
+
? paginationParams.isNextPage
|
|
233
|
+
? paginationParams.queryExecutionId
|
|
234
|
+
: queryExecutionId
|
|
235
|
+
: undefined,
|
|
236
|
+
data,
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FirehoseClient, FirehoseClientConfig } from "@aws-sdk/client-firehose";
|
|
2
|
+
|
|
3
|
+
export const getFirehoseClientConfig = (): FirehoseClientConfig => ({
|
|
4
|
+
region: process.env.AWS_REGION,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const firehoseClient = new FirehoseClient(getFirehoseClientConfig());
|
|
8
|
+
|
|
9
|
+
export const getFirehoseClient = () => {
|
|
10
|
+
return firehoseClient;
|
|
11
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { KinesisClient, KinesisClientConfig } from "@aws-sdk/client-kinesis";
|
|
2
|
+
|
|
3
|
+
export const getKinesisClientConfig = (): KinesisClientConfig => ({
|
|
4
|
+
region: process.env.AWS_REGION,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const kinesisClient = new KinesisClient(getKinesisClientConfig());
|
|
8
|
+
|
|
9
|
+
export const getKinesisClient = () => {
|
|
10
|
+
return kinesisClient;
|
|
11
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KinesisClient,
|
|
3
|
+
PutRecordsCommand,
|
|
4
|
+
PutRecordsRequestEntry,
|
|
5
|
+
} from "@aws-sdk/client-kinesis";
|
|
6
|
+
|
|
7
|
+
export const putRecords = async (
|
|
8
|
+
streamName: string,
|
|
9
|
+
records: PutRecordsRequestEntry[],
|
|
10
|
+
kinesis: KinesisClient
|
|
11
|
+
) => {
|
|
12
|
+
const command = new PutRecordsCommand({
|
|
13
|
+
StreamName: streamName,
|
|
14
|
+
Records: records,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
await kinesis.send(command);
|
|
18
|
+
};
|