├── README.md ├── deploy └── dockerfile │ └── contact.Dockerfile ├── go.mod ├── go.sum ├── pkg ├── README.md ├── protobuff │ └── README.md ├── store │ └── postgres │ │ └── postrges.go ├── tools │ ├── converter │ │ └── string.go │ └── transaction │ │ └── transaction.go ├── tracing │ └── type.go └── type │ ├── columnCode │ └── type.go │ ├── context │ ├── type.go │ └── value.go │ ├── email │ └── type.go │ ├── gender │ └── type.go │ ├── logger │ ├── logger.go │ └── singelton.go │ ├── pagination │ └── type.go │ ├── phoneNumber │ ├── data.go │ └── type.go │ ├── query │ ├── parse.go │ └── type.go │ ├── queryParameter │ └── type.go │ └── sort │ └── type.go └── services ├── README.md ├── contact ├── README.md ├── cmd │ ├── README.md │ └── app │ │ └── main.go ├── configs │ └── README.md ├── internal │ ├── delivery │ │ ├── grpc │ │ │ ├── delivery.go │ │ │ ├── group.go │ │ │ └── interface │ │ │ │ ├── contact.pb.go │ │ │ │ └── contact_grpc.pb.go │ │ └── http │ │ │ ├── contact.go │ │ │ ├── contact │ │ │ ├── converter.go │ │ │ └── type.go │ │ │ ├── contactInGroup.go │ │ │ ├── delivery.go │ │ │ ├── error.go │ │ │ ├── group.go │ │ │ ├── group │ │ │ ├── converter.go │ │ │ └── type.go │ │ │ ├── middleware.go │ │ │ ├── router.go │ │ │ ├── swag.go │ │ │ └── swagger │ │ │ └── docs │ │ │ ├── docs.go │ │ │ ├── swagger.json │ │ │ └── swagger.yaml │ ├── domain │ │ ├── contact │ │ │ ├── age │ │ │ │ └── type.go │ │ │ ├── name │ │ │ │ └── type.go │ │ │ ├── patronymic │ │ │ │ └── type.go │ │ │ ├── surname │ │ │ │ └── type.go │ │ │ └── type.go │ │ └── group │ │ │ ├── description │ │ │ └── type.go │ │ │ ├── name │ │ │ └── type.go │ │ │ └── type.go │ ├── repository │ │ ├── contact │ │ │ ├── interface.go │ │ │ └── postgres │ │ │ │ ├── contact.go │ │ │ │ ├── convereter.go │ │ │ │ ├── dao │ │ │ │ └── contact.go │ │ │ │ └── repository.go │ │ ├── group │ │ │ ├── interface.go │ │ │ └── postgres │ │ │ │ ├── contactInGroup.go │ │ │ │ ├── convereter.go │ │ │ │ ├── dao │ │ │ │ └── group.go │ │ │ │ ├── group.go │ │ │ │ └── repository.go │ │ └── storage │ │ │ ├── mock │ │ │ ├── Contact.go │ │ │ ├── ContactInGroup.go │ │ │ ├── ContactReader.go │ │ │ ├── Group.go │ │ │ ├── GroupReader.go │ │ │ └── Storage.go │ │ │ └── postgres │ │ │ ├── contact.go │ │ │ ├── contactInGroup.go │ │ │ ├── convereter.go │ │ │ ├── dao │ │ │ ├── contact.go │ │ │ └── group.go │ │ │ ├── group.go │ │ │ ├── migrations │ │ │ └── 20220707173102_init_slurm.sql │ │ │ └── repository.go │ └── useCase │ │ ├── adapters │ │ └── storage │ │ │ ├── interface.go │ │ │ └── mock.sh │ │ ├── contact │ │ ├── contact.go │ │ ├── contect_test.go │ │ └── useCase.go │ │ ├── error.go │ │ ├── group │ │ ├── contactInGroup.go │ │ ├── group.go │ │ └── useCase.go │ │ └── interface.go ├── migrations │ ├── README.md │ └── postgres │ │ └── README.md ├── proto │ └── contact.proto ├── protogen.go └── scripts │ └── README.md └── user └── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Проект "architecture_golang" 2 | 3 | Проект "architecture_golang" на GitHub, представляет собой реализацию архитектуры "Чистая архитектура" на языке программирования Golang. 4 | В этом проекте использованы принципы и структура, представленные в репозитории "golang-standards/project-layout", чтобы обеспечить хорошую организацию кода. 5 | 6 | Основной идеей проекта является разделение приложения на четко определенные слои с явно выраженными зависимостями между ними. 7 | Это способствует облегчению поддержки и расширения приложения, а также повышает его чистоту и понимание. 8 | 9 | 1. Структура каталогов: 10 | - Проект использует структуру каталогов, определенную в репозитории "golang-standards/project-layout". Это помогает упорядочить код и логически разделить различные компоненты проекта. 11 | 2. Main-пакет: 12 | - В корневом каталоге проекта будет находиться main-файл, откуда начинается выполнение приложения. 13 | 3. Пакеты слоев: 14 | - В проекте реализованы различные пакеты, представляющие различные слои архитектуры Clean Architecture. Каждый слой имеет свою ответственность и взаимодействует только с ближайшими соседями. 15 | 4. Entity (Сущности): 16 | - В этом пакете определены основные сущности приложения, представляющие основные бизнес-объекты и правила. 17 | 5. Use Cases (Использование случаев): 18 | - Этот пакет содержит реализацию различных use cases (использование случаев), предоставляя основные бизнес-сценарии и операции над сущностями. 19 | 6. Repositories (Репозитории): 20 | - Здесь определены интерфейсы репозиториев, которые служат для взаимодействия с хранилищами данных. Реализация репозиториев находится в более низких уровнях, чтобы соблюдать принцип инверсии зависимостей. 21 | 7. Delivery (Доставка): 22 | - Этот пакет представляет собой слой доставки, который обрабатывает внешние запросы и преобразует их в операции use cases. Это могут быть HTTP-обработчики или другие механизмы взаимодействия с пользователем. 23 | 8. Frameworks & Drivers (Фреймворки и драйверы): 24 | - В этом каталоге находятся различные инструменты и библиотеки, используемые для взаимодействия с фреймворками и внешними системами. 25 | 9. Тесты: 26 | - В проекте также предусмотрены тесты, которые проверяют корректность работы компонентов приложения. 27 | 10. Документация: 28 | - Чтобы облегчить понимание проекта и его компонентов, проект содержит документацию, описывающую структуру, функциональность и примеры использования. 29 | 30 | Обратите внимание, что вся структура проекта и его компоненты разработаны с учетом принципов чистой архитектуры, что делает его легко расширяемым, модульным и поддерживаемым. 31 | 32 | Для разработки этого проекта использовал следующие ссылки: 33 | 34 | - Статья на Хабре от компании Олега Бунина, где рассматривается подход к построению Clean Architecture в Go (https://habr.com/ru/company/oleg-bunin/blog/516500/). 35 | - Проект go-clean-template от evrone, который также предоставляет примеры реализации Clean Architecture на Go (https://github.com/evrone/go-clean-template). 36 | - Проект go-clean-arch от bxcodec, который демонстрирует применение чистой архитектуры в Go (https://github.com/bxcodec/go-clean-arch). 37 | - Проект go-clean-architecture от zhashkevych, представляющий пример Clean Architecture на Go (https://github.com/zhashkevych/go-clean-architecture). 38 | - Проект golang-clean-architecture от theartofdevel, предоставляющий пример применения чистой архитектуры в Go (https://github.com/theartofdevel/golang-clean-architecture). 39 | - Проект wild-workouts-go-ddd-example от ThreeDotsLabs, демонстрирующий использование Domain-Driven Design (DDD) в Go (https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example). 40 | 41 | Кроме того, использовал видеоресурсы которые помогли составить архитектуру проекта: 42 | - Видео с YouTube о Clean Architecture in Golang (https://www.youtube.com/watch?v=PTE4VJIdHPg). 43 | - Видео с YouTube о Building Scalable Web Services with Go (https://www.youtube.com/watch?v=oL6JBUk6tj0). 44 | - Видео с YouTube о Go and Clean Architecture (https://www.youtube.com/watch?v=3gQa1LWwuzk). 45 | 46 | ## Обратите внимание, что приведенные ссылки и видео предоставляют полезные материалы для изучения и понимание чистой архитектуры на Golang. 47 | 48 | -------------------------------------------------------------------------------- /deploy/dockerfile/contact.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18.4-alpine3.15 as builder 2 | 3 | ENV GO111MODULE=on \ 4 | CGO_ENABLED=0 \ 5 | GOOS=linux \ 6 | GOARCH=amd64 7 | 8 | # Install dependencies 9 | RUN apk add --update curl 10 | 11 | WORKDIR /app 12 | 13 | COPY . . 14 | #COPY go.mod ./ 15 | #COPY go.sum ./ 16 | #RUN go mod download 17 | RUN go mod vendor 18 | RUN go build ./services/contact/cmd/app 19 | 20 | FROM scratch 21 | WORKDIR / 22 | COPY --from=builder /app/app . 23 | COPY --from=builder /app/services/contact/internal/repository/storage/postgres/migrations /services/contact/internal/repository/storage/postgres/migrations 24 | COPY --from=builder /app/services/contact/internal/delivery/http/swagger/docs /services/contact/internal/delivery/http/swagger/docs 25 | 26 | 27 | 28 | CMD [ "/app" ] 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module architecture_go 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/Masterminds/squirrel v1.5.3 7 | github.com/georgysavva/scany v1.0.0 8 | github.com/gin-contrib/zap v0.0.2 9 | github.com/gin-gonic/gin v1.8.1 10 | github.com/google/uuid v1.1.2 11 | github.com/jackc/pgx/v4 v4.16.1 12 | github.com/lib/pq v1.10.2 13 | github.com/opentracing/opentracing-go v1.2.0 14 | github.com/pkg/errors v0.9.1 15 | github.com/pressly/goose v2.7.0+incompatible 16 | github.com/spf13/viper v1.12.0 17 | github.com/stretchr/testify v1.8.0 18 | github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe 19 | github.com/swaggo/gin-swagger v1.5.1 20 | github.com/swaggo/swag v1.8.3 21 | github.com/uber/jaeger-client-go v2.30.0+incompatible 22 | go.uber.org/zap v1.19.1 23 | google.golang.org/grpc v1.46.2 24 | google.golang.org/protobuf v1.28.0 25 | ) 26 | 27 | require ( 28 | cloud.google.com/go v0.100.2 // indirect 29 | cloud.google.com/go/compute v1.6.1 // indirect 30 | cloud.google.com/go/firestore v1.6.1 // indirect 31 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect 32 | github.com/KyleBanks/depth v1.2.1 // indirect 33 | github.com/PuerkitoBio/purell v1.1.1 // indirect 34 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 35 | github.com/armon/go-metrics v0.3.10 // indirect 36 | github.com/coreos/go-semver v0.3.0 // indirect 37 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 38 | github.com/davecgh/go-spew v1.1.1 // indirect 39 | github.com/fatih/color v1.13.0 // indirect 40 | github.com/fsnotify/fsnotify v1.5.4 // indirect 41 | github.com/gin-contrib/sse v0.1.0 // indirect 42 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 43 | github.com/go-openapi/jsonreference v0.19.6 // indirect 44 | github.com/go-openapi/spec v0.20.4 // indirect 45 | github.com/go-openapi/swag v0.19.15 // indirect 46 | github.com/go-playground/locales v0.14.0 // indirect 47 | github.com/go-playground/universal-translator v0.18.0 // indirect 48 | github.com/go-playground/validator/v10 v10.10.0 // indirect 49 | github.com/goccy/go-json v0.9.7 // indirect 50 | github.com/gogo/protobuf v1.3.2 // indirect 51 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 52 | github.com/golang/protobuf v1.5.2 // indirect 53 | github.com/google/go-cmp v0.5.8 // indirect 54 | github.com/googleapis/gax-go/v2 v2.4.0 // indirect 55 | github.com/hashicorp/consul/api v1.12.0 // indirect 56 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 57 | github.com/hashicorp/go-hclog v1.2.0 // indirect 58 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 59 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 60 | github.com/hashicorp/golang-lru v0.5.4 // indirect 61 | github.com/hashicorp/hcl v1.0.0 // indirect 62 | github.com/hashicorp/serf v0.9.7 // indirect 63 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 64 | github.com/jackc/pgconn v1.12.1 // indirect 65 | github.com/jackc/pgio v1.0.0 // indirect 66 | github.com/jackc/pgpassfile v1.0.0 // indirect 67 | github.com/jackc/pgproto3/v2 v2.3.0 // indirect 68 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 69 | github.com/jackc/pgtype v1.11.0 // indirect 70 | github.com/jackc/puddle v1.2.1 // indirect 71 | github.com/josharian/intern v1.0.0 // indirect 72 | github.com/json-iterator/go v1.1.12 // indirect 73 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 74 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 75 | github.com/leodido/go-urn v1.2.1 // indirect 76 | github.com/magiconair/properties v1.8.6 // indirect 77 | github.com/mailru/easyjson v0.7.6 // indirect 78 | github.com/mattn/go-colorable v0.1.12 // indirect 79 | github.com/mattn/go-isatty v0.0.14 // indirect 80 | github.com/mitchellh/go-homedir v1.1.0 // indirect 81 | github.com/mitchellh/mapstructure v1.5.0 // indirect 82 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 83 | github.com/modern-go/reflect2 v1.0.2 // indirect 84 | github.com/pelletier/go-toml v1.9.5 // indirect 85 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 86 | github.com/pmezard/go-difflib v1.0.0 // indirect 87 | github.com/sagikazarmark/crypt v0.6.0 // indirect 88 | github.com/spf13/afero v1.8.2 // indirect 89 | github.com/spf13/cast v1.5.0 // indirect 90 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 91 | github.com/spf13/pflag v1.0.5 // indirect 92 | github.com/stretchr/objx v0.4.0 // indirect 93 | github.com/subosito/gotenv v1.3.0 // indirect 94 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 95 | github.com/ugorji/go/codec v1.2.7 // indirect 96 | go.etcd.io/etcd/api/v3 v3.5.4 // indirect 97 | go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect 98 | go.etcd.io/etcd/client/v2 v2.305.4 // indirect 99 | go.etcd.io/etcd/client/v3 v3.5.4 // indirect 100 | go.opencensus.io v0.23.0 // indirect 101 | go.uber.org/atomic v1.7.0 // indirect 102 | go.uber.org/multierr v1.6.0 // indirect 103 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect 104 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect 105 | golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect 106 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 107 | golang.org/x/text v0.3.7 // indirect 108 | golang.org/x/tools v0.1.10 // indirect 109 | golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect 110 | google.golang.org/api v0.81.0 // indirect 111 | google.golang.org/appengine v1.6.7 // indirect 112 | google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect 113 | gopkg.in/ini.v1 v1.66.4 // indirect 114 | gopkg.in/yaml.v2 v2.4.0 // indirect 115 | gopkg.in/yaml.v3 v3.0.1 // indirect 116 | ) 117 | -------------------------------------------------------------------------------- /pkg/README.md: -------------------------------------------------------------------------------- 1 | ### `/pkg` 2 | 3 | Код библиотек, пригодных для использования в сторонних приложениях. (например, `/pkg/mypubliclib`). Другие проекты будут импортировать эти библиотеки, ожидая их автономной работы, поэтому стоит подумать дважды, прежде чем класть сюда какой-нибудь код :-) Заметьте, что использование директории `internal` - более оптимальный способ не дать импортировать внутренние пакеты, потому что это обеспечит сам Golang. Директория `/pkg` - всё еще хороший путь дать понять, что код в этой директории могут безопасно использовать другие. Пост [`I'll take pkg over internal`](https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal/) в блоге Трэвиса Джеффери предоставляет хороший обзор директорий `pkg` и `internal` и когда есть смысл их использовать. 4 | 5 | Существует возможность группировать код на Golang в одном месте, когда ваша корневая директория содержит множество не относящихся к Go компонентов и директорий, что позволит облегчить работу с разными инструментами Go (как упомянуто в этих разговорах: [`Best Practices for Industrial Programming`](https://www.youtube.com/watch?v=PTE4VJIdHPg) с GopherCon EU 2018, [GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps](https://www.youtube.com/watch?v=oL6JBUk6tj0) и [GoLab 2018 - Massimiliano Pippi - Project layout patterns in Go](https://www.youtube.com/watch?v=3gQa1LWwuzk)). 6 | 7 | Ознакомьтесь с директорией [`/pkg`](pkg/README.md), если хотите увидеть, какие популярные репозитории используют такой шаблон организации проекта. Несмотря на его распространенность, он не был принят всеми, а некоторые в сообществе Go и вовсе не рекомендует его использовать. 8 | 9 | Вы можете не использовать эту директорию, если проект совсем небольшой и добавление нового уровня вложенности не имеет практического смысла (разве что вы сами этого не хотите :-)). Подумайте об этом, когда ваша корневая директория начинает слишком сильно разрастаться, особенно, если у вас много компонентов, написанных не на Go. -------------------------------------------------------------------------------- /pkg/protobuff/README.md: -------------------------------------------------------------------------------- 1 | ### Сгенерированные файлы `proto` -------------------------------------------------------------------------------- /pkg/store/postgres/postrges.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/jackc/pgx/v4/pgxpool" 11 | _ "github.com/lib/pq" 12 | "github.com/pkg/errors" 13 | _ "github.com/spf13/viper/remote" 14 | ) 15 | 16 | func init() { 17 | if err := initDefaultEnv(); err != nil { 18 | panic(err) 19 | } 20 | } 21 | 22 | // Env to pg params 23 | // "PGHOST": "host", 24 | // "PGPORT": "port", 25 | // "PGDATABASE": "database", 26 | // "PGUSER": "user", 27 | // "PGPASSWORD": "password", 28 | // "PGPASSFILE": "passfile", 29 | // "PGAPPNAME": "application_name", 30 | // "PGCONNECT_TIMEOUT": "connect_timeout", 31 | // "PGSSLMODE": "sslmode", 32 | // "PGSSLKEY": "sslkey", 33 | // "PGSSLCERT": "sslcert", 34 | // "PGSSLROOTCERT": "sslrootcert", 35 | // "PGTARGETSESSIONATTRS": "target_session_attrs", 36 | // "PGSERVICE": "service", 37 | // "PGSERVICEFILE": "servicefile", 38 | func initDefaultEnv() error { 39 | if len(os.Getenv("PGHOST")) == 0 { 40 | if err := os.Setenv("PGHOST", "postgres"); err != nil { 41 | return errors.WithStack(err) 42 | } 43 | } 44 | if len(os.Getenv("PGPORT")) == 0 { 45 | if err := os.Setenv("PGPORT", "5838"); err != nil { 46 | return errors.WithStack(err) 47 | } 48 | } 49 | if len(os.Getenv("PGDATABASE")) == 0 { 50 | if err := os.Setenv("PGDATABASE", "postgres"); err != nil { 51 | return errors.WithStack(err) 52 | } 53 | } 54 | if len(os.Getenv("PGUSER")) == 0 { 55 | if err := os.Setenv("PGUSER", "postgres"); err != nil { 56 | return errors.WithStack(err) 57 | } 58 | } 59 | if len(os.Getenv("PGPASSWORD")) == 0 { 60 | if err := os.Setenv("PGPASSWORD", "password"); err != nil { 61 | return errors.WithStack(err) 62 | } 63 | } 64 | if len(os.Getenv("PGSSLMODE")) == 0 { 65 | if err := os.Setenv("PGSSLMODE", "disable"); err != nil { 66 | return errors.WithStack(err) 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | type Store struct { 73 | Pool *pgxpool.Pool 74 | } 75 | 76 | type Settings struct { 77 | Host string 78 | Port uint16 79 | Database string 80 | User string 81 | Password string 82 | SSLMode string 83 | } 84 | 85 | func (s Settings) toDSN() string { 86 | var args []string 87 | 88 | if len(s.Host) > 0 { 89 | args = append(args, fmt.Sprintf("host=%s", s.Host)) 90 | } 91 | 92 | if s.Port > 0 { 93 | args = append(args, fmt.Sprintf("port=%d", s.Port)) 94 | } 95 | 96 | if len(s.Database) > 0 { 97 | args = append(args, fmt.Sprintf("dbname=%s", s.Database)) 98 | } 99 | 100 | if len(s.User) > 0 { 101 | args = append(args, fmt.Sprintf("user=%s", s.User)) 102 | } 103 | 104 | if len(s.Password) > 0 { 105 | args = append(args, fmt.Sprintf("password=%s", s.Password)) 106 | } 107 | 108 | if len(s.SSLMode) > 0 { 109 | args = append(args, fmt.Sprintf("sslmode=%s", s.SSLMode)) 110 | } 111 | 112 | return strings.Join(args, " ") 113 | } 114 | 115 | func New(settings Settings) (*Store, error) { 116 | 117 | config, err := pgxpool.ParseConfig(settings.toDSN()) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | conn, err := pgxpool.ConnectConfig(context.Background(), config) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 128 | defer cancel() 129 | if err = conn.Ping(ctx); err != nil { 130 | return nil, err 131 | } 132 | 133 | return &Store{Pool: conn}, nil 134 | } 135 | -------------------------------------------------------------------------------- /pkg/tools/converter/string.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | func StringToUUID(str string) uuid.UUID { 8 | if len(str) == 0 { 9 | return uuid.Nil 10 | } else { 11 | if value, err := uuid.Parse(str); err != nil { 12 | return uuid.Nil 13 | } else { 14 | return value 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/tools/transaction/transaction.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | "github.com/jackc/pgx/v4" 5 | 6 | "architecture_go/pkg/type/context" 7 | log "architecture_go/pkg/type/logger" 8 | ) 9 | 10 | func Finish(ctx context.Context, tx pgx.Tx, err error) error { 11 | if err != nil { 12 | if rollbackErr := tx.Rollback(ctx); rollbackErr != nil { 13 | return log.ErrorWithContext(ctx, rollbackErr) 14 | } 15 | return err 16 | } else { 17 | if commitErr := tx.Commit(ctx); commitErr != nil { 18 | return log.ErrorWithContext(ctx, commitErr) 19 | } 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/tracing/type.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | opentracing "github.com/opentracing/opentracing-go" 8 | "github.com/spf13/viper" 9 | "github.com/uber/jaeger-client-go" 10 | "github.com/uber/jaeger-client-go/config" 11 | 12 | "architecture_go/pkg/type/context" 13 | log "architecture_go/pkg/type/logger" 14 | ) 15 | 16 | func init() { 17 | viper.SetConfigName(".env") 18 | viper.SetConfigType("dotenv") 19 | viper.AddConfigPath(".") 20 | viper.AutomaticEnv() 21 | 22 | viper.SetDefault("JAEGER_AGENT_HOST", "127.0.0.1") 23 | viper.SetDefault("JAEGER_AGENT_PORT", 6831) 24 | } 25 | 26 | func New(ctx context.Context) (io.Closer, error) { 27 | 28 | cfg := &config.Configuration{ 29 | ServiceName: viper.GetString("SERVICE_NAME"), 30 | RPCMetrics: true, 31 | Sampler: &config.SamplerConfig{ 32 | Type: "const", 33 | Param: 1, 34 | }, 35 | Reporter: &config.ReporterConfig{ 36 | LogSpans: false, 37 | LocalAgentHostPort: fmt.Sprintf("%s:%d", viper.GetString("JAEGER_AGENT_HOST"), viper.GetUint32("JAEGER_AGENT_PORT")), 38 | }, 39 | } 40 | 41 | tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger)) 42 | if err != nil { 43 | return nil, log.ErrorWithContext(ctx, err) 44 | } 45 | 46 | opentracing.SetGlobalTracer(tracer) 47 | 48 | return closer, nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/type/columnCode/type.go: -------------------------------------------------------------------------------- 1 | package columnCode 2 | 3 | type ColumnCode string 4 | 5 | func New(str string) (ColumnCode, error) { 6 | return ColumnCode(str), nil 7 | } 8 | 9 | func (c ColumnCode) String() string { 10 | return string(c) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/type/context/type.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func init() { 13 | keyRequestID = "id" 14 | if value := os.Getenv("CONTEXT_KEY_REQUEST_ID"); len(value) > 0 { 15 | keyRequestID = value 16 | } 17 | } 18 | 19 | type Context interface { 20 | context.Context 21 | 22 | WithDeadline(d time.Time) 23 | CopyWithDeadline(d time.Time) Context 24 | 25 | WithTimeout(timeout time.Duration) 26 | CopyWithTimeout(timeout time.Duration) Context 27 | Cancel() 28 | 29 | Copy() Context 30 | 31 | Value 32 | } 33 | 34 | var ( 35 | keyRequestID string 36 | ) 37 | 38 | type local struct { 39 | base context.Context 40 | cancelFunc context.CancelFunc 41 | } 42 | 43 | func (l local) Deadline() (deadline time.Time, ok bool) { 44 | return l.base.Deadline() 45 | } 46 | 47 | func (l local) Done() <-chan struct{} { 48 | return l.base.Done() 49 | } 50 | 51 | func (l local) Err() error { 52 | return l.base.Err() 53 | } 54 | 55 | func (l *local) Cancel() { 56 | if l.cancelFunc != nil { 57 | l.cancelFunc() 58 | } 59 | } 60 | 61 | func (l local) Copy() Context { 62 | return &l 63 | } 64 | 65 | func (l *local) isEmptyID() bool { 66 | _, ok := l.id() 67 | return !ok 68 | } 69 | 70 | var cancelFunc = func() {} 71 | 72 | func Empty() Context { 73 | ctx := &local{ 74 | base: context.Background(), 75 | cancelFunc: cancelFunc, 76 | } 77 | 78 | ctx.WithValue(keyRequestID, uuid.New()) 79 | 80 | return ctx 81 | } 82 | 83 | func New(option interface{}) Context { 84 | ctx := &local{ 85 | base: context.Background(), 86 | cancelFunc: cancelFunc, 87 | } 88 | 89 | switch baseCtx := option.(type) { 90 | case gin.Context: 91 | ctx.base = baseCtx.Request.Context() 92 | for key, value := range baseCtx.Keys { 93 | ctx.WithValue(key, value) 94 | } 95 | case *gin.Context: 96 | ctx.base = baseCtx.Request.Context() 97 | for key, value := range baseCtx.Keys { 98 | ctx.WithValue(key, value) 99 | } 100 | case Context: 101 | ctx.withValue(keyRequestID, baseCtx.ID()) 102 | case context.Context: 103 | ctx.base = baseCtx 104 | } 105 | 106 | if ctx.isEmptyID() { 107 | ctx.withValue(keyRequestID, uuid.New().String()) 108 | } 109 | return ctx 110 | } 111 | -------------------------------------------------------------------------------- /pkg/type/context/value.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type Value interface { 9 | Value(key any) any 10 | WithValue(key, value any) 11 | 12 | ID() string 13 | } 14 | 15 | func (l *local) ID() string { 16 | id, _ := l.id() 17 | return id 18 | } 19 | 20 | func (l *local) id() (string, bool) { 21 | value := l.Value(keyRequestID) 22 | id, ok := value.(string) 23 | return id, ok 24 | } 25 | 26 | func (l *local) Value(key any) any { 27 | return l.base.Value(key) 28 | } 29 | 30 | func (l *local) WithValue(key, value any) { 31 | if key == keyRequestID { 32 | return // ignore 33 | } 34 | 35 | l.withValue(key, value) 36 | } 37 | 38 | func (l *local) withValue(key, value any) { 39 | l.base = context.WithValue(l.base, key, value) 40 | } 41 | 42 | func (l *local) WithTimeout(timeout time.Duration) { 43 | l.base, l.cancelFunc = context.WithTimeout(l.base, timeout) 44 | } 45 | 46 | func (l local) CopyWithTimeout(timeout time.Duration) Context { 47 | l.base, l.cancelFunc = context.WithTimeout(l.base, timeout) 48 | return &l 49 | } 50 | 51 | func (l *local) WithDeadline(d time.Time) { 52 | l.base, l.cancelFunc = context.WithDeadline(l.base, d) 53 | } 54 | 55 | func (l local) CopyWithDeadline(d time.Time) Context { 56 | l.base, l.cancelFunc = context.WithDeadline(l.base, d) 57 | return &l 58 | } 59 | -------------------------------------------------------------------------------- /pkg/type/email/type.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var regexpEmail = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) 10 | 11 | type Email struct { 12 | value string 13 | } 14 | 15 | func New(email string) (Email, error) { 16 | if len(email) == 0 { 17 | return Email{}, nil 18 | } 19 | 20 | if !regexpEmail.MatchString(strings.ToLower(email)) { 21 | return Email{}, errors.New("invalid email format") 22 | } 23 | 24 | return Email{value: email}, nil 25 | } 26 | 27 | func (e Email) Email() Email { 28 | return e 29 | } 30 | 31 | func (e Email) String() string { 32 | return e.value 33 | } 34 | 35 | func (e Email) Equal(email Email) bool { 36 | return e.value == email.value 37 | } 38 | 39 | func (e Email) IsEmpty() bool { 40 | return len(strings.TrimSpace(e.String())) == 0 41 | } 42 | 43 | func (e *Email) MarshalJSON() ([]byte, error) { 44 | return []byte(`"` + e.value + `"`), nil 45 | } 46 | 47 | func (e *Email) UnmarshalJSON(bin []byte) error { 48 | str := string(bin) 49 | if strings.HasPrefix(str, `"`) { 50 | str = str[1:] 51 | } 52 | if strings.HasSuffix(str, `"`) { 53 | str = str[:len(str)-1] 54 | } 55 | tmp, err := New(str) 56 | if err != nil { 57 | return err 58 | } 59 | e.value = tmp.value 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/type/gender/type.go: -------------------------------------------------------------------------------- 1 | package gender 2 | 3 | type Gender uint8 4 | 5 | func New(gender uint8) Gender { 6 | switch gender { 7 | case 1: 8 | return MALE 9 | case 2: 10 | return FEMALE 11 | default: 12 | return UNKNOWN 13 | } 14 | } 15 | 16 | const ( 17 | UNKNOWN Gender = 0 18 | MALE Gender = 1 19 | FEMALE Gender = 2 20 | ) 21 | 22 | func (g Gender) String() string { 23 | switch g { 24 | case 1: 25 | return "MALE" 26 | case 2: 27 | return "FEMALE" 28 | default: 29 | return "UNKNOWN" 30 | } 31 | } 32 | 33 | func (g Gender) Number() uint8 { 34 | return uint8(g) 35 | } 36 | 37 | func (g Gender) Equal(gender Gender) bool { 38 | return g == gender 39 | } 40 | 41 | func (g Gender) IsEmpty() bool { 42 | return g == UNKNOWN 43 | } 44 | 45 | func (g Gender) IsMale() bool { 46 | return g == MALE 47 | } 48 | 49 | func (g Gender) IsFemale() bool { 50 | return g == FEMALE 51 | } 52 | -------------------------------------------------------------------------------- /pkg/type/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zapcore" 10 | 11 | "architecture_go/pkg/type/context" 12 | ) 13 | 14 | type options struct { 15 | IsProduction bool 16 | Level zapcore.Level 17 | } 18 | 19 | type Logger struct { 20 | logger *zap.Logger 21 | } 22 | 23 | var opt *options 24 | 25 | func init() { 26 | 27 | opt = &options{IsProduction: false} 28 | if strings.ToLower(strings.TrimSpace(os.Getenv("IS_PRODUCTION"))) == "true" { 29 | opt.IsProduction = true 30 | } 31 | 32 | switch strings.ToUpper(strings.TrimSpace(os.Getenv("LOG_LEVEL"))) { 33 | case "ERR", "ERROR": 34 | opt.Level = zapcore.ErrorLevel 35 | case "WARN", "WARNING": 36 | opt.Level = zapcore.WarnLevel 37 | case "INFO": 38 | opt.Level = zapcore.InfoLevel 39 | case "DEBUG": 40 | opt.Level = zapcore.DebugLevel 41 | case "FATAL": 42 | opt.Level = zapcore.FatalLevel 43 | default: 44 | opt.Level = zapcore.InfoLevel 45 | } 46 | } 47 | 48 | func new() (*Logger, error) { 49 | var config zap.Config 50 | 51 | if opt.IsProduction { 52 | config = zap.NewProductionConfig() 53 | } else { 54 | config = zap.NewDevelopmentConfig() 55 | } 56 | 57 | config.Level = zap.NewAtomicLevelAt(opt.Level) 58 | 59 | newLogger, err := config.Build(zap.AddCallerSkip(2)) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | newLogger.Info("Set LOG_LEVEL", zap.Stringer("level", opt.Level)) 65 | 66 | log = &Logger{logger: newLogger} 67 | 68 | return log, nil 69 | } 70 | 71 | func New() (*Logger, error) { 72 | return new() 73 | } 74 | 75 | func (l *Logger) getContextFields(ctx context.Context) []zap.Field { 76 | return []zap.Field{zap.String("id", ctx.ID())} 77 | } 78 | 79 | func (l *Logger) Debug(msg string, fields ...zap.Field) { 80 | l.logger.Debug(msg, fields...) 81 | } 82 | 83 | func (l *Logger) DebugWithContext(ctx context.Context, msg string, fields ...zap.Field) { 84 | fields = append(fields, l.getContextFields(ctx)...) 85 | l.Debug(msg, fields...) 86 | } 87 | 88 | func (l *Logger) Info(msg string, fields ...zap.Field) { 89 | l.logger.Info(msg, fields...) 90 | } 91 | 92 | func (l *Logger) InfoWithContext(ctx context.Context, msg string, fields ...zap.Field) { 93 | fields = append(fields, l.getContextFields(ctx)...) 94 | l.Info(msg, fields...) 95 | } 96 | 97 | func (l *Logger) Warn(msg string, fields ...zap.Field) { 98 | l.logger.Warn(msg, fields...) 99 | } 100 | 101 | func (l *Logger) WarnWithContext(ctx context.Context, msg string, fields ...zap.Field) { 102 | fields = append(fields, l.getContextFields(ctx)...) 103 | l.Warn(msg, fields...) 104 | } 105 | 106 | func (l *Logger) Error(msg interface{}, fields ...zap.Field) { 107 | if msg == nil { 108 | return 109 | } 110 | 111 | switch v := msg.(type) { 112 | case string: 113 | l.logger.Error(v, fields...) 114 | case error: 115 | l.logger.Error(v.Error(), fields...) 116 | case fmt.Stringer: 117 | l.logger.Error(v.String(), fields...) 118 | default: 119 | l.logger.Error(fmt.Sprintf("%v", v), fields...) 120 | } 121 | } 122 | 123 | func (l *Logger) ErrorWithContext(ctx context.Context, err error, fields ...zap.Field) error { 124 | fields = append(fields, l.getContextFields(ctx)...) 125 | l.Error(err, fields...) 126 | return err 127 | } 128 | 129 | func (l *Logger) Fatal(msg interface{}, fields ...zap.Field) { 130 | if msg == nil { 131 | return 132 | } 133 | 134 | switch msg.(type) { 135 | case string: 136 | if v, ok := msg.(string); ok { 137 | l.logger.Fatal(v, fields...) 138 | } 139 | case error: 140 | if v, ok := msg.(error); ok { 141 | l.logger.Fatal(v.Error(), fields...) 142 | } 143 | case fmt.Stringer: 144 | if v, ok := msg.(fmt.Stringer); ok { 145 | l.logger.Fatal(v.String(), fields...) 146 | } 147 | default: 148 | l.logger.Fatal(fmt.Sprintf("%v", msg), fields...) 149 | } 150 | } 151 | 152 | func (l *Logger) FatalWithContext(ctx context.Context, err error, fields ...zap.Field) error { 153 | fields = append(fields, l.getContextFields(ctx)...) 154 | l.Fatal(err, fields...) 155 | return err 156 | } 157 | -------------------------------------------------------------------------------- /pkg/type/logger/singelton.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | 6 | "architecture_go/pkg/type/context" 7 | ) 8 | 9 | var log *Logger 10 | 11 | func init() { 12 | if log == nil { 13 | newLogger, err := new() 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | log = newLogger 19 | } 20 | } 21 | 22 | func GetLogger() *zap.Logger { 23 | return log.logger 24 | } 25 | 26 | func Debug(msg string, fields ...zap.Field) { 27 | log.Debug(msg, fields...) 28 | } 29 | 30 | func DebugWithContext(ctx context.Context, msg string, fields ...zap.Field) { 31 | log.DebugWithContext(ctx, msg, fields...) 32 | } 33 | 34 | func Info(msg string, fields ...zap.Field) { 35 | log.Info(msg, fields...) 36 | } 37 | 38 | func InfoWithContext(ctx context.Context, msg string, fields ...zap.Field) { 39 | log.InfoWithContext(ctx, msg, fields...) 40 | } 41 | 42 | func Warn(msg string, fields ...zap.Field) { 43 | log.Warn(msg, fields...) 44 | } 45 | 46 | func WarnWithContext(ctx context.Context, msg string, fields ...zap.Field) { 47 | log.WarnWithContext(ctx, msg, fields...) 48 | } 49 | 50 | func Error(msg interface{}, fields ...zap.Field) { 51 | log.Error(msg, fields...) 52 | } 53 | 54 | func ErrorWithContext(ctx context.Context, err error, fields ...zap.Field) error { 55 | return log.ErrorWithContext(ctx, err, fields...) 56 | } 57 | 58 | func Fatal(msg interface{}, fields ...zap.Field) { 59 | log.Fatal(msg, fields...) 60 | } 61 | 62 | func FatalWithContext(ctx context.Context, err error, fields ...zap.Field) error { 63 | return log.FatalWithContext(ctx, err, fields...) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/type/pagination/type.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | type Pagination struct { 4 | Limit uint64 5 | Offset uint64 6 | } 7 | -------------------------------------------------------------------------------- /pkg/type/phoneNumber/data.go: -------------------------------------------------------------------------------- 1 | package phoneNumber 2 | 3 | func getNumbers(input string) string { 4 | var number string 5 | 6 | for _, t := range input { 7 | if t >= 48 && t <= 57 { // 48 - 57 in ASCII this numbers 0-9 8 | number += string(t) 9 | } 10 | } 11 | 12 | return number 13 | } 14 | -------------------------------------------------------------------------------- /pkg/type/phoneNumber/type.go: -------------------------------------------------------------------------------- 1 | package phoneNumber 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type PhoneNumber struct { 8 | value string 9 | } 10 | 11 | func (p PhoneNumber) String() string { 12 | return p.value 13 | } 14 | 15 | func New(phone string) *PhoneNumber { 16 | return &PhoneNumber{ 17 | value: getNumbers(phone), 18 | } 19 | } 20 | 21 | func (p PhoneNumber) Equal(phoneNumber PhoneNumber) bool { 22 | return p.value == phoneNumber.value 23 | } 24 | 25 | func (p PhoneNumber) IsEmpty() bool { 26 | return len(strings.TrimSpace(p.value)) == 0 27 | } 28 | -------------------------------------------------------------------------------- /pkg/type/query/parse.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "architecture_go/pkg/type/columnCode" 8 | "architecture_go/pkg/type/sort" 9 | ) 10 | 11 | var ( 12 | fieldSeparationCharacter = "," 13 | sortTypeCharacters = []string{"-", "+"} 14 | 15 | defaultValueForLimit uint64 = 10 16 | maxValueForLimit uint64 = 100 17 | ) 18 | 19 | func parseSorts(strQuery string, options SortsOptions) (sort.Sorts, error) { 20 | var result = make(sort.Sorts, 0) 21 | if len(strQuery) == 0 { 22 | return result, nil 23 | } 24 | 25 | // Получаем массив из значений через запятую "-name,+full_name,phone" => ["-name", "+full_name", "phone"] 26 | for _, field := range strings.Split(strQuery, fieldSeparationCharacter) { 27 | 28 | if len(field) < 2 { 29 | continue 30 | } 31 | var name = field 32 | 33 | var direction = sort.DirectionAsc 34 | if strings.HasPrefix(field, sortTypeCharacters[1]) { 35 | direction = sort.DirectionAsc 36 | name = field[len(sortTypeCharacters[1]):] 37 | } 38 | 39 | if strings.HasPrefix(field, sortTypeCharacters[0]) { 40 | direction = sort.DirectionDesc 41 | name = field[len(sortTypeCharacters[0]):] 42 | } 43 | 44 | key, err := columnCode.New(name) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | if _, ok := options[key.String()]; !ok { 50 | continue 51 | } 52 | 53 | result = append(result, &sort.Sort{ 54 | Key: key, 55 | Direction: direction, 56 | }) 57 | } 58 | 59 | return result, nil 60 | 61 | } 62 | 63 | func parseLimit(strLimit string) uint64 { 64 | limit, err := strconv.ParseUint(strLimit, 10, 64) 65 | if err != nil || limit == 0 { 66 | return defaultValueForLimit 67 | } 68 | 69 | if limit > maxValueForLimit { 70 | return maxValueForLimit 71 | } 72 | 73 | return limit 74 | } 75 | 76 | func parseOffset(strOffset string) uint64 { 77 | offset, err := strconv.ParseUint(strOffset, 10, 64) 78 | if err != nil { 79 | return 0 80 | } 81 | 82 | return offset 83 | } 84 | -------------------------------------------------------------------------------- /pkg/type/query/type.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "architecture_go/pkg/type/sort" 7 | ) 8 | 9 | type Query struct { 10 | Sorts sort.Sorts 11 | Limit uint64 12 | Offset uint64 13 | } 14 | 15 | type SortOptions struct { 16 | } 17 | 18 | type Options struct { 19 | // Тут можно добавить фильтр 20 | Sorts SortsOptions 21 | } 22 | 23 | type SortsOptions map[string]SortOptions // map[front_key]FilterOptions 24 | 25 | var ( 26 | keyForSort = "sort" 27 | defaultKeyForSort = "" 28 | keyForLimit = "limit" 29 | keyForOffset = "offset" 30 | ) 31 | 32 | func ParseQuery(c *gin.Context, options Options) (*Query, error) { 33 | sorts, err := parseSorts(c.DefaultQuery(keyForSort, defaultKeyForSort), options.Sorts) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return &Query{ 39 | Sorts: sorts, 40 | Limit: parseLimit(c.Query(keyForLimit)), 41 | Offset: parseOffset(c.Query(keyForOffset)), 42 | }, nil 43 | } 44 | 45 | func ParseSorts(c *gin.Context, options SortsOptions) (sort.Sorts, error) { 46 | return parseSorts(c.DefaultQuery(keyForSort, defaultKeyForSort), options) 47 | } 48 | 49 | func ParseLimit(c *gin.Context) uint64 { 50 | return parseLimit(c.Query(keyForLimit)) 51 | } 52 | 53 | func ParseOffset(c *gin.Context) uint64 { 54 | return parseOffset(c.Query(keyForOffset)) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/type/queryParameter/type.go: -------------------------------------------------------------------------------- 1 | package queryParameter 2 | 3 | import ( 4 | "architecture_go/pkg/type/pagination" 5 | "architecture_go/pkg/type/sort" 6 | ) 7 | 8 | type QueryParameter struct { 9 | Sorts sort.Sorts 10 | Pagination pagination.Pagination 11 | /*Тут можно добавить фильтр*/ 12 | } 13 | -------------------------------------------------------------------------------- /pkg/type/sort/type.go: -------------------------------------------------------------------------------- 1 | package sort 2 | 3 | import "architecture_go/pkg/type/columnCode" 4 | 5 | type Sort struct { 6 | Key columnCode.ColumnCode 7 | Direction 8 | } 9 | 10 | type Direction string 11 | 12 | const ( 13 | DirectionAsc Direction = "ASC" 14 | DirectionDesc Direction = "DESC" 15 | ) 16 | 17 | func (d Direction) String() string { 18 | return string(d) 19 | } 20 | 21 | func (s Sort) Parsing(mapping map[columnCode.ColumnCode]string) string { 22 | column, ok := mapping[s.Key] 23 | if !ok { 24 | return "" 25 | } 26 | return column + " " + s.Direction.String() 27 | } 28 | 29 | type Sorts []*Sort 30 | 31 | func (s Sorts) Parsing(mapping map[columnCode.ColumnCode]string) []string { 32 | var result []string 33 | for _, sort := range s { 34 | 35 | result = append(result, sort.Parsing(mapping)) 36 | } 37 | return result 38 | } 39 | -------------------------------------------------------------------------------- /services/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGoodman08/architecture_golang/c16cec67bfda08f6edadf33f2988e20e817d78db/services/README.md -------------------------------------------------------------------------------- /services/contact/README.md: -------------------------------------------------------------------------------- 1 | ### Реализация конкретного сервиса + все данные для его запуска -------------------------------------------------------------------------------- /services/contact/cmd/README.md: -------------------------------------------------------------------------------- 1 | ### Место вызова функции `main` 2 | 3 | Не стоит располагать в этой директории большие объёмы кода. Если вы предполагает дальнейшее использование кода в других проектах, вам стоит хранить его в директории `/pkg` в корне проекта. Если же код не должен быть переиспользован где-то еще - ему самое место в директории `/internal`. 4 | 5 | Самой распространнёной практикой является использование маленькой `main` функции, которая импортирует и вызывает весь необходимый код из директорий `/internal` и `/pkg` и никаких других. -------------------------------------------------------------------------------- /services/contact/cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/spf13/viper" 10 | 11 | "architecture_go/pkg/store/postgres" 12 | "architecture_go/pkg/tracing" 13 | "architecture_go/pkg/type/context" 14 | log "architecture_go/pkg/type/logger" 15 | deliveryGrpc "architecture_go/services/contact/internal/delivery/grpc" 16 | deliveryHttp "architecture_go/services/contact/internal/delivery/http" 17 | // repositoryContact "architecture_go/services/contact/internal/repository/contact/postgres" 18 | // repositoryGroup "architecture_go/services/contact/internal/repository/group/postgres" 19 | repositoryStorage "architecture_go/services/contact/internal/repository/storage/postgres" 20 | useCaseContact "architecture_go/services/contact/internal/useCase/contact" 21 | useCaseGroup "architecture_go/services/contact/internal/useCase/group" 22 | ) 23 | 24 | func init() { 25 | viper.SetConfigName(".env") 26 | viper.SetConfigType("dotenv") 27 | viper.AddConfigPath(".") 28 | viper.AutomaticEnv() 29 | viper.SetDefault("SERVICE_NAME", "contactService") 30 | } 31 | 32 | func main() { 33 | conn, err := postgres.New(postgres.Settings{}) 34 | if err != nil { 35 | panic(err) 36 | } 37 | defer conn.Pool.Close() 38 | 39 | closer, err := tracing.New(context.Empty()) 40 | if err != nil { 41 | panic(err) 42 | } 43 | defer func() { 44 | if err = closer.Close(); err != nil { 45 | log.Error(err) 46 | } 47 | }() 48 | 49 | // repoContact, err:= repositoryContact.New(conn.Pool, repositoryContact.Options{}) 50 | // if err != nil { 51 | // panic(err) 52 | // } 53 | // repoGroup, err:= repositoryGroup.New(conn.Pool, repoContact, repositoryGroup.Options{}) 54 | // if err != nil { 55 | // panic(err) 56 | // } 57 | 58 | repoStorage, err := repositoryStorage.New(conn.Pool, repositoryStorage.Options{}) 59 | if err != nil { 60 | panic(err) 61 | } 62 | var ( 63 | ucContact = useCaseContact.New(repoStorage, useCaseContact.Options{}) 64 | // ucGroup = useCaseGroup.New(repoGroup, useCaseGroup.Options{}) 65 | ucGroup = useCaseGroup.New(repoStorage, useCaseGroup.Options{}) 66 | _ = deliveryGrpc.New(ucContact, ucGroup, deliveryGrpc.Options{}) 67 | listenerHttp = deliveryHttp.New(ucContact, ucGroup, deliveryHttp.Options{}) 68 | ) 69 | 70 | go func() { 71 | fmt.Printf("service started successfully on http port: %d", viper.GetUint("HTTP_PORT")) 72 | if err = listenerHttp.Run(); err != nil { 73 | panic(err) 74 | } 75 | }() 76 | 77 | signalCh := make(chan os.Signal, 1) 78 | signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) 79 | <-signalCh 80 | 81 | } 82 | -------------------------------------------------------------------------------- /services/contact/configs/README.md: -------------------------------------------------------------------------------- 1 | ### Шаблоны файлов конфигураций и файлы настроек по-умолчанию. 2 | 3 | Положите файлы конфигураций confd или consul-template сюда. -------------------------------------------------------------------------------- /services/contact/internal/delivery/grpc/delivery.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | contact "architecture_go/services/contact/internal/delivery/grpc/interface" 5 | "architecture_go/services/contact/internal/useCase" 6 | ) 7 | 8 | type Delivery struct { 9 | contact.UnimplementedContactServiceServer 10 | ucContact useCase.Contact 11 | ucGroup useCase.Group 12 | 13 | options Options 14 | } 15 | 16 | type Options struct{} 17 | 18 | func New(ucContact useCase.Contact, ucGroup useCase.Group, o Options) *Delivery { 19 | var d = &Delivery{ 20 | ucContact: ucContact, 21 | ucGroup: ucGroup, 22 | } 23 | 24 | d.SetOptions(o) 25 | return d 26 | } 27 | 28 | func (d *Delivery) SetOptions(options Options) { 29 | if d.options != options { 30 | d.options = options 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/grpc/group.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | contact "architecture_go/services/contact/internal/delivery/grpc/interface" 7 | ) 8 | 9 | func (d *Delivery) CreateGroup(ctx context.Context, request *contact.CreateGroupRequest) (*contact.CreateGroupResponse, error) { 10 | panic("implement me") 11 | } 12 | 13 | func (d *Delivery) UpdateGroup(ctx context.Context, request *contact.UpdateGroupRequest) (*contact.UpdateGroupResponse, error) { 14 | panic("implement me") 15 | } 16 | 17 | func (d *Delivery) DeleteGroup(ctx context.Context, request *contact.DeleteGroupRequest) (*contact.DeleteGroupResponse, error) { 18 | panic("implement me") 19 | } 20 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/grpc/interface/contact_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v3.21.1 5 | // source: contact.proto 6 | 7 | package contact 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // ContactServiceClient is the client API for ContactService service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type ContactServiceClient interface { 25 | CreateGroup(ctx context.Context, in *CreateGroupRequest, opts ...grpc.CallOption) (*CreateGroupResponse, error) 26 | UpdateGroup(ctx context.Context, in *UpdateGroupRequest, opts ...grpc.CallOption) (*UpdateGroupResponse, error) 27 | DeleteGroup(ctx context.Context, in *DeleteGroupRequest, opts ...grpc.CallOption) (*DeleteGroupResponse, error) 28 | } 29 | 30 | type contactServiceClient struct { 31 | cc grpc.ClientConnInterface 32 | } 33 | 34 | func NewContactServiceClient(cc grpc.ClientConnInterface) ContactServiceClient { 35 | return &contactServiceClient{cc} 36 | } 37 | 38 | func (c *contactServiceClient) CreateGroup(ctx context.Context, in *CreateGroupRequest, opts ...grpc.CallOption) (*CreateGroupResponse, error) { 39 | out := new(CreateGroupResponse) 40 | err := c.cc.Invoke(ctx, "/contact.ContactService/CreateGroup", in, out, opts...) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return out, nil 45 | } 46 | 47 | func (c *contactServiceClient) UpdateGroup(ctx context.Context, in *UpdateGroupRequest, opts ...grpc.CallOption) (*UpdateGroupResponse, error) { 48 | out := new(UpdateGroupResponse) 49 | err := c.cc.Invoke(ctx, "/contact.ContactService/UpdateGroup", in, out, opts...) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return out, nil 54 | } 55 | 56 | func (c *contactServiceClient) DeleteGroup(ctx context.Context, in *DeleteGroupRequest, opts ...grpc.CallOption) (*DeleteGroupResponse, error) { 57 | out := new(DeleteGroupResponse) 58 | err := c.cc.Invoke(ctx, "/contact.ContactService/DeleteGroup", in, out, opts...) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return out, nil 63 | } 64 | 65 | // ContactServiceServer is the server API for ContactService service. 66 | // All implementations must embed UnimplementedContactServiceServer 67 | // for forward compatibility 68 | type ContactServiceServer interface { 69 | CreateGroup(context.Context, *CreateGroupRequest) (*CreateGroupResponse, error) 70 | UpdateGroup(context.Context, *UpdateGroupRequest) (*UpdateGroupResponse, error) 71 | DeleteGroup(context.Context, *DeleteGroupRequest) (*DeleteGroupResponse, error) 72 | mustEmbedUnimplementedContactServiceServer() 73 | } 74 | 75 | // UnimplementedContactServiceServer must be embedded to have forward compatible implementations. 76 | type UnimplementedContactServiceServer struct { 77 | } 78 | 79 | func (UnimplementedContactServiceServer) CreateGroup(context.Context, *CreateGroupRequest) (*CreateGroupResponse, error) { 80 | return nil, status.Errorf(codes.Unimplemented, "method CreateGroup not implemented") 81 | } 82 | func (UnimplementedContactServiceServer) UpdateGroup(context.Context, *UpdateGroupRequest) (*UpdateGroupResponse, error) { 83 | return nil, status.Errorf(codes.Unimplemented, "method UpdateGroup not implemented") 84 | } 85 | func (UnimplementedContactServiceServer) DeleteGroup(context.Context, *DeleteGroupRequest) (*DeleteGroupResponse, error) { 86 | return nil, status.Errorf(codes.Unimplemented, "method DeleteGroup not implemented") 87 | } 88 | func (UnimplementedContactServiceServer) mustEmbedUnimplementedContactServiceServer() {} 89 | 90 | // UnsafeContactServiceServer may be embedded to opt out of forward compatibility for this service. 91 | // Use of this interface is not recommended, as added methods to ContactServiceServer will 92 | // result in compilation errors. 93 | type UnsafeContactServiceServer interface { 94 | mustEmbedUnimplementedContactServiceServer() 95 | } 96 | 97 | func RegisterContactServiceServer(s grpc.ServiceRegistrar, srv ContactServiceServer) { 98 | s.RegisterService(&ContactService_ServiceDesc, srv) 99 | } 100 | 101 | func _ContactService_CreateGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 102 | in := new(CreateGroupRequest) 103 | if err := dec(in); err != nil { 104 | return nil, err 105 | } 106 | if interceptor == nil { 107 | return srv.(ContactServiceServer).CreateGroup(ctx, in) 108 | } 109 | info := &grpc.UnaryServerInfo{ 110 | Server: srv, 111 | FullMethod: "/contact.ContactService/CreateGroup", 112 | } 113 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 114 | return srv.(ContactServiceServer).CreateGroup(ctx, req.(*CreateGroupRequest)) 115 | } 116 | return interceptor(ctx, in, info, handler) 117 | } 118 | 119 | func _ContactService_UpdateGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 120 | in := new(UpdateGroupRequest) 121 | if err := dec(in); err != nil { 122 | return nil, err 123 | } 124 | if interceptor == nil { 125 | return srv.(ContactServiceServer).UpdateGroup(ctx, in) 126 | } 127 | info := &grpc.UnaryServerInfo{ 128 | Server: srv, 129 | FullMethod: "/contact.ContactService/UpdateGroup", 130 | } 131 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 132 | return srv.(ContactServiceServer).UpdateGroup(ctx, req.(*UpdateGroupRequest)) 133 | } 134 | return interceptor(ctx, in, info, handler) 135 | } 136 | 137 | func _ContactService_DeleteGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 138 | in := new(DeleteGroupRequest) 139 | if err := dec(in); err != nil { 140 | return nil, err 141 | } 142 | if interceptor == nil { 143 | return srv.(ContactServiceServer).DeleteGroup(ctx, in) 144 | } 145 | info := &grpc.UnaryServerInfo{ 146 | Server: srv, 147 | FullMethod: "/contact.ContactService/DeleteGroup", 148 | } 149 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 150 | return srv.(ContactServiceServer).DeleteGroup(ctx, req.(*DeleteGroupRequest)) 151 | } 152 | return interceptor(ctx, in, info, handler) 153 | } 154 | 155 | // ContactService_ServiceDesc is the grpc.ServiceDesc for ContactService service. 156 | // It's only intended for direct use with grpc.RegisterService, 157 | // and not to be introspected or modified (even as a copy) 158 | var ContactService_ServiceDesc = grpc.ServiceDesc{ 159 | ServiceName: "contact.ContactService", 160 | HandlerType: (*ContactServiceServer)(nil), 161 | Methods: []grpc.MethodDesc{ 162 | { 163 | MethodName: "CreateGroup", 164 | Handler: _ContactService_CreateGroup_Handler, 165 | }, 166 | { 167 | MethodName: "UpdateGroup", 168 | Handler: _ContactService_UpdateGroup_Handler, 169 | }, 170 | { 171 | MethodName: "DeleteGroup", 172 | Handler: _ContactService_DeleteGroup_Handler, 173 | }, 174 | }, 175 | Streams: []grpc.StreamDesc{}, 176 | Metadata: "contact.proto", 177 | } 178 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/contact.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | "go.uber.org/zap" 10 | 11 | "architecture_go/pkg/tools/converter" 12 | "architecture_go/pkg/type/context" 13 | "architecture_go/pkg/type/logger" 14 | "architecture_go/pkg/type/pagination" 15 | "architecture_go/pkg/type/phoneNumber" 16 | "architecture_go/pkg/type/query" 17 | "architecture_go/pkg/type/queryParameter" 18 | jsonContact "architecture_go/services/contact/internal/delivery/http/contact" 19 | domainContact "architecture_go/services/contact/internal/domain/contact" 20 | "architecture_go/services/contact/internal/domain/contact/age" 21 | "architecture_go/services/contact/internal/domain/contact/name" 22 | "architecture_go/services/contact/internal/domain/contact/patronymic" 23 | "architecture_go/services/contact/internal/domain/contact/surname" 24 | "architecture_go/services/contact/internal/useCase" 25 | ) 26 | 27 | var mappingSortsContact = query.SortsOptions{ 28 | "name": {}, 29 | "surname": {}, 30 | "patronymic": {}, 31 | "phoneNumber": {}, 32 | "email": {}, 33 | "gender": {}, 34 | "age": {}, 35 | } 36 | 37 | // CreateContact 38 | // @Summary Метод позволяет создать контакт. 39 | // @Description Метод позволяет создать контакт. 40 | // @Tags contacts 41 | // @Accept json 42 | // @Produce json 43 | // @Param contact body jsonContact.ShortContact true "Данные по контакту" 44 | // @Success 201 {object} jsonContact.ContactResponse true "Структура контакта" 45 | // @Success 200 46 | // @Failure 400 {object} ErrorResponse 47 | // @Failure 403 "Forbidden" 48 | // @Failure 404 {object} ErrorResponse "404 Not Found" 49 | // @Router /contacts/ [post] 50 | func (d *Delivery) CreateContact(c *gin.Context) { 51 | 52 | var ctx = context.New(c) 53 | 54 | contact := jsonContact.ShortContact{} 55 | if err := c.ShouldBindJSON(&contact); err != nil { 56 | SetError(c, http.StatusBadRequest, err) 57 | return 58 | } 59 | 60 | contactAge, err := age.New(contact.Age) 61 | if err != nil { 62 | SetError(c, http.StatusBadRequest, err) 63 | return 64 | } 65 | 66 | contactName, err := name.New(contact.Name) 67 | if err != nil { 68 | SetError(c, http.StatusBadRequest, err) 69 | return 70 | } 71 | 72 | contactSurname, err := surname.New(contact.Surname) 73 | if err != nil { 74 | SetError(c, http.StatusBadRequest, err) 75 | return 76 | } 77 | 78 | contactPatronymic, err := patronymic.New(contact.Patronymic) 79 | if err != nil { 80 | SetError(c, http.StatusBadRequest, err) 81 | return 82 | } 83 | 84 | dContact, err := domainContact.New( 85 | *phoneNumber.New(contact.PhoneNumber), 86 | contact.Email, 87 | *contactName, 88 | *contactSurname, 89 | *contactPatronymic, 90 | *contactAge, 91 | contact.Gender, 92 | ) 93 | if err != nil { 94 | SetError(c, http.StatusBadRequest, err) 95 | return 96 | } 97 | 98 | response, err := d.ucContact.Create(ctx, dContact) 99 | if err != nil { 100 | 101 | SetError(c, http.StatusInternalServerError, err) 102 | return 103 | } 104 | logger.InfoWithContext(ctx, "test log", zap.Any("Test", "Test")) 105 | if len(response) > 0 { 106 | c.JSON(http.StatusCreated, jsonContact.ToContactResponse(response[0])) 107 | } else { 108 | c.Status(http.StatusOK) 109 | } 110 | } 111 | 112 | // UpdateContact 113 | // @Summary Метод позволяет обновить данные контакта. 114 | // @Description Метод позволяет обновить данные контакта. 115 | // @Tags contacts 116 | // @Accept json 117 | // @Produce json 118 | // @Param id path string true "Идентификатор контакта" 119 | // @Param contact body jsonContact.ShortContact true "Данные по контакту" 120 | // @Success 200 {object} jsonContact.ContactResponse true "Структура контакта" 121 | // @Failure 400 {object} ErrorResponse 122 | // @Failure 403 "Forbidden" 123 | // @Failure 404 {object} ErrorResponse "404 Not Found" 124 | // @Router /contacts/{id} [put] 125 | func (d *Delivery) UpdateContact(c *gin.Context) { 126 | 127 | var ctx = context.New(c) 128 | 129 | var id jsonContact.ID 130 | if err := c.ShouldBindUri(&id); err != nil { 131 | SetError(c, http.StatusBadRequest, err) 132 | return 133 | } 134 | 135 | contact := jsonContact.ShortContact{} 136 | if err := c.ShouldBindJSON(&contact); err != nil { 137 | SetError(c, http.StatusInternalServerError, err) 138 | return 139 | } 140 | 141 | contactAge, err := age.New(contact.Age) 142 | if err != nil { 143 | SetError(c, http.StatusBadRequest, err) 144 | return 145 | } 146 | 147 | contactName, err := name.New(contact.Name) 148 | if err != nil { 149 | SetError(c, http.StatusBadRequest, err) 150 | return 151 | } 152 | 153 | contactSurname, err := surname.New(contact.Surname) 154 | if err != nil { 155 | SetError(c, http.StatusBadRequest, err) 156 | return 157 | } 158 | 159 | contactPatronymic, err := patronymic.New(contact.Patronymic) 160 | if err != nil { 161 | SetError(c, http.StatusBadRequest, err) 162 | return 163 | } 164 | 165 | var dContact, _ = domainContact.NewWithID( 166 | converter.StringToUUID(id.Value), 167 | time.Now().UTC(), 168 | time.Now().UTC(), 169 | *phoneNumber.New(contact.PhoneNumber), 170 | contact.Email, 171 | *contactName, 172 | *contactSurname, 173 | *contactPatronymic, 174 | *contactAge, 175 | contact.Gender, 176 | ) 177 | 178 | response, err := d.ucContact.Update(ctx, *dContact) 179 | if err != nil { 180 | if errors.Is(err, useCase.ErrContactNotFound) { 181 | SetError(c, http.StatusNotFound, err) 182 | return 183 | } 184 | 185 | SetError(c, http.StatusInternalServerError, err) 186 | return 187 | } 188 | 189 | c.JSON(http.StatusOK, jsonContact.ToContactResponse(response)) 190 | 191 | } 192 | 193 | // DeleteContact 194 | // @Summary Метод позволяет удалить контакт. 195 | // @Description Метод позволяет удалить контакт. 196 | // @Tags contacts 197 | // @Accept json 198 | // @Produce json 199 | // @Param id path string true "Идентификатор контакта" 200 | // @Failure 400 {object} ErrorResponse 201 | // @Failure 403 "Forbidden" 202 | // @Failure 404 {object} ErrorResponse "404 Not Found" 203 | // @Router /contacts/{id} [delete] 204 | func (d *Delivery) DeleteContact(c *gin.Context) { 205 | 206 | var ctx = context.New(c) 207 | 208 | var id jsonContact.ID 209 | if err := c.ShouldBindUri(&id); err != nil { 210 | SetError(c, http.StatusBadRequest, err) 211 | return 212 | } 213 | 214 | if err := d.ucContact.Delete(ctx, converter.StringToUUID(id.Value)); err != nil { 215 | SetError(c, http.StatusInternalServerError, err) 216 | return 217 | } 218 | c.Status(http.StatusOK) 219 | } 220 | 221 | // ListContact 222 | // @Summary Получить список контактов. 223 | // @Description Метод позволяет получить список контактов. 224 | // @Tags contacts 225 | // @Accept json 226 | // @Produce json 227 | // @Param limit query int false "Количество записей" default(10) mininum(0) maxinum(100) 228 | // @Param offset query int false "Смещение при получении записей" default(0) mininum(0) 229 | // @Param sort query string false "Сортировка по полю" default(name) 230 | // @Success 200 {object} jsonContact.ListContact true "Список контактов" 231 | // @Failure 400 {object} ErrorResponse 232 | // @Failure 403 "Forbidden" 233 | // @Failure 404 {object} ErrorResponse "404 Not Found" 234 | // @Router /contacts/ [get] 235 | func (d *Delivery) ListContact(c *gin.Context) { 236 | 237 | var ctx = context.New(c) 238 | params, err := query.ParseQuery(c, query.Options{ 239 | Sorts: mappingSortsContact, 240 | }) 241 | 242 | if err != nil { 243 | SetError(c, http.StatusBadRequest, err) 244 | return 245 | } 246 | 247 | contacts, err := d.ucContact.List(ctx, queryParameter.QueryParameter{ 248 | Sorts: params.Sorts, 249 | Pagination: pagination.Pagination{ 250 | Limit: params.Limit, 251 | Offset: params.Offset, 252 | }, 253 | }) 254 | if err != nil { 255 | SetError(c, http.StatusInternalServerError, err) 256 | return 257 | } 258 | 259 | count, err := d.ucContact.Count(ctx) 260 | if err != nil { 261 | SetError(c, http.StatusInternalServerError, err) 262 | return 263 | } 264 | 265 | var result = jsonContact.ListContact{ 266 | Total: count, 267 | Limit: params.Limit, 268 | Offset: params.Offset, 269 | List: []*jsonContact.ContactResponse{}, 270 | } 271 | for _, value := range contacts { 272 | result.List = append(result.List, jsonContact.ToContactResponse(value)) 273 | } 274 | 275 | c.JSON(http.StatusOK, result) 276 | } 277 | 278 | // ReadContactByID 279 | // @Summary Получить контакт. 280 | // @Description Метод позволяет получить контакт по мдентификатору контакта. 281 | // @Tags contacts 282 | // @Accept json 283 | // @Produce json 284 | // @Param id path string true "Идентификатор контакта" 285 | // @Success 200 {object} jsonContact.ContactResponse true "Структура контакта" 286 | // @Failure 400 {object} ErrorResponse 287 | // @Failure 403 "Forbidden" 288 | // @Failure 404 {object} ErrorResponse "404 Not Found" 289 | // @Router /contacts/{id} [get] 290 | func (d *Delivery) ReadContactByID(c *gin.Context) { 291 | 292 | var ctx = context.New(c) 293 | 294 | var id jsonContact.ID 295 | if err := c.ShouldBindUri(&id); err != nil { 296 | SetError(c, http.StatusBadRequest, err) 297 | return 298 | } 299 | 300 | response, err := d.ucContact.ReadByID(ctx, converter.StringToUUID(id.Value)) 301 | if err != nil { 302 | if errors.Is(err, useCase.ErrContactNotFound) { 303 | SetError(c, http.StatusNotFound, err) 304 | return 305 | } 306 | 307 | SetError(c, http.StatusInternalServerError, err) 308 | return 309 | } 310 | 311 | c.JSON(http.StatusOK, jsonContact.ToContactResponse(response)) 312 | 313 | } 314 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/contact/converter.go: -------------------------------------------------------------------------------- 1 | package contact 2 | 3 | import ( 4 | domainContact "architecture_go/services/contact/internal/domain/contact" 5 | ) 6 | 7 | func ToContactResponse(response *domainContact.Contact) *ContactResponse { 8 | return &ContactResponse{ 9 | ID: response.ID().String(), 10 | CreatedAt: response.CreatedAt(), 11 | ModifiedAt: response.ModifiedAt(), 12 | ShortContact: ShortContact{ 13 | PhoneNumber: response.PhoneNumber().String(), 14 | Email: response.Email(), 15 | Gender: response.Gender(), 16 | Age: uint8(response.Age()), 17 | Name: response.Name().String(), 18 | Surname: response.Surname().String(), 19 | Patronymic: response.Patronymic().String(), 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/contact/type.go: -------------------------------------------------------------------------------- 1 | package contact 2 | 3 | import ( 4 | "time" 5 | 6 | "architecture_go/pkg/type/email" 7 | "architecture_go/pkg/type/gender" 8 | ) 9 | 10 | type ID struct { 11 | Value string `json:"id" uri:"id" binding:"required,uuid" example:"00000000-0000-0000-0000-000000000000" format:"uuid"` 12 | } 13 | 14 | type ContactResponse struct { 15 | // Идетификатор записи 16 | ID string `json:"id" binding:"required,uuid" example:"00000000-0000-0000-0000-000000000000" format:"uuid"` 17 | // Дата создания контакта 18 | CreatedAt time.Time `json:"createdAt" binding:"required"` 19 | // Дата последнего изменения контакта 20 | ModifiedAt time.Time `json:"modifiedAt" binding:"required"` 21 | ShortContact 22 | } 23 | 24 | type ShortContact struct { 25 | // Мобильный телефон 26 | PhoneNumber string `json:"phoneNumber" binding:"required,max=50" maxLength:"50" example:"78002002020"` 27 | // Электронная почта 28 | Email email.Email `json:"email" binding:"omitempty,max=250,email" maxLength:"250" example:"example@gmail.com" format:"email" swaggertype:"string"` 29 | // Пол 30 | Gender gender.Gender `json:"gender" example:"1" enums:"1,2" swaggertype:"integer"` 31 | // Возраст 32 | Age uint8 `json:"age" binding:"min=0,max=200" minimum:"0" maximum:"200" default:"0" example:"42"` 33 | // Имя клиента 34 | Name string `json:"name" binding:"max=50" maxLength:"50" example:"Иван"` 35 | // Фамилия клиента 36 | Surname string `json:"surname" binding:"max=100" maxLength:"100" example:"Иванов"` 37 | // Отчество клиента 38 | Patronymic string `json:"patronymic" binding:"max=100" maxLength:"100" example:"Иванович"` 39 | } 40 | 41 | type ListContact struct { 42 | // Всего 43 | Total uint64 `json:"total" example:"10" default:"0" binding:"min=0" minimum:"0"` 44 | // Количество записей 45 | Limit uint64 `json:"limit" example:"10" default:"10" binding:"min=0" minimum:"0"` 46 | // Смещение при получении записей 47 | Offset uint64 `json:"offset" example:"20" default:"0" binding:"min=0" minimum:"0"` 48 | 49 | List []*ContactResponse `json:"list"` 50 | } 51 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/contactInGroup.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "architecture_go/pkg/tools/converter" 9 | "architecture_go/pkg/type/context" 10 | "architecture_go/pkg/type/phoneNumber" 11 | jsonContact "architecture_go/services/contact/internal/delivery/http/contact" 12 | jsonGroup "architecture_go/services/contact/internal/delivery/http/group" 13 | domainContact "architecture_go/services/contact/internal/domain/contact" 14 | "architecture_go/services/contact/internal/domain/contact/age" 15 | "architecture_go/services/contact/internal/domain/contact/name" 16 | "architecture_go/services/contact/internal/domain/contact/patronymic" 17 | "architecture_go/services/contact/internal/domain/contact/surname" 18 | ) 19 | 20 | // CreateContactIntoGroup 21 | // @Summary Создание контакта и добавление его в существующую группу. 22 | // @Description Создание контакта и добавление его в существующую группу. 23 | // @Security Cookies 24 | // @Tags groups 25 | // @Accept json 26 | // @Produce json 27 | // @Param id path string true "Идентификатор группы контактов" 28 | // @Param contact body jsonContact.ShortContact true "Данные по контакту" 29 | // @Success 200 30 | // @Failure 400 {object} ErrorResponse 31 | // @Failure 403 "Forbidden" 32 | // @Failure 404 {object} ErrorResponse "404 Not Found" 33 | // @Router /groups/{id}/contacts/ [post] 34 | func (d *Delivery) CreateContactIntoGroup(c *gin.Context) { 35 | 36 | var ctx = context.New(c) 37 | 38 | var id jsonGroup.ID 39 | if err := c.ShouldBindUri(&id); err != nil { 40 | SetError(c, http.StatusBadRequest, err) 41 | return 42 | } 43 | 44 | contact := jsonContact.ShortContact{} 45 | if err := c.ShouldBindJSON(&contact); err != nil { 46 | SetError(c, http.StatusBadRequest, err) 47 | return 48 | } 49 | 50 | contactAge, err := age.New(contact.Age) 51 | if err != nil { 52 | SetError(c, http.StatusBadRequest, err) 53 | return 54 | } 55 | 56 | contactName, err := name.New(contact.Name) 57 | if err != nil { 58 | SetError(c, http.StatusBadRequest, err) 59 | return 60 | } 61 | 62 | contactSurname, err := surname.New(contact.Surname) 63 | if err != nil { 64 | SetError(c, http.StatusBadRequest, err) 65 | return 66 | } 67 | 68 | contactPatronymic, err := patronymic.New(contact.Patronymic) 69 | if err != nil { 70 | SetError(c, http.StatusBadRequest, err) 71 | return 72 | } 73 | 74 | dContact, err := domainContact.New( 75 | *phoneNumber.New(contact.PhoneNumber), 76 | contact.Email, 77 | *contactName, 78 | *contactSurname, 79 | *contactPatronymic, 80 | *contactAge, 81 | contact.Gender, 82 | ) 83 | if err != nil { 84 | SetError(c, http.StatusBadRequest, err) 85 | return 86 | } 87 | 88 | contacts, err := d.ucGroup.CreateContactIntoGroup(ctx, converter.StringToUUID(id.Value), dContact) 89 | if err != nil { 90 | SetError(c, http.StatusInternalServerError, err) 91 | return 92 | } 93 | 94 | var list = []*jsonContact.ContactResponse{} 95 | for _, value := range contacts { 96 | list = append(list, jsonContact.ToContactResponse(value)) 97 | } 98 | 99 | c.JSON(http.StatusOK, list) 100 | 101 | } 102 | 103 | // AddContactToGroup 104 | // @Summary Метод позволяет добавить контакты в группу. 105 | // @Description Метод позволяет добавить контакты в группу. 106 | // @Tags groups 107 | // @Accept json 108 | // @Produce json 109 | // @Param id path string true "Идентификатор группы" 110 | // @Param contactId path string true "Идентификатор контакта" 111 | // @Success 200 112 | // @Failure 400 {object} ErrorResponse 113 | // @Failure 403 "Forbidden" 114 | // @Failure 404 {object} ErrorResponse "404 Not Found" 115 | // @Router /groups/{id}/contacts/{contactId} [post] 116 | func (d *Delivery) AddContactToGroup(c *gin.Context) { 117 | 118 | var ctx = context.New(c) 119 | 120 | var id jsonGroup.ID 121 | if err := c.ShouldBindUri(&id); err != nil { 122 | SetError(c, http.StatusBadRequest, err) 123 | return 124 | } 125 | 126 | var contactID jsonGroup.ContactID 127 | if err := c.ShouldBindUri(&contactID); err != nil { 128 | SetError(c, http.StatusBadRequest, err) 129 | return 130 | } 131 | 132 | if err := d.ucGroup.AddContactToGroup(ctx, converter.StringToUUID(id.Value), converter.StringToUUID(contactID.Value)); err != nil { 133 | SetError(c, http.StatusInternalServerError, err) 134 | return 135 | } 136 | 137 | c.Status(http.StatusOK) 138 | 139 | } 140 | 141 | // DeleteContactFromGroup 142 | // @Summary Метод позволяет удалить контакт из группы. 143 | // @Description Метод позволяет удалить контакт из группы. 144 | // @Tags groups 145 | // @Accept json 146 | // @Produce json 147 | // @Param id path string true "Идентификатор группы" 148 | // @Param contactId path string true "Идентификатор контакта" 149 | // @Success 200 150 | // @Failure 400 {object} ErrorResponse 151 | // @Failure 403 "Forbidden" 152 | // @Failure 404 {object} ErrorResponse "404 Not Found" 153 | // @Router /groups/{id}/contacts/{contactId} [delete] 154 | func (d *Delivery) DeleteContactFromGroup(c *gin.Context) { 155 | 156 | var ctx = context.New(c) 157 | 158 | var id jsonGroup.ID 159 | if err := c.ShouldBindUri(&id); err != nil { 160 | SetError(c, http.StatusBadRequest, err) 161 | return 162 | } 163 | 164 | var contactID jsonGroup.ContactID 165 | if err := c.ShouldBindUri(&contactID); err != nil { 166 | SetError(c, http.StatusBadRequest, err) 167 | return 168 | } 169 | 170 | if err := d.ucGroup.DeleteContactFromGroup(ctx, converter.StringToUUID(id.Value), converter.StringToUUID(contactID.Value)); err != nil { 171 | SetError(c, http.StatusInternalServerError, err) 172 | return 173 | } 174 | 175 | c.Status(http.StatusOK) 176 | } 177 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/delivery.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/spf13/viper" 8 | 9 | "architecture_go/services/contact/internal/useCase" 10 | ) 11 | 12 | // @title slurm contact service on clean architecture 13 | // @version 1.0 14 | // @description contact service on clean architecture 15 | // @license.name kolyadkons 16 | 17 | // @contact.name API Support 18 | // @contact.email kolyadkons@gmail.com 19 | 20 | // @BasePath / 21 | 22 | func init() { 23 | viper.SetConfigName(".env") 24 | viper.SetConfigType("dotenv") 25 | viper.AddConfigPath(".") 26 | viper.AutomaticEnv() 27 | 28 | viper.SetDefault("HTTP_PORT", 80) 29 | } 30 | 31 | type Delivery struct { 32 | ucContact useCase.Contact 33 | ucGroup useCase.Group 34 | router *gin.Engine 35 | 36 | options Options 37 | } 38 | 39 | type Options struct{} 40 | 41 | func New(ucContact useCase.Contact, ucGroup useCase.Group, options Options) *Delivery { 42 | var d = &Delivery{ 43 | ucContact: ucContact, 44 | ucGroup: ucGroup, 45 | } 46 | 47 | d.SetOptions(options) 48 | 49 | d.router = d.initRouter() 50 | return d 51 | } 52 | 53 | func (d *Delivery) SetOptions(options Options) { 54 | if d.options != options { 55 | d.options = options 56 | } 57 | } 58 | 59 | func (d *Delivery) Run() error { 60 | return d.router.Run(fmt.Sprintf(":%d", uint16(viper.GetUint("HTTP_PORT")))) 61 | } 62 | 63 | func checkAuth(c *gin.Context) { 64 | c.Next() 65 | } 66 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/error.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/google/uuid" 6 | "github.com/opentracing/opentracing-go" 7 | "github.com/uber/jaeger-client-go" 8 | "go.uber.org/zap" 9 | 10 | "architecture_go/pkg/type/logger" 11 | ) 12 | 13 | type ErrorResponse struct { 14 | ID uuid.UUID `json:"id"` 15 | Error string `json:"message,omitempty"` 16 | Errors []string `json:"errors,omitempty"` 17 | Info interface{} `json:"info,omitempty"` 18 | } 19 | 20 | func SetError(c *gin.Context, statusCode int, errs ...error) { 21 | var response = ErrorResponse{ 22 | ID: uuid.New(), 23 | } 24 | 25 | if len(errs) == 0 { 26 | return 27 | } 28 | 29 | if len(errs) > 0 { 30 | response.Error = errs[0].Error() 31 | 32 | if len(errs) > 1 { 33 | for _, err := range errs { 34 | response.Errors = append(response.Errors, c.Error(err).Error()) 35 | } 36 | } 37 | } 38 | c.JSON(statusCode, response) 39 | 40 | fields := getContextFields(c) 41 | if statusCode >= 400 && statusCode < 500 { 42 | logger.Warn(errs[len(errs)-1].Error(), fields...) 43 | } else if statusCode >= 500 { 44 | logger.Error(errs[len(errs)-1], fields...) 45 | } 46 | } 47 | 48 | func getContextFields(c *gin.Context) []zap.Field { 49 | var fields = []zap.Field{zap.Int("status", c.Writer.Status()), 50 | zap.String("method", c.Request.Method), 51 | zap.String("path", c.Request.URL.Path), 52 | zap.String("query", c.Request.URL.RawQuery), 53 | zap.String("ip", c.ClientIP()), 54 | zap.String("user-agent", c.Request.UserAgent()), 55 | } 56 | 57 | if span := opentracing.SpanFromContext(c.Request.Context()); span != nil { 58 | if jaegerSpan, ok := span.Context().(jaeger.SpanContext); ok { 59 | fields = append(fields, zap.Stringer("traceID", jaegerSpan.TraceID())) 60 | } 61 | } 62 | 63 | return fields 64 | } 65 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/group.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | "architecture_go/pkg/tools/converter" 11 | "architecture_go/pkg/type/context" 12 | "architecture_go/pkg/type/pagination" 13 | "architecture_go/pkg/type/query" 14 | "architecture_go/pkg/type/queryParameter" 15 | jsonGroup "architecture_go/services/contact/internal/delivery/http/group" 16 | domainGroup "architecture_go/services/contact/internal/domain/group" 17 | "architecture_go/services/contact/internal/domain/group/description" 18 | "architecture_go/services/contact/internal/domain/group/name" 19 | "architecture_go/services/contact/internal/useCase" 20 | ) 21 | 22 | var mappingSortsGroup = query.SortsOptions{ 23 | "id": {}, 24 | "name": {}, 25 | "description": {}, 26 | "contactCount": {}, 27 | } 28 | 29 | // CreateGroup 30 | // @Summary Метод позволяет создать группу контактов. 31 | // @Description Метод позволяет создать группу контактов. 32 | // @Tags groups 33 | // @Accept json 34 | // @Produce json 35 | // @Param group body jsonGroup.ShortGroup true "Данные по группе" 36 | // @Success 200 {object} jsonGroup.GroupResponse true 37 | // @Failure 400 {object} ErrorResponse 38 | // @Failure 403 "Forbidden" 39 | // @Failure 404 {object} ErrorResponse "404 Not Found" 40 | // @Router /groups/ [post] 41 | func (d *Delivery) CreateGroup(c *gin.Context) { 42 | 43 | var ctx = context.New(c) 44 | 45 | var group = &jsonGroup.ShortGroup{} 46 | 47 | if err := c.ShouldBindJSON(&group); err != nil { 48 | SetError(c, http.StatusBadRequest, err) 49 | return 50 | } 51 | 52 | groupName, err := name.New(group.Name) 53 | if err != nil { 54 | SetError(c, http.StatusBadRequest, err) 55 | return 56 | } 57 | groupDescription, err := description.New(group.Description) 58 | if err != nil { 59 | SetError(c, http.StatusBadRequest, err) 60 | return 61 | } 62 | newGroup, err := d.ucGroup.Create(ctx, domainGroup.New( 63 | groupName, 64 | groupDescription, 65 | )) 66 | if err != nil { 67 | SetError(c, http.StatusInternalServerError, err) 68 | return 69 | } 70 | 71 | c.JSON(http.StatusOK, jsonGroup.GroupResponse{ 72 | ID: newGroup.ID().String(), 73 | CreatedAt: newGroup.CreatedAt(), 74 | ModifiedAt: newGroup.ModifiedAt(), 75 | Group: jsonGroup.Group{ 76 | ShortGroup: jsonGroup.ShortGroup{ 77 | Name: newGroup.Name().Value(), 78 | Description: newGroup.Description().Value(), 79 | }, 80 | ContactsAmount: newGroup.ContactCount(), 81 | }, 82 | }) 83 | } 84 | 85 | // UpdateGroup 86 | // @Summary Метод позволяет обновить данные группы. 87 | // @Description Метод позволяет обновить данные группы. 88 | // @Tags groups 89 | // @Accept json 90 | // @Produce json 91 | // @Param id path string true "Идентификатор группы" 92 | // @Param group body jsonGroup.ShortGroup true "Данные по группе" 93 | // @Success 200 {object} jsonGroup.GroupResponse 94 | // @Failure 400 {object} ErrorResponse 95 | // @Failure 403 "Forbidden" 96 | // @Failure 404 {object} ErrorResponse "404 Not Found" 97 | // @Router /groups/{id} [put] 98 | func (d *Delivery) UpdateGroup(c *gin.Context) { 99 | 100 | var ctx = context.New(c) 101 | 102 | var id jsonGroup.ID 103 | if err := c.ShouldBindUri(&id); err != nil { 104 | SetError(c, http.StatusBadRequest, err) 105 | return 106 | } 107 | 108 | group := jsonGroup.ShortGroup{} 109 | if err := c.ShouldBindJSON(&group); err != nil { 110 | SetError(c, http.StatusBadRequest, err) 111 | return 112 | } 113 | 114 | groupName, err := name.New(group.Name) 115 | if err != nil { 116 | SetError(c, http.StatusBadRequest, err) 117 | return 118 | } 119 | groupDescription, err := description.New(group.Description) 120 | if err != nil { 121 | SetError(c, http.StatusBadRequest, err) 122 | return 123 | } 124 | 125 | response, err := d.ucGroup.Update(ctx, domainGroup.NewWithID( 126 | converter.StringToUUID(id.Value), 127 | time.Now().UTC(), 128 | time.Now().UTC(), 129 | groupName, 130 | groupDescription, 131 | 0, 132 | )) 133 | if err != nil { 134 | if errors.Is(err, useCase.ErrGroupNotFound) { 135 | SetError(c, http.StatusNotFound, err) 136 | return 137 | } 138 | 139 | SetError(c, http.StatusInternalServerError, err) 140 | return 141 | } 142 | c.JSON(http.StatusOK, jsonGroup.ProtoToGroupResponse(response)) 143 | } 144 | 145 | // DeleteGroup 146 | // @Summary Метод позволяет удалить группу. 147 | // @Description Метод позволяет удалить группу. 148 | // @Tags groups 149 | // @Accept json 150 | // @Produce json 151 | // @Param id path string true "Идентификатор группы" 152 | // @Success 200 {object} string 153 | // @Failure 400 {object} ErrorResponse 154 | // @Failure 403 "Forbidden" 155 | // @Failure 404 {object} ErrorResponse "404 Not Found" 156 | // @Router /groups/{id} [delete] 157 | func (d *Delivery) DeleteGroup(c *gin.Context) { 158 | 159 | var ctx = context.New(c) 160 | 161 | var id jsonGroup.ID 162 | if err := c.ShouldBindUri(&id); err != nil { 163 | SetError(c, http.StatusBadRequest, err) 164 | return 165 | } 166 | 167 | if err := d.ucGroup.Delete(ctx, converter.StringToUUID(id.Value)); err != nil { 168 | SetError(c, http.StatusInternalServerError, err) 169 | return 170 | } 171 | c.Status(http.StatusOK) 172 | } 173 | 174 | // ListGroup 175 | // @Summary Метод позволяет получить список групп. 176 | // @Description Метод позволяет получить список групп. 177 | // @Tags groups 178 | // @Accept json 179 | // @Produce json 180 | // @Param limit query int false "Количество записей" default(10) mininum(0) maxinum(100) 181 | // @Param offset query int false "Смещение при получении записей" default(0) mininum(0) 182 | // @Param sort query string false "Сортировка по полю" default(name) 183 | // @Success 200 {object} jsonGroup.GroupList 184 | // @Failure 400 {object} ErrorResponse 185 | // @Failure 403 "Forbidden" 186 | // @Failure 404 {object} ErrorResponse "404 Not Found" 187 | // @Router /groups/ [get] 188 | func (d *Delivery) ListGroup(c *gin.Context) { 189 | 190 | var ctx = context.New(c) 191 | 192 | params, err := query.ParseQuery(c, query.Options{ 193 | Sorts: mappingSortsGroup, 194 | }) 195 | 196 | if err != nil { 197 | SetError(c, http.StatusBadRequest, err) 198 | return 199 | } 200 | 201 | groups, err := d.ucGroup.List(ctx, queryParameter.QueryParameter{ 202 | Sorts: params.Sorts, 203 | Pagination: pagination.Pagination{ 204 | Limit: params.Limit, 205 | Offset: params.Offset, 206 | }, 207 | }) 208 | if err != nil { 209 | SetError(c, http.StatusInternalServerError, err) 210 | return 211 | } 212 | 213 | count, err := d.ucContact.Count(ctx) 214 | if err != nil { 215 | SetError(c, http.StatusInternalServerError, err) 216 | return 217 | } 218 | 219 | var list = make([]*jsonGroup.GroupResponse, len(groups)) 220 | 221 | for i, elem := range groups { 222 | list[i] = jsonGroup.ProtoToGroupResponse(elem) 223 | } 224 | 225 | c.JSON(http.StatusOK, jsonGroup.GroupList{ 226 | Total: count, 227 | Limit: params.Limit, 228 | Offset: params.Offset, 229 | List: list, 230 | }) 231 | } 232 | 233 | // ReadGroupByID 234 | // @Summary Метод позволяет получить данные по группе. 235 | // @Description Метод позволяет получить данные по группе. 236 | // @Tags groups 237 | // @Accept json 238 | // @Produce json 239 | // @Param id path string true "Идентификатор группы контактов" 240 | // @Success 200 {object} jsonGroup.GroupResponse 241 | // @Failure 400 {object} ErrorResponse 242 | // @Failure 403 "Forbidden" 243 | // @Failure 404 {object} ErrorResponse "404 Not Found" 244 | // @Router /groups/{id} [get] 245 | func (d *Delivery) ReadGroupByID(c *gin.Context) { 246 | 247 | var ctx = context.New(c) 248 | 249 | var id jsonGroup.ID 250 | if err := c.ShouldBindUri(&id); err != nil { 251 | SetError(c, http.StatusBadRequest, err) 252 | return 253 | } 254 | 255 | response, err := d.ucGroup.ReadByID(ctx, converter.StringToUUID(id.Value)) 256 | if err != nil { 257 | if errors.Is(err, useCase.ErrGroupNotFound) { 258 | SetError(c, http.StatusNotFound, err) 259 | return 260 | } 261 | 262 | SetError(c, http.StatusInternalServerError, err) 263 | return 264 | } 265 | 266 | c.JSON(http.StatusOK, jsonGroup.ProtoToGroupResponse(response)) 267 | } 268 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/group/converter.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "architecture_go/services/contact/internal/domain/group" 5 | ) 6 | 7 | func ProtoToGroupResponse(response *group.Group) *GroupResponse { 8 | return &GroupResponse{ 9 | ID: response.ID().String(), 10 | CreatedAt: response.CreatedAt(), 11 | ModifiedAt: response.ModifiedAt(), 12 | Group: Group{ 13 | ShortGroup: ShortGroup{ 14 | Name: response.Name().Value(), 15 | Description: response.Description().Value(), 16 | }, 17 | ContactsAmount: response.ContactCount(), 18 | }, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/group/type.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import "time" 4 | 5 | type GroupResponse struct { 6 | // Идентификатор группы 7 | ID string `json:"id" binding:"required,uuid" example:"00000000-0000-0000-0000-000000000000" format:"uuid"` 8 | // Дата создания группы 9 | CreatedAt time.Time `json:"createdAt" binding:"required"` 10 | // Дата последнего изменения группы 11 | ModifiedAt time.Time `json:"modifiedAt" binding:"required"` 12 | Group 13 | } 14 | 15 | // Group 16 | // Описывает объект, который содержит информацию о группе. 17 | type Group struct { 18 | ShortGroup 19 | // Кол-во контактов в группе 20 | ContactsAmount uint64 `json:"contactsAmount" default:"10" binding:"min=0" minimum:"0"` 21 | } 22 | 23 | type ShortGroup struct { 24 | // Название группы 25 | Name string `json:"name" binding:"required,max=100" example:"Название группы" maxLength:"100"` 26 | // Описание 27 | Description string `json:"description" example:"Описание группы" binding:"max=1000" maxLength:"1000"` 28 | } 29 | 30 | // GroupList 31 | // Описывает объект, который содержит информацию о группе. 32 | type GroupList struct { 33 | // Всего 34 | Total uint64 `json:"total" example:"10" default:"0" binding:"min=0" minimum:"0"` 35 | // Количество записей 36 | Limit uint64 `json:"limit" example:"10" default:"10" binding:"min=0" minimum:"0"` 37 | // Смещение при получении записей 38 | Offset uint64 `json:"offset" example:"20" default:"0" binding:"min=0" minimum:"0"` 39 | // Список групп 40 | List []*GroupResponse `json:"list" binding:"min=0" minimum:"0"` 41 | } 42 | 43 | type ID struct { 44 | // Идентификатор группы 45 | Value string `json:"id" uri:"id" binding:"required,uuid" example:"00000000-0000-0000-0000-000000000000" format:"uuid"` 46 | } 47 | 48 | type ContactID struct { 49 | // Идентификатор контакта 50 | Value string `json:"id" uri:"contactId" binding:"required,uuid" example:"00000000-0000-0000-0000-000000000000" format:"uuid"` 51 | } 52 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/middleware.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/opentracing/opentracing-go" 8 | "github.com/opentracing/opentracing-go/ext" 9 | "github.com/uber/jaeger-client-go" 10 | 11 | log "architecture_go/pkg/type/logger" 12 | ) 13 | 14 | func Tracer() gin.HandlerFunc { 15 | return func(c *gin.Context) { 16 | span := opentracing.SpanFromContext(c.Request.Context()) 17 | if span == nil { 18 | span = StartSpanWithHeader(&c.Request.Header, "rest-request-"+c.Request.Method, c.Request.Method, c.Request.URL.Path) 19 | } 20 | defer span.Finish() 21 | c.Request = c.Request.WithContext(opentracing.ContextWithSpan(c.Request.Context(), span)) 22 | 23 | if traceID, ok := span.Context().(jaeger.SpanContext); ok { 24 | c.Header("uber-trace-id", traceID.TraceID().String()) 25 | } 26 | 27 | c.Next() 28 | 29 | ext.HTTPStatusCode.Set(span, uint16(c.Writer.Status())) 30 | 31 | if len(c.Errors) == 0 { 32 | log.Info("", getContextFields(c)...) 33 | } 34 | } 35 | } 36 | 37 | func StartSpanWithHeader(header *http.Header, operationName, method, path string) opentracing.Span { 38 | var wireContext opentracing.SpanContext 39 | 40 | if header != nil { 41 | wireContext, _ = opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(*header)) 42 | } 43 | 44 | return StartSpanWithParent(wireContext, operationName, method, path) 45 | } 46 | 47 | // StartSpanWithParent will start a new span with a parent span. 48 | // example: 49 | // span:= StartSpanWithParent(c.Get("tracing-context"), 50 | func StartSpanWithParent(parent opentracing.SpanContext, operationName, method, path string) opentracing.Span { 51 | options := []opentracing.StartSpanOption{ 52 | opentracing.Tag{Key: ext.SpanKindRPCServer.Key, Value: ext.SpanKindRPCServer.Value}, 53 | opentracing.Tag{Key: string(ext.HTTPMethod), Value: method}, 54 | opentracing.Tag{Key: string(ext.HTTPUrl), Value: path}, 55 | } 56 | if parent != nil { 57 | options = append(options, opentracing.ChildOf(parent)) 58 | } 59 | 60 | return opentracing.StartSpan(operationName, options...) 61 | } 62 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/router.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-contrib/zap" 7 | "github.com/gin-gonic/gin" 8 | "github.com/spf13/viper" 9 | swaggerFiles "github.com/swaggo/files" 10 | ginSwagger "github.com/swaggo/gin-swagger" 11 | 12 | "architecture_go/pkg/type/logger" 13 | docs "architecture_go/services/contact/internal/delivery/http/swagger/docs" 14 | ) 15 | 16 | func (d *Delivery) initRouter() *gin.Engine { 17 | 18 | if viper.GetBool("IS_PRODUCTION") { 19 | switch strings.ToUpper(strings.TrimSpace(viper.GetString("LOG_LEVEL"))) { 20 | case "DEBUG": 21 | gin.SetMode(gin.DebugMode) 22 | default: 23 | gin.SetMode(gin.ReleaseMode) 24 | } 25 | } else { 26 | gin.SetMode(gin.DebugMode) 27 | } 28 | 29 | var router = gin.New() 30 | 31 | router.Use(Tracer()) 32 | 33 | // Logs all panic to error log 34 | // - stack means whether output the stack info. 35 | router.Use(ginzap.RecoveryWithZap(logger.GetLogger(), true)) 36 | 37 | d.routerDocs(router.Group("/docs")) 38 | 39 | router.Use(checkAuth) 40 | 41 | d.routerContacts(router.Group("/contacts")) 42 | 43 | d.routerGroups(router.Group("/groups")) 44 | 45 | return router 46 | } 47 | 48 | func (d *Delivery) routerContacts(router *gin.RouterGroup) { 49 | router.POST("/", d.CreateContact) 50 | router.PUT("/:id", d.UpdateContact) 51 | router.DELETE("/:id", d.DeleteContact) 52 | router.GET("/", d.ListContact) 53 | router.GET("/:id", d.ReadContactByID) 54 | } 55 | 56 | func (d *Delivery) routerGroups(router *gin.RouterGroup) { 57 | router.POST("/", d.CreateGroup) 58 | router.PUT("/:id", d.UpdateGroup) 59 | router.DELETE("/:id", d.DeleteGroup) 60 | router.GET("/", d.ListGroup) 61 | router.GET("/:id", d.ReadGroupByID) 62 | 63 | router.POST("/:id/contacts/", d.CreateContactIntoGroup) 64 | router.POST("/:id/contacts/:contactId", d.AddContactToGroup) 65 | router.DELETE("/:id/contacts/:contactId", d.DeleteContactFromGroup) 66 | } 67 | func (d *Delivery) routerDocs(router *gin.RouterGroup) { 68 | docs.SwaggerInfo.BasePath = "/" 69 | 70 | router.Any("/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 71 | } 72 | -------------------------------------------------------------------------------- /services/contact/internal/delivery/http/swag.go: -------------------------------------------------------------------------------- 1 | //go:build swag 2 | // +build swag 3 | 4 | package http 5 | 6 | //go:generate swag init --parseDependency --generalInfo delivery.go --output swagger/docs/ 7 | -------------------------------------------------------------------------------- /services/contact/internal/domain/contact/age/type.go: -------------------------------------------------------------------------------- 1 | package age 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | var ( 10 | MaxLength uint8 = 200 11 | ErrWrongLength = errors.Errorf("Age must be less than or equal to %d", MaxLength) 12 | ) 13 | 14 | type Age uint8 15 | 16 | func (a Age) String() string { 17 | return strconv.FormatUint(uint64(a), 10) 18 | } 19 | 20 | func New(age uint8) (*Age, error) { 21 | if age > MaxLength { 22 | return nil, ErrWrongLength 23 | } 24 | a := Age(age) 25 | return &a, nil 26 | } 27 | 28 | func (a Age) Equal(age Age) bool { 29 | return a == age 30 | } 31 | 32 | func (a Age) Less(age Age) bool { 33 | return a < age 34 | } 35 | 36 | func (a Age) More(age Age) bool { 37 | return a > age 38 | } 39 | -------------------------------------------------------------------------------- /services/contact/internal/domain/contact/name/type.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import "github.com/pkg/errors" 4 | 5 | var ( 6 | MaxLength = 50 7 | ErrWrongLength = errors.Errorf("name must be less than or equal to %d characters", MaxLength) 8 | ) 9 | 10 | type Name string 11 | 12 | func (n Name) String() string { 13 | return string(n) 14 | } 15 | 16 | func New(name string) (*Name, error) { 17 | if len([]rune(name)) > MaxLength { 18 | return nil, ErrWrongLength 19 | } 20 | n := Name(name) 21 | return &n, nil 22 | } 23 | -------------------------------------------------------------------------------- /services/contact/internal/domain/contact/patronymic/type.go: -------------------------------------------------------------------------------- 1 | package patronymic 2 | 3 | import "github.com/pkg/errors" 4 | 5 | var ( 6 | MaxLength = 100 7 | ErrWrongLength = errors.Errorf("patronymic must be less than or equal to %d characters", MaxLength) 8 | ) 9 | 10 | type Patronymic string 11 | 12 | func (p Patronymic) String() string { 13 | return string(p) 14 | } 15 | 16 | func New(patronymic string) (*Patronymic, error) { 17 | if len([]rune(patronymic)) > MaxLength { 18 | return nil, ErrWrongLength 19 | } 20 | p := Patronymic(patronymic) 21 | return &p, nil 22 | } 23 | -------------------------------------------------------------------------------- /services/contact/internal/domain/contact/surname/type.go: -------------------------------------------------------------------------------- 1 | package surname 2 | 3 | import "github.com/pkg/errors" 4 | 5 | var ( 6 | MaxLength = 100 7 | ErrWrongLength = errors.Errorf("surname must be less than or equal to %d characters", MaxLength) 8 | ) 9 | 10 | type Surname string 11 | 12 | func (s Surname) String() string { 13 | return string(s) 14 | } 15 | 16 | func New(surname string) (*Surname, error) { 17 | if len([]rune(surname)) > MaxLength { 18 | return nil, ErrWrongLength 19 | } 20 | s := Surname(surname) 21 | return &s, nil 22 | } 23 | -------------------------------------------------------------------------------- /services/contact/internal/domain/contact/type.go: -------------------------------------------------------------------------------- 1 | package contact 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | "github.com/pkg/errors" 9 | 10 | "architecture_go/pkg/type/email" 11 | "architecture_go/pkg/type/gender" 12 | "architecture_go/pkg/type/phoneNumber" 13 | "architecture_go/services/contact/internal/domain/contact/age" 14 | "architecture_go/services/contact/internal/domain/contact/name" 15 | "architecture_go/services/contact/internal/domain/contact/patronymic" 16 | "architecture_go/services/contact/internal/domain/contact/surname" 17 | ) 18 | 19 | var ( 20 | ErrPhoneNumberRequired = errors.New("phone number is required") 21 | ) 22 | 23 | type Contact struct { 24 | id uuid.UUID 25 | createdAt time.Time 26 | modifiedAt time.Time 27 | 28 | phoneNumber phoneNumber.PhoneNumber 29 | email email.Email 30 | 31 | name name.Name 32 | surname surname.Surname 33 | patronymic patronymic.Patronymic 34 | 35 | age age.Age 36 | 37 | gender gender.Gender 38 | } 39 | 40 | func NewWithID( 41 | id uuid.UUID, 42 | createdAt time.Time, 43 | modifiedAt time.Time, 44 | phoneNumber phoneNumber.PhoneNumber, 45 | email email.Email, 46 | name name.Name, 47 | surname surname.Surname, 48 | patronymic patronymic.Patronymic, 49 | age age.Age, 50 | gender gender.Gender, 51 | ) (*Contact, error) { 52 | 53 | if phoneNumber.IsEmpty() { 54 | return nil, ErrPhoneNumberRequired 55 | } 56 | 57 | if id == uuid.Nil { 58 | id = uuid.New() 59 | } 60 | 61 | return &Contact{ 62 | id: id, 63 | createdAt: createdAt.UTC(), 64 | modifiedAt: modifiedAt.UTC(), 65 | phoneNumber: phoneNumber, 66 | email: email, 67 | name: name, 68 | surname: surname, 69 | patronymic: patronymic, 70 | age: age, 71 | gender: gender, 72 | }, nil 73 | } 74 | 75 | func New( 76 | phoneNumber phoneNumber.PhoneNumber, 77 | email email.Email, 78 | name name.Name, 79 | surname surname.Surname, 80 | patronymic patronymic.Patronymic, 81 | age age.Age, 82 | gender gender.Gender, 83 | ) (*Contact, error) { 84 | 85 | if phoneNumber.IsEmpty() { 86 | return nil, ErrPhoneNumberRequired 87 | } 88 | 89 | var timeNow = time.Now().UTC() 90 | return &Contact{ 91 | id: uuid.New(), 92 | createdAt: timeNow, 93 | modifiedAt: timeNow, 94 | phoneNumber: phoneNumber, 95 | email: email, 96 | name: name, 97 | surname: surname, 98 | patronymic: patronymic, 99 | age: age, 100 | gender: gender, 101 | }, nil 102 | } 103 | 104 | func (c Contact) ID() uuid.UUID { 105 | return c.id 106 | } 107 | 108 | func (c Contact) CreatedAt() time.Time { 109 | return c.createdAt 110 | } 111 | 112 | func (c Contact) ModifiedAt() time.Time { 113 | return c.modifiedAt 114 | } 115 | 116 | func (c Contact) Email() email.Email { 117 | return c.email 118 | } 119 | 120 | func (c Contact) PhoneNumber() phoneNumber.PhoneNumber { 121 | return c.phoneNumber 122 | } 123 | 124 | func (c Contact) Name() name.Name { 125 | return c.name 126 | } 127 | 128 | func (c Contact) Surname() surname.Surname { 129 | return c.surname 130 | } 131 | 132 | func (c Contact) Patronymic() patronymic.Patronymic { 133 | return c.patronymic 134 | } 135 | 136 | func (c Contact) FullName() string { 137 | return fmt.Sprintf("%s %s %s", c.surname, c.name, c.patronymic) 138 | } 139 | 140 | func (c Contact) Age() age.Age { 141 | return c.age 142 | } 143 | 144 | func (c Contact) Gender() gender.Gender { 145 | return c.gender 146 | } 147 | 148 | func (c Contact) Equal(contact Contact) bool { 149 | return c.id == contact.id 150 | } 151 | -------------------------------------------------------------------------------- /services/contact/internal/domain/group/description/type.go: -------------------------------------------------------------------------------- 1 | package description 2 | 3 | import "github.com/pkg/errors" 4 | 5 | var ( 6 | MaxLength = 1000 7 | ErrWrongLength = errors.Errorf("description must be less than or equal to %d characters", MaxLength) 8 | ) 9 | 10 | type Description struct { 11 | value string 12 | } 13 | 14 | func New(description string) (Description, error) { 15 | if len([]rune(description)) > MaxLength { 16 | return Description{}, ErrWrongLength 17 | } 18 | return Description{value: description}, nil 19 | } 20 | 21 | func (d Description) Value() string { 22 | return d.value 23 | } 24 | -------------------------------------------------------------------------------- /services/contact/internal/domain/group/name/type.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | var ( 8 | MaxLength = 250 9 | ErrWrongLength = errors.Errorf("name must be less than or equal to %d characters", MaxLength) 10 | ) 11 | 12 | type Name struct { 13 | value string 14 | } 15 | 16 | func New(name string) (Name, error) { 17 | if len([]rune(name)) > MaxLength { 18 | return Name{}, ErrWrongLength 19 | } 20 | return Name{value: name}, nil 21 | } 22 | 23 | func (n Name) Value() string { 24 | return n.value 25 | } 26 | -------------------------------------------------------------------------------- /services/contact/internal/domain/group/type.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | 8 | "architecture_go/services/contact/internal/domain/group/description" 9 | "architecture_go/services/contact/internal/domain/group/name" 10 | ) 11 | 12 | type Group struct { 13 | id uuid.UUID 14 | createdAt time.Time 15 | modifiedAt time.Time 16 | name name.Name 17 | description description.Description 18 | contactCount uint64 19 | } 20 | 21 | func NewWithID(id uuid.UUID, createdAt time.Time, modifiedAt time.Time, name name.Name, description description.Description, contactCount uint64) *Group { 22 | return &Group{ 23 | id: id, 24 | createdAt: createdAt.UTC(), 25 | modifiedAt: modifiedAt.UTC(), 26 | name: name, 27 | description: description, 28 | contactCount: contactCount, 29 | } 30 | } 31 | 32 | func New(name name.Name, description description.Description) *Group { 33 | var timeNow = time.Now().UTC() 34 | return &Group{ 35 | id: uuid.New(), 36 | name: name, 37 | description: description, 38 | createdAt: timeNow, 39 | modifiedAt: timeNow, 40 | } 41 | } 42 | 43 | func (g Group) ContactCount() uint64 { 44 | return g.contactCount 45 | } 46 | 47 | func (g Group) ID() uuid.UUID { 48 | return g.id 49 | } 50 | 51 | func (g Group) CreatedAt() time.Time { 52 | return g.createdAt 53 | } 54 | 55 | func (g Group) ModifiedAt() time.Time { 56 | return g.modifiedAt 57 | } 58 | 59 | func (g Group) Name() name.Name { 60 | return g.name 61 | } 62 | 63 | func (g Group) Description() description.Description { 64 | return g.description 65 | } 66 | -------------------------------------------------------------------------------- /services/contact/internal/repository/contact/interface.go: -------------------------------------------------------------------------------- 1 | package contact 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgx/v4" 7 | 8 | "architecture_go/services/contact/internal/domain/contact" 9 | ) 10 | 11 | type Contact interface { 12 | CreateContactTx(ctx context.Context, tx pgx.Tx, contacts ...*contact.Contact) ([]*contact.Contact, error) 13 | } 14 | -------------------------------------------------------------------------------- /services/contact/internal/repository/contact/postgres/contact.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/uuid" 7 | "github.com/jackc/pgx/v4" 8 | 9 | "architecture_go/pkg/tools/transaction" 10 | "architecture_go/pkg/type/queryParameter" 11 | "architecture_go/services/contact/internal/domain/contact" 12 | "architecture_go/services/contact/internal/repository/storage/postgres/dao" 13 | ) 14 | 15 | func (r *Repository) CreateContact(contacts ...*contact.Contact) ([]*contact.Contact, error) { 16 | var ctx = context.Background() 17 | tx, err := r.db.Begin(ctx) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | defer func(ctx context.Context, t pgx.Tx) { 23 | err = transaction.Finish(ctx, t, err) 24 | }(ctx, tx) 25 | 26 | response, err := r.CreateContactTx(ctx, tx, contacts...) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return response, nil 32 | } 33 | 34 | func (r *Repository) CreateContactTx(ctx context.Context, tx pgx.Tx, contacts ...*contact.Contact) ([]*contact.Contact, error) { 35 | if len(contacts) == 0 { 36 | return []*contact.Contact{}, nil 37 | } 38 | 39 | _, err := tx.CopyFrom( 40 | ctx, 41 | pgx.Identifier{"slurm", "contact"}, 42 | dao.CreateColumnContact, 43 | r.toCopyFromSource(contacts...)) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return contacts, nil 49 | } 50 | 51 | func (r *Repository) UpdateContact(ID uuid.UUID, updateFn func(c *contact.Contact) (*contact.Contact, error)) (*contact.Contact, error) { 52 | panic("implement me") 53 | } 54 | 55 | func (r *Repository) DeleteContact(ID uuid.UUID) error { 56 | panic("implement me") 57 | } 58 | 59 | func (r *Repository) ListContact(parameter queryParameter.QueryParameter) ([]*contact.Contact, error) { 60 | panic("implement me") 61 | } 62 | 63 | func (r *Repository) ReadContactByID(ID uuid.UUID) (response *contact.Contact, err error) { 64 | panic("implement me") 65 | } 66 | 67 | func (r *Repository) CountContact() (uint64, error) { 68 | panic("implement me") 69 | } 70 | -------------------------------------------------------------------------------- /services/contact/internal/repository/contact/postgres/convereter.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "github.com/jackc/pgx/v4" 5 | 6 | "architecture_go/pkg/type/email" 7 | "architecture_go/pkg/type/gender" 8 | "architecture_go/pkg/type/phoneNumber" 9 | "architecture_go/services/contact/internal/domain/contact" 10 | "architecture_go/services/contact/internal/domain/contact/age" 11 | "architecture_go/services/contact/internal/domain/contact/name" 12 | "architecture_go/services/contact/internal/domain/contact/patronymic" 13 | "architecture_go/services/contact/internal/domain/contact/surname" 14 | "architecture_go/services/contact/internal/repository/storage/postgres/dao" 15 | ) 16 | 17 | func (r Repository) toCopyFromSource(contacts ...*contact.Contact) pgx.CopyFromSource { 18 | rows := make([][]interface{}, len(contacts)) 19 | 20 | for i, val := range contacts { 21 | rows[i] = []interface{}{ 22 | val.ID(), 23 | val.CreatedAt(), 24 | val.ModifiedAt(), 25 | val.PhoneNumber().String(), 26 | val.Email().String(), 27 | val.Name().String(), 28 | val.Surname().String(), 29 | val.Patronymic().String(), 30 | val.Age(), 31 | val.Gender(), 32 | } 33 | } 34 | return pgx.CopyFromRows(rows) 35 | } 36 | 37 | func (r Repository) toDomainContact(dao *dao.Contact) (*contact.Contact, error) { 38 | 39 | nameObject, err := name.New(dao.Name) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | surnameObject, err := surname.New(dao.Surname) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | patronymicObject, err := patronymic.New(dao.Patronymic) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | ageObject, err := age.New(dao.Age) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | localEmail, err := email.New(dao.Email) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | result, err := contact.NewWithID( 65 | dao.ID, 66 | dao.CreatedAt, 67 | dao.ModifiedAt, 68 | *phoneNumber.New(dao.PhoneNumber), 69 | localEmail, 70 | *nameObject, 71 | *surnameObject, 72 | *patronymicObject, 73 | *ageObject, 74 | gender.Gender(dao.Gender), 75 | ) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return result, nil 80 | } 81 | 82 | func (r Repository) toDomainContacts(dao []*dao.Contact) ([]*contact.Contact, error) { 83 | var result = make([]*contact.Contact, len(dao)) 84 | for i, v := range dao { 85 | c, err := r.toDomainContact(v) 86 | if err != nil { 87 | return nil, err 88 | } 89 | result[i] = c 90 | } 91 | return result, nil 92 | } 93 | -------------------------------------------------------------------------------- /services/contact/internal/repository/contact/postgres/dao/contact.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type Contact struct { 10 | ID uuid.UUID `db:"id"` 11 | CreatedAt time.Time `db:"created_at"` 12 | ModifiedAt time.Time `db:"modified_at"` 13 | 14 | Email string `db:"email"` 15 | PhoneNumber string `db:"phone_number"` 16 | 17 | Name string `db:"name"` 18 | Surname string `db:"surname"` 19 | Patronymic string `db:"patronymic"` 20 | 21 | Age uint64 `db:"age"` 22 | Gender uint8 `db:"gender"` 23 | } 24 | 25 | var CreateColumnContact = []string{ 26 | "id", 27 | "created_at", 28 | "modified_at", 29 | "phone_number", 30 | "email", 31 | "name", 32 | "surname", 33 | "patronymic", 34 | "age", 35 | "gender", 36 | } 37 | 38 | var CreateColumnContactInGroup = []string{ 39 | "created_at", 40 | "modified_at", 41 | "group_id", 42 | "contact_id", 43 | } 44 | -------------------------------------------------------------------------------- /services/contact/internal/repository/contact/postgres/repository.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "github.com/Masterminds/squirrel" 5 | "github.com/jackc/pgx/v4/pgxpool" 6 | ) 7 | 8 | type Repository struct { 9 | db *pgxpool.Pool 10 | genSQL squirrel.StatementBuilderType 11 | 12 | options Options 13 | } 14 | 15 | type Options struct { 16 | DefaultLimit uint64 17 | DefaultOffset uint64 18 | } 19 | 20 | func New(db *pgxpool.Pool, o Options) (*Repository, error) { 21 | 22 | var r = &Repository{ 23 | genSQL: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), 24 | db: db, 25 | } 26 | 27 | r.SetOptions(o) 28 | return r, nil 29 | } 30 | 31 | func (r *Repository) SetOptions(options Options) { 32 | if options.DefaultLimit == 0 { 33 | options.DefaultLimit = 10 34 | } 35 | 36 | if r.options != options { 37 | r.options = options 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /services/contact/internal/repository/group/interface.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/jackc/pgx/v4" 6 | "robovoice/micro-services/_helpers/context" 7 | ) 8 | 9 | type Group interface { 10 | UpdateGroupsContactCountByFilters(ctx context.Context, tx pgx.Tx, ID uuid.UUID) error 11 | } 12 | -------------------------------------------------------------------------------- /services/contact/internal/repository/group/postgres/contactInGroup.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/Masterminds/squirrel" 8 | "github.com/google/uuid" 9 | "github.com/jackc/pgx/v4" 10 | 11 | "architecture_go/pkg/tools/transaction" 12 | "architecture_go/services/contact/internal/domain/contact" 13 | "architecture_go/services/contact/internal/repository/storage/postgres/dao" 14 | ) 15 | 16 | func (r *Repository) CreateContactIntoGroup(groupID uuid.UUID, contacts ...*contact.Contact) ([]*contact.Contact, error) { 17 | var ctx = context.Background() 18 | 19 | tx, err := r.db.Begin(ctx) 20 | if err != nil { 21 | return nil, err 22 | } 23 | defer func(ctx context.Context, t pgx.Tx) { 24 | err = transaction.Finish(ctx, t, err) 25 | }(ctx, tx) 26 | 27 | response, err := r.repoContact.CreateContactTx(ctx, tx, contacts...) 28 | if err != nil { 29 | return nil, err 30 | } 31 | var contactIDs = make([]uuid.UUID, len(response)) 32 | for i, c := range response { 33 | contactIDs[i] = c.ID() 34 | } 35 | 36 | if err = r.fillGroupTx(ctx, tx, groupID, contactIDs...); err != nil { 37 | return nil, err 38 | } 39 | 40 | return response, nil 41 | } 42 | 43 | func (r *Repository) DeleteContactFromGroup(groupID, contactID uuid.UUID) error { 44 | var ctx = context.Background() 45 | 46 | tx, err := r.db.Begin(ctx) 47 | if err != nil { 48 | return err 49 | } 50 | defer func(ctx context.Context, t pgx.Tx) { 51 | err = transaction.Finish(ctx, t, err) 52 | }(ctx, tx) 53 | 54 | query, args, err := r.genSQL. 55 | Delete("slurm.contact_in_group"). 56 | Where(squirrel.Eq{"contact_id": contactID, "group_id": groupID}). 57 | ToSql() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | _, err = tx.Exec(ctx, query, args...) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | if err = r.updateGroupContactCount(ctx, tx, groupID); err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (r *Repository) AddContactsToGroup(groupID uuid.UUID, contactIDs ...uuid.UUID) error { 75 | var ctx = context.Background() 76 | 77 | tx, err := r.db.Begin(ctx) 78 | if err != nil { 79 | return err 80 | } 81 | defer func(ctx context.Context, t pgx.Tx) { 82 | err = transaction.Finish(ctx, t, err) 83 | }(ctx, tx) 84 | 85 | if err = r.fillGroupTx(ctx, tx, groupID, contactIDs...); err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | 91 | func (r *Repository) fillGroupTx(ctx context.Context, tx pgx.Tx, groupID uuid.UUID, contactIDs ...uuid.UUID) error { 92 | _, mapExist, err := r.checkExistContactInGroup(ctx, tx, groupID, contactIDs...) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | for i := 0; i < len(contactIDs); { 98 | var contactID = contactIDs[i] 99 | if exist := mapExist[contactID]; exist { 100 | contactIDs[i] = contactIDs[len(contactIDs)-1] 101 | contactIDs = contactIDs[:len(contactIDs)-1] 102 | continue 103 | } 104 | i++ 105 | } 106 | 107 | if len(contactIDs) == 0 { 108 | return nil 109 | } 110 | 111 | var rows [][]interface{} 112 | var timeNow = time.Now().UTC() 113 | for _, contactID := range contactIDs { 114 | rows = append(rows, []interface{}{ 115 | timeNow, 116 | timeNow, 117 | groupID, 118 | contactID, 119 | }) 120 | } 121 | 122 | _, err = tx.CopyFrom( 123 | ctx, 124 | pgx.Identifier{"slurm", "contact_in_group"}, 125 | dao.CreateColumnContactInGroup, 126 | pgx.CopyFromRows(rows), 127 | ) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | if err = r.updateGroupContactCount(ctx, tx, groupID); err != nil { 133 | return err 134 | } 135 | 136 | return nil 137 | } 138 | 139 | // checkExistContactInGroup 140 | // return listExist -- list existing contactID 141 | // return mapExist -- mapping contact ID how exist or not exist 142 | func (r *Repository) checkExistContactInGroup(ctx context.Context, tx pgx.Tx, groupID uuid.UUID, contactIDs ...uuid.UUID) (listExist []uuid.UUID, mapExist map[uuid.UUID]bool, err error) { 143 | listExist = make([]uuid.UUID, 0) 144 | mapExist = make(map[uuid.UUID]bool) 145 | 146 | if len(contactIDs) == 0 { 147 | return listExist, mapExist, nil 148 | } 149 | 150 | query, args, err := r.genSQL. 151 | Select("contact_id"). 152 | From("slurm.contact_in_group"). 153 | Where(squirrel.Eq{"contact_id": contactIDs, "group_id": groupID}).ToSql() 154 | 155 | if err != nil { 156 | return nil, nil, err 157 | } 158 | 159 | rows, err := tx.Query(ctx, query, args...) 160 | if err != nil { 161 | return nil, nil, err 162 | } 163 | 164 | for rows.Next() { 165 | var contactID = uuid.UUID{} 166 | 167 | if err = rows.Scan(&contactID); err != nil { 168 | return nil, nil, err 169 | } 170 | 171 | listExist = append(listExist, contactID) 172 | mapExist[contactID] = true 173 | } 174 | 175 | for _, contactID := range contactIDs { 176 | if _, ok := mapExist[contactID]; !ok { 177 | mapExist[contactID] = false 178 | } 179 | } 180 | 181 | if err = rows.Err(); err != nil { 182 | return nil, nil, err 183 | } 184 | 185 | return listExist, mapExist, nil 186 | } 187 | -------------------------------------------------------------------------------- /services/contact/internal/repository/group/postgres/convereter.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "github.com/jackc/pgx/v4" 5 | 6 | "architecture_go/pkg/type/email" 7 | "architecture_go/pkg/type/gender" 8 | "architecture_go/pkg/type/phoneNumber" 9 | "architecture_go/services/contact/internal/domain/contact" 10 | "architecture_go/services/contact/internal/domain/contact/age" 11 | "architecture_go/services/contact/internal/domain/contact/name" 12 | "architecture_go/services/contact/internal/domain/contact/patronymic" 13 | "architecture_go/services/contact/internal/domain/contact/surname" 14 | "architecture_go/services/contact/internal/repository/storage/postgres/dao" 15 | ) 16 | 17 | func (r Repository) toCopyFromSource(contacts ...*contact.Contact) pgx.CopyFromSource { 18 | rows := make([][]interface{}, len(contacts)) 19 | 20 | for i, val := range contacts { 21 | rows[i] = []interface{}{ 22 | val.ID(), 23 | val.CreatedAt(), 24 | val.ModifiedAt(), 25 | val.PhoneNumber().String(), 26 | val.Email().String(), 27 | val.Name().String(), 28 | val.Surname().String(), 29 | val.Patronymic().String(), 30 | val.Age(), 31 | val.Gender(), 32 | } 33 | } 34 | return pgx.CopyFromRows(rows) 35 | } 36 | 37 | func (r Repository) toDomainContact(dao *dao.Contact) (*contact.Contact, error) { 38 | 39 | nameObject, err := name.New(dao.Name) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | surnameObject, err := surname.New(dao.Surname) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | patronymicObject, err := patronymic.New(dao.Patronymic) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | ageObject, err := age.New(dao.Age) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | localEmail, err := email.New(dao.Email) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | result, err := contact.NewWithID( 65 | dao.ID, 66 | dao.CreatedAt, 67 | dao.ModifiedAt, 68 | *phoneNumber.New(dao.PhoneNumber), 69 | localEmail, 70 | *nameObject, 71 | *surnameObject, 72 | *patronymicObject, 73 | *ageObject, 74 | gender.Gender(dao.Gender), 75 | ) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return result, nil 80 | } 81 | 82 | func (r Repository) toDomainContacts(dao []*dao.Contact) ([]*contact.Contact, error) { 83 | var result = make([]*contact.Contact, len(dao)) 84 | for i, v := range dao { 85 | c, err := r.toDomainContact(v) 86 | if err != nil { 87 | return nil, err 88 | } 89 | result[i] = c 90 | } 91 | return result, nil 92 | } 93 | -------------------------------------------------------------------------------- /services/contact/internal/repository/group/postgres/dao/group.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | 8 | "architecture_go/services/contact/internal/domain/group" 9 | "architecture_go/services/contact/internal/domain/group/description" 10 | "architecture_go/services/contact/internal/domain/group/name" 11 | ) 12 | 13 | type Group struct { 14 | ID uuid.UUID `db:"id"` 15 | Name string `db:"name"` 16 | Description string `db:"description"` 17 | CreatedAt time.Time `db:"created_at"` 18 | ModifiedAt time.Time `db:"modified_at"` 19 | ContactCount uint64 `db:"contact_count"` 20 | IsArchived bool `db:"is_archived"` 21 | } 22 | 23 | func (g *Group) ToDomainGroup() (*group.Group, error) { 24 | gN, err := name.New(g.Name) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | gD, err := description.New(g.Description) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return group.NewWithID( 34 | g.ID, 35 | g.CreatedAt, 36 | g.ModifiedAt, 37 | gN, 38 | gD, 39 | g.ContactCount, 40 | ), nil 41 | } 42 | -------------------------------------------------------------------------------- /services/contact/internal/repository/group/postgres/group.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "time" 8 | 9 | "github.com/Masterminds/squirrel" 10 | "github.com/georgysavva/scany/pgxscan" 11 | "github.com/google/uuid" 12 | "github.com/jackc/pgx/v4" 13 | 14 | "architecture_go/pkg/tools/converter" 15 | "architecture_go/pkg/tools/transaction" 16 | "architecture_go/pkg/type/columnCode" 17 | "architecture_go/pkg/type/queryParameter" 18 | "architecture_go/services/contact/internal/domain/group" 19 | "architecture_go/services/contact/internal/repository/storage/postgres/dao" 20 | ) 21 | 22 | var mappingSort = map[columnCode.ColumnCode]string{ 23 | "id": "id", 24 | "name": "name", 25 | "description": "description", 26 | } 27 | 28 | func (r *Repository) CreateGroup(group *group.Group) (*group.Group, error) { 29 | query, args, err := r.genSQL.Insert("slurm.group"). 30 | Columns( 31 | "id", 32 | "name", 33 | "description", 34 | "created_at", 35 | "modified_at", 36 | ). 37 | Values( 38 | group.ID(), 39 | group.Name().Value(), 40 | group.Description().Value(), 41 | group.CreatedAt(), 42 | group.ModifiedAt()). 43 | ToSql() 44 | if err != nil { 45 | return nil, err 46 | } 47 | var ctx = context.Background() 48 | 49 | if _, err = r.db.Exec(ctx, query, args...); err != nil { 50 | return nil, err 51 | } 52 | return group, nil 53 | } 54 | 55 | func (r *Repository) UpdateGroup(ID uuid.UUID, updateFn func(group *group.Group) (*group.Group, error)) (*group.Group, error) { 56 | var ctx = context.Background() 57 | 58 | tx, err := r.db.Begin(ctx) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | defer func(ctx context.Context, t pgx.Tx) { 64 | err = transaction.Finish(ctx, t, err) 65 | }(ctx, tx) 66 | 67 | upGroup, err := r.oneGroupTx(ctx, tx, ID) 68 | if err != nil { 69 | return nil, err 70 | } 71 | groupForUpdate, err := updateFn(upGroup) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | query, args, err := r.genSQL.Update("slurm.group"). 77 | Set("name", groupForUpdate.Name().Value()). 78 | Set("description", groupForUpdate.Description().Value()). 79 | Set("modified_at", groupForUpdate.ModifiedAt()). 80 | Where(squirrel.And{ 81 | squirrel.Eq{ 82 | "id": ID, 83 | "is_archived": false, 84 | }, 85 | }). 86 | Suffix(`RETURNING 87 | id, 88 | name, 89 | description, 90 | created_at, 91 | modified_at`, 92 | ). 93 | ToSql() 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | rows, err := tx.Query(ctx, query, args...) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | var daoGroup []*dao.Group 104 | if err = pgxscan.ScanAll(&daoGroup, rows); err != nil { 105 | return nil, err 106 | } 107 | 108 | return groupForUpdate, nil 109 | } 110 | 111 | func (r *Repository) DeleteGroup(ID uuid.UUID) error { 112 | var ctx = context.Background() 113 | 114 | tx, err := r.db.Begin(ctx) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | defer func(ctx context.Context, t pgx.Tx) { 120 | err = transaction.Finish(ctx, t, err) 121 | }(ctx, tx) 122 | 123 | if err = r.deleteGroupTx(ctx, tx, ID); err != nil { 124 | return err 125 | } 126 | 127 | return nil 128 | } 129 | 130 | func (r *Repository) deleteGroupTx(ctx context.Context, tx pgx.Tx, ID uuid.UUID) error { 131 | query, args, err := r.genSQL.Update("slurm.group"). 132 | Set("is_archived", true). 133 | Set("modified_at", time.Now().UTC()). 134 | Where(squirrel.Eq{ 135 | "id": ID, 136 | "is_archived": false, 137 | }).ToSql() 138 | 139 | if err != nil { 140 | return err 141 | } 142 | 143 | if _, errEx := tx.Exec(ctx, query, args...); errEx != nil { 144 | return err 145 | } 146 | 147 | if err = r.clearGroupTx(ctx, tx, ID); err != nil { 148 | return err 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func (r *Repository) clearGroupTx(ctx context.Context, tx pgx.Tx, groupID uuid.UUID) error { 155 | query, args, err := r.genSQL. 156 | Delete("slurm.contact_in_group"). 157 | Where(squirrel.Eq{"group_id": groupID}). 158 | ToSql() 159 | if err != nil { 160 | return err 161 | } 162 | 163 | if _, err = tx.Exec(ctx, query, args...); err != nil { 164 | return err 165 | } 166 | 167 | if err = r.updateGroupContactCount(ctx, tx, groupID); err != nil { 168 | return err 169 | } 170 | 171 | return nil 172 | } 173 | 174 | func (r *Repository) ListGroup(parameter queryParameter.QueryParameter) ([]*group.Group, error) { 175 | var ctx = context.Background() 176 | 177 | tx, err := r.db.Begin(ctx) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | defer func(ctx context.Context, t pgx.Tx) { 183 | err = transaction.Finish(ctx, t, err) 184 | }(ctx, tx) 185 | 186 | response, err := r.listGroupTx(ctx, tx, parameter) 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | return response, nil 192 | } 193 | 194 | func (r *Repository) listGroupTx(ctx context.Context, tx pgx.Tx, parameter queryParameter.QueryParameter) ([]*group.Group, error) { 195 | var result []*group.Group 196 | 197 | var builder = r.genSQL.Select( 198 | "id", 199 | "name", 200 | "description", 201 | "created_at", 202 | "modified_at", 203 | "contact_count", 204 | "is_archived", 205 | ). 206 | From("slurm.group") 207 | 208 | builder = builder.Where(squirrel.Eq{"is_archived": false}) 209 | 210 | if len(parameter.Sorts) > 0 { 211 | builder = builder.OrderBy(parameter.Sorts.Parsing(mappingSort)...) 212 | } else { 213 | builder = builder.OrderBy("created_at DESC") 214 | } 215 | 216 | if parameter.Pagination.Limit > 0 { 217 | builder = builder.Limit(parameter.Pagination.Limit) 218 | } 219 | if parameter.Pagination.Offset > 0 { 220 | builder = builder.Offset(parameter.Pagination.Offset) 221 | } 222 | 223 | query, args, err := builder.ToSql() 224 | rows, err := tx.Query(ctx, query, args...) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | var groups []*dao.Group 230 | if err = pgxscan.ScanAll(&groups, rows); err != nil { 231 | return nil, err 232 | } 233 | 234 | for _, g := range groups { 235 | domainGroup, err := g.ToDomainGroup() 236 | if err != nil { 237 | return nil, err 238 | } 239 | result = append(result, domainGroup) 240 | } 241 | return result, nil 242 | } 243 | 244 | func (r *Repository) ReadGroupByID(ID uuid.UUID) (*group.Group, error) { 245 | var ctx = context.Background() 246 | 247 | tx, err := r.db.Begin(ctx) 248 | if err != nil { 249 | return nil, err 250 | } 251 | 252 | defer func(ctx context.Context, t pgx.Tx) { 253 | err = transaction.Finish(ctx, t, err) 254 | }(ctx, tx) 255 | 256 | response, err := r.oneGroupTx(ctx, tx, ID) 257 | if err != nil { 258 | return nil, err 259 | } 260 | 261 | return response, nil 262 | } 263 | 264 | func (r *Repository) oneGroupTx(ctx context.Context, tx pgx.Tx, ID uuid.UUID) (response *group.Group, err error) { 265 | 266 | var builder = r.genSQL.Select( 267 | "id", 268 | "name", 269 | "description", 270 | "created_at", 271 | "modified_at", 272 | "contact_count", 273 | "is_archived", 274 | ). 275 | From("slurm.group") 276 | 277 | builder = builder.Where(squirrel.Eq{"is_archived": false, "id": ID}) 278 | 279 | query, args, err := builder.ToSql() 280 | rows, err := tx.Query(ctx, query, args...) 281 | if err != nil { 282 | return nil, err 283 | } 284 | 285 | var daoGroup []*dao.Group 286 | if err = pgxscan.ScanAll(&daoGroup, rows); err != nil { 287 | return nil, err 288 | } 289 | 290 | if len(daoGroup) == 0 { 291 | return nil, errors.New("group not found") 292 | } 293 | 294 | return daoGroup[0].ToDomainGroup() 295 | 296 | } 297 | 298 | func (r *Repository) CountGroup() (uint64, error) { 299 | var ctx = context.Background() 300 | 301 | var builder = r.genSQL.Select( 302 | "COUNT(id)", 303 | ).From("slurm.group") 304 | 305 | builder = builder.Where(squirrel.Eq{"is_archived": false}) 306 | 307 | query, args, err := builder.ToSql() 308 | if err != nil { 309 | return 0, err 310 | } 311 | 312 | row := r.db.QueryRow(ctx, query, args...) 313 | var total uint64 314 | 315 | if err = row.Scan(&total); err != nil { 316 | return 0, err 317 | } 318 | 319 | return total, nil 320 | } 321 | 322 | func (r *Repository) UpdateGroupsContactCountByFilters(ctx context.Context, tx pgx.Tx, ID uuid.UUID) error { 323 | 324 | builder := r.genSQL.Select("contact_in_group.group_id"). 325 | From("slurm.contact_in_group"). 326 | InnerJoin("slurm.contact ON contact_in_group.contact_id = contact.id"). 327 | GroupBy("contact_in_group.group_id") 328 | 329 | builder = builder.Where(squirrel.Eq{"contact_in_group.contact_id": ID}) 330 | 331 | query, args, err := builder.ToSql() 332 | if err != nil { 333 | return err 334 | } 335 | 336 | rows, err := tx.Query(ctx, query, args...) 337 | if err != nil { 338 | return err 339 | } 340 | var groupIDs []uuid.UUID 341 | for rows.Next() { 342 | var groupID sql.NullString 343 | if err = rows.Scan(&groupID); err != nil { 344 | return err 345 | } 346 | groupIDs = append(groupIDs, converter.StringToUUID(groupID.String)) 347 | } 348 | 349 | for _, groupID := range groupIDs { 350 | if err = r.updateGroupContactCount(ctx, tx, groupID); err != nil { 351 | return err 352 | } 353 | } 354 | 355 | if err = rows.Err(); err != nil { 356 | return err 357 | } 358 | 359 | return nil 360 | } 361 | 362 | func (r *Repository) updateGroupContactCount(ctx context.Context, tx pgx.Tx, groupID uuid.UUID) error { 363 | subSelect := r.genSQL.Select("count(contact_in_group.id)"). 364 | From("slurm.contact_in_group"). 365 | InnerJoin("slurm.contact ON contact_in_group.contact_id = contact.id"). 366 | Where(squirrel.Eq{"group_id": groupID, "is_archived": false}) 367 | 368 | query, _, err := r.genSQL. 369 | Update("slurm.group"). 370 | Set("contact_count", subSelect). 371 | Where(squirrel.Eq{"id": groupID}). 372 | ToSql() 373 | if err != nil { 374 | return err 375 | } 376 | 377 | var args = []interface{}{groupID, false} 378 | 379 | if _, err = tx.Exec(ctx, query, args...); err != nil { 380 | return err 381 | } 382 | return nil 383 | } 384 | -------------------------------------------------------------------------------- /services/contact/internal/repository/group/postgres/repository.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Masterminds/squirrel" 7 | "github.com/jackc/pgx/v4/pgxpool" 8 | "github.com/spf13/viper" 9 | 10 | "github.com/pressly/goose" 11 | 12 | "architecture_go/services/contact/internal/repository/contact" 13 | ) 14 | 15 | func init() { 16 | viper.SetDefault("MIGRATIONS_DIR", "./services/contact/internal/repository/storage/postgres/migrations") 17 | } 18 | 19 | type Repository struct { 20 | db *pgxpool.Pool 21 | genSQL squirrel.StatementBuilderType 22 | 23 | repoContact contact.Contact 24 | 25 | options Options 26 | } 27 | 28 | type Options struct { 29 | DefaultLimit uint64 30 | DefaultOffset uint64 31 | } 32 | 33 | func New(db *pgxpool.Pool, repoContact contact.Contact, o Options) (*Repository, error) { 34 | if err := migrations(db); err != nil { 35 | return nil, err 36 | } 37 | 38 | var r = &Repository{ 39 | genSQL: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), 40 | repoContact: repoContact, 41 | db: db, 42 | } 43 | 44 | r.SetOptions(o) 45 | return r, nil 46 | } 47 | 48 | func (r *Repository) SetOptions(options Options) { 49 | if options.DefaultLimit == 0 { 50 | options.DefaultLimit = 10 51 | } 52 | 53 | if r.options != options { 54 | r.options = options 55 | } 56 | } 57 | 58 | func migrations(pool *pgxpool.Pool) (err error) { 59 | db, err := goose.OpenDBWithDriver("postgres", pool.Config().ConnConfig.ConnString()) 60 | if err != nil { 61 | return err 62 | } 63 | defer func() { 64 | if errClose := db.Close(); errClose != nil { 65 | err = errClose 66 | return 67 | } 68 | }() 69 | 70 | dir := viper.GetString("MIGRATIONS_DIR") 71 | goose.SetTableName("contact_version") 72 | if err = goose.Run("up", db, dir); err != nil { 73 | return fmt.Errorf("goose %s error : %w", "up", err) 74 | } 75 | return 76 | } 77 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/mock/Contact.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.11.0. DO NOT EDIT. 2 | 3 | package mockStorage 4 | 5 | import ( 6 | context "architecture_go/pkg/type/context" 7 | contact "architecture_go/services/contact/internal/domain/contact" 8 | 9 | mock "github.com/stretchr/testify/mock" 10 | 11 | queryParameter "architecture_go/pkg/type/queryParameter" 12 | 13 | testing "testing" 14 | 15 | uuid "github.com/google/uuid" 16 | ) 17 | 18 | // Contact is an autogenerated mock type for the Contact type 19 | type Contact struct { 20 | mock.Mock 21 | } 22 | 23 | // CountContact provides a mock function with given fields: ctx 24 | func (_m *Contact) CountContact(ctx context.Context) (uint64, error) { 25 | ret := _m.Called(ctx) 26 | 27 | var r0 uint64 28 | if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { 29 | r0 = rf(ctx) 30 | } else { 31 | r0 = ret.Get(0).(uint64) 32 | } 33 | 34 | var r1 error 35 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 36 | r1 = rf(ctx) 37 | } else { 38 | r1 = ret.Error(1) 39 | } 40 | 41 | return r0, r1 42 | } 43 | 44 | // CreateContact provides a mock function with given fields: ctx, contacts 45 | func (_m *Contact) CreateContact(ctx context.Context, contacts ...*contact.Contact) ([]*contact.Contact, error) { 46 | _va := make([]interface{}, len(contacts)) 47 | for _i := range contacts { 48 | _va[_i] = contacts[_i] 49 | } 50 | var _ca []interface{} 51 | _ca = append(_ca, ctx) 52 | _ca = append(_ca, _va...) 53 | ret := _m.Called(_ca...) 54 | 55 | var r0 []*contact.Contact 56 | if rf, ok := ret.Get(0).(func(context.Context, ...*contact.Contact) []*contact.Contact); ok { 57 | r0 = rf(ctx, contacts...) 58 | } else { 59 | if ret.Get(0) != nil { 60 | r0 = ret.Get(0).([]*contact.Contact) 61 | } 62 | } 63 | 64 | var r1 error 65 | if rf, ok := ret.Get(1).(func(context.Context, ...*contact.Contact) error); ok { 66 | r1 = rf(ctx, contacts...) 67 | } else { 68 | r1 = ret.Error(1) 69 | } 70 | 71 | return r0, r1 72 | } 73 | 74 | // DeleteContact provides a mock function with given fields: ctx, ID 75 | func (_m *Contact) DeleteContact(ctx context.Context, ID uuid.UUID) error { 76 | ret := _m.Called(ctx, ID) 77 | 78 | var r0 error 79 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) error); ok { 80 | r0 = rf(ctx, ID) 81 | } else { 82 | r0 = ret.Error(0) 83 | } 84 | 85 | return r0 86 | } 87 | 88 | // ListContact provides a mock function with given fields: ctx, parameter 89 | func (_m *Contact) ListContact(ctx context.Context, parameter queryParameter.QueryParameter) ([]*contact.Contact, error) { 90 | ret := _m.Called(ctx, parameter) 91 | 92 | var r0 []*contact.Contact 93 | if rf, ok := ret.Get(0).(func(context.Context, queryParameter.QueryParameter) []*contact.Contact); ok { 94 | r0 = rf(ctx, parameter) 95 | } else { 96 | if ret.Get(0) != nil { 97 | r0 = ret.Get(0).([]*contact.Contact) 98 | } 99 | } 100 | 101 | var r1 error 102 | if rf, ok := ret.Get(1).(func(context.Context, queryParameter.QueryParameter) error); ok { 103 | r1 = rf(ctx, parameter) 104 | } else { 105 | r1 = ret.Error(1) 106 | } 107 | 108 | return r0, r1 109 | } 110 | 111 | // ReadContactByID provides a mock function with given fields: ctx, ID 112 | func (_m *Contact) ReadContactByID(ctx context.Context, ID uuid.UUID) (*contact.Contact, error) { 113 | ret := _m.Called(ctx, ID) 114 | 115 | var r0 *contact.Contact 116 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *contact.Contact); ok { 117 | r0 = rf(ctx, ID) 118 | } else { 119 | if ret.Get(0) != nil { 120 | r0 = ret.Get(0).(*contact.Contact) 121 | } 122 | } 123 | 124 | var r1 error 125 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { 126 | r1 = rf(ctx, ID) 127 | } else { 128 | r1 = ret.Error(1) 129 | } 130 | 131 | return r0, r1 132 | } 133 | 134 | // UpdateContact provides a mock function with given fields: ctx, ID, updateFn 135 | func (_m *Contact) UpdateContact(ctx context.Context, ID uuid.UUID, updateFn func(*contact.Contact) (*contact.Contact, error)) (*contact.Contact, error) { 136 | ret := _m.Called(ctx, ID, updateFn) 137 | 138 | var r0 *contact.Contact 139 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, func(*contact.Contact) (*contact.Contact, error)) *contact.Contact); ok { 140 | r0 = rf(ctx, ID, updateFn) 141 | } else { 142 | if ret.Get(0) != nil { 143 | r0 = ret.Get(0).(*contact.Contact) 144 | } 145 | } 146 | 147 | var r1 error 148 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, func(*contact.Contact) (*contact.Contact, error)) error); ok { 149 | r1 = rf(ctx, ID, updateFn) 150 | } else { 151 | r1 = ret.Error(1) 152 | } 153 | 154 | return r0, r1 155 | } 156 | 157 | // NewContact creates a new instance of Contact. It also registers a cleanup function to assert the mocks expectations. 158 | func NewContact(t testing.TB) *Contact { 159 | mock := &Contact{} 160 | 161 | t.Cleanup(func() { mock.AssertExpectations(t) }) 162 | 163 | return mock 164 | } 165 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/mock/ContactInGroup.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.11.0. DO NOT EDIT. 2 | 3 | package mockStorage 4 | 5 | import ( 6 | context "architecture_go/pkg/type/context" 7 | contact "architecture_go/services/contact/internal/domain/contact" 8 | 9 | mock "github.com/stretchr/testify/mock" 10 | 11 | testing "testing" 12 | 13 | uuid "github.com/google/uuid" 14 | ) 15 | 16 | // ContactInGroup is an autogenerated mock type for the ContactInGroup type 17 | type ContactInGroup struct { 18 | mock.Mock 19 | } 20 | 21 | // AddContactsToGroup provides a mock function with given fields: ctx, groupID, contactIDs 22 | func (_m *ContactInGroup) AddContactsToGroup(ctx context.Context, groupID uuid.UUID, contactIDs ...uuid.UUID) error { 23 | _va := make([]interface{}, len(contactIDs)) 24 | for _i := range contactIDs { 25 | _va[_i] = contactIDs[_i] 26 | } 27 | var _ca []interface{} 28 | _ca = append(_ca, ctx, groupID) 29 | _ca = append(_ca, _va...) 30 | ret := _m.Called(_ca...) 31 | 32 | var r0 error 33 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, ...uuid.UUID) error); ok { 34 | r0 = rf(ctx, groupID, contactIDs...) 35 | } else { 36 | r0 = ret.Error(0) 37 | } 38 | 39 | return r0 40 | } 41 | 42 | // CreateContactIntoGroup provides a mock function with given fields: ctx, groupID, contacts 43 | func (_m *ContactInGroup) CreateContactIntoGroup(ctx context.Context, groupID uuid.UUID, contacts ...*contact.Contact) ([]*contact.Contact, error) { 44 | _va := make([]interface{}, len(contacts)) 45 | for _i := range contacts { 46 | _va[_i] = contacts[_i] 47 | } 48 | var _ca []interface{} 49 | _ca = append(_ca, ctx, groupID) 50 | _ca = append(_ca, _va...) 51 | ret := _m.Called(_ca...) 52 | 53 | var r0 []*contact.Contact 54 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, ...*contact.Contact) []*contact.Contact); ok { 55 | r0 = rf(ctx, groupID, contacts...) 56 | } else { 57 | if ret.Get(0) != nil { 58 | r0 = ret.Get(0).([]*contact.Contact) 59 | } 60 | } 61 | 62 | var r1 error 63 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, ...*contact.Contact) error); ok { 64 | r1 = rf(ctx, groupID, contacts...) 65 | } else { 66 | r1 = ret.Error(1) 67 | } 68 | 69 | return r0, r1 70 | } 71 | 72 | // DeleteContactFromGroup provides a mock function with given fields: ctx, groupID, contactID 73 | func (_m *ContactInGroup) DeleteContactFromGroup(ctx context.Context, groupID uuid.UUID, contactID uuid.UUID) error { 74 | ret := _m.Called(ctx, groupID, contactID) 75 | 76 | var r0 error 77 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) error); ok { 78 | r0 = rf(ctx, groupID, contactID) 79 | } else { 80 | r0 = ret.Error(0) 81 | } 82 | 83 | return r0 84 | } 85 | 86 | // NewContactInGroup creates a new instance of ContactInGroup. It also registers a cleanup function to assert the mocks expectations. 87 | func NewContactInGroup(t testing.TB) *ContactInGroup { 88 | mock := &ContactInGroup{} 89 | 90 | t.Cleanup(func() { mock.AssertExpectations(t) }) 91 | 92 | return mock 93 | } 94 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/mock/ContactReader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.11.0. DO NOT EDIT. 2 | 3 | package mockStorage 4 | 5 | import ( 6 | context "architecture_go/pkg/type/context" 7 | contact "architecture_go/services/contact/internal/domain/contact" 8 | 9 | mock "github.com/stretchr/testify/mock" 10 | 11 | queryParameter "architecture_go/pkg/type/queryParameter" 12 | 13 | testing "testing" 14 | 15 | uuid "github.com/google/uuid" 16 | ) 17 | 18 | // ContactReader is an autogenerated mock type for the ContactReader type 19 | type ContactReader struct { 20 | mock.Mock 21 | } 22 | 23 | // CountContact provides a mock function with given fields: ctx 24 | func (_m *ContactReader) CountContact(ctx context.Context) (uint64, error) { 25 | ret := _m.Called(ctx) 26 | 27 | var r0 uint64 28 | if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { 29 | r0 = rf(ctx) 30 | } else { 31 | r0 = ret.Get(0).(uint64) 32 | } 33 | 34 | var r1 error 35 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 36 | r1 = rf(ctx) 37 | } else { 38 | r1 = ret.Error(1) 39 | } 40 | 41 | return r0, r1 42 | } 43 | 44 | // ListContact provides a mock function with given fields: ctx, parameter 45 | func (_m *ContactReader) ListContact(ctx context.Context, parameter queryParameter.QueryParameter) ([]*contact.Contact, error) { 46 | ret := _m.Called(ctx, parameter) 47 | 48 | var r0 []*contact.Contact 49 | if rf, ok := ret.Get(0).(func(context.Context, queryParameter.QueryParameter) []*contact.Contact); ok { 50 | r0 = rf(ctx, parameter) 51 | } else { 52 | if ret.Get(0) != nil { 53 | r0 = ret.Get(0).([]*contact.Contact) 54 | } 55 | } 56 | 57 | var r1 error 58 | if rf, ok := ret.Get(1).(func(context.Context, queryParameter.QueryParameter) error); ok { 59 | r1 = rf(ctx, parameter) 60 | } else { 61 | r1 = ret.Error(1) 62 | } 63 | 64 | return r0, r1 65 | } 66 | 67 | // ReadContactByID provides a mock function with given fields: ctx, ID 68 | func (_m *ContactReader) ReadContactByID(ctx context.Context, ID uuid.UUID) (*contact.Contact, error) { 69 | ret := _m.Called(ctx, ID) 70 | 71 | var r0 *contact.Contact 72 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *contact.Contact); ok { 73 | r0 = rf(ctx, ID) 74 | } else { 75 | if ret.Get(0) != nil { 76 | r0 = ret.Get(0).(*contact.Contact) 77 | } 78 | } 79 | 80 | var r1 error 81 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { 82 | r1 = rf(ctx, ID) 83 | } else { 84 | r1 = ret.Error(1) 85 | } 86 | 87 | return r0, r1 88 | } 89 | 90 | // NewContactReader creates a new instance of ContactReader. It also registers a cleanup function to assert the mocks expectations. 91 | func NewContactReader(t testing.TB) *ContactReader { 92 | mock := &ContactReader{} 93 | 94 | t.Cleanup(func() { mock.AssertExpectations(t) }) 95 | 96 | return mock 97 | } 98 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/mock/Group.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.11.0. DO NOT EDIT. 2 | 3 | package mockStorage 4 | 5 | import ( 6 | context "architecture_go/pkg/type/context" 7 | contact "architecture_go/services/contact/internal/domain/contact" 8 | 9 | group "architecture_go/services/contact/internal/domain/group" 10 | 11 | mock "github.com/stretchr/testify/mock" 12 | 13 | queryParameter "architecture_go/pkg/type/queryParameter" 14 | 15 | testing "testing" 16 | 17 | uuid "github.com/google/uuid" 18 | ) 19 | 20 | // Group is an autogenerated mock type for the Group type 21 | type Group struct { 22 | mock.Mock 23 | } 24 | 25 | // AddContactsToGroup provides a mock function with given fields: ctx, groupID, contactIDs 26 | func (_m *Group) AddContactsToGroup(ctx context.Context, groupID uuid.UUID, contactIDs ...uuid.UUID) error { 27 | _va := make([]interface{}, len(contactIDs)) 28 | for _i := range contactIDs { 29 | _va[_i] = contactIDs[_i] 30 | } 31 | var _ca []interface{} 32 | _ca = append(_ca, ctx, groupID) 33 | _ca = append(_ca, _va...) 34 | ret := _m.Called(_ca...) 35 | 36 | var r0 error 37 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, ...uuid.UUID) error); ok { 38 | r0 = rf(ctx, groupID, contactIDs...) 39 | } else { 40 | r0 = ret.Error(0) 41 | } 42 | 43 | return r0 44 | } 45 | 46 | // CountGroup provides a mock function with given fields: ctx 47 | func (_m *Group) CountGroup(ctx context.Context) (uint64, error) { 48 | ret := _m.Called(ctx) 49 | 50 | var r0 uint64 51 | if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { 52 | r0 = rf(ctx) 53 | } else { 54 | r0 = ret.Get(0).(uint64) 55 | } 56 | 57 | var r1 error 58 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 59 | r1 = rf(ctx) 60 | } else { 61 | r1 = ret.Error(1) 62 | } 63 | 64 | return r0, r1 65 | } 66 | 67 | // CreateContactIntoGroup provides a mock function with given fields: ctx, groupID, contacts 68 | func (_m *Group) CreateContactIntoGroup(ctx context.Context, groupID uuid.UUID, contacts ...*contact.Contact) ([]*contact.Contact, error) { 69 | _va := make([]interface{}, len(contacts)) 70 | for _i := range contacts { 71 | _va[_i] = contacts[_i] 72 | } 73 | var _ca []interface{} 74 | _ca = append(_ca, ctx, groupID) 75 | _ca = append(_ca, _va...) 76 | ret := _m.Called(_ca...) 77 | 78 | var r0 []*contact.Contact 79 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, ...*contact.Contact) []*contact.Contact); ok { 80 | r0 = rf(ctx, groupID, contacts...) 81 | } else { 82 | if ret.Get(0) != nil { 83 | r0 = ret.Get(0).([]*contact.Contact) 84 | } 85 | } 86 | 87 | var r1 error 88 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, ...*contact.Contact) error); ok { 89 | r1 = rf(ctx, groupID, contacts...) 90 | } else { 91 | r1 = ret.Error(1) 92 | } 93 | 94 | return r0, r1 95 | } 96 | 97 | // CreateGroup provides a mock function with given fields: ctx, _a1 98 | func (_m *Group) CreateGroup(ctx context.Context, _a1 *group.Group) (*group.Group, error) { 99 | ret := _m.Called(ctx, _a1) 100 | 101 | var r0 *group.Group 102 | if rf, ok := ret.Get(0).(func(context.Context, *group.Group) *group.Group); ok { 103 | r0 = rf(ctx, _a1) 104 | } else { 105 | if ret.Get(0) != nil { 106 | r0 = ret.Get(0).(*group.Group) 107 | } 108 | } 109 | 110 | var r1 error 111 | if rf, ok := ret.Get(1).(func(context.Context, *group.Group) error); ok { 112 | r1 = rf(ctx, _a1) 113 | } else { 114 | r1 = ret.Error(1) 115 | } 116 | 117 | return r0, r1 118 | } 119 | 120 | // DeleteContactFromGroup provides a mock function with given fields: ctx, groupID, contactID 121 | func (_m *Group) DeleteContactFromGroup(ctx context.Context, groupID uuid.UUID, contactID uuid.UUID) error { 122 | ret := _m.Called(ctx, groupID, contactID) 123 | 124 | var r0 error 125 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) error); ok { 126 | r0 = rf(ctx, groupID, contactID) 127 | } else { 128 | r0 = ret.Error(0) 129 | } 130 | 131 | return r0 132 | } 133 | 134 | // DeleteGroup provides a mock function with given fields: ctx, ID 135 | func (_m *Group) DeleteGroup(ctx context.Context, ID uuid.UUID) error { 136 | ret := _m.Called(ctx, ID) 137 | 138 | var r0 error 139 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) error); ok { 140 | r0 = rf(ctx, ID) 141 | } else { 142 | r0 = ret.Error(0) 143 | } 144 | 145 | return r0 146 | } 147 | 148 | // ListGroup provides a mock function with given fields: ctx, parameter 149 | func (_m *Group) ListGroup(ctx context.Context, parameter queryParameter.QueryParameter) ([]*group.Group, error) { 150 | ret := _m.Called(ctx, parameter) 151 | 152 | var r0 []*group.Group 153 | if rf, ok := ret.Get(0).(func(context.Context, queryParameter.QueryParameter) []*group.Group); ok { 154 | r0 = rf(ctx, parameter) 155 | } else { 156 | if ret.Get(0) != nil { 157 | r0 = ret.Get(0).([]*group.Group) 158 | } 159 | } 160 | 161 | var r1 error 162 | if rf, ok := ret.Get(1).(func(context.Context, queryParameter.QueryParameter) error); ok { 163 | r1 = rf(ctx, parameter) 164 | } else { 165 | r1 = ret.Error(1) 166 | } 167 | 168 | return r0, r1 169 | } 170 | 171 | // ReadGroupByID provides a mock function with given fields: ctx, ID 172 | func (_m *Group) ReadGroupByID(ctx context.Context, ID uuid.UUID) (*group.Group, error) { 173 | ret := _m.Called(ctx, ID) 174 | 175 | var r0 *group.Group 176 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *group.Group); ok { 177 | r0 = rf(ctx, ID) 178 | } else { 179 | if ret.Get(0) != nil { 180 | r0 = ret.Get(0).(*group.Group) 181 | } 182 | } 183 | 184 | var r1 error 185 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { 186 | r1 = rf(ctx, ID) 187 | } else { 188 | r1 = ret.Error(1) 189 | } 190 | 191 | return r0, r1 192 | } 193 | 194 | // UpdateGroup provides a mock function with given fields: ctx, ID, updateFn 195 | func (_m *Group) UpdateGroup(ctx context.Context, ID uuid.UUID, updateFn func(*group.Group) (*group.Group, error)) (*group.Group, error) { 196 | ret := _m.Called(ctx, ID, updateFn) 197 | 198 | var r0 *group.Group 199 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, func(*group.Group) (*group.Group, error)) *group.Group); ok { 200 | r0 = rf(ctx, ID, updateFn) 201 | } else { 202 | if ret.Get(0) != nil { 203 | r0 = ret.Get(0).(*group.Group) 204 | } 205 | } 206 | 207 | var r1 error 208 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, func(*group.Group) (*group.Group, error)) error); ok { 209 | r1 = rf(ctx, ID, updateFn) 210 | } else { 211 | r1 = ret.Error(1) 212 | } 213 | 214 | return r0, r1 215 | } 216 | 217 | // NewGroup creates a new instance of Group. It also registers a cleanup function to assert the mocks expectations. 218 | func NewGroup(t testing.TB) *Group { 219 | mock := &Group{} 220 | 221 | t.Cleanup(func() { mock.AssertExpectations(t) }) 222 | 223 | return mock 224 | } 225 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/mock/GroupReader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.11.0. DO NOT EDIT. 2 | 3 | package mockStorage 4 | 5 | import ( 6 | context "architecture_go/pkg/type/context" 7 | group "architecture_go/services/contact/internal/domain/group" 8 | 9 | mock "github.com/stretchr/testify/mock" 10 | 11 | queryParameter "architecture_go/pkg/type/queryParameter" 12 | 13 | testing "testing" 14 | 15 | uuid "github.com/google/uuid" 16 | ) 17 | 18 | // GroupReader is an autogenerated mock type for the GroupReader type 19 | type GroupReader struct { 20 | mock.Mock 21 | } 22 | 23 | // CountGroup provides a mock function with given fields: ctx 24 | func (_m *GroupReader) CountGroup(ctx context.Context) (uint64, error) { 25 | ret := _m.Called(ctx) 26 | 27 | var r0 uint64 28 | if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { 29 | r0 = rf(ctx) 30 | } else { 31 | r0 = ret.Get(0).(uint64) 32 | } 33 | 34 | var r1 error 35 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 36 | r1 = rf(ctx) 37 | } else { 38 | r1 = ret.Error(1) 39 | } 40 | 41 | return r0, r1 42 | } 43 | 44 | // ListGroup provides a mock function with given fields: ctx, parameter 45 | func (_m *GroupReader) ListGroup(ctx context.Context, parameter queryParameter.QueryParameter) ([]*group.Group, error) { 46 | ret := _m.Called(ctx, parameter) 47 | 48 | var r0 []*group.Group 49 | if rf, ok := ret.Get(0).(func(context.Context, queryParameter.QueryParameter) []*group.Group); ok { 50 | r0 = rf(ctx, parameter) 51 | } else { 52 | if ret.Get(0) != nil { 53 | r0 = ret.Get(0).([]*group.Group) 54 | } 55 | } 56 | 57 | var r1 error 58 | if rf, ok := ret.Get(1).(func(context.Context, queryParameter.QueryParameter) error); ok { 59 | r1 = rf(ctx, parameter) 60 | } else { 61 | r1 = ret.Error(1) 62 | } 63 | 64 | return r0, r1 65 | } 66 | 67 | // ReadGroupByID provides a mock function with given fields: ctx, ID 68 | func (_m *GroupReader) ReadGroupByID(ctx context.Context, ID uuid.UUID) (*group.Group, error) { 69 | ret := _m.Called(ctx, ID) 70 | 71 | var r0 *group.Group 72 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *group.Group); ok { 73 | r0 = rf(ctx, ID) 74 | } else { 75 | if ret.Get(0) != nil { 76 | r0 = ret.Get(0).(*group.Group) 77 | } 78 | } 79 | 80 | var r1 error 81 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { 82 | r1 = rf(ctx, ID) 83 | } else { 84 | r1 = ret.Error(1) 85 | } 86 | 87 | return r0, r1 88 | } 89 | 90 | // NewGroupReader creates a new instance of GroupReader. It also registers a cleanup function to assert the mocks expectations. 91 | func NewGroupReader(t testing.TB) *GroupReader { 92 | mock := &GroupReader{} 93 | 94 | t.Cleanup(func() { mock.AssertExpectations(t) }) 95 | 96 | return mock 97 | } 98 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/mock/Storage.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.11.0. DO NOT EDIT. 2 | 3 | package mockStorage 4 | 5 | import ( 6 | context "architecture_go/pkg/type/context" 7 | contact "architecture_go/services/contact/internal/domain/contact" 8 | 9 | group "architecture_go/services/contact/internal/domain/group" 10 | 11 | mock "github.com/stretchr/testify/mock" 12 | 13 | queryParameter "architecture_go/pkg/type/queryParameter" 14 | 15 | testing "testing" 16 | 17 | uuid "github.com/google/uuid" 18 | ) 19 | 20 | // Storage is an autogenerated mock type for the Storage type 21 | type Storage struct { 22 | mock.Mock 23 | } 24 | 25 | // AddContactsToGroup provides a mock function with given fields: ctx, groupID, contactIDs 26 | func (_m *Storage) AddContactsToGroup(ctx context.Context, groupID uuid.UUID, contactIDs ...uuid.UUID) error { 27 | _va := make([]interface{}, len(contactIDs)) 28 | for _i := range contactIDs { 29 | _va[_i] = contactIDs[_i] 30 | } 31 | var _ca []interface{} 32 | _ca = append(_ca, ctx, groupID) 33 | _ca = append(_ca, _va...) 34 | ret := _m.Called(_ca...) 35 | 36 | var r0 error 37 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, ...uuid.UUID) error); ok { 38 | r0 = rf(ctx, groupID, contactIDs...) 39 | } else { 40 | r0 = ret.Error(0) 41 | } 42 | 43 | return r0 44 | } 45 | 46 | // CountContact provides a mock function with given fields: ctx 47 | func (_m *Storage) CountContact(ctx context.Context) (uint64, error) { 48 | ret := _m.Called(ctx) 49 | 50 | var r0 uint64 51 | if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { 52 | r0 = rf(ctx) 53 | } else { 54 | r0 = ret.Get(0).(uint64) 55 | } 56 | 57 | var r1 error 58 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 59 | r1 = rf(ctx) 60 | } else { 61 | r1 = ret.Error(1) 62 | } 63 | 64 | return r0, r1 65 | } 66 | 67 | // CountGroup provides a mock function with given fields: ctx 68 | func (_m *Storage) CountGroup(ctx context.Context) (uint64, error) { 69 | ret := _m.Called(ctx) 70 | 71 | var r0 uint64 72 | if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { 73 | r0 = rf(ctx) 74 | } else { 75 | r0 = ret.Get(0).(uint64) 76 | } 77 | 78 | var r1 error 79 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 80 | r1 = rf(ctx) 81 | } else { 82 | r1 = ret.Error(1) 83 | } 84 | 85 | return r0, r1 86 | } 87 | 88 | // CreateContact provides a mock function with given fields: ctx, contacts 89 | func (_m *Storage) CreateContact(ctx context.Context, contacts ...*contact.Contact) ([]*contact.Contact, error) { 90 | _va := make([]interface{}, len(contacts)) 91 | for _i := range contacts { 92 | _va[_i] = contacts[_i] 93 | } 94 | var _ca []interface{} 95 | _ca = append(_ca, ctx) 96 | _ca = append(_ca, _va...) 97 | ret := _m.Called(_ca...) 98 | 99 | var r0 []*contact.Contact 100 | if rf, ok := ret.Get(0).(func(context.Context, ...*contact.Contact) []*contact.Contact); ok { 101 | r0 = rf(ctx, contacts...) 102 | } else { 103 | if ret.Get(0) != nil { 104 | r0 = ret.Get(0).([]*contact.Contact) 105 | } 106 | } 107 | 108 | var r1 error 109 | if rf, ok := ret.Get(1).(func(context.Context, ...*contact.Contact) error); ok { 110 | r1 = rf(ctx, contacts...) 111 | } else { 112 | r1 = ret.Error(1) 113 | } 114 | 115 | return r0, r1 116 | } 117 | 118 | // CreateContactIntoGroup provides a mock function with given fields: ctx, groupID, contacts 119 | func (_m *Storage) CreateContactIntoGroup(ctx context.Context, groupID uuid.UUID, contacts ...*contact.Contact) ([]*contact.Contact, error) { 120 | _va := make([]interface{}, len(contacts)) 121 | for _i := range contacts { 122 | _va[_i] = contacts[_i] 123 | } 124 | var _ca []interface{} 125 | _ca = append(_ca, ctx, groupID) 126 | _ca = append(_ca, _va...) 127 | ret := _m.Called(_ca...) 128 | 129 | var r0 []*contact.Contact 130 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, ...*contact.Contact) []*contact.Contact); ok { 131 | r0 = rf(ctx, groupID, contacts...) 132 | } else { 133 | if ret.Get(0) != nil { 134 | r0 = ret.Get(0).([]*contact.Contact) 135 | } 136 | } 137 | 138 | var r1 error 139 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, ...*contact.Contact) error); ok { 140 | r1 = rf(ctx, groupID, contacts...) 141 | } else { 142 | r1 = ret.Error(1) 143 | } 144 | 145 | return r0, r1 146 | } 147 | 148 | // CreateGroup provides a mock function with given fields: ctx, _a1 149 | func (_m *Storage) CreateGroup(ctx context.Context, _a1 *group.Group) (*group.Group, error) { 150 | ret := _m.Called(ctx, _a1) 151 | 152 | var r0 *group.Group 153 | if rf, ok := ret.Get(0).(func(context.Context, *group.Group) *group.Group); ok { 154 | r0 = rf(ctx, _a1) 155 | } else { 156 | if ret.Get(0) != nil { 157 | r0 = ret.Get(0).(*group.Group) 158 | } 159 | } 160 | 161 | var r1 error 162 | if rf, ok := ret.Get(1).(func(context.Context, *group.Group) error); ok { 163 | r1 = rf(ctx, _a1) 164 | } else { 165 | r1 = ret.Error(1) 166 | } 167 | 168 | return r0, r1 169 | } 170 | 171 | // DeleteContact provides a mock function with given fields: ctx, ID 172 | func (_m *Storage) DeleteContact(ctx context.Context, ID uuid.UUID) error { 173 | ret := _m.Called(ctx, ID) 174 | 175 | var r0 error 176 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) error); ok { 177 | r0 = rf(ctx, ID) 178 | } else { 179 | r0 = ret.Error(0) 180 | } 181 | 182 | return r0 183 | } 184 | 185 | // DeleteContactFromGroup provides a mock function with given fields: ctx, groupID, contactID 186 | func (_m *Storage) DeleteContactFromGroup(ctx context.Context, groupID uuid.UUID, contactID uuid.UUID) error { 187 | ret := _m.Called(ctx, groupID, contactID) 188 | 189 | var r0 error 190 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) error); ok { 191 | r0 = rf(ctx, groupID, contactID) 192 | } else { 193 | r0 = ret.Error(0) 194 | } 195 | 196 | return r0 197 | } 198 | 199 | // DeleteGroup provides a mock function with given fields: ctx, ID 200 | func (_m *Storage) DeleteGroup(ctx context.Context, ID uuid.UUID) error { 201 | ret := _m.Called(ctx, ID) 202 | 203 | var r0 error 204 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) error); ok { 205 | r0 = rf(ctx, ID) 206 | } else { 207 | r0 = ret.Error(0) 208 | } 209 | 210 | return r0 211 | } 212 | 213 | // ListContact provides a mock function with given fields: ctx, parameter 214 | func (_m *Storage) ListContact(ctx context.Context, parameter queryParameter.QueryParameter) ([]*contact.Contact, error) { 215 | ret := _m.Called(ctx, parameter) 216 | 217 | var r0 []*contact.Contact 218 | if rf, ok := ret.Get(0).(func(context.Context, queryParameter.QueryParameter) []*contact.Contact); ok { 219 | r0 = rf(ctx, parameter) 220 | } else { 221 | if ret.Get(0) != nil { 222 | r0 = ret.Get(0).([]*contact.Contact) 223 | } 224 | } 225 | 226 | var r1 error 227 | if rf, ok := ret.Get(1).(func(context.Context, queryParameter.QueryParameter) error); ok { 228 | r1 = rf(ctx, parameter) 229 | } else { 230 | r1 = ret.Error(1) 231 | } 232 | 233 | return r0, r1 234 | } 235 | 236 | // ListGroup provides a mock function with given fields: ctx, parameter 237 | func (_m *Storage) ListGroup(ctx context.Context, parameter queryParameter.QueryParameter) ([]*group.Group, error) { 238 | ret := _m.Called(ctx, parameter) 239 | 240 | var r0 []*group.Group 241 | if rf, ok := ret.Get(0).(func(context.Context, queryParameter.QueryParameter) []*group.Group); ok { 242 | r0 = rf(ctx, parameter) 243 | } else { 244 | if ret.Get(0) != nil { 245 | r0 = ret.Get(0).([]*group.Group) 246 | } 247 | } 248 | 249 | var r1 error 250 | if rf, ok := ret.Get(1).(func(context.Context, queryParameter.QueryParameter) error); ok { 251 | r1 = rf(ctx, parameter) 252 | } else { 253 | r1 = ret.Error(1) 254 | } 255 | 256 | return r0, r1 257 | } 258 | 259 | // ReadContactByID provides a mock function with given fields: ctx, ID 260 | func (_m *Storage) ReadContactByID(ctx context.Context, ID uuid.UUID) (*contact.Contact, error) { 261 | ret := _m.Called(ctx, ID) 262 | 263 | var r0 *contact.Contact 264 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *contact.Contact); ok { 265 | r0 = rf(ctx, ID) 266 | } else { 267 | if ret.Get(0) != nil { 268 | r0 = ret.Get(0).(*contact.Contact) 269 | } 270 | } 271 | 272 | var r1 error 273 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { 274 | r1 = rf(ctx, ID) 275 | } else { 276 | r1 = ret.Error(1) 277 | } 278 | 279 | return r0, r1 280 | } 281 | 282 | // ReadGroupByID provides a mock function with given fields: ctx, ID 283 | func (_m *Storage) ReadGroupByID(ctx context.Context, ID uuid.UUID) (*group.Group, error) { 284 | ret := _m.Called(ctx, ID) 285 | 286 | var r0 *group.Group 287 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *group.Group); ok { 288 | r0 = rf(ctx, ID) 289 | } else { 290 | if ret.Get(0) != nil { 291 | r0 = ret.Get(0).(*group.Group) 292 | } 293 | } 294 | 295 | var r1 error 296 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { 297 | r1 = rf(ctx, ID) 298 | } else { 299 | r1 = ret.Error(1) 300 | } 301 | 302 | return r0, r1 303 | } 304 | 305 | // UpdateContact provides a mock function with given fields: ctx, ID, updateFn 306 | func (_m *Storage) UpdateContact(ctx context.Context, ID uuid.UUID, updateFn func(*contact.Contact) (*contact.Contact, error)) (*contact.Contact, error) { 307 | ret := _m.Called(ctx, ID, updateFn) 308 | 309 | var r0 *contact.Contact 310 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, func(*contact.Contact) (*contact.Contact, error)) *contact.Contact); ok { 311 | r0 = rf(ctx, ID, updateFn) 312 | } else { 313 | if ret.Get(0) != nil { 314 | r0 = ret.Get(0).(*contact.Contact) 315 | } 316 | } 317 | 318 | var r1 error 319 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, func(*contact.Contact) (*contact.Contact, error)) error); ok { 320 | r1 = rf(ctx, ID, updateFn) 321 | } else { 322 | r1 = ret.Error(1) 323 | } 324 | 325 | return r0, r1 326 | } 327 | 328 | // UpdateGroup provides a mock function with given fields: ctx, ID, updateFn 329 | func (_m *Storage) UpdateGroup(ctx context.Context, ID uuid.UUID, updateFn func(*group.Group) (*group.Group, error)) (*group.Group, error) { 330 | ret := _m.Called(ctx, ID, updateFn) 331 | 332 | var r0 *group.Group 333 | if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, func(*group.Group) (*group.Group, error)) *group.Group); ok { 334 | r0 = rf(ctx, ID, updateFn) 335 | } else { 336 | if ret.Get(0) != nil { 337 | r0 = ret.Get(0).(*group.Group) 338 | } 339 | } 340 | 341 | var r1 error 342 | if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, func(*group.Group) (*group.Group, error)) error); ok { 343 | r1 = rf(ctx, ID, updateFn) 344 | } else { 345 | r1 = ret.Error(1) 346 | } 347 | 348 | return r0, r1 349 | } 350 | 351 | // NewStorage creates a new instance of Storage. It also registers a cleanup function to assert the mocks expectations. 352 | func NewStorage(t testing.TB) *Storage { 353 | mock := &Storage{} 354 | 355 | t.Cleanup(func() { mock.AssertExpectations(t) }) 356 | 357 | return mock 358 | } 359 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/postgres/contact.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Masterminds/squirrel" 7 | "github.com/georgysavva/scany/pgxscan" 8 | "github.com/google/uuid" 9 | "github.com/jackc/pgx/v4" 10 | "github.com/opentracing/opentracing-go" 11 | 12 | "architecture_go/pkg/tools/transaction" 13 | "architecture_go/pkg/type/columnCode" 14 | "architecture_go/pkg/type/context" 15 | log "architecture_go/pkg/type/logger" 16 | "architecture_go/pkg/type/queryParameter" 17 | "architecture_go/services/contact/internal/domain/contact" 18 | "architecture_go/services/contact/internal/repository/storage/postgres/dao" 19 | "architecture_go/services/contact/internal/useCase" 20 | ) 21 | 22 | var mappingSortContact = map[columnCode.ColumnCode]string{ 23 | "id": "id", 24 | "fullName": "full_name", 25 | "phoneNumber": "phone_number", 26 | "name": "name", 27 | "surname": "surname", 28 | "patronymic": "patronymic", 29 | "email": "email", 30 | "gender": "gender", 31 | "age": "age", 32 | } 33 | 34 | func (r *Repository) CreateContact(c context.Context, contacts ...*contact.Contact) ([]*contact.Contact, error) { 35 | 36 | ctx := c.CopyWithTimeout(r.options.Timeout) 37 | defer ctx.Cancel() 38 | 39 | tx, err := r.db.Begin(ctx) 40 | if err != nil { 41 | return nil, log.ErrorWithContext(ctx, err) 42 | } 43 | 44 | defer func(ctx context.Context, t pgx.Tx) { 45 | err = transaction.Finish(ctx, t, err) 46 | }(ctx, tx) 47 | 48 | response, err := r.createContactTx(ctx, tx, contacts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return response, nil 54 | } 55 | 56 | func (r *Repository) createContactTx(ctx context.Context, tx pgx.Tx, contacts ...*contact.Contact) ([]*contact.Contact, error) { 57 | if len(contacts) == 0 { 58 | return []*contact.Contact{}, nil 59 | } 60 | 61 | _, err := tx.CopyFrom( 62 | ctx, 63 | pgx.Identifier{"slurm", "contact"}, 64 | dao.CreateColumnContact, 65 | r.toCopyFromSource(contacts...)) 66 | if err != nil { 67 | return nil, log.ErrorWithContext(ctx, err) 68 | } 69 | 70 | return contacts, nil 71 | } 72 | 73 | func (r *Repository) UpdateContact(c context.Context, ID uuid.UUID, updateFn func(c *contact.Contact) (*contact.Contact, error)) (*contact.Contact, error) { 74 | 75 | ctx := c.CopyWithTimeout(r.options.Timeout) 76 | defer ctx.Cancel() 77 | 78 | tx, err := r.db.Begin(ctx) 79 | if err != nil { 80 | return nil, log.ErrorWithContext(ctx, err) 81 | } 82 | 83 | defer func(ctx context.Context, t pgx.Tx) { 84 | err = transaction.Finish(ctx, t, err) 85 | }(ctx, tx) 86 | 87 | upContact, err := r.oneContactTx(ctx, tx, ID) 88 | if err != nil { 89 | return nil, err 90 | } 91 | in, err := updateFn(upContact) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return r.updateContactTx(ctx, tx, in) 97 | } 98 | 99 | func (r *Repository) updateContactTx(ctx context.Context, tx pgx.Tx, in *contact.Contact) (*contact.Contact, error) { 100 | 101 | builder := r.genSQL.Update("slurm.contact"). 102 | Set("email", in.Email().String()). 103 | Set("phone_number", in.PhoneNumber().String()). 104 | Set("age", in.Age()). 105 | Set("gender", in.Gender()). 106 | Set("modified_at", in.ModifiedAt()). 107 | Set("name", in.Name().String()). 108 | Set("surname", in.Surname().String()). 109 | Set("patronymic", in.Patronymic().String()). 110 | Where(squirrel.And{ 111 | squirrel.Eq{ 112 | "id": in.ID(), 113 | "is_archived": false, 114 | }, 115 | }). 116 | Suffix(`RETURNING 117 | id, 118 | created_at, 119 | modified_at, 120 | phone_number, 121 | email, 122 | name, 123 | surname, 124 | patronymic, 125 | age, 126 | gender`, 127 | ) 128 | 129 | query, args, err := builder.ToSql() 130 | if err != nil { 131 | return nil, log.ErrorWithContext(ctx, err) 132 | } 133 | 134 | rows, err := tx.Query(ctx, query, args...) 135 | if err != nil { 136 | return nil, log.ErrorWithContext(ctx, err) 137 | } 138 | 139 | var daoContacts []*dao.Contact 140 | if err = pgxscan.ScanAll(&daoContacts, rows); err != nil { 141 | return nil, log.ErrorWithContext(ctx, err) 142 | } 143 | 144 | return r.toDomainContact(daoContacts[0]) 145 | } 146 | 147 | func (r *Repository) DeleteContact(c context.Context, ID uuid.UUID) error { 148 | 149 | ctx := c.CopyWithTimeout(r.options.Timeout) 150 | defer ctx.Cancel() 151 | 152 | tx, err := r.db.Begin(ctx) 153 | if err != nil { 154 | return log.ErrorWithContext(ctx, err) 155 | } 156 | 157 | defer func(ctx context.Context, t pgx.Tx) { 158 | err = transaction.Finish(ctx, t, err) 159 | }(ctx, tx) 160 | 161 | if err = r.deleteContactTx(ctx, tx, ID); err != nil { 162 | return err 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func (r *Repository) deleteContactTx(ctx context.Context, tx pgx.Tx, ID uuid.UUID) error { 169 | builder := r.genSQL.Update("slurm.contact"). 170 | Set("is_archived", true). 171 | Set("modified_at", time.Now().UTC()). 172 | Where(squirrel.Eq{"is_archived": false, "id": ID}) 173 | 174 | query, args, err := builder.ToSql() 175 | if err != nil { 176 | return log.ErrorWithContext(ctx, err) 177 | } 178 | 179 | rows, err := tx.Query(ctx, query, args...) 180 | if err != nil { 181 | return log.ErrorWithContext(ctx, err) 182 | } 183 | 184 | var daoContacts []*dao.Contact 185 | if err = pgxscan.ScanAll(&daoContacts, rows); err != nil { 186 | return log.ErrorWithContext(ctx, err) 187 | } 188 | 189 | if err = r.updateGroupsContactCountByFilters(ctx, tx, ID); err != nil { 190 | return err 191 | } 192 | 193 | return nil 194 | } 195 | 196 | func (r *Repository) ListContact(c context.Context, parameter queryParameter.QueryParameter) ([]*contact.Contact, error) { 197 | 198 | ctx := c.CopyWithTimeout(r.options.Timeout) 199 | defer ctx.Cancel() 200 | 201 | span, tmp := opentracing.StartSpanFromContext(c, "ListContact") 202 | defer span.Finish() 203 | ctx = context.New(tmp) 204 | 205 | tx, err := r.db.Begin(ctx) 206 | if err != nil { 207 | return nil, log.ErrorWithContext(ctx, err) 208 | } 209 | 210 | defer func(ctx context.Context, t pgx.Tx) { 211 | err = transaction.Finish(ctx, t, err) 212 | }(ctx, tx) 213 | 214 | if parameter.Pagination.Limit == 0 { 215 | parameter.Pagination.Limit = r.options.DefaultLimit 216 | } 217 | 218 | contacts, err := r.listContactTx(ctx, tx, parameter) 219 | if err != nil { 220 | return nil, err 221 | } 222 | 223 | return contacts, nil 224 | } 225 | 226 | func (r *Repository) listContactTx(ctx context.Context, tx pgx.Tx, parameter queryParameter.QueryParameter) ([]*contact.Contact, error) { 227 | var builder = r.genSQL.Select( 228 | "id", 229 | "created_at", 230 | "modified_at", 231 | "phone_number", 232 | "email", 233 | "name", 234 | "surname", 235 | "patronymic", 236 | "age", 237 | "gender", 238 | ).From("slurm.contact") 239 | 240 | builder = builder.Where(squirrel.Eq{"is_archived": false}) 241 | 242 | if len(parameter.Sorts) > 0 { 243 | builder = builder.OrderBy(parameter.Sorts.Parsing(mappingSortContact)...) 244 | } else { 245 | builder = builder.OrderBy("created_at DESC") 246 | } 247 | 248 | if parameter.Pagination.Limit > 0 { 249 | builder = builder.Limit(parameter.Pagination.Limit) 250 | } 251 | if parameter.Pagination.Offset > 0 { 252 | builder = builder.Offset(parameter.Pagination.Offset) 253 | } 254 | 255 | query, args, err := builder.ToSql() 256 | if err != nil { 257 | return nil, log.ErrorWithContext(ctx, err) 258 | } 259 | 260 | rows, err := tx.Query(ctx, query, args...) 261 | if err != nil { 262 | return nil, log.ErrorWithContext(ctx, err) 263 | } 264 | 265 | var daoContacts []*dao.Contact 266 | if err = pgxscan.ScanAll(&daoContacts, rows); err != nil { 267 | return nil, log.ErrorWithContext(ctx, err) 268 | } 269 | 270 | return r.toDomainContacts(daoContacts) 271 | } 272 | 273 | func (r *Repository) ReadContactByID(c context.Context, ID uuid.UUID) (response *contact.Contact, err error) { 274 | 275 | ctx := c.CopyWithTimeout(r.options.Timeout) 276 | defer ctx.Cancel() 277 | 278 | tx, err := r.db.Begin(ctx) 279 | if err != nil { 280 | return nil, log.ErrorWithContext(ctx, err) 281 | } 282 | 283 | defer func(ctx context.Context, t pgx.Tx) { 284 | err = transaction.Finish(ctx, t, err) 285 | }(ctx, tx) 286 | 287 | response, err = r.oneContactTx(ctx, tx, ID) 288 | if err != nil { 289 | return nil, err 290 | } 291 | 292 | return response, nil 293 | } 294 | 295 | func (r *Repository) oneContactTx(ctx context.Context, tx pgx.Tx, ID uuid.UUID) (*contact.Contact, error) { 296 | var builder = r.genSQL.Select( 297 | "id", 298 | "created_at", 299 | "modified_at", 300 | "phone_number", 301 | "email", 302 | "name", 303 | "surname", 304 | "patronymic", 305 | "age", 306 | "gender", 307 | ).From("slurm.contact") 308 | 309 | builder = builder.Where(squirrel.Eq{"is_archived": false, "id": ID}) 310 | 311 | query, args, err := builder.ToSql() 312 | if err != nil { 313 | return nil, log.ErrorWithContext(ctx, err) 314 | } 315 | 316 | rows, err := tx.Query(ctx, query, args...) 317 | if err != nil { 318 | return nil, log.ErrorWithContext(ctx, err) 319 | } 320 | 321 | var daoContact []*dao.Contact 322 | if err = pgxscan.ScanAll(&daoContact, rows); err != nil { 323 | return nil, log.ErrorWithContext(ctx, err) 324 | } 325 | 326 | if len(daoContact) == 0 { 327 | return nil, useCase.ErrContactNotFound 328 | } 329 | 330 | return r.toDomainContact(daoContact[0]) 331 | } 332 | 333 | func (r *Repository) CountContact(c context.Context) (uint64, error) { 334 | 335 | span, tmp := opentracing.StartSpanFromContext(c, "CountContact") 336 | defer span.Finish() 337 | ctx := context.New(tmp) 338 | 339 | var builder = r.genSQL.Select( 340 | "COUNT(id)", 341 | ).From("slurm.contact") 342 | 343 | builder = builder.Where(squirrel.Eq{"is_archived": false}) 344 | 345 | query, args, err := builder.ToSql() 346 | if err != nil { 347 | return 0, log.ErrorWithContext(ctx, err) 348 | } 349 | 350 | var row = r.db.QueryRow(ctx, query, args...) 351 | var total uint64 352 | 353 | if err = row.Scan(&total); err != nil { 354 | return 0, log.ErrorWithContext(ctx, err) 355 | } 356 | 357 | return total, nil 358 | } 359 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/postgres/contactInGroup.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Masterminds/squirrel" 7 | "github.com/google/uuid" 8 | "github.com/jackc/pgx/v4" 9 | 10 | "architecture_go/pkg/tools/transaction" 11 | "architecture_go/pkg/type/context" 12 | log "architecture_go/pkg/type/logger" 13 | "architecture_go/services/contact/internal/domain/contact" 14 | "architecture_go/services/contact/internal/repository/storage/postgres/dao" 15 | ) 16 | 17 | func (r *Repository) CreateContactIntoGroup(c context.Context, groupID uuid.UUID, contacts ...*contact.Contact) ([]*contact.Contact, error) { 18 | 19 | ctx := c.CopyWithTimeout(r.options.Timeout) 20 | defer ctx.Cancel() 21 | 22 | tx, err := r.db.Begin(ctx) 23 | if err != nil { 24 | return nil, log.ErrorWithContext(ctx, err) 25 | } 26 | defer func(ctx context.Context, t pgx.Tx) { 27 | err = transaction.Finish(ctx, t, err) 28 | }(ctx, tx) 29 | 30 | response, err := r.createContactTx(ctx, tx, contacts...) 31 | if err != nil { 32 | return nil, err 33 | } 34 | var contactIDs = make([]uuid.UUID, len(response)) 35 | for i, c := range response { 36 | contactIDs[i] = c.ID() 37 | } 38 | 39 | if err = r.fillGroupTx(ctx, tx, groupID, contactIDs...); err != nil { 40 | return nil, err 41 | } 42 | 43 | return response, nil 44 | } 45 | 46 | func (r *Repository) DeleteContactFromGroup(c context.Context, groupID, contactID uuid.UUID) error { 47 | 48 | ctx := c.CopyWithTimeout(r.options.Timeout) 49 | defer ctx.Cancel() 50 | 51 | tx, err := r.db.Begin(ctx) 52 | if err != nil { 53 | return log.ErrorWithContext(ctx, err) 54 | } 55 | defer func(ctx context.Context, t pgx.Tx) { 56 | err = transaction.Finish(ctx, t, err) 57 | }(ctx, tx) 58 | 59 | query, args, err := r.genSQL. 60 | Delete("slurm.contact_in_group"). 61 | Where(squirrel.Eq{"contact_id": contactID, "group_id": groupID}). 62 | ToSql() 63 | if err != nil { 64 | return log.ErrorWithContext(ctx, err) 65 | } 66 | 67 | if _, err = tx.Exec(ctx, query, args...); err != nil { 68 | return log.ErrorWithContext(ctx, err) 69 | } 70 | 71 | if err = r.updateGroupContactCount(ctx, tx, groupID); err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (r *Repository) AddContactsToGroup(c context.Context, groupID uuid.UUID, contactIDs ...uuid.UUID) error { 79 | 80 | ctx := c.CopyWithTimeout(r.options.Timeout) 81 | defer ctx.Cancel() 82 | 83 | tx, err := r.db.Begin(ctx) 84 | if err != nil { 85 | return log.ErrorWithContext(ctx, err) 86 | } 87 | defer func(ctx context.Context, t pgx.Tx) { 88 | err = transaction.Finish(ctx, t, err) 89 | }(ctx, tx) 90 | 91 | if err = r.fillGroupTx(ctx, tx, groupID, contactIDs...); err != nil { 92 | return err 93 | } 94 | return nil 95 | } 96 | 97 | func (r *Repository) fillGroupTx(ctx context.Context, tx pgx.Tx, groupID uuid.UUID, contactIDs ...uuid.UUID) error { 98 | _, mapExist, err := r.checkExistContactInGroup(ctx, tx, groupID, contactIDs...) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | for i := 0; i < len(contactIDs); { 104 | var contactID = contactIDs[i] 105 | if exist := mapExist[contactID]; exist { 106 | contactIDs[i] = contactIDs[len(contactIDs)-1] 107 | contactIDs = contactIDs[:len(contactIDs)-1] 108 | continue 109 | } 110 | i++ 111 | } 112 | 113 | if len(contactIDs) == 0 { 114 | return nil 115 | } 116 | 117 | var rows [][]interface{} 118 | var timeNow = time.Now().UTC() 119 | for _, contactID := range contactIDs { 120 | rows = append(rows, []interface{}{ 121 | timeNow, 122 | timeNow, 123 | groupID, 124 | contactID, 125 | }) 126 | } 127 | 128 | _, err = tx.CopyFrom( 129 | ctx, 130 | pgx.Identifier{"slurm", "contact_in_group"}, 131 | dao.CreateColumnContactInGroup, 132 | pgx.CopyFromRows(rows), 133 | ) 134 | if err != nil { 135 | return log.ErrorWithContext(ctx, err) 136 | } 137 | 138 | if err = r.updateGroupContactCount(ctx, tx, groupID); err != nil { 139 | return err 140 | } 141 | 142 | return nil 143 | } 144 | 145 | // checkExistContactInGroup 146 | // return listExist -- list existing contactID 147 | // return mapExist -- mapping contact ID how exist or not exist 148 | func (r *Repository) checkExistContactInGroup(ctx context.Context, tx pgx.Tx, groupID uuid.UUID, contactIDs ...uuid.UUID) (listExist []uuid.UUID, mapExist map[uuid.UUID]bool, err error) { 149 | listExist = make([]uuid.UUID, 0) 150 | mapExist = make(map[uuid.UUID]bool) 151 | 152 | if len(contactIDs) == 0 { 153 | return listExist, mapExist, nil 154 | } 155 | 156 | query, args, err := r.genSQL. 157 | Select("contact_id"). 158 | From("slurm.contact_in_group"). 159 | Where(squirrel.Eq{"contact_id": contactIDs, "group_id": groupID}).ToSql() 160 | 161 | if err != nil { 162 | return nil, nil, log.ErrorWithContext(ctx, err) 163 | } 164 | 165 | rows, err := tx.Query(ctx, query, args...) 166 | if err != nil { 167 | return nil, nil, log.ErrorWithContext(ctx, err) 168 | } 169 | 170 | for rows.Next() { 171 | var contactID = uuid.UUID{} 172 | 173 | if err = rows.Scan(&contactID); err != nil { 174 | return nil, nil, log.ErrorWithContext(ctx, err) 175 | } 176 | 177 | listExist = append(listExist, contactID) 178 | mapExist[contactID] = true 179 | } 180 | 181 | for _, contactID := range contactIDs { 182 | if _, ok := mapExist[contactID]; !ok { 183 | mapExist[contactID] = false 184 | } 185 | } 186 | 187 | if err = rows.Err(); err != nil { 188 | return nil, nil, log.ErrorWithContext(ctx, err) 189 | } 190 | 191 | return listExist, mapExist, nil 192 | } 193 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/postgres/convereter.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "github.com/jackc/pgx/v4" 5 | 6 | "architecture_go/pkg/type/email" 7 | "architecture_go/pkg/type/gender" 8 | "architecture_go/pkg/type/phoneNumber" 9 | "architecture_go/services/contact/internal/domain/contact" 10 | "architecture_go/services/contact/internal/domain/contact/age" 11 | "architecture_go/services/contact/internal/domain/contact/name" 12 | "architecture_go/services/contact/internal/domain/contact/patronymic" 13 | "architecture_go/services/contact/internal/domain/contact/surname" 14 | "architecture_go/services/contact/internal/repository/storage/postgres/dao" 15 | ) 16 | 17 | func (r Repository) toCopyFromSource(contacts ...*contact.Contact) pgx.CopyFromSource { 18 | rows := make([][]interface{}, len(contacts)) 19 | 20 | for i, val := range contacts { 21 | rows[i] = []interface{}{ 22 | val.ID(), 23 | val.CreatedAt(), 24 | val.ModifiedAt(), 25 | val.PhoneNumber().String(), 26 | val.Email().String(), 27 | val.Name().String(), 28 | val.Surname().String(), 29 | val.Patronymic().String(), 30 | val.Age(), 31 | val.Gender(), 32 | } 33 | } 34 | return pgx.CopyFromRows(rows) 35 | } 36 | 37 | func (r Repository) toDomainContact(dao *dao.Contact) (*contact.Contact, error) { 38 | 39 | nameObject, err := name.New(dao.Name) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | surnameObject, err := surname.New(dao.Surname) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | patronymicObject, err := patronymic.New(dao.Patronymic) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | ageObject, err := age.New(dao.Age) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | localEmail, err := email.New(dao.Email) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | result, err := contact.NewWithID( 65 | dao.ID, 66 | dao.CreatedAt, 67 | dao.ModifiedAt, 68 | *phoneNumber.New(dao.PhoneNumber), 69 | localEmail, 70 | *nameObject, 71 | *surnameObject, 72 | *patronymicObject, 73 | *ageObject, 74 | gender.Gender(dao.Gender), 75 | ) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return result, nil 80 | } 81 | 82 | func (r Repository) toDomainContacts(dao []*dao.Contact) ([]*contact.Contact, error) { 83 | var result = make([]*contact.Contact, len(dao)) 84 | for i, v := range dao { 85 | c, err := r.toDomainContact(v) 86 | if err != nil { 87 | return nil, err 88 | } 89 | result[i] = c 90 | } 91 | return result, nil 92 | } 93 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/postgres/dao/contact.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type Contact struct { 10 | ID uuid.UUID `db:"id"` 11 | CreatedAt time.Time `db:"created_at"` 12 | ModifiedAt time.Time `db:"modified_at"` 13 | 14 | Email string `db:"email"` 15 | PhoneNumber string `db:"phone_number"` 16 | 17 | Name string `db:"name"` 18 | Surname string `db:"surname"` 19 | Patronymic string `db:"patronymic"` 20 | 21 | Age uint8 `db:"age"` 22 | Gender uint8 `db:"gender"` 23 | } 24 | 25 | var CreateColumnContact = []string{ 26 | "id", 27 | "created_at", 28 | "modified_at", 29 | "phone_number", 30 | "email", 31 | "name", 32 | "surname", 33 | "patronymic", 34 | "age", 35 | "gender", 36 | } 37 | 38 | var CreateColumnContactInGroup = []string{ 39 | "created_at", 40 | "modified_at", 41 | "group_id", 42 | "contact_id", 43 | } 44 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/postgres/dao/group.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | 8 | "architecture_go/services/contact/internal/domain/group" 9 | "architecture_go/services/contact/internal/domain/group/description" 10 | "architecture_go/services/contact/internal/domain/group/name" 11 | ) 12 | 13 | type Group struct { 14 | ID uuid.UUID `db:"id"` 15 | Name string `db:"name"` 16 | Description string `db:"description"` 17 | CreatedAt time.Time `db:"created_at"` 18 | ModifiedAt time.Time `db:"modified_at"` 19 | ContactCount uint64 `db:"contact_count"` 20 | IsArchived bool `db:"is_archived"` 21 | } 22 | 23 | func (g *Group) ToDomainGroup() (*group.Group, error) { 24 | gN, err := name.New(g.Name) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | gD, err := description.New(g.Description) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return group.NewWithID( 34 | g.ID, 35 | g.CreatedAt, 36 | g.ModifiedAt, 37 | gN, 38 | gD, 39 | g.ContactCount, 40 | ), nil 41 | } 42 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/postgres/group.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | 7 | "github.com/Masterminds/squirrel" 8 | "github.com/georgysavva/scany/pgxscan" 9 | "github.com/google/uuid" 10 | "github.com/jackc/pgx/v4" 11 | 12 | "architecture_go/pkg/tools/converter" 13 | "architecture_go/pkg/tools/transaction" 14 | "architecture_go/pkg/type/columnCode" 15 | "architecture_go/pkg/type/context" 16 | log "architecture_go/pkg/type/logger" 17 | "architecture_go/pkg/type/queryParameter" 18 | "architecture_go/services/contact/internal/domain/group" 19 | "architecture_go/services/contact/internal/repository/storage/postgres/dao" 20 | "architecture_go/services/contact/internal/useCase" 21 | ) 22 | 23 | var mappingSortGroup = map[columnCode.ColumnCode]string{ 24 | "id": "id", 25 | "name": "name", 26 | "description": "description", 27 | "contactCount": "contact_count", 28 | } 29 | 30 | func (r *Repository) CreateGroup(c context.Context, group *group.Group) (*group.Group, error) { 31 | 32 | ctx := c.CopyWithTimeout(r.options.Timeout) 33 | defer ctx.Cancel() 34 | 35 | query, args, err := r.genSQL.Insert("slurm.group"). 36 | Columns( 37 | "id", 38 | "name", 39 | "description", 40 | "created_at", 41 | "modified_at", 42 | ). 43 | Values( 44 | group.ID(), 45 | group.Name().Value(), 46 | group.Description().Value(), 47 | group.CreatedAt(), 48 | group.ModifiedAt()). 49 | ToSql() 50 | if err != nil { 51 | return nil, log.ErrorWithContext(ctx, err) 52 | } 53 | 54 | if _, err = r.db.Exec(ctx, query, args...); err != nil { 55 | return nil, log.ErrorWithContext(ctx, err) 56 | } 57 | return group, nil 58 | } 59 | 60 | func (r *Repository) UpdateGroup(c context.Context, ID uuid.UUID, updateFn func(group *group.Group) (*group.Group, error)) (*group.Group, error) { 61 | 62 | ctx := c.CopyWithTimeout(r.options.Timeout) 63 | defer ctx.Cancel() 64 | 65 | tx, err := r.db.Begin(ctx) 66 | if err != nil { 67 | return nil, log.ErrorWithContext(ctx, err) 68 | } 69 | 70 | defer func(ctx context.Context, t pgx.Tx) { 71 | err = transaction.Finish(ctx, t, err) 72 | }(ctx, tx) 73 | 74 | upGroup, err := r.oneGroupTx(ctx, tx, ID) 75 | if err != nil { 76 | return nil, err 77 | } 78 | groupForUpdate, err := updateFn(upGroup) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | query, args, err := r.genSQL.Update("slurm.group"). 84 | Set("name", groupForUpdate.Name().Value()). 85 | Set("description", groupForUpdate.Description().Value()). 86 | Set("modified_at", groupForUpdate.ModifiedAt()). 87 | Where(squirrel.And{ 88 | squirrel.Eq{ 89 | "id": ID, 90 | "is_archived": false, 91 | }, 92 | }). 93 | Suffix(`RETURNING 94 | id, 95 | name, 96 | description, 97 | created_at, 98 | modified_at`, 99 | ). 100 | ToSql() 101 | if err != nil { 102 | return nil, log.ErrorWithContext(ctx, err) 103 | } 104 | 105 | rows, err := tx.Query(ctx, query, args...) 106 | if err != nil { 107 | return nil, log.ErrorWithContext(ctx, err) 108 | } 109 | 110 | var daoGroup []*dao.Group 111 | if err = pgxscan.ScanAll(&daoGroup, rows); err != nil { 112 | return nil, log.ErrorWithContext(ctx, err) 113 | } 114 | 115 | return groupForUpdate, nil 116 | } 117 | 118 | func (r *Repository) DeleteGroup(c context.Context, ID uuid.UUID) error { 119 | 120 | ctx := c.CopyWithTimeout(r.options.Timeout) 121 | defer ctx.Cancel() 122 | 123 | tx, err := r.db.Begin(ctx) 124 | if err != nil { 125 | return log.ErrorWithContext(ctx, err) 126 | } 127 | 128 | defer func(ctx context.Context, t pgx.Tx) { 129 | err = transaction.Finish(ctx, t, err) 130 | }(ctx, tx) 131 | 132 | if err = r.deleteGroupTx(ctx, tx, ID); err != nil { 133 | return err 134 | } 135 | 136 | return nil 137 | } 138 | 139 | func (r *Repository) deleteGroupTx(ctx context.Context, tx pgx.Tx, ID uuid.UUID) error { 140 | query, args, err := r.genSQL.Update("slurm.group"). 141 | Set("is_archived", true). 142 | Set("modified_at", time.Now().UTC()). 143 | Where(squirrel.Eq{ 144 | "id": ID, 145 | "is_archived": false, 146 | }).ToSql() 147 | 148 | if err != nil { 149 | return log.ErrorWithContext(ctx, err) 150 | } 151 | 152 | if _, err = tx.Exec(ctx, query, args...); err != nil { 153 | return log.ErrorWithContext(ctx, err) 154 | } 155 | 156 | if err = r.clearGroupTx(ctx, tx, ID); err != nil { 157 | return err 158 | } 159 | 160 | return nil 161 | } 162 | 163 | func (r *Repository) clearGroupTx(ctx context.Context, tx pgx.Tx, groupID uuid.UUID) error { 164 | query, args, err := r.genSQL. 165 | Delete("slurm.contact_in_group"). 166 | Where(squirrel.Eq{"group_id": groupID}). 167 | ToSql() 168 | if err != nil { 169 | return log.ErrorWithContext(ctx, err) 170 | } 171 | 172 | if _, err = tx.Exec(ctx, query, args...); err != nil { 173 | return log.ErrorWithContext(ctx, err) 174 | } 175 | 176 | if err = r.updateGroupContactCount(ctx, tx, groupID); err != nil { 177 | return err 178 | } 179 | 180 | return nil 181 | } 182 | 183 | func (r *Repository) ListGroup(c context.Context, parameter queryParameter.QueryParameter) ([]*group.Group, error) { 184 | 185 | ctx := c.CopyWithTimeout(r.options.Timeout) 186 | defer ctx.Cancel() 187 | 188 | tx, err := r.db.Begin(ctx) 189 | if err != nil { 190 | return nil, log.ErrorWithContext(ctx, err) 191 | } 192 | 193 | defer func(ctx context.Context, t pgx.Tx) { 194 | err = transaction.Finish(ctx, t, err) 195 | }(ctx, tx) 196 | 197 | response, err := r.listGroupTx(ctx, tx, parameter) 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | return response, nil 203 | } 204 | 205 | func (r *Repository) listGroupTx(ctx context.Context, tx pgx.Tx, parameter queryParameter.QueryParameter) ([]*group.Group, error) { 206 | var result []*group.Group 207 | 208 | var builder = r.genSQL.Select( 209 | "id", 210 | "name", 211 | "description", 212 | "created_at", 213 | "modified_at", 214 | "contact_count", 215 | "is_archived", 216 | ). 217 | From("slurm.group") 218 | 219 | builder = builder.Where(squirrel.Eq{"is_archived": false}) 220 | 221 | if len(parameter.Sorts) > 0 { 222 | builder = builder.OrderBy(parameter.Sorts.Parsing(mappingSortGroup)...) 223 | } else { 224 | builder = builder.OrderBy("created_at DESC") 225 | } 226 | 227 | if parameter.Pagination.Limit > 0 { 228 | builder = builder.Limit(parameter.Pagination.Limit) 229 | } 230 | if parameter.Pagination.Offset > 0 { 231 | builder = builder.Offset(parameter.Pagination.Offset) 232 | } 233 | 234 | query, args, err := builder.ToSql() 235 | rows, err := tx.Query(ctx, query, args...) 236 | if err != nil { 237 | return nil, log.ErrorWithContext(ctx, err) 238 | } 239 | 240 | var groups []*dao.Group 241 | if err = pgxscan.ScanAll(&groups, rows); err != nil { 242 | return nil, log.ErrorWithContext(ctx, err) 243 | } 244 | 245 | for _, g := range groups { 246 | domainGroup, err := g.ToDomainGroup() 247 | if err != nil { 248 | return nil, log.ErrorWithContext(ctx, err) 249 | } 250 | result = append(result, domainGroup) 251 | } 252 | return result, nil 253 | } 254 | 255 | func (r *Repository) ReadGroupByID(c context.Context, ID uuid.UUID) (*group.Group, error) { 256 | 257 | ctx := c.CopyWithTimeout(r.options.Timeout) 258 | defer ctx.Cancel() 259 | 260 | tx, err := r.db.Begin(ctx) 261 | if err != nil { 262 | return nil, log.ErrorWithContext(ctx, err) 263 | } 264 | 265 | defer func(ctx context.Context, t pgx.Tx) { 266 | err = transaction.Finish(ctx, t, err) 267 | }(ctx, tx) 268 | 269 | response, err := r.oneGroupTx(ctx, tx, ID) 270 | if err != nil { 271 | return nil, err 272 | } 273 | 274 | return response, nil 275 | } 276 | 277 | func (r *Repository) oneGroupTx(ctx context.Context, tx pgx.Tx, ID uuid.UUID) (response *group.Group, err error) { 278 | 279 | var builder = r.genSQL.Select( 280 | "id", 281 | "name", 282 | "description", 283 | "created_at", 284 | "modified_at", 285 | "contact_count", 286 | "is_archived", 287 | ). 288 | From("slurm.group") 289 | 290 | builder = builder.Where(squirrel.Eq{"is_archived": false, "id": ID}) 291 | 292 | query, args, err := builder.ToSql() 293 | rows, err := tx.Query(ctx, query, args...) 294 | if err != nil { 295 | return nil, log.ErrorWithContext(ctx, err) 296 | } 297 | 298 | var daoGroup []*dao.Group 299 | if err = pgxscan.ScanAll(&daoGroup, rows); err != nil { 300 | return nil, log.ErrorWithContext(ctx, err) 301 | } 302 | 303 | if len(daoGroup) == 0 { 304 | return nil, useCase.ErrGroupNotFound 305 | } 306 | 307 | return daoGroup[0].ToDomainGroup() 308 | 309 | } 310 | 311 | func (r *Repository) CountGroup(ctx context.Context) (uint64, error) { 312 | var builder = r.genSQL.Select( 313 | "COUNT(id)", 314 | ).From("slurm.group") 315 | 316 | builder = builder.Where(squirrel.Eq{"is_archived": false}) 317 | 318 | query, args, err := builder.ToSql() 319 | if err != nil { 320 | return 0, log.ErrorWithContext(ctx, err) 321 | } 322 | 323 | row := r.db.QueryRow(ctx, query, args...) 324 | var total uint64 325 | 326 | if err = row.Scan(&total); err != nil { 327 | return 0, log.ErrorWithContext(ctx, err) 328 | } 329 | 330 | return total, nil 331 | } 332 | 333 | func (r *Repository) updateGroupsContactCountByFilters(ctx context.Context, tx pgx.Tx, ID uuid.UUID) error { 334 | 335 | builder := r.genSQL.Select("contact_in_group.group_id"). 336 | From("slurm.contact_in_group"). 337 | InnerJoin("slurm.contact ON contact_in_group.contact_id = contact.id"). 338 | GroupBy("contact_in_group.group_id") 339 | 340 | builder = builder.Where(squirrel.Eq{"contact_in_group.contact_id": ID}) 341 | 342 | query, args, err := builder.ToSql() 343 | if err != nil { 344 | return log.ErrorWithContext(ctx, err) 345 | } 346 | 347 | rows, err := tx.Query(ctx, query, args...) 348 | if err != nil { 349 | return log.ErrorWithContext(ctx, err) 350 | } 351 | var groupIDs []uuid.UUID 352 | for rows.Next() { 353 | var groupID sql.NullString 354 | if err = rows.Scan(&groupID); err != nil { 355 | return log.ErrorWithContext(ctx, err) 356 | } 357 | groupIDs = append(groupIDs, converter.StringToUUID(groupID.String)) 358 | } 359 | 360 | for _, groupID := range groupIDs { 361 | if err = r.updateGroupContactCount(ctx, tx, groupID); err != nil { 362 | return err 363 | } 364 | } 365 | 366 | if err = rows.Err(); err != nil { 367 | return log.ErrorWithContext(ctx, err) 368 | } 369 | 370 | return nil 371 | } 372 | 373 | func (r *Repository) updateGroupContactCount(ctx context.Context, tx pgx.Tx, groupID uuid.UUID) error { 374 | subSelect := r.genSQL.Select("count(contact_in_group.id)"). 375 | From("slurm.contact_in_group"). 376 | InnerJoin("slurm.contact ON contact_in_group.contact_id = contact.id"). 377 | Where(squirrel.Eq{"group_id": groupID, "is_archived": false}) 378 | 379 | query, _, err := r.genSQL. 380 | Update("slurm.group"). 381 | Set("contact_count", subSelect). 382 | Where(squirrel.Eq{"id": groupID}). 383 | ToSql() 384 | if err != nil { 385 | return log.ErrorWithContext(ctx, err) 386 | } 387 | 388 | var args = []interface{}{groupID, false} 389 | 390 | if _, err = tx.Exec(ctx, query, args...); err != nil { 391 | return log.ErrorWithContext(ctx, err) 392 | } 393 | return nil 394 | } 395 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/postgres/migrations/20220707173102_init_slurm.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | 4 | CREATE SCHEMA IF NOT EXISTS slurm; 5 | 6 | CREATE TABLE IF NOT EXISTS slurm.contact 7 | ( 8 | id uuid DEFAULT gen_random_uuid() NOT NULL 9 | CONSTRAINT pk_contact 10 | PRIMARY KEY, 11 | created_at timestamp, 12 | modified_at timestamp, 13 | name varchar(50) DEFAULT '':: character varying NOT NULL, 14 | surname varchar(100) DEFAULT '':: character varying NOT NULL, 15 | patronymic varchar(100) DEFAULT '':: character varying NOT NULL, 16 | email varchar(250), 17 | phone_number varchar(50), 18 | age smallint 19 | CONSTRAINT age_check 20 | CHECK ((age >= 0) AND (age <= 200)), 21 | gender smallint, 22 | is_archived boolean DEFAULT FALSE NOT NULL 23 | ); 24 | 25 | CREATE TABLE IF NOT EXISTS slurm."group" 26 | ( 27 | id uuid DEFAULT gen_random_uuid() NOT NULL 28 | CONSTRAINT pk_group 29 | PRIMARY KEY, 30 | created_at timestamp DEFAULT CURRENT_TIMESTAMP, 31 | modified_at timestamp DEFAULT CURRENT_TIMESTAMP, 32 | name varchar(250) NOT NULL, 33 | description varchar(1000), 34 | contact_count bigint DEFAULT 0 NOT NULL, 35 | is_archived boolean DEFAULT FALSE NOT NULL 36 | ); 37 | 38 | CREATE TABLE IF NOT EXISTS slurm.contact_in_group 39 | ( 40 | id uuid DEFAULT gen_random_uuid() NOT NULL 41 | CONSTRAINT pk_contact_in_group 42 | PRIMARY KEY, 43 | created_at timestamp DEFAULT CURRENT_TIMESTAMP, 44 | modified_at timestamp DEFAULT CURRENT_TIMESTAMP, 45 | contact_id uuid not null 46 | constraint fk_contact_id 47 | references slurm.contact, 48 | group_id uuid not null 49 | constraint fk_group_id 50 | references slurm."group" 51 | ); 52 | 53 | -- +goose StatementEnd 54 | 55 | -- +goose Down 56 | -- +goose StatementBegin 57 | 58 | DROP TABLE IF EXISTS slurm.contact_in_group; 59 | 60 | DROP TABLE IF EXISTS slurm.contact; 61 | 62 | DROP TABLE IF EXISTS slurm.group; 63 | 64 | DROP SCHEMA IF EXISTS slurm; 65 | 66 | -- +goose StatementEnd 67 | -------------------------------------------------------------------------------- /services/contact/internal/repository/storage/postgres/repository.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Masterminds/squirrel" 7 | "github.com/jackc/pgx/v4/pgxpool" 8 | "github.com/spf13/viper" 9 | "go.uber.org/zap" 10 | 11 | "github.com/pressly/goose" 12 | 13 | log "architecture_go/pkg/type/logger" 14 | ) 15 | 16 | func init() { 17 | viper.SetDefault("MIGRATIONS_DIR", "./services/contact/internal/repository/storage/postgres/migrations") 18 | } 19 | 20 | type Repository struct { 21 | db *pgxpool.Pool 22 | genSQL squirrel.StatementBuilderType 23 | options Options 24 | } 25 | 26 | type Options struct { 27 | Timeout time.Duration 28 | DefaultLimit uint64 29 | DefaultOffset uint64 30 | } 31 | 32 | func New(db *pgxpool.Pool, o Options) (*Repository, error) { 33 | if err := migrations(db); err != nil { 34 | return nil, err 35 | } 36 | 37 | var r = &Repository{ 38 | genSQL: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), 39 | db: db, 40 | } 41 | 42 | r.SetOptions(o) 43 | return r, nil 44 | } 45 | 46 | func (r *Repository) SetOptions(options Options) { 47 | if options.DefaultLimit == 0 { 48 | options.DefaultLimit = 10 49 | log.Debug("set default options.DefaultLimit", zap.Any("defaultLimit", options.DefaultLimit)) 50 | } 51 | 52 | if options.Timeout == 0 { 53 | options.Timeout = time.Second * 30 54 | log.Debug("set default options.Timeout", zap.Any("timeout", options.Timeout)) 55 | } 56 | 57 | if r.options != options { 58 | r.options = options 59 | log.Info("set new options", zap.Any("options", r.options)) 60 | } 61 | } 62 | 63 | func migrations(pool *pgxpool.Pool) (err error) { 64 | db, err := goose.OpenDBWithDriver("postgres", pool.Config().ConnConfig.ConnString()) 65 | if err != nil { 66 | log.Error(err) 67 | return err 68 | } 69 | defer func() { 70 | if errClose := db.Close(); errClose != nil { 71 | log.Error(errClose) 72 | err = errClose 73 | return 74 | } 75 | }() 76 | 77 | dir := viper.GetString("MIGRATIONS_DIR") 78 | goose.SetTableName("contact_version") 79 | if err = goose.Run("up", db, dir); err != nil { 80 | log.Error(err, zap.String("command", "up")) 81 | return err 82 | } 83 | return 84 | } 85 | -------------------------------------------------------------------------------- /services/contact/internal/useCase/adapters/storage/interface.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | 6 | "architecture_go/pkg/type/context" 7 | "architecture_go/pkg/type/queryParameter" 8 | "architecture_go/services/contact/internal/domain/contact" 9 | "architecture_go/services/contact/internal/domain/group" 10 | ) 11 | 12 | type Storage interface { 13 | Contact 14 | Group 15 | } 16 | 17 | type Contact interface { 18 | CreateContact(ctx context.Context, contacts ...*contact.Contact) ([]*contact.Contact, error) 19 | UpdateContact(ctx context.Context, ID uuid.UUID, updateFn func(c *contact.Contact) (*contact.Contact, error)) (*contact.Contact, error) 20 | DeleteContact(ctx context.Context, ID uuid.UUID) error 21 | 22 | ContactReader 23 | } 24 | 25 | type ContactReader interface { 26 | ListContact(ctx context.Context, parameter queryParameter.QueryParameter) ([]*contact.Contact, error) 27 | ReadContactByID(ctx context.Context, ID uuid.UUID) (response *contact.Contact, err error) 28 | CountContact(ctx context.Context /*Тут можно передавать фильтр*/) (uint64, error) 29 | } 30 | 31 | type Group interface { 32 | CreateGroup(ctx context.Context, group *group.Group) (*group.Group, error) 33 | UpdateGroup(ctx context.Context, ID uuid.UUID, updateFn func(group *group.Group) (*group.Group, error)) (*group.Group, error) 34 | DeleteGroup(ctx context.Context, ID uuid.UUID /*Тут можно передавать фильтр*/) error 35 | 36 | GroupReader 37 | ContactInGroup 38 | } 39 | 40 | type GroupReader interface { 41 | ListGroup(ctx context.Context, parameter queryParameter.QueryParameter) ([]*group.Group, error) 42 | ReadGroupByID(ctx context.Context, ID uuid.UUID) (*group.Group, error) 43 | CountGroup(ctx context.Context /*Тут можно передавать фильтр*/) (uint64, error) 44 | } 45 | 46 | type ContactInGroup interface { 47 | CreateContactIntoGroup(ctx context.Context, groupID uuid.UUID, contacts ...*contact.Contact) ([]*contact.Contact, error) 48 | DeleteContactFromGroup(ctx context.Context, groupID, contactID uuid.UUID) error 49 | AddContactsToGroup(ctx context.Context, groupID uuid.UUID, contactIDs ...uuid.UUID) error 50 | } 51 | -------------------------------------------------------------------------------- /services/contact/internal/useCase/adapters/storage/mock.sh: -------------------------------------------------------------------------------- 1 | mockery --all --keeptree --output ../../../repository/storage/mock --outpkg mockStorage 2 | -------------------------------------------------------------------------------- /services/contact/internal/useCase/contact/contact.go: -------------------------------------------------------------------------------- 1 | package contact 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | "github.com/opentracing/opentracing-go" 8 | 9 | "architecture_go/pkg/type/context" 10 | "architecture_go/pkg/type/queryParameter" 11 | "architecture_go/services/contact/internal/domain/contact" 12 | ) 13 | 14 | func (uc *UseCase) Create(ctx context.Context, contacts ...*contact.Contact) ([]*contact.Contact, error) { 15 | return uc.adapterStorage.CreateContact(ctx, contacts...) 16 | } 17 | 18 | func (uc *UseCase) Update(ctx context.Context, contactUpdate contact.Contact) (*contact.Contact, error) { 19 | return uc.adapterStorage.UpdateContact(ctx, contactUpdate.ID(), func(oldContact *contact.Contact) (*contact.Contact, error) { 20 | return contact.NewWithID( 21 | oldContact.ID(), 22 | oldContact.CreatedAt(), 23 | time.Now().UTC(), 24 | contactUpdate.PhoneNumber(), 25 | contactUpdate.Email(), 26 | contactUpdate.Name(), 27 | contactUpdate.Surname(), 28 | contactUpdate.Patronymic(), 29 | contactUpdate.Age(), 30 | contactUpdate.Gender(), 31 | ) 32 | }) 33 | } 34 | 35 | func (uc *UseCase) Delete(ctx context.Context, ID uuid.UUID) error { 36 | return uc.adapterStorage.DeleteContact(ctx, ID) 37 | } 38 | 39 | func (uc *UseCase) List(c context.Context, parameter queryParameter.QueryParameter) ([]*contact.Contact, error) { 40 | 41 | span, ctx := opentracing.StartSpanFromContext(c, "List") 42 | defer span.Finish() 43 | 44 | return uc.adapterStorage.ListContact(context.New(ctx), parameter) 45 | } 46 | 47 | func (uc *UseCase) ReadByID(ctx context.Context, ID uuid.UUID) (response *contact.Contact, err error) { 48 | 49 | return uc.adapterStorage.ReadContactByID(ctx, ID) 50 | } 51 | 52 | func (uc *UseCase) Count(c context.Context) (uint64, error) { 53 | span, ctx := opentracing.StartSpanFromContext(c, "Count") 54 | defer span.Finish() 55 | 56 | return uc.adapterStorage.CountContact(context.New(ctx)) 57 | } 58 | -------------------------------------------------------------------------------- /services/contact/internal/useCase/contact/contect_test.go: -------------------------------------------------------------------------------- 1 | package contact 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | 11 | "architecture_go/pkg/type/context" 12 | "architecture_go/pkg/type/email" 13 | "architecture_go/pkg/type/gender" 14 | "architecture_go/pkg/type/phoneNumber" 15 | "architecture_go/services/contact/internal/domain/contact" 16 | "architecture_go/services/contact/internal/domain/contact/age" 17 | "architecture_go/services/contact/internal/domain/contact/name" 18 | "architecture_go/services/contact/internal/domain/contact/patronymic" 19 | "architecture_go/services/contact/internal/domain/contact/surname" 20 | mockStorage "architecture_go/services/contact/internal/repository/storage/mock" 21 | "architecture_go/services/contact/internal/useCase" 22 | ) 23 | 24 | var ( 25 | storageRepository = new(mockStorage.Contact) 26 | ucDialog *UseCase 27 | data = make(map[uuid.UUID]*contact.Contact) 28 | createContacts []*contact.Contact 29 | ) 30 | 31 | func TestMain(m *testing.M) { 32 | 33 | contactAge, _ := age.New(42) 34 | contactName, _ := name.New("Иван") 35 | contactSurname, _ := surname.New("Иванов") 36 | contactPatronymic, _ := patronymic.New("Иванович") 37 | contactEmail, _ := email.New("ivanii@gmail.com") 38 | createContact, _ := contact.New( 39 | *phoneNumber.New("88002002020"), 40 | contactEmail, 41 | *contactName, 42 | *contactSurname, 43 | *contactPatronymic, 44 | *contactAge, 45 | gender.MALE, 46 | ) 47 | createContacts = append(createContacts, createContact) 48 | os.Exit(m.Run()) 49 | } 50 | 51 | func initTestUseCaseContact(t *testing.T) { 52 | assertion := assert.New(t) 53 | storageRepository.On("CreateContact", 54 | mock.Anything, 55 | mock.Anything). 56 | Return(func(ctx context.Context, contacts ...*contact.Contact) []*contact.Contact { 57 | assertion.Equal(contacts, createContacts) 58 | for _, c := range contacts { 59 | data[c.ID()] = c 60 | } 61 | return contacts 62 | }, func(ctx context.Context, contacts ...*contact.Contact) error { 63 | return nil 64 | }) 65 | 66 | storageRepository.On("ReadContactByID", 67 | mock.Anything, 68 | mock.AnythingOfType("uuid.UUID")). 69 | Return(func(ctx context.Context, ID uuid.UUID) *contact.Contact { 70 | if c, ok := data[ID]; ok { 71 | return c 72 | } 73 | return nil 74 | }, func(ctx context.Context, ID uuid.UUID) error { 75 | if _, ok := data[ID]; !ok { 76 | return useCase.ErrContactNotFound 77 | } 78 | return nil 79 | }) 80 | 81 | storageRepository.On("UpdateContact", 82 | mock.Anything, 83 | mock.Anything). 84 | Return(func(ctx context.Context, ID uuid.UUID, updateFn func(c *contact.Contact) (*contact.Contact, error)) *contact.Contact { 85 | return nil 86 | }, func(ctx context.Context, ID uuid.UUID, updateFn func(c *contact.Contact) (*contact.Contact, error)) error { 87 | return nil 88 | }) 89 | } 90 | 91 | func TestContact(t *testing.T) { 92 | 93 | initTestUseCaseContact(t) 94 | ucDialog = New(storageRepository, Options{}) 95 | 96 | assertion := assert.New(t) 97 | t.Run("create contact", func(t *testing.T) { 98 | var ctx = context.Empty() 99 | 100 | result, err := ucDialog.Create(ctx, createContacts...) 101 | assertion.NoError(err) 102 | assertion.Equal(result, createContacts) 103 | }) 104 | 105 | t.Run("get contact", func(t *testing.T) { 106 | var ctx = context.Empty() 107 | 108 | result, err := ucDialog.ReadByID(ctx, createContacts[0].ID()) 109 | assertion.NoError(err) 110 | assertion.Equal(result, createContacts[0]) 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /services/contact/internal/useCase/contact/useCase.go: -------------------------------------------------------------------------------- 1 | package contact 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | 6 | "architecture_go/services/contact/internal/useCase/adapters/storage" 7 | 8 | log "architecture_go/pkg/type/logger" 9 | ) 10 | 11 | type UseCase struct { 12 | adapterStorage storage.Contact 13 | options Options 14 | } 15 | 16 | type Options struct{} 17 | 18 | func New(storage storage.Contact, options Options) *UseCase { 19 | var uc = &UseCase{ 20 | adapterStorage: storage, 21 | } 22 | uc.SetOptions(options) 23 | return uc 24 | } 25 | 26 | func (uc *UseCase) SetOptions(options Options) { 27 | if uc.options != options { 28 | uc.options = options 29 | log.Info("set new options", zap.Any("options", uc.options)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/contact/internal/useCase/error.go: -------------------------------------------------------------------------------- 1 | package useCase 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrContactNotFound = errors.New("contact not found") 7 | ErrGroupNotFound = errors.New("group not found") 8 | ) 9 | -------------------------------------------------------------------------------- /services/contact/internal/useCase/group/contactInGroup.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | 6 | "architecture_go/pkg/type/context" 7 | "architecture_go/services/contact/internal/domain/contact" 8 | ) 9 | 10 | func (uc *UseCase) CreateContactIntoGroup(ctx context.Context, groupID uuid.UUID, contacts ...*contact.Contact) ([]*contact.Contact, error) { 11 | return uc.adapterStorage.CreateContactIntoGroup(ctx, groupID, contacts...) 12 | } 13 | 14 | func (uc *UseCase) AddContactToGroup(ctx context.Context, groupID, contactID uuid.UUID) error { 15 | return uc.adapterStorage.AddContactsToGroup(ctx, groupID, contactID) 16 | } 17 | 18 | func (uc *UseCase) DeleteContactFromGroup(ctx context.Context, groupID, contactID uuid.UUID) error { 19 | return uc.adapterStorage.DeleteContactFromGroup(ctx, groupID, contactID) 20 | } 21 | -------------------------------------------------------------------------------- /services/contact/internal/useCase/group/group.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | 8 | "architecture_go/pkg/type/context" 9 | "architecture_go/pkg/type/queryParameter" 10 | "architecture_go/services/contact/internal/domain/group" 11 | ) 12 | 13 | func (uc *UseCase) Create(ctx context.Context, groupCreate *group.Group) (*group.Group, error) { 14 | return uc.adapterStorage.CreateGroup(ctx, groupCreate) 15 | } 16 | 17 | func (uc *UseCase) Update(ctx context.Context, groupUpdate *group.Group) (*group.Group, error) { 18 | return uc.adapterStorage.UpdateGroup(ctx, groupUpdate.ID(), func(oldGroup *group.Group) (*group.Group, error) { 19 | return group.NewWithID(oldGroup.ID(), oldGroup.CreatedAt(), time.Now().UTC(), groupUpdate.Name(), groupUpdate.Description(), oldGroup.ContactCount()), nil 20 | }) 21 | } 22 | 23 | func (uc *UseCase) Delete(ctx context.Context, ID uuid.UUID) error { 24 | return uc.adapterStorage.DeleteGroup(ctx, ID) 25 | } 26 | 27 | func (uc *UseCase) List(ctx context.Context, parameter queryParameter.QueryParameter) ([]*group.Group, error) { 28 | return uc.adapterStorage.ListGroup(ctx, parameter) 29 | } 30 | 31 | func (uc *UseCase) ReadByID(ctx context.Context, ID uuid.UUID) (*group.Group, error) { 32 | return uc.adapterStorage.ReadGroupByID(ctx, ID) 33 | } 34 | 35 | func (uc *UseCase) Count(ctx context.Context) (uint64, error) { 36 | return uc.adapterStorage.CountGroup(ctx) 37 | } 38 | -------------------------------------------------------------------------------- /services/contact/internal/useCase/group/useCase.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | 6 | log "architecture_go/pkg/type/logger" 7 | "architecture_go/services/contact/internal/useCase/adapters/storage" 8 | ) 9 | 10 | type UseCase struct { 11 | adapterStorage storage.Group 12 | options Options 13 | } 14 | 15 | type Options struct{} 16 | 17 | func New(storage storage.Group, options Options) *UseCase { 18 | var uc = &UseCase{ 19 | adapterStorage: storage, 20 | } 21 | uc.SetOptions(options) 22 | return uc 23 | } 24 | 25 | func (uc *UseCase) SetOptions(options Options) { 26 | if uc.options != options { 27 | uc.options = options 28 | log.Info("set new options", zap.Any("options", uc.options)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /services/contact/internal/useCase/interface.go: -------------------------------------------------------------------------------- 1 | package useCase 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | 6 | "architecture_go/pkg/type/context" 7 | "architecture_go/pkg/type/queryParameter" 8 | "architecture_go/services/contact/internal/domain/contact" 9 | "architecture_go/services/contact/internal/domain/group" 10 | ) 11 | 12 | type Contact interface { 13 | Create(c context.Context, contacts ...*contact.Contact) ([]*contact.Contact, error) 14 | Update(c context.Context, contactUpdate contact.Contact) (*contact.Contact, error) 15 | Delete(c context.Context, ID uuid.UUID /*Тут можно передавать фильтр*/) error 16 | 17 | ContactReader 18 | } 19 | 20 | type ContactReader interface { 21 | List(c context.Context, parameter queryParameter.QueryParameter) ([]*contact.Contact, error) 22 | ReadByID(c context.Context, ID uuid.UUID) (response *contact.Contact, err error) 23 | Count(c context.Context /*Тут можно передавать фильтр*/) (uint64, error) 24 | } 25 | 26 | type Group interface { 27 | Create(c context.Context, groupCreate *group.Group) (*group.Group, error) 28 | Update(c context.Context, groupUpdate *group.Group) (*group.Group, error) 29 | Delete(c context.Context, ID uuid.UUID /*Тут можно передавать фильтр*/) error 30 | 31 | GroupReader 32 | ContactInGroup 33 | } 34 | 35 | type GroupReader interface { 36 | List(c context.Context, parameter queryParameter.QueryParameter) ([]*group.Group, error) 37 | ReadByID(c context.Context, ID uuid.UUID) (*group.Group, error) 38 | Count(c context.Context /*Тут можно передавать фильтр*/) (uint64, error) 39 | } 40 | 41 | type ContactInGroup interface { 42 | CreateContactIntoGroup(c context.Context, groupID uuid.UUID, contacts ...*contact.Contact) ([]*contact.Contact, error) 43 | AddContactToGroup(c context.Context, groupID, contactID uuid.UUID) error 44 | DeleteContactFromGroup(c context.Context, groupID, contactID uuid.UUID) error 45 | } 46 | -------------------------------------------------------------------------------- /services/contact/migrations/README.md: -------------------------------------------------------------------------------- 1 | ### Данные для миграции 2 | 3 | Положите sql файлы сюда. -------------------------------------------------------------------------------- /services/contact/migrations/postgres/README.md: -------------------------------------------------------------------------------- 1 | ### Данные для миграции postgres -------------------------------------------------------------------------------- /services/contact/proto/contact.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package contact; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | option go_package = "./;contact"; 8 | 9 | service ContactService { 10 | rpc CreateGroup (CreateGroupRequest) returns (CreateGroupResponse) {} 11 | rpc UpdateGroup (UpdateGroupRequest) returns (UpdateGroupResponse) {} 12 | rpc DeleteGroup (DeleteGroupRequest) returns (DeleteGroupResponse) {} 13 | } 14 | 15 | message CreateGroupRequest { 16 | string created_by = 1; 17 | 18 | string name = 2; 19 | string description = 3; 20 | } 21 | 22 | message GroupResponse { 23 | string id = 1; 24 | string name = 2; 25 | string description = 3; 26 | 27 | google.protobuf.Timestamp created_at = 4; 28 | google.protobuf.Timestamp modified_at = 5; 29 | 30 | uint64 contactCount = 6; 31 | } 32 | 33 | message CreateGroupResponse { 34 | GroupResponse response = 1; 35 | } 36 | 37 | message UpdateGroupRequest { 38 | string id = 1; 39 | string created_by = 2; 40 | 41 | string name = 3; 42 | string description = 4; 43 | } 44 | 45 | message UpdateGroupResponse { 46 | GroupResponse response = 1; 47 | } 48 | 49 | message DeleteGroupRequest { 50 | string id = 1; 51 | string created_by = 2; 52 | } 53 | 54 | message DeleteGroupResponse { 55 | GroupResponse response = 1; 56 | } 57 | -------------------------------------------------------------------------------- /services/contact/protogen.go: -------------------------------------------------------------------------------- 1 | //go:build proto 2 | // +build proto 3 | 4 | package main 5 | 6 | //go:generate protoc --proto_path=. --proto_path=proto --go_opt=paths=source_relative --go_out=internal/delivery/grpc/interface contact.proto 7 | //go:generate protoc --proto_path=. --proto_path=proto --go-grpc_opt=paths=source_relative --go-grpc_out=internal/delivery/grpc/interface contact.proto 8 | -------------------------------------------------------------------------------- /services/contact/scripts/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertGoodman08/architecture_golang/c16cec67bfda08f6edadf33f2988e20e817d78db/services/contact/scripts/README.md -------------------------------------------------------------------------------- /services/user/README.md: -------------------------------------------------------------------------------- 1 | ### Реализация конкретного сервиса + все данные для его запуска --------------------------------------------------------------------------------