@friggframework/core 2.0.0--canary.545.1176a00.0 → 2.0.0--canary.545.29ef032.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/core/Worker.js +33 -16
  2. package/database/encryption/encryption-schema-registry.js +1 -1
  3. package/database/models/WebsocketConnection.js +37 -16
  4. package/encrypt/Cryptor.js +53 -54
  5. package/encrypt/aes-encryption-key-provider.js +82 -0
  6. package/encrypt/encryption-key-provider-interface.js +50 -0
  7. package/handlers/routers/db-migration.js +57 -34
  8. package/handlers/routers/health.js +17 -244
  9. package/handlers/workers/db-migration.js +15 -11
  10. package/index.js +21 -0
  11. package/infrastructure/scheduler/index.js +1 -1
  12. package/infrastructure/scheduler/scheduler-service-factory.js +1 -1
  13. package/package.json +5 -9
  14. package/queues/index.js +2 -2
  15. package/queues/providers/index.js +0 -2
  16. package/queues/queue-client-interface.js +55 -0
  17. package/queues/queue-provider-factory.js +11 -8
  18. package/queues/queuer-util.js +33 -29
  19. package/types/core/index.d.ts +44 -5
  20. package/websocket/repositories/websocket-connection-repository-documentdb.js +29 -17
  21. package/websocket/repositories/websocket-connection-repository-mongo.js +26 -20
  22. package/websocket/repositories/websocket-connection-repository-postgres.js +26 -19
  23. package/websocket/repositories/websocket-connection-repository.js +26 -24
  24. package/websocket/websocket-message-sender-interface.js +38 -0
  25. package/database/adapters/lambda-invoker.js +0 -98
  26. package/database/repositories/migration-status-repository-s3.js +0 -142
  27. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
  28. package/queues/providers/sqs-queue-provider.js +0 -84
@@ -93,257 +93,30 @@ const validateApiKey = (req, res, next) => {
93
93
 
94
94
  router.use(validateApiKey);
95
95
 
96
- // Helper to detect VPC configuration
97
- const detectVpcConfiguration = async () => {
98
- const results = {
99
- isInVpc: false,
100
- hasInternetAccess: false,
101
- canResolvePublicDns: false,
102
- canConnectToAws: false,
103
- vpcEndpoints: [],
104
- };
105
-
96
+ // AWS-specific health checks (VPC, KMS) are lazy-loaded from provider-aws
97
+ // to avoid pulling in @aws-sdk on non-AWS platforms.
98
+ function getAwsHealthChecks() {
106
99
  try {
107
- // Check if we're in a VPC by looking for VPC-specific environment
108
- // Lambda in VPC has specific network interface configuration
109
- const dns = require('dns').promises;
110
-
111
- // Test 1: Can we resolve public DNS? (indicates DNS configuration)
112
- try {
113
- await Promise.race([
114
- dns.resolve4('www.google.com'),
115
- new Promise((_, reject) =>
116
- setTimeout(() => reject(new Error('timeout')), 2000)
117
- ),
118
- ]);
119
- results.canResolvePublicDns = true;
120
- } catch (e) {
121
- console.log('Public DNS resolution failed:', e.message);
122
- }
123
-
124
- // Test 2: Can we reach internet? (indicates NAT gateway)
125
- try {
126
- const https = require('https');
127
- await new Promise((resolve, reject) => {
128
- const req = https.get(
129
- 'https://www.google.com',
130
- { timeout: 2000 },
131
- (res) => {
132
- res.destroy();
133
- resolve(true);
134
- }
135
- );
136
- req.on('error', reject);
137
- req.on('timeout', () => {
138
- req.destroy();
139
- reject(new Error('timeout'));
140
- });
141
- });
142
- results.hasInternetAccess = true;
143
- } catch (e) {
144
- console.log('Internet connectivity test failed:', e.message);
145
- }
146
-
147
- // Test 3: Check for VPC endpoints by trying to resolve internal AWS endpoints
148
- const region = process.env.AWS_REGION; // Lambda always provides this
149
- const vpcEndpointDomains = [
150
- `com.amazonaws.${region}.kms`,
151
- `com.amazonaws.vpce.${region}`,
152
- `kms.${region}.amazonaws.com`,
153
- ];
154
-
155
- for (const domain of vpcEndpointDomains) {
156
- try {
157
- const addresses = await Promise.race([
158
- dns.resolve4(domain).catch(() => dns.resolve6(domain)),
159
- new Promise((_, reject) =>
160
- setTimeout(() => reject(new Error('timeout')), 1000)
161
- ),
162
- ]);
163
- if (addresses && addresses.length > 0) {
164
- // Check if it's a private IP (VPC endpoint indicator)
165
- const isPrivateIp = addresses.some(
166
- (ip) =>
167
- ip.startsWith('10.') ||
168
- ip.startsWith('172.') ||
169
- ip.startsWith('192.168.')
170
- );
171
- if (isPrivateIp) {
172
- results.vpcEndpoints.push(domain);
173
- }
174
- }
175
- } catch (e) {
176
- // Expected for non-existent endpoints
177
- }
178
- }
179
-
180
- // Check if Lambda is in VPC using VPC_ENABLED env var set by infrastructure
181
- results.isInVpc =
182
- process.env.VPC_ENABLED === 'true' ||
183
- (!results.hasInternetAccess && results.canResolvePublicDns) ||
184
- results.vpcEndpoints.length > 0;
185
-
186
- results.canConnectToAws =
187
- results.hasInternetAccess || results.vpcEndpoints.length > 0;
188
- } catch (error) {
189
- console.error('VPC detection error:', error.message);
100
+ return require('@friggframework/provider-aws/health/kms-health-check');
101
+ } catch {
102
+ return null;
190
103
  }
104
+ }
191
105
 
192
- return results;
193
- };
194
-
195
- // KMS decrypt capability check
196
106
  const checkKmsDecryptCapability = async () => {
197
- const start = Date.now();
198
- const { KMS_KEY_ARN } = process.env;
199
- if (!KMS_KEY_ARN) {
200
- return {
201
- status: 'skipped',
202
- reason: 'KMS_KEY_ARN not configured',
203
- };
107
+ const awsHealth = getAwsHealthChecks();
108
+ if (!awsHealth) {
109
+ return { status: 'skipped', reason: 'AWS provider not available' };
204
110
  }
111
+ return awsHealth.checkKmsDecryptCapability();
112
+ };
205
113
 
206
- // Log environment for debugging
207
- console.log('KMS Check Debug:', {
208
- hasKmsKeyArn: !!KMS_KEY_ARN,
209
- kmsKeyArnPrefix: KMS_KEY_ARN?.substring(0, 30),
210
- awsRegion: process.env.AWS_REGION,
211
- hasDiscoveryKey: !!process.env.AWS_DISCOVERY_KMS_KEY_ID,
212
- });
213
-
214
- // First, detect VPC configuration
215
- const vpcConfig = await detectVpcConfiguration();
216
- console.log('VPC Configuration:', vpcConfig);
217
-
218
- // Test DNS resolution for KMS endpoint
219
- try {
220
- const dns = require('dns').promises;
221
- const region = process.env.AWS_REGION; // Lambda always provides this
222
- const kmsEndpoint = `kms.${region}.amazonaws.com`;
223
- console.log('Testing DNS resolution for:', kmsEndpoint);
224
-
225
- // Wrap DNS resolution in a timeout
226
- const dnsPromise = dns.resolve4(kmsEndpoint);
227
- const timeoutPromise = new Promise((_, reject) =>
228
- setTimeout(() => reject(new Error('DNS resolution timeout')), 3000)
229
- );
230
-
231
- const addresses = await Promise.race([dnsPromise, timeoutPromise]);
232
- console.log('KMS endpoint resolved to:', addresses);
233
-
234
- // Check if resolved to private IP (VPC endpoint)
235
- const isVpcEndpoint = addresses.some(
236
- (ip) =>
237
- ip.startsWith('10.') ||
238
- ip.startsWith('172.') ||
239
- ip.startsWith('192.168.')
240
- );
241
-
242
- if (isVpcEndpoint) {
243
- console.log(
244
- 'KMS VPC Endpoint detected - using private connectivity'
245
- );
246
- }
247
-
248
- // Test TCP connectivity to KMS (port 443)
249
- const net = require('net');
250
- const testConnection = () =>
251
- new Promise((resolve) => {
252
- const socket = new net.Socket();
253
- const connectionTimeout = setTimeout(() => {
254
- socket.destroy();
255
- resolve({ connected: false, error: 'Connection timeout' });
256
- }, 3000);
257
-
258
- socket.on('connect', () => {
259
- clearTimeout(connectionTimeout);
260
- socket.destroy();
261
- resolve({ connected: true });
262
- });
263
-
264
- socket.on('error', (err) => {
265
- clearTimeout(connectionTimeout);
266
- resolve({ connected: false, error: err.message });
267
- });
268
-
269
- // Try connecting to first resolved address on HTTPS port
270
- socket.connect(443, addresses[0]);
271
- });
272
-
273
- const connResult = await testConnection();
274
- console.log('TCP connectivity test:', connResult);
275
-
276
- if (!connResult.connected) {
277
- return {
278
- status: 'unhealthy',
279
- error: `Cannot connect to KMS endpoint: ${connResult.error}`,
280
- dnsResolved: true,
281
- tcpConnection: false,
282
- vpcConfig,
283
- latencyMs: Date.now() - start,
284
- };
285
- }
286
- } catch (dnsError) {
287
- console.error('DNS resolution failed:', dnsError.message);
288
- return {
289
- status: 'unhealthy',
290
- error: `Cannot resolve KMS endpoint: ${dnsError.message}`,
291
- dnsResolved: false,
292
- vpcConfig,
293
- latencyMs: Date.now() - start,
294
- };
295
- }
296
-
297
- try {
298
- // Use AWS SDK v3 for consistency with the rest of the codebase
299
- // eslint-disable-next-line global-require
300
- const {
301
- KMSClient,
302
- GenerateDataKeyCommand,
303
- DecryptCommand,
304
- } = require('@aws-sdk/client-kms');
305
-
306
- // Lambda always provides AWS_REGION
307
- const region = process.env.AWS_REGION;
308
-
309
- const kms = new KMSClient({
310
- region,
311
- requestHandler: {
312
- connectionTimeout: 10000, // 10 second connection timeout
313
- requestTimeout: 25000, // 25 second timeout for slow VPC connections
314
- },
315
- maxAttempts: 1, // No retries on health checks
316
- });
317
-
318
- // Generate a data key (without plaintext logging) then immediately decrypt ciphertext to ensure decrypt perms.
319
- const dataKeyResp = await kms.send(
320
- new GenerateDataKeyCommand({
321
- KeyId: KMS_KEY_ARN,
322
- KeySpec: 'AES_256',
323
- })
324
- );
325
- const decryptResp = await kms.send(
326
- new DecryptCommand({ CiphertextBlob: dataKeyResp.CiphertextBlob })
327
- );
328
-
329
- const success = Boolean(
330
- dataKeyResp.CiphertextBlob && decryptResp.Plaintext
331
- );
332
-
333
- return {
334
- status: success ? 'healthy' : 'unhealthy',
335
- kmsKeyArnSuffix: KMS_KEY_ARN.slice(-12),
336
- vpcConfig,
337
- latencyMs: Date.now() - start,
338
- };
339
- } catch (error) {
340
- return {
341
- status: 'unhealthy',
342
- error: error.message,
343
- vpcConfig,
344
- latencyMs: Date.now() - start,
345
- };
114
+ const detectVpcConfiguration = async () => {
115
+ const awsHealth = getAwsHealthChecks();
116
+ if (!awsHealth) {
117
+ return { status: 'skipped', reason: 'AWS provider not available' };
346
118
  }
119
+ return awsHealth.detectVpcConfiguration();
347
120
  };
348
121
 
349
122
  router.get('/health', async (_req, res) => {
@@ -49,17 +49,21 @@ const {
49
49
  const {
50
50
  CheckDatabaseStateUseCase,
51
51
  } = require('../../database/use-cases/check-database-state-use-case');
52
- const {
53
- MigrationStatusRepositoryS3,
54
- } = require('../../database/repositories/migration-status-repository-s3');
55
-
56
52
  // Inject prisma-runner as dependency
57
53
  const prismaRunner = require('../../database/utils/prisma-runner');
58
54
 
59
- // Use S3 repository for migration status tracking (no User table dependency)
60
- const bucketName =
61
- process.env.S3_BUCKET_NAME || process.env.MIGRATION_STATUS_BUCKET;
62
- const migrationStatusRepository = new MigrationStatusRepositoryS3(bucketName);
55
+ // S3 repository is lazy-loaded from provider-aws to avoid pulling in
56
+ // @aws-sdk/client-s3 on non-AWS platforms.
57
+ let _migrationStatusRepository = null;
58
+ function getMigrationStatusRepository() {
59
+ if (!_migrationStatusRepository) {
60
+ const { MigrationStatusRepositoryS3 } = require('@friggframework/provider-aws');
61
+ const bucketName =
62
+ process.env.S3_BUCKET_NAME || process.env.MIGRATION_STATUS_BUCKET;
63
+ _migrationStatusRepository = new MigrationStatusRepositoryS3(bucketName);
64
+ }
65
+ return _migrationStatusRepository;
66
+ }
63
67
 
64
68
  /**
65
69
  * Sanitizes error messages to prevent credential leaks
@@ -238,7 +242,7 @@ exports.handler = async (event, context) => {
238
242
  console.log(
239
243
  `\n✓ Updating migration status to RUNNING: ${migrationId}`
240
244
  );
241
- await migrationStatusRepository.update({
245
+ await getMigrationStatusRepository().update({
242
246
  migrationId,
243
247
  stage,
244
248
  state: 'RUNNING',
@@ -278,7 +282,7 @@ exports.handler = async (event, context) => {
278
282
  console.log(
279
283
  `\n✓ Updating migration status to COMPLETED: ${migrationId}`
280
284
  );
281
- await migrationStatusRepository.update({
285
+ await getMigrationStatusRepository().update({
282
286
  migrationId,
283
287
  stage,
284
288
  state: 'COMPLETED',
@@ -341,7 +345,7 @@ exports.handler = async (event, context) => {
341
345
  console.log(
342
346
  `\n✓ Updating migration status to FAILED: ${migrationId}`
343
347
  );
344
- await migrationStatusRepository.update({
348
+ await getMigrationStatusRepository().update({
345
349
  migrationId,
346
350
  stage,
347
351
  state: 'FAILED',
package/index.js CHANGED
@@ -95,10 +95,22 @@ const utils = require('./utils');
95
95
  const {
96
96
  QueuerUtil,
97
97
  QueueProvider,
98
+ QueueClientInterface,
98
99
  createQueueProvider,
99
100
  QUEUE_PROVIDERS,
100
101
  } = require('./queues');
101
102
 
103
+ const {
104
+ EncryptionKeyProviderInterface,
105
+ } = require('./encrypt/encryption-key-provider-interface');
106
+ const {
107
+ AesEncryptionKeyProvider,
108
+ } = require('./encrypt/aes-encryption-key-provider');
109
+ const {
110
+ WebSocketMessageSenderInterface,
111
+ StaleConnectionError,
112
+ } = require('./websocket/websocket-message-sender-interface');
113
+
102
114
  const {
103
115
  resolveProvider,
104
116
  determineProviderName,
@@ -200,9 +212,18 @@ module.exports = {
200
212
  // queues
201
213
  QueuerUtil,
202
214
  QueueProvider,
215
+ QueueClientInterface,
203
216
  createQueueProvider,
204
217
  QUEUE_PROVIDERS,
205
218
 
219
+ // encryption interfaces
220
+ EncryptionKeyProviderInterface,
221
+ AesEncryptionKeyProvider,
222
+
223
+ // websocket interfaces
224
+ WebSocketMessageSenderInterface,
225
+ StaleConnectionError,
226
+
206
227
  // providers
207
228
  resolveProvider,
208
229
  determineProviderName,
@@ -28,7 +28,7 @@ module.exports = {
28
28
 
29
29
  // Adapters — lazy to avoid eager require of @aws-sdk/client-scheduler
30
30
  get EventBridgeSchedulerAdapter() {
31
- return require('./eventbridge-scheduler-adapter').EventBridgeSchedulerAdapter;
31
+ return require('@friggframework/provider-aws').EventBridgeSchedulerAdapter;
32
32
  },
33
33
  get MockSchedulerAdapter() {
34
34
  return require('./mock-scheduler-adapter').MockSchedulerAdapter;
@@ -57,7 +57,7 @@ function createSchedulerService(options = {}) {
57
57
 
58
58
  switch (provider) {
59
59
  case SCHEDULER_PROVIDERS.EVENTBRIDGE: {
60
- const { EventBridgeSchedulerAdapter } = require('./eventbridge-scheduler-adapter');
60
+ const { EventBridgeSchedulerAdapter } = require('@friggframework/provider-aws');
61
61
  return new EventBridgeSchedulerAdapter({
62
62
  region: options.region,
63
63
  });
package/package.json CHANGED
@@ -1,12 +1,8 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.545.1176a00.0",
4
+ "version": "2.0.0--canary.545.29ef032.0",
5
5
  "dependencies": {
6
- "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
- "@aws-sdk/client-kms": "^3.588.0",
8
- "@aws-sdk/client-lambda": "^3.714.0",
9
- "@aws-sdk/client-sqs": "^3.588.0",
10
6
  "@hapi/boom": "^10.0.1",
11
7
  "bcryptjs": "^2.4.3",
12
8
  "body-parser": "^1.20.2",
@@ -39,9 +35,9 @@
39
35
  }
40
36
  },
41
37
  "devDependencies": {
42
- "@friggframework/eslint-config": "2.0.0--canary.545.1176a00.0",
43
- "@friggframework/prettier-config": "2.0.0--canary.545.1176a00.0",
44
- "@friggframework/test": "2.0.0--canary.545.1176a00.0",
38
+ "@friggframework/eslint-config": "2.0.0--canary.545.29ef032.0",
39
+ "@friggframework/prettier-config": "2.0.0--canary.545.29ef032.0",
40
+ "@friggframework/test": "2.0.0--canary.545.29ef032.0",
45
41
  "@prisma/client": "^6.17.0",
46
42
  "@types/lodash": "4.17.15",
47
43
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -82,5 +78,5 @@
82
78
  "publishConfig": {
83
79
  "access": "public"
84
80
  },
85
- "gitHead": "1176a004e97f7558574e56aef89ab5fe15ab8e31"
81
+ "gitHead": "29ef03208c79ed5986a4353e7f1230da52b3b2aa"
86
82
  }
package/queues/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  const { QueuerUtil } = require('./queuer-util');
2
2
  const { QueueProvider } = require('./queue-provider');
3
+ const { QueueClientInterface } = require('./queue-client-interface');
3
4
  const {
4
5
  createQueueProvider,
5
6
  determineProvider,
6
7
  QUEUE_PROVIDERS,
7
8
  } = require('./queue-provider-factory');
8
9
  const {
9
- SqsQueueProvider,
10
10
  NetlifyBackgroundProvider,
11
11
  QStashQueueProvider,
12
12
  } = require('./providers');
@@ -14,10 +14,10 @@ const {
14
14
  module.exports = {
15
15
  QueuerUtil,
16
16
  QueueProvider,
17
+ QueueClientInterface,
17
18
  createQueueProvider,
18
19
  determineProvider,
19
20
  QUEUE_PROVIDERS,
20
- SqsQueueProvider,
21
21
  NetlifyBackgroundProvider,
22
22
  QStashQueueProvider,
23
23
  };
@@ -1,9 +1,7 @@
1
- const { SqsQueueProvider } = require('./sqs-queue-provider');
2
1
  const { NetlifyBackgroundProvider } = require('./netlify-background-provider');
3
2
  const { QStashQueueProvider } = require('./qstash-queue-provider');
4
3
 
5
4
  module.exports = {
6
- SqsQueueProvider,
7
5
  NetlifyBackgroundProvider,
8
6
  QStashQueueProvider,
9
7
  };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Queue Client Interface (Port)
3
+ *
4
+ * Defines the contract for low-level queue message operations.
5
+ * Used by Worker class and QueuerUtil for sending/receiving queue messages.
6
+ *
7
+ * Following Frigg's hexagonal architecture pattern:
8
+ * - Port defines WHAT the service does (contract)
9
+ * - Adapters implement HOW (AWS SQS, Netlify, QStash, etc.)
10
+ *
11
+ * Distinct from QueueProvider which is the higher-level provider for
12
+ * integration-level queue operations (send/batchSend/parseEvent).
13
+ * QueueClientInterface is the lower-level "how do I talk to the queue service" port.
14
+ */
15
+ class QueueClientInterface {
16
+ /**
17
+ * Send a single message to a queue
18
+ *
19
+ * @param {Object} params
20
+ * @param {string} params.QueueUrl - Queue endpoint/identifier
21
+ * @param {string} params.MessageBody - Serialized message body
22
+ * @param {number} [params.DelaySeconds] - Delay before message becomes visible
23
+ * @returns {Promise<{MessageId: string}>} Result with message identifier
24
+ */
25
+ async sendMessage(params) {
26
+ throw new Error('Method sendMessage must be implemented by subclass');
27
+ }
28
+
29
+ /**
30
+ * Send a batch of messages to a queue
31
+ *
32
+ * @param {Object} params
33
+ * @param {string} params.QueueUrl - Queue endpoint/identifier
34
+ * @param {Array<{Id: string, MessageBody: string}>} params.Entries - Messages to send
35
+ * @returns {Promise<Object>} Batch send result
36
+ */
37
+ async sendMessageBatch(params) {
38
+ throw new Error(
39
+ 'Method sendMessageBatch must be implemented by subclass'
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Resolve a queue name to a URL/endpoint
45
+ *
46
+ * @param {Object} params
47
+ * @param {string} params.QueueName - Queue name to resolve
48
+ * @returns {Promise<string>} Resolved queue URL/endpoint
49
+ */
50
+ async getQueueUrl(params) {
51
+ throw new Error('Method getQueueUrl must be implemented by subclass');
52
+ }
53
+ }
54
+
55
+ module.exports = { QueueClientInterface };
@@ -14,11 +14,8 @@
14
14
  * - 'netlify-background' → Netlify Background Functions
15
15
  * - 'qstash' → Upstash QStash (platform-agnostic)
16
16
  */
17
- const { SqsQueueProvider } = require('./providers/sqs-queue-provider');
18
- const {
19
- NetlifyBackgroundProvider,
20
- } = require('./providers/netlify-background-provider');
21
- const { QStashQueueProvider } = require('./providers/qstash-queue-provider');
17
+ // Provider implementations are lazily required inside the switch cases
18
+ // to avoid pulling in AWS SDK on non-AWS platforms.
22
19
 
23
20
  const QUEUE_PROVIDERS = {
24
21
  SQS: 'sqs',
@@ -64,12 +61,18 @@ function createQueueProvider(options = {}) {
64
61
  const providerOptions = options.providerOptions || {};
65
62
 
66
63
  switch (provider) {
67
- case QUEUE_PROVIDERS.SQS:
64
+ case QUEUE_PROVIDERS.SQS: {
65
+ const { SqsQueueProvider } = require('@friggframework/provider-aws');
68
66
  return new SqsQueueProvider(providerOptions);
69
- case QUEUE_PROVIDERS.NETLIFY_BACKGROUND:
67
+ }
68
+ case QUEUE_PROVIDERS.NETLIFY_BACKGROUND: {
69
+ const { NetlifyBackgroundProvider } = require('./providers/netlify-background-provider');
70
70
  return new NetlifyBackgroundProvider(providerOptions);
71
- case QUEUE_PROVIDERS.QSTASH:
71
+ }
72
+ case QUEUE_PROVIDERS.QSTASH: {
73
+ const { QStashQueueProvider } = require('./providers/qstash-queue-provider');
72
74
  return new QStashQueueProvider(providerOptions);
75
+ }
73
76
  default:
74
77
  throw new Error(
75
78
  `Unknown queue provider: '${provider}'. Supported: ${Object.values(QUEUE_PROVIDERS).join(', ')}`
@@ -1,34 +1,41 @@
1
1
  const { v4: uuid } = require('uuid');
2
- const {
3
- SQSClient,
4
- SendMessageCommand,
5
- SendMessageBatchCommand,
6
- } = require('@aws-sdk/client-sqs');
7
2
 
8
- const awsConfigOptions = () => {
9
- const config = {};
10
- if (process.env.IS_OFFLINE) {
11
- config.credentials = {
12
- accessKeyId: 'test-aws-key',
13
- secretAccessKey: 'test-aws-secret',
14
- };
15
- config.region = 'us-east-1';
16
- }
17
- if (process.env.AWS_ENDPOINT) {
18
- config.endpoint = process.env.AWS_ENDPOINT;
19
- }
20
- return config;
21
- };
3
+ /**
4
+ * QueuerUtil - Queue message utility.
5
+ *
6
+ * BREAKING CHANGE (v3): A queue client must be set via setQueueClient()
7
+ * before calling send() or batchSend().
8
+ * For AWS/SQS, pass `new SqsQueueClient()` from @friggframework/provider-aws.
9
+ * See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.
10
+ */
11
+ let _queueClient = null;
22
12
 
23
- const sqs = new SQSClient(awsConfigOptions());
13
+ function getQueueClient() {
14
+ if (!_queueClient) {
15
+ throw new Error(
16
+ 'QueuerUtil requires a queue client. Call QueuerUtil.setQueueClient() first, e.g.:\n' +
17
+ ' const { SqsQueueClient } = require("@friggframework/provider-aws");\n' +
18
+ ' QueuerUtil.setQueueClient(new SqsQueueClient());\n' +
19
+ 'See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.'
20
+ );
21
+ }
22
+ return _queueClient;
23
+ }
24
24
 
25
25
  const QueuerUtil = {
26
+ /**
27
+ * Set the queue client. Must be called before send() or batchSend().
28
+ * @param {QueueClientInterface} client
29
+ */
30
+ setQueueClient(client) {
31
+ _queueClient = client;
32
+ },
33
+
26
34
  send: async (message, queueUrl) => {
27
- const command = new SendMessageCommand({
35
+ return getQueueClient().sendMessage({
28
36
  MessageBody: JSON.stringify(message),
29
37
  QueueUrl: queueUrl,
30
38
  });
31
- return sqs.send(command);
32
39
  },
33
40
 
34
41
  batchSend: async (entries = [], queueUrl) => {
@@ -42,23 +49,20 @@ const QueuerUtil = {
42
49
  });
43
50
  // Sends 10, then purges the buffer
44
51
  if (buffer.length === batchSize) {
45
- const command = new SendMessageBatchCommand({
46
- Entries: buffer,
52
+ await getQueueClient().sendMessageBatch({
53
+ Entries: [...buffer],
47
54
  QueueUrl: queueUrl,
48
55
  });
49
- await sqs.send(command);
50
- // Purge the buffer
51
- buffer.splice(0, buffer.length);
56
+ buffer.length = 0;
52
57
  }
53
58
  }
54
59
 
55
60
  // If any remaining entries under 10 are left in the buffer, send and return
56
61
  if (buffer.length > 0) {
57
- const command = new SendMessageBatchCommand({
62
+ return getQueueClient().sendMessageBatch({
58
63
  Entries: buffer,
59
64
  QueueUrl: queueUrl,
60
65
  });
61
- return sqs.send(command);
62
66
  }
63
67
 
64
68
  // If we're exact... just return an empty object for now