@onivoro/server-aws-redshift 22.11.0 → 24.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +787 -0
- package/{dist/cjs/index.d.ts → index.ts} +2 -0
- package/jest.config.ts +11 -0
- package/package.json +10 -47
- package/project.json +23 -0
- package/{dist/esm/index.d.ts → src/index.ts} +2 -0
- package/src/lib/classes/server-aws-redshift-config.class.ts +4 -0
- package/src/lib/server-aws-redshift.module.ts +56 -0
- package/{dist/cjs/lib/services/redshift.service.js → src/lib/services/redshift.service.ts} +141 -84
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +21 -0
- package/dist/cjs/index.js +0 -19
- package/dist/cjs/lib/classes/server-aws-redshift-config.class.d.ts +0 -6
- package/dist/cjs/lib/classes/server-aws-redshift-config.class.js +0 -10
- package/dist/cjs/lib/server-aws-redshift.module.d.ts +0 -4
- package/dist/cjs/lib/server-aws-redshift.module.js +0 -65
- package/dist/cjs/lib/services/redshift.service.d.ts +0 -43
- package/dist/esm/index.js +0 -19
- package/dist/esm/lib/classes/server-aws-redshift-config.class.d.ts +0 -6
- package/dist/esm/lib/classes/server-aws-redshift-config.class.js +0 -10
- package/dist/esm/lib/server-aws-redshift.module.d.ts +0 -4
- package/dist/esm/lib/server-aws-redshift.module.js +0 -65
- package/dist/esm/lib/services/redshift.service.d.ts +0 -43
- package/dist/esm/lib/services/redshift.service.js +0 -236
- package/dist/types/index.d.ts +0 -3
- package/dist/types/lib/classes/server-aws-redshift-config.class.d.ts +0 -6
- package/dist/types/lib/server-aws-redshift.module.d.ts +0 -4
- 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.
|