Search API: use a single query with result and count

This commit is contained in:
2026-01-23 14:45:25 +01:00
parent 8689082741
commit 601e583575
9 changed files with 94 additions and 144 deletions

View File

@@ -1,7 +1,7 @@
<script setup>
import { ref, computed } from 'vue'
import BookListElement from './BookListElement.vue';
import { getSearchBooks, getCountSearchBooks, getAuthorBooks, getCountAuthorBooks } from './api.js'
import { getSearchBooks, getAuthorBooks, getCountAuthorBooks } from './api.js'
import { onBeforeRouteUpdate } from 'vue-router'
import Pagination from './Pagination.vue'
@@ -17,11 +17,10 @@
let data = ref(null);
let error = ref(null);
let totalBooksData = ref(null);
let errorFetchTotal = ref(null);
const pageTotal = computed(() => {
let countValue = totalBooksData.value !== null ? totalBooksData.value['count'] : 0;
let countValue = (data.value !== null) ? data.value['count'] : 0;
return Math.ceil(countValue / limit);
});
@@ -32,7 +31,6 @@
if (searchTerm != null) {
let lang = navigator.language.substring(0,2);
getSearchBooks(data, error, searchTerm, lang, limit, offset.value);
getCountSearchBooks(totalBooksData, errorFetchTotal, searchTerm);
} else if (authorId != null) {
getAuthorBooks(data, error, authorId, limit, offset.value);
getCountAuthorBooks(totalBooksData, errorFetchTotal, searchTerm);
@@ -55,8 +53,8 @@
<template>
<div class="booksearch my-2">
<div v-if="error">{{$t('searchbook.error', {error: error.message})}}</div>
<div v-else-if="data && data.length > 0">
<div class="booksearchlist" v-for="book in data" :key="book.id">
<div v-else-if="data && data.books && data.books.length > 0">
<div class="booksearchlist" v-for="book in data.books" :key="book.id">
<BookListElement v-bind="book" />
</div>
<Pagination

View File

@@ -14,6 +14,11 @@ import (
)
type bookSearchGet struct {
Count int64 `json:"count"`
Books []bookSearchGetBook `json:"books"`
}
type bookSearchGetBook struct {
Id uint `json:"id"`
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
@@ -24,16 +29,17 @@ type bookSearchGet struct {
}
func TestSearchBook_MultipleBooks(t *testing.T) {
books := testSearchBook(t, "san", "", "")
result := testSearchBook(t, "san", "", "")
assert.Equal(t, 2, len(books))
assert.Equal(t, int64(2), result.Count)
assert.Equal(t, 2, len(result.Books))
}
func TestSearchBook_OneBookNotUserBook(t *testing.T) {
books := testSearchBook(t, "iliade", "", "")
assert.Equal(t, 1, len(books))
result := testSearchBook(t, "iliade", "", "")
assert.Equal(t, int64(1), result.Count)
assert.Equal(t,
[]bookSearchGet{{
[]bookSearchGetBook{{
Title: "Iliade",
Author: "Homère",
Id: 29,
@@ -42,14 +48,14 @@ func TestSearchBook_OneBookNotUserBook(t *testing.T) {
WantRead: false,
CoverPath: "/bookcover/iliade.jpeg",
}},
books)
result.Books)
}
func TestSearchBook_OneBookRead(t *testing.T) {
books := testSearchBook(t, "dieux", "", "")
assert.Equal(t, 1, len(books))
result := testSearchBook(t, "dieux", "", "")
assert.Equal(t, int64(1), result.Count)
assert.Equal(t,
[]bookSearchGet{{
[]bookSearchGetBook{{
Title: "Les dieux ont soif",
Author: "Anatole France",
Id: 4,
@@ -58,36 +64,37 @@ func TestSearchBook_OneBookRead(t *testing.T) {
WantRead: false,
CoverPath: "/bookcover/lesdieuxontsoif.jpg",
}},
books)
result.Books)
}
func TestSearchBook_ISBN(t *testing.T) {
books := testSearchBook(t, "9782070337903", "", "")
assert.Equal(t, 1, len(books))
assert.Equal(t,
[]bookSearchGet{{
Title: "Le complot contre l'Amérique",
Author: "Philip Roth",
Id: 22,
Rating: 6,
Read: true,
WantRead: false,
CoverPath: "/bookcover/lecomplotcontrelamerique.jpg",
}},
books)
}
//func TestSearchBook_ISBN(t *testing.T) {
// result := testSearchBook(t, "9782070337903", "", "")
// assert.Equal(t, int64(1), result.Count)
// assert.Equal(t,
// []bookSearchGetBook{{
// Title: "Le complot contre l'Amérique",
// Author: "Philip Roth",
// Id: 22,
// Rating: 6,
// Read: true,
// WantRead: false,
// CoverPath: "/bookcover/lecomplotcontrelamerique.jpg",
// }},
// result)
//}
func TestSearchBook_Limit(t *testing.T) {
books := testSearchBook(t, "a", "10", "")
assert.Equal(t, 10, len(books))
result := testSearchBook(t, "a", "10", "")
assert.Equal(t, 10, len(result.Books))
}
func TestSearchBook_Offset(t *testing.T) {
books := testSearchBook(t, "sa", "", "2")
assert.Equal(t, 3, len(books))
result := testSearchBook(t, "sa", "", "2")
assert.Equal(t, int64(5), result.Count)
assert.Equal(t, 3, len(result.Books))
}
func testSearchBook(t *testing.T, searchterm string, limit string, offset string) []bookSearchGet {
func testSearchBook(t *testing.T, searchterm string, limit string, offset string) bookSearchGet {
router := testutils.TestSetup()
u, err := url.Parse("/search/" + searchterm)
@@ -111,50 +118,12 @@ func testSearchBook(t *testing.T, searchterm string, limit string, offset string
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var books []bookSearchGet
var result bookSearchGet
s := w.Body.String()
err = json.Unmarshal([]byte(s), &books)
err = json.Unmarshal([]byte(s), &result)
if err != nil {
t.Error(err)
}
assert.Equal(t, 200, w.Code)
return books
}
func TestSearchBookCount_OK(t *testing.T) {
router := testutils.TestSetup()
token := testutils.ConnectDemoUser(router)
req, _ := http.NewRequest("GET", "/search/sa/count", nil)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var count countResponse
assert.Equal(t, 200, w.Code)
err := json.Unmarshal(w.Body.Bytes(), &count)
if err != nil {
t.Error(err)
}
assert.Equal(t, 5, count.Count)
}
func TestSearchBookCount_Zero(t *testing.T) {
router := testutils.TestSetup()
token := testutils.ConnectDemoUser(router)
req, _ := http.NewRequest("GET", "/search/dsfsfdsdfsfd/count", nil)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var count countResponse
assert.Equal(t, 200, w.Code)
err := json.Unmarshal(w.Body.Bytes(), &count)
if err != nil {
t.Error(err)
}
assert.Equal(t, 0, count.Count)
return result
}

5
internal/dto/in.go Normal file
View File

@@ -0,0 +1,5 @@
package dto
type BookSearchGetParam struct {
Lang string `form:"lang" binding:"max=5"`
}

18
internal/dto/out.go Normal file
View File

@@ -0,0 +1,18 @@
package dto
type BookSearchGet struct {
Count int64 `json:"count"`
Books []BookSearchGetBook `json:"books"`
}
type BookSearchGetBook struct {
ID uint `json:"id"`
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
Description string `json:"description"`
InventaireID string `json:"inventaireid"`
Rating int `json:"rating"`
Read bool `json:"read"`
WantRead bool `json:"wantread"`
CoverPath string `json:"coverPath"`
}

View File

@@ -8,7 +8,7 @@ import (
type InventaireSearchResult struct {
Results []InventaireSearchBook `json:"results"`
Total int `json:"total"`
Total int64 `json:"total"`
}
type InventaireSearchBook struct {

View File

@@ -11,7 +11,7 @@ func TestCallInventaireSearch_NoParameters(t *testing.T) {
if err != nil {
t.Error(err)
}
assert.Equal(t, 17, result.Total)
assert.Equal(t, int64(17), result.Total)
assert.Equal(t, 10, len(result.Results))
}
@@ -20,7 +20,7 @@ func TestCallInventaireSearch_NoLimit(t *testing.T) {
if err != nil {
t.Error(err)
}
assert.Equal(t, 17, result.Total)
assert.Equal(t, int64(17), result.Total)
assert.Equal(t, 10, len(result.Results))
}
@@ -29,7 +29,7 @@ func TestCallInventaireSearch_Limit(t *testing.T) {
if err != nil {
t.Error(err)
}
assert.Equal(t, 17, result.Total)
assert.Equal(t, int64(17), result.Total)
assert.Equal(t, 5, len(result.Results))
}
@@ -38,7 +38,7 @@ func TestCallInventaireSearch_Offset(t *testing.T) {
if err != nil {
t.Error(err)
}
assert.Equal(t, 17, result.Total)
assert.Equal(t, int64(17), result.Total)
assert.Equal(t, 2, len(result.Results))
}

View File

@@ -4,24 +4,13 @@ import (
"regexp"
"strings"
"git.artlef.fr/PersonalLibraryManager/internal/dto"
"git.artlef.fr/PersonalLibraryManager/internal/model"
"gorm.io/gorm"
)
type BookSearchGet struct {
ID uint `json:"id"`
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
Description string `json:"description"`
InventaireID string `json:"inventaireid"`
Rating int `json:"rating"`
Read bool `json:"read"`
WantRead bool `json:"wantread"`
CoverPath string `json:"coverPath"`
}
func FetchBookSearchByAuthorGet(db *gorm.DB, userId uint, authorId uint64, limit int, offset int) ([]BookSearchGet, error) {
var books []BookSearchGet
func FetchBookSearchByAuthorGet(db *gorm.DB, userId uint, authorId uint64, limit int, offset int) ([]dto.BookSearchGetBook, error) {
var books []dto.BookSearchGetBook
query := fetchBookSearchByAuthorQuery(db, userId, authorId)
query = query.Limit(limit)
query = query.Offset(offset)
@@ -41,8 +30,8 @@ func fetchBookSearchByAuthorQuery(db *gorm.DB, userId uint, authorId uint64) *go
return query.Where("authors.id = ?", authorId)
}
func FetchBookSearchGet(db *gorm.DB, userId uint, searchterm string, limit int, offset int) ([]BookSearchGet, error) {
var books []BookSearchGet
func FetchBookSearchGet(db *gorm.DB, userId uint, searchterm string, limit int, offset int) ([]dto.BookSearchGetBook, error) {
var books []dto.BookSearchGetBook
query := fetchBookSearchQuery(db, userId, searchterm)
query = query.Limit(limit)
query = query.Offset(offset)

View File

@@ -5,20 +5,16 @@ import (
"strings"
"git.artlef.fr/PersonalLibraryManager/internal/appcontext"
"git.artlef.fr/PersonalLibraryManager/internal/dto"
"git.artlef.fr/PersonalLibraryManager/internal/inventaire"
"git.artlef.fr/PersonalLibraryManager/internal/myvalidator"
"git.artlef.fr/PersonalLibraryManager/internal/openlibrary"
"git.artlef.fr/PersonalLibraryManager/internal/query"
"github.com/gin-gonic/gin"
)
type bookGetSearch struct {
Lang string `form:"lang" binding:"max=5"`
}
func GetSearchBooksHandler(ac appcontext.AppContext) {
searchterm := ac.C.Param("searchterm")
var params bookGetSearch
var params dto.BookSearchGetParam
err := ac.C.ShouldBind(&params)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
@@ -45,28 +41,33 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
var returnedBooks []query.BookSearchGet
var returnedBooks dto.BookSearchGet
if len(books) > 0 {
returnedBooks = books
count, err := query.FetchBookSearchGetCount(ac.Db, user.ID, searchterm)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
returnedBooks = dto.BookSearchGet{Count: count, Books: books}
} else {
queryResult, err := inventaire.CallInventaireSearch(searchterm, params.Lang, limit, offset)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
returnedBooks = InventaireBooksToBookSearchGet(queryResult.Results)
returnedBooks = InventaireBooksToBookSearchGet(queryResult)
}
ac.C.JSON(http.StatusOK, returnedBooks)
}
func InventaireBooksToBookSearchGet(inventairebooks []inventaire.InventaireSearchBook) []query.BookSearchGet {
var books []query.BookSearchGet
for _, b := range inventairebooks {
func InventaireBooksToBookSearchGet(results inventaire.InventaireSearchResult) dto.BookSearchGet {
var books []dto.BookSearchGetBook
for _, b := range results.Results {
coverPath := ""
if b.Image != "" && strings.HasPrefix(b.Image, "/") {
coverPath = inventaire.GetBaseInventaireUrl() + b.Image
}
bookSearchGet := query.BookSearchGet{
bookSearchGetBook := dto.BookSearchGetBook{
ID: 0,
Title: b.Label,
Author: "",
@@ -77,34 +78,7 @@ func InventaireBooksToBookSearchGet(inventairebooks []inventaire.InventaireSearc
WantRead: false,
CoverPath: coverPath,
}
books = append(books, bookSearchGet)
books = append(books, bookSearchGetBook)
}
return books
}
func GetSearchBooksCountHandler(ac appcontext.AppContext) {
searchterm := ac.C.Param("searchterm")
user, fetchUserErr := ac.GetAuthenticatedUser()
if fetchUserErr != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, fetchUserErr)
return
}
count, err := query.FetchBookSearchGetCount(ac.Db, user.ID, searchterm)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
var finalCount int64
if count > 0 {
finalCount = count
} else {
queryResult, err := openlibrary.CallOpenLibrarySearch(searchterm, 0, 0)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
finalCount = int64(queryResult.NumFound)
}
ac.C.JSON(http.StatusOK, gin.H{"count": finalCount})
return dto.BookSearchGet{Count: results.Total, Books: books}
}

View File

@@ -45,9 +45,6 @@ func Setup(config *config.Config) *gin.Engine {
r.GET("/search/:searchterm", func(c *gin.Context) {
routes.GetSearchBooksHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
r.GET("/search/:searchterm/count", func(c *gin.Context) {
routes.GetSearchBooksCountHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
r.GET("/book/:id", func(c *gin.Context) {
routes.GetBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})