From 360fb47b05437f129265d0d51cc63501f3229f70 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Fri, 3 Apr 2026 20:17:29 -0400 Subject: [PATCH] fix: total counts --- api/app-routes.go | 9 +- database/documents_test.go | 321 ++++++++++++++++++++++++++++++++++++- database/query.sql | 7 +- database/query.sql.go | 17 +- 4 files changed, 333 insertions(+), 21 deletions(-) diff --git a/api/app-routes.go b/api/app-routes.go index 26ba7fe..8506af4 100644 --- a/api/app-routes.go +++ b/api/app-routes.go @@ -124,10 +124,13 @@ func (api *API) appGetDocuments(c *gin.Context) { return } - length, err := api.db.Queries.GetDocumentsSize(c, query) + length, err := api.db.Queries.GetDocumentsWithStatsCount(c, database.GetDocumentsWithStatsCountParams{ + Deleted: ptr.Of(false), + Query: query, + }) if err != nil { - log.Error("GetDocumentsSize DB Error: ", err) - appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsSize DB Error: %v", err)) + log.Error("GetDocumentsWithStatsCount DB Error: ", err) + appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsWithStatsCount DB Error: %v", err)) return } diff --git a/database/documents_test.go b/database/documents_test.go index cda01b1..7e00753 100644 --- a/database/documents_test.go +++ b/database/documents_test.go @@ -4,10 +4,13 @@ import ( "context" "fmt" "testing" + "time" "github.com/stretchr/testify/suite" "reichard.io/antholume/config" + "reichard.io/antholume/pkg/ptr" + "reichard.io/antholume/utils" ) type DocumentsTestSuite struct { @@ -36,12 +39,54 @@ func (suite *DocumentsTestSuite) SetupTest() { suite.NoError(err) } -// DOCUMENT - TODO: -// - 󰊕 (q *Queries) GetDocumentProgress -// - 󰊕 (q *Queries) GetDocumentWithStats -// - 󰊕 (q *Queries) GetDocumentsSize -// - 󰊕 (q *Queries) GetDocumentsWithStats -// - 󰊕 (q *Queries) GetMissingDocuments +func (suite *DocumentsTestSuite) seedDocumentStats() { + suite.createTestUserAndDevice() + + var err error + _, err = suite.dbm.Queries.AddActivity(context.Background(), AddActivityParams{ + DocumentID: documentID, + DeviceID: deviceID, + UserID: userID, + StartTime: time.Now().UTC().Format(time.RFC3339), + Duration: 60, + StartPercentage: 0.10, + EndPercentage: 0.20, + }) + suite.Require().NoError(err) + + _, err = suite.dbm.Queries.UpdateProgress(context.Background(), UpdateProgressParams{ + UserID: userID, + DocumentID: documentID, + DeviceID: deviceID, + Percentage: 0.42, + Progress: "/6/2[test]", + }) + suite.Require().NoError(err) + + err = suite.dbm.CacheTempTables(context.Background()) + suite.Require().NoError(err) +} + +func (suite *DocumentsTestSuite) createTestUserAndDevice() { + rawAuthHash, err := utils.GenerateToken(64) + suite.Require().NoError(err) + authHash := fmt.Sprintf("%x", rawAuthHash) + + _, err = suite.dbm.Queries.CreateUser(context.Background(), CreateUserParams{ + ID: userID, + Pass: &userPass, + AuthHash: &authHash, + }) + suite.Require().NoError(err) + + _, err = suite.dbm.Queries.UpsertDevice(context.Background(), UpsertDeviceParams{ + ID: deviceID, + UserID: userID, + DeviceName: deviceName, + }) + suite.Require().NoError(err) +} + func (suite *DocumentsTestSuite) TestGetDocument() { doc, err := suite.dbm.Queries.GetDocument(context.Background(), documentID) suite.Nil(err, "should have nil err") @@ -63,6 +108,265 @@ func (suite *DocumentsTestSuite) TestUpsertDocument() { suite.Equal(documentAuthor, *doc.Author, "should have document author") } +func (suite *DocumentsTestSuite) TestGetDocumentProgress() { + suite.seedDocumentStats() + + progress, err := suite.dbm.Queries.GetDocumentProgress(context.Background(), GetDocumentProgressParams{ + UserID: userID, + DocumentID: documentID, + }) + + suite.NoError(err) + suite.Equal(userID, progress.UserID) + suite.Equal(documentID, progress.DocumentID) + suite.Equal(deviceID, progress.DeviceID) + suite.Equal(deviceName, progress.DeviceName) + suite.Equal(0.42, progress.Percentage) + suite.Equal("/6/2[test]", progress.Progress) +} + +func (suite *DocumentsTestSuite) TestGetDocumentWithStats() { + suite.seedDocumentStats() + + doc, err := suite.dbm.GetDocument(context.Background(), documentID, userID) + + suite.NoError(err) + suite.Equal(documentID, doc.ID) + suite.Equal(documentTitle, *doc.Title) + suite.Equal(documentAuthor, *doc.Author) + suite.Equal(documentWords, *doc.Words) + suite.Equal(float64(42), doc.Percentage) + suite.Equal(int64(60), doc.TotalTimeSeconds) + suite.Equal(int64(500), doc.Wpm) + suite.Equal(int64(6), doc.SecondsPerPercent) +} + +func (suite *DocumentsTestSuite) TestGetDocumentsSize() { + count, err := suite.dbm.Queries.GetDocumentsSize(context.Background(), nil) + + suite.NoError(err) + suite.Equal(int64(1), count) + + count, err = suite.dbm.Queries.GetDocumentsSize(context.Background(), "%testTitle%") + suite.NoError(err) + suite.Equal(int64(1), count) + + count, err = suite.dbm.Queries.GetDocumentsSize(context.Background(), "%missing%") + suite.NoError(err) + suite.Equal(int64(0), count) +} + +func (suite *DocumentsTestSuite) TestGetDocumentsWithStatsCount() { + count, err := suite.dbm.Queries.GetDocumentsWithStatsCount(context.Background(), GetDocumentsWithStatsCountParams{}) + suite.NoError(err) + suite.Equal(int64(1), count) + + count, err = suite.dbm.Queries.GetDocumentsWithStatsCount(context.Background(), GetDocumentsWithStatsCountParams{ + Query: ptr.Of("%testTitle%"), + }) + suite.NoError(err) + suite.Equal(int64(1), count) + + count, err = suite.dbm.Queries.GetDocumentsWithStatsCount(context.Background(), GetDocumentsWithStatsCountParams{ + Query: ptr.Of("%testAuthor%"), + }) + suite.NoError(err) + suite.Equal(int64(1), count) + + count, err = suite.dbm.Queries.GetDocumentsWithStatsCount(context.Background(), GetDocumentsWithStatsCountParams{ + Query: ptr.Of("%missing%"), + }) + suite.NoError(err) + suite.Equal(int64(0), count) + + count, err = suite.dbm.Queries.GetDocumentsWithStatsCount(context.Background(), GetDocumentsWithStatsCountParams{ + ID: ptr.Of(documentID), + }) + suite.NoError(err) + suite.Equal(int64(1), count) + + count, err = suite.dbm.Queries.GetDocumentsWithStatsCount(context.Background(), GetDocumentsWithStatsCountParams{ + ID: ptr.Of("missing-id"), + }) + suite.NoError(err) + suite.Equal(int64(0), count) + + _, err = suite.dbm.Queries.DeleteDocument(context.Background(), documentID) + suite.Require().NoError(err) + + count, err = suite.dbm.Queries.GetDocumentsWithStatsCount(context.Background(), GetDocumentsWithStatsCountParams{ + Deleted: ptr.Of(false), + }) + suite.NoError(err) + suite.Equal(int64(0), count) + + count, err = suite.dbm.Queries.GetDocumentsWithStatsCount(context.Background(), GetDocumentsWithStatsCountParams{ + Deleted: ptr.Of(true), + }) + suite.NoError(err) + suite.Equal(int64(1), count) + + count, err = suite.dbm.Queries.GetDocumentsWithStatsCount(context.Background(), GetDocumentsWithStatsCountParams{}) + suite.NoError(err) + suite.Equal(int64(1), count) + + count, err = suite.dbm.Queries.GetDocumentsWithStatsCount(context.Background(), GetDocumentsWithStatsCountParams{ + ID: ptr.Of(documentID), + Deleted: ptr.Of(true), + }) + suite.NoError(err) + suite.Equal(int64(1), count) +} + +func (suite *DocumentsTestSuite) TestGetDocumentsWithStats() { + suite.seedDocumentStats() + + rows, err := suite.dbm.Queries.GetDocumentsWithStats(context.Background(), GetDocumentsWithStatsParams{ + UserID: userID, + Deleted: ptr.Of(false), + Query: ptr.Of("%testTitle%"), + Offset: 0, + Limit: 10, + }) + + suite.NoError(err) + suite.Len(rows, 1) + suite.Equal(documentID, rows[0].ID) + suite.Equal(documentTitle, *rows[0].Title) + suite.Equal(float64(42), rows[0].Percentage) + suite.Equal(int64(60), rows[0].TotalTimeSeconds) + + _, err = suite.dbm.Queries.DeleteDocument(context.Background(), documentID) + suite.NoError(err) + + rows, err = suite.dbm.Queries.GetDocumentsWithStats(context.Background(), GetDocumentsWithStatsParams{ + UserID: userID, + Deleted: ptr.Of(false), + Offset: 0, + Limit: 10, + }) + suite.NoError(err) + suite.Len(rows, 0) +} + +func (suite *DocumentsTestSuite) TestGetDocumentsWithStatsFilters() { + suite.createTestUserAndDevice() + + otherDocID := "testDocument2" + otherTitle := "otherTitle" + otherAuthor := "otherAuthor" + otherWords := int64(3000) + _, err := suite.dbm.Queries.UpsertDocument(context.Background(), UpsertDocumentParams{ + ID: otherDocID, + Title: &otherTitle, + Author: &otherAuthor, + Words: &otherWords, + }) + suite.Require().NoError(err) + + _, err = suite.dbm.Queries.AddActivity(context.Background(), AddActivityParams{ + DocumentID: documentID, + DeviceID: deviceID, + UserID: userID, + StartTime: time.Now().Add(-2 * time.Hour).UTC().Format(time.RFC3339), + Duration: 60, + StartPercentage: 0.10, + EndPercentage: 0.20, + }) + suite.Require().NoError(err) + + _, err = suite.dbm.Queries.UpdateProgress(context.Background(), UpdateProgressParams{ + UserID: userID, + DocumentID: documentID, + DeviceID: deviceID, + Percentage: 0.42, + Progress: "/6/2[test]", + }) + suite.Require().NoError(err) + + _, err = suite.dbm.Queries.AddActivity(context.Background(), AddActivityParams{ + DocumentID: otherDocID, + DeviceID: deviceID, + UserID: userID, + StartTime: time.Now().Add(-1 * time.Hour).UTC().Format(time.RFC3339), + Duration: 30, + StartPercentage: 0.20, + EndPercentage: 0.30, + }) + suite.Require().NoError(err) + + err = suite.dbm.CacheTempTables(context.Background()) + suite.Require().NoError(err) + + rows, err := suite.dbm.Queries.GetDocumentsWithStats(context.Background(), GetDocumentsWithStatsParams{ + UserID: userID, + ID: ptr.Of(documentID), + Offset: 0, + Limit: 10, + }) + suite.NoError(err) + suite.Len(rows, 1) + suite.Equal(documentID, rows[0].ID) + + rows, err = suite.dbm.Queries.GetDocumentsWithStats(context.Background(), GetDocumentsWithStatsParams{ + UserID: userID, + Query: ptr.Of("%otherAuthor%"), + Offset: 0, + Limit: 10, + }) + suite.NoError(err) + suite.Len(rows, 1) + suite.Equal(otherDocID, rows[0].ID) + + rows, err = suite.dbm.Queries.GetDocumentsWithStats(context.Background(), GetDocumentsWithStatsParams{ + UserID: userID, + Query: ptr.Of("%does-not-match%"), + Offset: 0, + Limit: 10, + }) + suite.NoError(err) + suite.Len(rows, 0) + + _, err = suite.dbm.Queries.DeleteDocument(context.Background(), otherDocID) + suite.Require().NoError(err) + + rows, err = suite.dbm.Queries.GetDocumentsWithStats(context.Background(), GetDocumentsWithStatsParams{ + UserID: userID, + Deleted: ptr.Of(true), + Offset: 0, + Limit: 10, + }) + suite.NoError(err) + suite.Len(rows, 1) + suite.Equal(otherDocID, rows[0].ID) + + rows, err = suite.dbm.Queries.GetDocumentsWithStats(context.Background(), GetDocumentsWithStatsParams{ + UserID: userID, + Offset: 0, + Limit: 10, + }) + suite.NoError(err) + suite.Len(rows, 2) + + rows, err = suite.dbm.Queries.GetDocumentsWithStats(context.Background(), GetDocumentsWithStatsParams{ + UserID: userID, + Offset: 0, + Limit: 1, + }) + suite.NoError(err) + suite.Len(rows, 1) + suite.Equal(otherDocID, rows[0].ID) + + rows, err = suite.dbm.Queries.GetDocumentsWithStats(context.Background(), GetDocumentsWithStatsParams{ + UserID: userID, + Offset: 1, + Limit: 1, + }) + suite.NoError(err) + suite.Len(rows, 1) + suite.Equal(documentID, rows[0].ID) +} + func (suite *DocumentsTestSuite) TestDeleteDocument() { changed, err := suite.dbm.Queries.DeleteDocument(context.Background(), documentID) suite.Nil(err, "should have nil err") @@ -85,7 +389,10 @@ func (suite *DocumentsTestSuite) TestGetDeletedDocuments() { // TODO - Convert GetWantedDocuments -> (sqlc.slice('document_ids')); func (suite *DocumentsTestSuite) TestGetWantedDocuments() { - wantedDocs, err := suite.dbm.Queries.GetWantedDocuments(context.Background(), fmt.Sprintf("[\"%s\"]", documentID)) + wantedDocs, err := suite.dbm.Queries.GetWantedDocuments(context.Background(), GetWantedDocumentsParams{ + JsonEach: fmt.Sprintf("[\"%s\"]", documentID), + DocumentIds: fmt.Sprintf("[\"%s\"]", documentID), + }) suite.Nil(err, "should have nil err") suite.Len(wantedDocs, 1, "should have one wanted document") } diff --git a/database/query.sql b/database/query.sql index 5bd28ce..eac98f8 100644 --- a/database/query.sql +++ b/database/query.sql @@ -202,6 +202,7 @@ SELECT END AS REAL), 2) AS percentage, CAST(CASE WHEN dus.total_time_seconds IS NULL THEN 0.0 + WHEN dus.read_percentage IS NULL OR dus.read_percentage <= 0 THEN 0.0 ELSE CAST(dus.total_time_seconds AS REAL) / (dus.read_percentage * 100.0) @@ -215,10 +216,10 @@ WHERE (docs.id = sqlc.narg('id') OR $id IS NULL) AND (docs.deleted = sqlc.narg(deleted) OR $deleted IS NULL) AND ( - ( + $query IS NULL OR ( docs.title LIKE sqlc.narg('query') OR - docs.author LIKE $query - ) OR $query IS NULL + docs.author LIKE sqlc.narg('query') + ) ) ORDER BY dus.last_read DESC, docs.created_at DESC LIMIT $limit diff --git a/database/query.sql.go b/database/query.sql.go index f5d612b..a5da03b 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -666,6 +666,7 @@ SELECT END AS REAL), 2) AS percentage, CAST(CASE WHEN dus.total_time_seconds IS NULL THEN 0.0 + WHEN dus.read_percentage IS NULL OR dus.read_percentage <= 0 THEN 0.0 ELSE CAST(dus.total_time_seconds AS REAL) / (dus.read_percentage * 100.0) @@ -679,10 +680,10 @@ WHERE (docs.id = ?2 OR ?2 IS NULL) AND (docs.deleted = ?3 OR ?3 IS NULL) AND ( - ( + ?4 IS NULL OR ( docs.title LIKE ?4 OR docs.author LIKE ?4 - ) OR ?4 IS NULL + ) ) ORDER BY dus.last_read DESC, docs.created_at DESC LIMIT ?6 @@ -690,12 +691,12 @@ OFFSET ?5 ` type GetDocumentsWithStatsParams struct { - UserID string `json:"user_id"` - ID *string `json:"id"` - Deleted *bool `json:"-"` - Query *string `json:"query"` - Offset int64 `json:"offset"` - Limit int64 `json:"limit"` + UserID string `json:"user_id"` + ID *string `json:"id"` + Deleted *bool `json:"-"` + Query interface{} `json:"query"` + Offset int64 `json:"offset"` + Limit int64 `json:"limit"` } type GetDocumentsWithStatsRow struct {