├── bootstrap └── bootstrap.go ├── configurations ├── app.yaml └── database.yaml ├── database.sql ├── domain ├── entities │ ├── note.go │ └── paginator.go ├── interfaces │ ├── note_cache.go │ └── note_repository.go └── usecases │ └── usecases.go ├── externals ├── adapters │ └── db.go ├── cache │ └── notes_cache.go └── repositories │ └── notes_repository.go ├── go.mod ├── go.sum ├── http ├── controllers │ └── notes_controller.go ├── errors │ └── handler.go ├── router │ └── router.go ├── server │ └── server.go ├── transport │ ├── request │ │ ├── decoders │ │ │ ├── note.go │ │ │ ├── paginator.go │ │ │ └── update_note.go │ │ ├── interface.go │ │ └── request.go │ └── response │ │ ├── encode.go │ │ └── response.go └── validators │ └── validators.go ├── main.go ├── notes_app.postman_collection.json ├── readme.md └── utils ├── config ├── app.go ├── config.go ├── db.go └── parser.go └── container ├── container.go └── resolver.go /bootstrap/bootstrap.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "notes/http/router" 7 | "notes/http/server" 8 | "notes/utils/config" 9 | "notes/utils/container" 10 | "os" 11 | "os/signal" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | func Start(ctx context.Context) { 17 | conf, err := config.Parse() 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | ctr, err := container.Resolve(conf) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | r := router.Init(ctr) 28 | 29 | server := server.NewHTTPServer(conf, r) 30 | 31 | go server.ListnAndServe(ctx) 32 | 33 | c := make(chan os.Signal, 1) 34 | 35 | signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT) 36 | 37 | <-c 38 | 39 | Destruct(ctx, ctr, server) 40 | 41 | os.Exit(0) 42 | } 43 | 44 | func Destruct(ctx context.Context, ctr container.Containers, server server.HTTPServer) { 45 | 46 | ctx, cancel := context.WithTimeout(ctx, time.Second*5) 47 | defer cancel() 48 | 49 | go server.Shutdown(ctx) 50 | 51 | <-ctx.Done() 52 | 53 | log.Println("service shutdown gracefully") 54 | } 55 | -------------------------------------------------------------------------------- /configurations/app.yaml: -------------------------------------------------------------------------------- 1 | service-port: 8080 2 | service-host: "localhost" -------------------------------------------------------------------------------- /configurations/database.yaml: -------------------------------------------------------------------------------- 1 | user: "root" 2 | password: "password" 3 | host: "localhost" 4 | port: 3306 5 | database: "notes" 6 | idle-connection: 10 7 | open-connection: 20 8 | connection-life-time: 600000 9 | connection-idle-time: 600000 10 | read-timeout: 60000 11 | write-timeout: 60000 12 | timeout: 60000 -------------------------------------------------------------------------------- /database.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE notes; 2 | 3 | DROP TABLE IF EXISTS `note`; 4 | CREATE TABLE `note` ( 5 | `id` bigint unsigned NOT NULL auto_increment primary key, 6 | `user_id` bigint unsigned NOT NULL, 7 | `title` varchar(120) NOT NULL, 8 | `description` varchar(255) NOT NULL, 9 | `archived` tinyint(1) NOT NULL DEFAULT "0", 10 | `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 12 | ); -------------------------------------------------------------------------------- /domain/entities/note.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import "time" 4 | 5 | type Note struct { 6 | ID int64 `json:"id"` 7 | UserID int64 `json:"user_id"` 8 | Title string `json:"title"` 9 | Description string `json:"description"` 10 | Archived bool `json:"-"` 11 | CreatedAt time.Time `json:"created_at"` 12 | UpdatedAt time.Time `json:"updated_at"` 13 | } 14 | -------------------------------------------------------------------------------- /domain/entities/paginator.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | type Paginator struct { 4 | Page int 5 | Size int 6 | } 7 | -------------------------------------------------------------------------------- /domain/interfaces/note_cache.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "notes/domain/entities" 4 | 5 | type NoteCache interface { 6 | SetNote(note entities.Note) 7 | GetNote(id int64) (entities.Note, bool) 8 | DeleteNote(id int64) 9 | ArchiveNote(id int64) 10 | UnArchiveNote(id int64) 11 | IsArchived(id int64) bool 12 | Refresh() 13 | } 14 | -------------------------------------------------------------------------------- /domain/interfaces/note_repository.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "context" 5 | "notes/domain/entities" 6 | ) 7 | 8 | type NoteRepository interface { 9 | GetAllUnarchivedNotes(ctx context.Context, paginator entities.Paginator) ([]entities.Note, error) 10 | GetAllArchviedNotes(ctx context.Context, paginator entities.Paginator) ([]entities.Note, error) 11 | GetSingle(ctx context.Context, id int64) (entities.Note, error) 12 | CreateNote(ctx context.Context, note entities.Note) (int64, error) 13 | ArchiveNote(ctx context.Context, id int64) (bool, error) 14 | UnArchiveNote(ctx context.Context, id int64) (bool, error) 15 | UpdateNote(ctx context.Context, id int64, newNote entities.Note) (bool, error) 16 | DeleteNote(ctx context.Context, id int64) (bool, error) 17 | IsExists(ctx context.Context, id int64) (bool, error) 18 | IsArchived(ctx context.Context, id int64) (bool, error) 19 | RefreshCache() 20 | } 21 | -------------------------------------------------------------------------------- /domain/usecases/usecases.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "notes/domain/entities" 8 | "notes/domain/interfaces" 9 | ) 10 | 11 | type NoteUsecase struct { 12 | repo interfaces.NoteRepository 13 | } 14 | 15 | func NewNoteUsecase(repo interfaces.NoteRepository) NoteUsecase { 16 | usecase := NoteUsecase{ 17 | repo: repo, 18 | } 19 | 20 | return usecase 21 | } 22 | 23 | func (usecase NoteUsecase) GetAllUnarchivedNotes(ctx context.Context, paginator entities.Paginator) ([]entities.Note, error) { 24 | 25 | return usecase.repo.GetAllUnarchivedNotes(ctx, paginator) 26 | } 27 | 28 | func (usecase NoteUsecase) GetAllArchviedNotes(ctx context.Context, paginator entities.Paginator) ([]entities.Note, error) { 29 | 30 | return usecase.repo.GetAllArchviedNotes(ctx, paginator) 31 | } 32 | 33 | func (usecase NoteUsecase) CreateNote(ctx context.Context, note entities.Note) (int64, error) { 34 | 35 | // validate max length 36 | if len(note.Title) > 120 { 37 | return 0, errors.New("title lenght is too long") 38 | } 39 | 40 | if len(note.Description) > 255 { 41 | return 0, errors.New("description lenght is too long") 42 | } 43 | 44 | // save the note 45 | id, err := usecase.repo.CreateNote(ctx, note) 46 | if err != nil { 47 | return 0, err 48 | } 49 | 50 | return id, nil 51 | } 52 | 53 | func (usecase NoteUsecase) ArchiveNote(ctx context.Context, id int64) (bool, error) { 54 | 55 | // check for existence 56 | isExists, err := usecase.repo.IsExists(ctx, id) 57 | if err != nil { 58 | return false, err 59 | } 60 | 61 | if !isExists { 62 | return false, fmt.Errorf("%d note is not exists", id) 63 | } 64 | 65 | // check whether note is already archived 66 | isArchived, err := usecase.repo.IsArchived(ctx, id) 67 | if err != nil { 68 | return false, err 69 | } 70 | 71 | if isArchived { 72 | return false, fmt.Errorf("%d note is already archived", id) 73 | } 74 | 75 | // archive the note 76 | archived, err := usecase.repo.ArchiveNote(ctx, id) 77 | if err != nil { 78 | return false, err 79 | } 80 | 81 | return archived, nil 82 | } 83 | 84 | func (usecase NoteUsecase) UnArchiveNote(ctx context.Context, id int64) (bool, error) { 85 | 86 | // check for existence 87 | isExists, err := usecase.repo.IsExists(ctx, id) 88 | if err != nil { 89 | return false, err 90 | } 91 | 92 | if !isExists { 93 | return false, fmt.Errorf("%d note is not exists", id) 94 | } 95 | 96 | // check whether note is already archived 97 | isArchived, err := usecase.repo.IsArchived(ctx, id) 98 | if err != nil { 99 | return false, err 100 | } 101 | 102 | if !isArchived { 103 | return false, fmt.Errorf("%d note is already un archived", id) 104 | } 105 | 106 | // archive the note 107 | unArchived, err := usecase.repo.UnArchiveNote(ctx, id) 108 | if err != nil { 109 | return false, err 110 | } 111 | 112 | return unArchived, nil 113 | } 114 | 115 | func (usecase NoteUsecase) UpdateNote(ctx context.Context, id int64, newNote entities.Note) (bool, error) { 116 | 117 | // validate max length 118 | if len(newNote.Title) > 120 { 119 | return false, errors.New("title lenght is too long") 120 | } 121 | 122 | if len(newNote.Description) > 255 { 123 | return false, errors.New("description lenght is too long") 124 | } 125 | 126 | // check for existence 127 | isExists, err := usecase.repo.IsExists(ctx, id) 128 | if err != nil { 129 | return false, err 130 | } 131 | 132 | if !isExists { 133 | return false, fmt.Errorf("%d note is not exists", id) 134 | } 135 | 136 | // get the old note 137 | oldNote, err := usecase.repo.GetSingle(ctx, id) 138 | if err != nil { 139 | return false, err 140 | } 141 | 142 | // update old values if new values note provided 143 | if newNote.Title == "" { 144 | newNote.Title = oldNote.Title 145 | } 146 | 147 | if newNote.Description == "" { 148 | newNote.Description = oldNote.Description 149 | } 150 | 151 | // update the note 152 | updated, err := usecase.repo.UpdateNote(ctx, id, newNote) 153 | if err != nil { 154 | return false, err 155 | } 156 | 157 | return updated, nil 158 | } 159 | 160 | func (usecase NoteUsecase) DeleteNote(ctx context.Context, id int64) (bool, error) { 161 | 162 | // check for existence 163 | isExists, err := usecase.repo.IsExists(ctx, id) 164 | if err != nil { 165 | return false, err 166 | } 167 | 168 | if !isExists { 169 | return false, fmt.Errorf("%d note is not exists", id) 170 | } 171 | 172 | // delete the note 173 | deleted, err := usecase.repo.DeleteNote(ctx, id) 174 | if err != nil { 175 | return false, err 176 | } 177 | 178 | return deleted, nil 179 | } 180 | -------------------------------------------------------------------------------- /externals/adapters/db.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "database/sql" 5 | "notes/utils/config" 6 | "strconv" 7 | "time" 8 | 9 | mysql "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | func NewDB(conf config.Database) (*sql.DB, error) { 13 | 14 | config := mysql.Config{ 15 | User: conf.User, 16 | Passwd: conf.Password, 17 | Net: "tcp", 18 | Addr: conf.Host + ":" + strconv.Itoa(conf.Port), 19 | DBName: conf.Database, 20 | AllowNativePasswords: true, 21 | ParseTime: true, 22 | Timeout: time.Duration(conf.Timeout) * time.Millisecond, 23 | ReadTimeout: time.Duration(conf.ReadTimeout) * time.Millisecond, 24 | WriteTimeout: time.Duration(conf.WriteTimeout) * time.Millisecond, 25 | } 26 | 27 | conn := config.FormatDSN() 28 | 29 | db, err := sql.Open("mysql", conn) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | if err = db.Ping(); err != nil { 35 | return nil, err 36 | } 37 | 38 | db.SetMaxOpenConns(conf.OpenConnection) 39 | db.SetMaxIdleConns(conf.IdleConnection) 40 | db.SetConnMaxLifetime(time.Duration(conf.ConnectionLifeTime) * time.Millisecond) 41 | db.SetConnMaxIdleTime(time.Duration(conf.ConnectionIdleTime) * time.Millisecond) 42 | 43 | return db, nil 44 | } 45 | -------------------------------------------------------------------------------- /externals/cache/notes_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "notes/domain/entities" 5 | "notes/domain/interfaces" 6 | ) 7 | 8 | type NoteCache struct { 9 | noteIndex map[int64]entities.Note 10 | archiveIndex map[int64]struct{} 11 | } 12 | 13 | func NewNoteCache() interfaces.NoteCache { 14 | 15 | cache := &NoteCache{ 16 | noteIndex: make(map[int64]entities.Note), 17 | archiveIndex: make(map[int64]struct{}), 18 | } 19 | 20 | return cache 21 | } 22 | 23 | func (cache *NoteCache) SetNote(note entities.Note) { 24 | cache.noteIndex[note.ID] = note 25 | } 26 | 27 | func (cache NoteCache) GetNote(id int64) (entities.Note, bool) { 28 | note, ok := cache.noteIndex[id] 29 | if !ok { 30 | return entities.Note{}, false 31 | } 32 | 33 | return note, true 34 | } 35 | 36 | func (cache *NoteCache) DeleteNote(id int64) { 37 | delete(cache.noteIndex, id) 38 | } 39 | 40 | func (cache *NoteCache) ArchiveNote(id int64) { 41 | cache.archiveIndex[id] = struct{}{} 42 | } 43 | 44 | func (cache *NoteCache) UnArchiveNote(id int64) { 45 | delete(cache.archiveIndex, id) 46 | } 47 | 48 | func (cache NoteCache) IsArchived(id int64) bool { 49 | _, ok := cache.archiveIndex[id] 50 | 51 | return ok 52 | } 53 | 54 | func (cache *NoteCache) Refresh() { 55 | cache.noteIndex = make(map[int64]entities.Note) 56 | cache.archiveIndex = make(map[int64]struct{}) 57 | } 58 | -------------------------------------------------------------------------------- /externals/repositories/notes_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "log" 7 | "notes/domain/entities" 8 | "notes/domain/interfaces" 9 | ) 10 | 11 | type NoteRepository struct { 12 | db *sql.DB 13 | cache interfaces.NoteCache 14 | } 15 | 16 | func NewNoteRepository(db *sql.DB, cache interfaces.NoteCache) interfaces.NoteRepository { 17 | repo := &NoteRepository{ 18 | db: db, 19 | cache: cache, 20 | } 21 | 22 | return repo 23 | } 24 | 25 | func (repo NoteRepository) GetAllUnarchivedNotes(ctx context.Context, paginator entities.Paginator) ([]entities.Note, error) { 26 | 27 | offset := (paginator.Page - 1) * paginator.Size 28 | 29 | query := ` 30 | SELECT id, user_id, title, description, created_at, updated_at 31 | FROM note WHERE archived = 0 ORDER BY id ASC 32 | LIMIT ? OFFSET ?; 33 | ` 34 | 35 | stmt, err := repo.db.PrepareContext(ctx, query) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | defer stmt.Close() 41 | 42 | rows, err := stmt.QueryContext(ctx, paginator.Size, offset) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | defer rows.Close() 48 | 49 | notes := make([]entities.Note, 0) 50 | 51 | for rows.Next() { 52 | note := entities.Note{} 53 | 54 | err := rows.Scan( 55 | ¬e.ID, 56 | ¬e.UserID, 57 | ¬e.Title, 58 | ¬e.Description, 59 | ¬e.CreatedAt, 60 | ¬e.UpdatedAt, 61 | ) 62 | 63 | if err != nil { 64 | log.Println(err) 65 | continue 66 | } 67 | 68 | notes = append(notes, note) 69 | } 70 | 71 | return notes, nil 72 | } 73 | 74 | func (repo NoteRepository) GetAllArchviedNotes(ctx context.Context, paginator entities.Paginator) ([]entities.Note, error) { 75 | 76 | offset := (paginator.Page - 1) * paginator.Size 77 | 78 | query := ` 79 | SELECT id, user_id, title, description, created_at, updated_at 80 | FROM note WHERE archived = 1 ORDER BY id ASC 81 | LIMIT ? OFFSET ?; 82 | ` 83 | 84 | stmt, err := repo.db.PrepareContext(ctx, query) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | defer stmt.Close() 90 | 91 | rows, err := stmt.QueryContext(ctx, paginator.Size, offset) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | defer rows.Close() 97 | 98 | notes := make([]entities.Note, 0) 99 | 100 | for rows.Next() { 101 | note := entities.Note{} 102 | 103 | err := rows.Scan( 104 | ¬e.ID, 105 | ¬e.UserID, 106 | ¬e.Title, 107 | ¬e.Description, 108 | ¬e.CreatedAt, 109 | ¬e.UpdatedAt, 110 | ) 111 | 112 | if err != nil { 113 | log.Println(err) 114 | continue 115 | } 116 | 117 | notes = append(notes, note) 118 | } 119 | 120 | return notes, nil 121 | } 122 | 123 | func (repo NoteRepository) GetSingle(ctx context.Context, id int64) (entities.Note, error) { 124 | 125 | note, ok := repo.cache.GetNote(id) 126 | if ok { 127 | return note, nil 128 | } 129 | 130 | note = entities.Note{} 131 | 132 | query := ` 133 | SELECT id, user_id, title, description, created_at, updated_at 134 | FROM note WHERE id = ?; 135 | ` 136 | 137 | stmt, err := repo.db.PrepareContext(ctx, query) 138 | if err != nil { 139 | return note, err 140 | } 141 | 142 | defer stmt.Close() 143 | 144 | row := stmt.QueryRowContext(ctx, id) 145 | 146 | err = row.Scan( 147 | ¬e.ID, 148 | ¬e.UserID, 149 | ¬e.Title, 150 | ¬e.Description, 151 | ¬e.CreatedAt, 152 | ¬e.UpdatedAt, 153 | ) 154 | 155 | if err == sql.ErrNoRows { 156 | return note, nil 157 | } 158 | 159 | if err != nil { 160 | return note, err 161 | } 162 | 163 | repo.cache.SetNote(note) 164 | 165 | return note, nil 166 | } 167 | 168 | func (repo NoteRepository) CreateNote(ctx context.Context, note entities.Note) (int64, error) { 169 | 170 | query := `INSERT INTO note (user_id, title, description) VALUES (?, ?, ?);` 171 | 172 | stmt, err := repo.db.PrepareContext(ctx, query) 173 | if err != nil { 174 | return 0, err 175 | } 176 | 177 | defer stmt.Close() 178 | 179 | result, err := stmt.ExecContext(ctx, note.UserID, note.Title, note.Description) 180 | if err != nil { 181 | return 0, err 182 | } 183 | 184 | id, err := result.LastInsertId() 185 | if err != nil { 186 | return 0, err 187 | } 188 | 189 | note.ID = id 190 | 191 | repo.cache.SetNote(note) 192 | 193 | return id, nil 194 | } 195 | 196 | func (repo NoteRepository) ArchiveNote(ctx context.Context, id int64) (bool, error) { 197 | 198 | query := `UPDATE note SET archived = 1 WHERE id = ?;` 199 | 200 | stmt, err := repo.db.PrepareContext(ctx, query) 201 | if err != nil { 202 | return false, err 203 | } 204 | 205 | defer stmt.Close() 206 | 207 | _, err = stmt.Exec(id) 208 | if err != nil { 209 | return false, err 210 | } 211 | 212 | repo.cache.ArchiveNote(id) 213 | 214 | return true, nil 215 | } 216 | 217 | func (repo NoteRepository) UnArchiveNote(ctx context.Context, id int64) (bool, error) { 218 | 219 | query := `UPDATE note SET archived = 0 WHERE id = ?;` 220 | 221 | stmt, err := repo.db.PrepareContext(ctx, query) 222 | if err != nil { 223 | return false, err 224 | } 225 | 226 | defer stmt.Close() 227 | 228 | _, err = stmt.Exec(id) 229 | if err != nil { 230 | return false, err 231 | } 232 | 233 | repo.cache.UnArchiveNote(id) 234 | 235 | return true, nil 236 | } 237 | 238 | func (repo NoteRepository) UpdateNote(ctx context.Context, id int64, newNote entities.Note) (bool, error) { 239 | 240 | query := `UPDATE note SET title = ?, description = ? WHERE id = ?;` 241 | 242 | stmt, err := repo.db.PrepareContext(ctx, query) 243 | if err != nil { 244 | return false, err 245 | } 246 | 247 | defer stmt.Close() 248 | 249 | _, err = stmt.Exec(newNote.Title, newNote.Description, id) 250 | if err != nil { 251 | return false, err 252 | } 253 | 254 | newNote.ID = id 255 | 256 | repo.cache.SetNote(newNote) 257 | 258 | return true, nil 259 | } 260 | 261 | func (repo NoteRepository) DeleteNote(ctx context.Context, id int64) (bool, error) { 262 | 263 | query := `DELETE FROM note WHERE id = ?;` 264 | 265 | stmt, err := repo.db.PrepareContext(ctx, query) 266 | 267 | if err != nil { 268 | return false, err 269 | } 270 | 271 | defer stmt.Close() 272 | 273 | _, err = stmt.Exec(id) 274 | if err != nil { 275 | return false, err 276 | } 277 | 278 | repo.cache.DeleteNote(id) 279 | 280 | return true, nil 281 | } 282 | 283 | func (repo NoteRepository) IsExists(ctx context.Context, id int64) (bool, error) { 284 | 285 | _, ok := repo.cache.GetNote(id) 286 | 287 | if ok { 288 | return true, nil 289 | } 290 | 291 | query := `SELECT EXISTS(SELECT 1 FROM note WHERE id = ?);` 292 | 293 | stmt, err := repo.db.PrepareContext(ctx, query) 294 | 295 | if err != nil { 296 | return false, err 297 | } 298 | 299 | defer stmt.Close() 300 | 301 | row := stmt.QueryRowContext(ctx, id) 302 | 303 | var exists bool 304 | 305 | err = row.Scan(&exists) 306 | 307 | if err == sql.ErrNoRows { 308 | return false, nil 309 | } 310 | 311 | if err != nil { 312 | return false, err 313 | } 314 | 315 | return exists, nil 316 | } 317 | 318 | func (repo NoteRepository) IsArchived(ctx context.Context, id int64) (bool, error) { 319 | 320 | archived := repo.cache.IsArchived(id) 321 | 322 | if archived { 323 | return true, nil 324 | } 325 | 326 | query := `SELECT EXISTS(SELECT 1 FROM note WHERE id = ? and archived = 1);` 327 | 328 | stmt, err := repo.db.PrepareContext(ctx, query) 329 | 330 | if err != nil { 331 | return false, err 332 | } 333 | 334 | defer stmt.Close() 335 | 336 | row := stmt.QueryRowContext(ctx, id) 337 | 338 | var exists bool 339 | 340 | err = row.Scan(&exists) 341 | 342 | if err == sql.ErrNoRows { 343 | return false, nil 344 | } 345 | 346 | if err != nil { 347 | return false, err 348 | } 349 | 350 | return exists, nil 351 | } 352 | 353 | func (repo NoteRepository) RefreshCache() { 354 | repo.cache.Refresh() 355 | } 356 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module notes 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-playground/universal-translator v0.18.0 7 | github.com/go-sql-driver/mysql v1.6.0 8 | github.com/gorilla/mux v1.8.0 9 | gopkg.in/go-playground/validator.v9 v9.31.0 10 | gopkg.in/yaml.v2 v2.4.0 11 | ) 12 | 13 | require ( 14 | github.com/go-playground/locales v0.14.0 // indirect 15 | github.com/leodido/go-urn v1.2.1 // indirect 16 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 17 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 4 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 5 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 6 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 7 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 8 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 9 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 10 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 11 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 12 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 17 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 19 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 23 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 24 | gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= 25 | gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 26 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 27 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 28 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 30 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | -------------------------------------------------------------------------------- /http/controllers/notes_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "notes/domain/usecases" 7 | "notes/http/errors" 8 | "notes/http/transport/request" 9 | "notes/http/transport/request/decoders" 10 | "notes/http/transport/response" 11 | "notes/http/validators" 12 | "notes/utils/container" 13 | "strconv" 14 | 15 | "github.com/gorilla/mux" 16 | ) 17 | 18 | type NoteController struct { 19 | usecase usecases.NoteUsecase 20 | validator validators.Validator 21 | } 22 | 23 | func NewNoteController(ctr container.Containers) NoteController { 24 | ctl := NoteController{ 25 | usecase: usecases.NewNoteUsecase(ctr.Repositories.Note), 26 | validator: validators.NewValidator(), 27 | } 28 | 29 | return ctl 30 | } 31 | 32 | func (ctl NoteController) GetAllUnarchivedNotes(w http.ResponseWriter, r *http.Request) { 33 | 34 | ctx := r.Context() 35 | 36 | param := r.FormValue("paginator") 37 | 38 | pageDecoder := decoders.Paginator{} 39 | 40 | err := json.Unmarshal([]byte(param), &pageDecoder) 41 | if err != nil { 42 | errors.HandleError(w, err, http.StatusBadRequest) 43 | return 44 | } 45 | 46 | err = ctl.validator.Validate(ctx, pageDecoder) 47 | if err != nil { 48 | errors.HandleError(w, err, http.StatusBadRequest) 49 | return 50 | } 51 | 52 | paginator, err := pageDecoder.Validate() 53 | if err != nil { 54 | errors.HandleError(w, err, http.StatusBadRequest) 55 | return 56 | } 57 | 58 | notes, err := ctl.usecase.GetAllUnarchivedNotes(ctx, paginator) 59 | if err != nil { 60 | errors.HandleError(w, err, http.StatusUnprocessableEntity) 61 | return 62 | } 63 | 64 | payload := response.Encode(notes, nil, "true") 65 | 66 | response.Send(w, payload, http.StatusOK) 67 | } 68 | 69 | func (ctl NoteController) GetAllArchviedNotes(w http.ResponseWriter, r *http.Request) { 70 | 71 | ctx := r.Context() 72 | 73 | param := r.FormValue("paginator") 74 | 75 | pageDecoder := decoders.Paginator{} 76 | 77 | err := json.Unmarshal([]byte(param), &pageDecoder) 78 | if err != nil { 79 | errors.HandleError(w, err, http.StatusBadRequest) 80 | return 81 | } 82 | 83 | err = ctl.validator.Validate(ctx, pageDecoder) 84 | if err != nil { 85 | errors.HandleError(w, err, http.StatusBadRequest) 86 | return 87 | } 88 | 89 | paginator, err := pageDecoder.Validate() 90 | if err != nil { 91 | errors.HandleError(w, err, http.StatusBadRequest) 92 | return 93 | } 94 | 95 | notes, err := ctl.usecase.GetAllArchviedNotes(ctx, paginator) 96 | if err != nil { 97 | errors.HandleError(w, err, http.StatusUnprocessableEntity) 98 | return 99 | } 100 | 101 | payload := response.Encode(notes, nil, "true") 102 | 103 | response.Send(w, payload, http.StatusOK) 104 | } 105 | 106 | func (ctl NoteController) CreateNote(w http.ResponseWriter, r *http.Request) { 107 | 108 | ctx := r.Context() 109 | 110 | noteDecoder := decoders.Note{} 111 | 112 | err := request.Decode(ctx, r, ¬eDecoder) 113 | if err != nil { 114 | errors.HandleError(w, err, http.StatusBadRequest) 115 | return 116 | } 117 | 118 | err = ctl.validator.Validate(ctx, noteDecoder) 119 | if err != nil { 120 | errors.HandleError(w, err, http.StatusBadRequest) 121 | return 122 | } 123 | 124 | note, err := noteDecoder.Validate() 125 | if err != nil { 126 | errors.HandleError(w, err, http.StatusBadRequest) 127 | return 128 | } 129 | 130 | id, err := ctl.usecase.CreateNote(ctx, note) 131 | if err != nil { 132 | errors.HandleError(w, err, http.StatusUnprocessableEntity) 133 | return 134 | } 135 | 136 | payload := response.Encode(id, nil, "true") 137 | 138 | response.Send(w, payload, http.StatusCreated) 139 | } 140 | 141 | func (ctl NoteController) ArchiveNote(w http.ResponseWriter, r *http.Request) { 142 | 143 | ctx := r.Context() 144 | 145 | vars := mux.Vars(r) 146 | 147 | id, err := strconv.Atoi(vars["id"]) 148 | if err != nil { 149 | errors.HandleError(w, err, http.StatusBadRequest) 150 | return 151 | } 152 | 153 | archived, err := ctl.usecase.ArchiveNote(ctx, int64(id)) 154 | if err != nil { 155 | errors.HandleError(w, err, http.StatusUnprocessableEntity) 156 | return 157 | } 158 | 159 | payload := response.Encode(id, nil, archived) 160 | 161 | response.Send(w, payload, http.StatusAccepted) 162 | } 163 | 164 | func (ctl NoteController) UnArchiveNote(w http.ResponseWriter, r *http.Request) { 165 | 166 | ctx := r.Context() 167 | 168 | vars := mux.Vars(r) 169 | 170 | id, err := strconv.Atoi(vars["id"]) 171 | if err != nil { 172 | errors.HandleError(w, err, http.StatusBadRequest) 173 | return 174 | } 175 | 176 | unArchived, err := ctl.usecase.UnArchiveNote(ctx, int64(id)) 177 | if err != nil { 178 | errors.HandleError(w, err, http.StatusUnprocessableEntity) 179 | return 180 | } 181 | 182 | payload := response.Encode(id, nil, unArchived) 183 | 184 | response.Send(w, payload, http.StatusAccepted) 185 | } 186 | 187 | func (ctl NoteController) UpdateNote(w http.ResponseWriter, r *http.Request) { 188 | 189 | ctx := r.Context() 190 | 191 | vars := mux.Vars(r) 192 | 193 | id, err := strconv.Atoi(vars["id"]) 194 | if err != nil { 195 | errors.HandleError(w, err, http.StatusBadRequest) 196 | return 197 | } 198 | 199 | updateNoteDecoder := decoders.UpdateNote{} 200 | 201 | err = request.Decode(ctx, r, &updateNoteDecoder) 202 | if err != nil { 203 | errors.HandleError(w, err, http.StatusBadRequest) 204 | return 205 | } 206 | 207 | err = ctl.validator.Validate(ctx, updateNoteDecoder) 208 | if err != nil { 209 | errors.HandleError(w, err, http.StatusBadRequest) 210 | return 211 | } 212 | 213 | note, err := updateNoteDecoder.Validate() 214 | if err != nil { 215 | errors.HandleError(w, err, http.StatusBadRequest) 216 | return 217 | } 218 | 219 | updated, err := ctl.usecase.UpdateNote(ctx, int64(id), note) 220 | if err != nil { 221 | errors.HandleError(w, err, http.StatusUnprocessableEntity) 222 | return 223 | } 224 | 225 | payload := response.Encode(id, nil, updated) 226 | 227 | response.Send(w, payload, http.StatusAccepted) 228 | } 229 | 230 | func (ctl NoteController) DeleteNote(w http.ResponseWriter, r *http.Request) { 231 | 232 | ctx := r.Context() 233 | 234 | vars := mux.Vars(r) 235 | 236 | id, err := strconv.Atoi(vars["id"]) 237 | if err != nil { 238 | errors.HandleError(w, err, http.StatusBadRequest) 239 | return 240 | } 241 | 242 | deleted, err := ctl.usecase.DeleteNote(ctx, int64(id)) 243 | if err != nil { 244 | errors.HandleError(w, err, http.StatusUnprocessableEntity) 245 | return 246 | } 247 | 248 | payload := response.Encode(id, nil, deleted) 249 | 250 | response.Send(w, payload, http.StatusAccepted) 251 | } 252 | -------------------------------------------------------------------------------- /http/errors/handler.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "net/http" 5 | "notes/http/transport/response" 6 | ) 7 | 8 | func HandleError(w http.ResponseWriter, err error, code int) { 9 | 10 | payload := response.Encode(nil, err.Error(), "false") 11 | 12 | response.Send(w, payload, code) 13 | } 14 | -------------------------------------------------------------------------------- /http/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | "notes/http/controllers" 6 | "notes/http/transport/response" 7 | "notes/utils/container" 8 | 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | func Init(ctr container.Containers) *mux.Router { 13 | 14 | r := mux.NewRouter() 15 | 16 | noteController := controllers.NewNoteController(ctr) 17 | 18 | r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 19 | response.Send(w, []byte("Notes APP"), http.StatusOK) 20 | }).Methods(http.MethodGet) 21 | 22 | r.HandleFunc("/note/get_un_archived", noteController.GetAllUnarchivedNotes).Methods(http.MethodGet) 23 | r.HandleFunc("/note/get_archived", noteController.GetAllArchviedNotes).Methods(http.MethodGet) 24 | r.HandleFunc("/note/create", noteController.CreateNote).Methods(http.MethodPost) 25 | r.HandleFunc("/note/archive/{id}", noteController.ArchiveNote).Methods(http.MethodGet) 26 | r.HandleFunc("/note/un_archive/{id}", noteController.UnArchiveNote).Methods(http.MethodGet) 27 | r.HandleFunc("/note/update/{id}", noteController.UpdateNote).Methods(http.MethodPut) 28 | r.HandleFunc("/note/delete/{id}", noteController.DeleteNote).Methods(http.MethodDelete) 29 | 30 | r.HandleFunc("/note/refresh_cache", func(w http.ResponseWriter, r *http.Request) { 31 | ctr.Repositories.Note.RefreshCache() 32 | response.Send(w, []byte("cache refreshed"), http.StatusOK) 33 | }) 34 | 35 | return r 36 | } 37 | -------------------------------------------------------------------------------- /http/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "notes/utils/config" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | type HTTPServer struct { 15 | server *http.Server 16 | address string 17 | } 18 | 19 | func NewHTTPServer(config config.Config, r *mux.Router) HTTPServer { 20 | 21 | address := config.App.Host + ":" + strconv.Itoa(config.App.Port) 22 | 23 | server := &http.Server{ 24 | Addr: address, 25 | WriteTimeout: time.Second * 10, 26 | ReadTimeout: time.Second * 10, 27 | IdleTimeout: time.Second * 10, 28 | 29 | Handler: r, 30 | } 31 | 32 | httpServer := HTTPServer{ 33 | server: server, 34 | address: address, 35 | } 36 | 37 | return httpServer 38 | } 39 | 40 | func (srv HTTPServer) ListnAndServe(ctx context.Context) { 41 | 42 | log.Printf("server listening on %s", srv.address) 43 | 44 | err := srv.server.ListenAndServe() 45 | if err != nil { 46 | log.Println(err) 47 | } 48 | } 49 | 50 | func (srv HTTPServer) Shutdown(ctx context.Context) { 51 | 52 | log.Println("stropping HTTP server") 53 | 54 | srv.server.SetKeepAlivesEnabled(false) 55 | 56 | err := srv.server.Shutdown(ctx) 57 | if err != nil { 58 | log.Println(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /http/transport/request/decoders/note.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | import ( 4 | "errors" 5 | "notes/domain/entities" 6 | ) 7 | 8 | type Note struct { 9 | UserID int64 `json:"user_id" validate:"required"` 10 | Title string `json:"title" validate:"required"` 11 | Description string `json:"description" validate:"required"` 12 | } 13 | 14 | func (note Note) Format() string { 15 | return ` 16 | { 17 | "user_id": 123, 18 | "title": "title", 19 | "description": "description" 20 | } 21 | ` 22 | } 23 | 24 | func (note Note) Validate() (entities.Note, error) { 25 | 26 | n := entities.Note{} 27 | 28 | if note.UserID <= 0 { 29 | return n, errors.New("user_id must be a positive integer") 30 | } 31 | 32 | n.UserID = note.UserID 33 | n.Title = note.Title 34 | n.Description = note.Description 35 | 36 | return n, nil 37 | } 38 | -------------------------------------------------------------------------------- /http/transport/request/decoders/paginator.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | import ( 4 | "errors" 5 | "notes/domain/entities" 6 | ) 7 | 8 | type Paginator struct { 9 | Page int `json:"page"` 10 | Size int `json:"size"` 11 | } 12 | 13 | func (paginator Paginator) Format() string { 14 | return ` 15 | { 16 | "page": 1, 17 | "size": 10 18 | } 19 | ` 20 | } 21 | 22 | func (paginator Paginator) Validate() (entities.Paginator, error) { 23 | 24 | p := entities.Paginator{} 25 | 26 | if paginator.Page < 1 { 27 | return p, errors.New("page must be a positive integer") 28 | } 29 | 30 | if paginator.Size < 10 || paginator.Size > 100 { 31 | return p, errors.New("size must be inbetween 10 - 100") 32 | } 33 | 34 | p.Page = paginator.Page 35 | p.Size = paginator.Size 36 | 37 | return p, nil 38 | } 39 | -------------------------------------------------------------------------------- /http/transport/request/decoders/update_note.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | import ( 4 | "errors" 5 | "notes/domain/entities" 6 | ) 7 | 8 | type UpdateNote struct { 9 | Title string `json:"title"` 10 | Description string `json:"description"` 11 | } 12 | 13 | func (note UpdateNote) Format() string { 14 | return ` 15 | { 16 | "title": "title", 17 | "description": "description" 18 | } 19 | ` 20 | } 21 | 22 | func (note UpdateNote) Validate() (entities.Note, error) { 23 | 24 | n := entities.Note{} 25 | 26 | if note.Description == "" && note.Title == "" { 27 | return n, errors.New("either title or description must be given") 28 | } 29 | 30 | n.Title = note.Title 31 | n.Description = note.Description 32 | 33 | return n, nil 34 | } 35 | -------------------------------------------------------------------------------- /http/transport/request/interface.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type DecoderInterface interface { 4 | Format() string 5 | } 6 | -------------------------------------------------------------------------------- /http/transport/request/request.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | func Decode(ctx context.Context, r *http.Request, decoder DecoderInterface) error { 11 | 12 | bite, err := ioutil.ReadAll(r.Body) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | defer r.Body.Close() 18 | 19 | err = json.Unmarshal(bite, decoder) 20 | if err != nil { 21 | return nil 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /http/transport/response/encode.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type Encoder struct { 8 | Data interface{} `json:"data"` 9 | Error interface{} `json:"error"` 10 | Success interface{} `json:"success"` 11 | } 12 | 13 | func Encode(data interface{}, err interface{}, success interface{}) []byte { 14 | 15 | payload := Encoder{ 16 | Data: data, 17 | Error: err, 18 | Success: success, 19 | } 20 | 21 | message, _ := json.Marshal(payload) 22 | 23 | return message 24 | } 25 | -------------------------------------------------------------------------------- /http/transport/response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import "net/http" 4 | 5 | func Send(w http.ResponseWriter, payload []byte, code int) { 6 | 7 | w.Header().Set("content-type", "application/json") 8 | w.WriteHeader(code) 9 | 10 | w.Write(payload) 11 | } 12 | -------------------------------------------------------------------------------- /http/validators/validators.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | ut "github.com/go-playground/universal-translator" 9 | "gopkg.in/go-playground/validator.v9" 10 | ) 11 | 12 | type Validator struct { 13 | validator *validator.Validate 14 | translator ut.Translator 15 | } 16 | 17 | func NewValidator() Validator { 18 | validator := Validator{ 19 | validator: validator.New(), 20 | } 21 | 22 | return validator 23 | } 24 | 25 | func (val Validator) Validate(ctx context.Context, data interface{}) error { 26 | 27 | err := val.validator.Struct(data) 28 | if err == nil { 29 | return nil 30 | } 31 | 32 | fieldErrors := err.(validator.ValidationErrors) 33 | 34 | errMessage := "" 35 | 36 | for i, e := range fieldErrors { 37 | errMessage += fmt.Sprintf("param:%v is invalid. %s", e.Value(), e.Translate(val.translator)) 38 | 39 | if i == len(fieldErrors)-1 { 40 | continue 41 | } 42 | 43 | errMessage += " | " 44 | } 45 | 46 | return errors.New(errMessage) 47 | } 48 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "notes/bootstrap" 6 | ) 7 | 8 | func main() { 9 | ctx := context.Background() 10 | 11 | bootstrap.Start(ctx) 12 | } 13 | -------------------------------------------------------------------------------- /notes_app.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "d698bc76-6738-4606-9f23-d3f92717072e", 4 | "name": "notes app", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "get all unarchived notes", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "url": { 14 | "raw": "localhost:8080/note/get_un_archived?paginator={\"page\": 1, \"size\":10}", 15 | "host": [ 16 | "localhost" 17 | ], 18 | "port": "8080", 19 | "path": [ 20 | "note", 21 | "get_un_archived" 22 | ], 23 | "query": [ 24 | { 25 | "key": "paginator", 26 | "value": "{\"page\": 1, \"size\":10}" 27 | } 28 | ] 29 | } 30 | }, 31 | "response": [] 32 | }, 33 | { 34 | "name": "get archived notes", 35 | "request": { 36 | "method": "GET", 37 | "header": [], 38 | "url": { 39 | "raw": "localhost:8080/note/get_archived?paginator={\"page\": 1,\"size\":10} ", 40 | "host": [ 41 | "localhost" 42 | ], 43 | "port": "8080", 44 | "path": [ 45 | "note", 46 | "get_archived" 47 | ], 48 | "query": [ 49 | { 50 | "key": "paginator", 51 | "value": "{\"page\": 1,\"size\":10} " 52 | } 53 | ] 54 | } 55 | }, 56 | "response": [] 57 | }, 58 | { 59 | "name": "save a note", 60 | "request": { 61 | "method": "POST", 62 | "header": [], 63 | "url": { 64 | "raw": "localhost:8080/note/get_un_archived?paginator={\"page\": 1, \"size\":10}", 65 | "host": [ 66 | "localhost" 67 | ], 68 | "port": "8080", 69 | "path": [ 70 | "note", 71 | "get_un_archived" 72 | ], 73 | "query": [ 74 | { 75 | "key": "paginator", 76 | "value": "{\"page\": 1, \"size\":10}" 77 | } 78 | ] 79 | } 80 | }, 81 | "response": [] 82 | }, 83 | { 84 | "name": "archive a note", 85 | "request": { 86 | "method": "GET", 87 | "header": [], 88 | "url": { 89 | "raw": "localhost:8080/note/archive/1", 90 | "host": [ 91 | "localhost" 92 | ], 93 | "port": "8080", 94 | "path": [ 95 | "note", 96 | "archive", 97 | "1" 98 | ] 99 | } 100 | }, 101 | "response": [] 102 | }, 103 | { 104 | "name": "update a note", 105 | "request": { 106 | "method": "PUT", 107 | "header": [], 108 | "body": { 109 | "mode": "raw", 110 | "raw": "{\n \"title\" : \"hi\",\n \"description\": \"adding\"\n}", 111 | "options": { 112 | "raw": { 113 | "language": "json" 114 | } 115 | } 116 | }, 117 | "url": { 118 | "raw": "localhost:8080/note/update/1", 119 | "host": [ 120 | "localhost" 121 | ], 122 | "port": "8080", 123 | "path": [ 124 | "note", 125 | "update", 126 | "1" 127 | ] 128 | } 129 | }, 130 | "response": [] 131 | }, 132 | { 133 | "name": "delete a note", 134 | "request": { 135 | "method": "DELETE", 136 | "header": [], 137 | "url": { 138 | "raw": "localhost:8080/note/delete/3", 139 | "host": [ 140 | "localhost" 141 | ], 142 | "port": "8080", 143 | "path": [ 144 | "note", 145 | "delete", 146 | "3" 147 | ] 148 | } 149 | }, 150 | "response": [] 151 | }, 152 | { 153 | "name": "refresh cache", 154 | "request": { 155 | "method": "GET", 156 | "header": [], 157 | "url": { 158 | "raw": "localhost:8080/note/refresh_cache", 159 | "host": [ 160 | "localhost" 161 | ], 162 | "port": "8080", 163 | "path": [ 164 | "note", 165 | "refresh_cache" 166 | ] 167 | } 168 | }, 169 | "response": [] 170 | }, 171 | { 172 | "name": "un archive a note", 173 | "request": { 174 | "method": "GET", 175 | "header": [], 176 | "url": { 177 | "raw": "localhost:8080/note/un_archive/3", 178 | "host": [ 179 | "localhost" 180 | ], 181 | "port": "8080", 182 | "path": [ 183 | "note", 184 | "un_archive", 185 | "3" 186 | ] 187 | } 188 | }, 189 | "response": [] 190 | } 191 | ] 192 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Notes APP 3 | 4 | A REST API backend application that can be used to manage personal notes in a multi-user environment. 5 | 6 | 7 | ### How to start 8 | 9 | - Make sure database is up and running and you have update the ```configurations/database.yaml``` file with relevant values. 10 | - Run the ```database.sql``` query to get database and table created. 11 | - Clone the service locally and run the service by typing ```go run main.go``` 12 | - make sure service is up and running. 13 | - Now you can send request to the service. 14 | 15 | Note: Use the postman collection ```notes_app.postman_collection.json``` 16 | 17 | ## API Reference 18 | 19 | #### Get all un archived notes 20 | 21 | ```http 22 | GET /note/get_un_archived 23 | ``` 24 | 25 | | Parameter | Type | Description | 26 | | :-------- | :------- | :------------------------- | 27 | | `paginator` | `{"page":1, "size": 10}` | **required**. page must be greater than zero & size must be in between 10 - 100 | 28 | 29 | - Response 30 | ```json 31 | { 32 | "data": [ 33 | { 34 | "id": 3, 35 | "user_id": 1, 36 | "title": "abc", 37 | "description": "cc bnbj v", 38 | "created_at": "2022-03-26T22:10:40Z", 39 | "updated_at": "2022-03-26T22:31:01Z" 40 | } 41 | ], 42 | "error": null, 43 | "success": "true" 44 | } 45 | ``` 46 | 47 | #### Get all archived notes 48 | 49 | ```http 50 | GET /note/get_archived 51 | ``` 52 | 53 | | Parameter | Type | Description | 54 | | :-------- | :------- | :------------------------- | 55 | | `paginator` | `{"page":1, "size": 10}` | **required**. page must be greater than zero & size must be in between 10 - 100 | 56 | 57 | - Response 58 | ```json 59 | { 60 | "data": [ 61 | { 62 | "id": 2, 63 | "user_id": 1, 64 | "title": "hello", 65 | "description": "cc bnbj v", 66 | "created_at": "2022-03-26T22:10:40Z", 67 | "updated_at": "2022-03-26T22:31:01Z" 68 | } 69 | ], 70 | "error": null, 71 | "success": "true" 72 | } 73 | ``` 74 | 75 | #### Save a note 76 | 77 | ```http 78 | POST /note/create 79 | ``` 80 | 81 | - Request body 82 | 83 | 84 | ```json 85 | { 86 | "user_id": 123, 87 | "title": "title", 88 | "description": "description" 89 | } 90 | ``` 91 | 92 | - Response 93 | ```json 94 | { 95 | "data": 3, 96 | "error": null, 97 | "success": "true" 98 | } 99 | ``` 100 | 101 | #### Archive a note 102 | 103 | ```http 104 | GET /note/archive/{id} 105 | ``` 106 | 107 | - Response 108 | ```json 109 | { 110 | "data": 3, 111 | "error": null, 112 | "success": true 113 | } 114 | ``` 115 | 116 | #### Un Archive a note 117 | 118 | ```http 119 | GET /note/un_archive/{id} 120 | ``` 121 | 122 | - Response 123 | ```json 124 | { 125 | "data": 3, 126 | "error": null, 127 | "success": true 128 | } 129 | ``` 130 | 131 | #### Update a note 132 | 133 | ```http 134 | PUT /note/update/{id} 135 | ``` 136 | 137 | - Request body 138 | 139 | 140 | ```json 141 | { 142 | "title": "title", 143 | "description": "description" 144 | } 145 | ``` 146 | 147 | - Response 148 | ```json 149 | { 150 | "data": 2, 151 | "error": null, 152 | "success": true 153 | } 154 | ``` 155 | 156 | #### Delete a note 157 | 158 | ```http 159 | DELETE /note/delete/{id} 160 | ``` 161 | 162 | - Response 163 | ```json 164 | { 165 | "data": 3, 166 | "error": null, 167 | "success": true 168 | } 169 | ``` 170 | -------------------------------------------------------------------------------- /utils/config/app.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | type App struct { 10 | Port int `yaml:"service-port"` 11 | Host string `yaml:"service-host"` 12 | } 13 | 14 | func (app *App) Parse() error { 15 | 16 | yamlFile, err := ioutil.ReadFile("configurations/app.yaml") 17 | if err != nil { 18 | return err 19 | } 20 | err = yaml.Unmarshal(yamlFile, app) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /utils/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | App App 5 | Database Database 6 | } 7 | -------------------------------------------------------------------------------- /utils/config/db.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | type Database struct { 10 | User string `yaml:"user"` 11 | Password string `yaml:"password"` 12 | Host string `yaml:"host"` 13 | Port int `yaml:"port"` 14 | Database string `yaml:"database"` 15 | IdleConnection int `yaml:"idle-connection"` 16 | OpenConnection int `yaml:"open-connection"` 17 | ConnectionLifeTime int `yaml:"connection-life-time"` 18 | ConnectionIdleTime int `yaml:"connection-idle-time"` 19 | ReadTimeout int `yaml:"read-timeout"` 20 | WriteTimeout int `yaml:"write-timeout"` 21 | Timeout int `yaml:"timeout"` 22 | } 23 | 24 | func (db *Database) Parse() error { 25 | 26 | yamlFile, err := ioutil.ReadFile("configurations/database.yaml") 27 | if err != nil { 28 | return err 29 | } 30 | err = yaml.Unmarshal(yamlFile, db) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /utils/config/parser.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | func Parse() (Config, error) { 4 | app := &App{} 5 | db := &Database{} 6 | 7 | err := app.Parse() 8 | if err != nil { 9 | return Config{}, err 10 | } 11 | 12 | err = db.Parse() 13 | if err != nil { 14 | return Config{}, err 15 | } 16 | 17 | configs := Config{ 18 | App: *app, 19 | Database: *db, 20 | } 21 | 22 | return configs, nil 23 | } 24 | -------------------------------------------------------------------------------- /utils/container/container.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "database/sql" 5 | "notes/domain/interfaces" 6 | ) 7 | 8 | type Containers struct { 9 | Adapters Adapters 10 | Repositories Repositories 11 | Caches Caches 12 | } 13 | 14 | type Adapters struct { 15 | Db *sql.DB 16 | } 17 | 18 | type Repositories struct { 19 | Note interfaces.NoteRepository 20 | } 21 | 22 | type Caches struct { 23 | Note interfaces.NoteCache 24 | } 25 | -------------------------------------------------------------------------------- /utils/container/resolver.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "database/sql" 5 | "notes/domain/interfaces" 6 | "notes/externals/adapters" 7 | "notes/externals/cache" 8 | "notes/externals/repositories" 9 | "notes/utils/config" 10 | ) 11 | 12 | func Resolve(config config.Config) (Containers, error) { 13 | adaptrs, err := resolveAdapters(config) 14 | if err != nil { 15 | return Containers{}, err 16 | } 17 | 18 | caches := resolveCaches() 19 | 20 | repos, err := resolveRepostories(adaptrs.Db, caches.Note) 21 | if err != nil { 22 | return Containers{}, err 23 | } 24 | 25 | cont := Containers{ 26 | Adapters: adaptrs, 27 | Repositories: repos, 28 | } 29 | 30 | return cont, nil 31 | } 32 | 33 | func resolveAdapters(config config.Config) (Adapters, error) { 34 | 35 | mysql, err := adapters.NewDB(config.Database) 36 | if err != nil { 37 | return Adapters{}, err 38 | } 39 | 40 | adapters := Adapters{ 41 | Db: mysql, 42 | } 43 | 44 | return adapters, nil 45 | } 46 | 47 | func resolveCaches() Caches { 48 | noteCache := cache.NewNoteCache() 49 | 50 | caches := Caches{ 51 | Note: noteCache, 52 | } 53 | 54 | return caches 55 | } 56 | 57 | func resolveRepostories(db *sql.DB, noteCache interfaces.NoteCache) (Repositories, error) { 58 | noteRepo := repositories.NewNoteRepository(db, noteCache) 59 | 60 | repos := Repositories{ 61 | Note: noteRepo, 62 | } 63 | 64 | return repos, nil 65 | } 66 | --------------------------------------------------------------------------------