@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.
- package/core/Worker.js +33 -16
- package/database/encryption/encryption-schema-registry.js +1 -1
- package/database/models/WebsocketConnection.js +37 -16
- package/encrypt/Cryptor.js +53 -54
- package/encrypt/aes-encryption-key-provider.js +82 -0
- package/encrypt/encryption-key-provider-interface.js +50 -0
- package/handlers/routers/db-migration.js +57 -34
- package/handlers/routers/health.js +17 -244
- package/handlers/workers/db-migration.js +15 -11
- package/index.js +21 -0
- package/infrastructure/scheduler/index.js +1 -1
- package/infrastructure/scheduler/scheduler-service-factory.js +1 -1
- package/package.json +5 -9
- package/queues/index.js +2 -2
- package/queues/providers/index.js +0 -2
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-provider-factory.js +11 -8
- package/queues/queuer-util.js +33 -29
- package/types/core/index.d.ts +44 -5
- package/websocket/repositories/websocket-connection-repository-documentdb.js +29 -17
- package/websocket/repositories/websocket-connection-repository-mongo.js +26 -20
- package/websocket/repositories/websocket-connection-repository-postgres.js +26 -19
- package/websocket/repositories/websocket-connection-repository.js +26 -24
- package/websocket/websocket-message-sender-interface.js +38 -0
- package/database/adapters/lambda-invoker.js +0 -98
- package/database/repositories/migration-status-repository-s3.js +0 -142
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
- 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
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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('
|
|
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.
|
|
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.
|
|
43
|
-
"@friggframework/prettier-config": "2.0.0--canary.545.
|
|
44
|
-
"@friggframework/test": "2.0.0--canary.545.
|
|
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": "
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
67
|
+
}
|
|
68
|
+
case QUEUE_PROVIDERS.NETLIFY_BACKGROUND: {
|
|
69
|
+
const { NetlifyBackgroundProvider } = require('./providers/netlify-background-provider');
|
|
70
70
|
return new NetlifyBackgroundProvider(providerOptions);
|
|
71
|
-
|
|
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(', ')}`
|
package/queues/queuer-util.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
Entries: buffer,
|
|
52
|
+
await getQueueClient().sendMessageBatch({
|
|
53
|
+
Entries: [...buffer],
|
|
47
54
|
QueueUrl: queueUrl,
|
|
48
55
|
});
|
|
49
|
-
|
|
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
|
-
|
|
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
|