Merge dev into update-oauth-docs

This commit is contained in:
Konsti Wohlwend 2025-10-15 04:31:43 -07:00 committed by GitHub
commit a15365bfe7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 116 additions and 51 deletions

View File

@ -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

View File

@ -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": {

View File

@ -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());
}

View File

@ -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,

View File

@ -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',

View File

@ -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,
});
}));

View File

@ -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 ./

View File

@ -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

View File

@ -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<HTMLDivElement>(null);
const searchTimeoutRef = useRef<NodeJS.Timeout>();
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
</div>
{/* Footer */}
<div className="border-t border-fd-border px-3 py-2 text-xs text-fd-muted-foreground flex justify-between items-center">
<span>Use to navigate, Enter to select, Esc to close</span>
<span>
{filteredResults.length} result group{filteredResults.length !== 1 ? 's' : ''}
{selectedPlatformFilter !== 'all' && filteredResults.length > 0 && (
<span className="ml-2 text-fd-primary">
{PLATFORM_NAMES[selectedPlatformFilter as keyof typeof PLATFORM_NAMES]} only
</span>
)}
</span>
<div className="border-t border-fd-border px-3 py-2 text-xs text-fd-muted-foreground">
<div className="flex justify-between items-center mb-2">
<span>Use to navigate, Enter to select, Esc to close</span>
<span>
{filteredResults.length} result group{filteredResults.length !== 1 ? 's' : ''}
{selectedPlatformFilter !== 'all' && filteredResults.length > 0 && (
<span className="ml-2 text-fd-primary">
{PLATFORM_NAMES[selectedPlatformFilter as keyof typeof PLATFORM_NAMES]} only
</span>
)}
</span>
</div>
{/* AI Chat Fallback */}
<div className="flex justify-center items-center gap-2">
<span className="text-fd-muted-foreground">Can&apos;t find what you&apos;re looking for?</span>
<button
onClick={handleOpenAIChat}
className="flex items-center gap-1 px-2 py-1 text-xs rounded-md transition-all duration-300 ease-out relative overflow-hidden text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg"
>
<Sparkles className="h-3 w-3 relative z-10" />
<span className="font-medium relative z-10">Ask AI</span>
</button>
</div>
</div>
</div>
</div>

View File

@ -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 (
<button
onClick={toggleChat}
className="flex items-center justify-center transition-all duration-500 ease-out w-8 h-8 rounded-lg text-sm font-medium relative overflow-hidden text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg"
title={isChatOpen ? 'Close AI chat' : 'Open AI chat'}
>
<Sparkles className="h-4 w-4 relative z-10" />
</button>
);
}
return (
<button
onClick={toggleChat}
className="flex items-center justify-center transition-all duration-500 ease-out w-8 h-8 rounded-lg text-sm font-medium relative overflow-hidden text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg"
className="flex items-center gap-2 transition-all duration-500 ease-out px-2 py-1 rounded-lg text-xs font-medium relative overflow-hidden text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg"
title={isChatOpen ? 'Close AI chat' : 'Open AI chat'}
>
<Sparkles className="h-4 w-4 relative z-10" />
<Sparkles className="h-3 w-3 relative z-10" />
<span className="font-medium relative z-10">AI Chat</span>
</button>
);
}
@ -248,7 +261,7 @@ function HomeNavbar() {
<ThemeToggle compact />
{/* Compact AI Chat Toggle */}
<HomeAIChatToggleButton />
<HomeAIChatToggleButton compact />
{/* Compact User Button */}
<UserButton />

View File

@ -81,18 +81,21 @@ function AIChatToggleButton() {
return null;
}
const { toggleChat } = sidebarContext;
const { toggleChat, isChatOpen } = sidebarContext;
return (
<button
className={cn(
'flex items-center justify-center rounded-md w-8 h-8 text-xs transition-all duration-500 ease-out relative overflow-hidden',
'text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg'
'flex items-center gap-2 rounded-md px-2 py-1 text-xs transition-all duration-500 ease-out relative overflow-hidden',
isChatOpen
? 'text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg'
: 'text-white chat-gradient-active hover:scale-105 hover:brightness-110 hover:shadow-lg'
)}
onClick={toggleChat}
title="AI Chat"
>
<Sparkles className="h-4 w-4 relative z-10" />
<Sparkles className="h-3 w-3 relative z-10" />
<span className="font-medium relative z-10">AI Chat</span>
</button>
);
}
@ -312,7 +315,7 @@ export function SharedHeader({
</div>
{/* Right side - Mobile Menu and Search */}
<div className="flex items-center gap-4 relative z-10">
<div className="flex items-center gap-3 relative z-10">
{/* Search Bar - Responsive sizing */}
{showSearch && (
<>
@ -344,7 +347,7 @@ export function SharedHeader({
</div>
{/* User Button */}
<div className="hidden md:block">
<div className="hidden md:block ml-2">
<UserButton />
</div>