@friggframework/devtools 2.0.0--canary.461.849e166.0 → 2.0.0--canary.474.aa465e4.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.
Files changed (32) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/domains/database/aurora-builder.js +234 -57
  3. package/infrastructure/domains/database/aurora-builder.test.js +7 -2
  4. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  5. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  6. package/infrastructure/domains/database/migration-builder.js +256 -215
  7. package/infrastructure/domains/database/migration-builder.test.js +5 -111
  8. package/infrastructure/domains/database/migration-resolver.js +163 -0
  9. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  10. package/infrastructure/domains/integration/integration-builder.js +258 -84
  11. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  12. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  13. package/infrastructure/domains/networking/vpc-builder.js +856 -135
  14. package/infrastructure/domains/networking/vpc-builder.test.js +10 -6
  15. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  16. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  17. package/infrastructure/domains/security/kms-builder.js +179 -22
  18. package/infrastructure/domains/security/kms-resolver.js +96 -0
  19. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  20. package/infrastructure/domains/shared/base-resolver.js +186 -0
  21. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  22. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  23. package/infrastructure/domains/shared/cloudformation-discovery.test.js +26 -1
  24. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  25. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  26. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  27. package/infrastructure/domains/shared/types/index.js +46 -0
  28. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  29. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  30. package/package.json +6 -6
  31. package/infrastructure/REFACTOR.md +0 -532
  32. package/infrastructure/TRANSFORMATION-VISUAL.md +0 -239
@@ -14,6 +14,8 @@
14
14
  */
15
15
 
16
16
  const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
17
+ const { MigrationResourceResolver } = require('./migration-resolver');
18
+ const { createEmptyDiscoveryResult, ResourceOwnership } = require('../shared/types');
17
19
 
18
20
  class MigrationBuilder extends InfrastructureBuilder {
19
21
  constructor() {
@@ -47,18 +49,190 @@ class MigrationBuilder extends InfrastructureBuilder {
47
49
  }
48
50
 
49
51
  /**
50
- * Build migration infrastructure
52
+ * Build migration infrastructure using ownership-based architecture
51
53
  */
52
54
  async build(appDefinition, discoveredResources) {
53
55
  console.log(`\n[${this.name}] Configuring database migration infrastructure...`);
54
56
 
57
+ // Backwards compatibility: Translate old schema to new ownership schema
58
+ appDefinition = this.translateLegacyConfig(appDefinition, discoveredResources);
59
+
55
60
  const result = {
56
61
  resources: {},
57
- functions: {},
58
62
  iamStatements: [],
59
63
  environment: {},
60
64
  };
61
65
 
66
+ // Get structured discovery result
67
+ const discovery = discoveredResources._structured || this.convertFlatDiscoveryToStructured(discoveredResources, appDefinition);
68
+
69
+ // Use MigrationResourceResolver to make ownership decisions
70
+ const resolver = new MigrationResourceResolver();
71
+ const decisions = resolver.resolveAll(appDefinition, discovery);
72
+
73
+ console.log('\n 📋 Resource Ownership Decisions:');
74
+ console.log(` Bucket: ${decisions.bucket.ownership} - ${decisions.bucket.reason}`);
75
+ console.log(` Queue: ${decisions.queue.ownership} - ${decisions.queue.reason}`);
76
+
77
+ // Build resources based on ownership decisions
78
+ await this.buildFromDecisions(decisions, appDefinition, discoveredResources, result);
79
+
80
+ console.log(`[${this.name}] ✅ Migration infrastructure configuration completed`);
81
+ return result;
82
+ }
83
+
84
+ /**
85
+ * Convert flat discovery to structured discovery
86
+ * Provides backwards compatibility for tests
87
+ */
88
+ convertFlatDiscoveryToStructured(flatDiscovery, appDefinition = {}) {
89
+ const discovery = createEmptyDiscoveryResult();
90
+
91
+ if (!flatDiscovery) {
92
+ return discovery;
93
+ }
94
+
95
+ // Check if resources are from CloudFormation stack
96
+ const isManagedIsolated = appDefinition.managementMode === 'managed' &&
97
+ (appDefinition.vpcIsolation === 'isolated' || !appDefinition.vpcIsolation);
98
+ const hasExistingStackResources = isManagedIsolated &&
99
+ (flatDiscovery.migrationStatusBucket || flatDiscovery.migrationQueueUrl);
100
+
101
+ if (flatDiscovery.fromCloudFormationStack || hasExistingStackResources) {
102
+ discovery.fromCloudFormation = true;
103
+ discovery.stackName = flatDiscovery.stackName || 'assumed-stack';
104
+
105
+ // Add stack-managed resources
106
+ let existingLogicalIds = flatDiscovery.existingLogicalIds || [];
107
+
108
+ // Infer logical IDs from physical IDs if needed
109
+ if (hasExistingStackResources && existingLogicalIds.length === 0) {
110
+ if (flatDiscovery.migrationStatusBucket) existingLogicalIds.push('FriggMigrationStatusBucket');
111
+ if (flatDiscovery.migrationQueueUrl) existingLogicalIds.push('DbMigrationQueue');
112
+ }
113
+
114
+ existingLogicalIds.forEach(logicalId => {
115
+ let resourceType = '';
116
+ let physicalId = '';
117
+
118
+ if (logicalId === 'FriggMigrationStatusBucket') {
119
+ resourceType = 'AWS::S3::Bucket';
120
+ physicalId = flatDiscovery.migrationStatusBucket;
121
+ } else if (logicalId === 'DbMigrationQueue') {
122
+ resourceType = 'AWS::SQS::Queue';
123
+ physicalId = flatDiscovery.migrationQueueUrl;
124
+ }
125
+
126
+ if (physicalId && typeof physicalId === 'string') {
127
+ discovery.stackManaged.push({
128
+ logicalId,
129
+ physicalId,
130
+ resourceType
131
+ });
132
+ }
133
+ });
134
+ } else {
135
+ // Resources discovered from AWS API (external)
136
+ if (flatDiscovery.migrationStatusBucket && typeof flatDiscovery.migrationStatusBucket === 'string') {
137
+ discovery.external.push({
138
+ physicalId: flatDiscovery.migrationStatusBucket,
139
+ resourceType: 'AWS::S3::Bucket',
140
+ source: 'aws-discovery'
141
+ });
142
+ }
143
+
144
+ if (flatDiscovery.migrationQueueUrl && typeof flatDiscovery.migrationQueueUrl === 'string') {
145
+ discovery.external.push({
146
+ physicalId: flatDiscovery.migrationQueueUrl,
147
+ resourceType: 'AWS::SQS::Queue',
148
+ source: 'aws-discovery'
149
+ });
150
+ }
151
+ }
152
+
153
+ return discovery;
154
+ }
155
+
156
+ /**
157
+ * Translate legacy configuration to ownership-based configuration
158
+ * Provides backwards compatibility
159
+ */
160
+ translateLegacyConfig(appDefinition, discoveredResources) {
161
+ // If already using ownership schema, return as-is
162
+ if (appDefinition.migration?.ownership) {
163
+ return appDefinition;
164
+ }
165
+
166
+ const translated = JSON.parse(JSON.stringify(appDefinition));
167
+
168
+ // Initialize ownership sections
169
+ if (!translated.migration) translated.migration = {};
170
+ if (!translated.migration.ownership) {
171
+ translated.migration.ownership = {};
172
+ }
173
+
174
+ // Handle top-level managementMode
175
+ const globalMode = appDefinition.managementMode || 'discover';
176
+ const vpcIsolation = appDefinition.vpcIsolation || 'shared';
177
+
178
+ if (globalMode === 'managed') {
179
+ if (vpcIsolation === 'isolated') {
180
+ const hasStackResources = discoveredResources?.migrationStatusBucket ||
181
+ discoveredResources?.migrationQueueUrl;
182
+
183
+ if (hasStackResources) {
184
+ translated.migration.ownership.bucket = 'auto';
185
+ translated.migration.ownership.queue = 'auto';
186
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → stack has migration resources, reusing`);
187
+ } else {
188
+ translated.migration.ownership.bucket = 'stack';
189
+ translated.migration.ownership.queue = 'stack';
190
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → no stack migration resources, creating new`);
191
+ }
192
+ } else {
193
+ translated.migration.ownership.bucket = 'auto';
194
+ translated.migration.ownership.queue = 'auto';
195
+ console.log(` managementMode='managed' + vpcIsolation='shared' → discovering migration resources`);
196
+ }
197
+ } else {
198
+ // Default to creating resources (current behavior)
199
+ translated.migration.ownership.bucket = 'stack';
200
+ translated.migration.ownership.queue = 'stack';
201
+ }
202
+
203
+ return translated;
204
+ }
205
+
206
+ /**
207
+ * Build migration resources based on ownership decisions
208
+ */
209
+ async buildFromDecisions(decisions, appDefinition, discoveredResources, result) {
210
+ // Determine if we need to create resources or use existing ones
211
+ const shouldCreateBucket = decisions.bucket.ownership === ResourceOwnership.STACK;
212
+ const shouldCreateQueue = decisions.queue.ownership === ResourceOwnership.STACK;
213
+
214
+ if (shouldCreateBucket && shouldCreateQueue && !decisions.bucket.physicalId && !decisions.queue.physicalId) {
215
+ // Create all new migration infrastructure
216
+ console.log(' → Creating new migration infrastructure in stack');
217
+ await this.createMigrationInfrastructure(appDefinition, result);
218
+ } else if ((decisions.bucket.ownership === ResourceOwnership.STACK && decisions.bucket.physicalId) ||
219
+ (decisions.queue.ownership === ResourceOwnership.STACK && decisions.queue.physicalId)) {
220
+ // Resources exist in stack - add definitions (CloudFormation idempotency)
221
+ console.log(' → Adding migration definitions to template (existing in stack)');
222
+ await this.createMigrationInfrastructure(appDefinition, result);
223
+ } else {
224
+ // Use external resources
225
+ console.log(' → Using external migration resources');
226
+ await this.useExternalMigrationResources(decisions, appDefinition, result);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Create migration infrastructure CloudFormation resources
232
+ * Only creates S3 bucket and SQS queue - Lambda functions are defined in serverless.yml
233
+ */
234
+ async createMigrationInfrastructure(appDefinition, result) {
235
+
62
236
  // Create S3 bucket for migration status tracking
63
237
  result.resources.FriggMigrationStatusBucket = {
64
238
  Type: 'AWS::S3::Bucket',
@@ -110,198 +284,7 @@ class MigrationBuilder extends InfrastructureBuilder {
110
284
 
111
285
  console.log(' ✓ Created DbMigrationQueue resource');
112
286
 
113
- // Package configuration for migration WORKER (needs Prisma CLI with WASM)
114
- const migrationWorkerPackageConfig = {
115
- individually: true,
116
- include: [
117
- // Explicitly include Prisma CLI and WASM files (needed for migrate commands)
118
- 'node_modules/prisma/**',
119
- 'node_modules/.bin/prisma',
120
- ],
121
- exclude: [
122
- // Exclude Prisma runtime client - it's in the Lambda Layer
123
- 'node_modules/@prisma/client/**',
124
- 'node_modules/.prisma/**',
125
- 'node_modules/@friggframework/core/generated/**',
126
- // But KEEP node_modules/prisma/** (the CLI with WASM)
127
-
128
- // Same base exclusions as router
129
- 'node_modules/**/node_modules/**',
130
- 'node_modules/aws-sdk/**',
131
- 'node_modules/@aws-sdk/**',
132
- 'node_modules/esbuild/**',
133
- 'node_modules/@esbuild/**',
134
- 'node_modules/typescript/**',
135
- 'node_modules/webpack/**',
136
- 'node_modules/osls/**',
137
- 'node_modules/serverless-esbuild/**',
138
- 'node_modules/serverless-jetpack/**',
139
- 'node_modules/serverless-offline/**',
140
- 'node_modules/serverless-offline-sqs/**',
141
- 'node_modules/serverless-dotenv-plugin/**',
142
- 'node_modules/serverless-kms-grants/**',
143
- 'node_modules/@friggframework/test/**',
144
- 'node_modules/@friggframework/eslint-config/**',
145
- 'node_modules/@friggframework/prettier-config/**',
146
- 'node_modules/@friggframework/devtools/**',
147
- 'node_modules/@friggframework/serverless-plugin/**',
148
- 'node_modules/jest/**',
149
- 'node_modules/prettier/**',
150
- 'node_modules/eslint/**',
151
- 'node_modules/@friggframework/core/generated/prisma-mongodb/**',
152
- 'node_modules/@friggframework/core/integrations/**',
153
- 'node_modules/@friggframework/core/user/**',
154
- '**/query-engine-darwin*',
155
- '**/schema-engine-darwin*',
156
- '**/libquery_engine-darwin*',
157
- '**/*-darwin-arm64*',
158
- '**/*-darwin*',
159
- // Note: Migration worker DOES need Prisma CLI WASM files (for migrate deploy)
160
- // Only exclude runtime engine WASM (query engine internals)
161
- '**/runtime/*.wasm',
162
- // Additional size optimizations for worker
163
- '**/*.map', // Source maps not needed in production
164
- '**/*.md', // Documentation
165
- '**/examples/**',
166
- '**/docs/**',
167
- '**/*.d.ts', // TypeScript declarations
168
- 'src/**',
169
- 'test/**',
170
- 'layers/**',
171
- 'coverage/**',
172
- 'deploy.log',
173
- '.env.backup',
174
- 'docker-compose.yml',
175
- 'jest.config.js',
176
- 'jest.unit.config.js',
177
- 'package-lock.json',
178
- '**/*.test.js',
179
- '**/*.spec.js',
180
- '**/.claude-flow/**',
181
- '**/.swarm/**',
182
- ],
183
- };
184
-
185
- // Package configuration for migration ROUTER (doesn't need Prisma CLI)
186
- const migrationRouterPackageConfig = {
187
- individually: true,
188
- exclude: [
189
- // Router doesn't access database - exclude ALL Prisma
190
- 'node_modules/prisma/**', // Prisma CLI with engines (54MB!)
191
- 'node_modules/@prisma/**', // Prisma engines
192
- 'node_modules/.prisma/**',
193
- 'node_modules/@friggframework/core/generated/**', // Generated clients
194
-
195
- // Base exclusions
196
- 'node_modules/**/node_modules/**',
197
- 'node_modules/aws-sdk/**',
198
- 'node_modules/@aws-sdk/**',
199
- 'node_modules/esbuild/**',
200
- 'node_modules/@esbuild/**',
201
- 'node_modules/typescript/**',
202
- 'node_modules/webpack/**',
203
- 'node_modules/osls/**',
204
- 'node_modules/serverless-esbuild/**',
205
- 'node_modules/serverless-jetpack/**',
206
- 'node_modules/serverless-offline/**',
207
- 'node_modules/serverless-offline-sqs/**',
208
- 'node_modules/serverless-dotenv-plugin/**',
209
- 'node_modules/serverless-kms-grants/**',
210
- 'node_modules/@friggframework/test/**',
211
- 'node_modules/@friggframework/eslint-config/**',
212
- 'node_modules/@friggframework/prettier-config/**',
213
- 'node_modules/@friggframework/devtools/**',
214
- 'node_modules/@friggframework/serverless-plugin/**',
215
- 'node_modules/jest/**',
216
- 'node_modules/prettier/**',
217
- 'node_modules/eslint/**',
218
- 'node_modules/@friggframework/core/generated/prisma-mongodb/**',
219
- // Note: DO NOT exclude integrations/** - migration router needs process-repository-factory
220
- 'node_modules/@friggframework/core/user/**',
221
- // Note: DO NOT exclude handlers/routers/** or handlers/workers/** - migration functions need them!
222
- '**/query-engine-darwin*',
223
- '**/schema-engine-darwin*',
224
- '**/libquery_engine-darwin*',
225
- '**/*-darwin-arm64*',
226
- '**/*-darwin*',
227
- // Router doesn't run migrations - exclude ALL WASM files
228
- '**/runtime/*.wasm',
229
- '**/*.wasm*', // Exclude all WASM (Prisma CLI + query engine)
230
- // Additional size optimizations for router
231
- '**/*.map', // Source maps not needed in production
232
- '**/*.md', // Documentation
233
- '**/test/**',
234
- '**/tests/**',
235
- '**/__tests__/**',
236
- '**/examples/**',
237
- '**/docs/**',
238
- '**/*.d.ts', // TypeScript declarations
239
- 'src/**',
240
- 'test/**',
241
- 'layers/**',
242
- 'coverage/**',
243
- 'deploy.log',
244
- '.env.backup',
245
- 'docker-compose.yml',
246
- 'jest.config.js',
247
- 'jest.unit.config.js',
248
- 'package-lock.json',
249
- '**/*.test.js',
250
- '**/*.spec.js',
251
- '**/.claude-flow/**',
252
- '**/.swarm/**',
253
- ],
254
- };
255
-
256
- // Create migration worker Lambda (triggered by SQS)
257
- result.functions.dbMigrationWorker = {
258
- handler: 'node_modules/@friggframework/core/handlers/workers/db-migration.handler',
259
- layers: [{ Ref: 'PrismaLambdaLayer' }], // Use layer for Prisma client runtime
260
- skipEsbuild: true,
261
- timeout: 900, // 15 minutes for long migrations
262
- memorySize: 1024, // Extra memory for Prisma operations
263
- reservedConcurrency: 1, // Process one migration at a time (critical for safety)
264
- description: 'Database migration worker (triggered by SQS queue)',
265
- package: migrationWorkerPackageConfig,
266
- environment: {
267
- // Ensure migration functions get DATABASE_URL from provider.environment
268
- // Note: Serverless will merge this with provider.environment
269
- },
270
- events: [
271
- {
272
- sqs: {
273
- arn: { 'Fn::GetAtt': ['DbMigrationQueue', 'Arn'] },
274
- batchSize: 1, // Process one migration at a time
275
- },
276
- },
277
- ],
278
- };
279
-
280
- console.log(' ✓ Created dbMigrationWorker function');
281
-
282
- // Create migration router Lambda (HTTP API)
283
- result.functions.dbMigrationRouter = {
284
- handler: 'node_modules/@friggframework/core/handlers/routers/db-migration.handler',
285
- // No Prisma layer needed - router doesn't access database
286
- skipEsbuild: true,
287
- timeout: 30, // Router just queues jobs, doesn't run migrations
288
- memorySize: 512,
289
- description: 'Database migration HTTP API (POST to trigger, GET to check status)',
290
- package: migrationRouterPackageConfig,
291
- environment: {
292
- // Ensure migration functions get DATABASE_URL from provider.environment
293
- // Note: Serverless will merge this with provider.environment
294
- },
295
- events: [
296
- { httpApi: { path: '/db-migrate/status', method: 'GET' } },
297
- { httpApi: { path: '/db-migrate', method: 'POST' } },
298
- { httpApi: { path: '/db-migrate/{processId}', method: 'GET' } },
299
- ],
300
- };
301
-
302
- console.log(' ✓ Created dbMigrationRouter function');
303
-
304
- // Add S3 bucket name to environment (for migration status tracking)
287
+ // Add S3 bucket name to environment (for migration Lambda functions)
305
288
  result.environment.S3_BUCKET_NAME = { Ref: 'FriggMigrationStatusBucket' };
306
289
  result.environment.MIGRATION_STATUS_BUCKET = { Ref: 'FriggMigrationStatusBucket' };
307
290
 
@@ -309,23 +292,11 @@ class MigrationBuilder extends InfrastructureBuilder {
309
292
  result.environment.DB_MIGRATION_QUEUE_URL = { Ref: 'DbMigrationQueue' };
310
293
 
311
294
  // Hardcode DB_TYPE for PostgreSQL-only migrations
312
- // Avoids Prisma needing to load app definition to determine database type
313
295
  result.environment.DB_TYPE = 'postgresql';
314
296
 
315
297
  console.log(' ✓ Added S3_BUCKET_NAME, DB_MIGRATION_QUEUE_URL, and DB_TYPE environment variables');
316
298
 
317
- // Add worker function name to router environment (for Lambda invocation)
318
- // Router needs this to invoke worker for database state checks
319
- if (!result.functions.dbMigrationRouter.environment) {
320
- result.functions.dbMigrationRouter.environment = {};
321
- }
322
- result.functions.dbMigrationRouter.environment.WORKER_FUNCTION_NAME = {
323
- Ref: 'DbMigrationWorkerLambdaFunction',
324
- };
325
-
326
- console.log(' ✓ Added WORKER_FUNCTION_NAME environment variable to router');
327
-
328
- // Add IAM permissions for SQS
299
+ // Add IAM permissions for SQS (for Lambda functions)
329
300
  result.iamStatements.push({
330
301
  Effect: 'Allow',
331
302
  Action: [
@@ -339,9 +310,6 @@ class MigrationBuilder extends InfrastructureBuilder {
339
310
  console.log(' ✓ Added SQS IAM permissions');
340
311
 
341
312
  // Add IAM permissions for S3 (migration status storage)
342
- // Migration functions need to read/write migration status in S3
343
- // to avoid chicken-and-egg dependency on User/Process tables
344
-
345
313
  // Object-level permissions (put, get, delete)
346
314
  result.iamStatements.push({
347
315
  Effect: 'Allow',
@@ -361,7 +329,7 @@ class MigrationBuilder extends InfrastructureBuilder {
361
329
  },
362
330
  });
363
331
 
364
- // Bucket-level permissions (list objects, needed to check if migration status exists)
332
+ // Bucket-level permissions (list objects)
365
333
  result.iamStatements.push({
366
334
  Effect: 'Allow',
367
335
  Action: ['s3:ListBucket'],
@@ -371,8 +339,6 @@ class MigrationBuilder extends InfrastructureBuilder {
371
339
  console.log(' ✓ Added S3 IAM permissions for migration status tracking');
372
340
 
373
341
  // Add IAM permission for router to invoke worker Lambda
374
- // Router invokes worker for database state checks (keeps router lightweight)
375
- // Use Fn::Sub to avoid circular dependency (IAM role → Lambda → IAM role)
376
342
  result.iamStatements.push({
377
343
  Effect: 'Allow',
378
344
  Action: ['lambda:InvokeFunction'],
@@ -382,9 +348,84 @@ class MigrationBuilder extends InfrastructureBuilder {
382
348
  });
383
349
 
384
350
  console.log(' ✓ Added Lambda invocation permissions for router → worker');
351
+ }
385
352
 
386
- console.log(`[${this.name}] ✅ Migration infrastructure configuration completed`);
387
- return result;
353
+ /**
354
+ * Use external migration resources (S3 bucket and SQS queue)
355
+ * Only references external resources - Lambda functions are defined in serverless.yml
356
+ */
357
+ async useExternalMigrationResources(decisions, appDefinition, result) {
358
+ // Reference external bucket
359
+ const bucketName = decisions.bucket.physicalId;
360
+ if (!bucketName) {
361
+ throw new Error('External bucket specified but no migrationStatusBucket discovered');
362
+ }
363
+
364
+ // Reference external queue
365
+ const queueUrl = decisions.queue.physicalId;
366
+ if (!queueUrl) {
367
+ throw new Error('External queue specified but no migrationQueueUrl discovered');
368
+ }
369
+
370
+ console.log(` ✓ Using external S3 bucket: ${bucketName}`);
371
+ console.log(` ✓ Using external SQS queue: ${queueUrl}`);
372
+
373
+ // Extract queue ARN from queue URL for IAM permissions
374
+ const queueArn = queueUrl.replace('https://sqs.', 'arn:aws:sqs:')
375
+ .replace('.amazonaws.com/', ':')
376
+ .replace(/\//g, ':');
377
+
378
+ // Add environment variables (using external resource names/URLs)
379
+ result.environment.S3_BUCKET_NAME = bucketName;
380
+ result.environment.MIGRATION_STATUS_BUCKET = bucketName;
381
+ result.environment.DB_MIGRATION_QUEUE_URL = queueUrl;
382
+ result.environment.DB_TYPE = 'postgresql';
383
+
384
+ console.log(' ✓ Added S3_BUCKET_NAME, DB_MIGRATION_QUEUE_URL, and DB_TYPE environment variables');
385
+
386
+ // Add IAM permissions for external SQS queue
387
+ result.iamStatements.push({
388
+ Effect: 'Allow',
389
+ Action: [
390
+ 'sqs:SendMessage',
391
+ 'sqs:GetQueueUrl',
392
+ 'sqs:GetQueueAttributes',
393
+ ],
394
+ Resource: queueArn,
395
+ });
396
+
397
+ console.log(' ✓ Added SQS IAM permissions');
398
+
399
+ // Add IAM permissions for external S3 bucket
400
+ const bucketArn = `arn:aws:s3:::${bucketName}`;
401
+ result.iamStatements.push({
402
+ Effect: 'Allow',
403
+ Action: [
404
+ 's3:PutObject',
405
+ 's3:GetObject',
406
+ 's3:DeleteObject',
407
+ ],
408
+ Resource: `${bucketArn}/migrations/*`,
409
+ });
410
+
411
+ result.iamStatements.push({
412
+ Effect: 'Allow',
413
+ Action: ['s3:ListBucket'],
414
+ Resource: bucketArn,
415
+ });
416
+
417
+ console.log(' ✓ Added S3 IAM permissions for migration status tracking');
418
+
419
+ // Add IAM permission for router to invoke worker Lambda
420
+ result.iamStatements.push({
421
+ Effect: 'Allow',
422
+ Action: ['lambda:InvokeFunction'],
423
+ Resource: {
424
+ 'Fn::Sub': 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AWS::StackName}-dbMigrationWorker',
425
+ },
426
+ });
427
+
428
+ console.log(' ✓ Added Lambda invocation permissions for router → worker');
388
429
  }
389
430
  }
390
431
 
@@ -120,7 +120,7 @@ describe('MigrationBuilder', () => {
120
120
  expect(result.resources.DbMigrationQueue.Properties.VisibilityTimeout).toBe(900);
121
121
  });
122
122
 
123
- it('should create migration worker function', async () => {
123
+ it('should create S3 migration status bucket', async () => {
124
124
  const appDef = {
125
125
  database: {
126
126
  postgres: {
@@ -131,84 +131,10 @@ describe('MigrationBuilder', () => {
131
131
 
132
132
  const result = await builder.build(appDef, {});
133
133
 
134
- expect(result.functions.dbMigrationWorker).toBeDefined();
135
- expect(result.functions.dbMigrationWorker.handler).toBe(
136
- 'node_modules/@friggframework/core/handlers/workers/db-migration.handler'
137
- );
138
- expect(result.functions.dbMigrationWorker.timeout).toBe(900);
139
- expect(result.functions.dbMigrationWorker.memorySize).toBe(1024);
140
- expect(result.functions.dbMigrationWorker.reservedConcurrency).toBe(1);
141
- expect(result.functions.dbMigrationWorker.events).toEqual([
142
- {
143
- sqs: {
144
- arn: { 'Fn::GetAtt': ['DbMigrationQueue', 'Arn'] },
145
- batchSize: 1,
146
- },
147
- },
148
- ]);
149
- });
150
-
151
- it('should create migration router function', async () => {
152
- const appDef = {
153
- database: {
154
- postgres: {
155
- enable: true,
156
- },
157
- },
158
- };
159
-
160
- const result = await builder.build(appDef, {});
161
-
162
- expect(result.functions.dbMigrationRouter).toBeDefined();
163
- expect(result.functions.dbMigrationRouter.handler).toBe(
164
- 'node_modules/@friggframework/core/handlers/routers/db-migration.handler'
165
- );
166
- expect(result.functions.dbMigrationRouter.timeout).toBe(30);
167
- expect(result.functions.dbMigrationRouter.events).toContainEqual({
168
- httpApi: { path: '/db-migrate/status', method: 'GET' },
169
- });
170
- expect(result.functions.dbMigrationRouter.events).toContainEqual({
171
- httpApi: { path: '/db-migrate', method: 'POST' },
172
- });
173
- expect(result.functions.dbMigrationRouter.events).toContainEqual({
174
- httpApi: { path: '/db-migrate/{processId}', method: 'GET' },
175
- });
176
- });
177
-
178
- it('should configure package exclusions for migration functions to reduce Lambda size', async () => {
179
- const appDef = {
180
- database: {
181
- postgres: {
182
- enable: true,
183
- },
184
- },
185
- };
186
-
187
- const result = await builder.build(appDef, {});
188
-
189
- // Worker and router now have DIFFERENT package configs (split for size optimization)
190
- expect(result.functions.dbMigrationWorker.package).toBeDefined();
191
- expect(result.functions.dbMigrationRouter.package).toBeDefined();
192
- expect(result.functions.dbMigrationWorker.package).not.toBe(result.functions.dbMigrationRouter.package);
193
-
194
- const workerPackage = result.functions.dbMigrationWorker.package;
195
- const routerPackage = result.functions.dbMigrationRouter.package;
196
-
197
- // Verify worker excludes Prisma client (in layer) but keeps CLI
198
- expect(workerPackage.exclude).toContain('node_modules/@prisma/client/**');
199
- expect(workerPackage.exclude).toContain('node_modules/@friggframework/core/generated/**');
200
-
201
- // Verify router excludes ALL WASM files (doesn't run migrations)
202
- expect(routerPackage.exclude).toContain('**/*.wasm*');
203
-
204
- // Verify common exclusions for both
205
- expect(workerPackage.exclude).toContain('node_modules/**/node_modules/**');
206
- expect(workerPackage.exclude).toContain('**/*.test.js');
207
- expect(workerPackage.exclude).toContain('src/**');
208
-
209
- // Should NOT exclude migration handlers - they're needed!
210
- expect(workerPackage.exclude).not.toContain('node_modules/@friggframework/core/handlers/routers/**');
211
- expect(workerPackage.exclude).not.toContain('node_modules/@friggframework/core/handlers/workers/**');
134
+ expect(result.resources.FriggMigrationStatusBucket).toBeDefined();
135
+ expect(result.resources.FriggMigrationStatusBucket.Type).toBe('AWS::S3::Bucket');
136
+ expect(result.resources.FriggMigrationStatusBucket.DeletionPolicy).toBe('Retain');
137
+ expect(result.resources.FriggMigrationStatusBucket.Properties.VersioningConfiguration.Status).toBe('Enabled');
212
138
  });
213
139
 
214
140
  it('should add queue URL to environment', async () => {
@@ -284,38 +210,6 @@ describe('MigrationBuilder', () => {
284
210
  })
285
211
  );
286
212
  });
287
-
288
- it('should only include Prisma layer in worker (router doesn\'t need database)', async () => {
289
- const appDef = {
290
- database: {
291
- postgres: {
292
- enable: true,
293
- },
294
- },
295
- };
296
-
297
- const result = await builder.build(appDef, {});
298
-
299
- // Worker needs Prisma layer for runtime client
300
- expect(result.functions.dbMigrationWorker.layers).toEqual([{ Ref: 'PrismaLambdaLayer' }]);
301
- // Router doesn't access database - no Prisma layer needed
302
- expect(result.functions.dbMigrationRouter.layers).toBeUndefined();
303
- });
304
-
305
- it('should set skipEsbuild for both functions', async () => {
306
- const appDef = {
307
- database: {
308
- postgres: {
309
- enable: true,
310
- },
311
- },
312
- };
313
-
314
- const result = await builder.build(appDef, {});
315
-
316
- expect(result.functions.dbMigrationWorker.skipEsbuild).toBe(true);
317
- expect(result.functions.dbMigrationRouter.skipEsbuild).toBe(true);
318
- });
319
213
  });
320
214
 
321
215
  describe('getName', () => {