diff --git a/cli.go b/cli.go index 3f73977a8..bcc656221 100644 --- a/cli.go +++ b/cli.go @@ -79,6 +79,7 @@ import ( bedrockbackend "github.com/blackbirdworks/gopherstack/services/bedrock" bedrockruntimebackend "github.com/blackbirdworks/gopherstack/services/bedrockruntime" cebackend "github.com/blackbirdworks/gopherstack/services/ce" + cleanroomsbackend "github.com/blackbirdworks/gopherstack/services/cleanrooms" cloudcontrolbackend "github.com/blackbirdworks/gopherstack/services/cloudcontrol" cfnbackend "github.com/blackbirdworks/gopherstack/services/cloudformation" cloudfrontbackend "github.com/blackbirdworks/gopherstack/services/cloudfront" @@ -2758,6 +2759,7 @@ func getMostRecentServiceProviders() []service.Provider { &xraybackend.Provider{}, &s3tablesbackend.Provider{}, &databrewbackend.Provider{}, + &cleanroomsbackend.Provider{}, &forecastbackend.Provider{}, &macie2backend.Provider{}, &appmeshbackend.Provider{}, diff --git a/go.mod b/go.mod index 555657a79..424d53c00 100644 --- a/go.mod +++ b/go.mod @@ -208,6 +208,8 @@ require github.com/aws/aws-sdk-go-v2/service/networkmonitor v1.14.6 require github.com/aws/aws-sdk-go-v2/service/omics v1.45.0 +require github.com/aws/aws-sdk-go-v2/service/cleanrooms v1.45.6 + require ( github.com/antlr/antlr4 v0.0.0-20181218183524-be58ebffde8e // indirect github.com/aws/aws-dax-go v1.2.15 diff --git a/go.sum b/go.sum index 381a7914a..2cde54e58 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/aws/aws-sdk-go-v2/service/bedrockagent v1.54.0 h1:OnHTo0dbX2kWlAYHQZc github.com/aws/aws-sdk-go-v2/service/bedrockagent v1.54.0/go.mod h1:zue4MN4ji6nlKYQYwVLmaPXJ66wB9JnIePX1e1yg5MU= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.1 h1:tnLUbtNW5c056BEbQ4xvlZaakvgdaEdiKF87R1fxuoo= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.1/go.mod h1:DYDD64rVUpCvpLyuWCiTaaSfrW2O9GiDo8S6fNo8ZI0= +github.com/aws/aws-sdk-go-v2/service/cleanrooms v1.45.6 h1:bxQlOwnJeYYz6P0ghQkPyrN1Kd5N02LbA6pEPhYw31U= +github.com/aws/aws-sdk-go-v2/service/cleanrooms v1.45.6/go.mod h1:fz3Qwhfu3co4zcOyQoTbqS2isrZviHAhi0ml0xoUpEU= github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.29.15 h1:E3HjmGRKmA5R7YUzdidZWuxOSKqW95tZZlZ06wND9a0= github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.29.15/go.mod h1:qdsQO5+urrlkcsolFWgiNQ0lpFB0UCQbTKK9j79b1Wg= github.com/aws/aws-sdk-go-v2/service/cloudformation v1.71.7 h1:QkM9aGnVnXrXpxXJMu7GO+E/eho+RfItwDp71aPa79o= diff --git a/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json b/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json index 8fe0621c1..7a5c7c150 100644 --- a/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +++ b/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json @@ -1 +1 @@ -{"version":"4.1.5","results":[[":ui/src/routes/serverlessrepo/page.test.ts",{"duration":0,"failed":true}],[":ui/src/routes/cloudformation/page.test.ts",{"duration":0,"failed":true}],[":ui/src/routes/apprunner/page.test.ts",{"duration":0,"failed":true}],[":ui/src/routes/acm/page.test.ts",{"duration":0,"failed":true}],[":ui/src/lib/nav.test.ts",{"duration":6.260708000000008,"failed":false}],[":ui/src/lib/vitest-examples/greet.spec.ts",{"duration":1.4286249999999967,"failed":false}],[":ui/src/lib/stream.test.ts",{"duration":2.257542000000001,"failed":false}],[":ui/src/lib/settings.test.ts",{"duration":24.43287499999998,"failed":false}],[":ui/src/lib/components/ServiceIcon.test.ts",{"duration":0,"failed":true}],[":ui/src/lib/dynamodb.test.ts",{"duration":4.747832999999986,"failed":false}],[":ui/src/lib/components/ConfirmDialog.test.ts",{"duration":0,"failed":true}],[":ui/src/lib/theme.test.ts",{"duration":4.543415999999979,"failed":true}],[":ui/src/lib/aws/client.test.ts",{"duration":5.851458000000008,"failed":false}]]} \ No newline at end of file +{"version":"4.1.5","results":[[":ui/src/routes/serverlessrepo/page.test.ts",{"duration":0,"failed":true}],[":ui/src/routes/cloudformation/page.test.ts",{"duration":0,"failed":true}],[":ui/src/routes/apprunner/page.test.ts",{"duration":0,"failed":true}],[":ui/src/routes/acm/page.test.ts",{"duration":0,"failed":true}],[":ui/src/lib/nav.test.ts",{"duration":6.260708000000008,"failed":false}],[":ui/src/lib/vitest-examples/greet.spec.ts",{"duration":1.4286249999999967,"failed":false}],[":ui/src/lib/stream.test.ts",{"duration":2.257542000000001,"failed":false}],[":ui/src/lib/settings.test.ts",{"duration":24.43287499999998,"failed":false}],[":ui/src/lib/components/ServiceIcon.test.ts",{"duration":0,"failed":true}],[":ui/src/lib/dynamodb.test.ts",{"duration":4.747832999999986,"failed":false}],[":ui/src/lib/components/ConfirmDialog.test.ts",{"duration":0,"failed":true}],[":ui/src/lib/theme.test.ts",{"duration":4.543415999999979,"failed":true}],[":ui/src/lib/aws/client.test.ts",{"duration":5.851458000000008,"failed":false}],[":ui/src/routes/pipes/page.test.ts",{"duration":0,"failed":true}]]} \ No newline at end of file diff --git a/services/cleanrooms/backend.go b/services/cleanrooms/backend.go new file mode 100644 index 000000000..c9f2e3ffd --- /dev/null +++ b/services/cleanrooms/backend.go @@ -0,0 +1,2776 @@ +// Package cleanrooms implements an in-memory AWS Clean Rooms service backend. +package cleanrooms + +import ( + "context" + "fmt" + "maps" + "slices" + "sort" + "strconv" + "sync" + "time" + + "github.com/google/uuid" + + "github.com/blackbirdworks/gopherstack/pkgs/arn" + "github.com/blackbirdworks/gopherstack/pkgs/awserr" + "github.com/blackbirdworks/gopherstack/pkgs/lockmetrics" +) + +var ( + ErrNotFound = awserr.New("ResourceNotFoundException", awserr.ErrNotFound) + ErrAlreadyExists = awserr.New("ConflictException", awserr.ErrAlreadyExists) + ErrValidation = awserr.New("ValidationException", awserr.ErrInvalidParameter) +) + +const ( + statusActive = "ACTIVE" + errCodeNotFound = "ResourceNotFoundException" + errMsgNotFound = "not found" +) + +// ---- types ---- + +type MemberSpec struct { + PaymentConfig map[string]any `json:"paymentConfiguration,omitempty"` + AccountID string `json:"accountId"` + DisplayName string `json:"displayName"` + Abilities []string `json:"memberAbilities"` +} + +type MemberSummary struct { + AccountID string `json:"accountId"` + DisplayName string `json:"displayName"` + Status string `json:"status"` + Abilities []string `json:"abilities"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type Collaboration struct { + Tags map[string]string `json:"tags,omitempty"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + Arn string `json:"arn"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + CreatorAccountID string `json:"creatorAccountId"` + CreatorDisplayName string `json:"creatorDisplayName"` + QueryLogStatus string `json:"queryLogStatus,omitempty"` + MemberAbilities []string `json:"memberAbilities,omitempty"` + Members []*MemberSummary `json:"members,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type CollaborationSummary struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + Arn string `json:"arn"` + Name string `json:"name"` + CreatorAccountID string `json:"creatorAccountId"` + CreatorDisplayName string `json:"creatorDisplayName"` + MemberStatus string `json:"memberStatus"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type Membership struct { + DefaultResultConfiguration map[string]any `json:"defaultResultConfiguration,omitempty"` + PaymentConfiguration map[string]any `json:"paymentConfiguration,omitempty"` + CollaborationName string `json:"collaborationName"` + CollaborationArn string `json:"collaborationArn"` + CollaborationCreatorAccountID string `json:"collaborationCreatorAccountId"` + CollaborationCreatorDisplayName string `json:"collaborationCreatorDisplayName"` + MembershipIdentifier string `json:"membershipIdentifier"` + Status string `json:"status"` + QueryLogStatus string `json:"queryLogStatus,omitempty"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + Arn string `json:"arn"` + MemberAbilities []string `json:"memberAbilities,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type MembershipSummary struct { + MembershipIdentifier string `json:"membershipIdentifier"` + Arn string `json:"arn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + CollaborationArn string `json:"collaborationArn"` + CollaborationCreatorAccountID string `json:"collaborationCreatorAccountId"` + CollaborationCreatorDisplayName string `json:"collaborationCreatorDisplayName"` + CollaborationName string `json:"collaborationName"` + Status string `json:"status"` + MemberAbilities []string `json:"memberAbilities,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type ConfiguredTable struct { + TableReference map[string]any `json:"tableReference,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + Arn string `json:"arn"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + AnalysisMethod string `json:"analysisMethod,omitempty"` + AllowedColumns []string `json:"allowedColumns,omitempty"` + AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type ConfiguredTableSummary struct { + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + Arn string `json:"arn"` + Name string `json:"name"` + AnalysisMethod string `json:"analysisMethod,omitempty"` + AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type ConfiguredTableAnalysisRule struct { + Policy map[string]any `json:"policy,omitempty"` + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + ConfiguredTableArn string `json:"configuredTableArn"` + Type string `json:"type"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type ConfiguredTableAssociation struct { + Tags map[string]string `json:"tags,omitempty"` + Name string `json:"name"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + ConfiguredTableArn string `json:"configuredTableArn"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + Description string `json:"description,omitempty"` + RoleArn string `json:"roleArn,omitempty"` + Arn string `json:"arn"` + AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type ConfiguredTableAssociationSummary struct { + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + Arn string `json:"arn"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + Name string `json:"name"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type ConfiguredTableAssociationAnalysisRule struct { + Policy map[string]any `json:"policy,omitempty"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + ConfiguredTableAssociationArn string `json:"configuredTableAssociationArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + Type string `json:"type"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type AnalysisTemplate struct { + Source map[string]any `json:"source,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Schema map[string]any `json:"schema,omitempty"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + AnalysisTemplateIdentifier string `json:"analysisTemplateIdentifier"` + CollaborationArn string `json:"collaborationArn"` + Format string `json:"format,omitempty"` + Arn string `json:"arn"` + AnalysisParameters []map[string]any `json:"analysisParameters,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type AnalysisTemplateSummary struct { + AnalysisTemplateIdentifier string `json:"analysisTemplateIdentifier"` + Arn string `json:"arn"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + Name string `json:"name"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type BatchError struct { + Arn string `json:"arn,omitempty"` + Name string `json:"name,omitempty"` + Code string `json:"code"` + Message string `json:"message"` +} + +type Schema struct { + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + CreatorAccountID string `json:"creatorAccountId"` + Name string `json:"name"` + Type string `json:"type"` + AnalysisMethod string `json:"analysisMethod,omitempty"` + Columns []map[string]any `json:"columns,omitempty"` + PartitionKeys []map[string]any `json:"partitionKeys,omitempty"` + AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type SchemaSummary struct { + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + CreatorAccountID string `json:"creatorAccountId"` + Name string `json:"name"` + Type string `json:"type"` + AnalysisMethod string `json:"analysisMethod,omitempty"` + AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type SchemaAnalysisRule struct { + Policy map[string]any `json:"policy,omitempty"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + Name string `json:"name"` + Type string `json:"type"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type ProtectedQuery struct { + SQLParameters map[string]any `json:"sqlParameters,omitempty"` + ResultConfiguration map[string]any `json:"resultConfiguration,omitempty"` + ComputeConfiguration map[string]any `json:"computeConfiguration,omitempty"` + Statistics map[string]any `json:"statistics,omitempty"` + Result map[string]any `json:"result,omitempty"` + Error map[string]any `json:"error,omitempty"` + ID string `json:"id"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + Status string `json:"status"` + CreateTime float64 `json:"createTime,omitempty"` +} + +type ProtectedQuerySummary struct { + ID string `json:"id"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + Status string `json:"status"` + CreateTime float64 `json:"createTime,omitempty"` +} + +type ProtectedJob struct { + JobParameters map[string]any `json:"jobParameters,omitempty"` + ResultConfiguration map[string]any `json:"resultConfiguration,omitempty"` + Statistics map[string]any `json:"statistics,omitempty"` + Result map[string]any `json:"result,omitempty"` + Error map[string]any `json:"error,omitempty"` + ID string `json:"id"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + Status string `json:"status"` + Type string `json:"type"` + CreateTime float64 `json:"createTime,omitempty"` +} + +type ProtectedJobSummary struct { + ID string `json:"id"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + Status string `json:"status"` + Type string `json:"type"` + CreateTime float64 `json:"createTime,omitempty"` +} + +type PrivacyBudgetTemplate struct { + Parameters map[string]any `json:"parameters,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + PrivacyBudgetTemplateIdentifier string `json:"privacyBudgetTemplateIdentifier"` + Arn string `json:"arn"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipArn string `json:"membershipArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + PrivacyBudgetType string `json:"privacyBudgetType"` + AutoRefresh string `json:"autoRefresh,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type PrivacyBudgetTemplateSummary struct { + PrivacyBudgetTemplateIdentifier string `json:"privacyBudgetTemplateIdentifier"` + Arn string `json:"arn"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipArn string `json:"membershipArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + PrivacyBudgetType string `json:"privacyBudgetType"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type PrivacyBudget struct { + Budget map[string]any `json:"budget,omitempty"` + ID string `json:"id"` + PrivacyBudgetTemplateArn string `json:"privacyBudgetTemplateArn"` + PrivacyBudgetTemplateIdentifier string `json:"privacyBudgetTemplateIdentifier"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipArn string `json:"membershipArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + PrivacyBudgetType string `json:"privacyBudgetType"` +} + +type IDMappingTable struct { + InputReferenceConfig map[string]any `json:"inputReferenceConfig,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + InputReferenceProperties map[string]any `json:"inputReferenceProperties,omitempty"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipArn string `json:"membershipArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + IDMappingTableIdentifier string `json:"idMappingTableIdentifier"` + CollaborationArn string `json:"collaborationArn"` + KmsKeyArn string `json:"kmsKeyArn,omitempty"` + Arn string `json:"arn"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type IDMappingTableSummary struct { + IDMappingTableIdentifier string `json:"idMappingTableIdentifier"` + Arn string `json:"arn"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipArn string `json:"membershipArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type IDNamespaceAssociation struct { + InputReferenceConfig map[string]any `json:"inputReferenceConfig,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + IDMappingConfig map[string]any `json:"idMappingConfig,omitempty"` + InputReferenceProperties map[string]any `json:"inputReferenceProperties,omitempty"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + MembershipArn string `json:"membershipArn"` + IDNamespaceAssociationIdentifier string `json:"idNamespaceAssociationIdentifier"` + CollaborationArn string `json:"collaborationArn"` + Arn string `json:"arn"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type IDNamespaceAssociationSummary struct { + IDNamespaceAssociationIdentifier string `json:"idNamespaceAssociationIdentifier"` + Arn string `json:"arn"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipArn string `json:"membershipArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type ConfiguredAudienceModelAssociation struct { + Tags map[string]string `json:"tags,omitempty"` + ConfiguredAudienceModelArn string `json:"configuredAudienceModelArn"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipArn string `json:"membershipArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredAudienceModelAssociationIdentifier string `json:"configuredAudienceModelAssociationIdentifier"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Arn string `json:"arn"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` + ManageResourcePolicies bool `json:"manageResourcePolicies"` +} + +type ConfiguredAudienceModelAssociationSummary struct { + ConfiguredAudienceModelAssociationIdentifier string `json:"configuredAudienceModelAssociationIdentifier"` + Arn string `json:"arn"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipArn string `json:"membershipArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type CollaborationChangeRequest struct { + Details map[string]any `json:"details,omitempty"` + ChangeRequestIdentifier string `json:"changeRequestIdentifier"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + CollaborationArn string `json:"collaborationArn"` + Status string `json:"status"` + Type string `json:"type"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +// ---- InMemoryBackend ---- + +// InMemoryBackend is the in-memory implementation of StorageBackend. +type InMemoryBackend struct { + protectedQueries map[string]map[string]*ProtectedQuery + protectedJobs map[string]map[string]*ProtectedJob + nowFn func() float64 + collaborations map[string]*Collaboration + memberships map[string]*Membership + configuredTables map[string]*ConfiguredTable + ctAnalysisRules map[string]map[string]*ConfiguredTableAnalysisRule + ctAssociations map[string]map[string]*ConfiguredTableAssociation + ctaAnalysisRules map[string]map[string]*ConfiguredTableAssociationAnalysisRule + privacyBudgetTemplates map[string]map[string]*PrivacyBudgetTemplate + tagsByArn map[string]map[string]string + mu *lockmetrics.RWMutex + analysisTemplates map[string]map[string]*AnalysisTemplate + idMappingTables map[string]map[string]*IDMappingTable + idNamespaceAssociations map[string]map[string]*IDNamespaceAssociation + camaAssociations map[string]map[string]*ConfiguredAudienceModelAssociation + changeRequests map[string]map[string]*CollaborationChangeRequest + schemas map[string]map[string]*Schema + schemaAnalysisRules map[string]map[string]map[string]*SchemaAnalysisRule + accountID string + region string + muNow sync.Mutex +} + +// NewInMemoryBackendWithContext creates a backend tied to svcCtx (ignored; no lifecycle goroutines). +func NewInMemoryBackendWithContext(_ context.Context, accountID, region string) *InMemoryBackend { + return NewInMemoryBackend(accountID, region) +} + +// NewInMemoryBackend creates a new in-memory Clean Rooms backend. +func NewInMemoryBackend(accountID, region string) *InMemoryBackend { + return &InMemoryBackend{ + mu: lockmetrics.New("cleanrooms"), + accountID: accountID, + region: region, + collaborations: make(map[string]*Collaboration), + memberships: make(map[string]*Membership), + configuredTables: make(map[string]*ConfiguredTable), + ctAnalysisRules: make(map[string]map[string]*ConfiguredTableAnalysisRule), + ctAssociations: make(map[string]map[string]*ConfiguredTableAssociation), + ctaAnalysisRules: make( + map[string]map[string]*ConfiguredTableAssociationAnalysisRule, + ), + analysisTemplates: make(map[string]map[string]*AnalysisTemplate), + protectedQueries: make(map[string]map[string]*ProtectedQuery), + protectedJobs: make(map[string]map[string]*ProtectedJob), + privacyBudgetTemplates: make(map[string]map[string]*PrivacyBudgetTemplate), + idMappingTables: make(map[string]map[string]*IDMappingTable), + idNamespaceAssociations: make(map[string]map[string]*IDNamespaceAssociation), + camaAssociations: make(map[string]map[string]*ConfiguredAudienceModelAssociation), + changeRequests: make(map[string]map[string]*CollaborationChangeRequest), + schemas: make(map[string]map[string]*Schema), + schemaAnalysisRules: make(map[string]map[string]map[string]*SchemaAnalysisRule), + tagsByArn: make(map[string]map[string]string), + nowFn: func() float64 { return float64(time.Now().Unix()) }, + } +} + +func (b *InMemoryBackend) Region() string { return b.region } +func (b *InMemoryBackend) AccountID() string { return b.accountID } + +func (b *InMemoryBackend) Reset() { + b.mu.Lock("Reset") + defer b.mu.Unlock() + b.collaborations = make(map[string]*Collaboration) + b.memberships = make(map[string]*Membership) + b.configuredTables = make(map[string]*ConfiguredTable) + b.ctAnalysisRules = make(map[string]map[string]*ConfiguredTableAnalysisRule) + b.ctAssociations = make(map[string]map[string]*ConfiguredTableAssociation) + b.ctaAnalysisRules = make(map[string]map[string]*ConfiguredTableAssociationAnalysisRule) + b.analysisTemplates = make(map[string]map[string]*AnalysisTemplate) + b.protectedQueries = make(map[string]map[string]*ProtectedQuery) + b.protectedJobs = make(map[string]map[string]*ProtectedJob) + b.privacyBudgetTemplates = make(map[string]map[string]*PrivacyBudgetTemplate) + b.idMappingTables = make(map[string]map[string]*IDMappingTable) + b.idNamespaceAssociations = make(map[string]map[string]*IDNamespaceAssociation) + b.camaAssociations = make(map[string]map[string]*ConfiguredAudienceModelAssociation) + b.changeRequests = make(map[string]map[string]*CollaborationChangeRequest) + b.schemas = make(map[string]map[string]*Schema) + b.schemaAnalysisRules = make(map[string]map[string]map[string]*SchemaAnalysisRule) + b.tagsByArn = make(map[string]map[string]string) +} + +// ---- ARN helpers ---- + +func (b *InMemoryBackend) collaborationARN(id string) string { + return arn.Build("cleanrooms", b.region, b.accountID, "collaboration/"+id) +} +func (b *InMemoryBackend) membershipARN(id string) string { + return arn.Build("cleanrooms", b.region, b.accountID, "membership/"+id) +} +func (b *InMemoryBackend) configuredTableARN(id string) string { + return arn.Build("cleanrooms", b.region, b.accountID, "configuredtable/"+id) +} +func (b *InMemoryBackend) ctAssociationARN(membershipID, assocID string) string { + return arn.Build( + "cleanrooms", + b.region, + b.accountID, + fmt.Sprintf("membership/%s/configuredtableassociation/%s", membershipID, assocID), + ) +} +func (b *InMemoryBackend) analysisTemplateARN(membershipID, id string) string { + return arn.Build( + "cleanrooms", + b.region, + b.accountID, + fmt.Sprintf("membership/%s/analysistemplate/%s", membershipID, id), + ) +} +func (b *InMemoryBackend) privacyBudgetTemplateARN(membershipID, id string) string { + return arn.Build( + "cleanrooms", + b.region, + b.accountID, + fmt.Sprintf("membership/%s/privacybudgettemplate/%s", membershipID, id), + ) +} +func (b *InMemoryBackend) idMappingTableARN(membershipID, id string) string { + return arn.Build( + "cleanrooms", + b.region, + b.accountID, + fmt.Sprintf("membership/%s/idmappingtable/%s", membershipID, id), + ) +} +func (b *InMemoryBackend) idNamespaceAssocARN(membershipID, id string) string { + return arn.Build( + "cleanrooms", + b.region, + b.accountID, + fmt.Sprintf("membership/%s/idnamespaceassociation/%s", membershipID, id), + ) +} +func (b *InMemoryBackend) camaARN(membershipID, id string) string { + return arn.Build( + "cleanrooms", + b.region, + b.accountID, + fmt.Sprintf("membership/%s/configuredaudiencemodelassociation/%s", membershipID, id), + ) +} + +// ---- pagination and listing helpers ---- + +// listItems ranges over a flat map, optionally skipping items where include returns false, +// converts each item to a summary, sorts by the less predicate, then paginates. +func listItems[T, S any]( + items map[string]*T, + include func(*T) bool, + convert func(*T) *S, + less func(a, b *S) bool, + maxResults, nextToken string, +) ([]*S, string) { + result := make([]*S, 0, len(items)) + for _, t := range items { + if include != nil && !include(t) { + continue + } + result = append(result, convert(t)) + } + sort.Slice(result, func(i, j int) bool { return less(result[i], result[j]) }) + + return paginate(result, maxResults, nextToken) +} + +// listNestedItems ranges over a nested map, collecting items that satisfy match, +// converts them to summaries, sorts, and paginates. +func listNestedItems[T, S any]( + allItems map[string]map[string]*T, + match func(*T) bool, + convert func(*T) *S, + less func(a, b *S) bool, + maxResults, nextToken string, +) ([]*S, string) { + var result []*S + for _, inner := range allItems { + for _, t := range inner { + if match(t) { + result = append(result, convert(t)) + } + } + } + sort.Slice(result, func(i, j int) bool { return less(result[i], result[j]) }) + + return paginate(result, maxResults, nextToken) +} + +func paginate[T any](items []T, maxResultsStr, nextToken string) ([]T, string) { + if len(items) == 0 { + return items, "" + } + pageSize := 100 + if maxResultsStr != "" { + _, _ = fmt.Sscanf(maxResultsStr, "%d", &pageSize) + } + if pageSize <= 0 || pageSize > 1000 { + pageSize = 100 + } + start := 0 + if nextToken != "" { + _, _ = fmt.Sscanf(nextToken, "%d", &start) + } + if start >= len(items) { + return []T{}, "" + } + end := start + pageSize + if end >= len(items) { + return items[start:], "" + } + + return items[start:end], strconv.Itoa(end) +} + +func toAnalysisTemplateSummary(t *AnalysisTemplate) *AnalysisTemplateSummary { + return &AnalysisTemplateSummary{ + AnalysisTemplateIdentifier: t.AnalysisTemplateIdentifier, + Arn: t.Arn, + CollaborationArn: t.CollaborationArn, + CollaborationIdentifier: t.CollaborationIdentifier, + MembershipIdentifier: t.MembershipIdentifier, + MembershipArn: t.MembershipArn, + Name: t.Name, + CreateTime: t.CreateTime, + UpdateTime: t.UpdateTime, + } +} + +func toIDMappingTableSummary(t *IDMappingTable) *IDMappingTableSummary { + return &IDMappingTableSummary{ + IDMappingTableIdentifier: t.IDMappingTableIdentifier, + Arn: t.Arn, + CollaborationArn: t.CollaborationArn, + CollaborationIdentifier: t.CollaborationIdentifier, + MembershipArn: t.MembershipArn, + MembershipIdentifier: t.MembershipIdentifier, + Name: t.Name, + CreateTime: t.CreateTime, + UpdateTime: t.UpdateTime, + } +} + +func toPrivacyBudgetTemplateSummary(t *PrivacyBudgetTemplate) *PrivacyBudgetTemplateSummary { + return &PrivacyBudgetTemplateSummary{ + PrivacyBudgetTemplateIdentifier: t.PrivacyBudgetTemplateIdentifier, + Arn: t.Arn, + CollaborationArn: t.CollaborationArn, + CollaborationIdentifier: t.CollaborationIdentifier, + MembershipArn: t.MembershipArn, + MembershipIdentifier: t.MembershipIdentifier, + PrivacyBudgetType: t.PrivacyBudgetType, + CreateTime: t.CreateTime, + UpdateTime: t.UpdateTime, + } +} + +func toSchemaSummary(s *Schema) *SchemaSummary { + return &SchemaSummary{ + CollaborationArn: s.CollaborationArn, + CollaborationIdentifier: s.CollaborationIdentifier, + CreatorAccountID: s.CreatorAccountID, + Name: s.Name, + Type: s.Type, + AnalysisRuleTypes: s.AnalysisRuleTypes, + AnalysisMethod: s.AnalysisMethod, + CreateTime: s.CreateTime, + UpdateTime: s.UpdateTime, + } +} + +func toIDNamespaceAssociationSummary(a *IDNamespaceAssociation) *IDNamespaceAssociationSummary { + return &IDNamespaceAssociationSummary{ + IDNamespaceAssociationIdentifier: a.IDNamespaceAssociationIdentifier, + Arn: a.Arn, + CollaborationArn: a.CollaborationArn, + CollaborationIdentifier: a.CollaborationIdentifier, + MembershipArn: a.MembershipArn, + MembershipIdentifier: a.MembershipIdentifier, + Name: a.Name, + CreateTime: a.CreateTime, + UpdateTime: a.UpdateTime, + } +} + +func toConfiguredAudienceModelAssociationSummary( + a *ConfiguredAudienceModelAssociation, +) *ConfiguredAudienceModelAssociationSummary { + return &ConfiguredAudienceModelAssociationSummary{ + ConfiguredAudienceModelAssociationIdentifier: a.ConfiguredAudienceModelAssociationIdentifier, + Arn: a.Arn, + CollaborationArn: a.CollaborationArn, + CollaborationIdentifier: a.CollaborationIdentifier, + MembershipArn: a.MembershipArn, + MembershipIdentifier: a.MembershipIdentifier, + Name: a.Name, + CreateTime: a.CreateTime, + UpdateTime: a.UpdateTime, + } +} + +// ---- now helper ---- + +func (b *InMemoryBackend) now() float64 { + b.muNow.Lock() + defer b.muNow.Unlock() + + return b.nowFn() +} + +// ---- Collaboration ---- + +func (b *InMemoryBackend) CreateCollaboration( + name, description, creatorDisplayName string, + creatorMemberAbilities []string, + members []MemberSpec, + queryLogStatus string, + tags map[string]string, +) (*Collaboration, error) { + b.mu.Lock("CreateCollaboration") + defer b.mu.Unlock() + if name == "" { + return nil, ErrValidation + } + id := uuid.NewString() + ts := b.now() + memberSummaries := make([]*MemberSummary, 0, len(members)+1) + memberSummaries = append(memberSummaries, &MemberSummary{ + AccountID: b.accountID, + DisplayName: creatorDisplayName, + Abilities: creatorMemberAbilities, + Status: statusActive, + CreateTime: ts, + UpdateTime: ts, + }) + for _, m := range members { + memberSummaries = append(memberSummaries, &MemberSummary{ + AccountID: m.AccountID, + DisplayName: m.DisplayName, + Abilities: m.Abilities, + Status: "INVITED", + CreateTime: ts, + UpdateTime: ts, + }) + } + collab := &Collaboration{ + CollaborationIdentifier: id, + Arn: b.collaborationARN(id), + Name: name, + Description: description, + CreatorAccountID: b.accountID, + CreatorDisplayName: creatorDisplayName, + MemberAbilities: creatorMemberAbilities, + Members: memberSummaries, + QueryLogStatus: queryLogStatus, + CreateTime: ts, + UpdateTime: ts, + Tags: tags, + } + b.collaborations[id] = collab + if len(tags) > 0 { + b.tagsByArn[collab.Arn] = maps.Clone(tags) + } + + return collab, nil +} + +func (b *InMemoryBackend) GetCollaboration(id string) (*Collaboration, error) { + b.mu.RLock("GetCollaboration") + defer b.mu.RUnlock() + c, ok := b.collaborations[id] + if !ok { + return nil, ErrNotFound + } + + return c, nil +} + +func (b *InMemoryBackend) ListCollaborations( + _, maxResults, nextToken string, +) ([]*CollaborationSummary, string) { + b.mu.RLock("ListCollaborations") + defer b.mu.RUnlock() + items := make([]*CollaborationSummary, 0, len(b.collaborations)) + for _, c := range b.collaborations { + items = append(items, &CollaborationSummary{ + CollaborationIdentifier: c.CollaborationIdentifier, + Arn: c.Arn, + Name: c.Name, + CreatorAccountID: c.CreatorAccountID, + CreatorDisplayName: c.CreatorDisplayName, + MemberStatus: statusActive, + CreateTime: c.CreateTime, + UpdateTime: c.UpdateTime, + }) + } + sort.Slice( + items, + func(i, j int) bool { return items[i].CollaborationIdentifier < items[j].CollaborationIdentifier }, + ) + page, next := paginate(items, maxResults, nextToken) + + return page, next +} + +func (b *InMemoryBackend) UpdateCollaboration( + id, name, description string, +) (*Collaboration, error) { + b.mu.Lock("UpdateCollaboration") + defer b.mu.Unlock() + c, ok := b.collaborations[id] + if !ok { + return nil, ErrNotFound + } + if name != "" { + c.Name = name + } + if description != "" { + c.Description = description + } + c.UpdateTime = b.now() + + return c, nil +} + +func (b *InMemoryBackend) DeleteCollaboration(id string) error { + b.mu.Lock("DeleteCollaboration") + defer b.mu.Unlock() + c, ok := b.collaborations[id] + if !ok { + return ErrNotFound + } + delete(b.tagsByArn, c.Arn) + delete(b.collaborations, id) + + return nil +} + +func (b *InMemoryBackend) ListMembers( + collaborationID string, + maxResults, nextToken string, +) ([]*MemberSummary, string, error) { + b.mu.RLock("ListMembers") + defer b.mu.RUnlock() + c, ok := b.collaborations[collaborationID] + if !ok { + return nil, "", ErrNotFound + } + members := make([]*MemberSummary, len(c.Members)) + copy(members, c.Members) + page, next := paginate(members, maxResults, nextToken) + + return page, next, nil +} + +func (b *InMemoryBackend) DeleteMember(collaborationID, accountID string) error { + b.mu.Lock("DeleteMember") + defer b.mu.Unlock() + c, ok := b.collaborations[collaborationID] + if !ok { + return ErrNotFound + } + for i, m := range c.Members { + if m.AccountID == accountID { + c.Members = append(c.Members[:i], c.Members[i+1:]...) + + return nil + } + } + + return ErrNotFound +} + +// ---- Membership ---- + +func (b *InMemoryBackend) CreateMembership( + collaborationID, queryLogStatus string, + defaultResultConfiguration map[string]any, + paymentConfiguration map[string]any, + tags map[string]string, +) (*Membership, error) { + b.mu.Lock("CreateMembership") + defer b.mu.Unlock() + if collaborationID == "" { + return nil, ErrValidation + } + collab, ok := b.collaborations[collaborationID] + if !ok { + return nil, ErrNotFound + } + id := uuid.NewString() + ts := b.now() + m := &Membership{ + MembershipIdentifier: id, + Arn: b.membershipARN(id), + CollaborationIdentifier: collaborationID, + CollaborationArn: collab.Arn, + CollaborationCreatorAccountID: collab.CreatorAccountID, + CollaborationCreatorDisplayName: collab.CreatorDisplayName, + CollaborationName: collab.Name, + Status: statusActive, + QueryLogStatus: queryLogStatus, + DefaultResultConfiguration: defaultResultConfiguration, + PaymentConfiguration: paymentConfiguration, + CreateTime: ts, + UpdateTime: ts, + } + b.memberships[id] = m + if len(tags) > 0 { + b.tagsByArn[m.Arn] = maps.Clone(tags) + } + + return m, nil +} + +func (b *InMemoryBackend) GetMembership(id string) (*Membership, error) { + b.mu.RLock("GetMembership") + defer b.mu.RUnlock() + m, ok := b.memberships[id] + if !ok { + return nil, ErrNotFound + } + + return m, nil +} + +func (b *InMemoryBackend) ListMemberships( + status, maxResults, nextToken string, +) ([]*MembershipSummary, string) { + b.mu.RLock("ListMemberships") + defer b.mu.RUnlock() + var items []*MembershipSummary + for _, m := range b.memberships { + if status != "" && m.Status != status { + continue + } + items = append(items, &MembershipSummary{ + MembershipIdentifier: m.MembershipIdentifier, + Arn: m.Arn, + CollaborationIdentifier: m.CollaborationIdentifier, + CollaborationArn: m.CollaborationArn, + CollaborationCreatorAccountID: m.CollaborationCreatorAccountID, + CollaborationCreatorDisplayName: m.CollaborationCreatorDisplayName, + CollaborationName: m.CollaborationName, + Status: m.Status, + MemberAbilities: m.MemberAbilities, + CreateTime: m.CreateTime, + UpdateTime: m.UpdateTime, + }) + } + sort.Slice( + items, + func(i, j int) bool { return items[i].MembershipIdentifier < items[j].MembershipIdentifier }, + ) + page, next := paginate(items, maxResults, nextToken) + + return page, next +} + +func (b *InMemoryBackend) UpdateMembership( + id, queryLogStatus string, + defaultResultConfiguration map[string]any, +) (*Membership, error) { + b.mu.Lock("UpdateMembership") + defer b.mu.Unlock() + m, ok := b.memberships[id] + if !ok { + return nil, ErrNotFound + } + if queryLogStatus != "" { + m.QueryLogStatus = queryLogStatus + } + if defaultResultConfiguration != nil { + m.DefaultResultConfiguration = defaultResultConfiguration + } + m.UpdateTime = b.now() + + return m, nil +} + +func (b *InMemoryBackend) DeleteMembership(id string) error { + b.mu.Lock("DeleteMembership") + defer b.mu.Unlock() + m, ok := b.memberships[id] + if !ok { + return ErrNotFound + } + delete(b.tagsByArn, m.Arn) + delete(b.memberships, id) + + return nil +} + +// ---- ConfiguredTable ---- + +func (b *InMemoryBackend) CreateConfiguredTable( + name, description string, + tableReference map[string]any, + allowedColumns []string, + analysisMethod string, + tags map[string]string, +) (*ConfiguredTable, error) { + b.mu.Lock("CreateConfiguredTable") + defer b.mu.Unlock() + if name == "" { + return nil, ErrValidation + } + id := uuid.NewString() + ts := b.now() + ct := &ConfiguredTable{ + ConfiguredTableIdentifier: id, + Arn: b.configuredTableARN(id), + Name: name, + Description: description, + TableReference: tableReference, + AllowedColumns: allowedColumns, + AnalysisMethod: analysisMethod, + CreateTime: ts, + UpdateTime: ts, + Tags: tags, + } + b.configuredTables[id] = ct + if len(tags) > 0 { + b.tagsByArn[ct.Arn] = maps.Clone(tags) + } + + return ct, nil +} + +func (b *InMemoryBackend) GetConfiguredTable(id string) (*ConfiguredTable, error) { + b.mu.RLock("GetConfiguredTable") + defer b.mu.RUnlock() + ct, ok := b.configuredTables[id] + if !ok { + return nil, ErrNotFound + } + + return ct, nil +} + +func (b *InMemoryBackend) ListConfiguredTables( + maxResults, nextToken string, +) ([]*ConfiguredTableSummary, string) { + b.mu.RLock("ListConfiguredTables") + defer b.mu.RUnlock() + items := make([]*ConfiguredTableSummary, 0, len(b.configuredTables)) + for _, ct := range b.configuredTables { + items = append(items, &ConfiguredTableSummary{ + ConfiguredTableIdentifier: ct.ConfiguredTableIdentifier, + Arn: ct.Arn, + Name: ct.Name, + AnalysisMethod: ct.AnalysisMethod, + AnalysisRuleTypes: ct.AnalysisRuleTypes, + CreateTime: ct.CreateTime, + UpdateTime: ct.UpdateTime, + }) + } + sort.Slice( + items, + func(i, j int) bool { return items[i].ConfiguredTableIdentifier < items[j].ConfiguredTableIdentifier }, + ) + page, next := paginate(items, maxResults, nextToken) + + return page, next +} + +func (b *InMemoryBackend) UpdateConfiguredTable( + id, name, description string, +) (*ConfiguredTable, error) { + b.mu.Lock("UpdateConfiguredTable") + defer b.mu.Unlock() + ct, ok := b.configuredTables[id] + if !ok { + return nil, ErrNotFound + } + if name != "" { + ct.Name = name + } + if description != "" { + ct.Description = description + } + ct.UpdateTime = b.now() + + return ct, nil +} + +func (b *InMemoryBackend) DeleteConfiguredTable(id string) error { + b.mu.Lock("DeleteConfiguredTable") + defer b.mu.Unlock() + ct, ok := b.configuredTables[id] + if !ok { + return ErrNotFound + } + delete(b.tagsByArn, ct.Arn) + delete(b.configuredTables, id) + delete(b.ctAnalysisRules, id) + + return nil +} + +// ---- ConfiguredTableAnalysisRule ---- + +func (b *InMemoryBackend) CreateConfiguredTableAnalysisRule( + configuredTableID, analysisRuleType string, + policy map[string]any, +) (*ConfiguredTableAnalysisRule, error) { + b.mu.Lock("CreateConfiguredTableAnalysisRule") + defer b.mu.Unlock() + ct, ok := b.configuredTables[configuredTableID] + if !ok { + return nil, ErrNotFound + } + if b.ctAnalysisRules[configuredTableID] == nil { + b.ctAnalysisRules[configuredTableID] = make(map[string]*ConfiguredTableAnalysisRule) + } + if _, exists := b.ctAnalysisRules[configuredTableID][analysisRuleType]; exists { + return nil, ErrAlreadyExists + } + ts := b.now() + rule := &ConfiguredTableAnalysisRule{ + ConfiguredTableIdentifier: configuredTableID, + ConfiguredTableArn: ct.Arn, + Type: analysisRuleType, + Policy: policy, + CreateTime: ts, + UpdateTime: ts, + } + b.ctAnalysisRules[configuredTableID][analysisRuleType] = rule + if !contains(ct.AnalysisRuleTypes, analysisRuleType) { + ct.AnalysisRuleTypes = append(ct.AnalysisRuleTypes, analysisRuleType) + } + + return rule, nil +} + +func (b *InMemoryBackend) GetConfiguredTableAnalysisRule( + configuredTableID, analysisRuleType string, +) (*ConfiguredTableAnalysisRule, error) { + b.mu.RLock("GetConfiguredTableAnalysisRule") + defer b.mu.RUnlock() + rules, ok := b.ctAnalysisRules[configuredTableID] + if !ok { + return nil, ErrNotFound + } + rule, ok := rules[analysisRuleType] + if !ok { + return nil, ErrNotFound + } + + return rule, nil +} + +func (b *InMemoryBackend) UpdateConfiguredTableAnalysisRule( + configuredTableID, analysisRuleType string, + policy map[string]any, +) (*ConfiguredTableAnalysisRule, error) { + b.mu.Lock("UpdateConfiguredTableAnalysisRule") + defer b.mu.Unlock() + rules, ok := b.ctAnalysisRules[configuredTableID] + if !ok { + return nil, ErrNotFound + } + rule, ok := rules[analysisRuleType] + if !ok { + return nil, ErrNotFound + } + rule.Policy = policy + rule.UpdateTime = b.now() + + return rule, nil +} + +func (b *InMemoryBackend) DeleteConfiguredTableAnalysisRule( + configuredTableID, analysisRuleType string, +) error { + b.mu.Lock("DeleteConfiguredTableAnalysisRule") + defer b.mu.Unlock() + rules, ok := b.ctAnalysisRules[configuredTableID] + if !ok { + return ErrNotFound + } + if _, exists := rules[analysisRuleType]; !exists { + return ErrNotFound + } + delete(rules, analysisRuleType) + if ct, ctOK := b.configuredTables[configuredTableID]; ctOK { + ct.AnalysisRuleTypes = removeFrom(ct.AnalysisRuleTypes, analysisRuleType) + } + + return nil +} + +// ---- ConfiguredTableAssociation ---- + +func (b *InMemoryBackend) CreateConfiguredTableAssociation( + membershipID, name, description, configuredTableID, roleArn string, + tags map[string]string, +) (*ConfiguredTableAssociation, error) { + b.mu.Lock("CreateConfiguredTableAssociation") + defer b.mu.Unlock() + mem, ok := b.memberships[membershipID] + if !ok { + return nil, ErrNotFound + } + ct, ok := b.configuredTables[configuredTableID] + if !ok { + return nil, ErrNotFound + } + if b.ctAssociations[membershipID] == nil { + b.ctAssociations[membershipID] = make(map[string]*ConfiguredTableAssociation) + } + id := uuid.NewString() + ts := b.now() + assoc := &ConfiguredTableAssociation{ + ConfiguredTableAssociationIdentifier: id, + Arn: b.ctAssociationARN(membershipID, id), + MembershipIdentifier: membershipID, + MembershipArn: mem.Arn, + ConfiguredTableIdentifier: configuredTableID, + ConfiguredTableArn: ct.Arn, + Name: name, + Description: description, + RoleArn: roleArn, + CreateTime: ts, + UpdateTime: ts, + Tags: tags, + } + b.ctAssociations[membershipID][id] = assoc + if len(tags) > 0 { + b.tagsByArn[assoc.Arn] = maps.Clone(tags) + } + + return assoc, nil +} + +func (b *InMemoryBackend) GetConfiguredTableAssociation( + membershipID, assocID string, +) (*ConfiguredTableAssociation, error) { + b.mu.RLock("GetConfiguredTableAssociation") + defer b.mu.RUnlock() + assocs, ok := b.ctAssociations[membershipID] + if !ok { + return nil, ErrNotFound + } + assoc, ok := assocs[assocID] + if !ok { + return nil, ErrNotFound + } + + return assoc, nil +} + +func (b *InMemoryBackend) ListConfiguredTableAssociations( + membershipID, maxResults, nextToken string, +) ([]*ConfiguredTableAssociationSummary, string, error) { + b.mu.RLock("ListConfiguredTableAssociations") + defer b.mu.RUnlock() + if _, ok := b.memberships[membershipID]; !ok { + return nil, "", ErrNotFound + } + var items []*ConfiguredTableAssociationSummary + for _, a := range b.ctAssociations[membershipID] { + items = append(items, &ConfiguredTableAssociationSummary{ + ConfiguredTableAssociationIdentifier: a.ConfiguredTableAssociationIdentifier, + Arn: a.Arn, + MembershipIdentifier: a.MembershipIdentifier, + MembershipArn: a.MembershipArn, + ConfiguredTableIdentifier: a.ConfiguredTableIdentifier, + Name: a.Name, + CreateTime: a.CreateTime, + UpdateTime: a.UpdateTime, + }) + } + sort.Slice(items, func(i, j int) bool { + return items[i].ConfiguredTableAssociationIdentifier < items[j].ConfiguredTableAssociationIdentifier + }) + page, next := paginate(items, maxResults, nextToken) + + return page, next, nil +} + +func (b *InMemoryBackend) UpdateConfiguredTableAssociation( + membershipID, assocID, description, roleArn string, +) (*ConfiguredTableAssociation, error) { + b.mu.Lock("UpdateConfiguredTableAssociation") + defer b.mu.Unlock() + assocs, ok := b.ctAssociations[membershipID] + if !ok { + return nil, ErrNotFound + } + assoc, ok := assocs[assocID] + if !ok { + return nil, ErrNotFound + } + if description != "" { + assoc.Description = description + } + if roleArn != "" { + assoc.RoleArn = roleArn + } + assoc.UpdateTime = b.now() + + return assoc, nil +} + +func (b *InMemoryBackend) DeleteConfiguredTableAssociation(membershipID, assocID string) error { + b.mu.Lock("DeleteConfiguredTableAssociation") + defer b.mu.Unlock() + assocs, ok := b.ctAssociations[membershipID] + if !ok { + return ErrNotFound + } + assoc, ok := assocs[assocID] + if !ok { + return ErrNotFound + } + delete(b.tagsByArn, assoc.Arn) + delete(assocs, assocID) + delete(b.ctaAnalysisRules, assocID) + + return nil +} + +// ---- ConfiguredTableAssociationAnalysisRule ---- + +func (b *InMemoryBackend) CreateConfiguredTableAssociationAnalysisRule( + membershipID, assocID, ruleType string, + policy map[string]any, +) (*ConfiguredTableAssociationAnalysisRule, error) { + b.mu.Lock("CreateConfiguredTableAssociationAnalysisRule") + defer b.mu.Unlock() + assocs, ok := b.ctAssociations[membershipID] + if !ok { + return nil, ErrNotFound + } + assoc, ok := assocs[assocID] + if !ok { + return nil, ErrNotFound + } + if b.ctaAnalysisRules[assocID] == nil { + b.ctaAnalysisRules[assocID] = make(map[string]*ConfiguredTableAssociationAnalysisRule) + } + if _, exists := b.ctaAnalysisRules[assocID][ruleType]; exists { + return nil, ErrAlreadyExists + } + mem := b.memberships[membershipID] + ts := b.now() + rule := &ConfiguredTableAssociationAnalysisRule{ + ConfiguredTableAssociationIdentifier: assocID, + ConfiguredTableAssociationArn: assoc.Arn, + MembershipIdentifier: membershipID, + MembershipArn: mem.Arn, + Type: ruleType, + Policy: policy, + CreateTime: ts, + UpdateTime: ts, + } + b.ctaAnalysisRules[assocID][ruleType] = rule + if !contains(assoc.AnalysisRuleTypes, ruleType) { + assoc.AnalysisRuleTypes = append(assoc.AnalysisRuleTypes, ruleType) + } + + return rule, nil +} + +func (b *InMemoryBackend) GetConfiguredTableAssociationAnalysisRule( + _, assocID, ruleType string, +) (*ConfiguredTableAssociationAnalysisRule, error) { + b.mu.RLock("GetConfiguredTableAssociationAnalysisRule") + defer b.mu.RUnlock() + rules, ok := b.ctaAnalysisRules[assocID] + if !ok { + return nil, ErrNotFound + } + rule, ok := rules[ruleType] + if !ok { + return nil, ErrNotFound + } + + return rule, nil +} + +func (b *InMemoryBackend) UpdateConfiguredTableAssociationAnalysisRule( + _, assocID, ruleType string, + policy map[string]any, +) (*ConfiguredTableAssociationAnalysisRule, error) { + b.mu.Lock("UpdateConfiguredTableAssociationAnalysisRule") + defer b.mu.Unlock() + rules, ok := b.ctaAnalysisRules[assocID] + if !ok { + return nil, ErrNotFound + } + rule, ok := rules[ruleType] + if !ok { + return nil, ErrNotFound + } + rule.Policy = policy + rule.UpdateTime = b.now() + + return rule, nil +} + +func (b *InMemoryBackend) DeleteConfiguredTableAssociationAnalysisRule( + membershipID, assocID, ruleType string, +) error { + b.mu.Lock("DeleteConfiguredTableAssociationAnalysisRule") + defer b.mu.Unlock() + rules, ok := b.ctaAnalysisRules[assocID] + if !ok { + return ErrNotFound + } + if _, exists := rules[ruleType]; !exists { + return ErrNotFound + } + delete(rules, ruleType) + if assocs, assocsOK := b.ctAssociations[membershipID]; assocsOK { + if assoc, assocOK := assocs[assocID]; assocOK { + assoc.AnalysisRuleTypes = removeFrom(assoc.AnalysisRuleTypes, ruleType) + } + } + + return nil +} + +// ---- AnalysisTemplate ---- + +func (b *InMemoryBackend) CreateAnalysisTemplate( + membershipID, name, description, format string, + source map[string]any, + analysisParameters []map[string]any, + tags map[string]string, +) (*AnalysisTemplate, error) { + b.mu.Lock("CreateAnalysisTemplate") + defer b.mu.Unlock() + mem, ok := b.memberships[membershipID] + if !ok { + return nil, ErrNotFound + } + if b.analysisTemplates[membershipID] == nil { + b.analysisTemplates[membershipID] = make(map[string]*AnalysisTemplate) + } + id := uuid.NewString() + ts := b.now() + collab := b.collaborations[mem.CollaborationIdentifier] + var collabArn string + if collab != nil { + collabArn = collab.Arn + } + tmpl := &AnalysisTemplate{ + AnalysisTemplateIdentifier: id, + Arn: b.analysisTemplateARN(membershipID, id), + CollaborationArn: collabArn, + CollaborationIdentifier: mem.CollaborationIdentifier, + MembershipIdentifier: membershipID, + MembershipArn: mem.Arn, + Name: name, + Description: description, + Format: format, + Source: source, + AnalysisParameters: analysisParameters, + CreateTime: ts, + UpdateTime: ts, + Tags: tags, + } + b.analysisTemplates[membershipID][id] = tmpl + if len(tags) > 0 { + b.tagsByArn[tmpl.Arn] = maps.Clone(tags) + } + + return tmpl, nil +} + +func (b *InMemoryBackend) GetAnalysisTemplate( + membershipID, templateID string, +) (*AnalysisTemplate, error) { + b.mu.RLock("GetAnalysisTemplate") + defer b.mu.RUnlock() + tmpls, ok := b.analysisTemplates[membershipID] + if !ok { + return nil, ErrNotFound + } + tmpl, ok := tmpls[templateID] + if !ok { + return nil, ErrNotFound + } + + return tmpl, nil +} + +func (b *InMemoryBackend) ListAnalysisTemplates( + membershipID, maxResults, nextToken string, +) ([]*AnalysisTemplateSummary, string, error) { + b.mu.RLock("ListAnalysisTemplates") + defer b.mu.RUnlock() + if _, ok := b.memberships[membershipID]; !ok { + return nil, "", ErrNotFound + } + page, next := listItems( + b.analysisTemplates[membershipID], + nil, + toAnalysisTemplateSummary, + func(a, c *AnalysisTemplateSummary) bool { + return a.AnalysisTemplateIdentifier < c.AnalysisTemplateIdentifier + }, + maxResults, nextToken, + ) + + return page, next, nil +} + +func (b *InMemoryBackend) UpdateAnalysisTemplate( + membershipID, templateID, description string, +) (*AnalysisTemplate, error) { + b.mu.Lock("UpdateAnalysisTemplate") + defer b.mu.Unlock() + tmpls, ok := b.analysisTemplates[membershipID] + if !ok { + return nil, ErrNotFound + } + tmpl, ok := tmpls[templateID] + if !ok { + return nil, ErrNotFound + } + tmpl.Description = description + tmpl.UpdateTime = b.now() + + return tmpl, nil +} + +func (b *InMemoryBackend) DeleteAnalysisTemplate(membershipID, templateID string) error { + b.mu.Lock("DeleteAnalysisTemplate") + defer b.mu.Unlock() + tmpls, ok := b.analysisTemplates[membershipID] + if !ok { + return ErrNotFound + } + tmpl, ok := tmpls[templateID] + if !ok { + return ErrNotFound + } + delete(b.tagsByArn, tmpl.Arn) + delete(tmpls, templateID) + + return nil +} + +func (b *InMemoryBackend) GetCollaborationAnalysisTemplate( + collaborationID, templateArn string, +) (*AnalysisTemplate, error) { + b.mu.RLock("GetCollaborationAnalysisTemplate") + defer b.mu.RUnlock() + for _, tmpls := range b.analysisTemplates { + for _, t := range tmpls { + if t.CollaborationIdentifier == collaborationID && t.Arn == templateArn { + return t, nil + } + } + } + + return nil, ErrNotFound +} + +func (b *InMemoryBackend) ListCollaborationAnalysisTemplates( + collaborationID, maxResults, nextToken string, +) ([]*AnalysisTemplateSummary, string, error) { + b.mu.RLock("ListCollaborationAnalysisTemplates") + defer b.mu.RUnlock() + if _, ok := b.collaborations[collaborationID]; !ok { + return nil, "", ErrNotFound + } + page, next := listNestedItems( + b.analysisTemplates, + func(t *AnalysisTemplate) bool { return t.CollaborationIdentifier == collaborationID }, + toAnalysisTemplateSummary, + func(a, c *AnalysisTemplateSummary) bool { + return a.AnalysisTemplateIdentifier < c.AnalysisTemplateIdentifier + }, + maxResults, nextToken, + ) + + return page, next, nil +} + +func (b *InMemoryBackend) BatchGetCollaborationAnalysisTemplate( + collaborationID string, + templateArns []string, +) ([]*AnalysisTemplate, []BatchError, error) { + b.mu.RLock("BatchGetCollaborationAnalysisTemplate") + defer b.mu.RUnlock() + if _, ok := b.collaborations[collaborationID]; !ok { + return nil, nil, ErrNotFound + } + var results []*AnalysisTemplate + var errors []BatchError + for _, arnStr := range templateArns { + found := false + for _, tmpls := range b.analysisTemplates { + for _, t := range tmpls { + if t.CollaborationIdentifier == collaborationID && t.Arn == arnStr { + results = append(results, t) + found = true + + break + } + } + if found { + break + } + } + if !found { + errors = append( + errors, + BatchError{Arn: arnStr, Code: errCodeNotFound, Message: errMsgNotFound}, + ) + } + } + + return results, errors, nil +} + +// ---- Schema ---- + +func (b *InMemoryBackend) GetSchema(collaborationID, name string) (*Schema, error) { + b.mu.RLock("GetSchema") + defer b.mu.RUnlock() + schemas, ok := b.schemas[collaborationID] + if !ok { + return nil, ErrNotFound + } + s, ok := schemas[name] + if !ok { + return nil, ErrNotFound + } + + return s, nil +} + +func (b *InMemoryBackend) ListSchemas( + collaborationID, schemaType, maxResults, nextToken string, +) ([]*SchemaSummary, string, error) { + b.mu.RLock("ListSchemas") + defer b.mu.RUnlock() + if _, ok := b.collaborations[collaborationID]; !ok { + return nil, "", ErrNotFound + } + page, next := listItems( + b.schemas[collaborationID], + func(s *Schema) bool { return schemaType == "" || s.Type == schemaType }, + toSchemaSummary, + func(a, c *SchemaSummary) bool { return a.Name < c.Name }, + maxResults, nextToken, + ) + + return page, next, nil +} + +func (b *InMemoryBackend) BatchGetSchema( + collaborationID string, + names []string, +) ([]*Schema, []BatchError, error) { + b.mu.RLock("BatchGetSchema") + defer b.mu.RUnlock() + if _, ok := b.collaborations[collaborationID]; !ok { + return nil, nil, ErrNotFound + } + var results []*Schema + var errors []BatchError + for _, name := range names { + s, ok := b.schemas[collaborationID][name] + if ok { + results = append(results, s) + } else { + errors = append(errors, BatchError{Name: name, Code: errCodeNotFound, Message: errMsgNotFound}) + } + } + + return results, errors, nil +} + +func (b *InMemoryBackend) GetSchemaAnalysisRule( + collaborationID, name, ruleType string, +) (*SchemaAnalysisRule, error) { + b.mu.RLock("GetSchemaAnalysisRule") + defer b.mu.RUnlock() + collabRules, ok := b.schemaAnalysisRules[collaborationID] + if !ok { + return nil, ErrNotFound + } + schemaRules, ok := collabRules[name] + if !ok { + return nil, ErrNotFound + } + rule, ok := schemaRules[ruleType] + if !ok { + return nil, ErrNotFound + } + + return rule, nil +} + +func (b *InMemoryBackend) BatchGetSchemaAnalysisRule( + collaborationID string, + names []string, + ruleType string, +) ([]*SchemaAnalysisRule, []BatchError, error) { + b.mu.RLock("BatchGetSchemaAnalysisRule") + defer b.mu.RUnlock() + if _, ok := b.collaborations[collaborationID]; !ok { + return nil, nil, ErrNotFound + } + var results []*SchemaAnalysisRule + var errors []BatchError + for _, name := range names { + collabRules := b.schemaAnalysisRules[collaborationID] + if collabRules != nil { + if schemaRules, srOK := collabRules[name]; srOK { + if rule, ruleOK := schemaRules[ruleType]; ruleOK { + results = append(results, rule) + + continue + } + } + } + errors = append( + errors, + BatchError{Name: name, Code: errCodeNotFound, Message: errMsgNotFound}, + ) + } + + return results, errors, nil +} + +// ---- ProtectedQuery ---- + +func (b *InMemoryBackend) StartProtectedQuery( + membershipID, sqlText string, + resultConfig map[string]any, + computeConfiguration map[string]any, +) (*ProtectedQuery, error) { + b.mu.Lock("StartProtectedQuery") + defer b.mu.Unlock() + mem, ok := b.memberships[membershipID] + if !ok { + return nil, ErrNotFound + } + if b.protectedQueries[membershipID] == nil { + b.protectedQueries[membershipID] = make(map[string]*ProtectedQuery) + } + id := uuid.NewString() + ts := b.now() + var sqlParams map[string]any + if sqlText != "" { + sqlParams = map[string]any{"queryString": sqlText} + } + q := &ProtectedQuery{ + ID: id, + MembershipIdentifier: membershipID, + MembershipArn: mem.Arn, + Status: "STARTED", + SQLParameters: sqlParams, + ResultConfiguration: resultConfig, + ComputeConfiguration: computeConfiguration, + CreateTime: ts, + } + b.protectedQueries[membershipID][id] = q + + return q, nil +} + +func (b *InMemoryBackend) GetProtectedQuery(membershipID, queryID string) (*ProtectedQuery, error) { + b.mu.RLock("GetProtectedQuery") + defer b.mu.RUnlock() + queries, ok := b.protectedQueries[membershipID] + if !ok { + return nil, ErrNotFound + } + q, ok := queries[queryID] + if !ok { + return nil, ErrNotFound + } + + return q, nil +} + +func (b *InMemoryBackend) ListProtectedQueries( + membershipID, status, maxResults, nextToken string, +) ([]*ProtectedQuerySummary, string, error) { + b.mu.RLock("ListProtectedQueries") + defer b.mu.RUnlock() + if _, ok := b.memberships[membershipID]; !ok { + return nil, "", ErrNotFound + } + var items []*ProtectedQuerySummary + for _, q := range b.protectedQueries[membershipID] { + if status != "" && q.Status != status { + continue + } + items = append(items, &ProtectedQuerySummary{ + ID: q.ID, + MembershipIdentifier: q.MembershipIdentifier, + MembershipArn: q.MembershipArn, + Status: q.Status, + CreateTime: q.CreateTime, + }) + } + sort.Slice(items, func(i, j int) bool { return items[i].ID < items[j].ID }) + page, next := paginate(items, maxResults, nextToken) + + return page, next, nil +} + +func (b *InMemoryBackend) UpdateProtectedQuery( + membershipID, queryID, status string, +) (*ProtectedQuery, error) { + b.mu.Lock("UpdateProtectedQuery") + defer b.mu.Unlock() + queries, ok := b.protectedQueries[membershipID] + if !ok { + return nil, ErrNotFound + } + q, ok := queries[queryID] + if !ok { + return nil, ErrNotFound + } + q.Status = status + + return q, nil +} + +// ---- ProtectedJob ---- + +func (b *InMemoryBackend) StartProtectedJob( + membershipID, jobType string, + jobParameters map[string]any, + resultConfig map[string]any, +) (*ProtectedJob, error) { + b.mu.Lock("StartProtectedJob") + defer b.mu.Unlock() + mem, ok := b.memberships[membershipID] + if !ok { + return nil, ErrNotFound + } + if b.protectedJobs[membershipID] == nil { + b.protectedJobs[membershipID] = make(map[string]*ProtectedJob) + } + id := uuid.NewString() + j := &ProtectedJob{ + ID: id, + MembershipIdentifier: membershipID, + MembershipArn: mem.Arn, + Status: "STARTED", + Type: jobType, + JobParameters: jobParameters, + ResultConfiguration: resultConfig, + CreateTime: b.now(), + } + b.protectedJobs[membershipID][id] = j + + return j, nil +} + +func (b *InMemoryBackend) GetProtectedJob(membershipID, jobID string) (*ProtectedJob, error) { + b.mu.RLock("GetProtectedJob") + defer b.mu.RUnlock() + jobs, ok := b.protectedJobs[membershipID] + if !ok { + return nil, ErrNotFound + } + j, ok := jobs[jobID] + if !ok { + return nil, ErrNotFound + } + + return j, nil +} + +func (b *InMemoryBackend) ListProtectedJobs( + membershipID, status, maxResults, nextToken string, +) ([]*ProtectedJobSummary, string, error) { + b.mu.RLock("ListProtectedJobs") + defer b.mu.RUnlock() + if _, ok := b.memberships[membershipID]; !ok { + return nil, "", ErrNotFound + } + var items []*ProtectedJobSummary + for _, j := range b.protectedJobs[membershipID] { + if status != "" && j.Status != status { + continue + } + items = append(items, &ProtectedJobSummary{ + ID: j.ID, + MembershipIdentifier: j.MembershipIdentifier, + MembershipArn: j.MembershipArn, + Status: j.Status, + Type: j.Type, + CreateTime: j.CreateTime, + }) + } + sort.Slice(items, func(i, j int) bool { return items[i].ID < items[j].ID }) + page, next := paginate(items, maxResults, nextToken) + + return page, next, nil +} + +func (b *InMemoryBackend) UpdateProtectedJob( + membershipID, jobID, status string, +) (*ProtectedJob, error) { + b.mu.Lock("UpdateProtectedJob") + defer b.mu.Unlock() + jobs, ok := b.protectedJobs[membershipID] + if !ok { + return nil, ErrNotFound + } + j, ok := jobs[jobID] + if !ok { + return nil, ErrNotFound + } + j.Status = status + + return j, nil +} + +// ---- PrivacyBudgetTemplate ---- + +func (b *InMemoryBackend) CreatePrivacyBudgetTemplate( + membershipID, privacyBudgetType, autoRefresh string, + parameters map[string]any, + tags map[string]string, +) (*PrivacyBudgetTemplate, error) { + b.mu.Lock("CreatePrivacyBudgetTemplate") + defer b.mu.Unlock() + mem, ok := b.memberships[membershipID] + if !ok { + return nil, ErrNotFound + } + if b.privacyBudgetTemplates[membershipID] == nil { + b.privacyBudgetTemplates[membershipID] = make(map[string]*PrivacyBudgetTemplate) + } + id := uuid.NewString() + ts := b.now() + collab := b.collaborations[mem.CollaborationIdentifier] + var collabArn string + if collab != nil { + collabArn = collab.Arn + } + tmpl := &PrivacyBudgetTemplate{ + PrivacyBudgetTemplateIdentifier: id, + Arn: b.privacyBudgetTemplateARN(membershipID, id), + CollaborationArn: collabArn, + CollaborationIdentifier: mem.CollaborationIdentifier, + MembershipArn: mem.Arn, + MembershipIdentifier: membershipID, + PrivacyBudgetType: privacyBudgetType, + AutoRefresh: autoRefresh, + Parameters: parameters, + CreateTime: ts, + UpdateTime: ts, + Tags: tags, + } + b.privacyBudgetTemplates[membershipID][id] = tmpl + if len(tags) > 0 { + b.tagsByArn[tmpl.Arn] = maps.Clone(tags) + } + + return tmpl, nil +} + +func (b *InMemoryBackend) GetPrivacyBudgetTemplate( + membershipID, templateID string, +) (*PrivacyBudgetTemplate, error) { + b.mu.RLock("GetPrivacyBudgetTemplate") + defer b.mu.RUnlock() + tmpls, ok := b.privacyBudgetTemplates[membershipID] + if !ok { + return nil, ErrNotFound + } + tmpl, ok := tmpls[templateID] + if !ok { + return nil, ErrNotFound + } + + return tmpl, nil +} + +func (b *InMemoryBackend) ListPrivacyBudgetTemplates( + membershipID, privacyBudgetType, maxResults, nextToken string, +) ([]*PrivacyBudgetTemplateSummary, string, error) { + b.mu.RLock("ListPrivacyBudgetTemplates") + defer b.mu.RUnlock() + if _, ok := b.memberships[membershipID]; !ok { + return nil, "", ErrNotFound + } + page, next := listItems( + b.privacyBudgetTemplates[membershipID], + func(t *PrivacyBudgetTemplate) bool { + return privacyBudgetType == "" || t.PrivacyBudgetType == privacyBudgetType + }, + toPrivacyBudgetTemplateSummary, + func(a, c *PrivacyBudgetTemplateSummary) bool { + return a.PrivacyBudgetTemplateIdentifier < c.PrivacyBudgetTemplateIdentifier + }, + maxResults, nextToken, + ) + + return page, next, nil +} + +func (b *InMemoryBackend) UpdatePrivacyBudgetTemplate( + membershipID, templateID, autoRefresh string, + parameters map[string]any, +) (*PrivacyBudgetTemplate, error) { + b.mu.Lock("UpdatePrivacyBudgetTemplate") + defer b.mu.Unlock() + tmpls, ok := b.privacyBudgetTemplates[membershipID] + if !ok { + return nil, ErrNotFound + } + tmpl, ok := tmpls[templateID] + if !ok { + return nil, ErrNotFound + } + if autoRefresh != "" { + tmpl.AutoRefresh = autoRefresh + } + if parameters != nil { + tmpl.Parameters = parameters + } + tmpl.UpdateTime = b.now() + + return tmpl, nil +} + +func (b *InMemoryBackend) DeletePrivacyBudgetTemplate(membershipID, templateID string) error { + b.mu.Lock("DeletePrivacyBudgetTemplate") + defer b.mu.Unlock() + tmpls, ok := b.privacyBudgetTemplates[membershipID] + if !ok { + return ErrNotFound + } + tmpl, ok := tmpls[templateID] + if !ok { + return ErrNotFound + } + delete(b.tagsByArn, tmpl.Arn) + delete(tmpls, templateID) + + return nil +} + +func (b *InMemoryBackend) ListPrivacyBudgets( + membershipID, _, _, _ string, +) ([]*PrivacyBudget, string, error) { + b.mu.RLock("ListPrivacyBudgets") + defer b.mu.RUnlock() + if _, ok := b.memberships[membershipID]; !ok { + return nil, "", ErrNotFound + } + + return []*PrivacyBudget{}, "", nil +} + +func (b *InMemoryBackend) ListCollaborationPrivacyBudgets( + collaborationID, _, _, _ string, +) ([]*PrivacyBudget, string, error) { + b.mu.RLock("ListCollaborationPrivacyBudgets") + defer b.mu.RUnlock() + if _, ok := b.collaborations[collaborationID]; !ok { + return nil, "", ErrNotFound + } + + return []*PrivacyBudget{}, "", nil +} + +func (b *InMemoryBackend) GetCollaborationPrivacyBudgetTemplate( + collaborationID, templateID string, +) (*PrivacyBudgetTemplate, error) { + b.mu.RLock("GetCollaborationPrivacyBudgetTemplate") + defer b.mu.RUnlock() + for _, tmpls := range b.privacyBudgetTemplates { + for _, t := range tmpls { + if t.CollaborationIdentifier == collaborationID && + t.PrivacyBudgetTemplateIdentifier == templateID { + return t, nil + } + } + } + + return nil, ErrNotFound +} + +func (b *InMemoryBackend) ListCollaborationPrivacyBudgetTemplates( + collaborationID, maxResults, nextToken string, +) ([]*PrivacyBudgetTemplateSummary, string, error) { + b.mu.RLock("ListCollaborationPrivacyBudgetTemplates") + defer b.mu.RUnlock() + if _, ok := b.collaborations[collaborationID]; !ok { + return nil, "", ErrNotFound + } + page, next := listNestedItems( + b.privacyBudgetTemplates, + func(t *PrivacyBudgetTemplate) bool { return t.CollaborationIdentifier == collaborationID }, + toPrivacyBudgetTemplateSummary, + func(a, c *PrivacyBudgetTemplateSummary) bool { + return a.PrivacyBudgetTemplateIdentifier < c.PrivacyBudgetTemplateIdentifier + }, + maxResults, nextToken, + ) + + return page, next, nil +} + +func (b *InMemoryBackend) PreviewPrivacyImpact( + membershipID string, + _ map[string]any, +) (map[string]any, error) { + b.mu.RLock("PreviewPrivacyImpact") + defer b.mu.RUnlock() + if _, ok := b.memberships[membershipID]; !ok { + return nil, ErrNotFound + } + + return map[string]any{"privacyImpact": map[string]any{"aggregationCount": []any{}}}, nil +} + +// ---- IDMappingTable ---- + +func (b *InMemoryBackend) CreateIDMappingTable( + membershipID, name, description string, + inputReferenceConfig map[string]any, + kmsKeyArn string, + tags map[string]string, +) (*IDMappingTable, error) { + if name == "" { + return nil, ErrValidation + } + b.mu.Lock("CreateIDMappingTable") + defer b.mu.Unlock() + mem, ok := b.memberships[membershipID] + if !ok { + return nil, ErrNotFound + } + if b.idMappingTables[membershipID] == nil { + b.idMappingTables[membershipID] = make(map[string]*IDMappingTable) + } + id := uuid.NewString() + ts := b.now() + collab := b.collaborations[mem.CollaborationIdentifier] + var collabArn string + if collab != nil { + collabArn = collab.Arn + } + t := &IDMappingTable{ + IDMappingTableIdentifier: id, + Arn: b.idMappingTableARN(membershipID, id), + CollaborationArn: collabArn, + CollaborationIdentifier: mem.CollaborationIdentifier, + MembershipArn: mem.Arn, + MembershipIdentifier: membershipID, + Name: name, + Description: description, + InputReferenceConfig: inputReferenceConfig, + KmsKeyArn: kmsKeyArn, + CreateTime: ts, + UpdateTime: ts, + Tags: tags, + } + b.idMappingTables[membershipID][id] = t + if len(tags) > 0 { + b.tagsByArn[t.Arn] = maps.Clone(tags) + } + + return t, nil +} + +func (b *InMemoryBackend) GetIDMappingTable(membershipID, tableID string) (*IDMappingTable, error) { + b.mu.RLock("GetIDMappingTable") + defer b.mu.RUnlock() + tables, ok := b.idMappingTables[membershipID] + if !ok { + return nil, ErrNotFound + } + t, ok := tables[tableID] + if !ok { + return nil, ErrNotFound + } + + return t, nil +} + +func (b *InMemoryBackend) ListIDMappingTables( + membershipID, maxResults, nextToken string, +) ([]*IDMappingTableSummary, string, error) { + b.mu.RLock("ListIDMappingTables") + defer b.mu.RUnlock() + if _, ok := b.memberships[membershipID]; !ok { + return nil, "", ErrNotFound + } + page, next := listItems( + b.idMappingTables[membershipID], + nil, + toIDMappingTableSummary, + func(a, c *IDMappingTableSummary) bool { + return a.IDMappingTableIdentifier < c.IDMappingTableIdentifier + }, + maxResults, nextToken, + ) + + return page, next, nil +} + +func (b *InMemoryBackend) UpdateIDMappingTable( + membershipID, tableID, description, kmsKeyArn string, +) (*IDMappingTable, error) { + b.mu.Lock("UpdateIDMappingTable") + defer b.mu.Unlock() + tables, ok := b.idMappingTables[membershipID] + if !ok { + return nil, ErrNotFound + } + t, ok := tables[tableID] + if !ok { + return nil, ErrNotFound + } + if description != "" { + t.Description = description + } + if kmsKeyArn != "" { + t.KmsKeyArn = kmsKeyArn + } + t.UpdateTime = b.now() + + return t, nil +} + +func (b *InMemoryBackend) DeleteIDMappingTable(membershipID, tableID string) error { + b.mu.Lock("DeleteIDMappingTable") + defer b.mu.Unlock() + tables, ok := b.idMappingTables[membershipID] + if !ok { + return ErrNotFound + } + t, ok := tables[tableID] + if !ok { + return ErrNotFound + } + delete(b.tagsByArn, t.Arn) + delete(tables, tableID) + + return nil +} + +func (b *InMemoryBackend) PopulateIDMappingTable( + membershipID, tableID string, +) (map[string]any, error) { + b.mu.RLock("PopulateIDMappingTable") + defer b.mu.RUnlock() + if _, ok := b.idMappingTables[membershipID]; !ok { + return nil, ErrNotFound + } + if _, ok := b.idMappingTables[membershipID][tableID]; !ok { + return nil, ErrNotFound + } + + return map[string]any{"mappedJobIdentifier": uuid.NewString()}, nil +} + +// ---- IDNamespaceAssociation ---- + +func (b *InMemoryBackend) CreateIDNamespaceAssociation( + membershipID, name, description string, + inputReferenceConfig map[string]any, + idMappingConfig map[string]any, + tags map[string]string, +) (*IDNamespaceAssociation, error) { + b.mu.Lock("CreateIDNamespaceAssociation") + defer b.mu.Unlock() + mem, ok := b.memberships[membershipID] + if !ok { + return nil, ErrNotFound + } + if b.idNamespaceAssociations[membershipID] == nil { + b.idNamespaceAssociations[membershipID] = make(map[string]*IDNamespaceAssociation) + } + id := uuid.NewString() + ts := b.now() + collab := b.collaborations[mem.CollaborationIdentifier] + var collabArn string + if collab != nil { + collabArn = collab.Arn + } + assoc := &IDNamespaceAssociation{ + IDNamespaceAssociationIdentifier: id, + Arn: b.idNamespaceAssocARN(membershipID, id), + CollaborationArn: collabArn, + CollaborationIdentifier: mem.CollaborationIdentifier, + MembershipArn: mem.Arn, + MembershipIdentifier: membershipID, + Name: name, + Description: description, + InputReferenceConfig: inputReferenceConfig, + IDMappingConfig: idMappingConfig, + CreateTime: ts, + UpdateTime: ts, + Tags: tags, + } + b.idNamespaceAssociations[membershipID][id] = assoc + if len(tags) > 0 { + b.tagsByArn[assoc.Arn] = maps.Clone(tags) + } + + return assoc, nil +} + +func (b *InMemoryBackend) GetIDNamespaceAssociation( + membershipID, assocID string, +) (*IDNamespaceAssociation, error) { + b.mu.RLock("GetIDNamespaceAssociation") + defer b.mu.RUnlock() + assocs, ok := b.idNamespaceAssociations[membershipID] + if !ok { + return nil, ErrNotFound + } + assoc, ok := assocs[assocID] + if !ok { + return nil, ErrNotFound + } + + return assoc, nil +} + +func (b *InMemoryBackend) ListIDNamespaceAssociations( + membershipID, maxResults, nextToken string, +) ([]*IDNamespaceAssociationSummary, string, error) { + b.mu.RLock("ListIDNamespaceAssociations") + defer b.mu.RUnlock() + if _, ok := b.memberships[membershipID]; !ok { + return nil, "", ErrNotFound + } + page, next := listItems( + b.idNamespaceAssociations[membershipID], + nil, + toIDNamespaceAssociationSummary, + func(a, c *IDNamespaceAssociationSummary) bool { + return a.IDNamespaceAssociationIdentifier < c.IDNamespaceAssociationIdentifier + }, + maxResults, nextToken, + ) + + return page, next, nil +} + +func (b *InMemoryBackend) UpdateIDNamespaceAssociation( + membershipID, assocID, description string, + idMappingConfig map[string]any, +) (*IDNamespaceAssociation, error) { + b.mu.Lock("UpdateIDNamespaceAssociation") + defer b.mu.Unlock() + assocs, ok := b.idNamespaceAssociations[membershipID] + if !ok { + return nil, ErrNotFound + } + assoc, ok := assocs[assocID] + if !ok { + return nil, ErrNotFound + } + if description != "" { + assoc.Description = description + } + if idMappingConfig != nil { + assoc.IDMappingConfig = idMappingConfig + } + assoc.UpdateTime = b.now() + + return assoc, nil +} + +func (b *InMemoryBackend) DeleteIDNamespaceAssociation(membershipID, assocID string) error { + b.mu.Lock("DeleteIDNamespaceAssociation") + defer b.mu.Unlock() + assocs, ok := b.idNamespaceAssociations[membershipID] + if !ok { + return ErrNotFound + } + assoc, ok := assocs[assocID] + if !ok { + return ErrNotFound + } + delete(b.tagsByArn, assoc.Arn) + delete(assocs, assocID) + + return nil +} + +func (b *InMemoryBackend) GetCollaborationIDNamespaceAssociation( + collaborationID, assocID string, +) (*IDNamespaceAssociation, error) { + b.mu.RLock("GetCollaborationIDNamespaceAssociation") + defer b.mu.RUnlock() + for _, assocs := range b.idNamespaceAssociations { + for _, a := range assocs { + if a.CollaborationIdentifier == collaborationID && + a.IDNamespaceAssociationIdentifier == assocID { + return a, nil + } + } + } + + return nil, ErrNotFound +} + +func (b *InMemoryBackend) ListCollaborationIDNamespaceAssociations( + collaborationID, maxResults, nextToken string, +) ([]*IDNamespaceAssociationSummary, string, error) { + b.mu.RLock("ListCollaborationIDNamespaceAssociations") + defer b.mu.RUnlock() + if _, ok := b.collaborations[collaborationID]; !ok { + return nil, "", ErrNotFound + } + page, next := listNestedItems( + b.idNamespaceAssociations, + func(a *IDNamespaceAssociation) bool { return a.CollaborationIdentifier == collaborationID }, + toIDNamespaceAssociationSummary, + func(a, c *IDNamespaceAssociationSummary) bool { + return a.IDNamespaceAssociationIdentifier < c.IDNamespaceAssociationIdentifier + }, + maxResults, + nextToken, + ) + + return page, next, nil +} + +// ---- ConfiguredAudienceModelAssociation ---- + +func (b *InMemoryBackend) CreateConfiguredAudienceModelAssociation( + membershipID, configuredAudienceModelArn, name, description string, + manageResourcePolicies bool, + tags map[string]string, +) (*ConfiguredAudienceModelAssociation, error) { + if configuredAudienceModelArn == "" || name == "" { + return nil, ErrValidation + } + b.mu.Lock("CreateConfiguredAudienceModelAssociation") + defer b.mu.Unlock() + mem, ok := b.memberships[membershipID] + if !ok { + return nil, ErrNotFound + } + if b.camaAssociations[membershipID] == nil { + b.camaAssociations[membershipID] = make(map[string]*ConfiguredAudienceModelAssociation) + } + id := uuid.NewString() + ts := b.now() + collab := b.collaborations[mem.CollaborationIdentifier] + var collabArn string + if collab != nil { + collabArn = collab.Arn + } + assoc := &ConfiguredAudienceModelAssociation{ + ConfiguredAudienceModelAssociationIdentifier: id, + Arn: b.camaARN(membershipID, id), + CollaborationArn: collabArn, + CollaborationIdentifier: mem.CollaborationIdentifier, + MembershipArn: mem.Arn, + MembershipIdentifier: membershipID, + ConfiguredAudienceModelArn: configuredAudienceModelArn, + Name: name, + Description: description, + ManageResourcePolicies: manageResourcePolicies, + CreateTime: ts, + UpdateTime: ts, + Tags: tags, + } + b.camaAssociations[membershipID][id] = assoc + if len(tags) > 0 { + b.tagsByArn[assoc.Arn] = maps.Clone(tags) + } + + return assoc, nil +} + +func (b *InMemoryBackend) GetConfiguredAudienceModelAssociation( + membershipID, assocID string, +) (*ConfiguredAudienceModelAssociation, error) { + b.mu.RLock("GetConfiguredAudienceModelAssociation") + defer b.mu.RUnlock() + assocs, ok := b.camaAssociations[membershipID] + if !ok { + return nil, ErrNotFound + } + assoc, ok := assocs[assocID] + if !ok { + return nil, ErrNotFound + } + + return assoc, nil +} + +func (b *InMemoryBackend) ListConfiguredAudienceModelAssociations( + membershipID, maxResults, nextToken string, +) ([]*ConfiguredAudienceModelAssociationSummary, string, error) { + b.mu.RLock("ListConfiguredAudienceModelAssociations") + defer b.mu.RUnlock() + if _, ok := b.memberships[membershipID]; !ok { + return nil, "", ErrNotFound + } + page, next := listItems( + b.camaAssociations[membershipID], + nil, + toConfiguredAudienceModelAssociationSummary, + func(a, c *ConfiguredAudienceModelAssociationSummary) bool { + return a.ConfiguredAudienceModelAssociationIdentifier < c.ConfiguredAudienceModelAssociationIdentifier + }, + maxResults, nextToken, + ) + + return page, next, nil +} + +func (b *InMemoryBackend) UpdateConfiguredAudienceModelAssociation( + membershipID, assocID, name, description string, +) (*ConfiguredAudienceModelAssociation, error) { + b.mu.Lock("UpdateConfiguredAudienceModelAssociation") + defer b.mu.Unlock() + assocs, ok := b.camaAssociations[membershipID] + if !ok { + return nil, ErrNotFound + } + assoc, ok := assocs[assocID] + if !ok { + return nil, ErrNotFound + } + if name != "" { + assoc.Name = name + } + if description != "" { + assoc.Description = description + } + assoc.UpdateTime = b.now() + + return assoc, nil +} + +func (b *InMemoryBackend) DeleteConfiguredAudienceModelAssociation( + membershipID, assocID string, +) error { + b.mu.Lock("DeleteConfiguredAudienceModelAssociation") + defer b.mu.Unlock() + assocs, ok := b.camaAssociations[membershipID] + if !ok { + return ErrNotFound + } + assoc, ok := assocs[assocID] + if !ok { + return ErrNotFound + } + delete(b.tagsByArn, assoc.Arn) + delete(assocs, assocID) + + return nil +} + +func (b *InMemoryBackend) GetCollaborationConfiguredAudienceModelAssociation( + collaborationID, assocID string, +) (*ConfiguredAudienceModelAssociation, error) { + b.mu.RLock("GetCollaborationConfiguredAudienceModelAssociation") + defer b.mu.RUnlock() + for _, assocs := range b.camaAssociations { + for _, a := range assocs { + if a.CollaborationIdentifier == collaborationID && + a.ConfiguredAudienceModelAssociationIdentifier == assocID { + return a, nil + } + } + } + + return nil, ErrNotFound +} + +func (b *InMemoryBackend) ListCollaborationConfiguredAudienceModelAssociations( + collaborationID, maxResults, nextToken string, +) ([]*ConfiguredAudienceModelAssociationSummary, string, error) { + b.mu.RLock("ListCollaborationConfiguredAudienceModelAssociations") + defer b.mu.RUnlock() + if _, ok := b.collaborations[collaborationID]; !ok { + return nil, "", ErrNotFound + } + page, next := listNestedItems( + b.camaAssociations, + func(a *ConfiguredAudienceModelAssociation) bool { + return a.CollaborationIdentifier == collaborationID + }, + toConfiguredAudienceModelAssociationSummary, + func(a, c *ConfiguredAudienceModelAssociationSummary) bool { + return a.ConfiguredAudienceModelAssociationIdentifier < c.ConfiguredAudienceModelAssociationIdentifier + }, + maxResults, nextToken, + ) + + return page, next, nil +} + +// ---- CollaborationChangeRequest ---- + +func (b *InMemoryBackend) CreateCollaborationChangeRequest( + collaborationID, changeRequestType string, + details map[string]any, +) (*CollaborationChangeRequest, error) { + b.mu.Lock("CreateCollaborationChangeRequest") + defer b.mu.Unlock() + collab, ok := b.collaborations[collaborationID] + if !ok { + return nil, ErrNotFound + } + if b.changeRequests[collaborationID] == nil { + b.changeRequests[collaborationID] = make(map[string]*CollaborationChangeRequest) + } + id := uuid.NewString() + ts := b.now() + req := &CollaborationChangeRequest{ + ChangeRequestIdentifier: id, + CollaborationIdentifier: collaborationID, + CollaborationArn: collab.Arn, + Status: "PENDING", + Type: changeRequestType, + Details: details, + CreateTime: ts, + UpdateTime: ts, + } + b.changeRequests[collaborationID][id] = req + + return req, nil +} + +func (b *InMemoryBackend) GetCollaborationChangeRequest( + collaborationID, changeRequestID string, +) (*CollaborationChangeRequest, error) { + b.mu.RLock("GetCollaborationChangeRequest") + defer b.mu.RUnlock() + reqs, ok := b.changeRequests[collaborationID] + if !ok { + return nil, ErrNotFound + } + req, ok := reqs[changeRequestID] + if !ok { + return nil, ErrNotFound + } + + return req, nil +} + +func (b *InMemoryBackend) ListCollaborationChangeRequests( + collaborationID, maxResults, nextToken string, +) ([]*CollaborationChangeRequest, string, error) { + b.mu.RLock("ListCollaborationChangeRequests") + defer b.mu.RUnlock() + if _, ok := b.collaborations[collaborationID]; !ok { + return nil, "", ErrNotFound + } + var items []*CollaborationChangeRequest + for _, r := range b.changeRequests[collaborationID] { + items = append(items, r) + } + sort.Slice( + items, + func(i, j int) bool { return items[i].ChangeRequestIdentifier < items[j].ChangeRequestIdentifier }, + ) + page, next := paginate(items, maxResults, nextToken) + + return page, next, nil +} + +func (b *InMemoryBackend) UpdateCollaborationChangeRequest( + collaborationID, changeRequestID, status string, +) (*CollaborationChangeRequest, error) { + b.mu.Lock("UpdateCollaborationChangeRequest") + defer b.mu.Unlock() + reqs, ok := b.changeRequests[collaborationID] + if !ok { + return nil, ErrNotFound + } + req, ok := reqs[changeRequestID] + if !ok { + return nil, ErrNotFound + } + req.Status = status + req.UpdateTime = b.now() + + return req, nil +} + +// ---- Tags ---- + +func (b *InMemoryBackend) ListTagsForResource(resourceArn string) (map[string]string, error) { + b.mu.RLock("ListTagsForResource") + defer b.mu.RUnlock() + if tags, ok := b.tagsByArn[resourceArn]; ok { + return maps.Clone(tags), nil + } + + return map[string]string{}, nil +} + +func (b *InMemoryBackend) TagResource(resourceArn string, tags map[string]string) error { + b.mu.Lock("TagResource") + defer b.mu.Unlock() + if b.tagsByArn[resourceArn] == nil { + b.tagsByArn[resourceArn] = make(map[string]string) + } + maps.Copy(b.tagsByArn[resourceArn], tags) + + return nil +} + +func (b *InMemoryBackend) UntagResource(resourceArn string, tagKeys []string) error { + b.mu.Lock("UntagResource") + defer b.mu.Unlock() + tags := b.tagsByArn[resourceArn] + for _, k := range tagKeys { + delete(tags, k) + } + + return nil +} + +// ---- helpers ---- + +func contains(ss []string, s string) bool { + return slices.Contains(ss, s) +} + +func removeFrom(ss []string, s string) []string { + var out []string + for _, v := range ss { + if v != s { + out = append(out, v) + } + } + + return out +} diff --git a/services/cleanrooms/handler.go b/services/cleanrooms/handler.go new file mode 100644 index 000000000..e56b74274 --- /dev/null +++ b/services/cleanrooms/handler.go @@ -0,0 +1,3105 @@ +package cleanrooms + +import ( + "context" + "encoding/json" + "errors" + "maps" + "net/http" + "strings" + + "github.com/labstack/echo/v5" + + "github.com/blackbirdworks/gopherstack/pkgs/httputils" + "github.com/blackbirdworks/gopherstack/pkgs/logger" + "github.com/blackbirdworks/gopherstack/pkgs/service" +) + +// Path sub-resource name constants (goconst). +const ( + subAnalysisTemplates = "analysistemplates" + subCAMAAssociations = "configuredaudiencemodelassociations" + subIDNamespaceAssocs = "idnamespaceassociations" + subPrivacyBudgetTmpls = "privacybudgettemplates" + subSchemas = "schemas" + subAnalysisRule = "analysisRule" + subProtectedJobs = "protectedJobs" + subProtectedQueries = "protectedQueries" + subTags = "tags" +) + +// Response key constants (goconst). +const ( + keyCollaboration = "collaboration" + keyAnalysisTemplate = "analysisTemplate" + keyErrors = "errors" + keyCollaborationChangeRequest = "collaborationChangeRequest" + keyCAMAAssociation = "configuredAudienceModelAssociation" + keyIDNamespaceAssociation = "idNamespaceAssociation" + keyPrivacyBudgetTemplate = "privacyBudgetTemplate" + keyMembership = "membership" + keyConfiguredTable = "configuredTable" + keyConfiguredTableAssociation = "configuredTableAssociation" + keyProtectedQuery = "protectedQuery" + keyProtectedJob = "protectedJob" + keyIDMappingTable = "idMappingTable" + keyAnalysisRule = "analysisRule" +) + +// Path segment count constants (mnd). +const ( + segsRoot = 1 // just the resource name + segsWithID = 2 // resource + ID + segsWithSub = 3 // resource + ID + sub + segsWithSubID = 4 // resource + ID + sub + subID + segsWithSubSub = 5 // 5 segments + segsWithSubSubID = 6 // 6 segments +) + +const ( + cleanroomsHostPrefix = "cleanrooms." + + opBatchGetCollaborationAnalysisTemplate = "BatchGetCollaborationAnalysisTemplate" + opBatchGetSchema = "BatchGetSchema" + opBatchGetSchemaAnalysisRule = "BatchGetSchemaAnalysisRule" + opCreateAnalysisTemplate = "CreateAnalysisTemplate" + opCreateCollaboration = "CreateCollaboration" + opCreateCollaborationChangeRequest = "CreateCollaborationChangeRequest" + opCreateConfiguredAudienceModelAssociation = "CreateConfiguredAudienceModelAssociation" + opCreateConfiguredTable = "CreateConfiguredTable" + opCreateConfiguredTableAnalysisRule = "CreateConfiguredTableAnalysisRule" + opCreateConfiguredTableAssociation = "CreateConfiguredTableAssociation" + opCreateConfiguredTableAssociationAnalysisRule = "CreateConfiguredTableAssociationAnalysisRule" + opCreateIDMappingTable = "CreateIdMappingTable" + opCreateIDNamespaceAssociation = "CreateIdNamespaceAssociation" + opCreateMembership = "CreateMembership" + opCreatePrivacyBudgetTemplate = "CreatePrivacyBudgetTemplate" + opDeleteAnalysisTemplate = "DeleteAnalysisTemplate" + opDeleteCollaboration = "DeleteCollaboration" + opDeleteConfiguredAudienceModelAssociation = "DeleteConfiguredAudienceModelAssociation" + opDeleteConfiguredTable = "DeleteConfiguredTable" + opDeleteConfiguredTableAnalysisRule = "DeleteConfiguredTableAnalysisRule" + opDeleteConfiguredTableAssociation = "DeleteConfiguredTableAssociation" + opDeleteConfiguredTableAssociationAnalysisRule = "DeleteConfiguredTableAssociationAnalysisRule" + opDeleteIDMappingTable = "DeleteIdMappingTable" + opDeleteIDNamespaceAssociation = "DeleteIdNamespaceAssociation" + opDeleteMember = "DeleteMember" + opDeleteMembership = "DeleteMembership" + opDeletePrivacyBudgetTemplate = "DeletePrivacyBudgetTemplate" + opGetAnalysisTemplate = "GetAnalysisTemplate" + opGetCollaboration = "GetCollaboration" + opGetCollaborationAnalysisTemplate = "GetCollaborationAnalysisTemplate" + opGetCollaborationChangeRequest = "GetCollaborationChangeRequest" + opGetCollaborationConfiguredAudienceModelAssociation = "GetCollaborationConfiguredAudienceModelAssociation" + opGetCollaborationIDNamespaceAssociation = "GetCollaborationIdNamespaceAssociation" + opGetCollaborationPrivacyBudgetTemplate = "GetCollaborationPrivacyBudgetTemplate" + opGetConfiguredAudienceModelAssociation = "GetConfiguredAudienceModelAssociation" + opGetConfiguredTable = "GetConfiguredTable" + opGetConfiguredTableAnalysisRule = "GetConfiguredTableAnalysisRule" + opGetConfiguredTableAssociation = "GetConfiguredTableAssociation" + opGetConfiguredTableAssociationAnalysisRule = "GetConfiguredTableAssociationAnalysisRule" + opGetIDMappingTable = "GetIdMappingTable" + opGetIDNamespaceAssociation = "GetIdNamespaceAssociation" + opGetMembership = "GetMembership" + opGetPrivacyBudgetTemplate = "GetPrivacyBudgetTemplate" + opGetProtectedJob = "GetProtectedJob" + opGetProtectedQuery = "GetProtectedQuery" + opGetSchema = "GetSchema" + opGetSchemaAnalysisRule = "GetSchemaAnalysisRule" + opListAnalysisTemplates = "ListAnalysisTemplates" + opListCollaborationAnalysisTemplates = "ListCollaborationAnalysisTemplates" + opListCollaborationChangeRequests = "ListCollaborationChangeRequests" + opListCollaborationConfiguredAudienceModelAssociations = "ListCollaborationConfiguredAudienceModelAssociations" + opListCollaborationIDNamespaceAssociations = "ListCollaborationIdNamespaceAssociations" + opListCollaborationPrivacyBudgets = "ListCollaborationPrivacyBudgets" + opListCollaborationPrivacyBudgetTemplates = "ListCollaborationPrivacyBudgetTemplates" + opListCollaborations = "ListCollaborations" + opListConfiguredAudienceModelAssociations = "ListConfiguredAudienceModelAssociations" + opListConfiguredTableAssociations = "ListConfiguredTableAssociations" + opListConfiguredTables = "ListConfiguredTables" + opListIDMappingTables = "ListIdMappingTables" + opListIDNamespaceAssociations = "ListIdNamespaceAssociations" + opListMembers = "ListMembers" + opListMemberships = "ListMemberships" + opListPrivacyBudgets = "ListPrivacyBudgets" + opListPrivacyBudgetTemplates = "ListPrivacyBudgetTemplates" + opListProtectedJobs = "ListProtectedJobs" + opListProtectedQueries = "ListProtectedQueries" + opListSchemas = "ListSchemas" + opListTagsForResource = "ListTagsForResource" + opPopulateIDMappingTable = "PopulateIdMappingTable" + opPreviewPrivacyImpact = "PreviewPrivacyImpact" + opStartProtectedJob = "StartProtectedJob" + opStartProtectedQuery = "StartProtectedQuery" + opTagResource = "TagResource" + opUntagResource = "UntagResource" + opUpdateAnalysisTemplate = "UpdateAnalysisTemplate" + opUpdateCollaboration = "UpdateCollaboration" + opUpdateCollaborationChangeRequest = "UpdateCollaborationChangeRequest" + opUpdateConfiguredAudienceModelAssociation = "UpdateConfiguredAudienceModelAssociation" + opUpdateConfiguredTable = "UpdateConfiguredTable" + opUpdateConfiguredTableAnalysisRule = "UpdateConfiguredTableAnalysisRule" + opUpdateConfiguredTableAssociation = "UpdateConfiguredTableAssociation" + opUpdateConfiguredTableAssociationAnalysisRule = "UpdateConfiguredTableAssociationAnalysisRule" + opUpdateIDMappingTable = "UpdateIdMappingTable" + opUpdateIDNamespaceAssociation = "UpdateIdNamespaceAssociation" + opUpdateMembership = "UpdateMembership" + opUpdatePrivacyBudgetTemplate = "UpdatePrivacyBudgetTemplate" + opUpdateProtectedJob = "UpdateProtectedJob" + opUpdateProtectedQuery = "UpdateProtectedQuery" + opUnknown = "" +) + +var errUnknownAction = errors.New("unknown action") + +// Handler handles AWS Clean Rooms HTTP requests. +type Handler struct { + Backend StorageBackend + AccountID string + Region string +} + +// NewHandler creates a new Clean Rooms handler. +func NewHandler(backend StorageBackend) *Handler { + return &Handler{ + Backend: backend, + AccountID: backend.AccountID(), + Region: backend.Region(), + } +} + +func (h *Handler) Name() string { return "CleanRooms" } +func (h *Handler) Reset() { h.Backend.Reset() } +func (h *Handler) StartWorker(_ context.Context) error { return nil } + +func (h *Handler) GetSupportedOperations() []string { + return []string{ + opBatchGetCollaborationAnalysisTemplate, + opBatchGetSchema, + opBatchGetSchemaAnalysisRule, + opCreateAnalysisTemplate, + opCreateCollaboration, + opCreateCollaborationChangeRequest, + opCreateConfiguredAudienceModelAssociation, + opCreateConfiguredTable, + opCreateConfiguredTableAnalysisRule, + opCreateConfiguredTableAssociation, + opCreateConfiguredTableAssociationAnalysisRule, + opCreateIDMappingTable, + opCreateIDNamespaceAssociation, + opCreateMembership, + opCreatePrivacyBudgetTemplate, + opDeleteAnalysisTemplate, + opDeleteCollaboration, + opDeleteConfiguredAudienceModelAssociation, + opDeleteConfiguredTable, + opDeleteConfiguredTableAnalysisRule, + opDeleteConfiguredTableAssociation, + opDeleteConfiguredTableAssociationAnalysisRule, + opDeleteIDMappingTable, + opDeleteIDNamespaceAssociation, + opDeleteMember, + opDeleteMembership, + opDeletePrivacyBudgetTemplate, + opGetAnalysisTemplate, + opGetCollaboration, + opGetCollaborationAnalysisTemplate, + opGetCollaborationChangeRequest, + opGetCollaborationConfiguredAudienceModelAssociation, + opGetCollaborationIDNamespaceAssociation, + opGetCollaborationPrivacyBudgetTemplate, + opGetConfiguredAudienceModelAssociation, + opGetConfiguredTable, + opGetConfiguredTableAnalysisRule, + opGetConfiguredTableAssociation, + opGetConfiguredTableAssociationAnalysisRule, + opGetIDMappingTable, + opGetIDNamespaceAssociation, + opGetMembership, + opGetPrivacyBudgetTemplate, + opGetProtectedJob, + opGetProtectedQuery, + opGetSchema, + opGetSchemaAnalysisRule, + opListAnalysisTemplates, + opListCollaborationAnalysisTemplates, + opListCollaborationChangeRequests, + opListCollaborationConfiguredAudienceModelAssociations, + opListCollaborationIDNamespaceAssociations, + opListCollaborationPrivacyBudgets, + opListCollaborationPrivacyBudgetTemplates, + opListCollaborations, + opListConfiguredAudienceModelAssociations, + opListConfiguredTableAssociations, + opListConfiguredTables, + opListIDMappingTables, + opListIDNamespaceAssociations, + opListMembers, + opListMemberships, + opListPrivacyBudgets, + opListPrivacyBudgetTemplates, + opListProtectedJobs, + opListProtectedQueries, + opListSchemas, + opListTagsForResource, + opPopulateIDMappingTable, + opPreviewPrivacyImpact, + opStartProtectedJob, + opStartProtectedQuery, + opTagResource, + opUntagResource, + opUpdateAnalysisTemplate, + opUpdateCollaboration, + opUpdateCollaborationChangeRequest, + opUpdateConfiguredAudienceModelAssociation, + opUpdateConfiguredTable, + opUpdateConfiguredTableAnalysisRule, + opUpdateConfiguredTableAssociation, + opUpdateConfiguredTableAssociationAnalysisRule, + opUpdateIDMappingTable, + opUpdateIDNamespaceAssociation, + opUpdateMembership, + opUpdatePrivacyBudgetTemplate, + opUpdateProtectedJob, + opUpdateProtectedQuery, + } +} + +func (h *Handler) RouteMatcher() service.Matcher { + return func(c *echo.Context) bool { + host := c.Request().Host + path := c.Request().URL.Path + + return strings.HasPrefix(host, cleanroomsHostPrefix) || + strings.HasPrefix(path, "/collaborations") || + strings.HasPrefix(path, "/configuredTables") || + strings.HasPrefix(path, "/memberships") || + strings.HasPrefix(path, "/tags/") + } +} + +func (h *Handler) MatchPriority() int { return service.PriorityPathVersioned } + +func (h *Handler) ExtractOperation(c *echo.Context) string { + op, _ := classifyPath(c.Request().Method, c.Request().URL.Path) + + return op +} + +func (h *Handler) ExtractResource(c *echo.Context) string { + _, resource := classifyPath(c.Request().Method, c.Request().URL.Path) + + return resource +} + +func (h *Handler) Handler() echo.HandlerFunc { + return func(c *echo.Context) error { + ctx := c.Request().Context() + log := logger.Load(ctx) + + op, _ := classifyPath(c.Request().Method, c.Request().URL.Path) + if op == opUnknown { + return c.String(http.StatusNotFound, "not found") + } + + body, err := httputils.ReadBody(c.Request()) + if err != nil { + log.ErrorContext(ctx, "cleanrooms: failed to read request body", "error", err) + + return c.String(http.StatusInternalServerError, "internal server error") + } + + // Inject path parameters into body for handlers. + body = injectPathParams(c.Request().URL.Path, op, body) + + result, dispErr := h.dispatch(ctx, op, body, c) + if dispErr != nil { + return h.handleError(c, dispErr) + } + if result == nil { + return c.JSON(http.StatusOK, map[string]any{}) + } + + return c.JSONBlob(http.StatusOK, result) + } +} + +func (h *Handler) handleError(c *echo.Context, err error) error { + type errResp struct { + Message string `json:"message"` + } + switch { + case errors.Is(err, ErrNotFound): + return c.JSON(http.StatusNotFound, errResp{err.Error()}) + case errors.Is(err, ErrAlreadyExists): + return c.JSON(http.StatusConflict, errResp{err.Error()}) + case errors.Is(err, ErrValidation): + return c.JSON(http.StatusBadRequest, errResp{err.Error()}) + default: + return c.JSON(http.StatusInternalServerError, errResp{err.Error()}) + } +} + +// classifyPath maps (method, path) to an operation name and primary resource. +func classifyPath(method, path string) (string, string) { + // Trim leading slash and split + path = strings.TrimPrefix(path, "/") + segs := strings.Split(path, "/") + if len(segs) == 0 { + return opUnknown, "" + } + + root := segs[0] + + switch root { + case "collaborations": + return classifyCollaborations(method, segs) + case "configuredTables": + return classifyConfiguredTables(method, segs) + case "memberships": + return classifyMemberships(method, segs) + case "tags": + return classifyTags(method, segs) + } + + return opUnknown, "" +} + +func classifyCollaborations(method string, segs []string) (string, string) { + // /collaborations + if len(segs) == segsRoot { + switch method { + case http.MethodPost: + return opCreateCollaboration, "" + case http.MethodGet: + return opListCollaborations, "" + } + } + // /collaborations/{id} + if len(segs) == segsWithID { + id := segs[1] + switch method { + case http.MethodGet: + return opGetCollaboration, id + case http.MethodDelete: + return opDeleteCollaboration, id + case http.MethodPatch: + return opUpdateCollaboration, id + } + } + // /collaborations/{id}/{sub}[/...] + if len(segs) >= segsWithSub { + id := segs[1] + sub := segs[2] + + return classifyCollaboration(method, id, sub, segs) + } + + return opUnknown, "" +} + +// classifyCollaboration handles sub-resource routing for /collaborations/{id}/{sub}[/...]. +func classifyCollaboration(method, id, sub string, segs []string) (string, string) { + switch sub { + case subAnalysisTemplates: + return classifyCollabAnalysisTemplates(method, id, segs) + case "batch-analysistemplates", "batch-schema", "batch-schema-analysis-rule": + return classifyCollabBatchPost(method, id, sub) + case "changeRequests": + return classifyCollabChangeRequests(method, id, segs) + case subCAMAAssociations: + return classifyCollabCAMAAssocs(method, id, segs) + case subIDNamespaceAssocs: + return classifyCollabIDNamespaceAssocs(method, id, segs) + case "member": + return classifyCollabMember(method, id, segs) + case "members": + if method == http.MethodGet { + return opListMembers, id + } + case subPrivacyBudgetTmpls: + return classifyCollabPrivacyBudgetTmpls(method, id, segs) + case "privacybudgets": + if method == http.MethodGet { + return opListCollaborationPrivacyBudgets, id + } + case subSchemas: + return classifyCollabSchemas(method, id, segs) + } + + return opUnknown, "" +} + +func classifyCollabBatchPost(method, id, sub string) (string, string) { + if method != http.MethodPost { + return opUnknown, "" + } + switch sub { + case "batch-analysistemplates": + return opBatchGetCollaborationAnalysisTemplate, id + case "batch-schema": + return opBatchGetSchema, id + case "batch-schema-analysis-rule": + return opBatchGetSchemaAnalysisRule, id + } + + return opUnknown, "" +} + +func classifyCollabMember(method, id string, segs []string) (string, string) { + // /collaborations/{id}/member/{accountId} + if len(segs) == segsWithSubID && method == http.MethodDelete { + return opDeleteMember, id + } + + return opUnknown, "" +} + +func classifyCollabAnalysisTemplates(method, id string, segs []string) (string, string) { + if len(segs) == segsWithSub && method == http.MethodGet { + return opListCollaborationAnalysisTemplates, id + } + if len(segs) == segsWithSubID && method == http.MethodGet { + return opGetCollaborationAnalysisTemplate, id + } + + return opUnknown, "" +} + +func classifyCollabChangeRequests(method, id string, segs []string) (string, string) { + if len(segs) == segsWithSub { + switch method { + case http.MethodPost: + return opCreateCollaborationChangeRequest, id + case http.MethodGet: + return opListCollaborationChangeRequests, id + } + } + if len(segs) == segsWithSubID { + switch method { + case http.MethodGet: + return opGetCollaborationChangeRequest, id + case http.MethodPatch: + return opUpdateCollaborationChangeRequest, id + } + } + + return opUnknown, "" +} + +func classifyCollabCAMAAssocs(method, id string, segs []string) (string, string) { + if len(segs) == segsWithSub && method == http.MethodGet { + return opListCollaborationConfiguredAudienceModelAssociations, id + } + if len(segs) == segsWithSubID && method == http.MethodGet { + return opGetCollaborationConfiguredAudienceModelAssociation, id + } + + return opUnknown, "" +} + +func classifyCollabIDNamespaceAssocs(method, id string, segs []string) (string, string) { + if len(segs) == segsWithSub && method == http.MethodGet { + return opListCollaborationIDNamespaceAssociations, id + } + if len(segs) == segsWithSubID && method == http.MethodGet { + return opGetCollaborationIDNamespaceAssociation, id + } + + return opUnknown, "" +} + +func classifyCollabPrivacyBudgetTmpls(method, id string, segs []string) (string, string) { + if len(segs) == segsWithSub && method == http.MethodGet { + return opListCollaborationPrivacyBudgetTemplates, id + } + if len(segs) == segsWithSubID && method == http.MethodGet { + return opGetCollaborationPrivacyBudgetTemplate, id + } + + return opUnknown, "" +} + +func classifyCollabSchemas(method, id string, segs []string) (string, string) { + if len(segs) == segsWithSub && method == http.MethodGet { + return opListSchemas, id + } + if len(segs) == segsWithSubID && method == http.MethodGet { + return opGetSchema, id + } + // /collaborations/{id}/schemas/{name}/analysisRule/{type} + if len(segs) == segsWithSubSubID && segs[4] == subAnalysisRule && method == http.MethodGet { + return opGetSchemaAnalysisRule, id + } + + return opUnknown, "" +} + +func classifyConfiguredTables(method string, segs []string) (string, string) { + // /configuredTables + if len(segs) == segsRoot { + switch method { + case http.MethodPost: + return opCreateConfiguredTable, "" + case http.MethodGet: + return opListConfiguredTables, "" + } + } + // /configuredTables/{id} + if len(segs) == segsWithID { + id := segs[1] + switch method { + case http.MethodGet: + return opGetConfiguredTable, id + case http.MethodDelete: + return opDeleteConfiguredTable, id + case http.MethodPatch: + return opUpdateConfiguredTable, id + } + } + // /configuredTables/{id}/analysisRule[/{type}] + if len(segs) >= segsWithSub && segs[2] == subAnalysisRule { + return classifyConfiguredTableAnalysisRule(method, segs) + } + + return opUnknown, "" +} + +func classifyConfiguredTableAnalysisRule(method string, segs []string) (string, string) { + id := segs[1] + if len(segs) == segsWithSub && method == http.MethodPost { + return opCreateConfiguredTableAnalysisRule, id + } + if len(segs) == segsWithSubID { + switch method { + case http.MethodGet: + return opGetConfiguredTableAnalysisRule, id + case http.MethodDelete: + return opDeleteConfiguredTableAnalysisRule, id + case http.MethodPatch: + return opUpdateConfiguredTableAnalysisRule, id + } + } + + return opUnknown, "" +} + +func classifyMemberships(method string, segs []string) (string, string) { + // /memberships + if len(segs) == segsRoot { + switch method { + case http.MethodPost: + return opCreateMembership, "" + case http.MethodGet: + return opListMemberships, "" + } + } + // /memberships/{id} + if len(segs) == segsWithID { + id := segs[1] + switch method { + case http.MethodGet: + return opGetMembership, id + case http.MethodDelete: + return opDeleteMembership, id + case http.MethodPatch: + return opUpdateMembership, id + } + } + if len(segs) < segsWithSub { + return opUnknown, "" + } + membershipID := segs[1] + sub := segs[2] + + return classifyMembership(method, membershipID, sub, segs) +} + +// classifyMembership handles sub-resource routing for /memberships/{id}/{sub}[/...]. +func classifyMembership(method, membershipID, sub string, segs []string) (string, string) { + switch sub { + case subAnalysisTemplates: + return classifyMemAnalysisTemplates(method, membershipID, segs) + case "configuredTableAssociations": + return classifyMemCTAssociations(method, membershipID, segs) + case subCAMAAssociations: + return classifyMemCAMAAssocs(method, membershipID, segs) + case "idmappingtables": + return classifyMemIDMappingTables(method, membershipID, segs) + case subIDNamespaceAssocs: + return classifyMemIDNamespaceAssocs(method, membershipID, segs) + case "previewprivacyimpact": + if method == http.MethodPost { + return opPreviewPrivacyImpact, membershipID + } + case "privacybudgets": + if method == http.MethodGet { + return opListPrivacyBudgets, membershipID + } + case subPrivacyBudgetTmpls: + return classifyMemPrivacyBudgetTmpls(method, membershipID, segs) + case subProtectedJobs: + return classifyMemProtectedJobs(method, membershipID, segs) + case subProtectedQueries: + return classifyMemProtectedQueries(method, membershipID, segs) + } + + return opUnknown, "" +} + +func classifyMemAnalysisTemplates(method, membershipID string, segs []string) (string, string) { + if len(segs) == segsWithSub { + switch method { + case http.MethodPost: + return opCreateAnalysisTemplate, membershipID + case http.MethodGet: + return opListAnalysisTemplates, membershipID + } + } + if len(segs) == segsWithSubID { + switch method { + case http.MethodGet: + return opGetAnalysisTemplate, membershipID + case http.MethodDelete: + return opDeleteAnalysisTemplate, membershipID + case http.MethodPatch: + return opUpdateAnalysisTemplate, membershipID + } + } + + return opUnknown, "" +} + +func classifyMemCTAssociations(method, membershipID string, segs []string) (string, string) { + if len(segs) == segsWithSub { + switch method { + case http.MethodPost: + return opCreateConfiguredTableAssociation, membershipID + case http.MethodGet: + return opListConfiguredTableAssociations, membershipID + } + } + if len(segs) == segsWithSubID { + switch method { + case http.MethodGet: + return opGetConfiguredTableAssociation, membershipID + case http.MethodDelete: + return opDeleteConfiguredTableAssociation, membershipID + case http.MethodPatch: + return opUpdateConfiguredTableAssociation, membershipID + } + } + if len(segs) >= segsWithSubSub && segs[4] == subAnalysisRule { + return classifyMemCTAssocAnalysisRule(method, membershipID, segs) + } + + return opUnknown, "" +} + +func classifyMemCTAssocAnalysisRule(method, membershipID string, segs []string) (string, string) { + // /memberships/{id}/configuredTableAssociations/{assocId}/analysisRule + if len(segs) == segsWithSubSub && method == http.MethodPost { + return opCreateConfiguredTableAssociationAnalysisRule, membershipID + } + // /memberships/{id}/configuredTableAssociations/{assocId}/analysisRule/{type} + if len(segs) == segsWithSubSubID { + switch method { + case http.MethodGet: + return opGetConfiguredTableAssociationAnalysisRule, membershipID + case http.MethodDelete: + return opDeleteConfiguredTableAssociationAnalysisRule, membershipID + case http.MethodPatch: + return opUpdateConfiguredTableAssociationAnalysisRule, membershipID + } + } + + return opUnknown, "" +} + +func classifyMemCAMAAssocs(method, membershipID string, segs []string) (string, string) { + if len(segs) == segsWithSub { + switch method { + case http.MethodPost: + return opCreateConfiguredAudienceModelAssociation, membershipID + case http.MethodGet: + return opListConfiguredAudienceModelAssociations, membershipID + } + } + if len(segs) == segsWithSubID { + switch method { + case http.MethodGet: + return opGetConfiguredAudienceModelAssociation, membershipID + case http.MethodDelete: + return opDeleteConfiguredAudienceModelAssociation, membershipID + case http.MethodPatch: + return opUpdateConfiguredAudienceModelAssociation, membershipID + } + } + + return opUnknown, "" +} + +func classifyMemIDMappingTables(method, membershipID string, segs []string) (string, string) { + if len(segs) == segsWithSub { + switch method { + case http.MethodPost: + return opCreateIDMappingTable, membershipID + case http.MethodGet: + return opListIDMappingTables, membershipID + } + } + if len(segs) == segsWithSubID { + switch method { + case http.MethodGet: + return opGetIDMappingTable, membershipID + case http.MethodDelete: + return opDeleteIDMappingTable, membershipID + case http.MethodPatch: + return opUpdateIDMappingTable, membershipID + } + } + // /memberships/{id}/idmappingtables/{tableId}/populate + if len(segs) == segsWithSubSub && segs[4] == "populate" && method == http.MethodPost { + return opPopulateIDMappingTable, membershipID + } + + return opUnknown, "" +} + +func classifyMemIDNamespaceAssocs(method, membershipID string, segs []string) (string, string) { + if len(segs) == segsWithSub { + switch method { + case http.MethodPost: + return opCreateIDNamespaceAssociation, membershipID + case http.MethodGet: + return opListIDNamespaceAssociations, membershipID + } + } + if len(segs) == segsWithSubID { + switch method { + case http.MethodGet: + return opGetIDNamespaceAssociation, membershipID + case http.MethodDelete: + return opDeleteIDNamespaceAssociation, membershipID + case http.MethodPatch: + return opUpdateIDNamespaceAssociation, membershipID + } + } + + return opUnknown, "" +} + +func classifyMemPrivacyBudgetTmpls(method, membershipID string, segs []string) (string, string) { + if len(segs) == segsWithSub { + switch method { + case http.MethodPost: + return opCreatePrivacyBudgetTemplate, membershipID + case http.MethodGet: + return opListPrivacyBudgetTemplates, membershipID + } + } + if len(segs) == segsWithSubID { + switch method { + case http.MethodGet: + return opGetPrivacyBudgetTemplate, membershipID + case http.MethodDelete: + return opDeletePrivacyBudgetTemplate, membershipID + case http.MethodPatch: + return opUpdatePrivacyBudgetTemplate, membershipID + } + } + + return opUnknown, "" +} + +func classifyMemProtectedJobs(method, membershipID string, segs []string) (string, string) { + if len(segs) == segsWithSub { + switch method { + case http.MethodPost: + return opStartProtectedJob, membershipID + case http.MethodGet: + return opListProtectedJobs, membershipID + } + } + if len(segs) == segsWithSubID { + switch method { + case http.MethodGet: + return opGetProtectedJob, membershipID + case http.MethodPatch: + return opUpdateProtectedJob, membershipID + } + } + + return opUnknown, "" +} + +func classifyMemProtectedQueries(method, membershipID string, segs []string) (string, string) { + if len(segs) == segsWithSub { + switch method { + case http.MethodPost: + return opStartProtectedQuery, membershipID + case http.MethodGet: + return opListProtectedQueries, membershipID + } + } + if len(segs) == segsWithSubID { + switch method { + case http.MethodGet: + return opGetProtectedQuery, membershipID + case http.MethodPatch: + return opUpdateProtectedQuery, membershipID + } + } + + return opUnknown, "" +} + +func classifyTags(method string, segs []string) (string, string) { + if len(segs) < segsWithID { + return opUnknown, "" + } + resourceArn := strings.Join(segs[1:], "/") + switch method { + case http.MethodGet: + return opListTagsForResource, resourceArn + case http.MethodPost: + return opTagResource, resourceArn + case http.MethodDelete: + return opUntagResource, resourceArn + } + + return opUnknown, "" +} + +// injectPathParams merges URL path segments into the request body JSON. +func injectPathParams(path, _ string, body []byte) []byte { + path = strings.TrimPrefix(path, "/") + segs := strings.Split(path, "/") + + var m map[string]json.RawMessage + if len(body) > 0 { + _ = json.Unmarshal(body, &m) + } + if m == nil { + m = make(map[string]json.RawMessage) + } + + setStr := func(key, val string) { + if val != "" { + b, _ := json.Marshal(val) + m[key] = b + } + } + + switch { + case len(segs) >= segsWithID && segs[0] == "collaborations": + injectCollaborationParams(segs, setStr) + case len(segs) >= segsWithID && segs[0] == "configuredTables": + setStr("configuredTableIdentifier", segs[1]) + if len(segs) == segsWithSubID && segs[2] == subAnalysisRule { + setStr("analysisRuleType", segs[3]) + } + case len(segs) >= segsWithID && segs[0] == "memberships": + injectMembershipParams(segs, setStr) + case len(segs) >= segsWithID && segs[0] == subTags: + arnVal := strings.Join(segs[1:], "/") + setStr("resourceArn", arnVal) + } + + out, _ := json.Marshal(m) + + return out +} + +// injectCollaborationParams injects path parameters for /collaborations/... routes. +func injectCollaborationParams(segs []string, setStr func(string, string)) { + setStr("collaborationIdentifier", segs[1]) + if len(segs) >= segsWithSubID { + switch segs[2] { + case subAnalysisTemplates: + setStr("analysisTemplateArn", segs[3]) + case "changeRequests": + setStr("changeRequestIdentifier", segs[3]) + case subCAMAAssociations: + setStr("configuredAudienceModelAssociationIdentifier", segs[3]) + case subIDNamespaceAssocs: + setStr("idNamespaceAssociationIdentifier", segs[3]) + case "member": + setStr("accountId", segs[3]) + case subPrivacyBudgetTmpls: + setStr("privacyBudgetTemplateIdentifier", segs[3]) + case subSchemas: + setStr("name", segs[3]) + if len(segs) == segsWithSubSubID && segs[4] == subAnalysisRule { + setStr("type", segs[5]) + } + } + } +} + +// injectMembershipParams injects path parameters for /memberships/... routes. +func injectMembershipParams(segs []string, setStr func(string, string)) { + setStr("membershipIdentifier", segs[1]) + if len(segs) >= segsWithSubID { + switch segs[2] { + case subAnalysisTemplates: + setStr("analysisTemplateIdentifier", segs[3]) + case "configuredTableAssociations": + setStr("configuredTableAssociationIdentifier", segs[3]) + if len(segs) == segsWithSubSubID && segs[4] == subAnalysisRule { + setStr("analysisRuleType", segs[5]) + } + case subCAMAAssociations: + setStr("configuredAudienceModelAssociationIdentifier", segs[3]) + case "idmappingtables": + setStr("idMappingTableIdentifier", segs[3]) + case subIDNamespaceAssocs: + setStr("idNamespaceAssociationIdentifier", segs[3]) + case subPrivacyBudgetTmpls: + setStr("privacyBudgetTemplateIdentifier", segs[3]) + case subProtectedJobs: + setStr("protectedJobIdentifier", segs[3]) + case subProtectedQueries: + setStr("protectedQueryIdentifier", segs[3]) + } + } +} + +// ---- dispatch ---- + +// opHandlerFn is the unified type for operation handlers. +type opHandlerFn func(ctx context.Context, body []byte, c *echo.Context) ([]byte, error) + +// buildOpHandlers returns a map from operation name to handler function. +func (h *Handler) buildOpHandlers(_ *echo.Context) map[string]opHandlerFn { + out := h.buildCollaborationHandlers() + maps.Copy(out, h.buildMembershipHandlers()) + maps.Copy(out, h.buildConfiguredTableHandlers()) + maps.Copy(out, h.buildResourceHandlers()) + + return out +} + +func (h *Handler) buildCollaborationHandlers() map[string]opHandlerFn { + return map[string]opHandlerFn{ + // Collaboration + opCreateCollaboration: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreateCollaboration(ctx, body) + }, + opGetCollaboration: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetCollaboration(ctx, body) + }, + opListCollaborations: func(ctx context.Context, _ []byte, ec *echo.Context) ([]byte, error) { + return h.handleListCollaborations(ctx, ec) + }, + opUpdateCollaboration: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateCollaboration(ctx, body) + }, + opDeleteCollaboration: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeleteCollaboration(ctx, body) + }, + opListMembers: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListMembers(ctx, body, ec) + }, + opDeleteMember: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeleteMember(ctx, body) + }, + // Collaboration sub-resources + opGetCollaborationAnalysisTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetCollaborationAnalysisTemplate(ctx, body) + }, + opListCollaborationAnalysisTemplates: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListCollaborationAnalysisTemplates(ctx, body, ec) + }, + opBatchGetCollaborationAnalysisTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleBatchGetCollaborationAnalysisTemplate(ctx, body) + }, + opBatchGetSchema: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleBatchGetSchema(ctx, body) + }, + opBatchGetSchemaAnalysisRule: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleBatchGetSchemaAnalysisRule(ctx, body) + }, + opGetSchema: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetSchema(ctx, body) + }, + opListSchemas: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListSchemas(ctx, body, ec) + }, + opGetSchemaAnalysisRule: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetSchemaAnalysisRule(ctx, body) + }, + opCreateCollaborationChangeRequest: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreateCollaborationChangeRequest(ctx, body) + }, + opGetCollaborationChangeRequest: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetCollaborationChangeRequest(ctx, body) + }, + opListCollaborationChangeRequests: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListCollaborationChangeRequests(ctx, body, ec) + }, + opUpdateCollaborationChangeRequest: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateCollaborationChangeRequest(ctx, body) + }, + opGetCollaborationConfiguredAudienceModelAssociation: func( + ctx context.Context, body []byte, _ *echo.Context, + ) ([]byte, error) { + return h.handleGetCollaborationConfiguredAudienceModelAssociation(ctx, body) + }, + opListCollaborationConfiguredAudienceModelAssociations: func( + ctx context.Context, body []byte, ec *echo.Context, + ) ([]byte, error) { + return h.handleListCollaborationConfiguredAudienceModelAssociations(ctx, body, ec) + }, + opGetCollaborationIDNamespaceAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetCollaborationIDNamespaceAssociation(ctx, body) + }, + opListCollaborationIDNamespaceAssociations: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListCollaborationIDNamespaceAssociations(ctx, body, ec) + }, + opGetCollaborationPrivacyBudgetTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetCollaborationPrivacyBudgetTemplate(ctx, body) + }, + opListCollaborationPrivacyBudgetTemplates: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListCollaborationPrivacyBudgetTemplates(ctx, body, ec) + }, + opListCollaborationPrivacyBudgets: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListCollaborationPrivacyBudgets(ctx, body, ec) + }, + } +} + +func (h *Handler) buildMembershipHandlers() map[string]opHandlerFn { + return map[string]opHandlerFn{ + opCreateMembership: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreateMembership(ctx, body) + }, + opGetMembership: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetMembership(ctx, body) + }, + opListMemberships: func(ctx context.Context, _ []byte, ec *echo.Context) ([]byte, error) { + return h.handleListMemberships(ctx, ec) + }, + opUpdateMembership: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateMembership(ctx, body) + }, + opDeleteMembership: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeleteMembership(ctx, body) + }, + opCreateAnalysisTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreateAnalysisTemplate(ctx, body) + }, + opGetAnalysisTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetAnalysisTemplate(ctx, body) + }, + opListAnalysisTemplates: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListAnalysisTemplates(ctx, body, ec) + }, + opUpdateAnalysisTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateAnalysisTemplate(ctx, body) + }, + opDeleteAnalysisTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeleteAnalysisTemplate(ctx, body) + }, + opStartProtectedQuery: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleStartProtectedQuery(ctx, body) + }, + opGetProtectedQuery: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetProtectedQuery(ctx, body) + }, + opListProtectedQueries: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListProtectedQueries(ctx, body, ec) + }, + opUpdateProtectedQuery: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateProtectedQuery(ctx, body) + }, + opStartProtectedJob: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleStartProtectedJob(ctx, body) + }, + opGetProtectedJob: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetProtectedJob(ctx, body) + }, + opListProtectedJobs: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListProtectedJobs(ctx, body, ec) + }, + opUpdateProtectedJob: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateProtectedJob(ctx, body) + }, + } +} + +func (h *Handler) buildConfiguredTableHandlers() map[string]opHandlerFn { + return map[string]opHandlerFn{ + opCreateConfiguredTable: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreateConfiguredTable(ctx, body) + }, + opGetConfiguredTable: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetConfiguredTable(ctx, body) + }, + opListConfiguredTables: func(ctx context.Context, _ []byte, ec *echo.Context) ([]byte, error) { + return h.handleListConfiguredTables(ctx, ec) + }, + opUpdateConfiguredTable: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateConfiguredTable(ctx, body) + }, + opDeleteConfiguredTable: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeleteConfiguredTable(ctx, body) + }, + opCreateConfiguredTableAnalysisRule: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreateConfiguredTableAnalysisRule(ctx, body) + }, + opGetConfiguredTableAnalysisRule: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetConfiguredTableAnalysisRule(ctx, body) + }, + opUpdateConfiguredTableAnalysisRule: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateConfiguredTableAnalysisRule(ctx, body) + }, + opDeleteConfiguredTableAnalysisRule: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeleteConfiguredTableAnalysisRule(ctx, body) + }, + opCreateConfiguredTableAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreateConfiguredTableAssociation(ctx, body) + }, + opGetConfiguredTableAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetConfiguredTableAssociation(ctx, body) + }, + opListConfiguredTableAssociations: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListConfiguredTableAssociations(ctx, body, ec) + }, + opUpdateConfiguredTableAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateConfiguredTableAssociation(ctx, body) + }, + opDeleteConfiguredTableAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeleteConfiguredTableAssociation(ctx, body) + }, + opCreateConfiguredTableAssociationAnalysisRule: func( + ctx context.Context, body []byte, _ *echo.Context, + ) ([]byte, error) { + return h.handleCreateConfiguredTableAssociationAnalysisRule(ctx, body) + }, + opGetConfiguredTableAssociationAnalysisRule: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetConfiguredTableAssociationAnalysisRule(ctx, body) + }, + opUpdateConfiguredTableAssociationAnalysisRule: func( + ctx context.Context, body []byte, _ *echo.Context, + ) ([]byte, error) { + return h.handleUpdateConfiguredTableAssociationAnalysisRule(ctx, body) + }, + opDeleteConfiguredTableAssociationAnalysisRule: func( + ctx context.Context, body []byte, _ *echo.Context, + ) ([]byte, error) { + return h.handleDeleteConfiguredTableAssociationAnalysisRule(ctx, body) + }, + } +} + +func (h *Handler) buildResourceHandlers() map[string]opHandlerFn { + return map[string]opHandlerFn{ + // IDMappingTable + opCreateIDMappingTable: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreateIDMappingTable(ctx, body) + }, + opGetIDMappingTable: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetIDMappingTable(ctx, body) + }, + opListIDMappingTables: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListIDMappingTables(ctx, body, ec) + }, + opUpdateIDMappingTable: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateIDMappingTable(ctx, body) + }, + opDeleteIDMappingTable: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeleteIDMappingTable(ctx, body) + }, + opPopulateIDMappingTable: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handlePopulateIDMappingTable(ctx, body) + }, + // IDNamespaceAssociation + opCreateIDNamespaceAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreateIDNamespaceAssociation(ctx, body) + }, + opGetIDNamespaceAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetIDNamespaceAssociation(ctx, body) + }, + opListIDNamespaceAssociations: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListIDNamespaceAssociations(ctx, body, ec) + }, + opUpdateIDNamespaceAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateIDNamespaceAssociation(ctx, body) + }, + opDeleteIDNamespaceAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeleteIDNamespaceAssociation(ctx, body) + }, + // ConfiguredAudienceModelAssociation + opCreateConfiguredAudienceModelAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreateConfiguredAudienceModelAssociation(ctx, body) + }, + opGetConfiguredAudienceModelAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetConfiguredAudienceModelAssociation(ctx, body) + }, + opListConfiguredAudienceModelAssociations: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListConfiguredAudienceModelAssociations(ctx, body, ec) + }, + opUpdateConfiguredAudienceModelAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdateConfiguredAudienceModelAssociation(ctx, body) + }, + opDeleteConfiguredAudienceModelAssociation: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeleteConfiguredAudienceModelAssociation(ctx, body) + }, + // PrivacyBudget + opCreatePrivacyBudgetTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleCreatePrivacyBudgetTemplate(ctx, body) + }, + opGetPrivacyBudgetTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleGetPrivacyBudgetTemplate(ctx, body) + }, + opListPrivacyBudgetTemplates: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListPrivacyBudgetTemplates(ctx, body, ec) + }, + opUpdatePrivacyBudgetTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleUpdatePrivacyBudgetTemplate(ctx, body) + }, + opDeletePrivacyBudgetTemplate: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleDeletePrivacyBudgetTemplate(ctx, body) + }, + opListPrivacyBudgets: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleListPrivacyBudgets(ctx, body, ec) + }, + opPreviewPrivacyImpact: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handlePreviewPrivacyImpact(ctx, body) + }, + // Tags + opListTagsForResource: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleListTagsForResource(ctx, body) + }, + opTagResource: func(ctx context.Context, body []byte, _ *echo.Context) ([]byte, error) { + return h.handleTagResource(ctx, body) + }, + opUntagResource: func(ctx context.Context, body []byte, ec *echo.Context) ([]byte, error) { + return h.handleUntagResource(ctx, body, ec) + }, + } +} + +func (h *Handler) dispatch( + ctx context.Context, + op string, + body []byte, + c *echo.Context, +) ([]byte, error) { + handlers := h.buildOpHandlers(c) + if fn, ok := handlers[op]; ok { + return fn(ctx, body, c) + } + + return nil, errUnknownAction +} + +// ---- handler helpers ---- + +func mustJSON(v any) []byte { + b, _ := json.Marshal(v) + + return b +} + +func qp(c *echo.Context, key string) string { + return c.QueryParam(key) +} + +// ---- Collaboration handlers ---- + +func (h *Handler) handleCreateCollaboration(_ context.Context, body []byte) ([]byte, error) { + var req struct { + Tags map[string]string `json:"tags"` + Name string `json:"name"` + Description string `json:"description"` + CreatorDisplayName string `json:"creatorDisplayName"` + QueryLogStatus string `json:"queryLogStatus"` + CreatorMemberAbilities []string `json:"creatorMemberAbilities"` + Members []MemberSpec `json:"members"` + } + _ = json.Unmarshal(body, &req) + c, err := h.Backend.CreateCollaboration( + req.Name, + req.Description, + req.CreatorDisplayName, + req.CreatorMemberAbilities, + req.Members, + req.QueryLogStatus, + req.Tags, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyCollaboration: c}), nil +} + +func (h *Handler) handleGetCollaboration(_ context.Context, body []byte) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + } + _ = json.Unmarshal(body, &req) + c, err := h.Backend.GetCollaboration(req.CollaborationIdentifier) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyCollaboration: c}), nil +} + +func (h *Handler) handleListCollaborations( + _ context.Context, + c *echo.Context, +) ([]byte, error) { + items, next := h.Backend.ListCollaborations( + qp(c, "memberStatus"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + resp := map[string]any{"collaborationList": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateCollaboration(_ context.Context, body []byte) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + } + _ = json.Unmarshal(body, &req) + col, err := h.Backend.UpdateCollaboration( + req.CollaborationIdentifier, + req.Name, + req.Description, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyCollaboration: col}), nil +} + +func (h *Handler) handleDeleteCollaboration(_ context.Context, body []byte) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteCollaboration(req.CollaborationIdentifier) +} + +func (h *Handler) handleListMembers( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListMembers( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"memberList": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleDeleteMember(_ context.Context, body []byte) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + AccountID string `json:"accountId"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteMember(req.CollaborationIdentifier, req.AccountID) +} + +func (h *Handler) handleGetCollaborationAnalysisTemplate( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + AnalysisTemplateArn string `json:"analysisTemplateArn"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.GetCollaborationAnalysisTemplate( + req.CollaborationIdentifier, + req.AnalysisTemplateArn, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyAnalysisTemplate: t}), nil +} + +func (h *Handler) handleListCollaborationAnalysisTemplates( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListCollaborationAnalysisTemplates( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"analysisTemplateSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleBatchGetCollaborationAnalysisTemplate( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + AnalysisTemplateArns []string `json:"analysisTemplateArns"` + } + _ = json.Unmarshal(body, &req) + items, errs, err := h.Backend.BatchGetCollaborationAnalysisTemplate( + req.CollaborationIdentifier, + req.AnalysisTemplateArns, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{"analysisTemplates": items, keyErrors: errs}), nil +} + +func (h *Handler) handleBatchGetSchema(_ context.Context, body []byte) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + Names []string `json:"names"` + } + _ = json.Unmarshal(body, &req) + items, errs, err := h.Backend.BatchGetSchema(req.CollaborationIdentifier, req.Names) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{"schemas": items, keyErrors: errs}), nil +} + +func (h *Handler) handleBatchGetSchemaAnalysisRule(_ context.Context, body []byte) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + SchemaAnalysisRuleRequests []struct { + Name string `json:"name"` + Type string `json:"type"` + } `json:"schemaAnalysisRuleRequests"` + } + _ = json.Unmarshal(body, &req) + names := make([]string, 0, len(req.SchemaAnalysisRuleRequests)) + var ruleType string + for _, r := range req.SchemaAnalysisRuleRequests { + names = append(names, r.Name) + if ruleType == "" { + ruleType = r.Type + } + } + items, errs, err := h.Backend.BatchGetSchemaAnalysisRule( + req.CollaborationIdentifier, + names, + ruleType, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{"analysisRules": items, keyErrors: errs}), nil +} + +func (h *Handler) handleGetSchema(_ context.Context, body []byte) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + Name string `json:"name"` + } + _ = json.Unmarshal(body, &req) + s, err := h.Backend.GetSchema(req.CollaborationIdentifier, req.Name) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{"schema": s}), nil +} + +func (h *Handler) handleListSchemas( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListSchemas( + req.CollaborationIdentifier, + qp(c, "schemaType"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"schemaSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleGetSchemaAnalysisRule(_ context.Context, body []byte) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + Name string `json:"name"` + Type string `json:"type"` + } + _ = json.Unmarshal(body, &req) + r, err := h.Backend.GetSchemaAnalysisRule(req.CollaborationIdentifier, req.Name, req.Type) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{subAnalysisRule: r}), nil +} + +func (h *Handler) handleCreateCollaborationChangeRequest( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + Details map[string]any `json:"details"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + Type string `json:"type"` + } + _ = json.Unmarshal(body, &req) + r, err := h.Backend.CreateCollaborationChangeRequest( + req.CollaborationIdentifier, + req.Type, + req.Details, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyCollaborationChangeRequest: r}), nil +} + +func (h *Handler) handleGetCollaborationChangeRequest( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + ChangeRequestIdentifier string `json:"changeRequestIdentifier"` + } + _ = json.Unmarshal(body, &req) + r, err := h.Backend.GetCollaborationChangeRequest( + req.CollaborationIdentifier, + req.ChangeRequestIdentifier, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyCollaborationChangeRequest: r}), nil +} + +func (h *Handler) handleListCollaborationChangeRequests( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListCollaborationChangeRequests( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"collaborationChangeRequests": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateCollaborationChangeRequest( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + ChangeRequestIdentifier string `json:"changeRequestIdentifier"` + Status string `json:"status"` + } + _ = json.Unmarshal(body, &req) + r, err := h.Backend.UpdateCollaborationChangeRequest( + req.CollaborationIdentifier, + req.ChangeRequestIdentifier, + req.Status, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyCollaborationChangeRequest: r}), nil +} + +func (h *Handler) handleGetCollaborationConfiguredAudienceModelAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + ConfiguredAudienceModelAssociationIdentifier string `json:"configuredAudienceModelAssociationIdentifier"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.GetCollaborationConfiguredAudienceModelAssociation( + req.CollaborationIdentifier, + req.ConfiguredAudienceModelAssociationIdentifier, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyCAMAAssociation: a}), nil +} + +func (h *Handler) handleListCollaborationConfiguredAudienceModelAssociations( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListCollaborationConfiguredAudienceModelAssociations( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"configuredAudienceModelAssociationSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleGetCollaborationIDNamespaceAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + IDNamespaceAssociationIdentifier string `json:"idNamespaceAssociationIdentifier"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.GetCollaborationIDNamespaceAssociation( + req.CollaborationIdentifier, + req.IDNamespaceAssociationIdentifier, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyIDNamespaceAssociation: a}), nil +} + +func (h *Handler) handleListCollaborationIDNamespaceAssociations( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListCollaborationIDNamespaceAssociations( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"idNamespaceAssociationSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleGetCollaborationPrivacyBudgetTemplate( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + PrivacyBudgetTemplateIdentifier string `json:"privacyBudgetTemplateIdentifier"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.GetCollaborationPrivacyBudgetTemplate( + req.CollaborationIdentifier, + req.PrivacyBudgetTemplateIdentifier, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyPrivacyBudgetTemplate: t}), nil +} + +func (h *Handler) handleListCollaborationPrivacyBudgetTemplates( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListCollaborationPrivacyBudgetTemplates( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"privacyBudgetTemplateSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleListCollaborationPrivacyBudgets( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListCollaborationPrivacyBudgets( + req.CollaborationIdentifier, + qp(c, "privacyBudgetType"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"privacyBudgetSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +// ---- Membership handlers ---- + +func (h *Handler) handleCreateMembership(_ context.Context, body []byte) ([]byte, error) { + var req struct { + DefaultResultConfiguration map[string]any `json:"defaultResultConfiguration"` + PaymentConfiguration map[string]any `json:"paymentConfiguration"` + Tags map[string]string `json:"tags"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + QueryLogStatus string `json:"queryLogStatus"` + } + _ = json.Unmarshal(body, &req) + m, err := h.Backend.CreateMembership( + req.CollaborationIdentifier, + req.QueryLogStatus, + req.DefaultResultConfiguration, + req.PaymentConfiguration, + req.Tags, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyMembership: m}), nil +} + +func (h *Handler) handleGetMembership(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + m, err := h.Backend.GetMembership(req.MembershipIdentifier) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyMembership: m}), nil +} + +func (h *Handler) handleListMemberships( + _ context.Context, + c *echo.Context, +) ([]byte, error) { + items, next := h.Backend.ListMemberships( + qp(c, "status"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + resp := map[string]any{"membershipSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateMembership(_ context.Context, body []byte) ([]byte, error) { + var req struct { + DefaultResultConfiguration map[string]any `json:"defaultResultConfiguration"` + MembershipIdentifier string `json:"membershipIdentifier"` + QueryLogStatus string `json:"queryLogStatus"` + } + _ = json.Unmarshal(body, &req) + m, err := h.Backend.UpdateMembership( + req.MembershipIdentifier, + req.QueryLogStatus, + req.DefaultResultConfiguration, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyMembership: m}), nil +} + +func (h *Handler) handleDeleteMembership(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteMembership(req.MembershipIdentifier) +} + +// ---- ConfiguredTable handlers ---- + +func (h *Handler) handleCreateConfiguredTable(_ context.Context, body []byte) ([]byte, error) { + var req struct { + TableReference map[string]any `json:"tableReference"` + Tags map[string]string `json:"tags"` + Name string `json:"name"` + Description string `json:"description"` + AnalysisMethod string `json:"analysisMethod"` + AllowedColumns []string `json:"allowedColumns"` + } + _ = json.Unmarshal(body, &req) + ct, err := h.Backend.CreateConfiguredTable( + req.Name, + req.Description, + req.TableReference, + req.AllowedColumns, + req.AnalysisMethod, + req.Tags, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyConfiguredTable: ct}), nil +} + +func (h *Handler) handleGetConfiguredTable(_ context.Context, body []byte) ([]byte, error) { + var req struct { + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + } + _ = json.Unmarshal(body, &req) + ct, err := h.Backend.GetConfiguredTable(req.ConfiguredTableIdentifier) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyConfiguredTable: ct}), nil +} + +func (h *Handler) handleListConfiguredTables( + _ context.Context, + c *echo.Context, +) ([]byte, error) { + items, next := h.Backend.ListConfiguredTables(qp(c, "maxResults"), qp(c, "nextToken")) + resp := map[string]any{"configuredTableSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateConfiguredTable(_ context.Context, body []byte) ([]byte, error) { + var req struct { + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + } + _ = json.Unmarshal(body, &req) + ct, err := h.Backend.UpdateConfiguredTable( + req.ConfiguredTableIdentifier, + req.Name, + req.Description, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyConfiguredTable: ct}), nil +} + +func (h *Handler) handleDeleteConfiguredTable(_ context.Context, body []byte) ([]byte, error) { + var req struct { + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteConfiguredTable(req.ConfiguredTableIdentifier) +} + +// ---- ConfiguredTableAnalysisRule handlers ---- + +func (h *Handler) handleCreateConfiguredTableAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + } + _ = json.Unmarshal(body, &req) + r, err := h.Backend.CreateConfiguredTableAnalysisRule( + req.ConfiguredTableIdentifier, + req.AnalysisRuleType, + req.AnalysisRulePolicy, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{subAnalysisRule: r}), nil +} + +func (h *Handler) handleGetConfiguredTableAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + } + _ = json.Unmarshal(body, &req) + r, err := h.Backend.GetConfiguredTableAnalysisRule( + req.ConfiguredTableIdentifier, + req.AnalysisRuleType, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{subAnalysisRule: r}), nil +} + +func (h *Handler) handleUpdateConfiguredTableAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + } + _ = json.Unmarshal(body, &req) + r, err := h.Backend.UpdateConfiguredTableAnalysisRule( + req.ConfiguredTableIdentifier, + req.AnalysisRuleType, + req.AnalysisRulePolicy, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{subAnalysisRule: r}), nil +} + +func (h *Handler) handleDeleteConfiguredTableAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteConfiguredTableAnalysisRule( + req.ConfiguredTableIdentifier, + req.AnalysisRuleType, + ) +} + +// ---- ConfiguredTableAssociation handlers ---- + +func (h *Handler) handleCreateConfiguredTableAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + Tags map[string]string `json:"tags"` + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + RoleArn string `json:"roleArn"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.CreateConfiguredTableAssociation( + req.MembershipIdentifier, + req.Name, + req.Description, + req.ConfiguredTableIdentifier, + req.RoleArn, + req.Tags, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyConfiguredTableAssociation: a}), nil +} + +func (h *Handler) handleGetConfiguredTableAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.GetConfiguredTableAssociation( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyConfiguredTableAssociation: a}), nil +} + +func (h *Handler) handleListConfiguredTableAssociations( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListConfiguredTableAssociations( + req.MembershipIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"configuredTableAssociationSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateConfiguredTableAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + Description string `json:"description"` + RoleArn string `json:"roleArn"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.UpdateConfiguredTableAssociation( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + req.Description, + req.RoleArn, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyConfiguredTableAssociation: a}), nil +} + +func (h *Handler) handleDeleteConfiguredTableAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteConfiguredTableAssociation( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + ) +} + +// ---- ConfiguredTableAssociationAnalysisRule handlers ---- + +func (h *Handler) handleCreateConfiguredTableAssociationAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + } + _ = json.Unmarshal(body, &req) + r, err := h.Backend.CreateConfiguredTableAssociationAnalysisRule( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + req.AnalysisRuleType, + req.AnalysisRulePolicy, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{subAnalysisRule: r}), nil +} + +func (h *Handler) handleGetConfiguredTableAssociationAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + } + _ = json.Unmarshal(body, &req) + r, err := h.Backend.GetConfiguredTableAssociationAnalysisRule( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + req.AnalysisRuleType, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{subAnalysisRule: r}), nil +} + +func (h *Handler) handleUpdateConfiguredTableAssociationAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + } + _ = json.Unmarshal(body, &req) + r, err := h.Backend.UpdateConfiguredTableAssociationAnalysisRule( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + req.AnalysisRuleType, + req.AnalysisRulePolicy, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{subAnalysisRule: r}), nil +} + +func (h *Handler) handleDeleteConfiguredTableAssociationAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteConfiguredTableAssociationAnalysisRule( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + req.AnalysisRuleType, + ) +} + +// ---- AnalysisTemplate handlers ---- + +func (h *Handler) handleCreateAnalysisTemplate(_ context.Context, body []byte) ([]byte, error) { + var req struct { + Source map[string]any `json:"source"` + Tags map[string]string `json:"tags"` + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + Format string `json:"format"` + AnalysisParameters []map[string]any `json:"analysisParameters"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.CreateAnalysisTemplate( + req.MembershipIdentifier, + req.Name, + req.Description, + req.Format, + req.Source, + req.AnalysisParameters, + req.Tags, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyAnalysisTemplate: t}), nil +} + +func (h *Handler) handleGetAnalysisTemplate(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + AnalysisTemplateIdentifier string `json:"analysisTemplateIdentifier"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.GetAnalysisTemplate( + req.MembershipIdentifier, + req.AnalysisTemplateIdentifier, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyAnalysisTemplate: t}), nil +} + +func (h *Handler) handleListAnalysisTemplates( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListAnalysisTemplates( + req.MembershipIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"analysisTemplateSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateAnalysisTemplate(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + AnalysisTemplateIdentifier string `json:"analysisTemplateIdentifier"` + Description string `json:"description"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.UpdateAnalysisTemplate( + req.MembershipIdentifier, + req.AnalysisTemplateIdentifier, + req.Description, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyAnalysisTemplate: t}), nil +} + +func (h *Handler) handleDeleteAnalysisTemplate(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + AnalysisTemplateIdentifier string `json:"analysisTemplateIdentifier"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteAnalysisTemplate( + req.MembershipIdentifier, + req.AnalysisTemplateIdentifier, + ) +} + +// ---- ProtectedQuery handlers ---- + +func (h *Handler) handleStartProtectedQuery(_ context.Context, body []byte) ([]byte, error) { + var req struct { + SQLParameters map[string]any `json:"sqlParameters"` + ResultConfiguration map[string]any `json:"resultConfiguration"` + ComputeConfiguration map[string]any `json:"computeConfiguration"` + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + var sqlText string + if req.SQLParameters != nil { + if v, ok := req.SQLParameters["queryString"].(string); ok { + sqlText = v + } + } + q, err := h.Backend.StartProtectedQuery( + req.MembershipIdentifier, + sqlText, + req.ResultConfiguration, + req.ComputeConfiguration, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyProtectedQuery: q}), nil +} + +func (h *Handler) handleGetProtectedQuery(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ProtectedQueryIdentifier string `json:"protectedQueryIdentifier"` + } + _ = json.Unmarshal(body, &req) + q, err := h.Backend.GetProtectedQuery(req.MembershipIdentifier, req.ProtectedQueryIdentifier) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyProtectedQuery: q}), nil +} + +func (h *Handler) handleListProtectedQueries( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListProtectedQueries( + req.MembershipIdentifier, + qp(c, "status"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"protectedQueries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateProtectedQuery(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ProtectedQueryIdentifier string `json:"protectedQueryIdentifier"` + TargetStatus string `json:"targetStatus"` + } + _ = json.Unmarshal(body, &req) + q, err := h.Backend.UpdateProtectedQuery( + req.MembershipIdentifier, + req.ProtectedQueryIdentifier, + req.TargetStatus, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyProtectedQuery: q}), nil +} + +// ---- ProtectedJob handlers ---- + +func (h *Handler) handleStartProtectedJob(_ context.Context, body []byte) ([]byte, error) { + var req struct { + JobParameters map[string]any `json:"jobParameters"` + ResultConfiguration map[string]any `json:"resultConfiguration"` + MembershipIdentifier string `json:"membershipIdentifier"` + Type string `json:"type"` + } + _ = json.Unmarshal(body, &req) + j, err := h.Backend.StartProtectedJob( + req.MembershipIdentifier, + req.Type, + req.JobParameters, + req.ResultConfiguration, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyProtectedJob: j}), nil +} + +func (h *Handler) handleGetProtectedJob(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ProtectedJobIdentifier string `json:"protectedJobIdentifier"` + } + _ = json.Unmarshal(body, &req) + j, err := h.Backend.GetProtectedJob(req.MembershipIdentifier, req.ProtectedJobIdentifier) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyProtectedJob: j}), nil +} + +func (h *Handler) handleListProtectedJobs( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListProtectedJobs( + req.MembershipIdentifier, + qp(c, "status"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"protectedJobs": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateProtectedJob(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ProtectedJobIdentifier string `json:"protectedJobIdentifier"` + TargetStatus string `json:"targetStatus"` + } + _ = json.Unmarshal(body, &req) + j, err := h.Backend.UpdateProtectedJob( + req.MembershipIdentifier, + req.ProtectedJobIdentifier, + req.TargetStatus, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyProtectedJob: j}), nil +} + +// ---- PrivacyBudgetTemplate handlers ---- + +func (h *Handler) handleCreatePrivacyBudgetTemplate( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + Parameters map[string]any `json:"parameters"` + Tags map[string]string `json:"tags"` + MembershipIdentifier string `json:"membershipIdentifier"` + PrivacyBudgetType string `json:"privacyBudgetType"` + AutoRefresh string `json:"autoRefresh"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.CreatePrivacyBudgetTemplate( + req.MembershipIdentifier, + req.PrivacyBudgetType, + req.AutoRefresh, + req.Parameters, + req.Tags, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyPrivacyBudgetTemplate: t}), nil +} + +func (h *Handler) handleGetPrivacyBudgetTemplate(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + PrivacyBudgetTemplateIdentifier string `json:"privacyBudgetTemplateIdentifier"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.GetPrivacyBudgetTemplate( + req.MembershipIdentifier, + req.PrivacyBudgetTemplateIdentifier, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyPrivacyBudgetTemplate: t}), nil +} + +func (h *Handler) handleListPrivacyBudgetTemplates( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListPrivacyBudgetTemplates( + req.MembershipIdentifier, + qp(c, "privacyBudgetType"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"privacyBudgetTemplateSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdatePrivacyBudgetTemplate( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + Parameters map[string]any `json:"parameters"` + MembershipIdentifier string `json:"membershipIdentifier"` + PrivacyBudgetTemplateIdentifier string `json:"privacyBudgetTemplateIdentifier"` + AutoRefresh string `json:"autoRefresh"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.UpdatePrivacyBudgetTemplate( + req.MembershipIdentifier, + req.PrivacyBudgetTemplateIdentifier, + req.AutoRefresh, + req.Parameters, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyPrivacyBudgetTemplate: t}), nil +} + +func (h *Handler) handleDeletePrivacyBudgetTemplate( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + PrivacyBudgetTemplateIdentifier string `json:"privacyBudgetTemplateIdentifier"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeletePrivacyBudgetTemplate( + req.MembershipIdentifier, + req.PrivacyBudgetTemplateIdentifier, + ) +} + +func (h *Handler) handleListPrivacyBudgets( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListPrivacyBudgets( + req.MembershipIdentifier, + qp(c, "privacyBudgetType"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"privacyBudgetSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handlePreviewPrivacyImpact(_ context.Context, body []byte) ([]byte, error) { + var req struct { + Parameters map[string]any `json:"parameters"` + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + result, err := h.Backend.PreviewPrivacyImpact(req.MembershipIdentifier, req.Parameters) + if err != nil { + return nil, err + } + + return mustJSON(result), nil +} + +// ---- IdMappingTable handlers ---- + +func (h *Handler) handleCreateIDMappingTable(_ context.Context, body []byte) ([]byte, error) { + var req struct { + InputReferenceConfig map[string]any `json:"inputReferenceConfig"` + Tags map[string]string `json:"tags"` + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + KmsKeyArn string `json:"kmsKeyArn"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.CreateIDMappingTable( + req.MembershipIdentifier, + req.Name, + req.Description, + req.InputReferenceConfig, + req.KmsKeyArn, + req.Tags, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyIDMappingTable: t}), nil +} + +func (h *Handler) handleGetIDMappingTable(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + IDMappingTableIdentifier string `json:"idMappingTableIdentifier"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.GetIDMappingTable(req.MembershipIdentifier, req.IDMappingTableIdentifier) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyIDMappingTable: t}), nil +} + +func (h *Handler) handleListIDMappingTables( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListIDMappingTables( + req.MembershipIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"idMappingTableSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateIDMappingTable(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + IDMappingTableIdentifier string `json:"idMappingTableIdentifier"` + Description string `json:"description"` + KmsKeyArn string `json:"kmsKeyArn"` + } + _ = json.Unmarshal(body, &req) + t, err := h.Backend.UpdateIDMappingTable( + req.MembershipIdentifier, + req.IDMappingTableIdentifier, + req.Description, + req.KmsKeyArn, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyIDMappingTable: t}), nil +} + +func (h *Handler) handleDeleteIDMappingTable(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + IDMappingTableIdentifier string `json:"idMappingTableIdentifier"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteIDMappingTable( + req.MembershipIdentifier, + req.IDMappingTableIdentifier, + ) +} + +func (h *Handler) handlePopulateIDMappingTable(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + IDMappingTableIdentifier string `json:"idMappingTableIdentifier"` + } + _ = json.Unmarshal(body, &req) + result, err := h.Backend.PopulateIDMappingTable( + req.MembershipIdentifier, + req.IDMappingTableIdentifier, + ) + if err != nil { + return nil, err + } + + return mustJSON(result), nil +} + +// ---- IdNamespaceAssociation handlers ---- + +func (h *Handler) handleCreateIDNamespaceAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + InputReferenceConfig map[string]any `json:"inputReferenceConfig"` + IDMappingConfig map[string]any `json:"idMappingConfig"` + Tags map[string]string `json:"tags"` + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.CreateIDNamespaceAssociation( + req.MembershipIdentifier, + req.Name, + req.Description, + req.InputReferenceConfig, + req.IDMappingConfig, + req.Tags, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyIDNamespaceAssociation: a}), nil +} + +func (h *Handler) handleGetIDNamespaceAssociation(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + IDNamespaceAssociationIdentifier string `json:"idNamespaceAssociationIdentifier"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.GetIDNamespaceAssociation( + req.MembershipIdentifier, + req.IDNamespaceAssociationIdentifier, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyIDNamespaceAssociation: a}), nil +} + +func (h *Handler) handleListIDNamespaceAssociations( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListIDNamespaceAssociations( + req.MembershipIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"idNamespaceAssociationSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateIDNamespaceAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + IDMappingConfig map[string]any `json:"idMappingConfig"` + MembershipIdentifier string `json:"membershipIdentifier"` + IDNamespaceAssociationIdentifier string `json:"idNamespaceAssociationIdentifier"` + Description string `json:"description"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.UpdateIDNamespaceAssociation( + req.MembershipIdentifier, + req.IDNamespaceAssociationIdentifier, + req.Description, + req.IDMappingConfig, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyIDNamespaceAssociation: a}), nil +} + +func (h *Handler) handleDeleteIDNamespaceAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + IDNamespaceAssociationIdentifier string `json:"idNamespaceAssociationIdentifier"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteIDNamespaceAssociation( + req.MembershipIdentifier, + req.IDNamespaceAssociationIdentifier, + ) +} + +// ---- ConfiguredAudienceModelAssociation handlers ---- + +func (h *Handler) handleCreateConfiguredAudienceModelAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + Tags map[string]string `json:"tags"` + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredAudienceModelArn string `json:"configuredAudienceModelArn"` + Name string `json:"name"` + Description string `json:"description"` + ManageResourcePolicies bool `json:"manageResourcePolicies"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.CreateConfiguredAudienceModelAssociation( + req.MembershipIdentifier, + req.ConfiguredAudienceModelArn, + req.Name, + req.Description, + req.ManageResourcePolicies, + req.Tags, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyCAMAAssociation: a}), nil +} + +func (h *Handler) handleGetConfiguredAudienceModelAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredAudienceModelAssociationIdentifier string `json:"configuredAudienceModelAssociationIdentifier"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.GetConfiguredAudienceModelAssociation( + req.MembershipIdentifier, + req.ConfiguredAudienceModelAssociationIdentifier, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyCAMAAssociation: a}), nil +} + +func (h *Handler) handleListConfiguredAudienceModelAssociations( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + } + _ = json.Unmarshal(body, &req) + items, next, err := h.Backend.ListConfiguredAudienceModelAssociations( + req.MembershipIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) + if err != nil { + return nil, err + } + resp := map[string]any{"configuredAudienceModelAssociationSummaries": items} + if next != "" { + resp["nextToken"] = next + } + + return mustJSON(resp), nil +} + +func (h *Handler) handleUpdateConfiguredAudienceModelAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredAudienceModelAssociationIdentifier string `json:"configuredAudienceModelAssociationIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + } + _ = json.Unmarshal(body, &req) + a, err := h.Backend.UpdateConfiguredAudienceModelAssociation( + req.MembershipIdentifier, + req.ConfiguredAudienceModelAssociationIdentifier, + req.Name, + req.Description, + ) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{keyCAMAAssociation: a}), nil +} + +func (h *Handler) handleDeleteConfiguredAudienceModelAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredAudienceModelAssociationIdentifier string `json:"configuredAudienceModelAssociationIdentifier"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.DeleteConfiguredAudienceModelAssociation( + req.MembershipIdentifier, + req.ConfiguredAudienceModelAssociationIdentifier, + ) +} + +// ---- Tag handlers ---- + +func (h *Handler) handleListTagsForResource(_ context.Context, body []byte) ([]byte, error) { + var req struct { + ResourceArn string `json:"resourceArn"` + } + _ = json.Unmarshal(body, &req) + tags, err := h.Backend.ListTagsForResource(req.ResourceArn) + if err != nil { + return nil, err + } + + return mustJSON(map[string]any{"tags": tags}), nil +} + +func (h *Handler) handleTagResource(_ context.Context, body []byte) ([]byte, error) { + var req struct { + Tags map[string]string `json:"tags"` + ResourceArn string `json:"resourceArn"` + } + _ = json.Unmarshal(body, &req) + + return nil, h.Backend.TagResource(req.ResourceArn, req.Tags) +} + +func (h *Handler) handleUntagResource( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { + var req struct { + ResourceArn string `json:"resourceArn"` + TagKeys []string `json:"tagKeys"` + } + _ = json.Unmarshal(body, &req) + // tagKeys can also come from query params + if len(req.TagKeys) == 0 { + req.TagKeys = c.Request().URL.Query()["tagKeys"] + } + + return nil, h.Backend.UntagResource(req.ResourceArn, req.TagKeys) +} diff --git a/services/cleanrooms/handler_test.go b/services/cleanrooms/handler_test.go new file mode 100644 index 000000000..5a4f3c6c1 --- /dev/null +++ b/services/cleanrooms/handler_test.go @@ -0,0 +1,285 @@ +package cleanrooms_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/labstack/echo/v5" + + "github.com/blackbirdworks/gopherstack/services/cleanrooms" +) + +func newTestServer(t *testing.T) *echo.Echo { + t.Helper() + backend := cleanrooms.NewInMemoryBackend("123456789012", "us-east-1") + h := cleanrooms.NewHandler(backend) + e := echo.New() + e.Any("/*", h.Handler()) + + return e +} + +func doRequest( + t *testing.T, + e *echo.Echo, + method, path string, + body any, +) *httptest.ResponseRecorder { + t.Helper() + var reqBody []byte + if body != nil { + var err error + reqBody, err = json.Marshal(body) + if err != nil { + t.Fatalf("marshal request: %v", err) + } + } + req := httptest.NewRequest(method, path, bytes.NewReader(reqBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + return rec +} + +func TestCollaborationCRUD(t *testing.T) { + t.Parallel() + + e := newTestServer(t) + + createBody := map[string]any{ + "name": "test-collab", + "description": "desc", + "creatorDisplayName": "Alice", + "creatorMemberAbilities": []string{"CAN_QUERY"}, + "members": []any{}, + "queryLogStatus": "ENABLED", + } + + // Create collaboration + rec := doRequest(t, e, http.MethodPost, "/collaborations", createBody) + if rec.Code != http.StatusOK { + t.Fatalf("create: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } + var createResp map[string]any + if err := json.NewDecoder(rec.Body).Decode(&createResp); err != nil { + t.Fatalf("decode create: %v", err) + } + if _, ok := createResp["collaboration"]; !ok { + t.Fatalf("missing key %q in response: %v", "collaboration", createResp) + } + collabID := createResp["collaboration"].(map[string]any)["collaborationIdentifier"].(string) + + // List collaborations + rec = doRequest(t, e, http.MethodGet, "/collaborations", nil) + if rec.Code != http.StatusOK { + t.Fatalf("list: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } + var listResp map[string]any + if err := json.NewDecoder(rec.Body).Decode(&listResp); err != nil { + t.Fatalf("decode list: %v", err) + } + if _, ok := listResp["collaborationList"]; !ok { + t.Fatalf("missing key %q in response: %v", "collaborationList", listResp) + } + + // Get collaboration + rec = doRequest(t, e, http.MethodGet, "/collaborations/"+collabID, nil) + if rec.Code != http.StatusOK { + t.Fatalf("get: status %d: %s", rec.Code, rec.Body.String()) + } + + // Delete collaboration + rec = doRequest(t, e, http.MethodDelete, "/collaborations/"+collabID, nil) + if rec.Code != http.StatusOK { + t.Fatalf("delete: status %d: %s", rec.Code, rec.Body.String()) + } + + // Get deleted collaboration returns 404 + rec = doRequest(t, e, http.MethodGet, "/collaborations/"+collabID, nil) + if rec.Code != http.StatusNotFound { + t.Fatalf("get deleted: status %d want 404: %s", rec.Code, rec.Body.String()) + } +} + +func TestConfiguredTableCRUD(t *testing.T) { + t.Parallel() + + e := newTestServer(t) + + createBody := map[string]any{ + "name": "my-table", + "description": "desc", + "tableReference": map[string]any{ + "glue": map[string]any{"databaseName": "db", "tableName": "tbl"}, + }, + "allowedColumns": []string{"col1"}, + "analysisMethod": "DIRECT_QUERY", + } + + // Create configured table + rec := doRequest(t, e, http.MethodPost, "/configuredTables", createBody) + if rec.Code != http.StatusOK { + t.Fatalf("create: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } + var createResp map[string]any + if err := json.NewDecoder(rec.Body).Decode(&createResp); err != nil { + t.Fatalf("decode: %v", err) + } + ctID := createResp["configuredTable"].(map[string]any)["configuredTableIdentifier"].(string) + + // List configured tables + rec = doRequest(t, e, http.MethodGet, "/configuredTables", nil) + if rec.Code != http.StatusOK { + t.Fatalf("list: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } + + // Update configured table + rec = doRequest( + t, + e, + http.MethodPatch, + "/configuredTables/"+ctID, + map[string]any{"name": "new-name"}, + ) + if rec.Code != http.StatusOK { + t.Fatalf("update: status %d: %s", rec.Code, rec.Body.String()) + } + + // Delete configured table + rec = doRequest(t, e, http.MethodDelete, "/configuredTables/"+ctID, nil) + if rec.Code != http.StatusOK { + t.Fatalf("delete: status %d: %s", rec.Code, rec.Body.String()) + } +} + +func TestMembershipCRUD(t *testing.T) { + t.Parallel() + + e := newTestServer(t) + + colRec := doRequest(t, e, http.MethodPost, "/collaborations", map[string]any{ + "name": "c1", + "description": "d", + "creatorDisplayName": "Bob", + "creatorMemberAbilities": []string{}, + "members": []any{}, + "queryLogStatus": "DISABLED", + }) + if colRec.Code != http.StatusOK { + t.Fatalf("create collab: %s", colRec.Body.String()) + } + var colResp map[string]any + _ = json.NewDecoder(colRec.Body).Decode(&colResp) + colID := colResp["collaboration"].(map[string]any)["collaborationIdentifier"].(string) + + createBody := map[string]any{ + "collaborationIdentifier": colID, + "queryLogStatus": "DISABLED", + } + + // Create membership + rec := doRequest(t, e, http.MethodPost, "/memberships", createBody) + if rec.Code != http.StatusOK { + t.Fatalf("create: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } + var createResp map[string]any + _ = json.NewDecoder(rec.Body).Decode(&createResp) + mID := createResp["membership"].(map[string]any)["membershipIdentifier"].(string) + + // List memberships + rec = doRequest(t, e, http.MethodGet, "/memberships", nil) + if rec.Code != http.StatusOK { + t.Fatalf("list: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } + + // Get membership + rec = doRequest(t, e, http.MethodGet, "/memberships/"+mID, nil) + if rec.Code != http.StatusOK { + t.Fatalf("get: status %d: %s", rec.Code, rec.Body.String()) + } + + // Delete membership + rec = doRequest(t, e, http.MethodDelete, "/memberships/"+mID, nil) + if rec.Code != http.StatusOK { + t.Fatalf("delete: status %d: %s", rec.Code, rec.Body.String()) + } +} + +func TestTagOperations(t *testing.T) { + t.Parallel() + + e := newTestServer(t) + + const testARN = "arn:aws:cleanrooms:us-east-1:123456789012:collaboration/abc123" + + // Tag resource + rec := doRequest( + t, + e, + http.MethodPost, + "/tags/"+testARN, + map[string]any{"tags": map[string]string{"env": "test"}}, + ) + if rec.Code != http.StatusOK { + t.Fatalf("tag: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } + + // List tags + rec = doRequest(t, e, http.MethodGet, "/tags/"+testARN, nil) + if rec.Code != http.StatusOK { + t.Fatalf("list tags: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } + + // Untag resource + rec = doRequest(t, e, http.MethodDelete, "/tags/"+testARN+"?tagKeys=env", nil) + if rec.Code != http.StatusOK { + t.Fatalf("untag: status %d want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) + } +} + +func TestProtectedQueryLifecycle(t *testing.T) { + t.Parallel() + + e := newTestServer(t) + + // Create collaboration + colRec := doRequest(t, e, http.MethodPost, "/collaborations", map[string]any{ + "name": "c2", "description": "d", "creatorDisplayName": "Carol", + "creatorMemberAbilities": []string{}, "members": []any{}, "queryLogStatus": "DISABLED", + }) + var colResp map[string]any + _ = json.NewDecoder(colRec.Body).Decode(&colResp) + colID := colResp["collaboration"].(map[string]any)["collaborationIdentifier"].(string) + + // Create membership + memRec := doRequest(t, e, http.MethodPost, "/memberships", + map[string]any{"collaborationIdentifier": colID, "queryLogStatus": "DISABLED"}) + var memResp map[string]any + _ = json.NewDecoder(memRec.Body).Decode(&memResp) + mID := memResp["membership"].(map[string]any)["membershipIdentifier"].(string) + + // Start protected query + rec := doRequest(t, e, http.MethodPost, "/memberships/"+mID+"/protectedQueries", + map[string]any{ + "sqlParameters": map[string]any{"queryString": "SELECT 1"}, + "resultConfiguration": map[string]any{}, + }) + if rec.Code != http.StatusOK { + t.Fatalf("start query: status %d: %s", rec.Code, rec.Body.String()) + } + var resp map[string]any + _ = json.NewDecoder(rec.Body).Decode(&resp) + if _, ok := resp["protectedQuery"]; !ok { + t.Fatal("missing protectedQuery in response") + } + + // List protected queries + rec = doRequest(t, e, http.MethodGet, "/memberships/"+mID+"/protectedQueries", nil) + if rec.Code != http.StatusOK { + t.Fatalf("list queries: status %d: %s", rec.Code, rec.Body.String()) + } +} diff --git a/services/cleanrooms/interfaces.go b/services/cleanrooms/interfaces.go new file mode 100644 index 000000000..6e8ba2821 --- /dev/null +++ b/services/cleanrooms/interfaces.go @@ -0,0 +1,274 @@ +package cleanrooms + +// StorageBackend defines the interface for all Clean Rooms backend operations. +type StorageBackend interface { + Region() string + AccountID() string + Reset() + + // Collaboration operations. + CreateCollaboration( + name, description, creatorDisplayName string, + creatorMemberAbilities []string, + members []MemberSpec, + queryLogStatus string, + tags map[string]string, + ) (*Collaboration, error) + GetCollaboration(id string) (*Collaboration, error) + ListCollaborations(memberStatus, maxResults, nextToken string) ([]*CollaborationSummary, string) + UpdateCollaboration(id, name, description string) (*Collaboration, error) + DeleteCollaboration(id string) error + ListMembers( + collaborationID string, + maxResults, nextToken string, + ) ([]*MemberSummary, string, error) + DeleteMember(collaborationID, accountID string) error + + // Membership operations. + CreateMembership( + collaborationID, queryLogStatus string, + defaultResultConfiguration map[string]any, + paymentConfiguration map[string]any, + tags map[string]string, + ) (*Membership, error) + GetMembership(id string) (*Membership, error) + ListMemberships(status, maxResults, nextToken string) ([]*MembershipSummary, string) + UpdateMembership( + id, queryLogStatus string, + defaultResultConfiguration map[string]any, + ) (*Membership, error) + DeleteMembership(id string) error + + // ConfiguredTable operations. + CreateConfiguredTable( + name, description string, + tableReference map[string]any, + allowedColumns []string, + analysisMethod string, + tags map[string]string, + ) (*ConfiguredTable, error) + GetConfiguredTable(id string) (*ConfiguredTable, error) + ListConfiguredTables(maxResults, nextToken string) ([]*ConfiguredTableSummary, string) + UpdateConfiguredTable(id, name, description string) (*ConfiguredTable, error) + DeleteConfiguredTable(id string) error + + // ConfiguredTableAnalysisRule operations. + CreateConfiguredTableAnalysisRule( + configuredTableID, analysisRuleType string, + policy map[string]any, + ) (*ConfiguredTableAnalysisRule, error) + GetConfiguredTableAnalysisRule( + configuredTableID, analysisRuleType string, + ) (*ConfiguredTableAnalysisRule, error) + UpdateConfiguredTableAnalysisRule( + configuredTableID, analysisRuleType string, + policy map[string]any, + ) (*ConfiguredTableAnalysisRule, error) + DeleteConfiguredTableAnalysisRule(configuredTableID, analysisRuleType string) error + + // ConfiguredTableAssociation operations. + CreateConfiguredTableAssociation( + membershipID, name, description, configuredTableID, roleArn string, + tags map[string]string, + ) (*ConfiguredTableAssociation, error) + GetConfiguredTableAssociation(membershipID, assocID string) (*ConfiguredTableAssociation, error) + ListConfiguredTableAssociations( + membershipID, maxResults, nextToken string, + ) ([]*ConfiguredTableAssociationSummary, string, error) + UpdateConfiguredTableAssociation( + membershipID, assocID, description, roleArn string, + ) (*ConfiguredTableAssociation, error) + DeleteConfiguredTableAssociation(membershipID, assocID string) error + + // ConfiguredTableAssociationAnalysisRule operations. + CreateConfiguredTableAssociationAnalysisRule( + membershipID, assocID, ruleType string, + policy map[string]any, + ) (*ConfiguredTableAssociationAnalysisRule, error) + GetConfiguredTableAssociationAnalysisRule( + membershipID, assocID, ruleType string, + ) (*ConfiguredTableAssociationAnalysisRule, error) + UpdateConfiguredTableAssociationAnalysisRule( + membershipID, assocID, ruleType string, + policy map[string]any, + ) (*ConfiguredTableAssociationAnalysisRule, error) + DeleteConfiguredTableAssociationAnalysisRule(membershipID, assocID, ruleType string) error + + // AnalysisTemplate operations. + CreateAnalysisTemplate( + membershipID, name, description, format string, + source map[string]any, + analysisParameters []map[string]any, + tags map[string]string, + ) (*AnalysisTemplate, error) + GetAnalysisTemplate(membershipID, templateID string) (*AnalysisTemplate, error) + ListAnalysisTemplates( + membershipID, maxResults, nextToken string, + ) ([]*AnalysisTemplateSummary, string, error) + UpdateAnalysisTemplate(membershipID, templateID, description string) (*AnalysisTemplate, error) + DeleteAnalysisTemplate(membershipID, templateID string) error + + // Collaboration AnalysisTemplate operations (read-only views). + GetCollaborationAnalysisTemplate(collaborationID, templateArn string) (*AnalysisTemplate, error) + ListCollaborationAnalysisTemplates( + collaborationID, maxResults, nextToken string, + ) ([]*AnalysisTemplateSummary, string, error) + BatchGetCollaborationAnalysisTemplate( + collaborationID string, + templateArns []string, + ) ([]*AnalysisTemplate, []BatchError, error) + + // Schema operations. + GetSchema(collaborationID, name string) (*Schema, error) + ListSchemas( + collaborationID, schemaType, maxResults, nextToken string, + ) ([]*SchemaSummary, string, error) + BatchGetSchema(collaborationID string, names []string) ([]*Schema, []BatchError, error) + GetSchemaAnalysisRule(collaborationID, name, ruleType string) (*SchemaAnalysisRule, error) + BatchGetSchemaAnalysisRule( + collaborationID string, + names []string, + ruleType string, + ) ([]*SchemaAnalysisRule, []BatchError, error) + + // ProtectedQuery operations. + StartProtectedQuery( + membershipID, sqlText string, + resultConfig map[string]any, + computeConfiguration map[string]any, + ) (*ProtectedQuery, error) + GetProtectedQuery(membershipID, queryID string) (*ProtectedQuery, error) + ListProtectedQueries( + membershipID, status, maxResults, nextToken string, + ) ([]*ProtectedQuerySummary, string, error) + UpdateProtectedQuery(membershipID, queryID, status string) (*ProtectedQuery, error) + + // ProtectedJob operations. + StartProtectedJob( + membershipID, jobType string, + jobParameters map[string]any, + resultConfig map[string]any, + ) (*ProtectedJob, error) + GetProtectedJob(membershipID, jobID string) (*ProtectedJob, error) + ListProtectedJobs( + membershipID, status, maxResults, nextToken string, + ) ([]*ProtectedJobSummary, string, error) + UpdateProtectedJob(membershipID, jobID, status string) (*ProtectedJob, error) + + // PrivacyBudgetTemplate operations. + CreatePrivacyBudgetTemplate( + membershipID, privacyBudgetType, autoRefresh string, + parameters map[string]any, + tags map[string]string, + ) (*PrivacyBudgetTemplate, error) + GetPrivacyBudgetTemplate(membershipID, templateID string) (*PrivacyBudgetTemplate, error) + ListPrivacyBudgetTemplates( + membershipID, privacyBudgetType, maxResults, nextToken string, + ) ([]*PrivacyBudgetTemplateSummary, string, error) + UpdatePrivacyBudgetTemplate( + membershipID, templateID, autoRefresh string, + parameters map[string]any, + ) (*PrivacyBudgetTemplate, error) + DeletePrivacyBudgetTemplate(membershipID, templateID string) error + + // PrivacyBudget operations (read-only). + ListPrivacyBudgets( + membershipID, privacyBudgetType, maxResults, nextToken string, + ) ([]*PrivacyBudget, string, error) + ListCollaborationPrivacyBudgets( + collaborationID, privacyBudgetType, maxResults, nextToken string, + ) ([]*PrivacyBudget, string, error) + GetCollaborationPrivacyBudgetTemplate( + collaborationID, templateID string, + ) (*PrivacyBudgetTemplate, error) + ListCollaborationPrivacyBudgetTemplates( + collaborationID, maxResults, nextToken string, + ) ([]*PrivacyBudgetTemplateSummary, string, error) + PreviewPrivacyImpact(membershipID string, parameters map[string]any) (map[string]any, error) + + // IDMappingTable operations. + CreateIDMappingTable( + membershipID, name, description string, + inputReferenceConfig map[string]any, + kmsKeyArn string, + tags map[string]string, + ) (*IDMappingTable, error) + GetIDMappingTable(membershipID, tableID string) (*IDMappingTable, error) + ListIDMappingTables( + membershipID, maxResults, nextToken string, + ) ([]*IDMappingTableSummary, string, error) + UpdateIDMappingTable( + membershipID, tableID, description, kmsKeyArn string, + ) (*IDMappingTable, error) + DeleteIDMappingTable(membershipID, tableID string) error + PopulateIDMappingTable(membershipID, tableID string) (map[string]any, error) + + // IDNamespaceAssociation operations. + CreateIDNamespaceAssociation( + membershipID, name, description string, + inputReferenceConfig map[string]any, + idMappingConfig map[string]any, + tags map[string]string, + ) (*IDNamespaceAssociation, error) + GetIDNamespaceAssociation(membershipID, assocID string) (*IDNamespaceAssociation, error) + ListIDNamespaceAssociations( + membershipID, maxResults, nextToken string, + ) ([]*IDNamespaceAssociationSummary, string, error) + UpdateIDNamespaceAssociation( + membershipID, assocID, description string, + idMappingConfig map[string]any, + ) (*IDNamespaceAssociation, error) + DeleteIDNamespaceAssociation(membershipID, assocID string) error + GetCollaborationIDNamespaceAssociation( + collaborationID, assocID string, + ) (*IDNamespaceAssociation, error) + ListCollaborationIDNamespaceAssociations( + collaborationID, maxResults, nextToken string, + ) ([]*IDNamespaceAssociationSummary, string, error) + + // ConfiguredAudienceModelAssociation operations. + CreateConfiguredAudienceModelAssociation( + membershipID, configuredAudienceModelArn, name, description string, + manageResourcePolicies bool, + tags map[string]string, + ) (*ConfiguredAudienceModelAssociation, error) + GetConfiguredAudienceModelAssociation( + membershipID, assocID string, + ) (*ConfiguredAudienceModelAssociation, error) + ListConfiguredAudienceModelAssociations( + membershipID, maxResults, nextToken string, + ) ([]*ConfiguredAudienceModelAssociationSummary, string, error) + UpdateConfiguredAudienceModelAssociation( + membershipID, assocID, name, description string, + ) (*ConfiguredAudienceModelAssociation, error) + DeleteConfiguredAudienceModelAssociation(membershipID, assocID string) error + GetCollaborationConfiguredAudienceModelAssociation( + collaborationID, assocID string, + ) (*ConfiguredAudienceModelAssociation, error) + ListCollaborationConfiguredAudienceModelAssociations( + collaborationID, maxResults, nextToken string, + ) ([]*ConfiguredAudienceModelAssociationSummary, string, error) + + // CollaborationChangeRequest operations. + CreateCollaborationChangeRequest( + collaborationID, changeRequestType string, + details map[string]any, + ) (*CollaborationChangeRequest, error) + GetCollaborationChangeRequest( + collaborationID, changeRequestID string, + ) (*CollaborationChangeRequest, error) + ListCollaborationChangeRequests( + collaborationID, maxResults, nextToken string, + ) ([]*CollaborationChangeRequest, string, error) + UpdateCollaborationChangeRequest( + collaborationID, changeRequestID, status string, + ) (*CollaborationChangeRequest, error) + + // Tag operations. + ListTagsForResource(resourceArn string) (map[string]string, error) + TagResource(resourceArn string, tags map[string]string) error + UntagResource(resourceArn string, tagKeys []string) error +} + +// compile-time assertion that InMemoryBackend implements StorageBackend. +var _ StorageBackend = (*InMemoryBackend)(nil) diff --git a/services/cleanrooms/provider.go b/services/cleanrooms/provider.go new file mode 100644 index 000000000..f5c49ac67 --- /dev/null +++ b/services/cleanrooms/provider.go @@ -0,0 +1,40 @@ +package cleanrooms + +import ( + "errors" + + "github.com/blackbirdworks/gopherstack/pkgs/config" + "github.com/blackbirdworks/gopherstack/pkgs/service" +) + +// ErrNilAppContext is returned by Init when a nil AppContext is passed. +var ErrNilAppContext = errors.New("nil AppContext passed to CleanRooms Provider.Init") + +// Provider implements service.Provider for AWS Clean Rooms. +type Provider struct{} + +// Name returns the provider name. +func (p *Provider) Name() string { return "CleanRooms" } + +// Init initializes the Clean Rooms service backend and handler. +// +//nolint:ireturn,nolintlint // architecturally required to return interface +func (p *Provider) Init(ctx *service.AppContext) (service.Registerable, error) { + if ctx == nil { + return nil, ErrNilAppContext + } + + accountID := config.DefaultAccountID + region := config.DefaultRegion + + if cp, ok := ctx.Config.(config.Provider); ok { + cfg := cp.GetGlobalConfig() + accountID = cfg.GetAccountID() + region = cfg.GetRegion() + } + + backend := NewInMemoryBackendWithContext(ctx.JanitorCtx, accountID, region) + handler := NewHandler(backend) + + return handler, nil +} diff --git a/services/cleanrooms/sdk_completeness_test.go b/services/cleanrooms/sdk_completeness_test.go new file mode 100644 index 000000000..e1b9f8512 --- /dev/null +++ b/services/cleanrooms/sdk_completeness_test.go @@ -0,0 +1,18 @@ +package cleanrooms_test + +import ( + "testing" + + cleanroomssdk "github.com/aws/aws-sdk-go-v2/service/cleanrooms" + + "github.com/blackbirdworks/gopherstack/pkgs/sdkcheck" + "github.com/blackbirdworks/gopherstack/services/cleanrooms" +) + +func TestSDKCompleteness(t *testing.T) { + t.Parallel() + + backend := cleanrooms.NewInMemoryBackend("000000000000", "us-east-1") + h := cleanrooms.NewHandler(backend) + sdkcheck.CheckCompleteness(t, &cleanroomssdk.Client{}, h.GetSupportedOperations(), []string{}) +} diff --git a/ui/src/routes/pipes/+page.svelte b/ui/src/routes/pipes/+page.svelte index 8310d80f7..7f41b7854 100644 --- a/ui/src/routes/pipes/+page.svelte +++ b/ui/src/routes/pipes/+page.svelte @@ -243,7 +243,8 @@ Target: editTarget || undefined, RoleArn: editRoleArn || undefined, Description: editDescription, - DesiredState: editDesiredState as 'RUNNING' | 'STOPPED' | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + DesiredState: editDesiredState as any }) ); toast.success('Pipe updated'); @@ -346,6 +347,10 @@ } } + function setTab(tab: string) { + activeDetailTab = tab as 'overview' | 'tags' | 'config'; + } + function closeModalOnBackdrop(e: MouseEvent) { if (e.target === e.currentTarget) { showCreateModal = false; @@ -626,7 +631,7 @@