diff --git a/handlers/handlers.go b/handlers/handlers.go index 1e04b41..704e660 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -115,15 +115,13 @@ func HandleUserApiRequest( return } - user1 := models.User{"Adrian"} - user2 := models.User{"Evan"} - - usersById := map[models.UserId]models.User{ - 1: user1, - 2: user2, + usersById, err := env.Db.GetAllUsersById() + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return } - usersByIdJson, err := json.Marshal(usersById) + usersByIdJson, err := usersById.ToJson() if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return @@ -231,21 +229,30 @@ func HandleNoteApiRequest( switch request.Method { case http.MethodGet: - var notesById models.NoteMap = make(map[models.NoteId]*models.Note, 2) - - notesById[models.NoteId(1)] = &models.Note{ - AuthorId: 1, - Content: "This is an example note.", - CreationTime: time.Now().Add(-oneWeek).UTC(), + publishedNotes, err := env.Db.GetAllPublishedNotesVisibleBy(userId) + if err != nil { + http.Error(responseWriter, err.Error(), http.StatusInternalServerError) + return } - notesById[models.NoteId(2)] = &models.Note{ - AuthorId: 2, - Content: "What is this site for?", - CreationTime: time.Now().Add(-60 * 12).UTC(), + myUnpublishedNotes, err := env.Db.GetMyUnpublishedNotes(userId) + + fmt.Println("number of published notes") + fmt.Println(len(publishedNotes)) + fmt.Println("number of unpublished notes") + fmt.Println(len(myUnpublishedNotes)) + + allNotes := myUnpublishedNotes + + // TODO figure out how to surface the publication number + // for publicationNumber, noteMap := range publishedNotes { + for _, noteMap := range publishedNotes { + for id, note := range noteMap { + allNotes[id] = note + } } - notesInJson, err := notesById.ToJson() + notesInJson, err := allNotes.ToJson() if err != nil { http.Error(responseWriter, err.Error(), http.StatusInternalServerError) return diff --git a/integration_test.go b/integration_test.go index b5900c5..929c29f 100644 --- a/integration_test.go +++ b/integration_test.go @@ -117,11 +117,42 @@ func TestAuthenticatedFlow(t *testing.T) { // Test get notes { + + mockDb.Func_GetMyUnpublishedNotes = func(userId models.UserId) (models.NoteMap, error) { + if userIdAsInt != int64(userId) { + return nil, errors.New("Invalid userId passed in") + } + + return models.NoteMap(map[models.NoteId]*models.Note{ + models.NoteId(noteIdAsInt): &models.Note{ + AuthorId: models.UserId(userIdAsInt), + Content: content, + CreationTime: time.Now(), + }, + }), nil + + } + + mockDb.Func_GetAllPublishedNotesVisibleBy = func(userId models.UserId) (map[int64]models.NoteMap, error) { + if userIdAsInt != int64(userId) { + return nil, errors.New("Invalid userId passed in") + } + + return map[int64]models.NoteMap{ + 1: models.NoteMap(map[models.NoteId]*models.Note{ + models.NoteId(44): &models.Note{ + AuthorId: models.UserId(99), + Content: "another note", + CreationTime: time.Now(), + }, + }), + }, nil + + } + resp, err := client.Get(server.URL + paths.NoteApi) ok(t, err) equals(t, http.StatusOK, resp.StatusCode) - - // TODO when we implement a real get notes feature we should enhance this code. } // Test Add category @@ -215,6 +246,9 @@ type DiyMockDataStore struct { Func_GetIdForUserWithEmailAddress func(*models.EmailAddress) (models.UserId, error) Func_GetUsersNotes func(models.UserId) (models.NoteMap, error) Func_DeleteNoteById func(models.NoteId) error + Func_GetMyUnpublishedNotes func(models.UserId) (models.NoteMap, error) + Func_GetAllUsersById func() (models.UserMap, error) + Func_GetAllPublishedNotesVisibleBy func(models.UserId) (map[int64]models.NoteMap, error) } func (mock *DiyMockDataStore) StoreNewNote(note *models.Note) (models.NoteId, error) { @@ -245,6 +279,18 @@ func (mock *DiyMockDataStore) DeleteNoteById(noteId models.NoteId) error { return mock.Func_DeleteNoteById(noteId) } +func (mock *DiyMockDataStore) GetMyUnpublishedNotes(userId models.UserId) (models.NoteMap, error) { + return mock.Func_GetMyUnpublishedNotes(userId) +} + +func (mock *DiyMockDataStore) GetAllUsersById() (models.UserMap, error) { + return mock.Func_GetAllUsersById() +} + +func (mock *DiyMockDataStore) GetAllPublishedNotesVisibleBy(userId models.UserId) (map[int64]models.NoteMap, error) { + return mock.Func_GetAllPublishedNotesVisibleBy(userId) +} + // assert fails the test if the condition is false. func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { if !condition { diff --git a/models/datastore.go b/models/datastore.go index b3b4d3b..094fd57 100644 --- a/models/datastore.go +++ b/models/datastore.go @@ -26,6 +26,9 @@ type Datastore interface { GetIdForUserWithEmailAddress(*EmailAddress) (UserId, error) GetUsersNotes(UserId) (NoteMap, error) DeleteNoteById(NoteId) error + GetMyUnpublishedNotes(UserId) (NoteMap, error) + GetAllUsersById() (UserMap, error) + GetAllPublishedNotesVisibleBy(UserId) (map[int64]NoteMap, error) } type DB struct { diff --git a/models/datastore_test.go b/models/datastore_test.go index ff21c92..4d1327e 100644 --- a/models/datastore_test.go +++ b/models/datastore_test.go @@ -60,11 +60,21 @@ func TestUser(t *testing.T) { err = db.StoreNewUser(displayName, emailAddress, password) ok(t, err) - _, err = db.GetIdForUserWithEmailAddress(emailAddress) + id, err := db.GetIdForUserWithEmailAddress(emailAddress) ok(t, err) err = db.AuthenticateUserCredentials(emailAddress, password) ok(t, err) + + userMap, err := db.GetAllUsersById() + ok(t, err) + + equals(t, 1, len(userMap)) + + user, isOk := userMap[id] + assert(t, isOk, "Expected UserId missing") + + equals(t, displayName, user.DisplayName) } func TestNote(t *testing.T) { @@ -87,6 +97,52 @@ func TestNote(t *testing.T) { id, err := db.StoreNewNote(note) ok(t, err) assert(t, int64(id) > 0, "Note Id was not a valid index: "+strconv.Itoa(int(id))) + + notemap, err := db.GetMyUnpublishedNotes(userId) + ok(t, err) + + retrievedNote, isOk := notemap[id] + assert(t, isOk, "Expected NoteId missing") + + equals(t, note.AuthorId, retrievedNote.AuthorId) + equals(t, note.Content, retrievedNote.Content) + + err = db.DeleteNoteById(id) + ok(t, err) +} + +func TestPublication(t *testing.T) { + db, err := models.ConnectToDatabase(postgresUrl) + ok(t, err) + ClearValuesInTable(db, userTable) + ClearValuesInTable(db, noteTable) + ClearValuesInTable(db, publicationTable) + ClearValuesInTable(db, noteToPublicationTable) + + displayName := "bob" + password := "aPassword" + emailAddress := models.NewEmailAddress("thisIsMyEmail@gmail.com") + + err = db.StoreNewUser(displayName, emailAddress, password) + ok(t, err) + + userId, err := db.GetIdForUserWithEmailAddress(emailAddress) + ok(t, err) + + note := &models.Note{AuthorId: userId, Content: "I'm a note", CreationTime: time.Now()} + id, err := db.StoreNewNote(note) + ok(t, err) + assert(t, int64(id) > 0, "Note Id was not a valid index: "+strconv.Itoa(int(id))) + + fmt.Println(userId) + publicationToNoteMap, err := db.GetAllPublishedNotesVisibleBy(userId) + ok(t, err) + + equals(t, 0, len(publicationToNoteMap)) + + // TODO once we implement publication publishing, test publication adding, + // and that GetAllPublishedNotesVisibleBy has non-zero rows + } func TestCategory(t *testing.T) { diff --git a/models/note.go b/models/note.go index 6fbcb75..dbd1aec 100644 --- a/models/note.go +++ b/models/note.go @@ -38,27 +38,132 @@ func (db *DB) StoreNewNote( } func (db *DB) GetUsersNotes(userId UserId) (NoteMap, error) { - noteMap := make(map[NoteId]*Note) + sqlQuery := ` + SELECT id, author_id, content, creation_time FROM note + WHERE author_id = $1` - { - sqlQuery := ` - SELECT id, author_id, content, creation_time FROM note - WHERE author_id = $1` - rows, err := db.Query(sqlQuery, int64(userId)) - if err != nil { - return nil, convertPostgresError(err) + noteMap, err := db.getNoteMap(sqlQuery, int64(userId)) + if err != nil { + return nil, err + } + + return noteMap, nil +} + +func (db *DB) GetAllPublishedNotesVisibleBy(userId UserId) (map[int64]NoteMap, error) { + + sqlQueryIssueNumber := ` + SELECT COUNT(*) AS IssueNumber FROM publication + WHERE publication.author_id = $1` + + var publictionIssueNumber int64 + if err := db.execOneResult(sqlQueryIssueNumber, &publictionIssueNumber, int64(userId)); err != nil { + return nil, err + } + + sqlQueryGetNotes := ` + SELECT + note.id, + note.author_id, + note.content, + note.creation_time, + filtered_pubs.rank AS publication_issue + FROM (SELECT *, + Rank() + OVER( + partition BY pub.author_id + ORDER BY pub.creation_time) + FROM publication AS pub) filtered_pubs + INNER JOIN note_to_publication_relationship AS note2pub + ON note2pub.publication_id = filtered_pubs.id + INNER JOIN note + ON note.id = note2pub.note_id + WHERE rank <= ($1)` + + // sqlQueryGetNotes := ` + // SELECT + // note.id, + // note.author_id, + // note.content, + // note.creation_time, + // note2cat.type AS category, + // filtered_pubs.rank AS publication_issue + // FROM (SELECT *, + // Rank() + // OVER( + // partition BY pub.author_id + // ORDER BY pub.creation_time) + // FROM publication AS pub) filtered_pubs + // INNER JOIN note_to_publication_relationship AS note2pub + // ON note2pub.publication_id = filtered_pubs.id + // INNER JOIN note + // ON note.id = note2pub.note_id + // LEFT OUTER JOIN note_to_category_relationship AS note2cat + // ON note.id = note2cat.note_id + // WHERE rank <= ($1)` + + rows, err := db.Query(sqlQueryGetNotes, publictionIssueNumber) + if err != nil { + return nil, convertPostgresError(err) + } + + defer rows.Close() + + pubToNoteMap := make(map[int64]NoteMap) + + for rows.Next() { + var publicationNumber int64 + var noteId int64 + note := &Note{} + if err := rows.Scan(¬eId, ¬e.AuthorId, ¬e.Content, ¬e.CreationTime, &publicationNumber); err != nil { + return nil, err + } + + noteMap, ok := pubToNoteMap[publicationNumber] + if !ok { + pubToNoteMap[publicationNumber] = make(map[NoteId]*Note) } - defer rows.Close() - for rows.Next() { - var tempId int64 - tempNote := &Note{} - if err := rows.Scan(&tempId, &tempNote.AuthorId, &tempNote.Content, &tempNote.CreationTime); err != nil { - return nil, convertPostgresError(err) - } + noteMap[NoteId(noteId)] = note - noteMap[NoteId(tempId)] = tempNote + } + + return pubToNoteMap, nil +} + +func (db *DB) GetMyUnpublishedNotes(userId UserId) (NoteMap, error) { + sqlQuery := ` + SELECT id, author_id, content, creation_time FROM note + LEFT OUTER JOIN note_to_publication_relationship AS note2pub + ON note.id = note2pub.note_id + WHERE note2pub.note_id is NULL AND note.author_id = $1` + + noteMap, err := db.getNoteMap(sqlQuery, int64(userId)) + if err != nil { + return nil, err + } + + return noteMap, nil +} + +func (db *DB) getNoteMap(sqlQuery string, args ...interface{}) (NoteMap, error) { + + noteMap := make(map[NoteId]*Note) + + rows, err := db.Query(sqlQuery, args...) + if err != nil { + return nil, convertPostgresError(err) + } + defer rows.Close() + + for rows.Next() { + var tempId int64 + tempNote := &Note{} + if err := rows.Scan(&tempId, &tempNote.AuthorId, &tempNote.Content, &tempNote.CreationTime); err != nil { + return nil, convertPostgresError(err) } + + noteMap[NoteId(tempId)] = tempNote } return noteMap, nil @@ -79,7 +184,7 @@ func (db *DB) DeleteNoteById(noteId NoteId) error { } if num != 1 { - return errors.New("Somewhere we more than 1 note was deleted") + return errors.New("somehow more than 1 note was deleted") } return nil diff --git a/models/user.go b/models/user.go index 338d979..9fbc165 100644 --- a/models/user.go +++ b/models/user.go @@ -1,7 +1,9 @@ package models import ( + "encoding/json" "errors" + "fmt" "strings" "time" @@ -31,6 +33,19 @@ var EmailAddressAlreadyInUseError = errors.New("Email address already in use") var CredentialsNotAuthorizedError = errors.New("The provided credentials were not found") +type UserMap map[UserId]*User + +func (userMap UserMap) ToJson() ([]byte, error) { + // json doesn't support int indexed maps + userByIdString := make(map[string]User, len(userMap)) + + for id, user := range userMap { + userByIdString[fmt.Sprint(id)] = *user + } + + return json.Marshal(userByIdString) +} + // func (db *DB) StoreNewUser( @@ -102,3 +117,31 @@ func (db *DB) GetIdForUserWithEmailAddress(emailAddress *EmailAddress) (UserId, return UserId(userId), nil } + +func (db *DB) GetAllUsersById() (UserMap, error) { + sqlQuery := ` + SELECT id, display_name FROM app_user` + + rows, err := db.Query(sqlQuery) + if err != nil { + return nil, convertPostgresError(err) + } + + defer rows.Close() + + var userMap UserMap = make(map[UserId]*User) + + for rows.Next() { + var tempId int64 + user := &User{} + if err := rows.Scan(&tempId, &user.DisplayName); err != nil { + return nil, convertPostgresError(err) + } + + userMap[UserId(tempId)] = user + + } + + return userMap, nil + +}