From 79f7a418375317c25c9b7d034222968f25760df5 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:16:36 -0700 Subject: [PATCH] Add Discord webhook notifications for standings updates (#157) * Add Discord webhook notifications for standings updates Adds a Discord webhook integration that posts standings to a configured Discord channel whenever scores change after a scoring event. Commissioners set the webhook URL in league settings and can send a test notification. Co-Authored-By: Claude Sonnet 4.6 * Fix Discord notification edge cases from code review - Fix point delta sign: negative diffs now correctly show -5 pts not +-5 pts - Replace hardcoded soccer emoji with a neutral bullet (works for all sports) - Truncate embed description at Discord's 4096-char limit - Thread eventId through processMatchResult and bracket batch handler so scored matches appear in single-match notifications too - Pass eventName to finalizeQualifyingPoints ("Final Standings") and processSeasonStandings ("Season Complete") so those notifications have context - Test notification now fetches current season to show "League 2025" format Co-Authored-By: Claude Sonnet 4.6 * Fix process-match-result tests: add sportsSeasons to db mock recalculateAffectedLeagues now queries sportsSeasons to get the sport name for Discord notifications; the test mock db needed the stub added. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- .../__tests__/process-match-result.test.ts | 1 + app/models/scoring-calculator.ts | 77 +++++++- ...sons.$id.events.$eventId.bracket.server.ts | 3 +- app/routes/leagues/$leagueId.settings.tsx | 76 ++++++++ app/services/__tests__/discord.test.ts | 177 ++++++++++++++++++ app/services/discord.ts | 133 ++++++++----- database/schema.ts | 2 +- drizzle/0048_eager_songbird.sql | 1 + drizzle/meta/0048_snapshot.json | 4 +- drizzle/meta/_journal.json | 4 +- 10 files changed, 421 insertions(+), 57 deletions(-) create mode 100644 app/services/__tests__/discord.test.ts create mode 100644 drizzle/0048_eager_songbird.sql diff --git a/app/models/__tests__/process-match-result.test.ts b/app/models/__tests__/process-match-result.test.ts index 0d19abf..42fcfac 100644 --- a/app/models/__tests__/process-match-result.test.ts +++ b/app/models/__tests__/process-match-result.test.ts @@ -46,6 +46,7 @@ function makeDb(existingResult?: { id: string; isPartialScore: boolean }) { seasonSports: { findMany: vi.fn().mockResolvedValue([]), // empty → standings loop exits immediately }, + sportsSeasons: { findFirst: vi.fn().mockResolvedValue(null) }, scoringEvents: { findFirst: vi.fn().mockResolvedValue(null) }, }, } as any, diff --git a/app/models/scoring-calculator.ts b/app/models/scoring-calculator.ts index 078838a..f2b6dfc 100644 --- a/app/models/scoring-calculator.ts +++ b/app/models/scoring-calculator.ts @@ -4,7 +4,7 @@ import { eq, and, inArray } from "drizzle-orm"; import { getScoringRules, calculateFantasyPoints, calculateAveragedPoints, calculateBracketPoints } from "./scoring-rules"; import { getSeasonResults } from "./participant-season-result"; import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; -import { sendStandingsUpdateNotification } from "~/services/discord"; +import { sendStandingsUpdateNotification, type ScoredMatch } from "~/services/discord"; /** * Core scoring calculation engine @@ -274,7 +274,7 @@ export async function processPlayoffEvent( } // Recalculate standings for all affected leagues - await recalculateAffectedLeagues(event.sportsSeasonId, db, { eventName: event.name }); + await recalculateAffectedLeagues(event.sportsSeasonId, db, { eventName: event.name, eventId }); // Auto-trigger probability recalculation after result try { @@ -310,13 +310,14 @@ export async function processMatchResult( isScoring: boolean; sportsSeasonId: string; bracketTemplateId?: string | null; + eventId?: string; eventName?: string; skipSideEffects?: boolean; }, providedDb?: ReturnType ): Promise { const db = providedDb || database(); - const { round, winnerId, loserId, isScoring, sportsSeasonId, bracketTemplateId, eventName, skipSideEffects } = params; + const { round, winnerId, loserId, isScoring, sportsSeasonId, bracketTemplateId, eventId, eventName, skipSideEffects } = params; if (!isScoring) { // Non-scoring (pre-bracket) round: loser permanently eliminated (0 pts), @@ -346,7 +347,7 @@ export async function processMatchResult( } if (!skipSideEffects) { - await recalculateAffectedLeagues(sportsSeasonId, db, eventName ? { eventName } : undefined); + await recalculateAffectedLeagues(sportsSeasonId, db, (eventName || eventId) ? { eventName, eventId } : undefined); try { await updateProbabilitiesAfterResult(sportsSeasonId, true); } catch (error) { @@ -629,7 +630,7 @@ export async function finalizeQualifyingPoints( .where(eq(schema.sportsSeasons.id, sportsSeasonId)); // Trigger recalculation for all affected leagues - await recalculateAffectedLeagues(sportsSeasonId, db); + await recalculateAffectedLeagues(sportsSeasonId, db, { eventName: "Final Standings" }); // Auto-trigger probability recalculation after result try { @@ -739,7 +740,7 @@ export async function processSeasonStandings( } // Trigger recalculation for all affected leagues - await recalculateAffectedLeagues(sportsSeasonId, db); + await recalculateAffectedLeagues(sportsSeasonId, db, { eventName: "Season Complete" }); // Auto-trigger probability recalculation after result try { @@ -1108,7 +1109,7 @@ export async function recalculateStandings( export async function recalculateAffectedLeagues( sportsSeasonId: string, providedDb?: ReturnType, - options?: { eventName?: string } + options?: { eventName?: string; eventId?: string } ): Promise { const db = providedDb || database(); @@ -1119,6 +1120,42 @@ export async function recalculateAffectedLeagues( const seasonIds = seasonSports.map((ss) => ss.seasonId); + // Look up sport name for the notification header + const sportsSeason = await db.query.sportsSeasons.findFirst({ + where: eq(schema.sportsSeasons.id, sportsSeasonId), + with: { sport: true }, + }); + const sportName = sportsSeason?.sport?.name; + + // Look up completed matches for the event (with participant names) if an eventId was provided + let allCompletedMatches: Array<{ + winnerId: string | null; + loserId: string | null; + winnerName: string | null; + loserName: string | null; + }> = []; + + if (options?.eventId) { + const matches = await db.query.playoffMatches.findMany({ + where: and( + eq(schema.playoffMatches.scoringEventId, options.eventId), + eq(schema.playoffMatches.isComplete, true) + ), + with: { + winner: true, + loser: true, + }, + }); + allCompletedMatches = matches + .filter((m) => m.winnerId && m.loserId) + .map((m) => ({ + winnerId: m.winnerId, + loserId: m.loserId, + winnerName: m.winner?.name ?? null, + loserName: m.loser?.name ?? null, + })); + } + // Recalculate each affected season for (const seasonId of seasonIds) { // Capture standings before recalculation for delta calculation @@ -1155,6 +1192,30 @@ export async function recalculateAffectedLeagues( const webhookUrl = season?.league?.discordWebhookUrl; if (!webhookUrl) continue; + // Filter matches to only those involving participants drafted in this season + let scoredMatches: ScoredMatch[] | undefined; + if (allCompletedMatches.length > 0) { + const draftPicks = await db.query.draftPicks.findMany({ + where: eq(schema.draftPicks.seasonId, seasonId), + }); + const draftedIds = new Set(draftPicks.map((p) => p.participantId)); + + const relevant = allCompletedMatches.filter( + (m) => + (m.winnerId && draftedIds.has(m.winnerId)) || + (m.loserId && draftedIds.has(m.loserId)) + ); + + if (relevant.length > 0) { + scoredMatches = relevant + .filter((m) => m.winnerName && m.loserName) + .map((m) => ({ + winnerName: m.winnerName!, + loserName: m.loserName!, + })); + } + } + const standings = afterStandings.map((s) => ({ teamId: s.teamId, teamName: s.team.name, @@ -1168,7 +1229,9 @@ export async function recalculateAffectedLeagues( seasonName: `${season.league.name} ${season.year}`, standings, previousStandings: previousPointsById, + sportName, eventName: options?.eventName, + scoredMatches, }); } catch (err) { // Log but don't fail the scoring pipeline if Discord is unreachable diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts index d9f4b1f..5533e76 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts @@ -257,6 +257,7 @@ export async function action({ request, params }: Route.ActionArgs) { isScoring: match.isScoring ?? true, sportsSeasonId: event.sportsSeasonId, bracketTemplateId: event.bracketTemplateId, + eventId: event.id, eventName: event.name ?? undefined, }); @@ -375,7 +376,7 @@ export async function action({ request, params }: Route.ActionArgs) { // Run side effects once for the whole batch (avoids N redundant recalcs). if (successCount > 0) { const db = database(); - await recalculateAffectedLeagues(event.sportsSeasonId, db, event.name ? { eventName: event.name } : undefined); + await recalculateAffectedLeagues(event.sportsSeasonId, db, { eventId: event.id, eventName: event.name ?? undefined }); try { await updateProbabilitiesAfterResult(event.sportsSeasonId, true); } catch (error) { diff --git a/app/routes/leagues/$leagueId.settings.tsx b/app/routes/leagues/$leagueId.settings.tsx index 99908a3..9b0dcd2 100644 --- a/app/routes/leagues/$leagueId.settings.tsx +++ b/app/routes/leagues/$leagueId.settings.tsx @@ -4,6 +4,7 @@ import { getAuth } from "@clerk/react-router/server"; import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import { findLeagueById, updateLeague, deleteLeague } from "~/models/league"; +import { sendStandingsUpdateNotification } from "~/services/discord"; import { isCommissioner, findCommissionersByLeagueId, @@ -190,6 +191,7 @@ export async function action(args: Route.ActionArgs) { if (intent === "update") { const name = formData.get("name"); const isPublicDraftBoard = formData.get("isPublicDraftBoard") === "on"; + const discordWebhookUrl = formData.get("discordWebhookUrl"); if (typeof name !== "string" || !name.trim()) { return { error: "League name is required" }; @@ -198,10 +200,17 @@ export async function action(args: Route.ActionArgs) { return { error: "League name must be between 3 and 50 characters" }; } + const webhookUrl = + typeof discordWebhookUrl === "string" ? discordWebhookUrl.trim() : ""; + if (webhookUrl && !webhookUrl.startsWith("https://discord.com/api/webhooks/")) { + return { error: "Discord webhook URL must start with https://discord.com/api/webhooks/" }; + } + try { await updateLeague(leagueId, { name: name.trim(), isPublicDraftBoard, + discordWebhookUrl: webhookUrl || null, }); } catch (error) { console.error("Error updating league:", error); @@ -209,6 +218,36 @@ export async function action(args: Route.ActionArgs) { } } + if (intent === "test-discord-webhook") { + const webhookUrl = formData.get("webhookUrl"); + if (typeof webhookUrl !== "string" || !webhookUrl.startsWith("https://discord.com/api/webhooks/")) { + return { error: "Invalid Discord webhook URL" }; + } + try { + const testSeason = await findCurrentSeasonWithSports(leagueId); + const seasonName = testSeason ? `${league.name} ${testSeason.year}` : league.name; + await sendStandingsUpdateNotification({ + webhookUrl, + seasonName, + standings: [ + { teamId: "1", teamName: "Team Alpha", totalPoints: 150, rank: 1 }, + { teamId: "2", teamName: "Team Beta", totalPoints: 125, rank: 2 }, + { teamId: "3", teamName: "Team Gamma", totalPoints: 100, rank: 3 }, + ], + previousStandings: new Map([ + ["1", 125], + ["2", 125], + ["3", 100], + ]), + eventName: "Test Notification", + }); + return { testSuccess: true }; + } catch (err) { + console.error("Discord test webhook failed:", err); + return { error: "Failed to send test notification. Check your webhook URL." }; + } + } + // Get current season to check status const season = await findCurrentSeasonWithSports(leagueId); if (!season) { @@ -787,6 +826,43 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone + {/* Notifications */} + + + Notifications + + Get Discord alerts when standings change after scoring events + + + +
+ + +

+ Create a webhook in your Discord server under Server Settings → Integrations → Webhooks. +

+
+ {league.discordWebhookUrl && ( +
+ + + +
+ )} + {actionData && "testSuccess" in actionData && actionData.testSuccess && ( +

Test notification sent to Discord!

+ )} +
+
+ {/* Draft Rounds */} diff --git a/app/services/__tests__/discord.test.ts b/app/services/__tests__/discord.test.ts new file mode 100644 index 0000000..d3c8846 --- /dev/null +++ b/app/services/__tests__/discord.test.ts @@ -0,0 +1,177 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { sendDiscordWebhook, sendStandingsUpdateNotification } from "../discord"; + +const WEBHOOK_URL = "https://discord.com/api/webhooks/123/abc"; + +function mockFetch(status: number, body = "") { + return vi.fn().mockResolvedValue({ + ok: status >= 200 && status < 300, + status, + text: async () => body, + }); +} + +function getDescription(): string { + const body = JSON.parse( + (fetch as ReturnType).mock.calls[0][1].body + ); + return body.embeds[0].description as string; +} + +describe("sendDiscordWebhook", () => { + beforeEach(() => { + vi.stubGlobal("fetch", mockFetch(204)); + }); + + it("POSTs JSON to the webhook URL", async () => { + await sendDiscordWebhook(WEBHOOK_URL, { content: "hello" }); + + expect(fetch).toHaveBeenCalledWith(WEBHOOK_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: "hello" }), + }); + }); + + it("throws on non-2xx response", async () => { + vi.stubGlobal("fetch", mockFetch(400, "Bad Request")); + + await expect(sendDiscordWebhook(WEBHOOK_URL, {})).rejects.toThrow( + "Discord webhook failed: 400" + ); + }); +}); + +describe("sendStandingsUpdateNotification", () => { + beforeEach(() => { + vi.stubGlobal("fetch", mockFetch(204)); + }); + + it("sends an embed with the season name as title", async () => { + await sendStandingsUpdateNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }], + previousStandings: new Map(), + }); + + const body = JSON.parse((fetch as ReturnType).mock.calls[0][1].body); + expect(body.embeds[0].title).toBe("📊 Standings Update — My League 2025"); + }); + + it("shows point deltas as integers in description", async () => { + await sendStandingsUpdateNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "Test League", + standings: [ + { teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }, + { teamId: "b", teamName: "Beta", totalPoints: 100, rank: 2 }, + ], + previousStandings: new Map([ + ["a", 125], + ["b", 100], + ]), + }); + + const desc = getDescription(); + expect(desc).toContain("Alpha"); + expect(desc).toContain("+25 pts"); + expect(desc).not.toContain("+25.0"); + // Beta didn't change — no delta shown + expect(desc).toMatch(/Beta.*100 pts(?!\s*\()/); + }); + + it("includes sport name and event name in header when both provided", async () => { + await sendStandingsUpdateNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], + previousStandings: new Map(), + sportName: "UEFA Champions League", + eventName: "Knockout Stage", + }); + + const desc = getDescription(); + expect(desc).toContain("UEFA Champions League — Knockout Stage"); + }); + + it("shows only event name when no sport name provided", async () => { + await sendStandingsUpdateNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], + previousStandings: new Map(), + eventName: "Quarter Finals", + }); + + const desc = getDescription(); + expect(desc).toContain("Quarter Finals"); + expect(desc).not.toContain("—\n"); + }); + + it("omits header section when neither sport name nor event name provided", async () => { + await sendStandingsUpdateNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], + previousStandings: new Map(), + }); + + const desc = getDescription(); + // Description should start directly with the standings section + expect(desc).toContain("**Current Standings**"); + expect(desc).not.toContain("**Scored Matches**"); + }); + + it("lists scored matches above standings", async () => { + await sendStandingsUpdateNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], + previousStandings: new Map(), + scoredMatches: [ + { winnerName: "Real Madrid", loserName: "Bayern Munich" }, + { winnerName: "Arsenal", loserName: "PSG" }, + ], + }); + + const desc = getDescription(); + expect(desc).toContain("**Scored Matches**"); + expect(desc).toContain("• **Real Madrid** def. Bayern Munich"); + expect(desc).toContain("• **Arsenal** def. PSG"); + // Scored matches section should appear before standings + expect(desc.indexOf("Scored Matches")).toBeLessThan(desc.indexOf("Current Standings")); + }); + + it("uses medal emoji for top 3 ranks in description", async () => { + await sendStandingsUpdateNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + standings: [ + { teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }, + { teamId: "b", teamName: "Beta", totalPoints: 125, rank: 2 }, + { teamId: "c", teamName: "Gamma", totalPoints: 100, rank: 3 }, + { teamId: "d", teamName: "Delta", totalPoints: 75, rank: 4 }, + ], + previousStandings: new Map(), + }); + + const desc = getDescription(); + expect(desc).toContain("🥇 Alpha"); + expect(desc).toContain("🥈 Beta"); + expect(desc).toContain("🥉 Gamma"); + expect(desc).toContain("4. Delta"); + }); + + it("does not show fields on the embed", async () => { + await sendStandingsUpdateNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }], + previousStandings: new Map(), + }); + + const body = JSON.parse((fetch as ReturnType).mock.calls[0][1].body); + expect(body.embeds[0].fields).toBeUndefined(); + }); +}); diff --git a/app/services/discord.ts b/app/services/discord.ts index da72893..94b12bc 100644 --- a/app/services/discord.ts +++ b/app/services/discord.ts @@ -1,16 +1,41 @@ -interface StandingsEntry { +interface DiscordEmbed { + title?: string; + description?: string; + color?: number; + footer?: { text: string }; +} + +interface DiscordWebhookPayload { + content?: string; + embeds?: DiscordEmbed[]; +} + +export async function sendDiscordWebhook( + webhookUrl: string, + payload: DiscordWebhookPayload +): Promise { + const response = await fetch(webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const text = await response.text().catch(() => ""); + throw new Error(`Discord webhook failed: ${response.status} ${text}`); + } +} + +export interface StandingEntry { teamId: string; teamName: string; totalPoints: number; - rank: number | null; + rank: number; } -interface SendStandingsUpdateOptions { - webhookUrl: string; - seasonName: string; - standings: StandingsEntry[]; - previousStandings: Map | Record; - eventName?: string; +export interface ScoredMatch { + winnerName: string; + loserName: string; } export async function sendStandingsUpdateNotification({ @@ -18,44 +43,64 @@ export async function sendStandingsUpdateNotification({ seasonName, standings, previousStandings, + sportName, eventName, -}: SendStandingsUpdateOptions): Promise { - const title = eventName - ? `📊 ${seasonName} — Standings after ${eventName}` - : `📊 ${seasonName} — Standings Update`; + scoredMatches, +}: { + webhookUrl: string; + seasonName: string; + standings: StandingEntry[]; + previousStandings: Map; + sportName?: string; + eventName?: string; + scoredMatches?: ScoredMatch[]; +}): Promise { + const RANK_MEDALS = ["🥇", "🥈", "🥉"]; - const getPrev = (teamId: string) => - previousStandings instanceof Map - ? previousStandings.get(teamId) - : previousStandings[teamId]; + const sections: string[] = []; - const lines = standings - .sort((a, b) => (a.rank ?? 999) - (b.rank ?? 999)) - .map((s) => { - const prev = getPrev(s.teamId); - const diff = - prev !== undefined ? s.totalPoints - prev : null; - const diffStr = - diff !== null && diff !== 0 - ? diff > 0 - ? ` (+${diff.toFixed(1)})` - : ` (${diff.toFixed(1)})` - : ""; - const rankStr = s.rank != null ? `${s.rank}.` : "-"; - return `${rankStr} **${s.teamName}** — ${s.totalPoints.toFixed(1)} pts${diffStr}`; - }); - - const content = [`**${title}**`, "", ...lines].join("\n"); - - const response = await fetch(webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ content }), - }); - - if (!response.ok) { - throw new Error( - `Discord webhook returned ${response.status}: ${await response.text()}` - ); + // Header: "Sport Name — Event Name" + if (sportName || eventName) { + const parts = [sportName, eventName].filter(Boolean); + sections.push(`**${parts.join(" — ")}**`); } + + // Scored matches section + if (scoredMatches && scoredMatches.length > 0) { + sections.push("**Scored Matches**"); + for (const match of scoredMatches) { + sections.push(`• **${match.winnerName}** def. ${match.loserName}`); + } + } + + // Standings section + sections.push("**Current Standings**"); + for (const s of standings) { + const prev = previousStandings.get(s.teamId); + let delta = ""; + if (prev !== undefined && prev !== s.totalPoints) { + const diff = Math.round(s.totalPoints - prev); + const sign = diff > 0 ? "+" : ""; + delta = ` **(${sign}${diff} pts)**`; + } + const medal = RANK_MEDALS[s.rank - 1] ?? `${s.rank}.`; + sections.push(`${medal} ${s.teamName} — ${Math.round(s.totalPoints)} pts${delta}`); + } + + const MAX_DESCRIPTION = 4096; + let description = sections.join("\n"); + if (description.length > MAX_DESCRIPTION) { + description = description.slice(0, MAX_DESCRIPTION - 3) + "..."; + } + + await sendDiscordWebhook(webhookUrl, { + embeds: [ + { + title: `📊 Standings Update — ${seasonName}`, + description, + color: 0x5865f2, // Discord blurple + footer: { text: "brackt.com" }, + }, + ], + }); } diff --git a/database/schema.ts b/database/schema.ts index 5ce457b..fb50d91 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -99,7 +99,7 @@ export const leagues = pgTable("leagues", { createdBy: varchar("created_by", { length: 255 }).notNull(), // Clerk user ID currentSeasonId: uuid("current_season_id"), // References the active season isPublicDraftBoard: boolean("is_public_draft_board").notNull().default(false), - discordWebhookUrl: varchar("discord_webhook_url", { length: 500 }), + discordWebhookUrl: text("discord_webhook_url"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); diff --git a/drizzle/0048_eager_songbird.sql b/drizzle/0048_eager_songbird.sql new file mode 100644 index 0000000..a933eee --- /dev/null +++ b/drizzle/0048_eager_songbird.sql @@ -0,0 +1 @@ +ALTER TABLE "leagues" ADD COLUMN "discord_webhook_url" text; \ No newline at end of file diff --git a/drizzle/meta/0048_snapshot.json b/drizzle/meta/0048_snapshot.json index 90a66d4..ce375f0 100644 --- a/drizzle/meta/0048_snapshot.json +++ b/drizzle/meta/0048_snapshot.json @@ -1,5 +1,5 @@ { - "id": "073631b4-6a21-4bfe-b287-ec752c45fe05", + "id": "b36f2af2-46ba-48e1-9c59-eb38bcbfdedc", "prevId": "16019fc9-f516-4d33-86ba-c2971ca492c0", "version": "7", "dialect": "postgresql", @@ -681,7 +681,7 @@ }, "discord_webhook_url": { "name": "discord_webhook_url", - "type": "varchar(500)", + "type": "text", "primaryKey": false, "notNull": false }, diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index df3dc9c..db0615a 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -341,8 +341,8 @@ { "idx": 48, "version": "7", - "when": 1773769397138, - "tag": "0048_mean_enchantress", + "when": 1773765768905, + "tag": "0048_eager_songbird", "breakpoints": true } ]