diff --git a/db/pkg/db/custom.go b/db/pkg/db/custom.go
index 36a5398e..f738e8d6 100644
--- a/db/pkg/db/custom.go
+++ b/db/pkg/db/custom.go
@@ -143,6 +143,14 @@ func (q *Queries) ClearAll(ctx context.Context) error {
return nil
}
+// ClearAllTable clears data from a table
+func (q *Queries) ClearAllTable(ctx context.Context, tableName string) error {
+ if _, err := q.db.ExecContext(ctx, fmt.Sprintf("DELETE FROM %v; ", tableName)); err != nil {
+ return err
+ }
+ return nil
+}
+
var dbStorage storage.DBStorage
// SetDBStorage sets the storage.DBStorage used in model JSON marshall/unmarshall
diff --git a/db/pkg/db/source.go b/db/pkg/db/source.go
index ab4a15ec..469b0814 100644
--- a/db/pkg/db/source.go
+++ b/db/pkg/db/source.go
@@ -291,6 +291,9 @@ func (q *Queries) SourceStructuredIndex(ctx context.Context) ([]SourceStructured
return nil, err
}
+ if sources == nil {
+ return nil, nil
+ }
sourceStructureds := make([]SourceStructured, len(sources))
for i, source := range sources {
sourceStructureds[i] = source.ToSourceStructured()
diff --git a/db/pkg/db/source_test.go b/db/pkg/db/source_test.go
index a873edbf..3cafd4c7 100644
--- a/db/pkg/db/source_test.go
+++ b/db/pkg/db/source_test.go
@@ -234,3 +234,37 @@ func TestQueries_SourceUpdate(t *testing.T) {
testRecentTimestamps(t, source.UpdatedAt)
require.NotEqual(newSource.UpdatedAt, source.UpdatedAt)
}
+
+func TestQueries_SourceStructuredIndex(t *testing.T) {
+ testName := "TestQueries_SourceStructuredIndex"
+
+ testCases := []struct {
+ name string
+ }{
+ {name: "basic"},
+ {name: "clear"},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ require := require.New(t)
+
+ txQs := TxQsT(t, nil)
+ if tc.name == "clear" {
+ txQs = TxQsT(t, WriteOpts())
+ require.NoError(txQs.ClearAllTable(txQs.Ctx(), "sources"))
+ }
+
+ sourceStructureds, err := txQs.SourceStructuredIndex(txQs.Ctx())
+ require.NoError(err)
+
+ var staticCopy []SourceStructured
+ if len(sourceStructureds) > 0 {
+ staticCopy = make([]SourceStructured, len(sourceStructureds))
+ for i := range sourceStructureds {
+ staticCopy[i] = sourceStructureds[i].StaticCopy()
+ }
+ }
+ fixture.CompareReadOrUpdateJSON(t, path.Join(testName, tc.name), staticCopy)
+ })
+ }
+}
diff --git a/db/pkg/db/testdata/TestQueries_SourceStructuredIndex/basic.json b/db/pkg/db/testdata/TestQueries_SourceStructuredIndex/basic.json
new file mode 100644
index 00000000..b29584d1
--- /dev/null
+++ b/db/pkg/db/testdata/TestQueries_SourceStructuredIndex/basic.json
@@ -0,0 +1,233 @@
+[
+ {
+ "name": "split",
+ "reference": "split.txt",
+ "parts": [
+ {
+ "tokenized_texts": [
+ {
+ "text": "내가 가는 이길이",
+ "translation": "The road that I’m taking",
+ "tokens": [
+ {
+ "text": "내",
+ "part_of_speech": "Pronoun",
+ "start_index": 0,
+ "length": 1
+ },
+ {
+ "text": "가",
+ "part_of_speech": "Postposition",
+ "start_index": 1,
+ "length": 1
+ },
+ {
+ "text": "가",
+ "part_of_speech": "Verb",
+ "start_index": 3,
+ "length": 1
+ },
+ {
+ "text": "는",
+ "part_of_speech": "Ending",
+ "start_index": 4,
+ "length": 1
+ },
+ {
+ "text": "이길",
+ "part_of_speech": "Noun",
+ "start_index": 6,
+ "length": 2
+ },
+ {
+ "text": "이",
+ "part_of_speech": "Postposition",
+ "start_index": 8,
+ "length": 1
+ }
+ ]
+ },
+ {
+ "text": "어디로 가는지",
+ "translation": "Where it’s leading me to, where it’s taking me",
+ "tokens": [
+ {
+ "text": "어디",
+ "part_of_speech": "Pronoun",
+ "start_index": 0,
+ "length": 2
+ },
+ {
+ "text": "로",
+ "part_of_speech": "Postposition",
+ "start_index": 2,
+ "length": 1
+ },
+ {
+ "text": "가",
+ "part_of_speech": "Verb",
+ "start_index": 4,
+ "length": 1
+ },
+ {
+ "text": "는지",
+ "part_of_speech": "Ending",
+ "start_index": 5,
+ "length": 2
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "updated_at": "0001-01-01T00:00:00Z",
+ "created_at": "0001-01-01T00:00:00Z"
+ },
+ {
+ "name": "weave",
+ "reference": "weave.txt",
+ "parts": [
+ {
+ "media": {
+ "image_key": "testdb.SourcePartMediaImageKey.png"
+ },
+ "tokenized_texts": [
+ {
+ "text": "이것은 샘플 파일입니다. tmp/in.txt에 자신의 텍스트를 입력합니다.",
+ "translation": "This is a sample file. Put your own text at: tmp/in.txt.",
+ "tokens": [
+ {
+ "text": "이것",
+ "part_of_speech": "Pronoun",
+ "start_index": 0,
+ "length": 2
+ },
+ {
+ "text": "은",
+ "part_of_speech": "Postposition",
+ "start_index": 2,
+ "length": 1
+ },
+ {
+ "text": "샘플",
+ "part_of_speech": "Noun",
+ "start_index": 4,
+ "length": 2
+ },
+ {
+ "text": "파일",
+ "part_of_speech": "Noun",
+ "start_index": 7,
+ "length": 2
+ },
+ {
+ "text": "이",
+ "part_of_speech": "Copula",
+ "start_index": 9,
+ "length": 1
+ },
+ {
+ "text": "ㅂ니다",
+ "part_of_speech": "Ending",
+ "start_index": 10,
+ "length": 3
+ },
+ {
+ "text": ".",
+ "part_of_speech": "Punctuation",
+ "start_index": 13,
+ "length": 1
+ },
+ {
+ "text": "tmp",
+ "part_of_speech": "OtherLanguage",
+ "start_index": 15,
+ "length": 3
+ },
+ {
+ "text": "/",
+ "part_of_speech": "Punctuation",
+ "start_index": 18,
+ "length": 1
+ },
+ {
+ "text": "in",
+ "part_of_speech": "OtherLanguage",
+ "start_index": 19,
+ "length": 2
+ },
+ {
+ "text": ".",
+ "part_of_speech": "Punctuation",
+ "start_index": 21,
+ "length": 1
+ },
+ {
+ "text": "txt",
+ "part_of_speech": "OtherLanguage",
+ "start_index": 22,
+ "length": 3
+ },
+ {
+ "text": "에",
+ "part_of_speech": "Postposition",
+ "start_index": 25,
+ "length": 1
+ },
+ {
+ "text": "자신",
+ "part_of_speech": "Noun",
+ "start_index": 27,
+ "length": 2
+ },
+ {
+ "text": "의",
+ "part_of_speech": "Postposition",
+ "start_index": 29,
+ "length": 1
+ },
+ {
+ "text": "텍스트",
+ "part_of_speech": "Noun",
+ "start_index": 31,
+ "length": 3
+ },
+ {
+ "text": "를",
+ "part_of_speech": "Postposition",
+ "start_index": 34,
+ "length": 1
+ },
+ {
+ "text": "입력",
+ "part_of_speech": "Noun",
+ "start_index": 36,
+ "length": 2
+ },
+ {
+ "text": "하",
+ "part_of_speech": "Suffix",
+ "start_index": 38,
+ "length": 1
+ },
+ {
+ "text": "ㅂ니다",
+ "part_of_speech": "Ending",
+ "start_index": 39,
+ "length": 3
+ },
+ {
+ "text": ".",
+ "part_of_speech": "Punctuation",
+ "start_index": 42,
+ "length": 1
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "updated_at": "0001-01-01T00:00:00Z",
+ "created_at": "0001-01-01T00:00:00Z"
+ }
+]
\ No newline at end of file
diff --git a/db/pkg/db/testdata/TestQueries_SourceStructuredIndex/clear.json b/db/pkg/db/testdata/TestQueries_SourceStructuredIndex/clear.json
new file mode 100644
index 00000000..ec747fa4
--- /dev/null
+++ b/db/pkg/db/testdata/TestQueries_SourceStructuredIndex/clear.json
@@ -0,0 +1 @@
+null
\ No newline at end of file
diff --git a/pkg/api/apihelpers_test.go b/pkg/api/apihelpers_test.go
index 6d551ae6..5ed80283 100644
--- a/pkg/api/apihelpers_test.go
+++ b/pkg/api/apihelpers_test.go
@@ -10,6 +10,8 @@ import (
"strings"
"testing"
+ "github.com/stretchr/testify/require"
+
"github.com/s12chung/text2anki/db/pkg/db"
"github.com/s12chung/text2anki/db/pkg/db/testdb"
"github.com/s12chung/text2anki/pkg/api/config"
@@ -22,6 +24,28 @@ func joinPath(elem ...any) string {
return fmt.Sprintf(strings.Repeat("/%v", len(elem)), elem...)
}
+func testIndex[T test.StaticCopyable[T]](t *testing.T, s txServer, testName, tableName string) {
+ testCases := []struct {
+ name string
+ }{
+ {name: "basic"},
+ {name: "clear"},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ require := require.New(t)
+
+ txQs := testdb.TxQs(t, nil)
+ if tc.name == "clear" {
+ txQs = testdb.TxQs(t, db.WriteOpts())
+ require.NoError(txQs.ClearAllTable(txQs.Ctx(), tableName))
+ }
+ resp := test.HTTPDo(t, s.NewTxRequestWithMode(t, txQs, txReadOnly, http.MethodGet, "", nil))
+ testModelsResponse[T](t, resp, testName, tc.name, nil)
+ })
+ }
+}
+
func testIndent(t *testing.T, resp test.Response, testName, name string) {
jsonBody := test.IndentJSON(t, resp.Body.Bytes())
fixture.CompareReadOrUpdate(t, fixtureFileName(testName, name), jsonBody)
@@ -53,13 +77,12 @@ type txServer struct {
}
func (s txServer) NewRequest(t *testing.T, method, path string, body io.Reader) *http.Request {
- return s.newTxRequest(t, testdb.TxQs(t, nil), txReadOnly, method, path, body)
+ return s.NewTxRequestWithMode(t, testdb.TxQs(t, nil), txReadOnly, method, path, body)
}
func (s txServer) NewTxRequest(t *testing.T, tx db.TxQs, method, path string, body io.Reader) *http.Request {
- return s.newTxRequest(t, tx, txWritable, method, path, body)
+ return s.NewTxRequestWithMode(t, tx, txWritable, method, path, body)
}
-
-func (s txServer) newTxRequest(t *testing.T, tx db.TxQs, mode config.TxMode, method, path string, body io.Reader) *http.Request {
+func (s txServer) NewTxRequestWithMode(t *testing.T, tx db.TxQs, mode config.TxMode, method, path string, body io.Reader) *http.Request {
req := s.Server.NewRequest(t, tx.Ctx(), method, path, body)
s.pool.SetTx(t, req, tx, mode)
return req
diff --git a/pkg/api/notes.go b/pkg/api/notes.go
index 929e204b..62b22e57 100644
--- a/pkg/api/notes.go
+++ b/pkg/api/notes.go
@@ -23,7 +23,7 @@ func init() {
// NotesIndex shows lists all the notes
func (rs Routes) NotesIndex(_ *http.Request, txQs db.TxQs) (any, *jhttp.HTTPError) {
- return jhttp.ReturnModelOr500(func() (any, error) {
+ return jhttp.ReturnSliceOr500(func() ([]db.Note, error) {
return txQs.NotesIndex(txQs.Ctx())
})
}
diff --git a/pkg/api/notes_test.go b/pkg/api/notes_test.go
index 5f0e94b4..f942db13 100644
--- a/pkg/api/notes_test.go
+++ b/pkg/api/notes_test.go
@@ -24,8 +24,7 @@ func init() {
func TestRoutes_NotesIndex(t *testing.T) {
testName := "TestRoutes_NotesIndex"
- resp := test.HTTPDo(t, notesServer.NewRequest(t, http.MethodGet, "", nil))
- testModelsResponse[db.Note](t, resp, testName, "", nil)
+ testIndex[db.Note](t, notesServer, testName, "notes")
}
func TestRoutes_NoteCreate(t *testing.T) {
diff --git a/pkg/api/sources.go b/pkg/api/sources.go
index 78117354..e7a34a67 100644
--- a/pkg/api/sources.go
+++ b/pkg/api/sources.go
@@ -20,7 +20,7 @@ func init() {
// SourcesIndex returns a list of sources
func (rs Routes) SourcesIndex(_ *http.Request, txQs db.TxQs) (any, *jhttp.HTTPError) {
- return jhttp.ReturnModelOr500(func() (any, error) {
+ return jhttp.ReturnSliceOr500(func() ([]db.SourceStructured, error) {
return txQs.SourceStructuredIndex(txQs.Ctx())
})
}
diff --git a/pkg/api/sources_test.go b/pkg/api/sources_test.go
index ef6907c9..87af4e82 100644
--- a/pkg/api/sources_test.go
+++ b/pkg/api/sources_test.go
@@ -31,8 +31,7 @@ func createdSource(t *testing.T, txQs db.TxQs) db.Source {
func TestRoutes_SourcesIndex(t *testing.T) {
testName := "TestRoutes_SourcesIndex"
- resp := test.HTTPDo(t, sourcesServer.NewRequest(t, http.MethodGet, "", nil))
- testModelsResponse[db.SourceStructured](t, resp, testName, "", nil)
+ testIndex[db.SourceStructured](t, sourcesServer, testName, "sources")
}
func TestRoutes_SourceGet(t *testing.T) {
diff --git a/pkg/api/testdata/TestRoutes_NotesIndex.json b/pkg/api/testdata/TestRoutes_NotesIndex/basic_response.json
similarity index 100%
rename from pkg/api/testdata/TestRoutes_NotesIndex.json
rename to pkg/api/testdata/TestRoutes_NotesIndex/basic_response.json
diff --git a/pkg/api/testdata/TestRoutes_NotesIndex/clear_response.json b/pkg/api/testdata/TestRoutes_NotesIndex/clear_response.json
new file mode 100644
index 00000000..0637a088
--- /dev/null
+++ b/pkg/api/testdata/TestRoutes_NotesIndex/clear_response.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/pkg/api/testdata/TestRoutes_SourcesIndex.json b/pkg/api/testdata/TestRoutes_SourcesIndex/basic_response.json
similarity index 100%
rename from pkg/api/testdata/TestRoutes_SourcesIndex.json
rename to pkg/api/testdata/TestRoutes_SourcesIndex/basic_response.json
diff --git a/pkg/api/testdata/TestRoutes_SourcesIndex/clear_response.json b/pkg/api/testdata/TestRoutes_SourcesIndex/clear_response.json
new file mode 100644
index 00000000..0637a088
--- /dev/null
+++ b/pkg/api/testdata/TestRoutes_SourcesIndex/clear_response.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/pkg/util/jhttp/jhttp.go b/pkg/util/jhttp/jhttp.go
index d305e4fb..bcedfe19 100644
--- a/pkg/util/jhttp/jhttp.go
+++ b/pkg/util/jhttp/jhttp.go
@@ -142,3 +142,14 @@ func ReturnModelOr500(modelFunc func() (any, error)) (any, *HTTPError) {
}
return model, nil
}
+
+// ReturnSliceOr500 runs the sliceFunc, and returns http.StatusInternalServerError for the error
+func ReturnSliceOr500[T any](sliceFunc func() ([]T, error)) (any, *HTTPError) {
+ return ReturnModelOr500(func() (any, error) {
+ slice, err := sliceFunc()
+ if slice == nil {
+ return make([]T, 0), err
+ }
+ return slice, err
+ })
+}
diff --git a/pkg/util/jhttp/jhttp_test.go b/pkg/util/jhttp/jhttp_test.go
index 4ec988a2..a2acb76d 100644
--- a/pkg/util/jhttp/jhttp_test.go
+++ b/pkg/util/jhttp/jhttp_test.go
@@ -205,3 +205,28 @@ func TestReturnModelOr500(t *testing.T) {
})
}
}
+
+func TestReturnSliceOr500(t *testing.T) {
+ testCases := []struct {
+ name string
+ model []any
+ err error
+
+ expectedModel any
+ expectedErr *HTTPError
+ }{
+ {name: "nil", model: nil, expectedModel: []any{}},
+ {name: "empty", model: []any{}, expectedModel: []any{}},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ require := require.New(t)
+ model, err := ReturnSliceOr500(func() ([]any, error) {
+ return tc.model, tc.err
+ })
+ require.Equal(tc.expectedModel, model)
+ require.Equal(tc.expectedErr, err)
+ })
+ }
+}
diff --git a/pkg/util/jhttp/reqtx/reqtxtest/reqtxtest.go b/pkg/util/jhttp/reqtx/reqtxtest/reqtxtest.go
index 442f2e2d..891d9264 100644
--- a/pkg/util/jhttp/reqtx/reqtxtest/reqtxtest.go
+++ b/pkg/util/jhttp/reqtx/reqtxtest/reqtxtest.go
@@ -60,7 +60,7 @@ func (p Pool[T, Mode]) GetTx(r *http.Request, mode Mode) (T, error) {
return empty, fmt.Errorf("transaction with id, %v, does not exist", id)
}
if val.Mode != mode {
- return empty, fmt.Errorf("stored Tx mode (%v) is not matching passed mode (%v)", val.Mode, mode)
+ return empty, fmt.Errorf("stored Tx mode from request (%v) is not matching mode set by router (%v)", val.Mode, mode)
}
return val.Tx, nil
}
diff --git a/pkg/util/jhttp/reqtx/reqtxtest/reqtxtest_test.go b/pkg/util/jhttp/reqtx/reqtxtest/reqtxtest_test.go
index a9934486..e5516d78 100644
--- a/pkg/util/jhttp/reqtx/reqtxtest/reqtxtest_test.go
+++ b/pkg/util/jhttp/reqtx/reqtxtest/reqtxtest_test.go
@@ -32,7 +32,7 @@ func TestPool_SetTxGetTx(t *testing.T) {
}{
{name: "normal", mode: mode},
{name: "diff_request", req: newRequest(), mode: mode, err: errors.New("transaction with id, , does not exist")},
- {name: "diff_mode", mode: -9, err: errors.New("stored Tx mode (1) is not matching passed mode (-9)")},
+ {name: "diff_mode", mode: -9, err: errors.New("stored Tx mode from request (1) is not matching mode set by router (-9)")},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
diff --git a/pkg/util/test/data.go b/pkg/util/test/data.go
index 9d865ea2..2a1d611a 100644
--- a/pkg/util/test/data.go
+++ b/pkg/util/test/data.go
@@ -87,9 +87,12 @@ func StaticCopySlice[T StaticCopyable[T]](t *testing.T, b []byte, models *[]T) [
}
Unmarshall(t, b, models)
- staticCopies := make([]any, len(*models))
- for i, model := range *models {
- staticCopies[i] = model.StaticCopy()
+ var staticCopies []any
+ if *models != nil {
+ staticCopies = make([]any, len(*models))
+ for i, model := range *models {
+ staticCopies[i] = model.StaticCopy()
+ }
}
return JSON(t, staticCopies)
}
diff --git a/ui/src/components/sources/SourceShow.tsx b/ui/src/components/sources/SourceShow.tsx
index c5d987d4..61425935 100644
--- a/ui/src/components/sources/SourceShow.tsx
+++ b/ui/src/components/sources/SourceShow.tsx
@@ -86,7 +86,7 @@ const SourceComponent: React.FC<{ readonly source: Source }> = ({ source }) => {
{nav ? (