@friggframework/core 2.0.0--canary.545.302ab9b.0 → 2.0.0--canary.545.a8d08b4.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/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 +64 -34
- package/handlers/routers/health.js +18 -243
- package/handlers/workers/db-migration.js +22 -11
- package/index.js +21 -0
- package/infrastructure/scheduler/index.js +8 -15
- package/infrastructure/scheduler/scheduler-service-factory.js +36 -43
- 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 +36 -27
- 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
|
|
|
@@ -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,7 @@
|
|
|
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
|
+
const { resolveProvider } = require('../../providers/resolve-provider');
|
|
27
25
|
const {
|
|
28
26
|
TriggerDatabaseMigrationUseCase,
|
|
29
27
|
ValidationError: TriggerValidationError,
|
|
@@ -33,39 +31,71 @@ const {
|
|
|
33
31
|
ValidationError: GetValidationError,
|
|
34
32
|
NotFoundError,
|
|
35
33
|
} = require('../../database/use-cases/get-migration-status-use-case');
|
|
36
|
-
const { LambdaInvoker } = require('../../database/adapters/lambda-invoker');
|
|
37
34
|
const {
|
|
38
35
|
GetDatabaseStateViaWorkerUseCase,
|
|
39
36
|
} = require('../../database/use-cases/get-database-state-via-worker-use-case');
|
|
40
37
|
|
|
41
38
|
const router = Router();
|
|
42
39
|
|
|
43
|
-
// Dependency injection
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
40
|
+
// Dependency injection — lazy-initialized on first request.
|
|
41
|
+
// Uses resolveProvider() to get the correct adapters for the active platform.
|
|
42
|
+
let _migrationStatusRepository = null;
|
|
43
|
+
let _triggerMigrationUseCase = null;
|
|
44
|
+
let _getStatusUseCase = null;
|
|
45
|
+
let _functionInvoker = null;
|
|
46
|
+
|
|
47
|
+
function getMigrationDeps() {
|
|
48
|
+
if (!_migrationStatusRepository) {
|
|
49
|
+
const provider = resolveProvider();
|
|
50
|
+
|
|
51
|
+
// Migration status storage (AWS: S3, others: provider-specific)
|
|
52
|
+
const MigrationStatusRepository = provider.MigrationStatusRepositoryS3;
|
|
53
|
+
if (!MigrationStatusRepository) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Provider '${provider.name}' does not export a MigrationStatusRepository`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const bucketName =
|
|
59
|
+
process.env.S3_BUCKET_NAME || process.env.MIGRATION_STATUS_BUCKET;
|
|
60
|
+
_migrationStatusRepository = new MigrationStatusRepository(bucketName);
|
|
61
|
+
_triggerMigrationUseCase = new TriggerDatabaseMigrationUseCase({
|
|
62
|
+
migrationStatusRepository: _migrationStatusRepository,
|
|
63
|
+
});
|
|
64
|
+
_getStatusUseCase = new GetMigrationStatusUseCase({
|
|
65
|
+
migrationStatusRepository: _migrationStatusRepository,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Function invoker (AWS: LambdaInvoker, Netlify: HTTP-based)
|
|
69
|
+
_functionInvoker = provider.invokeFunctionAdapter;
|
|
70
|
+
if (!_functionInvoker) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Provider '${provider.name}' does not export an invokeFunctionAdapter`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
migrationStatusRepository: _migrationStatusRepository,
|
|
78
|
+
triggerMigrationUseCase: _triggerMigrationUseCase,
|
|
79
|
+
getStatusUseCase: _getStatusUseCase,
|
|
80
|
+
lambdaInvoker: _functionInvoker,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
let _getDatabaseStateUseCase = null;
|
|
84
|
+
function getDatabaseStateUseCaseInstance() {
|
|
85
|
+
if (!_getDatabaseStateUseCase) {
|
|
86
|
+
const { lambdaInvoker } = getMigrationDeps();
|
|
87
|
+
const workerFunctionName =
|
|
88
|
+
process.env.WORKER_FUNCTION_NAME ||
|
|
89
|
+
`${process.env.SERVICE || 'unknown'}-${
|
|
90
|
+
process.env.STAGE || 'production'
|
|
91
|
+
}-dbMigrationWorker`;
|
|
92
|
+
_getDatabaseStateUseCase = new GetDatabaseStateViaWorkerUseCase({
|
|
93
|
+
lambdaInvoker,
|
|
94
|
+
workerFunctionName,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return _getDatabaseStateUseCase;
|
|
98
|
+
}
|
|
69
99
|
|
|
70
100
|
// Apply admin API key validation to all routes (shared middleware)
|
|
71
101
|
router.use(validateAdminApiKey);
|
|
@@ -106,7 +136,7 @@ router.post(
|
|
|
106
136
|
);
|
|
107
137
|
|
|
108
138
|
try {
|
|
109
|
-
const result = await triggerMigrationUseCase.execute({
|
|
139
|
+
const result = await getMigrationDeps().triggerMigrationUseCase.execute({
|
|
110
140
|
userId,
|
|
111
141
|
dbType,
|
|
112
142
|
stage,
|
|
@@ -153,12 +183,12 @@ router.get(
|
|
|
153
183
|
const stage = req.query.stage || process.env.STAGE || 'production';
|
|
154
184
|
|
|
155
185
|
console.log(
|
|
156
|
-
`Checking database state: stage=${stage}
|
|
186
|
+
`Checking database state: stage=${stage}`
|
|
157
187
|
);
|
|
158
188
|
|
|
159
189
|
try {
|
|
160
190
|
// Invoke worker Lambda to check database state
|
|
161
|
-
const status = await
|
|
191
|
+
const status = await getDatabaseStateUseCaseInstance().execute(stage);
|
|
162
192
|
|
|
163
193
|
res.status(200).json(status);
|
|
164
194
|
} catch (error) {
|
|
@@ -210,7 +240,7 @@ router.get(
|
|
|
210
240
|
);
|
|
211
241
|
|
|
212
242
|
try {
|
|
213
|
-
const status = await getStatusUseCase.execute(migrationId, stage);
|
|
243
|
+
const status = await getMigrationDeps().getStatusUseCase.execute(migrationId, stage);
|
|
214
244
|
|
|
215
245
|
res.status(200).json(status);
|
|
216
246
|
} catch (error) {
|