@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
package/core/Worker.js CHANGED
@@ -1,23 +1,41 @@
1
- const {
2
- SQSClient,
3
- GetQueueUrlCommand,
4
- SendMessageCommand,
5
- } = require('@aws-sdk/client-sqs');
6
1
  const _ = require('lodash');
7
2
  const { RequiredPropertyError } = require('../errors');
8
3
  const { get } = require('../assertions');
9
4
 
10
- const sqs = new SQSClient({ region: process.env.AWS_REGION });
11
-
5
+ /**
6
+ * Worker - Queue message producer/consumer base class.
7
+ *
8
+ * Subclass and override _run() to implement your worker logic.
9
+ * The queue transport (SQS, Netlify, etc.) is abstracted behind
10
+ * QueueClientInterface, injected via constructor options.
11
+ *
12
+ * BREAKING CHANGE (v3): A queueClient must be explicitly provided.
13
+ * For AWS/SQS, pass `new SqsQueueClient()` from @friggframework/provider-aws.
14
+ * See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.
15
+ */
12
16
  class Worker {
17
+ constructor(options = {}) {
18
+ this._queueClient = options.queueClient || null;
19
+ }
20
+
21
+ /**
22
+ * Get the queue client. Throws if none was injected.
23
+ * @returns {QueueClientInterface}
24
+ */
25
+ _getQueueClient() {
26
+ if (!this._queueClient) {
27
+ throw new Error(
28
+ 'Worker requires a queueClient. Pass one via constructor options, e.g.:\n' +
29
+ ' const { SqsQueueClient } = require("@friggframework/provider-aws");\n' +
30
+ ' new MyWorker({ queueClient: new SqsQueueClient() })\n' +
31
+ 'See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.'
32
+ );
33
+ }
34
+ return this._queueClient;
35
+ }
36
+
13
37
  async getQueueURL(params) {
14
- // Passing params in because there will be multiple QueueNames
15
- // let params = {
16
- // QueueName: process.env.QueueName
17
- // };
18
- const command = new GetQueueUrlCommand(params);
19
- const data = await sqs.send(command);
20
- return data.QueueUrl;
38
+ return this._getQueueClient().getQueueUrl(params);
21
39
  }
22
40
 
23
41
  async run(params, context = {}) {
@@ -51,8 +69,7 @@ class Worker {
51
69
  }
52
70
 
53
71
  async sendAsyncSQSMessage(params) {
54
- const command = new SendMessageCommand(params);
55
- const data = await sqs.send(command);
72
+ const data = await this._getQueueClient().sendMessage(params);
56
73
  return data.MessageId;
57
74
  }
58
75
 
@@ -170,7 +170,7 @@ function loadModuleEncryptionSchemas(integrations) {
170
170
 
171
171
  const {
172
172
  getModulesDefinitionFromIntegrationClasses,
173
- } = require('../integrations/utils/map-integration-dto');
173
+ } = require('../../integrations/utils/map-integration-dto');
174
174
 
175
175
  const moduleDefinitions =
176
176
  getModulesDefinitionFromIntegrationClasses(integrations);
@@ -1,13 +1,31 @@
1
+ /**
2
+ * WebsocketConnection Mongoose Model
3
+ *
4
+ * BREAKING CHANGE (v3): Call WebsocketConnection.setMessageSender() before
5
+ * using getActiveConnections(). For AWS API Gateway, pass
6
+ * `new ApiGatewayMessageSender()` from @friggframework/provider-aws.
7
+ * See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.
8
+ */
1
9
  const { mongoose } = require('../mongoose');
2
10
  const {
3
- ApiGatewayManagementApiClient,
4
- PostToConnectionCommand,
5
- } = require('@aws-sdk/client-apigatewaymanagementapi');
11
+ StaleConnectionError,
12
+ } = require('../../websocket/websocket-message-sender-interface');
13
+
14
+ let _messageSender = null;
6
15
 
7
16
  const schema = new mongoose.Schema({
8
17
  connectionId: { type: mongoose.Schema.Types.String },
9
18
  });
10
19
 
20
+ /**
21
+ * Set the message sender for WebSocket send functionality.
22
+ * Must be called before getActiveConnections().
23
+ * @param {WebSocketMessageSenderInterface} sender
24
+ */
25
+ schema.statics.setMessageSender = function (sender) {
26
+ _messageSender = sender;
27
+ };
28
+
11
29
  // Add a static method to get active connections
12
30
  schema.statics.getActiveConnections = async function () {
13
31
  try {
@@ -16,25 +34,28 @@ schema.statics.getActiveConnections = async function () {
16
34
  return [];
17
35
  }
18
36
 
37
+ if (!_messageSender) {
38
+ throw new Error(
39
+ 'WebsocketConnection requires a message sender. Call WebsocketConnection.setMessageSender() first, e.g.:\n' +
40
+ ' const { ApiGatewayMessageSender } = require("@friggframework/provider-aws");\n' +
41
+ ' WebsocketConnection.setMessageSender(new ApiGatewayMessageSender());\n' +
42
+ 'See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.'
43
+ );
44
+ }
45
+
46
+ const sender = _messageSender;
19
47
  const connections = await this.find({}, 'connectionId');
20
48
  return connections.map((conn) => ({
21
49
  connectionId: conn.connectionId,
22
50
  send: async (data) => {
23
- const apigwManagementApi = new ApiGatewayManagementApiClient({
24
- endpoint: process.env.WEBSOCKET_API_ENDPOINT,
25
- });
26
-
27
51
  try {
28
- const command = new PostToConnectionCommand({
29
- ConnectionId: conn.connectionId,
30
- Data: JSON.stringify(data),
31
- });
32
- await apigwManagementApi.send(command);
52
+ await sender.send(
53
+ conn.connectionId,
54
+ data,
55
+ process.env.WEBSOCKET_API_ENDPOINT
56
+ );
33
57
  } catch (error) {
34
- if (
35
- error.statusCode === 410 ||
36
- error.$metadata?.httpStatusCode === 410
37
- ) {
58
+ if (error instanceof StaleConnectionError) {
38
59
  console.log(`Stale connection ${conn.connectionId}`);
39
60
  await this.deleteOne({
40
61
  connectionId: conn.connectionId,
@@ -1,64 +1,75 @@
1
1
  /**
2
2
  * Cryptor - Encryption Service Adapter
3
3
  *
4
- * Infrastructure Layer adapter for AWS KMS and local AES encryption.
5
- * Provides envelope encryption pattern for field-level encryption.
4
+ * Infrastructure Layer adapter for envelope encryption.
5
+ * Key management is delegated to an EncryptionKeyProviderInterface adapter:
6
+ * - KmsEncryptionKeyProvider (in @friggframework/provider-aws) for AWS KMS
7
+ * - AesEncryptionKeyProvider (in core) for local AES keys
6
8
  *
7
9
  * Envelope Encryption Pattern:
8
- * 1. Generate Data Encryption Key (DEK) via KMS or locally
10
+ * 1. Generate Data Encryption Key (DEK) via key provider
9
11
  * 2. Encrypt field value with DEK using AES-256-CTR
10
- * 3. Encrypt DEK with Master Key (KMS CMK or AES_KEY)
12
+ * 3. Store encrypted DEK alongside ciphertext
11
13
  * 4. Return format: "keyId:encryptedText:encryptedKey"
12
14
  *
13
- * Benefits:
14
- * - Reduces KMS API calls (unique DEK per operation)
15
- * - Master key never leaves KMS
16
- * - Enables key rotation without re-encrypting data
15
+ * BREAKING CHANGE (v3): A keyProvider must be explicitly provided,
16
+ * or pass { shouldUseAws: false } to auto-create AesEncryptionKeyProvider.
17
+ * For AWS KMS, pass `new KmsEncryptionKeyProvider()` from @friggframework/provider-aws.
18
+ * See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.
17
19
  */
18
20
 
19
- const crypto = require('crypto');
20
- const {
21
- KMSClient,
22
- GenerateDataKeyCommand,
23
- DecryptCommand,
24
- } = require('@aws-sdk/client-kms');
25
21
  const aes = require('./aes');
26
22
 
27
23
  class Cryptor {
28
- constructor({ shouldUseAws }) {
29
- this.shouldUseAws = shouldUseAws;
30
- }
31
-
32
- async generateDataKey() {
33
- if (this.shouldUseAws) {
34
- const kmsClient = new KMSClient({});
35
- const command = new GenerateDataKeyCommand({
36
- KeyId: process.env.KMS_KEY_ARN,
37
- KeySpec: 'AES_256',
38
- });
39
- const dataKey = await kmsClient.send(command);
40
-
41
- const keyId = Buffer.from(dataKey.KeyId).toString('base64');
42
- const encryptedKey = Buffer.from(dataKey.CiphertextBlob).toString(
43
- 'base64'
24
+ /**
25
+ * @param {Object} options
26
+ * @param {boolean} [options.shouldUseAws] - true requires explicit keyProvider; false auto-creates AesEncryptionKeyProvider
27
+ * @param {EncryptionKeyProviderInterface} [options.keyProvider] - Explicit key provider (takes precedence)
28
+ */
29
+ constructor({ shouldUseAws, keyProvider } = {}) {
30
+ if (keyProvider) {
31
+ this._keyProvider = keyProvider;
32
+ } else if (shouldUseAws) {
33
+ throw new Error(
34
+ 'Cryptor with shouldUseAws=true requires an explicit keyProvider. Pass one via constructor options, e.g.:\n' +
35
+ ' const { KmsEncryptionKeyProvider } = require("@friggframework/provider-aws");\n' +
36
+ ' new Cryptor({ shouldUseAws: true, keyProvider: new KmsEncryptionKeyProvider() })\n' +
37
+ 'See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.'
44
38
  );
45
- const plaintext = dataKey.Plaintext;
46
- return { keyId, encryptedKey, plaintext };
39
+ } else {
40
+ // AES mode no AWS dependency needed
41
+ const { AesEncryptionKeyProvider } = require('./aes-encryption-key-provider');
42
+ this._keyProvider = new AesEncryptionKeyProvider();
47
43
  }
44
+ this._shouldUseAws = shouldUseAws;
45
+ }
48
46
 
49
- const { AES_KEY, AES_KEY_ID } = process.env;
50
- const randomKey = crypto.randomBytes(32).toString('hex').slice(0, 32);
47
+ /**
48
+ * Get the key provider.
49
+ * @returns {EncryptionKeyProviderInterface}
50
+ */
51
+ _getKeyProvider() {
52
+ return this._keyProvider;
53
+ }
51
54
 
52
- return {
53
- keyId: Buffer.from(AES_KEY_ID).toString('base64'),
54
- encryptedKey: Buffer.from(aes.encrypt(randomKey, AES_KEY)).toString(
55
- 'base64'
56
- ),
57
- plaintext: randomKey,
58
- };
55
+ async generateDataKey() {
56
+ return this._getKeyProvider().generateDataKey();
59
57
  }
60
58
 
59
+ /**
60
+ * Look up an AES key by identifier from environment variables.
61
+ * Kept for backward compatibility.
62
+ *
63
+ * @param {string} keyId - Key identifier
64
+ * @returns {string} The master key
65
+ */
61
66
  getKeyFromEnvironment(keyId) {
67
+ const provider = this._getKeyProvider();
68
+ if (typeof provider.getKeyFromEnvironment === 'function') {
69
+ return provider.getKeyFromEnvironment(keyId);
70
+ }
71
+
72
+ // Fallback for providers that don't implement getKeyFromEnvironment
62
73
  const availableKeys = {
63
74
  [process.env.AES_KEY_ID]: process.env.AES_KEY,
64
75
  [process.env.DEPRECATED_AES_KEY_ID]: process.env.DEPRECATED_AES_KEY,
@@ -74,19 +85,7 @@ class Cryptor {
74
85
  }
75
86
 
76
87
  async decryptDataKey(keyId, encryptedKey) {
77
- if (this.shouldUseAws) {
78
- const kmsClient = new KMSClient({});
79
- const command = new DecryptCommand({
80
- KeyId: keyId,
81
- CiphertextBlob: encryptedKey,
82
- });
83
- const dataKey = await kmsClient.send(command);
84
-
85
- return dataKey.Plaintext;
86
- }
87
-
88
- const key = this.getKeyFromEnvironment(keyId);
89
- return aes.decrypt(encryptedKey, key);
88
+ return this._getKeyProvider().decryptDataKey(keyId, encryptedKey);
90
89
  }
91
90
 
92
91
  async encrypt(text) {
@@ -0,0 +1,82 @@
1
+ /**
2
+ * AES Encryption Key Provider (Adapter)
3
+ *
4
+ * Local AES-based implementation of EncryptionKeyProviderInterface.
5
+ * Uses environment-variable-based master keys for envelope encryption.
6
+ *
7
+ * No external dependencies — works on any platform.
8
+ *
9
+ * Environment Variables:
10
+ * - AES_KEY_ID: Identifier for the current AES master key
11
+ * - AES_KEY: The current AES master key (32 chars)
12
+ * - DEPRECATED_AES_KEY_ID: (optional) Previous key ID for key rotation
13
+ * - DEPRECATED_AES_KEY: (optional) Previous key for decrypting old data
14
+ */
15
+
16
+ const crypto = require('crypto');
17
+ const aes = require('./aes');
18
+ const {
19
+ EncryptionKeyProviderInterface,
20
+ } = require('./encryption-key-provider-interface');
21
+
22
+ class AesEncryptionKeyProvider extends EncryptionKeyProviderInterface {
23
+ /**
24
+ * Generate a data encryption key using local AES
25
+ *
26
+ * Creates a random DEK and encrypts it with the AES master key
27
+ * from environment variables.
28
+ *
29
+ * @returns {Promise<{keyId: string, encryptedKey: string, plaintext: string}>}
30
+ */
31
+ async generateDataKey() {
32
+ const { AES_KEY, AES_KEY_ID } = process.env;
33
+ const randomKey = crypto.randomBytes(32).toString('hex').slice(0, 32);
34
+
35
+ return {
36
+ keyId: Buffer.from(AES_KEY_ID).toString('base64'),
37
+ encryptedKey: Buffer.from(aes.encrypt(randomKey, AES_KEY)).toString(
38
+ 'base64'
39
+ ),
40
+ plaintext: randomKey,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Decrypt a data encryption key using the AES master key
46
+ *
47
+ * Looks up the master key by keyId from environment variables,
48
+ * supporting key rotation via DEPRECATED_AES_KEY.
49
+ *
50
+ * @param {string} keyId - Key identifier to look up in environment
51
+ * @param {string|Buffer} encryptedKey - Encrypted DEK to decrypt
52
+ * @returns {Promise<string>} Decrypted plaintext DEK
53
+ */
54
+ async decryptDataKey(keyId, encryptedKey) {
55
+ const key = this.getKeyFromEnvironment(keyId);
56
+ return aes.decrypt(encryptedKey, key);
57
+ }
58
+
59
+ /**
60
+ * Look up an AES master key by its identifier
61
+ *
62
+ * @param {string} keyId - Key identifier
63
+ * @returns {string} The master key
64
+ * @throws {Error} If key not found in environment
65
+ */
66
+ getKeyFromEnvironment(keyId) {
67
+ const availableKeys = {
68
+ [process.env.AES_KEY_ID]: process.env.AES_KEY,
69
+ [process.env.DEPRECATED_AES_KEY_ID]: process.env.DEPRECATED_AES_KEY,
70
+ };
71
+
72
+ const key = availableKeys[keyId];
73
+
74
+ if (!key) {
75
+ throw new Error('Encryption key not found');
76
+ }
77
+
78
+ return key;
79
+ }
80
+ }
81
+
82
+ module.exports = { AesEncryptionKeyProvider };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Encryption Key Provider Interface (Port)
3
+ *
4
+ * Defines the contract for envelope encryption key operations.
5
+ * Used by Cryptor for generating and decrypting data encryption keys.
6
+ *
7
+ * Following Frigg's hexagonal architecture pattern:
8
+ * - Port defines WHAT the service does (contract)
9
+ * - Adapters implement HOW (AWS KMS, local AES, Vault, etc.)
10
+ *
11
+ * Envelope Encryption Pattern:
12
+ * 1. generateDataKey() creates a fresh DEK (plaintext + encrypted form)
13
+ * 2. Caller encrypts data with the plaintext DEK
14
+ * 3. Caller stores the encrypted DEK alongside the ciphertext
15
+ * 4. decryptDataKey() recovers the plaintext DEK from the encrypted form
16
+ * 5. Caller decrypts data with the recovered DEK
17
+ */
18
+ class EncryptionKeyProviderInterface {
19
+ /**
20
+ * Generate a new data encryption key
21
+ *
22
+ * Returns both the plaintext key (for immediate encryption) and an
23
+ * encrypted copy (for storage alongside the ciphertext).
24
+ *
25
+ * @returns {Promise<{keyId: string, encryptedKey: string, plaintext: string|Buffer}>}
26
+ * - keyId: Base64-encoded identifier for the master key used
27
+ * - encryptedKey: Base64-encoded encrypted copy of the DEK
28
+ * - plaintext: The raw DEK (string or Buffer) for immediate use
29
+ */
30
+ async generateDataKey() {
31
+ throw new Error(
32
+ 'Method generateDataKey must be implemented by subclass'
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Decrypt a previously encrypted data encryption key
38
+ *
39
+ * @param {string} keyId - Identifier of the master key used for encryption
40
+ * @param {string|Buffer} encryptedKey - The encrypted DEK to decrypt
41
+ * @returns {Promise<string|Buffer>} The decrypted plaintext DEK
42
+ */
43
+ async decryptDataKey(keyId, encryptedKey) {
44
+ throw new Error(
45
+ 'Method decryptDataKey must be implemented by subclass'
46
+ );
47
+ }
48
+ }
49
+
50
+ module.exports = { EncryptionKeyProviderInterface };
@@ -21,9 +21,10 @@
21
21
  const { Router } = require('express');
22
22
  const catchAsyncError = require('express-async-handler');
23
23
  const { validateAdminApiKey } = require('../middleware/admin-auth');
24
- const {
25
- MigrationStatusRepositoryS3,
26
- } = require('../../database/repositories/migration-status-repository-s3');
24
+ // Lazy-loaded from provider-aws to avoid pulling in @aws-sdk/client-s3 on non-AWS platforms
25
+ function getMigrationStatusRepositoryS3() {
26
+ return require('@friggframework/provider-aws').MigrationStatusRepositoryS3;
27
+ }
27
28
  const {
28
29
  TriggerDatabaseMigrationUseCase,
29
30
  ValidationError: TriggerValidationError,
@@ -33,39 +34,61 @@ const {
33
34
  ValidationError: GetValidationError,
34
35
  NotFoundError,
35
36
  } = require('../../database/use-cases/get-migration-status-use-case');
36
- const { LambdaInvoker } = require('../../database/adapters/lambda-invoker');
37
+ // Lazy-loaded from provider-aws to avoid pulling in @aws-sdk/client-lambda on non-AWS platforms
38
+ function getLambdaInvoker() {
39
+ return require('@friggframework/provider-aws').LambdaInvoker;
40
+ }
37
41
  const {
38
42
  GetDatabaseStateViaWorkerUseCase,
39
43
  } = require('../../database/use-cases/get-database-state-via-worker-use-case');
40
44
 
41
45
  const router = Router();
42
46
 
43
- // Dependency injection
44
- // Use S3 repository to avoid User table dependency (chicken-and-egg problem)
45
- const bucketName =
46
- process.env.S3_BUCKET_NAME || process.env.MIGRATION_STATUS_BUCKET;
47
- const migrationStatusRepository = new MigrationStatusRepositoryS3(bucketName);
48
-
49
- const triggerMigrationUseCase = new TriggerDatabaseMigrationUseCase({
50
- migrationStatusRepository,
51
- // Note: QueuerUtil is used directly in the use case (static utility)
52
- });
53
- const getStatusUseCase = new GetMigrationStatusUseCase({
54
- migrationStatusRepository,
55
- });
56
-
57
- // Lambda invocation for database state check (keeps router lightweight)
58
- const lambdaInvoker = new LambdaInvoker();
59
- const workerFunctionName =
60
- process.env.WORKER_FUNCTION_NAME ||
61
- `${process.env.SERVICE || 'unknown'}-${
62
- process.env.STAGE || 'production'
63
- }-dbMigrationWorker`;
64
-
65
- const getDatabaseStateUseCase = new GetDatabaseStateViaWorkerUseCase({
66
- lambdaInvoker,
67
- workerFunctionName,
68
- });
47
+ // Dependency injection — lazy-initialized on first request to avoid
48
+ // pulling in AWS SDKs at module load time on non-AWS platforms.
49
+ let _migrationStatusRepository = null;
50
+ let _triggerMigrationUseCase = null;
51
+ let _getStatusUseCase = null;
52
+ let _lambdaInvoker = null;
53
+
54
+ function getMigrationDeps() {
55
+ if (!_migrationStatusRepository) {
56
+ const MigrationStatusRepositoryS3 = getMigrationStatusRepositoryS3();
57
+ const bucketName =
58
+ process.env.S3_BUCKET_NAME || process.env.MIGRATION_STATUS_BUCKET;
59
+ _migrationStatusRepository = new MigrationStatusRepositoryS3(bucketName);
60
+ _triggerMigrationUseCase = new TriggerDatabaseMigrationUseCase({
61
+ migrationStatusRepository: _migrationStatusRepository,
62
+ });
63
+ _getStatusUseCase = new GetMigrationStatusUseCase({
64
+ migrationStatusRepository: _migrationStatusRepository,
65
+ });
66
+ const LambdaInvoker = getLambdaInvoker();
67
+ _lambdaInvoker = new LambdaInvoker();
68
+ }
69
+ return {
70
+ migrationStatusRepository: _migrationStatusRepository,
71
+ triggerMigrationUseCase: _triggerMigrationUseCase,
72
+ getStatusUseCase: _getStatusUseCase,
73
+ lambdaInvoker: _lambdaInvoker,
74
+ };
75
+ }
76
+ let _getDatabaseStateUseCase = null;
77
+ function getDatabaseStateUseCaseInstance() {
78
+ if (!_getDatabaseStateUseCase) {
79
+ const { lambdaInvoker } = getMigrationDeps();
80
+ const workerFunctionName =
81
+ process.env.WORKER_FUNCTION_NAME ||
82
+ `${process.env.SERVICE || 'unknown'}-${
83
+ process.env.STAGE || 'production'
84
+ }-dbMigrationWorker`;
85
+ _getDatabaseStateUseCase = new GetDatabaseStateViaWorkerUseCase({
86
+ lambdaInvoker,
87
+ workerFunctionName,
88
+ });
89
+ }
90
+ return _getDatabaseStateUseCase;
91
+ }
69
92
 
70
93
  // Apply admin API key validation to all routes (shared middleware)
71
94
  router.use(validateAdminApiKey);
@@ -106,7 +129,7 @@ router.post(
106
129
  );
107
130
 
108
131
  try {
109
- const result = await triggerMigrationUseCase.execute({
132
+ const result = await getMigrationDeps().triggerMigrationUseCase.execute({
110
133
  userId,
111
134
  dbType,
112
135
  stage,
@@ -153,12 +176,12 @@ router.get(
153
176
  const stage = req.query.stage || process.env.STAGE || 'production';
154
177
 
155
178
  console.log(
156
- `Checking database state: stage=${stage}, worker=${workerFunctionName}`
179
+ `Checking database state: stage=${stage}`
157
180
  );
158
181
 
159
182
  try {
160
183
  // Invoke worker Lambda to check database state
161
- const status = await getDatabaseStateUseCase.execute(stage);
184
+ const status = await getDatabaseStateUseCaseInstance().execute(stage);
162
185
 
163
186
  res.status(200).json(status);
164
187
  } catch (error) {
@@ -210,7 +233,7 @@ router.get(
210
233
  );
211
234
 
212
235
  try {
213
- const status = await getStatusUseCase.execute(migrationId, stage);
236
+ const status = await getMigrationDeps().getStatusUseCase.execute(migrationId, stage);
214
237
 
215
238
  res.status(200).json(status);
216
239
  } catch (error) {