diff --git a/docs/api2.yaml b/docs/api2.yaml index d1aa6c1..698f2e7 100644 --- a/docs/api2.yaml +++ b/docs/api2.yaml @@ -79,6 +79,70 @@ paths: type: string example: password: crowdaction does not exist + "/crowdactions/{crowdactionID}/participants": + get: + tags: + - Crowdaction + summary: Get details of a specific crowdaction + parameters: + - $ref: "#/components/parameters/ApiVersionParameter" + - name: crowdactionID + in: path + required: true + schema: + type: string + - in: query + name: password + description: Only include if crowdaction requires password + required: false + schema: + type: string + format: password + responses: + "200": + description: Crowdaction details + content: + application/json: + schema: + type: object + properties: + status: + type: string + default: success + data: + type: array + items: + $ref: "#/components/schemas/Participation" + "401": + description: Unauthorized (Invalid password) + content: + application/json: + schema: + type: object + properties: + status: + type: string + default: fail + data: + type: string + example: + password: Invalid or missing password + "403": + $ref: "#/components/responses/UnsupportedClientVersion" + "404": + description: Crowdaction not found + content: + application/json: + schema: + type: object + properties: + status: + type: string + default: fail + data: + type: string + example: + password: crowdaction does not exist /crowdactions: get: tags: @@ -169,8 +233,17 @@ paths: summary: Stop participating in a particular crowdaction responses: "200": - $ref: "#/components/responses/EmptySuccess" - + description: Success message + content: + application/json: + schema: + type: object + properties: + status: + type: string + default: success + data: + default: null "403": $ref: "#/components/responses/UnsupportedClientVersion" "404": @@ -217,8 +290,17 @@ paths: - no-dairy responses: "201": - $ref: "#/components/responses/EmptySuccess" - + description: Success message + content: + application/json: + schema: + type: object + properties: + status: + type: string + default: success + data: + default: null "400": description: Bad request (Invalid commitments) content: @@ -416,6 +498,35 @@ paths: type: object default: userID: No such profile + /profiles/{userID}/participations: + parameters: + - $ref: "#/components/parameters/ApiVersionParameter" + - name: userID + in: path + required: true + schema: + type: string + get: + tags: + - Profile + summary: View the participations of a user + responses: + "200": + description: Profile was found + content: + application/json: + schema: + type: object + properties: + status: + type: string + default: success + data: + type: array + items: + $ref: "#/components/schemas/Participation" + "403": + $ref: "#/components/responses/UnsupportedClientVersion" /upload-profile-picture: parameters: - $ref: "#/components/parameters/ApiVersionParameter" diff --git a/event_examples/crowdaction_participations.json b/event_examples/crowdaction_participations.json new file mode 100644 index 0000000..00fa5da --- /dev/null +++ b/event_examples/crowdaction_participations.json @@ -0,0 +1,11 @@ +{ + "pathParameters": { + "crowdactionID": "sustainability#food#303e67d6" + }, + "requestContext": { + "http": { + "method": "GET", + "path": "/crowdactions/sustainability#food#303e67d6" + } + } + } \ No newline at end of file diff --git a/go.mod b/go.mod index d1913b4..2cac461 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/aws/aws-sdk-go v1.41.10 github.com/gin-gonic/gin v1.7.7 github.com/go-ozzo/ozzo-validation v3.6.0+incompatible + github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/go-playground/locales v0.14.0 github.com/go-playground/universal-translator v0.18.0 github.com/go-playground/validator/v10 v10.10.0 diff --git a/go.sum b/go.sum index 5f6577f..1c0801d 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-lambda-go v1.27.0 h1:aLzrJwdyHoF1A18YeVdJjX8Ixkd+bpogdxVInvHcWjM= @@ -17,6 +18,8 @@ github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= diff --git a/internal/constants/app.go b/internal/constants/app.go index 6fc46b3..0e17078 100644 --- a/internal/constants/app.go +++ b/internal/constants/app.go @@ -17,6 +17,7 @@ const ( var ( TableName = os.Getenv("TABLE_NAME") + IndexName = os.Getenv("INDEX_NAME") ProfileTablename = os.Getenv("PROFILE_TABLE") ParticipationQueueName = os.Getenv("PARTICIPATION_QUEUE") ) diff --git a/internal/models/profile.go b/internal/models/profile.go index cfcd86e..b1da906 100644 --- a/internal/models/profile.go +++ b/internal/models/profile.go @@ -2,7 +2,7 @@ package models import ( "github.com/CollActionteam/collaction_backend/internal/constants" - validation "github.com/go-ozzo/ozzo-validation" + validation "github.com/go-ozzo/ozzo-validation/v4" ) type Profile struct { diff --git a/internal/participation/service.go b/internal/participation/service.go index 2eb87bb..23778a7 100644 --- a/internal/participation/service.go +++ b/internal/participation/service.go @@ -14,12 +14,16 @@ type Service interface { GetParticipation(ctx context.Context, userID string, crowdactionID string) (*m.ParticipationRecord, error) RegisterParticipation(ctx context.Context, userID string, name string, crowdaction *models.Crowdaction, payload m.JoinPayload) error CancelParticipation(ctx context.Context, userID string, crowdaction *models.Crowdaction) error + GetParticipationsUser(ctx context.Context, userID string) (*[]m.ParticipationRecord, error) + GetParticipationsCrowdaction(ctx context.Context, crowdactionID string) (*[]m.ParticipationRecord, error) } type ParticipationManager interface { Get(ctx context.Context, userID string, crowdactionID string) (*m.ParticipationRecord, error) Register(ctx context.Context, userID string, name string, crowdaction *models.Crowdaction, payload m.JoinPayload) error Cancel(ctx context.Context, userID string, crowdaction *models.Crowdaction) error + ListByUser(ctx context.Context, userID string) (*[]m.ParticipationRecord, error) + ListByCrowdaction(ctx context.Context, crowdactionID string) (*[]m.ParticipationRecord, error) } type participationService struct { @@ -75,3 +79,11 @@ func (s *participationService) CancelParticipation(ctx context.Context, userID s } return recordEvent(userID, crowdaction.CrowdactionID, part.Commitments, -1) } + +func (s *participationService) GetParticipationsUser(ctx context.Context, userID string) (*[]m.ParticipationRecord, error) { + return s.participationRepository.ListByUser(ctx, userID) +} + +func (s *participationService) GetParticipationsCrowdaction(ctx context.Context, crowdactionID string) (*[]m.ParticipationRecord, error) { + return s.participationRepository.ListByCrowdaction(ctx, crowdactionID) +} diff --git a/internal/participation/service_test.go b/internal/participation/service_test.go index 96271c4..037478b 100644 --- a/internal/participation/service_test.go +++ b/internal/participation/service_test.go @@ -2,12 +2,13 @@ package participation_test import ( "context" + "testing" + m "github.com/CollActionteam/collaction_backend/internal/models" "github.com/CollActionteam/collaction_backend/internal/participation" "github.com/CollActionteam/collaction_backend/models" "github.com/CollActionteam/collaction_backend/pkg/mocks/repository" "github.com/stretchr/testify/assert" - "testing" ) func TestParticipation_RegisterParticipation(t *testing.T) { @@ -21,3 +22,41 @@ func TestParticipation_RegisterParticipation(t *testing.T) { assert.EqualError(t, err, "cannot change participation for this crowdaction anymore") }) } + +func Test_GetParticipationsUser(t *testing.T) { + ast := assert.New(t) + repository := &repository.Participation{} + cc := []m.ParticipationRecord{} + + t.Run("testing GetParticipationsUser", func(t *testing.T) { + userID := "123" + + repository.On("ListByUser", context.Background(), userID).Return(&cc, nil).Once() + + service := participation.NewParticipationService(repository) + crowdactions, err := service.GetParticipationsUser(context.Background(), userID) + ast.NoError(err) + ast.Equal(&cc, crowdactions) + + repository.AssertExpectations(t) + }) +} + +func Test_GetParticipationsCrowdaction(t *testing.T) { + ast := assert.New(t) + cc := []m.ParticipationRecord{} + repository := &repository.Participation{} + + t.Run("testing GetParticipationsCrowdaction", func(t *testing.T) { + crowdactionID := "1" + + repository.On("ListByCrowdaction", context.Background(), crowdactionID).Return(&cc, nil).Once() + + service := participation.NewParticipationService(repository) + crowdactions, err := service.GetParticipationsCrowdaction(context.Background(), crowdactionID) + ast.NoError(err) + ast.Equal(&cc, crowdactions) + + repository.AssertExpectations(t) + }) +} diff --git a/pkg/handler/aws/participations/main.go b/pkg/handler/aws/participations/main.go new file mode 100644 index 0000000..25f7f6f --- /dev/null +++ b/pkg/handler/aws/participations/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "context" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" +) + +func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { + return NewParticipationsHandler().getParticipations(ctx, req) +} + +func main() { + lambda.Start(handler) +} diff --git a/pkg/handler/aws/participations/participations.go b/pkg/handler/aws/participations/participations.go new file mode 100644 index 0000000..8940046 --- /dev/null +++ b/pkg/handler/aws/participations/participations.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "strings" + + m "github.com/CollActionteam/collaction_backend/internal/models" + "github.com/CollActionteam/collaction_backend/internal/participation" + "github.com/CollActionteam/collaction_backend/pkg/repository/aws" + "github.com/CollActionteam/collaction_backend/utils" + "github.com/aws/aws-lambda-go/events" +) + +type participations *[]m.ParticipationRecord + +type ParticipationsHandler struct { + service participation.Service +} + +func NewParticipationsHandler() *ParticipationsHandler { + participationRepository := aws.NewParticipation(aws.NewDynamo()) + return &ParticipationsHandler{service: participation.NewParticipationService(participationRepository)} +} + +func (h *ParticipationsHandler) getParticipations(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { + var err error = nil + var data *[]m.ParticipationRecord + path := req.RequestContext.HTTP.Path + // TODO pagination + fmt.Printf("Participations function called with path: %s\n", path) // TODO remove + if strings.HasPrefix(path, "/crowdactions") { + crowdactionID := req.PathParameters["crowdactionID"] + // TODO check password: + // 1. Fetch crowdaction + // 2. If the crowdaction is not found, return 404 + // 3. If the crowdaction is password protected, check the request for the password + fmt.Printf("getParticipations: crowdactionID: %s\n", crowdactionID) // TODO remove + data, err = h.service.GetParticipationsCrowdaction(ctx, crowdactionID) + } else if strings.HasPrefix(path, "/profiles") { + userID := req.PathParameters["userID"] + data, err = h.service.GetParticipationsUser(ctx, userID) + } else { + err = fmt.Errorf("invalid path: %s", path) + } + if err != nil { + fmt.Printf("getParticipations: error: %s\n", err) // TODO remove + handlerError(err) + } + return utils.GetDataHttpResponse(http.StatusOK, "", data), nil +} + +func handlerError(err error) events.APIGatewayV2HTTPResponse { + return utils.CreateMessageHttpResponse(http.StatusInternalServerError, err.Error()) +} diff --git a/pkg/mocks/repository/participation.go b/pkg/mocks/repository/participation.go index fcc98cd..b8126ae 100644 --- a/pkg/mocks/repository/participation.go +++ b/pkg/mocks/repository/participation.go @@ -2,6 +2,7 @@ package repository import ( "context" + m "github.com/CollActionteam/collaction_backend/internal/models" "github.com/CollActionteam/collaction_backend/models" "github.com/stretchr/testify/mock" @@ -25,3 +26,13 @@ func (s *Participation) Cancel(ctx context.Context, userID string, crowdaction * args := s.Mock.Called(ctx, userID, crowdaction) return args.Error(0) } + +func (s *Participation) ListByUser(ctx context.Context, userID string) (*[]m.ParticipationRecord, error) { + args := s.Mock.Called(ctx, userID) + return args.Get(0).(*[]m.ParticipationRecord), args.Error(1) +} + +func (s *Participation) ListByCrowdaction(ctx context.Context, crowdactionID string) (*[]m.ParticipationRecord, error) { + args := s.Mock.Called(ctx, crowdactionID) + return args.Get(0).(*[]m.ParticipationRecord), args.Error(1) +} diff --git a/pkg/repository/aws/participationManager.go b/pkg/repository/aws/participationManager.go index 761ab0f..a7374ab 100644 --- a/pkg/repository/aws/participationManager.go +++ b/pkg/repository/aws/participationManager.go @@ -9,20 +9,16 @@ import ( m "github.com/CollActionteam/collaction_backend/internal/models" "github.com/CollActionteam/collaction_backend/models" "github.com/CollActionteam/collaction_backend/utils" + "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/expression" ) -type Participation interface { - Get(ctx context.Context, userID string, crowdactionID string) (*m.ParticipationRecord, error) - Register(ctx context.Context, userID string, name string, crowdaction *models.Crowdaction, payload m.JoinPayload) error - Cancel(ctx context.Context, userID string, crowdaction *models.Crowdaction) error -} - type participation struct { dbClient *Dynamo } -func NewParticipation(dynamo *Dynamo) Participation { +func NewParticipation(dynamo *Dynamo) *participation { return &participation{ dbClient: dynamo, } @@ -67,3 +63,46 @@ func (s *participation) Cancel(ctx context.Context, userID string, crowdaction * sk := utils.PrefixParticipationSK_CrowdactionID + crowdaction.CrowdactionID return s.dbClient.DeleteDBItem(constants.TableName, pk, sk) } + +func (s *participation) listByPK(ctx context.Context, pk string, useGSI bool) (*[]m.ParticipationRecord, error) { + var indexName *string = nil + if useGSI { + indexName = &constants.IndexName + } + participationRecords := []m.ParticipationRecord{} + // TODO refactor (do not directly interact with dynamo) + dbClient := utils.CreateDBClient() + keyCond := expression.Key(utils.PartitionKey).Equal(expression.Value(pk)) + expr, err := expression.NewBuilder(). + WithKeyCondition(keyCond). + Build() + if err != nil { + return nil, err + } + out, err := dbClient.Query(&dynamodb.QueryInput{ + TableName: &constants.TableName, + IndexName: indexName, + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + FilterExpression: expr.Filter(), + }) + for _, item := range out.Items { + var participationRecord m.ParticipationRecord + itemErr := dynamodbattribute.UnmarshalMap(item, &participationRecord) + if itemErr == nil { + participationRecords = append(participationRecords, participationRecord) + } + } + return &participationRecords, err +} + +func (s *participation) ListByUser(ctx context.Context, userID string) (*[]m.ParticipationRecord, error) { + pk := utils.PrefixParticipationPK_UserID + userID + return s.listByPK(ctx, pk, false) +} + +func (s *participation) ListByCrowdaction(ctx context.Context, crowdactionID string) (*[]m.ParticipationRecord, error) { + pk := utils.PrefixParticipationSK_CrowdactionID + crowdactionID + return s.listByPK(ctx, pk, true) +} diff --git a/template.yaml b/template.yaml index 84b4bc5..9c7539d 100644 --- a/template.yaml +++ b/template.yaml @@ -284,7 +284,7 @@ Resources: FetchCrowdaction: Type: HttpApi Properties: - Path: /crowdactions/{crowdactionID} + Path: /crowdactions/{crowdactionID}/ Method: get ApiId: !Ref HttpApi Auth: @@ -349,12 +349,45 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref SingleTable - Statement: - - Sid: ParticipationQueuePutRecordPolicy - Effect: Allow - Action: - - sqs:SendMessage - Resource: !GetAtt ParticipationQueue.Arn + - Sid: ParticipationQueuePutRecordPolicy + Effect: Allow + Action: + - sqs:SendMessage + Resource: !GetAtt ParticipationQueue.Arn + ParticipationsFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: pkg/handler/aws/participations + Handler: participations + Runtime: go1.x + Environment: + Variables: + # Maybe use a different table to remove the overhead form the GSI on the other data + TABLE_NAME: !Ref SingleTable + INDEX_NAME: "invertedIndex" + Events: + Participants: + Type: HttpApi + Properties: + Path: /crowdactions/{crowdactionID}/participations + Method: any + ApiId: !Ref HttpApi + Auth: + Authorizer: "NONE" + Participations: + Type: HttpApi + Properties: + Path: /profiles/{userID}/participations + Method: any + ApiId: !Ref HttpApi + Auth: + Authorizer: "NONE" + Policies: + - DynamoDBCrudPolicy: + TableName: + !Ref SingleTable + ProfileCRUDFunction: Type: AWS::Serverless::Function Properties: diff --git a/utils/api.go b/utils/api.go index d9bb6fe..abd2697 100644 --- a/utils/api.go +++ b/utils/api.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-lambda-go/events" ) +// Deprecated: Use GetDataHttpResponse instead (uses APIGatewayV2HTTPResponse) func GetMessageHttpResponse(statusCode int, msg string) events.APIGatewayProxyResponse { // "Cannot go wrong" jsonPayload, _ := json.Marshal(map[string]interface{}{"message": msg})