From fd0fad00013d6e4262008b2e24c985a2baf8a13a Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 21:26:20 -0500 Subject: [PATCH 01/16] WIP: checkpoint (auto) --- services/cleanrooms/interfaces.go | 135 ++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 services/cleanrooms/interfaces.go diff --git a/services/cleanrooms/interfaces.go b/services/cleanrooms/interfaces.go new file mode 100644 index 000000000..66d9f0fef --- /dev/null +++ b/services/cleanrooms/interfaces.go @@ -0,0 +1,135 @@ +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) From 83d3bb1ad2ae712ffcfc27c3ab5db0fc8e18a02c Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 21:36:18 -0500 Subject: [PATCH 02/16] WIP: checkpoint (auto) --- services/cleanrooms/backend.go | 2408 ++++++++++++++++++++++++++++++++ services/cleanrooms/handler.go | 2261 ++++++++++++++++++++++++++++++ 2 files changed, 4669 insertions(+) create mode 100644 services/cleanrooms/backend.go create mode 100644 services/cleanrooms/handler.go diff --git a/services/cleanrooms/backend.go b/services/cleanrooms/backend.go new file mode 100644 index 000000000..fa018de8b --- /dev/null +++ b/services/cleanrooms/backend.go @@ -0,0 +1,2408 @@ +// Package cleanrooms implements an in-memory AWS Clean Rooms service backend. +package cleanrooms + +import ( + "fmt" + "maps" + "sort" + "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) +) + +// ---- types ---- + +type MemberSpec struct { + AccountID string `json:"accountId"` + DisplayName string `json:"displayName"` + Abilities []string `json:"memberAbilities"` + PaymentConfig map[string]any `json:"paymentConfiguration,omitempty"` +} + +type MemberSummary struct { + AccountID string `json:"accountId"` + DisplayName string `json:"displayName"` + Abilities []string `json:"abilities"` + Status string `json:"status"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type Collaboration struct { + 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"` + MemberAbilities []string `json:"memberAbilities,omitempty"` + Members []*MemberSummary `json:"members,omitempty"` + QueryLogStatus string `json:"queryLogStatus,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` + Tags map[string]string `json:"tags,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 { + 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"` + QueryLogStatus string `json:"queryLogStatus,omitempty"` + DefaultResultConfiguration map[string]any `json:"defaultResultConfiguration,omitempty"` + PaymentConfiguration map[string]any `json:"paymentConfiguration,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 { + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + Arn string `json:"arn"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + TableReference map[string]any `json:"tableReference,omitempty"` + AllowedColumns []string `json:"allowedColumns,omitempty"` + AnalysisMethod string `json:"analysisMethod,omitempty"` + AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` + Tags map[string]string `json:"tags,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 { + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + ConfiguredTableArn string `json:"configuredTableArn"` + Type string `json:"type"` + Policy map[string]any `json:"policy,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type ConfiguredTableAssociation struct { + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + Arn string `json:"arn"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + ConfiguredTableArn string `json:"configuredTableArn"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + RoleArn string `json:"roleArn,omitempty"` + AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` + Tags map[string]string `json:"tags,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 { + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + ConfiguredTableAssociationArn string `json:"configuredTableAssociationArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + Type string `json:"type"` + Policy map[string]any `json:"policy,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type AnalysisTemplate 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"` + Description string `json:"description,omitempty"` + Source map[string]any `json:"source,omitempty"` + Schema map[string]any `json:"schema,omitempty"` + Format string `json:"format,omitempty"` + AnalysisParameters []map[string]any `json:"analysisParameters,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` + Tags map[string]string `json:"tags,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"` + Columns []map[string]any `json:"columns,omitempty"` + PartitionKeys []map[string]any `json:"partitionKeys,omitempty"` + AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` + AnalysisMethod string `json:"analysisMethod,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"` + AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` + AnalysisMethod string `json:"analysisMethod,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type SchemaAnalysisRule struct { + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + Name string `json:"name"` + Type string `json:"type"` + Policy map[string]any `json:"policy,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +type ProtectedQuery struct { + Id string `json:"id"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + Status string `json:"status"` + 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"` + 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 { + Id string `json:"id"` + MembershipIdentifier string `json:"membershipIdentifier"` + MembershipArn string `json:"membershipArn"` + Status string `json:"status"` + Type string `json:"type"` + 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"` + 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 { + 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"` + Parameters map[string]any `json:"parameters,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` + Tags map[string]string `json:"tags,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 { + 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"` + Budget map[string]any `json:"budget,omitempty"` +} + +type IdMappingTable 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"` + Description string `json:"description,omitempty"` + InputReferenceConfig map[string]any `json:"inputReferenceConfig,omitempty"` + InputReferenceProperties map[string]any `json:"inputReferenceProperties,omitempty"` + KmsKeyArn string `json:"kmsKeyArn,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` + Tags map[string]string `json:"tags,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 { + 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"` + Description string `json:"description,omitempty"` + InputReferenceConfig map[string]any `json:"inputReferenceConfig,omitempty"` + InputReferenceProperties map[string]any `json:"inputReferenceProperties,omitempty"` + IdMappingConfig map[string]any `json:"idMappingConfig,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` + Tags map[string]string `json:"tags,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 { + ConfiguredAudienceModelAssociationIdentifier string `json:"configuredAudienceModelAssociationIdentifier"` + Arn string `json:"arn"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + MembershipArn string `json:"membershipArn"` + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredAudienceModelArn string `json:"configuredAudienceModelArn"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + ManageResourcePolicies bool `json:"manageResourcePolicies"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` + Tags map[string]string `json:"tags,omitempty"` +} + +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 { + ChangeRequestIdentifier string `json:"changeRequestIdentifier"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + CollaborationArn string `json:"collaborationArn"` + Status string `json:"status"` + Type string `json:"type"` + Details map[string]any `json:"details,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` +} + +// ---- InMemoryBackend ---- + +// InMemoryBackend is the in-memory implementation of StorageBackend. +type InMemoryBackend struct { + mu *lockmetrics.RWMutex + accountID string + region string + + 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 + analysisTemplates map[string]map[string]*AnalysisTemplate + protectedQueries map[string]map[string]*ProtectedQuery + protectedJobs map[string]map[string]*ProtectedJob + privacyBudgetTemplates map[string]map[string]*PrivacyBudgetTemplate + 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 + tagsByArn map[string]map[string]string +} + +// 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), + } +} + +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 helper ---- + +func paginate[T any](items []T, maxResultsStr, nextToken string) ([]T, string) { + if len(items) == 0 { + return items, "" + } + max := 100 + if maxResultsStr != "" { + fmt.Sscanf(maxResultsStr, "%d", &max) + } + if max <= 0 || max > 1000 { + max = 100 + } + start := 0 + if nextToken != "" { + fmt.Sscanf(nextToken, "%d", &start) + } + if start >= len(items) { + return []T{}, "" + } + end := start + max + if end >= len(items) { + return items[start:], "" + } + return items[start:end], fmt.Sprintf("%d", end) +} + +// ---- now helper ---- + +var nowFn = func() float64 { return float64(time.Now().Unix()) } +var muNow sync.Mutex + +func now() float64 { + muNow.Lock() + defer muNow.Unlock() + return 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 := now() + memberSummaries := make([]*MemberSummary, 0, len(members)+1) + memberSummaries = append(memberSummaries, &MemberSummary{ + AccountID: b.accountID, + DisplayName: creatorDisplayName, + Abilities: creatorMemberAbilities, + Status: "ACTIVE", + 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(memberStatus, maxResults, nextToken string) ([]*CollaborationSummary, string) { + b.mu.RLock("ListCollaborations") + defer b.mu.RUnlock() + var items []*CollaborationSummary + 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: "ACTIVE", + 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 = 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 := now() + m := &Membership{ + MembershipIdentifier: id, + Arn: b.membershipARN(id), + CollaborationIdentifier: collaborationID, + CollaborationArn: collab.Arn, + CollaborationCreatorAccountId: collab.CreatorAccountId, + CollaborationCreatorDisplayName: collab.CreatorDisplayName, + CollaborationName: collab.Name, + Status: "ACTIVE", + 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 = 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 := 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() + var items []*ConfiguredTableSummary + 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 = 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 := 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 = 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 _, ok := rules[analysisRuleType]; !ok { + return ErrNotFound + } + delete(rules, analysisRuleType) + if ct, ok := b.configuredTables[configuredTableID]; ok { + 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 := 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 = 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 := 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(membershipID, 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(membershipID, 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 = 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 _, ok := rules[ruleType]; !ok { + return ErrNotFound + } + delete(rules, ruleType) + if assocs, ok := b.ctAssociations[membershipID]; ok { + if assoc, ok := assocs[assocID]; ok { + 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 := 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 + } + var items []*AnalysisTemplateSummary + for _, t := range b.analysisTemplates[membershipID] { + items = append(items, &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, + }) + } + sort.Slice(items, func(i, j int) bool { return items[i].AnalysisTemplateIdentifier < items[j].AnalysisTemplateIdentifier }) + page, next := paginate(items, 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 = 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 + } + var items []*AnalysisTemplateSummary + for _, tmpls := range b.analysisTemplates { + for _, t := range tmpls { + if t.CollaborationIdentifier == collaborationID { + items = append(items, &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, + }) + } + } + } + sort.Slice(items, func(i, j int) bool { return items[i].AnalysisTemplateIdentifier < items[j].AnalysisTemplateIdentifier }) + page, next := paginate(items, 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: "ResourceNotFoundException", Message: "not found"}) + } + } + 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 + } + var items []*SchemaSummary + for _, s := range b.schemas[collaborationID] { + if schemaType != "" && s.Type != schemaType { + continue + } + items = append(items, &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, + }) + } + sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name }) + page, next := paginate(items, 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: "ResourceNotFoundException", Message: "not found"}) + } + } + 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, ok := collabRules[name]; ok { + if rule, ok := schemaRules[ruleType]; ok { + results = append(results, rule) + continue + } + } + } + errors = append(errors, BatchError{Name: name, Code: "ResourceNotFoundException", Message: "not found"}) + } + 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 := 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: 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 := 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 + } + var items []*PrivacyBudgetTemplateSummary + for _, t := range b.privacyBudgetTemplates[membershipID] { + if privacyBudgetType != "" && t.PrivacyBudgetType != privacyBudgetType { + continue + } + items = append(items, &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, + }) + } + sort.Slice(items, func(i, j int) bool { + return items[i].PrivacyBudgetTemplateIdentifier < items[j].PrivacyBudgetTemplateIdentifier + }) + page, next := paginate(items, 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 = 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, privacyBudgetType, maxResults, nextToken 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, privacyBudgetType, maxResults, nextToken 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 + } + var items []*PrivacyBudgetTemplateSummary + for _, tmpls := range b.privacyBudgetTemplates { + for _, t := range tmpls { + if t.CollaborationIdentifier == collaborationID { + items = append(items, &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, + }) + } + } + } + sort.Slice(items, func(i, j int) bool { + return items[i].PrivacyBudgetTemplateIdentifier < items[j].PrivacyBudgetTemplateIdentifier + }) + page, next := paginate(items, maxResults, nextToken) + return page, next, nil +} + +func (b *InMemoryBackend) PreviewPrivacyImpact(membershipID string, parameters 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) { + 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 := 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 + } + var items []*IdMappingTableSummary + for _, t := range b.idMappingTables[membershipID] { + items = append(items, &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, + }) + } + sort.Slice(items, func(i, j int) bool { return items[i].IdMappingTableIdentifier < items[j].IdMappingTableIdentifier }) + page, next := paginate(items, 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 = 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 := 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 + } + var items []*IdNamespaceAssociationSummary + for _, a := range b.idNamespaceAssociations[membershipID] { + items = append(items, &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, + }) + } + sort.Slice(items, func(i, j int) bool { + return items[i].IdNamespaceAssociationIdentifier < items[j].IdNamespaceAssociationIdentifier + }) + page, next := paginate(items, 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 = 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 + } + var items []*IdNamespaceAssociationSummary + for _, assocs := range b.idNamespaceAssociations { + for _, a := range assocs { + if a.CollaborationIdentifier == collaborationID { + items = append(items, &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, + }) + } + } + } + sort.Slice(items, func(i, j int) bool { + return items[i].IdNamespaceAssociationIdentifier < items[j].IdNamespaceAssociationIdentifier + }) + page, next := paginate(items, maxResults, nextToken) + return page, next, nil +} + +// ---- ConfiguredAudienceModelAssociation ---- + +func (b *InMemoryBackend) CreateConfiguredAudienceModelAssociation(membershipID, configuredAudienceModelArn, name, description string, manageResourcePolicies bool, tags map[string]string) (*ConfiguredAudienceModelAssociation, error) { + 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 := 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 + } + var items []*ConfiguredAudienceModelAssociationSummary + for _, a := range b.camaAssociations[membershipID] { + items = append(items, &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, + }) + } + sort.Slice(items, func(i, j int) bool { + return items[i].ConfiguredAudienceModelAssociationIdentifier < items[j].ConfiguredAudienceModelAssociationIdentifier + }) + page, next := paginate(items, 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 = 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 + } + var items []*ConfiguredAudienceModelAssociationSummary + for _, assocs := range b.camaAssociations { + for _, a := range assocs { + if a.CollaborationIdentifier == collaborationID { + items = append(items, &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, + }) + } + } + } + sort.Slice(items, func(i, j int) bool { + return items[i].ConfiguredAudienceModelAssociationIdentifier < items[j].ConfiguredAudienceModelAssociationIdentifier + }) + page, next := paginate(items, 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 := 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 = 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) + } + for k, v := range tags { + b.tagsByArn[resourceArn][k] = v + } + 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 { + for _, v := range ss { + if v == s { + return true + } + } + return false +} + +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..1cca9d24d --- /dev/null +++ b/services/cleanrooms/handler.go @@ -0,0 +1,2261 @@ +package cleanrooms + +import ( + "context" + "encoding/json" + "errors" + "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" +) + +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.PriorityPathPlain } + +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()}) + } +} + +// pathRouteEntry holds a parsed path classification. +type pathRouteEntry struct { + op string + seg []string +} + +// 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.SplitN(path, "/", -1) + 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) == 1 { + switch method { + case http.MethodPost: + return opCreateCollaboration, "" + case http.MethodGet: + return opListCollaborations, "" + } + } + // /collaborations/{id} + if len(segs) == 2 { + 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) >= 3 { + id := segs[1] + sub := segs[2] + switch sub { + case "analysistemplates": + if len(segs) == 3 { + if method == http.MethodGet { + return opListCollaborationAnalysisTemplates, id + } + } + if len(segs) == 4 { + if method == http.MethodGet { + return opGetCollaborationAnalysisTemplate, id + } + } + case "batch-analysistemplates": + if method == http.MethodPost { + return opBatchGetCollaborationAnalysisTemplate, id + } + case "batch-schema": + if method == http.MethodPost { + return opBatchGetSchema, id + } + case "batch-schema-analysis-rule": + if method == http.MethodPost { + return opBatchGetSchemaAnalysisRule, id + } + case "changeRequests": + if len(segs) == 3 { + switch method { + case http.MethodPost: + return opCreateCollaborationChangeRequest, id + case http.MethodGet: + return opListCollaborationChangeRequests, id + } + } + if len(segs) == 4 { + switch method { + case http.MethodGet: + return opGetCollaborationChangeRequest, id + case http.MethodPatch: + return opUpdateCollaborationChangeRequest, id + } + } + case "configuredaudiencemodelassociations": + if len(segs) == 3 { + if method == http.MethodGet { + return opListCollaborationConfiguredAudienceModelAssociations, id + } + } + if len(segs) == 4 { + if method == http.MethodGet { + return opGetCollaborationConfiguredAudienceModelAssociation, id + } + } + case "idnamespaceassociations": + if len(segs) == 3 { + if method == http.MethodGet { + return opListCollaborationIdNamespaceAssociations, id + } + } + if len(segs) == 4 { + if method == http.MethodGet { + return opGetCollaborationIdNamespaceAssociation, id + } + } + case "member": + // /collaborations/{id}/member/{accountId} + if len(segs) == 4 && method == http.MethodDelete { + return opDeleteMember, id + } + case "members": + if method == http.MethodGet { + return opListMembers, id + } + case "privacybudgettemplates": + if len(segs) == 3 { + if method == http.MethodGet { + return opListCollaborationPrivacyBudgetTemplates, id + } + } + if len(segs) == 4 { + if method == http.MethodGet { + return opGetCollaborationPrivacyBudgetTemplate, id + } + } + case "privacybudgets": + if method == http.MethodGet { + return opListCollaborationPrivacyBudgets, id + } + case "schemas": + if len(segs) == 3 { + if method == http.MethodGet { + return opListSchemas, id + } + } + if len(segs) == 4 { + if method == http.MethodGet { + return opGetSchema, id + } + } + // /collaborations/{id}/schemas/{name}/analysisRule/{type} + if len(segs) == 6 && segs[4] == "analysisRule" { + if method == http.MethodGet { + return opGetSchemaAnalysisRule, id + } + } + } + } + return opUnknown, "" +} + +func classifyConfiguredTables(method string, segs []string) (string, string) { + // /configuredTables + if len(segs) == 1 { + switch method { + case http.MethodPost: + return opCreateConfiguredTable, "" + case http.MethodGet: + return opListConfiguredTables, "" + } + } + // /configuredTables/{id} + if len(segs) == 2 { + 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 + if len(segs) == 3 && segs[2] == "analysisRule" { + id := segs[1] + if method == http.MethodPost { + return opCreateConfiguredTableAnalysisRule, id + } + } + // /configuredTables/{id}/analysisRule/{type} + if len(segs) == 4 && segs[2] == "analysisRule" { + id := segs[1] + 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) == 1 { + switch method { + case http.MethodPost: + return opCreateMembership, "" + case http.MethodGet: + return opListMemberships, "" + } + } + // /memberships/{id} + if len(segs) == 2 { + 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) < 3 { + return opUnknown, "" + } + membershipID := segs[1] + sub := segs[2] + switch sub { + case "analysistemplates": + if len(segs) == 3 { + switch method { + case http.MethodPost: + return opCreateAnalysisTemplate, membershipID + case http.MethodGet: + return opListAnalysisTemplates, membershipID + } + } + if len(segs) == 4 { + switch method { + case http.MethodGet: + return opGetAnalysisTemplate, membershipID + case http.MethodDelete: + return opDeleteAnalysisTemplate, membershipID + case http.MethodPatch: + return opUpdateAnalysisTemplate, membershipID + } + } + case "configuredTableAssociations": + if len(segs) == 3 { + switch method { + case http.MethodPost: + return opCreateConfiguredTableAssociation, membershipID + case http.MethodGet: + return opListConfiguredTableAssociations, membershipID + } + } + if len(segs) == 4 { + switch method { + case http.MethodGet: + return opGetConfiguredTableAssociation, membershipID + case http.MethodDelete: + return opDeleteConfiguredTableAssociation, membershipID + case http.MethodPatch: + return opUpdateConfiguredTableAssociation, membershipID + } + } + // /memberships/{id}/configuredTableAssociations/{assocId}/analysisRule + if len(segs) == 5 && segs[4] == "analysisRule" { + if method == http.MethodPost { + return opCreateConfiguredTableAssociationAnalysisRule, membershipID + } + } + // /memberships/{id}/configuredTableAssociations/{assocId}/analysisRule/{type} + if len(segs) == 6 && segs[4] == "analysisRule" { + switch method { + case http.MethodGet: + return opGetConfiguredTableAssociationAnalysisRule, membershipID + case http.MethodDelete: + return opDeleteConfiguredTableAssociationAnalysisRule, membershipID + case http.MethodPatch: + return opUpdateConfiguredTableAssociationAnalysisRule, membershipID + } + } + case "configuredaudiencemodelassociations": + if len(segs) == 3 { + switch method { + case http.MethodPost: + return opCreateConfiguredAudienceModelAssociation, membershipID + case http.MethodGet: + return opListConfiguredAudienceModelAssociations, membershipID + } + } + if len(segs) == 4 { + switch method { + case http.MethodGet: + return opGetConfiguredAudienceModelAssociation, membershipID + case http.MethodDelete: + return opDeleteConfiguredAudienceModelAssociation, membershipID + case http.MethodPatch: + return opUpdateConfiguredAudienceModelAssociation, membershipID + } + } + case "idmappingtables": + if len(segs) == 3 { + switch method { + case http.MethodPost: + return opCreateIdMappingTable, membershipID + case http.MethodGet: + return opListIdMappingTables, membershipID + } + } + if len(segs) == 4 { + 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) == 5 && segs[4] == "populate" { + if method == http.MethodPost { + return opPopulateIdMappingTable, membershipID + } + } + case "idnamespaceassociations": + if len(segs) == 3 { + switch method { + case http.MethodPost: + return opCreateIdNamespaceAssociation, membershipID + case http.MethodGet: + return opListIdNamespaceAssociations, membershipID + } + } + if len(segs) == 4 { + switch method { + case http.MethodGet: + return opGetIdNamespaceAssociation, membershipID + case http.MethodDelete: + return opDeleteIdNamespaceAssociation, membershipID + case http.MethodPatch: + return opUpdateIdNamespaceAssociation, membershipID + } + } + case "previewprivacyimpact": + if method == http.MethodPost { + return opPreviewPrivacyImpact, membershipID + } + case "privacybudgets": + if method == http.MethodGet { + return opListPrivacyBudgets, membershipID + } + case "privacybudgettemplates": + if len(segs) == 3 { + switch method { + case http.MethodPost: + return opCreatePrivacyBudgetTemplate, membershipID + case http.MethodGet: + return opListPrivacyBudgetTemplates, membershipID + } + } + if len(segs) == 4 { + switch method { + case http.MethodGet: + return opGetPrivacyBudgetTemplate, membershipID + case http.MethodDelete: + return opDeletePrivacyBudgetTemplate, membershipID + case http.MethodPatch: + return opUpdatePrivacyBudgetTemplate, membershipID + } + } + case "protectedJobs": + if len(segs) == 3 { + switch method { + case http.MethodPost: + return opStartProtectedJob, membershipID + case http.MethodGet: + return opListProtectedJobs, membershipID + } + } + if len(segs) == 4 { + switch method { + case http.MethodGet: + return opGetProtectedJob, membershipID + case http.MethodPatch: + return opUpdateProtectedJob, membershipID + } + } + case "protectedQueries": + if len(segs) == 3 { + switch method { + case http.MethodPost: + return opStartProtectedQuery, membershipID + case http.MethodGet: + return opListProtectedQueries, membershipID + } + } + if len(segs) == 4 { + 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) < 2 { + 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, op 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) >= 2 && segs[0] == "collaborations": + setStr("collaborationIdentifier", segs[1]) + if len(segs) >= 4 { + switch segs[2] { + case "analysistemplates": + setStr("analysisTemplateArn", segs[3]) + case "changeRequests": + setStr("changeRequestIdentifier", segs[3]) + case "configuredaudiencemodelassociations": + setStr("configuredAudienceModelAssociationIdentifier", segs[3]) + case "idnamespaceassociations": + setStr("idNamespaceAssociationIdentifier", segs[3]) + case "member": + setStr("accountId", segs[3]) + case "privacybudgettemplates": + setStr("privacyBudgetTemplateIdentifier", segs[3]) + case "schemas": + setStr("name", segs[3]) + if len(segs) == 6 && segs[4] == "analysisRule" { + setStr("type", segs[5]) + } + } + } + case len(segs) >= 2 && segs[0] == "configuredTables": + setStr("configuredTableIdentifier", segs[1]) + if len(segs) == 4 && segs[2] == "analysisRule" { + setStr("analysisRuleType", segs[3]) + } + case len(segs) >= 2 && segs[0] == "memberships": + setStr("membershipIdentifier", segs[1]) + if len(segs) >= 4 { + switch segs[2] { + case "analysistemplates": + setStr("analysisTemplateIdentifier", segs[3]) + case "configuredTableAssociations": + setStr("configuredTableAssociationIdentifier", segs[3]) + if len(segs) == 6 && segs[4] == "analysisRule" { + setStr("analysisRuleType", segs[5]) + } + case "configuredaudiencemodelassociations": + setStr("configuredAudienceModelAssociationIdentifier", segs[3]) + case "idmappingtables": + setStr("idMappingTableIdentifier", segs[3]) + case "idnamespaceassociations": + setStr("idNamespaceAssociationIdentifier", segs[3]) + case "privacybudgettemplates": + setStr("privacyBudgetTemplateIdentifier", segs[3]) + case "protectedJobs": + setStr("protectedJobIdentifier", segs[3]) + case "protectedQueries": + setStr("protectedQueryIdentifier", segs[3]) + } + } + case len(segs) >= 2 && segs[0] == "tags": + arnVal := strings.Join(segs[1:], "/") + setStr("resourceArn", arnVal) + } + + out, _ := json.Marshal(m) + return out +} + +// ---- dispatch ---- + +func (h *Handler) dispatch(ctx context.Context, op string, body []byte, c *echo.Context) ([]byte, error) { + switch op { + // Collaboration + case opCreateCollaboration: + return h.handleCreateCollaboration(ctx, body) + case opGetCollaboration: + return h.handleGetCollaboration(ctx, body) + case opListCollaborations: + return h.handleListCollaborations(ctx, body, c) + case opUpdateCollaboration: + return h.handleUpdateCollaboration(ctx, body) + case opDeleteCollaboration: + return h.handleDeleteCollaboration(ctx, body) + case opListMembers: + return h.handleListMembers(ctx, body, c) + case opDeleteMember: + return h.handleDeleteMember(ctx, body) + // Collaboration sub-resources + case opGetCollaborationAnalysisTemplate: + return h.handleGetCollaborationAnalysisTemplate(ctx, body) + case opListCollaborationAnalysisTemplates: + return h.handleListCollaborationAnalysisTemplates(ctx, body, c) + case opBatchGetCollaborationAnalysisTemplate: + return h.handleBatchGetCollaborationAnalysisTemplate(ctx, body) + case opBatchGetSchema: + return h.handleBatchGetSchema(ctx, body) + case opBatchGetSchemaAnalysisRule: + return h.handleBatchGetSchemaAnalysisRule(ctx, body) + case opGetSchema: + return h.handleGetSchema(ctx, body) + case opListSchemas: + return h.handleListSchemas(ctx, body, c) + case opGetSchemaAnalysisRule: + return h.handleGetSchemaAnalysisRule(ctx, body) + case opCreateCollaborationChangeRequest: + return h.handleCreateCollaborationChangeRequest(ctx, body) + case opGetCollaborationChangeRequest: + return h.handleGetCollaborationChangeRequest(ctx, body) + case opListCollaborationChangeRequests: + return h.handleListCollaborationChangeRequests(ctx, body, c) + case opUpdateCollaborationChangeRequest: + return h.handleUpdateCollaborationChangeRequest(ctx, body) + case opGetCollaborationConfiguredAudienceModelAssociation: + return h.handleGetCollaborationConfiguredAudienceModelAssociation(ctx, body) + case opListCollaborationConfiguredAudienceModelAssociations: + return h.handleListCollaborationConfiguredAudienceModelAssociations(ctx, body, c) + case opGetCollaborationIdNamespaceAssociation: + return h.handleGetCollaborationIdNamespaceAssociation(ctx, body) + case opListCollaborationIdNamespaceAssociations: + return h.handleListCollaborationIdNamespaceAssociations(ctx, body, c) + case opGetCollaborationPrivacyBudgetTemplate: + return h.handleGetCollaborationPrivacyBudgetTemplate(ctx, body) + case opListCollaborationPrivacyBudgetTemplates: + return h.handleListCollaborationPrivacyBudgetTemplates(ctx, body, c) + case opListCollaborationPrivacyBudgets: + return h.handleListCollaborationPrivacyBudgets(ctx, body, c) + // Membership + case opCreateMembership: + return h.handleCreateMembership(ctx, body) + case opGetMembership: + return h.handleGetMembership(ctx, body) + case opListMemberships: + return h.handleListMemberships(ctx, body, c) + case opUpdateMembership: + return h.handleUpdateMembership(ctx, body) + case opDeleteMembership: + return h.handleDeleteMembership(ctx, body) + // ConfiguredTable + case opCreateConfiguredTable: + return h.handleCreateConfiguredTable(ctx, body) + case opGetConfiguredTable: + return h.handleGetConfiguredTable(ctx, body) + case opListConfiguredTables: + return h.handleListConfiguredTables(ctx, body, c) + case opUpdateConfiguredTable: + return h.handleUpdateConfiguredTable(ctx, body) + case opDeleteConfiguredTable: + return h.handleDeleteConfiguredTable(ctx, body) + // ConfiguredTableAnalysisRule + case opCreateConfiguredTableAnalysisRule: + return h.handleCreateConfiguredTableAnalysisRule(ctx, body) + case opGetConfiguredTableAnalysisRule: + return h.handleGetConfiguredTableAnalysisRule(ctx, body) + case opUpdateConfiguredTableAnalysisRule: + return h.handleUpdateConfiguredTableAnalysisRule(ctx, body) + case opDeleteConfiguredTableAnalysisRule: + return h.handleDeleteConfiguredTableAnalysisRule(ctx, body) + // ConfiguredTableAssociation + case opCreateConfiguredTableAssociation: + return h.handleCreateConfiguredTableAssociation(ctx, body) + case opGetConfiguredTableAssociation: + return h.handleGetConfiguredTableAssociation(ctx, body) + case opListConfiguredTableAssociations: + return h.handleListConfiguredTableAssociations(ctx, body, c) + case opUpdateConfiguredTableAssociation: + return h.handleUpdateConfiguredTableAssociation(ctx, body) + case opDeleteConfiguredTableAssociation: + return h.handleDeleteConfiguredTableAssociation(ctx, body) + // ConfiguredTableAssociationAnalysisRule + case opCreateConfiguredTableAssociationAnalysisRule: + return h.handleCreateConfiguredTableAssociationAnalysisRule(ctx, body) + case opGetConfiguredTableAssociationAnalysisRule: + return h.handleGetConfiguredTableAssociationAnalysisRule(ctx, body) + case opUpdateConfiguredTableAssociationAnalysisRule: + return h.handleUpdateConfiguredTableAssociationAnalysisRule(ctx, body) + case opDeleteConfiguredTableAssociationAnalysisRule: + return h.handleDeleteConfiguredTableAssociationAnalysisRule(ctx, body) + // AnalysisTemplate + case opCreateAnalysisTemplate: + return h.handleCreateAnalysisTemplate(ctx, body) + case opGetAnalysisTemplate: + return h.handleGetAnalysisTemplate(ctx, body) + case opListAnalysisTemplates: + return h.handleListAnalysisTemplates(ctx, body, c) + case opUpdateAnalysisTemplate: + return h.handleUpdateAnalysisTemplate(ctx, body) + case opDeleteAnalysisTemplate: + return h.handleDeleteAnalysisTemplate(ctx, body) + // ProtectedQuery + case opStartProtectedQuery: + return h.handleStartProtectedQuery(ctx, body) + case opGetProtectedQuery: + return h.handleGetProtectedQuery(ctx, body) + case opListProtectedQueries: + return h.handleListProtectedQueries(ctx, body, c) + case opUpdateProtectedQuery: + return h.handleUpdateProtectedQuery(ctx, body) + // ProtectedJob + case opStartProtectedJob: + return h.handleStartProtectedJob(ctx, body) + case opGetProtectedJob: + return h.handleGetProtectedJob(ctx, body) + case opListProtectedJobs: + return h.handleListProtectedJobs(ctx, body, c) + case opUpdateProtectedJob: + return h.handleUpdateProtectedJob(ctx, body) + // PrivacyBudgetTemplate + case opCreatePrivacyBudgetTemplate: + return h.handleCreatePrivacyBudgetTemplate(ctx, body) + case opGetPrivacyBudgetTemplate: + return h.handleGetPrivacyBudgetTemplate(ctx, body) + case opListPrivacyBudgetTemplates: + return h.handleListPrivacyBudgetTemplates(ctx, body, c) + case opUpdatePrivacyBudgetTemplate: + return h.handleUpdatePrivacyBudgetTemplate(ctx, body) + case opDeletePrivacyBudgetTemplate: + return h.handleDeletePrivacyBudgetTemplate(ctx, body) + case opListPrivacyBudgets: + return h.handleListPrivacyBudgets(ctx, body, c) + case opPreviewPrivacyImpact: + return h.handlePreviewPrivacyImpact(ctx, body) + // IdMappingTable + case opCreateIdMappingTable: + return h.handleCreateIdMappingTable(ctx, body) + case opGetIdMappingTable: + return h.handleGetIdMappingTable(ctx, body) + case opListIdMappingTables: + return h.handleListIdMappingTables(ctx, body, c) + case opUpdateIdMappingTable: + return h.handleUpdateIdMappingTable(ctx, body) + case opDeleteIdMappingTable: + return h.handleDeleteIdMappingTable(ctx, body) + case opPopulateIdMappingTable: + return h.handlePopulateIdMappingTable(ctx, body) + // IdNamespaceAssociation + case opCreateIdNamespaceAssociation: + return h.handleCreateIdNamespaceAssociation(ctx, body) + case opGetIdNamespaceAssociation: + return h.handleGetIdNamespaceAssociation(ctx, body) + case opListIdNamespaceAssociations: + return h.handleListIdNamespaceAssociations(ctx, body, c) + case opUpdateIdNamespaceAssociation: + return h.handleUpdateIdNamespaceAssociation(ctx, body) + case opDeleteIdNamespaceAssociation: + return h.handleDeleteIdNamespaceAssociation(ctx, body) + case opGetCollaborationIdNamespaceAssociation: + return h.handleGetCollaborationIdNamespaceAssociation(ctx, body) + // ConfiguredAudienceModelAssociation + case opCreateConfiguredAudienceModelAssociation: + return h.handleCreateConfiguredAudienceModelAssociation(ctx, body) + case opGetConfiguredAudienceModelAssociation: + return h.handleGetConfiguredAudienceModelAssociation(ctx, body) + case opListConfiguredAudienceModelAssociations: + return h.handleListConfiguredAudienceModelAssociations(ctx, body, c) + case opUpdateConfiguredAudienceModelAssociation: + return h.handleUpdateConfiguredAudienceModelAssociation(ctx, body) + case opDeleteConfiguredAudienceModelAssociation: + return h.handleDeleteConfiguredAudienceModelAssociation(ctx, body) + // Tags + case opListTagsForResource: + return h.handleListTagsForResource(ctx, body) + case opTagResource: + return h.handleTagResource(ctx, body) + case opUntagResource: + return h.handleUntagResource(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 { + Name string `json:"name"` + Description string `json:"description"` + CreatorDisplayName string `json:"creatorDisplayName"` + CreatorMemberAbilities []string `json:"creatorMemberAbilities"` + Members []MemberSpec `json:"members"` + QueryLogStatus string `json:"queryLogStatus"` + Tags map[string]string `json:"tags"` + } + _ = 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{"collaboration": 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{"collaboration": c}), nil +} + +func (h *Handler) handleListCollaborations(_ context.Context, body []byte, 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{"collaboration": 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{"analysisTemplate": 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, "errors": 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, "errors": 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) + var names []string + 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, "errors": 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{"analysisRule": r}), nil +} + +func (h *Handler) handleCreateCollaborationChangeRequest(_ context.Context, body []byte) ([]byte, error) { + var req struct { + CollaborationIdentifier string `json:"collaborationIdentifier"` + Type string `json:"type"` + Details map[string]any `json:"details"` + } + _ = 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{"collaborationChangeRequest": 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{"collaborationChangeRequest": 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{"collaborationChangeRequest": 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{"configuredAudienceModelAssociation": 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{"idNamespaceAssociation": 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{"privacyBudgetTemplate": 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 { + CollaborationIdentifier string `json:"collaborationIdentifier"` + QueryLogStatus string `json:"queryLogStatus"` + DefaultResultConfiguration map[string]any `json:"defaultResultConfiguration"` + PaymentConfiguration map[string]any `json:"paymentConfiguration"` + Tags map[string]string `json:"tags"` + } + _ = 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{"membership": 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{"membership": m}), nil +} + +func (h *Handler) handleListMemberships(_ context.Context, body []byte, 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + QueryLogStatus string `json:"queryLogStatus"` + DefaultResultConfiguration map[string]any `json:"defaultResultConfiguration"` + } + _ = 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{"membership": 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 { + Name string `json:"name"` + Description string `json:"description"` + TableReference map[string]any `json:"tableReference"` + AllowedColumns []string `json:"allowedColumns"` + AnalysisMethod string `json:"analysisMethod"` + Tags map[string]string `json:"tags"` + } + _ = 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{"configuredTable": 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{"configuredTable": ct}), nil +} + +func (h *Handler) handleListConfiguredTables(_ context.Context, body []byte, 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{"configuredTable": 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 { + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` + } + _ = 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{"analysisRule": 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{"analysisRule": r}), nil +} + +func (h *Handler) handleUpdateConfiguredTableAnalysisRule(_ context.Context, body []byte) ([]byte, error) { + var req struct { + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` + } + _ = 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{"analysisRule": 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` + RoleArn string `json:"roleArn"` + Tags map[string]string `json:"tags"` + } + _ = 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{"configuredTableAssociation": 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{"configuredTableAssociation": 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{"configuredTableAssociation": 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` + } + _ = 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{"analysisRule": 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{"analysisRule": r}), nil +} + +func (h *Handler) handleUpdateConfiguredTableAssociationAnalysisRule(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` + AnalysisRuleType string `json:"analysisRuleType"` + AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` + } + _ = 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{"analysisRule": 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + Format string `json:"format"` + Source map[string]any `json:"source"` + AnalysisParameters []map[string]any `json:"analysisParameters"` + Tags map[string]string `json:"tags"` + } + _ = 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{"analysisTemplate": 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{"analysisTemplate": 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{"analysisTemplate": 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + SqlParameters map[string]any `json:"sqlParameters"` + ResultConfiguration map[string]any `json:"resultConfiguration"` + ComputeConfiguration map[string]any `json:"computeConfiguration"` + } + _ = 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{"protectedQuery": 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{"protectedQuery": 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{"protectedQuery": q}), nil +} + +// ---- ProtectedJob handlers ---- + +func (h *Handler) handleStartProtectedJob(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + Type string `json:"type"` + JobParameters map[string]any `json:"jobParameters"` + ResultConfiguration map[string]any `json:"resultConfiguration"` + } + _ = 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{"protectedJob": 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{"protectedJob": 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{"protectedJob": j}), nil +} + +// ---- PrivacyBudgetTemplate handlers ---- + +func (h *Handler) handleCreatePrivacyBudgetTemplate(_ context.Context, body []byte) ([]byte, error) { + var req struct { + MembershipIdentifier string `json:"membershipIdentifier"` + PrivacyBudgetType string `json:"privacyBudgetType"` + AutoRefresh string `json:"autoRefresh"` + Parameters map[string]any `json:"parameters"` + Tags map[string]string `json:"tags"` + } + _ = 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{"privacyBudgetTemplate": 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{"privacyBudgetTemplate": 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + PrivacyBudgetTemplateIdentifier string `json:"privacyBudgetTemplateIdentifier"` + AutoRefresh string `json:"autoRefresh"` + Parameters map[string]any `json:"parameters"` + } + _ = 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{"privacyBudgetTemplate": 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + Parameters map[string]any `json:"parameters"` + } + _ = 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + InputReferenceConfig map[string]any `json:"inputReferenceConfig"` + KmsKeyArn string `json:"kmsKeyArn"` + Tags map[string]string `json:"tags"` + } + _ = 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{"idMappingTable": 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{"idMappingTable": 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{"idMappingTable": 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + Name string `json:"name"` + Description string `json:"description"` + InputReferenceConfig map[string]any `json:"inputReferenceConfig"` + IdMappingConfig map[string]any `json:"idMappingConfig"` + Tags map[string]string `json:"tags"` + } + _ = 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{"idNamespaceAssociation": 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{"idNamespaceAssociation": 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + IdNamespaceAssociationIdentifier string `json:"idNamespaceAssociationIdentifier"` + Description string `json:"description"` + IdMappingConfig map[string]any `json:"idMappingConfig"` + } + _ = 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{"idNamespaceAssociation": 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 { + MembershipIdentifier string `json:"membershipIdentifier"` + ConfiguredAudienceModelArn string `json:"configuredAudienceModelArn"` + Name string `json:"name"` + Description string `json:"description"` + ManageResourcePolicies bool `json:"manageResourcePolicies"` + Tags map[string]string `json:"tags"` + } + _ = 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{"configuredAudienceModelAssociation": 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{"configuredAudienceModelAssociation": 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{"configuredAudienceModelAssociation": 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 { + ResourceArn string `json:"resourceArn"` + Tags map[string]string `json:"tags"` + } + _ = 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) +} From 5d69714c2545db2f5fc1742910f52c047de5af29 Mon Sep 17 00:00:00 2001 From: amber Date: Fri, 12 Jun 2026 21:44:16 -0500 Subject: [PATCH 03/16] feat(cleanrooms): implement full AWS Clean Rooms service (go-ca7c) Add services/cleanrooms with complete 88-op parity: - interfaces.go: StorageBackend interface for all resource types - backend.go: InMemoryBackend with nested maps, lockmetrics, ARN helpers - handler.go: REST path classifier + dispatch for all 88 operations - provider.go: service.Provider integration - sdk_completeness_test.go: verifies all SDK ops are covered - handler_test.go: table-driven CRUD and tag tests Register CleanRooms provider in cli.go. Co-Authored-By: Claude Sonnet 4.6 --- cli.go | 2 + services/cleanrooms/backend.go | 6 + services/cleanrooms/handler.go | 4 +- services/cleanrooms/handler_test.go | 355 +++++++++++++++++++ services/cleanrooms/provider.go | 40 +++ services/cleanrooms/sdk_completeness_test.go | 18 + 6 files changed, 422 insertions(+), 3 deletions(-) create mode 100644 services/cleanrooms/handler_test.go create mode 100644 services/cleanrooms/provider.go create mode 100644 services/cleanrooms/sdk_completeness_test.go diff --git a/cli.go b/cli.go index 3f73977a8..8b24712fd 100644 --- a/cli.go +++ b/cli.go @@ -92,6 +92,7 @@ import ( codedeploybackend "github.com/blackbirdworks/gopherstack/services/codedeploy" codepipelinebackend "github.com/blackbirdworks/gopherstack/services/codepipeline" codestarconnectionsbackend "github.com/blackbirdworks/gopherstack/services/codestarconnections" + cleanroomsbackend "github.com/blackbirdworks/gopherstack/services/cleanrooms" cognitoidentitybackend "github.com/blackbirdworks/gopherstack/services/cognitoidentity" cognitoidpbackend "github.com/blackbirdworks/gopherstack/services/cognitoidp" comprehendbackend "github.com/blackbirdworks/gopherstack/services/comprehend" @@ -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/services/cleanrooms/backend.go b/services/cleanrooms/backend.go index fa018de8b..37fcfdbd9 100644 --- a/services/cleanrooms/backend.go +++ b/services/cleanrooms/backend.go @@ -2,6 +2,7 @@ package cleanrooms import ( + "context" "fmt" "maps" "sort" @@ -448,6 +449,11 @@ type InMemoryBackend struct { tagsByArn map[string]map[string]string } +// 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{ diff --git a/services/cleanrooms/handler.go b/services/cleanrooms/handler.go index 1cca9d24d..20d931968 100644 --- a/services/cleanrooms/handler.go +++ b/services/cleanrooms/handler.go @@ -235,7 +235,7 @@ func (h *Handler) RouteMatcher() service.Matcher { } } -func (h *Handler) MatchPriority() int { return service.PriorityPathPlain } +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) @@ -989,8 +989,6 @@ func (h *Handler) dispatch(ctx context.Context, op string, body []byte, c *echo. return h.handleUpdateIdNamespaceAssociation(ctx, body) case opDeleteIdNamespaceAssociation: return h.handleDeleteIdNamespaceAssociation(ctx, body) - case opGetCollaborationIdNamespaceAssociation: - return h.handleGetCollaborationIdNamespaceAssociation(ctx, body) // ConfiguredAudienceModelAssociation case opCreateConfiguredAudienceModelAssociation: return h.handleCreateConfiguredAudienceModelAssociation(ctx, body) diff --git a/services/cleanrooms/handler_test.go b/services/cleanrooms/handler_test.go new file mode 100644 index 000000000..3be9ab61a --- /dev/null +++ b/services/cleanrooms/handler_test.go @@ -0,0 +1,355 @@ +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) (*cleanrooms.Handler, *echo.Echo) { + t.Helper() + backend := cleanrooms.NewInMemoryBackend("123456789012", "us-east-1") + h := cleanrooms.NewHandler(backend) + e := echo.New() + e.Any("/*", h.Handler()) + return h, 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() + + type tc struct { + name string + method string + path string + body any + wantStatus int + wantKey string + } + + _, e := newTestServer(t) + + createBody := map[string]any{ + "name": "test-collab", + "description": "desc", + "creatorDisplayName": "Alice", + "creatorMemberAbilities": []string{"CAN_QUERY"}, + "members": []any{}, + "queryLogStatus": "ENABLED", + } + + tests := []tc{ + { + name: "create collaboration", + method: http.MethodPost, + path: "/collaborations", + body: createBody, + wantStatus: http.StatusOK, + wantKey: "collaboration", + }, + { + name: "list collaborations", + method: http.MethodGet, + path: "/collaborations", + wantStatus: http.StatusOK, + wantKey: "collaborationList", + }, + } + + var collabID string + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := doRequest(t, e, tt.method, tt.path, tt.body) + if rec.Code != tt.wantStatus { + t.Fatalf("status %d want %d: %s", rec.Code, tt.wantStatus, rec.Body.String()) + } + var resp map[string]any + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if _, ok := resp[tt.wantKey]; !ok { + t.Fatalf("missing key %q in response: %v", tt.wantKey, resp) + } + if tt.wantKey == "collaboration" { + c := resp["collaboration"].(map[string]any) + collabID = c["collaborationIdentifier"].(string) + } + }) + } + + t.Run("get collaboration", func(t *testing.T) { + rec := doRequest(t, e, http.MethodGet, "/collaborations/"+collabID, nil) + if rec.Code != http.StatusOK { + t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) + } + }) + + t.Run("delete collaboration", func(t *testing.T) { + rec := doRequest(t, e, http.MethodDelete, "/collaborations/"+collabID, nil) + if rec.Code != http.StatusOK { + t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) + } + }) + + t.Run("get deleted collaboration returns 404", func(t *testing.T) { + rec := doRequest(t, e, http.MethodGet, "/collaborations/"+collabID, nil) + if rec.Code != http.StatusNotFound { + t.Fatalf("status %d want 404: %s", rec.Code, rec.Body.String()) + } + }) +} + +func TestConfiguredTableCRUD(t *testing.T) { + t.Parallel() + + _, e := newTestServer(t) + + type tc struct { + name string + method string + path string + body any + wantStatus int + } + + 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", + } + + tests := []tc{ + { + name: "create", + method: http.MethodPost, + path: "/configuredTables", + body: createBody, + wantStatus: http.StatusOK, + }, + { + name: "list", + method: http.MethodGet, + path: "/configuredTables", + wantStatus: http.StatusOK, + }, + } + + var ctID string + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := doRequest(t, e, tt.method, tt.path, tt.body) + if rec.Code != tt.wantStatus { + t.Fatalf("status %d want %d: %s", rec.Code, tt.wantStatus, rec.Body.String()) + } + if tt.method == http.MethodPost { + var resp map[string]any + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + ct := resp["configuredTable"].(map[string]any) + ctID = ct["configuredTableIdentifier"].(string) + } + }) + } + + t.Run("update", func(t *testing.T) { + rec := doRequest(t, e, http.MethodPatch, "/configuredTables/"+ctID, map[string]any{"name": "new-name"}) + if rec.Code != http.StatusOK { + t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) + } + }) + + t.Run("delete", func(t *testing.T) { + rec := doRequest(t, e, http.MethodDelete, "/configuredTables/"+ctID, nil) + if rec.Code != http.StatusOK { + t.Fatalf("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", + } + + type tc struct { + name string + method string + path string + body any + wantStatus int + } + + tests := []tc{ + {name: "create", method: http.MethodPost, path: "/memberships", body: createBody, wantStatus: http.StatusOK}, + {name: "list", method: http.MethodGet, path: "/memberships", wantStatus: http.StatusOK}, + } + + var mID string + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := doRequest(t, e, tt.method, tt.path, tt.body) + if rec.Code != tt.wantStatus { + t.Fatalf("status %d want %d: %s", rec.Code, tt.wantStatus, rec.Body.String()) + } + if tt.method == http.MethodPost { + var resp map[string]any + _ = json.NewDecoder(rec.Body).Decode(&resp) + mID = resp["membership"].(map[string]any)["membershipIdentifier"].(string) + + } + }) + } + + t.Run("get", func(t *testing.T) { + rec := doRequest(t, e, http.MethodGet, "/memberships/"+mID, nil) + if rec.Code != http.StatusOK { + t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) + } + }) + + t.Run("delete", func(t *testing.T) { + rec := doRequest(t, e, http.MethodDelete, "/memberships/"+mID, nil) + if rec.Code != http.StatusOK { + t.Fatalf("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" + + type tc struct { + name string + method string + path string + body any + wantStatus int + } + + tests := []tc{ + { + name: "tag resource", + method: http.MethodPost, + path: "/tags/" + testARN, + body: map[string]any{"tags": map[string]string{"env": "test"}}, + wantStatus: http.StatusOK, + }, + { + name: "list tags", + method: http.MethodGet, + path: "/tags/" + testARN, + wantStatus: http.StatusOK, + }, + { + name: "untag resource", + method: http.MethodDelete, + path: "/tags/" + testARN + "?tagKeys=env", + wantStatus: http.StatusOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := doRequest(t, e, tt.method, tt.path, tt.body) + if rec.Code != tt.wantStatus { + t.Fatalf("status %d want %d: %s", rec.Code, tt.wantStatus, rec.Body.String()) + } + }) + } +} + +func TestProtectedQueryLifecycle(t *testing.T) { + t.Parallel() + + _, e := newTestServer(t) + + // Create membership + 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) + + 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 + t.Run("start protected query", func(t *testing.T) { + 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("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 + t.Run("list protected queries", func(t *testing.T) { + rec := doRequest(t, e, http.MethodGet, "/memberships/"+mID+"/protectedQueries", nil) + if rec.Code != http.StatusOK { + t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) + } + }) +} 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{}) +} From 4c75a27ec1ad6f9553c6f7518c11aa08ae959e4b Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 22:58:56 -0500 Subject: [PATCH 04/16] WIP: checkpoint (auto) --- cli.go | 2 +- services/cleanrooms/backend.go | 156 +++++++++++------------ services/cleanrooms/handler.go | 218 ++++++++++++++++----------------- 3 files changed, 188 insertions(+), 188 deletions(-) diff --git a/cli.go b/cli.go index 8b24712fd..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" @@ -92,7 +93,6 @@ import ( codedeploybackend "github.com/blackbirdworks/gopherstack/services/codedeploy" codepipelinebackend "github.com/blackbirdworks/gopherstack/services/codepipeline" codestarconnectionsbackend "github.com/blackbirdworks/gopherstack/services/codestarconnections" - cleanroomsbackend "github.com/blackbirdworks/gopherstack/services/cleanrooms" cognitoidentitybackend "github.com/blackbirdworks/gopherstack/services/cognitoidentity" cognitoidpbackend "github.com/blackbirdworks/gopherstack/services/cognitoidp" comprehendbackend "github.com/blackbirdworks/gopherstack/services/comprehend" diff --git a/services/cleanrooms/backend.go b/services/cleanrooms/backend.go index 37fcfdbd9..73cedccd6 100644 --- a/services/cleanrooms/backend.go +++ b/services/cleanrooms/backend.go @@ -25,19 +25,19 @@ var ( // ---- types ---- type MemberSpec struct { - AccountID string `json:"accountId"` - DisplayName string `json:"displayName"` - Abilities []string `json:"memberAbilities"` - PaymentConfig map[string]any `json:"paymentConfiguration,omitempty"` + AccountID string `json:"accountId"` + DisplayName string `json:"displayName"` + Abilities []string `json:"memberAbilities"` + PaymentConfig map[string]any `json:"paymentConfiguration,omitempty"` } type MemberSummary struct { - AccountID string `json:"accountId"` - DisplayName string `json:"displayName"` - Abilities []string `json:"abilities"` - Status string `json:"status"` - CreateTime float64 `json:"createTime,omitempty"` - UpdateTime float64 `json:"updateTime,omitempty"` + AccountID string `json:"accountId"` + DisplayName string `json:"displayName"` + Abilities []string `json:"abilities"` + Status string `json:"status"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` } type Collaboration struct { @@ -67,34 +67,34 @@ type CollaborationSummary struct { } type Membership 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"` - QueryLogStatus string `json:"queryLogStatus,omitempty"` - DefaultResultConfiguration map[string]any `json:"defaultResultConfiguration,omitempty"` - PaymentConfiguration map[string]any `json:"paymentConfiguration,omitempty"` - CreateTime float64 `json:"createTime,omitempty"` - UpdateTime float64 `json:"updateTime,omitempty"` + 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"` + QueryLogStatus string `json:"queryLogStatus,omitempty"` + DefaultResultConfiguration map[string]any `json:"defaultResultConfiguration,omitempty"` + PaymentConfiguration map[string]any `json:"paymentConfiguration,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"` + 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"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` } type ConfiguredTable struct { @@ -112,13 +112,13 @@ type ConfiguredTable struct { } type ConfiguredTableSummary struct { - ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` - Arn string `json:"arn"` - Name string `json:"name"` - AnalysisMethod string `json:"analysisMethod,omitempty"` + 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"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` } type ConfiguredTableAnalysisRule struct { @@ -206,17 +206,17 @@ type BatchError struct { } type Schema struct { - CollaborationArn string `json:"collaborationArn"` - CollaborationIdentifier string `json:"collaborationIdentifier"` - CreatorAccountId string `json:"creatorAccountId"` - Name string `json:"name"` - Type string `json:"type"` + CollaborationArn string `json:"collaborationArn"` + CollaborationIdentifier string `json:"collaborationIdentifier"` + CreatorAccountId string `json:"creatorAccountId"` + Name string `json:"name"` + Type string `json:"type"` Columns []map[string]any `json:"columns,omitempty"` PartitionKeys []map[string]any `json:"partitionKeys,omitempty"` - AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` - AnalysisMethod string `json:"analysisMethod,omitempty"` - CreateTime float64 `json:"createTime,omitempty"` - UpdateTime float64 `json:"updateTime,omitempty"` + AnalysisRuleTypes []string `json:"analysisRuleTypes,omitempty"` + AnalysisMethod string `json:"analysisMethod,omitempty"` + CreateTime float64 `json:"createTime,omitempty"` + UpdateTime float64 `json:"updateTime,omitempty"` } type SchemaSummary struct { @@ -430,23 +430,23 @@ type InMemoryBackend struct { accountID string region string - 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 - analysisTemplates map[string]map[string]*AnalysisTemplate - protectedQueries map[string]map[string]*ProtectedQuery - protectedJobs map[string]map[string]*ProtectedJob - privacyBudgetTemplates map[string]map[string]*PrivacyBudgetTemplate - 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 - tagsByArn map[string]map[string]string + 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 + analysisTemplates map[string]map[string]*AnalysisTemplate + protectedQueries map[string]map[string]*ProtectedQuery + protectedJobs map[string]map[string]*ProtectedJob + privacyBudgetTemplates map[string]map[string]*PrivacyBudgetTemplate + 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 + tagsByArn map[string]map[string]string } // NewInMemoryBackendWithContext creates a backend tied to svcCtx (ignored; no lifecycle goroutines). @@ -2144,18 +2144,18 @@ func (b *InMemoryBackend) CreateConfiguredAudienceModelAssociation(membershipID, } 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, + 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 { diff --git a/services/cleanrooms/handler.go b/services/cleanrooms/handler.go index 20d931968..171695688 100644 --- a/services/cleanrooms/handler.go +++ b/services/cleanrooms/handler.go @@ -17,95 +17,95 @@ import ( 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" + 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 = "" + 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") @@ -295,8 +295,8 @@ func (h *Handler) handleError(c *echo.Context, err error) error { // pathRouteEntry holds a parsed path classification. type pathRouteEntry struct { - op string - seg []string + op string + seg []string } // classifyPath maps (method, path) to an operation name and primary resource. @@ -1167,7 +1167,7 @@ func (h *Handler) handleBatchGetSchema(_ context.Context, body []byte) ([]byte, func (h *Handler) handleBatchGetSchemaAnalysisRule(_ context.Context, body []byte) ([]byte, error) { var req struct { - CollaborationIdentifier string `json:"collaborationIdentifier"` + CollaborationIdentifier string `json:"collaborationIdentifier"` SchemaAnalysisRuleRequests []struct { Name string `json:"name"` Type string `json:"type"` @@ -1457,12 +1457,12 @@ func (h *Handler) handleDeleteMembership(_ context.Context, body []byte) ([]byte func (h *Handler) handleCreateConfiguredTable(_ context.Context, body []byte) ([]byte, error) { var req struct { - Name string `json:"name"` - Description string `json:"description"` - TableReference map[string]any `json:"tableReference"` - AllowedColumns []string `json:"allowedColumns"` - AnalysisMethod string `json:"analysisMethod"` - Tags map[string]string `json:"tags"` + Name string `json:"name"` + Description string `json:"description"` + TableReference map[string]any `json:"tableReference"` + AllowedColumns []string `json:"allowedColumns"` + AnalysisMethod string `json:"analysisMethod"` + Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) ct, err := h.Backend.CreateConfiguredTable(req.Name, req.Description, req.TableReference, req.AllowedColumns, req.AnalysisMethod, req.Tags) @@ -1792,7 +1792,7 @@ func (h *Handler) handleStartProtectedQuery(_ context.Context, body []byte) ([]b func (h *Handler) handleGetProtectedQuery(_ context.Context, body []byte) ([]byte, error) { var req struct { - MembershipIdentifier string `json:"membershipIdentifier"` + MembershipIdentifier string `json:"membershipIdentifier"` ProtectedQueryIdentifier string `json:"protectedQueryIdentifier"` } _ = json.Unmarshal(body, &req) @@ -1821,9 +1821,9 @@ func (h *Handler) handleListProtectedQueries(_ context.Context, body []byte, c * func (h *Handler) handleUpdateProtectedQuery(_ context.Context, body []byte) ([]byte, error) { var req struct { - MembershipIdentifier string `json:"membershipIdentifier"` + MembershipIdentifier string `json:"membershipIdentifier"` ProtectedQueryIdentifier string `json:"protectedQueryIdentifier"` - TargetStatus string `json:"targetStatus"` + TargetStatus string `json:"targetStatus"` } _ = json.Unmarshal(body, &req) q, err := h.Backend.UpdateProtectedQuery(req.MembershipIdentifier, req.ProtectedQueryIdentifier, req.TargetStatus) @@ -1852,7 +1852,7 @@ func (h *Handler) handleStartProtectedJob(_ context.Context, body []byte) ([]byt func (h *Handler) handleGetProtectedJob(_ context.Context, body []byte) ([]byte, error) { var req struct { - MembershipIdentifier string `json:"membershipIdentifier"` + MembershipIdentifier string `json:"membershipIdentifier"` ProtectedJobIdentifier string `json:"protectedJobIdentifier"` } _ = json.Unmarshal(body, &req) @@ -1881,9 +1881,9 @@ func (h *Handler) handleListProtectedJobs(_ context.Context, body []byte, c *ech func (h *Handler) handleUpdateProtectedJob(_ context.Context, body []byte) ([]byte, error) { var req struct { - MembershipIdentifier string `json:"membershipIdentifier"` + MembershipIdentifier string `json:"membershipIdentifier"` ProtectedJobIdentifier string `json:"protectedJobIdentifier"` - TargetStatus string `json:"targetStatus"` + TargetStatus string `json:"targetStatus"` } _ = json.Unmarshal(body, &req) j, err := h.Backend.UpdateProtectedJob(req.MembershipIdentifier, req.ProtectedJobIdentifier, req.TargetStatus) @@ -2154,12 +2154,12 @@ func (h *Handler) handleDeleteIdNamespaceAssociation(_ context.Context, body []b func (h *Handler) handleCreateConfiguredAudienceModelAssociation(_ context.Context, body []byte) ([]byte, error) { var req struct { - MembershipIdentifier string `json:"membershipIdentifier"` - ConfiguredAudienceModelArn string `json:"configuredAudienceModelArn"` - Name string `json:"name"` - Description string `json:"description"` - ManageResourcePolicies bool `json:"manageResourcePolicies"` - 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"` + Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) a, err := h.Backend.CreateConfiguredAudienceModelAssociation(req.MembershipIdentifier, req.ConfiguredAudienceModelArn, req.Name, req.Description, req.ManageResourcePolicies, req.Tags) From abdce3fcbe5477494883dc5c6aac2c043d7334c5 Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 23:36:42 -0500 Subject: [PATCH 05/16] WIP: checkpoint (auto) --- services/cleanrooms/backend.go | 701 ++++++++++++++++++-------- services/cleanrooms/handler.go | 746 +++++++++++++++++++++++----- services/cleanrooms/handler_test.go | 31 +- services/cleanrooms/interfaces.go | 237 +++++++-- 4 files changed, 1322 insertions(+), 393 deletions(-) diff --git a/services/cleanrooms/backend.go b/services/cleanrooms/backend.go index 73cedccd6..e117c05bb 100644 --- a/services/cleanrooms/backend.go +++ b/services/cleanrooms/backend.go @@ -457,15 +457,17 @@ func NewInMemoryBackendWithContext(_ context.Context, accountID, region string) // 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), + 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), @@ -517,26 +519,97 @@ 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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) } -// ---- pagination helper ---- - func paginate[T any](items []T, maxResultsStr, nextToken string) ([]T, string) { if len(items) == 0 { return items, "" @@ -575,7 +648,13 @@ func now() float64 { // ---- Collaboration ---- -func (b *InMemoryBackend) CreateCollaboration(name, description, creatorDisplayName string, creatorMemberAbilities []string, members []MemberSpec, queryLogStatus string, tags map[string]string) (*Collaboration, error) { +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 == "" { @@ -633,7 +712,9 @@ func (b *InMemoryBackend) GetCollaboration(id string) (*Collaboration, error) { return c, nil } -func (b *InMemoryBackend) ListCollaborations(memberStatus, maxResults, nextToken string) ([]*CollaborationSummary, string) { +func (b *InMemoryBackend) ListCollaborations( + memberStatus, maxResults, nextToken string, +) ([]*CollaborationSummary, string) { b.mu.RLock("ListCollaborations") defer b.mu.RUnlock() var items []*CollaborationSummary @@ -649,12 +730,17 @@ func (b *InMemoryBackend) ListCollaborations(memberStatus, maxResults, nextToken UpdateTime: c.UpdateTime, }) } - sort.Slice(items, func(i, j int) bool { return items[i].CollaborationIdentifier < items[j].CollaborationIdentifier }) + 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) { +func (b *InMemoryBackend) UpdateCollaboration( + id, name, description string, +) (*Collaboration, error) { b.mu.Lock("UpdateCollaboration") defer b.mu.Unlock() c, ok := b.collaborations[id] @@ -683,7 +769,10 @@ func (b *InMemoryBackend) DeleteCollaboration(id string) error { return nil } -func (b *InMemoryBackend) ListMembers(collaborationID string, maxResults, nextToken string) ([]*MemberSummary, string, error) { +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] @@ -714,7 +803,12 @@ func (b *InMemoryBackend) DeleteMember(collaborationID, accountID string) error // ---- Membership ---- -func (b *InMemoryBackend) CreateMembership(collaborationID, queryLogStatus string, defaultResultConfiguration map[string]any, paymentConfiguration map[string]any, tags map[string]string) (*Membership, error) { +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 == "" { @@ -758,7 +852,9 @@ func (b *InMemoryBackend) GetMembership(id string) (*Membership, error) { return m, nil } -func (b *InMemoryBackend) ListMemberships(status, maxResults, nextToken string) ([]*MembershipSummary, string) { +func (b *InMemoryBackend) ListMemberships( + status, maxResults, nextToken string, +) ([]*MembershipSummary, string) { b.mu.RLock("ListMemberships") defer b.mu.RUnlock() var items []*MembershipSummary @@ -780,12 +876,18 @@ func (b *InMemoryBackend) ListMemberships(status, maxResults, nextToken string) UpdateTime: m.UpdateTime, }) } - sort.Slice(items, func(i, j int) bool { return items[i].MembershipIdentifier < items[j].MembershipIdentifier }) + 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) { +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] @@ -816,7 +918,13 @@ func (b *InMemoryBackend) DeleteMembership(id string) error { // ---- ConfiguredTable ---- -func (b *InMemoryBackend) CreateConfiguredTable(name, description string, tableReference map[string]any, allowedColumns []string, analysisMethod string, tags map[string]string) (*ConfiguredTable, error) { +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 == "" { @@ -853,7 +961,9 @@ func (b *InMemoryBackend) GetConfiguredTable(id string) (*ConfiguredTable, error return ct, nil } -func (b *InMemoryBackend) ListConfiguredTables(maxResults, nextToken string) ([]*ConfiguredTableSummary, string) { +func (b *InMemoryBackend) ListConfiguredTables( + maxResults, nextToken string, +) ([]*ConfiguredTableSummary, string) { b.mu.RLock("ListConfiguredTables") defer b.mu.RUnlock() var items []*ConfiguredTableSummary @@ -868,12 +978,17 @@ func (b *InMemoryBackend) ListConfiguredTables(maxResults, nextToken string) ([] UpdateTime: ct.UpdateTime, }) } - sort.Slice(items, func(i, j int) bool { return items[i].ConfiguredTableIdentifier < items[j].ConfiguredTableIdentifier }) + 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) { +func (b *InMemoryBackend) UpdateConfiguredTable( + id, name, description string, +) (*ConfiguredTable, error) { b.mu.Lock("UpdateConfiguredTable") defer b.mu.Unlock() ct, ok := b.configuredTables[id] @@ -905,7 +1020,10 @@ func (b *InMemoryBackend) DeleteConfiguredTable(id string) error { // ---- ConfiguredTableAnalysisRule ---- -func (b *InMemoryBackend) CreateConfiguredTableAnalysisRule(configuredTableID, analysisRuleType string, policy map[string]any) (*ConfiguredTableAnalysisRule, error) { +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] @@ -934,7 +1052,9 @@ func (b *InMemoryBackend) CreateConfiguredTableAnalysisRule(configuredTableID, a return rule, nil } -func (b *InMemoryBackend) GetConfiguredTableAnalysisRule(configuredTableID, analysisRuleType string) (*ConfiguredTableAnalysisRule, error) { +func (b *InMemoryBackend) GetConfiguredTableAnalysisRule( + configuredTableID, analysisRuleType string, +) (*ConfiguredTableAnalysisRule, error) { b.mu.RLock("GetConfiguredTableAnalysisRule") defer b.mu.RUnlock() rules, ok := b.ctAnalysisRules[configuredTableID] @@ -948,7 +1068,10 @@ func (b *InMemoryBackend) GetConfiguredTableAnalysisRule(configuredTableID, anal return rule, nil } -func (b *InMemoryBackend) UpdateConfiguredTableAnalysisRule(configuredTableID, analysisRuleType string, policy map[string]any) (*ConfiguredTableAnalysisRule, error) { +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] @@ -964,7 +1087,9 @@ func (b *InMemoryBackend) UpdateConfiguredTableAnalysisRule(configuredTableID, a return rule, nil } -func (b *InMemoryBackend) DeleteConfiguredTableAnalysisRule(configuredTableID, analysisRuleType string) error { +func (b *InMemoryBackend) DeleteConfiguredTableAnalysisRule( + configuredTableID, analysisRuleType string, +) error { b.mu.Lock("DeleteConfiguredTableAnalysisRule") defer b.mu.Unlock() rules, ok := b.ctAnalysisRules[configuredTableID] @@ -983,7 +1108,10 @@ func (b *InMemoryBackend) DeleteConfiguredTableAnalysisRule(configuredTableID, a // ---- ConfiguredTableAssociation ---- -func (b *InMemoryBackend) CreateConfiguredTableAssociation(membershipID, name, description, configuredTableID, roleArn string, tags map[string]string) (*ConfiguredTableAssociation, error) { +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] @@ -1020,7 +1148,9 @@ func (b *InMemoryBackend) CreateConfiguredTableAssociation(membershipID, name, d return assoc, nil } -func (b *InMemoryBackend) GetConfiguredTableAssociation(membershipID, assocID string) (*ConfiguredTableAssociation, error) { +func (b *InMemoryBackend) GetConfiguredTableAssociation( + membershipID, assocID string, +) (*ConfiguredTableAssociation, error) { b.mu.RLock("GetConfiguredTableAssociation") defer b.mu.RUnlock() assocs, ok := b.ctAssociations[membershipID] @@ -1034,7 +1164,9 @@ func (b *InMemoryBackend) GetConfiguredTableAssociation(membershipID, assocID st return assoc, nil } -func (b *InMemoryBackend) ListConfiguredTableAssociations(membershipID, maxResults, nextToken string) ([]*ConfiguredTableAssociationSummary, string, error) { +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 { @@ -1060,7 +1192,9 @@ func (b *InMemoryBackend) ListConfiguredTableAssociations(membershipID, maxResul return page, next, nil } -func (b *InMemoryBackend) UpdateConfiguredTableAssociation(membershipID, assocID, description, roleArn string) (*ConfiguredTableAssociation, error) { +func (b *InMemoryBackend) UpdateConfiguredTableAssociation( + membershipID, assocID, description, roleArn string, +) (*ConfiguredTableAssociation, error) { b.mu.Lock("UpdateConfiguredTableAssociation") defer b.mu.Unlock() assocs, ok := b.ctAssociations[membershipID] @@ -1100,7 +1234,10 @@ func (b *InMemoryBackend) DeleteConfiguredTableAssociation(membershipID, assocID // ---- ConfiguredTableAssociationAnalysisRule ---- -func (b *InMemoryBackend) CreateConfiguredTableAssociationAnalysisRule(membershipID, assocID, ruleType string, policy map[string]any) (*ConfiguredTableAssociationAnalysisRule, error) { +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] @@ -1136,7 +1273,9 @@ func (b *InMemoryBackend) CreateConfiguredTableAssociationAnalysisRule(membershi return rule, nil } -func (b *InMemoryBackend) GetConfiguredTableAssociationAnalysisRule(membershipID, assocID, ruleType string) (*ConfiguredTableAssociationAnalysisRule, error) { +func (b *InMemoryBackend) GetConfiguredTableAssociationAnalysisRule( + membershipID, assocID, ruleType string, +) (*ConfiguredTableAssociationAnalysisRule, error) { b.mu.RLock("GetConfiguredTableAssociationAnalysisRule") defer b.mu.RUnlock() rules, ok := b.ctaAnalysisRules[assocID] @@ -1150,7 +1289,10 @@ func (b *InMemoryBackend) GetConfiguredTableAssociationAnalysisRule(membershipID return rule, nil } -func (b *InMemoryBackend) UpdateConfiguredTableAssociationAnalysisRule(membershipID, assocID, ruleType string, policy map[string]any) (*ConfiguredTableAssociationAnalysisRule, error) { +func (b *InMemoryBackend) UpdateConfiguredTableAssociationAnalysisRule( + membershipID, assocID, ruleType string, + policy map[string]any, +) (*ConfiguredTableAssociationAnalysisRule, error) { b.mu.Lock("UpdateConfiguredTableAssociationAnalysisRule") defer b.mu.Unlock() rules, ok := b.ctaAnalysisRules[assocID] @@ -1166,7 +1308,9 @@ func (b *InMemoryBackend) UpdateConfiguredTableAssociationAnalysisRule(membershi return rule, nil } -func (b *InMemoryBackend) DeleteConfiguredTableAssociationAnalysisRule(membershipID, assocID, ruleType string) error { +func (b *InMemoryBackend) DeleteConfiguredTableAssociationAnalysisRule( + membershipID, assocID, ruleType string, +) error { b.mu.Lock("DeleteConfiguredTableAssociationAnalysisRule") defer b.mu.Unlock() rules, ok := b.ctaAnalysisRules[assocID] @@ -1187,7 +1331,12 @@ func (b *InMemoryBackend) DeleteConfiguredTableAssociationAnalysisRule(membershi // ---- AnalysisTemplate ---- -func (b *InMemoryBackend) CreateAnalysisTemplate(membershipID, name, description, format string, source map[string]any, analysisParameters []map[string]any, tags map[string]string) (*AnalysisTemplate, error) { +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] @@ -1227,7 +1376,9 @@ func (b *InMemoryBackend) CreateAnalysisTemplate(membershipID, name, description return tmpl, nil } -func (b *InMemoryBackend) GetAnalysisTemplate(membershipID, templateID string) (*AnalysisTemplate, error) { +func (b *InMemoryBackend) GetAnalysisTemplate( + membershipID, templateID string, +) (*AnalysisTemplate, error) { b.mu.RLock("GetAnalysisTemplate") defer b.mu.RUnlock() tmpls, ok := b.analysisTemplates[membershipID] @@ -1241,32 +1392,41 @@ func (b *InMemoryBackend) GetAnalysisTemplate(membershipID, templateID string) ( return tmpl, nil } -func (b *InMemoryBackend) ListAnalysisTemplates(membershipID, maxResults, nextToken string) ([]*AnalysisTemplateSummary, string, error) { +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 } - var items []*AnalysisTemplateSummary - for _, t := range b.analysisTemplates[membershipID] { - items = append(items, &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, - }) - } - sort.Slice(items, func(i, j int) bool { return items[i].AnalysisTemplateIdentifier < items[j].AnalysisTemplateIdentifier }) - page, next := paginate(items, maxResults, nextToken) + page, next := listItems( + b.analysisTemplates[membershipID], + nil, + func(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(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) { +func (b *InMemoryBackend) UpdateAnalysisTemplate( + membershipID, templateID, description string, +) (*AnalysisTemplate, error) { b.mu.Lock("UpdateAnalysisTemplate") defer b.mu.Unlock() tmpls, ok := b.analysisTemplates[membershipID] @@ -1298,7 +1458,9 @@ func (b *InMemoryBackend) DeleteAnalysisTemplate(membershipID, templateID string return nil } -func (b *InMemoryBackend) GetCollaborationAnalysisTemplate(collaborationID, templateArn string) (*AnalysisTemplate, error) { +func (b *InMemoryBackend) GetCollaborationAnalysisTemplate( + collaborationID, templateArn string, +) (*AnalysisTemplate, error) { b.mu.RLock("GetCollaborationAnalysisTemplate") defer b.mu.RUnlock() for _, tmpls := range b.analysisTemplates { @@ -1311,36 +1473,42 @@ func (b *InMemoryBackend) GetCollaborationAnalysisTemplate(collaborationID, temp return nil, ErrNotFound } -func (b *InMemoryBackend) ListCollaborationAnalysisTemplates(collaborationID, maxResults, nextToken string) ([]*AnalysisTemplateSummary, string, error) { +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 } - var items []*AnalysisTemplateSummary - for _, tmpls := range b.analysisTemplates { - for _, t := range tmpls { - if t.CollaborationIdentifier == collaborationID { - items = append(items, &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, - }) + page, next := listNestedItems( + b.analysisTemplates, + func(t *AnalysisTemplate) bool { return t.CollaborationIdentifier == collaborationID }, + func(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, } - } - } - sort.Slice(items, func(i, j int) bool { return items[i].AnalysisTemplateIdentifier < items[j].AnalysisTemplateIdentifier }) - page, next := paginate(items, maxResults, nextToken) + }, + 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) { +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 { @@ -1363,7 +1531,10 @@ func (b *InMemoryBackend) BatchGetCollaborationAnalysisTemplate(collaborationID } } if !found { - errors = append(errors, BatchError{Arn: arnStr, Code: "ResourceNotFoundException", Message: "not found"}) + errors = append( + errors, + BatchError{Arn: arnStr, Code: "ResourceNotFoundException", Message: "not found"}, + ) } } return results, errors, nil @@ -1385,35 +1556,40 @@ func (b *InMemoryBackend) GetSchema(collaborationID, name string) (*Schema, erro return s, nil } -func (b *InMemoryBackend) ListSchemas(collaborationID, schemaType, maxResults, nextToken string) ([]*SchemaSummary, string, error) { +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 } - var items []*SchemaSummary - for _, s := range b.schemas[collaborationID] { - if schemaType != "" && s.Type != schemaType { - continue - } - items = append(items, &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, - }) - } - sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name }) - page, next := paginate(items, maxResults, nextToken) + page, next := listItems( + b.schemas[collaborationID], + func(s *Schema) bool { return schemaType == "" || s.Type == schemaType }, + func(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(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) { +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 { @@ -1432,7 +1608,9 @@ func (b *InMemoryBackend) BatchGetSchema(collaborationID string, names []string) return results, errors, nil } -func (b *InMemoryBackend) GetSchemaAnalysisRule(collaborationID, name, ruleType string) (*SchemaAnalysisRule, error) { +func (b *InMemoryBackend) GetSchemaAnalysisRule( + collaborationID, name, ruleType string, +) (*SchemaAnalysisRule, error) { b.mu.RLock("GetSchemaAnalysisRule") defer b.mu.RUnlock() collabRules, ok := b.schemaAnalysisRules[collaborationID] @@ -1450,7 +1628,11 @@ func (b *InMemoryBackend) GetSchemaAnalysisRule(collaborationID, name, ruleType return rule, nil } -func (b *InMemoryBackend) BatchGetSchemaAnalysisRule(collaborationID string, names []string, ruleType string) ([]*SchemaAnalysisRule, []BatchError, error) { +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 { @@ -1468,14 +1650,21 @@ func (b *InMemoryBackend) BatchGetSchemaAnalysisRule(collaborationID string, nam } } } - errors = append(errors, BatchError{Name: name, Code: "ResourceNotFoundException", Message: "not found"}) + errors = append( + errors, + BatchError{Name: name, Code: "ResourceNotFoundException", Message: "not found"}, + ) } return results, errors, nil } // ---- ProtectedQuery ---- -func (b *InMemoryBackend) StartProtectedQuery(membershipID, sqlText string, resultConfig map[string]any, computeConfiguration map[string]any) (*ProtectedQuery, error) { +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] @@ -1519,7 +1708,9 @@ func (b *InMemoryBackend) GetProtectedQuery(membershipID, queryID string) (*Prot return q, nil } -func (b *InMemoryBackend) ListProtectedQueries(membershipID, status, maxResults, nextToken string) ([]*ProtectedQuerySummary, string, error) { +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 { @@ -1543,7 +1734,9 @@ func (b *InMemoryBackend) ListProtectedQueries(membershipID, status, maxResults, return page, next, nil } -func (b *InMemoryBackend) UpdateProtectedQuery(membershipID, queryID, status string) (*ProtectedQuery, error) { +func (b *InMemoryBackend) UpdateProtectedQuery( + membershipID, queryID, status string, +) (*ProtectedQuery, error) { b.mu.Lock("UpdateProtectedQuery") defer b.mu.Unlock() queries, ok := b.protectedQueries[membershipID] @@ -1560,7 +1753,11 @@ func (b *InMemoryBackend) UpdateProtectedQuery(membershipID, queryID, status str // ---- ProtectedJob ---- -func (b *InMemoryBackend) StartProtectedJob(membershipID, jobType string, jobParameters map[string]any, resultConfig map[string]any) (*ProtectedJob, error) { +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] @@ -1599,7 +1796,9 @@ func (b *InMemoryBackend) GetProtectedJob(membershipID, jobID string) (*Protecte return j, nil } -func (b *InMemoryBackend) ListProtectedJobs(membershipID, status, maxResults, nextToken string) ([]*ProtectedJobSummary, string, error) { +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 { @@ -1624,7 +1823,9 @@ func (b *InMemoryBackend) ListProtectedJobs(membershipID, status, maxResults, ne return page, next, nil } -func (b *InMemoryBackend) UpdateProtectedJob(membershipID, jobID, status string) (*ProtectedJob, error) { +func (b *InMemoryBackend) UpdateProtectedJob( + membershipID, jobID, status string, +) (*ProtectedJob, error) { b.mu.Lock("UpdateProtectedJob") defer b.mu.Unlock() jobs, ok := b.protectedJobs[membershipID] @@ -1641,7 +1842,11 @@ func (b *InMemoryBackend) UpdateProtectedJob(membershipID, jobID, status string) // ---- PrivacyBudgetTemplate ---- -func (b *InMemoryBackend) CreatePrivacyBudgetTemplate(membershipID, privacyBudgetType, autoRefresh string, parameters map[string]any, tags map[string]string) (*PrivacyBudgetTemplate, error) { +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] @@ -1679,7 +1884,9 @@ func (b *InMemoryBackend) CreatePrivacyBudgetTemplate(membershipID, privacyBudge return tmpl, nil } -func (b *InMemoryBackend) GetPrivacyBudgetTemplate(membershipID, templateID string) (*PrivacyBudgetTemplate, error) { +func (b *InMemoryBackend) GetPrivacyBudgetTemplate( + membershipID, templateID string, +) (*PrivacyBudgetTemplate, error) { b.mu.RLock("GetPrivacyBudgetTemplate") defer b.mu.RUnlock() tmpls, ok := b.privacyBudgetTemplates[membershipID] @@ -1693,37 +1900,44 @@ func (b *InMemoryBackend) GetPrivacyBudgetTemplate(membershipID, templateID stri return tmpl, nil } -func (b *InMemoryBackend) ListPrivacyBudgetTemplates(membershipID, privacyBudgetType, maxResults, nextToken string) ([]*PrivacyBudgetTemplateSummary, string, error) { +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 } - var items []*PrivacyBudgetTemplateSummary - for _, t := range b.privacyBudgetTemplates[membershipID] { - if privacyBudgetType != "" && t.PrivacyBudgetType != privacyBudgetType { - continue - } - items = append(items, &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, - }) - } - sort.Slice(items, func(i, j int) bool { - return items[i].PrivacyBudgetTemplateIdentifier < items[j].PrivacyBudgetTemplateIdentifier - }) - page, next := paginate(items, maxResults, nextToken) + page, next := listItems( + b.privacyBudgetTemplates[membershipID], + func(t *PrivacyBudgetTemplate) bool { + return privacyBudgetType == "" || t.PrivacyBudgetType == privacyBudgetType + }, + func(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(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) { +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] @@ -1760,7 +1974,9 @@ func (b *InMemoryBackend) DeletePrivacyBudgetTemplate(membershipID, templateID s return nil } -func (b *InMemoryBackend) ListPrivacyBudgets(membershipID, privacyBudgetType, maxResults, nextToken string) ([]*PrivacyBudget, string, error) { +func (b *InMemoryBackend) ListPrivacyBudgets( + membershipID, privacyBudgetType, maxResults, nextToken string, +) ([]*PrivacyBudget, string, error) { b.mu.RLock("ListPrivacyBudgets") defer b.mu.RUnlock() if _, ok := b.memberships[membershipID]; !ok { @@ -1769,7 +1985,9 @@ func (b *InMemoryBackend) ListPrivacyBudgets(membershipID, privacyBudgetType, ma return []*PrivacyBudget{}, "", nil } -func (b *InMemoryBackend) ListCollaborationPrivacyBudgets(collaborationID, privacyBudgetType, maxResults, nextToken string) ([]*PrivacyBudget, string, error) { +func (b *InMemoryBackend) ListCollaborationPrivacyBudgets( + collaborationID, privacyBudgetType, maxResults, nextToken string, +) ([]*PrivacyBudget, string, error) { b.mu.RLock("ListCollaborationPrivacyBudgets") defer b.mu.RUnlock() if _, ok := b.collaborations[collaborationID]; !ok { @@ -1778,12 +1996,15 @@ func (b *InMemoryBackend) ListCollaborationPrivacyBudgets(collaborationID, priva return []*PrivacyBudget{}, "", nil } -func (b *InMemoryBackend) GetCollaborationPrivacyBudgetTemplate(collaborationID, templateID string) (*PrivacyBudgetTemplate, error) { +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 { + if t.CollaborationIdentifier == collaborationID && + t.PrivacyBudgetTemplateIdentifier == templateID { return t, nil } } @@ -1791,38 +2012,42 @@ func (b *InMemoryBackend) GetCollaborationPrivacyBudgetTemplate(collaborationID, return nil, ErrNotFound } -func (b *InMemoryBackend) ListCollaborationPrivacyBudgetTemplates(collaborationID, maxResults, nextToken string) ([]*PrivacyBudgetTemplateSummary, string, error) { +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 } - var items []*PrivacyBudgetTemplateSummary - for _, tmpls := range b.privacyBudgetTemplates { - for _, t := range tmpls { - if t.CollaborationIdentifier == collaborationID { - items = append(items, &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, - }) + page, next := listNestedItems( + b.privacyBudgetTemplates, + func(t *PrivacyBudgetTemplate) bool { return t.CollaborationIdentifier == collaborationID }, + func(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, } - } - } - sort.Slice(items, func(i, j int) bool { - return items[i].PrivacyBudgetTemplateIdentifier < items[j].PrivacyBudgetTemplateIdentifier - }) - page, next := paginate(items, maxResults, nextToken) + }, + func(a, c *PrivacyBudgetTemplateSummary) bool { + return a.PrivacyBudgetTemplateIdentifier < c.PrivacyBudgetTemplateIdentifier + }, + maxResults, nextToken, + ) return page, next, nil } -func (b *InMemoryBackend) PreviewPrivacyImpact(membershipID string, parameters map[string]any) (map[string]any, error) { +func (b *InMemoryBackend) PreviewPrivacyImpact( + membershipID string, + parameters map[string]any, +) (map[string]any, error) { b.mu.RLock("PreviewPrivacyImpact") defer b.mu.RUnlock() if _, ok := b.memberships[membershipID]; !ok { @@ -1833,7 +2058,12 @@ func (b *InMemoryBackend) PreviewPrivacyImpact(membershipID string, parameters m // ---- IdMappingTable ---- -func (b *InMemoryBackend) CreateIdMappingTable(membershipID, name, description string, inputReferenceConfig map[string]any, kmsKeyArn string, tags map[string]string) (*IdMappingTable, error) { +func (b *InMemoryBackend) CreateIdMappingTable( + membershipID, name, description string, + inputReferenceConfig map[string]any, + kmsKeyArn string, + tags map[string]string, +) (*IdMappingTable, error) { b.mu.Lock("CreateIdMappingTable") defer b.mu.Unlock() mem, ok := b.memberships[membershipID] @@ -1886,32 +2116,41 @@ func (b *InMemoryBackend) GetIdMappingTable(membershipID, tableID string) (*IdMa return t, nil } -func (b *InMemoryBackend) ListIdMappingTables(membershipID, maxResults, nextToken string) ([]*IdMappingTableSummary, string, error) { +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 } - var items []*IdMappingTableSummary - for _, t := range b.idMappingTables[membershipID] { - items = append(items, &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, - }) - } - sort.Slice(items, func(i, j int) bool { return items[i].IdMappingTableIdentifier < items[j].IdMappingTableIdentifier }) - page, next := paginate(items, maxResults, nextToken) + page, next := listItems( + b.idMappingTables[membershipID], + nil, + func(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(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) { +func (b *InMemoryBackend) UpdateIdMappingTable( + membershipID, tableID, description, kmsKeyArn string, +) (*IdMappingTable, error) { b.mu.Lock("UpdateIdMappingTable") defer b.mu.Unlock() tables, ok := b.idMappingTables[membershipID] @@ -1948,7 +2187,9 @@ func (b *InMemoryBackend) DeleteIdMappingTable(membershipID, tableID string) err return nil } -func (b *InMemoryBackend) PopulateIdMappingTable(membershipID, tableID string) (map[string]any, error) { +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 { @@ -1962,7 +2203,12 @@ func (b *InMemoryBackend) PopulateIdMappingTable(membershipID, tableID string) ( // ---- IdNamespaceAssociation ---- -func (b *InMemoryBackend) CreateIdNamespaceAssociation(membershipID, name, description string, inputReferenceConfig map[string]any, idMappingConfig map[string]any, tags map[string]string) (*IdNamespaceAssociation, error) { +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] @@ -2001,7 +2247,9 @@ func (b *InMemoryBackend) CreateIdNamespaceAssociation(membershipID, name, descr return assoc, nil } -func (b *InMemoryBackend) GetIdNamespaceAssociation(membershipID, assocID string) (*IdNamespaceAssociation, error) { +func (b *InMemoryBackend) GetIdNamespaceAssociation( + membershipID, assocID string, +) (*IdNamespaceAssociation, error) { b.mu.RLock("GetIdNamespaceAssociation") defer b.mu.RUnlock() assocs, ok := b.idNamespaceAssociations[membershipID] @@ -2015,7 +2263,9 @@ func (b *InMemoryBackend) GetIdNamespaceAssociation(membershipID, assocID string return assoc, nil } -func (b *InMemoryBackend) ListIdNamespaceAssociations(membershipID, maxResults, nextToken string) ([]*IdNamespaceAssociationSummary, string, error) { +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 { @@ -2042,7 +2292,10 @@ func (b *InMemoryBackend) ListIdNamespaceAssociations(membershipID, maxResults, return page, next, nil } -func (b *InMemoryBackend) UpdateIdNamespaceAssociation(membershipID, assocID, description string, idMappingConfig map[string]any) (*IdNamespaceAssociation, error) { +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] @@ -2079,12 +2332,15 @@ func (b *InMemoryBackend) DeleteIdNamespaceAssociation(membershipID, assocID str return nil } -func (b *InMemoryBackend) GetCollaborationIdNamespaceAssociation(collaborationID, assocID string) (*IdNamespaceAssociation, error) { +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 { + if a.CollaborationIdentifier == collaborationID && + a.IdNamespaceAssociationIdentifier == assocID { return a, nil } } @@ -2092,7 +2348,9 @@ func (b *InMemoryBackend) GetCollaborationIdNamespaceAssociation(collaborationID return nil, ErrNotFound } -func (b *InMemoryBackend) ListCollaborationIdNamespaceAssociations(collaborationID, maxResults, nextToken string) ([]*IdNamespaceAssociationSummary, string, error) { +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 { @@ -2125,7 +2383,11 @@ func (b *InMemoryBackend) ListCollaborationIdNamespaceAssociations(collaboration // ---- ConfiguredAudienceModelAssociation ---- -func (b *InMemoryBackend) CreateConfiguredAudienceModelAssociation(membershipID, configuredAudienceModelArn, name, description string, manageResourcePolicies bool, tags map[string]string) (*ConfiguredAudienceModelAssociation, error) { +func (b *InMemoryBackend) CreateConfiguredAudienceModelAssociation( + membershipID, configuredAudienceModelArn, name, description string, + manageResourcePolicies bool, + tags map[string]string, +) (*ConfiguredAudienceModelAssociation, error) { b.mu.Lock("CreateConfiguredAudienceModelAssociation") defer b.mu.Unlock() mem, ok := b.memberships[membershipID] @@ -2164,7 +2426,9 @@ func (b *InMemoryBackend) CreateConfiguredAudienceModelAssociation(membershipID, return assoc, nil } -func (b *InMemoryBackend) GetConfiguredAudienceModelAssociation(membershipID, assocID string) (*ConfiguredAudienceModelAssociation, error) { +func (b *InMemoryBackend) GetConfiguredAudienceModelAssociation( + membershipID, assocID string, +) (*ConfiguredAudienceModelAssociation, error) { b.mu.RLock("GetConfiguredAudienceModelAssociation") defer b.mu.RUnlock() assocs, ok := b.camaAssociations[membershipID] @@ -2178,7 +2442,9 @@ func (b *InMemoryBackend) GetConfiguredAudienceModelAssociation(membershipID, as return assoc, nil } -func (b *InMemoryBackend) ListConfiguredAudienceModelAssociations(membershipID, maxResults, nextToken string) ([]*ConfiguredAudienceModelAssociationSummary, string, error) { +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 { @@ -2205,7 +2471,9 @@ func (b *InMemoryBackend) ListConfiguredAudienceModelAssociations(membershipID, return page, next, nil } -func (b *InMemoryBackend) UpdateConfiguredAudienceModelAssociation(membershipID, assocID, name, description string) (*ConfiguredAudienceModelAssociation, error) { +func (b *InMemoryBackend) UpdateConfiguredAudienceModelAssociation( + membershipID, assocID, name, description string, +) (*ConfiguredAudienceModelAssociation, error) { b.mu.Lock("UpdateConfiguredAudienceModelAssociation") defer b.mu.Unlock() assocs, ok := b.camaAssociations[membershipID] @@ -2226,7 +2494,9 @@ func (b *InMemoryBackend) UpdateConfiguredAudienceModelAssociation(membershipID, return assoc, nil } -func (b *InMemoryBackend) DeleteConfiguredAudienceModelAssociation(membershipID, assocID string) error { +func (b *InMemoryBackend) DeleteConfiguredAudienceModelAssociation( + membershipID, assocID string, +) error { b.mu.Lock("DeleteConfiguredAudienceModelAssociation") defer b.mu.Unlock() assocs, ok := b.camaAssociations[membershipID] @@ -2242,12 +2512,15 @@ func (b *InMemoryBackend) DeleteConfiguredAudienceModelAssociation(membershipID, return nil } -func (b *InMemoryBackend) GetCollaborationConfiguredAudienceModelAssociation(collaborationID, assocID string) (*ConfiguredAudienceModelAssociation, error) { +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 { + if a.CollaborationIdentifier == collaborationID && + a.ConfiguredAudienceModelAssociationIdentifier == assocID { return a, nil } } @@ -2255,7 +2528,9 @@ func (b *InMemoryBackend) GetCollaborationConfiguredAudienceModelAssociation(col return nil, ErrNotFound } -func (b *InMemoryBackend) ListCollaborationConfiguredAudienceModelAssociations(collaborationID, maxResults, nextToken string) ([]*ConfiguredAudienceModelAssociationSummary, string, error) { +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 { @@ -2288,7 +2563,10 @@ func (b *InMemoryBackend) ListCollaborationConfiguredAudienceModelAssociations(c // ---- CollaborationChangeRequest ---- -func (b *InMemoryBackend) CreateCollaborationChangeRequest(collaborationID, changeRequestType string, details map[string]any) (*CollaborationChangeRequest, error) { +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] @@ -2314,7 +2592,9 @@ func (b *InMemoryBackend) CreateCollaborationChangeRequest(collaborationID, chan return req, nil } -func (b *InMemoryBackend) GetCollaborationChangeRequest(collaborationID, changeRequestID string) (*CollaborationChangeRequest, error) { +func (b *InMemoryBackend) GetCollaborationChangeRequest( + collaborationID, changeRequestID string, +) (*CollaborationChangeRequest, error) { b.mu.RLock("GetCollaborationChangeRequest") defer b.mu.RUnlock() reqs, ok := b.changeRequests[collaborationID] @@ -2328,7 +2608,9 @@ func (b *InMemoryBackend) GetCollaborationChangeRequest(collaborationID, changeR return req, nil } -func (b *InMemoryBackend) ListCollaborationChangeRequests(collaborationID, maxResults, nextToken string) ([]*CollaborationChangeRequest, string, error) { +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 { @@ -2338,12 +2620,17 @@ func (b *InMemoryBackend) ListCollaborationChangeRequests(collaborationID, maxRe 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 }) + 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) { +func (b *InMemoryBackend) UpdateCollaborationChangeRequest( + collaborationID, changeRequestID, status string, +) (*CollaborationChangeRequest, error) { b.mu.Lock("UpdateCollaborationChangeRequest") defer b.mu.Unlock() reqs, ok := b.changeRequests[collaborationID] diff --git a/services/cleanrooms/handler.go b/services/cleanrooms/handler.go index 171695688..20acc4a9b 100644 --- a/services/cleanrooms/handler.go +++ b/services/cleanrooms/handler.go @@ -480,16 +480,19 @@ func classifyConfiguredTables(method string, segs []string) (string, string) { return opUpdateConfiguredTable, id } } - // /configuredTables/{id}/analysisRule - if len(segs) == 3 && segs[2] == "analysisRule" { - id := segs[1] - if method == http.MethodPost { - return opCreateConfiguredTableAnalysisRule, id - } + // /configuredTables/{id}/analysisRule[/{type}] + if len(segs) >= 3 && segs[2] == "analysisRule" { + return classifyConfiguredTableAnalysisRule(method, segs) } - // /configuredTables/{id}/analysisRule/{type} - if len(segs) == 4 && segs[2] == "analysisRule" { - id := segs[1] + return opUnknown, "" +} + +func classifyConfiguredTableAnalysisRule(method string, segs []string) (string, string) { + id := segs[1] + if len(segs) == 3 && method == http.MethodPost { + return opCreateConfiguredTableAnalysisRule, id + } + if len(segs) == 4 { switch method { case http.MethodGet: return opGetConfiguredTableAnalysisRule, id @@ -814,7 +817,12 @@ func injectPathParams(path, op string, body []byte) []byte { // ---- dispatch ---- -func (h *Handler) dispatch(ctx context.Context, op string, body []byte, c *echo.Context) ([]byte, error) { +func (h *Handler) dispatch( + ctx context.Context, + op string, + body []byte, + c *echo.Context, +) ([]byte, error) { switch op { // Collaboration case opCreateCollaboration: @@ -1035,7 +1043,15 @@ func (h *Handler) handleCreateCollaboration(_ context.Context, body []byte) ([]b Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) - c, err := h.Backend.CreateCollaboration(req.Name, req.Description, req.CreatorDisplayName, req.CreatorMemberAbilities, req.Members, req.QueryLogStatus, req.Tags) + 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 } @@ -1054,8 +1070,16 @@ func (h *Handler) handleGetCollaboration(_ context.Context, body []byte) ([]byte return mustJSON(map[string]any{"collaboration": c}), nil } -func (h *Handler) handleListCollaborations(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { - items, next := h.Backend.ListCollaborations(qp(c, "memberStatus"), qp(c, "maxResults"), qp(c, "nextToken")) +func (h *Handler) handleListCollaborations( + _ context.Context, + body []byte, + 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 @@ -1070,7 +1094,11 @@ func (h *Handler) handleUpdateCollaboration(_ context.Context, body []byte) ([]b Description string `json:"description"` } _ = json.Unmarshal(body, &req) - col, err := h.Backend.UpdateCollaboration(req.CollaborationIdentifier, req.Name, req.Description) + col, err := h.Backend.UpdateCollaboration( + req.CollaborationIdentifier, + req.Name, + req.Description, + ) if err != nil { return nil, err } @@ -1085,12 +1113,20 @@ func (h *Handler) handleDeleteCollaboration(_ context.Context, body []byte) ([]b return nil, h.Backend.DeleteCollaboration(req.CollaborationIdentifier) } -func (h *Handler) handleListMembers(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListMembers( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1110,25 +1146,39 @@ func (h *Handler) handleDeleteMember(_ context.Context, body []byte) ([]byte, er return nil, h.Backend.DeleteMember(req.CollaborationIdentifier, req.AccountId) } -func (h *Handler) handleGetCollaborationAnalysisTemplate(_ context.Context, body []byte) ([]byte, error) { +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) + t, err := h.Backend.GetCollaborationAnalysisTemplate( + req.CollaborationIdentifier, + req.AnalysisTemplateArn, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"analysisTemplate": t}), nil } -func (h *Handler) handleListCollaborationAnalysisTemplates(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListCollaborationAnalysisTemplates( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1139,13 +1189,19 @@ func (h *Handler) handleListCollaborationAnalysisTemplates(_ context.Context, bo return mustJSON(resp), nil } -func (h *Handler) handleBatchGetCollaborationAnalysisTemplate(_ context.Context, body []byte) ([]byte, error) { +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) + items, errs, err := h.Backend.BatchGetCollaborationAnalysisTemplate( + req.CollaborationIdentifier, + req.AnalysisTemplateArns, + ) if err != nil { return nil, err } @@ -1182,7 +1238,11 @@ func (h *Handler) handleBatchGetSchemaAnalysisRule(_ context.Context, body []byt ruleType = r.Type } } - items, errs, err := h.Backend.BatchGetSchemaAnalysisRule(req.CollaborationIdentifier, names, ruleType) + items, errs, err := h.Backend.BatchGetSchemaAnalysisRule( + req.CollaborationIdentifier, + names, + ruleType, + ) if err != nil { return nil, err } @@ -1202,12 +1262,21 @@ func (h *Handler) handleGetSchema(_ context.Context, body []byte) ([]byte, error return mustJSON(map[string]any{"schema": s}), nil } -func (h *Handler) handleListSchemas(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListSchemas( + req.CollaborationIdentifier, + qp(c, "schemaType"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1232,39 +1301,60 @@ func (h *Handler) handleGetSchemaAnalysisRule(_ context.Context, body []byte) ([ return mustJSON(map[string]any{"analysisRule": r}), nil } -func (h *Handler) handleCreateCollaborationChangeRequest(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleCreateCollaborationChangeRequest( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { CollaborationIdentifier string `json:"collaborationIdentifier"` Type string `json:"type"` Details map[string]any `json:"details"` } _ = json.Unmarshal(body, &req) - r, err := h.Backend.CreateCollaborationChangeRequest(req.CollaborationIdentifier, req.Type, req.Details) + r, err := h.Backend.CreateCollaborationChangeRequest( + req.CollaborationIdentifier, + req.Type, + req.Details, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"collaborationChangeRequest": r}), nil } -func (h *Handler) handleGetCollaborationChangeRequest(_ context.Context, body []byte) ([]byte, error) { +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) + r, err := h.Backend.GetCollaborationChangeRequest( + req.CollaborationIdentifier, + req.ChangeRequestIdentifier, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"collaborationChangeRequest": r}), nil } -func (h *Handler) handleListCollaborationChangeRequests(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListCollaborationChangeRequests( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1275,39 +1365,60 @@ func (h *Handler) handleListCollaborationChangeRequests(_ context.Context, body return mustJSON(resp), nil } -func (h *Handler) handleUpdateCollaborationChangeRequest(_ context.Context, body []byte) ([]byte, error) { +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) + r, err := h.Backend.UpdateCollaborationChangeRequest( + req.CollaborationIdentifier, + req.ChangeRequestIdentifier, + req.Status, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"collaborationChangeRequest": r}), nil } -func (h *Handler) handleGetCollaborationConfiguredAudienceModelAssociation(_ context.Context, body []byte) ([]byte, error) { +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) + a, err := h.Backend.GetCollaborationConfiguredAudienceModelAssociation( + req.CollaborationIdentifier, + req.ConfiguredAudienceModelAssociationIdentifier, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"configuredAudienceModelAssociation": a}), nil } -func (h *Handler) handleListCollaborationConfiguredAudienceModelAssociations(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListCollaborationConfiguredAudienceModelAssociations( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1318,25 +1429,39 @@ func (h *Handler) handleListCollaborationConfiguredAudienceModelAssociations(_ c return mustJSON(resp), nil } -func (h *Handler) handleGetCollaborationIdNamespaceAssociation(_ context.Context, body []byte) ([]byte, error) { +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) + a, err := h.Backend.GetCollaborationIdNamespaceAssociation( + req.CollaborationIdentifier, + req.IdNamespaceAssociationIdentifier, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"idNamespaceAssociation": a}), nil } -func (h *Handler) handleListCollaborationIdNamespaceAssociations(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListCollaborationIdNamespaceAssociations( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1347,25 +1472,39 @@ func (h *Handler) handleListCollaborationIdNamespaceAssociations(_ context.Conte return mustJSON(resp), nil } -func (h *Handler) handleGetCollaborationPrivacyBudgetTemplate(_ context.Context, body []byte) ([]byte, error) { +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) + t, err := h.Backend.GetCollaborationPrivacyBudgetTemplate( + req.CollaborationIdentifier, + req.PrivacyBudgetTemplateIdentifier, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"privacyBudgetTemplate": t}), nil } -func (h *Handler) handleListCollaborationPrivacyBudgetTemplates(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListCollaborationPrivacyBudgetTemplates( + req.CollaborationIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1376,12 +1515,21 @@ func (h *Handler) handleListCollaborationPrivacyBudgetTemplates(_ context.Contex return mustJSON(resp), nil } -func (h *Handler) handleListCollaborationPrivacyBudgets(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListCollaborationPrivacyBudgets( + req.CollaborationIdentifier, + qp(c, "privacyBudgetType"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1403,7 +1551,13 @@ func (h *Handler) handleCreateMembership(_ context.Context, body []byte) ([]byte Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) - m, err := h.Backend.CreateMembership(req.CollaborationIdentifier, req.QueryLogStatus, req.DefaultResultConfiguration, req.PaymentConfiguration, req.Tags) + m, err := h.Backend.CreateMembership( + req.CollaborationIdentifier, + req.QueryLogStatus, + req.DefaultResultConfiguration, + req.PaymentConfiguration, + req.Tags, + ) if err != nil { return nil, err } @@ -1422,8 +1576,16 @@ func (h *Handler) handleGetMembership(_ context.Context, body []byte) ([]byte, e return mustJSON(map[string]any{"membership": m}), nil } -func (h *Handler) handleListMemberships(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { - items, next := h.Backend.ListMemberships(qp(c, "status"), qp(c, "maxResults"), qp(c, "nextToken")) +func (h *Handler) handleListMemberships( + _ context.Context, + body []byte, + 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 @@ -1438,7 +1600,11 @@ func (h *Handler) handleUpdateMembership(_ context.Context, body []byte) ([]byte DefaultResultConfiguration map[string]any `json:"defaultResultConfiguration"` } _ = json.Unmarshal(body, &req) - m, err := h.Backend.UpdateMembership(req.MembershipIdentifier, req.QueryLogStatus, req.DefaultResultConfiguration) + m, err := h.Backend.UpdateMembership( + req.MembershipIdentifier, + req.QueryLogStatus, + req.DefaultResultConfiguration, + ) if err != nil { return nil, err } @@ -1465,7 +1631,14 @@ func (h *Handler) handleCreateConfiguredTable(_ context.Context, body []byte) ([ Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) - ct, err := h.Backend.CreateConfiguredTable(req.Name, req.Description, req.TableReference, req.AllowedColumns, req.AnalysisMethod, req.Tags) + ct, err := h.Backend.CreateConfiguredTable( + req.Name, + req.Description, + req.TableReference, + req.AllowedColumns, + req.AnalysisMethod, + req.Tags, + ) if err != nil { return nil, err } @@ -1484,7 +1657,11 @@ func (h *Handler) handleGetConfiguredTable(_ context.Context, body []byte) ([]by return mustJSON(map[string]any{"configuredTable": ct}), nil } -func (h *Handler) handleListConfiguredTables(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +func (h *Handler) handleListConfiguredTables( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { items, next := h.Backend.ListConfiguredTables(qp(c, "maxResults"), qp(c, "nextToken")) resp := map[string]any{"configuredTableSummaries": items} if next != "" { @@ -1500,7 +1677,11 @@ func (h *Handler) handleUpdateConfiguredTable(_ context.Context, body []byte) ([ Description string `json:"description"` } _ = json.Unmarshal(body, &req) - ct, err := h.Backend.UpdateConfiguredTable(req.ConfiguredTableIdentifier, req.Name, req.Description) + ct, err := h.Backend.UpdateConfiguredTable( + req.ConfiguredTableIdentifier, + req.Name, + req.Description, + ) if err != nil { return nil, err } @@ -1517,59 +1698,88 @@ func (h *Handler) handleDeleteConfiguredTable(_ context.Context, body []byte) ([ // ---- ConfiguredTableAnalysisRule handlers ---- -func (h *Handler) handleCreateConfiguredTableAnalysisRule(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleCreateConfiguredTableAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` AnalysisRuleType string `json:"analysisRuleType"` AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` } _ = json.Unmarshal(body, &req) - r, err := h.Backend.CreateConfiguredTableAnalysisRule(req.ConfiguredTableIdentifier, req.AnalysisRuleType, req.AnalysisRulePolicy) + r, err := h.Backend.CreateConfiguredTableAnalysisRule( + req.ConfiguredTableIdentifier, + req.AnalysisRuleType, + req.AnalysisRulePolicy, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"analysisRule": r}), nil } -func (h *Handler) handleGetConfiguredTableAnalysisRule(_ context.Context, body []byte) ([]byte, error) { +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) + r, err := h.Backend.GetConfiguredTableAnalysisRule( + req.ConfiguredTableIdentifier, + req.AnalysisRuleType, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"analysisRule": r}), nil } -func (h *Handler) handleUpdateConfiguredTableAnalysisRule(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleUpdateConfiguredTableAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { ConfiguredTableIdentifier string `json:"configuredTableIdentifier"` AnalysisRuleType string `json:"analysisRuleType"` AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` } _ = json.Unmarshal(body, &req) - r, err := h.Backend.UpdateConfiguredTableAnalysisRule(req.ConfiguredTableIdentifier, req.AnalysisRuleType, req.AnalysisRulePolicy) + r, err := h.Backend.UpdateConfiguredTableAnalysisRule( + req.ConfiguredTableIdentifier, + req.AnalysisRuleType, + req.AnalysisRulePolicy, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"analysisRule": r}), nil } -func (h *Handler) handleDeleteConfiguredTableAnalysisRule(_ context.Context, body []byte) ([]byte, error) { +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) + return nil, h.Backend.DeleteConfiguredTableAnalysisRule( + req.ConfiguredTableIdentifier, + req.AnalysisRuleType, + ) } // ---- ConfiguredTableAssociation handlers ---- -func (h *Handler) handleCreateConfiguredTableAssociation(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleCreateConfiguredTableAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { MembershipIdentifier string `json:"membershipIdentifier"` Name string `json:"name"` @@ -1579,32 +1789,53 @@ func (h *Handler) handleCreateConfiguredTableAssociation(_ context.Context, body Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) - a, err := h.Backend.CreateConfiguredTableAssociation(req.MembershipIdentifier, req.Name, req.Description, req.ConfiguredTableIdentifier, req.RoleArn, req.Tags) + 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{"configuredTableAssociation": a}), nil } -func (h *Handler) handleGetConfiguredTableAssociation(_ context.Context, body []byte) ([]byte, error) { +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) + a, err := h.Backend.GetConfiguredTableAssociation( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"configuredTableAssociation": a}), nil } -func (h *Handler) handleListConfiguredTableAssociations(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListConfiguredTableAssociations( + req.MembershipIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1615,7 +1846,10 @@ func (h *Handler) handleListConfiguredTableAssociations(_ context.Context, body return mustJSON(resp), nil } -func (h *Handler) handleUpdateConfiguredTableAssociation(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleUpdateConfiguredTableAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { MembershipIdentifier string `json:"membershipIdentifier"` ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` @@ -1623,25 +1857,39 @@ func (h *Handler) handleUpdateConfiguredTableAssociation(_ context.Context, body RoleArn string `json:"roleArn"` } _ = json.Unmarshal(body, &req) - a, err := h.Backend.UpdateConfiguredTableAssociation(req.MembershipIdentifier, req.ConfiguredTableAssociationIdentifier, req.Description, req.RoleArn) + a, err := h.Backend.UpdateConfiguredTableAssociation( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + req.Description, + req.RoleArn, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"configuredTableAssociation": a}), nil } -func (h *Handler) handleDeleteConfiguredTableAssociation(_ context.Context, body []byte) ([]byte, error) { +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) + return nil, h.Backend.DeleteConfiguredTableAssociation( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + ) } // ---- ConfiguredTableAssociationAnalysisRule handlers ---- -func (h *Handler) handleCreateConfiguredTableAssociationAnalysisRule(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleCreateConfiguredTableAssociationAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { MembershipIdentifier string `json:"membershipIdentifier"` ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` @@ -1649,28 +1897,43 @@ func (h *Handler) handleCreateConfiguredTableAssociationAnalysisRule(_ context.C AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` } _ = json.Unmarshal(body, &req) - r, err := h.Backend.CreateConfiguredTableAssociationAnalysisRule(req.MembershipIdentifier, req.ConfiguredTableAssociationIdentifier, req.AnalysisRuleType, req.AnalysisRulePolicy) + r, err := h.Backend.CreateConfiguredTableAssociationAnalysisRule( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + req.AnalysisRuleType, + req.AnalysisRulePolicy, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"analysisRule": r}), nil } -func (h *Handler) handleGetConfiguredTableAssociationAnalysisRule(_ context.Context, body []byte) ([]byte, error) { +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) + r, err := h.Backend.GetConfiguredTableAssociationAnalysisRule( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + req.AnalysisRuleType, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"analysisRule": r}), nil } -func (h *Handler) handleUpdateConfiguredTableAssociationAnalysisRule(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleUpdateConfiguredTableAssociationAnalysisRule( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { MembershipIdentifier string `json:"membershipIdentifier"` ConfiguredTableAssociationIdentifier string `json:"configuredTableAssociationIdentifier"` @@ -1678,21 +1941,33 @@ func (h *Handler) handleUpdateConfiguredTableAssociationAnalysisRule(_ context.C AnalysisRulePolicy map[string]any `json:"analysisRulePolicy"` } _ = json.Unmarshal(body, &req) - r, err := h.Backend.UpdateConfiguredTableAssociationAnalysisRule(req.MembershipIdentifier, req.ConfiguredTableAssociationIdentifier, req.AnalysisRuleType, req.AnalysisRulePolicy) + r, err := h.Backend.UpdateConfiguredTableAssociationAnalysisRule( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + req.AnalysisRuleType, + req.AnalysisRulePolicy, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"analysisRule": r}), nil } -func (h *Handler) handleDeleteConfiguredTableAssociationAnalysisRule(_ context.Context, body []byte) ([]byte, error) { +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) + return nil, h.Backend.DeleteConfiguredTableAssociationAnalysisRule( + req.MembershipIdentifier, + req.ConfiguredTableAssociationIdentifier, + req.AnalysisRuleType, + ) } // ---- AnalysisTemplate handlers ---- @@ -1708,7 +1983,15 @@ func (h *Handler) handleCreateAnalysisTemplate(_ context.Context, body []byte) ( Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) - t, err := h.Backend.CreateAnalysisTemplate(req.MembershipIdentifier, req.Name, req.Description, req.Format, req.Source, req.AnalysisParameters, req.Tags) + 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 } @@ -1721,19 +2004,30 @@ func (h *Handler) handleGetAnalysisTemplate(_ context.Context, body []byte) ([]b AnalysisTemplateIdentifier string `json:"analysisTemplateIdentifier"` } _ = json.Unmarshal(body, &req) - t, err := h.Backend.GetAnalysisTemplate(req.MembershipIdentifier, req.AnalysisTemplateIdentifier) + t, err := h.Backend.GetAnalysisTemplate( + req.MembershipIdentifier, + req.AnalysisTemplateIdentifier, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"analysisTemplate": t}), nil } -func (h *Handler) handleListAnalysisTemplates(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListAnalysisTemplates( + req.MembershipIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1751,7 +2045,11 @@ func (h *Handler) handleUpdateAnalysisTemplate(_ context.Context, body []byte) ( Description string `json:"description"` } _ = json.Unmarshal(body, &req) - t, err := h.Backend.UpdateAnalysisTemplate(req.MembershipIdentifier, req.AnalysisTemplateIdentifier, req.Description) + t, err := h.Backend.UpdateAnalysisTemplate( + req.MembershipIdentifier, + req.AnalysisTemplateIdentifier, + req.Description, + ) if err != nil { return nil, err } @@ -1764,7 +2062,10 @@ func (h *Handler) handleDeleteAnalysisTemplate(_ context.Context, body []byte) ( AnalysisTemplateIdentifier string `json:"analysisTemplateIdentifier"` } _ = json.Unmarshal(body, &req) - return nil, h.Backend.DeleteAnalysisTemplate(req.MembershipIdentifier, req.AnalysisTemplateIdentifier) + return nil, h.Backend.DeleteAnalysisTemplate( + req.MembershipIdentifier, + req.AnalysisTemplateIdentifier, + ) } // ---- ProtectedQuery handlers ---- @@ -1783,7 +2084,12 @@ func (h *Handler) handleStartProtectedQuery(_ context.Context, body []byte) ([]b sqlText = v } } - q, err := h.Backend.StartProtectedQuery(req.MembershipIdentifier, sqlText, req.ResultConfiguration, req.ComputeConfiguration) + q, err := h.Backend.StartProtectedQuery( + req.MembershipIdentifier, + sqlText, + req.ResultConfiguration, + req.ComputeConfiguration, + ) if err != nil { return nil, err } @@ -1803,12 +2109,21 @@ func (h *Handler) handleGetProtectedQuery(_ context.Context, body []byte) ([]byt return mustJSON(map[string]any{"protectedQuery": q}), nil } -func (h *Handler) handleListProtectedQueries(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListProtectedQueries( + req.MembershipIdentifier, + qp(c, "status"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1826,7 +2141,11 @@ func (h *Handler) handleUpdateProtectedQuery(_ context.Context, body []byte) ([] TargetStatus string `json:"targetStatus"` } _ = json.Unmarshal(body, &req) - q, err := h.Backend.UpdateProtectedQuery(req.MembershipIdentifier, req.ProtectedQueryIdentifier, req.TargetStatus) + q, err := h.Backend.UpdateProtectedQuery( + req.MembershipIdentifier, + req.ProtectedQueryIdentifier, + req.TargetStatus, + ) if err != nil { return nil, err } @@ -1843,7 +2162,12 @@ func (h *Handler) handleStartProtectedJob(_ context.Context, body []byte) ([]byt ResultConfiguration map[string]any `json:"resultConfiguration"` } _ = json.Unmarshal(body, &req) - j, err := h.Backend.StartProtectedJob(req.MembershipIdentifier, req.Type, req.JobParameters, req.ResultConfiguration) + j, err := h.Backend.StartProtectedJob( + req.MembershipIdentifier, + req.Type, + req.JobParameters, + req.ResultConfiguration, + ) if err != nil { return nil, err } @@ -1863,12 +2187,21 @@ func (h *Handler) handleGetProtectedJob(_ context.Context, body []byte) ([]byte, return mustJSON(map[string]any{"protectedJob": j}), nil } -func (h *Handler) handleListProtectedJobs(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListProtectedJobs( + req.MembershipIdentifier, + qp(c, "status"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1886,7 +2219,11 @@ func (h *Handler) handleUpdateProtectedJob(_ context.Context, body []byte) ([]by TargetStatus string `json:"targetStatus"` } _ = json.Unmarshal(body, &req) - j, err := h.Backend.UpdateProtectedJob(req.MembershipIdentifier, req.ProtectedJobIdentifier, req.TargetStatus) + j, err := h.Backend.UpdateProtectedJob( + req.MembershipIdentifier, + req.ProtectedJobIdentifier, + req.TargetStatus, + ) if err != nil { return nil, err } @@ -1895,7 +2232,10 @@ func (h *Handler) handleUpdateProtectedJob(_ context.Context, body []byte) ([]by // ---- PrivacyBudgetTemplate handlers ---- -func (h *Handler) handleCreatePrivacyBudgetTemplate(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleCreatePrivacyBudgetTemplate( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { MembershipIdentifier string `json:"membershipIdentifier"` PrivacyBudgetType string `json:"privacyBudgetType"` @@ -1904,7 +2244,13 @@ func (h *Handler) handleCreatePrivacyBudgetTemplate(_ context.Context, body []by Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) - t, err := h.Backend.CreatePrivacyBudgetTemplate(req.MembershipIdentifier, req.PrivacyBudgetType, req.AutoRefresh, req.Parameters, req.Tags) + t, err := h.Backend.CreatePrivacyBudgetTemplate( + req.MembershipIdentifier, + req.PrivacyBudgetType, + req.AutoRefresh, + req.Parameters, + req.Tags, + ) if err != nil { return nil, err } @@ -1917,19 +2263,31 @@ func (h *Handler) handleGetPrivacyBudgetTemplate(_ context.Context, body []byte) PrivacyBudgetTemplateIdentifier string `json:"privacyBudgetTemplateIdentifier"` } _ = json.Unmarshal(body, &req) - t, err := h.Backend.GetPrivacyBudgetTemplate(req.MembershipIdentifier, req.PrivacyBudgetTemplateIdentifier) + t, err := h.Backend.GetPrivacyBudgetTemplate( + req.MembershipIdentifier, + req.PrivacyBudgetTemplateIdentifier, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"privacyBudgetTemplate": t}), nil } -func (h *Handler) handleListPrivacyBudgetTemplates(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListPrivacyBudgetTemplates( + req.MembershipIdentifier, + qp(c, "privacyBudgetType"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -1940,7 +2298,10 @@ func (h *Handler) handleListPrivacyBudgetTemplates(_ context.Context, body []byt return mustJSON(resp), nil } -func (h *Handler) handleUpdatePrivacyBudgetTemplate(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleUpdatePrivacyBudgetTemplate( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { MembershipIdentifier string `json:"membershipIdentifier"` PrivacyBudgetTemplateIdentifier string `json:"privacyBudgetTemplateIdentifier"` @@ -1948,28 +2309,48 @@ func (h *Handler) handleUpdatePrivacyBudgetTemplate(_ context.Context, body []by Parameters map[string]any `json:"parameters"` } _ = json.Unmarshal(body, &req) - t, err := h.Backend.UpdatePrivacyBudgetTemplate(req.MembershipIdentifier, req.PrivacyBudgetTemplateIdentifier, req.AutoRefresh, req.Parameters) + t, err := h.Backend.UpdatePrivacyBudgetTemplate( + req.MembershipIdentifier, + req.PrivacyBudgetTemplateIdentifier, + req.AutoRefresh, + req.Parameters, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"privacyBudgetTemplate": t}), nil } -func (h *Handler) handleDeletePrivacyBudgetTemplate(_ context.Context, body []byte) ([]byte, error) { +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) + return nil, h.Backend.DeletePrivacyBudgetTemplate( + req.MembershipIdentifier, + req.PrivacyBudgetTemplateIdentifier, + ) } -func (h *Handler) handleListPrivacyBudgets(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListPrivacyBudgets( + req.MembershipIdentifier, + qp(c, "privacyBudgetType"), + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -2005,7 +2386,14 @@ func (h *Handler) handleCreateIdMappingTable(_ context.Context, body []byte) ([] Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) - t, err := h.Backend.CreateIdMappingTable(req.MembershipIdentifier, req.Name, req.Description, req.InputReferenceConfig, req.KmsKeyArn, req.Tags) + t, err := h.Backend.CreateIdMappingTable( + req.MembershipIdentifier, + req.Name, + req.Description, + req.InputReferenceConfig, + req.KmsKeyArn, + req.Tags, + ) if err != nil { return nil, err } @@ -2025,12 +2413,20 @@ func (h *Handler) handleGetIdMappingTable(_ context.Context, body []byte) ([]byt return mustJSON(map[string]any{"idMappingTable": t}), nil } -func (h *Handler) handleListIdMappingTables(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListIdMappingTables( + req.MembershipIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -2049,7 +2445,12 @@ func (h *Handler) handleUpdateIdMappingTable(_ context.Context, body []byte) ([] KmsKeyArn string `json:"kmsKeyArn"` } _ = json.Unmarshal(body, &req) - t, err := h.Backend.UpdateIdMappingTable(req.MembershipIdentifier, req.IdMappingTableIdentifier, req.Description, req.KmsKeyArn) + t, err := h.Backend.UpdateIdMappingTable( + req.MembershipIdentifier, + req.IdMappingTableIdentifier, + req.Description, + req.KmsKeyArn, + ) if err != nil { return nil, err } @@ -2062,7 +2463,10 @@ func (h *Handler) handleDeleteIdMappingTable(_ context.Context, body []byte) ([] IdMappingTableIdentifier string `json:"idMappingTableIdentifier"` } _ = json.Unmarshal(body, &req) - return nil, h.Backend.DeleteIdMappingTable(req.MembershipIdentifier, req.IdMappingTableIdentifier) + return nil, h.Backend.DeleteIdMappingTable( + req.MembershipIdentifier, + req.IdMappingTableIdentifier, + ) } func (h *Handler) handlePopulateIdMappingTable(_ context.Context, body []byte) ([]byte, error) { @@ -2071,7 +2475,10 @@ func (h *Handler) handlePopulateIdMappingTable(_ context.Context, body []byte) ( IdMappingTableIdentifier string `json:"idMappingTableIdentifier"` } _ = json.Unmarshal(body, &req) - result, err := h.Backend.PopulateIdMappingTable(req.MembershipIdentifier, req.IdMappingTableIdentifier) + result, err := h.Backend.PopulateIdMappingTable( + req.MembershipIdentifier, + req.IdMappingTableIdentifier, + ) if err != nil { return nil, err } @@ -2080,7 +2487,10 @@ func (h *Handler) handlePopulateIdMappingTable(_ context.Context, body []byte) ( // ---- IdNamespaceAssociation handlers ---- -func (h *Handler) handleCreateIdNamespaceAssociation(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleCreateIdNamespaceAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { MembershipIdentifier string `json:"membershipIdentifier"` Name string `json:"name"` @@ -2090,7 +2500,14 @@ func (h *Handler) handleCreateIdNamespaceAssociation(_ context.Context, body []b Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) - a, err := h.Backend.CreateIdNamespaceAssociation(req.MembershipIdentifier, req.Name, req.Description, req.InputReferenceConfig, req.IdMappingConfig, req.Tags) + a, err := h.Backend.CreateIdNamespaceAssociation( + req.MembershipIdentifier, + req.Name, + req.Description, + req.InputReferenceConfig, + req.IdMappingConfig, + req.Tags, + ) if err != nil { return nil, err } @@ -2103,19 +2520,30 @@ func (h *Handler) handleGetIdNamespaceAssociation(_ context.Context, body []byte IdNamespaceAssociationIdentifier string `json:"idNamespaceAssociationIdentifier"` } _ = json.Unmarshal(body, &req) - a, err := h.Backend.GetIdNamespaceAssociation(req.MembershipIdentifier, req.IdNamespaceAssociationIdentifier) + a, err := h.Backend.GetIdNamespaceAssociation( + req.MembershipIdentifier, + req.IdNamespaceAssociationIdentifier, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"idNamespaceAssociation": a}), nil } -func (h *Handler) handleListIdNamespaceAssociations(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListIdNamespaceAssociations( + req.MembershipIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -2126,7 +2554,10 @@ func (h *Handler) handleListIdNamespaceAssociations(_ context.Context, body []by return mustJSON(resp), nil } -func (h *Handler) handleUpdateIdNamespaceAssociation(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleUpdateIdNamespaceAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { MembershipIdentifier string `json:"membershipIdentifier"` IdNamespaceAssociationIdentifier string `json:"idNamespaceAssociationIdentifier"` @@ -2134,25 +2565,39 @@ func (h *Handler) handleUpdateIdNamespaceAssociation(_ context.Context, body []b IdMappingConfig map[string]any `json:"idMappingConfig"` } _ = json.Unmarshal(body, &req) - a, err := h.Backend.UpdateIdNamespaceAssociation(req.MembershipIdentifier, req.IdNamespaceAssociationIdentifier, req.Description, req.IdMappingConfig) + a, err := h.Backend.UpdateIdNamespaceAssociation( + req.MembershipIdentifier, + req.IdNamespaceAssociationIdentifier, + req.Description, + req.IdMappingConfig, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"idNamespaceAssociation": a}), nil } -func (h *Handler) handleDeleteIdNamespaceAssociation(_ context.Context, body []byte) ([]byte, error) { +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) + return nil, h.Backend.DeleteIdNamespaceAssociation( + req.MembershipIdentifier, + req.IdNamespaceAssociationIdentifier, + ) } // ---- ConfiguredAudienceModelAssociation handlers ---- -func (h *Handler) handleCreateConfiguredAudienceModelAssociation(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleCreateConfiguredAudienceModelAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { MembershipIdentifier string `json:"membershipIdentifier"` ConfiguredAudienceModelArn string `json:"configuredAudienceModelArn"` @@ -2162,32 +2607,53 @@ func (h *Handler) handleCreateConfiguredAudienceModelAssociation(_ context.Conte Tags map[string]string `json:"tags"` } _ = json.Unmarshal(body, &req) - a, err := h.Backend.CreateConfiguredAudienceModelAssociation(req.MembershipIdentifier, req.ConfiguredAudienceModelArn, req.Name, req.Description, req.ManageResourcePolicies, req.Tags) + 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{"configuredAudienceModelAssociation": a}), nil } -func (h *Handler) handleGetConfiguredAudienceModelAssociation(_ context.Context, body []byte) ([]byte, error) { +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) + a, err := h.Backend.GetConfiguredAudienceModelAssociation( + req.MembershipIdentifier, + req.ConfiguredAudienceModelAssociationIdentifier, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"configuredAudienceModelAssociation": a}), nil } -func (h *Handler) handleListConfiguredAudienceModelAssociations(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +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")) + items, next, err := h.Backend.ListConfiguredAudienceModelAssociations( + req.MembershipIdentifier, + qp(c, "maxResults"), + qp(c, "nextToken"), + ) if err != nil { return nil, err } @@ -2198,7 +2664,10 @@ func (h *Handler) handleListConfiguredAudienceModelAssociations(_ context.Contex return mustJSON(resp), nil } -func (h *Handler) handleUpdateConfiguredAudienceModelAssociation(_ context.Context, body []byte) ([]byte, error) { +func (h *Handler) handleUpdateConfiguredAudienceModelAssociation( + _ context.Context, + body []byte, +) ([]byte, error) { var req struct { MembershipIdentifier string `json:"membershipIdentifier"` ConfiguredAudienceModelAssociationIdentifier string `json:"configuredAudienceModelAssociationIdentifier"` @@ -2206,20 +2675,31 @@ func (h *Handler) handleUpdateConfiguredAudienceModelAssociation(_ context.Conte Description string `json:"description"` } _ = json.Unmarshal(body, &req) - a, err := h.Backend.UpdateConfiguredAudienceModelAssociation(req.MembershipIdentifier, req.ConfiguredAudienceModelAssociationIdentifier, req.Name, req.Description) + a, err := h.Backend.UpdateConfiguredAudienceModelAssociation( + req.MembershipIdentifier, + req.ConfiguredAudienceModelAssociationIdentifier, + req.Name, + req.Description, + ) if err != nil { return nil, err } return mustJSON(map[string]any{"configuredAudienceModelAssociation": a}), nil } -func (h *Handler) handleDeleteConfiguredAudienceModelAssociation(_ context.Context, body []byte) ([]byte, error) { +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) + return nil, h.Backend.DeleteConfiguredAudienceModelAssociation( + req.MembershipIdentifier, + req.ConfiguredAudienceModelAssociationIdentifier, + ) } // ---- Tag handlers ---- @@ -2245,7 +2725,11 @@ func (h *Handler) handleTagResource(_ context.Context, body []byte) ([]byte, err return nil, h.Backend.TagResource(req.ResourceArn, req.Tags) } -func (h *Handler) handleUntagResource(_ context.Context, body []byte, c *echo.Context) ([]byte, error) { +func (h *Handler) handleUntagResource( + _ context.Context, + body []byte, + c *echo.Context, +) ([]byte, error) { var req struct { ResourceArn string `json:"resourceArn"` TagKeys []string `json:"tagKeys"` diff --git a/services/cleanrooms/handler_test.go b/services/cleanrooms/handler_test.go index 3be9ab61a..905096ad8 100644 --- a/services/cleanrooms/handler_test.go +++ b/services/cleanrooms/handler_test.go @@ -21,7 +21,12 @@ func newTestServer(t *testing.T) (*cleanrooms.Handler, *echo.Echo) { return h, e } -func doRequest(t *testing.T, e *echo.Echo, method, path string, body any) *httptest.ResponseRecorder { +func doRequest( + t *testing.T, + e *echo.Echo, + method, path string, + body any, +) *httptest.ResponseRecorder { t.Helper() var reqBody []byte if body != nil { @@ -136,9 +141,11 @@ func TestConfiguredTableCRUD(t *testing.T) { } createBody := map[string]any{ - "name": "my-table", - "description": "desc", - "tableReference": map[string]any{"glue": map[string]any{"databaseName": "db", "tableName": "tbl"}}, + "name": "my-table", + "description": "desc", + "tableReference": map[string]any{ + "glue": map[string]any{"databaseName": "db", "tableName": "tbl"}, + }, "allowedColumns": []string{"col1"}, "analysisMethod": "DIRECT_QUERY", } @@ -178,7 +185,13 @@ func TestConfiguredTableCRUD(t *testing.T) { } t.Run("update", func(t *testing.T) { - rec := doRequest(t, e, http.MethodPatch, "/configuredTables/"+ctID, map[string]any{"name": "new-name"}) + rec := doRequest( + t, + e, + http.MethodPatch, + "/configuredTables/"+ctID, + map[string]any{"name": "new-name"}, + ) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } @@ -226,7 +239,13 @@ func TestMembershipCRUD(t *testing.T) { } tests := []tc{ - {name: "create", method: http.MethodPost, path: "/memberships", body: createBody, wantStatus: http.StatusOK}, + { + name: "create", + method: http.MethodPost, + path: "/memberships", + body: createBody, + wantStatus: http.StatusOK, + }, {name: "list", method: http.MethodGet, path: "/memberships", wantStatus: http.StatusOK}, } diff --git a/services/cleanrooms/interfaces.go b/services/cleanrooms/interfaces.go index 66d9f0fef..7d973b412 100644 --- a/services/cleanrooms/interfaces.go +++ b/services/cleanrooms/interfaces.go @@ -7,123 +7,262 @@ type StorageBackend interface { Reset() // Collaboration operations. - CreateCollaboration(name, description, creatorDisplayName string, creatorMemberAbilities []string, members []MemberSpec, queryLogStatus string, tags map[string]string) (*Collaboration, error) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) From c4ff05075e42f985a4f1af3fbb376cd4de733dee Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 23:47:26 -0500 Subject: [PATCH 06/16] WIP: checkpoint (auto) --- services/cleanrooms/backend.go | 441 ++++++++++++++++----------------- 1 file changed, 214 insertions(+), 227 deletions(-) diff --git a/services/cleanrooms/backend.go b/services/cleanrooms/backend.go index e117c05bb..317734432 100644 --- a/services/cleanrooms/backend.go +++ b/services/cleanrooms/backend.go @@ -22,6 +22,11 @@ var ( ErrValidation = awserr.New("ValidationException", awserr.ErrInvalidParameter) ) +const ( + statusActive = "ACTIVE" + errCodeNotFound = "ResourceNotFoundException" +) + // ---- types ---- type MemberSpec struct { @@ -447,6 +452,8 @@ type InMemoryBackend struct { schemas map[string]map[string]*Schema schemaAnalysisRules map[string]map[string]map[string]*SchemaAnalysisRule tagsByArn map[string]map[string]string + nowFn func() float64 + muNow sync.Mutex } // NewInMemoryBackendWithContext creates a backend tied to svcCtx (ignored; no lifecycle goroutines). @@ -479,6 +486,7 @@ func NewInMemoryBackend(accountID, region string) *InMemoryBackend { 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()) }, } } @@ -507,6 +515,30 @@ func (b *InMemoryBackend) Reset() { b.tagsByArn = make(map[string]map[string]string) } +// membershipCtx holds common values computed when creating a resource within a membership. +type membershipCtx struct { + id string + ts float64 + membershipArn string + collaborationID string + collaborationArn string +} + +func (b *InMemoryBackend) newMembershipCtx(mem *Membership) membershipCtx { + collab := b.collaborations[mem.CollaborationIdentifier] + var collabArn string + if collab != nil { + collabArn = collab.Arn + } + return membershipCtx{ + id: uuid.NewString(), + ts: b.now(), + membershipArn: mem.Arn, + collaborationID: mem.CollaborationIdentifier, + collaborationArn: collabArn, + } +} + // ---- ARN helpers ---- func (b *InMemoryBackend) collaborationARN(id string) string { @@ -616,14 +648,14 @@ func paginate[T any](items []T, maxResultsStr, nextToken string) ([]T, string) { } max := 100 if maxResultsStr != "" { - fmt.Sscanf(maxResultsStr, "%d", &max) + _, _ = fmt.Sscanf(maxResultsStr, "%d", &max) } if max <= 0 || max > 1000 { max = 100 } start := 0 if nextToken != "" { - fmt.Sscanf(nextToken, "%d", &start) + _, _ = fmt.Sscanf(nextToken, "%d", &start) } if start >= len(items) { return []T{}, "" @@ -635,15 +667,96 @@ func paginate[T any](items []T, maxResultsStr, nextToken string) ([]T, string) { return items[start:end], fmt.Sprintf("%d", end) } -// ---- now helper ---- +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, + } +} -var nowFn = func() float64 { return float64(time.Now().Unix()) } -var muNow sync.Mutex +// ---- now helper ---- -func now() float64 { - muNow.Lock() - defer muNow.Unlock() - return nowFn() +func (b *InMemoryBackend) now() float64 { + b.muNow.Lock() + defer b.muNow.Unlock() + return b.nowFn() } // ---- Collaboration ---- @@ -661,13 +774,13 @@ func (b *InMemoryBackend) CreateCollaboration( return nil, ErrValidation } id := uuid.NewString() - ts := now() + ts := b.now() memberSummaries := make([]*MemberSummary, 0, len(members)+1) memberSummaries = append(memberSummaries, &MemberSummary{ AccountID: b.accountID, DisplayName: creatorDisplayName, Abilities: creatorMemberAbilities, - Status: "ACTIVE", + Status: statusActive, CreateTime: ts, UpdateTime: ts, }) @@ -725,7 +838,7 @@ func (b *InMemoryBackend) ListCollaborations( Name: c.Name, CreatorAccountId: c.CreatorAccountId, CreatorDisplayName: c.CreatorDisplayName, - MemberStatus: "ACTIVE", + MemberStatus: statusActive, CreateTime: c.CreateTime, UpdateTime: c.UpdateTime, }) @@ -753,7 +866,7 @@ func (b *InMemoryBackend) UpdateCollaboration( if description != "" { c.Description = description } - c.UpdateTime = now() + c.UpdateTime = b.now() return c, nil } @@ -819,7 +932,7 @@ func (b *InMemoryBackend) CreateMembership( return nil, ErrNotFound } id := uuid.NewString() - ts := now() + ts := b.now() m := &Membership{ MembershipIdentifier: id, Arn: b.membershipARN(id), @@ -828,7 +941,7 @@ func (b *InMemoryBackend) CreateMembership( CollaborationCreatorAccountId: collab.CreatorAccountId, CollaborationCreatorDisplayName: collab.CreatorDisplayName, CollaborationName: collab.Name, - Status: "ACTIVE", + Status: statusActive, QueryLogStatus: queryLogStatus, DefaultResultConfiguration: defaultResultConfiguration, PaymentConfiguration: paymentConfiguration, @@ -900,7 +1013,7 @@ func (b *InMemoryBackend) UpdateMembership( if defaultResultConfiguration != nil { m.DefaultResultConfiguration = defaultResultConfiguration } - m.UpdateTime = now() + m.UpdateTime = b.now() return m, nil } @@ -931,7 +1044,7 @@ func (b *InMemoryBackend) CreateConfiguredTable( return nil, ErrValidation } id := uuid.NewString() - ts := now() + ts := b.now() ct := &ConfiguredTable{ ConfiguredTableIdentifier: id, Arn: b.configuredTableARN(id), @@ -1001,7 +1114,7 @@ func (b *InMemoryBackend) UpdateConfiguredTable( if description != "" { ct.Description = description } - ct.UpdateTime = now() + ct.UpdateTime = b.now() return ct, nil } @@ -1036,7 +1149,7 @@ func (b *InMemoryBackend) CreateConfiguredTableAnalysisRule( if _, exists := b.ctAnalysisRules[configuredTableID][analysisRuleType]; exists { return nil, ErrAlreadyExists } - ts := now() + ts := b.now() rule := &ConfiguredTableAnalysisRule{ ConfiguredTableIdentifier: configuredTableID, ConfiguredTableArn: ct.Arn, @@ -1083,7 +1196,7 @@ func (b *InMemoryBackend) UpdateConfiguredTableAnalysisRule( return nil, ErrNotFound } rule.Policy = policy - rule.UpdateTime = now() + rule.UpdateTime = b.now() return rule, nil } @@ -1126,7 +1239,7 @@ func (b *InMemoryBackend) CreateConfiguredTableAssociation( b.ctAssociations[membershipID] = make(map[string]*ConfiguredTableAssociation) } id := uuid.NewString() - ts := now() + ts := b.now() assoc := &ConfiguredTableAssociation{ ConfiguredTableAssociationIdentifier: id, Arn: b.ctAssociationARN(membershipID, id), @@ -1211,7 +1324,7 @@ func (b *InMemoryBackend) UpdateConfiguredTableAssociation( if roleArn != "" { assoc.RoleArn = roleArn } - assoc.UpdateTime = now() + assoc.UpdateTime = b.now() return assoc, nil } @@ -1255,7 +1368,7 @@ func (b *InMemoryBackend) CreateConfiguredTableAssociationAnalysisRule( return nil, ErrAlreadyExists } mem := b.memberships[membershipID] - ts := now() + ts := b.now() rule := &ConfiguredTableAssociationAnalysisRule{ ConfiguredTableAssociationIdentifier: assocID, ConfiguredTableAssociationArn: assoc.Arn, @@ -1304,7 +1417,7 @@ func (b *InMemoryBackend) UpdateConfiguredTableAssociationAnalysisRule( return nil, ErrNotFound } rule.Policy = policy - rule.UpdateTime = now() + rule.UpdateTime = b.now() return rule, nil } @@ -1347,7 +1460,7 @@ func (b *InMemoryBackend) CreateAnalysisTemplate( b.analysisTemplates[membershipID] = make(map[string]*AnalysisTemplate) } id := uuid.NewString() - ts := now() + ts := b.now() collab := b.collaborations[mem.CollaborationIdentifier] var collabArn string if collab != nil { @@ -1403,19 +1516,7 @@ func (b *InMemoryBackend) ListAnalysisTemplates( page, next := listItems( b.analysisTemplates[membershipID], nil, - func(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, - } - }, + toAnalysisTemplateSummary, func(a, c *AnalysisTemplateSummary) bool { return a.AnalysisTemplateIdentifier < c.AnalysisTemplateIdentifier }, @@ -1438,7 +1539,7 @@ func (b *InMemoryBackend) UpdateAnalysisTemplate( return nil, ErrNotFound } tmpl.Description = description - tmpl.UpdateTime = now() + tmpl.UpdateTime = b.now() return tmpl, nil } @@ -1484,19 +1585,7 @@ func (b *InMemoryBackend) ListCollaborationAnalysisTemplates( page, next := listNestedItems( b.analysisTemplates, func(t *AnalysisTemplate) bool { return t.CollaborationIdentifier == collaborationID }, - func(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, - } - }, + toAnalysisTemplateSummary, func(a, c *AnalysisTemplateSummary) bool { return a.AnalysisTemplateIdentifier < c.AnalysisTemplateIdentifier }, @@ -1533,7 +1622,7 @@ func (b *InMemoryBackend) BatchGetCollaborationAnalysisTemplate( if !found { errors = append( errors, - BatchError{Arn: arnStr, Code: "ResourceNotFoundException", Message: "not found"}, + BatchError{Arn: arnStr, Code: errCodeNotFound, Message: "not found"}, ) } } @@ -1567,19 +1656,7 @@ func (b *InMemoryBackend) ListSchemas( page, next := listItems( b.schemas[collaborationID], func(s *Schema) bool { return schemaType == "" || s.Type == schemaType }, - func(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, - } - }, + toSchemaSummary, func(a, c *SchemaSummary) bool { return a.Name < c.Name }, maxResults, nextToken, ) @@ -1602,7 +1679,7 @@ func (b *InMemoryBackend) BatchGetSchema( if ok { results = append(results, s) } else { - errors = append(errors, BatchError{Name: name, Code: "ResourceNotFoundException", Message: "not found"}) + errors = append(errors, BatchError{Name: name, Code: errCodeNotFound, Message: "not found"}) } } return results, errors, nil @@ -1652,7 +1729,7 @@ func (b *InMemoryBackend) BatchGetSchemaAnalysisRule( } errors = append( errors, - BatchError{Name: name, Code: "ResourceNotFoundException", Message: "not found"}, + BatchError{Name: name, Code: errCodeNotFound, Message: "not found"}, ) } return results, errors, nil @@ -1675,7 +1752,7 @@ func (b *InMemoryBackend) StartProtectedQuery( b.protectedQueries[membershipID] = make(map[string]*ProtectedQuery) } id := uuid.NewString() - ts := now() + ts := b.now() var sqlParams map[string]any if sqlText != "" { sqlParams = map[string]any{"queryString": sqlText} @@ -1776,7 +1853,7 @@ func (b *InMemoryBackend) StartProtectedJob( Type: jobType, JobParameters: jobParameters, ResultConfiguration: resultConfig, - CreateTime: now(), + CreateTime: b.now(), } b.protectedJobs[membershipID][id] = j return j, nil @@ -1857,7 +1934,7 @@ func (b *InMemoryBackend) CreatePrivacyBudgetTemplate( b.privacyBudgetTemplates[membershipID] = make(map[string]*PrivacyBudgetTemplate) } id := uuid.NewString() - ts := now() + ts := b.now() collab := b.collaborations[mem.CollaborationIdentifier] var collabArn string if collab != nil { @@ -1913,19 +1990,7 @@ func (b *InMemoryBackend) ListPrivacyBudgetTemplates( func(t *PrivacyBudgetTemplate) bool { return privacyBudgetType == "" || t.PrivacyBudgetType == privacyBudgetType }, - func(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, - } - }, + toPrivacyBudgetTemplateSummary, func(a, c *PrivacyBudgetTemplateSummary) bool { return a.PrivacyBudgetTemplateIdentifier < c.PrivacyBudgetTemplateIdentifier }, @@ -1954,7 +2019,7 @@ func (b *InMemoryBackend) UpdatePrivacyBudgetTemplate( if parameters != nil { tmpl.Parameters = parameters } - tmpl.UpdateTime = now() + tmpl.UpdateTime = b.now() return tmpl, nil } @@ -2023,19 +2088,7 @@ func (b *InMemoryBackend) ListCollaborationPrivacyBudgetTemplates( page, next := listNestedItems( b.privacyBudgetTemplates, func(t *PrivacyBudgetTemplate) bool { return t.CollaborationIdentifier == collaborationID }, - func(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, - } - }, + toPrivacyBudgetTemplateSummary, func(a, c *PrivacyBudgetTemplateSummary) bool { return a.PrivacyBudgetTemplateIdentifier < c.PrivacyBudgetTemplateIdentifier }, @@ -2073,29 +2126,23 @@ func (b *InMemoryBackend) CreateIdMappingTable( if b.idMappingTables[membershipID] == nil { b.idMappingTables[membershipID] = make(map[string]*IdMappingTable) } - id := uuid.NewString() - ts := now() - collab := b.collaborations[mem.CollaborationIdentifier] - var collabArn string - if collab != nil { - collabArn = collab.Arn - } + ctx := b.newMembershipCtx(mem) t := &IdMappingTable{ - IdMappingTableIdentifier: id, - Arn: b.idMappingTableARN(membershipID, id), - CollaborationArn: collabArn, - CollaborationIdentifier: mem.CollaborationIdentifier, - MembershipArn: mem.Arn, + IdMappingTableIdentifier: ctx.id, + Arn: b.idMappingTableARN(membershipID, ctx.id), + CollaborationArn: ctx.collaborationArn, + CollaborationIdentifier: ctx.collaborationID, + MembershipArn: ctx.membershipArn, MembershipIdentifier: membershipID, Name: name, Description: description, InputReferenceConfig: inputReferenceConfig, KmsKeyArn: kmsKeyArn, - CreateTime: ts, - UpdateTime: ts, + CreateTime: ctx.ts, + UpdateTime: ctx.ts, Tags: tags, } - b.idMappingTables[membershipID][id] = t + b.idMappingTables[membershipID][ctx.id] = t if len(tags) > 0 { b.tagsByArn[t.Arn] = maps.Clone(tags) } @@ -2127,19 +2174,7 @@ func (b *InMemoryBackend) ListIdMappingTables( page, next := listItems( b.idMappingTables[membershipID], nil, - func(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, - } - }, + toIdMappingTableSummary, func(a, c *IdMappingTableSummary) bool { return a.IdMappingTableIdentifier < c.IdMappingTableIdentifier }, @@ -2167,7 +2202,7 @@ func (b *InMemoryBackend) UpdateIdMappingTable( if kmsKeyArn != "" { t.KmsKeyArn = kmsKeyArn } - t.UpdateTime = now() + t.UpdateTime = b.now() return t, nil } @@ -2218,29 +2253,23 @@ func (b *InMemoryBackend) CreateIdNamespaceAssociation( if b.idNamespaceAssociations[membershipID] == nil { b.idNamespaceAssociations[membershipID] = make(map[string]*IdNamespaceAssociation) } - id := uuid.NewString() - ts := now() - collab := b.collaborations[mem.CollaborationIdentifier] - var collabArn string - if collab != nil { - collabArn = collab.Arn - } + ctx := b.newMembershipCtx(mem) assoc := &IdNamespaceAssociation{ - IdNamespaceAssociationIdentifier: id, - Arn: b.idNamespaceAssocARN(membershipID, id), - CollaborationArn: collabArn, - CollaborationIdentifier: mem.CollaborationIdentifier, - MembershipArn: mem.Arn, + IdNamespaceAssociationIdentifier: ctx.id, + Arn: b.idNamespaceAssocARN(membershipID, ctx.id), + CollaborationArn: ctx.collaborationArn, + CollaborationIdentifier: ctx.collaborationID, + MembershipArn: ctx.membershipArn, MembershipIdentifier: membershipID, Name: name, Description: description, InputReferenceConfig: inputReferenceConfig, IdMappingConfig: idMappingConfig, - CreateTime: ts, - UpdateTime: ts, + CreateTime: ctx.ts, + UpdateTime: ctx.ts, Tags: tags, } - b.idNamespaceAssociations[membershipID][id] = assoc + b.idNamespaceAssociations[membershipID][ctx.id] = assoc if len(tags) > 0 { b.tagsByArn[assoc.Arn] = maps.Clone(tags) } @@ -2271,24 +2300,15 @@ func (b *InMemoryBackend) ListIdNamespaceAssociations( if _, ok := b.memberships[membershipID]; !ok { return nil, "", ErrNotFound } - var items []*IdNamespaceAssociationSummary - for _, a := range b.idNamespaceAssociations[membershipID] { - items = append(items, &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, - }) - } - sort.Slice(items, func(i, j int) bool { - return items[i].IdNamespaceAssociationIdentifier < items[j].IdNamespaceAssociationIdentifier - }) - page, next := paginate(items, maxResults, nextToken) + page, next := listItems( + b.idNamespaceAssociations[membershipID], + nil, + toIdNamespaceAssociationSummary, + func(a, c *IdNamespaceAssociationSummary) bool { + return a.IdNamespaceAssociationIdentifier < c.IdNamespaceAssociationIdentifier + }, + maxResults, nextToken, + ) return page, next, nil } @@ -2312,7 +2332,7 @@ func (b *InMemoryBackend) UpdateIdNamespaceAssociation( if idMappingConfig != nil { assoc.IdMappingConfig = idMappingConfig } - assoc.UpdateTime = now() + assoc.UpdateTime = b.now() return assoc, nil } @@ -2356,28 +2376,15 @@ func (b *InMemoryBackend) ListCollaborationIdNamespaceAssociations( if _, ok := b.collaborations[collaborationID]; !ok { return nil, "", ErrNotFound } - var items []*IdNamespaceAssociationSummary - for _, assocs := range b.idNamespaceAssociations { - for _, a := range assocs { - if a.CollaborationIdentifier == collaborationID { - items = append(items, &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, - }) - } - } - } - sort.Slice(items, func(i, j int) bool { - return items[i].IdNamespaceAssociationIdentifier < items[j].IdNamespaceAssociationIdentifier - }) - page, next := paginate(items, maxResults, nextToken) + 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 } @@ -2398,7 +2405,7 @@ func (b *InMemoryBackend) CreateConfiguredAudienceModelAssociation( b.camaAssociations[membershipID] = make(map[string]*ConfiguredAudienceModelAssociation) } id := uuid.NewString() - ts := now() + ts := b.now() collab := b.collaborations[mem.CollaborationIdentifier] var collabArn string if collab != nil { @@ -2450,24 +2457,15 @@ func (b *InMemoryBackend) ListConfiguredAudienceModelAssociations( if _, ok := b.memberships[membershipID]; !ok { return nil, "", ErrNotFound } - var items []*ConfiguredAudienceModelAssociationSummary - for _, a := range b.camaAssociations[membershipID] { - items = append(items, &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, - }) - } - sort.Slice(items, func(i, j int) bool { - return items[i].ConfiguredAudienceModelAssociationIdentifier < items[j].ConfiguredAudienceModelAssociationIdentifier - }) - page, next := paginate(items, maxResults, nextToken) + page, next := listItems( + b.camaAssociations[membershipID], + nil, + toConfiguredAudienceModelAssociationSummary, + func(a, c *ConfiguredAudienceModelAssociationSummary) bool { + return a.ConfiguredAudienceModelAssociationIdentifier < c.ConfiguredAudienceModelAssociationIdentifier + }, + maxResults, nextToken, + ) return page, next, nil } @@ -2490,7 +2488,7 @@ func (b *InMemoryBackend) UpdateConfiguredAudienceModelAssociation( if description != "" { assoc.Description = description } - assoc.UpdateTime = now() + assoc.UpdateTime = b.now() return assoc, nil } @@ -2536,28 +2534,17 @@ func (b *InMemoryBackend) ListCollaborationConfiguredAudienceModelAssociations( if _, ok := b.collaborations[collaborationID]; !ok { return nil, "", ErrNotFound } - var items []*ConfiguredAudienceModelAssociationSummary - for _, assocs := range b.camaAssociations { - for _, a := range assocs { - if a.CollaborationIdentifier == collaborationID { - items = append(items, &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, - }) - } - } - } - sort.Slice(items, func(i, j int) bool { - return items[i].ConfiguredAudienceModelAssociationIdentifier < items[j].ConfiguredAudienceModelAssociationIdentifier - }) - page, next := paginate(items, maxResults, nextToken) + 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 } @@ -2577,7 +2564,7 @@ func (b *InMemoryBackend) CreateCollaborationChangeRequest( b.changeRequests[collaborationID] = make(map[string]*CollaborationChangeRequest) } id := uuid.NewString() - ts := now() + ts := b.now() req := &CollaborationChangeRequest{ ChangeRequestIdentifier: id, CollaborationIdentifier: collaborationID, @@ -2642,7 +2629,7 @@ func (b *InMemoryBackend) UpdateCollaborationChangeRequest( return nil, ErrNotFound } req.Status = status - req.UpdateTime = now() + req.UpdateTime = b.now() return req, nil } From 6982f2920946c852515bfdec2c9d5a4274ac40c3 Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Fri, 12 Jun 2026 23:56:25 -0500 Subject: [PATCH 07/16] WIP: checkpoint (auto) --- services/cleanrooms/backend.go | 85 ++++++++++++++-------------------- 1 file changed, 36 insertions(+), 49 deletions(-) diff --git a/services/cleanrooms/backend.go b/services/cleanrooms/backend.go index 317734432..7c1592459 100644 --- a/services/cleanrooms/backend.go +++ b/services/cleanrooms/backend.go @@ -515,30 +515,6 @@ func (b *InMemoryBackend) Reset() { b.tagsByArn = make(map[string]map[string]string) } -// membershipCtx holds common values computed when creating a resource within a membership. -type membershipCtx struct { - id string - ts float64 - membershipArn string - collaborationID string - collaborationArn string -} - -func (b *InMemoryBackend) newMembershipCtx(mem *Membership) membershipCtx { - collab := b.collaborations[mem.CollaborationIdentifier] - var collabArn string - if collab != nil { - collabArn = collab.Arn - } - return membershipCtx{ - id: uuid.NewString(), - ts: b.now(), - membershipArn: mem.Arn, - collaborationID: mem.CollaborationIdentifier, - collaborationArn: collabArn, - } -} - // ---- ARN helpers ---- func (b *InMemoryBackend) collaborationARN(id string) string { @@ -737,7 +713,9 @@ func toIdNamespaceAssociationSummary(a *IdNamespaceAssociation) *IdNamespaceAsso } } -func toConfiguredAudienceModelAssociationSummary(a *ConfiguredAudienceModelAssociation) *ConfiguredAudienceModelAssociationSummary { +func toConfiguredAudienceModelAssociationSummary( + a *ConfiguredAudienceModelAssociation, +) *ConfiguredAudienceModelAssociationSummary { return &ConfiguredAudienceModelAssociationSummary{ ConfiguredAudienceModelAssociationIdentifier: a.ConfiguredAudienceModelAssociationIdentifier, Arn: a.Arn, @@ -2126,23 +2104,29 @@ func (b *InMemoryBackend) CreateIdMappingTable( if b.idMappingTables[membershipID] == nil { b.idMappingTables[membershipID] = make(map[string]*IdMappingTable) } - ctx := b.newMembershipCtx(mem) + id := uuid.NewString() + ts := b.now() + collab := b.collaborations[mem.CollaborationIdentifier] + var collabArn string + if collab != nil { + collabArn = collab.Arn + } t := &IdMappingTable{ - IdMappingTableIdentifier: ctx.id, - Arn: b.idMappingTableARN(membershipID, ctx.id), - CollaborationArn: ctx.collaborationArn, - CollaborationIdentifier: ctx.collaborationID, - MembershipArn: ctx.membershipArn, + 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: ctx.ts, - UpdateTime: ctx.ts, + CreateTime: ts, + UpdateTime: ts, Tags: tags, } - b.idMappingTables[membershipID][ctx.id] = t + b.idMappingTables[membershipID][id] = t if len(tags) > 0 { b.tagsByArn[t.Arn] = maps.Clone(tags) } @@ -2253,23 +2237,29 @@ func (b *InMemoryBackend) CreateIdNamespaceAssociation( if b.idNamespaceAssociations[membershipID] == nil { b.idNamespaceAssociations[membershipID] = make(map[string]*IdNamespaceAssociation) } - ctx := b.newMembershipCtx(mem) + id := uuid.NewString() + ts := b.now() + collab := b.collaborations[mem.CollaborationIdentifier] + var collabArn string + if collab != nil { + collabArn = collab.Arn + } assoc := &IdNamespaceAssociation{ - IdNamespaceAssociationIdentifier: ctx.id, - Arn: b.idNamespaceAssocARN(membershipID, ctx.id), - CollaborationArn: ctx.collaborationArn, - CollaborationIdentifier: ctx.collaborationID, - MembershipArn: ctx.membershipArn, + 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: ctx.ts, - UpdateTime: ctx.ts, + CreateTime: ts, + UpdateTime: ts, Tags: tags, } - b.idNamespaceAssociations[membershipID][ctx.id] = assoc + b.idNamespaceAssociations[membershipID][id] = assoc if len(tags) > 0 { b.tagsByArn[assoc.Arn] = maps.Clone(tags) } @@ -2383,18 +2373,15 @@ func (b *InMemoryBackend) ListCollaborationIdNamespaceAssociations( func(a, c *IdNamespaceAssociationSummary) bool { return a.IdNamespaceAssociationIdentifier < c.IdNamespaceAssociationIdentifier }, - maxResults, nextToken, + maxResults, + nextToken, ) return page, next, nil } // ---- ConfiguredAudienceModelAssociation ---- -func (b *InMemoryBackend) CreateConfiguredAudienceModelAssociation( - membershipID, configuredAudienceModelArn, name, description string, - manageResourcePolicies bool, - tags map[string]string, -) (*ConfiguredAudienceModelAssociation, error) { +func (b *InMemoryBackend) CreateConfiguredAudienceModelAssociation(membershipID, configuredAudienceModelArn, name, description string, manageResourcePolicies bool, tags map[string]string) (*ConfiguredAudienceModelAssociation, error) { b.mu.Lock("CreateConfiguredAudienceModelAssociation") defer b.mu.Unlock() mem, ok := b.memberships[membershipID] From 65d762d7025333c4b2f6c7f8095f72a253337244 Mon Sep 17 00:00:00 2001 From: Witness Patrol Date: Sat, 13 Jun 2026 00:09:24 -0500 Subject: [PATCH 08/16] WIP: checkpoint (auto) --- .../results.json | 2 +- services/cleanrooms/backend.go | 19 +- services/cloudformation/resources_phase3.go | 1 + services/pipes/backend.go | 677 ++++++++++++++++++ services/pipes/export_test.go | 31 + services/pipes/handler.go | 134 ++++ services/pipes/handler_test.go | 126 ++++ services/pipes/persistence.go | 1 + services/pipes/persistence_test.go | 23 + services/pipes/runner.go | 127 ++++ services/pipes/runner_test.go | 65 ++ ui/src/routes/pipes/+page.svelte | 17 +- ui/src/routes/pipes/page.test.ts | 104 ++- 13 files changed, 1310 insertions(+), 17 deletions(-) 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 index 7c1592459..aa538ae8b 100644 --- a/services/cleanrooms/backend.go +++ b/services/cleanrooms/backend.go @@ -25,6 +25,7 @@ var ( const ( statusActive = "ACTIVE" errCodeNotFound = "ResourceNotFoundException" + errMsgNotFound = "not found" ) // ---- types ---- @@ -1600,7 +1601,7 @@ func (b *InMemoryBackend) BatchGetCollaborationAnalysisTemplate( if !found { errors = append( errors, - BatchError{Arn: arnStr, Code: errCodeNotFound, Message: "not found"}, + BatchError{Arn: arnStr, Code: errCodeNotFound, Message: errMsgNotFound}, ) } } @@ -1657,7 +1658,7 @@ func (b *InMemoryBackend) BatchGetSchema( if ok { results = append(results, s) } else { - errors = append(errors, BatchError{Name: name, Code: errCodeNotFound, Message: "not found"}) + errors = append(errors, BatchError{Name: name, Code: errCodeNotFound, Message: errMsgNotFound}) } } return results, errors, nil @@ -1707,7 +1708,7 @@ func (b *InMemoryBackend) BatchGetSchemaAnalysisRule( } errors = append( errors, - BatchError{Name: name, Code: errCodeNotFound, Message: "not found"}, + BatchError{Name: name, Code: errCodeNotFound, Message: errMsgNotFound}, ) } return results, errors, nil @@ -2095,6 +2096,9 @@ func (b *InMemoryBackend) CreateIdMappingTable( 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] @@ -2381,7 +2385,14 @@ func (b *InMemoryBackend) ListCollaborationIdNamespaceAssociations( // ---- ConfiguredAudienceModelAssociation ---- -func (b *InMemoryBackend) CreateConfiguredAudienceModelAssociation(membershipID, configuredAudienceModelArn, name, description string, manageResourcePolicies bool, tags map[string]string) (*ConfiguredAudienceModelAssociation, error) { +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] diff --git a/services/cloudformation/resources_phase3.go b/services/cloudformation/resources_phase3.go index c9783bf48..0d0a08363 100644 --- a/services/cloudformation/resources_phase3.go +++ b/services/cloudformation/resources_phase3.go @@ -7,6 +7,7 @@ import ( "strings" apigatewayv2backend "github.com/blackbirdworks/gopherstack/services/apigatewayv2" + "github.com/blackbirdworks/gopherstack/services/pipes" autoscalingbackend "github.com/blackbirdworks/gopherstack/services/autoscaling" batchbackend "github.com/blackbirdworks/gopherstack/services/batch" codebuildbackend "github.com/blackbirdworks/gopherstack/services/codebuild" diff --git a/services/pipes/backend.go b/services/pipes/backend.go index a07c1f9f8..91bc6b3c7 100644 --- a/services/pipes/backend.go +++ b/services/pipes/backend.go @@ -1,14 +1,20 @@ package pipes import ( +<<<<<<< Updated upstream "context" +======= +>>>>>>> Stashed changes "encoding/base64" "encoding/json" "fmt" "maps" "regexp" "strings" +<<<<<<< Updated upstream "sync" +======= +>>>>>>> Stashed changes "time" "github.com/blackbirdworks/gopherstack/pkgs/arn" @@ -19,19 +25,25 @@ import ( const ( stateRunning = "RUNNING" stateStopped = "STOPPED" +<<<<<<< Updated upstream stateCreating = "CREATING" stateUpdating = "UPDATING" stateDeleting = "DELETING" +======= +>>>>>>> Stashed changes stateStarting = "STARTING" stateStopping = "STOPPING" stateCreateFailed = "CREATE_FAILED" stateUpdateFailed = "UPDATE_FAILED" stateDeleteFailed = "DELETE_FAILED" +<<<<<<< Updated upstream stateStartFailed = "START_FAILED" stateStopFailed = "STOP_FAILED" // stateTransitionDelay is the simulated delay for async state transitions. stateTransitionDelay = 10 * time.Millisecond +======= +>>>>>>> Stashed changes maxPipeNameLen = 64 maxTagKeyLen = 128 @@ -39,18 +51,33 @@ const ( maxTagsPerPipe = 50 maxPipesPerAcct = 1000 +<<<<<<< Updated upstream // nextTokenSep separates cursor values in pagination tokens. +======= + // nextTokenSep separates the name from the rest in a NextToken. +>>>>>>> Stashed changes nextTokenSep = "\x00" ) var ( +<<<<<<< Updated upstream ErrNotFound = awserr.New("NotFoundException", awserr.ErrNotFound) ErrAlreadyExists = awserr.New("ConflictException", awserr.ErrConflict) ErrValidation = awserr.New("ValidationException", awserr.ErrInvalidParameter) +======= + ErrNotFound = awserr.New("NotFoundException", awserr.ErrNotFound) + ErrAlreadyExists = awserr.New("ConflictException", awserr.ErrConflict) + ErrValidation = awserr.New("ValidationException", awserr.ErrInvalidParameter) +>>>>>>> Stashed changes pipeNameRE = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) ) +<<<<<<< Updated upstream +======= +// --- Sub-parameter types --- + +>>>>>>> Stashed changes // FilterCriteria holds event filter patterns applied before forwarding to the target. type FilterCriteria struct { Filters []Filter `json:"Filters,omitempty"` @@ -61,6 +88,7 @@ type Filter struct { Pattern string `json:"Pattern,omitempty"` } +<<<<<<< Updated upstream // AwsVpcConfiguration is the VPC network configuration for ECS tasks. type AwsVpcConfiguration struct { AssignPublicIP string `json:"AssignPublicIp,omitempty"` @@ -156,12 +184,15 @@ type RuntimeMetricsStreaming struct { Level string `json:"Level,omitempty"` } +======= +>>>>>>> Stashed changes // SQSSourceParameters holds SQS-specific source configuration. type SQSSourceParameters struct { BatchSize int `json:"BatchSize,omitempty"` MaximumBatchingWindowInSeconds int `json:"MaximumBatchingWindowInSeconds,omitempty"` } +<<<<<<< Updated upstream // KinesisStreamSourceParameters holds Kinesis-specific source configuration. type KinesisStreamSourceParameters struct { StartingPositionTimestamp *time.Time `json:"StartingPositionTimestamp,omitempty"` @@ -237,6 +268,12 @@ type SourceParameters struct { SelfManagedKafkaParameters *SelfManagedKafkaSourceParameters `json:"SelfManagedKafkaParameters,omitempty"` RabbitMQBrokerParameters *RabbitMQBrokerSourceParameters `json:"RabbitMQBrokerParameters,omitempty"` ActiveMQBrokerParameters *ActiveMQBrokerSourceParameters `json:"ActiveMQBrokerParameters,omitempty"` +======= +// SourceParameters holds source-specific configuration. +type SourceParameters struct { + FilterCriteria *FilterCriteria `json:"FilterCriteria,omitempty"` + SqsQueueParameters *SQSSourceParameters `json:"SqsQueueParameters,omitempty"` +>>>>>>> Stashed changes } // LambdaFunctionParameters holds Lambda-specific target configuration. @@ -244,6 +281,7 @@ type LambdaFunctionParameters struct { InvocationType string `json:"InvocationType,omitempty"` } +<<<<<<< Updated upstream // StepFunctionTargetParameters holds Step Functions target configuration. type StepFunctionTargetParameters struct { InvocationType string `json:"InvocationType,omitempty"` @@ -394,6 +432,12 @@ type TargetParameters struct { TimestreamParameters *TimestreamParameters `json:"TimestreamParameters,omitempty"` HTTPParameters *TargetHTTPParameters `json:"HttpParameters,omitempty"` InputTemplate string `json:"InputTemplate,omitempty"` +======= +// TargetParameters holds target-specific configuration. +type TargetParameters struct { + InputTemplate string `json:"InputTemplate,omitempty"` + LambdaFunctionParameters *LambdaFunctionParameters `json:"LambdaFunctionParameters,omitempty"` +>>>>>>> Stashed changes } // DeadLetterConfig identifies the DLQ for failed pipe events. @@ -401,6 +445,7 @@ type DeadLetterConfig struct { Arn string `json:"Arn,omitempty"` } +<<<<<<< Updated upstream // EnrichmentHTTPParameters holds HTTP parameters for enrichment calls. type EnrichmentHTTPParameters struct { HeaderParameters map[string]string `json:"HeaderParameters,omitempty"` @@ -415,10 +460,14 @@ type EnrichmentParameters struct { } // CloudwatchLogsLogDestination is a CloudWatch Logs target. +======= +// CloudwatchLogsLogDestination is a CloudWatch Logs target for pipe execution logs. +>>>>>>> Stashed changes type CloudwatchLogsLogDestination struct { LogGroupArn string `json:"LogGroupArn,omitempty"` } +<<<<<<< Updated upstream // FirehoseLogDestination is a Firehose delivery stream log target. type FirehoseLogDestination struct { DeliveryStreamArn string `json:"DeliveryStreamArn,omitempty"` @@ -437,10 +486,16 @@ type LogDestination struct { CloudwatchLogsLogDestination *CloudwatchLogsLogDestination `json:"CloudwatchLogsLogDestination,omitempty"` FirehoseLogDestination *FirehoseLogDestination `json:"FirehoseLogDestination,omitempty"` S3LogDestination *S3LogDestination `json:"S3LogDestination,omitempty"` +======= +// LogDestination wraps possible log destination types. +type LogDestination struct { + CloudwatchLogsLogDestination *CloudwatchLogsLogDestination `json:"CloudwatchLogsLogDestination,omitempty"` +>>>>>>> Stashed changes } // LogConfiguration controls pipe execution logging. type LogConfiguration struct { +<<<<<<< Updated upstream Level string `json:"Level,omitempty"` Destinations []LogDestination `json:"Destinations,omitempty"` IncludeExecutionData []string `json:"IncludeExecutionData,omitempty"` @@ -778,6 +833,50 @@ func cloneEnrichmentParameters(src *EnrichmentParameters) *EnrichmentParameters return &ep } +======= + Destinations []LogDestination `json:"Destinations,omitempty"` + IncludeExecutionData []string `json:"IncludeExecutionData,omitempty"` + Level string `json:"Level,omitempty"` +} + +// --- Pipe --- + +// Pipe represents an EventBridge Pipe. +type Pipe struct { + SourceParameters *SourceParameters `json:"sourceParameters,omitempty"` + TargetParameters *TargetParameters `json:"targetParameters,omitempty"` + DeadLetterConfig *DeadLetterConfig `json:"deadLetterConfig,omitempty"` + LogConfiguration *LogConfiguration `json:"logConfiguration,omitempty"` + LastModifiedTime time.Time `json:"lastModifiedTime"` + CreationTime time.Time `json:"creationTime"` + Tags map[string]string `json:"tags,omitempty"` + Description string `json:"description,omitempty"` + Enrichment string `json:"enrichment,omitempty"` + Source string `json:"source"` + Target string `json:"target"` + RoleARN string `json:"roleArn"` + StateReason string `json:"stateReason,omitempty"` + DesiredState string `json:"desiredState"` + CurrentState string `json:"currentState"` + AccountID string `json:"accountID"` + Region string `json:"region"` + ARN string `json:"arn"` + Name string `json:"name"` +} + +// effectiveBatchSize returns the configured SQS batch size, or the default. +func (p *Pipe) effectiveBatchSize() int { + if p.SourceParameters != nil && + p.SourceParameters.SqsQueueParameters != nil && + p.SourceParameters.SqsQueueParameters.BatchSize > 0 { + return p.SourceParameters.SqsQueueParameters.BatchSize + } + + return pipeDefaultBatchSize +} + +// clonePipe returns a deep copy of p. +>>>>>>> Stashed changes func clonePipe(p *Pipe) *Pipe { cp := *p cp.Tags = maps.Clone(p.Tags) @@ -813,10 +912,53 @@ func clonePipe(p *Pipe) *Pipe { cp.RuntimeMetricsStreaming = &rms } + if p.SourceParameters != nil { + sp := *p.SourceParameters + if p.SourceParameters.FilterCriteria != nil { + fc := *p.SourceParameters.FilterCriteria + fc.Filters = append([]Filter(nil), p.SourceParameters.FilterCriteria.Filters...) + sp.FilterCriteria = &fc + } + + if p.SourceParameters.SqsQueueParameters != nil { + sqsp := *p.SourceParameters.SqsQueueParameters + sp.SqsQueueParameters = &sqsp + } + + cp.SourceParameters = &sp + } + + if p.TargetParameters != nil { + tp := *p.TargetParameters + if p.TargetParameters.LambdaFunctionParameters != nil { + lfp := *p.TargetParameters.LambdaFunctionParameters + tp.LambdaFunctionParameters = &lfp + } + + cp.TargetParameters = &tp + } + + if p.DeadLetterConfig != nil { + dlc := *p.DeadLetterConfig + cp.DeadLetterConfig = &dlc + } + + if p.LogConfiguration != nil { + lc := *p.LogConfiguration + lc.Destinations = append([]LogDestination(nil), p.LogConfiguration.Destinations...) + lc.IncludeExecutionData = append([]string(nil), p.LogConfiguration.IncludeExecutionData...) + cp.LogConfiguration = &lc + } + return &cp } +<<<<<<< Updated upstream // InMemoryBackend is the in-memory store for pipes. +======= +// --- Backend --- + +>>>>>>> Stashed changes type InMemoryBackend struct { svcCtx context.Context pipes map[string]*Pipe @@ -913,6 +1055,7 @@ func (b *InMemoryBackend) Region() string { return b.region } // CreatePipeInput holds the full set of fields for pipe creation. type CreatePipeInput struct { +<<<<<<< Updated upstream Tags map[string]string SourceParameters *SourceParameters TargetParameters *TargetParameters @@ -930,10 +1073,28 @@ type CreatePipeInput struct { DesiredState string } +======= + Tags map[string]string + SourceParameters *SourceParameters + TargetParameters *TargetParameters + DeadLetterConfig *DeadLetterConfig + LogConfiguration *LogConfiguration + Name string + RoleARN string + Source string + Target string + Description string + Enrichment string + DesiredState string +} + +// CreatePipe creates a new pipe. +>>>>>>> Stashed changes func (b *InMemoryBackend) CreatePipe(in CreatePipeInput) (*Pipe, error) { if err := validatePipeName(in.Name); err != nil { return nil, err } +<<<<<<< Updated upstream if err := validateDesiredState(in.DesiredState); err != nil { return nil, err } @@ -949,11 +1110,30 @@ func (b *InMemoryBackend) CreatePipe(in CreatePipeInput) (*Pipe, error) { if err := validateSourceBatchSize(in.SourceParameters); err != nil { return nil, err } +======= + + if err := validateDesiredState(in.DesiredState); err != nil { + return nil, err + } + + if in.Source == "" { + return nil, fmt.Errorf("%w: Source is required", ErrValidation) + } + + if in.Target == "" { + return nil, fmt.Errorf("%w: Target is required", ErrValidation) + } + + if err := validateTags(in.Tags); err != nil { + return nil, err + } +>>>>>>> Stashed changes b.mu.Lock("CreatePipe") defer b.mu.Unlock() if len(b.pipes) >= maxPipesPerAcct { +<<<<<<< Updated upstream return nil, fmt.Errorf( "%w: account has reached the maximum number of pipes (%d)", ErrValidation, @@ -963,6 +1143,16 @@ func (b *InMemoryBackend) CreatePipe(in CreatePipeInput) (*Pipe, error) { if _, ok := b.pipes[in.Name]; ok { return nil, fmt.Errorf("%w: pipe %s already exists", ErrAlreadyExists, in.Name) } +======= + return nil, fmt.Errorf("%w: account has reached the maximum number of pipes (%d)", + ErrValidation, maxPipesPerAcct) + } + + if _, ok := b.pipes[in.Name]; ok { + return nil, fmt.Errorf("%w: pipe %s already exists", ErrAlreadyExists, in.Name) + } + +>>>>>>> Stashed changes if in.DesiredState == "" { in.DesiredState = stateRunning } @@ -970,6 +1160,7 @@ func (b *InMemoryBackend) CreatePipe(in CreatePipeInput) (*Pipe, error) { now := time.Now() pipeARN := arn.Build("pipes", b.region, b.accountID, "pipe/"+in.Name) p := &Pipe{ +<<<<<<< Updated upstream Name: in.Name, ARN: pipeARN, RoleARN: in.RoleARN, Source: in.Source, Target: in.Target, Description: in.Description, Enrichment: in.Enrichment, KmsKeyIdentifier: in.KmsKeyIdentifier, @@ -983,6 +1174,26 @@ func (b *InMemoryBackend) CreatePipe(in CreatePipeInput) (*Pipe, error) { LogConfiguration: in.LogConfiguration, EnrichmentParameters: in.EnrichmentParameters, RuntimeMetricsStreaming: in.RuntimeMetricsStreaming, +======= + Name: in.Name, + ARN: pipeARN, + RoleARN: in.RoleARN, + Source: in.Source, + Target: in.Target, + Description: in.Description, + Enrichment: in.Enrichment, + DesiredState: in.DesiredState, + CurrentState: in.DesiredState, + AccountID: b.accountID, + Region: b.region, + CreationTime: now, + LastModifiedTime: now, + Tags: mergeTags(nil, in.Tags), + SourceParameters: in.SourceParameters, + TargetParameters: in.TargetParameters, + DeadLetterConfig: in.DeadLetterConfig, + LogConfiguration: in.LogConfiguration, +>>>>>>> Stashed changes } b.pipes[in.Name] = p b.pipeARNIndex[pipeARN] = in.Name @@ -1033,10 +1244,18 @@ type ListPipesFilter struct { // ListPipesResult holds the paginated result of a ListPipes call. type ListPipesResult struct { +<<<<<<< Updated upstream NextToken string Pipes []*Pipe } +======= + Pipes []*Pipe + NextToken string +} + +// ListPipes returns pipes matching the filter with optional pagination. +>>>>>>> Stashed changes func (b *InMemoryBackend) ListPipes(f ListPipesFilter) ListPipesResult { b.mu.RLock("ListPipes") defer b.mu.RUnlock() @@ -1046,6 +1265,7 @@ func (b *InMemoryBackend) ListPipes(f ListPipesFilter) ListPipesResult { limit = 1000 } +<<<<<<< Updated upstream names := b.sortedPipeNames() startIdx := b.resolveStartIndex(names, f.NextToken) result, lastIncluded := b.collectMatchingPipes(names, startIdx, limit, f) @@ -1202,13 +1422,138 @@ func applyUpdateFields(p *Pipe, in UpdatePipeInput) { } } +======= + // Deterministic iteration order: sort pipe names so NextToken is stable. + names := make([]string, 0, len(b.pipes)) + for name := range b.pipes { + names = append(names, name) + } + + // Simple lexicographic sort without importing sort package; use a manual bubble for small N. + for i := 0; i < len(names); i++ { + for j := i + 1; j < len(names); j++ { + if names[j] < names[i] { + names[i], names[j] = names[j], names[i] + } + } + } + + // Apply NextToken offset. + startIdx := 0 + if f.NextToken != "" { + decoded, err := base64.StdEncoding.DecodeString(f.NextToken) + if err == nil { + cursor := strings.TrimSuffix(string(decoded), nextTokenSep) + for i, n := range names { + if n > cursor { + startIdx = i + break + } + startIdx = len(names) + } + } + } + + var result []*Pipe + var lastIncluded string + + for i := startIdx; i < len(names); i++ { + if len(result) >= limit { + break + } + + p := b.pipes[names[i]] + + if f.NamePrefix != "" && !strings.HasPrefix(p.Name, f.NamePrefix) { + continue + } + + if f.DesiredState != "" && p.DesiredState != f.DesiredState { + continue + } + + if f.CurrentState != "" && p.CurrentState != f.CurrentState { + continue + } + + if f.SourcePrefix != "" && !strings.HasPrefix(p.Source, f.SourcePrefix) { + continue + } + + if f.TargetPrefix != "" && !strings.HasPrefix(p.Target, f.TargetPrefix) { + continue + } + + result = append(result, clonePipe(p)) + lastIncluded = p.Name + } + + var nextToken string + + if len(result) == limit && lastIncluded != "" { + // Check if there are more matching pipes beyond the limit. + for i := startIdx + len(result); i < len(names); i++ { + p := b.pipes[names[i]] + if matchesFilter(p, f) { + nextToken = base64.StdEncoding.EncodeToString([]byte(lastIncluded + nextTokenSep)) + break + } + } + } + + return ListPipesResult{Pipes: result, NextToken: nextToken} +} + +// matchesFilter reports whether a pipe matches the filter criteria (excluding pagination). +func matchesFilter(p *Pipe, f ListPipesFilter) bool { + if f.NamePrefix != "" && !strings.HasPrefix(p.Name, f.NamePrefix) { + return false + } + + if f.DesiredState != "" && p.DesiredState != f.DesiredState { + return false + } + + if f.CurrentState != "" && p.CurrentState != f.CurrentState { + return false + } + + if f.SourcePrefix != "" && !strings.HasPrefix(p.Source, f.SourcePrefix) { + return false + } + + if f.TargetPrefix != "" && !strings.HasPrefix(p.Target, f.TargetPrefix) { + return false + } + + return true +} + +// UpdatePipeInput holds the fields that can be updated on an existing pipe. +type UpdatePipeInput struct { + SourceParameters *SourceParameters + TargetParameters *TargetParameters + DeadLetterConfig *DeadLetterConfig + LogConfiguration *LogConfiguration + RoleARN string + Target string + Description string + Enrichment string + DesiredState string +} + +// UpdatePipe updates an existing pipe. +>>>>>>> Stashed changes func (b *InMemoryBackend) UpdatePipe(name string, in UpdatePipeInput) (*Pipe, error) { if err := validateDesiredState(in.DesiredState); err != nil { return nil, err } +<<<<<<< Updated upstream if err := validateSourceBatchSize(in.SourceParameters); err != nil { return nil, err } +======= +>>>>>>> Stashed changes b.mu.Lock("UpdatePipe") defer b.mu.Unlock() @@ -1218,6 +1563,7 @@ func (b *InMemoryBackend) UpdatePipe(name string, in UpdatePipeInput) (*Pipe, er return nil, fmt.Errorf("%w: pipe %s not found", ErrNotFound, name) } +<<<<<<< Updated upstream applyUpdateFields(p, in) prevDesiredState := p.DesiredState @@ -1225,6 +1571,42 @@ func (b *InMemoryBackend) UpdatePipe(name string, in UpdatePipeInput) (*Pipe, er prevDesiredState = in.DesiredState } p.CurrentState = stateUpdating +======= + if in.RoleARN != "" { + p.RoleARN = in.RoleARN + } + + if in.Target != "" { + p.Target = in.Target + } + + if in.DesiredState != "" { + p.DesiredState = in.DesiredState + } + + if in.Enrichment != "" { + p.Enrichment = in.Enrichment + } + + p.Description = in.Description + + if in.SourceParameters != nil { + p.SourceParameters = in.SourceParameters + } + + if in.TargetParameters != nil { + p.TargetParameters = in.TargetParameters + } + + if in.DeadLetterConfig != nil { + p.DeadLetterConfig = in.DeadLetterConfig + } + + if in.LogConfiguration != nil { + p.LogConfiguration = in.LogConfiguration + } + +>>>>>>> Stashed changes p.LastModifiedTime = time.Now() cp := clonePipe(p) @@ -1281,6 +1663,10 @@ func (b *InMemoryBackend) completeDeleteTransition(name string) { } } +<<<<<<< Updated upstream +======= +// StartPipe transitions a pipe to the RUNNING desired/current state. +>>>>>>> Stashed changes func (b *InMemoryBackend) StartPipe(name string) (*Pipe, error) { b.mu.Lock("StartPipe") defer b.mu.Unlock() @@ -1288,12 +1674,22 @@ func (b *InMemoryBackend) StartPipe(name string) (*Pipe, error) { if !ok { return nil, fmt.Errorf("%w: pipe %s not found", ErrNotFound, name) } +<<<<<<< Updated upstream if p.DesiredState == stateRunning { return nil, fmt.Errorf("%w: pipe %s already has desired state RUNNING", ErrValidation, name) } p.DesiredState = stateRunning // Transition through STARTING → RUNNING to simulate AWS behavior. p.CurrentState = stateStarting +======= + + if p.DesiredState == stateRunning && p.CurrentState == stateRunning { + return nil, fmt.Errorf("%w: pipe %s is already in RUNNING state", ErrValidation, name) + } + + p.DesiredState = stateRunning + p.CurrentState = stateRunning +>>>>>>> Stashed changes p.StateReason = "" p.LastModifiedTime = time.Now() cp := clonePipe(p) @@ -1320,6 +1716,10 @@ func (b *InMemoryBackend) completeStartTransition(name string) { } } +<<<<<<< Updated upstream +======= +// StopPipe transitions a pipe to the STOPPED desired/current state. +>>>>>>> Stashed changes func (b *InMemoryBackend) StopPipe(name string) (*Pipe, error) { b.mu.Lock("StopPipe") defer b.mu.Unlock() @@ -1327,12 +1727,22 @@ func (b *InMemoryBackend) StopPipe(name string) (*Pipe, error) { if !ok { return nil, fmt.Errorf("%w: pipe %s not found", ErrNotFound, name) } +<<<<<<< Updated upstream if p.DesiredState == stateStopped { return nil, fmt.Errorf("%w: pipe %s already has desired state STOPPED", ErrValidation, name) } p.DesiredState = stateStopped // Transition through STOPPING → STOPPED to simulate AWS behavior. p.CurrentState = stateStopping +======= + + if p.DesiredState == stateStopped && p.CurrentState == stateStopped { + return nil, fmt.Errorf("%w: pipe %s is already in STOPPED state", ErrValidation, name) + } + + p.DesiredState = stateStopped + p.CurrentState = stateStopped +>>>>>>> Stashed changes p.StateReason = "" p.LastModifiedTime = time.Now() cp := clonePipe(p) @@ -1372,10 +1782,34 @@ func (b *InMemoryBackend) MarkPipeFailed(name, state, reason string) { p.LastModifiedTime = time.Now() } +<<<<<<< Updated upstream +======= +// MarkPipeFailed updates a pipe to a failed state with a reason message. +// This is called by the runner when a pipe encounters a persistent error. +func (b *InMemoryBackend) MarkPipeFailed(name, state, reason string) { + b.mu.Lock("MarkPipeFailed") + defer b.mu.Unlock() + + p, ok := b.pipes[name] + if !ok { + return + } + + p.CurrentState = state + p.StateReason = reason + p.LastModifiedTime = time.Now() +} + +// TagResource adds or updates tags on a pipe identified by ARN. +>>>>>>> Stashed changes func (b *InMemoryBackend) TagResource(resourceARN string, kv map[string]string) error { if err := validateTags(kv); err != nil { return err } +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes b.mu.Lock("TagResource") defer b.mu.Unlock() name, ok := b.pipeARNIndex[resourceARN] @@ -1383,10 +1817,18 @@ func (b *InMemoryBackend) TagResource(resourceARN string, kv map[string]string) return fmt.Errorf("%w: resource %s not found", ErrNotFound, resourceARN) } p := b.pipes[name] +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes merged := mergeTags(p.Tags, kv) if len(merged) > maxTagsPerPipe { return fmt.Errorf("%w: pipe would exceed %d tags limit", ErrValidation, maxTagsPerPipe) } +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes p.Tags = merged return nil @@ -1547,3 +1989,238 @@ func (b *InMemoryBackend) Restore(data []byte) error { return nil } + +// --- Validation --- + +func validatePipeName(name string) error { + if name == "" { + return fmt.Errorf("%w: pipe name must not be empty", ErrValidation) + } + + if len(name) > maxPipeNameLen { + return fmt.Errorf("%w: pipe name exceeds maximum length of %d characters", ErrValidation, maxPipeNameLen) + } + + if !pipeNameRE.MatchString(name) { + return fmt.Errorf("%w: pipe name %q contains invalid characters (allowed: a-z, A-Z, 0-9, -, _)", ErrValidation, name) + } + + return nil +} + +func validateDesiredState(state string) error { + if state == "" || state == stateRunning || state == stateStopped { + return nil + } + + return fmt.Errorf("%w: DesiredState must be RUNNING or STOPPED, got %q", ErrValidation, state) +} + +func validateTags(tags map[string]string) error { + for k, v := range tags { + if len(k) == 0 { + return fmt.Errorf("%w: tag key must not be empty", ErrValidation) + } + + if len(k) > maxTagKeyLen { + return fmt.Errorf("%w: tag key %q exceeds maximum length of %d", ErrValidation, k, maxTagKeyLen) + } + + if len(v) > maxTagValueLen { + return fmt.Errorf("%w: tag value for key %q exceeds maximum length of %d", ErrValidation, k, maxTagValueLen) + } + } + + return nil +} + +// cloneSourceParameters returns a deep copy of SourceParameters for snapshot serialisation. +func cloneSourceParameters(sp *SourceParameters) *SourceParameters { + if sp == nil { + return nil + } + + out := &SourceParameters{} + + if sp.FilterCriteria != nil { + fc := &FilterCriteria{ + Filters: append([]Filter(nil), sp.FilterCriteria.Filters...), + } + out.FilterCriteria = fc + } + + if sp.SqsQueueParameters != nil { + sqsp := *sp.SqsQueueParameters + out.SqsQueueParameters = &sqsp + } + + return out +} + +// pipeResponseJSON encodes a pipe snapshot for the Snapshot/Restore persistence path. +// This mirrors the Pipe struct but uses exported names for clarity. +type pipeSnapshot struct { + SourceParameters *SourceParameters `json:"sourceParameters,omitempty"` + TargetParameters *TargetParameters `json:"targetParameters,omitempty"` + DeadLetterConfig *DeadLetterConfig `json:"deadLetterConfig,omitempty"` + LogConfiguration *LogConfiguration `json:"logConfiguration,omitempty"` + LastModifiedTime time.Time `json:"lastModifiedTime"` + CreationTime time.Time `json:"creationTime"` + Tags map[string]string `json:"tags,omitempty"` + Description string `json:"description,omitempty"` + Enrichment string `json:"enrichment,omitempty"` + Source string `json:"source"` + Target string `json:"target"` + RoleARN string `json:"roleArn"` + StateReason string `json:"stateReason,omitempty"` + DesiredState string `json:"desiredState"` + CurrentState string `json:"currentState"` + AccountID string `json:"accountID"` + Region string `json:"region"` + ARN string `json:"arn"` + Name string `json:"name"` +} + +// marshalPipeForSnapshot converts a Pipe to JSON-serialisable form. +func marshalPipeForSnapshot(p *Pipe) pipeSnapshot { + return pipeSnapshot{ + Name: p.Name, + ARN: p.ARN, + RoleARN: p.RoleARN, + Source: p.Source, + Target: p.Target, + Description: p.Description, + Enrichment: p.Enrichment, + DesiredState: p.DesiredState, + CurrentState: p.CurrentState, + StateReason: p.StateReason, + AccountID: p.AccountID, + Region: p.Region, + CreationTime: p.CreationTime, + LastModifiedTime: p.LastModifiedTime, + Tags: p.Tags, + SourceParameters: cloneSourceParameters(p.SourceParameters), + TargetParameters: p.TargetParameters, + DeadLetterConfig: p.DeadLetterConfig, + LogConfiguration: p.LogConfiguration, + } +} + +// restorePipeFromSnapshot converts a pipeSnapshot back to a Pipe. +func restorePipeFromSnapshot(s pipeSnapshot) *Pipe { + return &Pipe{ + Name: s.Name, + ARN: s.ARN, + RoleARN: s.RoleARN, + Source: s.Source, + Target: s.Target, + Description: s.Description, + Enrichment: s.Enrichment, + DesiredState: s.DesiredState, + CurrentState: s.CurrentState, + StateReason: s.StateReason, + AccountID: s.AccountID, + Region: s.Region, + CreationTime: s.CreationTime, + LastModifiedTime: s.LastModifiedTime, + Tags: s.Tags, + SourceParameters: s.SourceParameters, + TargetParameters: s.TargetParameters, + DeadLetterConfig: s.DeadLetterConfig, + LogConfiguration: s.LogConfiguration, + } +} + +// snapshotV2 is the versioned snapshot format used since the addition of extended fields. +type snapshotV2 struct { + Pipes map[string]pipeSnapshot `json:"pipes"` + AccountID string `json:"accountID"` + Region string `json:"region"` + Version int `json:"version"` +} + +// Snapshot serialises the backend state to JSON (used by persistence layer). +func (b *InMemoryBackend) Snapshot() []byte { + b.mu.RLock("Snapshot") + defer b.mu.RUnlock() + + snap := snapshotV2{ + Version: 2, + AccountID: b.accountID, + Region: b.region, + Pipes: make(map[string]pipeSnapshot, len(b.pipes)), + } + + for name, p := range b.pipes { + snap.Pipes[name] = marshalPipeForSnapshot(p) + } + + data, err := json.Marshal(snap) + if err != nil { + return nil + } + + return data +} + +// Restore loads backend state from a JSON snapshot. +func (b *InMemoryBackend) Restore(data []byte) error { + var snap snapshotV2 + + if err := json.Unmarshal(data, &snap); err != nil { + // Attempt legacy v1 restore. + return b.restoreLegacy(data) + } + + if snap.Version == 0 { + return b.restoreLegacy(data) + } + + b.mu.Lock("Restore") + defer b.mu.Unlock() + + b.pipes = make(map[string]*Pipe, len(snap.Pipes)) + b.pipeARNIndex = make(map[string]string, len(snap.Pipes)) + b.accountID = snap.AccountID + b.region = snap.Region + + for name, ps := range snap.Pipes { + p := restorePipeFromSnapshot(ps) + b.pipes[name] = p + b.pipeARNIndex[p.ARN] = name + } + + return nil +} + +// restoreLegacy handles v1 snapshots that used the old Pipe struct directly. +func (b *InMemoryBackend) restoreLegacy(data []byte) error { + type legacySnap struct { + Pipes map[string]*Pipe `json:"pipes"` + AccountID string `json:"accountID"` + Region string `json:"region"` + } + + var snap legacySnap + if err := json.Unmarshal(data, &snap); err != nil { + return err + } + + b.mu.Lock("RestoreLegacy") + defer b.mu.Unlock() + + if snap.Pipes == nil { + snap.Pipes = make(map[string]*Pipe) + } + + b.pipes = snap.Pipes + b.accountID = snap.AccountID + b.region = snap.Region + b.pipeARNIndex = make(map[string]string, len(b.pipes)) + + for name, p := range b.pipes { + b.pipeARNIndex[p.ARN] = name + } + + return nil +} diff --git a/services/pipes/export_test.go b/services/pipes/export_test.go index 2e5feec23..9c8537cfb 100644 --- a/services/pipes/export_test.go +++ b/services/pipes/export_test.go @@ -6,13 +6,19 @@ import ( "time" ) +<<<<<<< Updated upstream // PollAllPipesOnce triggers a single synchronous poll cycle for tests. +======= +// PollAllPipesOnce triggers a single synchronous poll cycle on the given Runner. +// It polls each RUNNING pipe inline (no goroutines) for deterministic test behaviour. +>>>>>>> Stashed changes func PollAllPipesOnce(ctx context.Context, r *Runner) { res := r.backend.ListPipes(ListPipesFilter{CurrentState: stateRunning}) for _, p := range res.Pipes { r.pollPipe(ctx, p) } +<<<<<<< Updated upstream } // CreatePipeSimple is a test helper that creates a pipe using positional args. @@ -56,4 +62,29 @@ func WaitPipeRunning(t *testing.T, b *InMemoryBackend, name string) { } t.Fatalf("pipe %q did not reach RUNNING state within 500ms", name) +======= +>>>>>>> Stashed changes } + +// CreatePipeSimple is a test helper that creates a pipe using positional args +// (name, roleARN, source, target, description, desiredState, tags). +func (b *InMemoryBackend) CreatePipeSimple( + name, roleARN, source, target, description, desiredState string, + tags map[string]string, +) (*Pipe, error) { + return b.CreatePipe(CreatePipeInput{ + Name: name, + RoleARN: roleARN, + Source: source, + Target: target, + Description: description, + DesiredState: desiredState, + Tags: tags, + }) +} + +// ListPipesAll returns all pipes without filtering (test convenience). +func (b *InMemoryBackend) ListPipesAll() []*Pipe { + return b.ListPipes(ListPipesFilter{}).Pipes +} + diff --git a/services/pipes/handler.go b/services/pipes/handler.go index df715d8c2..98c768557 100644 --- a/services/pipes/handler.go +++ b/services/pipes/handler.go @@ -91,15 +91,20 @@ func (h *Handler) StartWorker(ctx context.Context) error { return nil } +<<<<<<< Updated upstream // Shutdown implements service.Shutdowner. It stops the background runner and // cancels any in-flight delayed state-transition goroutines in the backend so // they cannot mutate pipe state after the process begins shutting down. +======= +// Shutdown implements service.Shutdowner. +>>>>>>> Stashed changes func (h *Handler) Shutdown(ctx context.Context) { if h.cancel != nil { h.cancel() } h.runner.Wait(ctx) +<<<<<<< Updated upstream if h.Backend != nil { h.Backend.Shutdown(ctx) @@ -110,6 +115,12 @@ var ( _ service.BackgroundWorker = (*Handler)(nil) _ service.Shutdowner = (*Handler)(nil) ) +======= +} + +var _ service.BackgroundWorker = (*Handler)(nil) +var _ service.Shutdowner = (*Handler)(nil) +>>>>>>> Stashed changes // GetSupportedOperations returns the list of supported Pipes operations. func (h *Handler) GetSupportedOperations() []string { @@ -133,6 +144,10 @@ func (h *Handler) ChaosOperations() []string { return h.GetSupportedOperations() func (h *Handler) ChaosRegions() []string { return []string{h.Region} } +<<<<<<< Updated upstream +======= +// RouteMatcher returns a function that matches Pipes API requests. +>>>>>>> Stashed changes func (h *Handler) RouteMatcher() service.Matcher { return func(c *echo.Context) bool { if httputils.ExtractServiceFromRequest(c.Request()) != pipesService { @@ -296,7 +311,10 @@ func (h *Handler) dispatch( return h.handleDescribePipe(ctx, path) case opListPipes: +<<<<<<< Updated upstream +======= +>>>>>>> Stashed changes return h.handleListPipes(ctx, query) case opDeletePipe: @@ -346,7 +364,11 @@ func (h *Handler) handleError(c *echo.Context, err error) error { return c.JSONBlob(http.StatusConflict, payload) case errors.Is(err, ErrValidation): payload, _ := json.Marshal(map[string]string{ +<<<<<<< Updated upstream keyTypeField: "ValidationException", +======= + "__type": "ValidationException", +>>>>>>> Stashed changes keyMessageField: err.Error(), }) @@ -373,6 +395,7 @@ func extractPipeName(path string) string { return "" } +<<<<<<< Updated upstream // epochMillis returns t as a Unix epoch value with millisecond precision, // matching the AWS Pipes API timestamp format (fractional seconds, 3 decimal places). func epochMillis(t time.Time) float64 { @@ -411,9 +434,44 @@ type updatePipeRequest struct { Enrichment string `json:"Enrichment"` KmsKeyIdentifier string `json:"KmsKeyIdentifier"` DesiredState string `json:"DesiredState"` +======= +// epochSeconds converts a [time.Time] to Unix epoch seconds as float64, +// matching the AWS REST-JSON protocol for timestamp fields. +func epochSeconds(t time.Time) float64 { + return float64(t.Unix()) +>>>>>>> Stashed changes +} + +// --- Request/Response types --- + +type createPipeRequest struct { + Tags map[string]string `json:"Tags"` + SourceParameters *SourceParameters `json:"SourceParameters"` + TargetParameters *TargetParameters `json:"TargetParameters"` + DeadLetterConfig *DeadLetterConfig `json:"DeadLetterConfig"` + LogConfiguration *LogConfiguration `json:"LogConfiguration"` + RoleArn string `json:"RoleArn"` + Source string `json:"Source"` + Target string `json:"Target"` + Description string `json:"Description"` + Enrichment string `json:"Enrichment"` + DesiredState string `json:"DesiredState"` +} + +type updatePipeRequest struct { + SourceParameters *SourceParameters `json:"SourceParameters"` + TargetParameters *TargetParameters `json:"TargetParameters"` + DeadLetterConfig *DeadLetterConfig `json:"DeadLetterConfig"` + LogConfiguration *LogConfiguration `json:"LogConfiguration"` + RoleArn string `json:"RoleArn"` + Target string `json:"Target"` + Description string `json:"Description"` + Enrichment string `json:"Enrichment"` + DesiredState string `json:"DesiredState"` } type pipeResponse struct { +<<<<<<< Updated upstream SourceParameters *SourceParameters `json:"SourceParameters,omitempty"` TargetParameters *TargetParameters `json:"TargetParameters,omitempty"` DeadLetterConfig *DeadLetterConfig `json:"DeadLetterConfig,omitempty"` @@ -434,10 +492,30 @@ type pipeResponse struct { StateReason string `json:"StateReason,omitempty"` CreationTime float64 `json:"CreationTime"` LastModifiedTime float64 `json:"LastModifiedTime"` +======= + SourceParameters *SourceParameters `json:"SourceParameters,omitempty"` + TargetParameters *TargetParameters `json:"TargetParameters,omitempty"` + DeadLetterConfig *DeadLetterConfig `json:"DeadLetterConfig,omitempty"` + LogConfiguration *LogConfiguration `json:"LogConfiguration,omitempty"` + Tags map[string]string `json:"Tags,omitempty"` + Arn string `json:"Arn"` + Name string `json:"Name"` + RoleArn string `json:"RoleArn"` + Source string `json:"Source"` + Target string `json:"Target"` + Description string `json:"Description,omitempty"` + Enrichment string `json:"Enrichment,omitempty"` + DesiredState string `json:"DesiredState"` + CurrentState string `json:"CurrentState"` + StateReason string `json:"StateReason,omitempty"` + CreationTime float64 `json:"CreationTime"` + LastModifiedTime float64 `json:"LastModifiedTime"` +>>>>>>> Stashed changes } func toPipeResponse(p *Pipe) pipeResponse { return pipeResponse{ +<<<<<<< Updated upstream Arn: p.ARN, Name: p.Name, RoleArn: p.RoleARN, @@ -458,6 +536,25 @@ func toPipeResponse(p *Pipe) pipeResponse { LogConfiguration: p.LogConfiguration, EnrichmentParameters: p.EnrichmentParameters, RuntimeMetricsStreaming: p.RuntimeMetricsStreaming, +======= + Arn: p.ARN, + Name: p.Name, + RoleArn: p.RoleARN, + Source: p.Source, + Target: p.Target, + Description: p.Description, + Enrichment: p.Enrichment, + DesiredState: p.DesiredState, + CurrentState: p.CurrentState, + StateReason: p.StateReason, + CreationTime: epochSeconds(p.CreationTime), + LastModifiedTime: epochSeconds(p.LastModifiedTime), + Tags: p.Tags, + SourceParameters: p.SourceParameters, + TargetParameters: p.TargetParameters, + DeadLetterConfig: p.DeadLetterConfig, + LogConfiguration: p.LogConfiguration, +>>>>>>> Stashed changes } } @@ -473,6 +570,7 @@ func (h *Handler) handleCreatePipe(_ context.Context, path string, body []byte) } p, err := h.Backend.CreatePipe(CreatePipeInput{ +<<<<<<< Updated upstream Name: name, RoleARN: req.RoleArn, Source: req.Source, @@ -488,6 +586,20 @@ func (h *Handler) handleCreatePipe(_ context.Context, path string, body []byte) LogConfiguration: req.LogConfiguration, EnrichmentParameters: req.EnrichmentParameters, RuntimeMetricsStreaming: req.RuntimeMetricsStreaming, +======= + Name: name, + RoleARN: req.RoleArn, + Source: req.Source, + Target: req.Target, + Description: req.Description, + Enrichment: req.Enrichment, + DesiredState: req.DesiredState, + Tags: req.Tags, + SourceParameters: req.SourceParameters, + TargetParameters: req.TargetParameters, + DeadLetterConfig: req.DeadLetterConfig, + LogConfiguration: req.LogConfiguration, +>>>>>>> Stashed changes }) if err != nil { return nil, err @@ -510,6 +622,7 @@ func (h *Handler) handleDescribePipe(_ context.Context, path string) ([]byte, er return json.Marshal(toPipeResponse(p)) } +// pipeSummary is the condensed view returned by ListPipes. type pipeSummary struct { Arn string `json:"Arn"` Name string `json:"Name"` @@ -560,8 +673,13 @@ func (h *Handler) handleListPipes(_ context.Context, query url.Values) ([]byte, CurrentState: p.CurrentState, DesiredState: p.DesiredState, StateReason: p.StateReason, +<<<<<<< Updated upstream CreationTime: epochMillis(p.CreationTime), LastModifiedTime: epochMillis(p.LastModifiedTime), +======= + CreationTime: epochSeconds(p.CreationTime), + LastModifiedTime: epochSeconds(p.LastModifiedTime), +>>>>>>> Stashed changes }) } @@ -579,7 +697,11 @@ func (h *Handler) handleDeletePipe(_ context.Context, path string) ([]byte, erro return nil, err } +<<<<<<< Updated upstream return json.Marshal(toPipeResponse(p)) +======= + return nil, nil +>>>>>>> Stashed changes } func (h *Handler) handleUpdatePipe(_ context.Context, path string, body []byte) ([]byte, error) { @@ -594,6 +716,7 @@ func (h *Handler) handleUpdatePipe(_ context.Context, path string, body []byte) } p, err := h.Backend.UpdatePipe(name, UpdatePipeInput{ +<<<<<<< Updated upstream RoleARN: req.RoleArn, Target: req.Target, Description: req.Description, @@ -606,6 +729,17 @@ func (h *Handler) handleUpdatePipe(_ context.Context, path string, body []byte) LogConfiguration: req.LogConfiguration, EnrichmentParameters: req.EnrichmentParameters, RuntimeMetricsStreaming: req.RuntimeMetricsStreaming, +======= + RoleARN: req.RoleArn, + Target: req.Target, + Description: req.Description, + Enrichment: req.Enrichment, + DesiredState: req.DesiredState, + SourceParameters: req.SourceParameters, + TargetParameters: req.TargetParameters, + DeadLetterConfig: req.DeadLetterConfig, + LogConfiguration: req.LogConfiguration, +>>>>>>> Stashed changes }) if err != nil { return nil, err diff --git a/services/pipes/handler_test.go b/services/pipes/handler_test.go index 864bf434f..7a3009af4 100644 --- a/services/pipes/handler_test.go +++ b/services/pipes/handler_test.go @@ -545,16 +545,33 @@ func TestBackend_Validation(t *testing.T) { t.Run("name_too_long", func(t *testing.T) { t.Parallel() +<<<<<<< Updated upstream var sb strings.Builder for range 65 { sb.WriteString("a") } long := sb.String() +======= + long := "a" + for range 65 { + long += "a" + } +>>>>>>> Stashed changes _, err := b.CreatePipeSimple(long, "arn:r", "arn:s", "arn:t", "", "RUNNING", nil) require.Error(t, err) require.ErrorIs(t, err, pipes.ErrValidation) }) +<<<<<<< Updated upstream +======= + t.Run("invalid_name_chars", func(t *testing.T) { + t.Parallel() + _, err := b.CreatePipeSimple("my pipe!", "arn:r", "arn:s", "arn:t", "", "RUNNING", nil) + require.Error(t, err) + require.ErrorIs(t, err, pipes.ErrValidation) + }) + +>>>>>>> Stashed changes t.Run("invalid_desired_state", func(t *testing.T) { t.Parallel() _, err := b.CreatePipeSimple("valid-name", "arn:r", "arn:s", "arn:t", "", "INVALID", nil) @@ -568,6 +585,16 @@ func TestBackend_Validation(t *testing.T) { require.Error(t, err) require.ErrorIs(t, err, pipes.ErrValidation) }) +<<<<<<< Updated upstream +======= + + t.Run("missing_target", func(t *testing.T) { + t.Parallel() + _, err := b.CreatePipeSimple("valid-name", "arn:r", "arn:s", "", "", "RUNNING", nil) + require.Error(t, err) + require.ErrorIs(t, err, pipes.ErrValidation) + }) +>>>>>>> Stashed changes } func TestHandler_ValidationHTTP(t *testing.T) { @@ -597,6 +624,21 @@ func TestHandler_ValidationHTTP(t *testing.T) { assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "ValidationException") }) +<<<<<<< Updated upstream +======= + + t.Run("stop_stopped_pipe_returns_400", func(t *testing.T) { + t.Parallel() + h2 := newTestHandler(t) + doPipesRequest(t, h2, http.MethodPost, "/v1/pipes/stopped-pipe", map[string]any{ + "Source": "arn:aws:sqs:us-east-1:000000000000:src", + "Target": "arn:aws:lambda:us-east-1:000000000000:function:fn", + "DesiredState": "STOPPED", + }) + rec := doPipesRequest(t, h2, http.MethodPost, "/v1/pipes/stopped-pipe/stop", nil) + assert.Equal(t, http.StatusBadRequest, rec.Code) + }) +>>>>>>> Stashed changes } func TestHandler_ListPipesFiltering(t *testing.T) { @@ -621,11 +663,15 @@ func TestHandler_ListPipesFiltering(t *testing.T) { t.Parallel() rec := doPipesRequest(t, h, http.MethodGet, "/v1/pipes?NamePrefix=sqs", nil) require.Equal(t, http.StatusOK, rec.Code) +<<<<<<< Updated upstream var out struct { Pipes []struct { Name string `json:"Name"` } `json:"Pipes"` } +======= + var out struct{ Pipes []struct{ Name string } } +>>>>>>> Stashed changes require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out)) assert.Len(t, out.Pipes, 2) }) @@ -634,16 +680,32 @@ func TestHandler_ListPipesFiltering(t *testing.T) { t.Parallel() rec := doPipesRequest(t, h, http.MethodGet, "/v1/pipes?DesiredState=STOPPED", nil) require.Equal(t, http.StatusOK, rec.Code) +<<<<<<< Updated upstream var out struct { Pipes []struct { Name string `json:"Name"` } `json:"Pipes"` } +======= + var out struct{ Pipes []struct{ Name string } } +>>>>>>> Stashed changes require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out)) assert.Len(t, out.Pipes, 1) assert.Equal(t, "sqs-beta", out.Pipes[0].Name) }) +<<<<<<< Updated upstream +======= + t.Run("filter_by_source_prefix", func(t *testing.T) { + t.Parallel() + rec := doPipesRequest(t, h, http.MethodGet, "/v1/pipes?SourcePrefix=arn:aws:sqs", nil) + require.Equal(t, http.StatusOK, rec.Code) + var out struct{ Pipes []struct{ Name string } } + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out)) + assert.Len(t, out.Pipes, 3) + }) + +>>>>>>> Stashed changes t.Run("invalid_limit_returns_400", func(t *testing.T) { t.Parallel() h2 := newTestHandler(t) @@ -662,27 +724,84 @@ func TestHandler_SourceAndTargetParameters(t *testing.T) { "Target": "arn:aws:lambda:us-east-1:000000000000:function:fn", "RoleArn": "arn:aws:iam::000000000000:role/r", "SourceParameters": map[string]any{ +<<<<<<< Updated upstream "SqsQueueParameters": map[string]any{"BatchSize": 5}, }, "TargetParameters": map[string]any{ "InputTemplate": `{"fixed":"value"}`, +======= + "SqsQueueParameters": map[string]any{ + "BatchSize": 5, + "MaximumBatchingWindowInSeconds": 30, + }, + "FilterCriteria": map[string]any{ + "Filters": []map[string]any{ + {"Pattern": `{"type":["order"]}`}, + }, + }, + }, + "TargetParameters": map[string]any{ + "InputTemplate": `{"id": "<$.messageId>"}`, + "LambdaFunctionParameters": map[string]any{ + "InvocationType": "RequestResponse", + }, +>>>>>>> Stashed changes }, }) require.Equal(t, http.StatusOK, rec.Code) var created struct { +<<<<<<< Updated upstream TargetParameters struct { InputTemplate string `json:"InputTemplate"` } `json:"TargetParameters"` +======= +>>>>>>> Stashed changes SourceParameters struct { SqsQueueParameters struct { BatchSize int `json:"BatchSize"` } `json:"SqsQueueParameters"` } `json:"SourceParameters"` +<<<<<<< Updated upstream } require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &created)) assert.Equal(t, 5, created.SourceParameters.SqsQueueParameters.BatchSize) assert.JSONEq(t, `{"fixed":"value"}`, created.TargetParameters.InputTemplate) +======= + TargetParameters struct { + InputTemplate string `json:"InputTemplate"` + } `json:"TargetParameters"` + } + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &created)) + assert.Equal(t, 5, created.SourceParameters.SqsQueueParameters.BatchSize) + assert.Equal(t, `{"id": "<$.messageId>"}`, created.TargetParameters.InputTemplate) + + descRec := doPipesRequest(t, h, http.MethodGet, "/v1/pipes/param-pipe", nil) + require.Equal(t, http.StatusOK, descRec.Code) + require.NoError(t, json.Unmarshal(descRec.Body.Bytes(), &created)) + assert.Equal(t, 5, created.SourceParameters.SqsQueueParameters.BatchSize) +} + +func TestHandler_UpdatePipeDesiredState(t *testing.T) { + t.Parallel() + + h := newTestHandler(t) + + doPipesRequest(t, h, http.MethodPost, "/v1/pipes/update-state-pipe", map[string]any{ + "Source": "arn:aws:sqs:us-east-1:000000000000:src", + "Target": "arn:aws:lambda:us-east-1:000000000000:function:fn", + "DesiredState": "RUNNING", + }) + + rec := doPipesRequest(t, h, http.MethodPut, "/v1/pipes/update-state-pipe", map[string]any{ + "DesiredState": "STOPPED", + }) + require.Equal(t, http.StatusOK, rec.Code) + + var out struct{ DesiredState string } + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out)) + assert.Equal(t, "STOPPED", out.DesiredState) +>>>>>>> Stashed changes } func TestHandler_ListPipesIncludesSourceTarget(t *testing.T) { @@ -705,7 +824,11 @@ func TestHandler_ListPipesIncludesSourceTarget(t *testing.T) { Source string `json:"Source"` Target string `json:"Target"` Description string `json:"Description"` +<<<<<<< Updated upstream } `json:"Pipes"` +======= + } +>>>>>>> Stashed changes } require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out)) require.Len(t, out.Pipes, 1) @@ -713,6 +836,7 @@ func TestHandler_ListPipesIncludesSourceTarget(t *testing.T) { assert.Equal(t, "arn:aws:lambda:us-east-1:000000000000:function:my-fn", out.Pipes[0].Target) assert.Equal(t, "test pipe", out.Pipes[0].Description) } +<<<<<<< Updated upstream func TestHandler_UpdatePipeDesiredState(t *testing.T) { t.Parallel() @@ -736,3 +860,5 @@ func TestHandler_UpdatePipeDesiredState(t *testing.T) { require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out)) assert.Equal(t, "STOPPED", out.DesiredState) } +======= +>>>>>>> Stashed changes diff --git a/services/pipes/persistence.go b/services/pipes/persistence.go index d6a4c9c1e..0df77dc64 100644 --- a/services/pipes/persistence.go +++ b/services/pipes/persistence.go @@ -1,6 +1,7 @@ package pipes // Snapshot implements persistence.Persistable by delegating to the backend. +// The versioned snapshot format is defined in backend.go (snapshotV2). func (h *Handler) Snapshot() []byte { return h.Backend.Snapshot() } diff --git a/services/pipes/persistence_test.go b/services/pipes/persistence_test.go index 297aa68ce..44267568e 100644 --- a/services/pipes/persistence_test.go +++ b/services/pipes/persistence_test.go @@ -22,6 +22,10 @@ func TestPipes_PersistenceSnapshotRestore(t *testing.T) { setup: func(_ *pipes.InMemoryBackend) {}, verify: func(t *testing.T, b *pipes.InMemoryBackend) { t.Helper() +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes assert.Empty(t, b.ListPipesAll()) }, }, @@ -35,6 +39,10 @@ func TestPipes_PersistenceSnapshotRestore(t *testing.T) { }, verify: func(t *testing.T, b *pipes.InMemoryBackend) { t.Helper() +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes ps := b.ListPipesAll() require.Len(t, ps, 1) assert.Equal(t, "my-pipe", ps[0].Name) @@ -52,16 +60,31 @@ func TestPipes_PersistenceSnapshotRestore(t *testing.T) { Target: "arn:aws:lambda:us-east-1:123:function:fn", SourceParameters: &pipes.SourceParameters{ SqsQueueParameters: &pipes.SQSSourceParameters{BatchSize: 5}, +<<<<<<< Updated upstream +======= + FilterCriteria: &pipes.FilterCriteria{ + Filters: []pipes.Filter{{Pattern: `{"type":["order"]}`}}, + }, +>>>>>>> Stashed changes }, }) }, verify: func(t *testing.T, b *pipes.InMemoryBackend) { t.Helper() +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes ps := b.ListPipesAll() require.Len(t, ps, 1) require.NotNil(t, ps[0].SourceParameters) require.NotNil(t, ps[0].SourceParameters.SqsQueueParameters) assert.Equal(t, 5, ps[0].SourceParameters.SqsQueueParameters.BatchSize) +<<<<<<< Updated upstream +======= + require.NotNil(t, ps[0].SourceParameters.FilterCriteria) + require.Len(t, ps[0].SourceParameters.FilterCriteria.Filters, 1) +>>>>>>> Stashed changes }, }, } diff --git a/services/pipes/runner.go b/services/pipes/runner.go index 964d34540..c95abf26c 100644 --- a/services/pipes/runner.go +++ b/services/pipes/runner.go @@ -31,12 +31,19 @@ var ( const ( pipeRunnerTickInterval = 1 * time.Second +<<<<<<< Updated upstream pipeDefaultBatchSize = 10 maxPipeWorkers = 64 // Pipes invocation type constants. invocationTypeFireAndForget = "FIRE_AND_FORGET" invocationTypeRequestResponse = "REQUEST_RESPONSE" +======= + // pipeDefaultBatchSize is the default number of messages/records read per poll cycle. + pipeDefaultBatchSize = 10 + // maxPipeWorkers caps the number of concurrent per-pipe poll goroutines. + maxPipeWorkers = 64 +>>>>>>> Stashed changes ) // lambdaPipesInvocationType maps Pipes invocation types to Lambda invocation types. @@ -127,6 +134,7 @@ type Runner struct { sqsReader SQSReader lambda PipeLambdaInvoker sfn PipeStepFunctionsStarter +<<<<<<< Updated upstream sns SNSPublisher sqsSender SQSSender kinesis PipeKinesisPutter @@ -139,13 +147,20 @@ type Runner struct { doneMu sync.RWMutex wg sync.WaitGroup started bool +======= + wg sync.WaitGroup + sem chan struct{} // bounded concurrency semaphore +>>>>>>> Stashed changes } func NewRunner(backend *InMemoryBackend) *Runner { return &Runner{ backend: backend, sem: make(chan struct{}, maxPipeWorkers), +<<<<<<< Updated upstream done: make(chan struct{}), +======= +>>>>>>> Stashed changes } } @@ -160,6 +175,7 @@ func (r *Runner) SetCloudWatchLogsPutter(c PipeCloudWatchLogsPutter) { r.cwLogs func (r *Runner) SetFirehosePutter(f PipeFirehosePutter) { r.firehose = f } func (r *Runner) Start(ctx context.Context) { +<<<<<<< Updated upstream r.doneMu.Lock() if r.started { r.doneMu.Unlock() @@ -176,11 +192,19 @@ func (r *Runner) Start(ctx context.Context) { go func() { r.wg.Wait() close(done) +======= + r.wg.Add(1) + + go func() { + defer r.wg.Done() + r.run(ctx) +>>>>>>> Stashed changes }() } // Wait blocks until all runner goroutines have exited, or ctx expires. func (r *Runner) Wait(ctx context.Context) { +<<<<<<< Updated upstream r.doneMu.RLock() if !r.started { r.doneMu.RUnlock() @@ -189,6 +213,14 @@ func (r *Runner) Wait(ctx context.Context) { } done := r.done r.doneMu.RUnlock() +======= + done := make(chan struct{}) + + go func() { + r.wg.Wait() + close(done) + }() +>>>>>>> Stashed changes select { case <-done: @@ -210,21 +242,39 @@ func (r *Runner) run(ctx context.Context) { } } +// pollAllPipes dispatches a goroutine per RUNNING pipe, bounded by the semaphore. func (r *Runner) pollAllPipes(ctx context.Context) { res := r.backend.ListPipes(ListPipesFilter{CurrentState: stateRunning}) for _, p := range res.Pipes { +<<<<<<< Updated upstream +======= + p := p // capture for goroutine + + // Non-blocking semaphore acquire: skip pipe this tick if at capacity. +>>>>>>> Stashed changes select { case r.sem <- struct{}{}: default: continue } +<<<<<<< Updated upstream r.wg.Go(func() { defer func() { <-r.sem }() r.pollPipe(ctx, p) }) +======= + r.wg.Add(1) + + go func() { + defer r.wg.Done() + defer func() { <-r.sem }() + + r.pollPipe(ctx, p) + }() +>>>>>>> Stashed changes } } @@ -257,11 +307,16 @@ func (r *Runner) pollSQSPipe(ctx context.Context, p *Pipe) { return } +<<<<<<< Updated upstream +======= + // Apply filter criteria before forwarding. +>>>>>>> Stashed changes msgs = r.applyFilters(p, msgs) if len(msgs) == 0 { return } +<<<<<<< Updated upstream payload, err := json.Marshal(sqsPipeEvent{Records: buildSQSRecords(p, msgs)}) if err != nil { logger.Load(ctx).WarnContext(ctx, "pipes: failed to marshal SQS records", "pipe", p.Name, "error", err) @@ -287,6 +342,9 @@ func (r *Runner) pollSQSPipe(ctx context.Context, p *Pipe) { } receiptHandles, invokeErr := r.invokeTargetWithPayload(ctx, p, msgs, payload) +======= + receiptHandles, invokeErr := r.invokeTarget(ctx, p, msgs) +>>>>>>> Stashed changes if invokeErr != nil { logger.Load(ctx).WarnContext(ctx, "pipes: target invocation failed", "pipe", p.Name, "target", p.Target, "error", invokeErr) @@ -301,6 +359,7 @@ func (r *Runner) pollSQSPipe(ctx context.Context, p *Pipe) { } } +<<<<<<< Updated upstream // handlePipeFailure routes a failed batch to the configured dead-letter queue. When // a DLQ is configured and the send succeeds, the source messages are deleted so they // are not reprocessed in a tight loop. When no DLQ is configured, the messages are @@ -397,6 +456,10 @@ func (r *Runner) invokeEnrichment(ctx context.Context, p *Pipe, payload []byte) return nil, fmt.Errorf("%w %q for pipe %q", ErrUnsupportedPipeEnrichment, enrichARN, p.Name) } +======= +// applyFilters applies FilterCriteria patterns, returning only matching messages. +// If no filters are configured, all messages pass through. +>>>>>>> Stashed changes func (r *Runner) applyFilters(p *Pipe, msgs []*SQSMessage) []*SQSMessage { if p.SourceParameters == nil || p.SourceParameters.FilterCriteria == nil || @@ -415,11 +478,29 @@ func (r *Runner) applyFilters(p *Pipe, msgs []*SQSMessage) []*SQSMessage { return out } +<<<<<<< Updated upstream // invokeTargetWithPayload dispatches the pre-marshalled payload to the pipe's target. // It returns receipt handles on success so the caller can delete the source messages. func (r *Runner) invokeTargetWithPayload( ctx context.Context, p *Pipe, msgs []*SQSMessage, payload []byte, ) ([]string, error) { +======= +// matchesAnyFilter reports whether a message matches at least one filter pattern. +// Patterns are simple JSON substring matches for local simulation. +func matchesAnyFilter(m *SQSMessage, filters []Filter) bool { + for _, f := range filters { + if f.Pattern == "" || strings.Contains(m.Body, f.Pattern) { + return true + } + } + + return false +} + +// invokeTarget forwards the SQS messages to the pipe's target and returns the receipt handles. +// Returns an error (and does NOT delete messages) when the target ARN is unsupported. +func (r *Runner) invokeTarget(ctx context.Context, p *Pipe, msgs []*SQSMessage) ([]string, error) { +>>>>>>> Stashed changes receiptHandles := make([]string, len(msgs)) for i, m := range msgs { receiptHandles[i] = m.ReceiptHandle @@ -481,11 +562,36 @@ func buildSQSRecords(p *Pipe, msgs []*SQSMessage) []sqsPipeRecord { return records } +<<<<<<< Updated upstream // applyInputTemplate returns the InputTemplate payload if configured, otherwise the // pre-built default payload. This implements the Pipes TargetParameters.InputTemplate contract. func applyInputTemplate(p *Pipe, defaultPayload []byte) []byte { if p.TargetParameters != nil && p.TargetParameters.InputTemplate != "" { return []byte(p.TargetParameters.InputTemplate) +======= +func (r *Runner) invokeLambdaTarget(ctx context.Context, p *Pipe, msgs []*SQSMessage) error { + if r.lambda == nil { + return nil + } + + invocationType := "Event" + if p.TargetParameters != nil && + p.TargetParameters.LambdaFunctionParameters != nil && + p.TargetParameters.LambdaFunctionParameters.InvocationType != "" { + invocationType = p.TargetParameters.LambdaFunctionParameters.InvocationType + } + + var payload []byte + var err error + + if p.TargetParameters != nil && p.TargetParameters.InputTemplate != "" { + payload = []byte(p.TargetParameters.InputTemplate) + } else { + payload, err = json.Marshal(sqsPipeEvent{Records: buildSQSRecords(p, msgs)}) + if err != nil { + return err + } +>>>>>>> Stashed changes } return defaultPayload @@ -517,7 +623,11 @@ func (r *Runner) invokeLambdaTarget(ctx context.Context, p *Pipe, payload []byte fnName = p.Target } +<<<<<<< Updated upstream _, _, err := r.lambda.InvokeFunction(ctx, fnName, invocationType, payload) +======= + _, _, err = r.lambda.InvokeFunction(ctx, fnName, invocationType, payload) +>>>>>>> Stashed changes if err == nil { logger.Load(ctx).DebugContext(ctx, "pipes: invoked Lambda", "pipe", p.Name, "function", fnName) @@ -531,6 +641,7 @@ func (r *Runner) invokeSFNTarget(_ context.Context, p *Pipe, payload []byte) err return fmt.Errorf("%w: step functions target %q", ErrTargetInvokerUnwired, p.Target) } +<<<<<<< Updated upstream payload = applyInputTemplate(p, payload) invocationType := invocationTypeFireAndForget @@ -546,6 +657,22 @@ func (r *Runner) invokeSFNTarget(_ context.Context, p *Pipe, payload []byte) err _ = invocationType // passed to StartExecution when API supports it return r.sfn.StartExecution(p.Target, "", string(payload)) +======= + var inputStr string + + if p.TargetParameters != nil && p.TargetParameters.InputTemplate != "" { + inputStr = p.TargetParameters.InputTemplate + } else { + payload, err := json.Marshal(sqsPipeEvent{Records: buildSQSRecords(p, msgs)}) + if err != nil { + return err + } + + inputStr = string(payload) + } + + return r.sfn.StartExecution(p.Target, "", inputStr) +>>>>>>> Stashed changes } func (r *Runner) invokeSNSTarget(ctx context.Context, p *Pipe, payload []byte) error { diff --git a/services/pipes/runner_test.go b/services/pipes/runner_test.go index 08663136c..7849f2c45 100644 --- a/services/pipes/runner_test.go +++ b/services/pipes/runner_test.go @@ -20,6 +20,10 @@ type mockSQSReader struct { deleteErr error messages []*pipes.SQSMessage deletedIDs []string +<<<<<<< Updated upstream +======= + deleted []string // alias for deletedIDs used in filter tests +>>>>>>> Stashed changes receiveCalls int lastMaxMessages int mu sync.Mutex @@ -43,6 +47,7 @@ func (m *mockSQSReader) ReceivePipeMessages(_ string, maxMessages int) ([]*pipes func (m *mockSQSReader) DeletePipeMessages(_ string, receiptHandles []string) error { m.mu.Lock() m.deletedIDs = append(m.deletedIDs, receiptHandles...) + m.deleted = append(m.deleted, receiptHandles...) m.mu.Unlock() return m.deleteErr @@ -288,7 +293,10 @@ func TestPipesRunner_FilterCriteria(t *testing.T) { }, }) require.NoError(t, err) +<<<<<<< Updated upstream pipes.WaitPipeRunning(t, backend, "filter-pipe") +======= +>>>>>>> Stashed changes sqsReader := &mockSQSReader{ messages: []*pipes.SQSMessage{ @@ -311,12 +319,26 @@ func TestPipesRunner_FilterCriteria(t *testing.T) { require.Len(t, calls, 1) +<<<<<<< Updated upstream var event map[string]any require.NoError(t, json.Unmarshal(payloads[0], &event)) require.Len(t, event, 1) // payload is forwarded - just check Lambda was called once deleted := sqsReader.getDeleted() +======= + var event struct { + Records []struct{ MessageID string `json:"messageId"` } `json:"Records"` + } + require.NoError(t, json.Unmarshal(payloads[0], &event)) + require.Len(t, event.Records, 1) + assert.Equal(t, "m1", event.Records[0].MessageID) + + // Only the matched message should be deleted. + sqsReader.mu.Lock() + deleted := sqsReader.deleted + sqsReader.mu.Unlock() +>>>>>>> Stashed changes assert.Equal(t, []string{"rh1"}, deleted) } @@ -325,18 +347,32 @@ func TestPipesRunner_ConfigurableBatchSize(t *testing.T) { t.Parallel() backend := newTestPipeBackend(t) +<<<<<<< Updated upstream _, err := backend.CreatePipe(pipes.CreatePipeInput{ Name: "batch-pipe", RoleARN: "arn:aws:iam::000000000000:role/r", Source: "arn:aws:sqs:us-east-1:000000000000:batch-queue", Target: "arn:aws:lambda:us-east-1:000000000000:function:fn", +======= + sqsARN := "arn:aws:sqs:us-east-1:000000000000:batch-queue" + lambdaARN := "arn:aws:lambda:us-east-1:000000000000:function:fn" + + _, err := backend.CreatePipe(pipes.CreatePipeInput{ + Name: "batch-pipe", + RoleARN: "arn:aws:iam::000000000000:role/r", + Source: sqsARN, + Target: lambdaARN, +>>>>>>> Stashed changes DesiredState: "RUNNING", SourceParameters: &pipes.SourceParameters{ SqsQueueParameters: &pipes.SQSSourceParameters{BatchSize: 3}, }, }) require.NoError(t, err) +<<<<<<< Updated upstream pipes.WaitPipeRunning(t, backend, "batch-pipe") +======= +>>>>>>> Stashed changes sqsReader := &mockSQSReader{} runner := pipes.NewRunner(backend) @@ -344,26 +380,51 @@ func TestPipesRunner_ConfigurableBatchSize(t *testing.T) { pipes.PollAllPipesOnce(t.Context(), runner) +<<<<<<< Updated upstream assert.Equal(t, 3, sqsReader.getLastMaxMessages(), "runner should request batch size from source parameters") } // TestPipesRunner_InputTemplate tests that TargetParameters.InputTemplate overrides default payload. +======= + sqsReader.mu.Lock() + maxRequested := sqsReader.lastMaxMessages + sqsReader.mu.Unlock() + + assert.Equal(t, 3, maxRequested, "runner should request batch size from source parameters") +} + +// TestPipesRunner_InputTemplate tests that TargetParameters.InputTemplate overrides the default payload. +>>>>>>> Stashed changes func TestPipesRunner_InputTemplate(t *testing.T) { t.Parallel() backend := newTestPipeBackend(t) +<<<<<<< Updated upstream _, err := backend.CreatePipe(pipes.CreatePipeInput{ Name: "template-pipe", RoleARN: "arn:aws:iam::000000000000:role/r", Source: "arn:aws:sqs:us-east-1:000000000000:tmpl-queue", Target: "arn:aws:lambda:us-east-1:000000000000:function:fn", +======= + sqsARN := "arn:aws:sqs:us-east-1:000000000000:tmpl-queue" + lambdaARN := "arn:aws:lambda:us-east-1:000000000000:function:fn" + + _, err := backend.CreatePipe(pipes.CreatePipeInput{ + Name: "template-pipe", + RoleARN: "arn:aws:iam::000000000000:role/r", + Source: sqsARN, + Target: lambdaARN, +>>>>>>> Stashed changes DesiredState: "RUNNING", TargetParameters: &pipes.TargetParameters{ InputTemplate: `{"fixed":"value"}`, }, }) require.NoError(t, err) +<<<<<<< Updated upstream pipes.WaitPipeRunning(t, backend, "template-pipe") +======= +>>>>>>> Stashed changes sqsReader := &mockSQSReader{ messages: []*pipes.SQSMessage{{MessageID: "m1", ReceiptHandle: "rh1", Body: "hello"}}, @@ -381,5 +442,9 @@ func TestPipesRunner_InputTemplate(t *testing.T) { lambdaInvoker.mu.Unlock() require.Len(t, payloads, 1) +<<<<<<< Updated upstream assert.JSONEq(t, `{"fixed":"value"}`, string(payloads[0])) +======= + assert.Equal(t, `{"fixed":"value"}`, string(payloads[0])) +>>>>>>> Stashed changes } diff --git a/ui/src/routes/pipes/+page.svelte b/ui/src/routes/pipes/+page.svelte index 8310d80f7..5faf7ddbe 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 @@
{#each [['overview', 'Overview'], ['tags', 'Tags'], ['config', 'Config']] as [tab, label]}