@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.
- package/infrastructure/ARCHITECTURE.md +487 -0
- package/infrastructure/domains/database/aurora-builder.js +234 -57
- package/infrastructure/domains/database/aurora-builder.test.js +7 -2
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +256 -215
- package/infrastructure/domains/database/migration-builder.test.js +5 -111
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/integration/integration-builder.js +258 -84
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/networking/vpc-builder.js +856 -135
- package/infrastructure/domains/networking/vpc-builder.test.js +10 -6
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/security/kms-builder.js +179 -22
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +26 -1
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/package.json +6 -6
- package/infrastructure/REFACTOR.md +0 -532
- 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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
387
|
-
|
|
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
|
|
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.
|
|
135
|
-
expect(result.
|
|
136
|
-
|
|
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', () => {
|