From db8eb27e5f7c4979353df008f32cd6513dcf4c75 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Thu, 10 Sep 2026 15:42:02 +0000 Subject: [PATCH] fix(podcasts): serialize explicit as a real boolean in API responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLite has no boolean type, so raw rows return explicit as 0/1. The podcast edit form round-trips whatever GET /api/podcasts/:id sends it, and the update schema requires z.boolean() — so saving any change without also touching the explicit checkbox failed validation with "Expected boolean, received number". Co-Authored-By: Claude Sonnet 5 --- server/src/app.test.ts | 17 +++++++++++++++++ server/src/routes/podcasts.ts | 16 ++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/server/src/app.test.ts b/server/src/app.test.ts index ade9396..fa7c55c 100644 --- a/server/src/app.test.ts +++ b/server/src/app.test.ts @@ -171,6 +171,23 @@ describe('podcasts, episodes, feed', () => { expect(res.statusCode).toBe(201); podcastId = res.json().id; expect(res.json().podcast_guid).toMatch(/^[0-9a-f-]{36}$/); + expect(res.json().explicit).toBe(false); // not the raw SQLite 0/1 + }); + + it('lets the edit form round-trip a fetched podcast unmodified (regression: explicit came back as 0/1, not a bool)', async () => { + const fetched = await app.inject({ method: 'GET', url: `/api/podcasts/${podcastId}`, headers: { cookie } }); + expect(fetched.json().explicit).toBe(false); + + const { episodes: _episodes, feed_url: _feedUrl, ...editForm } = fetched.json(); + const res = await app.inject({ + method: 'PUT', + url: `/api/podcasts/${podcastId}`, + headers: { cookie }, + payload: editForm, + }); + expect(res.statusCode).toBe(200); + expect(res.json().lightning_address).toBe('tester@getalby.com'); + expect(res.json().explicit).toBe(false); }); it('registers an episode after verifying the blob on blossom', async () => { diff --git a/server/src/routes/podcasts.ts b/server/src/routes/podcasts.ts index ce7f935..34fa3c1 100644 --- a/server/src/routes/podcasts.ts +++ b/server/src/routes/podcasts.ts @@ -51,10 +51,18 @@ export default async function podcastRoutes(app: FastifyInstance) { return p && p.owner_pubkey === pubkey ? p : null; } + // SQLite has no boolean type — `explicit` comes back as a raw 0/1 integer. + // The client's edit form round-trips whatever this endpoint sends it, and the + // update schema requires a real boolean, so this needs to be a true boolean + // on the wire or re-submitting an untouched form fails validation. + function serializePodcast(p: Podcast): Omit & { explicit: boolean } { + return { ...p, explicit: !!p.explicit }; + } + app.get('/api/podcasts', { preHandler: app.requireAuth }, async (req) => { const podcasts = listPodcasts.all(req.userPubkey) as Podcast[]; return podcasts.map((p) => ({ - ...p, + ...serializePodcast(p), feed_url: `${settings.all().public_url}/feeds/${p.id}/feed.xml`, })); }); @@ -75,7 +83,7 @@ export default async function podcastRoutes(app: FastifyInstance) { d.category, d.explicit ? 1 : 0, d.lightning_address ?? null, d.keysend_node ?? null, d.value_suggested ?? null, podcastGuidForFeedUrl(feedUrl), nowSecs(), nowSecs(), ); - return reply.code(201).send({ ...(getPodcast.get(id) as Podcast), feed_url: feedUrl }); + return reply.code(201).send({ ...serializePodcast(getPodcast.get(id) as Podcast), feed_url: feedUrl }); }); app.get('/api/podcasts/:id', { preHandler: app.requireAuth }, async (req, reply) => { @@ -83,7 +91,7 @@ export default async function podcastRoutes(app: FastifyInstance) { const p = ownedPodcast(id, req.userPubkey!); if (!p) return reply.code(404).send({ error: 'podcast not found' }); return { - ...p, + ...serializePodcast(p), feed_url: `${settings.all().public_url}/feeds/${p.id}/feed.xml`, episodes: listEpisodes.all(id) as Episode[], }; @@ -106,7 +114,7 @@ export default async function podcastRoutes(app: FastifyInstance) { d.lightning_address ?? null, d.keysend_node ?? null, d.value_suggested ?? null, d.resale_producer_share_pct, nowSecs(), id, ); - return getPodcast.get(id) as Podcast; + return serializePodcast(getPodcast.get(id) as Podcast); }); app.delete('/api/podcasts/:id', { preHandler: app.requireAuth }, async (req, reply) => {