From 62c19594ab37e06bb20c4938ed072f25a7b505a6 Mon Sep 17 00:00:00 2001 From: Abtin Badiee <117802598+abtin81badie@users.noreply.github.com> Date: Wed, 4 Dec 2024 20:19:39 +0330 Subject: [PATCH] Add CRUD survey apis. --- api/http/dto/create_survey.go | 24 +++ api/http/dto/get_survey.go | 19 +++ api/http/dto/update_survey.go | 22 +++ api/http/handlers/surevey.go | 168 +++++++++++++++++++ api/http/mapper/survey_mapper.go | 93 ++++++++++ go.mod | 8 +- go.sum | 22 ++- internal/survey/domain/survey.go | 38 +++++ internal/survey/port/repository.go | 14 ++ internal/survey/port/service.go | 14 ++ internal/survey/service.go | 82 +++++++++ pkg/adapters/storage/mapper/survey_mapper.go | 47 ++++++ pkg/adapters/storage/models/survey.go | 24 +++ pkg/adapters/storage/models/user.go | 16 ++ pkg/adapters/storage/survey_repository.go | 58 +++++++ tests/CreateSurvey.js | 16 ++ tests/DeleteSurvay.js | 1 + tests/GetSurveyByID.js | 1 + tests/UpdateSurvey.js | 8 + 19 files changed, 668 insertions(+), 7 deletions(-) create mode 100644 api/http/dto/create_survey.go create mode 100644 api/http/dto/get_survey.go create mode 100644 api/http/dto/update_survey.go create mode 100644 api/http/handlers/surevey.go create mode 100644 api/http/mapper/survey_mapper.go create mode 100644 internal/survey/domain/survey.go create mode 100644 internal/survey/port/repository.go create mode 100644 internal/survey/port/service.go create mode 100644 internal/survey/service.go create mode 100644 pkg/adapters/storage/mapper/survey_mapper.go create mode 100644 pkg/adapters/storage/models/survey.go create mode 100644 pkg/adapters/storage/models/user.go create mode 100644 pkg/adapters/storage/survey_repository.go create mode 100644 tests/CreateSurvey.js create mode 100644 tests/DeleteSurvay.js create mode 100644 tests/GetSurveyByID.js create mode 100644 tests/UpdateSurvey.js diff --git a/api/http/dto/create_survey.go b/api/http/dto/create_survey.go new file mode 100644 index 0000000..6aceeb4 --- /dev/null +++ b/api/http/dto/create_survey.go @@ -0,0 +1,24 @@ +// api/http/dto/create_survey.go +package dto + +import "time" + +type CreateSurveyRequest struct { + Title string `json:"title" validate:"required,min=3,max=100"` + CreationTime time.Time `json:"creation_time" validate:"required"` + StartTime *time.Time `json:"start_time" validate:"required"` + EndTime *time.Time `json:"end_time" validate:"required,gtfield=StartTime"` + RandomOrder bool `json:"random_order"` + AllowReturn bool `json:"allow_return"` + NumParticipationAttempts int `json:"num_participation_attempts" validate:"gte=1,lte=10"` + ResponseTime int `json:"response_time" validate:"gte=60,lte=86400"` // Between 1 minute and 1 day + AnonymityLevel string `json:"anonymity_level" validate:"required,oneof=visible_to_creator visible_to_creator_and_admins anonymous"` + DemographicRestrictions string `json:"demographic_restrictions" validate:"omitempty,json"` + ResponseModification bool `json:"response_modification"` +} + +type CreateSurveyResponse struct { + ID uint `json:"id"` + Title string `json:"title"` + OwnerID uint `json:"owner_id"` +} diff --git a/api/http/dto/get_survey.go b/api/http/dto/get_survey.go new file mode 100644 index 0000000..0dab092 --- /dev/null +++ b/api/http/dto/get_survey.go @@ -0,0 +1,19 @@ +// api/http/dto/get_survey.go +package dto + +import "time" + +type GetSurveyResponse struct { + ID uint `json:"id"` + Title string `json:"title"` + CreationTime time.Time `json:"creation_time"` + StartTime *time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time"` + RandomOrder bool `json:"random_order"` + AllowReturn bool `json:"allow_return"` + NumParticipationAttempts int `json:"num_participation_attempts"` + ResponseTime int `json:"response_time"` + AnonymityLevel string `json:"anonymity_level"` + DemographicRestrictions string `json:"demographic_restrictions"` + ResponseModification bool `json:"response_modification"` +} diff --git a/api/http/dto/update_survey.go b/api/http/dto/update_survey.go new file mode 100644 index 0000000..7180d8b --- /dev/null +++ b/api/http/dto/update_survey.go @@ -0,0 +1,22 @@ +// api/http/dto/update_survey.go +package dto + +import "time" + +type UpdateSurveyRequest struct { + Title *string `json:"title,omitempty"` + StartTime *time.Time `json:"start_time,omitempty"` + EndTime *time.Time `json:"end_time,omitempty"` + RandomOrder *bool `json:"random_order,omitempty"` + AllowReturn *bool `json:"allow_return,omitempty"` + NumParticipationAttempts *int `json:"num_participation_attempts,omitempty"` + ResponseTime *int `json:"response_time,omitempty"` + AnonymityLevel *string `json:"anonymity_level,omitempty"` + DemographicRestrictions *string `json:"demographic_restrictions,omitempty"` + ResponseModification *bool `json:"response_modification,omitempty"` +} + +type UpdateSurveyResponse struct { + ID uint `json:"id"` + Title string `json:"title"` +} diff --git a/api/http/handlers/surevey.go b/api/http/handlers/surevey.go new file mode 100644 index 0000000..88c0331 --- /dev/null +++ b/api/http/handlers/surevey.go @@ -0,0 +1,168 @@ +// api/http/handlers/survey.go +package handlers + +import ( + "fmt" + "golipors/api/http/dto" + "golipors/api/http/mapper" + "golipors/internal/survey/port" + "net/http" + "strconv" + + "github.com/go-playground/validator/v10" + + "github.com/gofiber/fiber/v2" +) + +var validate = validator.New() + +type SurveyHandler struct { + service port.Service +} + +func NewSurveyHandler(service port.Service) *SurveyHandler { + return &SurveyHandler{ + service: service, + } +} + +func (h *SurveyHandler) RegisterRoutes(api fiber.Router) { + api.Post("/surveys", h.CreateSurvey) + api.Get("/surveys/:id", h.GetSurveyByID) + api.Put("/surveys/:id", h.UpdateSurvey) + api.Delete("/surveys/:id", h.DeleteSurvey) +} + +// CreateSurvey handles POST /api/surveys +func (h *SurveyHandler) CreateSurvey(c *fiber.Ctx) error { + var req dto.CreateSurveyRequest + + // Parse request body + if err := c.BodyParser(&req); err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body", + }) + } + + // Validate request body + if err := validate.Struct(req); err != nil { + // Extract validation errors + validationErrors := make(map[string]string) + for _, err := range err.(validator.ValidationErrors) { + validationErrors[err.Field()] = fmt.Sprintf("Validation failed on '%s' with tag '%s'", err.Field(), err.Tag()) + } + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Validation failed", + "details": validationErrors, + }) + } + + // Assuming owner ID is retrieved from context (e.g., after authentication) + ownerID := uint(1) // Placeholder + + // Convert DTO to domain model + survey := mapper.CreateSurveyRequestToDomain(req, ownerID) + + // Create survey + surveyID, err := h.service.CreateSurvey(c.Context(), survey) + if err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + // Build response + response := dto.CreateSurveyResponse{ + ID: surveyID, + Title: survey.Title, + OwnerID: ownerID, + } + + return c.Status(http.StatusCreated).JSON(response) +} + +// GetSurveyByID handles GET /api/surveys/:id +func (h *SurveyHandler) GetSurveyByID(c *fiber.Ctx) error { + idParam := c.Params("id") + id, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid survey ID", + }) + } + + survey, err := h.service.GetSurveyByID(c.Context(), uint(id)) + if err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + if survey == nil { + return c.Status(http.StatusNotFound).JSON(fiber.Map{ + "error": "Survey not found", + }) + } + + response := mapper.DomainSurveyToGetSurveyResponse(*survey) + return c.Status(http.StatusOK).JSON(response) +} + +// UpdateSurvey handles PUT /api/surveys/:id +func (h *SurveyHandler) UpdateSurvey(c *fiber.Ctx) error { + idParam := c.Params("id") + id, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid survey ID", + }) + } + + var req dto.UpdateSurveyRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body", + }) + } + + survey := mapper.UpdateSurveyRequestToDomain(req) + survey.ID = uint(id) + + if err := h.service.UpdateSurvey(c.Context(), survey); err != nil { + if err.Error() == "survey not found" { + return c.Status(http.StatusNotFound).JSON(fiber.Map{ + "error": "Survey not found", + }) + } + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + updatedSurvey, _ := h.service.GetSurveyByID(c.Context(), uint(id)) + response := mapper.DomainSurveyToUpdateSurveyResponse(*updatedSurvey) + return c.Status(http.StatusOK).JSON(response) +} + +// DeleteSurvey handles DELETE /api/surveys/:id +func (h *SurveyHandler) DeleteSurvey(c *fiber.Ctx) error { + idParam := c.Params("id") + id, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid survey ID", + }) + } + + if err := h.service.DeleteSurvey(c.Context(), uint(id)); err != nil { + if err.Error() == "survey not found" { + return c.Status(http.StatusNotFound).JSON(fiber.Map{ + "error": "Survey not found", + }) + } + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.SendStatus(http.StatusOK) +} diff --git a/api/http/mapper/survey_mapper.go b/api/http/mapper/survey_mapper.go new file mode 100644 index 0000000..57b432b --- /dev/null +++ b/api/http/mapper/survey_mapper.go @@ -0,0 +1,93 @@ +// api/http/mapper/survey_mapper.go +package mapper + +import ( + "golipors/api/http/dto" + "golipors/internal/survey/domain" +) + +func CreateSurveyRequestToDomain(req dto.CreateSurveyRequest, ownerID uint) domain.Survey { + return domain.Survey{ + Title: req.Title, + CreationTime: req.CreationTime, + StartTime: req.StartTime, + EndTime: req.EndTime, + RandomOrder: req.RandomOrder, + AllowReturn: req.AllowReturn, + NumParticipationAttempts: req.NumParticipationAttempts, + ResponseTime: req.ResponseTime, + AnonymityLevel: req.AnonymityLevel, + DemographicRestrictions: req.DemographicRestrictions, + ResponseModification: req.ResponseModification, + OwnerID: ownerID, + } +} + +func DomainSurveyToCreateSurveyResponse(survey domain.Survey) dto.CreateSurveyResponse { + return dto.CreateSurveyResponse{ + ID: survey.ID, + Title: survey.Title, + OwnerID: survey.OwnerID, + } +} + +func DomainSurveyToGetSurveyResponse(survey domain.Survey) dto.GetSurveyResponse { + return dto.GetSurveyResponse{ + ID: survey.ID, + Title: survey.Title, + CreationTime: survey.CreationTime, + StartTime: survey.StartTime, + EndTime: survey.EndTime, + RandomOrder: survey.RandomOrder, + AllowReturn: survey.AllowReturn, + NumParticipationAttempts: survey.NumParticipationAttempts, + ResponseTime: survey.ResponseTime, + AnonymityLevel: survey.AnonymityLevel, + DemographicRestrictions: survey.DemographicRestrictions, + ResponseModification: survey.ResponseModification, + } +} + +func UpdateSurveyRequestToDomain(req dto.UpdateSurveyRequest) domain.Survey { + survey := domain.Survey{} + + if req.Title != nil { + survey.Title = *req.Title + } + if req.StartTime != nil { + survey.StartTime = req.StartTime + } + if req.EndTime != nil { + survey.EndTime = req.EndTime + } + if req.RandomOrder != nil { + survey.RandomOrder = *req.RandomOrder + } + if req.AllowReturn != nil { + survey.AllowReturn = *req.AllowReturn + } + if req.NumParticipationAttempts != nil { + survey.NumParticipationAttempts = *req.NumParticipationAttempts + } + if req.ResponseTime != nil { + survey.ResponseTime = *req.ResponseTime + } + if req.AnonymityLevel != nil { + survey.AnonymityLevel = *req.AnonymityLevel + } + if req.DemographicRestrictions != nil { + survey.DemographicRestrictions = *req.DemographicRestrictions + } + if req.ResponseModification != nil { + survey.ResponseModification = *req.ResponseModification + } + + return survey +} + +func DomainSurveyToUpdateSurveyResponse(survey domain.Survey) dto.UpdateSurveyResponse { + return dto.UpdateSurveyResponse{ + ID: survey.ID, + Title: survey.Title, + } +} diff --git a/go.mod b/go.mod index b4e45d1..a9a198a 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module golipors go 1.23.2 require ( + github.com/go-playground/validator/v10 v10.23.0 github.com/gofiber/contrib/swagger v1.2.0 github.com/gofiber/fiber/v2 v2.52.5 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/redis/go-redis/v9 v9.7.0 github.com/swaggo/swag v1.16.4 - golang.org/x/crypto v0.17.0 gorm.io/driver/postgres v1.5.10 gorm.io/gorm v1.25.12 ) @@ -19,6 +19,7 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.4 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -29,6 +30,8 @@ require ( github.com/go-openapi/strfmt v0.21.8 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/google/uuid v1.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -38,6 +41,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -51,6 +55,8 @@ require ( github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect go.mongodb.org/mongo-driver v1.13.1 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/net v0.31.0 // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect diff --git a/go.sum b/go.sum index 79fd7f0..1d2d68c 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= @@ -46,6 +48,14 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/validate v0.22.3 h1:KxG9mu5HBRYbecRb37KRCihvGGtND2aXziBAv0NNfyI= github.com/go-openapi/validate v0.22.3/go.mod h1:kVxh31KbfsxU8ZyoHaDbLBWU5CnMdqBUEtadQ2G4d5M= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/gofiber/contrib/swagger v1.2.0 h1:+tm7mBLFfUxZASQyf1zkvRkAZRZGmnIT+E0Vvj7BZo4= github.com/gofiber/contrib/swagger v1.2.0/go.mod h1:NRtN6G1RkdpgwFifq4nID/5cdxv410RDH9rUr9fhiqU= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= @@ -83,6 +93,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -147,8 +159,8 @@ go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwD golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= @@ -158,6 +170,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -195,15 +209,11 @@ golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= -gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/survey/domain/survey.go b/internal/survey/domain/survey.go new file mode 100644 index 0000000..fa3a9ed --- /dev/null +++ b/internal/survey/domain/survey.go @@ -0,0 +1,38 @@ +// internal/survey/domain/survey.go +package domain + +import ( + "errors" + "time" +) + +type Survey struct { + ID uint + Title string + CreationTime time.Time + StartTime *time.Time + EndTime *time.Time + RandomOrder bool + AllowReturn bool + NumParticipationAttempts int + ResponseTime int // in seconds + AnonymityLevel string + OwnerID uint + DemographicRestrictions string + ResponseModification bool + CreatedAt time.Time + UpdatedAt time.Time +} + +func (s *Survey) Validate() error { + if s.Title == "" { + return errors.New("title cannot be empty") + } + if s.CreationTime.IsZero() { + return errors.New("creation_time is required") + } + if s.AnonymityLevel == "" { + return errors.New("anonymity_level cannot be empty") + } + return nil +} diff --git a/internal/survey/port/repository.go b/internal/survey/port/repository.go new file mode 100644 index 0000000..f7a5913 --- /dev/null +++ b/internal/survey/port/repository.go @@ -0,0 +1,14 @@ +// internal/survey/port/repository.go +package port + +import ( + "context" + "golipors/internal/survey/domain" +) + +type Repository interface { + CreateSurvey(ctx context.Context, survey domain.Survey) (uint, error) + GetSurveyByID(ctx context.Context, id uint) (*domain.Survey, error) + UpdateSurvey(ctx context.Context, survey domain.Survey) error + DeleteSurvey(ctx context.Context, id uint) error +} diff --git a/internal/survey/port/service.go b/internal/survey/port/service.go new file mode 100644 index 0000000..cc2c995 --- /dev/null +++ b/internal/survey/port/service.go @@ -0,0 +1,14 @@ +// internal/survey/port/service.go +package port + +import ( + "context" + "golipors/internal/survey/domain" +) + +type Service interface { + CreateSurvey(ctx context.Context, survey domain.Survey) (uint, error) + GetSurveyByID(ctx context.Context, id uint) (*domain.Survey, error) + UpdateSurvey(ctx context.Context, survey domain.Survey) error + DeleteSurvey(ctx context.Context, id uint) error +} diff --git a/internal/survey/service.go b/internal/survey/service.go new file mode 100644 index 0000000..628492a --- /dev/null +++ b/internal/survey/service.go @@ -0,0 +1,82 @@ +// internal/survey/service/service.go +package service + +import ( + "context" + "errors" + "golipors/internal/survey/domain" + "golipors/internal/survey/port" +) + +type surveyService struct { + repository port.Repository +} + +func NewService(repository port.Repository) port.Service { + return &surveyService{ + repository: repository, + } +} + +func (s *surveyService) CreateSurvey(ctx context.Context, survey domain.Survey) (uint, error) { + if err := survey.Validate(); err != nil { + return 0, err + } + return s.repository.CreateSurvey(ctx, survey) +} + +func (s *surveyService) GetSurveyByID(ctx context.Context, id uint) (*domain.Survey, error) { + return s.repository.GetSurveyByID(ctx, id) +} + +func (s *surveyService) UpdateSurvey(ctx context.Context, survey domain.Survey) error { + existingSurvey, err := s.repository.GetSurveyByID(ctx, survey.ID) + if err != nil { + return err + } + if existingSurvey == nil { + return errors.New("survey not found") + } + + // Merge existing and updated survey data + if survey.Title != "" { + existingSurvey.Title = survey.Title + } + if survey.StartTime != nil { + existingSurvey.StartTime = survey.StartTime + } + if survey.EndTime != nil { + existingSurvey.EndTime = survey.EndTime + } + if survey.RandomOrder != existingSurvey.RandomOrder { + existingSurvey.RandomOrder = survey.RandomOrder + } + if survey.AllowReturn != existingSurvey.AllowReturn { + existingSurvey.AllowReturn = survey.AllowReturn + } + if survey.NumParticipationAttempts != 0 { + existingSurvey.NumParticipationAttempts = survey.NumParticipationAttempts + } + if survey.ResponseTime != 0 { + existingSurvey.ResponseTime = survey.ResponseTime + } + if survey.AnonymityLevel != "" { + existingSurvey.AnonymityLevel = survey.AnonymityLevel + } + if survey.DemographicRestrictions != "" { + existingSurvey.DemographicRestrictions = survey.DemographicRestrictions + } + if survey.ResponseModification != existingSurvey.ResponseModification { + existingSurvey.ResponseModification = survey.ResponseModification + } + + if err := existingSurvey.Validate(); err != nil { + return err + } + + return s.repository.UpdateSurvey(ctx, *existingSurvey) +} + +func (s *surveyService) DeleteSurvey(ctx context.Context, id uint) error { + return s.repository.DeleteSurvey(ctx, id) +} diff --git a/pkg/adapters/storage/mapper/survey_mapper.go b/pkg/adapters/storage/mapper/survey_mapper.go new file mode 100644 index 0000000..fd3ebeb --- /dev/null +++ b/pkg/adapters/storage/mapper/survey_mapper.go @@ -0,0 +1,47 @@ +// pkg/adapters/storage/mapper/survey_mapper.go +package mapper + +import ( + "golipors/internal/survey/domain" + "golipors/pkg/adapters/storage/models" +) + +func DomainToModel(survey domain.Survey) models.Survey { + return models.Survey{ + ID: survey.ID, + Title: survey.Title, + CreationTime: survey.CreationTime, + StartTime: survey.StartTime, + EndTime: survey.EndTime, + RandomOrder: survey.RandomOrder, + AllowReturn: survey.AllowReturn, + NumParticipationAttempts: survey.NumParticipationAttempts, + ResponseTime: survey.ResponseTime, + AnonymityLevel: survey.AnonymityLevel, + OwnerID: survey.OwnerID, + DemographicRestrictions: survey.DemographicRestrictions, + ResponseModification: survey.ResponseModification, + CreatedAt: survey.CreatedAt, + UpdatedAt: survey.UpdatedAt, + } +} + +func ModelToDomain(survey models.Survey) domain.Survey { + return domain.Survey{ + ID: survey.ID, + Title: survey.Title, + CreationTime: survey.CreationTime, + StartTime: survey.StartTime, + EndTime: survey.EndTime, + RandomOrder: survey.RandomOrder, + AllowReturn: survey.AllowReturn, + NumParticipationAttempts: survey.NumParticipationAttempts, + ResponseTime: survey.ResponseTime, + AnonymityLevel: survey.AnonymityLevel, + OwnerID: survey.OwnerID, + DemographicRestrictions: survey.DemographicRestrictions, + ResponseModification: survey.ResponseModification, + CreatedAt: survey.CreatedAt, + UpdatedAt: survey.UpdatedAt, + } +} diff --git a/pkg/adapters/storage/models/survey.go b/pkg/adapters/storage/models/survey.go new file mode 100644 index 0000000..5f60d0e --- /dev/null +++ b/pkg/adapters/storage/models/survey.go @@ -0,0 +1,24 @@ +// pkg/adapters/storage/models/survey.go +package models + +import ( + "time" +) + +type Survey struct { + ID uint `gorm:"primaryKey"` + Title string `gorm:"not null"` + CreationTime time.Time `gorm:"not null"` + StartTime *time.Time + EndTime *time.Time + RandomOrder bool `gorm:"default:false"` + AllowReturn bool `gorm:"default:false"` + NumParticipationAttempts int `gorm:"default:1"` + ResponseTime int // in seconds + AnonymityLevel string `gorm:"not null"` + OwnerID uint + DemographicRestrictions string `gorm:"type:text"` + ResponseModification bool `gorm:"default:false"` + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/pkg/adapters/storage/models/user.go b/pkg/adapters/storage/models/user.go new file mode 100644 index 0000000..61b7954 --- /dev/null +++ b/pkg/adapters/storage/models/user.go @@ -0,0 +1,16 @@ +// // pkg/adapters/storage/models/user.go +// package models + +// import ( +// "time" +// ) + +// type User struct { +// ID uint `gorm:"primaryKey"` +// Username string `gorm:"unique;not null"` +// Email string `gorm:"unique;not null"` +// Password string `gorm:"not null"` +// CreatedAt time.Time +// UpdatedAt time.Time +// Surveys []Survey `gorm:"foreignKey:OwnerID"` +// } diff --git a/pkg/adapters/storage/survey_repository.go b/pkg/adapters/storage/survey_repository.go new file mode 100644 index 0000000..d1c27c9 --- /dev/null +++ b/pkg/adapters/storage/survey_repository.go @@ -0,0 +1,58 @@ +// pkg/adapters/storage/survey_repository.go +package storage + +import ( + "context" + "errors" + "golipors/internal/survey/domain" + "golipors/internal/survey/port" + "golipors/pkg/adapters/storage/mapper" + "golipors/pkg/adapters/storage/models" + + "gorm.io/gorm" +) + +type surveyRepository struct { + db *gorm.DB +} + +func NewSurveyRepository(db *gorm.DB) port.Repository { + return &surveyRepository{ + db: db, + } +} + +func (r *surveyRepository) CreateSurvey(ctx context.Context, survey domain.Survey) (uint, error) { + model := mapper.DomainToModel(survey) + if err := r.db.WithContext(ctx).Create(&model).Error; err != nil { + return 0, err + } + return model.ID, nil +} + +func (r *surveyRepository) GetSurveyByID(ctx context.Context, id uint) (*domain.Survey, error) { + var model models.Survey + if err := r.db.WithContext(ctx).First(&model, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + survey := mapper.ModelToDomain(model) + return &survey, nil +} + +func (r *surveyRepository) UpdateSurvey(ctx context.Context, survey domain.Survey) error { + model := mapper.DomainToModel(survey) + if err := r.db.WithContext(ctx).Model(&models.Survey{}).Where("id = ?", survey.ID).Updates(&model).Error; err != nil { + return err + } + return nil +} + +func (r *surveyRepository) DeleteSurvey(ctx context.Context, id uint) error { + if err := r.db.WithContext(ctx).Delete(&models.Survey{}, id).Error; err != nil { + return err + } + return nil +} diff --git a/tests/CreateSurvey.js b/tests/CreateSurvey.js new file mode 100644 index 0000000..0457912 --- /dev/null +++ b/tests/CreateSurvey.js @@ -0,0 +1,16 @@ +POST /api/surveys +Content-Type: application/json + +{ + "title": "Customer Feedback Survey", + "creation_time": "2024-12-01T00:00:00Z", + "start_time": "2024-12-05T00:00:00Z", + "end_time": "2024-12-31T23:59:59Z", + "random_order": true, + "allow_return": false, + "num_participation_attempts": 3, + "response_time": 1800, + "anonymity_level": "visible_to_creator_and_admins", + "demographic_restrictions": "{\"age\": \"18-65\", \"location\": \"USA\"}", + "response_modification": true +} diff --git a/tests/DeleteSurvay.js b/tests/DeleteSurvay.js new file mode 100644 index 0000000..2712004 --- /dev/null +++ b/tests/DeleteSurvay.js @@ -0,0 +1 @@ +DELETE / api / surveys / 1; diff --git a/tests/GetSurveyByID.js b/tests/GetSurveyByID.js new file mode 100644 index 0000000..cec31b6 --- /dev/null +++ b/tests/GetSurveyByID.js @@ -0,0 +1 @@ +GET / api / surveys / 1; diff --git a/tests/UpdateSurvey.js b/tests/UpdateSurvey.js new file mode 100644 index 0000000..9a8a685 --- /dev/null +++ b/tests/UpdateSurvey.js @@ -0,0 +1,8 @@ +PUT /api/surveys/1 +Content-Type: application/json + +{ + "title": "Updated Customer Feedback Survey", + "start_time": "2024-12-10T00:00:00Z", + "end_time": "2024-12-25T23:59:59Z" +}