@friggframework/devtools 2.0.0--canary.464.f9d3fc0.0 → 2.0.0--canary.454.25d396a.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.
@@ -1,6 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
  const { AWSDiscovery } = require('./aws-discovery');
4
+ const { buildPrismaLayer } = require('./scripts/build-prisma-layer');
4
5
 
5
6
  const shouldRunDiscovery = (AppDefinition) => {
6
7
  console.log(
@@ -17,7 +18,8 @@ const shouldRunDiscovery = (AppDefinition) => {
17
18
  return (
18
19
  AppDefinition.vpc?.enable === true ||
19
20
  AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
20
- AppDefinition.ssm?.enable === true
21
+ AppDefinition.ssm?.enable === true ||
22
+ AppDefinition.database?.postgres?.enable === true
21
23
  );
22
24
  };
23
25
 
@@ -543,10 +545,17 @@ const gatherDiscoveredResources = async (AppDefinition) => {
543
545
  try {
544
546
  const region = process.env.AWS_REGION || 'us-east-1';
545
547
  const discovery = new AWSDiscovery(region);
548
+ // Use Serverless Framework's stage resolution (opt:stage with 'dev' as default)
549
+ // This matches how serverless.yml resolves ${opt:stage, "dev"}
550
+ // IMPORTANT: Use SLS_STAGE (not STAGE) to match actual deployment stage
551
+ const stage = process.env.SLS_STAGE || 'dev';
552
+
546
553
  const config = {
547
554
  vpc: AppDefinition.vpc || {},
548
555
  encryption: AppDefinition.encryption || {},
549
556
  ssm: AppDefinition.ssm || {},
557
+ serviceName: AppDefinition.name || 'create-frigg-app',
558
+ stage: stage,
550
559
  };
551
560
 
552
561
  const discoveredResources = await discovery.discoverResources(config);
@@ -602,6 +611,22 @@ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
602
611
  }
603
612
  }
604
613
 
614
+ // Add Aurora discovery mappings
615
+ if (discoveredResources.aurora) {
616
+ if (discoveredResources.aurora.clusterIdentifier) {
617
+ environment.AWS_DISCOVERY_AURORA_CLUSTER_ID = discoveredResources.aurora.clusterIdentifier;
618
+ }
619
+ if (discoveredResources.aurora.endpoint) {
620
+ environment.AWS_DISCOVERY_AURORA_ENDPOINT = discoveredResources.aurora.endpoint;
621
+ }
622
+ if (discoveredResources.aurora.port) {
623
+ environment.AWS_DISCOVERY_AURORA_PORT = discoveredResources.aurora.port.toString();
624
+ }
625
+ if (discoveredResources.aurora.secretArn) {
626
+ environment.AWS_DISCOVERY_AURORA_SECRET_ARN = discoveredResources.aurora.secretArn;
627
+ }
628
+ }
629
+
605
630
  return environment;
606
631
  };
607
632
 
@@ -617,15 +642,27 @@ const createBaseDefinition = (
617
642
  service: AppDefinition.name || 'create-frigg-app',
618
643
  package: {
619
644
  individually: true,
620
- exclude: [
645
+ patterns: [
646
+ // Existing AWS SDK exclusions
621
647
  '!**/node_modules/aws-sdk/**',
622
648
  '!**/node_modules/@aws-sdk/**',
623
649
  '!package.json',
650
+
651
+ // GLOBAL Prisma exclusions - all Prisma packages moved to Lambda Layer
652
+ // This reduces each function from ~120MB to ~45MB (60% reduction)
653
+ '!node_modules/@prisma/**',
654
+ '!node_modules/.prisma/**',
655
+ '!node_modules/@prisma-mongodb/**',
656
+ '!node_modules/@prisma-postgresql/**',
657
+ '!node_modules/prisma/**',
658
+ // Prisma packages will be provided at runtime via Lambda Layer
659
+ // See: LAMBDA-LAYER-PRISMA.md for complete documentation
624
660
  ],
625
661
  },
626
662
  useDotenv: true,
627
663
  provider: {
628
664
  name: AppDefinition.provider || 'aws',
665
+ ...(process.env.AWS_PROFILE && { profile: process.env.AWS_PROFILE }),
629
666
  runtime: 'nodejs20.x',
630
667
  timeout: 30,
631
668
  region,
@@ -703,8 +740,8 @@ const createBaseDefinition = (
703
740
  },
704
741
  functions: {
705
742
  auth: {
706
- handler:
707
- 'node_modules/@friggframework/core/handlers/routers/auth.handler',
743
+ handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
744
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
708
745
  events: [
709
746
  { httpApi: { path: '/api/integrations', method: 'ANY' } },
710
747
  {
@@ -717,20 +754,57 @@ const createBaseDefinition = (
717
754
  ],
718
755
  },
719
756
  user: {
720
- handler:
721
- 'node_modules/@friggframework/core/handlers/routers/user.handler',
722
- events: [
723
- { httpApi: { path: '/user/{proxy+}', method: 'ANY' } },
724
- ],
757
+ handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
758
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
759
+ events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
725
760
  },
726
761
  health: {
727
- handler:
728
- 'node_modules/@friggframework/core/handlers/routers/health.handler',
762
+ handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
763
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
729
764
  events: [
730
765
  { httpApi: { path: '/health', method: 'GET' } },
731
766
  { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
732
767
  ],
733
768
  },
769
+ dbMigrate: {
770
+ handler: 'node_modules/@friggframework/core/handlers/workers/db-migration.handler',
771
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
772
+ timeout: 300, // 5 minutes for long-running migrations
773
+ memorySize: 512, // Extra memory for Prisma CLI operations
774
+ reservedConcurrency: 1, // Prevent concurrent migrations
775
+ description: 'Runs database migrations via Prisma (invoke manually from CI/CD)',
776
+ // No events - this function is invoked manually via AWS CLI
777
+ maximumEventAge: 60, // Don't retry old migration requests (60 seconds)
778
+ maximumRetryAttempts: 0, // Don't auto-retry failed migrations
779
+ tags: {
780
+ Purpose: 'DatabaseMigration',
781
+ ManagedBy: 'Frigg',
782
+ },
783
+ // Environment variables for non-interactive Prisma CLI operation
784
+ environment: {
785
+ CI: '1', // Forces Prisma to non-interactive mode
786
+ PRISMA_HIDE_UPDATE_MESSAGE: '1', // Suppress update messages
787
+ PRISMA_MIGRATE_SKIP_SEED: '1', // Skip seeding during migrations
788
+ },
789
+ // Function-specific packaging: Include Prisma schemas (CLI from layer)
790
+ package: {
791
+ patterns: [
792
+ // Include Prisma schemas from @friggframework/core
793
+ // Note: Prisma CLI and clients come from Lambda Layer
794
+ 'node_modules/@friggframework/core/prisma-mongodb/**',
795
+ 'node_modules/@friggframework/core/prisma-postgresql/**',
796
+ ],
797
+ },
798
+ },
799
+ },
800
+ layers: {
801
+ prisma: {
802
+ path: 'layers/prisma',
803
+ name: '${self:service}-prisma-${sls:stage}',
804
+ description: 'Prisma ORM clients for MongoDB and PostgreSQL with rhel-openssl-3.0.x binaries. Reduces function sizes by ~60% (120MB → 45MB). See LAMBDA-LAYER-PRISMA.md for details.',
805
+ compatibleRuntimes: ['nodejs18.x', 'nodejs20.x'],
806
+ retain: false, // Don't retain old layer versions
807
+ },
734
808
  },
735
809
  resources: {
736
810
  Resources: {
@@ -829,18 +903,22 @@ const applyKmsConfiguration = (
829
903
  }
830
904
 
831
905
  if (discoveredResources.defaultKmsKeyId) {
832
- console.log(
833
- `Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`
834
- );
835
- definition.resources.Resources.FriggKMSKeyAlias = {
836
- Type: 'AWS::KMS::Alias',
837
- DeletionPolicy: 'Retain',
838
- Properties: {
839
- AliasName:
840
- 'alias/${self:service}-${self:provider.stage}-frigg-kms',
841
- TargetKeyId: discoveredResources.defaultKmsKeyId,
842
- },
843
- };
906
+ console.log(`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`);
907
+
908
+ // Only create alias if it doesn't already exist
909
+ if (!discoveredResources.kmsAliasExists) {
910
+ console.log('Creating KMS alias for discovered key...');
911
+ definition.resources.Resources.FriggKMSKeyAlias = {
912
+ Type: 'AWS::KMS::Alias',
913
+ DeletionPolicy: 'Retain',
914
+ Properties: {
915
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
916
+ TargetKeyId: discoveredResources.defaultKmsKeyId,
917
+ },
918
+ };
919
+ } else {
920
+ console.log('KMS alias already exists, skipping alias creation');
921
+ }
844
922
 
845
923
  definition.provider.iamRoleStatements.push({
846
924
  Effect: 'Allow',
@@ -1894,7 +1972,9 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1894
1972
  },
1895
1973
  };
1896
1974
 
1897
- if (AppDefinition.secretsManager?.enable === true) {
1975
+ // Create Secrets Manager VPC Endpoint if explicitly enabled OR if Aurora is enabled
1976
+ // (Aurora requires Secrets Manager access for credential retrieval)
1977
+ if (AppDefinition.secretsManager?.enable === true || AppDefinition.database?.postgres?.enable === true) {
1898
1978
  definition.resources.Resources.VPCEndpointSecretsManager = {
1899
1979
  Type: 'AWS::EC2::VPCEndpoint',
1900
1980
  Properties: {
@@ -1912,6 +1992,283 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1912
1992
  }
1913
1993
  };
1914
1994
 
1995
+ const createAuroraInfrastructure = (definition, AppDefinition, discoveredResources) => {
1996
+ const dbConfig = AppDefinition.database.postgres;
1997
+
1998
+ console.log('šŸ”§ Creating Aurora Serverless v2 infrastructure...');
1999
+
2000
+ // 1. DB Subnet Group (using Lambda private subnets)
2001
+ definition.resources.Resources.FriggDBSubnetGroup = {
2002
+ Type: 'AWS::RDS::DBSubnetGroup',
2003
+ Properties: {
2004
+ DBSubnetGroupDescription: 'Subnet group for Frigg Aurora cluster',
2005
+ SubnetIds: [
2006
+ discoveredResources.privateSubnetId1,
2007
+ discoveredResources.privateSubnetId2
2008
+ ],
2009
+ Tags: [
2010
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-subnet-group' },
2011
+ { Key: 'ManagedBy', Value: 'Frigg' },
2012
+ { Key: 'Service', Value: '${self:service}' },
2013
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2014
+ ]
2015
+ }
2016
+ };
2017
+
2018
+ // 2. Security Group (allow Lambda SG to access 5432)
2019
+ // In create-new VPC mode, Lambda uses FriggLambdaSecurityGroup
2020
+ // In other modes, use discovered default security group
2021
+ const lambdaSecurityGroupId = AppDefinition.vpc?.management === 'create-new'
2022
+ ? { Ref: 'FriggLambdaSecurityGroup' }
2023
+ : discoveredResources.defaultSecurityGroupId;
2024
+
2025
+ definition.resources.Resources.FriggAuroraSecurityGroup = {
2026
+ Type: 'AWS::EC2::SecurityGroup',
2027
+ Properties: {
2028
+ GroupDescription: 'Security group for Frigg Aurora PostgreSQL',
2029
+ VpcId: discoveredResources.defaultVpcId,
2030
+ SecurityGroupIngress: [
2031
+ {
2032
+ IpProtocol: 'tcp',
2033
+ FromPort: 5432,
2034
+ ToPort: 5432,
2035
+ SourceSecurityGroupId: lambdaSecurityGroupId,
2036
+ Description: 'PostgreSQL access from Lambda functions'
2037
+ }
2038
+ ],
2039
+ Tags: [
2040
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-sg' },
2041
+ { Key: 'ManagedBy', Value: 'Frigg' },
2042
+ { Key: 'Service', Value: '${self:service}' },
2043
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2044
+ ]
2045
+ }
2046
+ };
2047
+
2048
+ // 3. Secrets Manager Secret (database credentials)
2049
+ definition.resources.Resources.FriggDatabaseSecret = {
2050
+ Type: 'AWS::SecretsManager::Secret',
2051
+ Properties: {
2052
+ Name: '${self:service}-${self:provider.stage}-aurora-credentials',
2053
+ Description: 'Aurora PostgreSQL credentials for Frigg application',
2054
+ GenerateSecretString: {
2055
+ SecretStringTemplate: JSON.stringify({
2056
+ username: dbConfig.masterUsername || 'frigg_admin'
2057
+ }),
2058
+ GenerateStringKey: 'password',
2059
+ PasswordLength: 32,
2060
+ ExcludeCharacters: '"@/\\'
2061
+ },
2062
+ Tags: [
2063
+ { Key: 'ManagedBy', Value: 'Frigg' },
2064
+ { Key: 'Service', Value: '${self:service}' },
2065
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2066
+ ]
2067
+ }
2068
+ };
2069
+
2070
+ // 4. Aurora Serverless v2 Cluster
2071
+ definition.resources.Resources.FriggAuroraCluster = {
2072
+ Type: 'AWS::RDS::DBCluster',
2073
+ DeletionPolicy: 'Snapshot',
2074
+ UpdateReplacePolicy: 'Snapshot',
2075
+ Properties: {
2076
+ Engine: 'aurora-postgresql',
2077
+ EngineVersion: dbConfig.engineVersion || '15.3',
2078
+ EngineMode: 'provisioned', // Required for Serverless v2
2079
+ DatabaseName: dbConfig.databaseName || 'frigg_db',
2080
+ MasterUsername: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2081
+ MasterUserPassword: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2082
+ DBSubnetGroupName: { Ref: 'FriggDBSubnetGroup' },
2083
+ VpcSecurityGroupIds: [{ Ref: 'FriggAuroraSecurityGroup' }],
2084
+ ServerlessV2ScalingConfiguration: {
2085
+ MinCapacity: dbConfig.scaling?.minCapacity || 0.5,
2086
+ MaxCapacity: dbConfig.scaling?.maxCapacity || 1.0
2087
+ },
2088
+ BackupRetentionPeriod: dbConfig.backupRetentionDays || 7,
2089
+ PreferredBackupWindow: dbConfig.preferredBackupWindow || '03:00-04:00',
2090
+ DeletionProtection: dbConfig.deletionProtection !== false,
2091
+ EnableCloudwatchLogsExports: ['postgresql'],
2092
+ Tags: [
2093
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-cluster' },
2094
+ { Key: 'ManagedBy', Value: 'Frigg' },
2095
+ { Key: 'Service', Value: '${self:service}' },
2096
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2097
+ ]
2098
+ }
2099
+ };
2100
+
2101
+ // 5. Aurora Serverless v2 Instance
2102
+ definition.resources.Resources.FriggAuroraInstance = {
2103
+ Type: 'AWS::RDS::DBInstance',
2104
+ Properties: {
2105
+ Engine: 'aurora-postgresql',
2106
+ DBInstanceClass: 'db.serverless',
2107
+ DBClusterIdentifier: { Ref: 'FriggAuroraCluster' },
2108
+ PubliclyAccessible: false,
2109
+ EnablePerformanceInsights: dbConfig.enablePerformanceInsights || false,
2110
+ Tags: [
2111
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-instance' },
2112
+ { Key: 'ManagedBy', Value: 'Frigg' },
2113
+ { Key: 'Service', Value: '${self:service}' },
2114
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2115
+ ]
2116
+ }
2117
+ };
2118
+
2119
+ // 6. Secret Attachment (links cluster to secret)
2120
+ definition.resources.Resources.FriggSecretAttachment = {
2121
+ Type: 'AWS::SecretsManager::SecretTargetAttachment',
2122
+ Properties: {
2123
+ SecretId: { Ref: 'FriggDatabaseSecret' },
2124
+ TargetId: { Ref: 'FriggAuroraCluster' },
2125
+ TargetType: 'AWS::RDS::DBCluster'
2126
+ }
2127
+ };
2128
+
2129
+ // 7. Add IAM permissions for Secrets Manager
2130
+ definition.provider.iamRoleStatements.push({
2131
+ Effect: 'Allow',
2132
+ Action: [
2133
+ 'secretsmanager:GetSecretValue',
2134
+ 'secretsmanager:DescribeSecret'
2135
+ ],
2136
+ Resource: { Ref: 'FriggDatabaseSecret' }
2137
+ });
2138
+
2139
+ // 8. Set DATABASE_URL environment variable
2140
+ definition.provider.environment.DATABASE_URL = {
2141
+ 'Fn::Sub': [
2142
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2143
+ {
2144
+ Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2145
+ Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2146
+ Endpoint: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint'] },
2147
+ Port: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Port'] },
2148
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2149
+ }
2150
+ ]
2151
+ };
2152
+
2153
+ // 9. Set DB_TYPE for Prisma client selection
2154
+ definition.provider.environment.DB_TYPE = 'postgresql';
2155
+
2156
+ console.log('āœ… Aurora infrastructure resources created');
2157
+ };
2158
+
2159
+ const useExistingAurora = (definition, AppDefinition, discoveredResources) => {
2160
+ const dbConfig = AppDefinition.database.postgres;
2161
+
2162
+ console.log(`šŸ”— Using existing Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
2163
+
2164
+ // Add IAM permissions for Secrets Manager if secret exists
2165
+ if (discoveredResources.aurora.secretArn) {
2166
+ definition.provider.iamRoleStatements.push({
2167
+ Effect: 'Allow',
2168
+ Action: [
2169
+ 'secretsmanager:GetSecretValue',
2170
+ 'secretsmanager:DescribeSecret'
2171
+ ],
2172
+ Resource: discoveredResources.aurora.secretArn
2173
+ });
2174
+
2175
+ // Set DATABASE_URL from discovered secret
2176
+ definition.provider.environment.DATABASE_URL = {
2177
+ 'Fn::Sub': [
2178
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2179
+ {
2180
+ Username: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:username}}` },
2181
+ Password: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:password}}` },
2182
+ Endpoint: discoveredResources.aurora.endpoint,
2183
+ Port: discoveredResources.aurora.port,
2184
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2185
+ }
2186
+ ]
2187
+ };
2188
+ } else if (dbConfig.secretArn) {
2189
+ // Use user-provided secret ARN
2190
+ definition.provider.iamRoleStatements.push({
2191
+ Effect: 'Allow',
2192
+ Action: [
2193
+ 'secretsmanager:GetSecretValue',
2194
+ 'secretsmanager:DescribeSecret'
2195
+ ],
2196
+ Resource: dbConfig.secretArn
2197
+ });
2198
+
2199
+ definition.provider.environment.DATABASE_URL = {
2200
+ 'Fn::Sub': [
2201
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2202
+ {
2203
+ Username: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:username}}` },
2204
+ Password: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:password}}` },
2205
+ Endpoint: discoveredResources.aurora.endpoint,
2206
+ Port: discoveredResources.aurora.port,
2207
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2208
+ }
2209
+ ]
2210
+ };
2211
+ } else {
2212
+ throw new Error('No database secret found. Provide secretArn in database.postgres configuration or ensure Secrets Manager secret exists.');
2213
+ }
2214
+
2215
+ // Set DB_TYPE for Prisma client selection
2216
+ definition.provider.environment.DB_TYPE = 'postgresql';
2217
+
2218
+ console.log('āœ… Existing Aurora cluster configured');
2219
+ };
2220
+
2221
+ const useDiscoveredAurora = (definition, AppDefinition, discoveredResources) => {
2222
+ console.log(`šŸ” Using discovered Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
2223
+ useExistingAurora(definition, AppDefinition, discoveredResources);
2224
+ };
2225
+
2226
+ const configurePostgres = (definition, AppDefinition, discoveredResources) => {
2227
+ if (!AppDefinition.database?.postgres?.enable) {
2228
+ return;
2229
+ }
2230
+
2231
+ // Validate VPC is enabled (required for Aurora deployment)
2232
+ if (!AppDefinition.vpc?.enable) {
2233
+ throw new Error(
2234
+ 'Aurora PostgreSQL requires VPC deployment. ' +
2235
+ 'Set vpc.enable to true in your app definition.'
2236
+ );
2237
+ }
2238
+
2239
+ // Validate private subnets exist (Aurora requires at least 2 subnets in different AZs)
2240
+ // Skip validation if VPC management is 'create-new' (subnets will be created)
2241
+ const vpcManagement = AppDefinition.vpc?.management || 'discover';
2242
+ if (vpcManagement !== 'create-new' && (!discoveredResources.privateSubnetId1 || !discoveredResources.privateSubnetId2)) {
2243
+ throw new Error(
2244
+ 'Aurora PostgreSQL requires at least 2 private subnets in different availability zones. ' +
2245
+ 'No private subnets were discovered in your VPC. ' +
2246
+ 'Please create private subnets or use VPC management mode "create-new".'
2247
+ );
2248
+ }
2249
+
2250
+ const dbConfig = AppDefinition.database.postgres;
2251
+ const management = dbConfig.management || 'discover';
2252
+
2253
+ console.log(`\n🐘 PostgreSQL Management Mode: ${management}`);
2254
+
2255
+ if (management === 'create-new' || discoveredResources.aurora?.needsCreation) {
2256
+ createAuroraInfrastructure(definition, AppDefinition, discoveredResources);
2257
+ } else if (management === 'use-existing') {
2258
+ if (!discoveredResources.aurora?.clusterIdentifier && !dbConfig.clusterIdentifier) {
2259
+ throw new Error('PostgreSQL management is set to "use-existing" but no clusterIdentifier was found or provided');
2260
+ }
2261
+ useExistingAurora(definition, AppDefinition, discoveredResources);
2262
+ } else {
2263
+ // discover mode
2264
+ if (discoveredResources.aurora?.clusterIdentifier) {
2265
+ useDiscoveredAurora(definition, AppDefinition, discoveredResources);
2266
+ } else {
2267
+ throw new Error('No Aurora cluster found in discovery mode. Set management to "create-new" or provide clusterIdentifier with "use-existing".');
2268
+ }
2269
+ }
2270
+ };
2271
+
1915
2272
  const configureSsm = (definition, AppDefinition) => {
1916
2273
  if (AppDefinition.ssm?.enable !== true) {
1917
2274
  return;
@@ -1959,30 +2316,6 @@ const attachIntegrations = (definition, AppDefinition) => {
1959
2316
  }Queue`;
1960
2317
  const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
1961
2318
 
1962
- // Add webhook handler if enabled (BEFORE catch-all proxy route)
1963
- const webhookConfig = integration.Definition.webhooks;
1964
- if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
1965
- const webhookFunctionName = `${integrationName}Webhook`;
1966
-
1967
- definition.functions[webhookFunctionName] = {
1968
- handler: `node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.${integrationName}Webhook.handler`,
1969
- events: [
1970
- {
1971
- httpApi: {
1972
- path: `/api/${integrationName}-integration/webhooks`,
1973
- method: 'POST',
1974
- },
1975
- },
1976
- {
1977
- httpApi: {
1978
- path: `/api/${integrationName}-integration/webhooks/{integrationId}`,
1979
- method: 'POST',
1980
- },
1981
- },
1982
- ],
1983
- };
1984
- }
1985
-
1986
2319
  definition.functions[integrationName] = {
1987
2320
  handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
1988
2321
  events: [
@@ -2033,6 +2366,30 @@ const attachIntegrations = (definition, AppDefinition) => {
2033
2366
  };
2034
2367
 
2035
2368
  definition.custom[queueReference] = queueName;
2369
+
2370
+ // Add webhook handler if enabled
2371
+ const webhookConfig = integration.Definition.webhooks;
2372
+ if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
2373
+ const webhookFunctionName = `${integrationName}Webhook`;
2374
+
2375
+ definition.functions[webhookFunctionName] = {
2376
+ handler: `node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.${integrationName}Webhook.handler`,
2377
+ events: [
2378
+ {
2379
+ httpApi: {
2380
+ path: `/api/${integrationName}-integration/webhooks`,
2381
+ method: 'POST',
2382
+ },
2383
+ },
2384
+ {
2385
+ httpApi: {
2386
+ path: `/api/${integrationName}-integration/webhooks/{integrationId}`,
2387
+ method: 'POST',
2388
+ },
2389
+ },
2390
+ ],
2391
+ };
2392
+ }
2036
2393
  }
2037
2394
  };
2038
2395
 
@@ -2052,9 +2409,40 @@ const configureWebsockets = (definition, AppDefinition) => {
2052
2409
  };
2053
2410
  };
2054
2411
 
2412
+ /**
2413
+ * Ensure Prisma Lambda Layer exists
2414
+ * Automatically builds the layer if it doesn't exist in the project root
2415
+ */
2416
+ async function ensurePrismaLayerExists() {
2417
+ const projectRoot = process.cwd();
2418
+ const layerPath = path.join(projectRoot, 'layers/prisma');
2419
+
2420
+ // Check if layer already exists
2421
+ if (fs.existsSync(layerPath)) {
2422
+ console.log('āœ“ Prisma Lambda Layer already exists at', layerPath);
2423
+ return;
2424
+ }
2425
+
2426
+ // Layer doesn't exist - build it automatically
2427
+ console.log('šŸ“¦ Prisma Lambda Layer not found - building automatically...');
2428
+ console.log(' This may take a minute on first deployment.\n');
2429
+
2430
+ try {
2431
+ await buildPrismaLayer();
2432
+ console.log('āœ“ Prisma Lambda Layer built successfully\n');
2433
+ } catch (error) {
2434
+ console.error('āœ— Failed to build Prisma Lambda Layer:', error.message);
2435
+ console.error(' You may need to run: npm install @friggframework/core\n');
2436
+ throw error;
2437
+ }
2438
+ }
2439
+
2055
2440
  const composeServerlessDefinition = async (AppDefinition) => {
2056
2441
  console.log('composeServerlessDefinition', AppDefinition);
2057
2442
 
2443
+ // Ensure Prisma layer exists before generating serverless config
2444
+ await ensurePrismaLayerExists();
2445
+
2058
2446
  const discoveredResources = await gatherDiscoveredResources(AppDefinition);
2059
2447
  const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
2060
2448
  const definition = createBaseDefinition(
@@ -2076,6 +2464,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
2076
2464
  if (!isLocalBuild) {
2077
2465
  applyKmsConfiguration(definition, AppDefinition, discoveredResources);
2078
2466
  configureVpc(definition, AppDefinition, discoveredResources);
2467
+ configurePostgres(definition, AppDefinition, discoveredResources);
2079
2468
  configureSsm(definition, AppDefinition);
2080
2469
  } else {
2081
2470
  console.log(