@owox/connectors 0.16.0-next-20251217153437 → 0.16.0-next-20251218103137
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/connector-runner.cjs +10 -0
- package/dist/connector-runner.js +10 -0
- package/dist/index.cjs +438 -0
- package/dist/index.js +438 -0
- package/package.json +1 -1
|
@@ -17,6 +17,12 @@ const {
|
|
|
17
17
|
ListObjectsV2Command,
|
|
18
18
|
ListBucketsCommand
|
|
19
19
|
} = require("@aws-sdk/client-s3");
|
|
20
|
+
const {
|
|
21
|
+
RedshiftDataClient,
|
|
22
|
+
ExecuteStatementCommand,
|
|
23
|
+
DescribeStatementCommand,
|
|
24
|
+
GetStatementResultCommand
|
|
25
|
+
} = require("@aws-sdk/client-redshift-data");
|
|
20
26
|
const { Upload } = require("@aws-sdk/lib-storage");
|
|
21
27
|
global.OWOX = OWOX;
|
|
22
28
|
global.AdmZip = AdmZip;
|
|
@@ -31,6 +37,10 @@ global.S3Client = S3Client;
|
|
|
31
37
|
global.DeleteObjectsCommand = DeleteObjectsCommand;
|
|
32
38
|
global.ListObjectsV2Command = ListObjectsV2Command;
|
|
33
39
|
global.ListBucketsCommand = ListBucketsCommand;
|
|
40
|
+
global.RedshiftDataClient = RedshiftDataClient;
|
|
41
|
+
global.ExecuteStatementCommand = ExecuteStatementCommand;
|
|
42
|
+
global.DescribeStatementCommand = DescribeStatementCommand;
|
|
43
|
+
global.GetStatementResultCommand = GetStatementResultCommand;
|
|
34
44
|
global.Upload = Upload;
|
|
35
45
|
const { Core, Connectors, Storages } = OWOX;
|
|
36
46
|
Object.keys(Core).forEach((key) => {
|
package/dist/connector-runner.js
CHANGED
|
@@ -16,6 +16,12 @@ const {
|
|
|
16
16
|
ListObjectsV2Command,
|
|
17
17
|
ListBucketsCommand
|
|
18
18
|
} = require("@aws-sdk/client-s3");
|
|
19
|
+
const {
|
|
20
|
+
RedshiftDataClient,
|
|
21
|
+
ExecuteStatementCommand,
|
|
22
|
+
DescribeStatementCommand,
|
|
23
|
+
GetStatementResultCommand
|
|
24
|
+
} = require("@aws-sdk/client-redshift-data");
|
|
19
25
|
const { Upload } = require("@aws-sdk/lib-storage");
|
|
20
26
|
global.OWOX = OWOX;
|
|
21
27
|
global.AdmZip = AdmZip;
|
|
@@ -30,6 +36,10 @@ global.S3Client = S3Client;
|
|
|
30
36
|
global.DeleteObjectsCommand = DeleteObjectsCommand;
|
|
31
37
|
global.ListObjectsV2Command = ListObjectsV2Command;
|
|
32
38
|
global.ListBucketsCommand = ListBucketsCommand;
|
|
39
|
+
global.RedshiftDataClient = RedshiftDataClient;
|
|
40
|
+
global.ExecuteStatementCommand = ExecuteStatementCommand;
|
|
41
|
+
global.DescribeStatementCommand = DescribeStatementCommand;
|
|
42
|
+
global.GetStatementResultCommand = GetStatementResultCommand;
|
|
33
43
|
global.Upload = Upload;
|
|
34
44
|
const { Core, Connectors, Storages } = OWOX;
|
|
35
45
|
Object.keys(Core).forEach((key) => {
|
package/dist/index.cjs
CHANGED
|
@@ -1679,6 +1679,8 @@ class StorageConfigDto {
|
|
|
1679
1679
|
return "AwsAthena";
|
|
1680
1680
|
} else if (name === "SNOWFLAKE") {
|
|
1681
1681
|
return "Snowflake";
|
|
1682
|
+
} else if (name === "AWS_REDSHIFT") {
|
|
1683
|
+
return "AwsRedshift";
|
|
1682
1684
|
}
|
|
1683
1685
|
return name;
|
|
1684
1686
|
}
|
|
@@ -3015,6 +3017,439 @@ OPTIONS(description="${this.description}")`;
|
|
|
3015
3017
|
manifest
|
|
3016
3018
|
};
|
|
3017
3019
|
})();
|
|
3020
|
+
const AwsRedshift = (function() {
|
|
3021
|
+
const { AbstractException: AbstractException2, HttpRequestException: HttpRequestException2, OauthFlowException: OauthFlowException2, AbstractStorage: AbstractStorage2, AbstractSource: AbstractSource3, AbstractRunConfig: AbstractRunConfig3, AbstractConnector: AbstractConnector3, AbstractConfig: AbstractConfig2, HttpUtils: HttpUtils3, FileUtils: FileUtils3, DateUtils: DateUtils3, CryptoUtils: CryptoUtils3, AsyncUtils: AsyncUtils3, RunConfigDto: RunConfigDto2, OauthCredentialsDto: OauthCredentialsDto2, OauthCredentialsDtoBuilder: OauthCredentialsDtoBuilder2, SourceConfigDto: SourceConfigDto2, StorageConfigDto: StorageConfigDto2, ConfigDto: ConfigDto2, HTTP_STATUS: HTTP_STATUS2, EXECUTION_STATUS: EXECUTION_STATUS2, RUN_CONFIG_TYPE: RUN_CONFIG_TYPE2, CONFIG_ATTRIBUTES: CONFIG_ATTRIBUTES2, OAUTH_CONSTANTS: OAUTH_CONSTANTS2, OAUTH_SOURCE_CREDENTIALS_KEY: OAUTH_SOURCE_CREDENTIALS_KEY2 } = Core;
|
|
3022
|
+
var AwsRedshiftStorage = class AwsRedshiftStorage extends AbstractStorage2 {
|
|
3023
|
+
//---- constructor -------------------------------------------------
|
|
3024
|
+
/**
|
|
3025
|
+
* Class for managing data in AWS Redshift using Data API
|
|
3026
|
+
*
|
|
3027
|
+
* @param config (object) instance of AbstractConfig
|
|
3028
|
+
* @param uniqueKeyColumns (mixed) a name of column with unique key or array with columns names
|
|
3029
|
+
* @param schema (object) object with structure like {fieldName: {type: "string", description: "smth" } }
|
|
3030
|
+
* @param description (string) string with storage description
|
|
3031
|
+
*/
|
|
3032
|
+
constructor(config, uniqueKeyColumns, schema = null, description = null) {
|
|
3033
|
+
super(
|
|
3034
|
+
config.mergeParameters({
|
|
3035
|
+
AWSRegion: {
|
|
3036
|
+
isRequired: true,
|
|
3037
|
+
requiredType: "string"
|
|
3038
|
+
},
|
|
3039
|
+
AWSAccessKeyId: {
|
|
3040
|
+
isRequired: true,
|
|
3041
|
+
requiredType: "string"
|
|
3042
|
+
},
|
|
3043
|
+
AWSSecretAccessKey: {
|
|
3044
|
+
isRequired: true,
|
|
3045
|
+
requiredType: "string"
|
|
3046
|
+
},
|
|
3047
|
+
Database: {
|
|
3048
|
+
isRequired: true,
|
|
3049
|
+
requiredType: "string"
|
|
3050
|
+
},
|
|
3051
|
+
WorkgroupName: {
|
|
3052
|
+
isRequired: false,
|
|
3053
|
+
requiredType: "string"
|
|
3054
|
+
},
|
|
3055
|
+
ClusterIdentifier: {
|
|
3056
|
+
isRequired: false,
|
|
3057
|
+
requiredType: "string"
|
|
3058
|
+
},
|
|
3059
|
+
Schema: {
|
|
3060
|
+
isRequired: true,
|
|
3061
|
+
requiredType: "string"
|
|
3062
|
+
},
|
|
3063
|
+
DestinationTableName: {
|
|
3064
|
+
isRequired: true,
|
|
3065
|
+
requiredType: "string"
|
|
3066
|
+
},
|
|
3067
|
+
MaxBufferSize: {
|
|
3068
|
+
isRequired: true,
|
|
3069
|
+
default: 250
|
|
3070
|
+
}
|
|
3071
|
+
}),
|
|
3072
|
+
uniqueKeyColumns,
|
|
3073
|
+
schema,
|
|
3074
|
+
description
|
|
3075
|
+
);
|
|
3076
|
+
this.initAWS();
|
|
3077
|
+
this.updatedRecordsBuffer = {};
|
|
3078
|
+
this.existingColumns = {};
|
|
3079
|
+
}
|
|
3080
|
+
//---- init --------------------------------------------------------
|
|
3081
|
+
/**
|
|
3082
|
+
* Initializing storage
|
|
3083
|
+
*/
|
|
3084
|
+
async init() {
|
|
3085
|
+
await this.checkConnection();
|
|
3086
|
+
this.config.logMessage("Connection to Redshift established");
|
|
3087
|
+
}
|
|
3088
|
+
//----------------------------------------------------------------
|
|
3089
|
+
//---- initAWS ----------------------------------------------------
|
|
3090
|
+
/**
|
|
3091
|
+
* Initialize AWS SDK clients
|
|
3092
|
+
*/
|
|
3093
|
+
initAWS() {
|
|
3094
|
+
try {
|
|
3095
|
+
const clientConfig = {
|
|
3096
|
+
region: this.config.AWSRegion.value,
|
|
3097
|
+
credentials: {
|
|
3098
|
+
accessKeyId: this.config.AWSAccessKeyId.value,
|
|
3099
|
+
secretAccessKey: this.config.AWSSecretAccessKey.value
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
this.redshiftDataClient = new RedshiftDataClient(clientConfig);
|
|
3103
|
+
this.config.logMessage("AWS SDK initialized successfully");
|
|
3104
|
+
} catch (error) {
|
|
3105
|
+
throw new Error(`Failed to initialize AWS SDK: ${error.message}`);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
//----------------------------------------------------------------
|
|
3109
|
+
//---- checkConnection ---------------------------------------------
|
|
3110
|
+
/**
|
|
3111
|
+
* Check connection to Redshift
|
|
3112
|
+
* @returns {Promise}
|
|
3113
|
+
*/
|
|
3114
|
+
async checkConnection() {
|
|
3115
|
+
const params = {
|
|
3116
|
+
Sql: "SELECT 1",
|
|
3117
|
+
Database: this.config.Database.value
|
|
3118
|
+
};
|
|
3119
|
+
if (this.config.WorkgroupName.value) {
|
|
3120
|
+
params.WorkgroupName = this.config.WorkgroupName.value;
|
|
3121
|
+
} else if (this.config.ClusterIdentifier.value) {
|
|
3122
|
+
params.ClusterIdentifier = this.config.ClusterIdentifier.value;
|
|
3123
|
+
} else {
|
|
3124
|
+
throw new Error("Either WorkgroupName or ClusterIdentifier must be provided");
|
|
3125
|
+
}
|
|
3126
|
+
try {
|
|
3127
|
+
const command = new ExecuteStatementCommand(params);
|
|
3128
|
+
const response = await this.redshiftDataClient.send(command);
|
|
3129
|
+
await this.waitForQueryCompletion(response.Id);
|
|
3130
|
+
return true;
|
|
3131
|
+
} catch (error) {
|
|
3132
|
+
throw new Error(`Connection check failed: ${error.message}`);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
//----------------------------------------------------------------
|
|
3136
|
+
//---- executeQuery -----------------------------------------------
|
|
3137
|
+
/**
|
|
3138
|
+
* Execute SQL query using Redshift Data API
|
|
3139
|
+
* @param {string} sql - SQL query to execute
|
|
3140
|
+
* @param {string} type - Query type ('ddl' or 'dml')
|
|
3141
|
+
* @returns {Promise}
|
|
3142
|
+
*/
|
|
3143
|
+
async executeQuery(sql, type = "dml") {
|
|
3144
|
+
const params = {
|
|
3145
|
+
Sql: sql,
|
|
3146
|
+
Database: this.config.Database.value
|
|
3147
|
+
};
|
|
3148
|
+
if (this.config.WorkgroupName.value) {
|
|
3149
|
+
params.WorkgroupName = this.config.WorkgroupName.value;
|
|
3150
|
+
} else if (this.config.ClusterIdentifier.value) {
|
|
3151
|
+
params.ClusterIdentifier = this.config.ClusterIdentifier.value;
|
|
3152
|
+
}
|
|
3153
|
+
try {
|
|
3154
|
+
const command = new ExecuteStatementCommand(params);
|
|
3155
|
+
const response = await this.redshiftDataClient.send(command);
|
|
3156
|
+
await this.waitForQueryCompletion(response.Id);
|
|
3157
|
+
return response.Id;
|
|
3158
|
+
} catch (error) {
|
|
3159
|
+
this.config.logMessage(`Query execution failed: ${error.message}`, "error");
|
|
3160
|
+
throw error;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
//----------------------------------------------------------------
|
|
3164
|
+
//---- waitForQueryCompletion -------------------------------------
|
|
3165
|
+
/**
|
|
3166
|
+
* Wait for query execution to complete
|
|
3167
|
+
* @param {string} statementId - Statement ID to check
|
|
3168
|
+
* @returns {Promise}
|
|
3169
|
+
*/
|
|
3170
|
+
async waitForQueryCompletion(statementId) {
|
|
3171
|
+
const maxAttempts = 300;
|
|
3172
|
+
let attempts = 0;
|
|
3173
|
+
while (attempts < maxAttempts) {
|
|
3174
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3175
|
+
const describeCommand = new DescribeStatementCommand({ Id: statementId });
|
|
3176
|
+
const response = await this.redshiftDataClient.send(describeCommand);
|
|
3177
|
+
const status = response.Status;
|
|
3178
|
+
if (status === "FINISHED") {
|
|
3179
|
+
return;
|
|
3180
|
+
} else if (status === "FAILED" || status === "ABORTED") {
|
|
3181
|
+
const error = response.Error || "Unknown error";
|
|
3182
|
+
throw new Error(`Query ${status}: ${error}`);
|
|
3183
|
+
}
|
|
3184
|
+
attempts++;
|
|
3185
|
+
}
|
|
3186
|
+
throw new Error(`Query timeout after ${maxAttempts} seconds for statement ${statementId}`);
|
|
3187
|
+
}
|
|
3188
|
+
//----------------------------------------------------------------
|
|
3189
|
+
//---- getColumnType ----------------------------------------------
|
|
3190
|
+
/**
|
|
3191
|
+
* Get Redshift column type for a field
|
|
3192
|
+
* @param {string} columnName - Column name
|
|
3193
|
+
* @returns {string} - Redshift column type
|
|
3194
|
+
*/
|
|
3195
|
+
getColumnType(columnName) {
|
|
3196
|
+
const field = this.schema[columnName];
|
|
3197
|
+
if (!field) {
|
|
3198
|
+
return "VARCHAR(65535)";
|
|
3199
|
+
}
|
|
3200
|
+
if (field.RedshiftType) {
|
|
3201
|
+
return field.RedshiftType;
|
|
3202
|
+
}
|
|
3203
|
+
const type = field.type ? field.type.toLowerCase() : "string";
|
|
3204
|
+
switch (type) {
|
|
3205
|
+
case "integer":
|
|
3206
|
+
case "int":
|
|
3207
|
+
return "BIGINT";
|
|
3208
|
+
case "float":
|
|
3209
|
+
case "number":
|
|
3210
|
+
return "DOUBLE PRECISION";
|
|
3211
|
+
case "boolean":
|
|
3212
|
+
case "bool":
|
|
3213
|
+
return "BOOLEAN";
|
|
3214
|
+
case "date":
|
|
3215
|
+
return "DATE";
|
|
3216
|
+
case "datetime":
|
|
3217
|
+
case "timestamp":
|
|
3218
|
+
return "TIMESTAMP";
|
|
3219
|
+
case "json":
|
|
3220
|
+
return "SUPER";
|
|
3221
|
+
default:
|
|
3222
|
+
return "VARCHAR(65535)";
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
//----------------------------------------------------------------
|
|
3226
|
+
//---- createSchemaIfNotExist -------------------------------------
|
|
3227
|
+
/**
|
|
3228
|
+
* Create schema in Redshift if it doesn't exist
|
|
3229
|
+
* @returns {Promise}
|
|
3230
|
+
*/
|
|
3231
|
+
async createSchemaIfNotExist() {
|
|
3232
|
+
const schemaName = this.config.Schema.value;
|
|
3233
|
+
if (!schemaName) {
|
|
3234
|
+
throw new Error("Schema name is required but not provided");
|
|
3235
|
+
}
|
|
3236
|
+
const createSchemaQuery = `CREATE SCHEMA IF NOT EXISTS "${schemaName}"`;
|
|
3237
|
+
await this.executeQuery(createSchemaQuery, "ddl");
|
|
3238
|
+
this.config.logMessage(`Schema "${schemaName}" ensured`);
|
|
3239
|
+
}
|
|
3240
|
+
//----------------------------------------------------------------
|
|
3241
|
+
//---- createTable ------------------------------------------------
|
|
3242
|
+
/**
|
|
3243
|
+
* Create table in Redshift
|
|
3244
|
+
* @returns {Promise}
|
|
3245
|
+
*/
|
|
3246
|
+
async createTable() {
|
|
3247
|
+
await this.createSchemaIfNotExist();
|
|
3248
|
+
const existingColumns = {};
|
|
3249
|
+
const columnDefinitions = [];
|
|
3250
|
+
for (let columnName of this.uniqueKeyColumns) {
|
|
3251
|
+
let columnType = this.getColumnType(columnName);
|
|
3252
|
+
columnDefinitions.push(`"${columnName}" ${columnType}`);
|
|
3253
|
+
existingColumns[columnName] = columnType;
|
|
3254
|
+
}
|
|
3255
|
+
let selectedFields = this.getSelectedFields();
|
|
3256
|
+
for (let columnName in this.schema) {
|
|
3257
|
+
if (!this.uniqueKeyColumns.includes(columnName) && selectedFields.includes(columnName)) {
|
|
3258
|
+
let columnType = this.getColumnType(columnName);
|
|
3259
|
+
columnDefinitions.push(`"${columnName}" ${columnType}`);
|
|
3260
|
+
existingColumns[columnName] = columnType;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
const pkColumns = this.uniqueKeyColumns.map((col) => `"${col}"`).join(", ");
|
|
3264
|
+
const query = `
|
|
3265
|
+
CREATE TABLE IF NOT EXISTS "${this.config.Schema.value}"."${this.config.DestinationTableName.value}" (
|
|
3266
|
+
${columnDefinitions.join(",\n ")},
|
|
3267
|
+
PRIMARY KEY (${pkColumns})
|
|
3268
|
+
)
|
|
3269
|
+
`;
|
|
3270
|
+
await this.executeQuery(query, "ddl");
|
|
3271
|
+
this.config.logMessage(`Table "${this.config.Schema.value}"."${this.config.DestinationTableName.value}" created`);
|
|
3272
|
+
this.existingColumns = existingColumns;
|
|
3273
|
+
return existingColumns;
|
|
3274
|
+
}
|
|
3275
|
+
//----------------------------------------------------------------
|
|
3276
|
+
//---- addNewColumns ----------------------------------------------
|
|
3277
|
+
/**
|
|
3278
|
+
* Add new columns to the Redshift table
|
|
3279
|
+
* @param {Array} newColumns - Array of column names to add
|
|
3280
|
+
* @returns {Promise}
|
|
3281
|
+
*/
|
|
3282
|
+
async addNewColumns(newColumns) {
|
|
3283
|
+
const columnsToAdd = [];
|
|
3284
|
+
for (let columnName of newColumns) {
|
|
3285
|
+
if (columnName in this.schema) {
|
|
3286
|
+
let columnType = this.getColumnType(columnName);
|
|
3287
|
+
columnsToAdd.push(`"${columnName}" ${columnType}`);
|
|
3288
|
+
this.existingColumns[columnName] = columnType;
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
if (columnsToAdd.length > 0) {
|
|
3292
|
+
const query = `
|
|
3293
|
+
ALTER TABLE "${this.config.Schema.value}"."${this.config.DestinationTableName.value}"
|
|
3294
|
+
ADD COLUMN ${columnsToAdd.join(", ADD COLUMN ")}
|
|
3295
|
+
`;
|
|
3296
|
+
await this.executeQuery(query, "ddl");
|
|
3297
|
+
this.config.logMessage(`Columns '${newColumns.join(",")}' were added to "${this.config.Schema.value}"."${this.config.DestinationTableName.value}" table`);
|
|
3298
|
+
return newColumns;
|
|
3299
|
+
}
|
|
3300
|
+
return newColumns;
|
|
3301
|
+
}
|
|
3302
|
+
//----------------------------------------------------------------
|
|
3303
|
+
//---- saveData ---------------------------------------------------
|
|
3304
|
+
/**
|
|
3305
|
+
* Saving data to Redshift using MERGE
|
|
3306
|
+
* @param {Array} data - Array of objects with records to save
|
|
3307
|
+
* @returns {Promise}
|
|
3308
|
+
*/
|
|
3309
|
+
async saveData(data) {
|
|
3310
|
+
if (!data || data.length === 0) {
|
|
3311
|
+
return Promise.resolve();
|
|
3312
|
+
}
|
|
3313
|
+
if (Object.keys(this.existingColumns).length === 0) {
|
|
3314
|
+
await this.createTable();
|
|
3315
|
+
}
|
|
3316
|
+
const dataKeys = Object.keys(data[0]);
|
|
3317
|
+
const newColumns = dataKeys.filter((key) => !(key in this.existingColumns));
|
|
3318
|
+
if (newColumns.length > 0) {
|
|
3319
|
+
await this.addNewColumns(newColumns);
|
|
3320
|
+
}
|
|
3321
|
+
const selectedFields = this.getSelectedFields();
|
|
3322
|
+
const columnsToInsert = dataKeys.filter((key) => selectedFields.includes(key));
|
|
3323
|
+
const tempTableName = `temp_${this.config.DestinationTableName.value}_${Date.now()}`;
|
|
3324
|
+
const schemaName = this.config.Schema.value;
|
|
3325
|
+
if (!schemaName) {
|
|
3326
|
+
throw new Error("Schema name is required but not provided");
|
|
3327
|
+
}
|
|
3328
|
+
const tempColumns = columnsToInsert.map(
|
|
3329
|
+
(col) => `"${col}" ${this.existingColumns[col] || this.getColumnType(col)}`
|
|
3330
|
+
).join(", ");
|
|
3331
|
+
await this.executeQuery(`
|
|
3332
|
+
CREATE TABLE "${schemaName}"."${tempTableName}" (${tempColumns})
|
|
3333
|
+
`, "ddl");
|
|
3334
|
+
try {
|
|
3335
|
+
const batchSize = this.config.MaxBufferSize.value;
|
|
3336
|
+
for (let i = 0; i < data.length; i += batchSize) {
|
|
3337
|
+
const batch = data.slice(i, i + batchSize);
|
|
3338
|
+
await this.insertBatch(tempTableName, columnsToInsert, batch);
|
|
3339
|
+
}
|
|
3340
|
+
await this.mergeTempTable(tempTableName, columnsToInsert);
|
|
3341
|
+
} finally {
|
|
3342
|
+
try {
|
|
3343
|
+
await this.executeQuery(`DROP TABLE "${schemaName}"."${tempTableName}"`, "ddl");
|
|
3344
|
+
this.config.logMessage(`Temp table "${tempTableName}" cleaned up`);
|
|
3345
|
+
} catch (dropError) {
|
|
3346
|
+
this.config.logMessage(`Warning: Failed to drop temp table "${tempTableName}": ${dropError.message}`);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
this.config.logMessage(`Successfully saved ${data.length} records`);
|
|
3350
|
+
return data.length;
|
|
3351
|
+
}
|
|
3352
|
+
//----------------------------------------------------------------
|
|
3353
|
+
//---- insertBatch ------------------------------------------------
|
|
3354
|
+
/**
|
|
3355
|
+
* Insert batch of records into temp table
|
|
3356
|
+
* @param {string} tableName - Table name
|
|
3357
|
+
* @param {Array} columns - Column names
|
|
3358
|
+
* @param {Array} records - Records to insert
|
|
3359
|
+
* @returns {Promise}
|
|
3360
|
+
*/
|
|
3361
|
+
async insertBatch(tableName, columns, records) {
|
|
3362
|
+
const values = records.map((record) => {
|
|
3363
|
+
const vals = columns.map((col) => {
|
|
3364
|
+
const value = record[col];
|
|
3365
|
+
if (value === null || value === void 0) {
|
|
3366
|
+
return "NULL";
|
|
3367
|
+
}
|
|
3368
|
+
if (typeof value === "boolean") {
|
|
3369
|
+
return value ? "TRUE" : "FALSE";
|
|
3370
|
+
}
|
|
3371
|
+
if (typeof value === "number") {
|
|
3372
|
+
if (isNaN(value) || !isFinite(value)) {
|
|
3373
|
+
return "NULL";
|
|
3374
|
+
}
|
|
3375
|
+
return value;
|
|
3376
|
+
}
|
|
3377
|
+
const stringValue = String(value).replace(/'/g, "''");
|
|
3378
|
+
return `'${stringValue}'`;
|
|
3379
|
+
}).join(", ");
|
|
3380
|
+
return `(${vals})`;
|
|
3381
|
+
}).join(",\n ");
|
|
3382
|
+
const columnList = columns.map((col) => `"${col}"`).join(", ");
|
|
3383
|
+
const query = `
|
|
3384
|
+
INSERT INTO "${this.config.Schema.value}"."${tableName}" (${columnList})
|
|
3385
|
+
VALUES ${values}
|
|
3386
|
+
`;
|
|
3387
|
+
await this.executeQuery(query, "dml");
|
|
3388
|
+
}
|
|
3389
|
+
//----------------------------------------------------------------
|
|
3390
|
+
//---- mergeTempTable ---------------------------------------------
|
|
3391
|
+
/**
|
|
3392
|
+
* Merge temp table data into main table
|
|
3393
|
+
* @param {string} tempTableName - Temp table name
|
|
3394
|
+
* @param {Array} columns - Column names
|
|
3395
|
+
* @returns {Promise}
|
|
3396
|
+
*/
|
|
3397
|
+
async mergeTempTable(tempTableName, columns) {
|
|
3398
|
+
const targetTable = `"${this.config.Schema.value}"."${this.config.DestinationTableName.value}"`;
|
|
3399
|
+
const sourceTable = `"${this.config.Schema.value}"."${tempTableName}"`;
|
|
3400
|
+
const onClause = this.uniqueKeyColumns.map(
|
|
3401
|
+
(col) => `${targetTable}."${col}" = ${sourceTable}."${col}"`
|
|
3402
|
+
).join(" AND ");
|
|
3403
|
+
const updateColumns = columns.filter((col) => !this.uniqueKeyColumns.includes(col));
|
|
3404
|
+
const updateSet = updateColumns.map(
|
|
3405
|
+
(col) => `"${col}" = ${sourceTable}."${col}"`
|
|
3406
|
+
).join(", ");
|
|
3407
|
+
const insertColumns = columns.map((col) => `"${col}"`).join(", ");
|
|
3408
|
+
const insertValues = columns.map((col) => `${sourceTable}."${col}"`).join(", ");
|
|
3409
|
+
const query = `
|
|
3410
|
+
MERGE INTO ${targetTable}
|
|
3411
|
+
USING ${sourceTable}
|
|
3412
|
+
ON ${onClause}
|
|
3413
|
+
WHEN MATCHED THEN
|
|
3414
|
+
UPDATE SET ${updateSet}
|
|
3415
|
+
WHEN NOT MATCHED THEN
|
|
3416
|
+
INSERT (${insertColumns})
|
|
3417
|
+
VALUES (${insertValues})
|
|
3418
|
+
`;
|
|
3419
|
+
await this.executeQuery(query, "dml");
|
|
3420
|
+
}
|
|
3421
|
+
//----------------------------------------------------------------
|
|
3422
|
+
};
|
|
3423
|
+
const manifest = {
|
|
3424
|
+
"name": "AwsRedshiftStorage",
|
|
3425
|
+
"description": "Storage for AWS Redshift using Data API with support for Serverless and Provisioned clusters",
|
|
3426
|
+
"title": "AWS Redshift",
|
|
3427
|
+
"version": "1.0.0",
|
|
3428
|
+
"author": "OWOX, Inc.",
|
|
3429
|
+
"license": "MIT",
|
|
3430
|
+
"environment": {
|
|
3431
|
+
"node": {
|
|
3432
|
+
"enabled": true,
|
|
3433
|
+
"dependencies": [
|
|
3434
|
+
{
|
|
3435
|
+
"name": "@aws-sdk/client-redshift-data",
|
|
3436
|
+
"version": "3.952.0",
|
|
3437
|
+
"global": [
|
|
3438
|
+
"RedshiftDataClient",
|
|
3439
|
+
"ExecuteStatementCommand",
|
|
3440
|
+
"DescribeStatementCommand",
|
|
3441
|
+
"GetStatementResultCommand"
|
|
3442
|
+
]
|
|
3443
|
+
}
|
|
3444
|
+
]
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
return {
|
|
3449
|
+
AwsRedshiftStorage,
|
|
3450
|
+
manifest
|
|
3451
|
+
};
|
|
3452
|
+
})();
|
|
3018
3453
|
const AwsAthena = (function() {
|
|
3019
3454
|
const { AbstractException: AbstractException2, HttpRequestException: HttpRequestException2, OauthFlowException: OauthFlowException2, AbstractStorage: AbstractStorage2, AbstractSource: AbstractSource3, AbstractRunConfig: AbstractRunConfig3, AbstractConnector: AbstractConnector3, AbstractConfig: AbstractConfig2, HttpUtils: HttpUtils3, FileUtils: FileUtils3, DateUtils: DateUtils3, CryptoUtils: CryptoUtils3, AsyncUtils: AsyncUtils3, RunConfigDto: RunConfigDto2, OauthCredentialsDto: OauthCredentialsDto2, OauthCredentialsDtoBuilder: OauthCredentialsDtoBuilder2, SourceConfigDto: SourceConfigDto2, StorageConfigDto: StorageConfigDto2, ConfigDto: ConfigDto2, HTTP_STATUS: HTTP_STATUS2, EXECUTION_STATUS: EXECUTION_STATUS2, RUN_CONFIG_TYPE: RUN_CONFIG_TYPE2, CONFIG_ATTRIBUTES: CONFIG_ATTRIBUTES2, OAUTH_CONSTANTS: OAUTH_CONSTANTS2, OAUTH_SOURCE_CREDENTIALS_KEY: OAUTH_SOURCE_CREDENTIALS_KEY2 } = Core;
|
|
3020
3455
|
var AwsAthenaStorage = class AwsAthenaStorage extends AbstractStorage2 {
|
|
@@ -3656,6 +4091,7 @@ const AwsAthena = (function() {
|
|
|
3656
4091
|
const Storages = {
|
|
3657
4092
|
Snowflake,
|
|
3658
4093
|
GoogleBigQuery,
|
|
4094
|
+
AwsRedshift,
|
|
3659
4095
|
AwsAthena
|
|
3660
4096
|
};
|
|
3661
4097
|
const XAds = (function() {
|
|
@@ -24581,6 +25017,7 @@ const AvailableConnectors = [
|
|
|
24581
25017
|
const AvailableStorages = [
|
|
24582
25018
|
"Snowflake",
|
|
24583
25019
|
"GoogleBigQuery",
|
|
25020
|
+
"AwsRedshift",
|
|
24584
25021
|
"AwsAthena"
|
|
24585
25022
|
];
|
|
24586
25023
|
const OWOX = {
|
|
@@ -24607,6 +25044,7 @@ const OWOX = {
|
|
|
24607
25044
|
// Individual storages
|
|
24608
25045
|
Snowflake,
|
|
24609
25046
|
GoogleBigQuery,
|
|
25047
|
+
AwsRedshift,
|
|
24610
25048
|
AwsAthena
|
|
24611
25049
|
};
|
|
24612
25050
|
if (typeof module !== "undefined" && module.exports) {
|
package/dist/index.js
CHANGED
|
@@ -1677,6 +1677,8 @@ class StorageConfigDto {
|
|
|
1677
1677
|
return "AwsAthena";
|
|
1678
1678
|
} else if (name === "SNOWFLAKE") {
|
|
1679
1679
|
return "Snowflake";
|
|
1680
|
+
} else if (name === "AWS_REDSHIFT") {
|
|
1681
|
+
return "AwsRedshift";
|
|
1680
1682
|
}
|
|
1681
1683
|
return name;
|
|
1682
1684
|
}
|
|
@@ -3013,6 +3015,439 @@ OPTIONS(description="${this.description}")`;
|
|
|
3013
3015
|
manifest
|
|
3014
3016
|
};
|
|
3015
3017
|
})();
|
|
3018
|
+
const AwsRedshift = (function() {
|
|
3019
|
+
const { AbstractException: AbstractException2, HttpRequestException: HttpRequestException2, OauthFlowException: OauthFlowException2, AbstractStorage: AbstractStorage2, AbstractSource: AbstractSource3, AbstractRunConfig: AbstractRunConfig3, AbstractConnector: AbstractConnector3, AbstractConfig: AbstractConfig2, HttpUtils: HttpUtils3, FileUtils: FileUtils3, DateUtils: DateUtils3, CryptoUtils: CryptoUtils3, AsyncUtils: AsyncUtils3, RunConfigDto: RunConfigDto2, OauthCredentialsDto: OauthCredentialsDto2, OauthCredentialsDtoBuilder: OauthCredentialsDtoBuilder2, SourceConfigDto: SourceConfigDto2, StorageConfigDto: StorageConfigDto2, ConfigDto: ConfigDto2, HTTP_STATUS: HTTP_STATUS2, EXECUTION_STATUS: EXECUTION_STATUS2, RUN_CONFIG_TYPE: RUN_CONFIG_TYPE2, CONFIG_ATTRIBUTES: CONFIG_ATTRIBUTES2, OAUTH_CONSTANTS: OAUTH_CONSTANTS2, OAUTH_SOURCE_CREDENTIALS_KEY: OAUTH_SOURCE_CREDENTIALS_KEY2 } = Core;
|
|
3020
|
+
var AwsRedshiftStorage = class AwsRedshiftStorage extends AbstractStorage2 {
|
|
3021
|
+
//---- constructor -------------------------------------------------
|
|
3022
|
+
/**
|
|
3023
|
+
* Class for managing data in AWS Redshift using Data API
|
|
3024
|
+
*
|
|
3025
|
+
* @param config (object) instance of AbstractConfig
|
|
3026
|
+
* @param uniqueKeyColumns (mixed) a name of column with unique key or array with columns names
|
|
3027
|
+
* @param schema (object) object with structure like {fieldName: {type: "string", description: "smth" } }
|
|
3028
|
+
* @param description (string) string with storage description
|
|
3029
|
+
*/
|
|
3030
|
+
constructor(config, uniqueKeyColumns, schema = null, description = null) {
|
|
3031
|
+
super(
|
|
3032
|
+
config.mergeParameters({
|
|
3033
|
+
AWSRegion: {
|
|
3034
|
+
isRequired: true,
|
|
3035
|
+
requiredType: "string"
|
|
3036
|
+
},
|
|
3037
|
+
AWSAccessKeyId: {
|
|
3038
|
+
isRequired: true,
|
|
3039
|
+
requiredType: "string"
|
|
3040
|
+
},
|
|
3041
|
+
AWSSecretAccessKey: {
|
|
3042
|
+
isRequired: true,
|
|
3043
|
+
requiredType: "string"
|
|
3044
|
+
},
|
|
3045
|
+
Database: {
|
|
3046
|
+
isRequired: true,
|
|
3047
|
+
requiredType: "string"
|
|
3048
|
+
},
|
|
3049
|
+
WorkgroupName: {
|
|
3050
|
+
isRequired: false,
|
|
3051
|
+
requiredType: "string"
|
|
3052
|
+
},
|
|
3053
|
+
ClusterIdentifier: {
|
|
3054
|
+
isRequired: false,
|
|
3055
|
+
requiredType: "string"
|
|
3056
|
+
},
|
|
3057
|
+
Schema: {
|
|
3058
|
+
isRequired: true,
|
|
3059
|
+
requiredType: "string"
|
|
3060
|
+
},
|
|
3061
|
+
DestinationTableName: {
|
|
3062
|
+
isRequired: true,
|
|
3063
|
+
requiredType: "string"
|
|
3064
|
+
},
|
|
3065
|
+
MaxBufferSize: {
|
|
3066
|
+
isRequired: true,
|
|
3067
|
+
default: 250
|
|
3068
|
+
}
|
|
3069
|
+
}),
|
|
3070
|
+
uniqueKeyColumns,
|
|
3071
|
+
schema,
|
|
3072
|
+
description
|
|
3073
|
+
);
|
|
3074
|
+
this.initAWS();
|
|
3075
|
+
this.updatedRecordsBuffer = {};
|
|
3076
|
+
this.existingColumns = {};
|
|
3077
|
+
}
|
|
3078
|
+
//---- init --------------------------------------------------------
|
|
3079
|
+
/**
|
|
3080
|
+
* Initializing storage
|
|
3081
|
+
*/
|
|
3082
|
+
async init() {
|
|
3083
|
+
await this.checkConnection();
|
|
3084
|
+
this.config.logMessage("Connection to Redshift established");
|
|
3085
|
+
}
|
|
3086
|
+
//----------------------------------------------------------------
|
|
3087
|
+
//---- initAWS ----------------------------------------------------
|
|
3088
|
+
/**
|
|
3089
|
+
* Initialize AWS SDK clients
|
|
3090
|
+
*/
|
|
3091
|
+
initAWS() {
|
|
3092
|
+
try {
|
|
3093
|
+
const clientConfig = {
|
|
3094
|
+
region: this.config.AWSRegion.value,
|
|
3095
|
+
credentials: {
|
|
3096
|
+
accessKeyId: this.config.AWSAccessKeyId.value,
|
|
3097
|
+
secretAccessKey: this.config.AWSSecretAccessKey.value
|
|
3098
|
+
}
|
|
3099
|
+
};
|
|
3100
|
+
this.redshiftDataClient = new RedshiftDataClient(clientConfig);
|
|
3101
|
+
this.config.logMessage("AWS SDK initialized successfully");
|
|
3102
|
+
} catch (error) {
|
|
3103
|
+
throw new Error(`Failed to initialize AWS SDK: ${error.message}`);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
//----------------------------------------------------------------
|
|
3107
|
+
//---- checkConnection ---------------------------------------------
|
|
3108
|
+
/**
|
|
3109
|
+
* Check connection to Redshift
|
|
3110
|
+
* @returns {Promise}
|
|
3111
|
+
*/
|
|
3112
|
+
async checkConnection() {
|
|
3113
|
+
const params = {
|
|
3114
|
+
Sql: "SELECT 1",
|
|
3115
|
+
Database: this.config.Database.value
|
|
3116
|
+
};
|
|
3117
|
+
if (this.config.WorkgroupName.value) {
|
|
3118
|
+
params.WorkgroupName = this.config.WorkgroupName.value;
|
|
3119
|
+
} else if (this.config.ClusterIdentifier.value) {
|
|
3120
|
+
params.ClusterIdentifier = this.config.ClusterIdentifier.value;
|
|
3121
|
+
} else {
|
|
3122
|
+
throw new Error("Either WorkgroupName or ClusterIdentifier must be provided");
|
|
3123
|
+
}
|
|
3124
|
+
try {
|
|
3125
|
+
const command = new ExecuteStatementCommand(params);
|
|
3126
|
+
const response = await this.redshiftDataClient.send(command);
|
|
3127
|
+
await this.waitForQueryCompletion(response.Id);
|
|
3128
|
+
return true;
|
|
3129
|
+
} catch (error) {
|
|
3130
|
+
throw new Error(`Connection check failed: ${error.message}`);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
//----------------------------------------------------------------
|
|
3134
|
+
//---- executeQuery -----------------------------------------------
|
|
3135
|
+
/**
|
|
3136
|
+
* Execute SQL query using Redshift Data API
|
|
3137
|
+
* @param {string} sql - SQL query to execute
|
|
3138
|
+
* @param {string} type - Query type ('ddl' or 'dml')
|
|
3139
|
+
* @returns {Promise}
|
|
3140
|
+
*/
|
|
3141
|
+
async executeQuery(sql, type = "dml") {
|
|
3142
|
+
const params = {
|
|
3143
|
+
Sql: sql,
|
|
3144
|
+
Database: this.config.Database.value
|
|
3145
|
+
};
|
|
3146
|
+
if (this.config.WorkgroupName.value) {
|
|
3147
|
+
params.WorkgroupName = this.config.WorkgroupName.value;
|
|
3148
|
+
} else if (this.config.ClusterIdentifier.value) {
|
|
3149
|
+
params.ClusterIdentifier = this.config.ClusterIdentifier.value;
|
|
3150
|
+
}
|
|
3151
|
+
try {
|
|
3152
|
+
const command = new ExecuteStatementCommand(params);
|
|
3153
|
+
const response = await this.redshiftDataClient.send(command);
|
|
3154
|
+
await this.waitForQueryCompletion(response.Id);
|
|
3155
|
+
return response.Id;
|
|
3156
|
+
} catch (error) {
|
|
3157
|
+
this.config.logMessage(`Query execution failed: ${error.message}`, "error");
|
|
3158
|
+
throw error;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
//----------------------------------------------------------------
|
|
3162
|
+
//---- waitForQueryCompletion -------------------------------------
|
|
3163
|
+
/**
|
|
3164
|
+
* Wait for query execution to complete
|
|
3165
|
+
* @param {string} statementId - Statement ID to check
|
|
3166
|
+
* @returns {Promise}
|
|
3167
|
+
*/
|
|
3168
|
+
async waitForQueryCompletion(statementId) {
|
|
3169
|
+
const maxAttempts = 300;
|
|
3170
|
+
let attempts = 0;
|
|
3171
|
+
while (attempts < maxAttempts) {
|
|
3172
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3173
|
+
const describeCommand = new DescribeStatementCommand({ Id: statementId });
|
|
3174
|
+
const response = await this.redshiftDataClient.send(describeCommand);
|
|
3175
|
+
const status = response.Status;
|
|
3176
|
+
if (status === "FINISHED") {
|
|
3177
|
+
return;
|
|
3178
|
+
} else if (status === "FAILED" || status === "ABORTED") {
|
|
3179
|
+
const error = response.Error || "Unknown error";
|
|
3180
|
+
throw new Error(`Query ${status}: ${error}`);
|
|
3181
|
+
}
|
|
3182
|
+
attempts++;
|
|
3183
|
+
}
|
|
3184
|
+
throw new Error(`Query timeout after ${maxAttempts} seconds for statement ${statementId}`);
|
|
3185
|
+
}
|
|
3186
|
+
//----------------------------------------------------------------
|
|
3187
|
+
//---- getColumnType ----------------------------------------------
|
|
3188
|
+
/**
|
|
3189
|
+
* Get Redshift column type for a field
|
|
3190
|
+
* @param {string} columnName - Column name
|
|
3191
|
+
* @returns {string} - Redshift column type
|
|
3192
|
+
*/
|
|
3193
|
+
getColumnType(columnName) {
|
|
3194
|
+
const field = this.schema[columnName];
|
|
3195
|
+
if (!field) {
|
|
3196
|
+
return "VARCHAR(65535)";
|
|
3197
|
+
}
|
|
3198
|
+
if (field.RedshiftType) {
|
|
3199
|
+
return field.RedshiftType;
|
|
3200
|
+
}
|
|
3201
|
+
const type = field.type ? field.type.toLowerCase() : "string";
|
|
3202
|
+
switch (type) {
|
|
3203
|
+
case "integer":
|
|
3204
|
+
case "int":
|
|
3205
|
+
return "BIGINT";
|
|
3206
|
+
case "float":
|
|
3207
|
+
case "number":
|
|
3208
|
+
return "DOUBLE PRECISION";
|
|
3209
|
+
case "boolean":
|
|
3210
|
+
case "bool":
|
|
3211
|
+
return "BOOLEAN";
|
|
3212
|
+
case "date":
|
|
3213
|
+
return "DATE";
|
|
3214
|
+
case "datetime":
|
|
3215
|
+
case "timestamp":
|
|
3216
|
+
return "TIMESTAMP";
|
|
3217
|
+
case "json":
|
|
3218
|
+
return "SUPER";
|
|
3219
|
+
default:
|
|
3220
|
+
return "VARCHAR(65535)";
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
//----------------------------------------------------------------
|
|
3224
|
+
//---- createSchemaIfNotExist -------------------------------------
|
|
3225
|
+
/**
|
|
3226
|
+
* Create schema in Redshift if it doesn't exist
|
|
3227
|
+
* @returns {Promise}
|
|
3228
|
+
*/
|
|
3229
|
+
async createSchemaIfNotExist() {
|
|
3230
|
+
const schemaName = this.config.Schema.value;
|
|
3231
|
+
if (!schemaName) {
|
|
3232
|
+
throw new Error("Schema name is required but not provided");
|
|
3233
|
+
}
|
|
3234
|
+
const createSchemaQuery = `CREATE SCHEMA IF NOT EXISTS "${schemaName}"`;
|
|
3235
|
+
await this.executeQuery(createSchemaQuery, "ddl");
|
|
3236
|
+
this.config.logMessage(`Schema "${schemaName}" ensured`);
|
|
3237
|
+
}
|
|
3238
|
+
//----------------------------------------------------------------
|
|
3239
|
+
//---- createTable ------------------------------------------------
|
|
3240
|
+
/**
|
|
3241
|
+
* Create table in Redshift
|
|
3242
|
+
* @returns {Promise}
|
|
3243
|
+
*/
|
|
3244
|
+
async createTable() {
|
|
3245
|
+
await this.createSchemaIfNotExist();
|
|
3246
|
+
const existingColumns = {};
|
|
3247
|
+
const columnDefinitions = [];
|
|
3248
|
+
for (let columnName of this.uniqueKeyColumns) {
|
|
3249
|
+
let columnType = this.getColumnType(columnName);
|
|
3250
|
+
columnDefinitions.push(`"${columnName}" ${columnType}`);
|
|
3251
|
+
existingColumns[columnName] = columnType;
|
|
3252
|
+
}
|
|
3253
|
+
let selectedFields = this.getSelectedFields();
|
|
3254
|
+
for (let columnName in this.schema) {
|
|
3255
|
+
if (!this.uniqueKeyColumns.includes(columnName) && selectedFields.includes(columnName)) {
|
|
3256
|
+
let columnType = this.getColumnType(columnName);
|
|
3257
|
+
columnDefinitions.push(`"${columnName}" ${columnType}`);
|
|
3258
|
+
existingColumns[columnName] = columnType;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
const pkColumns = this.uniqueKeyColumns.map((col) => `"${col}"`).join(", ");
|
|
3262
|
+
const query = `
|
|
3263
|
+
CREATE TABLE IF NOT EXISTS "${this.config.Schema.value}"."${this.config.DestinationTableName.value}" (
|
|
3264
|
+
${columnDefinitions.join(",\n ")},
|
|
3265
|
+
PRIMARY KEY (${pkColumns})
|
|
3266
|
+
)
|
|
3267
|
+
`;
|
|
3268
|
+
await this.executeQuery(query, "ddl");
|
|
3269
|
+
this.config.logMessage(`Table "${this.config.Schema.value}"."${this.config.DestinationTableName.value}" created`);
|
|
3270
|
+
this.existingColumns = existingColumns;
|
|
3271
|
+
return existingColumns;
|
|
3272
|
+
}
|
|
3273
|
+
//----------------------------------------------------------------
|
|
3274
|
+
//---- addNewColumns ----------------------------------------------
|
|
3275
|
+
/**
|
|
3276
|
+
* Add new columns to the Redshift table
|
|
3277
|
+
* @param {Array} newColumns - Array of column names to add
|
|
3278
|
+
* @returns {Promise}
|
|
3279
|
+
*/
|
|
3280
|
+
async addNewColumns(newColumns) {
|
|
3281
|
+
const columnsToAdd = [];
|
|
3282
|
+
for (let columnName of newColumns) {
|
|
3283
|
+
if (columnName in this.schema) {
|
|
3284
|
+
let columnType = this.getColumnType(columnName);
|
|
3285
|
+
columnsToAdd.push(`"${columnName}" ${columnType}`);
|
|
3286
|
+
this.existingColumns[columnName] = columnType;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
if (columnsToAdd.length > 0) {
|
|
3290
|
+
const query = `
|
|
3291
|
+
ALTER TABLE "${this.config.Schema.value}"."${this.config.DestinationTableName.value}"
|
|
3292
|
+
ADD COLUMN ${columnsToAdd.join(", ADD COLUMN ")}
|
|
3293
|
+
`;
|
|
3294
|
+
await this.executeQuery(query, "ddl");
|
|
3295
|
+
this.config.logMessage(`Columns '${newColumns.join(",")}' were added to "${this.config.Schema.value}"."${this.config.DestinationTableName.value}" table`);
|
|
3296
|
+
return newColumns;
|
|
3297
|
+
}
|
|
3298
|
+
return newColumns;
|
|
3299
|
+
}
|
|
3300
|
+
//----------------------------------------------------------------
|
|
3301
|
+
//---- saveData ---------------------------------------------------
|
|
3302
|
+
/**
|
|
3303
|
+
* Saving data to Redshift using MERGE
|
|
3304
|
+
* @param {Array} data - Array of objects with records to save
|
|
3305
|
+
* @returns {Promise}
|
|
3306
|
+
*/
|
|
3307
|
+
async saveData(data) {
|
|
3308
|
+
if (!data || data.length === 0) {
|
|
3309
|
+
return Promise.resolve();
|
|
3310
|
+
}
|
|
3311
|
+
if (Object.keys(this.existingColumns).length === 0) {
|
|
3312
|
+
await this.createTable();
|
|
3313
|
+
}
|
|
3314
|
+
const dataKeys = Object.keys(data[0]);
|
|
3315
|
+
const newColumns = dataKeys.filter((key) => !(key in this.existingColumns));
|
|
3316
|
+
if (newColumns.length > 0) {
|
|
3317
|
+
await this.addNewColumns(newColumns);
|
|
3318
|
+
}
|
|
3319
|
+
const selectedFields = this.getSelectedFields();
|
|
3320
|
+
const columnsToInsert = dataKeys.filter((key) => selectedFields.includes(key));
|
|
3321
|
+
const tempTableName = `temp_${this.config.DestinationTableName.value}_${Date.now()}`;
|
|
3322
|
+
const schemaName = this.config.Schema.value;
|
|
3323
|
+
if (!schemaName) {
|
|
3324
|
+
throw new Error("Schema name is required but not provided");
|
|
3325
|
+
}
|
|
3326
|
+
const tempColumns = columnsToInsert.map(
|
|
3327
|
+
(col) => `"${col}" ${this.existingColumns[col] || this.getColumnType(col)}`
|
|
3328
|
+
).join(", ");
|
|
3329
|
+
await this.executeQuery(`
|
|
3330
|
+
CREATE TABLE "${schemaName}"."${tempTableName}" (${tempColumns})
|
|
3331
|
+
`, "ddl");
|
|
3332
|
+
try {
|
|
3333
|
+
const batchSize = this.config.MaxBufferSize.value;
|
|
3334
|
+
for (let i = 0; i < data.length; i += batchSize) {
|
|
3335
|
+
const batch = data.slice(i, i + batchSize);
|
|
3336
|
+
await this.insertBatch(tempTableName, columnsToInsert, batch);
|
|
3337
|
+
}
|
|
3338
|
+
await this.mergeTempTable(tempTableName, columnsToInsert);
|
|
3339
|
+
} finally {
|
|
3340
|
+
try {
|
|
3341
|
+
await this.executeQuery(`DROP TABLE "${schemaName}"."${tempTableName}"`, "ddl");
|
|
3342
|
+
this.config.logMessage(`Temp table "${tempTableName}" cleaned up`);
|
|
3343
|
+
} catch (dropError) {
|
|
3344
|
+
this.config.logMessage(`Warning: Failed to drop temp table "${tempTableName}": ${dropError.message}`);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
this.config.logMessage(`Successfully saved ${data.length} records`);
|
|
3348
|
+
return data.length;
|
|
3349
|
+
}
|
|
3350
|
+
//----------------------------------------------------------------
|
|
3351
|
+
//---- insertBatch ------------------------------------------------
|
|
3352
|
+
/**
|
|
3353
|
+
* Insert batch of records into temp table
|
|
3354
|
+
* @param {string} tableName - Table name
|
|
3355
|
+
* @param {Array} columns - Column names
|
|
3356
|
+
* @param {Array} records - Records to insert
|
|
3357
|
+
* @returns {Promise}
|
|
3358
|
+
*/
|
|
3359
|
+
async insertBatch(tableName, columns, records) {
|
|
3360
|
+
const values = records.map((record) => {
|
|
3361
|
+
const vals = columns.map((col) => {
|
|
3362
|
+
const value = record[col];
|
|
3363
|
+
if (value === null || value === void 0) {
|
|
3364
|
+
return "NULL";
|
|
3365
|
+
}
|
|
3366
|
+
if (typeof value === "boolean") {
|
|
3367
|
+
return value ? "TRUE" : "FALSE";
|
|
3368
|
+
}
|
|
3369
|
+
if (typeof value === "number") {
|
|
3370
|
+
if (isNaN(value) || !isFinite(value)) {
|
|
3371
|
+
return "NULL";
|
|
3372
|
+
}
|
|
3373
|
+
return value;
|
|
3374
|
+
}
|
|
3375
|
+
const stringValue = String(value).replace(/'/g, "''");
|
|
3376
|
+
return `'${stringValue}'`;
|
|
3377
|
+
}).join(", ");
|
|
3378
|
+
return `(${vals})`;
|
|
3379
|
+
}).join(",\n ");
|
|
3380
|
+
const columnList = columns.map((col) => `"${col}"`).join(", ");
|
|
3381
|
+
const query = `
|
|
3382
|
+
INSERT INTO "${this.config.Schema.value}"."${tableName}" (${columnList})
|
|
3383
|
+
VALUES ${values}
|
|
3384
|
+
`;
|
|
3385
|
+
await this.executeQuery(query, "dml");
|
|
3386
|
+
}
|
|
3387
|
+
//----------------------------------------------------------------
|
|
3388
|
+
//---- mergeTempTable ---------------------------------------------
|
|
3389
|
+
/**
|
|
3390
|
+
* Merge temp table data into main table
|
|
3391
|
+
* @param {string} tempTableName - Temp table name
|
|
3392
|
+
* @param {Array} columns - Column names
|
|
3393
|
+
* @returns {Promise}
|
|
3394
|
+
*/
|
|
3395
|
+
async mergeTempTable(tempTableName, columns) {
|
|
3396
|
+
const targetTable = `"${this.config.Schema.value}"."${this.config.DestinationTableName.value}"`;
|
|
3397
|
+
const sourceTable = `"${this.config.Schema.value}"."${tempTableName}"`;
|
|
3398
|
+
const onClause = this.uniqueKeyColumns.map(
|
|
3399
|
+
(col) => `${targetTable}."${col}" = ${sourceTable}."${col}"`
|
|
3400
|
+
).join(" AND ");
|
|
3401
|
+
const updateColumns = columns.filter((col) => !this.uniqueKeyColumns.includes(col));
|
|
3402
|
+
const updateSet = updateColumns.map(
|
|
3403
|
+
(col) => `"${col}" = ${sourceTable}."${col}"`
|
|
3404
|
+
).join(", ");
|
|
3405
|
+
const insertColumns = columns.map((col) => `"${col}"`).join(", ");
|
|
3406
|
+
const insertValues = columns.map((col) => `${sourceTable}."${col}"`).join(", ");
|
|
3407
|
+
const query = `
|
|
3408
|
+
MERGE INTO ${targetTable}
|
|
3409
|
+
USING ${sourceTable}
|
|
3410
|
+
ON ${onClause}
|
|
3411
|
+
WHEN MATCHED THEN
|
|
3412
|
+
UPDATE SET ${updateSet}
|
|
3413
|
+
WHEN NOT MATCHED THEN
|
|
3414
|
+
INSERT (${insertColumns})
|
|
3415
|
+
VALUES (${insertValues})
|
|
3416
|
+
`;
|
|
3417
|
+
await this.executeQuery(query, "dml");
|
|
3418
|
+
}
|
|
3419
|
+
//----------------------------------------------------------------
|
|
3420
|
+
};
|
|
3421
|
+
const manifest = {
|
|
3422
|
+
"name": "AwsRedshiftStorage",
|
|
3423
|
+
"description": "Storage for AWS Redshift using Data API with support for Serverless and Provisioned clusters",
|
|
3424
|
+
"title": "AWS Redshift",
|
|
3425
|
+
"version": "1.0.0",
|
|
3426
|
+
"author": "OWOX, Inc.",
|
|
3427
|
+
"license": "MIT",
|
|
3428
|
+
"environment": {
|
|
3429
|
+
"node": {
|
|
3430
|
+
"enabled": true,
|
|
3431
|
+
"dependencies": [
|
|
3432
|
+
{
|
|
3433
|
+
"name": "@aws-sdk/client-redshift-data",
|
|
3434
|
+
"version": "3.952.0",
|
|
3435
|
+
"global": [
|
|
3436
|
+
"RedshiftDataClient",
|
|
3437
|
+
"ExecuteStatementCommand",
|
|
3438
|
+
"DescribeStatementCommand",
|
|
3439
|
+
"GetStatementResultCommand"
|
|
3440
|
+
]
|
|
3441
|
+
}
|
|
3442
|
+
]
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
};
|
|
3446
|
+
return {
|
|
3447
|
+
AwsRedshiftStorage,
|
|
3448
|
+
manifest
|
|
3449
|
+
};
|
|
3450
|
+
})();
|
|
3016
3451
|
const AwsAthena = (function() {
|
|
3017
3452
|
const { AbstractException: AbstractException2, HttpRequestException: HttpRequestException2, OauthFlowException: OauthFlowException2, AbstractStorage: AbstractStorage2, AbstractSource: AbstractSource3, AbstractRunConfig: AbstractRunConfig3, AbstractConnector: AbstractConnector3, AbstractConfig: AbstractConfig2, HttpUtils: HttpUtils3, FileUtils: FileUtils3, DateUtils: DateUtils3, CryptoUtils: CryptoUtils3, AsyncUtils: AsyncUtils3, RunConfigDto: RunConfigDto2, OauthCredentialsDto: OauthCredentialsDto2, OauthCredentialsDtoBuilder: OauthCredentialsDtoBuilder2, SourceConfigDto: SourceConfigDto2, StorageConfigDto: StorageConfigDto2, ConfigDto: ConfigDto2, HTTP_STATUS: HTTP_STATUS2, EXECUTION_STATUS: EXECUTION_STATUS2, RUN_CONFIG_TYPE: RUN_CONFIG_TYPE2, CONFIG_ATTRIBUTES: CONFIG_ATTRIBUTES2, OAUTH_CONSTANTS: OAUTH_CONSTANTS2, OAUTH_SOURCE_CREDENTIALS_KEY: OAUTH_SOURCE_CREDENTIALS_KEY2 } = Core;
|
|
3018
3453
|
var AwsAthenaStorage = class AwsAthenaStorage extends AbstractStorage2 {
|
|
@@ -3654,6 +4089,7 @@ const AwsAthena = (function() {
|
|
|
3654
4089
|
const Storages = {
|
|
3655
4090
|
Snowflake,
|
|
3656
4091
|
GoogleBigQuery,
|
|
4092
|
+
AwsRedshift,
|
|
3657
4093
|
AwsAthena
|
|
3658
4094
|
};
|
|
3659
4095
|
const XAds = (function() {
|
|
@@ -24579,6 +25015,7 @@ const AvailableConnectors = [
|
|
|
24579
25015
|
const AvailableStorages = [
|
|
24580
25016
|
"Snowflake",
|
|
24581
25017
|
"GoogleBigQuery",
|
|
25018
|
+
"AwsRedshift",
|
|
24582
25019
|
"AwsAthena"
|
|
24583
25020
|
];
|
|
24584
25021
|
const OWOX = {
|
|
@@ -24605,6 +25042,7 @@ const OWOX = {
|
|
|
24605
25042
|
// Individual storages
|
|
24606
25043
|
Snowflake,
|
|
24607
25044
|
GoogleBigQuery,
|
|
25045
|
+
AwsRedshift,
|
|
24608
25046
|
AwsAthena
|
|
24609
25047
|
};
|
|
24610
25048
|
if (typeof module !== "undefined" && module.exports) {
|