Compare commits

..

4 Commits

Author SHA1 Message Date
2eae8c2667 BLOG-43 docs: environment setup
All checks were successful
Frontend CI / build (push) Successful in 1m29s
2025-03-28 00:36:20 +08:00
d7c6c97051 BLOG-43 refactor: rename gateway -> repository
All checks were successful
Frontend CI / build (push) Successful in 1m29s
2025-03-28 00:15:39 +08:00
a4394eea9e BLOG-43 feat: get all posts
All checks were successful
Frontend CI / build (push) Successful in 1m56s
2025-03-28 00:02:34 +08:00
a9081734b3 BLOG-43 init: go gqlgen setup
All checks were successful
Frontend CI / build (push) Successful in 1m30s
2025-03-13 23:42:36 +08:00
27 changed files with 5266 additions and 3 deletions

View File

@ -2,15 +2,46 @@
## Development
- Frontend: Next.js
- Backend: Go (gin)
- Frontend: Typescript (Next.js)
- Backend: Go (gqlgen)
- Database: PostgreSQL
Despite Next.js being a full-stack framework, I still decided to adopt a separate front-end and back-end architecture for this blog project. I believe that this separation makes the project cleaner, reduces coupling, and aligns with modern development practices. Furthermore, I wanted to practice developing a purely back-end API.
Despite Next.js being a full-stack framework, I still decided to adopt a separate front-end and back-end architecture for this blog project. I believe that this separation makes the project cleaner, reduces coupling, and aligns with modern development practices. Furthermore, I wanted to practice developing a purely back-end API.
As for the more detailed development approach, I plan to use Clean Architecture for the overall structure and ATDD for testing. Of course, such a small project may not necessarily require such complex design patterns, but I want to give myself an opportunity to practice them.
These will allow me to become more proficient in these modern development practices and leave a lot of flexibility and room for adjustments in the future.
## Environment
### Database Instance
Using docker to run a database instance.
```bash
podman run -d -p 127.0.0.1:5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust --rm --name postgres --replace postgres:alpine
```
For more infomation, please refer to [PostgreSQL Docker Hub](https://hub.docker.com/_/postgres).
### Database Migration
Using golang-migrate to manage database migration.
- Create a new migration file
```bash
migrate create -ext sql -dir backend/internal/framework/db/postgres/migration -seq migration
```
- Run migration
```bash
migrate -path db/migrations -database "postgresql://postgres@localhost:5432/postgres?sslmode=disable" up
```
For more information, please refer to [golang-migrate GitHub](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate).
## License
This project uses a combination of the [MIT License and a custom license](./LICENSE.md). Based on the MIT License, anyone is permitted to use the code. However, before deploying the code, they must first replace any information belonging to "me" or any content that could identify "me," such as logos, names, and "about me" sections.

View File

@ -0,0 +1,61 @@
package main
import (
"database/sql"
"log"
"net/http"
"git.squidspirit.com/squid/blog.git/backend/internal/adapter/repository"
"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"
)
func main() {
db, err := postgres.Connect(env.GetDSN())
if err != nil {
log.Fatal(err)
}
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{})
srv.AddTransport(transport.POST{})
srv.SetQueryCache(lru.New[*ast.QueryDocument](1000))
srv.Use(extension.Introspection{})
srv.Use(extension.AutomaticPersistedQuery{
Cache: lru.New[string](100),
})
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
http.Handle("/query", srv)
return srv
}
func createResolver(db *sql.DB) *graph.Resolver {
postRepo := repository.NewPostRepo(postgres.NewPostDBService(db))
return &graph.Resolver{
GetAllPostsUseCase: application.NewGetAllPostsUseCase(postRepo),
}
}

32
backend/go.mod Normal file
View File

@ -0,0 +1,32 @@
module git.squidspirit.com/squid/blog.git/backend
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
)
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.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
)

79
backend/go.sum Normal file
View File

@ -0,0 +1,79 @@
github.com/99designs/gqlgen v0.17.66 h1:2/SRc+h3115fCOZeTtsqrB5R5gTGm+8qCAwcrZa+CXA=
github.com/99designs/gqlgen v0.17.66/go.mod h1:gucrb5jK5pgCKzAGuOMMVU9C8PnReecHEHd2UxLQwCg=
github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0=
github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U=
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
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.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.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.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=

152
backend/gqlgen.yml Normal file
View File

@ -0,0 +1,152 @@
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema:
- internal/framework/api/graph/*.graphqls
# Where should the generated server code go?
exec:
package: graph
layout: single-file # Only other option is "follow-schema," ie multi-file.
# Only for single-file layout:
filename: internal/framework/api/graph/generated.go
# Only for follow-schema layout:
# dir: graph
# filename_template: "{name}.generated.go"
# Optional: Maximum number of goroutines in concurrency to use per child resolvers(default: unlimited)
# worker_limit: 1000
# Uncomment to enable federation
# federation:
# filename: graph/federation.go
# package: graph
# version: 2
# options:
# computed_requires: true
# Where should any generated models go?
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]
# Where should the resolver implementations go?
resolver:
package: graph
layout: follow-schema # Only other option is "single-file."
# Only for single-file layout:
# filename: graph/resolver.go
# Only for follow-schema layout:
dir: internal/framework/api/graph
filename_template: "{name}.resolvers.go"
# Optional: turn on to not generate template comments above resolvers
# omit_template_comment: false
# Optional: Pass in a path to a new gotpl template to use for generating resolvers
# resolver_template: [your/path/resolver.gotpl]
# Optional: turn on to avoid rewriting existing resolver(s) when generating
# preserve_resolver: 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 on to exclude root models such as Query and Mutation from the generated models file.
# omit_root_models: false
# Optional: turn on to exclude resolver fields from the generated models file.
# omit_resolver_fields: 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
# Optional: if this is set to true, argument directives that
# decorate a field with a null value will still be called.
#
# This enables argumment directives to not just mutate
# argument values but to set them even if they're null.
call_argument_directives_with_null: true
# Optional: set build tags that will be used to load packages
# go_build_tags:
# - private
# - enterprise
# Optional: set to modify the initialisms regarded for Go names
# go_initialisms:
# replace_defaults: false # if true, the default initialisms will get dropped in favor of the new ones instead of being added
# initialisms: # List of initialisms to for Go names
# - 'CC'
# - 'BCC'
# 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.squidspirit.com/squid/blog.git/backend/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.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:
model:
- github.com/99designs/gqlgen/graphql.UUID
# The GraphQL spec explicitly states that the Int type is a signed 32-bit
# integer. Using Go int or int64 to represent it can lead to unexpected
# behavior, and some GraphQL tools like Apollo Router will fail when
# communicating numbers that overflow 32-bits.
#
# You may choose to use the custom, built-in Int64 scalar to represent 64-bit
# integers, or ignore the spec and bind Int to graphql.Int / graphql.Int64
# (the default behavior of gqlgen). This is fine in simple use cases when you
# do not need to worry about interoperability and only expect small numbers.
Int:
model:
- github.com/99designs/gqlgen/graphql.Int32
Int64:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64

View File

@ -0,0 +1,22 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package graphdto
type Label struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
}
type Post struct {
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 {
}

View File

@ -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,
}
}

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -0,0 +1,34 @@
package repository
import (
"git.squidspirit.com/squid/blog.git/backend/internal/adapter/repository/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")
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -0,0 +1,7 @@
package domain
type Label struct {
ID uint32
Name string
Color string
}

View File

@ -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
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +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 {
GetAllPostsUseCase application.GetAllPostsUseCase
}

View File

@ -0,0 +1,23 @@
scalar Date
type Label {
id: ID!
name: String!
color: String!
}
type Post {
id: ID!
title: String!
content: String!
description: String!
previewImageUrl: String!
labels: [Label!]!
publishedTime: Date
}
type Query {
posts: [Post!]!
post(id: ID!): Post!
label(id: ID!): Label!
}

View File

@ -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 }

View File

@ -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;

View File

@ -0,0 +1,79 @@
package postgres
import (
"context"
"database/sql"
"encoding/json"
"git.squidspirit.com/squid/blog.git/backend/internal/adapter/repository"
"git.squidspirit.com/squid/blog.git/backend/internal/adapter/repository/dbdto"
)
type postDBService struct {
db *sql.DB
}
func NewPostDBService(db *sql.DB) repository.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
}

View File

@ -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
}

21
backend/internal/pkg/env/env.go vendored Normal file
View File

@ -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
}

8
backend/tools.go Normal file
View File

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