@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.
- package/CHANGELOG.md +66 -67
- package/CLAUDE.md +148 -124
- package/README.md +165 -130
- package/admin-scripts/index.js +52 -0
- package/admin-scripts/repositories/admin-api-key-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/admin-api-key-repository-factory.js +51 -0
- package/admin-scripts/repositories/admin-api-key-repository-interface.js +121 -0
- package/admin-scripts/repositories/admin-api-key-repository-mongo.js +155 -0
- package/admin-scripts/repositories/admin-api-key-repository-postgres.js +189 -0
- package/admin-scripts/repositories/admin-process-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/admin-process-repository-factory.js +49 -0
- package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
- package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
- package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
- package/admin-scripts/repositories/script-execution-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/script-execution-repository-factory.js +51 -0
- package/admin-scripts/repositories/script-execution-repository-interface.js +193 -0
- package/admin-scripts/repositories/script-execution-repository-mongo.js +278 -0
- package/admin-scripts/repositories/script-execution-repository-postgres.js +320 -0
- package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/script-schedule-repository-factory.js +49 -0
- package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
- package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
- package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
- package/application/commands/admin-script-commands.js +348 -0
- package/application/commands/integration-commands.js +12 -6
- package/application/index.js +5 -3
- package/assertions/get.js +17 -7
- package/associations/association.js +59 -57
- package/core/CLAUDE.md +222 -156
- package/core/Worker.js +33 -12
- package/credential/credential-router.js +262 -0
- package/credential/repositories/credential-repository-documentdb.js +71 -1
- package/credential/repositories/credential-repository-factory.js +4 -6
- package/credential/repositories/credential-repository-interface.js +8 -2
- package/credential/repositories/credential-repository-mongo.js +45 -4
- package/credential/repositories/credential-repository-postgres.js +44 -3
- package/credential/repositories/credential-repository.js +3 -1
- package/credential/use-cases/delete-credential-for-user.js +50 -0
- package/credential/use-cases/get-credential-for-user.js +21 -3
- package/credential/use-cases/list-credentials-for-user.js +31 -0
- package/credential/use-cases/reauthorize-credential.js +103 -0
- package/credential/use-cases/update-authentication-status.js +5 -2
- package/database/MONGODB_TRANSACTION_FIX.md +72 -52
- package/database/config.js +13 -59
- package/database/documentdb-encryption-service.js +29 -13
- package/database/documentdb-utils.js +21 -12
- package/database/encryption/README.md +53 -36
- package/database/encryption/encryption-schema-registry.js +29 -23
- package/database/encryption/field-encryption-service.js +15 -4
- package/database/encryption/logger.js +3 -2
- package/database/encryption/prisma-encryption-extension.js +5 -8
- package/database/prisma.js +22 -14
- package/database/repositories/health-check-repository-factory.js +7 -5
- package/database/repositories/health-check-repository-interface.js +15 -5
- package/database/repositories/health-check-repository-postgres.js +1 -1
- package/database/use-cases/check-database-health-use-case.js +3 -2
- package/database/use-cases/check-database-state-use-case.js +8 -7
- package/database/use-cases/get-database-state-via-worker-use-case.js +15 -12
- package/database/use-cases/get-migration-status-use-case.js +4 -2
- package/database/use-cases/run-database-migration-use-case.js +41 -9
- package/database/use-cases/test-encryption-use-case.js +4 -5
- package/database/use-cases/trigger-database-migration-use-case.js +9 -8
- package/database/utils/mongodb-collection-utils.js +11 -3
- package/database/utils/mongodb-schema-init.js +10 -4
- package/database/utils/prisma-runner.js +144 -74
- package/database/utils/prisma-schema-parser.js +2 -2
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +135 -80
- package/encrypt/Cryptor.js +54 -49
- package/encrypt/aes-encryption-key-provider.js +82 -0
- package/encrypt/encryption-key-provider-interface.js +50 -0
- package/errors/fetch-error.js +1 -1
- package/generated/prisma-mongodb/edge.js +59 -4
- package/generated/prisma-mongodb/index-browser.js +49 -1
- package/generated/prisma-mongodb/index.d.ts +4023 -61
- package/generated/prisma-mongodb/index.js +59 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +87 -0
- package/generated/prisma-mongodb/wasm.js +59 -4
- package/generated/prisma-postgresql/edge.js +58 -4
- package/generated/prisma-postgresql/index-browser.js +48 -1
- package/generated/prisma-postgresql/index.d.ts +4738 -54
- package/generated/prisma-postgresql/index.js +58 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +85 -0
- package/generated/prisma-postgresql/wasm.js +58 -4
- package/handlers/WEBHOOKS.md +104 -81
- package/handlers/app-definition-loader.js +53 -10
- package/handlers/app-handler-helpers.js +14 -3
- package/handlers/backend-utils.js +0 -1
- package/handlers/database-migration-handler.js +64 -45
- package/handlers/middleware/admin-auth.js +53 -0
- package/handlers/routers/HEALTHCHECK.md +170 -135
- package/handlers/routers/admin.js +474 -0
- package/handlers/routers/auth.js +51 -9
- package/handlers/routers/db-migration.handler.js +2 -3
- package/handlers/routers/db-migration.js +102 -66
- package/handlers/routers/docs.js +180 -0
- package/handlers/routers/health.js +84 -283
- package/handlers/routers/integration-defined-routers.js +44 -33
- package/handlers/routers/integration-webhook-routers.js +73 -52
- package/handlers/routers/middleware/loadUser.js +39 -0
- package/handlers/routers/middleware/requireAdmin.js +43 -0
- package/handlers/routers/middleware/requireLoggedInUser.js +19 -0
- package/handlers/routers/user.js +30 -14
- package/handlers/routers/websocket.js +18 -4
- package/handlers/use-cases/check-integrations-health-use-case.js +13 -6
- package/handlers/workers/db-migration.js +74 -35
- package/handlers/workers/integration-defined-workers.js +31 -20
- package/index.js +50 -16
- package/infrastructure/scheduler/index.js +15 -11
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +38 -31
- package/integrations/WEBHOOK-QUICKSTART.md +21 -14
- package/integrations/index.js +4 -0
- package/integrations/integration-base.js +9 -6
- package/integrations/integration-router.js +1828 -157
- package/integrations/options.js +47 -20
- package/integrations/repositories/integration-mapping-repository-factory.js +4 -8
- package/integrations/repositories/integration-repository-documentdb.js +53 -24
- package/integrations/repositories/integration-repository-factory.js +6 -6
- package/integrations/repositories/integration-repository-interface.js +27 -9
- package/integrations/repositories/integration-repository-postgres.js +3 -3
- package/integrations/repositories/process-repository-factory.js +4 -7
- package/integrations/repositories/process-repository-interface.js +9 -5
- package/integrations/repositories/process-repository-mongo.js +7 -3
- package/integrations/repositories/process-repository-postgres.js +2 -2
- package/integrations/tests/doubles/config-capturing-integration.js +9 -8
- package/integrations/tests/doubles/dummy-integration-class.js +81 -8
- package/integrations/tests/doubles/test-integration-repository.js +51 -11
- package/integrations/use-cases/create-integration.js +36 -32
- package/integrations/use-cases/create-process.js +20 -9
- package/integrations/use-cases/delete-integration-for-user.js +5 -4
- package/integrations/use-cases/execute-proxy-request.js +564 -0
- package/integrations/use-cases/get-integration-for-user.js +25 -10
- package/integrations/use-cases/get-integration-instance-by-definition.js +13 -7
- package/integrations/use-cases/get-integrations-for-user.js +4 -1
- package/integrations/use-cases/get-possible-integrations.js +1 -1
- package/integrations/use-cases/get-process.js +5 -6
- package/integrations/use-cases/index.js +3 -1
- package/integrations/use-cases/load-integration-context.js +1 -5
- package/integrations/use-cases/update-process-state.js +10 -8
- package/integrations/utils/map-integration-dto.js +9 -3
- package/jest-setup.js +1 -1
- package/jest-teardown.js +1 -1
- package/lambda/TimeoutCatcher.js +32 -30
- package/lambda/index.js +1 -1
- package/logs/index.js +3 -3
- package/modules/domain/entities/AuthorizationSession.js +131 -0
- package/modules/domain/entities/index.js +10 -0
- package/modules/module.js +4 -9
- package/modules/repositories/authorization-session-repository-factory.js +53 -0
- package/modules/repositories/authorization-session-repository-interface.js +75 -0
- package/modules/repositories/authorization-session-repository-mongo.js +169 -0
- package/modules/repositories/authorization-session-repository-postgres.js +170 -0
- package/modules/repositories/module-repository-documentdb.js +89 -41
- package/modules/repositories/module-repository-factory.js +4 -6
- package/modules/repositories/module-repository-interface.js +13 -0
- package/modules/repositories/module-repository-mongo.js +40 -5
- package/modules/repositories/module-repository-postgres.js +42 -5
- package/modules/repositories/module-repository.js +28 -0
- package/modules/requester/api-key.js +0 -2
- package/modules/requester/basic.js +0 -1
- package/modules/requester/oauth-2.js +3 -14
- package/modules/test/mock-api/definition.js +15 -10
- package/modules/test/mock-api/mocks/hubspot.js +36 -36
- package/modules/tests/doubles/test-module-factory.js +10 -4
- package/modules/tests/doubles/test-module-repository.js +1 -1
- package/modules/use-cases/delete-module-entity.js +23 -0
- package/modules/use-cases/get-authorization-requirements.js +110 -0
- package/modules/use-cases/get-entities-for-user.js +1 -1
- package/modules/use-cases/get-entity-options-by-id.js +8 -6
- package/modules/use-cases/get-module-entity-by-id.js +16 -0
- package/modules/use-cases/get-module.js +12 -9
- package/modules/use-cases/process-authorization-callback.js +12 -6
- package/modules/use-cases/process-authorization-step.js +160 -0
- package/modules/use-cases/refresh-entity-options.js +8 -6
- package/modules/use-cases/start-authorization-session.js +86 -0
- package/modules/use-cases/test-module-auth.js +8 -6
- package/modules/use-cases/update-module-entity.js +27 -0
- package/modules/utils/map-module-dto.js +2 -2
- package/openapi/openapi-spec-generator.js +370 -0
- package/openapi/openapi-v1.yaml +1360 -0
- package/openapi/openapi-v2.yaml +1535 -0
- package/package.json +7 -9
- package/prisma-mongodb/schema.prisma +87 -0
- package/prisma-postgresql/migrations/20260222000000_add_admin_process_and_script_schedule/migration.sql +55 -0
- package/prisma-postgresql/schema.prisma +85 -0
- package/providers/index.js +13 -0
- package/providers/resolve-provider.js +93 -0
- package/queues/index.js +19 -0
- package/queues/providers/index.js +7 -0
- package/queues/providers/netlify-background-provider.js +76 -0
- package/queues/providers/qstash-queue-provider.js +122 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-provider-factory.js +93 -0
- package/queues/queue-provider.js +46 -0
- package/queues/queuer-util.js +33 -25
- package/syncs/repositories/sync-repository-documentdb.js +39 -19
- package/syncs/repositories/sync-repository-factory.js +4 -4
- package/syncs/repositories/sync-repository-interface.js +6 -2
- package/syncs/repositories/sync-repository-postgres.js +19 -13
- package/syncs/sync.js +104 -100
- package/token/repositories/token-repository-factory.js +4 -6
- package/types/assertions/index.d.ts +72 -72
- package/types/core/index.d.ts +98 -53
- package/types/errors/index.d.ts +61 -54
- package/types/eslint-config/index.d.ts +40 -40
- package/types/lambda/index.d.ts +26 -26
- package/types/logs/index.d.ts +4 -4
- package/types/prettier-config/index.d.ts +5 -5
- package/types/test-environment/index.d.ts +14 -14
- package/types/tsconfig.json +95 -93
- package/user/repositories/user-repository-factory.js +4 -4
- package/user/tests/doubles/test-user-repository.js +5 -2
- package/user/use-cases/authenticate-user.js +2 -4
- package/user/use-cases/authenticate-with-shared-secret.js +1 -1
- package/user/use-cases/create-individual-user.js +8 -7
- package/user/use-cases/create-organization-user.js +1 -1
- package/user/use-cases/create-token-for-user-id.js +1 -1
- package/user/use-cases/delete-user.js +47 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +0 -2
- package/user/use-cases/get-user-from-bearer-token.js +7 -3
- package/user/use-cases/get-user-from-x-frigg-headers.js +14 -11
- package/user/use-cases/login-user.js +8 -8
- package/user/user.js +18 -4
- package/utils/backend-path.js +2 -2
- package/utils/index.js +4 -1
- package/websocket/repositories/websocket-connection-repository-documentdb.js +46 -21
- package/websocket/repositories/websocket-connection-repository-factory.js +4 -8
- package/websocket/repositories/websocket-connection-repository-mongo.js +26 -16
- package/websocket/repositories/websocket-connection-repository-postgres.js +26 -15
- package/websocket/repositories/websocket-connection-repository.js +26 -20
- package/websocket/websocket-message-sender-interface.js +38 -0
- package/database/adapters/lambda-invoker.js +0 -97
- package/database/repositories/migration-status-repository-s3.js +0 -137
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
2
|
const {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
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;
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
await sender.send(
|
|
98
|
+
conn.connectionId,
|
|
99
|
+
data,
|
|
100
|
+
process.env.WEBSOCKET_API_ENDPOINT
|
|
101
|
+
);
|
|
95
102
|
} catch (error) {
|
|
96
|
-
if (error
|
|
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 };
|