@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: `Resolve process exited with code ${code}`
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 { 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,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
- 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);
284
+ const stage = req.body.stage || process.env.STAGE || 'production';
270
285
 
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}`,
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: error.message
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.9cf8622.0",
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.9cf8622.0",
50
- "@friggframework/prettier-config": "2.0.0--canary.627.9cf8622.0",
51
- "@friggframework/test": "2.0.0--canary.627.9cf8622.0",
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": "9cf8622beb42a7a630438459adc973dccbc54c3e"
91
+ "gitHead": "b5f8805cac1497009ffbc6278e74d1d9d62ca448"
92
92
  }