@onivoro/server-aws-redshift 24.30.12 → 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.
- package/README.md +138 -692
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @onivoro/server-aws-redshift
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
11
|
+
## Overview
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
The module uses environment-based configuration:
|
|
81
32
|
|
|
82
33
|
```typescript
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
65
|
+
// Create a new user
|
|
66
|
+
async createAnalystUser(username: string, password: string) {
|
|
67
|
+
await this.redshiftService.createUser(username, password);
|
|
68
|
+
}
|
|
186
69
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
87
|
+
## Available Methods
|
|
289
88
|
|
|
290
|
-
|
|
291
|
-
|
|
89
|
+
### Query Execution
|
|
90
|
+
- **query(sql: string)** - Execute a SQL query and return results
|
|
292
91
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
393
|
-
constructor(private redshiftService: RedshiftDataService) {}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
##
|
|
582
|
-
|
|
583
|
-
### Query Result Caching
|
|
142
|
+
## Example: Data Warehouse Operations
|
|
584
143
|
|
|
585
144
|
```typescript
|
|
586
|
-
@
|
|
587
|
-
|
|
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
|
-
|
|
148
|
+
@Module({
|
|
149
|
+
imports: [ServerAwsRedshiftModule.configure()],
|
|
150
|
+
providers: [AnalyticsService]
|
|
151
|
+
})
|
|
152
|
+
export class AnalyticsModule {}
|
|
625
153
|
|
|
626
|
-
```typescript
|
|
627
154
|
@Injectable()
|
|
628
|
-
export class
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
672
|
-
|
|
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
|
-
|
|
678
|
-
|
|
176
|
+
return {
|
|
177
|
+
username,
|
|
178
|
+
tempPassword,
|
|
179
|
+
message: 'User created successfully. Please change password on first login.'
|
|
180
|
+
};
|
|
679
181
|
} catch (error) {
|
|
680
|
-
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
200
|
+
## Environment Variables
|
|
740
201
|
|
|
741
|
-
```
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
##
|
|
215
|
+
## Limitations
|
|
777
216
|
|
|
778
|
-
|
|
779
|
-
-
|
|
780
|
-
-
|
|
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
|
-
|
|
783
|
-
|
|
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
|
-
|
|
233
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onivoro/server-aws-redshift",
|
|
3
|
-
"version": "24.30.
|
|
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.
|
|
14
|
-
"@onivoro/server-common": "24.30.
|
|
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": {
|