diff --git a/backend/server.go b/backend/cmd/server/main.go similarity index 56% rename from backend/server.go rename to backend/cmd/server/main.go index 974c4ea..60a70b5 100644 --- a/backend/server.go +++ b/backend/cmd/server/main.go @@ -1,28 +1,39 @@ package main import ( + "database/sql" "log" "net/http" - "os" - "git.squidspirit.com/squid/blog.git/backend/internal/framework/graph" + "git.squidspirit.com/squid/blog.git/backend/internal/adapter/gateway" + "git.squidspirit.com/squid/blog.git/backend/internal/application" + "git.squidspirit.com/squid/blog.git/backend/internal/framework/api/graph" + "git.squidspirit.com/squid/blog.git/backend/internal/framework/db/postgres" + "git.squidspirit.com/squid/blog.git/backend/internal/pkg/env" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/lru" "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/playground" + _ "github.com/lib/pq" "github.com/vektah/gqlparser/v2/ast" ) -const defaultPort = "8080" - func main() { - port := os.Getenv("PORT") - if port == "" { - port = defaultPort + db, err := postgres.Connect(env.GetDSN()) + if err != nil { + log.Fatal(err) } - srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}})) + initGraphqlHandler(createResolver(db)) + + port := env.GetPort() + log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) +} + +func initGraphqlHandler(resolver *graph.Resolver) *handler.Server { + srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: resolver})) srv.AddTransport(transport.Options{}) srv.AddTransport(transport.GET{}) @@ -38,6 +49,13 @@ func main() { 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)) + return srv +} + +func createResolver(db *sql.DB) *graph.Resolver { + postRepo := gateway.NewPostRepo(postgres.NewPostDBService(db)) + + return &graph.Resolver{ + GetAllPostsUseCase: application.NewGetAllPostsUseCase(postRepo), + } } diff --git a/backend/go.mod b/backend/go.mod index 2b306d1..12d84da 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,6 +4,8 @@ go 1.24.1 require ( github.com/99designs/gqlgen v0.17.66 + github.com/lib/pq v1.10.9 + github.com/thoas/go-funk v0.9.3 github.com/vektah/gqlparser/v2 v2.5.22 ) @@ -11,16 +13,20 @@ require ( github.com/agnivade/levenshtein v1.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/google/go-cmp v0.7.0 // 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/kr/pretty v0.3.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // 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.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/tools v0.30.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/tools v0.31.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 63bcc7f..23ffeff 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -12,47 +12,68 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/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-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= +github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/vektah/gqlparser/v2 v2.5.22 h1:yaaeJ0fu+nv1vUMW0Hl+aS1eiv1vMfapBNjpffAda1I= github.com/vektah/gqlparser/v2 v2.5.22/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnHZdw7tfhEGfTM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/gqlgen.yml b/backend/gqlgen.yml index c06c88f..3832c30 100644 --- a/backend/gqlgen.yml +++ b/backend/gqlgen.yml @@ -1,6 +1,6 @@ # Where are all the schema files located? globs are supported eg src/**/*.graphqls schema: - - internal/framework/graph/*.graphqls + - internal/framework/api/graph/*.graphqls # Where should the generated server code go? exec: @@ -8,7 +8,7 @@ exec: layout: single-file # Only other option is "follow-schema," ie multi-file. # Only for single-file layout: - filename: internal/framework/graph/generated.go + filename: internal/framework/api/graph/generated.go # Only for follow-schema layout: # dir: graph @@ -27,8 +27,8 @@ exec: # Where should any generated models go? model: - filename: internal/framework/graph/model/models_gen.go - package: model + filename: internal/adapter/controller/graphdto/dtos.go + package: graphdto # Optional: Pass in a path to a new gotpl template to use for generating the models # model_template: [your/path/model.gotpl] @@ -42,7 +42,7 @@ resolver: # filename: graph/resolver.go # Only for follow-schema layout: - dir: internal/framework/graph + dir: internal/framework/api/graph filename_template: "{name}.resolvers.go" # Optional: turn on to not generate template comments above resolvers @@ -127,10 +127,7 @@ autobind: 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.Uint32 # gqlgen provides a default GraphQL UUID convenience wrapper for github.com/google/uuid # but you can override this to provide your own GraphQL UUID implementation UUID: diff --git a/backend/internal/framework/graph/model/models_gen.go b/backend/internal/adapter/controller/graphdto/dtos.go similarity index 74% rename from backend/internal/framework/graph/model/models_gen.go rename to backend/internal/adapter/controller/graphdto/dtos.go index 18f83c4..617d2a0 100644 --- a/backend/internal/framework/graph/model/models_gen.go +++ b/backend/internal/adapter/controller/graphdto/dtos.go @@ -1,20 +1,21 @@ // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. -package model +package graphdto type Label struct { - ID string `json:"id"` + ID uint32 `json:"id"` Name string `json:"name"` Color string `json:"color"` } type Post struct { - ID string `json:"id"` + ID uint32 `json:"id"` Title string `json:"title"` Content string `json:"content"` Description string `json:"description"` PreviewImageURL string `json:"previewImageUrl"` Labels []*Label `json:"labels"` + PublishedTime *string `json:"publishedTime,omitempty"` } type Query struct { diff --git a/backend/internal/adapter/controller/graphdto/new_label_dto.go b/backend/internal/adapter/controller/graphdto/new_label_dto.go new file mode 100644 index 0000000..b564a45 --- /dev/null +++ b/backend/internal/adapter/controller/graphdto/new_label_dto.go @@ -0,0 +1,11 @@ +package graphdto + +import "git.squidspirit.com/squid/blog.git/backend/internal/domain" + +func NewLabelDTO(labelEntity *domain.Label) *Label { + return &Label{ + ID: labelEntity.ID, + Name: labelEntity.Name, + Color: labelEntity.Color, + } +} diff --git a/backend/internal/adapter/controller/graphdto/new_post_dto.go b/backend/internal/adapter/controller/graphdto/new_post_dto.go new file mode 100644 index 0000000..1bb118f --- /dev/null +++ b/backend/internal/adapter/controller/graphdto/new_post_dto.go @@ -0,0 +1,19 @@ +package graphdto + +import ( + "git.squidspirit.com/squid/blog.git/backend/internal/domain" + "github.com/thoas/go-funk" +) + +func NewPostDTO(entity *domain.Post) *Post { + return &Post{ + ID: entity.ID, + Title: entity.Title, + Content: entity.Content, + Description: entity.Description, + PreviewImageURL: entity.PreviewImageURL, + Labels: funk.Map(entity.Labels, func(label *domain.Label) *Label { + return NewLabelDTO(label) + }).([]*Label), + } +} diff --git a/backend/internal/adapter/controller/query_posts_controller.go b/backend/internal/adapter/controller/query_posts_controller.go new file mode 100644 index 0000000..fd8dfda --- /dev/null +++ b/backend/internal/adapter/controller/query_posts_controller.go @@ -0,0 +1,33 @@ +package controller + +import ( + "git.squidspirit.com/squid/blog.git/backend/internal/adapter/controller/graphdto" + "git.squidspirit.com/squid/blog.git/backend/internal/application" + "git.squidspirit.com/squid/blog.git/backend/internal/domain" + "github.com/thoas/go-funk" +) + +type QueryPostsController interface { + Handle() ([]*graphdto.Post, error) +} + +type queryPostsControllerImpl struct { + getAllPostsUseCase application.GetAllPostsUseCase +} + +func NewQueryPostsController(getAllPostsUseCase application.GetAllPostsUseCase) QueryPostsController { + return &queryPostsControllerImpl{ + getAllPostsUseCase: getAllPostsUseCase, + } +} + +func (c *queryPostsControllerImpl) Handle() ([]*graphdto.Post, error) { + entities, err := c.getAllPostsUseCase.Execute() + if err != nil { + return nil, err + } + + return funk.Map(entities, func(entity *domain.Post) *graphdto.Post { + return graphdto.NewPostDTO(entity) + }).([]*graphdto.Post), nil +} diff --git a/backend/internal/adapter/gateway/dbdto/label.go b/backend/internal/adapter/gateway/dbdto/label.go new file mode 100644 index 0000000..8f5538c --- /dev/null +++ b/backend/internal/adapter/gateway/dbdto/label.go @@ -0,0 +1,23 @@ +package dbdto + +import ( + "time" + + "git.squidspirit.com/squid/blog.git/backend/internal/domain" +) + +type Label struct { + ID uint32 `json:"id"` + Name string `json:"name"` + Color string `json:"color"` + CreatedTime time.Time + UpdatedTime time.Time +} + +func (l *Label) ToEntity() *domain.Label { + return &domain.Label{ + ID: l.ID, + Name: l.Name, + Color: l.Color, + } +} diff --git a/backend/internal/adapter/gateway/dbdto/post.go b/backend/internal/adapter/gateway/dbdto/post.go new file mode 100644 index 0000000..26a08f9 --- /dev/null +++ b/backend/internal/adapter/gateway/dbdto/post.go @@ -0,0 +1,34 @@ +package dbdto + +import ( + "time" + + "git.squidspirit.com/squid/blog.git/backend/internal/domain" + "github.com/thoas/go-funk" +) + +type Post struct { + ID uint32 + Title string + Content string + Description string + PreviewImageURL string + Labels []*Label + PublishedTime *time.Time + CreatedTime time.Time + UpdatedTime time.Time +} + +func (p *Post) ToEntity() *domain.Post { + return &domain.Post{ + ID: p.ID, + Title: p.Title, + Content: p.Content, + Description: p.Description, + PreviewImageURL: p.PreviewImageURL, + Labels: funk.Map(p.Labels, func(label *Label) *domain.Label { + return label.ToEntity() + }).([]*domain.Label), + PublishedTime: p.PublishedTime, + } +} diff --git a/backend/internal/adapter/gateway/post_repo_impl.go b/backend/internal/adapter/gateway/post_repo_impl.go new file mode 100644 index 0000000..81ff881 --- /dev/null +++ b/backend/internal/adapter/gateway/post_repo_impl.go @@ -0,0 +1,34 @@ +package gateway + +import ( + "git.squidspirit.com/squid/blog.git/backend/internal/adapter/gateway/dbdto" + "git.squidspirit.com/squid/blog.git/backend/internal/application" + "git.squidspirit.com/squid/blog.git/backend/internal/domain" + "github.com/thoas/go-funk" +) + +type postRepo struct { + dbService PostDBService +} + +type PostDBService interface { + QueryAll() ([]*dbdto.Post, error) +} + +func NewPostRepo(dbService PostDBService) application.PostRepo { + return &postRepo{dbService} +} + +func (r *postRepo) GetAll() ([]*domain.Post, error) { + postDtos, err := r.dbService.QueryAll() + if err != nil { + return nil, err + } + return funk.Map(postDtos, func(postDto *dbdto.Post) *domain.Post { + return postDto.ToEntity() + }).([]*domain.Post), nil +} + +func (r *postRepo) GetByID(id int) (*domain.Post, error) { + panic("unimplemented") +} diff --git a/backend/internal/application/get_all_posts_use_case.go b/backend/internal/application/get_all_posts_use_case.go new file mode 100644 index 0000000..8c59f5d --- /dev/null +++ b/backend/internal/application/get_all_posts_use_case.go @@ -0,0 +1,21 @@ +package application + +import "git.squidspirit.com/squid/blog.git/backend/internal/domain" + +type GetAllPostsUseCase interface { + Execute() ([]*domain.Post, error) +} + +type getAllPostsUseCaseImpl struct { + postRepo PostRepo +} + +func NewGetAllPostsUseCase(postRepo PostRepo) GetAllPostsUseCase { + return &getAllPostsUseCaseImpl{ + postRepo: postRepo, + } +} + +func (uc *getAllPostsUseCaseImpl) Execute() ([]*domain.Post, error) { + return uc.postRepo.GetAll() +} diff --git a/backend/internal/application/label_repo.go b/backend/internal/application/label_repo.go new file mode 100644 index 0000000..b47f7cf --- /dev/null +++ b/backend/internal/application/label_repo.go @@ -0,0 +1,7 @@ +package application + +import "git.squidspirit.com/squid/blog.git/backend/internal/domain" + +type LabelRepo interface { + GetByIDs(ids []int) ([]*domain.Label, error) +} diff --git a/backend/internal/application/post_repo.go b/backend/internal/application/post_repo.go new file mode 100644 index 0000000..7159a18 --- /dev/null +++ b/backend/internal/application/post_repo.go @@ -0,0 +1,8 @@ +package application + +import "git.squidspirit.com/squid/blog.git/backend/internal/domain" + +type PostRepo interface { + GetAll() ([]*domain.Post, error) + GetByID(id int) (*domain.Post, error) +} diff --git a/backend/internal/domain/label.go b/backend/internal/domain/label.go new file mode 100644 index 0000000..5488d1a --- /dev/null +++ b/backend/internal/domain/label.go @@ -0,0 +1,7 @@ +package domain + +type Label struct { + ID uint32 + Name string + Color string +} diff --git a/backend/internal/domain/post.go b/backend/internal/domain/post.go new file mode 100644 index 0000000..b65a9b8 --- /dev/null +++ b/backend/internal/domain/post.go @@ -0,0 +1,13 @@ +package domain + +import "time" + +type Post struct { + ID uint32 + Title string + Content string + Description string + PreviewImageURL string + Labels []*Label + PublishedTime *time.Time +} diff --git a/backend/internal/framework/graph/generated.go b/backend/internal/framework/api/graph/generated.go similarity index 93% rename from backend/internal/framework/graph/generated.go rename to backend/internal/framework/api/graph/generated.go index 9bb33f9..f15b022 100644 --- a/backend/internal/framework/graph/generated.go +++ b/backend/internal/framework/api/graph/generated.go @@ -12,7 +12,7 @@ import ( "sync" "sync/atomic" - "git.squidspirit.com/squid/blog.git/backend/internal/framework/graph/model" + "git.squidspirit.com/squid/blog.git/backend/internal/adapter/controller/graphdto" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" gqlparser "github.com/vektah/gqlparser/v2" @@ -58,18 +58,21 @@ type ComplexityRoot struct { ID func(childComplexity int) int Labels func(childComplexity int) int PreviewImageURL func(childComplexity int) int + PublishedTime func(childComplexity int) int Title func(childComplexity int) int } Query struct { - Post func(childComplexity int, id string) int + Label func(childComplexity int, id uint32) int + Post func(childComplexity int, id uint32) int Posts func(childComplexity int) int } } type QueryResolver interface { - Posts(ctx context.Context) ([]*model.Post, error) - Post(ctx context.Context, id string) (*model.Post, error) + Posts(ctx context.Context) ([]*graphdto.Post, error) + Post(ctx context.Context, id uint32) (*graphdto.Post, error) + Label(ctx context.Context, id uint32) (*graphdto.Label, error) } type executableSchema struct { @@ -147,6 +150,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Post.PreviewImageURL(childComplexity), true + case "Post.publishedTime": + if e.complexity.Post.PublishedTime == nil { + break + } + + return e.complexity.Post.PublishedTime(childComplexity), true + case "Post.title": if e.complexity.Post.Title == nil { break @@ -154,6 +164,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Post.Title(childComplexity), true + case "Query.label": + if e.complexity.Query.Label == nil { + break + } + + args, err := ec.field_Query_label_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.Label(childComplexity, args["id"].(uint32)), true + case "Query.post": if e.complexity.Query.Post == nil { break @@ -164,7 +186,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.Post(childComplexity, args["id"].(string)), true + return e.complexity.Query.Post(childComplexity, args["id"].(uint32)), true case "Query.posts": if e.complexity.Query.Posts == nil { @@ -304,6 +326,29 @@ func (ec *executionContext) field_Query___type_argsName( return zeroVal, nil } +func (ec *executionContext) field_Query_label_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Query_label_argsID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["id"] = arg0 + return args, nil +} +func (ec *executionContext) field_Query_label_argsID( + ctx context.Context, + rawArgs map[string]any, +) (uint32, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + if tmp, ok := rawArgs["id"]; ok { + return ec.unmarshalNID2uint32(ctx, tmp) + } + + var zeroVal uint32 + return zeroVal, nil +} + func (ec *executionContext) field_Query_post_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -317,13 +362,13 @@ func (ec *executionContext) field_Query_post_args(ctx context.Context, rawArgs m func (ec *executionContext) field_Query_post_argsID( ctx context.Context, rawArgs map[string]any, -) (string, error) { +) (uint32, error) { ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) if tmp, ok := rawArgs["id"]; ok { - return ec.unmarshalNID2string(ctx, tmp) + return ec.unmarshalNID2uint32(ctx, tmp) } - var zeroVal string + var zeroVal uint32 return zeroVal, nil } @@ -427,7 +472,7 @@ func (ec *executionContext) field___Type_fields_argsIncludeDeprecated( // region **************************** field.gotpl ***************************** -func (ec *executionContext) _Label_id(ctx context.Context, field graphql.CollectedField, obj *model.Label) (ret graphql.Marshaler) { +func (ec *executionContext) _Label_id(ctx context.Context, field graphql.CollectedField, obj *graphdto.Label) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Label_id(ctx, field) if err != nil { return graphql.Null @@ -453,9 +498,9 @@ func (ec *executionContext) _Label_id(ctx context.Context, field graphql.Collect } return graphql.Null } - res := resTmp.(string) + res := resTmp.(uint32) fc.Result = res - return ec.marshalNID2string(ctx, field.Selections, res) + return ec.marshalNID2uint32(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Label_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -471,7 +516,7 @@ func (ec *executionContext) fieldContext_Label_id(_ context.Context, field graph return fc, nil } -func (ec *executionContext) _Label_name(ctx context.Context, field graphql.CollectedField, obj *model.Label) (ret graphql.Marshaler) { +func (ec *executionContext) _Label_name(ctx context.Context, field graphql.CollectedField, obj *graphdto.Label) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Label_name(ctx, field) if err != nil { return graphql.Null @@ -515,7 +560,7 @@ func (ec *executionContext) fieldContext_Label_name(_ context.Context, field gra return fc, nil } -func (ec *executionContext) _Label_color(ctx context.Context, field graphql.CollectedField, obj *model.Label) (ret graphql.Marshaler) { +func (ec *executionContext) _Label_color(ctx context.Context, field graphql.CollectedField, obj *graphdto.Label) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Label_color(ctx, field) if err != nil { return graphql.Null @@ -559,7 +604,7 @@ func (ec *executionContext) fieldContext_Label_color(_ context.Context, field gr return fc, nil } -func (ec *executionContext) _Post_id(ctx context.Context, field graphql.CollectedField, obj *model.Post) (ret graphql.Marshaler) { +func (ec *executionContext) _Post_id(ctx context.Context, field graphql.CollectedField, obj *graphdto.Post) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Post_id(ctx, field) if err != nil { return graphql.Null @@ -585,9 +630,9 @@ func (ec *executionContext) _Post_id(ctx context.Context, field graphql.Collecte } return graphql.Null } - res := resTmp.(string) + res := resTmp.(uint32) fc.Result = res - return ec.marshalNID2string(ctx, field.Selections, res) + return ec.marshalNID2uint32(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Post_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -603,7 +648,7 @@ func (ec *executionContext) fieldContext_Post_id(_ context.Context, field graphq return fc, nil } -func (ec *executionContext) _Post_title(ctx context.Context, field graphql.CollectedField, obj *model.Post) (ret graphql.Marshaler) { +func (ec *executionContext) _Post_title(ctx context.Context, field graphql.CollectedField, obj *graphdto.Post) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Post_title(ctx, field) if err != nil { return graphql.Null @@ -647,7 +692,7 @@ func (ec *executionContext) fieldContext_Post_title(_ context.Context, field gra return fc, nil } -func (ec *executionContext) _Post_content(ctx context.Context, field graphql.CollectedField, obj *model.Post) (ret graphql.Marshaler) { +func (ec *executionContext) _Post_content(ctx context.Context, field graphql.CollectedField, obj *graphdto.Post) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Post_content(ctx, field) if err != nil { return graphql.Null @@ -691,7 +736,7 @@ func (ec *executionContext) fieldContext_Post_content(_ context.Context, field g return fc, nil } -func (ec *executionContext) _Post_description(ctx context.Context, field graphql.CollectedField, obj *model.Post) (ret graphql.Marshaler) { +func (ec *executionContext) _Post_description(ctx context.Context, field graphql.CollectedField, obj *graphdto.Post) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Post_description(ctx, field) if err != nil { return graphql.Null @@ -735,7 +780,7 @@ func (ec *executionContext) fieldContext_Post_description(_ context.Context, fie return fc, nil } -func (ec *executionContext) _Post_previewImageUrl(ctx context.Context, field graphql.CollectedField, obj *model.Post) (ret graphql.Marshaler) { +func (ec *executionContext) _Post_previewImageUrl(ctx context.Context, field graphql.CollectedField, obj *graphdto.Post) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Post_previewImageUrl(ctx, field) if err != nil { return graphql.Null @@ -779,7 +824,7 @@ func (ec *executionContext) fieldContext_Post_previewImageUrl(_ context.Context, return fc, nil } -func (ec *executionContext) _Post_labels(ctx context.Context, field graphql.CollectedField, obj *model.Post) (ret graphql.Marshaler) { +func (ec *executionContext) _Post_labels(ctx context.Context, field graphql.CollectedField, obj *graphdto.Post) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Post_labels(ctx, field) if err != nil { return graphql.Null @@ -805,9 +850,9 @@ func (ec *executionContext) _Post_labels(ctx context.Context, field graphql.Coll } return graphql.Null } - res := resTmp.([]*model.Label) + res := resTmp.([]*graphdto.Label) fc.Result = res - return ec.marshalNLabel2ᚕᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋframeworkᚋgraphᚋmodelᚐLabelᚄ(ctx, field.Selections, res) + return ec.marshalNLabel2ᚕᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐLabelᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Post_labels(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -831,6 +876,47 @@ func (ec *executionContext) fieldContext_Post_labels(_ context.Context, field gr return fc, nil } +func (ec *executionContext) _Post_publishedTime(ctx context.Context, field graphql.CollectedField, obj *graphdto.Post) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Post_publishedTime(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.PublishedTime, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalODate2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Post_publishedTime(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Post", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Date does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Query_posts(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_posts(ctx, field) if err != nil { @@ -857,9 +943,9 @@ func (ec *executionContext) _Query_posts(ctx context.Context, field graphql.Coll } return graphql.Null } - res := resTmp.([]*model.Post) + res := resTmp.([]*graphdto.Post) fc.Result = res - return ec.marshalNPost2ᚕᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋframeworkᚋgraphᚋmodelᚐPostᚄ(ctx, field.Selections, res) + return ec.marshalNPost2ᚕᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐPostᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_posts(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -882,6 +968,8 @@ func (ec *executionContext) fieldContext_Query_posts(_ context.Context, field gr return ec.fieldContext_Post_previewImageUrl(ctx, field) case "labels": return ec.fieldContext_Post_labels(ctx, field) + case "publishedTime": + return ec.fieldContext_Post_publishedTime(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Post", field.Name) }, @@ -903,7 +991,7 @@ func (ec *executionContext) _Query_post(ctx context.Context, field graphql.Colle }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Post(rctx, fc.Args["id"].(string)) + return ec.resolvers.Query().Post(rctx, fc.Args["id"].(uint32)) }) if err != nil { ec.Error(ctx, err) @@ -915,9 +1003,9 @@ func (ec *executionContext) _Query_post(ctx context.Context, field graphql.Colle } return graphql.Null } - res := resTmp.(*model.Post) + res := resTmp.(*graphdto.Post) fc.Result = res - return ec.marshalNPost2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋframeworkᚋgraphᚋmodelᚐPost(ctx, field.Selections, res) + return ec.marshalNPost2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐPost(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_post(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -940,6 +1028,8 @@ func (ec *executionContext) fieldContext_Query_post(ctx context.Context, field g return ec.fieldContext_Post_previewImageUrl(ctx, field) case "labels": return ec.fieldContext_Post_labels(ctx, field) + case "publishedTime": + return ec.fieldContext_Post_publishedTime(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Post", field.Name) }, @@ -958,6 +1048,69 @@ func (ec *executionContext) fieldContext_Query_post(ctx context.Context, field g return fc, nil } +func (ec *executionContext) _Query_label(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_label(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Label(rctx, fc.Args["id"].(uint32)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*graphdto.Label) + fc.Result = res + return ec.marshalNLabel2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐLabel(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_label(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Label_id(ctx, field) + case "name": + return ec.fieldContext_Label_name(ctx, field) + case "color": + return ec.fieldContext_Label_color(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Label", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_label_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___type(ctx, field) if err != nil { @@ -3050,7 +3203,7 @@ func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field var labelImplementors = []string{"Label"} -func (ec *executionContext) _Label(ctx context.Context, sel ast.SelectionSet, obj *model.Label) graphql.Marshaler { +func (ec *executionContext) _Label(ctx context.Context, sel ast.SelectionSet, obj *graphdto.Label) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, labelImplementors) out := graphql.NewFieldSet(fields) @@ -3099,7 +3252,7 @@ func (ec *executionContext) _Label(ctx context.Context, sel ast.SelectionSet, ob var postImplementors = []string{"Post"} -func (ec *executionContext) _Post(ctx context.Context, sel ast.SelectionSet, obj *model.Post) graphql.Marshaler { +func (ec *executionContext) _Post(ctx context.Context, sel ast.SelectionSet, obj *graphdto.Post) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, postImplementors) out := graphql.NewFieldSet(fields) @@ -3138,6 +3291,8 @@ func (ec *executionContext) _Post(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { out.Invalids++ } + case "publishedTime": + out.Values[i] = ec._Post_publishedTime(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -3223,6 +3378,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "label": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_label(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "__type": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -3605,13 +3782,13 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } -func (ec *executionContext) unmarshalNID2string(ctx context.Context, v any) (string, error) { - res, err := graphql.UnmarshalID(v) +func (ec *executionContext) unmarshalNID2uint32(ctx context.Context, v any) (uint32, error) { + res, err := graphql.UnmarshalUint32(v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { - res := graphql.MarshalID(v) +func (ec *executionContext) marshalNID2uint32(ctx context.Context, sel ast.SelectionSet, v uint32) graphql.Marshaler { + res := graphql.MarshalUint32(v) if res == graphql.Null { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -3620,7 +3797,11 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } -func (ec *executionContext) marshalNLabel2ᚕᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋframeworkᚋgraphᚋmodelᚐLabelᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Label) graphql.Marshaler { +func (ec *executionContext) marshalNLabel2gitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐLabel(ctx context.Context, sel ast.SelectionSet, v graphdto.Label) graphql.Marshaler { + return ec._Label(ctx, sel, &v) +} + +func (ec *executionContext) marshalNLabel2ᚕᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐLabelᚄ(ctx context.Context, sel ast.SelectionSet, v []*graphdto.Label) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -3644,7 +3825,7 @@ func (ec *executionContext) marshalNLabel2ᚕᚖgitᚗsquidspiritᚗcomᚋsquid if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNLabel2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋframeworkᚋgraphᚋmodelᚐLabel(ctx, sel, v[i]) + ret[i] = ec.marshalNLabel2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐLabel(ctx, sel, v[i]) } if isLen1 { f(i) @@ -3664,7 +3845,7 @@ func (ec *executionContext) marshalNLabel2ᚕᚖgitᚗsquidspiritᚗcomᚋsquid return ret } -func (ec *executionContext) marshalNLabel2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋframeworkᚋgraphᚋmodelᚐLabel(ctx context.Context, sel ast.SelectionSet, v *model.Label) graphql.Marshaler { +func (ec *executionContext) marshalNLabel2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐLabel(ctx context.Context, sel ast.SelectionSet, v *graphdto.Label) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -3674,11 +3855,11 @@ func (ec *executionContext) marshalNLabel2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋb return ec._Label(ctx, sel, v) } -func (ec *executionContext) marshalNPost2gitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋframeworkᚋgraphᚋmodelᚐPost(ctx context.Context, sel ast.SelectionSet, v model.Post) graphql.Marshaler { +func (ec *executionContext) marshalNPost2gitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐPost(ctx context.Context, sel ast.SelectionSet, v graphdto.Post) graphql.Marshaler { return ec._Post(ctx, sel, &v) } -func (ec *executionContext) marshalNPost2ᚕᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋframeworkᚋgraphᚋmodelᚐPostᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Post) graphql.Marshaler { +func (ec *executionContext) marshalNPost2ᚕᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐPostᚄ(ctx context.Context, sel ast.SelectionSet, v []*graphdto.Post) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -3702,7 +3883,7 @@ func (ec *executionContext) marshalNPost2ᚕᚖgitᚗsquidspiritᚗcomᚋsquid if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNPost2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋframeworkᚋgraphᚋmodelᚐPost(ctx, sel, v[i]) + ret[i] = ec.marshalNPost2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐPost(ctx, sel, v[i]) } if isLen1 { f(i) @@ -3722,7 +3903,7 @@ func (ec *executionContext) marshalNPost2ᚕᚖgitᚗsquidspiritᚗcomᚋsquid return ret } -func (ec *executionContext) marshalNPost2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋframeworkᚋgraphᚋmodelᚐPost(ctx context.Context, sel ast.SelectionSet, v *model.Post) graphql.Marshaler { +func (ec *executionContext) marshalNPost2ᚖgitᚗsquidspiritᚗcomᚋsquidᚋblogᚗgitᚋbackendᚋinternalᚋadapterᚋcontrollerᚋgraphdtoᚐPost(ctx context.Context, sel ast.SelectionSet, v *graphdto.Post) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -4026,6 +4207,22 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } +func (ec *executionContext) unmarshalODate2ᚖstring(ctx context.Context, v any) (*string, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalString(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalODate2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + res := graphql.MarshalString(*v) + return res +} + func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v any) (*string, error) { if v == nil { return nil, nil diff --git a/backend/internal/framework/graph/resolver.go b/backend/internal/framework/api/graph/resolver.go similarity index 59% rename from backend/internal/framework/graph/resolver.go rename to backend/internal/framework/api/graph/resolver.go index 6c177dc..61df366 100644 --- a/backend/internal/framework/graph/resolver.go +++ b/backend/internal/framework/api/graph/resolver.go @@ -1,9 +1,11 @@ package graph +import "git.squidspirit.com/squid/blog.git/backend/internal/application" + // This file will not be regenerated automatically. // // It serves as dependency injection for your app, add any dependencies you require here. type Resolver struct { - // db + GetAllPostsUseCase application.GetAllPostsUseCase } diff --git a/backend/internal/framework/graph/schema.graphqls b/backend/internal/framework/api/graph/schema.graphqls similarity index 79% rename from backend/internal/framework/graph/schema.graphqls rename to backend/internal/framework/api/graph/schema.graphqls index 71a6632..76d60e2 100644 --- a/backend/internal/framework/graph/schema.graphqls +++ b/backend/internal/framework/api/graph/schema.graphqls @@ -1,6 +1,4 @@ -# GraphQL schema example -# -# https://gqlgen.com/getting-started/ +scalar Date type Label { id: ID! @@ -15,9 +13,11 @@ type Post { description: String! previewImageUrl: String! labels: [Label!]! + publishedTime: Date } type Query { posts: [Post!]! post(id: ID!): Post! + label(id: ID!): Label! } diff --git a/backend/internal/framework/api/graph/schema.resolvers.go b/backend/internal/framework/api/graph/schema.resolvers.go new file mode 100644 index 0000000..af8a7f8 --- /dev/null +++ b/backend/internal/framework/api/graph/schema.resolvers.go @@ -0,0 +1,38 @@ +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.66 + +import ( + "context" + "fmt" + + "git.squidspirit.com/squid/blog.git/backend/internal/adapter/controller" + "git.squidspirit.com/squid/blog.git/backend/internal/adapter/controller/graphdto" +) + +// Posts is the resolver for the posts field. +func (r *queryResolver) Posts(ctx context.Context) ([]*graphdto.Post, error) { + c := controller.NewQueryPostsController(r.GetAllPostsUseCase) + dtos, err := c.Handle() + if err != nil { + return nil, err + } + return dtos, nil +} + +// Post is the resolver for the post field. +func (r *queryResolver) Post(ctx context.Context, id uint32) (*graphdto.Post, error) { + panic(fmt.Errorf("not implemented: Post - post")) +} + +// Label is the resolver for the label field. +func (r *queryResolver) Label(ctx context.Context, id uint32) (*graphdto.Label, error) { + panic(fmt.Errorf("not implemented: Label - label")) +} + +// Query returns QueryResolver implementation. +func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } + +type queryResolver struct{ *Resolver } diff --git a/backend/internal/framework/db/postgres/migration/000001_migration.down.sql b/backend/internal/framework/db/postgres/migration/000001_migration.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/framework/db/postgres/migration/000001_migration.up.sql b/backend/internal/framework/db/postgres/migration/000001_migration.up.sql new file mode 100644 index 0000000..10be3cc --- /dev/null +++ b/backend/internal/framework/db/postgres/migration/000001_migration.up.sql @@ -0,0 +1,31 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS "post" ( + "id" SERIAL PRIMARY KEY, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "description" TEXT NOT NULL, + "preview_image_url" TEXT NOT NULL, + "published_time" TIMESTAMP, + "created_time" TIMESTAMP NOT NULL, + "updated_time" TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS "label" ( + "id" SERIAL PRIMARY KEY, + "name" TEXT NOT NULL, + "color" TEXT NOT NULL, + "created_time" TIMESTAMP NOT NULL, + "updated_time" TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS "post_label" ( + "post_id" SERIAL NOT NULL, + "label_id" SERIAL NOT NULL, + "label_order" INTEGER NOT NULL, + PRIMARY KEY ("post_id", "label_id"), + FOREIGN KEY ("post_id") REFERENCES "post" ("id") ON DELETE CASCADE, + FOREIGN KEY ("label_id") REFERENCES "label" ("id") ON DELETE CASCADE +); + +COMMIT; \ No newline at end of file diff --git a/backend/internal/framework/db/postgres/post_db_service_impl.go b/backend/internal/framework/db/postgres/post_db_service_impl.go new file mode 100644 index 0000000..2a69e58 --- /dev/null +++ b/backend/internal/framework/db/postgres/post_db_service_impl.go @@ -0,0 +1,79 @@ +package postgres + +import ( + "context" + "database/sql" + "encoding/json" + + "git.squidspirit.com/squid/blog.git/backend/internal/adapter/gateway" + "git.squidspirit.com/squid/blog.git/backend/internal/adapter/gateway/dbdto" +) + +type postDBService struct { + db *sql.DB +} + +func NewPostDBService(db *sql.DB) gateway.PostDBService { + return &postDBService{db} +} + +func (srv *postDBService) QueryAll() ([]*dbdto.Post, error) { + ctx := context.Background() + + rows, err := srv.db.QueryContext(ctx, ` + SELECT + p.*, + COALESCE( + jsonb_agg( + jsonb_build_object( + 'id', l.id, + 'name', l.name, + 'color', l.color + ) ORDER BY pl.label_order ASC + ) + FILTER (WHERE l.id IS NOT NULL), + '[]'::jsonb + ) AS labels + FROM post p + LEFT JOIN post_label pl ON p.id = pl.post_id + LEFT JOIN label l ON pl.label_id = l.id + GROUP BY p.id + ORDER BY p.created_time DESC + `) + + if err != nil { + return nil, err + } + defer rows.Close() + + var labelsJSON []byte + + var postDtos []*dbdto.Post + for rows.Next() { + var postDto dbdto.Post + err = rows.Scan( + &postDto.ID, + &postDto.Title, + &postDto.Content, + &postDto.Description, + &postDto.PreviewImageURL, + &postDto.PublishedTime, + &postDto.CreatedTime, + &postDto.UpdatedTime, + &labelsJSON, + ) + + if err != nil { + return nil, err + } + + err := json.Unmarshal(labelsJSON, &postDto.Labels) + if err != nil { + return nil, err + } + + postDtos = append(postDtos, &postDto) + } + + return postDtos, nil +} diff --git a/backend/internal/framework/db/postgres/postgres.go b/backend/internal/framework/db/postgres/postgres.go new file mode 100644 index 0000000..d6b2503 --- /dev/null +++ b/backend/internal/framework/db/postgres/postgres.go @@ -0,0 +1,21 @@ +package postgres + +import ( + "database/sql" + + _ "github.com/lib/pq" +) + +func Connect(dsn string) (*sql.DB, error) { + db, err := sql.Open("postgres", dsn) + if err != nil { + return nil, err + } + + err = db.Ping() + if err != nil { + return nil, err + } + + return db, nil +} diff --git a/backend/internal/framework/graph/schema.resolvers.go b/backend/internal/framework/graph/schema.resolvers.go deleted file mode 100644 index 06b5c20..0000000 --- a/backend/internal/framework/graph/schema.resolvers.go +++ /dev/null @@ -1,27 +0,0 @@ -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.66 - -import ( - "context" - "fmt" - - model1 "git.squidspirit.com/squid/blog.git/backend/internal/framework/graph/model" -) - -// Posts is the resolver for the posts field. -func (r *queryResolver) Posts(ctx context.Context) ([]*model1.Post, error) { - panic(fmt.Errorf("not implemented: Posts - posts")) -} - -// Post is the resolver for the post field. -func (r *queryResolver) Post(ctx context.Context, id string) (*model1.Post, error) { - panic(fmt.Errorf("not implemented: Post - post")) -} - -// Query returns QueryResolver implementation. -func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } - -type queryResolver struct{ *Resolver } diff --git a/backend/internal/pkg/env/env.go b/backend/internal/pkg/env/env.go new file mode 100644 index 0000000..6caed39 --- /dev/null +++ b/backend/internal/pkg/env/env.go @@ -0,0 +1,21 @@ +package env + +import ( + "os" +) + +func GetPort() string { + port, exists := os.LookupEnv("PORT") + if !exists { + return "8000" + } + return port +} + +func GetDSN() string { + dsn, exists := os.LookupEnv("DSN") + if !exists { + return "postgres://postgres@127.0.0.1/postgres?sslmode=disable" + } + return dsn +}