@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
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
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
package/encrypt/Cryptor.js
CHANGED
|
@@ -1,64 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cryptor - Encryption Service Adapter
|
|
3
3
|
*
|
|
4
|
-
* Infrastructure Layer adapter for
|
|
5
|
-
*
|
|
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
|
|
10
|
+
* 1. Generate Data Encryption Key (DEK) via key provider
|
|
9
11
|
* 2. Encrypt field value with DEK using AES-256-CTR
|
|
10
|
-
* 3.
|
|
12
|
+
* 3. Store encrypted DEK alongside ciphertext
|
|
11
13
|
* 4. Return format: "keyId:encryptedText:encryptedKey"
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Get the key provider.
|
|
49
|
+
* @returns {EncryptionKeyProviderInterface}
|
|
50
|
+
*/
|
|
51
|
+
_getKeyProvider() {
|
|
52
|
+
return this._keyProvider;
|
|
53
|
+
}
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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}
|
|
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
|
|
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) {
|