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 ? ( ) : ( - + )}
{expandPartsCreate ? ( @@ -222,17 +222,11 @@ SourcePartWrapper.defaultProps = { } const textClassBase = "ko-sans text-2xl" -const translationClassBase = "text-lg" -const SourceShowComponent: React.FC<{ readonly source: Source }> = ({ source }) => { +const SourceReadComponent: React.FC<{ readonly source: Source }> = ({ source }) => { return ( - {(tokenizedText) => ( - <> -
{tokenizedText.text}
-
{tokenizedText.translation}
- - )} + {(tokenizedText) =>
{tokenizedText.text}
}
) } @@ -313,7 +307,7 @@ const SourceNavComponent: React.FC<{ readonly source: Source; readonly safeSet: onCustomToken={onCustomToken} /> ) : null} -
+
{tokenizedText.translation}
{textFocused && selectedToken ? (