@friggframework/core 2.0.0-next.103 → 2.0.0-next.104
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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve Migration Via Worker Use Case
|
|
3
|
+
*
|
|
4
|
+
* Resolves a failed Prisma migration (P3009) by invoking the worker Lambda,
|
|
5
|
+
* which has the Prisma CLI installed. Keeps the router Lambda lightweight —
|
|
6
|
+
* same delegation pattern as GetDatabaseStateViaWorkerUseCase.
|
|
7
|
+
*/
|
|
8
|
+
class ResolveMigrationViaWorkerUseCase {
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} dependencies
|
|
11
|
+
* @param {LambdaInvoker} dependencies.lambdaInvoker - Lambda invocation adapter
|
|
12
|
+
* @param {string} dependencies.workerFunctionName - Worker Lambda function name
|
|
13
|
+
*/
|
|
14
|
+
constructor({ lambdaInvoker, workerFunctionName }) {
|
|
15
|
+
if (!lambdaInvoker) {
|
|
16
|
+
throw new Error('lambdaInvoker dependency is required');
|
|
17
|
+
}
|
|
18
|
+
if (!workerFunctionName) {
|
|
19
|
+
throw new Error('workerFunctionName is required');
|
|
20
|
+
}
|
|
21
|
+
this.lambdaInvoker = lambdaInvoker;
|
|
22
|
+
this.workerFunctionName = workerFunctionName;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Object} params
|
|
27
|
+
* @param {string} params.migrationName - Migration to resolve
|
|
28
|
+
* @param {'applied'|'rolled-back'} [params.action] - Resolution mode
|
|
29
|
+
* @param {string} [params.stage] - Deployment stage
|
|
30
|
+
* @returns {Promise<Object>} Worker result body
|
|
31
|
+
*/
|
|
32
|
+
async execute({ migrationName, action = 'applied', stage }) {
|
|
33
|
+
const dbType = process.env.DB_TYPE || 'postgresql';
|
|
34
|
+
|
|
35
|
+
console.log(
|
|
36
|
+
`Invoking worker Lambda to resolve migration "${migrationName}" as ${action}: ${this.workerFunctionName}`
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return this.lambdaInvoker.invoke(this.workerFunctionName, {
|
|
40
|
+
action: 'resolve',
|
|
41
|
+
migrationName,
|
|
42
|
+
resolveAction: action,
|
|
43
|
+
dbType,
|
|
44
|
+
stage,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { ResolveMigrationViaWorkerUseCase };
|
|
@@ -405,14 +405,25 @@ async function runPrismaMigrateResolve(migrationName, action = 'applied', verbos
|
|
|
405
405
|
const [executable, ...executableArgs] = prismaBin.split(' ');
|
|
406
406
|
const fullArgs = [...executableArgs, ...args];
|
|
407
407
|
|
|
408
|
+
let stdout = '';
|
|
409
|
+
let stderr = '';
|
|
408
410
|
const proc = spawn(executable, fullArgs, {
|
|
409
|
-
stdio: 'inherit',
|
|
411
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
410
412
|
env: {
|
|
411
413
|
...process.env,
|
|
412
414
|
PRISMA_HIDE_UPDATE_MESSAGE: '1'
|
|
413
415
|
}
|
|
414
416
|
});
|
|
415
417
|
|
|
418
|
+
proc.stdout.on('data', (data) => {
|
|
419
|
+
stdout += data.toString();
|
|
420
|
+
if (verbose) process.stdout.write(data);
|
|
421
|
+
});
|
|
422
|
+
proc.stderr.on('data', (data) => {
|
|
423
|
+
stderr += data.toString();
|
|
424
|
+
if (verbose) process.stderr.write(data);
|
|
425
|
+
});
|
|
426
|
+
|
|
416
427
|
proc.on('error', (error) => {
|
|
417
428
|
resolve({
|
|
418
429
|
success: false,
|
|
@@ -427,9 +438,12 @@ async function runPrismaMigrateResolve(migrationName, action = 'applied', verbos
|
|
|
427
438
|
output: `Migration ${migrationName} marked as ${action}`
|
|
428
439
|
});
|
|
429
440
|
} else {
|
|
441
|
+
const detail = (stderr || stdout).trim();
|
|
430
442
|
resolve({
|
|
431
443
|
success: false,
|
|
432
|
-
error:
|
|
444
|
+
error: detail
|
|
445
|
+
? `Prisma migrate resolve failed (exit ${code}): ${detail}`
|
|
446
|
+
: `Resolve process exited with code ${code}`
|
|
433
447
|
});
|
|
434
448
|
}
|
|
435
449
|
});
|
|
@@ -31,10 +31,16 @@ const {
|
|
|
31
31
|
ValidationError: GetValidationError,
|
|
32
32
|
NotFoundError,
|
|
33
33
|
} = require('../../database/use-cases/get-migration-status-use-case');
|
|
34
|
-
const {
|
|
34
|
+
const {
|
|
35
|
+
LambdaInvoker,
|
|
36
|
+
LambdaInvocationError,
|
|
37
|
+
} = require('../../database/adapters/lambda-invoker');
|
|
35
38
|
const {
|
|
36
39
|
GetDatabaseStateViaWorkerUseCase,
|
|
37
40
|
} = require('../../database/use-cases/get-database-state-via-worker-use-case');
|
|
41
|
+
const {
|
|
42
|
+
ResolveMigrationViaWorkerUseCase,
|
|
43
|
+
} = require('../../database/use-cases/resolve-migration-via-worker-use-case');
|
|
38
44
|
|
|
39
45
|
const router = Router();
|
|
40
46
|
|
|
@@ -58,6 +64,10 @@ const getDatabaseStateUseCase = new GetDatabaseStateViaWorkerUseCase({
|
|
|
58
64
|
lambdaInvoker,
|
|
59
65
|
workerFunctionName,
|
|
60
66
|
});
|
|
67
|
+
const resolveMigrationUseCase = new ResolveMigrationViaWorkerUseCase({
|
|
68
|
+
lambdaInvoker,
|
|
69
|
+
workerFunctionName,
|
|
70
|
+
});
|
|
61
71
|
|
|
62
72
|
// Apply admin API key validation to all routes (shared middleware)
|
|
63
73
|
router.use(validateAdminApiKey);
|
|
@@ -255,6 +265,13 @@ router.post(
|
|
|
255
265
|
});
|
|
256
266
|
}
|
|
257
267
|
|
|
268
|
+
if (!/^\d{14}_[a-z0-9_]+$/i.test(migrationName)) {
|
|
269
|
+
return res.status(400).json({
|
|
270
|
+
success: false,
|
|
271
|
+
error: 'migrationName is not a valid migration identifier'
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
258
275
|
if (!['applied', 'rolled-back'].includes(action)) {
|
|
259
276
|
return res.status(400).json({
|
|
260
277
|
success: false,
|
|
@@ -262,30 +279,31 @@ router.post(
|
|
|
262
279
|
});
|
|
263
280
|
}
|
|
264
281
|
|
|
265
|
-
|
|
266
|
-
// Import prismaRunner here to avoid circular dependencies
|
|
267
|
-
const prismaRunner = require('../../database/utils/prisma-runner');
|
|
268
|
-
|
|
269
|
-
const result = await prismaRunner.runPrismaMigrateResolve(migrationName, action, true);
|
|
282
|
+
const stage = req.body.stage || process.env.STAGE || 'production';
|
|
270
283
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
success: false,
|
|
274
|
-
error: `Failed to resolve migration: ${result.error}`
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
res.status(200).json({
|
|
279
|
-
success: true,
|
|
280
|
-
message: `Migration ${migrationName} marked as ${action}`,
|
|
284
|
+
try {
|
|
285
|
+
const result = await resolveMigrationUseCase.execute({
|
|
281
286
|
migrationName,
|
|
282
|
-
action
|
|
287
|
+
action,
|
|
288
|
+
stage,
|
|
283
289
|
});
|
|
290
|
+
|
|
291
|
+
res.status(200).json(result);
|
|
284
292
|
} catch (error) {
|
|
285
293
|
console.error('Migration resolve failed:', error);
|
|
294
|
+
if (
|
|
295
|
+
error instanceof LambdaInvocationError &&
|
|
296
|
+
error.statusCode === 400
|
|
297
|
+
) {
|
|
298
|
+
return res.status(400).json({
|
|
299
|
+
success: false,
|
|
300
|
+
error: error.message,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
286
303
|
return res.status(500).json({
|
|
287
304
|
success: false,
|
|
288
|
-
error:
|
|
305
|
+
error: 'Failed to resolve migration',
|
|
306
|
+
details: error.message,
|
|
289
307
|
});
|
|
290
308
|
}
|
|
291
309
|
})
|
|
@@ -188,6 +188,81 @@ exports.handler = async (event, context) => {
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
if (action === 'resolve') {
|
|
192
|
+
const { migrationName, resolveAction = 'applied' } = event;
|
|
193
|
+
console.log(`\n========================================`);
|
|
194
|
+
console.log(
|
|
195
|
+
`Action: resolve (migration=${migrationName}, mode=${resolveAction})`
|
|
196
|
+
);
|
|
197
|
+
console.log(`========================================`);
|
|
198
|
+
|
|
199
|
+
if (!migrationName) {
|
|
200
|
+
return {
|
|
201
|
+
statusCode: 400,
|
|
202
|
+
body: { success: false, error: 'migrationName is required' },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (!/^\d{14}_[a-z0-9_]+$/i.test(migrationName)) {
|
|
206
|
+
return {
|
|
207
|
+
statusCode: 400,
|
|
208
|
+
body: {
|
|
209
|
+
success: false,
|
|
210
|
+
error: 'migrationName is not a valid migration identifier',
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (!['applied', 'rolled-back'].includes(resolveAction)) {
|
|
215
|
+
return {
|
|
216
|
+
statusCode: 400,
|
|
217
|
+
body: {
|
|
218
|
+
success: false,
|
|
219
|
+
error: 'resolveAction must be "applied" or "rolled-back"',
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (dbType !== 'postgresql') {
|
|
224
|
+
return {
|
|
225
|
+
statusCode: 400,
|
|
226
|
+
body: {
|
|
227
|
+
success: false,
|
|
228
|
+
error: `Migration resolve is only supported for postgresql, not "${dbType}"`,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const result = await prismaRunner.runPrismaMigrateResolve(
|
|
235
|
+
migrationName,
|
|
236
|
+
resolveAction,
|
|
237
|
+
true
|
|
238
|
+
);
|
|
239
|
+
if (!result.success) {
|
|
240
|
+
return {
|
|
241
|
+
statusCode: 500,
|
|
242
|
+
body: {
|
|
243
|
+
success: false,
|
|
244
|
+
error: sanitizeError(result.error),
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
statusCode: 200,
|
|
250
|
+
body: {
|
|
251
|
+
success: true,
|
|
252
|
+
message: `Migration ${migrationName} marked as ${resolveAction}`,
|
|
253
|
+
migrationName,
|
|
254
|
+
action: resolveAction,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('❌ Migration resolve failed:', error.message);
|
|
259
|
+
return {
|
|
260
|
+
statusCode: 500,
|
|
261
|
+
body: { success: false, error: sanitizeError(error.message) },
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
191
266
|
// Otherwise, handle migration (existing code)
|
|
192
267
|
console.log(`\n========================================`);
|
|
193
268
|
console.log(`Action: migrate (migrationId=${migrationId || 'new'})`);
|
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-next.
|
|
4
|
+
"version": "2.0.0-next.104",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
50
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
51
|
-
"@friggframework/test": "2.0.0-next.
|
|
49
|
+
"@friggframework/eslint-config": "2.0.0-next.104",
|
|
50
|
+
"@friggframework/prettier-config": "2.0.0-next.104",
|
|
51
|
+
"@friggframework/test": "2.0.0-next.104",
|
|
52
52
|
"@prisma/client": "^6.19.3",
|
|
53
53
|
"@types/lodash": "4.17.15",
|
|
54
54
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -88,5 +88,5 @@
|
|
|
88
88
|
"publishConfig": {
|
|
89
89
|
"access": "public"
|
|
90
90
|
},
|
|
91
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "deb915ccac00c3928ba337a202b500ffbfabe01a"
|
|
92
92
|
}
|