Skip to content

Commit a558a18

Browse files
committed
feat: add PostgreSQL, create follow relationships
1 parent 1a70f5f commit a558a18

File tree

11 files changed

+348
-28
lines changed

11 files changed

+348
-28
lines changed

cmd/social-backend/main.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ var args struct {
1313
ArangoUsername string `arg:"--arango-username,env:ARANGO_USERNAME"`
1414
ArangoPassword string `arg:"--arango-password,env:ARANGO_PASSWORD"`
1515
ArangoDatabase string `arg:"--arango-database,env:ARANGO_DATABASE"`
16-
ListenAddr string `arg:"--listen-addr,env:LISTEN_ADDR"`
16+
PostgresDSN string `arg:"--postgres-dsn,env:POSTGRESQL_DSN"`
17+
18+
ListenAddr string `arg:"--listen-addr,env:LISTEN_ADDR"`
1719
}
1820

1921
var logger = logrus.New()
@@ -32,7 +34,8 @@ func main() {
3234
Password: args.ArangoPassword,
3335
Database: args.ArangoDatabase,
3436
},
35-
Logger: logger,
37+
PostgresDSN: args.PostgresDSN,
38+
Logger: logger,
3639
})
3740

3841
if err != nil {

docker-compose.yaml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,16 @@ services:
88
- 8529:8529
99
volumes:
1010
- arango-data:/var/lib/arangodb3
11+
postgres:
12+
image: 'postgres:15-alpine'
13+
environment:
14+
POSTGRES_USER: postgres
15+
POSTGRES_PASSWORD: postgres
16+
POSTGRES_DB: social
17+
ports:
18+
- "5435:5432"
19+
volumes:
20+
- pgdata:/var/lib/postgresql/data
1121
volumes:
12-
arango-data:
22+
arango-data:
23+
pgdata:

docs/architecture.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Architecture
2+
3+
## Database
4+
5+
This project uses _two_ database engines. The reason of this choice is that we want
6+
the graph traversal speed of ArangoDB, but the schema enforcement of PostgreSQL (and all the other goodies of
7+
this DBMS).
8+
9+
### ArangoDB
10+
11+
ArangoDB is used to stored relationships that are likely to have a graph depth >= 2.
12+
Such relationships are for example:
13+
14+
- Follows / Followers relationships
15+
16+
### PostgreSQL
17+
18+
PostgreSQL contains everything else that doesn't need to be interacted with a graph traversal.
19+
For example, the followers relationship of a user will be stored in ArangoDB, whereas the user display name will
20+
be stored in PostgreSQL.

go.mod

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ require (
1717
github.com/go-playground/universal-translator v0.18.0 // indirect
1818
github.com/go-playground/validator/v10 v10.11.1 // indirect
1919
github.com/goccy/go-json v0.9.11 // indirect
20+
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
21+
github.com/jackc/pgconn v1.13.0 // indirect
22+
github.com/jackc/pgio v1.0.0 // indirect
23+
github.com/jackc/pgpassfile v1.0.0 // indirect
24+
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
25+
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
26+
github.com/jackc/pgtype v1.12.0 // indirect
27+
github.com/jackc/pgx/v4 v4.17.2 // indirect
28+
github.com/jinzhu/inflection v1.0.0 // indirect
29+
github.com/jinzhu/now v1.1.5 // indirect
2030
github.com/json-iterator/go v1.1.12 // indirect
2131
github.com/leodido/go-urn v1.2.1 // indirect
2232
github.com/mattn/go-isatty v0.0.16 // indirect
@@ -31,4 +41,6 @@ require (
3141
golang.org/x/text v0.4.0 // indirect
3242
google.golang.org/protobuf v1.28.1 // indirect
3343
gopkg.in/yaml.v2 v2.4.0 // indirect
44+
gorm.io/driver/postgres v1.4.5 // indirect
45+
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755 // indirect
3446
)

go.sum

Lines changed: 144 additions & 0 deletions
Large diffs are not rendered by default.

pkg/api_v1.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,24 @@ package server
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"github.com/arangodb/go-driver"
7-
"github.com/denysvitali/social/backend/pkg/models"
8+
"github.com/denysvitali/social/backend/pkg/models/arango"
9+
pg_model "github.com/denysvitali/social/backend/pkg/models/postgres"
810
v1requests "github.com/denysvitali/social/backend/pkg/requests/v1"
911
"github.com/gin-gonic/gin"
12+
"gorm.io/gorm"
1013
"net/http"
1114
)
1215

1316
func (s *Server) initAPIv1(g *gin.RouterGroup) {
14-
g.GET("/users/by_username/:username", s.apiV1UserByUsername)
15-
g.GET("/users/:id", s.apiV1Users)
1617
g.POST("/users", s.apiV1CreateUser)
18+
g.GET("/users/:id", s.apiV1Users)
19+
g.GET("/users/by-username/:username", s.apiV1UserByUsername)
1720
g.POST("/users/:id/follows/:target_id", s.apiV1SetUserFollows)
21+
22+
g.GET("/posts/:id", s.apiV1PostById)
1823
}
1924

2025
func (s *Server) apiV1Users(c *gin.Context) {
@@ -56,6 +61,24 @@ func (s *Server) apiV1UserByUsername(c *gin.Context) {
5661
)
5762
return
5863
}
64+
65+
var user pg_model.User
66+
tx := s.pgDB.First(&user, "username = ?", usernameKey)
67+
if tx.Error != nil {
68+
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
69+
s.notFound(c, "user not found")
70+
return
71+
}
72+
s.internalServerError(c, "unable to get user by username: %v", tx.Error)
73+
return
74+
}
75+
76+
if user.Deleted {
77+
s.notFound(c, "user doesn't exist anymore")
78+
return
79+
}
80+
81+
c.JSON(http.StatusOK, user)
5982
}
6083

6184
func (s *Server) apiV1SetUserFollows(c *gin.Context) {
@@ -164,7 +187,7 @@ func (s *Server) apiV1CreateUser(c *gin.Context) {
164187
return
165188
}
166189

167-
meta, err := coll.CreateDocument(ctx, models.User{
190+
meta, err := coll.CreateDocument(ctx, arango.User{
168191
Username: req.Username,
169192
FirstName: req.FirstName,
170193
LastName: req.LastName,

pkg/api_v1_posts.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package server
2+
3+
import (
4+
"errors"
5+
pgmodel "github.com/denysvitali/social/backend/pkg/models/postgres"
6+
"github.com/gin-gonic/gin"
7+
"gorm.io/gorm"
8+
"net/http"
9+
)
10+
11+
func (s *Server) apiV1PostById(c *gin.Context) {
12+
id := c.Param("id")
13+
if id == "" {
14+
s.badRequest(c, "id is empty", "id cannot be empty")
15+
return
16+
}
17+
18+
var p pgmodel.Post
19+
tx := s.pgDB.Preload("Author").First(&p, "id = ?", id)
20+
if tx.Error != nil {
21+
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
22+
s.notFound(c, "post not found")
23+
return
24+
}
25+
s.internalServerError(c, "unable to get post with id %s: %v", id, tx.Error)
26+
return
27+
}
28+
29+
c.JSON(http.StatusOK, p)
30+
}

pkg/models/user.go renamed to pkg/models/arango/user.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package models
1+
package arango
22

33
type User struct {
44
Username string `json:"username"`

pkg/models/postgres/post.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package pg_model
2+
3+
import "time"
4+
5+
type Post struct {
6+
ID uint64 `gorm:"primaryKey" json:"id"`
7+
CreatedAt time.Time `json:"createdAt"`
8+
Content string `json:"content"`
9+
10+
ParentPost *Post `json:"parentPost,omitempty"`
11+
ParentPostID uint64 `json:"parentPostID,omitempty"`
12+
13+
ReshareCount uint32 `json:"reshareCount"`
14+
ReplyCount uint32 `json:"replyCount"`
15+
16+
Author *User `json:"author,omitempty"`
17+
AuthorID uint64 `json:"authorId"`
18+
Deleted bool `json:"-"`
19+
}

pkg/models/postgres/user.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package pg_model
2+
3+
import "time"
4+
5+
type User struct {
6+
ID uint64 `gorm:"primaryKey" json:"id"`
7+
CreatedAt time.Time `json:"createdAt"`
8+
Username string `gorm:"index" json:"username"`
9+
DisplayName string `json:"displayName"`
10+
Biography string `json:"biography"`
11+
Location string `json:"location"`
12+
FollowersCount int `json:"followersCount"`
13+
FollowingCount int `json:"followingCount"`
14+
Verified bool `json:"verified"`
15+
Deleted bool `json:"-"`
16+
17+
// HasMany relations
18+
Posts []Post `gorm:"foreignKey:AuthorID" json:"posts,omitempty"`
19+
}

pkg/server.go

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import (
55
"fmt"
66
"github.com/arangodb/go-driver"
77
arangohttp "github.com/arangodb/go-driver/http"
8+
pg_model "github.com/denysvitali/social/backend/pkg/models/postgres"
89
"github.com/gin-gonic/gin"
910
"github.com/sirupsen/logrus"
11+
"gorm.io/driver/postgres"
12+
"gorm.io/gorm"
1013
)
1114

1215
type Server struct {
@@ -15,6 +18,7 @@ type Server struct {
1518

1619
graphConn driver.Client
1720
graphDb driver.Database
21+
pgDB *gorm.DB
1822
}
1923

2024
type ArangoConfig struct {
@@ -26,18 +30,53 @@ type ArangoConfig struct {
2630
}
2731

2832
type Config struct {
29-
Arango ArangoConfig
33+
Arango ArangoConfig
34+
PostgresDSN string
35+
3036
Logger *logrus.Logger
3137
}
3238

3339
func New(config Config) (*Server, error) {
40+
if config.Logger == nil {
41+
config.Logger = logrus.New()
42+
config.Logger.Warnf("nil logger passed in config, creating a new logger")
43+
}
44+
45+
c, db, err := setupArango(config)
46+
if err != nil {
47+
return nil, fmt.Errorf("unable to set-up ArangoDB")
48+
}
49+
50+
pgdb, err := setupPostgres(config)
51+
if err != nil {
52+
return nil, fmt.Errorf("unable to set-up PostgreSQL")
53+
}
54+
55+
s := Server{
56+
e: gin.New(),
57+
graphConn: c,
58+
graphDb: db,
59+
pgDB: pgdb,
60+
logger: config.Logger,
61+
}
62+
63+
s.init()
64+
65+
return &s, nil
66+
}
67+
68+
func setupPostgres(config Config) (*gorm.DB, error) {
69+
return gorm.Open(postgres.Open(config.PostgresDSN), &gorm.Config{})
70+
}
71+
72+
func setupArango(config Config) (driver.Client, driver.Database, error) {
3473
conn, err := arangohttp.NewConnection(
3574
arangohttp.ConnectionConfig{
3675
Endpoints: config.Arango.Endpoints,
3776
},
3877
)
3978
if err != nil {
40-
return nil, fmt.Errorf("unable to create arango HTTP connection: %v", err)
79+
return nil, nil, fmt.Errorf("unable to create arango HTTP connection: %v", err)
4180
}
4281

4382
c, err := driver.NewClient(driver.ClientConfig{
@@ -49,36 +88,36 @@ func New(config Config) (*Server, error) {
4988
SynchronizeEndpointsInterval: 0,
5089
})
5190
if err != nil {
52-
return nil, fmt.Errorf("unable to create ArangoDB client: %v", err)
53-
}
54-
55-
if config.Logger == nil {
56-
config.Logger = logrus.New()
57-
config.Logger.Warnf("nil logger passed in config, creating a new logger")
91+
return nil, nil, fmt.Errorf("unable to create ArangoDB client: %v", err)
5892
}
5993

6094
db, err := c.Database(context.TODO(), config.Arango.Database)
6195
if err != nil {
62-
return nil, fmt.Errorf("unable to get ArangoDB database: %v", err)
96+
return nil, nil, fmt.Errorf("unable to get ArangoDB database: %v", err)
6397
}
64-
65-
s := Server{
66-
e: gin.New(),
67-
graphConn: c,
68-
graphDb: db,
69-
logger: config.Logger,
70-
}
71-
72-
s.init()
73-
74-
return &s, nil
98+
return c, db, nil
7599
}
76100

77101
func (s *Server) Listen(addr ...string) error {
78102
return s.e.Run(addr...)
79103
}
80104

81105
func (s *Server) init() {
106+
// init db
107+
s.initPostgreSQL()
108+
82109
g := s.e.Group("/api/v1")
83110
s.initAPIv1(g)
84111
}
112+
113+
func (s *Server) initPostgreSQL() {
114+
for _, v := range []any{
115+
&pg_model.User{},
116+
&pg_model.Post{},
117+
} {
118+
err := s.pgDB.AutoMigrate(v)
119+
if err != nil {
120+
s.logger.Errorf("unable to automigrate %t: %v", v, err)
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)