From 2ea250f0814b91b593ffc338c0e3af3d5b7dc17b Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Wed, 22 Apr 2026 08:01:22 +0530 Subject: [PATCH] fix: prefer active config when reading user settings (fixes #271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multiple "select from configs where userId" queries had no ORDER BY, so when a user's database accidentally contained more than one config row for the same user (e.g. from an env-loader insert path or a partial default-config create), SQLite returned a non-deterministic row. In the reported case this caused /api/config to hand back an empty stub while /api/dashboard's repo/org counts came from the populated active row. The dashboard's useConfigStatus hook then saw missing username/ token, treated config as incomplete, and never fetched dashboard data — the UI rendered with all zeros even though 868 repos were sitting in the database, mirroring fine in the background. Add `ORDER BY isActive DESC, updatedAt DESC` before LIMIT 1 to every "fetch the user's config" query so the active and most-recently-updated row consistently wins. Also order env-config-loader's first-user pick by createdAt for deterministic behavior across restarts. Already-safe call sites that explicitly filter on isActive=true or iterate all active configs (cleanup/scheduler/repositories/orgs/cleanup trigger/sync-organization) are left unchanged. Updates the mirror-repo test mock to match the new orderBy().limit() chain. Closes #271 --- src/lib/env-config-loader.ts | 11 +++- src/lib/notification-service.ts | 6 +- src/lib/recovery.ts | 10 ++- src/lib/utils/config-defaults.ts | 6 +- src/pages/api/config/index.ts | 11 +++- src/pages/api/dashboard/index.ts | 7 ++- src/pages/api/github/starred-lists.ts | 5 +- src/pages/api/job/approve-sync.ts | 6 +- src/pages/api/job/mirror-org.ts | 6 +- src/pages/api/job/mirror-repo.test.ts | 83 ++++++++++++++----------- src/pages/api/job/mirror-repo.ts | 6 +- src/pages/api/job/reset-metadata.ts | 5 +- src/pages/api/job/retry-repo.ts | 6 +- src/pages/api/job/schedule-sync-repo.ts | 6 +- src/pages/api/job/sync-repo.ts | 6 +- src/pages/api/rate-limit/index.ts | 5 +- src/pages/api/sync/index.ts | 5 +- src/pages/api/sync/repository.ts | 6 +- 18 files changed, 126 insertions(+), 70 deletions(-) diff --git a/src/lib/env-config-loader.ts b/src/lib/env-config-loader.ts index 825895c..9630e00 100644 --- a/src/lib/env-config-loader.ts +++ b/src/lib/env-config-loader.ts @@ -4,7 +4,7 @@ */ import { db, configs, users } from '@/lib/db'; -import { eq, and } from 'drizzle-orm'; +import { eq, and, sql } from 'drizzle-orm'; import { v4 as uuidv4 } from 'uuid'; import { encrypt } from '@/lib/utils/encryption'; @@ -224,10 +224,12 @@ export async function initializeConfigFromEnv(): Promise { console.log('[ENV Config Loader] Found environment configuration, initializing...'); - // Get the first user (admin user) + // Get the first user (admin user) — deterministic order so we always pick the + // same row across restarts even if multiple users exist. const firstUser = await db .select() .from(users) + .orderBy(sql`${users.createdAt} ASC`) .limit(1); if (firstUser.length === 0) { @@ -237,11 +239,14 @@ export async function initializeConfigFromEnv(): Promise { const userId = firstUser[0].id; - // Check if config already exists for this user + // Check if config already exists for this user — prefer the active config and + // fall back to most-recently-updated so we never write env values into a stale + // inactive stub while the populated active row sits untouched (see issue #271). const existingConfig = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); // Determine mirror strategy based on environment variables or use explicit value diff --git a/src/lib/notification-service.ts b/src/lib/notification-service.ts index 4c8f981..0753e24 100644 --- a/src/lib/notification-service.ts +++ b/src/lib/notification-service.ts @@ -3,7 +3,7 @@ import type { NotificationEvent } from "./providers/ntfy"; import { sendNtfyNotification } from "./providers/ntfy"; import { sendAppriseNotification } from "./providers/apprise"; import { db, configs } from "@/lib/db"; -import { eq } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import { decrypt } from "@/lib/utils/encryption"; function sanitizeTestNotificationError(error: unknown): string { @@ -120,11 +120,13 @@ export async function triggerJobNotification({ return; } - // Fetch user's config from database + // Fetch user's config from database — prefer active and most-recently-updated + // to avoid picking a stale inactive stub when multiple rows exist (see issue #271). const configResults = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); if (configResults.length === 0) { diff --git a/src/lib/recovery.ts b/src/lib/recovery.ts index d0be12a..0bdecd8 100644 --- a/src/lib/recovery.ts +++ b/src/lib/recovery.ts @@ -5,7 +5,7 @@ import { findInterruptedJobs, resumeInterruptedJob } from './helpers'; import { db, repositories, organizations, mirrorJobs, configs } from './db'; -import { eq, and, lt, inArray } from 'drizzle-orm'; +import { eq, and, lt, inArray, sql } from 'drizzle-orm'; import { mirrorGithubRepoToGitea, mirrorGitHubOrgRepoToGiteaOrg, syncGiteaRepo } from './gitea'; import { createGitHubClient } from './github'; import { processWithResilience } from './utils/concurrency'; @@ -216,11 +216,13 @@ async function recoverMirrorJob(job: any, remainingItemIds: string[]) { console.log(`Recovering mirror job ${job.id} with ${remainingItemIds.length} remaining items`); try { - // Get the config for this user with better error handling + // Get the config for this user — prefer active and most-recently-updated + // to avoid picking a stale inactive stub when multiple rows exist (see issue #271). const userConfigs = await db .select() .from(configs) .where(eq(configs.userId, job.userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); if (userConfigs.length === 0) { @@ -347,11 +349,13 @@ async function recoverSyncJob(job: any, remainingItemIds: string[]) { console.log(`Recovering sync job ${job.id} with ${remainingItemIds.length} remaining items`); try { - // Get the config for this user with better error handling + // Get the config for this user — prefer active and most-recently-updated + // to avoid picking a stale inactive stub when multiple rows exist (see issue #271). const userConfigs = await db .select() .from(configs) .where(eq(configs.userId, job.userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); if (userConfigs.length === 0) { diff --git a/src/lib/utils/config-defaults.ts b/src/lib/utils/config-defaults.ts index 3af6be7..c5f0220 100644 --- a/src/lib/utils/config-defaults.ts +++ b/src/lib/utils/config-defaults.ts @@ -1,5 +1,5 @@ import { db, configs } from "@/lib/db"; -import { eq } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; import { encrypt } from "@/lib/utils/encryption"; import { getNextScheduledRun, normalizeTimezone } from "@/lib/utils/schedule-utils"; @@ -25,11 +25,13 @@ export interface DefaultConfigOptions { * Environment variables can override these defaults */ export async function createDefaultConfig({ userId, envOverrides = {} }: DefaultConfigOptions) { - // Check if config already exists + // Check if config already exists — prefer active and most-recently-updated + // to avoid returning a stale inactive stub when multiple rows exist (see issue #271). const existingConfig = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); if (existingConfig.length > 0) { diff --git a/src/pages/api/config/index.ts b/src/pages/api/config/index.ts index ee4df74..69e84ac 100644 --- a/src/pages/api/config/index.ts +++ b/src/pages/api/config/index.ts @@ -1,7 +1,7 @@ import type { APIRoute } from "astro"; import { db, configs, users } from "@/lib/db"; import { v4 as uuidv4 } from "uuid"; -import { eq } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import { createSecureErrorResponse } from "@/lib/utils"; import { mapUiToDbConfig, @@ -83,11 +83,13 @@ export const POST: APIRoute = async ({ request, locals }) => { } } - // Fetch existing config + // Fetch existing config — prefer the active config; fall back to most-recently-updated + // so a stale inactive stub never wins over a populated active row (see issue #271). const existingConfigResult = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); const existingConfig = existingConfigResult[0]; @@ -255,11 +257,14 @@ export const GET: APIRoute = async ({ request, locals }) => { if ("response" in authResult) return authResult.response; const userId = authResult.userId; - // Fetch the configuration for the user + // Fetch the configuration for the user — prefer the active config; fall back to + // most-recently-updated so a stale inactive stub never wins over a populated + // active row (see issue #271). const config = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); if (config.length === 0) { diff --git a/src/pages/api/dashboard/index.ts b/src/pages/api/dashboard/index.ts index 9963988..13f4dce 100644 --- a/src/pages/api/dashboard/index.ts +++ b/src/pages/api/dashboard/index.ts @@ -38,7 +38,12 @@ export const GET: APIRoute = async ({ request, locals }) => { .where(eq(mirrorJobs.userId, userId)) .orderBy(sql`${mirrorJobs.timestamp} DESC`) .limit(10), - db.select().from(configs).where(eq(configs.userId, userId)).limit(1), + db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) + .limit(1), db .select({ value: count() }) .from(repositories) diff --git a/src/pages/api/github/starred-lists.ts b/src/pages/api/github/starred-lists.ts index 40387c2..00e5b70 100644 --- a/src/pages/api/github/starred-lists.ts +++ b/src/pages/api/github/starred-lists.ts @@ -1,6 +1,6 @@ import type { APIRoute } from "astro"; import { db, configs } from "@/lib/db"; -import { eq } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import { createGitHubClient, getGithubStarredListNames, @@ -15,10 +15,13 @@ export const GET: APIRoute = async ({ request, locals }) => { if ("response" in authResult) return authResult.response; const userId = authResult.userId; + // Prefer active and most-recently-updated config to avoid picking a stale + // inactive stub when multiple rows exist (see issue #271). const [config] = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); if (!config) { diff --git a/src/pages/api/job/approve-sync.ts b/src/pages/api/job/approve-sync.ts index 14cec9b..0f9b945 100644 --- a/src/pages/api/job/approve-sync.ts +++ b/src/pages/api/job/approve-sync.ts @@ -1,6 +1,6 @@ import type { APIRoute } from "astro"; import { db, configs, repositories } from "@/lib/db"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, sql } from "drizzle-orm"; import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository"; import { syncGiteaRepoEnhanced } from "@/lib/gitea-enhanced"; import { createSecureErrorResponse } from "@/lib/utils"; @@ -38,11 +38,13 @@ export const POST: APIRoute = async ({ request, locals }) => { ); } - // Fetch config + // Fetch config — prefer active and most-recently-updated to avoid picking + // a stale inactive stub when multiple rows exist (see issue #271). const configResult = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); const config = configResult[0]; diff --git a/src/pages/api/job/mirror-org.ts b/src/pages/api/job/mirror-org.ts index 9342c96..44c49c6 100644 --- a/src/pages/api/job/mirror-org.ts +++ b/src/pages/api/job/mirror-org.ts @@ -1,7 +1,7 @@ import type { APIRoute } from "astro"; import type { MirrorOrgRequest, MirrorOrgResponse } from "@/types/mirror"; import { db, configs, organizations } from "@/lib/db"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, sql } from "drizzle-orm"; import { createGitHubClient } from "@/lib/github"; import { mirrorGitHubOrgToGitea } from "@/lib/gitea"; import { repoStatusEnum } from "@/types/Repository"; @@ -41,11 +41,13 @@ export const POST: APIRoute = async ({ request, locals }) => { ); } - // Fetch config + // Fetch config — prefer active and most-recently-updated to avoid picking + // a stale inactive stub when multiple rows exist (see issue #271). const configResult = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); const config = configResult[0]; diff --git a/src/pages/api/job/mirror-repo.test.ts b/src/pages/api/job/mirror-repo.test.ts index d8b2ca7..18dc384 100644 --- a/src/pages/api/job/mirror-repo.test.ts +++ b/src/pages/api/job/mirror-repo.test.ts @@ -3,6 +3,46 @@ import type { MirrorRepoRequest } from "@/types/mirror"; import { POST } from "./mirror-repo"; // Mock the database module +const mockConfigRow = [{ + id: "config-id", + userId: "user-id", + githubConfig: { + token: "github-token", + preserveOrgStructure: false, + mirrorIssues: false + }, + giteaConfig: { + url: "https://gitea.example.com", + token: "gitea-token", + username: "giteauser" + } +}]; + +const mockRepoRows = [ + { + id: "repo-id-1", + name: "test-repo-1", + visibility: "public", + status: "pending", + organization: null, + lastMirrored: null, + errorMessage: null, + forkedFrom: null, + mirroredLocation: "" + }, + { + id: "repo-id-2", + name: "test-repo-2", + visibility: "public", + status: "pending", + organization: null, + lastMirrored: null, + errorMessage: null, + forkedFrom: null, + mirroredLocation: "" + } +]; + const mockDb = { select: mock(() => ({ from: mock((table: any) => ({ @@ -10,47 +50,14 @@ const mockDb = { // Return config for configs table if (table === mockConfigs) { return { - limit: mock(() => Promise.resolve([{ - id: "config-id", - userId: "user-id", - githubConfig: { - token: "github-token", - preserveOrgStructure: false, - mirrorIssues: false - }, - giteaConfig: { - url: "https://gitea.example.com", - token: "gitea-token", - username: "giteauser" - } - }])) + orderBy: mock(() => ({ + limit: mock(() => Promise.resolve(mockConfigRow)) + })), + limit: mock(() => Promise.resolve(mockConfigRow)) }; } // Return repositories for repositories table - return Promise.resolve([ - { - id: "repo-id-1", - name: "test-repo-1", - visibility: "public", - status: "pending", - organization: null, - lastMirrored: null, - errorMessage: null, - forkedFrom: null, - mirroredLocation: "" - }, - { - id: "repo-id-2", - name: "test-repo-2", - visibility: "public", - status: "pending", - organization: null, - lastMirrored: null, - errorMessage: null, - forkedFrom: null, - mirroredLocation: "" - } - ]); + return Promise.resolve(mockRepoRows); }) })) })) diff --git a/src/pages/api/job/mirror-repo.ts b/src/pages/api/job/mirror-repo.ts index 9cf4908..1cb39ae 100644 --- a/src/pages/api/job/mirror-repo.ts +++ b/src/pages/api/job/mirror-repo.ts @@ -1,7 +1,7 @@ import type { APIRoute } from "astro"; import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror"; import { db, configs, repositories } from "@/lib/db"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, sql } from "drizzle-orm"; import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository"; import { mirrorGithubRepoToGitea, @@ -43,11 +43,13 @@ export const POST: APIRoute = async ({ request, locals }) => { ); } - // Fetch config + // Fetch config — prefer active and most-recently-updated to avoid picking + // a stale inactive stub when multiple rows exist (see issue #271). const configResult = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); const config = configResult[0]; diff --git a/src/pages/api/job/reset-metadata.ts b/src/pages/api/job/reset-metadata.ts index 7203ac9..de1d10d 100644 --- a/src/pages/api/job/reset-metadata.ts +++ b/src/pages/api/job/reset-metadata.ts @@ -1,5 +1,5 @@ import type { APIRoute } from "astro"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, sql } from "drizzle-orm"; import { db, configs, repositories } from "@/lib/db"; import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository"; import type { ResetMetadataRequest, ResetMetadataResponse } from "@/types/reset-metadata"; @@ -35,10 +35,13 @@ export const POST: APIRoute = async ({ request, locals }) => { ); } + // Prefer active and most-recently-updated config to avoid picking a stale + // inactive stub when multiple rows exist (see issue #271). const configResult = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); const config = configResult[0]; diff --git a/src/pages/api/job/retry-repo.ts b/src/pages/api/job/retry-repo.ts index 06d5a33..f8bbe39 100644 --- a/src/pages/api/job/retry-repo.ts +++ b/src/pages/api/job/retry-repo.ts @@ -1,6 +1,6 @@ import type { APIRoute } from "astro"; import { db, configs, repositories } from "@/lib/db"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, sql } from "drizzle-orm"; import { getGiteaRepoOwnerAsync, isRepoPresentInGitea } from "@/lib/gitea"; import { mirrorGithubRepoToGitea, @@ -45,11 +45,13 @@ export const POST: APIRoute = async ({ request, locals }) => { ); } - // Fetch user config + // Fetch user config — prefer active and most-recently-updated to avoid picking + // a stale inactive stub when multiple rows exist (see issue #271). const configResult = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); const config = configResult[0]; diff --git a/src/pages/api/job/schedule-sync-repo.ts b/src/pages/api/job/schedule-sync-repo.ts index 49e8e03..736a8c0 100644 --- a/src/pages/api/job/schedule-sync-repo.ts +++ b/src/pages/api/job/schedule-sync-repo.ts @@ -1,6 +1,6 @@ import type { APIRoute } from "astro"; import { db, configs, repositories } from "@/lib/db"; -import { and, eq, or } from "drizzle-orm"; +import { and, eq, or, sql } from "drizzle-orm"; import { repoStatusEnum, repositoryVisibilityEnum } from "@/types/Repository"; import { isRepoPresentInGitea, syncGiteaRepo } from "@/lib/gitea"; import type { @@ -19,11 +19,13 @@ export const POST: APIRoute = async ({ request, locals }) => { await request.json().catch(() => ({} as ScheduleSyncRepoRequest)); - // Fetch config for the user + // Fetch config for the user — prefer active and most-recently-updated to avoid + // picking a stale inactive stub when multiple rows exist (see issue #271). const configResult = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); const config = configResult[0]; diff --git a/src/pages/api/job/sync-repo.ts b/src/pages/api/job/sync-repo.ts index 7b2cc79..e96e222 100644 --- a/src/pages/api/job/sync-repo.ts +++ b/src/pages/api/job/sync-repo.ts @@ -1,7 +1,7 @@ import type { APIRoute } from "astro"; import type { MirrorRepoRequest } from "@/types/mirror"; import { db, configs, repositories } from "@/lib/db"; -import { and, eq, inArray } from "drizzle-orm"; +import { and, eq, inArray, sql } from "drizzle-orm"; import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository"; import { syncGiteaRepo } from "@/lib/gitea"; import type { SyncRepoResponse } from "@/types/sync"; @@ -38,11 +38,13 @@ export const POST: APIRoute = async ({ request, locals }) => { ); } - // Fetch config + // Fetch config — prefer active and most-recently-updated to avoid picking + // a stale inactive stub when multiple rows exist (see issue #271). const configResult = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); const config = configResult[0]; diff --git a/src/pages/api/rate-limit/index.ts b/src/pages/api/rate-limit/index.ts index 241f21c..f7043e6 100644 --- a/src/pages/api/rate-limit/index.ts +++ b/src/pages/api/rate-limit/index.ts @@ -1,6 +1,6 @@ import type { APIRoute } from "astro"; import { db, rateLimits } from "@/lib/db"; -import { eq, and, desc } from "drizzle-orm"; +import { eq, and, desc, sql } from "drizzle-orm"; import { jsonResponse, createSecureErrorResponse } from "@/lib/utils"; import { RateLimitManager } from "@/lib/rate-limit-manager"; import { createGitHubClient } from "@/lib/github"; @@ -19,10 +19,13 @@ export const GET: APIRoute = async ({ request, locals }) => { try { // If refresh is requested, fetch current rate limit from GitHub if (refresh) { + // Prefer active and most-recently-updated config to avoid picking a stale + // inactive stub when multiple rows exist (see issue #271). const [config] = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); if (config && config.githubConfig?.token) { diff --git a/src/pages/api/sync/index.ts b/src/pages/api/sync/index.ts index de92e0c..e23a4bc 100644 --- a/src/pages/api/sync/index.ts +++ b/src/pages/api/sync/index.ts @@ -1,6 +1,6 @@ import type { APIRoute } from "astro"; import { db, organizations, repositories, configs } from "@/lib/db"; -import { eq, and } from "drizzle-orm"; +import { eq, and, sql } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; import { createMirrorJob } from "@/lib/helpers"; import { @@ -21,10 +21,13 @@ export const POST: APIRoute = async ({ request, locals }) => { const userId = authResult.userId; try { + // Prefer active and most-recently-updated config to avoid picking a stale + // inactive stub when multiple rows exist (see issue #271). const [config] = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); if (!config) { diff --git a/src/pages/api/sync/repository.ts b/src/pages/api/sync/repository.ts index e3b4884..cd0afa6 100644 --- a/src/pages/api/sync/repository.ts +++ b/src/pages/api/sync/repository.ts @@ -2,7 +2,7 @@ import type { APIRoute } from "astro"; import { Octokit } from "@octokit/rest"; import { configs, db, repositories } from "@/lib/db"; import { v4 as uuidv4 } from "uuid"; -import { and, eq } from "drizzle-orm"; +import { and, eq, sql } from "drizzle-orm"; import { type Repository } from "@/lib/db/schema"; import { jsonResponse, createSecureErrorResponse } from "@/lib/utils"; import type { @@ -72,11 +72,13 @@ export const POST: APIRoute = async ({ request, locals }) => { }); } - // Get user's active config + // Get user's active config — prefer active and most-recently-updated to avoid + // picking a stale inactive stub when multiple rows exist (see issue #271). const [config] = await db .select() .from(configs) .where(eq(configs.userId, userId)) + .orderBy(sql`${configs.isActive} DESC`, sql`${configs.updatedAt} DESC`) .limit(1); if (!config) {