Merge fix/empty-list-null: empty lists return [] not null

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMHQTtnQtQqL8muAXHr9kd
This commit is contained in:
2026-07-02 09:04:33 +07:00
6 changed files with 44 additions and 6 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ func (s *Store) ListAccountsByTask(ctx context.Context, taskID int64) ([]Account
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var out []Account out := []Account{}
for rows.Next() { for rows.Next() {
var a Account var a Account
if err := rows.Scan(&a.ID, &a.TaskID, &a.SrcLogin, &a.SrcPassEnc, &a.DstLogin, &a.DstPassEnc, if err := rows.Scan(&a.ID, &a.TaskID, &a.SrcLogin, &a.SrcPassEnc, &a.DstLogin, &a.DstPassEnc,
+38
View File
@@ -0,0 +1,38 @@
package store
import (
"context"
"testing"
)
// Empty lists must serialize as JSON [] (non-nil slice), not null,
// otherwise the frontend crashes on `.length`/`.map`. Regression for
// "Uncaught TypeError: can't access property length, n is null".
func TestListsNeverNilWhenEmpty(t *testing.T) {
s := testStore(t)
ctx := context.Background()
eps, err := s.ListEndpoints(ctx)
if err != nil {
t.Fatalf("ListEndpoints: %v", err)
}
if eps == nil {
t.Fatal("ListEndpoints returned nil slice on empty DB (would serialize as null)")
}
tasks, err := s.ListTasks(ctx)
if err != nil {
t.Fatalf("ListTasks: %v", err)
}
if tasks == nil {
t.Fatal("ListTasks returned nil slice on empty DB (would serialize as null)")
}
accs, err := s.ListAccountsByTask(ctx, 999999)
if err != nil {
t.Fatalf("ListAccountsByTask: %v", err)
}
if accs == nil {
t.Fatal("ListAccountsByTask returned nil slice for no rows (would serialize as null)")
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ func (s *Store) ListEndpoints(ctx context.Context) ([]Endpoint, error) {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var out []Endpoint out := []Endpoint{}
for rows.Next() { for rows.Next() {
var e Endpoint var e Endpoint
if err := rows.Scan(&e.ID, &e.RoleLabel, &e.Host, &e.Port, &e.TLSMode); err != nil { if err := rows.Scan(&e.ID, &e.RoleLabel, &e.Host, &e.Port, &e.TLSMode); err != nil {
+1 -1
View File
@@ -40,7 +40,7 @@ func (s *Store) ListTasks(ctx context.Context) ([]Task, error) {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var out []Task out := []Task{}
for rows.Next() { for rows.Next() {
var t Task var t Task
if err := rows.Scan(&t.ID, &t.Name, &t.SrcEndpointID, &t.DstEndpointID, &t.Status, &t.FolderMapping); err != nil { if err := rows.Scan(&t.ID, &t.Name, &t.SrcEndpointID, &t.DstEndpointID, &t.Status, &t.FolderMapping); err != nil {
+1 -1
View File
@@ -11,7 +11,7 @@ export function Endpoints() {
function reload() { function reload() {
listEndpoints() listEndpoints()
.then(setEndpoints) .then((e) => setEndpoints(e ?? []))
.catch((e) => setError(String(e.message || e))) .catch((e) => setError(String(e.message || e)))
} }
+2 -2
View File
@@ -13,13 +13,13 @@ export function Tasks() {
function reload() { function reload() {
listTasks() listTasks()
.then(setTasks) .then((t) => setTasks(t ?? []))
.catch((e: unknown) => setError(e instanceof Error ? e.message : 'Failed to load tasks')) .catch((e: unknown) => setError(e instanceof Error ? e.message : 'Failed to load tasks'))
} }
useEffect(() => { useEffect(() => {
reload() reload()
listEndpoints().then(setEndpoints).catch(() => {}) listEndpoints().then((e) => setEndpoints(e ?? [])).catch(() => {})
}, []) }, [])
async function submit(e: FormEvent) { async function submit(e: FormEvent) {