@onivoro/server-aws-sns 24.0.2
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 +845 -0
- package/jest.config.ts +11 -0
- package/package.json +16 -0
- package/project.json +23 -0
- package/src/index.ts +3 -0
- package/src/lib/classes/server-aws-sns-config.class.ts +4 -0
- package/src/lib/server-aws-sns.module.ts +32 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
# @onivoro/server-aws-sns
|
|
2
|
+
|
|
3
|
+
A NestJS module for integrating with AWS SNS (Simple Notification Service), providing message publishing, subscription management, and notification delivery capabilities for your server applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @onivoro/server-aws-sns
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **SNS Client Integration**: Direct integration with AWS SNS service
|
|
14
|
+
- **Message Publishing**: Publish messages to SNS topics
|
|
15
|
+
- **Subscription Management**: Create and manage topic subscriptions
|
|
16
|
+
- **SMS Notifications**: Send SMS messages directly through SNS
|
|
17
|
+
- **Email Notifications**: Send email notifications via SNS
|
|
18
|
+
- **Mobile Push Notifications**: Support for mobile app push notifications
|
|
19
|
+
- **Topic Management**: Create, list, and manage SNS topics
|
|
20
|
+
- **Message Attributes**: Support for custom message attributes and filtering
|
|
21
|
+
- **Environment-Based Configuration**: Configurable SNS settings per environment
|
|
22
|
+
- **Credential Provider Integration**: Seamless integration with AWS credential providers
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### 1. Module Configuration
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { ServerAwsSnsModule } from '@onivoro/server-aws-sns';
|
|
30
|
+
|
|
31
|
+
@Module({
|
|
32
|
+
imports: [
|
|
33
|
+
ServerAwsSnsModule.configure({
|
|
34
|
+
AWS_REGION: 'us-east-1',
|
|
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 { SNSClient, PublishCommand, CreateTopicCommand } from '@aws-sdk/client-sns';
|
|
46
|
+
|
|
47
|
+
@Injectable()
|
|
48
|
+
export class NotificationService {
|
|
49
|
+
constructor(private snsClient: SNSClient) {}
|
|
50
|
+
|
|
51
|
+
async publishMessage(topicArn: string, message: string, subject?: string) {
|
|
52
|
+
const params = {
|
|
53
|
+
TopicArn: topicArn,
|
|
54
|
+
Message: message,
|
|
55
|
+
Subject: subject
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async sendSMS(phoneNumber: string, message: string) {
|
|
62
|
+
const params = {
|
|
63
|
+
PhoneNumber: phoneNumber,
|
|
64
|
+
Message: message
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createTopic(topicName: string) {
|
|
71
|
+
const params = {
|
|
72
|
+
Name: topicName
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return this.snsClient.send(new CreateTopicCommand(params));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
### ServerAwsSnsConfig
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { ServerAwsSnsConfig } from '@onivoro/server-aws-sns';
|
|
86
|
+
|
|
87
|
+
export class AppSnsConfig extends ServerAwsSnsConfig {
|
|
88
|
+
AWS_REGION = process.env.AWS_REGION || 'us-east-1';
|
|
89
|
+
AWS_PROFILE = process.env.AWS_PROFILE || 'default';
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Environment Variables
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# AWS Configuration
|
|
97
|
+
AWS_REGION=us-east-1
|
|
98
|
+
AWS_PROFILE=default
|
|
99
|
+
|
|
100
|
+
# Optional SNS Configuration
|
|
101
|
+
SNS_DEFAULT_TOPIC_ARN=arn:aws:sns:us-east-1:123456789012:my-topic
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Usage Examples
|
|
105
|
+
|
|
106
|
+
### Topic Management Service
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import {
|
|
110
|
+
SNSClient,
|
|
111
|
+
CreateTopicCommand,
|
|
112
|
+
DeleteTopicCommand,
|
|
113
|
+
ListTopicsCommand,
|
|
114
|
+
GetTopicAttributesCommand,
|
|
115
|
+
SetTopicAttributesCommand
|
|
116
|
+
} from '@aws-sdk/client-sns';
|
|
117
|
+
|
|
118
|
+
@Injectable()
|
|
119
|
+
export class SnsTopicService {
|
|
120
|
+
constructor(private snsClient: SNSClient) {}
|
|
121
|
+
|
|
122
|
+
async createTopic(topicName: string, displayName?: string) {
|
|
123
|
+
const createParams = {
|
|
124
|
+
Name: topicName,
|
|
125
|
+
Attributes: displayName ? { DisplayName: displayName } : undefined
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const result = await this.snsClient.send(new CreateTopicCommand(createParams));
|
|
129
|
+
return result.TopicArn;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async deleteTopic(topicArn: string) {
|
|
133
|
+
const params = {
|
|
134
|
+
TopicArn: topicArn
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return this.snsClient.send(new DeleteTopicCommand(params));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async listTopics() {
|
|
141
|
+
return this.snsClient.send(new ListTopicsCommand({}));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async getTopicAttributes(topicArn: string) {
|
|
145
|
+
const params = {
|
|
146
|
+
TopicArn: topicArn
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return this.snsClient.send(new GetTopicAttributesCommand(params));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async setTopicDisplayName(topicArn: string, displayName: string) {
|
|
153
|
+
const params = {
|
|
154
|
+
TopicArn: topicArn,
|
|
155
|
+
AttributeName: 'DisplayName',
|
|
156
|
+
AttributeValue: displayName
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return this.snsClient.send(new SetTopicAttributesCommand(params));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async enableDeliveryStatusLogging(topicArn: string, roleArn: string) {
|
|
163
|
+
const attributes = {
|
|
164
|
+
'DeliveryStatusSuccessSamplingRate': '100',
|
|
165
|
+
'DeliveryStatusFailureSamplingRate': '100',
|
|
166
|
+
'DeliveryStatusLogging': 'true',
|
|
167
|
+
'DeliveryStatusLogSuccessFeedbackRoleArn': roleArn,
|
|
168
|
+
'DeliveryStatusLogFailureFeedbackRoleArn': roleArn
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const promises = Object.entries(attributes).map(([AttributeName, AttributeValue]) =>
|
|
172
|
+
this.snsClient.send(new SetTopicAttributesCommand({
|
|
173
|
+
TopicArn: topicArn,
|
|
174
|
+
AttributeName,
|
|
175
|
+
AttributeValue
|
|
176
|
+
}))
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return Promise.all(promises);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Subscription Management Service
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import {
|
|
188
|
+
SNSClient,
|
|
189
|
+
SubscribeCommand,
|
|
190
|
+
UnsubscribeCommand,
|
|
191
|
+
ListSubscriptionsByTopicCommand,
|
|
192
|
+
ConfirmSubscriptionCommand
|
|
193
|
+
} from '@aws-sdk/client-sns';
|
|
194
|
+
|
|
195
|
+
@Injectable()
|
|
196
|
+
export class SnsSubscriptionService {
|
|
197
|
+
constructor(private snsClient: SNSClient) {}
|
|
198
|
+
|
|
199
|
+
async subscribeEmail(topicArn: string, emailAddress: string) {
|
|
200
|
+
const params = {
|
|
201
|
+
TopicArn: topicArn,
|
|
202
|
+
Protocol: 'email',
|
|
203
|
+
Endpoint: emailAddress
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return this.snsClient.send(new SubscribeCommand(params));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async subscribeSMS(topicArn: string, phoneNumber: string) {
|
|
210
|
+
const params = {
|
|
211
|
+
TopicArn: topicArn,
|
|
212
|
+
Protocol: 'sms',
|
|
213
|
+
Endpoint: phoneNumber
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return this.snsClient.send(new SubscribeCommand(params));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async subscribeHTTP(topicArn: string, httpEndpoint: string) {
|
|
220
|
+
const params = {
|
|
221
|
+
TopicArn: topicArn,
|
|
222
|
+
Protocol: 'http',
|
|
223
|
+
Endpoint: httpEndpoint
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return this.snsClient.send(new SubscribeCommand(params));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async subscribeHTTPS(topicArn: string, httpsEndpoint: string) {
|
|
230
|
+
const params = {
|
|
231
|
+
TopicArn: topicArn,
|
|
232
|
+
Protocol: 'https',
|
|
233
|
+
Endpoint: httpsEndpoint
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return this.snsClient.send(new SubscribeCommand(params));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async subscribeLambda(topicArn: string, lambdaArn: string) {
|
|
240
|
+
const params = {
|
|
241
|
+
TopicArn: topicArn,
|
|
242
|
+
Protocol: 'lambda',
|
|
243
|
+
Endpoint: lambdaArn
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return this.snsClient.send(new SubscribeCommand(params));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async subscribeSQS(topicArn: string, sqsQueueArn: string) {
|
|
250
|
+
const params = {
|
|
251
|
+
TopicArn: topicArn,
|
|
252
|
+
Protocol: 'sqs',
|
|
253
|
+
Endpoint: sqsQueueArn
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return this.snsClient.send(new SubscribeCommand(params));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async unsubscribe(subscriptionArn: string) {
|
|
260
|
+
const params = {
|
|
261
|
+
SubscriptionArn: subscriptionArn
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return this.snsClient.send(new UnsubscribeCommand(params));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async listSubscriptions(topicArn: string) {
|
|
268
|
+
const params = {
|
|
269
|
+
TopicArn: topicArn
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return this.snsClient.send(new ListSubscriptionsByTopicCommand(params));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async confirmSubscription(topicArn: string, token: string) {
|
|
276
|
+
const params = {
|
|
277
|
+
TopicArn: topicArn,
|
|
278
|
+
Token: token
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return this.snsClient.send(new ConfirmSubscriptionCommand(params));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Message Publishing Service
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { SNSClient, PublishCommand, PublishBatchCommand } from '@aws-sdk/client-sns';
|
|
290
|
+
|
|
291
|
+
interface MessageAttributes {
|
|
292
|
+
[key: string]: {
|
|
293
|
+
DataType: 'String' | 'Number' | 'Binary';
|
|
294
|
+
StringValue?: string;
|
|
295
|
+
BinaryValue?: Uint8Array;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@Injectable()
|
|
300
|
+
export class SnsPublishService {
|
|
301
|
+
constructor(private snsClient: SNSClient) {}
|
|
302
|
+
|
|
303
|
+
async publishMessage(
|
|
304
|
+
topicArn: string,
|
|
305
|
+
message: string,
|
|
306
|
+
subject?: string,
|
|
307
|
+
messageAttributes?: MessageAttributes
|
|
308
|
+
) {
|
|
309
|
+
const params = {
|
|
310
|
+
TopicArn: topicArn,
|
|
311
|
+
Message: message,
|
|
312
|
+
Subject: subject,
|
|
313
|
+
MessageAttributes: messageAttributes
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async publishStructuredMessage(
|
|
320
|
+
topicArn: string,
|
|
321
|
+
message: any,
|
|
322
|
+
subject?: string,
|
|
323
|
+
messageAttributes?: MessageAttributes
|
|
324
|
+
) {
|
|
325
|
+
const messagePayload = {
|
|
326
|
+
default: JSON.stringify(message),
|
|
327
|
+
email: JSON.stringify(message),
|
|
328
|
+
sms: typeof message === 'string' ? message : JSON.stringify(message)
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const params = {
|
|
332
|
+
TopicArn: topicArn,
|
|
333
|
+
Message: JSON.stringify(messagePayload),
|
|
334
|
+
Subject: subject,
|
|
335
|
+
MessageStructure: 'json',
|
|
336
|
+
MessageAttributes: messageAttributes
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async publishToPhone(phoneNumber: string, message: string, messageAttributes?: MessageAttributes) {
|
|
343
|
+
const params = {
|
|
344
|
+
PhoneNumber: phoneNumber,
|
|
345
|
+
Message: message,
|
|
346
|
+
MessageAttributes: messageAttributes
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async publishBatch(topicArn: string, messages: Array<{
|
|
353
|
+
Id: string;
|
|
354
|
+
Message: string;
|
|
355
|
+
Subject?: string;
|
|
356
|
+
MessageAttributes?: MessageAttributes;
|
|
357
|
+
}>) {
|
|
358
|
+
const params = {
|
|
359
|
+
TopicArn: topicArn,
|
|
360
|
+
PublishRequestEntries: messages
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return this.snsClient.send(new PublishBatchCommand(params));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async publishWithDeduplication(
|
|
367
|
+
topicArn: string,
|
|
368
|
+
message: string,
|
|
369
|
+
messageGroupId: string,
|
|
370
|
+
messageDeduplicationId: string,
|
|
371
|
+
subject?: string
|
|
372
|
+
) {
|
|
373
|
+
const params = {
|
|
374
|
+
TopicArn: topicArn,
|
|
375
|
+
Message: message,
|
|
376
|
+
Subject: subject,
|
|
377
|
+
MessageGroupId: messageGroupId,
|
|
378
|
+
MessageDeduplicationId: messageDeduplicationId
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async publishNotification(
|
|
385
|
+
topicArn: string,
|
|
386
|
+
notification: {
|
|
387
|
+
title: string;
|
|
388
|
+
body: string;
|
|
389
|
+
data?: any;
|
|
390
|
+
priority?: 'normal' | 'high';
|
|
391
|
+
}
|
|
392
|
+
) {
|
|
393
|
+
const messageAttributes: MessageAttributes = {
|
|
394
|
+
'notification.title': {
|
|
395
|
+
DataType: 'String',
|
|
396
|
+
StringValue: notification.title
|
|
397
|
+
},
|
|
398
|
+
'notification.body': {
|
|
399
|
+
DataType: 'String',
|
|
400
|
+
StringValue: notification.body
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
if (notification.priority) {
|
|
405
|
+
messageAttributes['notification.priority'] = {
|
|
406
|
+
DataType: 'String',
|
|
407
|
+
StringValue: notification.priority
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return this.publishMessage(
|
|
412
|
+
topicArn,
|
|
413
|
+
JSON.stringify(notification),
|
|
414
|
+
notification.title,
|
|
415
|
+
messageAttributes
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Mobile Push Notification Service
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { SNSClient, CreatePlatformApplicationCommand, CreatePlatformEndpointCommand, PublishCommand } from '@aws-sdk/client-sns';
|
|
425
|
+
|
|
426
|
+
@Injectable()
|
|
427
|
+
export class SnsMobilePushService {
|
|
428
|
+
constructor(private snsClient: SNSClient) {}
|
|
429
|
+
|
|
430
|
+
async createPlatformApplication(
|
|
431
|
+
name: string,
|
|
432
|
+
platform: 'GCM' | 'APNS' | 'APNS_SANDBOX',
|
|
433
|
+
credentials: { [key: string]: string }
|
|
434
|
+
) {
|
|
435
|
+
const params = {
|
|
436
|
+
Name: name,
|
|
437
|
+
Platform: platform,
|
|
438
|
+
Attributes: credentials
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
return this.snsClient.send(new CreatePlatformApplicationCommand(params));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async createPlatformEndpoint(
|
|
445
|
+
platformApplicationArn: string,
|
|
446
|
+
token: string,
|
|
447
|
+
customUserData?: string
|
|
448
|
+
) {
|
|
449
|
+
const params = {
|
|
450
|
+
PlatformApplicationArn: platformApplicationArn,
|
|
451
|
+
Token: token,
|
|
452
|
+
CustomUserData: customUserData
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
return this.snsClient.send(new CreatePlatformEndpointCommand(params));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async sendPushNotification(
|
|
459
|
+
targetArn: string,
|
|
460
|
+
message: string,
|
|
461
|
+
badge?: number,
|
|
462
|
+
sound?: string
|
|
463
|
+
) {
|
|
464
|
+
const gcmPayload = {
|
|
465
|
+
data: {
|
|
466
|
+
message: message
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const apnsPayload = {
|
|
471
|
+
aps: {
|
|
472
|
+
alert: message,
|
|
473
|
+
badge: badge,
|
|
474
|
+
sound: sound || 'default'
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const messagePayload = {
|
|
479
|
+
default: message,
|
|
480
|
+
GCM: JSON.stringify(gcmPayload),
|
|
481
|
+
APNS: JSON.stringify(apnsPayload),
|
|
482
|
+
APNS_SANDBOX: JSON.stringify(apnsPayload)
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const params = {
|
|
486
|
+
TargetArn: targetArn,
|
|
487
|
+
Message: JSON.stringify(messagePayload),
|
|
488
|
+
MessageStructure: 'json'
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async sendSilentPushNotification(targetArn: string, data: any) {
|
|
495
|
+
const gcmPayload = {
|
|
496
|
+
data: data
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const apnsPayload = {
|
|
500
|
+
aps: {
|
|
501
|
+
'content-available': 1
|
|
502
|
+
},
|
|
503
|
+
data: data
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const messagePayload = {
|
|
507
|
+
GCM: JSON.stringify(gcmPayload),
|
|
508
|
+
APNS: JSON.stringify(apnsPayload),
|
|
509
|
+
APNS_SANDBOX: JSON.stringify(apnsPayload)
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const params = {
|
|
513
|
+
TargetArn: targetArn,
|
|
514
|
+
Message: JSON.stringify(messagePayload),
|
|
515
|
+
MessageStructure: 'json'
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### SMS Service
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
import { SNSClient, PublishCommand, SetSMSAttributesCommand, GetSMSAttributesCommand } from '@aws-sdk/client-sns';
|
|
527
|
+
|
|
528
|
+
@Injectable()
|
|
529
|
+
export class SnsSmsService {
|
|
530
|
+
constructor(private snsClient: SNSClient) {}
|
|
531
|
+
|
|
532
|
+
async sendSMS(
|
|
533
|
+
phoneNumber: string,
|
|
534
|
+
message: string,
|
|
535
|
+
senderName?: string,
|
|
536
|
+
messageType?: 'Promotional' | 'Transactional'
|
|
537
|
+
) {
|
|
538
|
+
const messageAttributes: any = {};
|
|
539
|
+
|
|
540
|
+
if (senderName) {
|
|
541
|
+
messageAttributes['AWS.SNS.SMS.SenderID'] = {
|
|
542
|
+
DataType: 'String',
|
|
543
|
+
StringValue: senderName
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (messageType) {
|
|
548
|
+
messageAttributes['AWS.SNS.SMS.SMSType'] = {
|
|
549
|
+
DataType: 'String',
|
|
550
|
+
StringValue: messageType
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const params = {
|
|
555
|
+
PhoneNumber: phoneNumber,
|
|
556
|
+
Message: message,
|
|
557
|
+
MessageAttributes: Object.keys(messageAttributes).length > 0 ? messageAttributes : undefined
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async sendTransactionalSMS(phoneNumber: string, message: string, senderName?: string) {
|
|
564
|
+
return this.sendSMS(phoneNumber, message, senderName, 'Transactional');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async sendPromotionalSMS(phoneNumber: string, message: string, senderName?: string) {
|
|
568
|
+
return this.sendSMS(phoneNumber, message, senderName, 'Promotional');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async setSMSAttributes(attributes: { [key: string]: string }) {
|
|
572
|
+
const promises = Object.entries(attributes).map(([key, value]) =>
|
|
573
|
+
this.snsClient.send(new SetSMSAttributesCommand({
|
|
574
|
+
attributes: { [key]: value }
|
|
575
|
+
}))
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
return Promise.all(promises);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async getSMSAttributes() {
|
|
582
|
+
return this.snsClient.send(new GetSMSAttributesCommand({}));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async setDefaultSenderName(senderName: string) {
|
|
586
|
+
return this.setSMSAttributes({
|
|
587
|
+
'DefaultSenderID': senderName
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async setDefaultSMSType(smsType: 'Promotional' | 'Transactional') {
|
|
592
|
+
return this.setSMSAttributes({
|
|
593
|
+
'DefaultSMSType': smsType
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async setMonthlySpendLimit(limitUSD: string) {
|
|
598
|
+
return this.setSMSAttributes({
|
|
599
|
+
'MonthlySpendLimit': limitUSD
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
## Advanced Usage
|
|
606
|
+
|
|
607
|
+
### Message Filtering Service
|
|
608
|
+
|
|
609
|
+
```typescript
|
|
610
|
+
@Injectable()
|
|
611
|
+
export class SnsFilteringService {
|
|
612
|
+
constructor(private snsClient: SNSClient) {}
|
|
613
|
+
|
|
614
|
+
async subscribeWithFilter(
|
|
615
|
+
topicArn: string,
|
|
616
|
+
protocol: string,
|
|
617
|
+
endpoint: string,
|
|
618
|
+
filterPolicy: any
|
|
619
|
+
) {
|
|
620
|
+
const subscribeResult = await this.snsClient.send(new SubscribeCommand({
|
|
621
|
+
TopicArn: topicArn,
|
|
622
|
+
Protocol: protocol,
|
|
623
|
+
Endpoint: endpoint
|
|
624
|
+
}));
|
|
625
|
+
|
|
626
|
+
if (subscribeResult.SubscriptionArn && filterPolicy) {
|
|
627
|
+
await this.snsClient.send(new SetSubscriptionAttributesCommand({
|
|
628
|
+
SubscriptionArn: subscribeResult.SubscriptionArn,
|
|
629
|
+
AttributeName: 'FilterPolicy',
|
|
630
|
+
AttributeValue: JSON.stringify(filterPolicy)
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return subscribeResult;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async updateFilterPolicy(subscriptionArn: string, filterPolicy: any) {
|
|
638
|
+
const params = {
|
|
639
|
+
SubscriptionArn: subscriptionArn,
|
|
640
|
+
AttributeName: 'FilterPolicy',
|
|
641
|
+
AttributeValue: JSON.stringify(filterPolicy)
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
return this.snsClient.send(new SetSubscriptionAttributesCommand(params));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async publishWithAttributes(
|
|
648
|
+
topicArn: string,
|
|
649
|
+
message: string,
|
|
650
|
+
attributes: { [key: string]: string | number | boolean }
|
|
651
|
+
) {
|
|
652
|
+
const messageAttributes: any = {};
|
|
653
|
+
|
|
654
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
655
|
+
messageAttributes[key] = {
|
|
656
|
+
DataType: typeof value === 'number' ? 'Number' : 'String',
|
|
657
|
+
StringValue: value.toString()
|
|
658
|
+
};
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const params = {
|
|
662
|
+
TopicArn: topicArn,
|
|
663
|
+
Message: message,
|
|
664
|
+
MessageAttributes: messageAttributes
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### Event-Driven Notification Service
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
interface NotificationEvent {
|
|
676
|
+
type: 'user.signup' | 'order.created' | 'payment.failed' | 'system.alert';
|
|
677
|
+
userId?: string;
|
|
678
|
+
orderId?: string;
|
|
679
|
+
data: any;
|
|
680
|
+
timestamp: string;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
@Injectable()
|
|
684
|
+
export class SnsEventNotificationService {
|
|
685
|
+
constructor(private snsClient: SNSClient) {}
|
|
686
|
+
|
|
687
|
+
async publishEvent(topicArn: string, event: NotificationEvent) {
|
|
688
|
+
const messageAttributes = {
|
|
689
|
+
'event.type': {
|
|
690
|
+
DataType: 'String',
|
|
691
|
+
StringValue: event.type
|
|
692
|
+
},
|
|
693
|
+
'event.timestamp': {
|
|
694
|
+
DataType: 'String',
|
|
695
|
+
StringValue: event.timestamp
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
if (event.userId) {
|
|
700
|
+
messageAttributes['event.userId'] = {
|
|
701
|
+
DataType: 'String',
|
|
702
|
+
StringValue: event.userId
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (event.orderId) {
|
|
707
|
+
messageAttributes['event.orderId'] = {
|
|
708
|
+
DataType: 'String',
|
|
709
|
+
StringValue: event.orderId
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const params = {
|
|
714
|
+
TopicArn: topicArn,
|
|
715
|
+
Message: JSON.stringify(event),
|
|
716
|
+
Subject: `Event: ${event.type}`,
|
|
717
|
+
MessageAttributes: messageAttributes
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
return this.snsClient.send(new PublishCommand(params));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async publishUserSignupEvent(topicArn: string, userId: string, userData: any) {
|
|
724
|
+
const event: NotificationEvent = {
|
|
725
|
+
type: 'user.signup',
|
|
726
|
+
userId,
|
|
727
|
+
data: userData,
|
|
728
|
+
timestamp: new Date().toISOString()
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
return this.publishEvent(topicArn, event);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async publishOrderEvent(topicArn: string, orderId: string, userId: string, orderData: any) {
|
|
735
|
+
const event: NotificationEvent = {
|
|
736
|
+
type: 'order.created',
|
|
737
|
+
userId,
|
|
738
|
+
orderId,
|
|
739
|
+
data: orderData,
|
|
740
|
+
timestamp: new Date().toISOString()
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
return this.publishEvent(topicArn, event);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async publishSystemAlert(topicArn: string, alertData: any) {
|
|
747
|
+
const event: NotificationEvent = {
|
|
748
|
+
type: 'system.alert',
|
|
749
|
+
data: alertData,
|
|
750
|
+
timestamp: new Date().toISOString()
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
return this.publishEvent(topicArn, event);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
## Best Practices
|
|
759
|
+
|
|
760
|
+
### 1. Error Handling
|
|
761
|
+
|
|
762
|
+
```typescript
|
|
763
|
+
async safeSnsOperation<T>(operation: () => Promise<T>): Promise<T | null> {
|
|
764
|
+
try {
|
|
765
|
+
return await operation();
|
|
766
|
+
} catch (error: any) {
|
|
767
|
+
if (error.name === 'NotFound') {
|
|
768
|
+
console.warn('SNS resource not found');
|
|
769
|
+
return null;
|
|
770
|
+
} else if (error.name === 'InvalidParameter') {
|
|
771
|
+
console.error('Invalid SNS parameter:', error.message);
|
|
772
|
+
throw new Error('Invalid SNS parameter');
|
|
773
|
+
} else {
|
|
774
|
+
console.error('SNS operation failed:', error);
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### 2. Message Size Optimization
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
validateMessageSize(message: string): boolean {
|
|
785
|
+
// SNS has a 256KB limit for messages
|
|
786
|
+
const sizeInBytes = Buffer.byteLength(message, 'utf8');
|
|
787
|
+
const maxSize = 256 * 1024; // 256KB
|
|
788
|
+
|
|
789
|
+
if (sizeInBytes > maxSize) {
|
|
790
|
+
throw new Error(`Message size (${sizeInBytes} bytes) exceeds SNS limit (${maxSize} bytes)`);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### 3. Phone Number Validation
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
validatePhoneNumber(phoneNumber: string): boolean {
|
|
801
|
+
// E.164 format validation
|
|
802
|
+
const e164Regex = /^\+[1-9]\d{1,14}$/;
|
|
803
|
+
return e164Regex.test(phoneNumber);
|
|
804
|
+
}
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
## Testing
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
811
|
+
import { ServerAwsSnsModule } from '@onivoro/server-aws-sns';
|
|
812
|
+
import { SNSClient } from '@aws-sdk/client-sns';
|
|
813
|
+
|
|
814
|
+
describe('SNSClient', () => {
|
|
815
|
+
let snsClient: SNSClient;
|
|
816
|
+
|
|
817
|
+
beforeEach(async () => {
|
|
818
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
819
|
+
imports: [ServerAwsSnsModule.configure({
|
|
820
|
+
AWS_REGION: 'us-east-1',
|
|
821
|
+
AWS_PROFILE: 'test'
|
|
822
|
+
})],
|
|
823
|
+
}).compile();
|
|
824
|
+
|
|
825
|
+
snsClient = module.get<SNSClient>(SNSClient);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('should be defined', () => {
|
|
829
|
+
expect(snsClient).toBeDefined();
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
## API Reference
|
|
835
|
+
|
|
836
|
+
### Exported Classes
|
|
837
|
+
- `ServerAwsSnsConfig`: Configuration class for SNS settings
|
|
838
|
+
- `ServerAwsSnsModule`: NestJS module for SNS integration
|
|
839
|
+
|
|
840
|
+
### Exported Services
|
|
841
|
+
- `SNSClient`: AWS SNS client instance (from @aws-sdk/client-sns)
|
|
842
|
+
|
|
843
|
+
## License
|
|
844
|
+
|
|
845
|
+
This package is part of the Onivoro monorepo and follows the same licensing terms.
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
export default {
|
|
3
|
+
displayName: 'lib-server-aws-sns',
|
|
4
|
+
preset: '../../../jest.preset.js',
|
|
5
|
+
testEnvironment: 'node',
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
|
8
|
+
},
|
|
9
|
+
moduleFileExtensions: ['ts', 'js', 'html'],
|
|
10
|
+
coverageDirectory: '../../../coverage/libs/server/aws-sns',
|
|
11
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onivoro/server-aws-sns",
|
|
3
|
+
"version": "24.0.2",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"types": "./src/index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@onivoro/server-aws-credential-providers": "24.0.2",
|
|
9
|
+
"@onivoro/server-common": "24.0.2",
|
|
10
|
+
"tslib": "^2.3.0"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@aws-sdk/client-sns": "~3.782.0",
|
|
14
|
+
"@nestjs/common": "~10.4.6"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lib-server-aws-sns",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/server/aws-sns/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"targets": {
|
|
7
|
+
"build": {
|
|
8
|
+
"executor": "@nx/js:tsc",
|
|
9
|
+
"outputs": ["{options.outputPath}"],
|
|
10
|
+
"options": {
|
|
11
|
+
"outputPath": "dist/libs/server/aws-sns",
|
|
12
|
+
"main": "libs/server/aws-sns/src/index.ts",
|
|
13
|
+
"tsConfig": "libs/server/aws-sns/tsconfig.lib.json",
|
|
14
|
+
"assets": [
|
|
15
|
+
"libs/server/aws-sns/README.md",
|
|
16
|
+
"libs/server/aws-sns/package.json"
|
|
17
|
+
],
|
|
18
|
+
"declaration": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"tags": []
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { moduleFactory } from '@onivoro/server-common';
|
|
3
|
+
import { SNSClient } from '@aws-sdk/client-sns';
|
|
4
|
+
import { ServerAwsSnsConfig } from './classes/server-aws-sns-config.class';
|
|
5
|
+
import { AwsCredentials, ServerAwsCredentialProvidersModule } from '@onivoro/server-aws-credential-providers';
|
|
6
|
+
|
|
7
|
+
let snsClient: SNSClient | null = null;
|
|
8
|
+
|
|
9
|
+
@Module({
|
|
10
|
+
})
|
|
11
|
+
export class ServerAwsSnsModule {
|
|
12
|
+
static configure(config: ServerAwsSnsConfig) {
|
|
13
|
+
return moduleFactory({
|
|
14
|
+
module: ServerAwsSnsModule,
|
|
15
|
+
imports: [ServerAwsCredentialProvidersModule.configure(config)],
|
|
16
|
+
providers: [
|
|
17
|
+
{
|
|
18
|
+
provide: SNSClient,
|
|
19
|
+
useFactory: (credentials: AwsCredentials) => snsClient
|
|
20
|
+
? snsClient
|
|
21
|
+
: snsClient = new SNSClient({
|
|
22
|
+
region: config.AWS_REGION,
|
|
23
|
+
logger: console,
|
|
24
|
+
credentials
|
|
25
|
+
}),
|
|
26
|
+
inject: [AwsCredentials]
|
|
27
|
+
},
|
|
28
|
+
{ provide: ServerAwsSnsConfig, useValue: config },
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.server.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../../dist/out-tsc"
|
|
5
|
+
},
|
|
6
|
+
"files": [],
|
|
7
|
+
"include": [],
|
|
8
|
+
"references": [
|
|
9
|
+
{
|
|
10
|
+
"path": "./tsconfig.lib.json"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "./tsconfig.spec.json"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"types": [
|
|
5
|
+
"jest",
|
|
6
|
+
"node"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"include": [
|
|
10
|
+
"jest.config.ts",
|
|
11
|
+
"**/*.test.ts",
|
|
12
|
+
"**/*.spec.ts",
|
|
13
|
+
"**/*.test.tsx",
|
|
14
|
+
"**/*.spec.tsx",
|
|
15
|
+
"**/*.test.js",
|
|
16
|
+
"**/*.spec.js",
|
|
17
|
+
"**/*.test.jsx",
|
|
18
|
+
"**/*.spec.jsx",
|
|
19
|
+
"**/*.d.ts"
|
|
20
|
+
]
|
|
21
|
+
}
|