mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-02 11:31:57 +00:00
fix: fire resolvespec AfterRead/AfterCreate/AfterDelete, key restheadspec total cache by record id
This commit is contained in:
@@ -22,6 +22,7 @@ type expandOptionKey struct {
|
||||
// queryCacheKey represents the components used to build a cache key for query total count
|
||||
type queryCacheKey struct {
|
||||
TableName string `json:"table_name"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Filters []common.FilterOption `json:"filters"`
|
||||
Sort []common.SortOption `json:"sort"`
|
||||
CustomSQLWhere string `json:"custom_sql_where,omitempty"`
|
||||
@@ -39,12 +40,15 @@ type cachedTotal struct {
|
||||
}
|
||||
|
||||
// buildExtendedQueryCacheKey builds a cache key for extended query options (restheadspec)
|
||||
// Includes expand, distinct, and cursor pagination options
|
||||
func buildExtendedQueryCacheKey(tableName string, filters []common.FilterOption, sort []common.SortOption,
|
||||
// Includes expand, distinct, and cursor pagination options. id is the record id of a
|
||||
// single-record read: it constrains the counted query, so a count cached for one id
|
||||
// (or for the whole list) must never be served for another.
|
||||
func buildExtendedQueryCacheKey(tableName, id string, filters []common.FilterOption, sort []common.SortOption,
|
||||
customWhere, customOr string, customJoin []string, expandOpts []interface{}, distinct bool, cursorFwd, cursorBwd string) string {
|
||||
|
||||
key := queryCacheKey{
|
||||
TableName: tableName,
|
||||
ID: id,
|
||||
Filters: filters,
|
||||
Sort: sort,
|
||||
CustomSQLWhere: customWhere,
|
||||
@@ -77,8 +81,8 @@ func buildExtendedQueryCacheKey(tableName string, filters []common.FilterOption,
|
||||
jsonData, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
// Fallback to simple string concatenation if JSON fails
|
||||
return hashString(fmt.Sprintf("%s_%v_%v_%s_%s_%v_%v_%v_%s_%s",
|
||||
tableName, filters, sort, customWhere, customOr, customJoin, expandOpts, distinct, cursorFwd, cursorBwd))
|
||||
return hashString(fmt.Sprintf("%s_%s_%v_%v_%s_%s_%v_%v_%v_%s_%s",
|
||||
tableName, id, filters, sort, customWhere, customOr, customJoin, expandOpts, distinct, cursorFwd, cursorBwd))
|
||||
}
|
||||
|
||||
return hashString(string(jsonData))
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package restheadspec
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||
)
|
||||
|
||||
func TestQueryTotalCacheKeyIncludesRecordID(t *testing.T) {
|
||||
list := buildExtendedQueryCacheKey("items", "", nil, nil, "", "", nil, nil, false, "", "")
|
||||
one := buildExtendedQueryCacheKey("items", "7", nil, nil, "", "", nil, nil, false, "", "")
|
||||
other := buildExtendedQueryCacheKey("items", "8", nil, nil, "", "", nil, nil, false, "", "")
|
||||
if list == one || one == other {
|
||||
t.Fatal("list, id 7 and id 8 must not share a cache key")
|
||||
}
|
||||
if one != buildExtendedQueryCacheKey("items", "7", nil, nil, "", "", nil, nil, false, "", "") {
|
||||
t.Fatal("the key must be stable for the same query")
|
||||
}
|
||||
}
|
||||
|
||||
// A read by id counts 1 row; before the id was part of the key, that total was
|
||||
// cached under the list query's key and served as the list total for 2 minutes.
|
||||
func TestReadByIDDoesNotPoisonListTotal(t *testing.T) {
|
||||
resetTotalCache(t)
|
||||
h, mock := newBunHarness(t)
|
||||
cols := []string{"id", "name"}
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows(cols).AddRow(7, "a"))
|
||||
mock.ExpectCommit()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectCommit()
|
||||
// The list must run its own count query, not reuse the id read's total.
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(5))
|
||||
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows(cols).AddRow(7, "a").AddRow(8, "b"))
|
||||
mock.ExpectCommit()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectCommit()
|
||||
|
||||
read := func(id string) *httptest.ResponseRecorder {
|
||||
rec := httptest.NewRecorder()
|
||||
w, _ := common.WrapHTTPRequest(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
h.handleRead(itemCtx(t), w, id, ExtendedRequestOptions{})
|
||||
return rec
|
||||
}
|
||||
if rec := read("7"); rec.Code != http.StatusOK {
|
||||
t.Fatalf("read by id: status %d body %s", rec.Code, rec.Body)
|
||||
}
|
||||
if rec := read(""); rec.Code != http.StatusOK {
|
||||
t.Fatalf("list: status %d body %s", rec.Code, rec.Body)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("the list must run its own count query: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -824,6 +824,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
||||
|
||||
cacheKeyHash := buildExtendedQueryCacheKey(
|
||||
tableName,
|
||||
id,
|
||||
options.Filters,
|
||||
options.Sort,
|
||||
options.CustomSQLWhere,
|
||||
|
||||
Reference in New Issue
Block a user