@friggframework/core 2.0.0-next.45 → 2.0.0-next.46
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/README.md +28 -0
- package/application/commands/integration-commands.js +19 -0
- package/core/Worker.js +8 -21
- package/credential/repositories/credential-repository-mongo.js +14 -8
- package/credential/repositories/credential-repository-postgres.js +14 -8
- package/credential/repositories/credential-repository.js +3 -8
- package/database/MONGODB_TRANSACTION_FIX.md +198 -0
- package/database/adapters/lambda-invoker.js +97 -0
- package/database/config.js +11 -2
- package/database/models/WebsocketConnection.js +11 -10
- package/database/prisma.js +63 -3
- package/database/repositories/health-check-repository-mongodb.js +3 -0
- package/database/repositories/migration-status-repository-s3.js +137 -0
- package/database/use-cases/check-database-state-use-case.js +81 -0
- package/database/use-cases/check-encryption-health-use-case.js +3 -2
- package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
- package/database/use-cases/get-migration-status-use-case.js +93 -0
- package/database/use-cases/run-database-migration-use-case.js +137 -0
- package/database/use-cases/trigger-database-migration-use-case.js +157 -0
- package/database/utils/mongodb-collection-utils.js +91 -0
- package/database/utils/mongodb-schema-init.js +106 -0
- package/database/utils/prisma-runner.js +400 -0
- package/database/utils/prisma-schema-parser.js +182 -0
- package/encrypt/Cryptor.js +14 -16
- package/generated/prisma-mongodb/client.d.ts +1 -0
- package/generated/prisma-mongodb/client.js +4 -0
- package/generated/prisma-mongodb/default.d.ts +1 -0
- package/generated/prisma-mongodb/default.js +4 -0
- package/generated/prisma-mongodb/edge.d.ts +1 -0
- package/generated/prisma-mongodb/edge.js +334 -0
- package/generated/prisma-mongodb/index-browser.js +316 -0
- package/generated/prisma-mongodb/index.d.ts +22897 -0
- package/generated/prisma-mongodb/index.js +359 -0
- package/generated/prisma-mongodb/package.json +183 -0
- package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
- package/generated/prisma-mongodb/runtime/binary.js +289 -0
- package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
- package/generated/prisma-mongodb/runtime/edge.js +34 -0
- package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
- package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
- package/generated/prisma-mongodb/runtime/react-native.js +83 -0
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-mongodb/schema.prisma +362 -0
- package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm.d.ts +1 -0
- package/generated/prisma-mongodb/wasm.js +341 -0
- package/generated/prisma-postgresql/client.d.ts +1 -0
- package/generated/prisma-postgresql/client.js +4 -0
- package/generated/prisma-postgresql/default.d.ts +1 -0
- package/generated/prisma-postgresql/default.js +4 -0
- package/generated/prisma-postgresql/edge.d.ts +1 -0
- package/generated/prisma-postgresql/edge.js +356 -0
- package/generated/prisma-postgresql/index-browser.js +338 -0
- package/generated/prisma-postgresql/index.d.ts +25071 -0
- package/generated/prisma-postgresql/index.js +381 -0
- package/generated/prisma-postgresql/package.json +183 -0
- package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query_engine_bg.js +2 -0
- package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
- package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
- package/generated/prisma-postgresql/runtime/binary.js +289 -0
- package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
- package/generated/prisma-postgresql/runtime/edge.js +34 -0
- package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
- package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
- package/generated/prisma-postgresql/runtime/react-native.js +83 -0
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-postgresql/schema.prisma +345 -0
- package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm.d.ts +1 -0
- package/generated/prisma-postgresql/wasm.js +363 -0
- package/handlers/database-migration-handler.js +227 -0
- package/handlers/routers/auth.js +1 -1
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +256 -0
- package/handlers/routers/health.js +41 -6
- package/handlers/routers/integration-webhook-routers.js +2 -2
- package/handlers/use-cases/check-integrations-health-use-case.js +22 -10
- package/handlers/workers/db-migration.js +352 -0
- package/index.js +12 -0
- package/integrations/integration-router.js +60 -70
- package/integrations/repositories/integration-repository-interface.js +12 -0
- package/integrations/repositories/integration-repository-mongo.js +32 -0
- package/integrations/repositories/integration-repository-postgres.js +33 -0
- package/integrations/repositories/process-repository-postgres.js +2 -2
- package/integrations/tests/doubles/test-integration-repository.js +2 -2
- package/logs/logger.js +0 -4
- package/modules/entity.js +0 -1
- package/modules/repositories/module-repository-mongo.js +3 -12
- package/modules/repositories/module-repository-postgres.js +0 -11
- package/modules/repositories/module-repository.js +1 -12
- package/modules/use-cases/get-entity-options-by-id.js +1 -1
- package/modules/use-cases/get-module.js +1 -2
- package/modules/use-cases/refresh-entity-options.js +1 -1
- package/modules/use-cases/test-module-auth.js +1 -1
- package/package.json +82 -66
- package/prisma-mongodb/schema.prisma +21 -21
- package/prisma-postgresql/schema.prisma +15 -15
- package/queues/queuer-util.js +24 -21
- package/types/core/index.d.ts +2 -2
- package/types/module-plugin/index.d.ts +0 -2
- package/user/use-cases/authenticate-user.js +127 -0
- package/user/use-cases/authenticate-with-shared-secret.js +48 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
- package/user/user.js +16 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +11 -10
- package/websocket/repositories/websocket-connection-repository-postgres.js +11 -10
- package/websocket/repositories/websocket-connection-repository.js +11 -10
- package/application/commands/integration-commands.test.js +0 -123
- package/database/encryption/encryption-integration.test.js +0 -553
- package/database/encryption/encryption-schema-registry.test.js +0 -392
- package/database/encryption/field-encryption-service.test.js +0 -525
- package/database/encryption/mongo-decryption-fix-verification.test.js +0 -348
- package/database/encryption/postgres-decryption-fix-verification.test.js +0 -371
- package/database/encryption/postgres-relation-decryption.test.js +0 -245
- package/database/encryption/prisma-encryption-extension.test.js +0 -439
- package/errors/base-error.test.js +0 -32
- package/errors/fetch-error.test.js +0 -79
- package/errors/halt-error.test.js +0 -11
- package/errors/validation-errors.test.js +0 -120
- package/handlers/auth-flow.integration.test.js +0 -147
- package/handlers/integration-event-dispatcher.test.js +0 -209
- package/handlers/routers/health.test.js +0 -210
- package/handlers/routers/integration-webhook-routers.test.js +0 -126
- package/handlers/webhook-flow.integration.test.js +0 -356
- package/handlers/workers/integration-defined-workers.test.js +0 -184
- package/integrations/tests/use-cases/create-integration.test.js +0 -131
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +0 -150
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +0 -92
- package/integrations/tests/use-cases/get-integration-for-user.test.js +0 -150
- package/integrations/tests/use-cases/get-integration-instance.test.js +0 -176
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +0 -176
- package/integrations/tests/use-cases/get-possible-integrations.test.js +0 -188
- package/integrations/tests/use-cases/update-integration-messages.test.js +0 -142
- package/integrations/tests/use-cases/update-integration-status.test.js +0 -103
- package/integrations/tests/use-cases/update-integration.test.js +0 -141
- package/integrations/use-cases/create-process.test.js +0 -178
- package/integrations/use-cases/get-process.test.js +0 -190
- package/integrations/use-cases/load-integration-context-full.test.js +0 -329
- package/integrations/use-cases/load-integration-context.test.js +0 -114
- package/integrations/use-cases/update-process-metrics.test.js +0 -308
- package/integrations/use-cases/update-process-state.test.js +0 -256
- package/lambda/TimeoutCatcher.test.js +0 -68
- package/logs/logger.test.js +0 -76
- package/modules/module-hydration.test.js +0 -205
- package/modules/requester/requester.test.js +0 -28
- package/user/tests/use-cases/create-individual-user.test.js +0 -24
- package/user/tests/use-cases/create-organization-user.test.js +0 -28
- package/user/tests/use-cases/create-token-for-user-id.test.js +0 -19
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +0 -64
- package/user/tests/use-cases/login-user.test.js +0 -220
- package/user/tests/user-password-encryption-isolation.test.js +0 -237
- package/user/tests/user-password-hashing.test.js +0 -235
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const { mongoose } = require('../mongoose');
|
|
2
|
-
const
|
|
2
|
+
const {
|
|
3
|
+
ApiGatewayManagementApiClient,
|
|
4
|
+
PostToConnectionCommand,
|
|
5
|
+
} = require('@aws-sdk/client-apigatewaymanagementapi');
|
|
3
6
|
|
|
4
7
|
const schema = new mongoose.Schema({
|
|
5
8
|
connectionId: { type: mongoose.Schema.Types.String },
|
|
@@ -17,20 +20,18 @@ schema.statics.getActiveConnections = async function () {
|
|
|
17
20
|
return connections.map((conn) => ({
|
|
18
21
|
connectionId: conn.connectionId,
|
|
19
22
|
send: async (data) => {
|
|
20
|
-
const apigwManagementApi = new
|
|
21
|
-
apiVersion: '2018-11-29',
|
|
23
|
+
const apigwManagementApi = new ApiGatewayManagementApiClient({
|
|
22
24
|
endpoint: process.env.WEBSOCKET_API_ENDPOINT,
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
try {
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.promise();
|
|
28
|
+
const command = new PostToConnectionCommand({
|
|
29
|
+
ConnectionId: conn.connectionId,
|
|
30
|
+
Data: JSON.stringify(data),
|
|
31
|
+
});
|
|
32
|
+
await apigwManagementApi.send(command);
|
|
32
33
|
} catch (error) {
|
|
33
|
-
if (error.statusCode === 410) {
|
|
34
|
+
if (error.statusCode === 410 || error.$metadata?.httpStatusCode === 410) {
|
|
34
35
|
console.log(`Stale connection ${conn.connectionId}`);
|
|
35
36
|
await this.deleteOne({
|
|
36
37
|
connectionId: conn.connectionId,
|
package/database/prisma.js
CHANGED
|
@@ -6,6 +6,32 @@ const { logger } = require('./encryption/logger');
|
|
|
6
6
|
const { Cryptor } = require('../encrypt/Cryptor');
|
|
7
7
|
const config = require('./config');
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Ensures DATABASE_URL is set for MongoDB connections
|
|
11
|
+
* Falls back to MONGO_URI if DATABASE_URL is not set
|
|
12
|
+
* Infrastructure layer concern - maps legacy MONGO_URI to Prisma's expected DATABASE_URL
|
|
13
|
+
*
|
|
14
|
+
* Note: This should only be called when DB_TYPE is 'mongodb'
|
|
15
|
+
*/
|
|
16
|
+
function ensureMongoDbUrl() {
|
|
17
|
+
// If DATABASE_URL is already set, use it
|
|
18
|
+
if (process.env.DATABASE_URL && process.env.DATABASE_URL.trim()) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Fallback to MONGO_URI for backwards compatibility with DocumentDB deployments
|
|
23
|
+
if (process.env.MONGO_URI && process.env.MONGO_URI.trim()) {
|
|
24
|
+
process.env.DATABASE_URL = process.env.MONGO_URI;
|
|
25
|
+
logger.debug('Using MONGO_URI as DATABASE_URL for MongoDB connection');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Neither is set - error
|
|
30
|
+
throw new Error(
|
|
31
|
+
'DATABASE_URL or MONGO_URI environment variable must be set for MongoDB'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
9
35
|
function getEncryptionConfig() {
|
|
10
36
|
const STAGE = process.env.STAGE || process.env.NODE_ENV || 'development';
|
|
11
37
|
const shouldBypassEncryption = ['dev', 'test', 'local'].includes(STAGE);
|
|
@@ -22,7 +48,7 @@ function getEncryptionConfig() {
|
|
|
22
48
|
if (!hasKMS && !hasAES) {
|
|
23
49
|
logger.warn(
|
|
24
50
|
'No encryption keys configured (KMS_KEY_ARN or AES_KEY_ID). ' +
|
|
25
|
-
|
|
51
|
+
'Field-level encryption disabled. Set STAGE=production and configure keys to enable.'
|
|
26
52
|
);
|
|
27
53
|
return { enabled: false };
|
|
28
54
|
}
|
|
@@ -76,10 +102,34 @@ function loadCustomEncryptionSchema() {
|
|
|
76
102
|
const prismaClientSingleton = () => {
|
|
77
103
|
let PrismaClient;
|
|
78
104
|
|
|
105
|
+
// Helper to try loading Prisma client from multiple locations
|
|
106
|
+
const loadPrismaClient = (dbType) => {
|
|
107
|
+
const paths = [
|
|
108
|
+
// Lambda layer location (when using Prisma Lambda layer)
|
|
109
|
+
`/opt/nodejs/node_modules/generated/prisma-${dbType}`,
|
|
110
|
+
// Local development location (relative to core package)
|
|
111
|
+
`../generated/prisma-${dbType}`,
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (const path of paths) {
|
|
115
|
+
try {
|
|
116
|
+
return require(path).PrismaClient;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
// Continue to next path
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Cannot find Prisma client for ${dbType}. Tried paths: ${paths.join(', ')}`
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
79
127
|
if (config.DB_TYPE === 'mongodb') {
|
|
80
|
-
|
|
128
|
+
// Ensure DATABASE_URL is set (fallback to MONGO_URI if needed)
|
|
129
|
+
ensureMongoDbUrl();
|
|
130
|
+
PrismaClient = loadPrismaClient('mongodb');
|
|
81
131
|
} else if (config.DB_TYPE === 'postgresql') {
|
|
82
|
-
PrismaClient =
|
|
132
|
+
PrismaClient = loadPrismaClient('postgresql');
|
|
83
133
|
} else {
|
|
84
134
|
throw new Error(
|
|
85
135
|
`Unsupported database type: ${config.DB_TYPE}. Supported values: 'mongodb', 'postgresql'`
|
|
@@ -151,6 +201,15 @@ async function disconnectPrisma() {
|
|
|
151
201
|
|
|
152
202
|
async function connectPrisma() {
|
|
153
203
|
await getPrismaClient().$connect();
|
|
204
|
+
|
|
205
|
+
// Initialize MongoDB schema - ensure all collections exist
|
|
206
|
+
// Only run for MongoDB/DocumentDB (not PostgreSQL)
|
|
207
|
+
// This prevents "Cannot create namespace in multi-document transaction" errors
|
|
208
|
+
if (config.DB_TYPE === 'mongodb') {
|
|
209
|
+
const { initializeMongoDBSchema } = require('./utils/mongodb-schema-init');
|
|
210
|
+
await initializeMongoDBSchema();
|
|
211
|
+
}
|
|
212
|
+
|
|
154
213
|
return getPrismaClient();
|
|
155
214
|
}
|
|
156
215
|
|
|
@@ -159,4 +218,5 @@ module.exports = {
|
|
|
159
218
|
connectPrisma,
|
|
160
219
|
disconnectPrisma,
|
|
161
220
|
getEncryptionConfig,
|
|
221
|
+
ensureMongoDbUrl, // Exported for testing
|
|
162
222
|
};
|
|
@@ -38,6 +38,9 @@ class HealthCheckRepositoryMongoDB extends HealthCheckRepositoryInterface {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
async createCredential(credentialData) {
|
|
41
|
+
// Note: Collection existence is ensured at application startup via
|
|
42
|
+
// initializeMongoDBSchema() in database/utils/mongodb-schema-init.js
|
|
43
|
+
// This prevents "Cannot create namespace in multi-document transaction" errors
|
|
41
44
|
return await prisma.credential.create({
|
|
42
45
|
data: credentialData,
|
|
43
46
|
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Status Repository - S3 Storage
|
|
3
|
+
*
|
|
4
|
+
* Infrastructure Layer - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Stores migration status in S3 to avoid chicken-and-egg dependency on User/Process tables.
|
|
7
|
+
* Initial database migrations can't use Process table (requires User FK which doesn't exist yet).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
11
|
+
const { randomUUID } = require('crypto');
|
|
12
|
+
|
|
13
|
+
class MigrationStatusRepositoryS3 {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} bucketName - S3 bucket name for migration status storage
|
|
16
|
+
* @param {S3Client} s3Client - Optional S3 client (for testing)
|
|
17
|
+
*/
|
|
18
|
+
constructor(bucketName, s3Client = null) {
|
|
19
|
+
this.bucketName = bucketName;
|
|
20
|
+
this.s3Client = s3Client || new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build S3 key for migration status
|
|
25
|
+
* @param {string} migrationId - Migration identifier
|
|
26
|
+
* @param {string} stage - Deployment stage
|
|
27
|
+
* @returns {string} S3 key
|
|
28
|
+
*/
|
|
29
|
+
_buildS3Key(migrationId, stage) {
|
|
30
|
+
return `migrations/${stage}/${migrationId}.json`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create new migration status record
|
|
35
|
+
* @param {Object} data - Migration data
|
|
36
|
+
* @param {string} [data.migrationId] - Migration ID (generates UUID if not provided)
|
|
37
|
+
* @param {string} data.stage - Deployment stage
|
|
38
|
+
* @param {string} [data.triggeredBy] - User or system that triggered migration
|
|
39
|
+
* @param {string} [data.triggeredAt] - ISO timestamp
|
|
40
|
+
* @returns {Promise<Object>} Created migration status
|
|
41
|
+
*/
|
|
42
|
+
async create(data) {
|
|
43
|
+
const migrationId = data.migrationId || randomUUID();
|
|
44
|
+
const timestamp = data.triggeredAt || new Date().toISOString();
|
|
45
|
+
|
|
46
|
+
const status = {
|
|
47
|
+
migrationId,
|
|
48
|
+
stage: data.stage,
|
|
49
|
+
state: 'INITIALIZING',
|
|
50
|
+
progress: 0,
|
|
51
|
+
triggeredBy: data.triggeredBy || 'system',
|
|
52
|
+
triggeredAt: timestamp,
|
|
53
|
+
createdAt: timestamp,
|
|
54
|
+
updatedAt: timestamp,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const key = this._buildS3Key(migrationId, data.stage);
|
|
58
|
+
|
|
59
|
+
await this.s3Client.send(
|
|
60
|
+
new PutObjectCommand({
|
|
61
|
+
Bucket: this.bucketName,
|
|
62
|
+
Key: key,
|
|
63
|
+
Body: JSON.stringify(status, null, 2),
|
|
64
|
+
ContentType: 'application/json',
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return status;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Update existing migration status
|
|
73
|
+
* @param {Object} data - Update data
|
|
74
|
+
* @param {string} data.migrationId - Migration ID
|
|
75
|
+
* @param {string} data.stage - Deployment stage
|
|
76
|
+
* @param {string} [data.state] - New state
|
|
77
|
+
* @param {number} [data.progress] - Progress percentage (0-100)
|
|
78
|
+
* @param {string} [data.error] - Error message if failed
|
|
79
|
+
* @param {string} [data.completedAt] - Completion timestamp
|
|
80
|
+
* @returns {Promise<Object>} Updated migration status
|
|
81
|
+
*/
|
|
82
|
+
async update(data) {
|
|
83
|
+
const key = this._buildS3Key(data.migrationId, data.stage);
|
|
84
|
+
|
|
85
|
+
// Get existing status
|
|
86
|
+
const existing = await this.get(data.migrationId, data.stage);
|
|
87
|
+
|
|
88
|
+
// Merge updates
|
|
89
|
+
const updated = {
|
|
90
|
+
...existing,
|
|
91
|
+
...data,
|
|
92
|
+
updatedAt: new Date().toISOString(),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
await this.s3Client.send(
|
|
96
|
+
new PutObjectCommand({
|
|
97
|
+
Bucket: this.bucketName,
|
|
98
|
+
Key: key,
|
|
99
|
+
Body: JSON.stringify(updated, null, 2),
|
|
100
|
+
ContentType: 'application/json',
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return updated;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get migration status by ID
|
|
109
|
+
* @param {string} migrationId - Migration ID
|
|
110
|
+
* @param {string} stage - Deployment stage
|
|
111
|
+
* @returns {Promise<Object>} Migration status
|
|
112
|
+
* @throws {Error} If migration not found
|
|
113
|
+
*/
|
|
114
|
+
async get(migrationId, stage) {
|
|
115
|
+
const key = this._buildS3Key(migrationId, stage);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const response = await this.s3Client.send(
|
|
119
|
+
new GetObjectCommand({
|
|
120
|
+
Bucket: this.bucketName,
|
|
121
|
+
Key: key,
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const body = await response.Body.transformToString();
|
|
126
|
+
return JSON.parse(body);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.name === 'NoSuchKey') {
|
|
129
|
+
throw new Error(`Migration not found: ${migrationId}`);
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { MigrationStatusRepositoryS3 };
|
|
137
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check Database State Use Case
|
|
3
|
+
*
|
|
4
|
+
* Domain logic for checking database state (pending migrations, errors, etc).
|
|
5
|
+
* Does NOT trigger migrations, just reports current state.
|
|
6
|
+
*
|
|
7
|
+
* Architecture: Hexagonal/Clean
|
|
8
|
+
* - Use Case (Domain Layer)
|
|
9
|
+
* - Depends on prismaRunner (Infrastructure abstraction)
|
|
10
|
+
* - Called by Router or other Use Cases (Adapter Layer)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class ValidationError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'ValidationError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class CheckDatabaseStateUseCase {
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object} dependencies
|
|
23
|
+
* @param {Object} dependencies.prismaRunner - Prisma runner utility
|
|
24
|
+
*/
|
|
25
|
+
constructor({ prismaRunner }) {
|
|
26
|
+
if (!prismaRunner) {
|
|
27
|
+
throw new Error('prismaRunner dependency is required');
|
|
28
|
+
}
|
|
29
|
+
this.prismaRunner = prismaRunner;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Execute check migration status
|
|
34
|
+
*
|
|
35
|
+
* @param {string} dbType - Database type (postgresql or mongodb)
|
|
36
|
+
* @param {string} stage - Deployment stage (default: 'production')
|
|
37
|
+
* @returns {Promise<Object>} Migration status
|
|
38
|
+
*/
|
|
39
|
+
async execute(dbType, stage = 'production') {
|
|
40
|
+
// Validate inputs
|
|
41
|
+
if (!dbType) {
|
|
42
|
+
throw new ValidationError('dbType is required');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!['postgresql', 'mongodb'].includes(dbType)) {
|
|
46
|
+
throw new ValidationError('dbType must be postgresql or mongodb');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`Checking migration status for ${dbType} in ${stage}`);
|
|
50
|
+
|
|
51
|
+
// Check database state using Prisma
|
|
52
|
+
const state = await this.prismaRunner.checkDatabaseState(dbType);
|
|
53
|
+
|
|
54
|
+
// Build response
|
|
55
|
+
const response = {
|
|
56
|
+
upToDate: state.upToDate,
|
|
57
|
+
pendingMigrations: state.pendingMigrations || 0,
|
|
58
|
+
dbType,
|
|
59
|
+
stage,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Add error if present
|
|
63
|
+
if (state.error) {
|
|
64
|
+
response.error = state.error;
|
|
65
|
+
response.recommendation = 'Run POST /db-migrate to initialize database';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add recommendation if migrations pending
|
|
69
|
+
if (!state.upToDate && state.pendingMigrations > 0) {
|
|
70
|
+
response.recommendation = `Run POST /db-migrate to apply ${state.pendingMigrations} pending migration(s)`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
CheckDatabaseStateUseCase,
|
|
79
|
+
ValidationError,
|
|
80
|
+
};
|
|
81
|
+
|
|
@@ -66,8 +66,9 @@ class CheckEncryptionHealthUseCase {
|
|
|
66
66
|
|
|
67
67
|
const isBypassed = bypassStages.includes(STAGE);
|
|
68
68
|
const hasAES = AES_KEY_ID && AES_KEY_ID.trim() !== '';
|
|
69
|
-
const hasKMS = KMS_KEY_ARN && KMS_KEY_ARN.trim() !== ''
|
|
70
|
-
|
|
69
|
+
const hasKMS = KMS_KEY_ARN && KMS_KEY_ARN.trim() !== '';
|
|
70
|
+
// Prefer KMS over AES when both are configured (KMS is more secure)
|
|
71
|
+
const mode = hasKMS ? 'kms' : hasAES ? 'aes' : 'none';
|
|
71
72
|
|
|
72
73
|
return {
|
|
73
74
|
stage: STAGE || null,
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
|
|
61
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get Migration Status Use Case
|
|
3
|
+
*
|
|
4
|
+
* Retrieves the status of a database migration by process ID.
|
|
5
|
+
* Formats the Process record for migration-specific response.
|
|
6
|
+
*
|
|
7
|
+
* This use case follows the Frigg hexagonal architecture pattern where:
|
|
8
|
+
* - Routers (adapters) call use cases
|
|
9
|
+
* - Use cases contain business logic and formatting
|
|
10
|
+
* - Use cases call repositories for data access
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class GetMigrationStatusUseCase {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} dependencies
|
|
16
|
+
* @param {Object} dependencies.migrationStatusRepository - Repository for migration status (S3)
|
|
17
|
+
*/
|
|
18
|
+
constructor({ migrationStatusRepository }) {
|
|
19
|
+
if (!migrationStatusRepository) {
|
|
20
|
+
throw new Error('migrationStatusRepository dependency is required');
|
|
21
|
+
}
|
|
22
|
+
this.migrationStatusRepository = migrationStatusRepository;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute get migration status
|
|
27
|
+
*
|
|
28
|
+
* @param {string} migrationId - Migration ID to retrieve
|
|
29
|
+
* @param {string} [stage] - Deployment stage (defaults to env.STAGE)
|
|
30
|
+
* @returns {Promise<Object>} Migration status from S3
|
|
31
|
+
* @throws {NotFoundError} If migration not found
|
|
32
|
+
* @throws {ValidationError} If migrationId is invalid
|
|
33
|
+
*/
|
|
34
|
+
async execute(migrationId, stage = null) {
|
|
35
|
+
// Validation
|
|
36
|
+
this._validateParams(migrationId);
|
|
37
|
+
|
|
38
|
+
const effectiveStage = stage || process.env.STAGE || 'production';
|
|
39
|
+
|
|
40
|
+
// Get migration status from S3
|
|
41
|
+
try {
|
|
42
|
+
const migrationStatus = await this.migrationStatusRepository.get(migrationId, effectiveStage);
|
|
43
|
+
return migrationStatus;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.message.includes('not found')) {
|
|
46
|
+
throw new NotFoundError(`Migration not found: ${migrationId}`);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate parameters
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
_validateParams(migrationId) {
|
|
57
|
+
if (!migrationId) {
|
|
58
|
+
throw new ValidationError('migrationId is required');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof migrationId !== 'string') {
|
|
62
|
+
throw new ValidationError('migrationId must be a string');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Custom error for validation failures
|
|
69
|
+
*/
|
|
70
|
+
class ValidationError extends Error {
|
|
71
|
+
constructor(message) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = 'ValidationError';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Custom error for not found resources
|
|
79
|
+
*/
|
|
80
|
+
class NotFoundError extends Error {
|
|
81
|
+
constructor(message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = 'NotFoundError';
|
|
84
|
+
this.statusCode = 404;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
GetMigrationStatusUseCase,
|
|
90
|
+
ValidationError,
|
|
91
|
+
NotFoundError,
|
|
92
|
+
};
|
|
93
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run Database Migration Use Case
|
|
3
|
+
*
|
|
4
|
+
* Business logic for running Prisma database migrations.
|
|
5
|
+
* Orchestrates Prisma client generation and migration execution.
|
|
6
|
+
*
|
|
7
|
+
* This use case follows the Frigg hexagonal architecture pattern where:
|
|
8
|
+
* - Handlers (adapters) call use cases
|
|
9
|
+
* - Use cases contain business logic and orchestration
|
|
10
|
+
* - Use cases call repositories/utilities for data access
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class RunDatabaseMigrationUseCase {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} dependencies
|
|
16
|
+
* @param {Object} dependencies.prismaRunner - Prisma runner utilities
|
|
17
|
+
*/
|
|
18
|
+
constructor({ prismaRunner }) {
|
|
19
|
+
if (!prismaRunner) {
|
|
20
|
+
throw new Error('prismaRunner dependency is required');
|
|
21
|
+
}
|
|
22
|
+
this.prismaRunner = prismaRunner;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute database migration
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} params
|
|
29
|
+
* @param {string} params.dbType - Database type ('postgresql' or 'mongodb')
|
|
30
|
+
* @param {string} params.stage - Deployment stage (determines migration command)
|
|
31
|
+
* @param {boolean} [params.verbose=false] - Enable verbose output
|
|
32
|
+
* @returns {Promise<Object>} Migration result { success, dbType, stage, command, message }
|
|
33
|
+
* @throws {MigrationError} If migration fails
|
|
34
|
+
* @throws {ValidationError} If parameters are invalid
|
|
35
|
+
*/
|
|
36
|
+
async execute({ dbType, stage, verbose = false }) {
|
|
37
|
+
// Validation
|
|
38
|
+
this._validateParams({ dbType, stage });
|
|
39
|
+
|
|
40
|
+
// Step 1: Generate Prisma client
|
|
41
|
+
const generateResult = await this.prismaRunner.runPrismaGenerate(dbType, verbose);
|
|
42
|
+
|
|
43
|
+
if (!generateResult.success) {
|
|
44
|
+
throw new MigrationError(
|
|
45
|
+
`Failed to generate Prisma client: ${generateResult.error || 'Unknown error'}`,
|
|
46
|
+
{ dbType, stage, step: 'generate', output: generateResult.output }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Step 2: Run migrations based on database type
|
|
51
|
+
let migrationResult;
|
|
52
|
+
let migrationCommand;
|
|
53
|
+
|
|
54
|
+
if (dbType === 'postgresql') {
|
|
55
|
+
migrationCommand = this.prismaRunner.getMigrationCommand(stage);
|
|
56
|
+
migrationResult = await this.prismaRunner.runPrismaMigrate(migrationCommand, verbose);
|
|
57
|
+
|
|
58
|
+
if (!migrationResult.success) {
|
|
59
|
+
throw new MigrationError(
|
|
60
|
+
`PostgreSQL migration failed: ${migrationResult.error || 'Unknown error'}`,
|
|
61
|
+
{ dbType, stage, command: migrationCommand, step: 'migrate', output: migrationResult.output }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
} else if (dbType === 'mongodb') {
|
|
65
|
+
migrationCommand = 'db push';
|
|
66
|
+
// Use non-interactive mode for automated/Lambda environments
|
|
67
|
+
migrationResult = await this.prismaRunner.runPrismaDbPush(verbose, true);
|
|
68
|
+
|
|
69
|
+
if (!migrationResult.success) {
|
|
70
|
+
throw new MigrationError(
|
|
71
|
+
`MongoDB push failed: ${migrationResult.error || 'Unknown error'}`,
|
|
72
|
+
{ dbType, stage, command: migrationCommand, step: 'push', output: migrationResult.output }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
throw new ValidationError(`Unsupported database type: ${dbType}. Must be 'postgresql' or 'mongodb'.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Return success result
|
|
80
|
+
return {
|
|
81
|
+
success: true,
|
|
82
|
+
dbType,
|
|
83
|
+
stage,
|
|
84
|
+
command: migrationCommand,
|
|
85
|
+
message: 'Database migration completed successfully',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate execution parameters
|
|
91
|
+
* @private
|
|
92
|
+
*/
|
|
93
|
+
_validateParams({ dbType, stage }) {
|
|
94
|
+
if (!dbType) {
|
|
95
|
+
throw new ValidationError('dbType is required');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof dbType !== 'string') {
|
|
99
|
+
throw new ValidationError('dbType must be a string');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!stage) {
|
|
103
|
+
throw new ValidationError('stage is required');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof stage !== 'string') {
|
|
107
|
+
throw new ValidationError('stage must be a string');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Custom error for migration failures
|
|
114
|
+
*/
|
|
115
|
+
class MigrationError extends Error {
|
|
116
|
+
constructor(message, context = {}) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.name = 'MigrationError';
|
|
119
|
+
this.context = context;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Custom error for validation failures
|
|
125
|
+
*/
|
|
126
|
+
class ValidationError extends Error {
|
|
127
|
+
constructor(message) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.name = 'ValidationError';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
RunDatabaseMigrationUseCase,
|
|
135
|
+
MigrationError,
|
|
136
|
+
ValidationError,
|
|
137
|
+
};
|