@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: `Resolve process exited with code ${code}`
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 { LambdaInvoker } = require('../../database/adapters/lambda-invoker');
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
- try {
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
- if (!result.success) {
272
- return res.status(500).json({
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: error.message
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.103",
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.103",
50
- "@friggframework/prettier-config": "2.0.0-next.103",
51
- "@friggframework/test": "2.0.0-next.103",
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": "9fc434b0c6e1323bd16be165d311b6880a23de12"
91
+ "gitHead": "deb915ccac00c3928ba337a202b500ffbfabe01a"
92
92
  }