@stilyng94/athena-query-client 1.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2024] [Paul Osei Kofi]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ **athena-query-client**
2
+
3
+ # @stilyng94/athena-query-client
4
+
5
+ A lightweight client library for executing Amazon Athena queries.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @stilyng94/athena-query-client
11
+ ```
12
+
13
+ ### !!Notice
14
+
15
+ Ensure aws `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` envs are set
16
+
17
+ ### Usage
18
+
19
+ ## Get query results via S3 and write to JsonFile
20
+
21
+ ```typescript
22
+ import {
23
+ AthenaQueryClient,
24
+ S3QueryResultProcessor,
25
+ JsonFileAppender,
26
+ } from "@stilyng94/athena-query-client";
27
+
28
+ const query = "SELECT * from api_logs";
29
+
30
+ const execute = async (query) => {
31
+ const athena = new AthenaQueryClient({
32
+ Catalog: "catalog",
33
+ Database: "logs",
34
+ ClientConfig: { region: "us-west-2" },
35
+ WorkGroup: "primary",
36
+ s3OutputLocation: "s3://results/queries",
37
+ s3Region: "us-west-2",
38
+ });
39
+
40
+ const queryExecutionId = await athena.query();
41
+
42
+ const jsonFileAppender = new JsonFileAppender({
43
+ directory: "",
44
+ fileName: "",
45
+ });
46
+
47
+ const s3QueryResultProcessor = new S3QueryResultProcessor({
48
+ s3OutputLocation: "s3://results/queries",
49
+ s3Region: "us-west-2",
50
+ batchSize: 999,
51
+ onData: async (data) => {
52
+ await jsonFileAppender.flush(data);
53
+ },
54
+ onComplete: async () => {
55
+ await jsonFileAppender.closeFileWithBracket();
56
+ },
57
+ });
58
+ s3QueryResultProcessor
59
+ .processResults(queryExecutionId)
60
+ .catch((error) => console.error(error));
61
+ };
62
+
63
+ execute().catch(console.error);
64
+ ```
65
+
66
+ ## Get query results and map to Js object
67
+
68
+ ```typescript
69
+ import {
70
+ AthenaQueryClient,
71
+ MappedQueryResultProcessor,
72
+ } from "@stilyng94/athena-query-client";
73
+
74
+ const query = "SELECT * from api_logs";
75
+
76
+ const execute = async (query) => {
77
+ const athena = new AthenaQueryClient({
78
+ Catalog: "catalog",
79
+ Database: "logs",
80
+ ClientConfig: { region: "us-west-2" },
81
+ WorkGroup: "primary",
82
+ s3OutputLocation: "s3://results/queries",
83
+ s3Region: "us-west-2",
84
+ });
85
+
86
+ const queryExecutionId = await athena.query();
87
+
88
+ const mappedQueryResultProcessor = new MappedQueryResultProcessor({
89
+ athenaClient: athena,
90
+ });
91
+ mappedQueryResultProcessor.processResults(queryExecutionId).then((data) => {
92
+ console.log(data);
93
+ });
94
+ };
95
+
96
+ execute().catch(console.error);
97
+ ```
@@ -0,0 +1,20 @@
1
+ import type { AthenaQueryClientConfig } from './types.js';
2
+ /**
3
+ * Client for executing queries against AWS Athena and retrieving results
4
+ * @class AthenaQueryClient
5
+ */
6
+ export declare class AthenaQueryClient {
7
+ #private;
8
+ /**
9
+ * Creates an instance of AthenaQueryClient
10
+ * @param {AthenaQueryClientConfig} config - Configuration object for the client
11
+ */
12
+ constructor(config: AthenaQueryClientConfig);
13
+ /**
14
+ * Executes an SQL query in Athena and returns the QueryExecutionId
15
+ * @param {string} sqlQuery - The SQL query string to execute
16
+ * @returns {Promise<string>} Promise resolving to QueryExecutionId
17
+ * @throws {Error} If query execution fails or encounters an unknown state
18
+ */
19
+ query(sqlQuery: string): Promise<string>;
20
+ }
package/dist/athena.js ADDED
@@ -0,0 +1,92 @@
1
+ import { AthenaClient, GetQueryExecutionCommand, QueryExecutionState, StartQueryExecutionCommand, } from '@aws-sdk/client-athena';
2
+ import { EMPTY, defer, firstValueFrom, from, of, throwError, timer, } from 'rxjs';
3
+ import { expand, filter, switchMap, take } from 'rxjs/operators';
4
+ import { AthenaQueryError } from './errors.js';
5
+ /**
6
+ * Client for executing queries against AWS Athena and retrieving results
7
+ * @class AthenaQueryClient
8
+ */
9
+ export class AthenaQueryClient {
10
+ #database;
11
+ #client;
12
+ #catalog;
13
+ #workGroup;
14
+ #resultReuseConfiguration;
15
+ #s3OutputLocation;
16
+ /**
17
+ * Creates an instance of AthenaQueryClient
18
+ * @param {AthenaQueryClientConfig} config - Configuration object for the client
19
+ */
20
+ constructor(config) {
21
+ this.#client = new AthenaClient({
22
+ ...config.ClientConfig,
23
+ region: config.s3Region,
24
+ });
25
+ this.#database = config.Database;
26
+ this.#catalog = config.Catalog;
27
+ this.#workGroup = config.WorkGroup || 'primary';
28
+ this.#resultReuseConfiguration = config.ResultReuseConfiguration || {
29
+ ResultReuseByAgeConfiguration: { Enabled: true, MaxAgeInMinutes: 60 },
30
+ };
31
+ this.#s3OutputLocation = config.s3OutputLocation;
32
+ }
33
+ /**
34
+ * Executes an SQL query in Athena and returns the QueryExecutionId
35
+ * @param {string} sqlQuery - The SQL query string to execute
36
+ * @returns {Promise<string>} Promise resolving to QueryExecutionId
37
+ * @throws {Error} If query execution fails or encounters an unknown state
38
+ */
39
+ async query(sqlQuery) {
40
+ console.log('Executing query:', sqlQuery);
41
+ const queryExecutionInput = {
42
+ QueryString: sqlQuery,
43
+ QueryExecutionContext: {
44
+ Database: this.#database,
45
+ Catalog: this.#catalog,
46
+ },
47
+ WorkGroup: this.#workGroup,
48
+ ResultReuseConfiguration: this.#resultReuseConfiguration,
49
+ ResultConfiguration: { OutputLocation: this.#s3OutputLocation },
50
+ };
51
+ const { QueryExecutionId } = await this.#client.send(new StartQueryExecutionCommand(queryExecutionInput));
52
+ console.log('Query execution started with ID:', QueryExecutionId);
53
+ if (!QueryExecutionId) {
54
+ throw new AthenaQueryError('Failed to start query: QueryExecutionId is undefined');
55
+ }
56
+ return await firstValueFrom(this.#checkQueryExecutionState(QueryExecutionId)).catch((error) => {
57
+ console.error('Query execution failed:', error);
58
+ throw new AthenaQueryError(`Query execution failed: ${error.message}`);
59
+ });
60
+ }
61
+ /**
62
+ * Polls the Athena query execution state until completion
63
+ * @param {string} QueryExecutionId - The ID of the query being executed
64
+ * @returns {Observable<string>} Observable emitting QueryExecutionId on success
65
+ */
66
+ #checkQueryExecutionState(QueryExecutionId) {
67
+ return defer(() => from(this.#client.send(new GetQueryExecutionCommand({ QueryExecutionId })))).pipe(switchMap((response) => {
68
+ const state = response.QueryExecution?.Status?.State;
69
+ console.log(`Query execution state: ${state}`);
70
+ switch (state) {
71
+ case QueryExecutionState.SUCCEEDED:
72
+ return of(QueryExecutionId);
73
+ case QueryExecutionState.FAILED: {
74
+ const errorMessage = response.QueryExecution?.Status?.StateChangeReason ||
75
+ 'Unknown reason';
76
+ console.error('Query failed:', errorMessage);
77
+ return throwError(() => new Error(`Query failed: ${errorMessage}`));
78
+ }
79
+ case QueryExecutionState.CANCELLED:
80
+ return throwError(() => new Error('Query was cancelled'));
81
+ case QueryExecutionState.QUEUED:
82
+ case QueryExecutionState.RUNNING:
83
+ return of(null); // Continue polling
84
+ default:
85
+ return throwError(() => new Error(`Unknown query state: ${state}`));
86
+ }
87
+ }), expand((result) => result === null
88
+ ? timer(1000).pipe(switchMap(() => this.#checkQueryExecutionState(QueryExecutionId)))
89
+ : EMPTY), filter((result) => result !== null), take(1));
90
+ }
91
+ }
92
+ //# sourceMappingURL=athena.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"athena.js","sourceRoot":"","sources":["../src/athena.ts"],"names":[],"mappings":"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"}
package/dist/demo.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/demo.js ADDED
@@ -0,0 +1,15 @@
1
+ import { AthenaQueryClient } from "./athena.js";
2
+ const client = new AthenaQueryClient({
3
+ Catalog: "",
4
+ s3OutputLocation: "s3://sjka/s",
5
+ s3Region: "us-east-1",
6
+ Database: "",
7
+ ClientConfig: {
8
+ region: "us-east-1",
9
+ },
10
+ });
11
+ client
12
+ .query("select * from db")
13
+ .then((d) => console.log(d))
14
+ .then((e) => console.error({ e }));
15
+ //# sourceMappingURL=demo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demo.js","sourceRoot":"","sources":["../src/demo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,MAAM,GAAG,IAAI,iBAAiB,CAAC;IACnC,OAAO,EAAE,EAAE;IACX,gBAAgB,EAAE,aAAa;IAC/B,QAAQ,EAAE,WAAW;IACrB,QAAQ,EAAE,EAAE;IACZ,YAAY,EAAE;QACZ,MAAM,EAAE,WAAW;KACpB;CACF,CAAC,CAAC;AACH,MAAM;KACH,KAAK,CAAC,kBAAkB,CAAC;KACzB,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KAC3B,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare class AthenaQueryError extends Error {
2
+ readonly queryExecutionId?: string | undefined;
3
+ constructor(message: string, queryExecutionId?: string | undefined);
4
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,9 @@
1
+ export class AthenaQueryError extends Error {
2
+ queryExecutionId;
3
+ constructor(message, queryExecutionId) {
4
+ super(message);
5
+ this.queryExecutionId = queryExecutionId;
6
+ this.name = 'AthenaQueryError';
7
+ }
8
+ }
9
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAGvB;IAFlB,YACE,OAAe,EACC,gBAAyB;QAEzC,KAAK,CAAC,OAAO,CAAC,CAAA;QAFE,qBAAgB,GAAhB,gBAAgB,CAAS;QAGzC,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;IAChC,CAAC;CACF"}
@@ -0,0 +1,5 @@
1
+ export { AthenaQueryClient } from './athena.js';
2
+ export { AthenaQueryError } from './errors.js';
3
+ export { MappedQueryResultProcessor, S3QueryResultProcessor, } from './query-results-processor.js';
4
+ export type { AthenaQueryClientConfig, MappedQueryResultProcessorParams, QueryResultProcessor, S3QueryResultProcessorParams, JsonFileAppenderOptions, } from './types.js';
5
+ export { JsonFileAppender } from './json-file-appender.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { AthenaQueryClient } from './athena.js';
2
+ export { AthenaQueryError } from './errors.js';
3
+ export { MappedQueryResultProcessor, S3QueryResultProcessor, } from './query-results-processor.js';
4
+ export { JsonFileAppender } from './json-file-appender.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EACL,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,8BAA8B,CAAA;AAQrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA"}
@@ -0,0 +1,24 @@
1
+ import type { JsonFileAppenderOptions } from './types.js';
2
+ /**
3
+ *@description Handles appending JSON data to files with proper formatting
4
+ */
5
+ export declare class JsonFileAppender {
6
+ #private;
7
+ private readonly options;
8
+ /**
9
+ *@description Creates a new JsonFileAppender instance
10
+ * @param {AppendOptions} options - Configuration options
11
+ */
12
+ constructor(options: JsonFileAppenderOptions);
13
+ /**
14
+ *@description Flushes the current batch of data to the file
15
+ * @param {unknown[]} currentBatch - Array of items to write
16
+ * @returns {Promise<void>}
17
+ */
18
+ flush(currentBatch: unknown[]): Promise<void>;
19
+ /**
20
+ * @description Adds closing bracket to JSON array file
21
+ * @returns {void}
22
+ */
23
+ closeFileWithBracket(): void;
24
+ }
@@ -0,0 +1,105 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { access, mkdir, stat, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ /**
5
+ *@description Handles appending JSON data to files with proper formatting
6
+ */
7
+ export class JsonFileAppender {
8
+ options;
9
+ #isFirstWrite = true;
10
+ #filePath;
11
+ /**
12
+ *@description Creates a new JsonFileAppender instance
13
+ * @param {AppendOptions} options - Configuration options
14
+ */
15
+ constructor(options) {
16
+ this.options = options;
17
+ this.#filePath = path.join(options.directory, options.fileName);
18
+ }
19
+ /**
20
+ *@description Flushes the current batch of data to the file
21
+ * @param {unknown[]} currentBatch - Array of items to write
22
+ * @returns {Promise<void>}
23
+ */
24
+ async flush(currentBatch) {
25
+ console.log(`Flushing batch of ${currentBatch.length} items to ${this.options.fileName}`);
26
+ await this.#ensureDirectoryExists(this.options.directory);
27
+ const filePath = path.join(this.options.directory, this.options.fileName);
28
+ await this.#initializeFile();
29
+ const writeStream = createWriteStream(filePath, {
30
+ flags: 'a',
31
+ });
32
+ try {
33
+ for (let i = 0; i < currentBatch.length; i++) {
34
+ const item = currentBatch[i];
35
+ // Add comma if not first item and not first write
36
+ if (!this.#isFirstWrite || i > 0) {
37
+ writeStream.write(',\n');
38
+ }
39
+ writeStream.write(JSON.stringify(item, null));
40
+ }
41
+ this.#isFirstWrite = false;
42
+ console.log(`Successfully flushed ${currentBatch.length} items`);
43
+ }
44
+ catch (error) {
45
+ console.error('Error while flushing data:', error);
46
+ throw error;
47
+ }
48
+ finally {
49
+ writeStream.end();
50
+ }
51
+ }
52
+ /**
53
+ * @description Adds closing bracket to JSON array file
54
+ * @returns {void}
55
+ */
56
+ closeFileWithBracket() {
57
+ console.log(`Closing JSON array in file: ${this.#filePath}`);
58
+ const writeStream = createWriteStream(this.#filePath, { flags: 'a' });
59
+ try {
60
+ writeStream.write('\n]');
61
+ }
62
+ catch (error) {
63
+ console.error('Error while closing JSON array:', error);
64
+ throw error;
65
+ }
66
+ finally {
67
+ writeStream.end();
68
+ }
69
+ }
70
+ /**
71
+ *@description Initializes the file with proper JSON array structure
72
+ * @returns {Promise<void>}
73
+ */
74
+ async #initializeFile() {
75
+ try {
76
+ await access(this.#filePath);
77
+ // File exists, check if it's empty or has content
78
+ const statistics = await stat(this.#filePath);
79
+ this.#isFirstWrite = statistics.size === 0;
80
+ console.log(`File exists. Size: ${statistics.size} bytes. First write: ${this.#isFirstWrite}`);
81
+ }
82
+ catch {
83
+ // File doesn't exist, create it with initial array bracket
84
+ console.log(`Creating new file: ${this.#filePath}`);
85
+ await writeFile(this.#filePath, '[\n', 'utf-8');
86
+ this.#isFirstWrite = true;
87
+ }
88
+ }
89
+ /**
90
+ *@description Ensures the specified directory exists, creates if it doesn't
91
+ * @param {string} directory - Directory path to check/create
92
+ * @returns {Promise<void>}
93
+ */
94
+ async #ensureDirectoryExists(directory) {
95
+ try {
96
+ await access(directory);
97
+ console.log(`Directory ${directory} exists`);
98
+ }
99
+ catch {
100
+ await mkdir(directory, { recursive: true });
101
+ console.log(`Created directory ${directory}`);
102
+ }
103
+ }
104
+ }
105
+ //# sourceMappingURL=json-file-appender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-file-appender.js","sourceRoot":"","sources":["../src/json-file-appender.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,IAAI,MAAM,WAAW,CAAA;AAG5B;;GAEG;AACH,MAAM,OAAO,gBAAgB;IAQE;IAP7B,aAAa,GAAG,IAAI,CAAA;IACX,SAAS,CAAQ;IAE1B;;;OAGG;IACH,YAA6B,OAAgC;QAAhC,YAAO,GAAP,OAAO,CAAyB;QAC3D,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IACjE,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK,CAAC,YAAuB;QACjC,OAAO,CAAC,GAAG,CACT,qBAAqB,YAAY,CAAC,MAAM,aAAa,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAC7E,CAAA;QACD,MAAM,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;QACzD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QACzE,MAAM,IAAI,CAAC,eAAe,EAAE,CAAA;QAE5B,MAAM,WAAW,GAAG,iBAAiB,CAAC,QAAQ,EAAE;YAC9C,KAAK,EAAE,GAAG;SACX,CAAC,CAAA;QAEF,IAAI,CAAC;YACH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC7C,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA;gBAC5B,kDAAkD;gBAClD,IAAI,CAAC,IAAI,CAAC,aAAa,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;oBACjC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;gBAC1B,CAAC;gBAED,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;YAC/C,CAAC;YAED,IAAI,CAAC,aAAa,GAAG,KAAK,CAAA;YAC1B,OAAO,CAAC,GAAG,CAAC,wBAAwB,YAAY,CAAC,MAAM,QAAQ,CAAC,CAAA;QAClE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAA;YAClD,MAAM,KAAK,CAAA;QACb,CAAC;gBAAS,CAAC;YACT,WAAW,CAAC,GAAG,EAAE,CAAA;QACnB,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,oBAAoB;QAClB,OAAO,CAAC,GAAG,CAAC,+BAA+B,IAAI,CAAC,SAAS,EAAE,CAAC,CAAA;QAC5D,MAAM,WAAW,GAAG,iBAAiB,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;QACrE,IAAI,CAAC;YACH,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAA;YACvD,MAAM,KAAK,CAAA;QACb,CAAC;gBAAS,CAAC;YACT,WAAW,CAAC,GAAG,EAAE,CAAA;QACnB,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,eAAe;QACnB,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YAC5B,kDAAkD;YAClD,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YAC7C,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,IAAI,KAAK,CAAC,CAAA;YAC1C,OAAO,CAAC,GAAG,CACT,sBAAsB,UAAU,CAAC,IAAI,wBACnC,IAAI,CAAC,aACP,EAAE,CACH,CAAA;QACH,CAAC;QAAC,MAAM,CAAC;YACP,2DAA2D;YAC3D,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,CAAC,SAAS,EAAE,CAAC,CAAA;YACnD,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,CAAA;YAC/C,IAAI,CAAC,aAAa,GAAG,IAAI,CAAA;QAC3B,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,sBAAsB,CAAC,SAAiB;QAC5C,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,SAAS,CAAC,CAAA;YACvB,OAAO,CAAC,GAAG,CAAC,aAAa,SAAS,SAAS,CAAC,CAAA;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YAC3C,OAAO,CAAC,GAAG,CAAC,qBAAqB,SAAS,EAAE,CAAC,CAAA;QAC/C,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,37 @@
1
+ import { type MappedQueryResultProcessorParams, type QueryResultProcessor, type S3QueryResultProcessorParams } from './types.js';
2
+ /**
3
+ * Processes Athena query results stored in S3
4
+ */
5
+ export declare class S3QueryResultProcessor implements QueryResultProcessor {
6
+ #private;
7
+ private readonly config;
8
+ /**
9
+ * Creates a new S3QueryResultProcessor
10
+ * @param {S3QueryResultProcessorParams} config - Configuration parameters
11
+ */
12
+ constructor(config: S3QueryResultProcessorParams);
13
+ /**
14
+ * Processes query results from S3
15
+ * @param {string} queryExecutionId - The ID of the query execution
16
+ * @returns {Promise<void>} Promise that resolves when processing is complete
17
+ */
18
+ processResults(queryExecutionId: string): Promise<void>;
19
+ }
20
+ /**
21
+ * Processes Athena query results by mapping them to objects
22
+ */
23
+ export declare class MappedQueryResultProcessor implements QueryResultProcessor {
24
+ #private;
25
+ private readonly config;
26
+ /**
27
+ * Creates a new MappedQueryResultProcessor
28
+ * @param {MappedQueryResultProcessorParams} config - Configuration parameters
29
+ */
30
+ constructor(config: MappedQueryResultProcessorParams);
31
+ /**
32
+ * Processes query results by mapping them to objects
33
+ * @param {string} queryExecutionId - The ID of the query execution
34
+ * @returns {Promise<Record<string, string>[]>} Promise resolving to array of mapped objects
35
+ */
36
+ processResults(queryExecutionId: string): Promise<Record<string, string>[]>;
37
+ }
@@ -0,0 +1,231 @@
1
+ import { finished } from 'node:stream/promises';
2
+ import { GetQueryResultsCommand, } from '@aws-sdk/client-athena';
3
+ import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
4
+ import { parse } from 'csv-parse';
5
+ import { MAX_BATCH_SIZE, } from './types.js';
6
+ /**
7
+ * Processes Athena query results stored in S3
8
+ */
9
+ export class S3QueryResultProcessor {
10
+ config;
11
+ #batchSize = MAX_BATCH_SIZE;
12
+ #s3Client;
13
+ #s3OutputLocation;
14
+ /**
15
+ * Creates a new S3QueryResultProcessor
16
+ * @param {S3QueryResultProcessorParams} config - Configuration parameters
17
+ */
18
+ constructor(config) {
19
+ this.config = config;
20
+ this.#batchSize = this.#validateBatchSize(config.batchSize);
21
+ this.#s3OutputLocation = config.s3OutputLocation;
22
+ this.#s3Client = new S3Client({ region: config.s3Region });
23
+ }
24
+ /**
25
+ * Processes query results from S3
26
+ * @param {string} queryExecutionId - The ID of the query execution
27
+ * @returns {Promise<void>} Promise that resolves when processing is complete
28
+ */
29
+ async processResults(queryExecutionId) {
30
+ // Get the S3 location of results
31
+ const s3Location = `${this.#s3OutputLocation}/${queryExecutionId}.csv`;
32
+ const { Bucket, Key } = this.#parseS3Url(s3Location);
33
+ console.log('Fetching query results from S3:', s3Location);
34
+ const responseStream = await this.#fetchS3Object(Bucket, Key);
35
+ return this.#processStreamingResults(responseStream);
36
+ }
37
+ /**
38
+ * Processes streaming results from S3
39
+ * @param {Readable} stream - The readable stream of results
40
+ * @returns {Promise<void>} Promise that resolves when processing is complete
41
+ */
42
+ async #processStreamingResults(stream) {
43
+ const batch = [];
44
+ let totalProcessed = 0;
45
+ const parser = stream.pipe(parse({ ...(this.config.csvParseOptions ?? {}) }));
46
+ try {
47
+ parser.on('readable', async () => {
48
+ const record = parser.read();
49
+ while (record !== null) {
50
+ // Work with each record
51
+ batch.push(record);
52
+ if (batch.length >= this.#batchSize) {
53
+ console.log(`Processing batch of ${batch.length} records...`);
54
+ await this.#processBatch(batch);
55
+ totalProcessed += batch.length;
56
+ batch.length = 0; // Clear array efficiently
57
+ console.log(`Total records processed: ${totalProcessed}`);
58
+ }
59
+ }
60
+ });
61
+ // Wait for parsing to complete
62
+ await finished(parser);
63
+ // Process remaining records
64
+ if (batch.length > 0) {
65
+ console.log(`Processing final batch of ${batch.length} records...`);
66
+ await this.#processBatch(batch);
67
+ totalProcessed += batch.length;
68
+ console.log(`Final total records processed: ${totalProcessed}`);
69
+ }
70
+ await this.config?.onComplete?.();
71
+ }
72
+ catch (error) {
73
+ throw new Error(`Error processing results: ${error instanceof Error ? error.message : 'Unknown error'}`);
74
+ }
75
+ }
76
+ /**
77
+ * Processes a batch of records
78
+ * @param {unknown[]} batch - Array of records to process
79
+ * @returns {Promise<void>} Promise that resolves when batch is processed
80
+ */
81
+ async #processBatch(batch) {
82
+ try {
83
+ await this.config.onData(batch);
84
+ }
85
+ catch (error) {
86
+ throw new Error(`Error processing batch: ${error instanceof Error ? error.message : 'Unknown error'}`);
87
+ }
88
+ }
89
+ /**
90
+ * Validates the batch size configuration
91
+ * @param {number} [batchSize] - The batch size to validate
92
+ * @returns {number} The validated batch size
93
+ * @throws {Error} If batch size exceeds maximum allowed
94
+ */
95
+ #validateBatchSize(batchSize) {
96
+ if (!batchSize)
97
+ return MAX_BATCH_SIZE;
98
+ if (batchSize > MAX_BATCH_SIZE) {
99
+ throw new Error(`Batch size cannot be greater than ${MAX_BATCH_SIZE}`);
100
+ }
101
+ return batchSize;
102
+ }
103
+ /**
104
+ * Fetches an object from S3
105
+ * @param {string} Bucket - The S3 bucket name
106
+ * @param {string} Key - The S3 object key
107
+ * @returns {Promise<Readable>} Promise resolving to readable stream
108
+ * @throws {Error} If object fetch fails
109
+ */
110
+ async #fetchS3Object(Bucket, Key) {
111
+ const response = await this.#s3Client.send(new GetObjectCommand({ Bucket, Key }));
112
+ if (!response.Body) {
113
+ throw new Error(`Failed to fetch file: ${Bucket}/${Key}`);
114
+ }
115
+ console.log('S3 response metadata:', response.$metadata);
116
+ return response.Body;
117
+ }
118
+ /**
119
+ * Parses an S3 URL into bucket and key components
120
+ * @param {string} url - The S3 URL to parse
121
+ * @returns {{ Bucket: string; Key: string }} Object containing bucket and key
122
+ * @throws {Error} If URL is invalid or missing bucket name
123
+ */
124
+ #parseS3Url(url) {
125
+ try {
126
+ const parsedUrl = new URL(url);
127
+ const bucket = parsedUrl.hostname.split('.')[0];
128
+ if (!bucket) {
129
+ throw new Error('Invalid S3 URL: missing bucket name');
130
+ }
131
+ return {
132
+ Bucket: bucket,
133
+ Key: parsedUrl.pathname.slice(1),
134
+ };
135
+ }
136
+ catch (error) {
137
+ throw new Error(`Invalid S3 URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
138
+ }
139
+ }
140
+ }
141
+ /**
142
+ * Processes Athena query results by mapping them to objects
143
+ */
144
+ export class MappedQueryResultProcessor {
145
+ config;
146
+ /**
147
+ * Creates a new MappedQueryResultProcessor
148
+ * @param {MappedQueryResultProcessorParams} config - Configuration parameters
149
+ */
150
+ constructor(config) {
151
+ this.config = config;
152
+ }
153
+ /**
154
+ * Processes query results by mapping them to objects
155
+ * @param {string} queryExecutionId - The ID of the query execution
156
+ * @returns {Promise<Record<string, string>[]>} Promise resolving to array of mapped objects
157
+ */
158
+ async processResults(queryExecutionId) {
159
+ console.log('Fetching results for QueryExecutionId:', queryExecutionId);
160
+ try {
161
+ const resultSet = await this.#fetchQueryResults(queryExecutionId);
162
+ return this.#extractRows(resultSet);
163
+ }
164
+ catch (error) {
165
+ throw new Error(`Error processing results: ${error instanceof Error ? error.message : 'Unknown error'}`);
166
+ }
167
+ }
168
+ /**
169
+ * Fetches query results from Athena
170
+ * @param {string} queryExecutionId - The ID of the query execution
171
+ * @returns {Promise<ResultSet>} Promise resolving to Athena ResultSet
172
+ * @throws {Error} If results are empty or undefined
173
+ */
174
+ async #fetchQueryResults(queryExecutionId) {
175
+ const response = await this.config.athenaClient.send(new GetQueryResultsCommand({ QueryExecutionId: queryExecutionId }));
176
+ if (!response.ResultSet) {
177
+ throw new Error('Query results are empty or undefined');
178
+ }
179
+ return response.ResultSet;
180
+ }
181
+ /**
182
+ * Extracts column headers from result rows
183
+ * @param {Row[]} rows - Array of result rows
184
+ * @returns {string[]} Array of header names
185
+ * @throws {Error} If no headers are found
186
+ */
187
+ #extractHeaders(rows) {
188
+ const headers = rows[0]?.Data?.map((column) => column.VarCharValue || '');
189
+ if (!headers || headers.length === 0) {
190
+ throw new Error('No headers found in the result set');
191
+ }
192
+ return headers;
193
+ }
194
+ /**
195
+ * Maps a row to an object using column headers as keys
196
+ * @param {Row} row - The row to map
197
+ * @param {string[]} headers - Array of column headers
198
+ * @returns {Record<string, string>} Object with header-value pairs
199
+ */
200
+ #mapRowToObject(row, headers) {
201
+ const mappedRow = {};
202
+ row.Data?.forEach((value, index) => {
203
+ const header = headers[index];
204
+ if (header) {
205
+ mappedRow[header] = value.VarCharValue || '';
206
+ }
207
+ });
208
+ return mappedRow;
209
+ }
210
+ /**
211
+ * Extracts Athena ResultSet data into an array of key-value objects
212
+ * Extracts Athena ResultSet data into an array of key-value objects
213
+ * @private
214
+ * @param {ResultSet} resultSet - The ResultSet object returned by Athena
215
+ * @returns {Record<string, string>[]} Array of objects representing query rows
216
+ * @throws {Error} If no headers are found
217
+ */
218
+ #extractRows(resultSet) {
219
+ const { Rows } = resultSet;
220
+ if (!Rows || Rows.length === 0) {
221
+ console.error('No rows found in result set');
222
+ return [];
223
+ }
224
+ const headers = this.#extractHeaders(Rows);
225
+ if (!headers.length) {
226
+ throw new Error('No headers found in the result set');
227
+ }
228
+ return Rows.slice(1).map((row) => this.#mapRowToObject(row, headers));
229
+ }
230
+ }
231
+ //# sourceMappingURL=query-results-processor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-results-processor.js","sourceRoot":"","sources":["../src/query-results-processor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EACL,sBAAsB,GAGvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AACjC,OAAO,EACL,cAAc,GAIf,MAAM,YAAY,CAAA;AAEnB;;GAEG;AACH,MAAM,OAAO,sBAAsB;IASJ;IARpB,UAAU,GAAG,cAAc,CAAA;IAC3B,SAAS,CAAU;IACnB,iBAAiB,CAAQ;IAElC;;;OAGG;IACH,YAA6B,MAAoC;QAApC,WAAM,GAAN,MAAM,CAA8B;QAC/D,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QAC3D,IAAI,CAAC,iBAAiB,GAAG,MAAM,CAAC,gBAAgB,CAAA;QAChD,IAAI,CAAC,SAAS,GAAG,IAAI,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;IAC5D,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,cAAc,CAAC,gBAAwB;QAC3C,iCAAiC;QACjC,MAAM,UAAU,GAAG,GAAG,IAAI,CAAC,iBAAiB,IAAI,gBAAgB,MAAM,CAAA;QACtE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,CAAA;QACpD,OAAO,CAAC,GAAG,CAAC,iCAAiC,EAAE,UAAU,CAAC,CAAA;QAE1D,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;QAC7D,OAAO,IAAI,CAAC,wBAAwB,CAAC,cAAc,CAAC,CAAA;IACtD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,wBAAwB,CAAC,MAAgB;QAC7C,MAAM,KAAK,GAAc,EAAE,CAAA;QAC3B,IAAI,cAAc,GAAG,CAAC,CAAA;QAEtB,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CACxB,KAAK,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,IAAI,EAAE,CAAC,EAAE,CAAC,CAClD,CAAA;QAED,IAAI,CAAC;YACH,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,KAAK,IAAI,EAAE;gBAC/B,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAA;gBAC5B,OAAO,MAAM,KAAK,IAAI,EAAE,CAAC;oBACvB,wBAAwB;oBACxB,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;oBAClB,IAAI,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;wBACpC,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,CAAC,MAAM,aAAa,CAAC,CAAA;wBAC7D,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;wBAC/B,cAAc,IAAI,KAAK,CAAC,MAAM,CAAA;wBAC9B,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA,CAAC,0BAA0B;wBAC3C,OAAO,CAAC,GAAG,CAAC,4BAA4B,cAAc,EAAE,CAAC,CAAA;oBAC3D,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAA;YAEF,+BAA+B;YAC/B,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAA;YACtB,4BAA4B;YAC5B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrB,OAAO,CAAC,GAAG,CAAC,6BAA6B,KAAK,CAAC,MAAM,aAAa,CAAC,CAAA;gBACnE,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;gBAC/B,cAAc,IAAI,KAAK,CAAC,MAAM,CAAA;gBAC9B,OAAO,CAAC,GAAG,CAAC,kCAAkC,cAAc,EAAE,CAAC,CAAA;YACjE,CAAC;YACD,MAAM,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,CAAA;QACnC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,6BACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAC3C,EAAE,CACH,CAAA;QACH,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,aAAa,CAAC,KAAgB;QAClC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACjC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,2BACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAC3C,EAAE,CACH,CAAA;QACH,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,kBAAkB,CAAC,SAAkB;QACnC,IAAI,CAAC,SAAS;YAAE,OAAO,cAAc,CAAA;QACrC,IAAI,SAAS,GAAG,cAAc,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,qCAAqC,cAAc,EAAE,CAAC,CAAA;QACxE,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,cAAc,CAAC,MAAc,EAAE,GAAW;QAC9C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CACxC,IAAI,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CACtC,CAAA;QAED,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,yBAAyB,MAAM,IAAI,GAAG,EAAE,CAAC,CAAA;QAC3D,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAA;QAExD,OAAO,QAAQ,CAAC,IAAgB,CAAA;IAClC,CAAC;IAED;;;;;OAKG;IACH,WAAW,CAAC,GAAW;QACrB,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;YAC9B,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;YAE/C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;YACxD,CAAC;YAED,OAAO;gBACL,MAAM,EAAE,MAAM;gBACd,GAAG,EAAE,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;aACjC,CAAA;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,mBACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAC3C,EAAE,CACH,CAAA;QACH,CAAC;IACH,CAAC;CACF;AAED;;GAEG;AACH,MAAM,OAAO,0BAA0B;IAKR;IAJ7B;;;OAGG;IACH,YAA6B,MAAwC;QAAxC,WAAM,GAAN,MAAM,CAAkC;IAAG,CAAC;IAEzE;;;;OAIG;IACH,KAAK,CAAC,cAAc,CAClB,gBAAwB;QAExB,OAAO,CAAC,GAAG,CAAC,wCAAwC,EAAE,gBAAgB,CAAC,CAAA;QAEvE,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAA;YACjE,OAAO,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAA;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,6BACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAC3C,EAAE,CACH,CAAA;QACH,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,kBAAkB,CAAC,gBAAwB;QAC/C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAClD,IAAI,sBAAsB,CAAC,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC,CACnE,CAAA;QAED,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAA;QACzD,CAAC;QAED,OAAO,QAAQ,CAAC,SAAS,CAAA;IAC3B,CAAC;IAED;;;;;OAKG;IACH,eAAe,CAAC,IAAW;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC,CAAA;QAEzE,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;QACvD,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;IAED;;;;;OAKG;IACH,eAAe,CAAC,GAAQ,EAAE,OAAiB;QACzC,MAAM,SAAS,GAA2B,EAAE,CAAA;QAE5C,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YACjC,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;YAC7B,IAAI,MAAM,EAAE,CAAC;gBACX,SAAS,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,YAAY,IAAI,EAAE,CAAA;YAC9C,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;;;;;OAOG;IACH,YAAY,CAAC,SAAoB;QAC/B,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAA;QAC1B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAA;YAC5C,OAAO,EAAE,CAAA;QACX,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;QAC1C,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;QACvD,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAA;IACvE,CAAC;CACF"}
@@ -0,0 +1,76 @@
1
+ import type { AthenaClient, AthenaClientConfig, ResultReuseConfiguration } from '@aws-sdk/client-athena';
2
+ import type { Options } from 'csv-parse';
3
+ /** Maximum number of records to process in a single batch */
4
+ export declare const MAX_BATCH_SIZE: number;
5
+ /**
6
+ * Configuration interface for AthenaQueryClient
7
+ * @interface AthenaQueryClientConfig
8
+ */
9
+ export interface AthenaQueryClientConfig {
10
+ /** AWS SDK Athena client configuration */
11
+ ClientConfig: AthenaClientConfig;
12
+ /** AWS Athena database name. The database must exist in the catalog */
13
+ Database: string;
14
+ /** AWS Athena data source catalog name */
15
+ Catalog: string;
16
+ /** The name of the workgroup in which the query is being started. Optional, defaults to 'primary' */
17
+ WorkGroup?: string;
18
+ /** Configuration for result reuse in Athena */
19
+ ResultReuseConfiguration?: ResultReuseConfiguration;
20
+ /** AWS region for S3 operations */
21
+ s3Region: string;
22
+ /** S3 location where query results will be stored */
23
+ s3OutputLocation: string;
24
+ }
25
+ /**
26
+ * Interface for processing Athena query results
27
+ */
28
+ export interface QueryResultProcessor {
29
+ /**
30
+ * Processes the results of an Athena query execution
31
+ * @param {string} queryExecutionId - The ID of the query execution
32
+ * @returns {Promise<unknown>} Promise resolving to the processed results
33
+ */
34
+ processResults(queryExecutionId: string): Promise<unknown>;
35
+ }
36
+ export interface S3QueryResultProcessorParams {
37
+ /**
38
+ * Callback function to process each batch of data
39
+ */
40
+ onData: (data: unknown[]) => Promise<void>;
41
+ /**
42
+ * Optional Callback function to call when processing is done
43
+ * @param data Optional data to pass to the callback function
44
+ * @returns Promise resolving to void
45
+ */
46
+ onComplete?: (data?: unknown) => Promise<void>;
47
+ /**
48
+ * Optional: The maximum number of records to process in a single batch
49
+ * @default 999
50
+ */
51
+ batchSize?: number;
52
+ /**
53
+ * Options for CSV parsing
54
+ * @default { columns: true }
55
+ */
56
+ csvParseOptions?: Options;
57
+ /** S3 location where query results are being stored */
58
+ s3OutputLocation: string;
59
+ /** AWS region for S3 operations */
60
+ s3Region: string;
61
+ }
62
+ export interface MappedQueryResultProcessorParams {
63
+ /**
64
+ *
65
+ */
66
+ athenaClient: AthenaClient;
67
+ }
68
+ /**
69
+ *@description Options for configuring the JsonFileAppender
70
+ */
71
+ export interface JsonFileAppenderOptions {
72
+ /** Name of the file to append to */
73
+ fileName: string;
74
+ /** Directory path where the file is located */
75
+ directory: string;
76
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ /** Maximum number of records to process in a single batch */
2
+ export const MAX_BATCH_SIZE = 999;
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,6DAA6D;AAC7D,MAAM,CAAC,MAAM,cAAc,GAAW,GAAG,CAAA"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@stilyng94/athena-query-client",
3
+ "version": "1.0.1",
4
+ "description": "An Athena client to query data from Athena utilizing Aws-S3 or direct results from Athena query",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "homepage": "https://github.com/stilyng94/athena-query-client",
8
+ "bugs": {
9
+ "url": "https://github.com/stilyng94/athena-query-client"
10
+ },
11
+ "keywords": [
12
+ "athena",
13
+ "s3",
14
+ "aws"
15
+ ],
16
+ "author": {
17
+ "name": "Paul Osei Kofi",
18
+ "email": "oseipaulkofi@gmail.com"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/stilyng94/athena-query-client.git"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@aws-sdk/client-athena": "^3.696.0",
30
+ "@aws-sdk/client-s3": "^3.717.0",
31
+ "csv-parse": "^5.6.0",
32
+ "rxjs": "^7.8.1"
33
+ },
34
+ "devDependencies": {
35
+ "@arethetypeswrong/cli": "^0.17.2",
36
+ "@biomejs/biome": "1.9.4",
37
+ "@changesets/cli": "^2.27.11",
38
+ "@total-typescript/tsconfig": "^1.0.4",
39
+ "@types/node": "^22.10.2",
40
+ "typescript": "^5.6.3"
41
+ },
42
+ "scripts": {
43
+ "test": "echo \"Error: no test specified\" && exit 1",
44
+ "build": "tsc",
45
+ "local-release": "changeset version && changeset publish",
46
+ "format-and-fix": "biome format --write .",
47
+ "lint-and-fix": "biome lint --write .",
48
+ "check": "biome check --write .",
49
+ "check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
50
+ "pre-ci": "pnpm format-and-fix && pnpm lint-and-fix && pnpm check",
51
+ "ci": "pnpm build && pnpm check-exports"
52
+ }
53
+ }