├── .gitignore ├── app ├── schemas │ ├── schema.graphql │ ├── question_option.graphql │ ├── answer.graphql │ └── question.graphql ├── domain │ └── repository │ │ ├── answer │ │ └── answer.go │ │ ├── question │ │ └── question.go │ │ └── question_option │ │ └── question_option.go ├── interfaces │ ├── resolver.go │ ├── setup_test.go │ ├── answer.resolvers.go │ ├── answer.resolvers_test.go │ ├── question.resolvers.go │ └── question.resolvers_test.go ├── infrastructure │ ├── db │ │ └── db.go │ └── persistence │ │ ├── question_repository.go │ │ ├── answer_repository.go │ │ ├── question_option_repository.go │ │ ├── answer_repository_test.go │ │ ├── question_repository_test.go │ │ ├── setup_test.go │ │ └── question_option_repository_test.go └── models │ ├── model_hooks.go │ ├── model_tags │ └── model_tags.go │ └── models.go ├── Makefile ├── README.md ├── .env ├── go.mod ├── LICENSE ├── helpers └── helpers.go ├── .circleci └── config.yml ├── gqlgen.yml ├── main.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /app/schemas/schema.graphql: -------------------------------------------------------------------------------- 1 | # Custom schema 2 | 3 | scalar Time 4 | 5 | 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | go run github.com/99designs/gqlgen init 3 | 4 | generate: 5 | go run github.com/99designs/gqlgen && go run ./app/models/model_tags/model_tags.go 6 | 7 | run: 8 | go run main.go 9 | 10 | test: 11 | go test -v ./... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/victorsteven/multi-choice-graphql-go-app.svg?style=svg)](https://circleci.com/gh/victorsteven/multi-choice-graphql-go-app) 2 | 3 | 4 | An article was written about this project [here](https://dev.to/stevensunflash/using-graphql-in-golang-3ob5) 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATABASE_DRIVER=postgres 2 | DATABASE_USER=postgres 3 | DATABASE_NAME=multi-choice 4 | DATABASE_HOST=localhost 5 | DATABASE_PORT=5432 6 | DATABASE_PASSWORD=password 7 | 8 | 9 | TEST_DB_DRIVER=postgres 10 | TEST_DB_HOST=127.0.0.1 11 | TEST_DB_PASSWORD=password 12 | TEST_DB_USER=postgres 13 | TEST_DB_NAME=multi-choice-test 14 | TEST_DB_PORT=5432 15 | 16 | -------------------------------------------------------------------------------- /app/schemas/question_option.graphql: -------------------------------------------------------------------------------- 1 | type QuestionOption { 2 | id: ID! 3 | questionId: ID! 4 | title: String! 5 | position: Int! 6 | isCorrect: Boolean! 7 | createdAt: Time! 8 | updatedAt: Time! 9 | } 10 | 11 | input QuestionOptionInput { 12 | title: String! 13 | position: Int! 14 | isCorrect: Boolean! 15 | } 16 | 17 | -------------------------------------------------------------------------------- /app/domain/repository/answer/answer.go: -------------------------------------------------------------------------------- 1 | package answer 2 | 3 | import ( 4 | "multi-choice/app/models" 5 | ) 6 | 7 | type AnsService interface { 8 | CreateAnswer(answer *models.Answer) (*models.Answer, error) 9 | UpdateAnswer(answer *models.Answer) (*models.Answer, error) 10 | DeleteAnswer(id string) error 11 | GetAnswerByID(id string) (*models.Answer, error) 12 | GetAllQuestionAnswers(questionId string) ([]*models.Answer, error) 13 | } 14 | 15 | -------------------------------------------------------------------------------- /app/domain/repository/question/question.go: -------------------------------------------------------------------------------- 1 | package question 2 | 3 | import ( 4 | "multi-choice/app/models" 5 | ) 6 | 7 | type QuesService interface { 8 | CreateQuestion(question *models.Question) (*models.Question, error) 9 | UpdateQuestion(question *models.Question) (*models.Question, error) 10 | DeleteQuestion(id string) error 11 | GetQuestionByID(id string) (*models.Question, error) 12 | GetAllQuestions() ([]*models.Question, error) 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/interfaces/resolver.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "multi-choice/app/domain/repository/answer" 5 | "multi-choice/app/domain/repository/question" 6 | "multi-choice/app/domain/repository/question_option" 7 | ) 8 | 9 | // This file will not be regenerated automatically. 10 | // 11 | // It serves as dependency injection for your app, add any dependencies you require here. 12 | 13 | type Resolver struct { 14 | AnsService answer.AnsService 15 | QuestionService question.QuesService 16 | QuestionOptionService question_option.OptService 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/infrastructure/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "log" 6 | "multi-choice/app/models" 7 | "os" 8 | ) 9 | 10 | func OpenDB(database string) *gorm.DB { 11 | 12 | databaseDriver := os.Getenv("DATABASE_DRIVER") 13 | 14 | db, err := gorm.Open(databaseDriver, database) 15 | if err != nil { 16 | log.Fatalf("%s", err) 17 | } 18 | if err := Automigrate(db); err != nil { 19 | panic(err) 20 | } 21 | return db 22 | } 23 | 24 | func Automigrate(db *gorm.DB) error { 25 | return db.AutoMigrate(&models.Question{}, &models.QuestionOption{}, &models.Answer{}).Error 26 | } 27 | -------------------------------------------------------------------------------- /app/domain/repository/question_option/question_option.go: -------------------------------------------------------------------------------- 1 | package question_option 2 | 3 | import ( 4 | "multi-choice/app/models" 5 | ) 6 | 7 | type OptService interface { 8 | CreateQuestionOption(question *models.QuestionOption) (*models.QuestionOption, error) 9 | UpdateQuestionOption(question *models.QuestionOption) (*models.QuestionOption, error) 10 | DeleteQuestionOption(id string) error 11 | DeleteQuestionOptionByQuestionID(questionId string) error 12 | GetQuestionOptionByID(id string) (*models.QuestionOption, error) 13 | GetQuestionOptionByQuestionID(questionId string) ([]*models.QuestionOption, error) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /app/models/model_hooks.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/twinj/uuid" 6 | ) 7 | 8 | //We want our ids to be uuids, so we define that here 9 | 10 | func (mod *Question) BeforeCreate(scope *gorm.Scope) error { 11 | uuid := uuid.NewV4() 12 | return scope.SetColumn("id", uuid.String()) 13 | } 14 | 15 | func (mod *QuestionOption) BeforeCreate(scope *gorm.Scope) error { 16 | uuid := uuid.NewV4() 17 | return scope.SetColumn("id", uuid.String()) 18 | } 19 | 20 | func (mod *Answer) BeforeCreate(scope *gorm.Scope) error { 21 | uuid := uuid.NewV4() 22 | return scope.SetColumn("id", uuid.String()) 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module multi-choice 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.12.2 7 | github.com/go-playground/universal-translator v0.17.0 // indirect 8 | github.com/jinzhu/gorm v1.9.16 9 | github.com/joho/godotenv v1.3.0 10 | github.com/leodido/go-urn v1.2.0 // indirect 11 | github.com/lib/pq v1.1.1 12 | github.com/myesui/uuid v1.0.0 // indirect 13 | github.com/stretchr/testify v1.4.0 14 | github.com/twinj/uuid v1.0.0 15 | github.com/vektah/gqlparser/v2 v2.0.1 16 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 17 | gopkg.in/go-playground/validator.v9 v9.31.0 18 | gopkg.in/stretchr/testify.v1 v1.2.2 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /app/schemas/answer.graphql: -------------------------------------------------------------------------------- 1 | type Answer { 2 | id: ID! 3 | questionId: ID! 4 | optionId: ID! 5 | isCorrect: Boolean! 6 | createdAt: Time! 7 | updatedAt: Time! 8 | } 9 | 10 | extend type Mutation { 11 | CreateAnswer(questionId: ID!, optionId: ID!): AnswerResponse 12 | UpdateAnswer(id: ID! questionId: ID!, optionId: ID!): AnswerResponse 13 | DeleteAnswer(id: ID!): AnswerResponse 14 | } 15 | 16 | extend type Query { 17 | GetOneAnswer(id: ID!): AnswerResponse 18 | GetAllQuestionAnswers(questionId: ID!): AnswerResponse 19 | } 20 | 21 | type AnswerResponse { 22 | message: String! 23 | status: Int! 24 | data: Answer #For single record 25 | dataList: [Answer] # For array of records. 26 | } 27 | 28 | -------------------------------------------------------------------------------- /app/schemas/question.graphql: -------------------------------------------------------------------------------- 1 | type Question { 2 | id: ID! 3 | title: String! 4 | questionOption: [QuestionOption] 5 | createdAt: Time! 6 | updatedAt: Time! 7 | } 8 | 9 | input QuestionInput { 10 | title: String!, 11 | options: [QuestionOptionInput!]! 12 | } 13 | 14 | type Mutation { 15 | CreateQuestion(question: QuestionInput!): QuestionResponse 16 | UpdateQuestion(id: ID!, question: QuestionInput!): QuestionResponse 17 | DeleteQuestion(id: ID!): QuestionResponse 18 | } 19 | 20 | type Query { 21 | GetOneQuestion(id: ID!): QuestionResponse 22 | GetAllQuestions: QuestionResponse 23 | } 24 | 25 | type QuestionResponse { 26 | message: String! 27 | status: Int! 28 | data: Question 29 | dataList: [Question] 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Steven Victor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/models/model_tags/model_tags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/99designs/gqlgen/api" 6 | "github.com/99designs/gqlgen/codegen/config" 7 | "github.com/99designs/gqlgen/plugin/modelgen" 8 | "os" 9 | ) 10 | 11 | //Add the gorm tags to the model definition 12 | func addGormTags(b *modelgen.ModelBuild) *modelgen.ModelBuild { 13 | for _, model := range b.Models { 14 | for _, field := range model.Fields { 15 | if model.Name == "Question" && field.Name == "title" { 16 | field.Tag += ` gorm:"unique" db:"` + field.Name + `"` 17 | } else { 18 | field.Tag += ` db:"` + field.Name + `"` 19 | } 20 | } 21 | } 22 | 23 | return b 24 | } 25 | 26 | func main() { 27 | cfg, err := config.LoadConfigFromDefaultLocations() 28 | if err != nil { 29 | _, _ = fmt.Fprintln(os.Stderr, "failed to load config", err.Error()) 30 | os.Exit(2) 31 | } 32 | 33 | // Attaching the mutation function onto modelgen plugin 34 | p := modelgen.Plugin{ 35 | MutateHook: addGormTags, 36 | } 37 | 38 | err = api.Generate(cfg, 39 | api.NoPlugins(), 40 | api.AddPlugin(&p), 41 | ) 42 | if err != nil { 43 | _, _ = fmt.Fprintln(os.Stderr, err.Error()) 44 | os.Exit(3) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "gopkg.in/go-playground/validator.v9" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | 10 | // ValidateInputs validates the inputs 11 | func ValidateInputs(dataSet interface{}) (bool, string) { 12 | 13 | var validate *validator.Validate 14 | 15 | validate = validator.New() 16 | 17 | err := validate.Struct(dataSet) 18 | 19 | if err != nil { 20 | 21 | //Validation syntax is invalid 22 | if err, ok := err.(*validator.InvalidValidationError); ok { 23 | panic(err) 24 | } 25 | 26 | //Validation errors occurred 27 | var errString string 28 | 29 | reflected := reflect.ValueOf(dataSet) 30 | 31 | for _, err := range err.(validator.ValidationErrors) { 32 | 33 | // Attempt to find field by name and get json tag name 34 | field, _ := reflected.Type().FieldByName(err.StructField()) 35 | var name string 36 | //If json tag doesn't exist, use lower case of name 37 | if name = field.Tag.Get("json"); name == "" { 38 | name = strings.ToLower(err.StructField()) 39 | } 40 | 41 | switch err.Tag() { 42 | case "required": 43 | errString = "The " + name + " is required" 44 | break 45 | case "email": 46 | errString = "The " + name + " should be a valid email" 47 | break 48 | case "eqfield": 49 | errString = "The " + name + " should be equal to the " + err.Param() 50 | break 51 | default: 52 | 53 | errString = "The " + name + " is invalid" 54 | break 55 | } 56 | } 57 | return false, errString 58 | } 59 | return true, "" 60 | } 61 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 # use CircleCI 2.0 2 | jobs: # basic units of work in a run 3 | build: # runs not using Workflows must have a `build` job as entry point 4 | docker: # run the steps with Docker 5 | - image: circleci/golang:1.14 6 | - image: circleci/postgres:9.6-alpine 7 | environment: # environment variables for primary container 8 | POSTGRES_USER: postgres 9 | POSTGRES_DB: multi-choice-test 10 | 11 | environment: # environment variables for the build itself 12 | GO111MODULE: "on" #we don't rely on GOPATH 13 | 14 | working_directory: ~/usr/src/app # Go module is used, so we dont need to worry about GOPATH 15 | 16 | steps: # steps that comprise the `build` job 17 | - checkout # check out source code to working directory 18 | - run: 19 | name: "Fetch dependencies" 20 | command: go mod download 21 | 22 | # Wait for Postgres to be ready before proceeding 23 | - run: 24 | name: Waiting for Postgres to be ready 25 | command: dockerize -wait tcp://localhost:5432 -timeout 1m 26 | 27 | - run: 28 | name: Run unit tests 29 | environment: # environment variables for the database url and path to migration files 30 | FORUM_DB_URL: "postgres://postgres@localhost:5432/multi-choice-test?sslmode=disable" 31 | command: go test -v ./... # our test is inside the "tests" folder, so target only that 32 | 33 | workflows: 34 | version: 2 35 | build-workflow: 36 | jobs: 37 | - build 38 | -------------------------------------------------------------------------------- /gqlgen.yml: -------------------------------------------------------------------------------- 1 | # Where are all the schema files located? globs are supported eg src/**/*.graphqls 2 | schema: 3 | - app/schemas/*.graphql 4 | 5 | # Where should the generated server code go? 6 | exec: 7 | filename: app/generated/generated.go 8 | package: generated 9 | 10 | # Uncomment to enable federation 11 | # federation: 12 | # filename: app/generated/federation.go 13 | # package: generated 14 | 15 | # Where should any generated models go? 16 | model: 17 | filename: app/models/models.go 18 | package: models 19 | 20 | # Where should the resolver implementations go? 21 | resolver: 22 | layout: follow-schema 23 | dir: app/interfaces 24 | package: interfaces 25 | 26 | # Optional: turn on use `gqlgen:"fieldName"` tags in your models 27 | # struct_tag: json 28 | 29 | # Optional: turn on to use []Thing instead of []*Thing 30 | # omit_slice_element_pointers: false 31 | 32 | # Optional: set to speed up generation time by not performing a final validation pass. 33 | # skip_validation: true 34 | 35 | # gqlgen will search for any type names in the schema in these go packages 36 | # if they match it will use them, otherwise it will generate them. 37 | autobind: 38 | - "multi-choice/app/models" 39 | 40 | # This section declares type mapping between the GraphQL and go type systems 41 | # 42 | # The first line in each type will be used as defaults for resolver arguments and 43 | # modelgen, the others will be allowed when binding to fields. Configure them to 44 | # your liking 45 | 46 | models: 47 | ID: 48 | model: 49 | - github.com/99designs/gqlgen/graphql.ID 50 | - github.com/99designs/gqlgen/graphql.Int 51 | - github.com/99designs/gqlgen/graphql.Int64 52 | - github.com/99designs/gqlgen/graphql.Int32 53 | Int: 54 | model: 55 | - github.com/99designs/gqlgen/graphql.Int 56 | - github.com/99designs/gqlgen/graphql.Int64 57 | - github.com/99designs/gqlgen/graphql.Int32 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/infrastructure/persistence/question_repository.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "errors" 5 | "github.com/jinzhu/gorm" 6 | "multi-choice/app/domain/repository/question" 7 | "multi-choice/app/models" 8 | "strings" 9 | ) 10 | 11 | type quesService struct { 12 | db *gorm.DB 13 | } 14 | 15 | func NewQuestion(db *gorm.DB) *quesService { 16 | return &quesService{ 17 | db, 18 | } 19 | } 20 | 21 | //We implement the interface defined in the domain 22 | var _ question.QuesService = &quesService{} 23 | 24 | func (s *quesService) CreateQuestion(question *models.Question) (*models.Question, error) { 25 | 26 | err := s.db.Create(&question).Error 27 | if err != nil { 28 | if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "Duplicate") { 29 | return nil, errors.New("question title already taken") 30 | } 31 | return nil, err 32 | } 33 | 34 | return question, nil 35 | 36 | } 37 | 38 | func (s *quesService) UpdateQuestion(question *models.Question) (*models.Question, error) { 39 | 40 | err := s.db.Save(&question).Error 41 | if err != nil { 42 | if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "Duplicate") { 43 | return nil, errors.New("question title already taken") 44 | } 45 | return nil, err 46 | } 47 | 48 | return question, nil 49 | 50 | } 51 | 52 | func (s *quesService) DeleteQuestion(id string) error { 53 | 54 | ques := &models.Question{} 55 | 56 | err := s.db.Where("id = ?", id).Delete(&ques).Error 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (s *quesService) GetQuestionByID(id string) (*models.Question, error) { 65 | 66 | ques := &models.Question{} 67 | 68 | err := s.db.Where("id = ?", id).Preload("QuestionOption").Take(&ques).Error 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return ques, nil 74 | } 75 | 76 | func (s *quesService) GetAllQuestions() ([]*models.Question, error) { 77 | 78 | var questions []*models.Question 79 | 80 | err := s.db.Preload("QuestionOption").Find(&questions).Error 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return questions, nil 86 | } 87 | -------------------------------------------------------------------------------- /app/infrastructure/persistence/answer_repository.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "errors" 5 | "github.com/jinzhu/gorm" 6 | "multi-choice/app/domain/repository/answer" 7 | "multi-choice/app/models" 8 | ) 9 | 10 | type ansService struct { 11 | db *gorm.DB 12 | } 13 | 14 | func NewAnswer(db *gorm.DB) *ansService { 15 | return &ansService{ 16 | db, 17 | } 18 | } 19 | 20 | //We implement the interface defined in the domain 21 | var _ answer.AnsService = &ansService{} 22 | 23 | func (s *ansService) CreateAnswer(answer *models.Answer) (*models.Answer, error) { 24 | 25 | //first we need to check if the ans have been entered for this question: 26 | oldAns, _ := s.GetAllQuestionAnswers(answer.QuestionID) 27 | if len(oldAns) > 0 { 28 | for _, v := range oldAns { 29 | //We cannot have two correct answers for this type of quiz 30 | if v.IsCorrect == true && answer.IsCorrect { 31 | return nil, errors.New("cannot have two correct answers for the same question") 32 | } 33 | } 34 | } 35 | 36 | err := s.db.Create(&answer).Error 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return answer, nil 42 | } 43 | 44 | func (s *ansService) UpdateAnswer(answer *models.Answer) (*models.Answer, error) { 45 | 46 | err := s.db.Save(&answer).Error 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return answer, nil 52 | 53 | } 54 | 55 | func (s *ansService) DeleteAnswer(id string) error { 56 | 57 | ans := &models.Answer{} 58 | 59 | err := s.db.Where("id = ?", id).Delete(ans).Error 60 | if err != nil { 61 | return err 62 | } 63 | 64 | return nil 65 | 66 | } 67 | 68 | func (s *ansService) GetAnswerByID(id string) (*models.Answer, error) { 69 | 70 | var ans = &models.Answer{} 71 | 72 | err := s.db.Where("id = ?", id).Take(&ans).Error 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return ans, nil 78 | } 79 | 80 | func (s *ansService) GetAllQuestionAnswers(questionId string) ([]*models.Answer, error) { 81 | 82 | var answers []*models.Answer 83 | 84 | err := s.db.Where("question_id = ?", questionId).Find(&answers).Error 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | return answers, nil 90 | 91 | } 92 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/99designs/gqlgen/graphql/handler" 6 | "github.com/99designs/gqlgen/graphql/playground" 7 | "github.com/joho/godotenv" 8 | _ "github.com/lib/pq" 9 | "log" 10 | "multi-choice/app/domain/repository/answer" 11 | "multi-choice/app/domain/repository/question" 12 | "multi-choice/app/domain/repository/question_option" 13 | "multi-choice/app/generated" 14 | "multi-choice/app/infrastructure/db" 15 | "multi-choice/app/infrastructure/persistence" 16 | "multi-choice/app/interfaces" 17 | "net/http" 18 | "os" 19 | ) 20 | 21 | func init() { 22 | // loads values from .env into the system 23 | if err := godotenv.Load(); err != nil { 24 | log.Print("No .env file found") 25 | } 26 | } 27 | 28 | func main() { 29 | 30 | var ( 31 | defaultPort = "8080" 32 | databaseUser = os.Getenv("DATABASE_USER") 33 | databaseName = os.Getenv("DATABASE_NAME") 34 | databaseHost = os.Getenv("DATABASE_HOST") 35 | databasePort = os.Getenv("DATABASE_PORT") 36 | databasePassword = os.Getenv("DATABASE_PASSWORD") 37 | ) 38 | 39 | port := os.Getenv("PORT") 40 | if port == "" { 41 | port = defaultPort 42 | } 43 | 44 | dbConn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", databaseHost, databasePort, databaseUser, databaseName, databasePassword) 45 | 46 | conn := db.OpenDB(dbConn) 47 | 48 | var ansService answer.AnsService 49 | var questionService question.QuesService 50 | var questionOptService question_option.OptService 51 | 52 | ansService = persistence.NewAnswer(conn) 53 | questionService = persistence.NewQuestion(conn) 54 | questionOptService = persistence.NewQuestionOption(conn) 55 | 56 | srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 57 | AnsService: ansService, 58 | QuestionService: questionService, 59 | QuestionOptionService: questionOptService, 60 | }})) 61 | 62 | http.Handle("/", playground.Handler("GraphQL playground", "/query")) 63 | http.Handle("/query", srv) 64 | 65 | log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) 66 | log.Fatal(http.ListenAndServe(":"+port, nil)) 67 | } 68 | -------------------------------------------------------------------------------- /app/models/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package models 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | type Answer struct { 10 | ID string `json:"id" db:"id"` 11 | QuestionID string `json:"questionId" db:"questionId"` 12 | OptionID string `json:"optionId" db:"optionId"` 13 | IsCorrect bool `json:"isCorrect" db:"isCorrect"` 14 | CreatedAt time.Time `json:"createdAt" db:"createdAt"` 15 | UpdatedAt time.Time `json:"updatedAt" db:"updatedAt"` 16 | } 17 | 18 | type AnswerResponse struct { 19 | Message string `json:"message" db:"message"` 20 | Status int `json:"status" db:"status"` 21 | Data *Answer `json:"data" db:"data"` 22 | DataList []*Answer `json:"dataList" db:"dataList"` 23 | } 24 | 25 | type Question struct { 26 | ID string `json:"id" db:"id"` 27 | Title string `json:"title" gorm:"unique" db:"title"` 28 | QuestionOption []*QuestionOption `json:"questionOption" db:"questionOption"` 29 | CreatedAt time.Time `json:"createdAt" db:"createdAt"` 30 | UpdatedAt time.Time `json:"updatedAt" db:"updatedAt"` 31 | } 32 | 33 | type QuestionInput struct { 34 | Title string `json:"title" db:"title"` 35 | Options []*QuestionOptionInput `json:"options" db:"options"` 36 | } 37 | 38 | type QuestionOption struct { 39 | ID string `json:"id" db:"id"` 40 | QuestionID string `json:"questionId" db:"questionId"` 41 | Title string `json:"title" db:"title"` 42 | Position int `json:"position" db:"position"` 43 | IsCorrect bool `json:"isCorrect" db:"isCorrect"` 44 | CreatedAt time.Time `json:"createdAt" db:"createdAt"` 45 | UpdatedAt time.Time `json:"updatedAt" db:"updatedAt"` 46 | } 47 | 48 | type QuestionOptionInput struct { 49 | Title string `json:"title" db:"title"` 50 | Position int `json:"position" db:"position"` 51 | IsCorrect bool `json:"isCorrect" db:"isCorrect"` 52 | } 53 | 54 | type QuestionResponse struct { 55 | Message string `json:"message" db:"message"` 56 | Status int `json:"status" db:"status"` 57 | Data *Question `json:"data" db:"data"` 58 | DataList []*Question `json:"dataList" db:"dataList"` 59 | } 60 | -------------------------------------------------------------------------------- /app/infrastructure/persistence/question_option_repository.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "errors" 5 | "github.com/jinzhu/gorm" 6 | "multi-choice/app/domain/repository/question_option" 7 | "multi-choice/app/models" 8 | ) 9 | 10 | type optService struct { 11 | db *gorm.DB 12 | } 13 | 14 | func NewQuestionOption(db *gorm.DB) *optService { 15 | return &optService{ 16 | db, 17 | } 18 | } 19 | 20 | //We implement the interface defined in the domain 21 | var _ question_option.OptService = &optService{} 22 | 23 | func (s *optService) CreateQuestionOption(questOpt *models.QuestionOption) (*models.QuestionOption, error) { 24 | 25 | //check if this question option title or the position or the correctness already exist for the question 26 | oldOpts, _ := s.GetQuestionOptionByQuestionID(questOpt.QuestionID) 27 | if len(oldOpts) > 0 { 28 | for _, v := range oldOpts { 29 | if v.Title == questOpt.Title || v.Position == questOpt.Position || (v.IsCorrect == true && questOpt.IsCorrect == true) { 30 | return nil, errors.New("two question options can't have the same title, position and/or the same correct answer") 31 | } 32 | } 33 | } 34 | 35 | err := s.db.Create(&questOpt).Error 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return questOpt, nil 41 | } 42 | 43 | func (s *optService) UpdateQuestionOption(questOpt *models.QuestionOption) (*models.QuestionOption, error) { 44 | 45 | err := s.db.Save(&questOpt).Error 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return questOpt, nil 51 | 52 | } 53 | 54 | func (s *optService) DeleteQuestionOption(id string) error { 55 | 56 | err := s.db.Delete(id).Error 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (s *optService) DeleteQuestionOptionByQuestionID(questId string) error { 65 | 66 | err := s.db.Delete(questId).Error 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (s *optService) GetQuestionOptionByID(id string) (*models.QuestionOption, error) { 75 | 76 | quesOpt := &models.QuestionOption{} 77 | 78 | err := s.db.Where("id = ?", id).Take(&quesOpt).Error 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | return quesOpt, nil 84 | 85 | } 86 | 87 | func (s *optService) GetQuestionOptionByQuestionID(id string) ([]*models.QuestionOption, error) { 88 | 89 | var quesOpts []*models.QuestionOption 90 | 91 | err := s.db.Where("question_id = ?", id).Find(&quesOpts).Error 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return quesOpts, nil 97 | } 98 | -------------------------------------------------------------------------------- /app/infrastructure/persistence/answer_repository_test.go: -------------------------------------------------------------------------------- 1 | package persistence_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "multi-choice/app/infrastructure/persistence" 7 | "multi-choice/app/models" 8 | "testing" 9 | ) 10 | 11 | func TestCreateAnswer_Success(t *testing.T) { 12 | conn, err := DBConn() 13 | if err != nil { 14 | t.Fatalf("want non error, got %#v", err) 15 | } 16 | var ans = models.Answer{ 17 | QuestionID: "1", 18 | OptionID: "1", 19 | IsCorrect: false, 20 | } 21 | 22 | repo := persistence.NewAnswer(conn) 23 | 24 | a, saveErr := repo.CreateAnswer(&ans) 25 | 26 | assert.Nil(t, saveErr) 27 | assert.EqualValues(t, a.QuestionID, "1") 28 | assert.EqualValues(t, a.OptionID, "1") 29 | assert.EqualValues(t, a.IsCorrect, false) 30 | } 31 | 32 | func TestGetAnswerByID_Success(t *testing.T) { 33 | conn, err := DBConn() 34 | if err != nil { 35 | t.Fatalf("want non error, got %#v", err) 36 | } 37 | ans, err := seedAnswer(conn) 38 | if err != nil { 39 | t.Fatalf("want non error, got %#v", err) 40 | } 41 | 42 | repo := persistence.NewAnswer(conn) 43 | 44 | a, saveErr := repo.GetAnswerByID(ans.ID) 45 | 46 | assert.Nil(t, saveErr) 47 | assert.EqualValues(t, a.QuestionID, ans.QuestionID) 48 | assert.EqualValues(t, a.IsCorrect, ans.IsCorrect) 49 | assert.EqualValues(t, a.OptionID, ans.OptionID) 50 | } 51 | 52 | func TestGetAllAnswersForQuestion_Success(t *testing.T) { 53 | 54 | conn, err := DBConn() 55 | if err != nil { 56 | t.Fatalf("want non error, got %#v", err) 57 | } 58 | answers, err := seedAnswers(conn) 59 | if err != nil { 60 | t.Fatalf("want non error, got %#v", err) 61 | } 62 | 63 | fmt.Println(answers) 64 | 65 | repo := persistence.NewAnswer(conn) 66 | 67 | //since is the same question from the seeded, let pick the first iteration: 68 | //anss, getErr := repo.GetAllQuestionAnswers(answers[0].QuestionID) 69 | anss, getErr := repo.GetAllQuestionAnswers("1") 70 | 71 | assert.Nil(t, getErr) 72 | assert.EqualValues(t, len(anss), 2) 73 | } 74 | 75 | func TestUpdateAnswer_Success(t *testing.T) { 76 | 77 | conn, err := DBConn() 78 | if err != nil { 79 | t.Fatalf("want non error, got %#v", err) 80 | } 81 | ans, err := seedAnswer(conn) 82 | if err != nil { 83 | t.Fatalf("want non error, got %#v", err) 84 | } 85 | 86 | //updating 87 | ans.IsCorrect = false 88 | 89 | repo := persistence.NewAnswer(conn) 90 | a, updateErr := repo.UpdateAnswer(ans) 91 | 92 | assert.Nil(t, updateErr) 93 | assert.EqualValues(t, a.IsCorrect, ans.IsCorrect) 94 | } 95 | 96 | func TestDeleteAnswer_Success(t *testing.T) { 97 | conn, err := DBConn() 98 | if err != nil { 99 | t.Fatalf("want non error, got %#v", err) 100 | } 101 | ans, err := seedAnswer(conn) 102 | if err != nil { 103 | t.Fatalf("want non error, got %#v", err) 104 | } 105 | repo := persistence.NewAnswer(conn) 106 | 107 | deleteErr := repo.DeleteAnswer(ans.ID) 108 | 109 | assert.Nil(t, deleteErr) 110 | } 111 | -------------------------------------------------------------------------------- /app/interfaces/setup_test.go: -------------------------------------------------------------------------------- 1 | package interfaces_test 2 | 3 | import ( 4 | "multi-choice/app/models" 5 | ) 6 | 7 | //We need to mock the domain layer, so we can achieve unit test in the interfaces layer: 8 | 9 | //Question Domain Mocking 10 | var ( 11 | CreateQuestionFn func(*models.Question) (*models.Question, error) 12 | UpdateQuestionFn func(*models.Question) (*models.Question, error) 13 | DeleteQuestionFn func(string) error 14 | GetQuestionByIDFn func(string) (*models.Question, error) 15 | GetAllQuestionsFn func() ([]*models.Question, error) 16 | ) 17 | 18 | func (q *fakeQuestionService) CreateQuestion(question *models.Question) (*models.Question, error) { 19 | return CreateQuestionFn(question) 20 | } 21 | 22 | func (q *fakeQuestionService) UpdateQuestion(question *models.Question) (*models.Question, error) { 23 | return UpdateQuestionFn(question) 24 | } 25 | 26 | func (q *fakeQuestionService) DeleteQuestion(id string) error { 27 | return DeleteQuestionFn(id) 28 | } 29 | 30 | func (q *fakeQuestionService) GetQuestionByID(id string) (*models.Question, error) { 31 | return GetQuestionByIDFn(id) 32 | } 33 | 34 | func (q *fakeQuestionService) GetAllQuestions() ([]*models.Question, error) { 35 | return GetAllQuestionsFn() 36 | } 37 | 38 | //////////////////////////////////////// 39 | //QuestionOption Domain Mocking 40 | var ( 41 | CreateQuestionOptionFn func(option *models.QuestionOption) (*models.QuestionOption, error) 42 | UpdateQuestionOptionFn func(option *models.QuestionOption) (*models.QuestionOption, error) 43 | DeleteQuestionOptionFn func(string) error 44 | DeleteQuestionOptionByQuestionIDFn func(questionId string) error 45 | GetQuestionOptionByIDFn func(string) (*models.QuestionOption, error) 46 | GetQuestionOptionByQuestionID func(questionId string) ([]*models.QuestionOption, error) 47 | ) 48 | 49 | func (o *fakeQuestionOptionService) CreateQuestionOption(option *models.QuestionOption) (*models.QuestionOption, error) { 50 | return CreateQuestionOptionFn(option) 51 | } 52 | 53 | func (o *fakeQuestionOptionService) UpdateQuestionOption(option *models.QuestionOption) (*models.QuestionOption, error) { 54 | return UpdateQuestionOptionFn(option) 55 | } 56 | 57 | func (o *fakeQuestionOptionService) GetQuestionOptionByID(id string) (*models.QuestionOption, error) { 58 | return GetQuestionOptionByIDFn(id) 59 | } 60 | 61 | func (o *fakeQuestionOptionService) DeleteQuestionOption(id string) error { 62 | return DeleteQuestionOptionFn(id) 63 | } 64 | 65 | func (o *fakeQuestionOptionService) DeleteQuestionOptionByQuestionID(questionId string) error { 66 | return DeleteQuestionOptionByQuestionIDFn(questionId) 67 | } 68 | 69 | func (o *fakeQuestionOptionService) GetQuestionOptionByQuestionID(questionId string) ([]*models.QuestionOption, error) { 70 | return GetQuestionOptionByQuestionID(questionId) 71 | } 72 | 73 | //////////////////////////////////////// 74 | //QuestionOption Domain Mocking 75 | var ( 76 | CreateAnswerFn func(answer *models.Answer) (*models.Answer, error) 77 | UpdateAnswerFn func(answer *models.Answer) (*models.Answer, error) 78 | DeleteAnswerFn func(id string) error 79 | GetAnswerByIDFn func(id string) (*models.Answer, error) 80 | GetAllQuestionAnswersFn func(questionId string) ([]*models.Answer, error) 81 | ) 82 | 83 | func (a *fakeAnswerService) CreateAnswer(answer *models.Answer) (*models.Answer, error) { 84 | return CreateAnswerFn(answer) 85 | } 86 | 87 | func (a *fakeAnswerService) UpdateAnswer(answer *models.Answer) (*models.Answer, error) { 88 | return UpdateAnswerFn(answer) 89 | } 90 | 91 | func (a *fakeAnswerService) DeleteAnswer(id string) error { 92 | return DeleteAnswerFn(id) 93 | } 94 | 95 | func (a *fakeAnswerService) GetAnswerByID(id string) (*models.Answer, error) { 96 | return GetAnswerByIDFn(id) 97 | } 98 | 99 | func (a *fakeAnswerService) GetAllQuestionAnswers(questionId string) ([]*models.Answer, error) { 100 | return GetAllQuestionAnswersFn(questionId) 101 | } 102 | -------------------------------------------------------------------------------- /app/infrastructure/persistence/question_repository_test.go: -------------------------------------------------------------------------------- 1 | package persistence_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "multi-choice/app/infrastructure/persistence" 7 | "multi-choice/app/models" 8 | "testing" 9 | ) 10 | 11 | func TestCreateQuestion_Success(t *testing.T) { 12 | conn, err := DBConn() 13 | if err != nil { 14 | t.Fatalf("want non error, got %#v", err) 15 | } 16 | var ques = models.Question{} 17 | ques.Title = "Good question" 18 | 19 | repo := persistence.NewQuestion(conn) 20 | 21 | q, saveErr := repo.CreateQuestion(&ques) 22 | assert.Nil(t, saveErr) 23 | assert.EqualValues(t, q.Title, "Good question") 24 | } 25 | 26 | func TestCreateQuestion_Failure(t *testing.T) { 27 | conn, err := DBConn() 28 | if err != nil { 29 | t.Fatalf("want non error, got %#v", err) 30 | } 31 | //seed the question 32 | _, err = seedQuestion(conn) 33 | if err != nil { 34 | t.Fatalf("want non error, got %#v", err) 35 | } 36 | 37 | var ques = models.Question{} 38 | ques.Title = "First Question" 39 | 40 | repo := persistence.NewQuestion(conn) 41 | f, saveErr := repo.CreateQuestion(&ques) 42 | 43 | dbMsg := errors.New("question title already taken") 44 | 45 | assert.Nil(t, f) 46 | assert.EqualValues(t, dbMsg, saveErr) 47 | } 48 | 49 | func TestGetQuestionByID_Success(t *testing.T) { 50 | conn, err := DBConn() 51 | if err != nil { 52 | t.Fatalf("want non error, got %#v", err) 53 | } 54 | ques, err := seedQuestion(conn) 55 | if err != nil { 56 | t.Fatalf("want non error, got %#v", err) 57 | } 58 | repo := persistence.NewQuestion(conn) 59 | 60 | q, saveErr := repo.GetQuestionByID(ques.ID) 61 | 62 | assert.Nil(t, saveErr) 63 | assert.EqualValues(t, q.Title, ques.Title) 64 | } 65 | 66 | func TestGetAllQuestion_Success(t *testing.T) { 67 | conn, err := DBConn() 68 | if err != nil { 69 | t.Fatalf("want non error, got %#v", err) 70 | } 71 | _, err = seedQuestions(conn) 72 | if err != nil { 73 | t.Fatalf("want non error, got %#v", err) 74 | } 75 | repo := persistence.NewQuestion(conn) 76 | questions, getErr := repo.GetAllQuestions() 77 | 78 | assert.Nil(t, getErr) 79 | assert.EqualValues(t, len(questions), 2) 80 | } 81 | 82 | func TestUpdateQuestion_Success(t *testing.T) { 83 | conn, err := DBConn() 84 | if err != nil { 85 | t.Fatalf("want non error, got %#v", err) 86 | } 87 | ques, err := seedQuestion(conn) 88 | if err != nil { 89 | t.Fatalf("want non error, got %#v", err) 90 | } 91 | //updating 92 | ques.Title = "question title update" 93 | 94 | repo := persistence.NewQuestion(conn) 95 | q, updateErr := repo.UpdateQuestion(ques) 96 | 97 | assert.Nil(t, updateErr) 98 | //assert.EqualValues(t, q.ID, "1") 99 | assert.EqualValues(t, q.Title, "question title update") 100 | } 101 | 102 | //Duplicate title error 103 | func TestUpdateQuestion_Failure(t *testing.T) { 104 | conn, err := DBConn() 105 | if err != nil { 106 | t.Fatalf("want non error, got %#v", err) 107 | } 108 | questions, err := seedQuestions(conn) 109 | if err != nil { 110 | t.Fatalf("want non error, got %#v", err) 111 | } 112 | var secondQuestion models.Question 113 | 114 | //get the second question title 115 | for _, v := range questions { 116 | if v.ID == "1" { 117 | continue 118 | } 119 | secondQuestion = v 120 | } 121 | secondQuestion.Title = "First Question" //this title belongs to the first question already, so the question question cannot use it 122 | 123 | repo := persistence.NewQuestion(conn) 124 | q, updateErr := repo.UpdateQuestion(&secondQuestion) 125 | 126 | dbMsg := errors.New("question title already taken") 127 | 128 | assert.NotNil(t, updateErr) 129 | assert.Nil(t, q) 130 | assert.EqualValues(t, dbMsg, updateErr) 131 | } 132 | 133 | func TestDeleteQuestion_Success(t *testing.T) { 134 | conn, err := DBConn() 135 | if err != nil { 136 | t.Fatalf("want non error, got %#v", err) 137 | } 138 | ques, err := seedQuestion(conn) 139 | if err != nil { 140 | t.Fatalf("want non error, got %#v", err) 141 | } 142 | repo := persistence.NewQuestion(conn) 143 | 144 | deleteErr := repo.DeleteQuestion(ques.ID) 145 | 146 | assert.Nil(t, deleteErr) 147 | } 148 | -------------------------------------------------------------------------------- /app/infrastructure/persistence/setup_test.go: -------------------------------------------------------------------------------- 1 | package persistence_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jinzhu/gorm" 6 | _ "github.com/jinzhu/gorm/dialects/postgres" 7 | "github.com/joho/godotenv" 8 | "log" 9 | "multi-choice/app/models" 10 | "os" 11 | ) 12 | 13 | func DBConn() (*gorm.DB, error) { 14 | 15 | if _, err := os.Stat("./../../../.env"); !os.IsNotExist(err) { 16 | var err error 17 | err = godotenv.Load(os.ExpandEnv("./../../../.env")) 18 | if err != nil { 19 | log.Fatalf("Error getting env %v\n", err) 20 | } else { 21 | fmt.Println("we have the env") 22 | } 23 | return LocalDatabase() 24 | } 25 | return CIBuild() 26 | } 27 | 28 | //Circle CI DB 29 | func CIBuild() (*gorm.DB, error) { 30 | var err error 31 | DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", "127.0.0.1", "5432", "postgres", "multi-choice-test", "password") 32 | conn, err := gorm.Open("postgres", DBURL) 33 | if err != nil { 34 | log.Fatal("This is the error:", err) 35 | } 36 | return conn, nil 37 | } 38 | 39 | //Local DB 40 | func LocalDatabase() (*gorm.DB, error) { 41 | 42 | dbdriver := os.Getenv("TEST_DB_DRIVER") 43 | host := os.Getenv("TEST_DB_HOST") 44 | password := os.Getenv("TEST_DB_PASSWORD") 45 | user := os.Getenv("TEST_DB_USER") 46 | dbname := os.Getenv("TEST_DB_NAME") 47 | port := os.Getenv("TEST_DB_PORT") 48 | 49 | DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", host, port, user, dbname, password) 50 | conn, err := gorm.Open(dbdriver, DBURL) 51 | if err != nil { 52 | return nil, err 53 | } else { 54 | log.Println("CONNECTED TO: ", dbdriver) 55 | } 56 | 57 | err = conn.DropTableIfExists(&models.Question{}, &models.Answer{}, &models.QuestionOption{}).Error 58 | if err != nil { 59 | return nil, err 60 | } 61 | err = conn.Debug().AutoMigrate( 62 | models.Question{}, 63 | models.Answer{}, 64 | models.QuestionOption{}, 65 | ).Error 66 | if err != nil { 67 | return nil, err 68 | } 69 | return conn, nil 70 | } 71 | 72 | func seedQuestion(db *gorm.DB) (*models.Question, error) { 73 | question := &models.Question{ 74 | ID: "1", 75 | Title: "First Question", 76 | } 77 | err := db.Create(&question).Error 78 | if err != nil { 79 | return nil, err 80 | } 81 | return question, nil 82 | } 83 | 84 | func seedQuestions(db *gorm.DB) ([]models.Question, error) { 85 | questions := []models.Question{ 86 | { 87 | ID: "1", 88 | Title: "First Question", 89 | }, 90 | { 91 | ID: "2", 92 | Title: "Second Question", 93 | }, 94 | } 95 | for _, v := range questions { 96 | err := db.Create(&v).Error 97 | if err != nil { 98 | return nil, err 99 | } 100 | } 101 | return questions, nil 102 | } 103 | 104 | func seedQuestionOption(db *gorm.DB) (*models.QuestionOption, error) { 105 | quesOpt := &models.QuestionOption{ 106 | ID: "1", 107 | QuestionID: "1", 108 | Title: "Option 1", 109 | Position: 1, 110 | IsCorrect: false, 111 | } 112 | err := db.Create(&quesOpt).Error 113 | if err != nil { 114 | return nil, err 115 | } 116 | return quesOpt, nil 117 | } 118 | 119 | func seedQuestionOptions(db *gorm.DB) ([]models.QuestionOption, error) { 120 | quesOpts := []models.QuestionOption{ 121 | { 122 | ID: "1", 123 | QuestionID: "1", 124 | Title: "Option 1", 125 | Position: 1, 126 | IsCorrect: false, 127 | }, 128 | { 129 | ID: "2", 130 | QuestionID: "2", 131 | Title: "Option 2", 132 | Position: 2, 133 | IsCorrect: true, 134 | }, 135 | } 136 | for _, v := range quesOpts { 137 | err := db.Create(&v).Error 138 | if err != nil { 139 | return nil, err 140 | } 141 | } 142 | return quesOpts, nil 143 | } 144 | 145 | func seedAnswer(db *gorm.DB) (*models.Answer, error) { 146 | ans := &models.Answer{ 147 | QuestionID: "1", 148 | OptionID: "1", 149 | IsCorrect: true, 150 | } 151 | err := db.Create(&ans).Error 152 | if err != nil { 153 | return nil, err 154 | } 155 | return ans, nil 156 | } 157 | 158 | func seedAnswers(db *gorm.DB) ([]models.Answer, error) { 159 | answers := []models.Answer{ 160 | { 161 | QuestionID: "1", 162 | OptionID: "1", 163 | IsCorrect: false, 164 | }, 165 | { 166 | QuestionID: "1", 167 | OptionID: "2", 168 | IsCorrect: true, 169 | }, 170 | } 171 | for _, v := range answers { 172 | err := db.Create(&v).Error 173 | if err != nil { 174 | return nil, err 175 | } 176 | } 177 | 178 | return answers, nil 179 | } 180 | -------------------------------------------------------------------------------- /app/infrastructure/persistence/question_option_repository_test.go: -------------------------------------------------------------------------------- 1 | package persistence_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "multi-choice/app/infrastructure/persistence" 7 | "multi-choice/app/models" 8 | "testing" 9 | ) 10 | 11 | func TestCreateQuestionOption_Success(t *testing.T) { 12 | conn, err := DBConn() 13 | if err != nil { 14 | t.Fatalf("want non error, got %#v", err) 15 | } 16 | var quesOpt = models.QuestionOption{ 17 | QuestionID: "1", 18 | Title: "Option 1", 19 | Position: 1, 20 | IsCorrect: false, 21 | } 22 | 23 | repo := persistence.NewQuestionOption(conn) 24 | 25 | q, saveErr := repo.CreateQuestionOption(&quesOpt) 26 | 27 | assert.Nil(t, saveErr) 28 | assert.EqualValues(t, q.Title, "Option 1") 29 | assert.EqualValues(t, q.Position, 1) 30 | assert.EqualValues(t, q.IsCorrect, false) 31 | } 32 | 33 | //Cannot create a question question twice for the same question 34 | func TestCreateQuestionOption_Failure(t *testing.T) { 35 | conn, err := DBConn() 36 | if err != nil { 37 | t.Fatalf("want non error, got %#v", err) 38 | } 39 | //seed question options 40 | _, err = seedQuestionOptions(conn) 41 | if err != nil { 42 | t.Fatalf("want non error, got %#v", err) 43 | } 44 | 45 | var quesOpt = models.QuestionOption{ 46 | QuestionID: "1", 47 | Title: "Option 1", //this option has already been seeded 48 | Position: 3, 49 | IsCorrect: true, 50 | } 51 | 52 | repo := persistence.NewQuestionOption(conn) 53 | f, saveErr := repo.CreateQuestionOption(&quesOpt) 54 | 55 | dbMsg := errors.New("two question options can't have the same title, position and/or the same correct answer") 56 | 57 | assert.Nil(t, f) 58 | assert.EqualValues(t, dbMsg, saveErr) 59 | } 60 | 61 | func TestGetQuestionOptionByID_Success(t *testing.T) { 62 | conn, err := DBConn() 63 | if err != nil { 64 | t.Fatalf("want non error, got %#v", err) 65 | } 66 | quesOpt, err := seedQuestionOption(conn) 67 | if err != nil { 68 | t.Fatalf("want non error, got %#v", err) 69 | } 70 | 71 | repo := persistence.NewQuestionOption(conn) 72 | 73 | q, saveErr := repo.GetQuestionOptionByID(quesOpt.ID) 74 | 75 | assert.Nil(t, saveErr) 76 | assert.EqualValues(t, q.Title, quesOpt.Title) 77 | } 78 | 79 | func TestGetAllQuestionOption_Success(t *testing.T) { 80 | conn, err := DBConn() 81 | if err != nil { 82 | t.Fatalf("want non error, got %#v", err) 83 | } 84 | _, err = seedQuestions(conn) 85 | if err != nil { 86 | t.Fatalf("want non error, got %#v", err) 87 | } 88 | repo := persistence.NewQuestion(conn) 89 | quesOpts, getErr := repo.GetAllQuestions() 90 | 91 | assert.Nil(t, getErr) 92 | assert.EqualValues(t, len(quesOpts), 2) 93 | } 94 | 95 | func TestUpdateQuestionOption_Success(t *testing.T) { 96 | conn, err := DBConn() 97 | if err != nil { 98 | t.Fatalf("want non error, got %#v", err) 99 | } 100 | ques, err := seedQuestion(conn) 101 | if err != nil { 102 | t.Fatalf("want non error, got %#v", err) 103 | } 104 | //updating 105 | ques.Title = "question title update" 106 | 107 | repo := persistence.NewQuestion(conn) 108 | q, updateErr := repo.UpdateQuestion(ques) 109 | 110 | assert.Nil(t, updateErr) 111 | //assert.EqualValues(t, q.ID, "1") 112 | assert.EqualValues(t, q.Title, "question title update") 113 | } 114 | 115 | //Duplicate title error 116 | func TestUpdateQuestionOption_Failure(t *testing.T) { 117 | conn, err := DBConn() 118 | if err != nil { 119 | t.Fatalf("want non error, got %#v", err) 120 | } 121 | questions, err := seedQuestions(conn) 122 | if err != nil { 123 | t.Fatalf("want non error, got %#v", err) 124 | } 125 | var secondQuestion models.Question 126 | 127 | //get the second question title 128 | for _, v := range questions { 129 | if v.ID == "1" { 130 | continue 131 | } 132 | secondQuestion = v 133 | } 134 | secondQuestion.Title = "First Question" //this title belongs to the first question already, so the question question cannot use it 135 | 136 | repo := persistence.NewQuestion(conn) 137 | q, updateErr := repo.UpdateQuestion(&secondQuestion) 138 | 139 | dbMsg := errors.New("question title already taken") 140 | 141 | assert.NotNil(t, updateErr) 142 | assert.Nil(t, q) 143 | assert.EqualValues(t, dbMsg, updateErr) 144 | } 145 | 146 | func TestDeleteQuestionOption_Success(t *testing.T) { 147 | conn, err := DBConn() 148 | if err != nil { 149 | t.Fatalf("want non error, got %#v", err) 150 | } 151 | ques, err := seedQuestion(conn) 152 | if err != nil { 153 | t.Fatalf("want non error, got %#v", err) 154 | } 155 | repo := persistence.NewQuestion(conn) 156 | 157 | deleteErr := repo.DeleteQuestion(ques.ID) 158 | 159 | assert.Nil(t, deleteErr) 160 | } 161 | -------------------------------------------------------------------------------- /app/interfaces/answer.resolvers.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "multi-choice/app/models" 10 | "multi-choice/helpers" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | func (r *mutationResolver) CreateAnswer(ctx context.Context, questionID string, optionID string) (*models.AnswerResponse, error) { 16 | ans := &models.Answer{ 17 | QuestionID: questionID, 18 | OptionID: optionID, 19 | } 20 | 21 | if ok, errorString := helpers.ValidateInputs(*ans); !ok { 22 | return &models.AnswerResponse{ 23 | Message: errorString, 24 | Status: http.StatusUnprocessableEntity, 25 | }, nil 26 | } 27 | 28 | //check if the answer is correct: 29 | correctOpt, err := r.QuestionOptionService.GetQuestionOptionByID(optionID) 30 | if err != nil { 31 | return &models.AnswerResponse{ 32 | Message: "Error getting question option", 33 | Status: http.StatusInternalServerError, 34 | }, nil 35 | } 36 | 37 | if correctOpt.IsCorrect == true { 38 | ans.IsCorrect = true 39 | } else { 40 | ans.IsCorrect = false 41 | } 42 | 43 | ans.CreatedAt = time.Now() 44 | ans.UpdatedAt = time.Now() 45 | 46 | answer, err := r.AnsService.CreateAnswer(ans) 47 | if err != nil { 48 | log.Println("Answer creation error: ", err) 49 | return &models.AnswerResponse{ 50 | Message: "Error creating answer", 51 | Status: http.StatusInternalServerError, 52 | }, nil 53 | } 54 | 55 | return &models.AnswerResponse{ 56 | Message: "Successfully created answer", 57 | Status: http.StatusCreated, 58 | Data: answer, 59 | }, nil 60 | } 61 | 62 | func (r *mutationResolver) UpdateAnswer(ctx context.Context, id string, questionID string, optionID string) (*models.AnswerResponse, error) { 63 | ans, err := r.AnsService.GetAnswerByID(id) 64 | if err != nil { 65 | log.Println("Error getting the answer to update: ", err) 66 | return &models.AnswerResponse{ 67 | Message: "Error getting the answer", 68 | Status: http.StatusUnprocessableEntity, 69 | }, nil 70 | } 71 | 72 | ans.OptionID = optionID 73 | ans.QuestionID = questionID 74 | ans.UpdatedAt = time.Now() 75 | 76 | if ok, errorString := helpers.ValidateInputs(*ans); !ok { 77 | return &models.AnswerResponse{ 78 | Message: errorString, 79 | Status: http.StatusUnprocessableEntity, 80 | }, nil 81 | } 82 | 83 | //check if the answer is correct: 84 | correctOpt, err := r.AnsService.GetAnswerByID(optionID) 85 | if err != nil { 86 | return &models.AnswerResponse{ 87 | Message: "Error getting question option", 88 | Status: http.StatusInternalServerError, 89 | }, nil 90 | } 91 | 92 | if correctOpt.IsCorrect == true { 93 | ans.IsCorrect = true 94 | } else { 95 | ans.IsCorrect = false 96 | } 97 | 98 | answer, err := r.AnsService.UpdateAnswer(ans) 99 | if err != nil { 100 | log.Println("Answer updating error: ", err) 101 | return &models.AnswerResponse{ 102 | Message: "Error updating answer", 103 | Status: http.StatusInternalServerError, 104 | }, nil 105 | } 106 | 107 | return &models.AnswerResponse{ 108 | Message: "Successfully updated answer", 109 | Status: http.StatusOK, 110 | Data: answer, 111 | }, nil 112 | } 113 | 114 | func (r *mutationResolver) DeleteAnswer(ctx context.Context, id string) (*models.AnswerResponse, error) { 115 | err := r.AnsService.DeleteAnswer(id) 116 | if err != nil { 117 | return &models.AnswerResponse{ 118 | Message: "Something went wrong deleting the answer.", 119 | Status: http.StatusInternalServerError, 120 | }, nil 121 | } 122 | 123 | return &models.AnswerResponse{ 124 | Message: "Successfully deleted answer", 125 | Status: http.StatusOK, 126 | }, nil 127 | } 128 | 129 | func (r *queryResolver) GetOneAnswer(ctx context.Context, id string) (*models.AnswerResponse, error) { 130 | answer, err := r.AnsService.GetAnswerByID(id) 131 | if err != nil { 132 | log.Println("getting answer error: ", err) 133 | return &models.AnswerResponse{ 134 | Message: "Something went wrong getting the answer.", 135 | Status: http.StatusInternalServerError, 136 | }, nil 137 | } 138 | 139 | return &models.AnswerResponse{ 140 | Message: "Successfully retrieved answer", 141 | Status: http.StatusOK, 142 | Data: answer, 143 | }, nil 144 | } 145 | 146 | func (r *queryResolver) GetAllQuestionAnswers(ctx context.Context, questionID string) (*models.AnswerResponse, error) { 147 | answers, err := r.AnsService.GetAllQuestionAnswers(questionID) 148 | if err != nil { 149 | log.Println("getting all questions error: ", err) 150 | return &models.AnswerResponse{ 151 | Message: "Something went wrong getting all questions.", 152 | Status: http.StatusInternalServerError, 153 | }, nil 154 | } 155 | 156 | return &models.AnswerResponse{ 157 | Message: "Successfully retrieved all answers", 158 | Status: http.StatusOK, 159 | DataList: answers, 160 | }, nil 161 | } 162 | -------------------------------------------------------------------------------- /app/interfaces/answer.resolvers_test.go: -------------------------------------------------------------------------------- 1 | package interfaces_test 2 | 3 | import ( 4 | "github.com/99designs/gqlgen/client" 5 | "github.com/99designs/gqlgen/graphql/handler" 6 | "github.com/stretchr/testify/assert" 7 | "multi-choice/app/domain/repository/answer" 8 | "multi-choice/app/generated" 9 | "multi-choice/app/interfaces" 10 | "multi-choice/app/models" 11 | "testing" 12 | ) 13 | 14 | type fakeAnswerService struct{} 15 | 16 | var fakeAnswer answer.AnsService = &fakeAnswerService{} //this is where the real implementation is swap with our fake implementation 17 | 18 | func TestCreateAnswer_Success(t *testing.T) { 19 | 20 | var srv = client.New(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 21 | AnsService: fakeAnswer, //this is swap with the real interface 22 | QuestionOptionService: fakeQuestionOption, //this is swap with the real interface 23 | }}))) 24 | 25 | //We dont call the domain method, we swap it with this 26 | CreateAnswerFn = func(answer *models.Answer) (*models.Answer, error) { 27 | return &models.Answer{ 28 | ID: "1", 29 | QuestionID: "1", 30 | OptionID: "1", 31 | IsCorrect: false, 32 | }, nil 33 | } 34 | 35 | //We dont call the domain method, we swap it with this 36 | GetQuestionOptionByIDFn = func(id string) (*models.QuestionOption, error) { 37 | return &models.QuestionOption{ 38 | ID: "1", 39 | Title: "Option 1", 40 | IsCorrect: true, 41 | }, nil 42 | } 43 | 44 | var resp struct { 45 | CreateAnswer struct { 46 | Message string 47 | Status int 48 | Data models.Answer 49 | } 50 | } 51 | 52 | srv.MustPost(`mutation { CreateAnswer(questionId: "1", optionId: "1") { message, status, data { id questionId optionId isCorrect } }}`, &resp) 53 | 54 | assert.Equal(t, 201, resp.CreateAnswer.Status) 55 | assert.Equal(t, "Successfully created answer", resp.CreateAnswer.Message) 56 | assert.Equal(t, false, resp.CreateAnswer.Data.IsCorrect) 57 | assert.Equal(t, "1", resp.CreateAnswer.Data.ID) 58 | } 59 | 60 | func TestUpdateAnswer_Success(t *testing.T) { 61 | 62 | var srv = client.New(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 63 | AnsService: fakeAnswer, //this is swap with the real interface 64 | }}))) 65 | 66 | //We dont call the domain method, we swap it with this 67 | GetAnswerByIDFn = func(id string) (*models.Answer, error) { 68 | return &models.Answer{ 69 | ID: "1", 70 | QuestionID: "1", 71 | OptionID: "1", 72 | }, nil 73 | } 74 | 75 | //We dont call the domain method, we swap it with this 76 | UpdateAnswerFn = func(ans *models.Answer) (*models.Answer, error) { 77 | return &models.Answer{ 78 | ID: "1", 79 | QuestionID: "1", 80 | OptionID: "2", 81 | IsCorrect: false, 82 | }, nil 83 | } 84 | 85 | var resp struct { 86 | UpdateAnswer struct { 87 | Message string 88 | Status int 89 | Data models.Answer 90 | } 91 | } 92 | 93 | srv.MustPost(`mutation { UpdateAnswer(id: "1", questionId: "1", optionId: "2") { message, status, data { id questionId optionId isCorrect } }}`, &resp) 94 | 95 | assert.Equal(t, 200, resp.UpdateAnswer.Status) 96 | assert.Equal(t, "Successfully updated answer", resp.UpdateAnswer.Message) 97 | assert.Equal(t, "1", resp.UpdateAnswer.Data.ID) 98 | assert.Equal(t, false, resp.UpdateAnswer.Data.IsCorrect) 99 | } 100 | 101 | func TestDeleteAnswer_Success(t *testing.T) { 102 | 103 | var srv = client.New(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 104 | AnsService: fakeAnswer, //this is swap with the real interface 105 | }}))) 106 | 107 | //We dont call the domain method, we swap it with this 108 | DeleteAnswerFn = func(id string) error { 109 | return nil 110 | } 111 | 112 | var resp struct { 113 | DeleteAnswer struct { 114 | Message string 115 | Status int 116 | } 117 | } 118 | 119 | srv.MustPost(`mutation { DeleteAnswer(id: "1") { message, status }}`, &resp) 120 | 121 | assert.Equal(t, 200, resp.DeleteAnswer.Status) 122 | assert.Equal(t, "Successfully deleted answer", resp.DeleteAnswer.Message) 123 | } 124 | 125 | func TestGetOneAnswer_Success(t *testing.T) { 126 | 127 | var srv = client.New(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 128 | AnsService: fakeAnswer, //this is swap with the real interface 129 | }}))) 130 | 131 | //We dont call the domain method, we swap it with this 132 | GetAnswerByIDFn = func(id string) (*models.Answer, error) { 133 | return &models.Answer{ 134 | ID: "1", 135 | QuestionID: "1", 136 | OptionID: "1", 137 | IsCorrect: true, 138 | }, nil 139 | } 140 | 141 | var resp struct { 142 | GetOneAnswer struct { 143 | Message string 144 | Status int 145 | Data models.Answer 146 | } 147 | } 148 | 149 | srv.MustPost(`query { GetOneAnswer(id: "1") { 150 | message, status, data { id questionId optionId isCorrect } 151 | 152 | }}`, &resp) 153 | 154 | assert.Equal(t, 200, resp.GetOneAnswer.Status) 155 | assert.Equal(t, "Successfully retrieved answer", resp.GetOneAnswer.Message) 156 | assert.Equal(t, true, resp.GetOneAnswer.Data.IsCorrect) 157 | assert.Equal(t, "1", resp.GetOneAnswer.Data.ID) 158 | assert.Equal(t, "1", resp.GetOneAnswer.Data.QuestionID) 159 | assert.Equal(t, "1", resp.GetOneAnswer.Data.OptionID) 160 | } 161 | 162 | func TestGetAllQuestionAnswers_Success(t *testing.T) { 163 | 164 | var srv = client.New(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 165 | AnsService: fakeAnswer, //this is swap with the real interface 166 | }}))) 167 | 168 | //We dont call the domain method, we swap it with this 169 | GetAllQuestionAnswersFn = func(questionId string) ([]*models.Answer, error) { 170 | return []*models.Answer{ 171 | { 172 | ID: "1", 173 | QuestionID: "1", 174 | OptionID: "1", 175 | IsCorrect: true, 176 | }, 177 | { 178 | ID: "2", 179 | QuestionID: "1", 180 | OptionID: "2", 181 | IsCorrect: false, 182 | }, 183 | }, nil 184 | } 185 | 186 | var resp struct { 187 | GetAllQuestionAnswers struct { 188 | Message string 189 | Status int 190 | DataList []*models.Answer 191 | } 192 | } 193 | 194 | srv.MustPost(`query { GetAllQuestionAnswers(questionId: "1") { 195 | message, status, dataList { id questionId optionId isCorrect } 196 | }}`, &resp) 197 | 198 | assert.Equal(t, 200, resp.GetAllQuestionAnswers.Status) 199 | assert.Equal(t, "Successfully retrieved all answers", resp.GetAllQuestionAnswers.Message) 200 | assert.Equal(t, 2, len(resp.GetAllQuestionAnswers.DataList)) 201 | } 202 | -------------------------------------------------------------------------------- /app/interfaces/question.resolvers.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "multi-choice/app/generated" 11 | "multi-choice/app/models" 12 | "multi-choice/helpers" 13 | "net/http" 14 | "time" 15 | ) 16 | 17 | func (r *mutationResolver) CreateQuestion(ctx context.Context, question models.QuestionInput) (*models.QuestionResponse, error) { 18 | //validate the title: 19 | if question.Title == "" { 20 | return &models.QuestionResponse{ 21 | Message: "The title is required", 22 | Status: http.StatusBadRequest, 23 | }, nil 24 | } 25 | 26 | ques := &models.Question{ 27 | Title: question.Title, 28 | } 29 | 30 | ques.CreatedAt = time.Now() 31 | ques.UpdatedAt = time.Now() 32 | 33 | //save the question: 34 | quest, err := r.QuestionService.CreateQuestion(ques) 35 | if err != nil { 36 | fmt.Println("the error with this: ", err) 37 | return &models.QuestionResponse{ 38 | Message: err.Error(), 39 | Status: http.StatusInternalServerError, 40 | }, nil 41 | } 42 | 43 | //validate the question options: 44 | for _, v := range question.Options { 45 | 46 | if ok, errorString := helpers.ValidateInputs(*v); !ok { 47 | return &models.QuestionResponse{ 48 | Message: errorString, 49 | Status: http.StatusUnprocessableEntity, 50 | }, nil 51 | } 52 | 53 | quesOpt := &models.QuestionOption{ 54 | QuestionID: quest.ID, 55 | Title: v.Title, 56 | Position: v.Position, 57 | IsCorrect: v.IsCorrect, 58 | CreatedAt: time.Now(), 59 | UpdatedAt: time.Now(), 60 | } 61 | 62 | _, err := r.QuestionOptionService.CreateQuestionOption(quesOpt) 63 | if err != nil { 64 | return &models.QuestionResponse{ 65 | Message: "Error creating question option", 66 | Status: http.StatusInternalServerError, 67 | }, nil 68 | } 69 | } 70 | 71 | return &models.QuestionResponse{ 72 | Message: "Successfully created question", 73 | Status: http.StatusCreated, 74 | Data: quest, 75 | }, nil 76 | } 77 | 78 | func (r *mutationResolver) UpdateQuestion(ctx context.Context, id string, question models.QuestionInput) (*models.QuestionResponse, error) { 79 | //validate the title: 80 | if question.Title == "" { 81 | return &models.QuestionResponse{ 82 | Message: "The title is required", 83 | Status: http.StatusBadRequest, 84 | }, nil 85 | } 86 | 87 | //get the question: 88 | ques, err := r.QuestionService.GetQuestionByID(id) 89 | if err != nil { 90 | return &models.QuestionResponse{ 91 | Message: "Error getting the question", 92 | Status: http.StatusInternalServerError, 93 | }, nil 94 | } 95 | 96 | ques.Title = question.Title 97 | ques.UpdatedAt = time.Now() 98 | 99 | //save the question: 100 | quest, err := r.QuestionService.UpdateQuestion(ques) 101 | if err != nil { 102 | return &models.QuestionResponse{ 103 | Message: "Error creating question", 104 | Status: http.StatusInternalServerError, 105 | }, nil 106 | } 107 | 108 | //For the options, we will discard the previous options and insert new ones: 109 | err = r.QuestionOptionService.DeleteQuestionOptionByQuestionID(quest.ID) 110 | if err != nil { 111 | return &models.QuestionResponse{ 112 | Message: "Error Deleting question options", 113 | Status: http.StatusInternalServerError, 114 | }, nil 115 | } 116 | 117 | for _, v := range question.Options { 118 | 119 | if ok, errorString := helpers.ValidateInputs(*v); !ok { 120 | return &models.QuestionResponse{ 121 | Message: errorString, 122 | Status: http.StatusUnprocessableEntity, 123 | }, nil 124 | } 125 | 126 | quesOpt := &models.QuestionOption{ 127 | QuestionID: quest.ID, 128 | Title: v.Title, 129 | Position: v.Position, 130 | IsCorrect: v.IsCorrect, 131 | CreatedAt: time.Now(), 132 | UpdatedAt: time.Now(), 133 | } 134 | 135 | _, err := r.QuestionOptionService.CreateQuestionOption(quesOpt) 136 | if err != nil { 137 | return &models.QuestionResponse{ 138 | Message: "Error creating question options", 139 | Status: http.StatusInternalServerError, 140 | }, nil 141 | } 142 | } 143 | 144 | return &models.QuestionResponse{ 145 | Message: "Successfully updated question", 146 | Status: http.StatusOK, 147 | Data: quest, 148 | }, nil 149 | } 150 | 151 | func (r *mutationResolver) DeleteQuestion(ctx context.Context, id string) (*models.QuestionResponse, error) { 152 | err := r.QuestionService.DeleteQuestion(id) 153 | if err != nil { 154 | return &models.QuestionResponse{ 155 | Message: "Something went wrong deleting the question.", 156 | Status: http.StatusInternalServerError, 157 | }, nil 158 | } 159 | 160 | //also delete the options created too: 161 | err = r.QuestionOptionService.DeleteQuestionOptionByQuestionID(id) 162 | if err != nil { 163 | return &models.QuestionResponse{ 164 | Message: "Error Deleting question options", 165 | Status: http.StatusInternalServerError, 166 | }, nil 167 | } 168 | 169 | return &models.QuestionResponse{ 170 | Message: "Successfully deleted question", 171 | Status: http.StatusOK, 172 | }, nil 173 | } 174 | 175 | func (r *queryResolver) GetOneQuestion(ctx context.Context, id string) (*models.QuestionResponse, error) { 176 | question, err := r.QuestionService.GetQuestionByID(id) 177 | if err != nil { 178 | log.Println("getting question error: ", err) 179 | return &models.QuestionResponse{ 180 | Message: "Something went wrong getting the question.", 181 | Status: http.StatusInternalServerError, 182 | }, nil 183 | } 184 | 185 | return &models.QuestionResponse{ 186 | Message: "Successfully retrieved question", 187 | Status: http.StatusOK, 188 | Data: question, 189 | }, nil 190 | } 191 | 192 | func (r *queryResolver) GetAllQuestions(ctx context.Context) (*models.QuestionResponse, error) { 193 | questions, err := r.QuestionService.GetAllQuestions() 194 | if err != nil { 195 | log.Println("getting all questions error: ", err) 196 | return &models.QuestionResponse{ 197 | Message: "Something went wrong getting all questions.", 198 | Status: http.StatusInternalServerError, 199 | }, nil 200 | } 201 | 202 | return &models.QuestionResponse{ 203 | Message: "Successfully retrieved all questions", 204 | Status: http.StatusOK, 205 | DataList: questions, 206 | }, nil 207 | } 208 | 209 | // Mutation returns generated.MutationResolver implementation. 210 | func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } 211 | 212 | // Query returns generated.QueryResolver implementation. 213 | func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } 214 | 215 | type mutationResolver struct{ *Resolver } 216 | type queryResolver struct{ *Resolver } 217 | -------------------------------------------------------------------------------- /app/interfaces/question.resolvers_test.go: -------------------------------------------------------------------------------- 1 | package interfaces_test 2 | 3 | import ( 4 | "github.com/99designs/gqlgen/client" 5 | "github.com/99designs/gqlgen/graphql/handler" 6 | "github.com/stretchr/testify/assert" 7 | "multi-choice/app/domain/repository/question" 8 | "multi-choice/app/domain/repository/question_option" 9 | "multi-choice/app/generated" 10 | "multi-choice/app/interfaces" 11 | "multi-choice/app/models" 12 | "testing" 13 | ) 14 | 15 | type fakeQuestionService struct{} 16 | 17 | type fakeQuestionOptionService struct{} 18 | 19 | var fakeQuestion question.QuesService = &fakeQuestionService{} //this is where the real implementation is swap with our fake implementation 20 | 21 | var fakeQuestionOption question_option.OptService = &fakeQuestionOptionService{} //this is where the real implementation is swap with our fake implementation 22 | 23 | func TestCreateQuestion_Success(t *testing.T) { 24 | 25 | var srv = client.New(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 26 | QuestionService: fakeQuestion, //this is swap with the real interface 27 | QuestionOptionService: fakeQuestionOption, //this is swap with the real interface 28 | }}))) 29 | 30 | //We dont call the domain method, we swap it with this 31 | CreateQuestionFn = func(question *models.Question) (*models.Question, error) { 32 | return &models.Question{ 33 | ID: "1", 34 | Title: "Question title", 35 | }, nil 36 | } 37 | 38 | //also the mock on the question option: 39 | CreateQuestionOptionFn = func(question *models.QuestionOption) (*models.QuestionOption, error) { 40 | return &models.QuestionOption{ 41 | ID: "1", 42 | Title: "Option 1", 43 | Position: 1, 44 | IsCorrect: false, 45 | }, nil 46 | } 47 | 48 | var resp struct { 49 | CreateQuestion struct { 50 | Message string 51 | Status int 52 | Data models.Question 53 | } 54 | } 55 | 56 | srv.MustPost(`mutation { CreateQuestion(question:{title:"Question title", options: [{title: "Option 1", position: 1, isCorrect: false}]}) { message, status, data { id title } }}`, &resp) 57 | 58 | assert.Equal(t, 201, resp.CreateQuestion.Status) 59 | assert.Equal(t, "Successfully created question", resp.CreateQuestion.Message) 60 | assert.Equal(t, "Question title", resp.CreateQuestion.Data.Title) 61 | assert.Equal(t, "1", resp.CreateQuestion.Data.ID) 62 | } 63 | 64 | func TestUpdateQuestion_Success(t *testing.T) { 65 | 66 | var srv = client.New(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 67 | QuestionService: fakeQuestion, //this is swap with the real interface 68 | QuestionOptionService: fakeQuestionOption, //this is swap with the real interface 69 | }}))) 70 | 71 | //We dont call the domain method, we swap it with this 72 | GetQuestionByIDFn = func(id string) (*models.Question, error) { 73 | return &models.Question{ 74 | ID: "1", 75 | Title: "Question title", 76 | }, nil 77 | } 78 | 79 | //We dont call the domain method, we swap it with this 80 | DeleteQuestionOptionByQuestionIDFn = func(questionId string) error { 81 | return nil 82 | } 83 | 84 | //We dont call the domain method, we swap it with this 85 | UpdateQuestionFn = func(question *models.Question) (*models.Question, error) { 86 | return &models.Question{ 87 | ID: "1", 88 | Title: "Question title updated", 89 | }, nil 90 | } 91 | 92 | //also the mock on the question option: 93 | CreateQuestionOptionFn = func(question *models.QuestionOption) (*models.QuestionOption, error) { 94 | return &models.QuestionOption{ 95 | ID: "1", 96 | Title: "Option 1", 97 | Position: 1, 98 | IsCorrect: true, 99 | }, nil 100 | } 101 | 102 | var resp struct { 103 | UpdateQuestion struct { 104 | Message string 105 | Status int 106 | Data models.Question 107 | } 108 | } 109 | 110 | srv.MustPost(`mutation { UpdateQuestion(id: "1", question:{title:"Question title updated", options: [{title: "Option 1", position: 1, isCorrect: true}]}) { message, status, data { id title } }}`, &resp) 111 | 112 | assert.Equal(t, 200, resp.UpdateQuestion.Status) 113 | assert.Equal(t, "Successfully updated question", resp.UpdateQuestion.Message) 114 | assert.Equal(t, "Question title updated", resp.UpdateQuestion.Data.Title) 115 | assert.Equal(t, "1", resp.UpdateQuestion.Data.ID) 116 | } 117 | 118 | func TestDeleteQuestion_Success(t *testing.T) { 119 | 120 | var srv = client.New(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 121 | QuestionService: fakeQuestion, //this is swap with the real interface 122 | QuestionOptionService: fakeQuestionOption, //this is swap with the real interface 123 | }}))) 124 | 125 | //We dont call the domain method, we swap it with this 126 | DeleteQuestionFn = func(id string) error { 127 | return nil 128 | } 129 | 130 | //We dont call the domain method, we swap it with this 131 | DeleteQuestionOptionByQuestionIDFn = func(questionId string) error { 132 | return nil 133 | } 134 | 135 | var resp struct { 136 | DeleteQuestion struct { 137 | Message string 138 | Status int 139 | } 140 | } 141 | 142 | srv.MustPost(`mutation { DeleteQuestion(id: "1") { message, status }}`, &resp) 143 | 144 | assert.Equal(t, 200, resp.DeleteQuestion.Status) 145 | assert.Equal(t, "Successfully deleted question", resp.DeleteQuestion.Message) 146 | } 147 | 148 | func TestGetOneQuestion_Success(t *testing.T) { 149 | 150 | var srv = client.New(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 151 | QuestionService: fakeQuestion, //this is swap with the real interface 152 | }}))) 153 | 154 | //just populate only one option 155 | options := []*models.QuestionOption{ 156 | { 157 | ID: "1", 158 | Title: "Option 1", 159 | Position: 1, 160 | IsCorrect: false, 161 | }, 162 | } 163 | 164 | //We dont call the domain method, we swap it with this 165 | GetQuestionByIDFn = func(id string) (*models.Question, error) { 166 | return &models.Question{ 167 | ID: "1", 168 | Title: "Question title", 169 | QuestionOption: options, 170 | }, nil 171 | } 172 | 173 | var resp struct { 174 | GetOneQuestion struct { 175 | Message string 176 | Status int 177 | Data models.Question 178 | } 179 | } 180 | 181 | srv.MustPost(`query { GetOneQuestion(id: "1") { 182 | message, status, data { 183 | id title, questionOption { 184 | title 185 | position 186 | isCorrect 187 | } 188 | } 189 | 190 | }}`, &resp) 191 | 192 | assert.Equal(t, 200, resp.GetOneQuestion.Status) 193 | assert.Equal(t, "Successfully retrieved question", resp.GetOneQuestion.Message) 194 | assert.Equal(t, "Question title", resp.GetOneQuestion.Data.Title) 195 | assert.Equal(t, "1", resp.GetOneQuestion.Data.ID) 196 | assert.Equal(t, 1, len(resp.GetOneQuestion.Data.QuestionOption)) 197 | } 198 | 199 | func TestGetAllQuestions_Success(t *testing.T) { 200 | 201 | var srv = client.New(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &interfaces.Resolver{ 202 | QuestionService: fakeQuestion, //this is swap with the real interface 203 | }}))) 204 | 205 | //just populate only one option 206 | options := []*models.QuestionOption{ 207 | { 208 | ID: "1", 209 | Title: "Option 1", 210 | Position: 1, 211 | IsCorrect: false, 212 | }, 213 | { 214 | ID: "2", 215 | Title: "Option 2", 216 | Position: 2, 217 | IsCorrect: true, 218 | }, 219 | } 220 | 221 | //We dont call the domain method, we swap it with this 222 | GetAllQuestionsFn = func() ([]*models.Question, error) { 223 | return []*models.Question{ 224 | { 225 | ID: "1", 226 | Title: "Question title 1", 227 | QuestionOption: options, 228 | }, 229 | { 230 | ID: "2", 231 | Title: "Question title 2", 232 | QuestionOption: options, 233 | }, 234 | }, nil 235 | } 236 | 237 | var resp struct { 238 | GetAllQuestions struct { 239 | Message string 240 | Status int 241 | DataList []*models.Question 242 | } 243 | } 244 | 245 | srv.MustPost(`query { GetAllQuestions() { 246 | message, status, dataList { 247 | id title, questionOption { 248 | title 249 | position 250 | isCorrect 251 | } 252 | } 253 | 254 | }}`, &resp) 255 | 256 | assert.Equal(t, 200, resp.GetAllQuestions.Status) 257 | assert.Equal(t, "Successfully retrieved all questions", resp.GetAllQuestions.Message) 258 | assert.Equal(t, 2, len(resp.GetAllQuestions.DataList)) 259 | for _, v := range resp.GetAllQuestions.DataList { 260 | assert.Equal(t, 2, len(v.QuestionOption)) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/gqlgen v0.12.2 h1:aOdpsiCycFtCnAv8CAI1exnKrIDHMqtMzQoXeTziY4o= 2 | github.com/99designs/gqlgen v0.12.2/go.mod h1:7zdGo6ry9u1YBp/qlb2uxSU5Mt2jQKLcBETQiKk+Bxo= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 5 | github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= 6 | github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0= 7 | github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs= 8 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 9 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 10 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 11 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= 12 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 14 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 19 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 20 | github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM= 21 | github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 22 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 23 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 24 | github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 25 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 26 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 27 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 28 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 29 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 30 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 31 | github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 32 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 33 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 34 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 35 | github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 36 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 37 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 38 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 39 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 40 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= 41 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 42 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 43 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 44 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 45 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 46 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 47 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 48 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 49 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 50 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 51 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 52 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 53 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 54 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 55 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 56 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 57 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 58 | github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= 59 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 60 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 61 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 62 | github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= 63 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 64 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8= 65 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 66 | github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI= 67 | github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= 68 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= 69 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 70 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 71 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 72 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 73 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 74 | github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 75 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 76 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 77 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 78 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 79 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 80 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 81 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 82 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 83 | github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= 84 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 85 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 86 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 87 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 88 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 89 | github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk= 90 | github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= 91 | github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= 92 | github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 93 | github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= 94 | github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU= 95 | github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= 96 | github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o= 97 | github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 100 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 101 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= 102 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 103 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 104 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 105 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 106 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 107 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 108 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 109 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 110 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 111 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 117 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 118 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 119 | golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 120 | golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 121 | golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM= 122 | golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 123 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 124 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 125 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 126 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 127 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 128 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 129 | gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= 130 | gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 131 | gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= 132 | gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= 133 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 134 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 135 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 136 | sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= 137 | sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= 138 | --------------------------------------------------------------------------------