diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 8b5147d8..b6f304e1 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -2,21 +2,21 @@ name: Build Test on: pull_request: - branches: [ main ] + branches: [ main, staging ] paths: - 'PetAdoptions/payforadoption-go/**' - - 'PetAdoptions/petadoptionshistory-py/**' - - 'PetAdoptions/petlistadoptions-go/**' + - 'PetAdoptions/pethistory/**' + - 'PetAdoptions/petlistadoptions-py/**' - 'PetAdoptions/petsearch-java/**' - 'PetAdoptions/petsite/**' - 'PetAdoptions/petstatusupdater/**' - 'PetAdoptions/trafficgenerator/**' push: - branches: [ main ] + branches: [ main, staging ] paths: - 'PetAdoptions/payforadoption-go/**' - - 'PetAdoptions/petadoptionshistory-py/**' - - 'PetAdoptions/petlistadoptions-go/**' + - 'PetAdoptions/pethistory/**' + - 'PetAdoptions/petlistadoptions-py/**' - 'PetAdoptions/petsearch-java/**' - 'PetAdoptions/petsite/**' - 'PetAdoptions/petstatusupdater/**' @@ -31,10 +31,8 @@ jobs: service: - name: payforadoption-go path: PetAdoptions/payforadoption-go - - name: petadoptionshistory-py - path: PetAdoptions/petadoptionshistory-py - - name: petlistadoptions-go - path: PetAdoptions/petlistadoptions-go + - name: petlistadoptions-py + path: PetAdoptions/petlistadoptions-py - name: petsearch-java path: PetAdoptions/petsearch-java - name: petsite @@ -58,25 +56,37 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - nodejs-build: + nodejs-builds: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + service: + - name: pethistory + path: PetAdoptions/pethistory + node-version: '18' + steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: '16' + node-version: ${{ matrix.service.node-version }} cache: 'npm' - cache-dependency-path: PetAdoptions/petstatusupdater/package-lock.json + cache-dependency-path: ${{ matrix.service.path }}/package-lock.json - name: Install dependencies run: npm ci - working-directory: PetAdoptions/petstatusupdater + working-directory: ${{ matrix.service.path }} + + - name: Run tests + run: npm test + working-directory: ${{ matrix.service.path }} - name: Verify build run: npm run build --if-present - working-directory: PetAdoptions/petstatusupdater + working-directory: ${{ matrix.service.path }} dotnet-builds: runs-on: ubuntu-latest @@ -95,7 +105,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: Restore dependencies run: dotnet restore ${{ matrix.project.path }} diff --git a/.gitignore b/.gitignore index 0f9e7cc9..13e92ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,390 @@ # Go binaries +# cspell:ignore petadoptions PetAdoptions/payforadoption-go/petadoptions + **/.DS_Store **/assets **/.idea # editor settings .vscode/settings.json +PetAdoptions/petsite/petsite/appsettings.Development.json +PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/indexLayout.xml +PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/modules.xml + + +# Merging gitignore files + +./petsite/.idea +./trafficgenerator/.idea +./petstatusupdater/function.zip +cscdk + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*[.json, .xml, .info] + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# pethistory + +pethistory/.aws-sam/ +pethistory/cdk-example.ts +pethistory/coverage/ +pethistory/events/ +pethistory/samconfig.toml diff --git a/PetAdoptions/.gitignore b/PetAdoptions/.gitignore deleted file mode 100644 index 730ef60a..00000000 --- a/PetAdoptions/.gitignore +++ /dev/null @@ -1,365 +0,0 @@ -./petsite/.idea -./trafficgenerator/.idea -./petstatusupdater/function.zip -cscdk - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*[.json, .xml, .info] - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd diff --git a/PetAdoptions/cdk/pet_stack/.gitignore b/PetAdoptions/cdk/pet_stack/.gitignore index 41ce9aef..2b097adf 100644 --- a/PetAdoptions/cdk/pet_stack/.gitignore +++ b/PetAdoptions/cdk/pet_stack/.gitignore @@ -1,5 +1,6 @@ !jest.config.js *.d.ts +*.js node_modules # CDK asset staging directory diff --git a/PetAdoptions/cdk/pet_stack/jest.config.js b/PetAdoptions/cdk/pet_stack/jest.config.js deleted file mode 100644 index 772f9749..00000000 --- a/PetAdoptions/cdk/pet_stack/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - roots: ['/test'], - testMatch: ['**/*.test.ts'], - transform: { - '^.+\\.tsx?$': 'ts-jest' - } -}; diff --git a/PetAdoptions/cdk/pet_stack/lib/applications.ts b/PetAdoptions/cdk/pet_stack/lib/applications.ts index a37fa0a0..3f602dbf 100644 --- a/PetAdoptions/cdk/pet_stack/lib/applications.ts +++ b/PetAdoptions/cdk/pet_stack/lib/applications.ts @@ -8,7 +8,6 @@ import { Stack, StackProps, CfnJson, Fn, CfnOutput } from 'aws-cdk-lib'; import { readFileSync } from 'fs'; import { Construct } from 'constructs' import { ContainerImageBuilderProps, ContainerImageBuilder } from './common/container-image-builder' -import { PetAdoptionsHistory } from './applications/pet-adoptions-history-application' import { KubectlV31Layer } from '@aws-cdk/lambda-layer-kubectl-v31'; export class Applications extends Stack { @@ -93,27 +92,6 @@ export class Applications extends Stack { manifest: deploymentYaml }); - // PetAdoptionsHistory application definitions----------------------------------------------------------------------- - const petAdoptionsHistoryContainerImage = new ContainerImageBuilder(this, 'pet-adoptions-history-container-image', { - repositoryName: "pet-adoptions-history", - dockerImageAssetDirectory: "./resources/microservices/petadoptionshistory-py", - }); - new ssm.StringParameter(this,"putPetAdoptionHistoryRepositoryName",{ - stringValue: petAdoptionsHistoryContainerImage.repositoryUri, - parameterName: '/petstore/pethistoryrepositoryuri' - }); - - const petAdoptionsHistoryApplication = new PetAdoptionsHistory(this, 'pet-adoptions-history-application', { - cluster: cluster, - app_trustRelationship: app_trustRelationship, - kubernetesManifestPath: "./resources/microservices/petadoptionshistory-py/deployment.yaml", - otelConfigMapPath: "./resources/microservices/petadoptionshistory-py/otel-collector-config.yaml", - rdsSecretArn: rdsSecretArn, - region: region, - imageUri: petAdoptionsHistoryContainerImage.imageUri, - targetGroupArn: petHistoryTargetGroupArn - }); - this.createSsmParameters(new Map(Object.entries({ '/eks/petsite/stackname': stackName }))); diff --git a/PetAdoptions/cdk/pet_stack/lib/applications/pet-adoptions-history-application.ts b/PetAdoptions/cdk/pet_stack/lib/applications/pet-adoptions-history-application.ts deleted file mode 100644 index 9853e0c7..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/applications/pet-adoptions-history-application.ts +++ /dev/null @@ -1,112 +0,0 @@ -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as eks from 'aws-cdk-lib/aws-eks'; -import * as rds from 'aws-cdk-lib/aws-rds'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; -import * as yaml from 'js-yaml'; -import { CfnJson } from 'aws-cdk-lib'; -import { EksApplication, EksApplicationProps } from './eks-application' -import { readFileSync } from 'fs'; -import { Construct } from 'constructs' - -export interface PetAdoptionsHistoryProps extends EksApplicationProps { - rdsSecretArn: string, - targetGroupArn: string, - otelConfigMapPath: string, -} - -export class PetAdoptionsHistory extends EksApplication { - - constructor(scope: Construct, id: string, props: PetAdoptionsHistoryProps) { - super(scope, id, props); - - const petadoptionhistoryserviceaccount = new iam.Role(this, 'PetSiteServiceAccount', { -// assumedBy: eksFederatedPrincipal, - assumedBy: new iam.AccountRootPrincipal(), - managedPolicies: [ - iam.ManagedPolicy.fromManagedPolicyArn(this, 'PetAdoptionHistoryServiceAccount-AWSXRayDaemonWriteAccess', 'arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'PetAdoptionHistoryServiceAccount-AmazonPrometheusRemoteWriteAccess', 'arn:aws:iam::aws:policy/AmazonPrometheusRemoteWriteAccess') - ], - }); - petadoptionhistoryserviceaccount.assumeRolePolicy?.addStatements(props.app_trustRelationship); - - const readSSMParamsPolicy = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - "ssm:GetParametersByPath", - "ssm:GetParameters", - "ssm:GetParameter", - "ec2:DescribeVpcs" - ], - resources: ['*'] - }); - petadoptionhistoryserviceaccount.addToPolicy(readSSMParamsPolicy); - - const ddbSeedPolicy = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - "dynamodb:BatchWriteItem", - "dynamodb:ListTables", - "dynamodb:Scan", - "dynamodb:Query" - ], - resources: ['*'] - }); - petadoptionhistoryserviceaccount.addToPolicy(ddbSeedPolicy); - - const rdsSecretPolicy = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - "secretsmanager:GetSecretValue" - ], - resources: [props.rdsSecretArn] - }); - petadoptionhistoryserviceaccount.addToPolicy(rdsSecretPolicy); - - const awsOtelPolicy = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - "logs:PutLogEvents", - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:DescribeLogStreams", - "logs:DescribeLogGroups", - "xray:PutTraceSegments", - "xray:PutTelemetryRecords", - "xray:GetSamplingRules", - "xray:GetSamplingTargets", - "xray:GetSamplingStatisticSummaries", - "ssm:GetParameters" - ], - resources: ['*'] - }); - petadoptionhistoryserviceaccount.addToPolicy(awsOtelPolicy); - - // otel collector config - var otelConfigMapManifest = readFileSync(props.otelConfigMapPath,"utf8"); - var otelConfigMapYaml = yaml.loadAll(otelConfigMapManifest) as Record[]; - otelConfigMapYaml[0].data["otel-config.yaml"] = otelConfigMapYaml[0].data["otel-config.yaml"].replace(/{{AWS_REGION}}/g, props.region); - - const otelConfigDeploymentManifest = new eks.KubernetesManifest(this,"otelConfigDeployment",{ - cluster: props.cluster, - manifest: otelConfigMapYaml - }); - - // deployment manifest - var manifest = readFileSync(props.kubernetesManifestPath,"utf8"); - var deploymentYaml = yaml.loadAll(manifest) as Record[]; - - deploymentYaml[0].metadata.annotations["eks.amazonaws.com/role-arn"] = petadoptionhistoryserviceaccount.roleArn; - deploymentYaml[2].spec.template.spec.containers[0].image = props.imageUri; - deploymentYaml[2].spec.template.spec.containers[0].env[1].value = props.region; - deploymentYaml[2].spec.template.spec.containers[0].env[3].value = `ClusterName=${props.cluster.clusterName}`; - deploymentYaml[2].spec.template.spec.containers[0].env[5].value = props.region; - deploymentYaml[2].spec.template.spec.containers[1].env[0].value = props.region; - deploymentYaml[3].spec.targetGroupARN = props.targetGroupArn; - - const deploymentManifest = new eks.KubernetesManifest(this,"petsitedeployment",{ - cluster: props.cluster, - manifest: deploymentYaml - }); - } - -} diff --git a/PetAdoptions/cdk/pet_stack/lib/services.ts b/PetAdoptions/cdk/pet_stack/lib/services.ts index de64cebf..b1e30cae 100644 --- a/PetAdoptions/cdk/pet_stack/lib/services.ts +++ b/PetAdoptions/cdk/pet_stack/lib/services.ts @@ -18,6 +18,7 @@ import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import * as applicationinsights from 'aws-cdk-lib/aws-applicationinsights'; import * as resourcegroups from 'aws-cdk-lib/aws-resourcegroups'; +import * as applicationsignals from 'aws-cdk-lib/aws-applicationsignals'; import { Construct } from 'constructs' import { PayForAdoptionService } from './services/pay-for-adoption-service' @@ -44,6 +45,11 @@ export class Services extends Stack { visibilityTimeout: Duration.seconds(300) }); + // Enable Application Signals in the account + const cfnDiscovery = new applicationsignals.CfnDiscovery(this, + 'ApplicationSignalsServiceRole', { } + ); + // Create SNS and an email topic to send notifications to const topic_petadoption = new sns.Topic(this, 'topic_petadoption'); var topic_email = this.node.tryGetContext('snstopic_email'); @@ -162,6 +168,13 @@ export class Services extends Stack { resources: ['*'] }); + const adoptionSQSPolicy = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'sqs:SendMessage', + ], + resources: [sqsQueue.queueArn] + }); const ddbSeedPolicy = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, @@ -204,6 +217,7 @@ export class Services extends Stack { }); payForAdoptionService.taskDefinition.taskRole?.addToPrincipalPolicy(readSSMParamsPolicy); payForAdoptionService.taskDefinition.taskRole?.addToPrincipalPolicy(ddbSeedPolicy); + payForAdoptionService.taskDefinition.taskRole?.addToPrincipalPolicy(adoptionSQSPolicy); const ecsPetListAdoptionCluster = new ecs.Cluster(this, "PetListAdoptions", { @@ -238,7 +252,7 @@ export class Services extends Stack { //repositoryURI: repositoryURI, healthCheck: '/health/status', desiredTaskCount: 2, - instrumentation: 'otel', + instrumentation: 'none', region: region, securityGroup: ecsServicesSecurityGroup }) @@ -298,30 +312,6 @@ export class Services extends Stack { defaultTargetGroups: [targetGroup], }); - // PetAdoptionHistory - attach service to path /petadoptionhistory on PetSite ALB - const petadoptionshistory_targetGroup = new elbv2.ApplicationTargetGroup(this, 'PetAdoptionsHistoryTargetGroup', { - port: 80, - protocol: elbv2.ApplicationProtocol.HTTP, - vpc: theVPC, - targetType: elbv2.TargetType.IP, - healthCheck: { - path: '/health/status', - } - }); - - listener.addTargetGroups('PetAdoptionsHistoryTargetGroups', { - priority: 10, - conditions: [ - elbv2.ListenerCondition.pathPatterns(['/petadoptionshistory/*']), - ], - targetGroups: [petadoptionshistory_targetGroup] - }); - - new ssm.StringParameter(this, "putPetHistoryParamTargetGroupArn", { - stringValue: petadoptionshistory_targetGroup.targetGroupArn, - parameterName: '/eks/pethistory/TargetGroupArn' - }); - // PetSite - EKS Cluster const clusterAdmin = new iam.Role(this, 'AdminRole', { assumedBy: new iam.AccountRootPrincipal() @@ -393,6 +383,8 @@ export class Services extends Stack { }); cwserviceaccount.assumeRolePolicy?.addStatements(cw_trustRelationship); + // Comment out X-Ray service account for petsite + /* const xray_federatedPrincipal = new iam.FederatedPrincipal( cluster.openIdConnectProvider.openIdConnectProviderArn, { @@ -418,6 +410,7 @@ export class Services extends Stack { ], }); xrayserviceaccount.assumeRolePolicy?.addStatements(xray_trustRelationship); + */ const loadbalancer_federatedPrincipal = new iam.FederatedPrincipal( cluster.openIdConnectProvider.openIdConnectProviderArn, @@ -455,6 +448,8 @@ export class Services extends Stack { ]); } + // Comment out X-Ray deployment for petsite + /* var xRayYaml = yaml.loadAll(readFileSync("./resources/k8s_petsite/xray-daemon-config.yaml", "utf8")) as Record[]; xRayYaml[0].metadata.annotations["eks.amazonaws.com/role-arn"] = new CfnJson(this, "xray_Role", { value: `${xrayserviceaccount.roleArn}` }); @@ -463,6 +458,7 @@ export class Services extends Stack { cluster: cluster, manifest: xRayYaml }); + */ var loadBalancerServiceAccountYaml = yaml.loadAll(readFileSync("./resources/load_balancer/service_account.yaml", "utf8")) as Record[]; loadBalancerServiceAccountYaml[0].metadata.annotations["eks.amazonaws.com/role-arn"] = new CfnJson(this, "loadBalancer_Role", { value: `${loadBalancerserviceaccount.roleArn}` }); @@ -507,7 +503,7 @@ export class Services extends Stack { // NOTE: Amazon CloudWatch Observability Addon for CloudWatch Agent and Fluentbit - const otelAddon = new eks.CfnAddon(this, 'otelObservabilityAddon', { + const cwAddon = new eks.CfnAddon(this, 'CloudWatchObservabilityAddon', { addonName: 'amazon-cloudwatch-observability', addonVersion: 'v3.3.0-eksbuild.1', clusterName: cluster.clusterName, @@ -657,7 +653,7 @@ export class Services extends Stack { this.createOuputs(new Map(Object.entries({ 'CWServiceAccountArn': cwserviceaccount.roleArn, 'NetworkFlowMonitorServiceAccountArn': networkFlowMonitorRole.attrArn, - 'XRayServiceAccountArn': xrayserviceaccount.roleArn, + //'XRayServiceAccountArn': xrayserviceaccount.roleArn, 'OIDCProviderUrl': cluster.clusterOpenIdConnectIssuerUrl, 'OIDCProviderArn': cluster.openIdConnectProvider.openIdConnectProviderArn, 'PetSiteUrl': `http://${alb.loadBalancerDnsName}`, @@ -680,16 +676,15 @@ export class Services extends Stack { '/petstore/searchimage': searchService.container.imageName, '/petstore/petlistadoptionsurl': `http://${listAdoptionsService.service.loadBalancer.loadBalancerDnsName}/api/adoptionlist/`, '/petstore/petlistadoptionsmetricsurl': `http://${listAdoptionsService.service.loadBalancer.loadBalancerDnsName}/metrics`, - '/petstore/paymentapiurl': `http://${payForAdoptionService.service.loadBalancer.loadBalancerDnsName}/api/home/completeadoption`, + '/petstore/paymentapiurl': `http://${payForAdoptionService.service.loadBalancer.loadBalancerDnsName}/api/completeadoption`, '/petstore/payforadoptionmetricsurl': `http://${payForAdoptionService.service.loadBalancer.loadBalancerDnsName}/metrics`, - '/petstore/cleanupadoptionsurl': `http://${payForAdoptionService.service.loadBalancer.loadBalancerDnsName}/api/home/cleanupadoptions`, + '/petstore/cleanupadoptionsurl': `http://${payForAdoptionService.service.loadBalancer.loadBalancerDnsName}/api/cleanupadoptions`, '/petstore/petsearch-collector-manual-config': readFileSync("./resources/collector/ecs-xray-manual.yaml", "utf8"), '/petstore/rdssecretarn': `${auroraCluster.secret?.secretArn}`, '/petstore/rdsendpoint': auroraCluster.clusterEndpoint.hostname, '/petstore/rds-reader-endpoint': auroraCluster.clusterReadEndpoint.hostname, '/petstore/stackname': stackName, '/petstore/petsiteurl': `http://${alb.loadBalancerDnsName}`, - '/petstore/pethistoryurl': `http://${alb.loadBalancerDnsName}/petadoptionshistory`, '/eks/petsite/OIDCProviderUrl': cluster.clusterOpenIdConnectIssuerUrl, '/eks/petsite/OIDCProviderArn': cluster.openIdConnectProvider.openIdConnectProviderArn, '/petstore/errormode1': "false" @@ -715,4 +710,4 @@ export class Services extends Stack { new CfnOutput(this, key, { value: value }) }); } -} +} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/lib/services/list-adoptions-service.ts b/PetAdoptions/cdk/pet_stack/lib/services/list-adoptions-service.ts index c7b4e375..f0c2cbee 100644 --- a/PetAdoptions/cdk/pet_stack/lib/services/list-adoptions-service.ts +++ b/PetAdoptions/cdk/pet_stack/lib/services/list-adoptions-service.ts @@ -1,6 +1,7 @@ import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as rds from 'aws-cdk-lib/aws-rds'; import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; +import { Stack } from 'aws-cdk-lib'; import { EcsService, EcsServiceProps } from './ecs-service' import { Construct } from 'constructs' @@ -15,15 +16,20 @@ export class ListAdoptionsService extends EcsService { super(scope, id, props); props.database.secret?.grantRead(this.taskDefinition.taskRole); + + // Add environment variables for the Python service + this.container.addEnvironment('PORT', '80'); + this.container.addEnvironment('WORKERS', '4'); + this.container.addEnvironment('AWS_REGION', Stack.of(this).region); } containerImageFromRepository(repositoryURI: string) : ecs.ContainerImage { return ecs.ContainerImage.fromRegistry(`${repositoryURI}/pet-listadoptions:latest`) } - createContainerImage() : ecs.ContainerImage { - return ecs.ContainerImage.fromDockerImageAsset(new DockerImageAsset(this,"petlistadoptions-go", - { directory: "./resources/microservices/petlistadoptions-go"} + createContainerImage() : ecs.ContainerImage { + return ecs.ContainerImage.fromDockerImageAsset(new DockerImageAsset(this,"petlistadoptions-python", + { directory: "../../petlistadoptions-py"} )) } } diff --git a/PetAdoptions/cdk/pet_stack/lib/services/search-service.ts b/PetAdoptions/cdk/pet_stack/lib/services/search-service.ts index 03445850..b38ea370 100644 --- a/PetAdoptions/cdk/pet_stack/lib/services/search-service.ts +++ b/PetAdoptions/cdk/pet_stack/lib/services/search-service.ts @@ -1,5 +1,6 @@ import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as iam from 'aws-cdk-lib/aws-iam'; +import * as appsignals from '@aws-cdk/aws-applicationsignals-alpha'; import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; import { EcsService, EcsServiceProps } from './ecs-service' import { Construct } from 'constructs' @@ -11,6 +12,21 @@ export class SearchService extends EcsService { this.taskDefinition.taskRole?.addManagedPolicy(iam.ManagedPolicy.fromManagedPolicyArn(this, 'AmazonDynamoDBReadOnlyAccess', 'arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess')); this.taskDefinition.taskRole?.addManagedPolicy(iam.ManagedPolicy.fromManagedPolicyArn(this, 'AmazonS3ReadOnlyAccess', 'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess')); + + // Add Application Signals integration with CloudWatch agent + new appsignals.ApplicationSignalsIntegration(this, 'ApplicationSignalsIntegration', { + taskDefinition: this.taskDefinition, + instrumentation: { + sdkVersion: appsignals.JavaInstrumentationVersion.V2_10_0, + }, + serviceName: 'PetSearch', + cloudWatchAgentSidecar: { + containerName: 'ecs-cwagent', + enableLogging: true, + cpu: 256, + memoryLimitMiB: 512, + } + }); } containerImageFromRepository(repositoryURI: string) : ecs.ContainerImage { diff --git a/PetAdoptions/cdk/pet_stack/package.json b/PetAdoptions/cdk/pet_stack/package.json index 0355dd09..ec4f96fa 100644 --- a/PetAdoptions/cdk/pet_stack/package.json +++ b/PetAdoptions/cdk/pet_stack/package.json @@ -12,6 +12,7 @@ "cdk": "cdk" }, "dependencies": { + "@aws-cdk/aws-applicationsignals-alpha": "^2.204.0-alpha.0", "@aws-cdk/aws-lambda-python-alpha": "^2.204.0-alpha.0", "@aws-cdk/lambda-layer-kubectl-v31": "^2.0.3", "@types/js-yaml": "^4.0.9", diff --git a/PetAdoptions/cdk/pet_stack/resources/bunnies.zip b/PetAdoptions/cdk/pet_stack/resources/bunnies.zip index 7899ef7e..8aa01adb 100644 Binary files a/PetAdoptions/cdk/pet_stack/resources/bunnies.zip and b/PetAdoptions/cdk/pet_stack/resources/bunnies.zip differ diff --git a/PetAdoptions/cdk/pet_stack/resources/k8s_petsite/deployment.yaml b/PetAdoptions/cdk/pet_stack/resources/k8s_petsite/deployment.yaml index 1e02d0b9..1d73a392 100644 --- a/PetAdoptions/cdk/pet_stack/resources/k8s_petsite/deployment.yaml +++ b/PetAdoptions/cdk/pet_stack/resources/k8s_petsite/deployment.yaml @@ -39,6 +39,8 @@ spec: metadata: labels: app: petsite + annotations: + instrumentation.opentelemetry.io/inject-dotnet: "true" spec: serviceAccountName: petsite-sa containers: @@ -48,9 +50,6 @@ spec: ports: - containerPort: 80 protocol: TCP - env: - - name: AWS_XRAY_DAEMON_ADDRESS - value: xray-service.default:2000 --- apiVersion: elbv2.k8s.aws/v1beta1 kind: TargetGroupBinding diff --git a/PetAdoptions/cdk/pet_stack/resources/k8s_petsite/xray-daemon-config.yaml b/PetAdoptions/cdk/pet_stack/resources/k8s_petsite/xray-daemon-config.yaml deleted file mode 100644 index ef420312..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/k8s_petsite/xray-daemon-config.yaml +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -apiVersion: v1 -kind: ServiceAccount -metadata: - annotations: - eks.amazonaws.com/role-arn: XRAY_SA_ROLE - labels: - app: xray-daemon - name: xray-daemon - namespace: default ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: xray-daemon - labels: - app: xray-daemon -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: cluster-admin -subjects: -- kind: ServiceAccount - name: xray-daemon - namespace: default ---- -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: xray-daemon -spec: - updateStrategy: - type: RollingUpdate - selector: - matchLabels: - app: xray-daemon - template: - metadata: - labels: - app: xray-daemon - spec: - serviceAccountName: xray-daemon - volumes: - - name: config-volume - configMap: - name: xray-config - hostNetwork: true - containers: - - name: xray-daemon - image: public.ecr.aws/xray/aws-xray-daemon:3.3.3 - imagePullPolicy: Always - command: - - "/xray" - - "-c" - - "/aws/xray/config.yaml" - resources: - limits: - memory: 24Mi - ports: - - name: xray-ingest - containerPort: 2000 - hostPort: 2000 - protocol: UDP - volumeMounts: - - name: config-volume - mountPath: "/aws/xray" - readOnly: true ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: xray-config - namespace: default -data: - config.yaml: |- - TotalBufferSizeMB: 24 - Socket: - UDPAddress: "0.0.0.0:2000" - TCPAddress: "0.0.0.0:2000" - Version: 2 - Logging: - LogLevel: "info" ---- -apiVersion: v1 -kind: Service -metadata: - name: xray-service -spec: - selector: - app: xray-daemon - clusterIP: None - ports: - - name: incoming - port: 2000 - protocol: UDP diff --git a/PetAdoptions/cdk/pet_stack/resources/puppies.zip b/PetAdoptions/cdk/pet_stack/resources/puppies.zip index 6a7473a4..52daa93d 100644 Binary files a/PetAdoptions/cdk/pet_stack/resources/puppies.zip and b/PetAdoptions/cdk/pet_stack/resources/puppies.zip differ diff --git a/PetAdoptions/payforadoption-go/README.md b/PetAdoptions/payforadoption-go/README.md new file mode 100644 index 00000000..8ed591aa --- /dev/null +++ b/PetAdoptions/payforadoption-go/README.md @@ -0,0 +1,305 @@ +# Pay for Adoption Go Service + +This microservice handles the complete pet adoption workflow with a clean separation between real-time adoption processing and asynchronous history tracking. The service implements modern event-driven architecture patterns for optimal performance and reliability. + +## Architecture Overview + +``` +POST /completeadoption → payforadoption-go: +├── CreateTransaction() → transactions table (synchronous) +├── UpdateAvailability() → petstatus service (synchronous) +└── SendHistoryMessage() → SQS → pethistory → transaction_history table (async) +``` + +### Design Principles +- **Real-time adoption flow**: Critical operations (transaction + pet status) are synchronous +- **Asynchronous history tracking**: Non-critical historical data is processed in background +- **Clean domain boundaries**: Clear separation between current transactions and historical records +- **Resilient design**: History tracking failures don't impact adoption success + +## API Endpoints + +### Complete Adoption +`POST /api/completeadoption` + +Processes a complete pet adoption workflow including payment, database transaction, pet status update, and history tracking. + +**Query Parameters:** +- `petId` (required): The ID of the pet being adopted +- `petType` (required): The type of pet (e.g., "dog", "cat", "bunny") +- `userId` (required): The ID of the user adopting the pet + +**Response:** +```json +{ + "transactionid": "123e4567-e89b-12d3-a456-426614174000", + "petid": "pet123", + "pettype": "puppy", + "userid": "user456", + "adoptiondate": "2025-08-08T10:30:00Z" +} +``` + +**Processing Flow:** +1. **Generate transaction ID** - Creates unique UUID for adoption +2. **Create transaction record** - Writes to `transactions` table (synchronous) +3. **Update pet availability** - Calls petstatus service to mark pet as adopted (synchronous) +4. **Send history message** - Publishes to SQS for background processing (asynchronous) + +### Health Check +`GET /health/status` + +Returns the health status of the service. + +### Cleanup Adoptions +`POST /api/cleanupadoptions` + +Clears the current transactions table. Historical data is maintained separately by the pethistory service. + +### Trigger Seeding +`POST /api/triggerseeding` + +Seeds the DynamoDB table with sample pet data and creates the `transactions` SQL table. + +## Database Operations + +### Transactions Table +The service manages the current transactions table for real-time adoption data: + +```sql +CREATE TABLE IF NOT EXISTS transactions ( + id SERIAL PRIMARY KEY, + pet_id VARCHAR, + adoption_date DATE, + transaction_id VARCHAR, + user_id VARCHAR +); +``` + +### Repository Methods + +#### CreateTransaction +```go +func (r *repo) CreateTransaction(ctx context.Context, a Adoption) error +``` +- **Purpose**: Writes adoption record directly to transactions table +- **Synchronous**: Part of critical adoption path +- **Error handling**: Failures block adoption completion +- **Observability**: Traced and logged for monitoring + +#### SendHistoryMessage +```go +func (r *repo) SendHistoryMessage(ctx context.Context, a Adoption) error +``` +- **Purpose**: Sends adoption data to SQS for historical tracking +- **Asynchronous**: Non-blocking background operation +- **Error handling**: Failures logged but don't block adoption +- **Resilient**: History tracking is separate from adoption success + +## Event-Driven Architecture + +### SQS Integration +The service publishes adoption history messages to Amazon SQS for asynchronous processing by the `pethistory` Lambda function. + +#### Message Format +```json +{ + "transactionId": "123e4567-e89b-12d3-a456-426614174000", + "petId": "pet123", + "petType": "dog", + "userId": "user456", + "adoptionDate": "2025-08-08T10:30:00Z", + "timestamp": "2025-08-08T10:30:00Z" +} +``` + +#### Message Attributes +- `PetType`: For message filtering and routing +- `UserID`: For user-specific processing +- `TransactionID`: For correlation and deduplication + +#### Processing Flow +1. **payforadoption-go** → Publishes message to SQS +2. **SQS** → Triggers pethistory Lambda function +3. **pethistory** → Processes message and writes to `transaction_history` table + +### Benefits of Event-Driven Design +- ✅ **Performance**: Real-time adoption flow is fast (no SQS dependency) +- ✅ **Reliability**: History tracking failures don't break adoptions +- ✅ **Scalability**: Independent scaling of adoption vs history processing +- ✅ **Maintainability**: Clear domain boundaries and responsibilities +- ✅ **Observability**: Separate metrics and tracing for each concern + +## Error Handling & Resilience + +### Synchronous Operations (Critical Path) +- **Database transaction failures**: Block adoption and return error +- **Pet status update failures**: Block adoption and return error +- **Proper error propagation**: Client receives clear error messages + +### Asynchronous Operations (Non-Critical) +- **SQS message failures**: Logged as warnings but don't block adoption +- **History tracking issues**: Monitored separately from adoption success +- **Graceful degradation**: Adoption succeeds even if history tracking fails + +### Error Mode Scenarios +The service includes comprehensive error simulation for workshop scenarios: + +#### Memory Leak Mode (Bunny Adoptions) +```go +if petType == "bunny" { + if s.repository.ErrorModeOn(ctx) { + memoryLeak() + return a, errors.New("illegal memory allocation") + } +} +``` + +- **Trigger**: Adopting pets with type "bunny" when error mode is enabled +- **Behavior**: Simulates memory pressure and allocation failures +- **Purpose**: Demonstrates memory monitoring and alerting in AWS + +#### Error Mode Control +- **Parameter**: `/petstore/errormode1` in AWS Systems Manager Parameter Store +- **Values**: `"true"` (enabled) or `"false"` (disabled) +- **Scope**: Affects all bunny adoptions when enabled + +## Observability + +### CloudWatch Application Signals +- **Automatic instrumentation** with distributed tracing +- **Service maps** showing dependencies (database, petstatus, SQS) +- **Performance metrics** for each operation +- **Error tracking** and alerting + +### Key Metrics +- **Adoption success rate**: Percentage of successful adoptions +- **Database operation latency**: Transaction creation timing +- **Pet status update latency**: External service call timing +- **SQS message publish rate**: History message throughput +- **Error rates**: By operation type and error category + +### Distributed Tracing +- **End-to-end traces** from API call to database write +- **Service dependencies** clearly mapped +- **Performance bottlenecks** easily identified +- **Error correlation** across service boundaries + +## Configuration + +### Environment Variables +| Variable | Description | Example | +|----------|-------------|---------| +| `RDS_SECRET_ARN` | ARN of the RDS secret in Secrets Manager | `arn:aws:secretsmanager:us-west-2:123456789012:secret:rds-secret` | +| `SQS_QUEUE_URL` | URL of the SQS queue for history messages | `https://sqs.us-west-2.amazonaws.com/123456789012/adoption-history` | +| `UPDATE_ADOPTION_URL` | URL of the pet status updater service | `https://api.example.com/update-pet-status` | +| `DYNAMODB_TABLE` | DynamoDB table name for pet data | `PetAdoptions` | +| `S3_BUCKET_NAME` | S3 bucket for pet images | `pet-adoption-images` | + +### AWS Services Integration +- **Amazon RDS**: PostgreSQL database for transactions +- **Amazon SQS**: Message queue for history processing +- **Amazon DynamoDB**: Pet catalog and availability +- **AWS Secrets Manager**: Database credentials +- **AWS Systems Manager**: Configuration parameters +- **Amazon S3**: Pet image storage + +## Development + +### Building +```bash +go mod tidy +go build -o payforadoption . +``` + +### Running Locally +```bash +./payforadoption +``` + +### Testing +```bash +# Run unit tests +go test ./payforadoption -v + +# Run integration tests +go test ./payforadoption -tags=integration -v + +# Test with coverage +go test ./payforadoption -cover +``` + +### Local Development Setup +1. **PostgreSQL**: Local database for transactions table +2. **LocalStack**: Local AWS services (SQS, DynamoDB, S3) +3. **Environment variables**: Set required configuration +4. **Mock services**: Use test doubles for external dependencies + +## Deployment + +### ECS Deployment +The service runs on Amazon ECS with the following configuration: +- **Runtime**: Go 1.23 +- **Memory**: 512 MB +- **CPU**: 256 units +- **Health check**: `/health/status` endpoint +- **Auto-scaling**: Based on CPU and memory utilization + +### Docker Build +```bash +# Build for production (Linux/AMD64) +docker buildx build --platform linux/amd64 -t payforadoption:latest . + +# Local development +docker build -t payforadoption:dev . +``` + +### Infrastructure as Code +- **AWS CDK**: Primary deployment method +- **CloudFormation**: Generated from CDK +- **ECS Service**: Auto-scaling and load balancing +- **Application Load Balancer**: Traffic distribution +- **VPC**: Private subnets with NAT Gateway + +## Monitoring and Alerting + +### Key Metrics to Monitor +1. **Adoption Success Rate**: Should be > 95% +2. **API Response Time**: Should be < 2 seconds +3. **Database Connection Pool**: Monitor for exhaustion +4. **SQS Message Publish Rate**: History tracking throughput +5. **Error Rate by Pet Type**: Identify problematic scenarios + +### Recommended CloudWatch Alarms +- API error rate > 5% +- Database connection failures > 10/minute +- Average response time > 3 seconds +- SQS message publish failures > 1% +- Memory utilization > 80% + +### Application Signals Benefits +- **Zero-code instrumentation**: Automatic observability +- **Service dependency mapping**: Visual architecture understanding +- **Performance insights**: Bottleneck identification +- **Error correlation**: Root cause analysis +- **SLA monitoring**: Service level objective tracking + +## Workshop Learning Objectives + +This service demonstrates: +- ✅ **Event-driven architecture** with synchronous and asynchronous patterns +- ✅ **Domain separation** between real-time and historical data +- ✅ **Microservices communication** via REST APIs and message queues +- ✅ **Database integration** with PostgreSQL and connection management +- ✅ **Error handling strategies** for distributed systems +- ✅ **Observability patterns** with CloudWatch Application Signals +- ✅ **Resilience patterns** with graceful degradation +- ✅ **Modern Go development** with clean architecture principles + +Perfect for learning AWS observability tools and modern microservices patterns! 🚀 + +## Related Documentation +- [ERROR_MODE_GUIDE.md](ERROR_MODE_GUIDE.md) - Comprehensive error simulation scenarios +- [DATABASE_CONNECTION_EXHAUSTION.md](DATABASE_CONNECTION_EXHAUSTION.md) - Real database exhaustion patterns +- [EVENT_DRIVEN_ARCHITECTURE.md](EVENT_DRIVEN_ARCHITECTURE.md) - SQS integration details +- [REFACTORING_SUMMARY.md](REFACTORING_SUMMARY.md) - Architecture evolution and improvements \ No newline at end of file diff --git a/PetAdoptions/payforadoption-go/benchmark/benchmark.yaml b/PetAdoptions/payforadoption-go/benchmark/benchmark.yaml index 63914874..a7addc6b 100644 --- a/PetAdoptions/payforadoption-go/benchmark/benchmark.yaml +++ b/PetAdoptions/payforadoption-go/benchmark/benchmark.yaml @@ -12,7 +12,7 @@ plan: - name: Adopt Bunnies request: - url: /api/home/completeadoption?petId={{ item }}&petType=bunny + url: /api/completeadoption?petId={{ item }}&petType=bunny&userId=user007 method: POST body: "" with_items: @@ -24,7 +24,7 @@ plan: - name: Adopt Kittens request: - url: /api/home/completeadoption?petId={{ item }}&petType=kitten + url: /api/completeadoption?petId={{ item }}&petType=kitten&userId=user007 method: POST body: "" with_items: @@ -39,7 +39,7 @@ plan: - name: Adopt Puppies request: - url: /api/home/completeadoption?petId={{ item }}&petType=puppy + url: /api/completeadoption?petId={{ item }}&petType=puppy&userId=user007 method: POST body: "" with_items: @@ -62,6 +62,6 @@ plan: - name: Cleanup Adoptions request: - url: /api/home/cleanupadoptions + url: /api/cleanupadoptions method: POST body: "" diff --git a/PetAdoptions/payforadoption-go/config.go b/PetAdoptions/payforadoption-go/config.go index 7a0a500c..5da08a16 100644 --- a/PetAdoptions/payforadoption-go/config.go +++ b/PetAdoptions/payforadoption-go/config.go @@ -2,25 +2,16 @@ package main import ( "context" - "encoding/json" - "fmt" - "net/url" "petadoptions/payforadoption" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/secretsmanager" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/spf13/viper" ) -type dbConfig struct { - Engine, Host, Username, Password, Dbname string - Port int -} - func fetchConfig(ctx context.Context, logger log.Logger) (payforadoption.Config, error) { // fetch from env @@ -34,11 +25,12 @@ func fetchConfig(ctx context.Context, logger log.Logger) (payforadoption.Config, cfg := payforadoption.Config{ UpdateAdoptionURL: viper.GetString("UPDATE_ADOPTION_URL"), RDSSecretArn: viper.GetString("RDS_SECRET_ARN"), + SQSQueueURL: viper.GetString("SQS_QUEUE_URL"), AWSRegion: viper.GetString("AWS_REGION"), AWSCfg: awsCfg, } - if cfg.UpdateAdoptionURL == "" || cfg.RDSSecretArn == "" { + if cfg.UpdateAdoptionURL == "" || cfg.RDSSecretArn == "" || cfg.SQSQueueURL == "" { return fetchConfigFromParameterStore(ctx, cfg) } @@ -54,6 +46,7 @@ func fetchConfigFromParameterStore(ctx context.Context, cfg payforadoption.Confi "/petstore/rdssecretarn", "/petstore/s3bucketname", "/petstore/dynamodbtablename", + "/petstore/queueurl", }, }) @@ -77,50 +70,15 @@ func fetchConfigFromParameterStore(ctx context.Context, cfg payforadoption.Confi newCfg.S3BucketName = pValue case "/petstore/dynamodbtablename": newCfg.DynamoDBTable = pValue + case "/petstore/queueurl": + newCfg.SQSQueueURL = pValue } } return newCfg, err } -func getSecretValue(ctx context.Context, cfg payforadoption.Config) (string, error) { - svc := secretsmanager.NewFromConfig(cfg.AWSCfg) - res, err := svc.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ - SecretId: aws.String(cfg.RDSSecretArn), - }) - - if err != nil { - return "", err - } - - return aws.ToString(res.SecretString), nil -} - // Call aws secrets manager and return parsed sql server query str func getRDSConnectionString(ctx context.Context, cfg payforadoption.Config) (string, error) { - jsonstr, err := getSecretValue(ctx, cfg) - if err != nil { - return "", err - } - - var c dbConfig - - if err := json.Unmarshal([]byte(jsonstr), &c); err != nil { - return "", err - } - - u := &url.URL{ - Scheme: c.Engine, - User: url.UserPassword(c.Username, c.Password), - Host: fmt.Sprintf("%s:%d", c.Host, c.Port), - Path: c.Dbname, - } - - fmt.Println(u.String()) - - connStr := u.String() - connStr += "?sslmode=disable" - - // return u.String(), nil - return connStr, nil + return payforadoption.GetRDSConnectionString(ctx, cfg) } diff --git a/PetAdoptions/payforadoption-go/example_request.sh b/PetAdoptions/payforadoption-go/example_request.sh new file mode 100755 index 00000000..6b81e47f --- /dev/null +++ b/PetAdoptions/payforadoption-go/example_request.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Example script to test the payforadoption service with the new userid parameter + +# Set the service URL (adjust as needed) +SERVICE_URL="http://localhost:80" + +echo "Testing Complete Adoption API with User ID..." + +# Test the complete adoption endpoint with all required parameters +curl -X POST "${SERVICE_URL}/api/completeadoption?petId=pet123&petType=dog&userId=user456" \ + -H "Content-Type: application/json" \ + -w "\nHTTP Status: %{http_code}\n" \ + -s + +echo -e "\n\nTesting Health Check..." + +# Test health check +curl -X GET "${SERVICE_URL}/health/status" \ + -w "\nHTTP Status: %{http_code}\n" \ + -s + +echo -e "\n\nTesting with missing userId parameter (should return 400)..." + +# Test with missing userId parameter (should fail) +curl -X POST "${SERVICE_URL}/api/completeadoption?petId=pet123&petType=dog" \ + -H "Content-Type: application/json" \ + -w "\nHTTP Status: %{http_code}\n" \ + -s + +echo -e "\n\nTesting Error Mode scenarios (if enabled)..." + +# Test different pet types to see various degradation scenarios +echo "Testing bunny (critical failure):" +curl -X POST "${SERVICE_URL}/api/completeadoption?petId=bunny1&petType=bunny&userId=user1" \ + -H "Content-Type: application/json" \ + -w "\nHTTP Status: %{http_code}\n" \ + -s + +echo -e "\nTesting dog (intermittent failures):" +curl -X POST "${SERVICE_URL}/api/completeadoption?petId=dog1&petType=dog&userId=user2" \ + -H "Content-Type: application/json" \ + -w "\nHTTP Status: %{http_code}\n" \ + -s + +echo -e "\nTesting cat (REAL database connection exhaustion - will impact database!):" +curl -X POST "${SERVICE_URL}/api/completeadoption?petId=cat1&petType=cat&userId=user3" \ + -H "Content-Type: application/json" \ + -w "\nHTTP Status: %{http_code}\n" \ + -s + +echo -e "\nTesting impact during connection exhaustion (try another request immediately):" +curl -X POST "${SERVICE_URL}/api/completeadoption?petId=dog2&petType=dog&userId=user5" \ + -H "Content-Type: application/json" \ + -w "\nHTTP Status: %{http_code}\n" \ + -s + +echo -e "\nTesting fish (partial degradation):" +curl -X POST "${SERVICE_URL}/api/completeadoption?petId=fish1&petType=fish&userId=user4" \ + -H "Content-Type: application/json" \ + -w "\nHTTP Status: %{http_code}\n" \ + -s + +echo -e "\n\nNote: Error mode scenarios only activate when /petstore/errormode1 parameter is set to 'true'" +echo "Done!" \ No newline at end of file diff --git a/PetAdoptions/payforadoption-go/go.mod b/PetAdoptions/payforadoption-go/go.mod index 673e439e..0e1b5432 100644 --- a/PetAdoptions/payforadoption-go/go.mod +++ b/PetAdoptions/payforadoption-go/go.mod @@ -9,6 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.32.7 github.com/aws/aws-sdk-go-v2/config v1.28.7 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.8 + github.com/aws/aws-sdk-go-v2/service/sqs v1.37.2 github.com/aws/aws-sdk-go-v2/service/ssm v1.56.2 github.com/dghubble/sling v1.4.2 github.com/go-kit/kit v0.13.0 @@ -43,7 +44,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.6 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect github.com/aws/aws-sdk-go-v2/service/sns v1.33.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sqs v1.37.2 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect diff --git a/PetAdoptions/payforadoption-go/main.go b/PetAdoptions/payforadoption-go/main.go index 22832571..c229f9ca 100644 --- a/PetAdoptions/payforadoption-go/main.go +++ b/PetAdoptions/payforadoption-go/main.go @@ -126,14 +126,6 @@ func main() { os.Exit(-1) } - // Register DB stats to meter - err = otelsql.RegisterDBStatsMetrics(db, otelsql.WithAttributes( - semconv.DBSystemMySQL, - )) - if err != nil { - level.Error(logger).Log("RegisterDBStatsMetrics error", err) - } - defer db.Close() } diff --git a/PetAdoptions/payforadoption-go/payforadoption/benchmark_test.go b/PetAdoptions/payforadoption-go/payforadoption/benchmark_test.go new file mode 100644 index 00000000..d6ea7d6b --- /dev/null +++ b/PetAdoptions/payforadoption-go/payforadoption/benchmark_test.go @@ -0,0 +1,91 @@ +package payforadoption + +import ( + "context" + "testing" + "time" + + "github.com/go-kit/log" + "go.opentelemetry.io/otel/trace/noop" +) + +func BenchmarkCompleteAdoptionNormal(b *testing.B) { + logger := log.NewNopLogger() + repo := &mockRepository{} + tracer := noop.NewTracerProvider().Tracer("benchmark") + service := NewService(logger, repo, tracer) + + ctx := context.Background() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := service.CompleteAdoption(ctx, "pet123", "dog", "user456") + if err != nil { + b.Fatalf("Unexpected error: %v", err) + } + } +} + +func BenchmarkCompleteAdoptionWithErrorMode(b *testing.B) { + logger := log.NewNopLogger() + repo := &mockRepository{ + errorModeEnabled: true, + } + tracer := noop.NewTracerProvider().Tracer("benchmark") + service := NewService(logger, repo, tracer) + + ctx := context.Background() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + // Error mode will sometimes fail, sometimes succeed + service.CompleteAdoption(ctx, "pet123", "dog", "user456") + } +} + +func BenchmarkDegradationScenarios(b *testing.B) { + logger := log.NewNopLogger() + startTime := time.Now() + adoption := Adoption{ + TransactionID: "bench-123", + PetID: "pet123", + PetType: "dog", + UserID: "user456", + AdoptionDate: time.Now(), + } + ctx := context.Background() + + b.Run("DefaultDegradation", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + defaultDegradation(ctx, logger, adoption, startTime) + } + }) + + b.Run("CircuitBreakerDegradation", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + circuitBreakerDegradation(ctx, logger, adoption, startTime) + } + }) + + b.Run("SystemStressDegradation", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + systemStressDegradation(ctx, logger, adoption, startTime) + } + }) +} + +func BenchmarkDatabaseConfigService(b *testing.B) { + cfg := Config{ + RDSSecretArn: "arn:aws:secretsmanager:us-west-2:123456789012:secret:test-secret", + AWSRegion: "us-west-2", + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + dbSvc := NewDatabaseConfigService(cfg) + _ = dbSvc + } +} diff --git a/PetAdoptions/payforadoption-go/payforadoption/database.go b/PetAdoptions/payforadoption-go/payforadoption/database.go new file mode 100644 index 00000000..7c200041 --- /dev/null +++ b/PetAdoptions/payforadoption-go/payforadoption/database.go @@ -0,0 +1,90 @@ +package payforadoption + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" +) + +// DatabaseConfig represents the database configuration from AWS Secrets Manager +type DatabaseConfig struct { + Engine, Host, Username, Password, Dbname string + Port int +} + +// DatabaseConfigService handles database configuration operations +type DatabaseConfigService struct { + cfg Config +} + +// NewDatabaseConfigService creates a new database configuration service +func NewDatabaseConfigService(cfg Config) *DatabaseConfigService { + return &DatabaseConfigService{cfg: cfg} +} + +// GetSecretValue retrieves the RDS secret from AWS Secrets Manager +func (dcs *DatabaseConfigService) GetSecretValue(ctx context.Context) (string, error) { + svc := secretsmanager.NewFromConfig(dcs.cfg.AWSCfg) + res, err := svc.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ + SecretId: aws.String(dcs.cfg.RDSSecretArn), + }) + + if err != nil { + return "", err + } + + return aws.ToString(res.SecretString), nil +} + +// GetDatabaseConfig retrieves and parses the database configuration +func (dcs *DatabaseConfigService) GetDatabaseConfig(ctx context.Context) (*DatabaseConfig, error) { + jsonstr, err := dcs.GetSecretValue(ctx) + if err != nil { + return nil, err + } + + var config DatabaseConfig + if err := json.Unmarshal([]byte(jsonstr), &config); err != nil { + return nil, err + } + + return &config, nil +} + +// GetConnectionString builds the PostgreSQL connection string from AWS Secrets Manager +func (dcs *DatabaseConfigService) GetConnectionString(ctx context.Context) (string, error) { + config, err := dcs.GetDatabaseConfig(ctx) + if err != nil { + return "", err + } + + u := &url.URL{ + Scheme: config.Engine, + User: url.UserPassword(config.Username, config.Password), + Host: fmt.Sprintf("%s:%d", config.Host, config.Port), + Path: config.Dbname, + } + + connStr := u.String() + connStr += "?sslmode=disable" + + return connStr, nil +} + +// Package-level convenience functions for backward compatibility + +// GetSecretValue is a package-level function for retrieving secrets +func GetSecretValue(ctx context.Context, cfg Config) (string, error) { + dcs := NewDatabaseConfigService(cfg) + return dcs.GetSecretValue(ctx) +} + +// GetRDSConnectionString is a package-level function for getting connection strings +func GetRDSConnectionString(ctx context.Context, cfg Config) (string, error) { + dcs := NewDatabaseConfigService(cfg) + return dcs.GetConnectionString(ctx) +} diff --git a/PetAdoptions/payforadoption-go/payforadoption/database_test.go b/PetAdoptions/payforadoption-go/payforadoption/database_test.go new file mode 100644 index 00000000..39b436ec --- /dev/null +++ b/PetAdoptions/payforadoption-go/payforadoption/database_test.go @@ -0,0 +1,126 @@ +package payforadoption + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +func TestNewDatabaseConfigService(t *testing.T) { + cfg := Config{ + RDSSecretArn: "arn:aws:secretsmanager:us-west-2:123456789012:secret:test-secret", + AWSRegion: "us-west-2", + AWSCfg: aws.Config{Region: "us-west-2"}, + } + + dbSvc := NewDatabaseConfigService(cfg) + + if dbSvc == nil { + t.Fatal("Expected DatabaseConfigService to be created") + } + + if dbSvc.cfg.RDSSecretArn != cfg.RDSSecretArn { + t.Errorf("Expected RDSSecretArn %s, got %s", cfg.RDSSecretArn, dbSvc.cfg.RDSSecretArn) + } + + if dbSvc.cfg.AWSRegion != cfg.AWSRegion { + t.Errorf("Expected AWSRegion %s, got %s", cfg.AWSRegion, dbSvc.cfg.AWSRegion) + } +} + +func TestDatabaseConfigServiceMethods(t *testing.T) { + cfg := Config{ + RDSSecretArn: "arn:aws:secretsmanager:us-west-2:123456789012:secret:test-secret", + AWSRegion: "us-west-2", + // Note: AWSCfg would need to be properly configured for real AWS calls + } + + dbSvc := NewDatabaseConfigService(cfg) + ctx := context.Background() + + // Test GetSecretValue (will fail without real AWS config, but tests the method exists) + _, err := dbSvc.GetSecretValue(ctx) + if err == nil { + t.Log("GetSecretValue method exists and can be called") + } else { + t.Logf("GetSecretValue failed as expected without real AWS config: %v", err) + } + + // Test GetDatabaseConfig (will fail without real AWS config, but tests the method exists) + _, err = dbSvc.GetDatabaseConfig(ctx) + if err == nil { + t.Log("GetDatabaseConfig method exists and can be called") + } else { + t.Logf("GetDatabaseConfig failed as expected without real AWS config: %v", err) + } + + // Test GetConnectionString (will fail without real AWS config, but tests the method exists) + _, err = dbSvc.GetConnectionString(ctx) + if err == nil { + t.Log("GetConnectionString method exists and can be called") + } else { + t.Logf("GetConnectionString failed as expected without real AWS config: %v", err) + } +} + +func TestPackageLevelFunctions(t *testing.T) { + cfg := Config{ + RDSSecretArn: "arn:aws:secretsmanager:us-west-2:123456789012:secret:test-secret", + AWSRegion: "us-west-2", + } + + ctx := context.Background() + + // Test package-level GetSecretValue function + _, err := GetSecretValue(ctx, cfg) + if err == nil { + t.Log("Package-level GetSecretValue function exists") + } else { + t.Logf("Package-level GetSecretValue failed as expected: %v", err) + } + + // Test package-level GetRDSConnectionString function + _, err = GetRDSConnectionString(ctx, cfg) + if err == nil { + t.Log("Package-level GetRDSConnectionString function exists") + } else { + t.Logf("Package-level GetRDSConnectionString failed as expected: %v", err) + } +} + +func TestDatabaseConfigParsing(t *testing.T) { + // Test the DatabaseConfig struct + config := DatabaseConfig{ + Engine: "postgres", + Host: "localhost", + Port: 5432, + Username: "testuser", + Password: "testpass", + Dbname: "testdb", + } + + if config.Engine != "postgres" { + t.Errorf("Expected Engine postgres, got %s", config.Engine) + } + + if config.Host != "localhost" { + t.Errorf("Expected Host localhost, got %s", config.Host) + } + + if config.Port != 5432 { + t.Errorf("Expected Port 5432, got %d", config.Port) + } + + if config.Username != "testuser" { + t.Errorf("Expected Username testuser, got %s", config.Username) + } + + if config.Password != "testpass" { + t.Errorf("Expected Password testpass, got %s", config.Password) + } + + if config.Dbname != "testdb" { + t.Errorf("Expected Dbname testdb, got %s", config.Dbname) + } +} diff --git a/PetAdoptions/payforadoption-go/payforadoption/endpoint.go b/PetAdoptions/payforadoption-go/payforadoption/endpoint.go index 04a2c654..a0858fac 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/endpoint.go +++ b/PetAdoptions/payforadoption-go/payforadoption/endpoint.go @@ -31,7 +31,7 @@ func makeHealthCheckEndpoint(s Service) endpoint.Endpoint { func makeCompleteAdoptionEndpoint(s Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(completeAdoptionRequest) - return s.CompleteAdoption(ctx, req.PetId, req.PetType) + return s.CompleteAdoption(ctx, req.PetId, req.PetType, req.UserID) } } diff --git a/PetAdoptions/payforadoption-go/payforadoption/middlewares.go b/PetAdoptions/payforadoption-go/payforadoption/middlewares.go index 7621ad1e..41464598 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/middlewares.go +++ b/PetAdoptions/payforadoption-go/payforadoption/middlewares.go @@ -38,7 +38,7 @@ func NewInstrumenting(logger log.Logger, s Service) Service { } } -func (mw *middleware) CompleteAdoption(ctx context.Context, petId, petType string) (a Adoption, err error) { +func (mw *middleware) CompleteAdoption(ctx context.Context, petId, petType, userID string) (a Adoption, err error) { defer func(begin time.Time) { labelValues := []string{ @@ -54,6 +54,7 @@ func (mw *middleware) CompleteAdoption(ctx context.Context, petId, petType strin span.SetAttributes( attribute.String("PetId", petId), attribute.String("PetType", petType), + attribute.String("UserID", userID), attribute.Float64("TimeTakenSeconds", time.Since(begin).Seconds()), ) @@ -62,12 +63,13 @@ func (mw *middleware) CompleteAdoption(ctx context.Context, petId, petType strin "traceId", span.SpanContext().SpanID(), "PetId", petId, "PetType", petType, + "UserID", userID, "took", time.Since(begin), "customer", getFakeCustomer(), "err", err) }(time.Now()) - return mw.Service.CompleteAdoption(ctx, petId, petType) + return mw.Service.CompleteAdoption(ctx, petId, petType, userID) } func (mw *middleware) CleanupAdoptions(ctx context.Context) (err error) { diff --git a/PetAdoptions/payforadoption-go/payforadoption/repository.go b/PetAdoptions/payforadoption-go/payforadoption/repository.go index a56696c5..20a71863 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/repository.go +++ b/PetAdoptions/payforadoption-go/payforadoption/repository.go @@ -12,6 +12,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/aws/aws-sdk-go-v2/service/sqs/types" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/dghubble/sling" "github.com/go-kit/log" @@ -24,11 +26,13 @@ import ( // Repository as an interface to define data store interactions type Repository interface { CreateTransaction(ctx context.Context, a Adoption) error + SendHistoryMessage(ctx context.Context, a Adoption) error DropTransactions(ctx context.Context) error UpdateAvailability(ctx context.Context, a Adoption) error TriggerSeeding(ctx context.Context) error CreateSQLTables(ctx context.Context) error ErrorModeOn(ctx context.Context) bool + GetConnectionString(ctx context.Context) (string, error) } type Config struct { @@ -36,6 +40,7 @@ type Config struct { RDSSecretArn string S3BucketName string DynamoDBTable string + SQSQueueURL string AWSRegion string Tracer trace.Tracer AWSCfg aws.Config @@ -48,6 +53,7 @@ type repo struct { db *sql.DB cfg Config logger log.Logger + dbSvc *DatabaseConfigService } func NewRepository(db *sql.DB, cfg Config, logger log.Logger) Repository { @@ -55,39 +61,104 @@ func NewRepository(db *sql.DB, cfg Config, logger log.Logger) Repository { db: db, cfg: cfg, logger: log.With(logger, "repo", "sql"), + dbSvc: NewDatabaseConfigService(cfg), } } func (r *repo) CreateTransaction(ctx context.Context, a Adoption) error { + span := trace.SpanFromContext(ctx) + span.AddEvent("creating transaction in PG DB") - sql := ` - INSERT INTO transactions (pet_id, transaction_id, adoption_date) - VALUES ($1, $2, $3) - ` + sql := `INSERT INTO transactions (pet_id, adoption_date, transaction_id, user_id) VALUES ($1, $2, $3, $4)` r.logger.Log("sql", sql) - _, err := r.db.ExecContext(ctx, sql, a.PetID, a.TransactionID, a.AdoptionDate) + _, err := r.db.ExecContext(ctx, sql, a.PetID, a.AdoptionDate, a.TransactionID, a.UserID) + if err != nil { + span.RecordError(err) + level.Error(r.logger).Log("error", "failed to create transaction", "err", err) + return err + } + + level.Info(r.logger).Log( + "action", "transaction_created", + "transactionId", a.TransactionID, + "petId", a.PetID, + "userId", a.UserID, + ) + + return nil +} +func (r *repo) SendHistoryMessage(ctx context.Context, a Adoption) error { + // Create SQS client + sqsClient := sqs.NewFromConfig(r.cfg.AWSCfg) + + // Prepare the adoption history message + historyMessage := map[string]interface{}{ + "transactionId": a.TransactionID, + "petId": a.PetID, + "petType": a.PetType, + "userId": a.UserID, + "adoptionDate": a.AdoptionDate.Format(time.RFC3339), + "timestamp": time.Now().Format(time.RFC3339), + } + + // Convert to JSON + messageBody, err := json.Marshal(historyMessage) if err != nil { + level.Error(r.logger).Log("error", "failed to marshal history message", "err", err) return err } + + // Send message to SQS + input := &sqs.SendMessageInput{ + QueueUrl: aws.String(r.cfg.SQSQueueURL), + MessageBody: aws.String(string(messageBody)), + MessageAttributes: map[string]types.MessageAttributeValue{ + "PetType": { + DataType: aws.String("String"), + StringValue: aws.String(a.PetType), + }, + "UserID": { + DataType: aws.String("String"), + StringValue: aws.String(a.UserID), + }, + "TransactionID": { + DataType: aws.String("String"), + StringValue: aws.String(a.TransactionID), + }, + }, + } + + result, err := sqsClient.SendMessage(ctx, input) + if err != nil { + level.Error(r.logger).Log("error", "failed to send history message to SQS", "err", err, "queueUrl", r.cfg.SQSQueueURL) + return err + } + + level.Info(r.logger).Log( + "action", "history_message_sent", + "messageId", aws.ToString(result.MessageId), + "queueUrl", r.cfg.SQSQueueURL, + "transactionId", a.TransactionID, + "petId", a.PetID, + "userId", a.UserID, + ) + return nil } func (r *repo) DropTransactions(ctx context.Context) error { span := trace.SpanFromContext(ctx) - span.AddEvent("saving history and removing transctions in PG DB") + span.AddEvent("removing transactions in PG DB") - sql := []string{`INSERT INTO transactions_history SELECT * FROM transactions`, - `DELETE FROM transactions`} + sql := `DELETE FROM transactions` - for _, s := range sql { - r.logger.Log("sql", s) - _, err := r.db.ExecContext(ctx, s) - if err != nil { - span.RecordError(err) - return err - } + r.logger.Log("sql", sql) + _, err := r.db.ExecContext(ctx, sql) + if err != nil { + span.RecordError(err) + return err } return nil @@ -110,7 +181,7 @@ func (r *repo) UpdateAvailability(ctx context.Context, a Adoption) error { updateAdoptionStatusCtx, updateAdoptionStatusSpan := r.cfg.Tracer.Start(ctx, "Update Adoption Status") defer updateAdoptionStatusSpan.End() - body := &completeAdoptionRequest{a.PetID, a.PetType} + body := &completeAdoptionRequest{a.PetID, a.PetType, a.UserID} req, _ := sling.New().Put(r.cfg.UpdateAdoptionURL).BodyJSON(body).Request() resp, err := client.Do(req.WithContext(updateAdoptionStatusCtx)) if err != nil { @@ -243,31 +314,25 @@ func (r *repo) ErrorModeOn(ctx context.Context) bool { } func (r *repo) CreateSQLTables(ctx context.Context) error { - sql := []string{ - `CREATE TABLE IF NOT EXISTS transactions ( - id SERIAL PRIMARY KEY, - pet_id VARCHAR, - adoption_date DATE, - transaction_id VARCHAR - ); - `, - `CREATE TABLE IF NOT EXISTS transactions_history ( - id SERIAL PRIMARY KEY, - pet_id VARCHAR, - adoption_date DATE, - transaction_id VARCHAR - ); - `} - - var err error = nil - - for _, s := range sql { - r.logger.Log("sql", s) - _, err = r.db.ExecContext(ctx, s) - if err != nil { - return err - } + // cSpell:ignore VARCHAR + sql := `CREATE TABLE IF NOT EXISTS transactions ( + id SERIAL PRIMARY KEY, + pet_id VARCHAR, + adoption_date DATE, + transaction_id VARCHAR, + user_id VARCHAR + );` + + r.logger.Log("sql", sql) + _, err := r.db.ExecContext(ctx, sql) + if err != nil { + return err } - return err + return nil +} + +// GetConnectionString retrieves the database connection string for error mode scenarios +func (r *repo) GetConnectionString(ctx context.Context) (string, error) { + return r.dbSvc.GetConnectionString(ctx) } diff --git a/PetAdoptions/payforadoption-go/payforadoption/service.go b/PetAdoptions/payforadoption-go/payforadoption/service.go index 3568ae18..18d98bf5 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/service.go +++ b/PetAdoptions/payforadoption-go/payforadoption/service.go @@ -2,8 +2,6 @@ package payforadoption import ( "context" - "errors" - "runtime" "time" "github.com/go-kit/log" @@ -13,16 +11,17 @@ import ( ) type Adoption struct { - TransactionID string `json:"transactionid,omitempty"` - PetID string `json:"petid,omitempty"` - PetType string `json:"pettype,omitempty"` - AdoptionDate time.Time + TransactionID string `json:"transactionid,omitempty"` + PetID string `json:"petid,omitempty"` + PetType string `json:"pettype,omitempty"` + UserID string `json:"userid,omitempty"` + AdoptionDate time.Time `json:"adoptiondate,omitempty"` } // links endpoints to transport type Service interface { HealthCheck(ctx context.Context) error - CompleteAdoption(ctx context.Context, petId, petType string) (Adoption, error) + CompleteAdoption(ctx context.Context, petId, petType, userID string) (Adoption, error) CleanupAdoptions(ctx context.Context) error TriggerSeeding(ctx context.Context) error } @@ -49,7 +48,7 @@ func (s service) HealthCheck(ctx context.Context) error { } // /api/completeadoption logic -func (s service) CompleteAdoption(ctx context.Context, petId, petType string) (Adoption, error) { +func (s service) CompleteAdoption(ctx context.Context, petId, petType, userID string) (Adoption, error) { logger := log.With(s.logger, "method", "CompleteAdoption") uuid, _ := uuid.NewV4() @@ -57,28 +56,47 @@ func (s service) CompleteAdoption(ctx context.Context, petId, petType string) (A TransactionID: uuid.String(), PetID: petId, PetType: petType, + UserID: userID, AdoptionDate: time.Now(), } - // Introduce memory leaks for pettype bunnies. Sorry bunnies :) - if petType == "bunny" { - if s.repository.ErrorModeOn(ctx) { - level.Error(logger).Log("errorMode", "On") - memoryLeak() - return a, errors.New("illegal memory allocation") - } else { - level.Error(logger).Log("errorMode", "Off") + // Introduce degraded experience when error mode is enabled + if s.repository.ErrorModeOn(ctx) { + level.Error(logger).Log("errorMode", "On", "petType", petType, "userID", userID) + + startTime := time.Now() + + // Apply different degradation strategies + result := handleDefaultDegradation(ctx, logger, a, startTime, s.repository) + + // Return the result from the degradation scenario + if result.Error != nil { + return result.Adoption, result.Error } + + // Update the adoption with any modifications from degradation + a = result.Adoption } + // Step 1: Create transaction in database (synchronous) if err := s.repository.CreateTransaction(ctx, a); err != nil { - level.Error(logger).Log("err", err) + level.Error(logger).Log("err", err, "action", "create_transaction_failed") return Adoption{}, err } - err := s.repository.UpdateAvailability(ctx, a) + // Step 2: Update pet availability (synchronous) + if err := s.repository.UpdateAvailability(ctx, a); err != nil { + level.Error(logger).Log("err", err, "action", "update_availability_failed") + return Adoption{}, err + } + + // Step 3: Send history message to SQS (asynchronous - don't fail if this fails) + if err := s.repository.SendHistoryMessage(ctx, a); err != nil { + level.Warn(logger).Log("err", err, "action", "send_history_message_failed", "note", "continuing despite history message failure") + // Don't return error - history tracking is not critical for adoption success + } - return a, err + return a, nil } func (s service) CleanupAdoptions(ctx context.Context) error { @@ -111,26 +129,3 @@ func (s service) TriggerSeeding(ctx context.Context) error { return nil } - -func memoryLeak() { - - // loosing time - time.Sleep(time.Duration(1000 * time.Millisecond)) - - type T struct { - v [2 << 20]int - t *T - } - - var finalizer = func(t *T) {} - - var x, y T - - // The SetFinalizer call makes x escape to heap. - runtime.SetFinalizer(&x, finalizer) - - // The following line forms a cyclic reference - // group with two members, x and y. - // This causes x and y are not collectable. - x.t, y.t = &y, &x // y also escapes to heap. -} diff --git a/PetAdoptions/payforadoption-go/payforadoption/service_test.go b/PetAdoptions/payforadoption-go/payforadoption/service_test.go new file mode 100644 index 00000000..e36ab27e --- /dev/null +++ b/PetAdoptions/payforadoption-go/payforadoption/service_test.go @@ -0,0 +1,341 @@ +package payforadoption + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/go-kit/log" + "go.opentelemetry.io/otel/trace/noop" +) + +// Mock repository for testing +type mockRepository struct { + errorModeEnabled bool + createTransactionErr error + updateAvailabilityErr error + sendHistoryMessageErr error + connectionString string +} + +func (m *mockRepository) CreateTransaction(ctx context.Context, a Adoption) error { + return m.createTransactionErr +} + +func (m *mockRepository) SendHistoryMessage(ctx context.Context, a Adoption) error { + return m.sendHistoryMessageErr +} + +func (m *mockRepository) DropTransactions(ctx context.Context) error { + return nil +} + +func (m *mockRepository) UpdateAvailability(ctx context.Context, a Adoption) error { + return m.updateAvailabilityErr +} + +func (m *mockRepository) TriggerSeeding(ctx context.Context) error { + return nil +} + +func (m *mockRepository) CreateSQLTables(ctx context.Context) error { + return nil +} + +func (m *mockRepository) GetConnectionString(ctx context.Context) (string, error) { + if m.connectionString != "" { + return m.connectionString, nil + } + return "postgres://user:pass@localhost:5432/testdb?sslmode=disable", nil +} + +func (m *mockRepository) ErrorModeOn(ctx context.Context) bool { + return m.errorModeEnabled +} + +func TestCompleteAdoptionSuccess(t *testing.T) { + logger := log.NewNopLogger() + repo := &mockRepository{} + tracer := noop.NewTracerProvider().Tracer("test") + + service := NewService(logger, repo, tracer) + + ctx := context.Background() + petID := "pet123" + petType := "dog" + userID := "user456" + + adoption, err := service.CompleteAdoption(ctx, petID, petType, userID) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if adoption.PetID != petID { + t.Errorf("Expected PetID %s, got %s", petID, adoption.PetID) + } + + if adoption.PetType != petType { + t.Errorf("Expected PetType %s, got %s", petType, adoption.PetType) + } + + if adoption.UserID != userID { + t.Errorf("Expected UserID %s, got %s", userID, adoption.UserID) + } + + if adoption.TransactionID == "" { + t.Error("Expected TransactionID to be generated") + } + + if adoption.AdoptionDate.IsZero() { + t.Error("Expected AdoptionDate to be set") + } + + // Verify the adoption date is recent (within last minute) + if time.Since(adoption.AdoptionDate) > time.Minute { + t.Error("Expected AdoptionDate to be recent") + } +} + +func TestCompleteAdoptionCreateTransactionFailure(t *testing.T) { + logger := log.NewNopLogger() + repo := &mockRepository{ + createTransactionErr: errors.New("database connection failed"), + } + tracer := noop.NewTracerProvider().Tracer("test") + + service := NewService(logger, repo, tracer) + + ctx := context.Background() + _, err := service.CompleteAdoption(ctx, "pet123", "dog", "user456") + + if err == nil { + t.Fatal("Expected error when CreateTransaction fails") + } + + if err.Error() != "database connection failed" { + t.Errorf("Expected specific error message, got %v", err) + } +} + +func TestCompleteAdoptionUpdateAvailabilityFailure(t *testing.T) { + logger := log.NewNopLogger() + repo := &mockRepository{ + updateAvailabilityErr: errors.New("pet status service unavailable"), + } + tracer := noop.NewTracerProvider().Tracer("test") + + service := NewService(logger, repo, tracer) + + ctx := context.Background() + _, err := service.CompleteAdoption(ctx, "pet123", "dog", "user456") + + if err == nil { + t.Fatal("Expected error when UpdateAvailability fails") + } + + if err.Error() != "pet status service unavailable" { + t.Errorf("Expected specific error message, got %v", err) + } +} + +func TestCompleteAdoptionHistoryMessageFailure(t *testing.T) { + logger := log.NewNopLogger() + repo := &mockRepository{ + sendHistoryMessageErr: errors.New("SQS unavailable"), + } + tracer := noop.NewTracerProvider().Tracer("test") + + service := NewService(logger, repo, tracer) + + ctx := context.Background() + adoption, err := service.CompleteAdoption(ctx, "pet123", "dog", "user456") + + // Should succeed even if history message fails + if err != nil { + t.Fatalf("Expected no error when history message fails, got %v", err) + } + + if adoption.PetID != "pet123" { + t.Errorf("Expected adoption to be processed despite history failure") + } +} + +func TestCompleteAdoptionWithErrorMode(t *testing.T) { + logger := log.NewNopLogger() + repo := &mockRepository{ + errorModeEnabled: true, + } + tracer := noop.NewTracerProvider().Tracer("test") + + service := NewService(logger, repo, tracer) + + ctx := context.Background() + + // Test multiple times to potentially hit different degradation scenarios + for i := 0; i < 5; i++ { + _, err := service.CompleteAdoption(ctx, "pet123", "dog", "user456") + + // Error mode should sometimes cause failures + // We don't assert specific errors since they're randomized + if err != nil { + t.Logf("Error mode triggered failure (expected): %v", err) + } else { + t.Logf("Error mode allowed success (possible)") + } + } +} +func TestDatabaseConfigService(t *testing.T) { + // Test the database configuration service + cfg := Config{ + RDSSecretArn: "test-secret-arn", + // AWSCfg would normally be set, but we're testing the structure + } + + dbSvc := NewDatabaseConfigService(cfg) + if dbSvc == nil { + t.Fatal("Expected DatabaseConfigService to be created") + } + + // Test that the service holds the correct config + if dbSvc.cfg.RDSSecretArn != "test-secret-arn" { + t.Errorf("Expected RDSSecretArn to be set correctly") + } +} + +func TestDatabaseConnectionExhaustion(t *testing.T) { + logger := log.NewNopLogger() + + // Test the connection exhauster directly + exhauster := NewDatabaseConnectionExhauster(logger) + + // Test with a mock connection string (this won't actually connect) + mockConnStr := "postgres://user:pass@nonexistent:5432/testdb?sslmode=disable" + + // This should fail gracefully since the host doesn't exist + err := exhauster.ExhaustConnections(context.Background(), mockConnStr, 2) + + // We expect this to fail since the connection string points to a non-existent host + if err == nil { + t.Error("Expected connection exhaustion to fail with non-existent host") + } + + // Verify connection count (should be 0 since connections failed) + count := exhauster.GetConnectionCount() + if count != 0 { + t.Errorf("Expected 0 connections after failure, got %d", count) + } + + // Test cleanup (should not panic even with no connections) + exhauster.ReleaseConnections() +} + +func TestDegradationScenarios(t *testing.T) { + logger := log.NewNopLogger() + startTime := time.Now() + adoption := Adoption{ + TransactionID: "test-123", + PetID: "pet123", + PetType: "dog", + UserID: "user456", + AdoptionDate: time.Now(), + } + + ctx := context.Background() + + t.Run("DefaultDegradation", func(t *testing.T) { + result := defaultDegradation(ctx, logger, adoption, startTime) + + if result.Error != nil { + t.Errorf("Default degradation should not return error, got %v", result.Error) + } + + if result.Duration == 0 { + t.Error("Expected duration to be recorded") + } + + if result.Adoption.TransactionID != adoption.TransactionID { + t.Error("Expected adoption to be preserved") + } + }) + + t.Run("CircuitBreakerDegradation", func(t *testing.T) { + result := circuitBreakerDegradation(ctx, logger, adoption, startTime) + + if result.Error == nil { + t.Error("Circuit breaker degradation should return error") + } + + if result.Duration == 0 { + t.Error("Expected duration to be recorded") + } + }) + + t.Run("SystemStressDegradation", func(t *testing.T) { + result := systemStressDegradation(ctx, logger, adoption, startTime) + + if result.Error == nil { + t.Error("System stress degradation should return error") + } + + if result.Duration == 0 { + t.Error("Expected duration to be recorded") + } + }) + + t.Run("DatabaseConnectionDegradation", func(t *testing.T) { + repo := &mockRepository{ + connectionString: "postgres://user:pass@nonexistent:5432/testdb?sslmode=disable", + } + + result := databaseConnectionDegradation(ctx, logger, adoption, startTime, repo) + + if result.Error == nil { + t.Error("Database connection degradation should return error") + } + + if result.Duration == 0 { + t.Error("Expected duration to be recorded") + } + }) +} + +func TestHealthCheck(t *testing.T) { + logger := log.NewNopLogger() + repo := &mockRepository{} + tracer := noop.NewTracerProvider().Tracer("test") + + service := NewService(logger, repo, tracer) + + err := service.HealthCheck(context.Background()) + if err != nil { + t.Errorf("HealthCheck should always return nil, got %v", err) + } +} + +func TestCleanupAdoptions(t *testing.T) { + logger := log.NewNopLogger() + repo := &mockRepository{} + tracer := noop.NewTracerProvider().Tracer("test") + + service := NewService(logger, repo, tracer) + + err := service.CleanupAdoptions(context.Background()) + if err != nil { + t.Errorf("CleanupAdoptions should succeed with mock repo, got %v", err) + } +} + +func TestTriggerSeeding(t *testing.T) { + logger := log.NewNopLogger() + repo := &mockRepository{} + tracer := noop.NewTracerProvider().Tracer("test") + + service := NewService(logger, repo, tracer) + + err := service.TriggerSeeding(context.Background()) + if err != nil { + t.Errorf("TriggerSeeding should succeed with mock repo, got %v", err) + } +} diff --git a/PetAdoptions/payforadoption-go/payforadoption/transport.go b/PetAdoptions/payforadoption-go/payforadoption/transport.go index f146a9ff..26f2f264 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/transport.go +++ b/PetAdoptions/payforadoption-go/payforadoption/transport.go @@ -47,14 +47,14 @@ func MakeHTTPHandler(s Service, logger log.Logger) http.Handler { options..., )) - r.Methods("POST").Path("/api/home/completeadoption").Handler(httptransport.NewServer( + r.Methods("POST").Path("/api/completeadoption").Handler(httptransport.NewServer( e.CompleteAdoptionEndpoint, decodeCompleteAdoptionRequest, encodeResponse, options..., )) - r.Methods("POST").Path("/api/home/cleanupadoptions").Handler(httptransport.NewServer( + r.Methods("POST").Path("/api/cleanupadoptions").Handler(httptransport.NewServer( e.CleanupAdoptionsEndpoint, decodeEmptyRequest, encodeEmptyResponse, @@ -62,7 +62,7 @@ func MakeHTTPHandler(s Service, logger log.Logger) http.Handler { )) // Trigger DDB seeding - r.Methods("POST").Path("/api/home/triggerseeding").Handler(httptransport.NewServer( + r.Methods("POST").Path("/api/triggerseeding").Handler(httptransport.NewServer( e.TriggerSeedingEndpoint, decodeEmptyRequest, encodeEmptyResponse, @@ -81,6 +81,7 @@ type errorer interface { type completeAdoptionRequest struct { PetId string `json:"petid"` PetType string `json:"pettype"` + UserID string `json:"userid"` } var ( @@ -96,12 +97,13 @@ func decodeCompleteAdoptionRequest(_ context.Context, r *http.Request) (interfac petId := r.URL.Query().Get("petId") petType := r.URL.Query().Get("petType") + userID := r.URL.Query().Get("userId") - if petId == "" || petType == "" { + if petId == "" || petType == "" || userID == "" { return nil, ErrBadRequest } - return completeAdoptionRequest{petId, petType}, nil + return completeAdoptionRequest{petId, petType, userID}, nil } func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { diff --git a/PetAdoptions/payforadoption-go/payforadoption/utils.go b/PetAdoptions/payforadoption-go/payforadoption/utils.go index 8c23515a..5508525f 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/utils.go +++ b/PetAdoptions/payforadoption-go/payforadoption/utils.go @@ -1,10 +1,20 @@ package payforadoption import ( + "context" + "database/sql" + "errors" "fmt" "math/rand" + "runtime" "strings" + "sync" "time" + + "github.com/XSAM/otelsql" + "github.com/go-kit/log" + "github.com/go-kit/log/level" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" ) type CustomerInfo struct { @@ -95,3 +105,288 @@ func getAddresses(r *rand.Rand) string { } return seed[r.Intn(len(seed))] } + +// ======================================== +// Error Mode Degradation Scenarios +// ======================================== + +// DegradationResult contains the result of a degradation scenario +type DegradationResult struct { + Adoption Adoption + Error error + Duration time.Duration +} + +// simulateHighCPU creates CPU pressure to simulate performance degradation +func simulateHighCPU(duration time.Duration) { + end := time.Now().Add(duration) + for time.Now().Before(end) { + // Busy loop to consume CPU cycles + for i := 0; i < 1000000; i++ { + _ = i * i + } + // Small sleep to prevent complete CPU starvation + time.Sleep(time.Microsecond) + } +} + +// simulateNetworkLatency adds artificial network-like delays with jitter +func simulateNetworkLatency(baseMs, jitterMs int) { + delay := time.Duration(baseMs+rand.Intn(jitterMs)) * time.Millisecond + time.Sleep(delay) +} + +// memoryLeak creates memory pressure (original function moved here for organization) +func memoryLeak() { + + type T struct { + v [2 << 20]int + t *T + } + + var finalizer = func(t *T) {} + + var x, y T + + // The SetFinalizer call makes x escape to heap. + runtime.SetFinalizer(&x, finalizer) + + // The following line forms a cyclic reference + // group with two members, x and y. + // This causes x and y are not collectable. + x.t, y.t = &y, &x // y also escapes to heap. +} + +// Critical system stress scenario +func systemStressDegradation(ctx context.Context, logger log.Logger, adoption Adoption, startTime time.Time) DegradationResult { + degradationType := "system stress" + level.Error(logger).Log("degradation", degradationType, "severity", "critical") + + // Add CPU pressure for realistic system stress + go simulateHighCPU(500 * time.Millisecond) + go memoryLeak() + + duration := time.Since(startTime) + + return DegradationResult{ + Adoption: adoption, + Error: errors.New("memory allocation failure"), + Duration: duration, + } +} + +// Circuit breaker pattern scenario +func circuitBreakerDegradation(ctx context.Context, logger log.Logger, adoption Adoption, startTime time.Time) DegradationResult { + // if rand.Intn(10) < 3 { // 30% failure rate + degradationType := "circuit breaker open" + level.Error(logger).Log("degradation", degradationType, "severity", "high") + + simulateNetworkLatency(500, 200) // Quick failure + duration := time.Since(startTime) + + return DegradationResult{ + Adoption: Adoption{}, + Error: errors.New("payment service unavailable"), + Duration: duration, + } + +} + +// Real database connection exhaustion scenario +func databaseConnectionDegradation(ctx context.Context, logger log.Logger, adoption Adoption, startTime time.Time, repository Repository) DegradationResult { + degradationType := "database connection exhaustion" + level.Error(logger).Log("degradation", degradationType, "severity", "critical") + + // Get the connection string from the repository + connStr, err := repository.GetConnectionString(ctx) + if err != nil { + level.Error(logger).Log("failed_to_get_connection_string", err) + // Fallback to simulated timeout + simulateNetworkLatency(5000, 1000) + duration := time.Since(startTime) + return DegradationResult{ + Adoption: Adoption{}, + Error: fmt.Errorf("failed to retrieve database connection configuration: %v", err), + Duration: duration, + } + } + + level.Info(logger).Log("connection_string_retrieved", "success", "degradation_mode", "database_exhaustion") + + // Get the connection exhauster + exhauster := GetConnectionExhauster(logger) + + maxConnections := 100 // Conservative number to avoid completely killing the database + level.Warn(logger).Log("attempting_connection_exhaustion", maxConnections, "connection_string_length", len(connStr)) + + // Attempt to exhaust connections + exhaustErr := exhauster.ExhaustConnections(ctx, connStr, maxConnections) + + duration := time.Since(startTime) + + if exhaustErr != nil { + level.Error(logger).Log("connection_exhaustion_failed", exhaustErr, "connections_held", exhauster.GetConnectionCount()) + + // Even if we couldn't exhaust all connections, we might have opened some + // Release them after a delay to simulate the issue + go func() { + time.Sleep(30 * time.Second) // Hold connections for 30 seconds + level.Info(logger).Log("releasing_partial_connections", "cleanup") + exhauster.ReleaseConnections() + }() + + return DegradationResult{ + Adoption: Adoption{}, + Error: fmt.Errorf("database connection pool exhausted - %v", exhaustErr), + Duration: duration, + } + } + + level.Error(logger).Log("database_connections_exhausted", exhauster.GetConnectionCount(), "duration_ms", duration.Milliseconds()) + + // Release connections after a delay to simulate the real issue + go func() { + time.Sleep(45 * time.Second) // Hold connections for 45 seconds to show real impact + level.Info(logger).Log("releasing_exhausted_connections", "auto_cleanup", "connections", exhauster.GetConnectionCount()) + exhauster.ReleaseConnections() + }() + + return DegradationResult{ + Adoption: Adoption{}, + Error: errors.New("database connection pool exhausted - no available connections"), + Duration: duration, + } +} + +// just slow requests +func defaultDegradation(ctx context.Context, logger log.Logger, adoption Adoption, startTime time.Time) DegradationResult { + degradationType := "default" + level.Error(logger).Log("degradation", degradationType, "severity", "low") + + // Simulate cascading delays + simulateNetworkLatency(1000, 800) + + duration := time.Since(startTime) + + return DegradationResult{ + Adoption: adoption, + Error: nil, + Duration: duration, + } +} + +// handleDefaultDegradation - Cascading slowness scenario +func handleDefaultDegradation(ctx context.Context, logger log.Logger, adoption Adoption, startTime time.Time, repository Repository) DegradationResult { + + // randomly choose between scenarios: defaultDegradation, circuitBreakerDegradation, systemStressDegradation, databaseConnectionDegradation + switch rand.Intn(10) { + case 0, 1: + return circuitBreakerDegradation(ctx, logger, adoption, startTime) + case 2, 3: + return systemStressDegradation(ctx, logger, adoption, startTime) + case 4, 5, 6: + return databaseConnectionDegradation(ctx, logger, adoption, startTime, repository) + default: + return defaultDegradation(ctx, logger, adoption, startTime) + } + +} + +// ======================================== +// Real Database Connection Exhaustion +// ======================================== + +// DatabaseConnectionExhauster manages real database connection exhaustion +type DatabaseConnectionExhauster struct { + connections []*sql.DB + mutex sync.Mutex + logger log.Logger +} + +// NewDatabaseConnectionExhauster creates a new connection exhauster +func NewDatabaseConnectionExhauster(logger log.Logger) *DatabaseConnectionExhauster { + return &DatabaseConnectionExhauster{ + connections: make([]*sql.DB, 0), + logger: logger, + } +} + +// ExhaustConnections opens many database connections to simulate connection pool exhaustion +func (dce *DatabaseConnectionExhauster) ExhaustConnections(ctx context.Context, connStr string, maxConnections int) error { + dce.mutex.Lock() + defer dce.mutex.Unlock() + + level.Warn(dce.logger).Log("action", "exhausting_database_connections", "target_connections", maxConnections) + + for i := 0; i < maxConnections; i++ { + // Open a new database connection + db, err := otelsql.Open("postgres", connStr, otelsql.WithAttributes( + semconv.DBSystemKey.String("postgres"), + )) + if err != nil { + level.Error(dce.logger).Log("connection_exhaustion_error", err, "connections_opened", i) + return err + } + + // Set connection pool settings to force individual connections + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + db.SetConnMaxLifetime(time.Hour) // Keep connections alive + + // Test the connection to ensure it's actually established + if err := db.PingContext(ctx); err != nil { + level.Error(dce.logger).Log("connection_ping_error", err, "connection_number", i) + db.Close() + return err + } + + // Store the connection + dce.connections = append(dce.connections, db) + + // Add some delay to make it more realistic + time.Sleep(10 * time.Millisecond) + + if i%10 == 0 { + level.Info(dce.logger).Log("connections_opened", i+1, "target", maxConnections) + } + } + + level.Warn(dce.logger).Log("database_connections_exhausted", maxConnections) + return nil +} + +// ReleaseConnections closes all opened connections +func (dce *DatabaseConnectionExhauster) ReleaseConnections() { + dce.mutex.Lock() + defer dce.mutex.Unlock() + + level.Info(dce.logger).Log("action", "releasing_database_connections", "count", len(dce.connections)) + + for i, db := range dce.connections { + if err := db.Close(); err != nil { + level.Error(dce.logger).Log("connection_close_error", err, "connection_number", i) + } + } + + dce.connections = dce.connections[:0] // Clear the slice + level.Info(dce.logger).Log("database_connections_released", "success") +} + +// GetConnectionCount returns the number of currently held connections +func (dce *DatabaseConnectionExhauster) GetConnectionCount() int { + dce.mutex.Lock() + defer dce.mutex.Unlock() + return len(dce.connections) +} + +// Global connection exhauster instance +var globalConnectionExhauster *DatabaseConnectionExhauster +var exhausterOnce sync.Once + +// GetConnectionExhauster returns a singleton instance of the connection exhauster +func GetConnectionExhauster(logger log.Logger) *DatabaseConnectionExhauster { + exhausterOnce.Do(func() { + globalConnectionExhauster = NewDatabaseConnectionExhauster(logger) + }) + return globalConnectionExhauster +} diff --git a/PetAdoptions/petadoptionshistory-py/Dockerfile b/PetAdoptions/petadoptionshistory-py/Dockerfile deleted file mode 100644 index 8819c2ef..00000000 --- a/PetAdoptions/petadoptionshistory-py/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -# syntax=docker/dockerfile:1 - -FROM python:3.8 - -WORKDIR /app - -# prerequisites for building psycopg2 -RUN apt-get update && apt-get install -y build-essential python3-dev libpq-dev - -COPY requirements.txt requirements.txt -RUN pip3 install -r requirements.txt - -COPY . . -ENV FLASK_APP=petadoptionshistory -ENV PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python - -CMD [ "flask", "run" , "--host=0.0.0.0", "--port=8080"] - -EXPOSE 8080/tcp \ No newline at end of file diff --git a/PetAdoptions/petadoptionshistory-py/config.py b/PetAdoptions/petadoptionshistory-py/config.py deleted file mode 100644 index 5ad2de43..00000000 --- a/PetAdoptions/petadoptionshistory-py/config.py +++ /dev/null @@ -1,66 +0,0 @@ -import os -import json -import boto3 - -def fetch_config(): - cfg = { - 'update_adoption_url': os.getenv("UPDATE_ADOPTION_URL"), - 'rds_secret_arn': os.getenv("RDS_SECRET_ARN"), - 'region': os.getenv("AWS_REGION") - } - - if cfg['update_adoption_url'] == None or cfg['rds_secret_arn'] == None: - return fetch_config_from_parameter_store(cfg['region']) - - return cfg - -def fetch_config_from_parameter_store(region): - client = boto3.client('ssm', region_name=region) - - result = client.get_parameters( - Names=[ - '/petstore/updateadoptionstatusurl', - '/petstore/rdssecretarn', - '/petstore/s3bucketname', - '/petstore/dynamodbtablename' - ] - ) - - cfg = { - 'region': region - } - - for p in result['Parameters']: - if p['Name'] == '/petstore/updateadoptionstatusurl': - cfg['update_adoption_url'] = p['Value'] - elif p['Name'] == '/petstore/rdssecretarn': - cfg['rds_secret_arn'] = p['Value'] - elif p['Name'] == '/petstore/s3bucketname': - cfg['s3_bucket_name'] = p['Value'] - elif p['Name'] == '/petstore/dynamodbtablename': - cfg['dynamodb_tablename'] = p['Value'] - - return cfg - -def get_secret_value(secret_id, region): - client = boto3.client('secretsmanager', region_name=region) - - response = client.get_secret_value( - SecretId=secret_id - ) - - return response['SecretString'] - -def get_rds_connection_parameters(secret_id, region): - jsonstr = get_secret_value(secret_id, region) - - c = json.loads(jsonstr) - - u = { - 'database': c['dbname'], - 'user': c['username'], - 'password': c['password'], - 'host': c['host'] - } - - return u \ No newline at end of file diff --git a/PetAdoptions/petadoptionshistory-py/deployment.yaml b/PetAdoptions/petadoptionshistory-py/deployment.yaml deleted file mode 100644 index 5f851e65..00000000 --- a/PetAdoptions/petadoptionshistory-py/deployment.yaml +++ /dev/null @@ -1,159 +0,0 @@ ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - annotations: - eks.amazonaws.com/role-arn: "{{PETSITE_SA_ROLE}}" - name: pethistory-sa - namespace: default ---- -apiVersion: v1 -kind: Service -metadata: - name: pethistory-service - namespace: default -spec: - ports: - - port: 8080 - nodePort: 30303 - targetPort: 8080 - protocol: TCP - type: NodePort - selector: - app: pethistory ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: pethistory-deployment - namespace: default -spec: - selector: - matchLabels: - app: pethistory - replicas: 1 - template: - metadata: - labels: - app: pethistory - spec: - serviceAccountName: pethistory-sa - containers: - - image: "{{ECR_IMAGE_URL}}" - imagePullPolicy: Always - name: pethistory - ports: - - containerPort: 8080 - protocol: TCP - env: - - name: AWS_XRAY_DAEMON_ADDRESS - value: xray-service.default:2000 - - name: AWS_REGION - value: "{{AWS_REGION}}" - - name: OTEL_OTLP_ENDPOINT - value: "localhost:4317" - - name: OTEL_RESOURCE - value: ClusterName={{CLUSTER_NAME}} - - name: OTEL_RESOURCE_ATTRIBUTES - value: "service.namespace=AWSObservability,service.name=PetAdoptionsHistory" - - name: S3_REGION - value: "{{AWS_REGION}}" - - name: OTEL_METRICS_EXPORTER - value: "otlp" - livenessProbe: - httpGet: - path: /health/status - port: 8080 - initialDelaySeconds: 3 - periodSeconds: 3 - - name: aws-otel-collector - image: amazon/aws-otel-collector:latest - args: ["--config=/etc/otel-config/otel-config.yaml"] - env: - - name: AWS_REGION - value: "{{AWS_REGION}}" - imagePullPolicy: Always - resources: - limits: - cpu: 256m - memory: 512Mi - requests: - cpu: 32m - memory: 24Mi - volumeMounts: - - name: otel-config - mountPath: /etc/otel-config - volumes: - - name: otel-config - configMap: - name: otel-config ---- -apiVersion: elbv2.k8s.aws/v1beta1 -kind: TargetGroupBinding -metadata: - name: pethistory-tgb -spec: - serviceRef: - name: pethistory-service - port: 8080 - targetGroupARN: "{{TARGET_GROUP_ARN}}" - targetType: ip ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: pethistory-otel-prometheus-role -rules: - - apiGroups: - - "" - resources: - - pods - verbs: - - get - - list - - watch - - nonResourceURLs: - - /metrics - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: pethistory-otel-prometheus-role-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: pethistory-otel-prometheus-role -subjects: - - kind: ServiceAccount - name: pethistory-sa - namespace: default ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: otel-awseksresourcedetector-role -rules: - - apiGroups: - - "" - resources: - - configmaps - resourceNames: - - aws-auth - - cluster-info - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: otel-awseksresourcedetector-role-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: otel-awseksresourcedetector-role -subjects: - - kind: ServiceAccount - name: pethistory-sa - namespace: default diff --git a/PetAdoptions/petadoptionshistory-py/otel-collector-config.yaml b/PetAdoptions/petadoptionshistory-py/otel-collector-config.yaml deleted file mode 100644 index 840eef11..00000000 --- a/PetAdoptions/petadoptionshistory-py/otel-collector-config.yaml +++ /dev/null @@ -1,118 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: otel-config - namespace: default -data: - # aws-otel-collector config - otel-config.yaml: | - extensions: - health_check: - sigv4auth: - service: "aps" - region: "{{AWS_REGION}}" - - receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - http: - endpoint: 0.0.0.0:4318 - prometheus: - config: - global: - scrape_interval: 20s - scrape_timeout: 10s - scrape_configs: - - job_name: "otel-collector" - kubernetes_sd_configs: - - role: pod - relabel_configs: - - source_labels: [__meta_kubernetes_pod_container_port_number] - action: keep - target_label: '^8080$' - - source_labels: [ __meta_kubernetes_pod_container_name ] - action: keep - target_label: '^pethistory$' - - source_labels: [ __meta_kubernetes_pod_name ] - action: replace - target_label: pod_name - - source_labels: [ __meta_kubernetes_pod_container_name ] - action: replace - target_label: container_name - - processors: - batch/metrics: - timeout: 60s - - exporters: - logging: - loglevel: debug - awsxray: - awsemf: - namespace: "PetAdoptionsHistory" - resource_to_telemetry_conversion: - enabled: true - dimension_rollup_option: NoDimensionRollup - metric_declarations: - - dimensions: [ [ pod_name, container_name ] ] - metric_name_selectors: - - "^transactions_get_count_total$" - - "^transactions_history_count$" - - "^process_.*" - label_matchers: - - label_names: - - container_name - regex: ^pethistory$ - - dimensions: [ [ pod_name, container_name, generation ] ] - metric_name_selectors: - - "^python_gc_objects_.*" - label_matchers: - - label_names: - - container_name - regex: ^pethistory$ - - dimensions: [ [ pod_name, container_name, endpoint, method, status ] ] - metric_name_selectors: - - "^flask_http_request_duration_seconds_created$" - label_matchers: - - label_names: - - container_name - regex: ^pethistory$ - - dimensions: [ [ pod_name, container_name, method, status ] ] - metric_name_selectors: - - "^flask_http_request_total$" - - "^flask_http_request_created$" - label_matchers: - - label_names: - - container_name - regex: ^pethistory$ - - dimensions: [ [ pod_name, container_name, implementation, major, minor, patchlegel, version ] ] - metric_name_selectors: - - "^python_info$" - label_matchers: - - label_names: - - container_name - regex: ^pethistory$ - - dimensions: [ [ pod_name, container_name, version ] ] - metric_name_selectors: - - "^flask_exporter_info$" - label_matchers: - - label_names: - - container_name - regex: ^pethistory$ - # prometheusremotewrite: - # endpoint: "{{AMP_WORKSPACE_URL}}" - # auth: - # authenticator: sigv4auth - - service: - extensions: [sigv4auth, health_check] - pipelines: - traces: - receivers: [otlp] - exporters: [awsxray] - metrics: - receivers: [prometheus] - processors: [batch/metrics] - exporters: [awsemf] diff --git a/PetAdoptions/petadoptionshistory-py/petadoptionshistory.py b/PetAdoptions/petadoptionshistory-py/petadoptionshistory.py deleted file mode 100644 index fe352002..00000000 --- a/PetAdoptions/petadoptionshistory-py/petadoptionshistory.py +++ /dev/null @@ -1,30 +0,0 @@ -import logging -import os -import psycopg -import config -import repository -from flask import Flask, jsonify - -# Setup flask app -app = Flask(__name__) - -logging.basicConfig(level=os.getenv('LOG_LEVEL', 20), format='%(message)s') -logger = logging.getLogger() -cfg = config.fetch_config() -conn_params = config.get_rds_connection_parameters(cfg['rds_secret_arn'], cfg['region']) -db = psycopg.connect(**conn_params) - -@app.route('/petadoptionshistory/api/home/transactions', methods=['GET']) -def transactions_get(): - transactions = repository.list_transaction_history(db) - return jsonify(transactions) - -@app.route('/petadoptionshistory/api/home/transactions', methods=['DELETE']) -def transactions_delete(): - repository.delete_transaction_history(db) - return jsonify(success=True) - -@app.route('/health/status') -def status_path(): - repository.check_alive(db) - return jsonify(success=True) \ No newline at end of file diff --git a/PetAdoptions/petadoptionshistory-py/repository.py b/PetAdoptions/petadoptionshistory-py/repository.py deleted file mode 100644 index 7619558e..00000000 --- a/PetAdoptions/petadoptionshistory-py/repository.py +++ /dev/null @@ -1,35 +0,0 @@ -def list_transaction_history(db): - sql = 'SELECT * FROM transactions_history' - - cur = db.cursor() - cur.execute(sql) - result = cur.fetchall() - db.commit() - - return result - -def delete_transaction_history(db): - sql = 'DELETE FROM transactions_history' - - cur = db.cursor() - result = cur.execute(sql) - db.commit() - - return result - -def count_transaction_history(db): - sql = 'SELECT count(*) FROM transactions_history' - - cur = db.cursor() - cur.execute(sql) - result = cur.fetchone() - db.commit() - - return result[0] - -def check_alive(db): - sql = 'SELECT NULL' # do nothing - - cur = db.cursor() - cur.execute(sql) - db.commit() \ No newline at end of file diff --git a/PetAdoptions/petadoptionshistory-py/requirements.txt b/PetAdoptions/petadoptionshistory-py/requirements.txt deleted file mode 100644 index d1f5a03f..00000000 --- a/PetAdoptions/petadoptionshistory-py/requirements.txt +++ /dev/null @@ -1,22 +0,0 @@ -Flask==2.2.5 -boto3==1.37.10 -psycopg==3.2.5 -opentelemetry-distro==0.32b0 -opentelemetry-exporter-otlp==1.11.1 -opentelemetry-exporter-otlp-proto-grpc==1.11.1 -opentelemetry-exporter-otlp-proto-http==1.11.1 -opentelemetry-exporter-prometheus==1.12.0rc1 -opentelemetry-instrumentation==0.32b0 -opentelemetry-instrumentation-botocore==0.32b0 -opentelemetry-instrumentation-dbapi==0.32b0 -opentelemetry-instrumentation-flask==0.32b0 -opentelemetry-instrumentation-psycopg2==0.32b0 -opentelemetry-instrumentation-wsgi==0.32b0 -opentelemetry-propagator-aws-xray==1.0.1 -opentelemetry-proto==1.11.1 -opentelemetry-sdk==1.12.0rc2 -opentelemetry-sdk-extension-aws==2.0.1 -opentelemetry-semantic-conventions==0.32b0 -opentelemetry-util-http==0.32b0 -prometheus-flask-exporter==0.20.2 -Werkzeug==2.2.2 diff --git a/PetAdoptions/pethistory/.eslintrc.js b/PetAdoptions/pethistory/.eslintrc.js new file mode 100644 index 00000000..508e980f --- /dev/null +++ b/PetAdoptions/pethistory/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + env: { + node: true, + es2021: true, + jest: true + }, + extends: [ + 'eslint:recommended' + ], + parserOptions: { + ecmaVersion: 12, + sourceType: 'module' + }, + rules: { + 'indent': ['error', 4], + 'linebreak-style': ['error', 'unix'], + 'quotes': ['error', 'single'], + 'semi': ['error', 'always'], + 'no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], + 'no-console': 'off' // Allow console.log in Lambda functions + } +}; \ No newline at end of file diff --git a/PetAdoptions/pethistory/.prettierrc b/PetAdoptions/pethistory/.prettierrc new file mode 100644 index 00000000..c660a0a1 --- /dev/null +++ b/PetAdoptions/pethistory/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "none", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 4, + "useTabs": false +} \ No newline at end of file diff --git a/PetAdoptions/pethistory/README.md b/PetAdoptions/pethistory/README.md new file mode 100644 index 00000000..3e0f3fcc --- /dev/null +++ b/PetAdoptions/pethistory/README.md @@ -0,0 +1,307 @@ +# Pet History Lambda Function + +## Overview +The Pet History Lambda function processes adoption history messages from Amazon SQS and maintains historical records of pet adoptions. This service is part of an event-driven architecture that separates real-time adoption processing from historical data tracking. + +**Key Responsibilities:** +1. **Asynchronous history tracking** - Records adoption history without blocking the main adoption flow +2. **Optimistic table creation** - Automatically creates database tables when needed +3. **Schema validation** - Prevents message poisoning with strict validation +4. **Batch processing** - Efficiently handles multiple messages with partial failure support + +## Architecture + +``` +payforadoption-go → SQS Queue → pethistory Lambda → transaction_history table + ↓ + (Auto-creates table if missing) +``` + +### Event-Driven Design +- **Real-time adoption**: `payforadoption-go` handles synchronous adoption processing +- **Asynchronous history**: `pethistory` tracks historical data in the background +- **Clean separation**: History tracking failures don't impact adoption success + +## Message Processing Flow + +1. **SQS Trigger**: Lambda receives adoption history messages from SQS +2. **Schema Validation**: Validates message format using Joi schema +3. **Optimistic Insert**: Attempts to insert directly into `transaction_history` table +4. **Auto Table Creation**: If table doesn't exist (PostgreSQL error 42P01), creates it automatically +5. **Batch Failure Handling**: Uses SQS `ReportBatchItemFailures` for partial batch processing +6. **Retry Mechanism**: Failed messages are retried by SQS, successful table creation enables future success + +## Message Schema + +The function expects SQS messages with the following JSON structure: + +```json +{ + "transactionId": "123e4567-e89b-12d3-a456-426614174000", + "petId": "pet123", + "petType": "dog", + "userId": "user456", + "adoptionDate": "2025-08-08T10:30:00Z", + "timestamp": "2025-08-08T10:30:00Z" +} +``` + +### Schema Validation Rules +- `transactionId`: Must be a valid UUID +- `petId`: Required string +- `petType`: Required string +- `userId`: Required string +- `adoptionDate`: Must be a valid ISO date string +- `timestamp`: Must be a valid ISO date string + +## Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `RDS_SECRET_ARN` | ARN of the RDS secret in Secrets Manager | `arn:aws:secretsmanager:us-west-2:123456789012:secret:rds-secret` | +| `AWS_REGION` | AWS region for services | `us-west-2` | + +## Database Operations + +### Transaction History Table +The function automatically creates and inserts into the `transaction_history` table: + +```sql +CREATE TABLE IF NOT EXISTS transaction_history ( + id SERIAL PRIMARY KEY, + pet_id VARCHAR(255) NOT NULL, + transaction_id VARCHAR(255) NOT NULL, + adoption_date TIMESTAMP NOT NULL, + user_id VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Performance indexes created automatically +CREATE INDEX IF NOT EXISTS idx_transaction_history_pet_id ON transaction_history(pet_id); +CREATE INDEX IF NOT EXISTS idx_transaction_history_user_id ON transaction_history(user_id); +CREATE INDEX IF NOT EXISTS idx_transaction_history_adoption_date ON transaction_history(adoption_date); +CREATE INDEX IF NOT EXISTS idx_transaction_history_transaction_id ON transaction_history(transaction_id); +``` + +### Optimistic Table Creation +- **First attempt**: Tries to INSERT directly (fastest path) +- **On table missing**: Catches PostgreSQL error code `42P01` and creates table + indexes +- **Subsequent attempts**: INSERT succeeds normally after table exists +- **Self-healing**: Service creates its own dependencies automatically + +### Database Connection +- Uses AWS Secrets Manager to retrieve database credentials +- Establishes PostgreSQL connection using the `pg` library +- Handles connection errors gracefully with SQS retry mechanism + +## Error Handling & Batch Processing + +### SQS Batch Processing +The function uses SQS's `ReportBatchItemFailures` feature for efficient batch processing: + +```javascript +// Response format for SQS +{ + "batchItemFailures": [ + { "itemIdentifier": "failed-message-id" } + ], + "processedCount": 8, + "results": [...] +} +``` + +### Error Types +1. **Schema Validation Errors**: Invalid JSON or missing fields → Message goes to batch failures +2. **Database Connection Errors**: Network/auth issues → Message retried by SQS +3. **Table Missing (42P01)**: Creates table automatically → Message retried and succeeds +4. **Other Database Errors**: Logged and message goes to batch failures + +### Retry Strategy +- **Successful messages**: Automatically deleted from SQS +- **Failed messages**: Remain in SQS for retry based on queue configuration +- **Persistent failures**: Eventually move to Dead Letter Queue (DLQ) + +## Observability + +### CloudWatch Application Signals +- **Automatic instrumentation** with AWS Distro for OpenTelemetry (ADOT) +- **Service name**: `pethistory` for easy identification +- **Distributed tracing** across the adoption workflow +- **Performance metrics** and dependency mapping +- **Error tracking** and alerting + +### CloudWatch Logs +- **Structured JSON logging** for all operations +- **Processing metrics**: Success/failure counts, timing +- **Error details**: Stack traces and context information +- **Table creation events**: Logged when auto-creation occurs + +### Key Metrics +- **Processing rate**: Messages processed per minute +- **Error rate**: Failed message percentage +- **Table creation events**: When auto-creation occurs +- **Batch efficiency**: Partial vs full batch failures + +## Testing + +### Unit Tests +```bash +npm test +``` + +The test suite covers: +- ✅ Single message processing +- ✅ Batch message processing +- ✅ Schema validation failures +- ✅ Mixed success/failure batches +- ✅ Correct SQS response format +- ✅ Error handling scenarios + +### Integration Testing +```bash +# Send test message to SQS queue +aws sqs send-message \ + --queue-url "https://sqs.us-west-2.amazonaws.com/123456789012/adoption-history-queue" \ + --message-body '{ + "transactionId": "123e4567-e89b-12d3-a456-426614174000", + "petId": "pet123", + "petType": "dog", + "userId": "user456", + "adoptionDate": "2025-08-08T10:30:00Z", + "timestamp": "2025-08-08T10:30:00Z" + }' +``` + +### Local Development +```bash +# Install dependencies +npm install + +# Run tests +npm test + +# Lint code +npm run lint + +# Format code +npm run format + +# Local SAM testing +sam build +sam local invoke PetHistoryFunction --event events/test-sqs-event.json +``` + +## Deployment + +### AWS SAM Deployment +```bash +# Interactive deployment +./deploy.sh + +# Manual deployment +sam build && sam deploy --guided +``` + +### Configuration +- **Function Name**: `pethistory` +- **Runtime**: Node.js 18.x +- **Memory**: 512 MB +- **Timeout**: 60 seconds +- **SQS Batch Size**: 10 messages +- **Batch Window**: 5 seconds +- **Dead Letter Queue**: Configured for failed messages + +## Monitoring and Alerts + +### Key Metrics to Monitor +1. **Processing Rate**: Messages processed per minute +2. **Error Rate**: Failed message percentage +3. **SQS Queue Depth**: Unprocessed history messages +4. **Table Creation Events**: Auto-creation frequency +5. **DLQ Messages**: Messages requiring investigation + +### Recommended CloudWatch Alarms +- Lambda error rate > 5% +- Lambda duration > 50 seconds +- SQS queue depth > 50 messages +- DLQ message count > 0 +- Table creation events (for monitoring) + +## Security + +### IAM Permissions +The Lambda function requires: +- `secretsmanager:GetSecretValue` - Database credentials +- `rds-db:connect` - Database connection (if using IAM auth) +- `sqs:ReceiveMessage`, `sqs:DeleteMessage` - SQS processing +- CloudWatch Application Signals permissions (auto-granted) + +### Network Security +- **VPC Deployment**: Private subnets with NAT Gateway +- **Security Groups**: Restrict database access to Lambda only +- **VPC Endpoints**: AWS services access without internet routing + +## Architecture Benefits + +### Event-Driven Design +- **Decoupled services**: History tracking independent of adoption flow +- **Resilient**: History failures don't impact adoptions +- **Scalable**: Independent scaling based on workload +- **Maintainable**: Clear domain boundaries + +### Optimistic Table Creation +- **Self-healing**: Creates dependencies automatically +- **Zero-downtime**: No manual setup required +- **Performance**: Fast path for normal operations +- **Idempotent**: Safe to run multiple times + +### Batch Processing +- **Efficient**: Processes multiple messages together +- **Partial failures**: Doesn't fail entire batch for single message +- **Cost-effective**: Reduces Lambda invocations +- **Reliable**: SQS handles retry logic automatically + +## Troubleshooting + +### Common Issues + +1. **Messages Not Being Processed** + - Check SQS queue for messages in flight + - Verify Lambda function is triggered by SQS + - Review CloudWatch logs for errors + +2. **Schema Validation Failures** + - Check message format in SQS queue + - Verify all required fields are present + - Ensure correct data types (UUID, ISO dates) + +3. **Database Connection Issues** + - Verify RDS secret ARN is correct + - Check VPC/security group configuration + - Ensure database is accessible from Lambda subnet + +4. **Table Creation Issues** + - Check database permissions for CREATE TABLE + - Verify PostgreSQL version compatibility + - Review CloudWatch logs for creation attempts + +### Debugging Steps +1. **Check CloudWatch Logs** for detailed error information +2. **Review Application Signals** service map for dependencies +3. **Monitor SQS metrics** for processing delays +4. **Inspect DLQ messages** for patterns in failures +5. **Use X-Ray traces** for end-to-end request tracking + +## Workshop Learning Objectives + +This service demonstrates: +- ✅ **Event-driven architecture** with asynchronous processing +- ✅ **SQS batch processing** with partial failure handling +- ✅ **Optimistic database operations** and auto-recovery +- ✅ **Schema validation** and data integrity +- ✅ **CloudWatch Application Signals** observability +- ✅ **Microservices patterns** and domain separation +- ✅ **Error handling strategies** in distributed systems +- ✅ **Infrastructure as Code** with AWS SAM + +Perfect for learning modern serverless patterns and AWS observability tools! 🚀 \ No newline at end of file diff --git a/PetAdoptions/pethistory/deploy.sh b/PetAdoptions/pethistory/deploy.sh new file mode 100755 index 00000000..f15196c4 --- /dev/null +++ b/PetAdoptions/pethistory/deploy.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# Pet history Lambda Deployment Script using AWS SAM + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +STACK_NAME="pet-history-stack" +REGION="us-west-2" + +echo -e "${GREEN}🚀 Pet history Lambda Deployment${NC}" +echo "==================================" + +# Check if AWS CLI is configured +if ! aws sts get-caller-identity > /dev/null 2>&1; then + echo -e "${RED}❌ AWS CLI not configured. Please run 'aws configure' first.${NC}" + exit 1 +fi + +# Check if SAM CLI is installed +if ! command -v sam &> /dev/null; then + echo -e "${RED}❌ SAM CLI not found. Please install AWS SAM CLI first.${NC}" + echo "Installation guide: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html" + exit 1 +fi + +# Get AWS Account ID +ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) +echo -e "${GREEN}📋 AWS Account ID: ${ACCOUNT_ID}${NC}" + + +# Confirm deployment +echo -e "${YELLOW}❓ Do you want to proceed with deployment? (y/N)${NC}" +read -r CONFIRM +if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}⏹️ Deployment cancelled.${NC}" + exit 0 +fi + +# Install dependencies +echo -e "${GREEN}📦 Installing dependencies...${NC}" +npm install + +# Run tests +echo -e "${GREEN}🧪 Running tests...${NC}" +npm test + +# Build the SAM application +echo -e "${GREEN}🔨 Building SAM application...${NC}" +sam build + +# Deploy the application +echo -e "${GREEN}🚀 Deploying to AWS...${NC}" +sam deploy \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" \ + --capabilities CAPABILITY_IAM \ + # --parameter-overrides \ + # "SQSQueueArn=${SQS_QUEUE_ARN}" \ + # "RDSSecretArn=${RDS_SECRET_ARN}" \ + # "UpdateAdoptionURL=${UPDATE_ADOPTION_URL}" \ + # "VpcId=${VPC_ID}" \ + # "SubnetIds=${SUBNET_IDS}" \ + # "SecurityGroupId=${SECURITY_GROUP_ID}" \ + +# Get outputs +echo -e "${GREEN}📋 Deployment completed! Getting stack outputs...${NC}" +aws cloudformation describe-stacks \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" \ + --query 'Stacks[0].Outputs' \ + --output table + +echo -e "${GREEN}✅ Pet history Lambda function deployed successfully!${NC}" +echo -e "${GREEN}🔍 Check CloudWatch Application Signals for observability data.${NC}" +echo -e "${GREEN}📊 Application Signals will automatically detect the service and create service maps.${NC}" +echo -e "${GREEN}🎯 X-Ray tracing is enabled for distributed tracing across services.${NC}" + +echo -e "${GREEN}🎉 Deployment complete!${NC}" \ No newline at end of file diff --git a/PetAdoptions/pethistory/index.js b/PetAdoptions/pethistory/index.js new file mode 100644 index 00000000..3e99bda7 --- /dev/null +++ b/PetAdoptions/pethistory/index.js @@ -0,0 +1,237 @@ +'use strict'; + +const AWS = require('aws-sdk'); +const { Client } = require('pg'); +const Joi = require('joi'); + +// Schema validation for SQS messages +const adoptionMessageSchema = Joi.object({ + transactionId: Joi.string().uuid().required(), + petId: Joi.string().required(), + petType: Joi.string().required(), + userId: Joi.string().required(), + adoptionDate: Joi.string().isoDate().required(), + timestamp: Joi.string().isoDate().required() +}); + +// Environment variables +const { + RDS_SECRET_ARN, + AWS_REGION, +} = process.env; + +// AWS clients +const secretsManager = new AWS.SecretsManager({ region: AWS_REGION }); + +/** + * Lambda handler for processing adoption history messages from SQS + */ +exports.handler = async (event, context) => { + console.log('Processing adoption history messages:', JSON.stringify(event, null, 2)); + + const results = []; + const batchItemFailures = []; + + // Process each SQS record + for (const record of event.Records) { + try { + const result = await processAdoptionMessage(record); + results.push(result); + } catch (error) { + console.error('Failed to process record:', record.messageId, error); + // Add to batch failures instead of throwing + batchItemFailures.push({ + itemIdentifier: record.messageId + }); + } + } + + console.log(`Processed ${results.length} messages successfully, ${batchItemFailures.length} failed`); + + // Return batch failure information for SQS to handle partial failures + return { + batchItemFailures: batchItemFailures, + processedCount: results.length, + results: results + }; +}; + +/** + * Process a single adoption history message from SQS + */ +async function processAdoptionMessage(record) { + const messageId = record.messageId; + // const receiptHandle = record.receiptHandle; + + console.log(`Processing message ${messageId}`); + + // Parse and validate message + let adoptionData; + try { + adoptionData = JSON.parse(record.body); + console.log('Parsed adoption data:', adoptionData); + } catch (error) { + console.error('Invalid JSON in message:', messageId, error); + throw new Error(`Invalid JSON in message ${messageId}: ${error.message}`); + } + // Validate message schema + const { error, value } = adoptionMessageSchema.validate(adoptionData); + if (error) { + console.error('Schema validation failed for message:', messageId, error.details); + throw new Error(`Schema validation failed for message ${messageId}: ${error.message}`); + } + const validatedData = value; + try { + console.log(`Processing adoption - Transaction: ${validatedData.transactionId}, Pet: ${validatedData.petId}, User: ${validatedData.userId}`); + // Write adoption history to database + await writeAdoptionHistoryToDatabase(validatedData); + + console.log(`Successfully processed adoption for pet ${validatedData.petId} by user ${validatedData.userId}`); + + return { + messageId: messageId, + transactionId: validatedData.transactionId, + status: 'success' + }; + + } catch (error) { + console.error(`Failed to process adoption for pet ${validatedData.petId}:`, error); + throw error; + } +} + +/** + * Write adoption history record to PostgreSQL database + */ +async function writeAdoptionHistoryToDatabase(adoptionData) { + let client; + + try { + console.log(`Writing adoption history to database: ${adoptionData.transactionId}`); + + // Get database connection details from Secrets Manager + const dbConfig = await getDatabaseConfig(); + + // Create database connection + client = new Client({ + host: dbConfig.host, + port: dbConfig.port, + database: dbConfig.dbname, + user: dbConfig.username, + password: dbConfig.password, + ssl: false, // Adjust based on your RDS configuration + connectionTimeoutMillis: 5000, + query_timeout: 10000 + }); + + await client.connect(); + console.log('Connected to database successfully'); + + // Optimistic approach: try to insert first + const insertQuery = ` + INSERT INTO transaction_history (pet_id, transaction_id, adoption_date, user_id, created_at) + VALUES ($1, $2, $3, $4, $5) + `; + + const values = [ + adoptionData.petId, + adoptionData.transactionId, + new Date(adoptionData.adoptionDate), + adoptionData.userId, + new Date() // Current timestamp for history record + ]; + + const result = await client.query(insertQuery, values); + console.log(`Successfully inserted adoption history record: ${adoptionData.transactionId}, rows affected: ${result.rowCount}`); + + } catch (error) { + // Check if error is due to missing table (PostgreSQL error code 42P01) + if (error.code === '42P01') { + console.log('Table does not exist, creating transaction_history table...'); + await ensureTransactionHistoryTableExists(client); + console.log('Table created, failing this attempt - SQS will retry the message'); + throw new Error('Table was created, message will be retried'); + } else { + console.error('Database write failed:', error); + throw new Error(`Database write failed: ${error.message}`); + } + } finally { + if (client) { + try { + await client.end(); + console.log('Database connection closed'); + } catch (closeError) { + console.error('Error closing database connection:', closeError); + } + } + } +} + +/** + * Ensure the transaction_history table exists, create it if it doesn't + */ +async function ensureTransactionHistoryTableExists(client) { + try { + console.log('Checking if transaction_history table exists...'); + + const createTableQuery = ` + CREATE TABLE IF NOT EXISTS transaction_history ( + id SERIAL PRIMARY KEY, + pet_id VARCHAR(255) NOT NULL, + transaction_id VARCHAR(255) NOT NULL, + adoption_date TIMESTAMP NOT NULL, + user_id VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `; + + await client.query(createTableQuery); + console.log('transaction_history table is ready'); + + // Create indexes for better performance if they don't exist + const indexQueries = [ + 'CREATE INDEX IF NOT EXISTS idx_transaction_history_pet_id ON transaction_history(pet_id)', + 'CREATE INDEX IF NOT EXISTS idx_transaction_history_user_id ON transaction_history(user_id)', + 'CREATE INDEX IF NOT EXISTS idx_transaction_history_adoption_date ON transaction_history(adoption_date)', + 'CREATE INDEX IF NOT EXISTS idx_transaction_history_transaction_id ON transaction_history(transaction_id)' + ]; + + for (const indexQuery of indexQueries) { + await client.query(indexQuery); + } + + console.log('transaction_history table indexes are ready'); + + } catch (error) { + console.error('Error ensuring transaction_history table exists:', error); + throw new Error(`Failed to ensure transaction_history table exists: ${error.message}`); + } +} + +/** + * Get database configuration from AWS Secrets Manager + */ +async function getDatabaseConfig() { + try { + console.log('Retrieving database configuration from Secrets Manager'); + + const result = await secretsManager.getSecretValue({ + SecretId: RDS_SECRET_ARN + }).promise(); + + const secret = JSON.parse(result.SecretString); + console.log('Successfully retrieved database configuration'); + + return { + host: secret.host, + port: secret.port, + database: secret.dbname, + username: secret.username, + password: secret.password + }; + + } catch (error) { + console.error('Failed to retrieve database configuration:', error); + throw new Error(`Failed to retrieve database configuration: ${error.message}`); + } +} \ No newline at end of file diff --git a/PetAdoptions/pethistory/jest.config.js b/PetAdoptions/pethistory/jest.config.js new file mode 100644 index 00000000..853ae34c --- /dev/null +++ b/PetAdoptions/pethistory/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testMatch: ['**/test/**/*.test.js'], + verbose: true +}; \ No newline at end of file diff --git a/PetAdoptions/pethistory/package-lock.json b/PetAdoptions/pethistory/package-lock.json new file mode 100644 index 00000000..39a20755 --- /dev/null +++ b/PetAdoptions/pethistory/package-lock.json @@ -0,0 +1,5291 @@ +{ + "name": "petadopter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "petadopter", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "aws-sdk": "^2.1691.0", + "joi": "^17.13.3", + "pg": "^8.12.0" + }, + "devDependencies": { + "eslint": "^8.57.0", + "jest": "^29.7.0", + "prettier": "^3.3.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001733", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001733.tgz", + "integrity": "sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.199", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", + "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/PetAdoptions/pethistory/package.json b/PetAdoptions/pethistory/package.json new file mode 100644 index 00000000..9a93f3e1 --- /dev/null +++ b/PetAdoptions/pethistory/package.json @@ -0,0 +1,30 @@ +{ + "name": "pethistory", + "version": "1.0.0", + "description": "Lambda function to process adoption messages from SQS and write to database", + "main": "index.js", + "scripts": { + "test": "jest", + "lint": "eslint .", + "format": "prettier --write ." + }, + "dependencies": { + "aws-sdk": "^2.1691.0", + "joi": "^17.13.3", + "pg": "^8.12.0" + }, + "devDependencies": { + "jest": "^29.7.0", + "eslint": "^8.57.0", + "prettier": "^3.3.3" + }, + "keywords": [ + "aws", + "lambda", + "sqs", + "pet-adoption", + "observability" + ], + "author": "AWS Pet Adoption Workshop", + "license": "MIT" +} diff --git a/PetAdoptions/pethistory/template.yaml b/PetAdoptions/pethistory/template.yaml new file mode 100644 index 00000000..fcd4b00a --- /dev/null +++ b/PetAdoptions/pethistory/template.yaml @@ -0,0 +1,215 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: Pet History Lambda function for processing adoption history messages from SQS + +Parameters: + SQSQueueArn: + Type: String + Default: "arn:aws:sqs:REGION:ACCOUNTID:QUEUE_NAME" + Description: ARN of the SQS queue for adoption messages + + RDSSecretArn: + Type: String + Default: "arn:aws:secretsmanager:REGION:ACCOUNTID:secret:SECRET_NAME" + Description: ARN of the RDS secret in Secrets Manager + + VpcId: + Type: String + Default: "vpc-123abc" + Description: VPC ID for Lambda deployment + + SubnetIds: + Type: CommaDelimitedList + Default: "subnet-123abc,subnet-456def" + Description: Comma-delimited list of subnet IDs for Lambda deployment + + SecurityGroupId: + Type: String + Default: "sg-123abc" + Description: Security group ID for Lambda function + +Globals: + Function: + Runtime: nodejs18.x + Architectures: + - x86_64 + Timeout: 60 + MemorySize: 512 + LoggingConfig: + LogFormat: JSON + Environment: + Variables: + AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1 + # Enable X-Ray tracing for Application Signals + Tracing: Active + +Resources: + # Dead Letter Queue for failed history messages + PetHistoryDLQ: + Type: AWS::SQS::Queue + Properties: + QueueName: pethistory-dlq + MessageRetentionPeriod: 1209600 # 14 days + VisibilityTimeout: 60 + Tags: + - Key: Purpose + Value: DeadLetterQueue + - Key: Service + Value: PetHistory + + PetHistoryFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: pethistory + Description: Lambda function to process adoption history messages from SQS and write to transaction_history table + CodeUri: ./ + Handler: index.handler + + # VPC Configuration for RDS access + VpcConfig: + SecurityGroupIds: + - !Ref SecurityGroupId + SubnetIds: !Ref SubnetIds + + # Environment Variables + Environment: + Variables: + RDS_SECRET_ARN: !Ref RDSSecretArn + # CloudWatch Application Signals configuration + AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-instrument + OTEL_SERVICE_NAME: pethistory + OTEL_SERVICE_VERSION: "1.0.0" + OTEL_PROPAGATORS: tracecontext,baggage,b3,xray + OTEL_NODE_DISABLED_INSTRUMENTATIONS: none + # Enable Application Signals + OTEL_AWS_APPLICATION_SIGNALS_ENABLED: true + LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT: lambda:default + OTEL_METRICS_EXPORTER: none + OTEL_LOGS_EXPORTER: none + + # AWS Distro for OpenTelemetry Layer for auto-instrumentation + # Using the correct AWS-managed OTEL layer for us-west-2 + Layers: + - arn:aws:lambda:us-west-2:615299751070:layer:AWSOpenTelemetryDistroJs:8 + + # SQS Event Source + Events: + SQSEvent: + Type: SQS + Properties: + Queue: !Ref SQSQueueArn + BatchSize: 10 + MaximumBatchingWindowInSeconds: 5 + FunctionResponseTypes: + - ReportBatchItemFailures + + # IAM Policies + Policies: + - AWSLambdaBasicExecutionRole + - AWSLambdaVPCAccessExecutionRole + + # Secrets Manager access + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref RDSSecretArn + + # RDS access (if using RDS Proxy or IAM database authentication) + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - rds-db:connect + Resource: !Sub "arn:aws:rds-db:${AWS::Region}:${AWS::AccountId}:dbuser:*/lambda-user" + + # X-Ray and CloudWatch Application Signals permissions + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - xray:PutTraceSegments + - xray:PutTelemetryRecords + - cloudwatch:PutMetricData + - logs:PutLogEvents + - logs:CreateLogGroup + - logs:CreateLogStream + Resource: "*" + + # Application Signals specific permissions + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - application-signals:PutServiceLevelObjective + - application-signals:GetServiceLevelObjective + - application-signals:ListServiceLevelObjectives + Resource: "*" + + # CloudWatch Log Group with retention + PetHistoryLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub "/aws/lambda/${PetHistoryFunction}" + RetentionInDays: 14 + + # CloudWatch Alarms for monitoring + PetHistoryErrorAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: !Sub "${PetHistoryFunction}-Errors" + AlarmDescription: "Pet History Lambda function error rate is too high" + MetricName: Errors + Namespace: AWS/Lambda + Statistic: Sum + Period: 300 + EvaluationPeriods: 2 + Threshold: 5 + ComparisonOperator: GreaterThanThreshold + Dimensions: + - Name: FunctionName + Value: !Ref PetHistoryFunction + TreatMissingData: notBreaching + + PetHistoryDurationAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: !Sub "${PetHistoryFunction}-Duration" + AlarmDescription: "Pet History Lambda function duration is too high" + MetricName: Duration + Namespace: AWS/Lambda + Statistic: Average + Period: 300 + EvaluationPeriods: 2 + Threshold: 50000 # 50 seconds + ComparisonOperator: GreaterThanThreshold + Dimensions: + - Name: FunctionName + Value: !Ref PetHistoryFunction + TreatMissingData: notBreaching + +Outputs: + PetHistoryFunctionArn: + Description: "Pet History Lambda Function ARN" + Value: !GetAtt PetHistoryFunction.Arn + Export: + Name: !Sub "${AWS::StackName}-PetHistoryFunctionArn" + + PetHistoryFunctionName: + Description: "Pet History Lambda Function Name" + Value: !Ref PetHistoryFunction + Export: + Name: !Sub "${AWS::StackName}-PetHistoryFunctionName" + + PetHistoryDLQArn: + Description: "Pet History Dead Letter Queue ARN" + Value: !GetAtt PetHistoryDLQ.Arn + Export: + Name: !Sub "${AWS::StackName}-PetHistoryDLQArn" + + PetHistoryDLQUrl: + Description: "Pet History Dead Letter Queue URL" + Value: !Ref PetHistoryDLQ + Export: + Name: !Sub "${AWS::StackName}-PetHistoryDLQUrl" diff --git a/PetAdoptions/pethistory/test/index.test.js b/PetAdoptions/pethistory/test/index.test.js new file mode 100644 index 00000000..37bb4367 --- /dev/null +++ b/PetAdoptions/pethistory/test/index.test.js @@ -0,0 +1,301 @@ +const { handler } = require('../index'); + +// Mock AWS SDK +jest.mock('aws-sdk', () => ({ + SecretsManager: jest.fn(() => ({ + getSecretValue: jest.fn(() => ({ + promise: jest.fn(() => Promise.resolve({ + SecretString: JSON.stringify({ + host: 'localhost', + port: 5432, + dbname: 'testdb', + username: 'testuser', + password: 'testpass' + }) + })) + })) + })) +})); + +// No X-Ray mocking needed - using CloudWatch Application Signals auto-instrumentation + +// Mock pg +jest.mock('pg', () => ({ + Client: jest.fn(() => ({ + connect: jest.fn(() => Promise.resolve()), + query: jest.fn(() => Promise.resolve({ rowCount: 1, rows: [] })), + end: jest.fn(() => Promise.resolve()) + })) +})); + + + +describe('PetHistory Lambda Function', () => { + beforeEach(() => { + process.env.RDS_SECRET_ARN = 'arn:aws:secretsmanager:us-west-2:123456789012:secret:test-secret'; + process.env.AWS_REGION = 'us-west-2'; + + // Clear the module cache to ensure fresh environment variables + jest.resetModules(); + }); + + afterEach(() => { + jest.clearAllMocks(); + // Clean up environment variables + delete process.env.RDS_SECRET_ARN; + delete process.env.AWS_REGION; + }); + + test('should process valid adoption message successfully', async () => { + const event = { + Records: [ + { + messageId: 'test-message-id', + receiptHandle: 'test-receipt-handle', + body: JSON.stringify({ + transactionId: '123e4567-e89b-12d3-a456-426614174000', + petId: 'pet123', + petType: 'dog', + userId: 'user456', + adoptionDate: '2025-08-08T10:30:00Z', + timestamp: '2025-08-08T10:30:00Z' + }) + } + ] + }; + + const result = await handler(event, {}); + + expect(result.batchItemFailures).toEqual([]); + expect(result.processedCount).toBe(1); + expect(result.results[0].status).toBe('success'); + expect(result.results[0].messageId).toBe('test-message-id'); + }); + + test('should handle invalid JSON message with batch failure', async () => { + const event = { + Records: [ + { + messageId: 'test-message-id', + receiptHandle: 'test-receipt-handle', + body: 'invalid json' + } + ] + }; + + const result = await handler(event, {}); + + expect(result.batchItemFailures).toEqual([ + { itemIdentifier: 'test-message-id' } + ]); + expect(result.processedCount).toBe(0); + expect(result.results).toHaveLength(0); + }); + + test('should handle message with invalid schema with batch failure', async () => { + const event = { + Records: [ + { + messageId: 'test-message-id', + receiptHandle: 'test-receipt-handle', + body: JSON.stringify({ + transactionId: 'invalid-uuid', + petId: 'pet123', + // Missing required fields + }) + } + ] + }; + + const result = await handler(event, {}); + + expect(result.batchItemFailures).toEqual([ + { itemIdentifier: 'test-message-id' } + ]); + expect(result.processedCount).toBe(0); + expect(result.results).toHaveLength(0); + }); + + test('should process multiple messages', async () => { + const event = { + Records: [ + { + messageId: 'test-message-id-1', + receiptHandle: 'test-receipt-handle-1', + body: JSON.stringify({ + transactionId: '123e4567-e89b-12d3-a456-426614174001', + petId: 'pet123', + petType: 'dog', + userId: 'user456', + adoptionDate: '2025-08-08T10:30:00Z', + timestamp: '2025-08-08T10:30:00Z' + }) + }, + { + messageId: 'test-message-id-2', + receiptHandle: 'test-receipt-handle-2', + body: JSON.stringify({ + transactionId: '123e4567-e89b-12d3-a456-426614174002', + petId: 'pet456', + petType: 'cat', + userId: 'user789', + adoptionDate: '2025-08-08T11:30:00Z', + timestamp: '2025-08-08T11:30:00Z' + }) + } + ] + }; + + const result = await handler(event, {}); + + expect(result.batchItemFailures).toEqual([]); + expect(result.processedCount).toBe(2); + expect(result.results).toHaveLength(2); + expect(result.results[0].status).toBe('success'); + expect(result.results[1].status).toBe('success'); + }); + + test('should validate message structure correctly', async () => { + const event = { + Records: [ + { + messageId: 'test-message-id', + receiptHandle: 'test-receipt-handle', + body: JSON.stringify({ + transactionId: '123e4567-e89b-12d3-a456-426614174000', + petId: 'pet123', + petType: 'dog', + userId: 'user456', + adoptionDate: '2025-08-08T10:30:00Z', + timestamp: '2025-08-08T10:30:00Z' + }) + } + ] + }; + + const result = await handler(event, {}); + + expect(result.batchItemFailures).toEqual([]); + expect(result.processedCount).toBe(1); + expect(result.results[0].transactionId).toBe('123e4567-e89b-12d3-a456-426614174000'); + expect(result.results[0].messageId).toBe('test-message-id'); + expect(result.results[0].status).toBe('success'); + }); + + test('should handle empty records array', async () => { + const event = { + Records: [] + }; + + const result = await handler(event, {}); + + expect(result.batchItemFailures).toEqual([]); + expect(result.processedCount).toBe(0); + expect(result.results).toHaveLength(0); + }); + + test('should handle mixed success and failure messages', async () => { + const event = { + Records: [ + { + messageId: 'success-message', + receiptHandle: 'test-receipt-handle-1', + body: JSON.stringify({ + transactionId: '123e4567-e89b-12d3-a456-426614174000', + petId: 'pet123', + petType: 'dog', + userId: 'user456', + adoptionDate: '2025-08-08T10:30:00Z', + timestamp: '2025-08-08T10:30:00Z' + }) + }, + { + messageId: 'failure-message', + receiptHandle: 'test-receipt-handle-2', + body: 'invalid json' + }, + { + messageId: 'another-success-message', + receiptHandle: 'test-receipt-handle-3', + body: JSON.stringify({ + transactionId: '456e7890-e89b-12d3-a456-426614174001', + petId: 'pet456', + petType: 'cat', + userId: 'user789', + adoptionDate: '2025-08-08T11:30:00Z', + timestamp: '2025-08-08T11:30:00Z' + }) + } + ] + }; + + const result = await handler(event, {}); + + expect(result.batchItemFailures).toEqual([ + { itemIdentifier: 'failure-message' } + ]); + expect(result.processedCount).toBe(2); + expect(result.results).toHaveLength(2); + expect(result.results[0].messageId).toBe('success-message'); + expect(result.results[0].status).toBe('success'); + expect(result.results[1].messageId).toBe('another-success-message'); + expect(result.results[1].status).toBe('success'); + }); + + test('should handle all messages failing', async () => { + const event = { + Records: [ + { + messageId: 'failure-message-1', + receiptHandle: 'test-receipt-handle-1', + body: 'invalid json 1' + }, + { + messageId: 'failure-message-2', + receiptHandle: 'test-receipt-handle-2', + body: 'invalid json 2' + } + ] + }; + + const result = await handler(event, {}); + + expect(result.batchItemFailures).toEqual([ + { itemIdentifier: 'failure-message-1' }, + { itemIdentifier: 'failure-message-2' } + ]); + expect(result.processedCount).toBe(0); + expect(result.results).toHaveLength(0); + }); + + test('should return correct response format for SQS batch processing', async () => { + const event = { + Records: [ + { + messageId: 'test-message-id', + receiptHandle: 'test-receipt-handle', + body: JSON.stringify({ + transactionId: '123e4567-e89b-12d3-a456-426614174000', + petId: 'pet123', + petType: 'dog', + userId: 'user456', + adoptionDate: '2025-08-08T10:30:00Z', + timestamp: '2025-08-08T10:30:00Z' + }) + } + ] + }; + + const result = await handler(event, {}); + + // Verify the response has the correct structure for SQS batch processing + expect(result).toHaveProperty('batchItemFailures'); + expect(result).toHaveProperty('processedCount'); + expect(result).toHaveProperty('results'); + expect(Array.isArray(result.batchItemFailures)).toBe(true); + expect(Array.isArray(result.results)).toBe(true); + expect(typeof result.processedCount).toBe('number'); + }); + + +}); \ No newline at end of file diff --git a/PetAdoptions/petlistadoptions-go/Dockerfile b/PetAdoptions/petlistadoptions-go/Dockerfile deleted file mode 100644 index 0fd64bc1..00000000 --- a/PetAdoptions/petlistadoptions-go/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM golang:1.23 as builder -WORKDIR /go/src/app -COPY . . -ENV GOPROXY=https://goproxy.io,direct -RUN go get . -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . - -FROM alpine:latest -WORKDIR /app -RUN apk --no-cache add ca-certificates -COPY --from=builder /go/src/app/app . -EXPOSE 80 -CMD ["./app"] diff --git a/PetAdoptions/petlistadoptions-go/benchmark/Dockerfile b/PetAdoptions/petlistadoptions-go/benchmark/Dockerfile deleted file mode 100644 index e1ea9ab9..00000000 --- a/PetAdoptions/petlistadoptions-go/benchmark/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM rust:latest as builder -WORKDIR /app -RUN -COPY . . -RUN cargo install drill -CMD ["./benchmark.sh"] diff --git a/PetAdoptions/petlistadoptions-go/benchmark/README.md b/PetAdoptions/petlistadoptions-go/benchmark/README.md deleted file mode 100644 index 9becb502..00000000 --- a/PetAdoptions/petlistadoptions-go/benchmark/README.md +++ /dev/null @@ -1,52 +0,0 @@ -## Drill for PetListAdoptions - -Using [drill](https://github.com/fcsonline/drill), this allows to generate -traffic in the PetSite EKS cluster or locally in dev, without spinning ECS -traffic generator. - -This can be useful to test all APIs or boost traffic for Application Signals. - -### (Optional) Pick your benchmark environment - -In the `benchmark.yaml` file, change the base URL to your PetListAdoptions ALB -endpoint if you are not testing it locally as the default is local. - -```yaml -concurrency: 4 -base: "http://Servic-lista-[...].eu-central-1.elb.amazonaws.com" -``` - -### Running locally - -[Install drill](https://github.com/fcsonline/drill?tab=readme-ov-file#install) -and run - -```bash -drill --benchmark benchmark.yaml -``` - -### Running in EKS - -1. Authenticate against your EKS cluster - -```bash -aws eks update-kubeconfig --name PetSite --region -``` - -2. Create an ECR image `drill-petlistadoptions` - -3. Build and push - -```bash -aws ecr get-login-password --region | docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com -docker buildx build -t drill-petlistadoptions . --platform=linux/amd64 -docker tag drill-petlistadoptions:latest .dkr.ecr..amazonaws.com/drill-petlistadoptions:latest -docker push .dkr.ecr..amazonaws.com/drill-petlistadoptions:latest -``` - -4. Run in EKS - -```bash -kubectl run -it drill-petlistadoptions --image .dkr.ecr..amazonaws.com/drill-petlistadoptions:latest -``` - diff --git a/PetAdoptions/petlistadoptions-go/benchmark/benchmark.sh b/PetAdoptions/petlistadoptions-go/benchmark/benchmark.sh deleted file mode 100755 index 029acc1e..00000000 --- a/PetAdoptions/petlistadoptions-go/benchmark/benchmark.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -while true -do - drill -s --benchmark benchmark.yaml - sleep 1 -done diff --git a/PetAdoptions/petlistadoptions-go/benchmark/benchmark.yaml b/PetAdoptions/petlistadoptions-go/benchmark/benchmark.yaml deleted file mode 100644 index c2c85126..00000000 --- a/PetAdoptions/petlistadoptions-go/benchmark/benchmark.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -concurrency: 5 -base: "http://localhost:80" -iterations: 100 -rampup: 2 - -plan: - - name: Health check - request: - url: /health/status - method: GET - - - name: List adoptions - request: - url: /api/adoptionlist/ - method: GET - - - name: Get Prometheus metrics - request: - url: /metrics - method: GET - - - name: List adoptions - request: - url: /api/adoptionlist/ - method: GET diff --git a/PetAdoptions/petlistadoptions-go/config.go b/PetAdoptions/petlistadoptions-go/config.go deleted file mode 100644 index f19ef70e..00000000 --- a/PetAdoptions/petlistadoptions-go/config.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "petadoptions/petlistadoptions" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/secretsmanager" - "github.com/aws/aws-sdk-go-v2/service/ssm" - "github.com/go-kit/log" - "github.com/go-kit/log/level" - "github.com/spf13/viper" -) - -type dbConfig struct { - Engine, Host, Username, Password, Dbname string - Port int -} - -// config is injected as environment variable -func fetchConfig(ctx context.Context, logger log.Logger) (petlistadoptions.Config, error) { - - // fetch from env - viper.SetEnvPrefix("app") - viper.AutomaticEnv() // Bind automatically all env vars that have the same prefix - - awsCfg, err := config.LoadDefaultConfig(ctx) - if err != nil { - level.Error(logger).Log("aws", err) - } - - cfg := petlistadoptions.Config{ - PetSearchURL: viper.GetString("PET_SEARCH_URL"), - RDSSecretArn: viper.GetString("RDS_SECRET_ARN"), - RDSReaderEndpoint: viper.GetString("RDS_READER_ENDPOINT"), - AWSCfg: awsCfg, - } - - if cfg.PetSearchURL == "" || cfg.RDSSecretArn == "" { - return fetchConfigFromParameterStore(ctx, cfg) - } - - return cfg, nil -} - -func fetchConfigFromParameterStore(ctx context.Context, cfg petlistadoptions.Config) (petlistadoptions.Config, error) { - svc := ssm.NewFromConfig(cfg.AWSCfg) - - res, err := svc.GetParameters(ctx, &ssm.GetParametersInput{ - Names: []string{ - "/petstore/rdssecretarn", - "/petstore/searchapiurl", - "/petstore/rds-reader-endpoint", - }, - }) - - newCfg := petlistadoptions.Config{ - AWSCfg: cfg.AWSCfg, - } - - if err != nil { - return newCfg, err - } - - for _, p := range res.Parameters { - pValue := aws.ToString(p.Value) - - switch aws.ToString(p.Name) { - case "/petstore/rdssecretarn": - newCfg.RDSSecretArn = pValue - case "/petstore/searchapiurl": - newCfg.PetSearchURL = pValue - case "/petstore/rds-reader-endpoint": - newCfg.RDSReaderEndpoint = pValue - } - } - - return newCfg, err -} - -func getSecretValue(ctx context.Context, cfg petlistadoptions.Config) (string, error) { - svc := secretsmanager.NewFromConfig(cfg.AWSCfg) - res, err := svc.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ - SecretId: aws.String(cfg.RDSSecretArn), - }) - - if err != nil { - return "", err - } - - return aws.ToString(res.SecretString), nil -} - -// Call aws secrets manager and return parsed sql server query str -func getRDSConnectionString(ctx context.Context, cfg petlistadoptions.Config) (string, error) { - jsonstr, err := getSecretValue(ctx, cfg) - if err != nil { - return "", err - } - - var c dbConfig - - if err := json.Unmarshal([]byte(jsonstr), &c); err != nil { - return "", err - } - c.Host = cfg.RDSReaderEndpoint - - query := url.Values{} - // database should be in config - query.Set("database", "adoptions") - - u := &url.URL{ - Scheme: c.Engine, - User: url.UserPassword(c.Username, c.Password), - Host: fmt.Sprintf("%s:%d", c.Host, c.Port), - Path: c.Dbname, - } - - return u.String(), nil -} diff --git a/PetAdoptions/petlistadoptions-go/go.mod b/PetAdoptions/petlistadoptions-go/go.mod deleted file mode 100644 index e584c825..00000000 --- a/PetAdoptions/petlistadoptions-go/go.mod +++ /dev/null @@ -1,82 +0,0 @@ -module petadoptions - -go 1.22.7 - -toolchain go1.23.3 - -require ( - github.com/XSAM/otelsql v0.36.0 - github.com/aws/aws-sdk-go-v2 v1.32.7 - github.com/aws/aws-sdk-go-v2/config v1.28.7 - github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.8 - github.com/aws/aws-sdk-go-v2/service/ssm v1.56.2 - github.com/go-kit/kit v0.13.0 - github.com/go-kit/log v0.2.1 - github.com/gorilla/mux v1.8.1 - github.com/lib/pq v1.10.9 - github.com/prometheus/client_golang v1.20.5 - github.com/spf13/viper v1.19.0 - go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.58.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 - go.opentelemetry.io/contrib/propagators/aws v1.33.0 - go.opentelemetry.io/otel v1.33.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 - go.opentelemetry.io/otel/sdk v1.33.0 - go.opentelemetry.io/otel/trace v1.33.0 - google.golang.org/grpc v1.69.2 -) - -require ( - github.com/aws/aws-sdk-go-v2/credentials v1.17.48 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect - github.com/aws/smithy-go v1.22.1 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/magiconair/properties v1.8.9 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.61.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/sagikazarmark/locafero v0.6.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/proto/otlp v1.4.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb // indirect - google.golang.org/protobuf v1.36.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/PetAdoptions/petlistadoptions-go/go.sum b/PetAdoptions/petlistadoptions-go/go.sum deleted file mode 100644 index ba63e2eb..00000000 --- a/PetAdoptions/petlistadoptions-go/go.sum +++ /dev/null @@ -1,181 +0,0 @@ -github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/XSAM/otelsql v0.36.0 h1:SvrlOd/Hp0ttvI9Hu0FUWtISTTDNhQYwxe8WB4J5zxo= -github.com/XSAM/otelsql v0.36.0/go.mod h1:fo4M8MU+fCn/jDfu+JwTQ0n6myv4cZ+FU5VxrllIlxY= -github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw= -github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= -github.com/aws/aws-sdk-go-v2/config v1.28.7 h1:GduUnoTXlhkgnxTD93g1nv4tVPILbdNQOzav+Wpg7AE= -github.com/aws/aws-sdk-go-v2/config v1.28.7/go.mod h1:vZGX6GVkIE8uECSUHB6MWAUsd4ZcG2Yq/dMa4refR3M= -github.com/aws/aws-sdk-go-v2/credentials v1.17.48 h1:IYdLD1qTJ0zanRavulofmqut4afs45mOWEI+MzZtTfQ= -github.com/aws/aws-sdk-go-v2/credentials v1.17.48/go.mod h1:tOscxHN3CGmuX9idQ3+qbkzrjVIx32lqDSU1/0d/qXs= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 h1:kqOrpojG71DxJm/KDPO+Z/y1phm1JlC8/iT+5XRmAn8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22/go.mod h1:NtSFajXVVL8TA2QNngagVZmUtXciyrHOt7xgz4faS/M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQzZHqe/3FE+cqwfH+0p5Jo8PFM/QYQSmeZ+M= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7/go.mod h1:kLPQvGUmxn/fqiCrDeohwG33bq2pQpGeY62yRO6Nrh0= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.8 h1:WT3EPriVEpHE2jeNqHqj7l43JCIWPoZjNNRluZ7agII= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.8/go.mod h1:By/yiMzR0yfhPaqRWE3GrT9B/Z6871z1GfWGc+vf4Y8= -github.com/aws/aws-sdk-go-v2/service/ssm v1.56.2 h1:MOxvXH2kRP5exvqJxAZ0/H9Ar51VmADJh95SgZE8u60= -github.com/aws/aws-sdk-go-v2/service/ssm v1.56.2/go.mod h1:RKWoqC9FlgMCkrfVOtgfqfwdaUIaq8H93UAt4xNaR0A= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 h1:CvuUmnXI7ebaUAhbJcDy9YQx8wHR69eZ9I7q5hszt/g= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.8/go.mod h1:XDeGv1opzwm8ubxddF0cgqkZWsyOtw4lr6dxwmb6YQg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N/NanhK16ydHW1pahX6E= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7/go.mod h1:JfyQ0g2JG8+Krq0EuZNnRwX0mU0HrwY/tG6JNfcqh4k= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgppnRczLxlMs5Ae/QY= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.3/go.mod h1:5Gn+d+VaaRgsjewpMvGazt0WfcFO+Md4wLOuBfGR9Bc= -github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= -github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= -github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= -github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= -github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.58.0 h1:2FsX0gnVQ86Oxl6+/upUEEEzp6zxCrdW6Vinn2AHf4c= -go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.58.0/go.mod h1:K2ZKy/OSebEHjXeym30VZUclNfVpJTkt/DlaP5fQRuw= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/contrib/propagators/aws v1.33.0 h1:MefPfPIut0IxEiQRK1qVv5AFADBOwizl189+m7QhpFg= -go.opentelemetry.io/contrib/propagators/aws v1.33.0/go.mod h1:VB6xPo12uW/PezOqtA/cY2/DiAGYshnhID606wC9NEY= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU= -go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= -golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb h1:B7GIB7sr443wZ/EAEl7VZjmh1V6qzkt5V+RYcUYtS1U= -google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:E5//3O5ZIG2l71Xnt+P/CYUY8Bxs8E7WMoZ9tlcMbAY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb h1:3oy2tynMOP1QbTC0MsNNAV+Se8M2Bd0A5+x1QHyw+pI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= -google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= -google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= -google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/PetAdoptions/petlistadoptions-go/main.go b/PetAdoptions/petlistadoptions-go/main.go deleted file mode 100644 index d4f4ade8..00000000 --- a/PetAdoptions/petlistadoptions-go/main.go +++ /dev/null @@ -1,151 +0,0 @@ -package main - -import ( - "context" - "database/sql" - "flag" - "fmt" - "net/http" - "os" - "os/signal" - "syscall" - - "petadoptions/petlistadoptions" - - "github.com/XSAM/otelsql" - "github.com/go-kit/log" - "github.com/go-kit/log/level" - _ "github.com/lib/pq" - "go.opentelemetry.io/contrib/propagators/aws/xray" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.26.0" - "go.opentelemetry.io/otel/trace" - "google.golang.org/grpc" -) - -const otelServiceName = "petlistadoptions" - -var tracer trace.Tracer - -func otelInit(ctx context.Context) { - // Create new OTLP Exporter struct - - endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") - if endpoint == "" { - endpoint = "0.0.0.0:4317" // setting default endpoint for exporter - } - traceExporter, _ := otlptracegrpc.New(ctx, otlptracegrpc.WithInsecure(), otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithDialOption(grpc.WithBlock())) - - res := resource.NewWithAttributes( - semconv.SchemaURL, - // the service name used to display traces in backends - semconv.ServiceNameKey.String(otelServiceName), - ) - // Create a new TraceProvider struct passing in the config, the exporter - // and the ID Generator we want to use for our tracing - tp := sdktrace.NewTracerProvider( - // AlwaysSample() returns a Sampler that samples every trace. - // Be careful about using this sampler in a production application with - // significant traffic: a new trace will be started and exported for every request. - sdktrace.WithSampler(sdktrace.AlwaysSample()), - sdktrace.WithBatcher(traceExporter), - sdktrace.WithResource(res), - ) - // Set the traceprovider and the propagator we want to use - otel.SetTracerProvider(tp) - otel.SetTextMapPropagator(xray.Propagator{}) - - tracer = tp.Tracer(otelServiceName) -} - -func main() { - ctx := context.Background() - otelInit(ctx) - - var ( - httpAddr = flag.String("http.addr", ":80", "HTTP Port binding") - ) - - flag.Parse() - - var logger log.Logger - logger = log.NewJSONLogger(log.NewSyncWriter(os.Stdout)) - logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller) - - var cfg petlistadoptions.Config - { - var err error - cfg, err = fetchConfig(ctx, logger) - if err != nil { - level.Error(logger).Log("exit", err) - os.Exit(-1) - } - cfg.Tracer = tracer - } - - var db *sql.DB - { - var err error - var connStr string - - connStr, err = getRDSConnectionString(ctx, cfg) - if err != nil { - level.Error(logger).Log("exit", err) - os.Exit(-1) - } - - // OTEL does not instrument yet database/sql, falling back to the native - // go sql interface - // https://github.com/open-telemetry/opentelemetry-go-contrib/issues/5 - db, err = otelsql.Open("postgres", connStr, otelsql.WithAttributes( - semconv.DBSystemKey.String("postgres"), - ), - ) - if err != nil { - level.Error(logger).Log("exit", err) - os.Exit(-1) - } - - // Register DB stats to meter - // TODO: collect DB metrics - err = otelsql.RegisterDBStatsMetrics(db, otelsql.WithAttributes( - semconv.DBSystemMySQL, - )) - if err != nil { - level.Error(logger).Log("RegisterDBStatsMetrics error", err) - } - - defer db.Close() - } - - var s petlistadoptions.Service - { - - safeConnStr, _ := getRDSConnectionString(ctx, cfg) - repo := petlistadoptions.NewRepository(db, logger, safeConnStr) - s = petlistadoptions.NewService(logger, repo, cfg.PetSearchURL) - s = petlistadoptions.NewInstrumenting(logger, s) - } - - var h http.Handler - { - h = petlistadoptions.MakeHTTPHandler(s, logger) - } - - errs := make(chan error) - go func() { - c := make(chan os.Signal) - signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) - errs <- fmt.Errorf("%s", <-c) - }() - - go func() { - logger.Log("transport", "HTTP", "addr", *httpAddr) - errs <- http.ListenAndServe(*httpAddr, h) - }() - - logger.Log("exit", <-errs) -} diff --git a/PetAdoptions/petlistadoptions-go/petlistadoptions/endpoint.go b/PetAdoptions/petlistadoptions-go/petlistadoptions/endpoint.go deleted file mode 100644 index da9b9a1b..00000000 --- a/PetAdoptions/petlistadoptions-go/petlistadoptions/endpoint.go +++ /dev/null @@ -1,31 +0,0 @@ -package petlistadoptions - -import ( - "context" - - "github.com/go-kit/kit/endpoint" -) - -type Endpoints struct { - HealthCheckEndpoint endpoint.Endpoint - ListAdoptionsEndpoint endpoint.Endpoint -} - -func MakeEndpoints(s Service) Endpoints { - return Endpoints{ - HealthCheckEndpoint: makeHealthCheckEndpoint(s), - ListAdoptionsEndpoint: makeListAdoptionsEndpoint(s), - } -} - -func makeHealthCheckEndpoint(s Service) endpoint.Endpoint { - return func(ctx context.Context, _ interface{}) (interface{}, error) { - return s.HealthCheck(ctx) - } -} - -func makeListAdoptionsEndpoint(s Service) endpoint.Endpoint { - return func(ctx context.Context, request interface{}) (interface{}, error) { - return s.ListAdoptions(ctx) - } -} diff --git a/PetAdoptions/petlistadoptions-go/petlistadoptions/middlewares.go b/PetAdoptions/petlistadoptions-go/petlistadoptions/middlewares.go deleted file mode 100644 index 520ef117..00000000 --- a/PetAdoptions/petlistadoptions-go/petlistadoptions/middlewares.go +++ /dev/null @@ -1,84 +0,0 @@ -package petlistadoptions - -import ( - "context" - "fmt" - "time" - - "github.com/go-kit/kit/metrics" - kitprometheus "github.com/go-kit/kit/metrics/prometheus" - "github.com/go-kit/log" - stdprometheus "github.com/prometheus/client_golang/prometheus" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -type middleware struct { - logger log.Logger - requestCount metrics.Counter - requestLatency metrics.Histogram - Service -} - -func NewInstrumenting(logger log.Logger, s Service) Service { - labels := []string{"endpoint", "error"} - return &middleware{ - logger: logger, - Service: s, - requestCount: kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: "petlistadoptions", - Name: "requests_total", - Help: "Number of requests received", - }, labels), - requestLatency: kitprometheus.NewHistogramFrom(stdprometheus.HistogramOpts{ - Namespace: "petlistadoptions", - Name: "requests_latency_seconds", - Help: "Request durations in seconds", - }, labels), - } -} - -func (mw *middleware) ListAdoptions(ctx context.Context) (ax []Adoption, err error) { - defer func(begin time.Time) { - labelValues := []string{"endpoint", "adoptionlist", "error", fmt.Sprint(err != nil)} - mw.requestCount.With(labelValues...).Add(1) - mw.requestLatency.With(labelValues...).Observe(time.Since(begin).Seconds()) - - span := trace.SpanFromContext(ctx) - if span == nil { - return - } - - span.SetAttributes( - attribute.Float64("timeTakenSeconds", time.Since(begin).Seconds()), - attribute.Int("resultCount", len(ax)), - ) - - err2 := mw.logger.Log( - "method", "ListAdoptionsMiddleware", - "xrayTraceId", getXrayTraceID(span), - "resultCount", len(ax), - "took", time.Since(begin), - "err", err) - if err2 != nil { - fmt.Println("log error", err2) - } - }(time.Now()) - - return mw.Service.ListAdoptions(ctx) -} - -func (mw *middleware) HealthCheck(ctx context.Context) (res string, err error) { - defer func(begin time.Time) { - labelValues := []string{"endpoint", "health_check", "error", fmt.Sprint(err != nil)} - mw.requestCount.With(labelValues...).Add(1) - mw.requestLatency.With(labelValues...).Observe(time.Since(begin).Seconds()) - }(time.Now()) - return mw.Service.HealthCheck(ctx) -} - -func getXrayTraceID(span trace.Span) string { - xrayTraceID := span.SpanContext().TraceID().String() - result := fmt.Sprintf("1-%s-%s", xrayTraceID[0:8], xrayTraceID[8:]) - return result -} diff --git a/PetAdoptions/petlistadoptions-go/petlistadoptions/repository.go b/PetAdoptions/petlistadoptions-go/petlistadoptions/repository.go deleted file mode 100644 index 9f2b413c..00000000 --- a/PetAdoptions/petlistadoptions-go/petlistadoptions/repository.go +++ /dev/null @@ -1,156 +0,0 @@ -package petlistadoptions - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "net/http" - "sync" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/go-kit/log" - "github.com/go-kit/log/level" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// Repository as an interface to define data store interactions -type Repository interface { - GetLatestAdoptions(ctx context.Context, petSearchURL string) ([]Adoption, error) -} - -type Config struct { - PetSearchURL string - RDSSecretArn string - RDSReaderEndpoint string - Tracer trace.Tracer - AWSCfg aws.Config -} - -// repo as an implementation of Repository with dependency injection -type repo struct { - db *sql.DB - logger log.Logger - safeConnStr string -} - -func NewRepository(db *sql.DB, logger log.Logger, safeConnStr string) Repository { - return &repo{ - db: db, - logger: logger, - safeConnStr: safeConnStr, - } -} - -type transaction struct { - TransactionID string - PetID string - AdoptionDate time.Time -} - -type pet struct { - Availability string `json:"availability,omitempty"` - CutenessRate string `json:"cuteness_rate,omitempty"` - PetColor string `json:"petcolor,omitempty"` - PetID string `json:"petid,omitempty"` - PetType string `json:"pettype,omitempty"` - PetURL string `json:"peturl,omitempty"` - Price string `json:"price,omitempty"` -} - -func (r *repo) GetLatestAdoptions(ctx context.Context, petSearchURL string) ([]Adoption, error) { - logger := log.With(r.logger, "method", "GetTopTransactions") - - tracer := otel.GetTracerProvider().Tracer("petlistadoptions") - _, span := tracer.Start(ctx, "PGSQL Query", trace.WithSpanKind(trace.SpanKindClient)) - - sql := `SELECT pet_id, transaction_id, adoption_date FROM transactions ORDER BY id DESC LIMIT 25` - // TODO: implement native sql instrumentation when issue is closed. - // https://github.com/open-telemetry/opentelemetry-go-contrib/issues/5 - //rows, err := r.db.QueryContext(ctx, sql) - - span.SetAttributes( - attribute.String("sql", sql), - attribute.String("url", r.safeConnStr), - ) - - rows, err := r.db.Query(sql) - if err != nil { - logger.Log("error", err) - return nil, err - } - span.End() - - var wg sync.WaitGroup - adoptions := make(chan Adoption) - - for rows.Next() { - t := transaction{} - - err := rows.Scan(&t.PetID, &t.TransactionID, &t.AdoptionDate) - - if err != nil { - level.Error(logger).Log("err", err) - continue - } - wg.Add(1) - go searchForPet(ctx, r.logger, &wg, adoptions, t, petSearchURL) - } - - go func() { - wg.Wait() - close(adoptions) - }() - - res := []Adoption{} - - for i := range adoptions { - logger.Log("petid", i.PetID, "pettype", i.PetType, "petcolor", i.PetColor, "xrayTraceId", getXrayTraceID(span)) - res = append(res, i) - } - - return res, nil -} - -func searchForPet(ctx context.Context, logger log.Logger, wg *sync.WaitGroup, queue chan Adoption, t transaction, petSearchURL string) { - logger = log.With(logger, "method", "searchForPet", "petid", t.PetID) - defer wg.Done() - - url := fmt.Sprintf("%spetid=%s", petSearchURL, t.PetID) - - client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} - - req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) - resp, err := client.Do(req) - if err != nil { - level.Error(logger).Log("err", err) - return - } - - pets := []pet{} - err = json.NewDecoder(resp.Body).Decode(&pets) - if err != nil { - level.Error(logger).Log("err", err) - return - } - - for _, p := range pets { - // Merging elements from response. Result for petsearch is return as array - - queue <- Adoption{ - AdoptionDate: t.AdoptionDate, - Availability: p.Availability, - CutenessRate: p.CutenessRate, - PetColor: p.PetColor, - PetID: p.PetID, - PetType: p.PetType, - PetURL: p.PetURL, - Price: p.Price, - TransactionID: t.TransactionID, - } - } -} diff --git a/PetAdoptions/petlistadoptions-go/petlistadoptions/service.go b/PetAdoptions/petlistadoptions-go/petlistadoptions/service.go deleted file mode 100644 index 463d7c82..00000000 --- a/PetAdoptions/petlistadoptions-go/petlistadoptions/service.go +++ /dev/null @@ -1,59 +0,0 @@ -package petlistadoptions - -import ( - "context" - "time" - - "github.com/go-kit/log" - "github.com/go-kit/log/level" -) - -type Adoption struct { - TransactionID string `json:"transactionid,omitempty"` - AdoptionDate time.Time `json:"adoptiondate,omitempty"` - Availability string `json:"availability,omitempty"` - CutenessRate string `json:"cuteness_rate,omitempty"` - PetColor string `json:"petcolor,omitempty"` - PetID string `json:"petid,omitempty"` - PetType string `json:"pettype,omitempty"` - PetURL string `json:"peturl,omitempty"` - Price string `json:"price,omitempty"` -} - -// links endpoints to transport -type Service interface { - HealthCheck(ctx context.Context) (string, error) - ListAdoptions(ctx context.Context) ([]Adoption, error) -} - -// object that handles the logic and complies with interface -type service struct { - logger log.Logger - repository Repository - petSearchURL string -} - -// inject dependencies into core logic -func NewService(logger log.Logger, rep Repository, petSearchURL string) Service { - return &service{ - logger: logger, - repository: rep, - petSearchURL: petSearchURL, - } -} - -func (s service) HealthCheck(ctx context.Context) (string, error) { - return "alive", nil -} - -func (s service) ListAdoptions(ctx context.Context) ([]Adoption, error) { - - res, err := s.repository.GetLatestAdoptions(ctx, s.petSearchURL) - - if err != nil { - logger := log.With(s.logger, "method", "ListAdoptions") - level.Error(logger).Log("err", err) - } - - return res, err -} diff --git a/PetAdoptions/petlistadoptions-go/petlistadoptions/transport.go b/PetAdoptions/petlistadoptions-go/petlistadoptions/transport.go deleted file mode 100644 index 584a53c5..00000000 --- a/PetAdoptions/petlistadoptions-go/petlistadoptions/transport.go +++ /dev/null @@ -1,104 +0,0 @@ -package petlistadoptions - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - - "github.com/go-kit/kit/transport" - httptransport "github.com/go-kit/kit/transport/http" - "github.com/go-kit/log" - "github.com/gorilla/mux" - "github.com/prometheus/client_golang/prometheus/promhttp" - "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" -) - -func MakeHTTPHandler(s Service, logger log.Logger) http.Handler { - r := mux.NewRouter() - - //Use open telementry instrumentation provided by gorilla - r.Use(otelmux.Middleware("petlistadoptions")) - - e := MakeEndpoints(s) - options := []httptransport.ServerOption{ - httptransport.ServerErrorHandler(transport.NewLogErrorHandler(logger)), - httptransport.ServerErrorEncoder(encodeError), - httptransport.ServerFinalizer(loggingMiddleware), - } - - r.Methods("GET").Path("/health/status").Handler(httptransport.NewServer( - e.HealthCheckEndpoint, - decodeEmptyRequest, - encodeResponse, - options..., - )) - - r.Methods("GET").Path("/api/adoptionlist/").Handler(httptransport.NewServer( - e.ListAdoptionsEndpoint, - decodeEmptyRequest, - encodeResponse, - options..., - )) - - r.Methods("GET").Path("/metrics").Handler(promhttp.Handler()) - - return r -} - -type errorer interface { - error() error -} - -var ( - ErrNotFound = errors.New("not found") - ErrBadRequest = errors.New("bad request parameters") -) - -func decodeEmptyRequest(_ context.Context, r *http.Request) (interface{}, error) { - return nil, nil -} - -func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { - if e, ok := response.(errorer); ok && e.error() != nil { - encodeError(ctx, e.error(), w) - return nil - } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - return json.NewEncoder(w).Encode(response) -} - -func encodeEmptyResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { - if e, ok := response.(errorer); ok && e.error() != nil { - encodeError(ctx, e.error(), w) - return nil - } - return nil -} - -func encodeError(_ context.Context, err error, w http.ResponseWriter) { - if err == nil { - panic("encodeError with nil error") - } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(codeFrom(err)) - json.NewEncoder(w).Encode(map[string]interface{}{ - "error": err.Error(), - }) -} - -func codeFrom(err error) int { - switch err { - case ErrNotFound: - return http.StatusNotFound - case ErrBadRequest: - return http.StatusBadRequest - default: - return http.StatusInternalServerError - } -} - -func loggingMiddleware(ctx context.Context, code int, r *http.Request) { - fmt.Println(r.Method, r.RequestURI, r.Proto, r.RemoteAddr, code) -} diff --git a/PetAdoptions/petlistadoptions-py/Dockerfile b/PetAdoptions/petlistadoptions-py/Dockerfile new file mode 100644 index 00000000..9cb21ca8 --- /dev/null +++ b/PetAdoptions/petlistadoptions-py/Dockerfile @@ -0,0 +1,49 @@ +FROM python:3.11-slim as builder + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Production stage +FROM python:3.11-slim + +WORKDIR /app + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + libpq5 \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy Python packages from builder +COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + +# Copy application code +COPY --from=builder /app . + +# Make start script executable +RUN chmod +x start.sh + +# Create non-root user for future use (but run as root for port 80 access) +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app + +# Add health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:80/health/status || exit 1 + +EXPOSE 80 + +# Use startup script (running as root for port 80 access) +CMD ["./start.sh"] \ No newline at end of file diff --git a/PetAdoptions/petlistadoptions-py/app.py b/PetAdoptions/petlistadoptions-py/app.py new file mode 100644 index 00000000..bb6c9993 --- /dev/null +++ b/PetAdoptions/petlistadoptions-py/app.py @@ -0,0 +1,235 @@ +import os +import json +import logging +import time +from datetime import datetime +from typing import List, Dict, Any, Optional +from contextlib import contextmanager + +import boto3 +import psycopg2 +import requests +from fastapi import FastAPI, HTTPException +from fastapi.responses import JSONResponse +from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST +from pydantic import BaseModel + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Prometheus metrics +REQUEST_COUNT = Counter('petlistadoptions_requests_total', 'Number of requests received', ['endpoint', 'error']) +REQUEST_LATENCY = Histogram('petlistadoptions_requests_latency_seconds', 'Request durations in seconds', ['endpoint', 'error']) + +# Pydantic models for type safety +class Adoption(BaseModel): + transactionid: Optional[str] = None + adoptiondate: Optional[str] = None + availability: Optional[str] = None + cuteness_rate: Optional[str] = None + petcolor: Optional[str] = None + petid: Optional[str] = None + pettype: Optional[str] = None + peturl: Optional[str] = None + price: Optional[str] = None + +class HealthResponse(BaseModel): + status: str + +# Create FastAPI app +app = FastAPI( + title="Pet List Adoptions Service", + description="Service for listing pet adoptions with enrichment from pet search", + version="1.0.0" +) + +class DatabaseConfig: + """Database configuration from AWS Secrets Manager""" + def __init__(self, engine: str, host: str, username: str, password: str, dbname: str, port: int): + self.engine = engine + self.host = host + self.username = username + self.password = password + self.dbname = dbname + self.port = port + +class PetAdoptionsService: + """Main service class following Python best practices""" + + def __init__(self): + self.pet_search_url = os.getenv("APP_PET_SEARCH_URL") + self.rds_secret_arn = os.getenv("APP_RDS_SECRET_ARN") + + # If not set via env vars, try to get from Parameter Store + if not self.pet_search_url or not self.rds_secret_arn: + self._fetch_from_parameter_store() + + def _fetch_from_parameter_store(self): + """Fetch configuration from AWS Parameter Store""" + try: + ssm = boto3.client('ssm') + response = ssm.get_parameters( + Names=[ + '/petstore/rdssecretarn', + '/petstore/searchapiurl' + ] + ) + + for param in response['Parameters']: + if param['Name'] == '/petstore/rdssecretarn': + self.rds_secret_arn = param['Value'] + elif param['Name'] == '/petstore/searchapiurl': + self.pet_search_url = param['Value'] + + except Exception as e: + logger.error(f"Failed to fetch from Parameter Store: {e}") + + def _get_database_connection_string(self) -> str: + """Get database connection string from AWS Secrets Manager""" + try: + # Check if this is a local test setup + if self.rds_secret_arn == "local-secret": + # Read from local file for testing + with open("/app/local-secret.json", "r") as f: + secret_data = json.loads(f.read()) + else: + # Use AWS Secrets Manager + secrets = boto3.client('secretsmanager') + response = secrets.get_secret_value(SecretId=self.rds_secret_arn) + secret_data = json.loads(response['SecretString']) + + # Build connection string for psycopg2 + connection_string = f"postgresql://{secret_data['username']}:{secret_data['password']}@{secret_data['host']}:{secret_data.get('port', 5432)}/{secret_data['dbname']}" + + logger.info(f"Generated connection string for host: {secret_data['host']}") + return connection_string + + except Exception as e: + logger.error(f"Failed to get database connection string: {e}") + raise + + @contextmanager + def _get_database_connection(self): + """Context manager for database connections""" + connection_string = self._get_database_connection_string() + conn = psycopg2.connect(connection_string) + try: + yield conn + finally: + conn.close() + + def _get_latest_adoptions(self) -> List[Dict[str, Any]]: + """Get latest adoptions from database""" + with self._get_database_connection() as conn: + with conn.cursor() as cursor: + cursor.execute("SELECT pet_id, transaction_id, adoption_date FROM transactions ORDER BY id DESC LIMIT 25") + rows = cursor.fetchall() + + return [ + { + 'pet_id': pet_id, + 'transaction_id': transaction_id, + 'adoption_date': adoption_date.isoformat() if adoption_date else None + } + for pet_id, transaction_id, adoption_date in rows + ] + + def _search_pet_info(self, pet_id: str) -> List[Dict[str, Any]]: + """Search for pet information by pet_id""" + url = f"{self.pet_search_url}petid={pet_id}" + + try: + response = requests.get(url) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Failed to search pet {pet_id}: {e}") + return [] + + def health_check(self) -> str: + """Health check endpoint""" + return "alive" + + def list_adoptions(self) -> List[Adoption]: + """List adoptions with pet information""" + start_time = time.time() + + try: + # Get adoptions from database + adoptions = self._get_latest_adoptions() + + # Enrich with pet information + enriched_adoptions = [] + for adoption in adoptions: + pet_info = self._search_pet_info(adoption['pet_id']) + + for pet in pet_info: + enriched_adoption = Adoption( + transactionid=adoption['transaction_id'], + adoptiondate=adoption['adoption_date'], + availability=pet.get('availability', ''), + cuteness_rate=pet.get('cuteness_rate', ''), + petcolor=pet.get('petcolor', ''), + petid=pet.get('petid', ''), + pettype=pet.get('pettype', ''), + peturl=pet.get('peturl', ''), + price=pet.get('price', '') + ) + enriched_adoptions.append(enriched_adoption) + + # Record metrics + duration = time.time() - start_time + REQUEST_COUNT.labels(endpoint='adoptionlist', error='false').inc() + REQUEST_LATENCY.labels(endpoint='adoptionlist', error='false').observe(duration) + + return enriched_adoptions + + except Exception as e: + # Record error metrics + duration = time.time() - start_time + REQUEST_COUNT.labels(endpoint='adoptionlist', error='true').inc() + REQUEST_LATENCY.labels(endpoint='adoptionlist', error='true').observe(duration) + logger.error(f"Error in list_adoptions: {e}") + raise + +# Initialize service +service = PetAdoptionsService() + +@app.get("/health/status", response_model=HealthResponse, tags=["health"]) +async def health_check(): + """Health check endpoint""" + start_time = time.time() + + try: + result = service.health_check() + duration = time.time() - start_time + + REQUEST_COUNT.labels(endpoint='health_check', error='false').inc() + REQUEST_LATENCY.labels(endpoint='health_check', error='false').observe(duration) + + return HealthResponse(status=result) + except Exception as e: + duration = time.time() - start_time + REQUEST_COUNT.labels(endpoint='health_check', error='true').inc() + REQUEST_LATENCY.labels(endpoint='health_check', error='true').observe(duration) + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/api/adoptionlist/", response_model=List[Adoption], tags=["adoptions"]) +async def list_adoptions(): + """List adoptions endpoint""" + try: + adoptions = service.list_adoptions() + return adoptions + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/metrics", tags=["monitoring"]) +async def metrics(): + """Prometheus metrics endpoint""" + return generate_latest(), 200, {'Content-Type': CONTENT_TYPE_LATEST} + +if __name__ == "__main__": + import uvicorn + port = int(os.environ.get('PORT', 80)) + uvicorn.run(app, host="0.0.0.0", port=port) \ No newline at end of file diff --git a/PetAdoptions/petlistadoptions-py/docker-compose.yml b/PetAdoptions/petlistadoptions-py/docker-compose.yml new file mode 100644 index 00000000..0656b9f0 --- /dev/null +++ b/PetAdoptions/petlistadoptions-py/docker-compose.yml @@ -0,0 +1,50 @@ +version: '3.8' + +services: + # PostgreSQL database for local testing + postgres: + image: postgres:15 + environment: + POSTGRES_DB: adoptions + POSTGRES_USER: petstore + POSTGRES_PASSWORD: petstore123 + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U petstore -d adoptions"] + interval: 10s + timeout: 5s + retries: 5 + + # Pet List Adoptions Service + petlistadoptions: + build: . + ports: + - "80:80" + environment: + - APP_PET_SEARCH_URL=http://localhost:8080/ + - APP_RDS_SECRET_ARN=local-secret + - OTEL_EXPORTER_OTLP_ENDPOINT=0.0.0.0:4317 + depends_on: + postgres: + condition: service_healthy + volumes: + - ./local-secret.json:/app/local-secret.json:ro + command: ["python", "app.py"] + + # Mock Pet Search Service (for testing) + petsearch-mock: + image: mockserver/mockserver:latest + ports: + - "8080:1080" + environment: + - MOCKSERVER_PROPERTY_FILE=/config/mockserver.properties + volumes: + - ./mock-petsearch.json:/config/mock-petsearch.json:ro + command: ["java", "-jar", "/opt/mockserver/mockserver-netty-jar-with-dependencies.jar", "-serverPort", "1080", "-logLevel", "INFO"] + +volumes: + postgres_data: \ No newline at end of file diff --git a/PetAdoptions/petlistadoptions-py/requirements.txt b/PetAdoptions/petlistadoptions-py/requirements.txt new file mode 100644 index 00000000..cfb6bd1d --- /dev/null +++ b/PetAdoptions/petlistadoptions-py/requirements.txt @@ -0,0 +1,7 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +psycopg2-binary==2.9.7 +requests==2.31.0 +boto3==1.28.44 +prometheus-client==0.17.1 +python-dotenv==1.0.0 \ No newline at end of file diff --git a/PetAdoptions/petlistadoptions-py/start.sh b/PetAdoptions/petlistadoptions-py/start.sh new file mode 100755 index 00000000..909b9716 --- /dev/null +++ b/PetAdoptions/petlistadoptions-py/start.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Pet List Adoptions Service Startup Script +# Note: Running as root for port 80 access + +echo "Starting Pet List Adoptions Service..." + +# Set default values +export PORT=${PORT:-80} +export WORKERS=${WORKERS:-4} + +# Check if running in Docker/ECS +if [ -f /.dockerenv ] || [ -n "$ECS_CONTAINER_METADATA_URI" ]; then + echo "Running in containerized environment (Docker/ECS)..." + echo "Port: $PORT" + echo "Workers: $WORKERS" + + # Wait for any dependencies if needed + if [ -n "$WAIT_FOR_DB" ]; then + echo "Waiting for database to be ready..." + sleep 10 + fi + + exec uvicorn app:app --host 0.0.0.0 --port $PORT --workers $WORKERS +else + echo "Running in development mode..." + exec uvicorn app:app --reload --host 0.0.0.0 --port $PORT +fi \ No newline at end of file diff --git a/PetAdoptions/petsearch-java/Dockerfile b/PetAdoptions/petsearch-java/Dockerfile index bfd341d1..3b0ee79d 100644 --- a/PetAdoptions/petsearch-java/Dockerfile +++ b/PetAdoptions/petsearch-java/Dockerfile @@ -11,17 +11,7 @@ RUN gradle build -DexcludeTags='integration' FROM amazoncorretto:17-alpine WORKDIR /app -ADD https://github.com/aws-observability/aws-otel-java-instrumentation/releases/download/v1.21.1/aws-opentelemetry-agent.jar /app/aws-opentelemetry-agent.jar -ENV JAVA_TOOL_OPTIONS "-javaagent:/app/aws-opentelemetry-agent.jar" - ARG JAR_FILE=build/libs/\*.jar COPY --from=build /app/${JAR_FILE} ./app.jar -# OpenTelemetry agent configuration -ENV OTEL_TRACES_SAMPLER "always_on" -ENV OTEL_PROPAGATORS "tracecontext,baggage,xray" -ENV OTEL_RESOURCE_ATTRIBUTES "service.name=PetSearch" -ENV OTEL_IMR_EXPORT_INTERVAL "10000" -ENV OTEL_EXPORTER_OTLP_ENDPOINT "http://localhost:4317" - ENTRYPOINT ["java","-jar","/app/app.jar"] diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/ErrorResponse.java b/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/ErrorResponse.java new file mode 100644 index 00000000..6ebf794b --- /dev/null +++ b/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/ErrorResponse.java @@ -0,0 +1,60 @@ +package ca.petsearch.controllers; + +import java.time.LocalDateTime; + +public class ErrorResponse { + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; + + public ErrorResponse(int status, String error, String message, String path) { + this.timestamp = LocalDateTime.now(); + this.status = status; + this.error = error; + this.message = message; + this.path = path; + } + + // Getters and setters + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } +} diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/SearchController.java b/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/SearchController.java index 1d312add..5d94a805 100644 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/SearchController.java +++ b/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/SearchController.java @@ -22,6 +22,11 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.server.ResponseStatusException; import java.util.*; import java.util.concurrent.TimeUnit; @@ -132,13 +137,29 @@ private Pet mapToPet(Map item) { @GetMapping("/api/search") - public List search( + public ResponseEntity search( @RequestParam(name = "pettype", defaultValue = "", required = false) String petType, @RequestParam(name = "petcolor", defaultValue = "", required = false) String petColor, @RequestParam(name = "petid", defaultValue = "", required = false) String petId ) throws InterruptedException { Span span = tracer.spanBuilder("Scanning DynamoDB Table").startSpan(); + // return 404 error with custom message for invalid pet type + if (petType != null && !petType.trim().isEmpty() && !petType.equals("puppy") && !petType.equals("kitten") && !petType.equals("bunny")) { + logger.warn(petType+" pet type requested - returning 404 error"); + span.setAttribute("error", true); + String errorMsg = petType+" pet type not found"; + span.setAttribute("error.message", errorMsg); + span.end(); + ErrorResponse errorResponse = new ErrorResponse( + 404, + errorMsg, + errorMsg, + "/api/search" + ); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); + } + // This line is intentional. Delays searches if (petType != null && !petType.trim().isEmpty() && petType.equals("bunny")) { logger.debug("Delaying the response on purpose, to show on traces as an issue"); @@ -151,7 +172,7 @@ public List search( .getItems().stream().map(this::mapToPet) .collect(Collectors.toList()); metricEmitter.emitPetsReturnedMetric(result.size()); - return result; + return ResponseEntity.ok(result); } catch (Exception e) { span.recordException(e); diff --git a/PetAdoptions/petsite/.vscode/launch.json b/PetAdoptions/petsite/.vscode/launch.json index f719590e..31192563 100644 --- a/PetAdoptions/petsite/.vscode/launch.json +++ b/PetAdoptions/petsite/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/petsite/bin/Debug/netcoreapp3.0/PetSite.dll", + "program": "${workspaceFolder}/petsite/bin/Debug/net8.0/PetSite.dll", "args": [], "cwd": "${workspaceFolder}/petsite", "stopAtEntry": false, diff --git a/PetAdoptions/petsite/petsite/Controllers/AdoptionController.cs b/PetAdoptions/petsite/petsite/Controllers/AdoptionController.cs index 72f371df..de9f22bd 100644 --- a/PetAdoptions/petsite/petsite/Controllers/AdoptionController.cs +++ b/PetAdoptions/petsite/petsite/Controllers/AdoptionController.cs @@ -1,85 +1,100 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon.XRay.Recorder.Core; -using Amazon.XRay.Recorder.Handlers.AwsSdk; -using Amazon.XRay.Recorder.Handlers.System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using PetSite.ViewModels; - -namespace PetSite.Controllers -{ - public class AdoptionController : Controller - { - private static readonly HttpClient HttpClient = new HttpClient(new HttpClientXRayTracingHandler(new HttpClientHandler())); - private static Variety _variety = new Variety(); - private static IConfiguration _configuration; - - private static string _searchApiurl; - - public AdoptionController(IConfiguration configuration) - { - _configuration = configuration; - - //_searchApiurl = _configuration["searchapiurl"]; - _searchApiurl = SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"searchapiurl"); - - AWSSDKHandler.RegisterXRayForAllServices(); - } - // GET: Adoption - [HttpGet] - public IActionResult Index([FromQuery] Pet pet) - { - return View(pet); - } - private async Task GetPetDetails(SearchParams searchParams) - { - string searchString = string.Empty; - - if (!String.IsNullOrEmpty(searchParams.pettype) && searchParams.pettype != "all") searchString = $"pettype={searchParams.pettype}"; - if (!String.IsNullOrEmpty(searchParams.petcolor) && searchParams.petcolor != "all") searchString = $"&{searchString}&petcolor={searchParams.petcolor}"; - if (!String.IsNullOrEmpty(searchParams.petid) && searchParams.petid != "all") searchString = $"&{searchString}&petid={searchParams.petid}"; - - return await HttpClient.GetStringAsync($"{_searchApiurl}{searchString}"); - } - - [HttpPost] - public async Task TakeMeHome([FromForm] SearchParams searchParams) - { - - Console.WriteLine( - $"[{AWSXRayRecorder.Instance.TraceContext.GetEntity().RootSegment.TraceId}][{AWSXRayRecorder.Instance.GetEntity().TraceId}] - Inside TakeMehome. Pet in context - PetId:{searchParams.petid}, PetType:{searchParams.pettype}, PetColor:{searchParams.petcolor}"); - - - AWSXRayRecorder.Instance.AddMetadata("PetType", searchParams.pettype); - AWSXRayRecorder.Instance.AddMetadata("PetId", searchParams.petid); - AWSXRayRecorder.Instance.AddMetadata("PetColor", searchParams.petcolor); - - //String traceId = TraceId.NewId(); // This function is present in : Amazon.XRay.Recorder.Core.Internal.Entities - AWSXRayRecorder.Instance - .BeginSubsegment("Calling Search API"); // custom traceId used while creating segment - string result; - - try - { - result = await GetPetDetails(searchParams); - } - catch (Exception e) - { - AWSXRayRecorder.Instance.AddException(e); - throw e; - } - finally - { - AWSXRayRecorder.Instance.EndSubsegment(); - } - - return View("Index", JsonSerializer.Deserialize>(result).FirstOrDefault()); - } - } -} \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +using PetSite.ViewModels; + + +namespace PetSite.Controllers +{ + public class AdoptionController : BaseController + { + private readonly PetSite.Services.IPetSearchService _petSearchService; + private static Variety _variety = new Variety(); + private readonly ILogger _logger; + + public AdoptionController(ILogger logger, PetSite.Services.IPetSearchService petSearchService) + { + _petSearchService = petSearchService; + _logger = logger; + } + + // GET: Adoption + [HttpGet] + public IActionResult Index([FromQuery] Pet pet) + { + if (EnsureUserId()) return new EmptyResult(); // Redirect happened, stop processing + + _logger.LogInformation($"In Index Adoption/Index method with pet: {JsonSerializer.Serialize(pet)}"); + + return View(pet); + } + + + + [HttpPost] + public async Task TakeMeHome([FromForm] SearchParams searchParams, string userId) + { + if(string.IsNullOrEmpty(userId)) EnsureUserId(); + + // Add custom span attributes using Activity API + var currentActivity = Activity.Current; + if (currentActivity != null) + { + currentActivity.SetTag("pet.id", searchParams.petid); + currentActivity.SetTag("pet.type", searchParams.pettype); + currentActivity.SetTag("pet.color", searchParams.petcolor); + + _logger.LogInformation($"Processing adoption request - PetId:{searchParams.petid}, PetType:{searchParams.pettype}, PetColor:{searchParams.petcolor}"); + } + + List pets; + + try + { + // Create tracing span for Search API operation + using (var activity = Activity.Current?.Source?.StartActivity("Calling Search API")) + { + if (activity != null) + { + activity.SetTag("pet.id", searchParams.petid); + activity.SetTag("pet.type", searchParams.pettype); + activity.SetTag("pet.color", searchParams.petcolor); + } + _logger.LogInformation($"Inside Adoption/TakeMeHome with - pettype: {searchParams.pettype}, petcolor: {searchParams.petcolor}, petid: {searchParams.petid}"); + pets = await _petSearchService.GetPetDetails(searchParams.pettype, searchParams.petcolor, searchParams.petid, "userxxx"); + } + } + catch (Exception e) + { + // Log the exception + _logger.LogError(e, "Error calling PetSearch API"); + throw e; + } + + var selectedPet = pets.FirstOrDefault(); + if (selectedPet != null) + { + return RedirectToAction("Index", new { + userId = userId, + petid = selectedPet.petid, + pettype = selectedPet.pettype, + petcolor = selectedPet.petcolor, + peturl = selectedPet.peturl, + price = selectedPet.price, + cuteness_rate = selectedPet.cuteness_rate + }); + } + + return RedirectToAction("Index", new { userId = userId }); + } + } +} diff --git a/PetAdoptions/petsite/petsite/Controllers/BaseController.cs b/PetAdoptions/petsite/petsite/Controllers/BaseController.cs new file mode 100644 index 00000000..5db51d7b --- /dev/null +++ b/PetAdoptions/petsite/petsite/Controllers/BaseController.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; + +namespace PetSite.Controllers +{ + public class BaseController : Controller + { + private static readonly List UserIds = new List + { + "user001", "user002", "user003", "user004", "user005", + "user006", "user007", "user008", "user009", "user010", + "user011", "user012", "user013", "user014", "user015", + "user016", "user017", "user018", "user019", "user020", + "user021", "user022", "user023", "user024", "user025" + }; + private static readonly Random Random = new Random(); + + protected bool EnsureUserId() + { + string userId = Request.Query["userId"].ToString(); + + // Generate userId only on Home/Index if not provided + if (string.IsNullOrEmpty(userId)) + { + // Only generate on Home/Index, otherwise require userId + if (ControllerContext.ActionDescriptor.ControllerName == "Home" && + ControllerContext.ActionDescriptor.ActionName == "Index") + { + userId = UserIds[Random.Next(UserIds.Count)]; + + if (Request.Method == "GET") + { + var queryString = Request.QueryString.HasValue ? Request.QueryString.Value + "&userId=" + userId : "?userId=" + userId; + Response.Redirect(Request.Path + queryString); + return true; + } + } + else + { + // Redirect to Home/Index if userId is missing on other pages + Response.Redirect("/Home/Index"); + return true; + } + } + + // Set ViewBag and ViewData for all views + ViewBag.UserId = userId; + ViewData["UserId"] = userId; + + var currentActivity = Activity.Current; + if (currentActivity != null && !currentActivity.Tags.Any(tag => tag.Key == "userId")) + { + currentActivity.SetTag("userId", userId); + } + + return false; + } + } +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Controllers/FoodServiceController.cs b/PetAdoptions/petsite/petsite/Controllers/FoodServiceController.cs new file mode 100644 index 00000000..d574adb6 --- /dev/null +++ b/PetAdoptions/petsite/petsite/Controllers/FoodServiceController.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; +using PetSite.Models; +using PetSite.Helpers; + +namespace PetSite.Controllers +{ + public class FoodServiceController : BaseController + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public FoodServiceController(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger logger) + { + _httpClientFactory = httpClientFactory; + _configuration = configuration; + _logger = logger; + } + + [HttpGet] + public async Task GetFoods() + { + if (EnsureUserId()) return new EmptyResult(); + + try + { + using var httpClient = _httpClientFactory.CreateClient(); + var foodApiUrl = _configuration["FOOD_API_URL"] ?? "https://api.example.com/foods"; + var userId = ViewBag.UserId?.ToString(); + var url = UrlHelper.BuildUrl(foodApiUrl, ("userId", userId)); + var response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + var jsonContent = await response.Content.ReadAsStringAsync(); + var foodResponse = JsonSerializer.Deserialize(jsonContent); + + return Json(foodResponse?.foods ?? new List()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching food data"); + return Json(new List()); + } + } + + [HttpPost] + public async Task BuyFood(string foodId, string userId) + { + if (EnsureUserId()) return new EmptyResult(); + + try + { + using var httpClient = _httpClientFactory.CreateClient(); + var purchaseApiUrl = _configuration["FOOD_PURCHASE_API_URL"] ?? "https://api.example.com/purchase"; + // var userId = ViewBag.UserId?.ToString(); + var url = UrlHelper.BuildUrl(purchaseApiUrl, ("foodId", foodId), ("userId", userId)); + var response = await httpClient.PostAsync(url, null); + response.EnsureSuccessStatusCode(); + + // Food purchase successful - could add ViewData or redirect with status + } + catch (Exception ex) + { + _logger.LogError(ex, "Error purchasing food"); + // Food purchase failed - could add ViewData or redirect with error + } + + return RedirectToAction("Index", "Payment", new { userId = userId }); + } + } +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Controllers/HomeController.cs b/PetAdoptions/petsite/petsite/Controllers/HomeController.cs index 77b7dee1..d7146d3f 100644 --- a/PetAdoptions/petsite/petsite/Controllers/HomeController.cs +++ b/PetAdoptions/petsite/petsite/Controllers/HomeController.cs @@ -1,199 +1,178 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using PetSite.Models; -using Amazon.XRay.Recorder.Handlers.AwsSdk; -using System.Net.Http; -using Amazon.XRay.Recorder.Handlers.System.Net; -using Amazon.XRay.Recorder.Core; -using System.Text.Json; -using Amazon; -using PetSite.ViewModels; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.Extensions.Configuration; -using Prometheus; - -namespace PetSite.Controllers -{ - public class HomeController : Controller - { - private readonly ILogger _logger; - private static HttpClient _httpClient; - private static Variety _variety = new Variety(); - - private IConfiguration _configuration; - - //Prometheus metric to count the number of searches performed - private static readonly Counter PetSearchCount = - Metrics.CreateCounter("petsite_petsearches_total", "Count the number of searches performed"); - - //Prometheus metric to count the number of puppy searches performed - private static readonly Counter PuppySearchCount = - Metrics.CreateCounter("petsite_pet_puppy_searches_total", "Count the number of puppy searches performed"); - - //Prometheus metric to count the number of kitten searches performed - private static readonly Counter KittenSearchCount = - Metrics.CreateCounter("petsite_pet_kitten_searches_total", "Count the number of kitten searches performed"); - - //Prometheus metric to count the number of bunny searches performed - private static readonly Counter BunnySearchCount = - Metrics.CreateCounter("petsite_pet_bunny_searches_total", "Count the number of bunny searches performed"); - - private static readonly Gauge PetsWaitingForAdoption = Metrics - .CreateGauge("petsite_pets_waiting_for_adoption", "Number of pets waiting for adoption."); - - public HomeController(ILogger logger, IConfiguration configuration) - { - AWSXRayRecorder.RegisterLogger(LoggingOptions.Console); - _configuration = configuration; - AWSSDKHandler.RegisterXRayForAllServices(); - - _httpClient = new HttpClient(new HttpClientXRayTracingHandler(new HttpClientHandler())); - _logger = logger; - - _variety.PetTypes = new List() - { - new SelectListItem() {Value = "all", Text = "All"}, - new SelectListItem() {Value = "puppy", Text = "Puppy"}, - new SelectListItem() {Value = "kitten", Text = "Kitten"}, - new SelectListItem() {Value = "bunny", Text = "Bunny"} - }; - - _variety.PetColors = new List() - { - new SelectListItem() {Value = "all", Text = "All"}, - new SelectListItem() {Value = "brown", Text = "Brown"}, - new SelectListItem() {Value = "black", Text = "Black"}, - new SelectListItem() {Value = "white", Text = "White"} - }; - } - - private async Task GetPetDetails(string pettype, string petcolor, string petid) - { - string searchUri = string.Empty; - - if (!String.IsNullOrEmpty(pettype) && pettype != "all") searchUri = $"pettype={pettype}"; - if (!String.IsNullOrEmpty(petcolor) && petcolor != "all") searchUri = $"&{searchUri}&petcolor={petcolor}"; - if (!String.IsNullOrEmpty(petid) && petid != "all") searchUri = $"&{searchUri}&petid={petid}"; - - switch (pettype) - { - case "puppy": - PuppySearchCount.Inc(); - PetSearchCount.Inc(); - break; - case "kitten": - KittenSearchCount.Inc(); - PetSearchCount.Inc(); - break; - case "bunny": - BunnySearchCount.Inc(); - PetSearchCount.Inc(); - break; - } - //string searchapiurl = _configuration["searchapiurl"]; - string searchapiurl = SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"searchapiurl"); - return await _httpClient.GetStringAsync($"{searchapiurl}{searchUri}"); - } - - [HttpGet("housekeeping")] - public async Task HouseKeeping() - { - Console.WriteLine( - $"[{AWSXRayRecorder.Instance.TraceContext.GetEntity().RootSegment.TraceId}][{AWSXRayRecorder.Instance.GetEntity().TraceId}] - In Housekeeping, trying to reset the app."); - - /*var result = await GetPetDetails(null, null, null); - var Pets = JsonSerializer.Deserialize>(result); - - var searchParams = new SearchParams(); - - //string updateadoptionstatusurl = _configuration["updateadoptionstatusurl"]; - string updateadoptionstatusurl = SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"updateadoptionstatusurl"); - - - foreach (var pet in Pets.Where(item => item.availability == "no")) - { - searchParams.pettype = pet.pettype; - searchParams.petid = pet.petid; - searchParams.petavailability = "yes"; - - StringContent putData = new StringContent(JsonSerializer.Serialize(searchParams)); - await _httpClient.PutAsync(updateadoptionstatusurl, putData); - }*/ - - //string cleanupadoptionsurl = _configuration["cleanupadoptionsurl"]; - string cleanupadoptionsurl = SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"cleanupadoptionsurl"); - - await _httpClient.PostAsync(cleanupadoptionsurl, null); - - return View(); - } - - [HttpGet] - public async Task Index(string selectedPetType, string selectedPetColor, string petid) - { - Console.WriteLine( - $"AWS_XRAY_DAEMON_ADDRESS:- {Environment.GetEnvironmentVariable("AWS_XRAY_DAEMON_ADDRESS")}"); - - - AWSXRayRecorder.Instance.BeginSubsegment("Calling Search API"); - - AWSXRayRecorder.Instance.AddMetadata("PetType", selectedPetType); - AWSXRayRecorder.Instance.AddMetadata("PetId", petid); - AWSXRayRecorder.Instance.AddMetadata("PetColor", selectedPetColor); - - - Console.WriteLine( - $"[{AWSXRayRecorder.Instance.TraceContext.GetEntity().RootSegment.TraceId}]- Search string - PetType:{selectedPetType} PetColor:{selectedPetColor} PetId:{petid}"); - - // | SegmentId: [{AWSXRayRecorder.Instance.TraceContext.GetEntity().RootSegment.Id} - string result; - - try - { - result = await GetPetDetails(selectedPetType, selectedPetColor, petid); - } - catch (Exception e) - { - AWSXRayRecorder.Instance.AddException(e); - throw e; - } - finally - { - AWSXRayRecorder.Instance.EndSubsegment(); - } - - var Pets = JsonSerializer.Deserialize>(result); - - var PetDetails = new PetDetails() - { - Pets = Pets, - Varieties = new Variety - { - PetTypes = _variety.PetTypes, - PetColors = _variety.PetColors, - SelectedPetColor = selectedPetColor, - SelectedPetType = selectedPetType - } - }; - AWSXRayRecorder.Instance.AddMetadata("results", System.Text.Json.JsonSerializer.Serialize(PetDetails)); - Console.WriteLine( - $" TraceId: [{AWSXRayRecorder.Instance.GetEntity().TraceId}] - {JsonSerializer.Serialize(PetDetails)}"); - - // Sets the metric value to the number of pets available for adoption at the moment - PetsWaitingForAdoption.Set(Pets.Where(pet => pet.availability == "yes").Count()); - - return View(PetDetails); - } - - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public IActionResult Error() - { - return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier}); - } - } -} \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using PetSite.Models; +using System.Net.Http; +using System.Text.Json; +using PetSite.ViewModels; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Http; +using PetSite.Helpers; +using Prometheus; + +namespace PetSite.Controllers +{ + public class HomeController : BaseController + { + private readonly ILogger _logger; + private readonly PetSite.Services.IPetSearchService _petSearchService; + private readonly IHttpClientFactory _httpClientFactory; + private static Variety _variety = new Variety(); + private readonly IConfiguration _configuration; + + //Prometheus metric to count the number of searches performed + private static readonly Counter PetSearchCount = + Metrics.CreateCounter("petsite_petsearches_total", "Count the number of searches performed"); + + //Prometheus metric to count the number of puppy searches performed + private static readonly Counter PuppySearchCount = + Metrics.CreateCounter("petsite_pet_puppy_searches_total", "Count the number of puppy searches performed"); + + //Prometheus metric to count the number of kitten searches performed + private static readonly Counter KittenSearchCount = + Metrics.CreateCounter("petsite_pet_kitten_searches_total", "Count the number of kitten searches performed"); + + //Prometheus metric to count the number of bunny searches performed + private static readonly Counter BunnySearchCount = + Metrics.CreateCounter("petsite_pet_bunny_searches_total", "Count the number of bunny searches performed"); + + private static readonly Gauge PetsWaitingForAdoption = Metrics + .CreateGauge("petsite_pets_waiting_for_adoption", "Number of pets waiting for adoption."); + + + + public HomeController(ILogger logger, IConfiguration configuration, PetSite.Services.IPetSearchService petSearchService, IHttpClientFactory httpClientFactory) + { + _configuration = configuration; + _petSearchService = petSearchService; + _httpClientFactory = httpClientFactory; + _logger = logger; + + _variety.PetTypes = new List() + { + new SelectListItem() {Value = "all", Text = "All"}, + new SelectListItem() {Value = "puppy", Text = "Puppy"}, + new SelectListItem() {Value = "kitten", Text = "Kitten"}, + new SelectListItem() {Value = "bunny", Text = "Bunny"} + }; + + _variety.PetColors = new List() + { + new SelectListItem() {Value = "all", Text = "All"}, + new SelectListItem() {Value = "brown", Text = "Brown"}, + new SelectListItem() {Value = "black", Text = "Black"}, + new SelectListItem() {Value = "white", Text = "White"} + }; + } + + [HttpGet("housekeeping")] + public async Task HouseKeeping() + { + if (EnsureUserId()) return new EmptyResult(); + _logger.LogInformation("In Housekeeping, trying to reset the app."); + + string cleanupadoptionsurl = _configuration["cleanupadoptionsurl"]; + + using var httpClient = _httpClientFactory.CreateClient(); + var userId = ViewBag.UserId?.ToString(); + var url = UrlHelper.BuildUrl(cleanupadoptionsurl, ("userId", userId)); + await httpClient.PostAsync(url, null); + + return View(); + } + + [HttpGet] + public async Task Index(string selectedPetType, string selectedPetColor, string petid) + { + if (EnsureUserId()) return new EmptyResult(); + // Add custom span attributes using Activity API + var currentActivity = Activity.Current; + if (currentActivity != null) + { + currentActivity.SetTag("pet.type", selectedPetType); + currentActivity.SetTag("pet.color", selectedPetColor); + currentActivity.SetTag("pet.id", petid); + + _logger.LogInformation($"Search string - PetType:{selectedPetType} PetColor:{selectedPetColor} PetId:{petid}"); + } + + List Pets; + + try + { + // Create a new activity for the API call + using (var activity = Activity.Current?.Source?.StartActivity("Calling Search API")) + { + if (activity != null) + { + activity.SetTag("pet.type", selectedPetType); + activity.SetTag("pet.color", selectedPetColor); + activity.SetTag("pet.id", petid); + } + + var userId = Request.Query["userId"].ToString(); + Pets = await _petSearchService.GetPetDetails(selectedPetType, selectedPetColor, petid, userId); + } + } + catch (HttpRequestException e) + { + _logger.LogError(e, "HTTP error received after calling PetSearch API"); + ViewBag.ErrorMessage = $"Unable to search pets at this time. Please try again later. \nError message received - {e.Message}"; + Pets = new List(); + throw e; + } + catch (TaskCanceledException e) + { + _logger.LogError(e, "Timeout calling PetSearch API"); + ViewBag.ErrorMessage = "Search request timed out. Please try again."; + Pets = new List(); + throw e; + } + catch (Exception e) + { + _logger.LogError(e, "Unexpected error calling PetSearch API"); + ViewBag.ErrorMessage = "An unexpected error occurred. Please try again."; + Pets = new List(); + throw e; + } + + var PetDetails = new PetDetails() + { + Pets = Pets, + Varieties = new Variety + { + PetTypes = _variety.PetTypes, + PetColors = _variety.PetColors, + SelectedPetColor = selectedPetColor, + SelectedPetType = selectedPetType + } + }; + + _logger.LogInformation("Search completed with {PetCount} pets found", Pets.Count); + + // Sets the metric value to the number of pets available for adoption at the moment + PetsWaitingForAdoption.Set(Pets.Where(pet => pet.availability == "yes").Count()); + + return View(PetDetails); + } + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error(string userId, string message) + { + if (!string.IsNullOrEmpty(userId)) + { + ViewBag.UserId = userId; + ViewData["UserId"] = userId; + } + + ViewBag.ErrorMessage = message; + + return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier}); + } + } +} diff --git a/PetAdoptions/petsite/petsite/Controllers/PaymentController.cs b/PetAdoptions/petsite/petsite/Controllers/PaymentController.cs index 56f99871..ae16b100 100644 --- a/PetAdoptions/petsite/petsite/Controllers/PaymentController.cs +++ b/PetAdoptions/petsite/petsite/Controllers/PaymentController.cs @@ -1,168 +1,107 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Amazon.XRay.Recorder.Core; -using Amazon.XRay.Recorder.Handlers.AwsSdk; -using Amazon.XRay.Recorder.Handlers.System.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Amazon.SQS; -using Amazon.SQS.Model; -using System.Text.Json.Serialization; -using System.Text.Json; -using Amazon; -using Amazon.Runtime; -using Amazon.SimpleNotificationService; -using Amazon.SimpleNotificationService.Model; -using Amazon.StepFunctions; -using Amazon.StepFunctions.Model; -using Microsoft.Extensions.Configuration; -using PetSite.Models; -using Prometheus; -using Newtonsoft; - -namespace PetSite.Controllers -{ - public class PaymentController : Controller - { - private static string _txStatus = String.Empty; - - private static HttpClient _httpClient = - new HttpClient(new HttpClientXRayTracingHandler(new HttpClientHandler())); - - private static AmazonSQSClient _sqsClient; - private static IConfiguration _configuration; - - //Prometheus metric to count the number of Pets adopted - private static readonly Counter PetAdoptionCount = - Metrics.CreateCounter("petsite_petadoptions_total", "Count the number of Pets adopted"); - - public PaymentController(IConfiguration configuration) - { - AWSSDKHandler.RegisterXRayForAllServices(); - _configuration = configuration; - - _sqsClient = new AmazonSQSClient(Amazon.Util.EC2InstanceMetadata.Region); - } - - // GET: Payment - [HttpGet] - private ActionResult Index() - { - return View(); - } - - // POST: Payment/MakePayment - [HttpPost] - // [ValidateAntiForgeryToken] - public async Task MakePayment(string petId, string pettype) - { - AWSXRayRecorder.Instance.AddMetadata("PetType", pettype); - AWSXRayRecorder.Instance.AddMetadata("PetId", petId); - - ViewData["txStatus"] = "success"; - - try - { - AWSXRayRecorder.Instance.BeginSubsegment("Call Payment API"); - - Console.WriteLine( - $"[{AWSXRayRecorder.Instance.TraceContext.GetEntity().RootSegment.TraceId}][{AWSXRayRecorder.Instance.GetEntity().TraceId}] - Inside MakePayment Action method - PetId:{petId} - PetType:{pettype}"); - - AWSXRayRecorder.Instance.AddAnnotation("PetId", petId); - AWSXRayRecorder.Instance.AddAnnotation("PetType", pettype); - - var result = await PostTransaction(petId, pettype); - AWSXRayRecorder.Instance.EndSubsegment(); - - AWSXRayRecorder.Instance.BeginSubsegment("Post Message to SQS"); - var messageResponse = PostMessageToSqs(petId, pettype).Result; - AWSXRayRecorder.Instance.EndSubsegment(); - - AWSXRayRecorder.Instance.BeginSubsegment("Send Notification"); - var snsResponse = SendNotification(petId).Result; - AWSXRayRecorder.Instance.EndSubsegment(); - - if ("bunny" == pettype) // Only call StepFunction for "bunny" pettype to reduce number of invocations - { - // Console.WriteLine($"STEPLOG- PETTYPE- {pettype}"); - // AWSXRayRecorder.Instance.BeginSubsegment("Start Step Function"); - var stepFunctionResult = StartStepFunctionExecution(petId, pettype).Result; - //Console.WriteLine($"STEPLOG - RESPONSE - {stepFunctionResult.HttpStatusCode}"); - // AWSXRayRecorder.Instance.EndSubsegment(); - } - - //Increase purchase metric count - PetAdoptionCount.Inc(); - return View("Index"); - } - catch (Exception ex) - { - ViewData["txStatus"] = "failure"; - ViewData["error"] = ex.Message; - AWSXRayRecorder.Instance.AddException(ex); - return View("Index"); - } - } - - private async Task PostTransaction(string petId, string pettype) - { - return await _httpClient.PostAsync($"{SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"paymentapiurl")}?petId={petId}&petType={pettype}", - null); - } - - private async Task PostMessageToSqs(string petId, string petType) - { - AWSSDKHandler.RegisterXRay(); - - return await _sqsClient.SendMessageAsync(new SendMessageRequest() - { - MessageBody = JsonSerializer.Serialize($"{petId}-{petType}"), - QueueUrl = SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"queueurl") - }); - } - - private async Task StartStepFunctionExecution(string petId, string petType) - { - /* - - // Code to invoke StepFunction through API Gateway - var stepFunctionInputModel = new StepFunctionInputModel() - { - input = JsonSerializer.Serialize(new SearchParams() {petid = petId, pettype = petType}), - name = $"{petType}-{petId}-{Guid.NewGuid()}", - stateMachineArn = SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"petadoptionsstepfnarn") - }; - - var content = new StringContent( - JsonSerializer.Serialize(stepFunctionInputModel), - Encoding.UTF8, - "application/json"); - - return await _httpClient.PostAsync(SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"petadoptionsstepfnurl"), content); - - */ - // Console.WriteLine($"STEPLOG -ARN - {SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"petadoptionsstepfnarn")}"); - //Console.WriteLine($"STEPLOG - SERIALIZE - {JsonSerializer.Serialize(new SearchParams() {petid = petId, pettype = petType})}"); - AWSSDKHandler.RegisterXRay(); - return await new AmazonStepFunctionsClient().StartExecutionAsync(new StartExecutionRequest() - { - Input = JsonSerializer.Serialize(new SearchParams() {petid = petId, pettype = petType}), - Name = $"{petType}-{petId}-{Guid.NewGuid()}", - StateMachineArn = SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"petadoptionsstepfnarn") - }); - } - - private async Task SendNotification(string petId) - { - AWSSDKHandler.RegisterXRay(); - - var snsClient = new AmazonSimpleNotificationServiceClient(); - return await snsClient.PublishAsync(topicArn: _configuration["snsarn"], - message: $"PetId {petId} was adopted on {DateTime.Now}"); - } - } +using System; +using System.Net.Http; +using System.Threading.Tasks; +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Amazon.SQS; +using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Http; +using PetSite.Helpers; +using Prometheus; + +namespace PetSite.Controllers +{ + public class PaymentController : BaseController + { + private static string _txStatus = String.Empty; + + private readonly ILogger _logger; + + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + + //Prometheus metric to count the number of Pets adopted + private static readonly Counter PetAdoptionCount = + Metrics.CreateCounter("petsite_petadoptions_total", "Count the number of Pets adopted"); + + public PaymentController(ILogger logger, IConfiguration configuration, + IHttpClientFactory httpClientFactory) + { + _configuration = configuration; + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + // GET: Payment + [HttpGet] + public ActionResult Index([FromQuery] string userId, string status) + { + if (EnsureUserId()) return new EmptyResult(); + + // Transfer Session to ViewData for the view + ViewData["txStatus"] = status; + + // ViewData["FoodPurchaseStatus"] = HttpContext.Session.GetString("FoodPurchaseStatus"); + // ViewData["PurchasedFoodId"] = HttpContext.Session.GetString("PurchasedFoodId"); + // + // Clear session data after reading + // HttpContext.Session.Remove("FoodPurchaseStatus"); + // HttpContext.Session.Remove("PurchasedFoodId"); + // + return View(); + } + + // POST: Payment/MakePayment + [HttpPost] + // [ValidateAntiForgeryToken] + public async Task MakePayment(string petId, string pettype, string userId) + { + //if (EnsureUserId()) return new EmptyResult(); + + if (string.IsNullOrEmpty(userId)) EnsureUserId(); + + // Add custom span attributes using Activity API + var currentActivity = Activity.Current; + if (currentActivity != null) + { + currentActivity.SetTag("pet.id", petId); + currentActivity.SetTag("pet.type", pettype); + + _logger.LogInformation($"Inside MakePayment Action method - PetId:{petId} - PetType:{pettype}"); + } + + try + { + // Create tracing span for Payment API operation + using (var activity = Activity.Current?.Source?.StartActivity("Calling Payment API")) + { + if (activity != null) + { + activity.SetTag("pet.id", petId); + activity.SetTag("pet.type", pettype); + } + + // userId parameter is already available + + using var httpClient = _httpClientFactory.CreateClient(); + + var url = UrlHelper.BuildUrl(_configuration["paymentapiurl"], + ("petId", petId), ("petType", pettype), ("userId", userId)); + await httpClient.PostAsync(url, null); + } + + //Increase purchase metric count + PetAdoptionCount.Inc(); + return RedirectToAction("Index", new { userId = userId, status = "success" }); + } + catch (Exception ex) + { + // Log the exception + _logger.LogError(ex, $"Error in MakePayment: {ex.Message}"); + + return RedirectToAction("Index", new { userId = userId, status = ex.Message }); + } + } + } } \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Controllers/PetFoodController.cs b/PetAdoptions/petsite/petsite/Controllers/PetFoodController.cs index 53c0a35f..fa00a99c 100644 --- a/PetAdoptions/petsite/petsite/Controllers/PetFoodController.cs +++ b/PetAdoptions/petsite/petsite/Controllers/PetFoodController.cs @@ -3,38 +3,48 @@ using System; using System.Net.Http; using System.Threading.Tasks; -using Amazon.XRay.Recorder.Core; -using Amazon.XRay.Recorder.Handlers.System.Net; -using Amazon.XRay.Recorder.Handlers.AwsSdk; - +using System.Diagnostics; namespace PetSite.Controllers { public class PetFoodController : Controller { - private static HttpClient httpClient; private IConfiguration _configuration; public PetFoodController(IConfiguration configuration) - { - AWSSDKHandler.RegisterXRayForAllServices(); + _configuration = configuration; + httpClient = new HttpClient(); } [HttpGet("/petfood")] public async Task Index() { - // X-Ray FTW - AWSXRayRecorder.Instance.BeginSubsegment("Calling PetFood"); - Console.WriteLine($"[{AWSXRayRecorder.Instance.GetEntity().TraceId}][{AWSXRayRecorder.Instance.TraceContext.GetEntity().RootSegment.TraceId}] - Calling PetFood"); + // Add custom span attributes using Activity API + var currentActivity = Activity.Current; + if (currentActivity != null) + { + currentActivity.SetTag("operation", "GetPetFood"); + Console.WriteLine("Calling PetFood"); + } - // Get our data from petfood - var httpClient = new HttpClient(new HttpClientXRayTracingHandler(new HttpClientHandler())); - string result = await httpClient.GetStringAsync("http://petfood"); + string result; - // Close the segment - AWSXRayRecorder.Instance.EndSubsegment(); + try + { + // Begin activity to monitor PetFood + using (var activity = Activity.Current?.Source?.StartActivity("Calling PetFood")) + { + // Get our data from petfood + result = await httpClient.GetStringAsync("http://petfood"); + } + } + catch (Exception e) + { + Console.WriteLine($"Error calling PetFood: {e.Message}"); + throw; + } // Return the result! return result; @@ -43,20 +53,40 @@ public async Task Index() [HttpGet("/petfood-metric/{entityId}/{value}")] public async Task PetFoodMetric(string entityId, float value) { - // X-Ray FTW - AWSXRayRecorder.Instance.BeginSubsegment("Calling PetFood metric"); - Console.WriteLine("Calling: " + "http://petfood-metric/metric/" + entityId + "/" + value.ToString()); - Console.WriteLine($"[{AWSXRayRecorder.Instance.GetEntity().TraceId}][{AWSXRayRecorder.Instance.TraceContext.GetEntity().RootSegment.TraceId}] - Calling PetFood metric"); + // Add custom span attributes using Activity API + var currentActivity = Activity.Current; + if (currentActivity != null) + { + currentActivity.SetTag("operation", "PetFoodMetric"); + currentActivity.SetTag("entityId", entityId); + currentActivity.SetTag("value", value.ToString()); + + Console.WriteLine("Calling: " + "http://petfood-metric/metric/" + entityId + "/" + value.ToString()); + } - var httpClient = new HttpClient(new HttpClientXRayTracingHandler(new HttpClientHandler())); - string result = await httpClient.GetStringAsync("http://petfood-metric/metric/" + entityId + "/" + value.ToString()); - - AWSXRayRecorder.Instance.AddAnnotation("entityId", entityId); - AWSXRayRecorder.Instance.AddAnnotation("value", value.ToString()); - AWSXRayRecorder.Instance.EndSubsegment(); + string result; + + try + { + // Begin activity to monitor PetFood metrics retrieval + using (var activity = Activity.Current?.Source?.StartActivity("Calling PetFood metrics")) + { + if (activity != null) + { + activity.SetTag("entityId", entityId); + activity.SetTag("value", value.ToString()); + } + + result = await httpClient.GetStringAsync("http://petfood-metric/metric/" + entityId + "/" + value.ToString()); + } + } + catch (Exception e) + { + Console.WriteLine($"Error calling PetFood metric: {e.Message}"); + throw; + } return result; } - } -} \ No newline at end of file +} diff --git a/PetAdoptions/petsite/petsite/Controllers/PetHistoryController.cs b/PetAdoptions/petsite/petsite/Controllers/PetHistoryController.cs index 11501dc4..392abda6 100644 --- a/PetAdoptions/petsite/petsite/Controllers/PetHistoryController.cs +++ b/PetAdoptions/petsite/petsite/Controllers/PetHistoryController.cs @@ -3,29 +3,26 @@ using System; using System.Net.Http; using System.Threading.Tasks; -using Amazon.XRay.Recorder.Core; -using Amazon.XRay.Recorder.Handlers.System.Net; -using Amazon.XRay.Recorder.Handlers.AwsSdk; +using System.Diagnostics; using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; +using PetSite.Helpers; namespace PetSite.Controllers; -public class PetHistoryController : Controller +public class PetHistoryController : BaseController { - private IConfiguration _configuration; - private readonly ILogger _logger; - private static HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly IHttpClientFactory _httpClientFactory; private static string _pethistoryurl; - public PetHistoryController(IConfiguration configuration) + public PetHistoryController(IConfiguration configuration, IHttpClientFactory httpClientFactory) { - AWSSDKHandler.RegisterXRayForAllServices(); _configuration = configuration; - _httpClient = new HttpClient(new HttpClientXRayTracingHandler(new HttpClientHandler())); + _httpClientFactory = httpClientFactory; _pethistoryurl = _configuration["pethistoryurl"]; //string _pethistoryurl = SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"pethistoryurl"); - } /// @@ -35,9 +32,31 @@ public PetHistoryController(IConfiguration configuration) [HttpGet] public async Task Index() { - AWSXRayRecorder.Instance.BeginSubsegment("Calling GetPetAdoptionsHistory"); - ViewData["pethistory"] = await _httpClient.GetStringAsync($"{_pethistoryurl}/api/home/transactions"); - AWSXRayRecorder.Instance.EndSubsegment(); + if (EnsureUserId()) return new EmptyResult(); + // Add custom span attributes using Activity API + var currentActivity = Activity.Current; + if (currentActivity != null) + { + currentActivity.SetTag("operation", "GetPetAdoptionsHistory"); + } + + try + { + // Begin activity span to track GetPetAdoptionsHistory API call + using (var activity = Activity.Current?.Source?.StartActivity("Calling GetPetAdoptionsHistory API")) + { + using var httpClient = _httpClientFactory.CreateClient(); + var userId = ViewBag.UserId?.ToString() ?? "unknown"; + var url = UrlHelper.BuildUrl($"{_pethistoryurl}/api/home/transactions", ("userId", userId)); + ViewData["pethistory"] = await httpClient.GetStringAsync(url); + } + } + catch (Exception e) + { + Console.WriteLine($"Error calling GetPetAdoptionsHistory: {e.Message}"); + throw; + } + return View(); } @@ -48,10 +67,31 @@ public async Task Index() [HttpDelete] public async Task DeletePetAdoptionsHistory() { - AWSXRayRecorder.Instance.BeginSubsegment("Calling DeletePetAdoptionsHistory"); - ViewData["pethistory"] = await _httpClient.DeleteAsync($"{_pethistoryurl}/api/home/transactions"); - AWSXRayRecorder.Instance.EndSubsegment(); + if (EnsureUserId()) return new EmptyResult(); + // Add custom span attributes using Activity API + var currentActivity = Activity.Current; + if (currentActivity != null) + { + currentActivity.SetTag("operation", "DeletePetAdoptionsHistory"); + } + + try + { + // Begin activity span to track DeletePetAdoptionsHistory API call + using (var activity = Activity.Current?.Source?.StartActivity("Calling DeletePetAdoptionsHistory API")) + { + using var httpClient = _httpClientFactory.CreateClient(); + var userId = ViewBag.UserId?.ToString() ?? "unknown"; + var url = UrlHelper.BuildUrl($"{_pethistoryurl}/api/home/transactions", ("userId", userId)); + ViewData["pethistory"] = await httpClient.DeleteAsync(url); + } + } + catch (Exception e) + { + Console.WriteLine($"Error calling DeletePetAdoptionsHistory: {e.Message}"); + throw; + } + return View("Index"); } - -} \ No newline at end of file +} diff --git a/PetAdoptions/petsite/petsite/Controllers/PetListAdoptionsController.cs b/PetAdoptions/petsite/petsite/Controllers/PetListAdoptionsController.cs index c7de96ed..c166d885 100644 --- a/PetAdoptions/petsite/petsite/Controllers/PetListAdoptionsController.cs +++ b/PetAdoptions/petsite/petsite/Controllers/PetListAdoptionsController.cs @@ -5,61 +5,63 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using PetSite.Models; -using Amazon.XRay.Recorder.Handlers.AwsSdk; using System.Net.Http; -using Amazon.XRay.Recorder.Handlers.System.Net; -using Amazon.XRay.Recorder.Core; using System.Text.Json; using PetSite.ViewModels; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Http; +using PetSite.Helpers; namespace PetSite.Controllers { - public class PetListAdoptionsController : Controller + public class PetListAdoptionsController : BaseController { - private static HttpClient _httpClient; - private IConfiguration _configuration; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; - public PetListAdoptionsController(IConfiguration configuration) + public PetListAdoptionsController(ILogger logger, IConfiguration configuration, IHttpClientFactory httpClientFactory) { _configuration = configuration; - AWSSDKHandler.RegisterXRayForAllServices(); - - _httpClient = new HttpClient(new HttpClientXRayTracingHandler(new HttpClientHandler())); + _httpClientFactory = httpClientFactory; + _logger= logger; } // GET public async Task Index() { - AWSXRayRecorder.Instance.BeginSubsegment("Calling PetListAdoptions"); - - Console.WriteLine($"[{AWSXRayRecorder.Instance.GetEntity().TraceId}][{AWSXRayRecorder.Instance.TraceContext.GetEntity().RootSegment.TraceId}] - Calling PetListAdoptions API"); + if (EnsureUserId()) return new EmptyResult(); + // Add custom span attributes using Activity API + var currentActivity = Activity.Current; + if (currentActivity != null) + { + _logger.LogInformation("Calling PetListAdoptions API"); + } string result; - List Pets = new List(); try { - //string petlistadoptionsurl = _configuration["petlistadoptionsurl"]; - string petlistadoptionsurl = SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"petlistadoptionsurl"); - - - result = await _httpClient.GetStringAsync($"{petlistadoptionsurl}"); - Pets = JsonSerializer.Deserialize>(result); + // Begin activity span to track PetListAdoptions API call + using (var activity = Activity.Current?.Source?.StartActivity("Calling PetListAdoptions API")) + { + string petlistadoptionsurl = _configuration["petlistadoptionsurl"]; + using var httpClient = _httpClientFactory.CreateClient(); + var userId = ViewBag.UserId?.ToString(); + var url = UrlHelper.BuildUrl(petlistadoptionsurl, ("userId", userId)); + result = await httpClient.GetStringAsync(url); + Pets = JsonSerializer.Deserialize>(result); + } } catch (Exception e) { - AWSXRayRecorder.Instance.AddException(e); - throw e; - } - finally - { - AWSXRayRecorder.Instance.EndSubsegment(); + _logger.LogError(e, $"Error calling PetListAdoptions API: {e.Message}"); + throw; } return View(Pets); } } -} \ No newline at end of file +} diff --git a/PetAdoptions/petsite/petsite/Dockerfile b/PetAdoptions/petsite/petsite/Dockerfile index 04a9114b..ea08fd22 100644 --- a/PetAdoptions/petsite/petsite/Dockerfile +++ b/PetAdoptions/petsite/petsite/Dockerfile @@ -1,19 +1,20 @@ -FROM mcr.microsoft.com/dotnet/aspnet:7.0-bullseye-slim-amd64 AS base -WORKDIR /app -EXPOSE 80 -EXPOSE 443 - -FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim-amd64 AS build -WORKDIR /src -COPY . . -RUN dotnet restore "PetSite.csproj" -RUN dotnet build "PetSite.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "PetSite.csproj" -c Release -o /app/publish - -FROM base AS final -WORKDIR /app -#ENV AWS_XRAY_DAEMON_ADDRESS=xray-service.default -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "PetSite.dll"] +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 +ENV ASPNETCORE_HTTP_PORTS=80 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY . . +RUN dotnet restore "PetSite.csproj" +RUN dotnet build "PetSite.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "PetSite.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +# Removed X-Ray daemon address environment variable +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "PetSite.dll"] \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Helpers/UrlHelper.cs b/PetAdoptions/petsite/petsite/Helpers/UrlHelper.cs new file mode 100644 index 00000000..3cf22429 --- /dev/null +++ b/PetAdoptions/petsite/petsite/Helpers/UrlHelper.cs @@ -0,0 +1,28 @@ +using System; + +namespace PetSite.Helpers +{ + public static class UrlHelper + { + public static string BuildUrl(string baseUrl, params (string key, string value)[] parameters) + { + if (string.IsNullOrEmpty(baseUrl)) + return string.Empty; + + var url = baseUrl; + var hasQuery = url.Contains("?"); + + foreach (var (key, value) in parameters) + { + if (!string.IsNullOrEmpty(value)) + { + var separator = hasQuery ? "&" : "?"; + url += $"{separator}{key}={Uri.EscapeDataString(value)}"; + hasQuery = true; + } + } + + return url; + } + } +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Middleware/ErrorHandlingMiddleware.cs b/PetAdoptions/petsite/petsite/Middleware/ErrorHandlingMiddleware.cs new file mode 100644 index 00000000..2893cbc7 --- /dev/null +++ b/PetAdoptions/petsite/petsite/Middleware/ErrorHandlingMiddleware.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +namespace PetSite.Middleware +{ + public class ErrorHandlingMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ErrorHandlingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unhandled exception occurred"); + + // Preserve userId and exception message + var userId = context.Request.Query["userId"].ToString(); + var errorMessage = Uri.EscapeDataString(ex.Message); + + var errorPath = $"/Home/Error?message={errorMessage}"; + if (!string.IsNullOrEmpty(userId)) + { + errorPath += $"&userId={userId}"; + } + + context.Response.Redirect(errorPath); + } + } + } +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Models/Food.cs b/PetAdoptions/petsite/petsite/Models/Food.cs new file mode 100644 index 00000000..971356a7 --- /dev/null +++ b/PetAdoptions/petsite/petsite/Models/Food.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace PetSite.Models +{ + public class Food + { + public string food_id { get; set; } + public string food_for { get; set; } + public string food_name { get; set; } + public string food_type { get; set; } + public string food_description { get; set; } + public decimal food_price { get; set; } + public string food_image { get; set; } + } + + public class FoodResponse + { + public List foods { get; set; } + } +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/PetSite.csproj b/PetAdoptions/petsite/petsite/PetSite.csproj index 463932d2..f1f4eb86 100644 --- a/PetAdoptions/petsite/petsite/PetSite.csproj +++ b/PetAdoptions/petsite/petsite/PetSite.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 a80ee246-1735-4630-bd6a-0fd3d01d8e35 Linux @@ -16,25 +16,21 @@ - + - - - - - - - - - - + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + diff --git a/PetAdoptions/petsite/petsite/Program.cs b/PetAdoptions/petsite/petsite/Program.cs index e90e88e3..b778ea22 100644 --- a/PetAdoptions/petsite/petsite/Program.cs +++ b/PetAdoptions/petsite/petsite/Program.cs @@ -1,51 +1,69 @@ -using System; -using Amazon.Extensions.NETCore.Setup; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Prometheus.DotNetRuntime; - -namespace PetSite -{ - public class Program - { - public static void Main(string[] args) - { - // Sets default settings to collect dotnet runtime specific metrics - DotNetRuntimeStatsBuilder.Default().StartCollecting(); - - //You can also set the specifics on what metrics you want to collect as below - // DotNetRuntimeStatsBuilder.Customize() - // .WithThreadPoolSchedulingStats() - // .WithContentionStats() - // .WithGcStats() - // .WithJitStats() - // .WithThreadPoolStats() - // .WithErrorHandler(ex => Console.WriteLine("ERROR: " + ex.ToString())) - // //.WithDebuggingMetrics(true); - // .StartCollecting(); - - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((hostingContext, config) => - { - var env = hostingContext.HostingEnvironment; - Console.WriteLine($"ENVIRONMENT NAME IS: {env.EnvironmentName}"); - if (env.EnvironmentName.ToLower() == "development") - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", - optional: true, reloadOnChange: true); - else - config.AddSystemsManagerWithReload(configureSource => - { - configureSource.Path = "/petstore"; - configureSource.Optional = true; - configureSource.ReloadAfter = TimeSpan.FromMinutes(5); - }); - }) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); - } -} \ No newline at end of file +using System; +using Amazon.Extensions.NETCore.Setup; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Prometheus.DotNetRuntime; +using System.Diagnostics; +using Amazon.Extensions.Configuration.SystemsManager; +using Microsoft.Extensions.DependencyInjection; + +namespace PetSite +{ + public class Program + { + public static void Main(string[] args) + { + // Sets default settings to collect dotnet runtime specific metrics + DotNetRuntimeStatsBuilder.Default().StartCollecting(); + + // Configure Activity source for custom spans + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((hostingContext, config) => + { + var env = hostingContext.HostingEnvironment; + Console.WriteLine($"ENVIRONMENT NAME IS: {env.EnvironmentName}"); + + // Add base configuration first + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + + if (env.EnvironmentName.ToLower() != "development") + { + Console.WriteLine("[DEBUG] Loading Systems Manager configuration..."); + // Build intermediate configuration to get AWS options + var tempConfig = config.Build(); + var awsOptions = tempConfig.GetAWSOptions(); + Console.WriteLine($"[DEBUG] AWS Region: {awsOptions.Region}"); + + config.AddSystemsManager(configureSource => + { + configureSource.Path = "/petstore"; + configureSource.Optional = true; + configureSource.ReloadAfter = TimeSpan.FromMinutes(5); + configureSource.AwsOptions = awsOptions; + }); + Console.WriteLine("[DEBUG] Systems Manager configuration added."); + } + else + { + Console.WriteLine("[DEBUG] Development mode - skipping Systems Manager."); + } + }) + .ConfigureServices((context, services) => + { + if (context.HostingEnvironment.EnvironmentName.ToLower() != "development") + { + services.AddDefaultAWSOptions(context.Configuration.GetAWSOptions()); + } + }) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} diff --git a/PetAdoptions/petsite/petsite/Properties/launchSettings.json b/PetAdoptions/petsite/petsite/Properties/launchSettings.json index fc701351..09b3ff56 100644 --- a/PetAdoptions/petsite/petsite/Properties/launchSettings.json +++ b/PetAdoptions/petsite/petsite/Properties/launchSettings.json @@ -29,8 +29,7 @@ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "environmentVariables": { "ASPNETCORE_URLS": "https://+:443;http://+:80", - "ASPNETCORE_HTTPS_PORT": "44348", - "AWS_XRAY_DAEMON_ADDRESS":"xray-service:default" + "ASPNETCORE_HTTPS_PORT": "44348" }, "httpPort": 55731, "useSSL": true, diff --git a/PetAdoptions/petsite/petsite/Services/PetSearchService.cs b/PetAdoptions/petsite/petsite/Services/PetSearchService.cs new file mode 100644 index 00000000..a257b77d --- /dev/null +++ b/PetAdoptions/petsite/petsite/Services/PetSearchService.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; +using PetSite.Models; +using PetSite.ViewModels; +using PetSite.Helpers; +using Prometheus; + +namespace PetSite.Services +{ + public interface IPetSearchService + { + Task> GetPetDetails(string pettype, string petcolor, string petid, string userId); + } + + public class PetSearchService : IPetSearchService + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + //Prometheus metrics + private static readonly Counter PetSearchCount = + Metrics.CreateCounter("petsite_petsearches_total", "Count the number of searches performed"); + private static readonly Counter PuppySearchCount = + Metrics.CreateCounter("petsite_pet_puppy_searches_total", "Count the number of puppy searches performed"); + private static readonly Counter KittenSearchCount = + Metrics.CreateCounter("petsite_pet_kitten_searches_total", "Count the number of kitten searches performed"); + private static readonly Counter BunnySearchCount = + Metrics.CreateCounter("petsite_pet_bunny_searches_total", "Count the number of bunny searches performed"); + + private readonly Microsoft.AspNetCore.Http.IHttpContextAccessor _httpContextAccessor; + + public PetSearchService(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger logger, Microsoft.AspNetCore.Http.IHttpContextAccessor httpContextAccessor) + { + _httpClientFactory = httpClientFactory; + _configuration = configuration; + _logger = logger; + _httpContextAccessor = httpContextAccessor; + } + + public async Task> GetPetDetails(string pettype, string petcolor, string petid, string userId) + { + switch (pettype) + { + case "puppy": + PuppySearchCount.Inc(); + PetSearchCount.Inc(); + break; + case "kitten": + KittenSearchCount.Inc(); + PetSearchCount.Inc(); + break; + case "bunny": + BunnySearchCount.Inc(); + PetSearchCount.Inc(); + break; + } + + string searchapiurl = _configuration["searchapiurl"]; + using var httpClient = _httpClientFactory.CreateClient(); + httpClient.Timeout = TimeSpan.FromSeconds(30); + + try + { + var url = UrlHelper.BuildUrl(searchapiurl, + ("pettype", pettype != "all" ? pettype : null), + ("petcolor", petcolor != "all" ? petcolor : null), + ("petid", petid != "all" ? petid : null), + ("userId", userId)); + + _logger.LogInformation($"Calling the PetSearch API with: {url}"); + + var response = await httpClient.GetAsync(url); + if (!response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException($"HTTP {(int)response.StatusCode} {response.StatusCode}: {response.ReasonPhrase}. Response: {responseContent}"); + } + + var jsonContent = await response.Content.ReadAsStringAsync(); + + _logger.LogInformation($"PetSearch API responded with: {jsonContent}"); + + if (string.IsNullOrEmpty(jsonContent)) + return new List(); + + return JsonSerializer.Deserialize>(jsonContent) ?? new List(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occurred while fetching pet details."); + throw ex; + } + } + } +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Startup.cs b/PetAdoptions/petsite/petsite/Startup.cs index 9c439d93..067a19ce 100644 --- a/PetAdoptions/petsite/petsite/Startup.cs +++ b/PetAdoptions/petsite/petsite/Startup.cs @@ -1,65 +1,75 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Prometheus; - -namespace PetSite -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - new ConfigurationBuilder() - .AddEnvironmentVariables() - .Build(); - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllersWithViews(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.UseXRay("PetSite", Configuration); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Home/Error"); - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - - app.UseRouting(); - app.UseHttpMetrics(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - endpoints.MapMetrics(); - }); - } - } -} \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Amazon.Extensions.NETCore.Setup; +using Amazon; +using Prometheus; +using PetSite.Middleware; + + +namespace PetSite +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllersWithViews(); + services.AddHttpClient(); + services.AddHttpContextAccessor(); + services.AddScoped(); + + // Configure AWS Services + services.AddAWSService(); + services.AddDefaultAWSOptions(Configuration.GetAWSOptions()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseMiddleware(); + //app.UseDeveloperExceptionPage(); + } + else + { + app.UseMiddleware(); + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + + app.UseRouting(); + app.UseHttpMetrics(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + endpoints.MapMetrics(); + }); + } + } +} diff --git a/PetAdoptions/petsite/petsite/SystemsManagerConfigurationProviderWithReload.cs b/PetAdoptions/petsite/petsite/SystemsManagerConfigurationProviderWithReload.cs deleted file mode 100644 index cd27aa00..00000000 --- a/PetAdoptions/petsite/petsite/SystemsManagerConfigurationProviderWithReload.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using Amazon.Extensions.Configuration.SystemsManager; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Primitives; - -namespace PetSite -{ - /// - /// Simple wrapper around to load - /// parameters from SSM on the first use. Helps with deployment scenarios when - /// the service starts before all SSM parameters are created. - /// - public static class SystemsManagerConfigurationProviderWithReloadExtensions - { - public static IConfigurationBuilder AddSystemsManagerWithReload( - this IConfigurationBuilder builder, - Action configureSource) - { - if (configureSource == null) - throw new ArgumentNullException(nameof (configureSource)); - - var configurationSource = new ConfigurationSource(); - configureSource(configurationSource); - - if (string.IsNullOrWhiteSpace(configurationSource.Path)) - throw new ArgumentNullException("Path"); - if (configurationSource.AwsOptions != null) - return builder.Add(configurationSource); - - configurationSource.AwsOptions = builder.Build().GetAWSOptions(); - return builder.Add(configurationSource); - } - - private class ConfigurationSource : SystemsManagerConfigurationSource, IConfigurationSource - { - IConfigurationProvider IConfigurationSource.Build(IConfigurationBuilder builder) - => new ConfigurationProvider(this); - } - - private class ConfigurationProvider : IConfigurationProvider - { - private readonly SystemsManagerConfigurationProvider _provider; - private readonly TimeSpan? _reloadAfter; - private DateTime _lastAccessTime; - - public ConfigurationProvider(SystemsManagerConfigurationSource source) - { - _reloadAfter = source.ReloadAfter; - _provider = new SystemsManagerConfigurationProvider(source); - } - - public IChangeToken GetReloadToken() - => _provider.GetReloadToken(); - - public IEnumerable GetChildKeys(IEnumerable earlierKeys, string parentPath) - => _provider.GetChildKeys(earlierKeys, parentPath); - - public void Set(string key, string value) - => _provider.Set(key, value); - - public void Load() - { - ReloadIfNeeded(forceReload: true); - } - - public bool TryGet(string key, out string value) - { - ReloadIfNeeded(); - return _provider.TryGet(key, out value); - } - - private void ReloadIfNeeded(bool forceReload = false) - { - if (forceReload || (_reloadAfter.HasValue && (DateTime.UtcNow - _lastAccessTime) > _reloadAfter)) - _provider.Load(); - - _lastAccessTime = DateTime.UtcNow; - } - } - - /*Amazon.Extensions.Configuration.SystemsManager doesn't support AssumeRoleWithWebIdentity see issue here. As a temporary solution, environment variables where provided to override configurations read from Parameter store as those were empty. Long term solution needs to update class SystemsManagerConfigurationProviderWithReloadExtensions - using a different base class or wait for the issue to be solved. - The workaround is to provide a way to inject the ParameterValues as environment variables*/ - - private static Dictionary ConfigurationMapping = new Dictionary { - { "searchapiurl", "SEARCH_API_URL"}, - { "updateadoptionstatusurl", "UPDATE_ADOPTION_STATUS_URL"}, - { "cleanupadoptionsurl", "CLEANUP_ADOPTIONS_URL"}, - { "paymentapiurl", "PAYMENT_API_URL"}, - { "queueurl", "QUEUE_URL"}, - { "snsarn", "SNS_ARN"}, - { "petlistadoptionsurl", "PET_LIST_ADOPTION_URL"}, - { "petadoptionsstepfnarn", "PET_ADOPTION_STEPFN_URL"} - }; - - public static string GetConfiguration(IConfiguration _configuration, string value) - { - string retVal = _configuration[value]; - - string envVar = ConfigurationMapping[value]; - if (!string.IsNullOrEmpty(envVar)) - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envVar))) - { - retVal = Environment.GetEnvironmentVariable(envVar); - } - } - return retVal; - - } - } -} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Views/Adoption/Index.cshtml b/PetAdoptions/petsite/petsite/Views/Adoption/Index.cshtml index d51f4af5..bf50172f 100644 --- a/PetAdoptions/petsite/petsite/Views/Adoption/Index.cshtml +++ b/PetAdoptions/petsite/petsite/Views/Adoption/Index.cshtml @@ -7,7 +7,7 @@ - + *@
-
-
-
- -
- @Model.pettype-@Model.petcolor -
-
- @Model.price -
-
- @for (int i = 0; i < Int32.Parse(Model.cuteness_rate); i++) - { - - } -
- - - - - - - -
-
-
-
-
- Information entered on this page is neither stored nor processed. -
- -
-
-
-
-
-
-

- Payment Details -

+
+
+
+
+
+ + + + @Model.pettype-@Model.petcolor + + + $@Model.price + + +
+ @if (!string.IsNullOrEmpty(Model.cuteness_rate) && Int32.TryParse(Model.cuteness_rate, out int rating)) + { + @for (int i = 0; i < rating; i++) + { + + } + } +
-
-
-
- -
- - + + + + + + + +
+ +
+
+ +

Payment Details

+
+ This is a demo - no real payment will be processed +
+ +
+ + +
+ +
+ + +
+ +
+
+
+ +
-
-
-
- -
- -
-
- -
-
+
+
+ +
-
-
- - -
+
+
+
+ + + + + +
- +
- -
-
- - - -
+ + diff --git a/PetAdoptions/petsite/petsite/Views/Home/HouseKeeping.cshtml b/PetAdoptions/petsite/petsite/Views/Home/HouseKeeping.cshtml index 02e50ac0..353d59d0 100644 --- a/PetAdoptions/petsite/petsite/Views/Home/HouseKeeping.cshtml +++ b/PetAdoptions/petsite/petsite/Views/Home/HouseKeeping.cshtml @@ -8,7 +8,8 @@
-
+
+