@friggframework/core 2.0.0--canary.627.9cf8622.0 → 2.0.0--canary.627.b5f8805.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,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,28 @@ async function runPrismaMigrateResolve(migrationName, action = 'applied', verbos
|
|
|
405
405
|
const [executable, ...executableArgs] = prismaBin.split(' ');
|
|
406
406
|
const fullArgs = [...executableArgs, ...args];
|
|
407
407
|
|
|
408
|
+
// Pipe stdout/stderr so Prisma's actual failure reason (e.g. P3011
|
|
409
|
+
// "migration not found" from a mistyped name) is returned to the
|
|
410
|
+
// caller — not just left in CloudWatch. Still echo when verbose.
|
|
411
|
+
let stdout = '';
|
|
412
|
+
let stderr = '';
|
|
408
413
|
const proc = spawn(executable, fullArgs, {
|
|
409
|
-
stdio: 'inherit',
|
|
414
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
410
415
|
env: {
|
|
411
416
|
...process.env,
|
|
412
417
|
PRISMA_HIDE_UPDATE_MESSAGE: '1'
|
|
413
418
|
}
|
|
414
419
|
});
|
|
415
420
|
|
|
421
|
+
proc.stdout.on('data', (data) => {
|
|
422
|
+
stdout += data.toString();
|
|
423
|
+
if (verbose) process.stdout.write(data);
|
|
424
|
+
});
|
|
425
|
+
proc.stderr.on('data', (data) => {
|
|
426
|
+
stderr += data.toString();
|
|
427
|
+
if (verbose) process.stderr.write(data);
|
|
428
|
+
});
|
|
429
|
+
|
|
416
430
|
proc.on('error', (error) => {
|
|
417
431
|
resolve({
|
|
418
432
|
success: false,
|
|
@@ -427,9 +441,12 @@ async function runPrismaMigrateResolve(migrationName, action = 'applied', verbos
|
|
|
427
441
|
output: `Migration ${migrationName} marked as ${action}`
|
|
428
442
|
});
|
|
429
443
|
} else {
|
|
444
|
+
const detail = (stderr || stdout).trim();
|
|
430
445
|
resolve({
|
|
431
446
|
success: false,
|
|
432
|
-
error:
|
|
447
|
+
error: detail
|
|
448
|
+
? `Prisma migrate resolve failed (exit ${code}): ${detail}`
|
|
449
|
+
: `Resolve process exited with code ${code}`
|
|
433
450
|
});
|
|
434
451
|
}
|
|
435
452
|
});
|
|
@@ -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,15 @@ router.post(
|
|
|
255
265
|
});
|
|
256
266
|
}
|
|
257
267
|
|
|
268
|
+
// Prisma migration name shape (<14-digit timestamp>_<name>). Rejects
|
|
269
|
+
// values that would be parsed as CLI flags (e.g. "--schema").
|
|
270
|
+
if (!/^\d{14}_[a-z0-9_]+$/i.test(migrationName)) {
|
|
271
|
+
return res.status(400).json({
|
|
272
|
+
success: false,
|
|
273
|
+
error: 'migrationName is not a valid migration identifier'
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
258
277
|
if (!['applied', 'rolled-back'].includes(action)) {
|
|
259
278
|
return res.status(400).json({
|
|
260
279
|
success: false,
|
|
@@ -262,30 +281,34 @@ router.post(
|
|
|
262
281
|
});
|
|
263
282
|
}
|
|
264
283
|
|
|
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);
|
|
284
|
+
const stage = req.body.stage || process.env.STAGE || 'production';
|
|
270
285
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
res.status(200).json({
|
|
279
|
-
success: true,
|
|
280
|
-
message: `Migration ${migrationName} marked as ${action}`,
|
|
286
|
+
try {
|
|
287
|
+
// Delegate to the worker Lambda (which has the Prisma CLI); the
|
|
288
|
+
// router Lambda is packaged without Prisma.
|
|
289
|
+
const result = await resolveMigrationUseCase.execute({
|
|
281
290
|
migrationName,
|
|
282
|
-
action
|
|
291
|
+
action,
|
|
292
|
+
stage,
|
|
283
293
|
});
|
|
294
|
+
|
|
295
|
+
res.status(200).json(result);
|
|
284
296
|
} catch (error) {
|
|
285
297
|
console.error('Migration resolve failed:', error);
|
|
298
|
+
// Surface a worker-side 400 (bad input) as a 400, not a generic 500.
|
|
299
|
+
if (
|
|
300
|
+
error instanceof LambdaInvocationError &&
|
|
301
|
+
error.statusCode === 400
|
|
302
|
+
) {
|
|
303
|
+
return res.status(400).json({
|
|
304
|
+
success: false,
|
|
305
|
+
error: error.message,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
286
308
|
return res.status(500).json({
|
|
287
309
|
success: false,
|
|
288
|
-
error:
|
|
310
|
+
error: 'Failed to resolve migration',
|
|
311
|
+
details: error.message,
|
|
289
312
|
});
|
|
290
313
|
}
|
|
291
314
|
})
|
|
@@ -188,6 +188,85 @@ exports.handler = async (event, context) => {
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// Handle resolve action (P3009 recovery — router delegates here because the
|
|
192
|
+
// worker has the Prisma CLI; the router does not).
|
|
193
|
+
if (action === 'resolve') {
|
|
194
|
+
const { migrationName, resolveAction = 'applied' } = event;
|
|
195
|
+
console.log(`\n========================================`);
|
|
196
|
+
console.log(
|
|
197
|
+
`Action: resolve (migration=${migrationName}, mode=${resolveAction})`
|
|
198
|
+
);
|
|
199
|
+
console.log(`========================================`);
|
|
200
|
+
|
|
201
|
+
if (!migrationName) {
|
|
202
|
+
return {
|
|
203
|
+
statusCode: 400,
|
|
204
|
+
body: { success: false, error: 'migrationName is required' },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// Prisma migration name shape (<14-digit timestamp>_<name>). Rejects
|
|
208
|
+
// values that would be parsed as CLI flags (e.g. "--schema").
|
|
209
|
+
if (!/^\d{14}_[a-z0-9_]+$/i.test(migrationName)) {
|
|
210
|
+
return {
|
|
211
|
+
statusCode: 400,
|
|
212
|
+
body: {
|
|
213
|
+
success: false,
|
|
214
|
+
error: 'migrationName is not a valid migration identifier',
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (!['applied', 'rolled-back'].includes(resolveAction)) {
|
|
219
|
+
return {
|
|
220
|
+
statusCode: 400,
|
|
221
|
+
body: {
|
|
222
|
+
success: false,
|
|
223
|
+
error: 'resolveAction must be "applied" or "rolled-back"',
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (dbType !== 'postgresql') {
|
|
228
|
+
return {
|
|
229
|
+
statusCode: 400,
|
|
230
|
+
body: {
|
|
231
|
+
success: false,
|
|
232
|
+
error: `Migration resolve is only supported for postgresql, not "${dbType}"`,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const result = await prismaRunner.runPrismaMigrateResolve(
|
|
239
|
+
migrationName,
|
|
240
|
+
resolveAction,
|
|
241
|
+
true
|
|
242
|
+
);
|
|
243
|
+
if (!result.success) {
|
|
244
|
+
return {
|
|
245
|
+
statusCode: 500,
|
|
246
|
+
body: {
|
|
247
|
+
success: false,
|
|
248
|
+
error: sanitizeError(result.error),
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
statusCode: 200,
|
|
254
|
+
body: {
|
|
255
|
+
success: true,
|
|
256
|
+
message: `Migration ${migrationName} marked as ${resolveAction}`,
|
|
257
|
+
migrationName,
|
|
258
|
+
action: resolveAction,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('❌ Migration resolve failed:', error.message);
|
|
263
|
+
return {
|
|
264
|
+
statusCode: 500,
|
|
265
|
+
body: { success: false, error: sanitizeError(error.message) },
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
191
270
|
// Otherwise, handle migration (existing code)
|
|
192
271
|
console.log(`\n========================================`);
|
|
193
272
|
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--canary.627.
|
|
4
|
+
"version": "2.0.0--canary.627.b5f8805.0",
|
|
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--canary.627.
|
|
50
|
-
"@friggframework/prettier-config": "2.0.0--canary.627.
|
|
51
|
-
"@friggframework/test": "2.0.0--canary.627.
|
|
49
|
+
"@friggframework/eslint-config": "2.0.0--canary.627.b5f8805.0",
|
|
50
|
+
"@friggframework/prettier-config": "2.0.0--canary.627.b5f8805.0",
|
|
51
|
+
"@friggframework/test": "2.0.0--canary.627.b5f8805.0",
|
|
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": "b5f8805cac1497009ffbc6278e74d1d9d62ca448"
|
|
92
92
|
}
|