diff --git a/.github/workflows/restart-dev-and-test.yaml b/.github/workflows/restart-dev-and-test.yaml index 58469200e..50bda5f82 100644 --- a/.github/workflows/restart-dev-and-test.yaml +++ b/.github/workflows/restart-dev-and-test.yaml @@ -17,6 +17,9 @@ env: jobs: restart-dev-and-test: runs-on: ubicloud-standard-8 + env: + STACK_DIRECT_DATABASE_CONNECTION_STRING: "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:5432/stackframe" + steps: - uses: actions/checkout@v3 diff --git a/apps/backend/package.json b/apps/backend/package.json index 10337e386..1b4dd4b44 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -10,7 +10,7 @@ "dev": "concurrently -n \"dev,codegen,prisma-studio\" -k \"next dev --turbopack --port 8102\" \"pnpm run codegen:watch\" \"pnpm run prisma-studio\"", "build": "pnpm run codegen && next build", "docker-build": "pnpm run codegen && next build --experimental-build-mode compile", - "build-self-host-seed-script": "tsup --config prisma/tsup.config.ts", + "build-self-host-migration-script": "tsup --config scripts/db-migrations.tsup.config.ts", "analyze-bundle": "ANALYZE_BUNDLE=1 pnpm run build", "start": "next start --port 8102", "codegen-prisma": "pnpm run prisma generate", @@ -19,7 +19,7 @@ "codegen-route-info:watch": "pnpm run with-env tsx watch --clear-screen=false scripts/generate-route-info.ts", "codegen": "pnpm run with-env pnpm run generate-migration-imports && pnpm run with-env bash -c 'if [ \"$STACK_ACCELERATE_ENABLED\" = \"true\" ]; then pnpm run prisma generate --no-engine; else pnpm run codegen-prisma; fi' && pnpm run codegen-route-info", "codegen:watch": "concurrently -n \"prisma,docs,route-info,migration-imports\" -k \"pnpm run codegen-prisma:watch\" \"pnpm run watch-docs\" \"pnpm run codegen-route-info:watch\" \"pnpm run generate-migration-imports:watch\"", - "psql-inner": "psql $STACK_DATABASE_CONNECTION_STRING", + "psql-inner": "psql $(echo $STACK_DATABASE_CONNECTION_STRING | sed 's/\\?.*$//')", "psql": "pnpm run with-env pnpm run psql-inner", "prisma-studio": "pnpm run with-env prisma studio --port 8106 --browser none", "prisma": "pnpm run with-env prisma", @@ -34,7 +34,7 @@ "watch-docs": "pnpm run with-env bash -c 'tsx watch --clear-screen=false scripts/generate-openapi-fumadocs.ts && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs'", "generate-openapi-fumadocs": "pnpm run with-env tsx scripts/generate-openapi-fumadocs.ts", "generate-keys": "pnpm run with-env tsx scripts/generate-keys.ts", - "db-seed-script": "pnpm run with-env tsx prisma/seed.ts", + "db-seed-script": "pnpm run db:seed", "verify-data-integrity": "pnpm run with-env tsx scripts/verify-data-integrity.ts" }, "prisma": { diff --git a/apps/backend/prisma/seed.ts b/apps/backend/prisma/seed.ts index 45aea975a..84971f82c 100644 --- a/apps/backend/prisma/seed.ts +++ b/apps/backend/prisma/seed.ts @@ -10,7 +10,7 @@ import { errorToNiceString, throwErr } from '@stackframe/stack-shared/dist/utils const globalPrisma = new PrismaClient(); -async function seed() { +export async function seed() { console.log('Seeding database...'); // Optional default admin user @@ -473,9 +473,11 @@ async function seed() { process.env.STACK_SEED_MODE = 'true'; -seed().catch(async (e) => { - console.error(errorToNiceString(e)); - await globalPrisma.$disconnect(); - process.exit(1); - // eslint-disable-next-line @typescript-eslint/no-misused-promises -}).finally(async () => await globalPrisma.$disconnect()); +if (require.main === module) { + seed().catch(async (e) => { + console.error(errorToNiceString(e)); + await globalPrisma.$disconnect(); + process.exit(1); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + }).finally(async () => await globalPrisma.$disconnect()); +} diff --git a/apps/backend/scripts/db-migrations.ts b/apps/backend/scripts/db-migrations.ts index 461cfdc46..bcab4e27b 100644 --- a/apps/backend/scripts/db-migrations.ts +++ b/apps/backend/scripts/db-migrations.ts @@ -4,6 +4,7 @@ import { globalPrismaClient, globalPrismaSchema, sqlQuoteIdent } from "@/prisma- import { Prisma } from "@prisma/client"; import { execSync } from "child_process"; import * as readline from 'readline'; +import { seed } from "../prisma/seed"; const dropSchema = async () => { await globalPrismaClient.$executeRaw(Prisma.sql`DROP SCHEMA ${sqlQuoteIdent(globalPrismaSchema)} CASCADE`); @@ -12,10 +13,6 @@ const dropSchema = async () => { await globalPrismaClient.$executeRaw(Prisma.sql`GRANT ALL ON SCHEMA ${sqlQuoteIdent(globalPrismaSchema)} TO public`); }; -const seed = async () => { - execSync('pnpm run db-seed-script', { stdio: 'inherit' }); -}; - const promptDropDb = async () => { const rl = readline.createInterface({ input: process.stdin, diff --git a/apps/backend/prisma/tsup.config.ts b/apps/backend/scripts/db-migrations.tsup.config.ts similarity index 82% rename from apps/backend/prisma/tsup.config.ts rename to apps/backend/scripts/db-migrations.tsup.config.ts index 4845065a1..c3c014a10 100644 --- a/apps/backend/prisma/tsup.config.ts +++ b/apps/backend/scripts/db-migrations.tsup.config.ts @@ -6,10 +6,10 @@ const customNoExternal = new Set([ ...Object.keys(packageJson.dependencies), ]); -// tsup config to build the self-hosting seed script so it can be +// tsup config to build the self-hosting migration script so it can be // run in the Docker container with no extra dependencies. export default defineConfig({ - entry: ['prisma/seed.ts'], + entry: ['scripts/db-migrations.ts'], format: ['cjs'], outDir: 'dist', target: 'node22', diff --git a/apps/backend/src/auto-migrations/auto-migration.tests.ts b/apps/backend/src/auto-migrations/auto-migration.tests.ts index 386022b35..305692fb3 100644 --- a/apps/backend/src/auto-migrations/auto-migration.tests.ts +++ b/apps/backend/src/auto-migrations/auto-migration.tests.ts @@ -8,14 +8,17 @@ const TEST_DB_PREFIX = 'stack_auth_test_db'; const getTestDbURL = (testDbName: string) => { // @ts-ignore - ImportMeta.env is provided by Vite const base = import.meta.env.STACK_DIRECT_DATABASE_CONNECTION_STRING.replace(/\/[^/]*$/, ''); + // @ts-ignore - ImportMeta.env is provided by Vite + const query = import.meta.env.STACK_DIRECT_DATABASE_CONNECTION_STRING.split('?')[1] ?? ''; return { full: `${base}/${testDbName}`, base, + query, }; }; -const applySql = async (options: { sql: string | string[], fullDbURL: string }) => { - const sql = postgres(options.fullDbURL); +const applySql = async (options: { sql: string | string[], dbUrl: string }) => { + const sql = postgres(options.dbUrl); try { for (const query of Array.isArray(options.sql) ? options.sql : [options.sql]) { @@ -31,12 +34,12 @@ const setupTestDatabase = async () => { const randomSuffix = Math.random().toString(16).substring(2, 12); const testDbName = `${TEST_DB_PREFIX}_${randomSuffix}`; const dbURL = getTestDbURL(testDbName); - await applySql({ sql: `CREATE DATABASE ${testDbName}`, fullDbURL: dbURL.base }); + await applySql({ sql: `CREATE DATABASE ${testDbName}`, dbUrl: dbURL.base }); const prismaClient = new PrismaClient({ datasources: { db: { - url: dbURL.full, + url: `${dbURL.full}?${dbURL.query}`, }, }, }); @@ -63,7 +66,7 @@ const teardownTestDatabase = async (prismaClient: PrismaClient, testDbName: stri `, `DROP DATABASE IF EXISTS ${testDbName}` ], - fullDbURL: dbURL.base + dbUrl: dbURL.base }); // Wait a bit to ensure connections are terminated @@ -234,12 +237,20 @@ import.meta.vitest?.test("applies migrations concurrently with 20 concurrent mig expect(result.length).toBe(1); expect(result[0].name).toBe('test_value'); }), { - timeout: 40_000, + timeout: 400_000, }); +// TODO: this test, or a variant of it, might fail because the migrations waiting for locks are exhausting all +// connections; the RUN_OUTSIDE_TRANSACTION_SENTINEL is then not able to run its own migration outside of the +// transaction +// +// The fix would be to only *try* acquiring the migration lock when we apply a migration, and if it fails to acquire, we +// wait *outside* of the transaction so it doesn't exhaust all connections +import.meta.vitest?.test.todo("applies migrations concurrently with 20 concurrent migrations with RUN_OUTSIDE_TRANSACTION_SENTINEL"); + import.meta.vitest?.test("applies migration with a DB previously migrated with prisma", runTest(async ({ expect, prismaClient, dbURL }) => { - await applySql({ sql: examplePrismaBasedInitQueries, fullDbURL: dbURL.full }); + await applySql({ sql: examplePrismaBasedInitQueries, dbUrl: dbURL.full }); const result = await applyMigrations({ prismaClient, migrationFiles: examplePrismaBasedMigrationFiles, schema: 'public' }); expect(result.newlyAppliedMigrationNames).toEqual(['20250314215050_age']); @@ -302,6 +313,7 @@ import.meta.vitest?.test("applies migration while running an interactive transac expect(result[0].name).toBe('test_value'); }, { isolationLevel: undefined, + timeout: 15_000, }); })); diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile index 0a2d31713..c008a89f8 100644 --- a/docker/server/Dockerfile +++ b/docker/server/Dockerfile @@ -29,6 +29,7 @@ RUN tsx ./scripts/generate-sdks.ts RUN turbo prune --scope=@stackframe/stack-backend --scope=@stackframe/stack-dashboard --docker + # Build stage FROM base AS builder @@ -55,7 +56,9 @@ ENV NEXT_CONFIG_OUTPUT=standalone RUN pnpm turbo run docker-build --filter=@stackframe/stack-backend... --filter=@stackframe/stack-dashboard... # Build the self-host seed script -RUN cd apps/backend && pnpm build-self-host-seed-script +RUN cd apps/backend && pnpm build-self-host-migration-script + + # Final image FROM node:${NODE_VERSION}-slim @@ -68,14 +71,11 @@ RUN apt-get update && \ apt-get install -y openssl socat && \ rm -rf /var/lib/apt/lists -# Install Prisma CLI globally so we can run migrations on startup -RUN npm i -g prisma - # Copy built backend COPY --from=builder --chown=node:node /app/apps/backend/.next/standalone ./ COPY --from=builder --chown=node:node /app/apps/backend/.next/static ./apps/backend/.next/static COPY --from=builder --chown=node:node /app/apps/backend/prisma ./apps/backend/prisma -COPY --from=builder --chown=node:node /app/apps/backend/dist/seed.js ./apps/backend +COPY --from=builder --chown=node:node /app/apps/backend/dist ./apps/backend/dist # Copy built dashboard COPY --from=builder --chown=node:node /app/apps/dashboard/.next/standalone ./ diff --git a/docker/server/entrypoint.sh b/docker/server/entrypoint.sh index d2b9a7bc1..1ae2924cb 100644 --- a/docker/server/entrypoint.sh +++ b/docker/server/entrypoint.sh @@ -37,7 +37,9 @@ if [ "$STACK_SKIP_MIGRATIONS" = "true" ]; then echo "Skipping migrations." else echo "Running migrations..." - pnpm run db:migrate + cd apps/backend + node dist/db-migrations.js migrate + cd ../.. fi if [ "$STACK_SKIP_SEED_SCRIPT" = "true" ]; then @@ -45,7 +47,7 @@ if [ "$STACK_SKIP_SEED_SCRIPT" = "true" ]; then else echo "Running seed script..." cd apps/backend - node seed.js + node dist/db-migrations.js seed cd ../.. fi diff --git a/docs/src/components/layout/custom-search-dialog.tsx b/docs/src/components/layout/custom-search-dialog.tsx index 8d3ad7aa4..2e4ca3d60 100644 --- a/docs/src/components/layout/custom-search-dialog.tsx +++ b/docs/src/components/layout/custom-search-dialog.tsx @@ -1,9 +1,10 @@ 'use client'; -import { AlignLeft, ChevronDown, ExternalLink, FileText, Hash, Search, X } from 'lucide-react'; +import { AlignLeft, ChevronDown, ExternalLink, FileText, Hash, Search, Sparkles, X } from 'lucide-react'; import Link from 'next/link'; import { useCallback, useEffect, useRef, useState } from 'react'; import { cn } from '../../lib/cn'; +import { useSidebar } from '../layouts/sidebar-context'; // Platform colors matching your theme const PLATFORM_COLORS = { @@ -137,10 +138,28 @@ export function CustomSearchDialog({ open, onOpenChange }: CustomSearchDialogPro const dropdownRef = useRef(null); const searchTimeoutRef = useRef(); + const sidebarContext = useSidebar(); // Available platforms for the dropdown const availablePlatforms = ['all', 'next', 'react', 'js', 'python', 'api']; + // Handle AI chat opening + const handleOpenAIChat = () => { + onOpenChange(false); // Close search dialog first + if (!sidebarContext) { + return; + } + + const { toggleChat } = sidebarContext; + + // Small delay to ensure search dialog closes smoothly + setTimeout(() => { + if (!sidebarContext.isChatOpen) { + toggleChat(); + } + }, 100); + }; + // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -436,16 +455,30 @@ export function CustomSearchDialog({ open, onOpenChange }: CustomSearchDialogPro {/* Footer */} -
- Use ↑↓ to navigate, Enter to select, Esc to close - - {filteredResults.length} result group{filteredResults.length !== 1 ? 's' : ''} - {selectedPlatformFilter !== 'all' && filteredResults.length > 0 && ( - - • {PLATFORM_NAMES[selectedPlatformFilter as keyof typeof PLATFORM_NAMES]} only - - )} - +
+
+ Use ↑↓ to navigate, Enter to select, Esc to close + + {filteredResults.length} result group{filteredResults.length !== 1 ? 's' : ''} + {selectedPlatformFilter !== 'all' && filteredResults.length > 0 && ( + + • {PLATFORM_NAMES[selectedPlatformFilter as keyof typeof PLATFORM_NAMES]} only + + )} + +
+ + {/* AI Chat Fallback */} +
+ Can't find what you're looking for? + +
diff --git a/docs/src/components/layouts/home-layout.tsx b/docs/src/components/layouts/home-layout.tsx index 3de8c5941..4f4772960 100644 --- a/docs/src/components/layouts/home-layout.tsx +++ b/docs/src/components/layouts/home-layout.tsx @@ -45,20 +45,33 @@ function StackAuthLogo() { } // AI Chat Toggle Button for Home Layout -function HomeAIChatToggleButton() { +function HomeAIChatToggleButton({ compact = false }: { compact?: boolean }) { const sidebarContext = useSidebar(); const { isChatOpen, toggleChat } = sidebarContext || { isChatOpen: false, toggleChat: () => {}, }; + if (compact) { + return ( + + ); + } + return ( ); } @@ -248,7 +261,7 @@ function HomeNavbar() { {/* Compact AI Chat Toggle */} - + {/* Compact User Button */} diff --git a/docs/src/components/layouts/shared-header.tsx b/docs/src/components/layouts/shared-header.tsx index 082a3b56d..6f0d7d777 100644 --- a/docs/src/components/layouts/shared-header.tsx +++ b/docs/src/components/layouts/shared-header.tsx @@ -81,18 +81,21 @@ function AIChatToggleButton() { return null; } - const { toggleChat } = sidebarContext; + const { toggleChat, isChatOpen } = sidebarContext; return ( ); } @@ -312,7 +315,7 @@ export function SharedHeader({ {/* Right side - Mobile Menu and Search */} -
+
{/* Search Bar - Responsive sizing */} {showSearch && ( <> @@ -344,7 +347,7 @@ export function SharedHeader({
{/* User Button */} -
+