@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.
@@ -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) => {
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owox/connectors",
3
- "version": "0.16.0-next-20251217153437",
3
+ "version": "0.16.0-next-20251218103137",
4
4
  "description": "Connectors and storages for different data sources",
5
5
  "license": "MIT",
6
6
  "publishConfig": {