From ea671535fc2bf9ba307df4a1f6ebf84d17d72d16 Mon Sep 17 00:00:00 2001 From: josh Date: Fri, 29 May 2026 15:57:58 -0400 Subject: [PATCH] feat: accept partial bodies on PUT /api/instances/:vmid Merge the request body onto the existing instance row before validating, so external callers (n8n, scripts) can send only the fields they want to change instead of GET-then-splat-then-PUT the full record. Mirrors the partial-update pattern already used by PUT /api/jobs/:id. Full-body PUTs (today's frontend) are unaffected. --- server/routes.js | 10 +++++++--- tests/api.test.js | 12 ++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/server/routes.js b/server/routes.js index 6f2a79e..6ff18d1 100644 --- a/server/routes.js +++ b/server/routes.js @@ -112,13 +112,17 @@ router.post('/instances', (req, res) => { router.put('/instances/:vmid', (req, res) => { const vmid = parseInt(req.params.vmid, 10); if (!vmid) return res.status(400).json({ error: 'invalid vmid' }); - if (!getInstance(vmid)) return res.status(404).json({ error: 'instance not found' }); - const errors = validate(req.body); + const existing = getInstance(vmid); + if (!existing) return res.status(404).json({ error: 'instance not found' }); + + const merged = { ...existing, ...(req.body ?? {}) }; + + const errors = validate(merged); if (errors.length) return res.status(400).json({ errors }); try { - const data = normalise(req.body); + const data = normalise(merged); updateInstance(vmid, data); res.json(getInstance(data.vmid)); } catch (e) { diff --git a/tests/api.test.js b/tests/api.test.js index 66a08df..cf48c6c 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -257,6 +257,18 @@ describe('PUT /api/instances/:vmid', () => { const res = await request(app).put('/api/instances/100').send({ ...base, vmid: 200 }) expect(res.status).toBe(409) }) + + it('accepts a partial body and preserves unspecified fields', async () => { + await request(app).post('/api/instances').send({ ...base, atlas: 1, tailscale_ip: '100.64.0.1' }) + const res = await request(app).put('/api/instances/100').send({ state: 'degraded' }) + expect(res.status).toBe(200) + expect(res.body.state).toBe('degraded') + expect(res.body.name).toBe(base.name) + expect(res.body.vmid).toBe(100) + expect(res.body.stack).toBe(base.stack) + expect(res.body.atlas).toBe(1) + expect(res.body.tailscale_ip).toBe('100.64.0.1') + }) }) // ── DELETE /api/instances/:vmid ─────────────────────────────────────────────── -- 2.39.5