@onivoro/server-aws-redshift 24.30.11 → 24.30.13

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.
Files changed (2) hide show
  1. package/README.md +138 -692
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @onivoro/server-aws-redshift
2
2
 
3
- A NestJS module for integrating with AWS Redshift Data API and Redshift Serverless, providing SQL query execution, database management, user management, and data warehouse operations for your server applications.
3
+ AWS Redshift Data API integration for NestJS applications.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,780 +8,226 @@ A NestJS module for integrating with AWS Redshift Data API and Redshift Serverle
8
8
  npm install @onivoro/server-aws-redshift
9
9
  ```
10
10
 
11
- ## Features
11
+ ## Overview
12
12
 
13
- - **SQL Query Execution**: Execute SQL queries using Redshift Data API
14
- - **Async Query Processing**: Non-blocking query execution with result polling
15
- - **User Management**: Create and manage Redshift database users
16
- - **Group Management**: Create and manage database groups with IAM integration
17
- - **Permission Management**: Grant schema and table permissions
18
- - **Workgroup Integration**: Support for Redshift Serverless workgroups
19
- - **Connection Management**: Automatic endpoint verification and access control
20
- - **Error Handling**: Comprehensive error handling for database operations
13
+ This library provides AWS Redshift Data API integration for NestJS applications, offering basic database operations, user management, and schema permissions.
21
14
 
22
- ## Quick Start
23
-
24
- ### 1. Module Configuration
15
+ ## Module Setup
25
16
 
26
17
  ```typescript
18
+ import { Module } from '@nestjs/common';
27
19
  import { ServerAwsRedshiftModule } from '@onivoro/server-aws-redshift';
28
20
 
29
21
  @Module({
30
22
  imports: [
31
- ServerAwsRedshiftModule.configure({
32
- AWS_REGION: 'us-east-1',
33
- REDSHIFT_DATABASE: process.env.REDSHIFT_DATABASE,
34
- REDSHIFT_WORKGROUP: process.env.REDSHIFT_WORKGROUP,
35
- AWS_PROFILE: process.env.AWS_PROFILE || 'default',
36
- }),
37
- ],
23
+ ServerAwsRedshiftModule.configure()
24
+ ]
38
25
  })
39
26
  export class AppModule {}
40
27
  ```
41
28
 
42
- ### 2. Basic Usage
43
-
44
- ```typescript
45
- import { RedshiftDataService } from '@onivoro/server-aws-redshift';
46
-
47
- @Injectable()
48
- export class DataWarehouseService {
49
- constructor(private redshiftService: RedshiftDataService) {}
50
-
51
- async executeQuery(sql: string) {
52
- const queryParams = {
53
- database: process.env.REDSHIFT_DATABASE!,
54
- workgroupName: process.env.REDSHIFT_WORKGROUP!
55
- };
56
-
57
- return this.redshiftService.query(queryParams, sql);
58
- }
59
-
60
- async createUser(username: string) {
61
- const queryParams = {
62
- database: process.env.REDSHIFT_DATABASE!,
63
- workgroupName: process.env.REDSHIFT_WORKGROUP!
64
- };
65
-
66
- return this.redshiftService.createDatabaseUser({
67
- ...queryParams,
68
- user: username
69
- });
70
- }
71
-
72
- async verifyConnection() {
73
- return this.redshiftService.verifyEndpointAccess(process.env.REDSHIFT_WORKGROUP!);
74
- }
75
- }
76
- ```
77
-
78
29
  ## Configuration
79
30
 
80
- ### ServerAwsRedshiftConfig
31
+ The module uses environment-based configuration:
81
32
 
82
33
  ```typescript
83
- import { ServerAwsRedshiftConfig } from '@onivoro/server-aws-redshift';
84
-
85
- export class AppRedshiftConfig extends ServerAwsRedshiftConfig {
86
- AWS_REGION = process.env.AWS_REGION || 'us-east-1';
87
- REDSHIFT_DATABASE = process.env.REDSHIFT_DATABASE || 'dev';
88
- REDSHIFT_WORKGROUP = process.env.REDSHIFT_WORKGROUP || 'default';
89
- REDSHIFT_CLUSTER_IDENTIFIER = process.env.REDSHIFT_CLUSTER_IDENTIFIER;
90
- AWS_PROFILE = process.env.AWS_PROFILE || 'default';
91
- QUERY_TIMEOUT = parseInt(process.env.REDSHIFT_QUERY_TIMEOUT) || 300000; // 5 minutes
92
- MAX_RETRY_ATTEMPTS = parseInt(process.env.REDSHIFT_MAX_RETRIES) || 3;
34
+ export class ServerAwsRedshiftConfig {
35
+ AWS_REGION: string;
36
+ AWS_PROFILE?: string; // Optional AWS profile
37
+ AWS_REDSHIFT_CLUSTER_IDENTIFIER: string;
38
+ AWS_REDSHIFT_DATABASE: string;
39
+ AWS_REDSHIFT_USER: string;
40
+ AWS_REDSHIFT_WORKGROUP?: string;
41
+ AWS_S3_BUCKET?: string; // For data operations
93
42
  }
94
43
  ```
95
44
 
96
- ### Environment Variables
97
-
98
- ```bash
99
- # AWS Configuration
100
- AWS_REGION=us-east-1
101
- AWS_PROFILE=default
102
-
103
- # Redshift Configuration
104
- REDSHIFT_DATABASE=analytics
105
- REDSHIFT_WORKGROUP=analytics-workgroup
106
- REDSHIFT_CLUSTER_IDENTIFIER=my-redshift-cluster
107
- REDSHIFT_QUERY_TIMEOUT=300000
108
- REDSHIFT_MAX_RETRIES=3
109
- ```
110
-
111
- ## Services
45
+ ## Service
112
46
 
113
47
  ### RedshiftDataService
114
48
 
115
- The main service for Redshift operations:
49
+ The service provides Redshift database operations:
116
50
 
117
51
  ```typescript
52
+ import { Injectable } from '@nestjs/common';
118
53
  import { RedshiftDataService } from '@onivoro/server-aws-redshift';
119
54
 
120
55
  @Injectable()
121
- export class AnalyticsService {
122
- constructor(private redshiftService: RedshiftDataService) {}
123
-
124
- async getUserAnalytics(userId: string, startDate: string, endDate: string) {
125
- const sql = `
126
- SELECT
127
- date_trunc('day', event_timestamp) as date,
128
- count(*) as event_count,
129
- count(distinct session_id) as session_count
130
- FROM user_events
131
- WHERE user_id = '${userId}'
132
- AND event_timestamp BETWEEN '${startDate}' AND '${endDate}'
133
- GROUP BY date_trunc('day', event_timestamp)
134
- ORDER BY date
135
- `;
136
-
137
- return this.redshiftService.query({
138
- database: process.env.REDSHIFT_DATABASE!,
139
- workgroupName: process.env.REDSHIFT_WORKGROUP!
140
- }, sql);
141
- }
142
-
143
- async createAnalyticsUser(username: string) {
144
- const queryParams = {
145
- database: process.env.REDSHIFT_DATABASE!,
146
- workgroupName: process.env.REDSHIFT_WORKGROUP!
147
- };
148
-
149
- // Create user
150
- const password = await this.redshiftService.createDatabaseUser({
151
- ...queryParams,
152
- user: username
153
- });
154
-
155
- // Create analytics group if it doesn't exist
156
- await this.redshiftService.createDbGroupFromIamGroupIfNotExists({
157
- ...queryParams,
158
- iamGroup: 'analytics-users'
159
- });
160
-
161
- // Add user to group
162
- await this.redshiftService.addIamUserToDatabaseGroup({
163
- ...queryParams,
164
- user: username,
165
- group: 'analytics-users'
166
- });
167
-
168
- // Grant permissions
169
- await this.redshiftService.grantUsageOnSchema({
170
- ...queryParams,
171
- schema: 'analytics',
172
- group: 'analytics-users'
173
- });
56
+ export class DataWarehouseService {
57
+ constructor(private readonly redshiftService: RedshiftDataService) {}
174
58
 
175
- return { username, password };
59
+ // Execute a query
60
+ async executeQuery(sql: string) {
61
+ const result = await this.redshiftService.query(sql);
62
+ return result;
176
63
  }
177
- }
178
- ```
179
-
180
- ## Usage Examples
181
64
 
182
- ### Data Analytics Service
183
-
184
- ```typescript
185
- import { RedshiftDataService } from '@onivoro/server-aws-redshift';
65
+ // Create a new user
66
+ async createAnalystUser(username: string, password: string) {
67
+ await this.redshiftService.createUser(username, password);
68
+ }
186
69
 
187
- @Injectable()
188
- export class DataAnalyticsService {
189
- constructor(private redshiftService: RedshiftDataService) {}
190
-
191
- async runDailyReport(reportDate: string) {
192
- const sql = `
193
- INSERT INTO daily_reports (report_date, total_users, total_orders, revenue)
194
- SELECT
195
- '${reportDate}' as report_date,
196
- count(distinct user_id) as total_users,
197
- count(distinct order_id) as total_orders,
198
- sum(order_amount) as revenue
199
- FROM fact_orders
200
- WHERE order_date = '${reportDate}'
201
- `;
202
-
203
- return this.redshiftService.query({
204
- database: process.env.REDSHIFT_DATABASE!,
205
- workgroupName: process.env.REDSHIFT_WORKGROUP!
206
- }, sql);
70
+ // Create a user group
71
+ async createAnalystGroup(groupName: string) {
72
+ await this.redshiftService.createGroup(groupName);
207
73
  }
208
74
 
209
- async getCustomerSegmentation() {
210
- const sql = `
211
- WITH customer_metrics AS (
212
- SELECT
213
- user_id,
214
- count(*) as order_count,
215
- sum(order_amount) as total_spent,
216
- max(order_date) as last_order_date,
217
- min(order_date) as first_order_date
218
- FROM fact_orders
219
- WHERE order_date >= current_date - interval '365 days'
220
- GROUP BY user_id
221
- )
222
- SELECT
223
- CASE
224
- WHEN total_spent > 1000 AND order_count > 10 THEN 'VIP'
225
- WHEN total_spent > 500 OR order_count > 5 THEN 'Regular'
226
- ELSE 'Casual'
227
- END as segment,
228
- count(*) as customer_count,
229
- avg(total_spent) as avg_spent,
230
- avg(order_count) as avg_orders
231
- FROM customer_metrics
232
- GROUP BY 1
233
- ORDER BY avg_spent DESC
234
- `;
235
-
236
- return this.redshiftService.query({
237
- database: process.env.REDSHIFT_DATABASE!,
238
- workgroupName: process.env.REDSHIFT_WORKGROUP!
239
- }, sql);
75
+ // Add user to group
76
+ async addUserToGroup(username: string, groupName: string) {
77
+ await this.redshiftService.addUserToGroup(username, groupName);
240
78
  }
241
79
 
242
- async getCohortAnalysis(startDate: string, endDate: string) {
243
- const sql = `
244
- WITH user_cohorts AS (
245
- SELECT
246
- user_id,
247
- date_trunc('month', min(order_date)) as cohort_month
248
- FROM fact_orders
249
- WHERE order_date BETWEEN '${startDate}' AND '${endDate}'
250
- GROUP BY user_id
251
- ),
252
- cohort_sizes AS (
253
- SELECT
254
- cohort_month,
255
- count(*) as cohort_size
256
- FROM user_cohorts
257
- GROUP BY cohort_month
258
- ),
259
- cohort_retention AS (
260
- SELECT
261
- c.cohort_month,
262
- date_trunc('month', o.order_date) as order_month,
263
- count(distinct o.user_id) as retained_users
264
- FROM user_cohorts c
265
- JOIN fact_orders o ON c.user_id = o.user_id
266
- WHERE o.order_date >= c.cohort_month
267
- GROUP BY c.cohort_month, date_trunc('month', o.order_date)
268
- )
269
- SELECT
270
- cr.cohort_month,
271
- cr.order_month,
272
- cs.cohort_size,
273
- cr.retained_users,
274
- round(100.0 * cr.retained_users / cs.cohort_size, 2) as retention_rate
275
- FROM cohort_retention cr
276
- JOIN cohort_sizes cs ON cr.cohort_month = cs.cohort_month
277
- ORDER BY cr.cohort_month, cr.order_month
278
- `;
279
-
280
- return this.redshiftService.query({
281
- database: process.env.REDSHIFT_DATABASE!,
282
- workgroupName: process.env.REDSHIFT_WORKGROUP!
283
- }, sql);
80
+ // Grant schema access
81
+ async grantSchemaAccess(groupName: string, schemaName: string) {
82
+ await this.redshiftService.grantSchemaPermissions(groupName, schemaName);
284
83
  }
285
84
  }
286
85
  ```
287
86
 
288
- ### ETL Pipeline Service
87
+ ## Available Methods
289
88
 
290
- ```typescript
291
- import { RedshiftDataService } from '@onivoro/server-aws-redshift';
89
+ ### Query Execution
90
+ - **query(sql: string)** - Execute a SQL query and return results
292
91
 
293
- @Injectable()
294
- export class ETLPipelineService {
295
- constructor(private redshiftService: RedshiftDataService) {}
296
-
297
- async loadDataFromS3(tableName: string, s3Path: string, copyOptions?: string) {
298
- const sql = `
299
- COPY ${tableName}
300
- FROM '${s3Path}'
301
- IAM_ROLE 'arn:aws:iam::123456789012:role/RedshiftRole'
302
- ${copyOptions || 'CSV IGNOREHEADER 1'}
303
- `;
304
-
305
- return this.redshiftService.query({
306
- database: process.env.REDSHIFT_DATABASE!,
307
- workgroupName: process.env.REDSHIFT_WORKGROUP!
308
- }, sql);
309
- }
92
+ ### User Management
93
+ - **createUser(username: string, password: string)** - Create a new Redshift user
94
+ - **createGroup(groupName: string)** - Create a new user group
95
+ - **addUserToGroup(username: string, groupName: string)** - Add user to a group
96
+ - **grantSchemaPermissions(groupName: string, schemaName: string)** - Grant schema access to a group
310
97
 
311
- async unloadDataToS3(tableName: string, s3Path: string, whereClause?: string) {
312
- const sql = `
313
- UNLOAD ('SELECT * FROM ${tableName} ${whereClause || ''}')
314
- TO '${s3Path}'
315
- IAM_ROLE 'arn:aws:iam::123456789012:role/RedshiftRole'
316
- CSV HEADER
317
- `;
318
-
319
- return this.redshiftService.query({
320
- database: process.env.REDSHIFT_DATABASE!,
321
- workgroupName: process.env.REDSHIFT_WORKGROUP!
322
- }, sql);
323
- }
98
+ ### Workgroup Operations
99
+ - **getWorkgroupEndpoint(workgroupName: string)** - Get the endpoint for a workgroup (used internally)
324
100
 
325
- async createFactTable(tableName: string, schema: TableSchema) {
326
- const columns = schema.columns.map(col =>
327
- `${col.name} ${col.dataType}${col.nullable ? '' : ' NOT NULL'}${col.primary ? ' PRIMARY KEY' : ''}`
328
- ).join(',\n ');
329
-
330
- const sql = `
331
- CREATE TABLE IF NOT EXISTS ${tableName} (
332
- ${columns}
333
- )
334
- ${schema.distKey ? `DISTKEY(${schema.distKey})` : ''}
335
- ${schema.sortKeys ? `SORTKEY(${schema.sortKeys.join(', ')})` : ''}
336
- `;
337
-
338
- return this.redshiftService.query({
339
- database: process.env.REDSHIFT_DATABASE!,
340
- workgroupName: process.env.REDSHIFT_WORKGROUP!
341
- }, sql);
342
- }
101
+ ## Direct Client Access
343
102
 
344
- async runDataQualityChecks(tableName: string) {
345
- const checks = [
346
- // Check for null values in required columns
347
- `SELECT 'null_check' as test, count(*) as failures FROM ${tableName} WHERE id IS NULL`,
348
-
349
- // Check for duplicate records
350
- `SELECT 'duplicate_check' as test, count(*) - count(distinct id) as failures FROM ${tableName}`,
351
-
352
- // Check for future dates
353
- `SELECT 'future_date_check' as test, count(*) as failures FROM ${tableName} WHERE created_at > current_date`,
354
- ];
355
-
356
- const results = [];
357
- for (const check of checks) {
358
- const result = await this.redshiftService.query({
359
- database: process.env.REDSHIFT_DATABASE!,
360
- workgroupName: process.env.REDSHIFT_WORKGROUP!
361
- }, check);
362
- results.push(result);
363
- }
364
-
365
- return results;
366
- }
367
-
368
- async optimizeTable(tableName: string) {
369
- // Vacuum to reclaim space
370
- await this.redshiftService.query({
371
- database: process.env.REDSHIFT_DATABASE!,
372
- workgroupName: process.env.REDSHIFT_WORKGROUP!
373
- }, `VACUUM ${tableName}`);
374
-
375
- // Analyze to update statistics
376
- await this.redshiftService.query({
377
- database: process.env.REDSHIFT_DATABASE!,
378
- workgroupName: process.env.REDSHIFT_WORKGROUP!
379
- }, `ANALYZE ${tableName}`);
380
-
381
- console.log(`Table ${tableName} optimized successfully`);
382
- }
383
- }
384
- ```
385
-
386
- ### User and Permission Management
103
+ The service exposes the underlying Redshift Data client:
387
104
 
388
105
  ```typescript
389
- import { RedshiftDataService } from '@onivoro/server-aws-redshift';
106
+ import {
107
+ ListDatabasesCommand,
108
+ ListTablesCommand,
109
+ DescribeTableCommand,
110
+ GetStatementResultCommand
111
+ } from '@aws-sdk/client-redshift-data';
390
112
 
391
113
  @Injectable()
392
- export class RedshiftUserManagementService {
393
- constructor(private redshiftService: RedshiftDataService) {}
394
-
395
- async setupDepartmentAccess(department: string, users: string[], schemas: string[]) {
396
- const queryParams = {
397
- database: process.env.REDSHIFT_DATABASE!,
398
- workgroupName: process.env.REDSHIFT_WORKGROUP!
399
- };
400
-
401
- // Create department group
402
- await this.redshiftService.createDbGroupFromIamGroupIfNotExists({
403
- ...queryParams,
404
- iamGroup: `${department}-group`
114
+ export class AdvancedRedshiftService {
115
+ constructor(private readonly redshiftService: RedshiftDataService) {}
116
+
117
+ // List all databases
118
+ async listDatabases() {
119
+ const command = new ListDatabasesCommand({
120
+ ClusterIdentifier: process.env.AWS_REDSHIFT_CLUSTER_IDENTIFIER,
121
+ Database: process.env.AWS_REDSHIFT_DATABASE,
122
+ DbUser: process.env.AWS_REDSHIFT_USER
405
123
  });
406
-
407
- // Create users and add to group
408
- for (const user of users) {
409
- await this.redshiftService.createDatabaseUser({
410
- ...queryParams,
411
- user
412
- });
413
-
414
- await this.redshiftService.addIamUserToDatabaseGroup({
415
- ...queryParams,
416
- user,
417
- group: `${department}-group`
418
- });
419
- }
420
-
421
- // Grant schema permissions
422
- for (const schema of schemas) {
423
- await this.redshiftService.grantUsageOnSchema({
424
- ...queryParams,
425
- schema,
426
- group: `${department}-group`
427
- });
428
- }
429
-
430
- return {
431
- department,
432
- groupName: `${department}-group`,
433
- usersCreated: users.length,
434
- schemasGranted: schemas.length
435
- };
436
- }
437
-
438
- async auditUserPermissions() {
439
- const sql = `
440
- SELECT
441
- u.usename as username,
442
- g.groname as group_name,
443
- s.schemaname as schema_name,
444
- p.privilege_type
445
- FROM pg_user u
446
- LEFT JOIN pg_group g ON u.usesysid = ANY(g.grolist)
447
- LEFT JOIN information_schema.schema_privileges p ON g.groname = p.grantee
448
- LEFT JOIN information_schema.schemata s ON p.schema_name = s.schema_name
449
- WHERE u.usename NOT LIKE 'rdsdb%'
450
- ORDER BY u.usename, g.groname, s.schemaname
451
- `;
452
-
453
- return this.redshiftService.query({
454
- database: process.env.REDSHIFT_DATABASE!,
455
- workgroupName: process.env.REDSHIFT_WORKGROUP!
456
- }, sql);
124
+
125
+ return await this.redshiftService.redshiftDataApiClient.send(command);
457
126
  }
458
127
 
459
- async getIamRoleAssociations() {
460
- return this.redshiftService.getAssociatedIAmRolesByWorkgroup({
461
- database: process.env.REDSHIFT_DATABASE!,
462
- workgroupName: process.env.REDSHIFT_WORKGROUP!
128
+ // List tables in a schema
129
+ async listTables(schemaName: string) {
130
+ const command = new ListTablesCommand({
131
+ ClusterIdentifier: process.env.AWS_REDSHIFT_CLUSTER_IDENTIFIER,
132
+ Database: process.env.AWS_REDSHIFT_DATABASE,
133
+ DbUser: process.env.AWS_REDSHIFT_USER,
134
+ SchemaPattern: schemaName
463
135
  });
464
- }
465
-
466
- async cleanupInactiveUsers(inactiveDays: number = 90) {
467
- const sql = `
468
- SELECT usename
469
- FROM pg_user
470
- WHERE usename NOT LIKE 'rdsdb%'
471
- AND usename NOT IN (
472
- SELECT DISTINCT usename
473
- FROM stl_query
474
- WHERE starttime > current_date - interval '${inactiveDays} days'
475
- )
476
- `;
477
-
478
- const inactiveUsers = await this.redshiftService.query({
479
- database: process.env.REDSHIFT_DATABASE!,
480
- workgroupName: process.env.REDSHIFT_WORKGROUP!
481
- }, sql);
482
-
483
- console.log(`Found ${inactiveUsers.length} inactive users`);
484
- return inactiveUsers;
485
- }
486
- }
487
- ```
488
-
489
- ### Query Monitoring and Performance
490
-
491
- ```typescript
492
- import { RedshiftDataService } from '@onivoro/server-aws-redshift';
493
-
494
- @Injectable()
495
- export class RedshiftMonitoringService {
496
- constructor(private redshiftService: RedshiftDataService) {}
497
-
498
- async getRunningQueries() {
499
- const sql = `
500
- SELECT
501
- pid,
502
- user_name,
503
- starttime,
504
- query,
505
- substring(query, 1, 100) as query_snippet
506
- FROM stv_recents
507
- WHERE status = 'Running'
508
- ORDER BY starttime DESC
509
- `;
510
-
511
- return this.redshiftService.query({
512
- database: process.env.REDSHIFT_DATABASE!,
513
- workgroupName: process.env.REDSHIFT_WORKGROUP!
514
- }, sql);
515
- }
516
-
517
- async getSlowQueries(durationThresholdSeconds: number = 300) {
518
- const sql = `
519
- SELECT
520
- query,
521
- userid,
522
- starttime,
523
- endtime,
524
- datediff(seconds, starttime, endtime) as duration_seconds,
525
- substring(querytxt, 1, 200) as query_text
526
- FROM stl_query
527
- WHERE datediff(seconds, starttime, endtime) > ${durationThresholdSeconds}
528
- AND starttime > current_date - 1
529
- ORDER BY duration_seconds DESC
530
- LIMIT 50
531
- `;
532
-
533
- return this.redshiftService.query({
534
- database: process.env.REDSHIFT_DATABASE!,
535
- workgroupName: process.env.REDSHIFT_WORKGROUP!
536
- }, sql);
537
- }
538
-
539
- async getTableStats() {
540
- const sql = `
541
- SELECT
542
- schemaname,
543
- tablename,
544
- size as size_mb,
545
- tbl_rows,
546
- skew_sortkey1,
547
- skew_rows
548
- FROM svv_table_info
549
- WHERE schemaname NOT IN ('information_schema', 'pg_catalog')
550
- ORDER BY size DESC
551
- `;
552
-
553
- return this.redshiftService.query({
554
- database: process.env.REDSHIFT_DATABASE!,
555
- workgroupName: process.env.REDSHIFT_WORKGROUP!
556
- }, sql);
557
- }
558
-
559
- async analyzeQueryPerformance(queryId: string) {
560
- const sql = `
561
- SELECT
562
- step,
563
- max_time,
564
- avg_time,
565
- rows,
566
- bytes,
567
- label
568
- FROM stl_explain
569
- WHERE query = ${queryId}
570
- ORDER BY step
571
- `;
572
-
573
- return this.redshiftService.query({
574
- database: process.env.REDSHIFT_DATABASE!,
575
- workgroupName: process.env.REDSHIFT_WORKGROUP!
576
- }, sql);
136
+
137
+ return await this.redshiftService.redshiftDataApiClient.send(command);
577
138
  }
578
139
  }
579
140
  ```
580
141
 
581
- ## Advanced Usage
582
-
583
- ### Query Result Caching
142
+ ## Example: Data Warehouse Operations
584
143
 
585
144
  ```typescript
586
- @Injectable()
587
- export class CachedRedshiftService {
588
- private queryCache = new Map<string, { result: any; timestamp: number }>();
589
- private readonly CACHE_TTL = 300000; // 5 minutes
590
-
591
- constructor(private redshiftService: RedshiftDataService) {}
592
-
593
- async queryCached(queryParams: any, sql: string, cacheTTL?: number) {
594
- const cacheKey = this.generateCacheKey(queryParams, sql);
595
- const cached = this.queryCache.get(cacheKey);
596
- const now = Date.now();
597
-
598
- if (cached && (now - cached.timestamp) < (cacheTTL || this.CACHE_TTL)) {
599
- console.log('Returning cached query result');
600
- return cached.result;
601
- }
602
-
603
- console.log('Executing fresh query');
604
- const result = await this.redshiftService.query(queryParams, sql);
605
-
606
- this.queryCache.set(cacheKey, {
607
- result,
608
- timestamp: now
609
- });
610
-
611
- return result;
612
- }
613
-
614
- private generateCacheKey(queryParams: any, sql: string): string {
615
- return Buffer.from(`${JSON.stringify(queryParams)}-${sql}`).toString('base64');
616
- }
617
-
618
- clearCache() {
619
- this.queryCache.clear();
620
- }
621
- }
622
- ```
145
+ import { Module, Injectable } from '@nestjs/common';
146
+ import { ServerAwsRedshiftModule, RedshiftDataService } from '@onivoro/server-aws-redshift';
623
147
 
624
- ### Batch Query Executor
148
+ @Module({
149
+ imports: [ServerAwsRedshiftModule.configure()],
150
+ providers: [AnalyticsService]
151
+ })
152
+ export class AnalyticsModule {}
625
153
 
626
- ```typescript
627
154
  @Injectable()
628
- export class BatchQueryService {
629
- constructor(private redshiftService: RedshiftDataService) {}
630
-
631
- async executeBatch(queries: Array<{ id: string; sql: string }>) {
632
- const queryParams = {
633
- database: process.env.REDSHIFT_DATABASE!,
634
- workgroupName: process.env.REDSHIFT_WORKGROUP!
635
- };
155
+ export class AnalyticsService {
156
+ constructor(private readonly redshiftService: RedshiftDataService) {}
636
157
 
637
- const results = [];
158
+ async setupAnalyticsUser(email: string) {
159
+ const username = email.split('@')[0].replace(/[^a-zA-Z0-9]/g, '_');
160
+ const tempPassword = `Temp123!${Math.random().toString(36).slice(-4)}`;
638
161
 
639
- for (const query of queries) {
640
- try {
641
- console.log(`Executing query ${query.id}`);
642
- const result = await this.redshiftService.query(queryParams, query.sql);
643
- results.push({
644
- id: query.id,
645
- success: true,
646
- result,
647
- rowCount: result.length
648
- });
649
- } catch (error) {
650
- console.error(`Query ${query.id} failed:`, error);
651
- results.push({
652
- id: query.id,
653
- success: false,
654
- error: error.message
655
- });
656
- }
657
- }
658
-
659
- return results;
660
- }
661
-
662
- async executeTransaction(queries: string[]) {
663
- const queryParams = {
664
- database: process.env.REDSHIFT_DATABASE!,
665
- workgroupName: process.env.REDSHIFT_WORKGROUP!
666
- };
667
-
668
162
  try {
669
- await this.redshiftService.query(queryParams, 'BEGIN');
163
+ // Create user
164
+ await this.redshiftService.createUser(username, tempPassword);
165
+
166
+ // Create or use existing analyst group
167
+ const groupName = 'analysts';
168
+ await this.redshiftService.createGroup(groupName);
169
+
170
+ // Add user to group
171
+ await this.redshiftService.addUserToGroup(username, groupName);
670
172
 
671
- const results = [];
672
- for (const sql of queries) {
673
- const result = await this.redshiftService.query(queryParams, sql);
674
- results.push(result);
675
- }
173
+ // Grant permissions to analytics schema
174
+ await this.redshiftService.grantSchemaPermissions(groupName, 'analytics');
676
175
 
677
- await this.redshiftService.query(queryParams, 'COMMIT');
678
- return results;
176
+ return {
177
+ username,
178
+ tempPassword,
179
+ message: 'User created successfully. Please change password on first login.'
180
+ };
679
181
  } catch (error) {
680
- await this.redshiftService.query(queryParams, 'ROLLBACK');
182
+ console.error('Failed to setup user:', error);
681
183
  throw error;
682
184
  }
683
185
  }
684
- }
685
- ```
686
-
687
- ## Best Practices
688
-
689
- ### 1. Query Optimization
690
-
691
- ```typescript
692
- // Use LIMIT for testing
693
- const testQuery = `SELECT * FROM large_table LIMIT 100`;
694
-
695
- // Use column names instead of SELECT *
696
- const optimizedQuery = `SELECT id, name, created_at FROM users WHERE active = true`;
697
-
698
- // Use proper data types
699
- const efficientQuery = `
700
- SELECT date_trunc('day', event_date) as day, count(*)
701
- FROM events
702
- WHERE event_date >= current_date - interval '7 days'
703
- GROUP BY 1
704
- `;
705
- ```
706
-
707
- ### 2. Error Handling
708
186
 
709
- ```typescript
710
- async safeQuery(queryParams: any, sql: string): Promise<any[]> {
711
- try {
712
- return await this.redshiftService.query(queryParams, sql);
713
- } catch (error: any) {
714
- console.error('Redshift query failed:', error);
715
-
716
- if (error.message.includes('permission denied')) {
717
- throw new Error('Insufficient permissions for this query');
718
- } else if (error.message.includes('relation does not exist')) {
719
- throw new Error('Table or view not found');
187
+ async runAnalyticsQuery(query: string) {
188
+ // Validate query is read-only
189
+ if (query.toLowerCase().includes('drop') ||
190
+ query.toLowerCase().includes('delete') ||
191
+ query.toLowerCase().includes('update')) {
192
+ throw new Error('Only SELECT queries are allowed');
720
193
  }
721
194
 
722
- throw error;
723
- }
724
- }
725
- ```
726
-
727
- ### 3. Connection Management
728
-
729
- ```typescript
730
- async ensureConnection(): Promise<void> {
731
- try {
732
- await this.redshiftService.verifyEndpointAccess(process.env.REDSHIFT_WORKGROUP!);
733
- } catch (error) {
734
- throw new Error('Cannot connect to Redshift cluster');
195
+ return await this.redshiftService.query(query);
735
196
  }
736
197
  }
737
198
  ```
738
199
 
739
- ## Testing
200
+ ## Environment Variables
740
201
 
741
- ```typescript
742
- import { Test, TestingModule } from '@nestjs/testing';
743
- import { ServerAwsRedshiftModule, RedshiftDataService } from '@onivoro/server-aws-redshift';
202
+ ```bash
203
+ # Required
204
+ AWS_REGION=us-east-1
205
+ AWS_REDSHIFT_CLUSTER_IDENTIFIER=my-redshift-cluster
206
+ AWS_REDSHIFT_DATABASE=mydb
207
+ AWS_REDSHIFT_USER=admin
744
208
 
745
- describe('RedshiftDataService', () => {
746
- let service: RedshiftDataService;
747
-
748
- beforeEach(async () => {
749
- const module: TestingModule = await Test.createTestingModule({
750
- imports: [ServerAwsRedshiftModule.configure({
751
- AWS_REGION: 'us-east-1',
752
- REDSHIFT_DATABASE: 'test',
753
- REDSHIFT_WORKGROUP: 'test-workgroup',
754
- AWS_PROFILE: 'test'
755
- })],
756
- }).compile();
757
-
758
- service = module.get<RedshiftDataService>(RedshiftDataService);
759
- });
760
-
761
- it('should be defined', () => {
762
- expect(service).toBeDefined();
763
- });
764
-
765
- it('should execute simple query', async () => {
766
- const result = await service.query({
767
- database: 'test',
768
- workgroupName: 'test-workgroup'
769
- }, 'SELECT 1 as test_value');
770
-
771
- expect(result).toBeDefined();
772
- });
773
- });
209
+ # Optional
210
+ AWS_PROFILE=my-profile
211
+ AWS_REDSHIFT_WORKGROUP=my-workgroup
212
+ AWS_S3_BUCKET=my-data-bucket
774
213
  ```
775
214
 
776
- ## API Reference
215
+ ## Limitations
777
216
 
778
- ### Exported Classes
779
- - `ServerAwsRedshiftConfig`: Configuration class for Redshift settings
780
- - `ServerAwsRedshiftModule`: NestJS module for Redshift integration
217
+ - Basic query execution only (no advanced features like prepared statements)
218
+ - Limited user management capabilities
219
+ - No built-in connection pooling or query optimization
220
+ - No support for advanced Redshift features (materialized views, stored procedures)
221
+ - For advanced operations, use the exposed `redshiftDataApiClient` directly
222
+
223
+ ## Best Practices
781
224
 
782
- ### Exported Services
783
- - `RedshiftDataService`: Main Redshift service with query execution and user management capabilities
225
+ 1. **Security**: Use least-privilege database users
226
+ 2. **Query Validation**: Always validate user input before executing queries
227
+ 3. **Error Handling**: Implement proper error handling for database operations
228
+ 4. **Performance**: Use appropriate cluster sizing and distribution keys
229
+ 5. **Monitoring**: Monitor query performance using Redshift console
784
230
 
785
231
  ## License
786
232
 
787
- This library is licensed under the MIT License. See the LICENSE file in this package for details.
233
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onivoro/server-aws-redshift",
3
- "version": "24.30.11",
3
+ "version": "24.30.13",
4
4
  "license": "MIT",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -10,8 +10,8 @@
10
10
  "url": "https://github.com/onivoro/monorepo.git"
11
11
  },
12
12
  "dependencies": {
13
- "@onivoro/server-aws-credential-providers": "24.30.11",
14
- "@onivoro/server-common": "24.30.11",
13
+ "@onivoro/server-aws-credential-providers": "24.30.13",
14
+ "@onivoro/server-common": "24.30.13",
15
15
  "tslib": "^2.3.0"
16
16
  },
17
17
  "peerDependencies": {