@friggframework/core 2.0.0--canary.461.e58db0a.0 → 2.0.0--canary.461.4f3c330.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.
@@ -0,0 +1,97 @@
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.processRepository - Repository for process data access
17
+ */
18
+ constructor({ processRepository }) {
19
+ if (!processRepository) {
20
+ throw new Error('processRepository dependency is required');
21
+ }
22
+ this.processRepository = processRepository;
23
+ }
24
+
25
+ /**
26
+ * Execute get migration status
27
+ *
28
+ * @param {Object} params
29
+ * @param {string} params.processId - Process ID to retrieve
30
+ * @returns {Promise<Object>} Migration status with process details
31
+ * @throws {NotFoundError} If process not found
32
+ * @throws {Error} If process is not a migration process
33
+ */
34
+ async execute({ processId }) {
35
+ // Validation
36
+ if (!processId) {
37
+ throw new ValidationError('processId is required');
38
+ }
39
+
40
+ if (typeof processId !== 'string') {
41
+ throw new ValidationError('processId must be a string');
42
+ }
43
+
44
+ // Get process from repository
45
+ const process = await this.processRepository.findById(processId);
46
+
47
+ if (!process) {
48
+ throw new NotFoundError(`Migration process not found: ${processId}`);
49
+ }
50
+
51
+ // Verify this is a migration process
52
+ if (process.type !== 'DATABASE_MIGRATION') {
53
+ throw new Error(
54
+ `Process ${processId} is not a migration process (type: ${process.type})`
55
+ );
56
+ }
57
+
58
+ // Format response
59
+ return {
60
+ processId: process.id,
61
+ type: process.type,
62
+ state: process.state,
63
+ context: process.context || {},
64
+ results: process.results || {},
65
+ createdAt: process.createdAt,
66
+ updatedAt: process.updatedAt,
67
+ };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Custom error for validation failures
73
+ */
74
+ class ValidationError extends Error {
75
+ constructor(message) {
76
+ super(message);
77
+ this.name = 'ValidationError';
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Custom error for not found resources
83
+ */
84
+ class NotFoundError extends Error {
85
+ constructor(message) {
86
+ super(message);
87
+ this.name = 'NotFoundError';
88
+ this.statusCode = 404;
89
+ }
90
+ }
91
+
92
+ module.exports = {
93
+ GetMigrationStatusUseCase,
94
+ ValidationError,
95
+ NotFoundError,
96
+ };
97
+
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Tests for GetMigrationStatusUseCase
3
+ */
4
+
5
+ const {
6
+ GetMigrationStatusUseCase,
7
+ ValidationError,
8
+ NotFoundError,
9
+ } = require('./get-migration-status-use-case');
10
+
11
+ describe('GetMigrationStatusUseCase', () => {
12
+ let useCase;
13
+ let mockProcessRepository;
14
+
15
+ beforeEach(() => {
16
+ // Create mock repository
17
+ mockProcessRepository = {
18
+ findById: jest.fn(),
19
+ };
20
+
21
+ // Create use case with mock
22
+ useCase = new GetMigrationStatusUseCase({
23
+ processRepository: mockProcessRepository,
24
+ });
25
+ });
26
+
27
+ afterEach(() => {
28
+ jest.clearAllMocks();
29
+ });
30
+
31
+ describe('constructor', () => {
32
+ it('should throw error if processRepository not provided', () => {
33
+ expect(() => {
34
+ new GetMigrationStatusUseCase({});
35
+ }).toThrow('processRepository dependency is required');
36
+ });
37
+ });
38
+
39
+ describe('execute', () => {
40
+ it('should return migration status for COMPLETED process', async () => {
41
+ const mockProcess = {
42
+ id: 'process-123',
43
+ type: 'DATABASE_MIGRATION',
44
+ state: 'COMPLETED',
45
+ context: {
46
+ dbType: 'postgresql',
47
+ stage: 'production',
48
+ migrationCommand: 'migrate deploy',
49
+ },
50
+ results: {
51
+ success: true,
52
+ duration: '2341ms',
53
+ timestamp: '2025-10-18T10:30:00Z',
54
+ },
55
+ createdAt: new Date('2025-10-18T10:29:55Z'),
56
+ updatedAt: new Date('2025-10-18T10:30:02Z'),
57
+ };
58
+
59
+ mockProcessRepository.findById.mockResolvedValue(mockProcess);
60
+
61
+ const result = await useCase.execute({ processId: 'process-123' });
62
+
63
+ expect(mockProcessRepository.findById).toHaveBeenCalledWith('process-123');
64
+ expect(result).toEqual({
65
+ processId: 'process-123',
66
+ type: 'DATABASE_MIGRATION',
67
+ state: 'COMPLETED',
68
+ context: mockProcess.context,
69
+ results: mockProcess.results,
70
+ createdAt: mockProcess.createdAt,
71
+ updatedAt: mockProcess.updatedAt,
72
+ });
73
+ });
74
+
75
+ it('should return migration status for RUNNING process', async () => {
76
+ const mockProcess = {
77
+ id: 'process-456',
78
+ type: 'DATABASE_MIGRATION',
79
+ state: 'RUNNING',
80
+ context: {
81
+ dbType: 'mongodb',
82
+ stage: 'dev',
83
+ startedAt: '2025-10-18T10:30:00Z',
84
+ },
85
+ results: {},
86
+ createdAt: new Date('2025-10-18T10:29:55Z'),
87
+ updatedAt: new Date('2025-10-18T10:30:00Z'),
88
+ };
89
+
90
+ mockProcessRepository.findById.mockResolvedValue(mockProcess);
91
+
92
+ const result = await useCase.execute({ processId: 'process-456' });
93
+
94
+ expect(result.state).toBe('RUNNING');
95
+ expect(result.context.dbType).toBe('mongodb');
96
+ });
97
+
98
+ it('should return migration status for FAILED process', async () => {
99
+ const mockProcess = {
100
+ id: 'process-789',
101
+ type: 'DATABASE_MIGRATION',
102
+ state: 'FAILED',
103
+ context: {
104
+ dbType: 'postgresql',
105
+ stage: 'production',
106
+ failedAt: '2025-10-18T10:30:00Z',
107
+ },
108
+ results: {
109
+ error: 'Migration failed: syntax error',
110
+ errorType: 'MigrationError',
111
+ },
112
+ createdAt: new Date('2025-10-18T10:29:55Z'),
113
+ updatedAt: new Date('2025-10-18T10:30:00Z'),
114
+ };
115
+
116
+ mockProcessRepository.findById.mockResolvedValue(mockProcess);
117
+
118
+ const result = await useCase.execute({ processId: 'process-789' });
119
+
120
+ expect(result.state).toBe('FAILED');
121
+ expect(result.results.error).toContain('Migration failed');
122
+ });
123
+
124
+ it('should handle empty context and results', async () => {
125
+ const mockProcess = {
126
+ id: 'process-999',
127
+ type: 'DATABASE_MIGRATION',
128
+ state: 'INITIALIZING',
129
+ createdAt: new Date(),
130
+ updatedAt: new Date(),
131
+ };
132
+
133
+ mockProcessRepository.findById.mockResolvedValue(mockProcess);
134
+
135
+ const result = await useCase.execute({ processId: 'process-999' });
136
+
137
+ expect(result.context).toEqual({});
138
+ expect(result.results).toEqual({});
139
+ });
140
+
141
+ it('should throw NotFoundError if process does not exist', async () => {
142
+ mockProcessRepository.findById.mockResolvedValue(null);
143
+
144
+ await expect(
145
+ useCase.execute({ processId: 'nonexistent-123' })
146
+ ).rejects.toThrow(NotFoundError);
147
+
148
+ await expect(
149
+ useCase.execute({ processId: 'nonexistent-123' })
150
+ ).rejects.toThrow('Migration process not found: nonexistent-123');
151
+ });
152
+
153
+ it('should throw error if process is not a migration process', async () => {
154
+ const nonMigrationProcess = {
155
+ id: 'process-999',
156
+ type: 'CRM_SYNC', // Not a migration
157
+ state: 'RUNNING',
158
+ context: {},
159
+ results: {},
160
+ createdAt: new Date(),
161
+ updatedAt: new Date(),
162
+ };
163
+
164
+ mockProcessRepository.findById.mockResolvedValue(nonMigrationProcess);
165
+
166
+ await expect(
167
+ useCase.execute({ processId: 'process-999' })
168
+ ).rejects.toThrow('Process process-999 is not a migration process (type: CRM_SYNC)');
169
+ });
170
+
171
+ it('should throw ValidationError if processId is missing', async () => {
172
+ await expect(
173
+ useCase.execute({})
174
+ ).rejects.toThrow(ValidationError);
175
+
176
+ await expect(
177
+ useCase.execute({})
178
+ ).rejects.toThrow('processId is required');
179
+ });
180
+
181
+ it('should throw ValidationError if processId is not a string', async () => {
182
+ await expect(
183
+ useCase.execute({ processId: 123 })
184
+ ).rejects.toThrow('processId must be a string');
185
+ });
186
+
187
+ it('should handle repository errors', async () => {
188
+ mockProcessRepository.findById.mockRejectedValue(new Error('Database connection failed'));
189
+
190
+ await expect(
191
+ useCase.execute({ processId: 'process-123' })
192
+ ).rejects.toThrow('Database connection failed');
193
+ });
194
+ });
195
+
196
+ describe('NotFoundError', () => {
197
+ it('should have correct properties', () => {
198
+ const error = new NotFoundError('test message');
199
+ expect(error.name).toBe('NotFoundError');
200
+ expect(error.message).toBe('test message');
201
+ expect(error.statusCode).toBe(404);
202
+ expect(error instanceof Error).toBe(true);
203
+ });
204
+ });
205
+
206
+ describe('ValidationError', () => {
207
+ it('should have correct name', () => {
208
+ const error = new ValidationError('test message');
209
+ expect(error.name).toBe('ValidationError');
210
+ expect(error.message).toBe('test message');
211
+ expect(error instanceof Error).toBe(true);
212
+ });
213
+ });
214
+ });
215
+
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Trigger Database Migration Use Case
3
+ *
4
+ * Business logic for triggering async database migrations via SQS queue.
5
+ * Creates a Process record for tracking and sends migration job to queue.
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 orchestration
10
+ * - Use cases call repositories for data access
11
+ * - Use cases delegate infrastructure concerns (SQS) to utilities
12
+ *
13
+ * Flow:
14
+ * 1. Validate migration parameters
15
+ * 2. Create Process record (state: INITIALIZING)
16
+ * 3. Send message to SQS queue (fire-and-forget)
17
+ * 4. Return process info immediately (async pattern)
18
+ */
19
+
20
+ const { QueuerUtil } = require('../../queues/queuer-util');
21
+
22
+ class TriggerDatabaseMigrationUseCase {
23
+ /**
24
+ * @param {Object} dependencies
25
+ * @param {Object} dependencies.processRepository - Repository for process data access
26
+ * @param {Object} [dependencies.queuerUtil] - SQS utility (injectable for testing)
27
+ */
28
+ constructor({ processRepository, queuerUtil = QueuerUtil }) {
29
+ if (!processRepository) {
30
+ throw new Error('processRepository dependency is required');
31
+ }
32
+ this.processRepository = processRepository;
33
+ this.queuerUtil = queuerUtil;
34
+ }
35
+
36
+ /**
37
+ * Execute database migration trigger
38
+ *
39
+ * @param {Object} params
40
+ * @param {string} params.userId - User ID triggering the migration
41
+ * @param {string} params.dbType - Database type ('postgresql' or 'mongodb')
42
+ * @param {string} params.stage - Deployment stage (determines migration command)
43
+ * @returns {Promise<Object>} Process info { success, processId, state, statusUrl, message }
44
+ * @throws {ValidationError} If parameters are invalid
45
+ * @throws {Error} If process creation or queue send fails
46
+ */
47
+ async execute({ userId, dbType, stage }) {
48
+ // Validation
49
+ this._validateParams({ userId, dbType, stage });
50
+
51
+ // Create Process record for tracking
52
+ const migrationProcess = await this.processRepository.create({
53
+ userId,
54
+ integrationId: null, // System operation, not tied to integration
55
+ name: 'database-migration',
56
+ type: 'DATABASE_MIGRATION',
57
+ state: 'INITIALIZING',
58
+ context: {
59
+ dbType,
60
+ stage,
61
+ triggeredAt: new Date().toISOString(),
62
+ },
63
+ results: {},
64
+ });
65
+
66
+ console.log(`Created migration process: ${migrationProcess.id}`);
67
+
68
+ // Get queue URL from environment
69
+ const queueUrl = process.env.DB_MIGRATION_QUEUE_URL;
70
+ if (!queueUrl) {
71
+ throw new Error(
72
+ 'DB_MIGRATION_QUEUE_URL environment variable is not set. ' +
73
+ 'Cannot send migration to queue.'
74
+ );
75
+ }
76
+
77
+ // Send message to SQS queue (async fire-and-forget)
78
+ try {
79
+ await this.queuerUtil.send(
80
+ {
81
+ processId: migrationProcess.id,
82
+ dbType,
83
+ stage,
84
+ },
85
+ queueUrl
86
+ );
87
+
88
+ console.log(`Sent migration job to queue for process: ${migrationProcess.id}`);
89
+ } catch (error) {
90
+ console.error(`Failed to send migration to queue:`, error);
91
+
92
+ // Update process state to FAILED
93
+ await this.processRepository.updateState(
94
+ migrationProcess.id,
95
+ 'FAILED',
96
+ {
97
+ error: 'Failed to queue migration job',
98
+ errorDetails: error.message,
99
+ }
100
+ );
101
+
102
+ throw new Error(
103
+ `Failed to queue migration: ${error.message}`
104
+ );
105
+ }
106
+
107
+ // Return process info immediately (don't wait for migration completion)
108
+ return {
109
+ success: true,
110
+ processId: migrationProcess.id,
111
+ state: migrationProcess.state,
112
+ statusUrl: `/db-migrate/${migrationProcess.id}`,
113
+ message: 'Database migration queued successfully',
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Validate execution parameters
119
+ * @private
120
+ */
121
+ _validateParams({ userId, dbType, stage }) {
122
+ if (!userId) {
123
+ throw new ValidationError('userId is required');
124
+ }
125
+
126
+ if (typeof userId !== 'string') {
127
+ throw new ValidationError('userId must be a string');
128
+ }
129
+
130
+ if (!dbType) {
131
+ throw new ValidationError('dbType is required');
132
+ }
133
+
134
+ if (typeof dbType !== 'string') {
135
+ throw new ValidationError('dbType must be a string');
136
+ }
137
+
138
+ const validDbTypes = ['postgresql', 'mongodb'];
139
+ if (!validDbTypes.includes(dbType)) {
140
+ throw new ValidationError(
141
+ `Invalid dbType: "${dbType}". Must be one of: ${validDbTypes.join(', ')}`
142
+ );
143
+ }
144
+
145
+ if (!stage) {
146
+ throw new ValidationError('stage is required');
147
+ }
148
+
149
+ if (typeof stage !== 'string') {
150
+ throw new ValidationError('stage must be a string');
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Custom error for validation failures
157
+ */
158
+ class ValidationError extends Error {
159
+ constructor(message) {
160
+ super(message);
161
+ this.name = 'ValidationError';
162
+ }
163
+ }
164
+
165
+ module.exports = {
166
+ TriggerDatabaseMigrationUseCase,
167
+ ValidationError,
168
+ };
169
+