@friggframework/core 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.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.
Files changed (237) hide show
  1. package/CHANGELOG.md +66 -67
  2. package/CLAUDE.md +148 -124
  3. package/README.md +165 -130
  4. package/admin-scripts/index.js +52 -0
  5. package/admin-scripts/repositories/admin-api-key-repository-documentdb.js +21 -0
  6. package/admin-scripts/repositories/admin-api-key-repository-factory.js +51 -0
  7. package/admin-scripts/repositories/admin-api-key-repository-interface.js +121 -0
  8. package/admin-scripts/repositories/admin-api-key-repository-mongo.js +155 -0
  9. package/admin-scripts/repositories/admin-api-key-repository-postgres.js +189 -0
  10. package/admin-scripts/repositories/admin-process-repository-documentdb.js +21 -0
  11. package/admin-scripts/repositories/admin-process-repository-factory.js +49 -0
  12. package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
  13. package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
  14. package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
  15. package/admin-scripts/repositories/script-execution-repository-documentdb.js +21 -0
  16. package/admin-scripts/repositories/script-execution-repository-factory.js +51 -0
  17. package/admin-scripts/repositories/script-execution-repository-interface.js +193 -0
  18. package/admin-scripts/repositories/script-execution-repository-mongo.js +278 -0
  19. package/admin-scripts/repositories/script-execution-repository-postgres.js +320 -0
  20. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  21. package/admin-scripts/repositories/script-schedule-repository-factory.js +49 -0
  22. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  23. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  24. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  25. package/application/commands/admin-script-commands.js +348 -0
  26. package/application/commands/integration-commands.js +12 -6
  27. package/application/index.js +5 -3
  28. package/assertions/get.js +17 -7
  29. package/associations/association.js +59 -57
  30. package/core/CLAUDE.md +222 -156
  31. package/core/Worker.js +33 -12
  32. package/credential/credential-router.js +262 -0
  33. package/credential/repositories/credential-repository-documentdb.js +71 -1
  34. package/credential/repositories/credential-repository-factory.js +4 -6
  35. package/credential/repositories/credential-repository-interface.js +8 -2
  36. package/credential/repositories/credential-repository-mongo.js +45 -4
  37. package/credential/repositories/credential-repository-postgres.js +44 -3
  38. package/credential/repositories/credential-repository.js +3 -1
  39. package/credential/use-cases/delete-credential-for-user.js +50 -0
  40. package/credential/use-cases/get-credential-for-user.js +21 -3
  41. package/credential/use-cases/list-credentials-for-user.js +31 -0
  42. package/credential/use-cases/reauthorize-credential.js +103 -0
  43. package/credential/use-cases/update-authentication-status.js +5 -2
  44. package/database/MONGODB_TRANSACTION_FIX.md +72 -52
  45. package/database/config.js +13 -59
  46. package/database/documentdb-encryption-service.js +29 -13
  47. package/database/documentdb-utils.js +21 -12
  48. package/database/encryption/README.md +53 -36
  49. package/database/encryption/encryption-schema-registry.js +29 -23
  50. package/database/encryption/field-encryption-service.js +15 -4
  51. package/database/encryption/logger.js +3 -2
  52. package/database/encryption/prisma-encryption-extension.js +5 -8
  53. package/database/prisma.js +22 -14
  54. package/database/repositories/health-check-repository-factory.js +7 -5
  55. package/database/repositories/health-check-repository-interface.js +15 -5
  56. package/database/repositories/health-check-repository-postgres.js +1 -1
  57. package/database/use-cases/check-database-health-use-case.js +3 -2
  58. package/database/use-cases/check-database-state-use-case.js +8 -7
  59. package/database/use-cases/get-database-state-via-worker-use-case.js +15 -12
  60. package/database/use-cases/get-migration-status-use-case.js +4 -2
  61. package/database/use-cases/run-database-migration-use-case.js +41 -9
  62. package/database/use-cases/test-encryption-use-case.js +4 -5
  63. package/database/use-cases/trigger-database-migration-use-case.js +9 -8
  64. package/database/utils/mongodb-collection-utils.js +11 -3
  65. package/database/utils/mongodb-schema-init.js +10 -4
  66. package/database/utils/prisma-runner.js +144 -74
  67. package/database/utils/prisma-schema-parser.js +2 -2
  68. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +135 -80
  69. package/encrypt/Cryptor.js +54 -49
  70. package/encrypt/aes-encryption-key-provider.js +82 -0
  71. package/encrypt/encryption-key-provider-interface.js +50 -0
  72. package/errors/fetch-error.js +1 -1
  73. package/generated/prisma-mongodb/edge.js +59 -4
  74. package/generated/prisma-mongodb/index-browser.js +49 -1
  75. package/generated/prisma-mongodb/index.d.ts +4023 -61
  76. package/generated/prisma-mongodb/index.js +59 -4
  77. package/generated/prisma-mongodb/package.json +1 -1
  78. package/generated/prisma-mongodb/schema.prisma +87 -0
  79. package/generated/prisma-mongodb/wasm.js +59 -4
  80. package/generated/prisma-postgresql/edge.js +58 -4
  81. package/generated/prisma-postgresql/index-browser.js +48 -1
  82. package/generated/prisma-postgresql/index.d.ts +4738 -54
  83. package/generated/prisma-postgresql/index.js +58 -4
  84. package/generated/prisma-postgresql/package.json +1 -1
  85. package/generated/prisma-postgresql/schema.prisma +85 -0
  86. package/generated/prisma-postgresql/wasm.js +58 -4
  87. package/handlers/WEBHOOKS.md +104 -81
  88. package/handlers/app-definition-loader.js +53 -10
  89. package/handlers/app-handler-helpers.js +14 -3
  90. package/handlers/backend-utils.js +0 -1
  91. package/handlers/database-migration-handler.js +64 -45
  92. package/handlers/middleware/admin-auth.js +53 -0
  93. package/handlers/routers/HEALTHCHECK.md +170 -135
  94. package/handlers/routers/admin.js +474 -0
  95. package/handlers/routers/auth.js +51 -9
  96. package/handlers/routers/db-migration.handler.js +2 -3
  97. package/handlers/routers/db-migration.js +102 -66
  98. package/handlers/routers/docs.js +180 -0
  99. package/handlers/routers/health.js +84 -283
  100. package/handlers/routers/integration-defined-routers.js +44 -33
  101. package/handlers/routers/integration-webhook-routers.js +73 -52
  102. package/handlers/routers/middleware/loadUser.js +39 -0
  103. package/handlers/routers/middleware/requireAdmin.js +43 -0
  104. package/handlers/routers/middleware/requireLoggedInUser.js +19 -0
  105. package/handlers/routers/user.js +30 -14
  106. package/handlers/routers/websocket.js +18 -4
  107. package/handlers/use-cases/check-integrations-health-use-case.js +13 -6
  108. package/handlers/workers/db-migration.js +74 -35
  109. package/handlers/workers/integration-defined-workers.js +31 -20
  110. package/index.js +50 -16
  111. package/infrastructure/scheduler/index.js +15 -11
  112. package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
  113. package/infrastructure/scheduler/scheduler-service-factory.js +38 -31
  114. package/integrations/WEBHOOK-QUICKSTART.md +21 -14
  115. package/integrations/index.js +4 -0
  116. package/integrations/integration-base.js +9 -6
  117. package/integrations/integration-router.js +1828 -157
  118. package/integrations/options.js +47 -20
  119. package/integrations/repositories/integration-mapping-repository-factory.js +4 -8
  120. package/integrations/repositories/integration-repository-documentdb.js +53 -24
  121. package/integrations/repositories/integration-repository-factory.js +6 -6
  122. package/integrations/repositories/integration-repository-interface.js +27 -9
  123. package/integrations/repositories/integration-repository-postgres.js +3 -3
  124. package/integrations/repositories/process-repository-factory.js +4 -7
  125. package/integrations/repositories/process-repository-interface.js +9 -5
  126. package/integrations/repositories/process-repository-mongo.js +7 -3
  127. package/integrations/repositories/process-repository-postgres.js +2 -2
  128. package/integrations/tests/doubles/config-capturing-integration.js +9 -8
  129. package/integrations/tests/doubles/dummy-integration-class.js +81 -8
  130. package/integrations/tests/doubles/test-integration-repository.js +51 -11
  131. package/integrations/use-cases/create-integration.js +36 -32
  132. package/integrations/use-cases/create-process.js +20 -9
  133. package/integrations/use-cases/delete-integration-for-user.js +5 -4
  134. package/integrations/use-cases/execute-proxy-request.js +564 -0
  135. package/integrations/use-cases/get-integration-for-user.js +25 -10
  136. package/integrations/use-cases/get-integration-instance-by-definition.js +13 -7
  137. package/integrations/use-cases/get-integrations-for-user.js +4 -1
  138. package/integrations/use-cases/get-possible-integrations.js +1 -1
  139. package/integrations/use-cases/get-process.js +5 -6
  140. package/integrations/use-cases/index.js +3 -1
  141. package/integrations/use-cases/load-integration-context.js +1 -5
  142. package/integrations/use-cases/update-process-state.js +10 -8
  143. package/integrations/utils/map-integration-dto.js +9 -3
  144. package/jest-setup.js +1 -1
  145. package/jest-teardown.js +1 -1
  146. package/lambda/TimeoutCatcher.js +32 -30
  147. package/lambda/index.js +1 -1
  148. package/logs/index.js +3 -3
  149. package/modules/domain/entities/AuthorizationSession.js +131 -0
  150. package/modules/domain/entities/index.js +10 -0
  151. package/modules/module.js +4 -9
  152. package/modules/repositories/authorization-session-repository-factory.js +53 -0
  153. package/modules/repositories/authorization-session-repository-interface.js +75 -0
  154. package/modules/repositories/authorization-session-repository-mongo.js +169 -0
  155. package/modules/repositories/authorization-session-repository-postgres.js +170 -0
  156. package/modules/repositories/module-repository-documentdb.js +89 -41
  157. package/modules/repositories/module-repository-factory.js +4 -6
  158. package/modules/repositories/module-repository-interface.js +13 -0
  159. package/modules/repositories/module-repository-mongo.js +40 -5
  160. package/modules/repositories/module-repository-postgres.js +42 -5
  161. package/modules/repositories/module-repository.js +28 -0
  162. package/modules/requester/api-key.js +0 -2
  163. package/modules/requester/basic.js +0 -1
  164. package/modules/requester/oauth-2.js +3 -14
  165. package/modules/test/mock-api/definition.js +15 -10
  166. package/modules/test/mock-api/mocks/hubspot.js +36 -36
  167. package/modules/tests/doubles/test-module-factory.js +10 -4
  168. package/modules/tests/doubles/test-module-repository.js +1 -1
  169. package/modules/use-cases/delete-module-entity.js +23 -0
  170. package/modules/use-cases/get-authorization-requirements.js +110 -0
  171. package/modules/use-cases/get-entities-for-user.js +1 -1
  172. package/modules/use-cases/get-entity-options-by-id.js +8 -6
  173. package/modules/use-cases/get-module-entity-by-id.js +16 -0
  174. package/modules/use-cases/get-module.js +12 -9
  175. package/modules/use-cases/process-authorization-callback.js +12 -6
  176. package/modules/use-cases/process-authorization-step.js +160 -0
  177. package/modules/use-cases/refresh-entity-options.js +8 -6
  178. package/modules/use-cases/start-authorization-session.js +86 -0
  179. package/modules/use-cases/test-module-auth.js +8 -6
  180. package/modules/use-cases/update-module-entity.js +27 -0
  181. package/modules/utils/map-module-dto.js +2 -2
  182. package/openapi/openapi-spec-generator.js +370 -0
  183. package/openapi/openapi-v1.yaml +1360 -0
  184. package/openapi/openapi-v2.yaml +1535 -0
  185. package/package.json +7 -9
  186. package/prisma-mongodb/schema.prisma +87 -0
  187. package/prisma-postgresql/migrations/20260222000000_add_admin_process_and_script_schedule/migration.sql +55 -0
  188. package/prisma-postgresql/schema.prisma +85 -0
  189. package/providers/index.js +13 -0
  190. package/providers/resolve-provider.js +93 -0
  191. package/queues/index.js +19 -0
  192. package/queues/providers/index.js +7 -0
  193. package/queues/providers/netlify-background-provider.js +76 -0
  194. package/queues/providers/qstash-queue-provider.js +122 -0
  195. package/queues/queue-client-interface.js +55 -0
  196. package/queues/queue-provider-factory.js +93 -0
  197. package/queues/queue-provider.js +46 -0
  198. package/queues/queuer-util.js +33 -25
  199. package/syncs/repositories/sync-repository-documentdb.js +39 -19
  200. package/syncs/repositories/sync-repository-factory.js +4 -4
  201. package/syncs/repositories/sync-repository-interface.js +6 -2
  202. package/syncs/repositories/sync-repository-postgres.js +19 -13
  203. package/syncs/sync.js +104 -100
  204. package/token/repositories/token-repository-factory.js +4 -6
  205. package/types/assertions/index.d.ts +72 -72
  206. package/types/core/index.d.ts +98 -53
  207. package/types/errors/index.d.ts +61 -54
  208. package/types/eslint-config/index.d.ts +40 -40
  209. package/types/lambda/index.d.ts +26 -26
  210. package/types/logs/index.d.ts +4 -4
  211. package/types/prettier-config/index.d.ts +5 -5
  212. package/types/test-environment/index.d.ts +14 -14
  213. package/types/tsconfig.json +95 -93
  214. package/user/repositories/user-repository-factory.js +4 -4
  215. package/user/tests/doubles/test-user-repository.js +5 -2
  216. package/user/use-cases/authenticate-user.js +2 -4
  217. package/user/use-cases/authenticate-with-shared-secret.js +1 -1
  218. package/user/use-cases/create-individual-user.js +8 -7
  219. package/user/use-cases/create-organization-user.js +1 -1
  220. package/user/use-cases/create-token-for-user-id.js +1 -1
  221. package/user/use-cases/delete-user.js +47 -0
  222. package/user/use-cases/get-user-from-adopter-jwt.js +0 -2
  223. package/user/use-cases/get-user-from-bearer-token.js +7 -3
  224. package/user/use-cases/get-user-from-x-frigg-headers.js +14 -11
  225. package/user/use-cases/login-user.js +8 -8
  226. package/user/user.js +18 -4
  227. package/utils/backend-path.js +2 -2
  228. package/utils/index.js +4 -1
  229. package/websocket/repositories/websocket-connection-repository-documentdb.js +46 -21
  230. package/websocket/repositories/websocket-connection-repository-factory.js +4 -8
  231. package/websocket/repositories/websocket-connection-repository-mongo.js +26 -16
  232. package/websocket/repositories/websocket-connection-repository-postgres.js +26 -15
  233. package/websocket/repositories/websocket-connection-repository.js +26 -20
  234. package/websocket/websocket-message-sender-interface.js +38 -0
  235. package/database/adapters/lambda-invoker.js +0 -97
  236. package/database/repositories/migration-status-repository-s3.js +0 -137
  237. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
@@ -1,8 +1,7 @@
1
1
  const { prisma } = require('../../database/prisma');
2
2
  const {
3
- ApiGatewayManagementApiClient,
4
- PostToConnectionCommand,
5
- } = require('@aws-sdk/client-apigatewaymanagementapi');
3
+ StaleConnectionError,
4
+ } = require('../websocket-message-sender-interface');
6
5
  const {
7
6
  WebsocketConnectionRepositoryInterface,
8
7
  } = require('./websocket-connection-repository-interface');
@@ -16,15 +15,16 @@ const {
16
15
  * - PostgreSQL: Integer IDs with auto-increment
17
16
  * - Both use same query patterns (no many-to-many differences)
18
17
  *
19
- * Migration from Mongoose:
20
- * - Constructor injection of Prisma client
21
- * - Static method getActiveConnections() → Instance method
22
- * - AWS API Gateway Management API integration preserved
18
+ * BREAKING CHANGE (v3): A messageSender must be explicitly provided for
19
+ * WebSocket send functionality. For AWS API Gateway, pass
20
+ * `new ApiGatewayMessageSender()` from @friggframework/provider-aws.
21
+ * See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.
23
22
  */
24
23
  class WebsocketConnectionRepository extends WebsocketConnectionRepositoryInterface {
25
- constructor(prismaClient = prisma) {
24
+ constructor(prismaClient = prisma, messageSender = null) {
26
25
  super();
27
- this.prisma = prismaClient; // Allow injection for testing
26
+ this.prisma = prismaClient;
27
+ this._messageSender = messageSender;
28
28
  }
29
29
 
30
30
  /**
@@ -79,25 +79,31 @@ class WebsocketConnectionRepository extends WebsocketConnectionRepositoryInterfa
79
79
  select: { connectionId: true },
80
80
  });
81
81
 
82
+ if (!this._messageSender) {
83
+ throw new Error(
84
+ 'WebsocketConnectionRepository requires a messageSender for send functionality.\n' +
85
+ 'Pass one via constructor, e.g.:\n' +
86
+ ' const { ApiGatewayMessageSender } = require("@friggframework/provider-aws");\n' +
87
+ ' new WebsocketConnectionRepository(prisma, new ApiGatewayMessageSender())\n' +
88
+ 'See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.'
89
+ );
90
+ }
91
+ const sender = this._messageSender;
92
+
82
93
  return connections.map((conn) => ({
83
94
  connectionId: conn.connectionId,
84
95
  send: async (data) => {
85
- const apigwManagementApi = new ApiGatewayManagementApiClient({
86
- endpoint: process.env.WEBSOCKET_API_ENDPOINT,
87
- });
88
-
89
96
  try {
90
- const command = new PostToConnectionCommand({
91
- ConnectionId: conn.connectionId,
92
- Data: JSON.stringify(data),
93
- });
94
- await apigwManagementApi.send(command);
97
+ await sender.send(
98
+ conn.connectionId,
99
+ data,
100
+ process.env.WEBSOCKET_API_ENDPOINT
101
+ );
95
102
  } catch (error) {
96
- if (error.statusCode === 410 || error.$metadata?.httpStatusCode === 410) {
103
+ if (error instanceof StaleConnectionError) {
97
104
  console.log(
98
105
  `Stale connection ${conn.connectionId}`
99
106
  );
100
- // Delete stale connection
101
107
  await this.prisma.websocketConnection.deleteMany({
102
108
  where: { connectionId: conn.connectionId },
103
109
  });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * WebSocket Message Sender Interface (Port)
3
+ *
4
+ * Defines the contract for sending messages to active WebSocket connections.
5
+ * Used by WebSocket connection repositories to push data to connected clients.
6
+ *
7
+ * Following Frigg's hexagonal architecture pattern:
8
+ * - Port defines WHAT the service does (contract)
9
+ * - Adapters implement HOW (AWS API Gateway, Netlify, etc.)
10
+ */
11
+ class WebSocketMessageSenderInterface {
12
+ /**
13
+ * Send data to a specific WebSocket connection
14
+ *
15
+ * @param {string} connectionId - The WebSocket connection identifier
16
+ * @param {*} data - Data to send (will be JSON-serialized by the adapter)
17
+ * @param {string} endpoint - The WebSocket API endpoint URL
18
+ * @returns {Promise<void>}
19
+ * @throws {StaleConnectionError} If the connection is no longer active (410 Gone)
20
+ */
21
+ async send(connectionId, data, endpoint) {
22
+ throw new Error('Method send must be implemented by subclass');
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Error thrown when a WebSocket connection is stale (disconnected).
28
+ * Repositories should catch this and clean up the stale connection record.
29
+ */
30
+ class StaleConnectionError extends Error {
31
+ constructor(connectionId) {
32
+ super(`Stale WebSocket connection: ${connectionId}`);
33
+ this.name = 'StaleConnectionError';
34
+ this.connectionId = connectionId;
35
+ }
36
+ }
37
+
38
+ module.exports = { WebSocketMessageSenderInterface, StaleConnectionError };
@@ -1,97 +0,0 @@
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
-
97
-
@@ -1,137 +0,0 @@
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
-
@@ -1,184 +0,0 @@
1
- /**
2
- * EventBridge Scheduler Adapter
3
- *
4
- * Infrastructure Layer - Hexagonal Architecture
5
- *
6
- * Responsible for:
7
- * - Creating one-time EventBridge Scheduler schedules
8
- * - Deleting schedules when no longer needed
9
- * - Checking schedule status
10
- *
11
- * This adapter implements SchedulerServiceInterface for AWS EventBridge Scheduler.
12
- */
13
-
14
- const {
15
- SchedulerClient,
16
- CreateScheduleCommand,
17
- DeleteScheduleCommand,
18
- GetScheduleCommand,
19
- ResourceNotFoundException,
20
- } = require('@aws-sdk/client-scheduler');
21
-
22
- const { SchedulerServiceInterface } = require('./scheduler-service-interface');
23
-
24
- class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
25
- constructor({ region } = {}) {
26
- super();
27
- this.client = new SchedulerClient({
28
- region: region || process.env.AWS_REGION || 'us-east-1',
29
- });
30
- this.scheduleGroupName =
31
- process.env.SCHEDULE_GROUP_NAME || 'frigg-integration-schedules';
32
- this.roleArn = process.env.SCHEDULER_ROLE_ARN;
33
- }
34
-
35
- /**
36
- * Create a one-time schedule that sends a message to SQS
37
- *
38
- * @param {Object} params
39
- * @param {string} params.scheduleName - Unique name for the schedule
40
- * @param {Date} params.scheduleAt - When to trigger the schedule
41
- * @param {string} params.queueResourceId - Queue resource identifier (ARN) to send message to
42
- * @param {Object} params.payload - Message payload
43
- * @returns {Promise<{scheduledJobId: string, scheduledAt: string}>}
44
- */
45
- async scheduleOneTime({ scheduleName, scheduleAt, queueResourceId, payload }) {
46
- if (!scheduleName) {
47
- throw new Error('scheduleName is required');
48
- }
49
- if (!scheduleAt || !(scheduleAt instanceof Date)) {
50
- throw new Error('scheduleAt must be a valid Date object');
51
- }
52
- if (!queueResourceId) {
53
- throw new Error('queueResourceId is required');
54
- }
55
- if (!this.roleArn) {
56
- throw new Error(
57
- 'SCHEDULER_ROLE_ARN environment variable is not set'
58
- );
59
- }
60
-
61
- // Format date to AWS schedule expression (at(yyyy-mm-ddThh:mm:ss))
62
- const scheduleExpression = `at(${scheduleAt.toISOString().replace(/\.\d{3}Z$/, '')})`;
63
-
64
- const command = new CreateScheduleCommand({
65
- Name: scheduleName,
66
- GroupName: this.scheduleGroupName,
67
- ScheduleExpression: scheduleExpression,
68
- ScheduleExpressionTimezone: 'UTC',
69
- FlexibleTimeWindow: {
70
- Mode: 'OFF',
71
- },
72
- Target: {
73
- Arn: queueResourceId,
74
- RoleArn: this.roleArn,
75
- Input: JSON.stringify(payload),
76
- },
77
- ActionAfterCompletion: 'DELETE', // Auto-cleanup after execution
78
- });
79
-
80
- try {
81
- const response = await this.client.send(command);
82
- console.log(
83
- `[Scheduler] Created schedule ${scheduleName} for ${scheduleAt.toISOString()}`
84
- );
85
-
86
- return {
87
- scheduledJobId: response.ScheduleArn,
88
- scheduledAt: scheduleAt.toISOString(),
89
- };
90
- } catch (error) {
91
- console.error(
92
- `[Scheduler] Failed to create schedule ${scheduleName}:`,
93
- error.message
94
- );
95
- throw error;
96
- }
97
- }
98
-
99
- /**
100
- * Delete a schedule
101
- *
102
- * @param {string} scheduleName - Name of the schedule to delete
103
- * @returns {Promise<void>}
104
- */
105
- async deleteSchedule(scheduleName) {
106
- if (!scheduleName) {
107
- throw new Error('scheduleName is required');
108
- }
109
-
110
- const command = new DeleteScheduleCommand({
111
- Name: scheduleName,
112
- GroupName: this.scheduleGroupName,
113
- });
114
-
115
- try {
116
- await this.client.send(command);
117
- console.log(`[Scheduler] Deleted schedule ${scheduleName}`);
118
- } catch (error) {
119
- if (error instanceof ResourceNotFoundException) {
120
- console.log(
121
- `[Scheduler] Schedule ${scheduleName} not found (already deleted or executed)`
122
- );
123
- return; // Graceful handling - schedule doesn't exist
124
- }
125
- console.error(
126
- `[Scheduler] Failed to delete schedule ${scheduleName}:`,
127
- error.message
128
- );
129
- throw error;
130
- }
131
- }
132
-
133
- /**
134
- * Get schedule status
135
- *
136
- * @param {string} scheduleName - Name of the schedule
137
- * @returns {Promise<{exists: boolean, scheduledAt?: string, state?: string}>}
138
- */
139
- async getScheduleStatus(scheduleName) {
140
- if (!scheduleName) {
141
- throw new Error('scheduleName is required');
142
- }
143
-
144
- const command = new GetScheduleCommand({
145
- Name: scheduleName,
146
- GroupName: this.scheduleGroupName,
147
- });
148
-
149
- try {
150
- const response = await this.client.send(command);
151
-
152
- // Parse the schedule expression to get the scheduled time
153
- // Format: at(yyyy-mm-ddThh:mm:ss)
154
- let scheduledAt = null;
155
- if (response.ScheduleExpression) {
156
- const match = response.ScheduleExpression.match(
157
- /^at\((.+)\)$/
158
- );
159
- if (match) {
160
- scheduledAt = new Date(match[1] + 'Z').toISOString();
161
- }
162
- }
163
-
164
- return {
165
- exists: true,
166
- scheduledAt,
167
- state: response.State,
168
- };
169
- } catch (error) {
170
- if (error instanceof ResourceNotFoundException) {
171
- return {
172
- exists: false,
173
- };
174
- }
175
- console.error(
176
- `[Scheduler] Failed to get schedule ${scheduleName}:`,
177
- error.message
178
- );
179
- throw error;
180
- }
181
- }
182
- }
183
-
184
- module.exports = { EventBridgeSchedulerAdapter };