@onivoro/server-aws-redshift 22.11.0 → 24.0.0

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 (29) hide show
  1. package/README.md +787 -0
  2. package/{dist/cjs/index.d.ts → index.ts} +2 -0
  3. package/jest.config.ts +11 -0
  4. package/package.json +10 -47
  5. package/project.json +23 -0
  6. package/{dist/esm/index.d.ts → src/index.ts} +2 -0
  7. package/src/lib/classes/server-aws-redshift-config.class.ts +4 -0
  8. package/src/lib/server-aws-redshift.module.ts +56 -0
  9. package/{dist/cjs/lib/services/redshift.service.js → src/lib/services/redshift.service.ts} +141 -84
  10. package/tsconfig.json +16 -0
  11. package/tsconfig.lib.json +8 -0
  12. package/tsconfig.spec.json +21 -0
  13. package/dist/cjs/index.js +0 -19
  14. package/dist/cjs/lib/classes/server-aws-redshift-config.class.d.ts +0 -6
  15. package/dist/cjs/lib/classes/server-aws-redshift-config.class.js +0 -10
  16. package/dist/cjs/lib/server-aws-redshift.module.d.ts +0 -4
  17. package/dist/cjs/lib/server-aws-redshift.module.js +0 -65
  18. package/dist/cjs/lib/services/redshift.service.d.ts +0 -43
  19. package/dist/esm/index.js +0 -19
  20. package/dist/esm/lib/classes/server-aws-redshift-config.class.d.ts +0 -6
  21. package/dist/esm/lib/classes/server-aws-redshift-config.class.js +0 -10
  22. package/dist/esm/lib/server-aws-redshift.module.d.ts +0 -4
  23. package/dist/esm/lib/server-aws-redshift.module.js +0 -65
  24. package/dist/esm/lib/services/redshift.service.d.ts +0 -43
  25. package/dist/esm/lib/services/redshift.service.js +0 -236
  26. package/dist/types/index.d.ts +0 -3
  27. package/dist/types/lib/classes/server-aws-redshift-config.class.d.ts +0 -6
  28. package/dist/types/lib/server-aws-redshift.module.d.ts +0 -4
  29. package/dist/types/lib/services/redshift.service.d.ts +0 -43
package/README.md ADDED
@@ -0,0 +1,787 @@
1
+ # @onivoro/server-aws-redshift
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.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @onivoro/server-aws-redshift
9
+ ```
10
+
11
+ ## Features
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
21
+
22
+ ## Quick Start
23
+
24
+ ### 1. Module Configuration
25
+
26
+ ```typescript
27
+ import { ServerAwsRedshiftModule } from '@onivoro/server-aws-redshift';
28
+
29
+ @Module({
30
+ 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
+ ],
38
+ })
39
+ export class AppModule {}
40
+ ```
41
+
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
+ ## Configuration
79
+
80
+ ### ServerAwsRedshiftConfig
81
+
82
+ ```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;
93
+ }
94
+ ```
95
+
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
112
+
113
+ ### RedshiftDataService
114
+
115
+ The main service for Redshift operations:
116
+
117
+ ```typescript
118
+ import { RedshiftDataService } from '@onivoro/server-aws-redshift';
119
+
120
+ @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
+ });
174
+
175
+ return { username, password };
176
+ }
177
+ }
178
+ ```
179
+
180
+ ## Usage Examples
181
+
182
+ ### Data Analytics Service
183
+
184
+ ```typescript
185
+ import { RedshiftDataService } from '@onivoro/server-aws-redshift';
186
+
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);
207
+ }
208
+
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);
240
+ }
241
+
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);
284
+ }
285
+ }
286
+ ```
287
+
288
+ ### ETL Pipeline Service
289
+
290
+ ```typescript
291
+ import { RedshiftDataService } from '@onivoro/server-aws-redshift';
292
+
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
+ }
310
+
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
+ }
324
+
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
+ }
343
+
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
387
+
388
+ ```typescript
389
+ import { RedshiftDataService } from '@onivoro/server-aws-redshift';
390
+
391
+ @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`
405
+ });
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);
457
+ }
458
+
459
+ async getIamRoleAssociations() {
460
+ return this.redshiftService.getAssociatedIAmRolesByWorkgroup({
461
+ database: process.env.REDSHIFT_DATABASE!,
462
+ workgroupName: process.env.REDSHIFT_WORKGROUP!
463
+ });
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);
577
+ }
578
+ }
579
+ ```
580
+
581
+ ## Advanced Usage
582
+
583
+ ### Query Result Caching
584
+
585
+ ```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
+ ```
623
+
624
+ ### Batch Query Executor
625
+
626
+ ```typescript
627
+ @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
+ };
636
+
637
+ const results = [];
638
+
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
+ try {
669
+ await this.redshiftService.query(queryParams, 'BEGIN');
670
+
671
+ const results = [];
672
+ for (const sql of queries) {
673
+ const result = await this.redshiftService.query(queryParams, sql);
674
+ results.push(result);
675
+ }
676
+
677
+ await this.redshiftService.query(queryParams, 'COMMIT');
678
+ return results;
679
+ } catch (error) {
680
+ await this.redshiftService.query(queryParams, 'ROLLBACK');
681
+ throw error;
682
+ }
683
+ }
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
+
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');
720
+ }
721
+
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');
735
+ }
736
+ }
737
+ ```
738
+
739
+ ## Testing
740
+
741
+ ```typescript
742
+ import { Test, TestingModule } from '@nestjs/testing';
743
+ import { ServerAwsRedshiftModule, RedshiftDataService } from '@onivoro/server-aws-redshift';
744
+
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
+ });
774
+ ```
775
+
776
+ ## API Reference
777
+
778
+ ### Exported Classes
779
+ - `ServerAwsRedshiftConfig`: Configuration class for Redshift settings
780
+ - `ServerAwsRedshiftModule`: NestJS module for Redshift integration
781
+
782
+ ### Exported Services
783
+ - `RedshiftDataService`: Main Redshift service with query execution and user management capabilities
784
+
785
+ ## License
786
+
787
+ This package is part of the Onivoro monorepo and follows the same licensing terms.