First commit

This commit is contained in:
Egor 2024-06-25 02:34:10 +03:00
commit 1161a56ec9
25 changed files with 5288 additions and 0 deletions

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
Dockerfile
gqlgen.yml
*.graphqls
deployments/
Makefile

0
.gitignore vendored Normal file
View file

12
Dockerfile Normal file
View file

@ -0,0 +1,12 @@
FROM golang:1.22.4
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /ozon-task cmd/server.go
CMD ["/ozon-task"]

16
Makefile Normal file
View file

@ -0,0 +1,16 @@
docker-postgres: docker
docker compose -f deployments/docker-compose/postgres/docker-compose.yml up
docker-standalone: docker
docker compose -f deployments/docker-compose/standalone/docker-compose.yml up
docker: go-generate Dockerfile
docker build --tag ozon-task .
go-generate:
go generate ./...
clean: docker-clean
docker-clean:
docker container rm postgres-ozon-task-1 postgres-postgres-1 && docker volume rm postgres_postgres-data

36
cmd/server.go Normal file
View file

@ -0,0 +1,36 @@
package main
import (
"log"
"net/http"
"os"
"git.obamna.ru/erius/ozon-task/graph"
"git.obamna.ru/erius/ozon-task/internal/storage"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
)
const defaultPort = "8080"
var port = os.Getenv("APP_PORT")
func main() {
if port == "" {
port = defaultPort
}
s, err := storage.InitStorage()
if err != nil {
log.Panic("could not connect to database")
}
res := graph.InitResolver(s)
srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: res}))
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
http.Handle("/query", srv)
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}

View file

@ -0,0 +1,23 @@
services:
ozon-task:
image: ozon-task:latest
ports:
- 8080:8080
env_file: ozon-task.env
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:latest
hostname: postgres
env_file: postgres.env
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:

View file

@ -0,0 +1,11 @@
# Define storage type for the app
# inmemory (default) - store data in memory (non-persistent)
# postgres - store data in postgres database
APP_STORAGE=postgres
# Postgres configuration for APP_STORAGE=postgres
APP_POSTGRES_HOST=postgres
APP_POSTGRES_PORT=5432
APP_POSTGRES_USER=username
APP_POSTGRES_PASSWORD=password
APP_POSTGRES_DB=database

View file

@ -0,0 +1,3 @@
POSTGRES_USER=username
POSTGRES_PASSWORD=password
POSTGRES_DB=database

View file

@ -0,0 +1,6 @@
services:
ozon-task:
image: ozon-task:latest
ports:
- 8080:8080
env_file: ozon-task.env

View file

@ -0,0 +1,4 @@
# Define storage type for the app
# inmemory (default) - store data in memory (non-persistent)
# postgres - store data in postgres database
APP_STORAGE=inmemory

37
go.mod Normal file
View file

@ -0,0 +1,37 @@
module git.obamna.ru/erius/ozon-task
go 1.22.4
require (
github.com/99designs/gqlgen v0.17.49
github.com/vektah/gqlparser/v2 v2.5.16
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.10
)
require (
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/urfave/cli/v2 v2.27.2 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/tools v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

89
go.sum Normal file
View file

@ -0,0 +1,89 @@
github.com/99designs/gqlgen v0.17.49 h1:b3hNGexHd33fBSAd4NDT/c3NCcQzcAVkknhN9ym36YQ=
github.com/99designs/gqlgen v0.17.49/go.mod h1:tC8YFVZMed81x7UJ7ORUwXF4Kn6SXuucFqQBhN8+BU0=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8=
github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

88
gqlgen.yml Normal file
View file

@ -0,0 +1,88 @@
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema:
- graph/*.graphqls
# Where should the generated server code go?
exec:
filename: graph/generated.go
package: graph
# Uncomment to enable federation
# federation:
# filename: graph/federation.go
# package: graph
# Where should any generated models go?
model:
filename: graph/model/models_gen.go
package: model
# Where should the resolver implementations go?
resolver:
layout: follow-schema
dir: graph
package: graph
filename_template: "{name}.resolvers.go"
# Optional: turn on to not generate template comments above resolvers
# omit_template_comment: false
# Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models
# struct_tag: json
# Optional: turn on to use []Thing instead of []*Thing
# omit_slice_element_pointers: false
# Optional: turn on to omit Is<Name>() methods to interface and unions
# omit_interface_checks : true
# Optional: turn on to skip generation of ComplexityRoot struct content and Complexity function
# omit_complexity: false
# Optional: turn on to not generate any file notice comments in generated files
# omit_gqlgen_file_notice: false
# Optional: turn on to exclude the gqlgen version in the generated file notice. No effect if `omit_gqlgen_file_notice` is true.
# omit_gqlgen_version_in_file_notice: false
# Optional: turn off to make struct-type struct fields not use pointers
# e.g. type Thing struct { FieldA OtherThing } instead of { FieldA *OtherThing }
# struct_fields_always_pointers: true
# Optional: turn off to make resolvers return values instead of pointers for structs
# resolvers_always_return_pointers: true
# Optional: turn on to return pointers instead of values in unmarshalInput
# return_pointers_in_unmarshalinput: false
# Optional: wrap nullable input fields with Omittable
# nullable_input_omittable: true
# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true
# Optional: set to skip running `go mod tidy` when generating server code
# skip_mod_tidy: true
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
autobind:
- "git.obamna.ru/erius/ozon-task/graph/model"
# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
- github.com/99designs/gqlgen/graphql.Uint
Int:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32

4593
graph/generated.go Normal file

File diff suppressed because it is too large Load diff

61
graph/model/data.go Normal file
View file

@ -0,0 +1,61 @@
package model
import (
"time"
"gorm.io/gorm"
)
const CommentLengthLimit = 2000
type Comment struct {
// db fields
ID uint `json:"id" gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
// db foreign key to reference parent post
PostID uint
Post *Post `json:"post"`
Author string `json:"author"`
Contents string `json:"contents"`
// db key to reference parent comment
ReplyToID *uint
ReplyTo *Comment `json:"reply_to,omitempty"`
Replies []*Comment `json:"replies" gorm:"many2many:comment_replies"`
}
type Post struct {
// db fields
ID uint `json:"id" gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Title string `json:"title"`
Author string `json:"author"`
Contents string `json:"contents"`
Comments []*Comment `json:"comments" gorm:"foreignKey:PostID"`
AllowComments bool `json:"allowComments"`
}
func PostFromInput(input *PostInput) *Post {
return &Post{
Author: input.Author,
Title: input.Title,
Contents: input.Contents,
AllowComments: input.AllowComments,
}
}
func CommentFromInput(input *CommentInput) *Comment {
return &Comment{
Author: input.Author,
Contents: input.Contents,
Replies: make([]*Comment, 0),
}
}

View file

@ -0,0 +1,9 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package model
type Mutation struct {
}
type Query struct {
}

19
graph/model/mutation.go Normal file
View file

@ -0,0 +1,19 @@
package model
type AddResult struct {
ItemID *uint `json:"item_id,omitempty"`
}
type CommentInput struct {
ParentPostID *uint `json:"parent_post_id,omitempty"`
ParentCommentID *uint `json:"parent_comment_id,omitempty"`
Author string `json:"author"`
Contents string `json:"contents"`
}
type PostInput struct {
Title string `json:"title"`
Author string `json:"author"`
Contents string `json:"contents"`
AllowComments bool `json:"allowComments"`
}

19
graph/resolver.go Normal file
View file

@ -0,0 +1,19 @@
package graph
//go:generate go run github.com/99designs/gqlgen generate
import (
"git.obamna.ru/erius/ozon-task/internal/storage"
)
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct {
Storage storage.Storage
}
func InitResolver(s storage.Storage) *Resolver {
return &Resolver{Storage: s}
}

44
graph/schema.graphqls Normal file
View file

@ -0,0 +1,44 @@
type Post {
id: ID!
title: String!
author: String!
contents: String!
comments: [Comment!]!
allowComments: Boolean!
}
type Comment {
id: ID!
post: Post!
author: String!
contents: String!
replyTo: Comment
replies: [Comment!]!
}
type Query {
allPosts: [Post!]!
}
type Mutation {
addPost(input: PostInput): AddResult!
addComment(input: CommentInput): AddResult!
}
input PostInput {
title: String!
author: String!
contents: String!
allowComments: Boolean! = true
}
input CommentInput {
parentPostId: ID
parentCommentId: ID
author: String!
contents: String!
}
type AddResult {
itemId: ID!
}

42
graph/schema.resolvers.go Normal file
View file

@ -0,0 +1,42 @@
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.49
import (
"context"
"fmt"
"git.obamna.ru/erius/ozon-task/graph/model"
)
// AddPost is the resolver for the add_post field.
func (r *mutationResolver) AddPost(ctx context.Context, input *model.PostInput) (*model.AddResult, error) {
return r.Storage.AddPost(input)
}
// AddComment is the resolver for the add_comment field.
func (r *mutationResolver) AddComment(ctx context.Context, input *model.CommentInput) (*model.AddResult, error) {
if len(input.Contents) > model.CommentLengthLimit {
return nil, fmt.Errorf("exceeded max comment length of %d chars", model.CommentLengthLimit)
}
if input.ParentCommentID == nil && input.ParentPostID == nil {
return nil, fmt.Errorf("parent post or parent comment ids weren't specified")
}
return r.Storage.AddComment(input)
}
// AllPosts is the resolver for the all_posts field.
func (r *queryResolver) AllPosts(ctx context.Context) ([]*model.Post, error) {
return r.Storage.GetPosts()
}
// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

View file

@ -0,0 +1,62 @@
package db
import (
"git.obamna.ru/erius/ozon-task/graph/model"
"gorm.io/gorm"
)
type Database struct {
db *gorm.DB
}
func (s *Database) AddPost(input *model.PostInput) (*model.AddResult, error) {
post := model.PostFromInput(input)
err := s.db.Create(post).Error
return &model.AddResult{ItemID: &post.ID}, err
}
func (s *Database) AddComment(input *model.CommentInput) (*model.AddResult, error) {
comment := model.CommentFromInput(input)
if input.ParentPostID == nil {
// multiple operations performed in one transaction
err := s.db.Transaction(func(tx *gorm.DB) error {
var parent model.Comment
// find parent comment in db
err := s.db.First(&parent, *input.ParentCommentID).Error
if err != nil {
return err
}
// set new comment fields to reference parent post and comment
comment.ReplyTo = &parent
comment.PostID = parent.PostID
// insert comment
err = s.db.Create(comment).Error
if err != nil {
return err
}
// add new reply to parent and save
parent.Replies = append(parent.Replies, comment)
err = s.db.Save(parent).Error
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
} else {
comment.PostID = *input.ParentPostID
err := s.db.Create(comment).Error
if err != nil {
return nil, err
}
}
return &model.AddResult{ItemID: &comment.ID}, nil
}
func (s Database) GetPosts() ([]*model.Post, error) {
var posts []*model.Post
err := s.db.Find(&posts).Error
return posts, err
}

View file

@ -0,0 +1,29 @@
package db
import (
"fmt"
"os"
"git.obamna.ru/erius/ozon-task/graph/model"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var (
host = os.Getenv("APP_POSTGRES_HOST")
port = os.Getenv("APP_POSTGRES_PORT")
user = os.Getenv("APP_POSTGRES_USER")
passwd = os.Getenv("APP_POSTGRES_PASSWORD")
db = os.Getenv("APP_POSTGRES_DB")
con = fmt.Sprintf("postgres://%s:%s@%s:%s/%s", user, passwd, host, port, db)
)
func InitPostgres() (*Database, error) {
// PrepareStmt is true for caching complex sql statements when adding comments or replies
db, err := gorm.Open(postgres.Open(con), &gorm.Config{PrepareStmt: true})
if err != nil {
return nil, err
}
db.AutoMigrate(&model.Post{}, &model.Comment{})
return &Database{db}, nil
}

View file

@ -0,0 +1,41 @@
package storage
import (
"git.obamna.ru/erius/ozon-task/graph/model"
)
type InMemory struct {
postId uint
commentId uint
posts []*model.Post
comments []*model.Comment
}
func InitInMemory() *InMemory {
return &InMemory{
postId: 0,
commentId: 0,
posts: make([]*model.Post, 0),
comments: make([]*model.Comment, 0),
}
}
func (s *InMemory) AddPost(input *model.PostInput) (*model.AddResult, error) {
post := model.PostFromInput(input)
post.ID = s.postId
s.posts = append(s.posts, post)
s.postId++
return &model.AddResult{ItemID: &post.ID}, nil
}
func (s *InMemory) AddComment(input *model.CommentInput) (*model.AddResult, error) {
comment := model.CommentFromInput(input)
comment.ID = s.commentId
s.comments = append(s.comments, comment)
s.commentId++
return &model.AddResult{ItemID: &comment.ID}, nil
}
func (s InMemory) GetPosts() ([]*model.Post, error) {
return s.posts, nil
}

View file

@ -0,0 +1,32 @@
package storage
import (
"os"
"git.obamna.ru/erius/ozon-task/graph/model"
"git.obamna.ru/erius/ozon-task/internal/storage/db"
)
const (
inMemory = "inmemory"
postgres = "postgres"
)
var storage = os.Getenv("APP_STORAGE")
type Storage interface {
AddPost(input *model.PostInput) (*model.AddResult, error)
AddComment(input *model.CommentInput) (*model.AddResult, error)
GetPosts() ([]*model.Post, error)
}
func InitStorage() (Storage, error) {
switch storage {
case inMemory:
return InitInMemory(), nil
case postgres:
return db.InitPostgres()
default:
return InitInMemory(), nil
}
}

7
tools.go Normal file
View file

@ -0,0 +1,7 @@
//go:build tools
package tools
import (
_ "github.com/99designs/gqlgen"
)