From 81335559f8b8602a35443031e0d8365659198417 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Sat, 21 Feb 2026 08:59:19 -0800 Subject: [PATCH 1/4] fix: change flowsheet.play_order from serial to integer play_order is manually managed by reorder operations and should not auto-increment. The serial type wasted a sequence and could collide with manually assigned values. Co-authored-by: Cursor --- shared/database/src/schema.ts | 2 +- tests/unit/database/schema.flowsheet.test.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 tests/unit/database/schema.flowsheet.test.ts diff --git a/shared/database/src/schema.ts b/shared/database/src/schema.ts index 7a9bfc5..3a11a10 100644 --- a/shared/database/src/schema.ts +++ b/shared/database/src/schema.ts @@ -304,7 +304,7 @@ export const flowsheet = wxyc_schema.table('flowsheet', { album_title: varchar('album_title', { length: 128 }), artist_name: varchar('artist_name', { length: 128 }), record_label: varchar('record_label', { length: 128 }), - play_order: serial('play_order').notNull(), + play_order: integer('play_order').notNull(), request_flag: boolean('request_flag').default(false).notNull(), message: varchar('message', { length: 250 }), add_time: timestamp('add_time').defaultNow().notNull(), diff --git a/tests/unit/database/schema.flowsheet.test.ts b/tests/unit/database/schema.flowsheet.test.ts new file mode 100644 index 0000000..014e016 --- /dev/null +++ b/tests/unit/database/schema.flowsheet.test.ts @@ -0,0 +1,18 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +describe('flowsheet schema', () => { + const schemaSource = readFileSync( + resolve(__dirname, '../../../shared/database/src/schema.ts'), + 'utf-8', + ); + + it('play_order should not use serial (it is manually managed)', () => { + const playOrderLine = schemaSource + .split('\n') + .find((line) => line.includes('play_order')); + + expect(playOrderLine).toBeDefined(); + expect(playOrderLine).not.toMatch(/serial\s*\(\s*['"]play_order['"]\s*\)/); + }); +}); From 46aa75751168426f65eedeb73268706d9a0dece2 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Fri, 27 Feb 2026 10:46:22 -0800 Subject: [PATCH 2/4] fix: provide play_order on all flowsheet inserts play_order changed from serial to integer, requiring explicit values. Add getNextPlayOrder() helper that computes max(play_order)+1 per show. Use it in addTrack, startShow, endShow, and DJ join/leave notifications. --- .../controllers/flowsheet.controller.ts | 8 ++--- apps/backend/services/flowsheet.service.ts | 31 +++++++++++++++++-- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/apps/backend/controllers/flowsheet.controller.ts b/apps/backend/controllers/flowsheet.controller.ts index c8074c5..8b18185 100644 --- a/apps/backend/controllers/flowsheet.controller.ts +++ b/apps/backend/controllers/flowsheet.controller.ts @@ -1,6 +1,6 @@ import { Request, RequestHandler } from 'express'; import { Mutex } from 'async-mutex'; -import { NewFSEntry, FSEntry, Show, ShowDJ, library } from '@wxyc/database'; +import { FSEntry, Show, ShowDJ, library } from '@wxyc/database'; import * as flowsheet_service from '../services/flowsheet.service.js'; import { fetchAndCacheMetadata } from '../services/metadata/index.js'; @@ -159,7 +159,7 @@ export const addEntry: RequestHandler = async (req: Request { } }; +/** Get the next play_order value for a given show (max + 1, or 1 if no entries exist) */ +export const getNextPlayOrder = async (showId: number): Promise => { + const result = await db + .select({ maxOrder: max(flowsheet.play_order) }) + .from(flowsheet) + .where(eq(flowsheet.show_id, showId)); + return (result[0]?.maxOrder ?? 0) + 1; +}; + // SQL query fields (flat structure from database) const FSEntryFieldsRaw = { id: flowsheet.id, @@ -174,7 +183,7 @@ export const getEntriesByShow = async (...show_ids: number[]): Promise => { +export const addTrack = async (entry: Omit & { play_order?: number }): Promise => { /* TODO: logic for updating album playcount */ @@ -202,7 +211,15 @@ export const addTrack = async (entry: NewFSEntry): Promise => { // } // } - const response = await db.insert(flowsheet).values(entry).returning(); + // Compute play_order if not already set + if (entry.show_id != null && !entry.play_order) { + entry = { ...entry, play_order: await getNextPlayOrder(entry.show_id) }; + } + + const response = await db + .insert(flowsheet) + .values(entry as NewFSEntry) + .returning(); updateLastModified(); return response[0]; }; @@ -291,8 +308,10 @@ export const startShow = async (dj_id: string, show_name?: string, specialty_id? }) .returning(); + const startPlayOrder = await getNextPlayOrder(new_show[0].id); await db.insert(flowsheet).values({ show_id: new_show[0].id, + play_order: startPlayOrder, entry_type: 'show_start', message: `Start of Show: DJ ${dj_info.djName || dj_info.name} joined the set at ${new Date().toLocaleString( 'en-US', @@ -352,10 +371,12 @@ const createJoinNotification = async (id: string, show_id: number): Promise => { const dj_information = (await db.select().from(user).where(eq(user.id, primary_dj_id)).limit(1))[0]; const dj_name = dj_information?.djName || dj_information?.name || 'A DJ'; + const endPlayOrder = await getNextPlayOrder(currentShow.id); await db.insert(flowsheet).values({ show_id: currentShow.id, + play_order: endPlayOrder, entry_type: 'show_end', message: `End of Show: ${dj_name} left the set at ${new Date().toLocaleString('en-US', { timeZone: 'America/New_York', @@ -431,10 +454,12 @@ const createLeaveNotification = async (dj_id: string, show_id: number): Promise< const message = `${dj_name} left the set!`; + const leavePlayOrder = await getNextPlayOrder(show_id); const notification = await db .insert(flowsheet) .values({ show_id: show_id, + play_order: leavePlayOrder, entry_type: 'dj_leave', message: message, }) From 8b6a9aa6e0bf95c71b761eed406d5976678900aa Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Fri, 27 Feb 2026 14:20:50 -0800 Subject: [PATCH 3/4] style: format files with Prettier --- tests/unit/database/schema.flowsheet.test.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/unit/database/schema.flowsheet.test.ts b/tests/unit/database/schema.flowsheet.test.ts index 014e016..caf9e5c 100644 --- a/tests/unit/database/schema.flowsheet.test.ts +++ b/tests/unit/database/schema.flowsheet.test.ts @@ -2,15 +2,10 @@ import { readFileSync } from 'fs'; import { resolve } from 'path'; describe('flowsheet schema', () => { - const schemaSource = readFileSync( - resolve(__dirname, '../../../shared/database/src/schema.ts'), - 'utf-8', - ); + const schemaSource = readFileSync(resolve(__dirname, '../../../shared/database/src/schema.ts'), 'utf-8'); it('play_order should not use serial (it is manually managed)', () => { - const playOrderLine = schemaSource - .split('\n') - .find((line) => line.includes('play_order')); + const playOrderLine = schemaSource.split('\n').find((line) => line.includes('play_order')); expect(playOrderLine).toBeDefined(); expect(playOrderLine).not.toMatch(/serial\s*\(\s*['"]play_order['"]\s*\)/); From 1547a111171f193f99f5e75a1f91cdd0439d4972 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Fri, 27 Feb 2026 15:22:07 -0800 Subject: [PATCH 4/4] fix: use global play_order sequence instead of per-show getNextPlayOrder() was scoped per-show (WHERE show_id = X), making play_order values non-globally-unique. Queries use ORDER BY play_order DESC globally, so the sequence must be monotonically increasing across all shows to preserve correct ordering. --- apps/backend/services/flowsheet.service.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/backend/services/flowsheet.service.ts b/apps/backend/services/flowsheet.service.ts index a8adeba..590b596 100644 --- a/apps/backend/services/flowsheet.service.ts +++ b/apps/backend/services/flowsheet.service.ts @@ -40,12 +40,9 @@ const updateLastModified = () => { } }; -/** Get the next play_order value for a given show (max + 1, or 1 if no entries exist) */ -export const getNextPlayOrder = async (showId: number): Promise => { - const result = await db - .select({ maxOrder: max(flowsheet.play_order) }) - .from(flowsheet) - .where(eq(flowsheet.show_id, showId)); +/** Get the next play_order value (global max + 1, or 1 if no entries exist) */ +export const getNextPlayOrder = async (): Promise => { + const result = await db.select({ maxOrder: max(flowsheet.play_order) }).from(flowsheet); return (result[0]?.maxOrder ?? 0) + 1; }; @@ -213,7 +210,7 @@ export const addTrack = async (entry: Omit & { play_or // Compute play_order if not already set if (entry.show_id != null && !entry.play_order) { - entry = { ...entry, play_order: await getNextPlayOrder(entry.show_id) }; + entry = { ...entry, play_order: await getNextPlayOrder() }; } const response = await db @@ -308,7 +305,7 @@ export const startShow = async (dj_id: string, show_name?: string, specialty_id? }) .returning(); - const startPlayOrder = await getNextPlayOrder(new_show[0].id); + const startPlayOrder = await getNextPlayOrder(); await db.insert(flowsheet).values({ show_id: new_show[0].id, play_order: startPlayOrder, @@ -371,7 +368,7 @@ const createJoinNotification = async (id: string, show_id: number): Promise => { const dj_information = (await db.select().from(user).where(eq(user.id, primary_dj_id)).limit(1))[0]; const dj_name = dj_information?.djName || dj_information?.name || 'A DJ'; - const endPlayOrder = await getNextPlayOrder(currentShow.id); + const endPlayOrder = await getNextPlayOrder(); await db.insert(flowsheet).values({ show_id: currentShow.id, play_order: endPlayOrder, @@ -454,7 +451,7 @@ const createLeaveNotification = async (dj_id: string, show_id: number): Promise< const message = `${dj_name} left the set!`; - const leavePlayOrder = await getNextPlayOrder(show_id); + const leavePlayOrder = await getNextPlayOrder(); const notification = await db .insert(flowsheet) .values({