@stacksjs/ts-cloud-core 0.1.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/LICENSE.md +21 -0
- package/README.md +321 -0
- package/package.json +31 -0
- package/src/advanced-features.test.ts +465 -0
- package/src/aws/cloudformation.ts +421 -0
- package/src/aws/cloudfront.ts +158 -0
- package/src/aws/credentials.test.ts +132 -0
- package/src/aws/credentials.ts +545 -0
- package/src/aws/index.ts +87 -0
- package/src/aws/s3.test.ts +188 -0
- package/src/aws/s3.ts +1088 -0
- package/src/aws/signature.test.ts +670 -0
- package/src/aws/signature.ts +1155 -0
- package/src/backup/disaster-recovery.test.ts +726 -0
- package/src/backup/disaster-recovery.ts +500 -0
- package/src/backup/index.ts +34 -0
- package/src/backup/manager.test.ts +498 -0
- package/src/backup/manager.ts +432 -0
- package/src/cicd/circleci.ts +430 -0
- package/src/cicd/github-actions.ts +424 -0
- package/src/cicd/gitlab-ci.ts +255 -0
- package/src/cicd/index.ts +8 -0
- package/src/cli/history.ts +396 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/progress.ts +458 -0
- package/src/cli/repl.ts +454 -0
- package/src/cli/suggestions.ts +327 -0
- package/src/cli/table.test.ts +319 -0
- package/src/cli/table.ts +332 -0
- package/src/cloudformation/builder.test.ts +327 -0
- package/src/cloudformation/builder.ts +378 -0
- package/src/cloudformation/builders/api-gateway.ts +449 -0
- package/src/cloudformation/builders/cache.ts +334 -0
- package/src/cloudformation/builders/cdn.ts +278 -0
- package/src/cloudformation/builders/compute.ts +485 -0
- package/src/cloudformation/builders/database.ts +392 -0
- package/src/cloudformation/builders/functions.ts +343 -0
- package/src/cloudformation/builders/messaging.ts +140 -0
- package/src/cloudformation/builders/monitoring.ts +300 -0
- package/src/cloudformation/builders/network.ts +264 -0
- package/src/cloudformation/builders/queue.ts +147 -0
- package/src/cloudformation/builders/security.ts +399 -0
- package/src/cloudformation/builders/storage.ts +285 -0
- package/src/cloudformation/index.ts +30 -0
- package/src/cloudformation/types.ts +173 -0
- package/src/compliance/aws-config.ts +543 -0
- package/src/compliance/cloudtrail.ts +376 -0
- package/src/compliance/compliance.test.ts +423 -0
- package/src/compliance/guardduty.ts +446 -0
- package/src/compliance/index.ts +66 -0
- package/src/compliance/security-hub.ts +456 -0
- package/src/containers/build-optimization.ts +416 -0
- package/src/containers/containers.test.ts +508 -0
- package/src/containers/image-scanning.ts +360 -0
- package/src/containers/index.ts +9 -0
- package/src/containers/registry.ts +293 -0
- package/src/containers/service-mesh.ts +520 -0
- package/src/database/database.test.ts +762 -0
- package/src/database/index.ts +9 -0
- package/src/database/migrations.ts +444 -0
- package/src/database/performance.ts +528 -0
- package/src/database/replicas.ts +534 -0
- package/src/database/users.ts +494 -0
- package/src/dependency-graph.ts +143 -0
- package/src/deployment/ab-testing.ts +582 -0
- package/src/deployment/blue-green.ts +452 -0
- package/src/deployment/canary.ts +500 -0
- package/src/deployment/deployment.test.ts +526 -0
- package/src/deployment/index.ts +61 -0
- package/src/deployment/progressive.ts +62 -0
- package/src/dns/dns.test.ts +641 -0
- package/src/dns/dnssec.ts +315 -0
- package/src/dns/index.ts +8 -0
- package/src/dns/resolver.ts +496 -0
- package/src/dns/routing.ts +593 -0
- package/src/email/advanced/analytics.ts +445 -0
- package/src/email/advanced/index.ts +11 -0
- package/src/email/advanced/rules.ts +465 -0
- package/src/email/advanced/scheduling.ts +352 -0
- package/src/email/advanced/search.ts +412 -0
- package/src/email/advanced/shared-mailboxes.ts +404 -0
- package/src/email/advanced/templates.ts +455 -0
- package/src/email/advanced/threading.ts +281 -0
- package/src/email/analytics.ts +467 -0
- package/src/email/bounce-handling.ts +425 -0
- package/src/email/email.test.ts +431 -0
- package/src/email/handlers/__tests__/inbound.test.ts +38 -0
- package/src/email/handlers/__tests__/outbound.test.ts +37 -0
- package/src/email/handlers/converter.ts +227 -0
- package/src/email/handlers/feedback.ts +228 -0
- package/src/email/handlers/inbound.ts +169 -0
- package/src/email/handlers/outbound.ts +178 -0
- package/src/email/index.ts +15 -0
- package/src/email/reputation.ts +303 -0
- package/src/email/templates.ts +352 -0
- package/src/errors/index.test.ts +434 -0
- package/src/errors/index.ts +416 -0
- package/src/health-checks/index.ts +40 -0
- package/src/index.ts +360 -0
- package/src/intrinsic-functions.ts +118 -0
- package/src/lambda/concurrency.ts +330 -0
- package/src/lambda/destinations.ts +345 -0
- package/src/lambda/dlq.ts +425 -0
- package/src/lambda/index.ts +11 -0
- package/src/lambda/lambda.test.ts +840 -0
- package/src/lambda/layers.ts +263 -0
- package/src/lambda/versions.ts +376 -0
- package/src/lambda/vpc.ts +399 -0
- package/src/local/config.ts +114 -0
- package/src/local/index.ts +6 -0
- package/src/local/mock-aws.ts +351 -0
- package/src/modules/ai.ts +340 -0
- package/src/modules/api.ts +478 -0
- package/src/modules/auth.ts +805 -0
- package/src/modules/cache.ts +417 -0
- package/src/modules/cdn.ts +1062 -0
- package/src/modules/communication.ts +1094 -0
- package/src/modules/compute.ts +3348 -0
- package/src/modules/database.ts +554 -0
- package/src/modules/deployment.ts +1079 -0
- package/src/modules/dns.ts +337 -0
- package/src/modules/email.ts +1538 -0
- package/src/modules/filesystem.ts +515 -0
- package/src/modules/index.ts +32 -0
- package/src/modules/messaging.ts +486 -0
- package/src/modules/monitoring.ts +2086 -0
- package/src/modules/network.ts +664 -0
- package/src/modules/parameter-store.ts +325 -0
- package/src/modules/permissions.ts +1081 -0
- package/src/modules/phone.ts +494 -0
- package/src/modules/queue.ts +1260 -0
- package/src/modules/redirects.ts +464 -0
- package/src/modules/registry.ts +699 -0
- package/src/modules/search.ts +401 -0
- package/src/modules/secrets.ts +416 -0
- package/src/modules/security.ts +731 -0
- package/src/modules/sms.ts +389 -0
- package/src/modules/storage.ts +1120 -0
- package/src/modules/workflow.ts +680 -0
- package/src/multi-account/config.ts +521 -0
- package/src/multi-account/index.ts +7 -0
- package/src/multi-account/manager.ts +427 -0
- package/src/multi-region/cross-region.ts +410 -0
- package/src/multi-region/index.ts +8 -0
- package/src/multi-region/manager.ts +483 -0
- package/src/multi-region/regions.ts +435 -0
- package/src/network-security/index.ts +48 -0
- package/src/observability/index.ts +9 -0
- package/src/observability/logs.ts +522 -0
- package/src/observability/metrics.ts +460 -0
- package/src/observability/observability.test.ts +782 -0
- package/src/observability/synthetics.ts +568 -0
- package/src/observability/xray.ts +358 -0
- package/src/phone/advanced/analytics.ts +349 -0
- package/src/phone/advanced/callbacks.ts +428 -0
- package/src/phone/advanced/index.ts +8 -0
- package/src/phone/advanced/ivr-builder.ts +504 -0
- package/src/phone/advanced/recording.ts +310 -0
- package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
- package/src/phone/handlers/incoming-call.ts +117 -0
- package/src/phone/handlers/missed-call.ts +116 -0
- package/src/phone/handlers/voicemail.ts +179 -0
- package/src/phone/index.ts +9 -0
- package/src/presets/api-backend.ts +134 -0
- package/src/presets/data-pipeline.ts +204 -0
- package/src/presets/extend.test.ts +295 -0
- package/src/presets/extend.ts +297 -0
- package/src/presets/fullstack-app.ts +144 -0
- package/src/presets/index.ts +27 -0
- package/src/presets/jamstack.ts +135 -0
- package/src/presets/microservices.ts +167 -0
- package/src/presets/ml-api.ts +208 -0
- package/src/presets/nodejs-server.ts +104 -0
- package/src/presets/nodejs-serverless.ts +114 -0
- package/src/presets/realtime-app.ts +184 -0
- package/src/presets/static-site.ts +64 -0
- package/src/presets/traditional-web-app.ts +339 -0
- package/src/presets/wordpress.ts +138 -0
- package/src/preview/github.test.ts +249 -0
- package/src/preview/github.ts +297 -0
- package/src/preview/index.ts +37 -0
- package/src/preview/manager.test.ts +440 -0
- package/src/preview/manager.ts +326 -0
- package/src/preview/notifications.test.ts +582 -0
- package/src/preview/notifications.ts +341 -0
- package/src/queue/batch-processing.ts +402 -0
- package/src/queue/dlq-monitoring.ts +402 -0
- package/src/queue/fifo.ts +342 -0
- package/src/queue/index.ts +9 -0
- package/src/queue/management.ts +428 -0
- package/src/queue/queue.test.ts +429 -0
- package/src/resource-mgmt/index.ts +39 -0
- package/src/resource-naming.ts +62 -0
- package/src/s3/index.ts +523 -0
- package/src/schema/cloud-config.schema.json +554 -0
- package/src/schema/index.ts +68 -0
- package/src/security/certificate-manager.ts +492 -0
- package/src/security/index.ts +9 -0
- package/src/security/scanning.ts +545 -0
- package/src/security/secrets-manager.ts +476 -0
- package/src/security/secrets-rotation.ts +456 -0
- package/src/security/security.test.ts +738 -0
- package/src/sms/advanced/ab-testing.ts +389 -0
- package/src/sms/advanced/analytics.ts +336 -0
- package/src/sms/advanced/campaigns.ts +523 -0
- package/src/sms/advanced/chatbot.ts +224 -0
- package/src/sms/advanced/index.ts +10 -0
- package/src/sms/advanced/link-tracking.ts +248 -0
- package/src/sms/advanced/mms.ts +308 -0
- package/src/sms/handlers/__tests__/send.test.ts +40 -0
- package/src/sms/handlers/delivery-status.ts +133 -0
- package/src/sms/handlers/receive.ts +162 -0
- package/src/sms/handlers/send.ts +174 -0
- package/src/sms/index.ts +9 -0
- package/src/stack-diff.ts +389 -0
- package/src/static-site/index.ts +85 -0
- package/src/template-builder.ts +110 -0
- package/src/template-validator.ts +574 -0
- package/src/utils/cache.ts +291 -0
- package/src/utils/diff.ts +269 -0
- package/src/utils/hash.ts +227 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/parallel.ts +294 -0
- package/src/validators/credentials.test.ts +274 -0
- package/src/validators/credentials.ts +233 -0
- package/src/validators/quotas.test.ts +434 -0
- package/src/validators/quotas.ts +217 -0
- package/test/ai.test.ts +327 -0
- package/test/api.test.ts +511 -0
- package/test/auth.test.ts +632 -0
- package/test/cache.test.ts +406 -0
- package/test/cdn.test.ts +247 -0
- package/test/compute.test.ts +861 -0
- package/test/database.test.ts +523 -0
- package/test/deployment.test.ts +499 -0
- package/test/dns.test.ts +270 -0
- package/test/email.test.ts +439 -0
- package/test/filesystem.test.ts +382 -0
- package/test/integration.test.ts +350 -0
- package/test/messaging.test.ts +514 -0
- package/test/monitoring.test.ts +634 -0
- package/test/network.test.ts +425 -0
- package/test/permissions.test.ts +488 -0
- package/test/queue.test.ts +484 -0
- package/test/registry.test.ts +306 -0
- package/test/security.test.ts +462 -0
- package/test/storage.test.ts +463 -0
- package/test/template-validator.test.ts +559 -0
- package/test/workflow.test.ts +592 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMS Campaigns and Scheduling
|
|
3
|
+
*
|
|
4
|
+
* Provides campaign management and scheduled SMS sending
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface SmsCampaign {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
description?: string
|
|
11
|
+
status: 'draft' | 'scheduled' | 'running' | 'paused' | 'completed' | 'cancelled'
|
|
12
|
+
message: {
|
|
13
|
+
body: string
|
|
14
|
+
template?: string
|
|
15
|
+
variables?: string[]
|
|
16
|
+
}
|
|
17
|
+
audience: CampaignAudience
|
|
18
|
+
schedule: CampaignSchedule
|
|
19
|
+
settings: CampaignSettings
|
|
20
|
+
stats: CampaignStats
|
|
21
|
+
createdAt: string
|
|
22
|
+
updatedAt: string
|
|
23
|
+
startedAt?: string
|
|
24
|
+
completedAt?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CampaignAudience {
|
|
28
|
+
type: 'list' | 'segment' | 'all'
|
|
29
|
+
listId?: string
|
|
30
|
+
segmentId?: string
|
|
31
|
+
filters?: AudienceFilter[]
|
|
32
|
+
estimatedSize?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AudienceFilter {
|
|
36
|
+
field: string
|
|
37
|
+
operator: 'equals' | 'not-equals' | 'contains' | 'greater-than' | 'less-than'
|
|
38
|
+
value: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CampaignSchedule {
|
|
42
|
+
type: 'immediate' | 'scheduled' | 'recurring'
|
|
43
|
+
scheduledFor?: string
|
|
44
|
+
timezone?: string
|
|
45
|
+
recurrence?: {
|
|
46
|
+
frequency: 'daily' | 'weekly' | 'monthly'
|
|
47
|
+
interval: number
|
|
48
|
+
endDate?: string
|
|
49
|
+
maxOccurrences?: number
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CampaignSettings {
|
|
54
|
+
messageType: 'TRANSACTIONAL' | 'PROMOTIONAL'
|
|
55
|
+
senderId?: string
|
|
56
|
+
originationNumber?: string
|
|
57
|
+
throttleRate?: number // messages per second
|
|
58
|
+
quietHours?: {
|
|
59
|
+
start: string // HH:MM
|
|
60
|
+
end: string
|
|
61
|
+
timezone: string
|
|
62
|
+
}
|
|
63
|
+
optOutHandling: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CampaignStats {
|
|
67
|
+
totalRecipients: number
|
|
68
|
+
sent: number
|
|
69
|
+
delivered: number
|
|
70
|
+
failed: number
|
|
71
|
+
optedOut: number
|
|
72
|
+
deliveryRate: number
|
|
73
|
+
cost: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* SMS Campaigns Module
|
|
78
|
+
*/
|
|
79
|
+
export class SmsCampaigns {
|
|
80
|
+
/**
|
|
81
|
+
* Lambda code for campaign management
|
|
82
|
+
*/
|
|
83
|
+
static CampaignManagerCode = `
|
|
84
|
+
const { DynamoDBClient, PutItemCommand, GetItemCommand, UpdateItemCommand, ScanCommand } = require('@aws-sdk/client-dynamodb');
|
|
85
|
+
|
|
86
|
+
const dynamodb = new DynamoDBClient({});
|
|
87
|
+
const CAMPAIGNS_TABLE = process.env.CAMPAIGNS_TABLE;
|
|
88
|
+
|
|
89
|
+
exports.handler = async (event) => {
|
|
90
|
+
console.log('Campaign manager event:', JSON.stringify(event, null, 2));
|
|
91
|
+
|
|
92
|
+
const { httpMethod, body, pathParameters } = event;
|
|
93
|
+
const campaignId = pathParameters?.id;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
switch (httpMethod) {
|
|
97
|
+
case 'POST':
|
|
98
|
+
return await createCampaign(JSON.parse(body || '{}'));
|
|
99
|
+
case 'GET':
|
|
100
|
+
if (campaignId) {
|
|
101
|
+
return await getCampaign(campaignId);
|
|
102
|
+
}
|
|
103
|
+
return await listCampaigns(event.queryStringParameters);
|
|
104
|
+
case 'PUT':
|
|
105
|
+
return await updateCampaign(campaignId, JSON.parse(body || '{}'));
|
|
106
|
+
case 'DELETE':
|
|
107
|
+
return await cancelCampaign(campaignId);
|
|
108
|
+
default:
|
|
109
|
+
return { statusCode: 405, body: JSON.stringify({ error: 'Method not allowed' }) };
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Error:', error);
|
|
113
|
+
return { statusCode: 500, body: JSON.stringify({ error: error.message }) };
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
async function createCampaign(data) {
|
|
118
|
+
const id = \`camp-\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`;
|
|
119
|
+
const now = new Date().toISOString();
|
|
120
|
+
|
|
121
|
+
const campaign = {
|
|
122
|
+
id: { S: id },
|
|
123
|
+
name: { S: data.name },
|
|
124
|
+
description: { S: data.description || '' },
|
|
125
|
+
status: { S: 'draft' },
|
|
126
|
+
message: { S: JSON.stringify(data.message || {}) },
|
|
127
|
+
audience: { S: JSON.stringify(data.audience || {}) },
|
|
128
|
+
schedule: { S: JSON.stringify(data.schedule || { type: 'immediate' }) },
|
|
129
|
+
settings: { S: JSON.stringify(data.settings || { messageType: 'TRANSACTIONAL', optOutHandling: true }) },
|
|
130
|
+
stats: { S: JSON.stringify({ totalRecipients: 0, sent: 0, delivered: 0, failed: 0, optedOut: 0, deliveryRate: 0, cost: 0 }) },
|
|
131
|
+
createdAt: { S: now },
|
|
132
|
+
updatedAt: { S: now },
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
await dynamodb.send(new PutItemCommand({
|
|
136
|
+
TableName: CAMPAIGNS_TABLE,
|
|
137
|
+
Item: campaign,
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
statusCode: 201,
|
|
142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
body: JSON.stringify({ id, status: 'draft', createdAt: now }),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function getCampaign(id) {
|
|
148
|
+
const result = await dynamodb.send(new GetItemCommand({
|
|
149
|
+
TableName: CAMPAIGNS_TABLE,
|
|
150
|
+
Key: { id: { S: id } },
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
if (!result.Item) {
|
|
154
|
+
return { statusCode: 404, body: JSON.stringify({ error: 'Campaign not found' }) };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
statusCode: 200,
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
body: JSON.stringify(unmarshallCampaign(result.Item)),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function listCampaigns(params) {
|
|
165
|
+
const result = await dynamodb.send(new ScanCommand({
|
|
166
|
+
TableName: CAMPAIGNS_TABLE,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
const campaigns = (result.Items || []).map(unmarshallCampaign);
|
|
170
|
+
campaigns.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
statusCode: 200,
|
|
174
|
+
headers: { 'Content-Type': 'application/json' },
|
|
175
|
+
body: JSON.stringify(campaigns),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function updateCampaign(id, data) {
|
|
180
|
+
const now = new Date().toISOString();
|
|
181
|
+
|
|
182
|
+
const updateExpressions = ['updatedAt = :now'];
|
|
183
|
+
const expressionValues = { ':now': { S: now } };
|
|
184
|
+
|
|
185
|
+
if (data.name) {
|
|
186
|
+
updateExpressions.push('name = :name');
|
|
187
|
+
expressionValues[':name'] = { S: data.name };
|
|
188
|
+
}
|
|
189
|
+
if (data.message) {
|
|
190
|
+
updateExpressions.push('message = :message');
|
|
191
|
+
expressionValues[':message'] = { S: JSON.stringify(data.message) };
|
|
192
|
+
}
|
|
193
|
+
if (data.audience) {
|
|
194
|
+
updateExpressions.push('audience = :audience');
|
|
195
|
+
expressionValues[':audience'] = { S: JSON.stringify(data.audience) };
|
|
196
|
+
}
|
|
197
|
+
if (data.schedule) {
|
|
198
|
+
updateExpressions.push('schedule = :schedule');
|
|
199
|
+
expressionValues[':schedule'] = { S: JSON.stringify(data.schedule) };
|
|
200
|
+
}
|
|
201
|
+
if (data.settings) {
|
|
202
|
+
updateExpressions.push('settings = :settings');
|
|
203
|
+
expressionValues[':settings'] = { S: JSON.stringify(data.settings) };
|
|
204
|
+
}
|
|
205
|
+
if (data.status) {
|
|
206
|
+
updateExpressions.push('#status = :status');
|
|
207
|
+
expressionValues[':status'] = { S: data.status };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await dynamodb.send(new UpdateItemCommand({
|
|
211
|
+
TableName: CAMPAIGNS_TABLE,
|
|
212
|
+
Key: { id: { S: id } },
|
|
213
|
+
UpdateExpression: 'SET ' + updateExpressions.join(', '),
|
|
214
|
+
ExpressionAttributeNames: data.status ? { '#status': 'status' } : undefined,
|
|
215
|
+
ExpressionAttributeValues: expressionValues,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
statusCode: 200,
|
|
220
|
+
headers: { 'Content-Type': 'application/json' },
|
|
221
|
+
body: JSON.stringify({ id, updatedAt: now }),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function cancelCampaign(id) {
|
|
226
|
+
await dynamodb.send(new UpdateItemCommand({
|
|
227
|
+
TableName: CAMPAIGNS_TABLE,
|
|
228
|
+
Key: { id: { S: id } },
|
|
229
|
+
UpdateExpression: 'SET #status = :status, updatedAt = :now',
|
|
230
|
+
ExpressionAttributeNames: { '#status': 'status' },
|
|
231
|
+
ExpressionAttributeValues: {
|
|
232
|
+
':status': { S: 'cancelled' },
|
|
233
|
+
':now': { S: new Date().toISOString() },
|
|
234
|
+
},
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
return { statusCode: 200, body: JSON.stringify({ id, status: 'cancelled' }) };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function unmarshallCampaign(item) {
|
|
241
|
+
return {
|
|
242
|
+
id: item.id.S,
|
|
243
|
+
name: item.name.S,
|
|
244
|
+
description: item.description?.S,
|
|
245
|
+
status: item.status.S,
|
|
246
|
+
message: JSON.parse(item.message?.S || '{}'),
|
|
247
|
+
audience: JSON.parse(item.audience?.S || '{}'),
|
|
248
|
+
schedule: JSON.parse(item.schedule?.S || '{}'),
|
|
249
|
+
settings: JSON.parse(item.settings?.S || '{}'),
|
|
250
|
+
stats: JSON.parse(item.stats?.S || '{}'),
|
|
251
|
+
createdAt: item.createdAt.S,
|
|
252
|
+
updatedAt: item.updatedAt.S,
|
|
253
|
+
startedAt: item.startedAt?.S,
|
|
254
|
+
completedAt: item.completedAt?.S,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
`
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Lambda code for campaign execution
|
|
261
|
+
*/
|
|
262
|
+
static CampaignExecutorCode = `
|
|
263
|
+
const { DynamoDBClient, GetItemCommand, UpdateItemCommand, ScanCommand } = require('@aws-sdk/client-dynamodb');
|
|
264
|
+
const { PinpointClient, SendMessagesCommand } = require('@aws-sdk/client-pinpoint');
|
|
265
|
+
|
|
266
|
+
const dynamodb = new DynamoDBClient({});
|
|
267
|
+
const pinpoint = new PinpointClient({});
|
|
268
|
+
|
|
269
|
+
const CAMPAIGNS_TABLE = process.env.CAMPAIGNS_TABLE;
|
|
270
|
+
const CONTACTS_TABLE = process.env.CONTACTS_TABLE;
|
|
271
|
+
const OPT_OUT_TABLE = process.env.OPT_OUT_TABLE;
|
|
272
|
+
const PINPOINT_APP_ID = process.env.PINPOINT_APP_ID;
|
|
273
|
+
|
|
274
|
+
exports.handler = async (event) => {
|
|
275
|
+
console.log('Campaign executor event:', JSON.stringify(event, null, 2));
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
// Get scheduled campaigns
|
|
279
|
+
const result = await dynamodb.send(new ScanCommand({
|
|
280
|
+
TableName: CAMPAIGNS_TABLE,
|
|
281
|
+
FilterExpression: '#status = :scheduled',
|
|
282
|
+
ExpressionAttributeNames: { '#status': 'status' },
|
|
283
|
+
ExpressionAttributeValues: {
|
|
284
|
+
':scheduled': { S: 'scheduled' },
|
|
285
|
+
},
|
|
286
|
+
}));
|
|
287
|
+
|
|
288
|
+
const campaigns = result.Items || [];
|
|
289
|
+
const now = new Date();
|
|
290
|
+
|
|
291
|
+
for (const item of campaigns) {
|
|
292
|
+
const campaign = unmarshallCampaign(item);
|
|
293
|
+
const schedule = campaign.schedule;
|
|
294
|
+
|
|
295
|
+
// Check if it's time to run
|
|
296
|
+
if (schedule.type === 'scheduled' && schedule.scheduledFor) {
|
|
297
|
+
const scheduledTime = new Date(schedule.scheduledFor);
|
|
298
|
+
if (scheduledTime > now) continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check quiet hours
|
|
302
|
+
if (campaign.settings.quietHours) {
|
|
303
|
+
const { start, end, timezone } = campaign.settings.quietHours;
|
|
304
|
+
const localTime = new Date().toLocaleTimeString('en-US', { timeZone: timezone, hour12: false });
|
|
305
|
+
if (localTime >= start && localTime <= end) {
|
|
306
|
+
console.log(\`Campaign \${campaign.id} skipped - quiet hours\`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Start campaign
|
|
312
|
+
await executeCampaign(campaign);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { statusCode: 200 };
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error('Error executing campaigns:', error);
|
|
318
|
+
return { statusCode: 500, error: error.message };
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
async function executeCampaign(campaign) {
|
|
323
|
+
console.log(\`Executing campaign: \${campaign.id}\`);
|
|
324
|
+
|
|
325
|
+
// Update status to running
|
|
326
|
+
await dynamodb.send(new UpdateItemCommand({
|
|
327
|
+
TableName: CAMPAIGNS_TABLE,
|
|
328
|
+
Key: { id: { S: campaign.id } },
|
|
329
|
+
UpdateExpression: 'SET #status = :status, startedAt = :now',
|
|
330
|
+
ExpressionAttributeNames: { '#status': 'status' },
|
|
331
|
+
ExpressionAttributeValues: {
|
|
332
|
+
':status': { S: 'running' },
|
|
333
|
+
':now': { S: new Date().toISOString() },
|
|
334
|
+
},
|
|
335
|
+
}));
|
|
336
|
+
|
|
337
|
+
// Get recipients
|
|
338
|
+
const recipients = await getRecipients(campaign.audience);
|
|
339
|
+
const optedOut = await getOptedOutNumbers();
|
|
340
|
+
|
|
341
|
+
// Filter out opted-out numbers
|
|
342
|
+
const eligibleRecipients = recipients.filter(r => !optedOut.has(r.phoneNumber));
|
|
343
|
+
|
|
344
|
+
let sent = 0;
|
|
345
|
+
let delivered = 0;
|
|
346
|
+
let failed = 0;
|
|
347
|
+
let cost = 0;
|
|
348
|
+
|
|
349
|
+
// Send messages in batches
|
|
350
|
+
const batchSize = campaign.settings.throttleRate || 20;
|
|
351
|
+
|
|
352
|
+
for (let i = 0; i < eligibleRecipients.length; i += batchSize) {
|
|
353
|
+
const batch = eligibleRecipients.slice(i, i + batchSize);
|
|
354
|
+
|
|
355
|
+
for (const recipient of batch) {
|
|
356
|
+
try {
|
|
357
|
+
const messageBody = resolveTemplate(campaign.message.body, recipient);
|
|
358
|
+
|
|
359
|
+
const result = await pinpoint.send(new SendMessagesCommand({
|
|
360
|
+
ApplicationId: PINPOINT_APP_ID,
|
|
361
|
+
MessageRequest: {
|
|
362
|
+
Addresses: {
|
|
363
|
+
[recipient.phoneNumber]: {
|
|
364
|
+
ChannelType: 'SMS',
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
MessageConfiguration: {
|
|
368
|
+
SMSMessage: {
|
|
369
|
+
Body: messageBody,
|
|
370
|
+
MessageType: campaign.settings.messageType,
|
|
371
|
+
SenderId: campaign.settings.senderId,
|
|
372
|
+
OriginationNumber: campaign.settings.originationNumber,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
}));
|
|
377
|
+
|
|
378
|
+
const status = result.MessageResponse?.Result?.[recipient.phoneNumber]?.DeliveryStatus;
|
|
379
|
+
if (status === 'SUCCESSFUL') {
|
|
380
|
+
delivered++;
|
|
381
|
+
cost += 0.00645; // Approximate US SMS cost
|
|
382
|
+
} else {
|
|
383
|
+
failed++;
|
|
384
|
+
}
|
|
385
|
+
sent++;
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error(\`Failed to send to \${recipient.phoneNumber}:\`, error);
|
|
388
|
+
failed++;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Throttle between batches
|
|
393
|
+
if (i + batchSize < eligibleRecipients.length) {
|
|
394
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Update campaign stats
|
|
399
|
+
await dynamodb.send(new UpdateItemCommand({
|
|
400
|
+
TableName: CAMPAIGNS_TABLE,
|
|
401
|
+
Key: { id: { S: campaign.id } },
|
|
402
|
+
UpdateExpression: 'SET #status = :status, stats = :stats, completedAt = :now',
|
|
403
|
+
ExpressionAttributeNames: { '#status': 'status' },
|
|
404
|
+
ExpressionAttributeValues: {
|
|
405
|
+
':status': { S: 'completed' },
|
|
406
|
+
':stats': { S: JSON.stringify({
|
|
407
|
+
totalRecipients: eligibleRecipients.length,
|
|
408
|
+
sent,
|
|
409
|
+
delivered,
|
|
410
|
+
failed,
|
|
411
|
+
optedOut: recipients.length - eligibleRecipients.length,
|
|
412
|
+
deliveryRate: sent > 0 ? (delivered / sent) * 100 : 0,
|
|
413
|
+
cost: Math.round(cost * 100) / 100,
|
|
414
|
+
})},
|
|
415
|
+
':now': { S: new Date().toISOString() },
|
|
416
|
+
},
|
|
417
|
+
}));
|
|
418
|
+
|
|
419
|
+
console.log(\`Campaign \${campaign.id} completed: \${delivered}/\${sent} delivered\`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function getRecipients(audience) {
|
|
423
|
+
// Simplified - in production, query from contacts table based on audience type
|
|
424
|
+
if (audience.type === 'list' && audience.listId) {
|
|
425
|
+
const result = await dynamodb.send(new ScanCommand({
|
|
426
|
+
TableName: CONTACTS_TABLE,
|
|
427
|
+
FilterExpression: 'listId = :listId',
|
|
428
|
+
ExpressionAttributeValues: {
|
|
429
|
+
':listId': { S: audience.listId },
|
|
430
|
+
},
|
|
431
|
+
}));
|
|
432
|
+
return (result.Items || []).map(item => ({
|
|
433
|
+
phoneNumber: item.phoneNumber.S,
|
|
434
|
+
name: item.name?.S,
|
|
435
|
+
...JSON.parse(item.attributes?.S || '{}'),
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function getOptedOutNumbers() {
|
|
442
|
+
const result = await dynamodb.send(new ScanCommand({
|
|
443
|
+
TableName: OPT_OUT_TABLE,
|
|
444
|
+
}));
|
|
445
|
+
return new Set((result.Items || []).map(item => item.phoneNumber.S));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function resolveTemplate(template, data) {
|
|
449
|
+
let result = template;
|
|
450
|
+
for (const [key, value] of Object.entries(data)) {
|
|
451
|
+
result = result.replace(new RegExp(\`{{\\\\s*\${key}\\\\s*}}\`, 'g'), String(value));
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function unmarshallCampaign(item) {
|
|
457
|
+
return {
|
|
458
|
+
id: item.id.S,
|
|
459
|
+
name: item.name.S,
|
|
460
|
+
status: item.status.S,
|
|
461
|
+
message: JSON.parse(item.message?.S || '{}'),
|
|
462
|
+
audience: JSON.parse(item.audience?.S || '{}'),
|
|
463
|
+
schedule: JSON.parse(item.schedule?.S || '{}'),
|
|
464
|
+
settings: JSON.parse(item.settings?.S || '{}'),
|
|
465
|
+
stats: JSON.parse(item.stats?.S || '{}'),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
`
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Create campaigns DynamoDB table
|
|
472
|
+
*/
|
|
473
|
+
static createCampaignsTable(config: { slug: string }): Record<string, any> {
|
|
474
|
+
return {
|
|
475
|
+
[`${config.slug}SmsCampaignsTable`]: {
|
|
476
|
+
Type: 'AWS::DynamoDB::Table',
|
|
477
|
+
Properties: {
|
|
478
|
+
TableName: `${config.slug}-sms-campaigns`,
|
|
479
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
480
|
+
AttributeDefinitions: [
|
|
481
|
+
{ AttributeName: 'id', AttributeType: 'S' },
|
|
482
|
+
],
|
|
483
|
+
KeySchema: [
|
|
484
|
+
{ AttributeName: 'id', KeyType: 'HASH' },
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Create campaign manager Lambda
|
|
493
|
+
*/
|
|
494
|
+
static createCampaignManagerLambda(config: {
|
|
495
|
+
slug: string
|
|
496
|
+
roleArn: string
|
|
497
|
+
campaignsTable: string
|
|
498
|
+
}): Record<string, any> {
|
|
499
|
+
return {
|
|
500
|
+
[`${config.slug}SmsCampaignManagerLambda`]: {
|
|
501
|
+
Type: 'AWS::Lambda::Function',
|
|
502
|
+
Properties: {
|
|
503
|
+
FunctionName: `${config.slug}-sms-campaign-manager`,
|
|
504
|
+
Runtime: 'nodejs20.x',
|
|
505
|
+
Handler: 'index.handler',
|
|
506
|
+
Role: config.roleArn,
|
|
507
|
+
Timeout: 30,
|
|
508
|
+
MemorySize: 256,
|
|
509
|
+
Code: {
|
|
510
|
+
ZipFile: SmsCampaigns.CampaignManagerCode,
|
|
511
|
+
},
|
|
512
|
+
Environment: {
|
|
513
|
+
Variables: {
|
|
514
|
+
CAMPAIGNS_TABLE: config.campaignsTable,
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export default SmsCampaigns
|