@friggframework/core 2.0.0--canary.461.3d6d8ad.0 → 2.0.0--canary.461.5100cd8.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/database/adapters/lambda-invoker.js +96 -0
- package/database/use-cases/{check-migration-status-use-case.js → check-database-state-use-case.js} +6 -6
- package/database/use-cases/get-database-state-via-worker-use-case.js +60 -0
- package/handlers/routers/db-migration.js +24 -17
- package/handlers/workers/db-migration.js +42 -0
- package/package.json +6 -5
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambda Invoker Adapter
|
|
3
|
+
* Infrastructure layer - handles AWS Lambda function invocations
|
|
4
|
+
*
|
|
5
|
+
* Part of Hexagonal Architecture:
|
|
6
|
+
* - Infrastructure Layer adapter for AWS SDK
|
|
7
|
+
* - Used by Domain Layer use cases
|
|
8
|
+
* - Isolates AWS-specific logic from business logic
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom error for Lambda invocation failures
|
|
15
|
+
* Provides structured error information for debugging
|
|
16
|
+
*/
|
|
17
|
+
class LambdaInvocationError extends Error {
|
|
18
|
+
constructor(message, functionName, statusCode) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'LambdaInvocationError';
|
|
21
|
+
this.functionName = functionName;
|
|
22
|
+
this.statusCode = statusCode;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Adapter for invoking AWS Lambda functions
|
|
28
|
+
*
|
|
29
|
+
* Infrastructure layer - handles AWS SDK communication
|
|
30
|
+
* Converts AWS SDK responses to domain-friendly formats
|
|
31
|
+
*/
|
|
32
|
+
class LambdaInvoker {
|
|
33
|
+
/**
|
|
34
|
+
* @param {LambdaClient} lambdaClient - AWS Lambda client (injected for testability)
|
|
35
|
+
*/
|
|
36
|
+
constructor(lambdaClient = new LambdaClient({})) {
|
|
37
|
+
this.client = lambdaClient;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Invoke Lambda function synchronously
|
|
42
|
+
*
|
|
43
|
+
* @param {string} functionName - Lambda function name or ARN
|
|
44
|
+
* @param {Object} payload - Event payload to send to Lambda
|
|
45
|
+
* @returns {Promise<Object>} Parsed response body
|
|
46
|
+
* @throws {LambdaInvocationError} If Lambda returns error status
|
|
47
|
+
* @throws {Error} If AWS SDK call fails
|
|
48
|
+
*/
|
|
49
|
+
async invoke(functionName, payload) {
|
|
50
|
+
try {
|
|
51
|
+
const command = new InvokeCommand({
|
|
52
|
+
FunctionName: functionName,
|
|
53
|
+
InvocationType: 'RequestResponse', // Synchronous
|
|
54
|
+
Payload: JSON.stringify(payload),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const response = await this.client.send(command);
|
|
58
|
+
|
|
59
|
+
// Parse response payload
|
|
60
|
+
let result;
|
|
61
|
+
try {
|
|
62
|
+
result = JSON.parse(Buffer.from(response.Payload).toString());
|
|
63
|
+
} catch (parseError) {
|
|
64
|
+
throw new LambdaInvocationError(
|
|
65
|
+
`Failed to parse Lambda response: ${parseError.message}`,
|
|
66
|
+
functionName,
|
|
67
|
+
null
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check status code
|
|
72
|
+
if (result.statusCode === 200) {
|
|
73
|
+
return result.body;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Lambda returned error status
|
|
77
|
+
const errorMessage = result.body?.error || 'Lambda invocation failed';
|
|
78
|
+
throw new LambdaInvocationError(
|
|
79
|
+
`Lambda ${functionName} returned error: ${errorMessage}`,
|
|
80
|
+
functionName,
|
|
81
|
+
result.statusCode
|
|
82
|
+
);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Re-throw LambdaInvocationError as-is
|
|
85
|
+
if (error instanceof LambdaInvocationError) {
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Wrap AWS SDK errors
|
|
90
|
+
throw new Error(`Failed to invoke Lambda ${functionName}: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { LambdaInvoker, LambdaInvocationError };
|
|
96
|
+
|
package/database/use-cases/{check-migration-status-use-case.js → check-database-state-use-case.js}
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Check
|
|
2
|
+
* Check Database State Use Case
|
|
3
3
|
*
|
|
4
|
-
* Domain logic for checking
|
|
5
|
-
* Does NOT trigger migrations, just reports
|
|
4
|
+
* Domain logic for checking database state (pending migrations, errors, etc).
|
|
5
|
+
* Does NOT trigger migrations, just reports current state.
|
|
6
6
|
*
|
|
7
7
|
* Architecture: Hexagonal/Clean
|
|
8
8
|
* - Use Case (Domain Layer)
|
|
9
9
|
* - Depends on prismaRunner (Infrastructure abstraction)
|
|
10
|
-
* - Called by Router (Adapter Layer)
|
|
10
|
+
* - Called by Router or other Use Cases (Adapter Layer)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
class ValidationError extends Error {
|
|
@@ -17,7 +17,7 @@ class ValidationError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
class
|
|
20
|
+
class CheckDatabaseStateUseCase {
|
|
21
21
|
/**
|
|
22
22
|
* @param {Object} dependencies
|
|
23
23
|
* @param {Object} dependencies.prismaRunner - Prisma runner utility
|
|
@@ -75,7 +75,7 @@ class CheckMigrationStatusUseCase {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
module.exports = {
|
|
78
|
-
|
|
78
|
+
CheckDatabaseStateUseCase,
|
|
79
79
|
ValidationError,
|
|
80
80
|
};
|
|
81
81
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get Database State Via Worker Use Case
|
|
3
|
+
*
|
|
4
|
+
* Domain logic for getting database state by invoking the worker Lambda.
|
|
5
|
+
* This use case delegates to the worker Lambda which has Prisma CLI installed,
|
|
6
|
+
* keeping the router Lambda lightweight.
|
|
7
|
+
*
|
|
8
|
+
* Architecture: Hexagonal/Clean
|
|
9
|
+
* - Use Case (Domain Layer)
|
|
10
|
+
* - Depends on LambdaInvoker (Infrastructure abstraction)
|
|
11
|
+
* - Called by Router (Adapter Layer)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Domain Use Case: Get database state by invoking worker Lambda
|
|
16
|
+
*
|
|
17
|
+
* This use case delegates database state checking to the worker Lambda,
|
|
18
|
+
* which has Prisma CLI installed. Keeps the router Lambda lightweight.
|
|
19
|
+
*/
|
|
20
|
+
class GetDatabaseStateViaWorkerUseCase {
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object} dependencies
|
|
23
|
+
* @param {LambdaInvoker} dependencies.lambdaInvoker - Lambda invocation adapter
|
|
24
|
+
* @param {string} dependencies.workerFunctionName - Worker Lambda function name
|
|
25
|
+
*/
|
|
26
|
+
constructor({ lambdaInvoker, workerFunctionName }) {
|
|
27
|
+
if (!lambdaInvoker) {
|
|
28
|
+
throw new Error('lambdaInvoker dependency is required');
|
|
29
|
+
}
|
|
30
|
+
if (!workerFunctionName) {
|
|
31
|
+
throw new Error('workerFunctionName is required');
|
|
32
|
+
}
|
|
33
|
+
this.lambdaInvoker = lambdaInvoker;
|
|
34
|
+
this.workerFunctionName = workerFunctionName;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execute database state check via worker Lambda
|
|
39
|
+
*
|
|
40
|
+
* @param {string} stage - Deployment stage (prod, dev, etc)
|
|
41
|
+
* @returns {Promise<Object>} Database state result
|
|
42
|
+
*/
|
|
43
|
+
async execute(stage = 'production') {
|
|
44
|
+
const dbType = process.env.DB_TYPE || 'postgresql';
|
|
45
|
+
|
|
46
|
+
console.log(`Invoking worker Lambda to check database state: ${this.workerFunctionName}`);
|
|
47
|
+
|
|
48
|
+
// Invoke worker Lambda with checkStatus action
|
|
49
|
+
const result = await this.lambdaInvoker.invoke(this.workerFunctionName, {
|
|
50
|
+
action: 'checkStatus',
|
|
51
|
+
dbType,
|
|
52
|
+
stage,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { GetDatabaseStateViaWorkerUseCase };
|
|
60
|
+
|
|
@@ -28,11 +28,10 @@ const {
|
|
|
28
28
|
ValidationError: GetValidationError,
|
|
29
29
|
NotFoundError,
|
|
30
30
|
} = require('../../database/use-cases/get-migration-status-use-case');
|
|
31
|
+
const { LambdaInvoker } = require('../../database/adapters/lambda-invoker');
|
|
31
32
|
const {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} = require('../../database/use-cases/check-migration-status-use-case');
|
|
35
|
-
const prismaRunner = require('../../database/utils/prisma-runner');
|
|
33
|
+
GetDatabaseStateViaWorkerUseCase,
|
|
34
|
+
} = require('../../database/use-cases/get-database-state-via-worker-use-case');
|
|
36
35
|
|
|
37
36
|
const router = Router();
|
|
38
37
|
|
|
@@ -46,7 +45,16 @@ const triggerMigrationUseCase = new TriggerDatabaseMigrationUseCase({
|
|
|
46
45
|
// Note: QueuerUtil is used directly in the use case (static utility)
|
|
47
46
|
});
|
|
48
47
|
const getStatusUseCase = new GetMigrationStatusUseCase({ migrationStatusRepository });
|
|
49
|
-
|
|
48
|
+
|
|
49
|
+
// Lambda invocation for database state check (keeps router lightweight)
|
|
50
|
+
const lambdaInvoker = new LambdaInvoker();
|
|
51
|
+
const workerFunctionName = process.env.WORKER_FUNCTION_NAME ||
|
|
52
|
+
`${process.env.SERVICE || 'unknown'}-${process.env.STAGE || 'production'}-dbMigrationWorker`;
|
|
53
|
+
|
|
54
|
+
const getDatabaseStateUseCase = new GetDatabaseStateViaWorkerUseCase({
|
|
55
|
+
lambdaInvoker,
|
|
56
|
+
workerFunctionName,
|
|
57
|
+
});
|
|
50
58
|
|
|
51
59
|
/**
|
|
52
60
|
* Admin API key validation middleware
|
|
@@ -147,25 +155,24 @@ router.get(
|
|
|
147
155
|
'/db-migrate/status',
|
|
148
156
|
catchAsyncError(async (req, res) => {
|
|
149
157
|
const stage = req.query.stage || process.env.STAGE || 'production';
|
|
150
|
-
const dbType = process.env.DB_TYPE || 'postgresql'; // Hardcoded for PostgreSQL migrations
|
|
151
158
|
|
|
152
|
-
console.log(`Checking
|
|
159
|
+
console.log(`Checking database state: stage=${stage}, worker=${workerFunctionName}`);
|
|
153
160
|
|
|
154
161
|
try {
|
|
155
|
-
|
|
162
|
+
// Invoke worker Lambda to check database state
|
|
163
|
+
const status = await getDatabaseStateUseCase.execute(stage);
|
|
156
164
|
|
|
157
165
|
res.status(200).json(status);
|
|
158
166
|
} catch (error) {
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
return res.status(400).json({
|
|
162
|
-
success: false,
|
|
163
|
-
error: error.message,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
167
|
+
// Log full error for debugging
|
|
168
|
+
console.error('Database state check failed:', error);
|
|
166
169
|
|
|
167
|
-
//
|
|
168
|
-
|
|
170
|
+
// Return sanitized error to client
|
|
171
|
+
return res.status(500).json({
|
|
172
|
+
success: false,
|
|
173
|
+
error: 'Failed to check database state',
|
|
174
|
+
details: error.message,
|
|
175
|
+
});
|
|
169
176
|
}
|
|
170
177
|
})
|
|
171
178
|
);
|
|
@@ -46,6 +46,9 @@ const {
|
|
|
46
46
|
MigrationError,
|
|
47
47
|
ValidationError,
|
|
48
48
|
} = require('../../database/use-cases/run-database-migration-use-case');
|
|
49
|
+
const {
|
|
50
|
+
CheckDatabaseStateUseCase,
|
|
51
|
+
} = require('../../database/use-cases/check-database-state-use-case');
|
|
49
52
|
const {
|
|
50
53
|
MigrationStatusRepositoryS3,
|
|
51
54
|
} = require('../../database/repositories/migration-status-repository-s3');
|
|
@@ -151,6 +154,45 @@ exports.handler = async (event, context) => {
|
|
|
151
154
|
// Extract migration parameters from event
|
|
152
155
|
const { migrationId, dbType, stage } = extractMigrationParams(event);
|
|
153
156
|
|
|
157
|
+
// Check for action parameter (direct invocation for status checks)
|
|
158
|
+
const action = event.action || 'migrate'; // Default to migration
|
|
159
|
+
|
|
160
|
+
// Handle checkStatus action
|
|
161
|
+
if (action === 'checkStatus') {
|
|
162
|
+
console.log(`\n========================================`);
|
|
163
|
+
console.log(`Action: checkStatus (dbType=${dbType}, stage=${stage})`);
|
|
164
|
+
console.log(`========================================`);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const checkDbStateUseCase = new CheckDatabaseStateUseCase({ prismaRunner });
|
|
168
|
+
const status = await checkDbStateUseCase.execute(dbType, stage);
|
|
169
|
+
|
|
170
|
+
console.log('✓ Database state check completed');
|
|
171
|
+
console.log(` Up to date: ${status.upToDate}`);
|
|
172
|
+
console.log(` Pending migrations: ${status.pendingMigrations}`);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
statusCode: 200,
|
|
176
|
+
body: status,
|
|
177
|
+
};
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('❌ Database state check failed:', error.message);
|
|
180
|
+
return {
|
|
181
|
+
statusCode: 500,
|
|
182
|
+
body: {
|
|
183
|
+
success: false,
|
|
184
|
+
error: sanitizeError(error.message),
|
|
185
|
+
upToDate: false,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Otherwise, handle migration (existing code)
|
|
192
|
+
console.log(`\n========================================`);
|
|
193
|
+
console.log(`Action: migrate (migrationId=${migrationId || 'new'})`);
|
|
194
|
+
console.log(`========================================`);
|
|
195
|
+
|
|
154
196
|
// Get environment variables
|
|
155
197
|
const databaseUrl = process.env.DATABASE_URL;
|
|
156
198
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.461.
|
|
4
|
+
"version": "2.0.0--canary.461.5100cd8.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
8
|
+
"@aws-sdk/client-lambda": "^3.714.0",
|
|
8
9
|
"@aws-sdk/client-sqs": "^3.588.0",
|
|
9
10
|
"@hapi/boom": "^10.0.1",
|
|
10
11
|
"bcryptjs": "^2.4.3",
|
|
@@ -37,9 +38,9 @@
|
|
|
37
38
|
}
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
|
-
"@friggframework/eslint-config": "2.0.0--canary.461.
|
|
41
|
-
"@friggframework/prettier-config": "2.0.0--canary.461.
|
|
42
|
-
"@friggframework/test": "2.0.0--canary.461.
|
|
41
|
+
"@friggframework/eslint-config": "2.0.0--canary.461.5100cd8.0",
|
|
42
|
+
"@friggframework/prettier-config": "2.0.0--canary.461.5100cd8.0",
|
|
43
|
+
"@friggframework/test": "2.0.0--canary.461.5100cd8.0",
|
|
43
44
|
"@prisma/client": "^6.17.0",
|
|
44
45
|
"@types/lodash": "4.17.15",
|
|
45
46
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -79,5 +80,5 @@
|
|
|
79
80
|
"publishConfig": {
|
|
80
81
|
"access": "public"
|
|
81
82
|
},
|
|
82
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "5100cd81ceb3fe903e128b5f9530e09cb3e89495"
|
|
83
84
|
}
|