@friggframework/core 2.0.0-next.45 → 2.0.0-next.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +28 -0
  2. package/application/commands/integration-commands.js +19 -0
  3. package/core/Worker.js +8 -21
  4. package/credential/repositories/credential-repository-mongo.js +14 -8
  5. package/credential/repositories/credential-repository-postgres.js +14 -8
  6. package/credential/repositories/credential-repository.js +3 -8
  7. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  8. package/database/adapters/lambda-invoker.js +97 -0
  9. package/database/config.js +11 -2
  10. package/database/models/WebsocketConnection.js +11 -10
  11. package/database/prisma.js +63 -3
  12. package/database/repositories/health-check-repository-mongodb.js +3 -0
  13. package/database/repositories/migration-status-repository-s3.js +137 -0
  14. package/database/use-cases/check-database-state-use-case.js +81 -0
  15. package/database/use-cases/check-encryption-health-use-case.js +3 -2
  16. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  17. package/database/use-cases/get-migration-status-use-case.js +93 -0
  18. package/database/use-cases/run-database-migration-use-case.js +137 -0
  19. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  20. package/database/utils/mongodb-collection-utils.js +91 -0
  21. package/database/utils/mongodb-schema-init.js +106 -0
  22. package/database/utils/prisma-runner.js +400 -0
  23. package/database/utils/prisma-schema-parser.js +182 -0
  24. package/encrypt/Cryptor.js +14 -16
  25. package/generated/prisma-mongodb/client.d.ts +1 -0
  26. package/generated/prisma-mongodb/client.js +4 -0
  27. package/generated/prisma-mongodb/default.d.ts +1 -0
  28. package/generated/prisma-mongodb/default.js +4 -0
  29. package/generated/prisma-mongodb/edge.d.ts +1 -0
  30. package/generated/prisma-mongodb/edge.js +334 -0
  31. package/generated/prisma-mongodb/index-browser.js +316 -0
  32. package/generated/prisma-mongodb/index.d.ts +22897 -0
  33. package/generated/prisma-mongodb/index.js +359 -0
  34. package/generated/prisma-mongodb/package.json +183 -0
  35. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  36. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  37. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  38. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  39. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  40. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  41. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  42. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  43. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  44. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  45. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  46. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  47. package/generated/prisma-mongodb/schema.prisma +362 -0
  48. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  49. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  50. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  51. package/generated/prisma-mongodb/wasm.js +341 -0
  52. package/generated/prisma-postgresql/client.d.ts +1 -0
  53. package/generated/prisma-postgresql/client.js +4 -0
  54. package/generated/prisma-postgresql/default.d.ts +1 -0
  55. package/generated/prisma-postgresql/default.js +4 -0
  56. package/generated/prisma-postgresql/edge.d.ts +1 -0
  57. package/generated/prisma-postgresql/edge.js +356 -0
  58. package/generated/prisma-postgresql/index-browser.js +338 -0
  59. package/generated/prisma-postgresql/index.d.ts +25071 -0
  60. package/generated/prisma-postgresql/index.js +381 -0
  61. package/generated/prisma-postgresql/package.json +183 -0
  62. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  63. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  64. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  65. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  66. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  67. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  68. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  69. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  70. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  71. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  72. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  73. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  74. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  75. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  76. package/generated/prisma-postgresql/schema.prisma +345 -0
  77. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  78. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  79. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  80. package/generated/prisma-postgresql/wasm.js +363 -0
  81. package/handlers/database-migration-handler.js +227 -0
  82. package/handlers/routers/auth.js +1 -1
  83. package/handlers/routers/db-migration.handler.js +29 -0
  84. package/handlers/routers/db-migration.js +256 -0
  85. package/handlers/routers/health.js +41 -6
  86. package/handlers/routers/integration-webhook-routers.js +2 -2
  87. package/handlers/use-cases/check-integrations-health-use-case.js +22 -10
  88. package/handlers/workers/db-migration.js +352 -0
  89. package/index.js +12 -0
  90. package/integrations/integration-router.js +60 -70
  91. package/integrations/repositories/integration-repository-interface.js +12 -0
  92. package/integrations/repositories/integration-repository-mongo.js +32 -0
  93. package/integrations/repositories/integration-repository-postgres.js +33 -0
  94. package/integrations/repositories/process-repository-postgres.js +2 -2
  95. package/integrations/tests/doubles/test-integration-repository.js +2 -2
  96. package/logs/logger.js +0 -4
  97. package/modules/entity.js +0 -1
  98. package/modules/repositories/module-repository-mongo.js +3 -12
  99. package/modules/repositories/module-repository-postgres.js +0 -11
  100. package/modules/repositories/module-repository.js +1 -12
  101. package/modules/use-cases/get-entity-options-by-id.js +1 -1
  102. package/modules/use-cases/get-module.js +1 -2
  103. package/modules/use-cases/refresh-entity-options.js +1 -1
  104. package/modules/use-cases/test-module-auth.js +1 -1
  105. package/package.json +82 -66
  106. package/prisma-mongodb/schema.prisma +21 -21
  107. package/prisma-postgresql/schema.prisma +15 -15
  108. package/queues/queuer-util.js +24 -21
  109. package/types/core/index.d.ts +2 -2
  110. package/types/module-plugin/index.d.ts +0 -2
  111. package/user/use-cases/authenticate-user.js +127 -0
  112. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  113. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  114. package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
  115. package/user/user.js +16 -0
  116. package/websocket/repositories/websocket-connection-repository-mongo.js +11 -10
  117. package/websocket/repositories/websocket-connection-repository-postgres.js +11 -10
  118. package/websocket/repositories/websocket-connection-repository.js +11 -10
  119. package/application/commands/integration-commands.test.js +0 -123
  120. package/database/encryption/encryption-integration.test.js +0 -553
  121. package/database/encryption/encryption-schema-registry.test.js +0 -392
  122. package/database/encryption/field-encryption-service.test.js +0 -525
  123. package/database/encryption/mongo-decryption-fix-verification.test.js +0 -348
  124. package/database/encryption/postgres-decryption-fix-verification.test.js +0 -371
  125. package/database/encryption/postgres-relation-decryption.test.js +0 -245
  126. package/database/encryption/prisma-encryption-extension.test.js +0 -439
  127. package/errors/base-error.test.js +0 -32
  128. package/errors/fetch-error.test.js +0 -79
  129. package/errors/halt-error.test.js +0 -11
  130. package/errors/validation-errors.test.js +0 -120
  131. package/handlers/auth-flow.integration.test.js +0 -147
  132. package/handlers/integration-event-dispatcher.test.js +0 -209
  133. package/handlers/routers/health.test.js +0 -210
  134. package/handlers/routers/integration-webhook-routers.test.js +0 -126
  135. package/handlers/webhook-flow.integration.test.js +0 -356
  136. package/handlers/workers/integration-defined-workers.test.js +0 -184
  137. package/integrations/tests/use-cases/create-integration.test.js +0 -131
  138. package/integrations/tests/use-cases/delete-integration-for-user.test.js +0 -150
  139. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +0 -92
  140. package/integrations/tests/use-cases/get-integration-for-user.test.js +0 -150
  141. package/integrations/tests/use-cases/get-integration-instance.test.js +0 -176
  142. package/integrations/tests/use-cases/get-integrations-for-user.test.js +0 -176
  143. package/integrations/tests/use-cases/get-possible-integrations.test.js +0 -188
  144. package/integrations/tests/use-cases/update-integration-messages.test.js +0 -142
  145. package/integrations/tests/use-cases/update-integration-status.test.js +0 -103
  146. package/integrations/tests/use-cases/update-integration.test.js +0 -141
  147. package/integrations/use-cases/create-process.test.js +0 -178
  148. package/integrations/use-cases/get-process.test.js +0 -190
  149. package/integrations/use-cases/load-integration-context-full.test.js +0 -329
  150. package/integrations/use-cases/load-integration-context.test.js +0 -114
  151. package/integrations/use-cases/update-process-metrics.test.js +0 -308
  152. package/integrations/use-cases/update-process-state.test.js +0 -256
  153. package/lambda/TimeoutCatcher.test.js +0 -68
  154. package/logs/logger.test.js +0 -76
  155. package/modules/module-hydration.test.js +0 -205
  156. package/modules/requester/requester.test.js +0 -28
  157. package/user/tests/use-cases/create-individual-user.test.js +0 -24
  158. package/user/tests/use-cases/create-organization-user.test.js +0 -28
  159. package/user/tests/use-cases/create-token-for-user-id.test.js +0 -19
  160. package/user/tests/use-cases/get-user-from-bearer-token.test.js +0 -64
  161. package/user/tests/use-cases/login-user.test.js +0 -220
  162. package/user/tests/user-password-encryption-isolation.test.js +0 -237
  163. package/user/tests/user-password-hashing.test.js +0 -235
@@ -1,5 +1,8 @@
1
1
  const { mongoose } = require('../mongoose');
2
- const AWS = require('aws-sdk');
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 AWS.ApiGatewayManagementApi({
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
- await apigwManagementApi
27
- .postToConnection({
28
- ConnectionId: conn.connectionId,
29
- Data: JSON.stringify(data),
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,
@@ -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
- 'Field-level encryption disabled. Set STAGE=production and configure keys to enable.'
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
- PrismaClient = require('@prisma-mongodb/client').PrismaClient;
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 = require('@prisma-postgresql/client').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() !== '' && !hasAES;
70
- const mode = hasAES ? 'aes' : hasKMS ? 'kms' : 'none';
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
+ };