From eb2936078dda29fd59dfa2c64daf884ad9b3c77c Mon Sep 17 00:00:00 2001 From: full-bars <45684698+full-bars@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:09:16 -0700 Subject: [PATCH 1/2] feat: add bulk delete endpoint for offline network clients I added a `RemoveNetworkClients` model function and a `POST /network/remove-clients` API handler to support bulk deletion. This replaces the need for the client to issue hundreds of sequential HTTP requests by processing the deletions efficiently in a single PostgreSQL `ANY()` update operation. --- api/handlers/network_client_handlers.go | 4 +++ api/main.go | 1 + model/network_client_model.go | 37 +++++++++++++++++++++++++ model/network_client_model_test.go | 30 ++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/api/handlers/network_client_handlers.go b/api/handlers/network_client_handlers.go index 69aa269d..daf96f8c 100644 --- a/api/handlers/network_client_handlers.go +++ b/api/handlers/network_client_handlers.go @@ -16,6 +16,10 @@ func RemoveNetworkClient(w http.ResponseWriter, r *http.Request) { router.WrapWithInputRequireAuth(model.RemoveNetworkClient, w, r) } +func RemoveNetworkClients(w http.ResponseWriter, r *http.Request) { + router.WrapWithInputRequireAuth(model.RemoveNetworkClients, w, r) +} + func RemoveNetwork(w http.ResponseWriter, r *http.Request) { router.WrapRequireAuth(controller.NetworkRemove, w, r) } diff --git a/api/main.go b/api/main.go index b97b736e..1ea52e86 100644 --- a/api/main.go +++ b/api/main.go @@ -102,6 +102,7 @@ Options: router.NewRoute("POST", "/auth/upgrade-guest-existing", handlers.UpgradeGuestExisting), router.NewRoute("POST", "/network/auth-client", handlers.AuthNetworkClient), router.NewRoute("POST", "/network/remove-client", handlers.RemoveNetworkClient), + router.NewRoute("POST", "/network/remove-clients", handlers.RemoveNetworkClients), router.NewRoute("GET", "/network/clients", handlers.NetworkClients), router.NewRoute("GET", "/network/provider-locations", handlers.NetworkGetProviderLocations), router.NewRoute("POST", "/network/find-provider-locations", handlers.NetworkFindProviderLocations), diff --git a/model/network_client_model.go b/model/network_client_model.go index 83940e5d..18d17724 100644 --- a/model/network_client_model.go +++ b/model/network_client_model.go @@ -445,6 +445,43 @@ func RemoveNetworkClient( return removeClientResult, removeClientErr } +type RemoveNetworkClientsArgs struct { + ClientIds []server.Id `json:"client_ids"` +} + +type RemoveNetworkClientsResult struct{} + +func RemoveNetworkClients( + removeClients *RemoveNetworkClientsArgs, + session *session.ClientSession, +) (*RemoveNetworkClientsResult, error) { + var removeClientsResult *RemoveNetworkClientsResult + if len(removeClients.ClientIds) == 0 { + return &RemoveNetworkClientsResult{}, nil + } + + server.Tx(session.Ctx, func(tx server.PgTx) { + _, err := tx.Exec( + session.Ctx, + ` + UPDATE network_client + SET + active = false + WHERE + client_id = ANY($1) AND + network_id = $2 + `, + removeClients.ClientIds, + session.ByJwt.NetworkId, + ) + server.Raise(err) + + removeClientsResult = &RemoveNetworkClientsResult{} + }) + + return removeClientsResult, nil +} + type NetworkClientsResult struct { Clients []*NetworkClientInfo `json:"clients"` } diff --git a/model/network_client_model_test.go b/model/network_client_model_test.go index 9df55e11..591426c0 100644 --- a/model/network_client_model_test.go +++ b/model/network_client_model_test.go @@ -9,6 +9,8 @@ import ( "github.com/go-playground/assert/v2" "github.com/urnetwork/server" + "github.com/urnetwork/server/jwt" + "github.com/urnetwork/server/session" ) func TestNetworkClientHandlerLifecycle(t *testing.T) { @@ -196,3 +198,31 @@ func TestSetProvide(t *testing.T) { }) } + +func TestRemoveNetworkClients(t *testing.T) { + server.DefaultTestEnv().Run(t, func(t testing.TB) { + ctx := context.Background() + networkId := server.NewId() + + // Create a mock session + sess := &session.ClientSession{ + Ctx: ctx, + ByJwt: &jwt.ByJwt{ + NetworkId: networkId, + }, + } + + // Generate random IDs to test the ANY($1) binding + clientIds := []server.Id{server.NewId(), server.NewId(), server.NewId()} + + args := &RemoveNetworkClientsArgs{ + ClientIds: clientIds, + } + + // This will panic if the driver fails to cast []server.Id to uuid[] + _, err := RemoveNetworkClients(args, sess) + + // Assert that the function ran without returning an error + assert.Equal(t, err, nil) + }) +} From 2128a0cd857e06706d2b17ead6bd060b557f48ff Mon Sep 17 00:00:00 2001 From: full-bars <45684698+full-bars@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:43:27 -0700 Subject: [PATCH 2/2] --amend --- model/network_client_model_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/network_client_model_test.go b/model/network_client_model_test.go index 591426c0..24ea30b0 100644 --- a/model/network_client_model_test.go +++ b/model/network_client_model_test.go @@ -221,7 +221,7 @@ func TestRemoveNetworkClients(t *testing.T) { // This will panic if the driver fails to cast []server.Id to uuid[] _, err := RemoveNetworkClients(args, sess) - + // Assert that the function ran without returning an error assert.Equal(t, err, nil) })