@friggframework/core 2.0.0--canary.461.a0527a3.0 → 2.0.0--canary.461.4b9ba0b.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/database/config.js +1 -1
- package/database/repositories/migration-status-repository-s3.js +137 -0
- package/database/repositories/migration-status-repository-s3.test.js +158 -0
- package/database/use-cases/get-migration-status-use-case.js +33 -37
- package/database/use-cases/get-migration-status-use-case.test.js +44 -88
- package/database/use-cases/trigger-database-migration-use-case.js +27 -39
- package/database/use-cases/trigger-database-migration-use-case.test.js +51 -66
- package/handlers/routers/db-migration.js +14 -12
- package/package.json +5 -5
package/database/config.js
CHANGED
|
@@ -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,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Migration Status Repository (S3)
|
|
3
|
+
*
|
|
4
|
+
* Tests S3-based storage for migration status tracking
|
|
5
|
+
* (avoids chicken-and-egg dependency on User/Process tables)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { MigrationStatusRepositoryS3 } = require('./migration-status-repository-s3');
|
|
9
|
+
|
|
10
|
+
describe('MigrationStatusRepositoryS3', () => {
|
|
11
|
+
let repository;
|
|
12
|
+
let mockS3Client;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockS3Client = {
|
|
16
|
+
send: jest.fn(),
|
|
17
|
+
};
|
|
18
|
+
repository = new MigrationStatusRepositoryS3('test-bucket', mockS3Client);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('create()', () => {
|
|
22
|
+
it('should create new migration status record in S3', async () => {
|
|
23
|
+
const migrationData = {
|
|
24
|
+
migrationId: 'migration-123',
|
|
25
|
+
stage: 'dev',
|
|
26
|
+
triggeredBy: 'admin',
|
|
27
|
+
triggeredAt: '2025-10-19T12:00:00Z',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
mockS3Client.send.mockResolvedValue({});
|
|
31
|
+
|
|
32
|
+
const result = await repository.create(migrationData);
|
|
33
|
+
|
|
34
|
+
expect(result.migrationId).toBe('migration-123');
|
|
35
|
+
expect(result.state).toBe('INITIALIZING');
|
|
36
|
+
expect(mockS3Client.send).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should generate UUID if migrationId not provided', async () => {
|
|
40
|
+
const migrationData = {
|
|
41
|
+
stage: 'dev',
|
|
42
|
+
triggeredBy: 'admin',
|
|
43
|
+
triggeredAt: '2025-10-19T12:00:00Z',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
mockS3Client.send.mockResolvedValue({});
|
|
47
|
+
|
|
48
|
+
const result = await repository.create(migrationData);
|
|
49
|
+
|
|
50
|
+
expect(result.migrationId).toMatch(/^[a-f0-9-]{36}$/); // UUID format
|
|
51
|
+
expect(result.state).toBe('INITIALIZING');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should store status at correct S3 key', async () => {
|
|
55
|
+
const migrationData = {
|
|
56
|
+
migrationId: 'migration-123',
|
|
57
|
+
stage: 'dev',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
mockS3Client.send.mockResolvedValue({});
|
|
61
|
+
|
|
62
|
+
await repository.create(migrationData);
|
|
63
|
+
|
|
64
|
+
const putCommand = mockS3Client.send.mock.calls[0][0];
|
|
65
|
+
expect(putCommand.input.Bucket).toBe('test-bucket');
|
|
66
|
+
expect(putCommand.input.Key).toBe('migrations/dev/migration-123.json');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('update()', () => {
|
|
71
|
+
it('should update existing migration status', async () => {
|
|
72
|
+
mockS3Client.send.mockResolvedValue({
|
|
73
|
+
Body: {
|
|
74
|
+
transformToString: () => JSON.stringify({
|
|
75
|
+
migrationId: 'migration-123',
|
|
76
|
+
state: 'INITIALIZING',
|
|
77
|
+
progress: 0,
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const updateData = {
|
|
83
|
+
migrationId: 'migration-123',
|
|
84
|
+
stage: 'dev',
|
|
85
|
+
state: 'RUNNING',
|
|
86
|
+
progress: 50,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await repository.update(updateData);
|
|
90
|
+
|
|
91
|
+
expect(mockS3Client.send).toHaveBeenCalledTimes(2); // GET then PUT
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should merge updates with existing data', async () => {
|
|
95
|
+
mockS3Client.send
|
|
96
|
+
.mockResolvedValueOnce({
|
|
97
|
+
Body: {
|
|
98
|
+
transformToString: () => JSON.stringify({
|
|
99
|
+
migrationId: 'migration-123',
|
|
100
|
+
state: 'INITIALIZING',
|
|
101
|
+
progress: 0,
|
|
102
|
+
triggeredAt: '2025-10-19T12:00:00Z',
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
.mockResolvedValueOnce({});
|
|
107
|
+
|
|
108
|
+
await repository.update({
|
|
109
|
+
migrationId: 'migration-123',
|
|
110
|
+
stage: 'dev',
|
|
111
|
+
state: 'COMPLETED',
|
|
112
|
+
progress: 100,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const putCommand = mockS3Client.send.mock.calls[1][0];
|
|
116
|
+
const storedData = JSON.parse(putCommand.input.Body);
|
|
117
|
+
expect(storedData.triggeredAt).toBe('2025-10-19T12:00:00Z'); // Preserved
|
|
118
|
+
expect(storedData.state).toBe('COMPLETED'); // Updated
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('get()', () => {
|
|
123
|
+
it('should retrieve migration status from S3', async () => {
|
|
124
|
+
const statusData = {
|
|
125
|
+
migrationId: 'migration-123',
|
|
126
|
+
state: 'COMPLETED',
|
|
127
|
+
progress: 100,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
mockS3Client.send.mockResolvedValue({
|
|
131
|
+
Body: {
|
|
132
|
+
transformToString: () => JSON.stringify(statusData),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = await repository.get('migration-123', 'dev');
|
|
137
|
+
|
|
138
|
+
expect(result).toEqual(statusData);
|
|
139
|
+
expect(mockS3Client.send).toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should throw error if migration not found', async () => {
|
|
143
|
+
mockS3Client.send.mockRejectedValue({ name: 'NoSuchKey' });
|
|
144
|
+
|
|
145
|
+
await expect(repository.get('nonexistent', 'dev')).rejects.toThrow(
|
|
146
|
+
'Migration not found'
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('S3 Key Generation', () => {
|
|
152
|
+
it('should use consistent key format', () => {
|
|
153
|
+
const key = repository._buildS3Key('migration-123', 'production');
|
|
154
|
+
expect(key).toBe('migrations/production/migration-123.json');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
@@ -13,58 +13,54 @@
|
|
|
13
13
|
class GetMigrationStatusUseCase {
|
|
14
14
|
/**
|
|
15
15
|
* @param {Object} dependencies
|
|
16
|
-
* @param {Object} dependencies.
|
|
16
|
+
* @param {Object} dependencies.migrationStatusRepository - Repository for migration status (S3)
|
|
17
17
|
*/
|
|
18
|
-
constructor({
|
|
19
|
-
if (!
|
|
20
|
-
throw new Error('
|
|
18
|
+
constructor({ migrationStatusRepository }) {
|
|
19
|
+
if (!migrationStatusRepository) {
|
|
20
|
+
throw new Error('migrationStatusRepository dependency is required');
|
|
21
21
|
}
|
|
22
|
-
this.
|
|
22
|
+
this.migrationStatusRepository = migrationStatusRepository;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Execute get migration status
|
|
27
27
|
*
|
|
28
|
-
* @param {
|
|
29
|
-
* @param {string}
|
|
30
|
-
* @returns {Promise<Object>} Migration status
|
|
31
|
-
* @throws {NotFoundError} If
|
|
32
|
-
* @throws {
|
|
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
33
|
*/
|
|
34
|
-
async execute(
|
|
34
|
+
async execute(migrationId, stage = null) {
|
|
35
35
|
// Validation
|
|
36
|
-
|
|
37
|
-
throw new ValidationError('processId is required');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (typeof processId !== 'string') {
|
|
41
|
-
throw new ValidationError('processId must be a string');
|
|
42
|
-
}
|
|
36
|
+
this._validateParams(migrationId);
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
const process = await this.processRepository.findById(processId);
|
|
38
|
+
const effectiveStage = stage || process.env.STAGE || 'production';
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
|
|
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
49
|
}
|
|
50
|
+
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Validate parameters
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
_validateParams(migrationId) {
|
|
57
|
+
if (!migrationId) {
|
|
58
|
+
throw new ValidationError('migrationId is required');
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
};
|
|
61
|
+
if (typeof migrationId !== 'string') {
|
|
62
|
+
throw new ValidationError('migrationId must be a string');
|
|
63
|
+
}
|
|
68
64
|
}
|
|
69
65
|
}
|
|
70
66
|
|
|
@@ -10,17 +10,17 @@ const {
|
|
|
10
10
|
|
|
11
11
|
describe('GetMigrationStatusUseCase', () => {
|
|
12
12
|
let useCase;
|
|
13
|
-
let
|
|
13
|
+
let mockMigrationStatusRepository;
|
|
14
14
|
|
|
15
15
|
beforeEach(() => {
|
|
16
16
|
// Create mock repository
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
mockMigrationStatusRepository = {
|
|
18
|
+
get: jest.fn(),
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
// Create use case with mock
|
|
22
22
|
useCase = new GetMigrationStatusUseCase({
|
|
23
|
-
|
|
23
|
+
migrationStatusRepository: mockMigrationStatusRepository,
|
|
24
24
|
});
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -29,10 +29,10 @@ describe('GetMigrationStatusUseCase', () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
describe('constructor', () => {
|
|
32
|
-
it('should throw error if
|
|
32
|
+
it('should throw error if migrationStatusRepository not provided', () => {
|
|
33
33
|
expect(() => {
|
|
34
34
|
new GetMigrationStatusUseCase({});
|
|
35
|
-
}).toThrow('
|
|
35
|
+
}).toThrow('migrationStatusRepository dependency is required');
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
|
|
@@ -56,25 +56,17 @@ describe('GetMigrationStatusUseCase', () => {
|
|
|
56
56
|
updatedAt: new Date('2025-10-18T10:30:02Z'),
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
mockMigrationStatusRepository.get.mockResolvedValue(mockProcess);
|
|
60
60
|
|
|
61
|
-
const result = await useCase.execute(
|
|
61
|
+
const result = await useCase.execute('migration-123', 'production');
|
|
62
62
|
|
|
63
|
-
expect(
|
|
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
|
-
});
|
|
63
|
+
expect(mockMigrationStatusRepository.get).toHaveBeenCalledWith('migration-123', 'production');
|
|
64
|
+
expect(result).toEqual(mockProcess); // S3 repository returns full status object
|
|
73
65
|
});
|
|
74
66
|
|
|
75
|
-
it('should return migration status for RUNNING
|
|
67
|
+
it('should return migration status for RUNNING migration', async () => {
|
|
76
68
|
const mockProcess = {
|
|
77
|
-
|
|
69
|
+
migrationId: 'migration-456',
|
|
78
70
|
type: 'DATABASE_MIGRATION',
|
|
79
71
|
state: 'RUNNING',
|
|
80
72
|
context: {
|
|
@@ -87,109 +79,73 @@ describe('GetMigrationStatusUseCase', () => {
|
|
|
87
79
|
updatedAt: new Date('2025-10-18T10:30:00Z'),
|
|
88
80
|
};
|
|
89
81
|
|
|
90
|
-
|
|
82
|
+
mockMigrationStatusRepository.get.mockResolvedValue(mockProcess);
|
|
91
83
|
|
|
92
|
-
const result = await useCase.execute(
|
|
84
|
+
const result = await useCase.execute('migration-456', 'dev');
|
|
93
85
|
|
|
94
86
|
expect(result.state).toBe('RUNNING');
|
|
95
87
|
expect(result.context.dbType).toBe('mongodb');
|
|
96
88
|
});
|
|
97
89
|
|
|
98
|
-
it('should return migration status for FAILED
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
it('should return migration status for FAILED migration', async () => {
|
|
91
|
+
const mockStatus = {
|
|
92
|
+
migrationId: 'migration-789',
|
|
93
|
+
stage: 'production',
|
|
102
94
|
state: 'FAILED',
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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'),
|
|
95
|
+
progress: 0,
|
|
96
|
+
error: 'Migration failed: syntax error',
|
|
97
|
+
triggeredBy: 'admin',
|
|
98
|
+
triggeredAt: '2025-10-18T10:29:55Z',
|
|
99
|
+
completedAt: '2025-10-18T10:30:00Z',
|
|
114
100
|
};
|
|
115
101
|
|
|
116
|
-
|
|
102
|
+
mockMigrationStatusRepository.get.mockResolvedValue(mockStatus);
|
|
117
103
|
|
|
118
|
-
const result = await useCase.execute(
|
|
104
|
+
const result = await useCase.execute('migration-789', 'production');
|
|
119
105
|
|
|
106
|
+
expect(mockMigrationStatusRepository.get).toHaveBeenCalledWith('migration-789', 'production');
|
|
120
107
|
expect(result.state).toBe('FAILED');
|
|
121
|
-
expect(result.
|
|
108
|
+
expect(result.error).toContain('Migration failed');
|
|
122
109
|
});
|
|
123
110
|
|
|
124
|
-
|
|
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);
|
|
111
|
+
// Removed - already covered by "should return minimal migration status"
|
|
134
112
|
|
|
135
|
-
|
|
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);
|
|
113
|
+
it('should throw NotFoundError if migration does not exist', async () => {
|
|
114
|
+
mockMigrationStatusRepository.get.mockRejectedValue(new Error('Migration not found: nonexistent-123'));
|
|
143
115
|
|
|
144
116
|
await expect(
|
|
145
|
-
useCase.execute(
|
|
117
|
+
useCase.execute('nonexistent-123', 'dev')
|
|
146
118
|
).rejects.toThrow(NotFoundError);
|
|
147
119
|
|
|
148
120
|
await expect(
|
|
149
|
-
useCase.execute(
|
|
150
|
-
).rejects.toThrow('Migration
|
|
121
|
+
useCase.execute('nonexistent-123', 'dev')
|
|
122
|
+
).rejects.toThrow('Migration not found');
|
|
151
123
|
});
|
|
152
124
|
|
|
153
|
-
|
|
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
|
-
});
|
|
125
|
+
// Removed: S3 repository only stores migrations, no type validation needed
|
|
170
126
|
|
|
171
|
-
it('should throw ValidationError if
|
|
127
|
+
it('should throw ValidationError if migrationId is missing', async () => {
|
|
172
128
|
await expect(
|
|
173
|
-
useCase.execute(
|
|
129
|
+
useCase.execute(null)
|
|
174
130
|
).rejects.toThrow(ValidationError);
|
|
175
131
|
|
|
176
132
|
await expect(
|
|
177
|
-
useCase.execute(
|
|
178
|
-
).rejects.toThrow('
|
|
133
|
+
useCase.execute(undefined)
|
|
134
|
+
).rejects.toThrow('migrationId is required');
|
|
179
135
|
});
|
|
180
136
|
|
|
181
|
-
it('should throw ValidationError if
|
|
137
|
+
it('should throw ValidationError if migrationId is not a string', async () => {
|
|
182
138
|
await expect(
|
|
183
|
-
useCase.execute(
|
|
184
|
-
).rejects.toThrow('
|
|
139
|
+
useCase.execute(123)
|
|
140
|
+
).rejects.toThrow('migrationId must be a string');
|
|
185
141
|
});
|
|
186
142
|
|
|
187
143
|
it('should handle repository errors', async () => {
|
|
188
|
-
|
|
144
|
+
mockMigrationStatusRepository.get.mockRejectedValue(new Error('S3 connection failed'));
|
|
189
145
|
|
|
190
146
|
await expect(
|
|
191
|
-
useCase.execute(
|
|
192
|
-
).rejects.toThrow('
|
|
147
|
+
useCase.execute('migration-123', 'dev')
|
|
148
|
+
).rejects.toThrow('S3 connection failed');
|
|
193
149
|
});
|
|
194
150
|
});
|
|
195
151
|
|
|
@@ -22,14 +22,14 @@ const { QueuerUtil } = require('../../queues/queuer-util');
|
|
|
22
22
|
class TriggerDatabaseMigrationUseCase {
|
|
23
23
|
/**
|
|
24
24
|
* @param {Object} dependencies
|
|
25
|
-
* @param {Object} dependencies.
|
|
25
|
+
* @param {Object} dependencies.migrationStatusRepository - Repository for migration status (S3)
|
|
26
26
|
* @param {Object} [dependencies.queuerUtil] - SQS utility (injectable for testing)
|
|
27
27
|
*/
|
|
28
|
-
constructor({
|
|
29
|
-
if (!
|
|
30
|
-
throw new Error('
|
|
28
|
+
constructor({ migrationStatusRepository, queuerUtil = QueuerUtil }) {
|
|
29
|
+
if (!migrationStatusRepository) {
|
|
30
|
+
throw new Error('migrationStatusRepository dependency is required');
|
|
31
31
|
}
|
|
32
|
-
this.
|
|
32
|
+
this.migrationStatusRepository = migrationStatusRepository;
|
|
33
33
|
this.queuerUtil = queuerUtil;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -48,22 +48,14 @@ class TriggerDatabaseMigrationUseCase {
|
|
|
48
48
|
// Validation
|
|
49
49
|
this._validateParams({ userId, dbType, stage });
|
|
50
50
|
|
|
51
|
-
// Create
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
type: 'DATABASE_MIGRATION',
|
|
57
|
-
state: 'INITIALIZING',
|
|
58
|
-
context: {
|
|
59
|
-
dbType,
|
|
60
|
-
stage,
|
|
61
|
-
triggeredAt: new Date().toISOString(),
|
|
62
|
-
},
|
|
63
|
-
results: {},
|
|
51
|
+
// Create migration status in S3 (no User table dependency)
|
|
52
|
+
const migrationStatus = await this.migrationStatusRepository.create({
|
|
53
|
+
stage: stage || process.env.STAGE || 'production',
|
|
54
|
+
triggeredBy: userId || 'system',
|
|
55
|
+
triggeredAt: new Date().toISOString(),
|
|
64
56
|
});
|
|
65
57
|
|
|
66
|
-
console.log(`Created migration
|
|
58
|
+
console.log(`Created migration status: ${migrationStatus.migrationId}`);
|
|
67
59
|
|
|
68
60
|
// Get queue URL from environment
|
|
69
61
|
const queueUrl = process.env.DB_MIGRATION_QUEUE_URL;
|
|
@@ -78,38 +70,37 @@ class TriggerDatabaseMigrationUseCase {
|
|
|
78
70
|
try {
|
|
79
71
|
await this.queuerUtil.send(
|
|
80
72
|
{
|
|
81
|
-
|
|
73
|
+
migrationId: migrationStatus.migrationId,
|
|
82
74
|
dbType,
|
|
83
75
|
stage,
|
|
84
76
|
},
|
|
85
77
|
queueUrl
|
|
86
78
|
);
|
|
87
79
|
|
|
88
|
-
console.log(`Sent migration job to queue
|
|
80
|
+
console.log(`Sent migration job to queue: ${migrationStatus.migrationId}`);
|
|
89
81
|
} catch (error) {
|
|
90
82
|
console.error(`Failed to send migration to queue:`, error);
|
|
91
83
|
|
|
92
|
-
// Update
|
|
93
|
-
await this.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
);
|
|
84
|
+
// Update migration status to FAILED
|
|
85
|
+
await this.migrationStatusRepository.update({
|
|
86
|
+
migrationId: migrationStatus.migrationId,
|
|
87
|
+
stage: migrationStatus.stage,
|
|
88
|
+
state: 'FAILED',
|
|
89
|
+
error: `Failed to queue migration: ${error.message}`,
|
|
90
|
+
});
|
|
101
91
|
|
|
102
92
|
throw new Error(
|
|
103
93
|
`Failed to queue migration: ${error.message}`
|
|
104
94
|
);
|
|
105
95
|
}
|
|
106
96
|
|
|
107
|
-
// Return
|
|
97
|
+
// Return migration info immediately (don't wait for migration completion)
|
|
108
98
|
return {
|
|
109
99
|
success: true,
|
|
110
|
-
|
|
111
|
-
state:
|
|
112
|
-
statusUrl: `/db-migrate/${
|
|
100
|
+
migrationId: migrationStatus.migrationId,
|
|
101
|
+
state: migrationStatus.state,
|
|
102
|
+
statusUrl: `/db-migrate/${migrationStatus.migrationId}`,
|
|
103
|
+
s3Key: `migrations/${migrationStatus.stage}/${migrationStatus.migrationId}.json`,
|
|
113
104
|
message: 'Database migration queued successfully',
|
|
114
105
|
};
|
|
115
106
|
}
|
|
@@ -119,11 +110,8 @@ class TriggerDatabaseMigrationUseCase {
|
|
|
119
110
|
* @private
|
|
120
111
|
*/
|
|
121
112
|
_validateParams({ userId, dbType, stage }) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (typeof userId !== 'string') {
|
|
113
|
+
// userId is optional for system migrations
|
|
114
|
+
if (userId && typeof userId !== 'string') {
|
|
127
115
|
throw new ValidationError('userId must be a string');
|
|
128
116
|
}
|
|
129
117
|
|
|
@@ -9,7 +9,7 @@ const {
|
|
|
9
9
|
|
|
10
10
|
describe('TriggerDatabaseMigrationUseCase', () => {
|
|
11
11
|
let useCase;
|
|
12
|
-
let
|
|
12
|
+
let mockMigrationStatusRepository;
|
|
13
13
|
let mockQueuerUtil;
|
|
14
14
|
let originalEnv;
|
|
15
15
|
|
|
@@ -21,23 +21,18 @@ describe('TriggerDatabaseMigrationUseCase', () => {
|
|
|
21
21
|
process.env.DB_MIGRATION_QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123456789/test-queue';
|
|
22
22
|
|
|
23
23
|
// Create mock repository
|
|
24
|
-
|
|
24
|
+
mockMigrationStatusRepository = {
|
|
25
25
|
create: jest.fn().mockResolvedValue({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
integrationId: null,
|
|
29
|
-
name: 'database-migration',
|
|
30
|
-
type: 'DATABASE_MIGRATION',
|
|
26
|
+
migrationId: 'migration-123',
|
|
27
|
+
stage: 'production',
|
|
31
28
|
state: 'INITIALIZING',
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
createdAt: new Date(),
|
|
38
|
-
updatedAt: new Date(),
|
|
29
|
+
progress: 0,
|
|
30
|
+
triggeredBy: 'user-456',
|
|
31
|
+
triggeredAt: new Date().toISOString(),
|
|
32
|
+
createdAt: new Date().toISOString(),
|
|
33
|
+
updatedAt: new Date().toISOString(),
|
|
39
34
|
}),
|
|
40
|
-
|
|
35
|
+
update: jest.fn().mockResolvedValue(true),
|
|
41
36
|
};
|
|
42
37
|
|
|
43
38
|
// Create mock queuer util
|
|
@@ -47,7 +42,7 @@ describe('TriggerDatabaseMigrationUseCase', () => {
|
|
|
47
42
|
|
|
48
43
|
// Create use case with mocks
|
|
49
44
|
useCase = new TriggerDatabaseMigrationUseCase({
|
|
50
|
-
|
|
45
|
+
migrationStatusRepository: mockMigrationStatusRepository,
|
|
51
46
|
queuerUtil: mockQueuerUtil,
|
|
52
47
|
});
|
|
53
48
|
});
|
|
@@ -63,16 +58,16 @@ describe('TriggerDatabaseMigrationUseCase', () => {
|
|
|
63
58
|
});
|
|
64
59
|
|
|
65
60
|
describe('constructor', () => {
|
|
66
|
-
it('should throw error if
|
|
61
|
+
it('should throw error if migrationStatusRepository not provided', () => {
|
|
67
62
|
expect(() => {
|
|
68
63
|
new TriggerDatabaseMigrationUseCase({});
|
|
69
|
-
}).toThrow('
|
|
64
|
+
}).toThrow('migrationStatusRepository dependency is required');
|
|
70
65
|
});
|
|
71
66
|
|
|
72
67
|
it('should accept custom queuerUtil', () => {
|
|
73
68
|
const customQueuer = { send: jest.fn() };
|
|
74
69
|
const instance = new TriggerDatabaseMigrationUseCase({
|
|
75
|
-
|
|
70
|
+
migrationStatusRepository: mockMigrationStatusRepository,
|
|
76
71
|
queuerUtil: customQueuer,
|
|
77
72
|
});
|
|
78
73
|
|
|
@@ -88,24 +83,17 @@ describe('TriggerDatabaseMigrationUseCase', () => {
|
|
|
88
83
|
stage: 'production',
|
|
89
84
|
});
|
|
90
85
|
|
|
91
|
-
// Verify
|
|
92
|
-
expect(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
type: 'DATABASE_MIGRATION',
|
|
97
|
-
state: 'INITIALIZING',
|
|
98
|
-
context: expect.objectContaining({
|
|
99
|
-
dbType: 'postgresql',
|
|
100
|
-
stage: 'production',
|
|
101
|
-
}),
|
|
102
|
-
results: {},
|
|
86
|
+
// Verify migration status creation (S3 repository interface)
|
|
87
|
+
expect(mockMigrationStatusRepository.create).toHaveBeenCalledWith({
|
|
88
|
+
stage: 'production',
|
|
89
|
+
triggeredBy: 'user-456',
|
|
90
|
+
triggeredAt: expect.any(String),
|
|
103
91
|
});
|
|
104
92
|
|
|
105
93
|
// Verify SQS message sent
|
|
106
94
|
expect(mockQueuerUtil.send).toHaveBeenCalledWith(
|
|
107
95
|
{
|
|
108
|
-
|
|
96
|
+
migrationId: 'migration-123',
|
|
109
97
|
dbType: 'postgresql',
|
|
110
98
|
stage: 'production',
|
|
111
99
|
},
|
|
@@ -115,9 +103,10 @@ describe('TriggerDatabaseMigrationUseCase', () => {
|
|
|
115
103
|
// Verify response
|
|
116
104
|
expect(result).toEqual({
|
|
117
105
|
success: true,
|
|
118
|
-
|
|
106
|
+
migrationId: 'migration-123',
|
|
119
107
|
state: 'INITIALIZING',
|
|
120
|
-
statusUrl: '/db-migrate/
|
|
108
|
+
statusUrl: '/db-migrate/migration-123',
|
|
109
|
+
s3Key: expect.stringContaining('migrations/'),
|
|
121
110
|
message: 'Database migration queued successfully',
|
|
122
111
|
});
|
|
123
112
|
});
|
|
@@ -129,30 +118,27 @@ describe('TriggerDatabaseMigrationUseCase', () => {
|
|
|
129
118
|
stage: 'dev',
|
|
130
119
|
});
|
|
131
120
|
|
|
132
|
-
expect(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}),
|
|
138
|
-
})
|
|
139
|
-
);
|
|
121
|
+
expect(mockMigrationStatusRepository.create).toHaveBeenCalledWith({
|
|
122
|
+
stage: 'dev',
|
|
123
|
+
triggeredBy: 'user-456',
|
|
124
|
+
triggeredAt: expect.any(String),
|
|
125
|
+
});
|
|
140
126
|
});
|
|
141
127
|
|
|
142
|
-
it('should
|
|
143
|
-
await
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
})
|
|
148
|
-
).rejects.toThrow(ValidationError);
|
|
128
|
+
it('should allow userId to be omitted (system migrations)', async () => {
|
|
129
|
+
const result = await useCase.execute({
|
|
130
|
+
dbType: 'postgresql',
|
|
131
|
+
stage: 'production',
|
|
132
|
+
});
|
|
149
133
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
134
|
+
// Should use 'system' as triggeredBy when userId not provided
|
|
135
|
+
expect(mockMigrationStatusRepository.create).toHaveBeenCalledWith({
|
|
136
|
+
stage: 'production',
|
|
137
|
+
triggeredBy: 'system',
|
|
138
|
+
triggeredAt: expect.any(String),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(result.success).toBe(true);
|
|
156
142
|
});
|
|
157
143
|
|
|
158
144
|
it('should throw ValidationError if userId is not a string', async () => {
|
|
@@ -226,19 +212,18 @@ describe('TriggerDatabaseMigrationUseCase', () => {
|
|
|
226
212
|
})
|
|
227
213
|
).rejects.toThrow('Failed to queue migration: SQS unavailable');
|
|
228
214
|
|
|
229
|
-
// Verify
|
|
230
|
-
expect(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
error: 'Failed to queue migration
|
|
235
|
-
|
|
236
|
-
}
|
|
215
|
+
// Verify migration status was marked as failed
|
|
216
|
+
expect(mockMigrationStatusRepository.update).toHaveBeenCalledWith(
|
|
217
|
+
expect.objectContaining({
|
|
218
|
+
migrationId: 'migration-123',
|
|
219
|
+
state: 'FAILED',
|
|
220
|
+
error: expect.stringContaining('Failed to queue migration'),
|
|
221
|
+
})
|
|
237
222
|
);
|
|
238
223
|
});
|
|
239
224
|
|
|
240
|
-
it('should handle
|
|
241
|
-
|
|
225
|
+
it('should handle migration status creation failure', async () => {
|
|
226
|
+
mockMigrationStatusRepository.create.mockRejectedValue(new Error('S3 error'));
|
|
242
227
|
|
|
243
228
|
await expect(
|
|
244
229
|
useCase.execute({
|
|
@@ -246,7 +231,7 @@ describe('TriggerDatabaseMigrationUseCase', () => {
|
|
|
246
231
|
dbType: 'postgresql',
|
|
247
232
|
stage: 'production',
|
|
248
233
|
})
|
|
249
|
-
).rejects.toThrow('
|
|
234
|
+
).rejects.toThrow('S3 error');
|
|
250
235
|
|
|
251
236
|
// Should not attempt to send to queue if process creation fails
|
|
252
237
|
expect(mockQueuerUtil.send).not.toHaveBeenCalled();
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const { Router } = require('express');
|
|
19
19
|
const catchAsyncError = require('express-async-handler');
|
|
20
|
-
const {
|
|
20
|
+
const { MigrationStatusRepositoryS3 } = require('../../database/repositories/migration-status-repository-s3');
|
|
21
21
|
const {
|
|
22
22
|
TriggerDatabaseMigrationUseCase,
|
|
23
23
|
ValidationError: TriggerValidationError,
|
|
@@ -31,14 +31,15 @@ const {
|
|
|
31
31
|
const router = Router();
|
|
32
32
|
|
|
33
33
|
// Dependency injection
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
const
|
|
34
|
+
// Use S3 repository to avoid User table dependency (chicken-and-egg problem)
|
|
35
|
+
const bucketName = process.env.S3_BUCKET_NAME || process.env.MIGRATION_STATUS_BUCKET;
|
|
36
|
+
const migrationStatusRepository = new MigrationStatusRepositoryS3(bucketName);
|
|
37
|
+
|
|
37
38
|
const triggerMigrationUseCase = new TriggerDatabaseMigrationUseCase({
|
|
38
|
-
|
|
39
|
+
migrationStatusRepository,
|
|
39
40
|
// Note: QueuerUtil is used directly in the use case (static utility)
|
|
40
41
|
});
|
|
41
|
-
const getStatusUseCase = new GetMigrationStatusUseCase({
|
|
42
|
+
const getStatusUseCase = new GetMigrationStatusUseCase({ migrationStatusRepository });
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* Admin API key validation middleware
|
|
@@ -118,9 +119,9 @@ router.post(
|
|
|
118
119
|
);
|
|
119
120
|
|
|
120
121
|
/**
|
|
121
|
-
* GET /db-migrate/:
|
|
122
|
+
* GET /db-migrate/:migrationId
|
|
122
123
|
*
|
|
123
|
-
* Get migration status by
|
|
124
|
+
* Get migration status by migration ID
|
|
124
125
|
*
|
|
125
126
|
* Response (200 OK):
|
|
126
127
|
* {
|
|
@@ -142,14 +143,15 @@ router.post(
|
|
|
142
143
|
* }
|
|
143
144
|
*/
|
|
144
145
|
router.get(
|
|
145
|
-
'/db-migrate/:
|
|
146
|
+
'/db-migrate/:migrationId',
|
|
146
147
|
catchAsyncError(async (req, res) => {
|
|
147
|
-
const {
|
|
148
|
+
const { migrationId } = req.params;
|
|
149
|
+
const stage = req.query.stage || process.env.STAGE || 'production';
|
|
148
150
|
|
|
149
|
-
console.log(`Migration status request:
|
|
151
|
+
console.log(`Migration status request: migrationId=${migrationId}, stage=${stage}`);
|
|
150
152
|
|
|
151
153
|
try {
|
|
152
|
-
const status = await getStatusUseCase.execute(
|
|
154
|
+
const status = await getStatusUseCase.execute(migrationId, stage);
|
|
153
155
|
|
|
154
156
|
res.status(200).json(status);
|
|
155
157
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.461.
|
|
4
|
+
"version": "2.0.0--canary.461.4b9ba0b.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@friggframework/eslint-config": "2.0.0--canary.461.
|
|
41
|
-
"@friggframework/prettier-config": "2.0.0--canary.461.
|
|
42
|
-
"@friggframework/test": "2.0.0--canary.461.
|
|
40
|
+
"@friggframework/eslint-config": "2.0.0--canary.461.4b9ba0b.0",
|
|
41
|
+
"@friggframework/prettier-config": "2.0.0--canary.461.4b9ba0b.0",
|
|
42
|
+
"@friggframework/test": "2.0.0--canary.461.4b9ba0b.0",
|
|
43
43
|
"@prisma/client": "^6.17.0",
|
|
44
44
|
"@types/lodash": "4.17.15",
|
|
45
45
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"publishConfig": {
|
|
80
80
|
"access": "public"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "4b9ba0be386a8d8c9af0398356f6644d41e89af6"
|
|
83
83
|
}
|