diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ad7643361..da24f7f3d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,5 +34,8 @@ jobs: - name: Run tests run: bun test + - name: Test migrations with demo seed + run: cd apps/mesh && SEED=demo bun run migrate + - name: Run knip run: bun run knip diff --git a/apps/mesh/migrations/seeds/demo.ts b/apps/mesh/migrations/seeds/demo.ts new file mode 100644 index 0000000000..344bef7a89 --- /dev/null +++ b/apps/mesh/migrations/seeds/demo.ts @@ -0,0 +1,11 @@ +/** + * Demo Seed + * + * Re-export from the refactored demo seed structure. + * All implementation has been moved to the demo/ folder for better organization. + * + * @deprecated Import from './demo/index' directly + */ + +export { seed } from "./demo/index"; +export type { DemoSeedResult } from "./demo/index"; diff --git a/apps/mesh/migrations/seeds/demo/catalog.ts b/apps/mesh/migrations/seeds/demo/catalog.ts new file mode 100644 index 0000000000..9e8f06d03e --- /dev/null +++ b/apps/mesh/migrations/seeds/demo/catalog.ts @@ -0,0 +1,569 @@ +/** + * Demo Catalog + * + * Consolidated catalog of all available: + * - MCP connections (verified and ready to use) + * - Gateway templates + * + * Data sourced from Deco Store API (https://api.decocms.com/mcp/registry) + */ + +import type { Connection, Gateway } from "./seeder"; + +// ============================================================================= +// Well-Known Connections (installed by default in production) +// ============================================================================= + +// Note: Tools are fetched dynamically from the MCP servers (not mocked) +// This matches production behavior where fetchToolsFromMCP is called +export const WELL_KNOWN_CONNECTIONS = { + meshMcp: { + title: "Mesh MCP", + description: "The MCP for the mesh API", + icon: "https://assets.decocache.com/mcp/09e44283-f47d-4046-955f-816d227c626f/app.png", + appName: "@deco/management-mcp", + connectionUrl: "https://mesh-admin.decocms.com/mcp", + connectionToken: null, + metadata: { provider: "deco", decoHosted: true }, + tools: [], // Mesh MCP tools are dynamic, populated at runtime + }, + + mcpRegistry: { + title: "MCP Registry", + description: "Community MCP registry with thousands of handy MCPs", + icon: "https://assets.decocache.com/decocms/cd7ca472-0f72-463a-b0de-6e44bdd0f9b4/mcp.png", + appName: "mcp-registry", + connectionUrl: "https://sites-registry.decocache.com/mcp", + connectionToken: null, + metadata: { provider: "deco", decoHosted: true }, + // Tools will be fetched dynamically from the server + }, + + decoStore: { + title: "Deco Store", + description: "Official deco MCP registry with curated integrations", + icon: "https://assets.decocache.com/decocms/00ccf6c3-9e13-4517-83b0-75ab84554bb9/596364c63320075ca58483660156b6d9de9b526e.png", + appName: "deco-registry", + connectionUrl: "https://api.decocms.com/mcp/registry", + connectionToken: null, + metadata: { provider: "deco", decoHosted: true }, + // Tools will be fetched dynamically from the server + }, +} as const satisfies Record; + +export type WellKnownConnectionKey = keyof typeof WELL_KNOWN_CONNECTIONS; + +// ============================================================================= +// Connection Catalog (data from Deco Store) +// ============================================================================= + +export const CONNECTIONS = { + // Development & Infrastructure + github: { + title: "GitHub", + description: + "GitHub MCP Server Official - Interact with GitHub platform through natural language. Manage repositories, issues, pull requests, analyze code, and automate workflows. This is the official MCP server from GitHub.", + icon: "https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png", + appName: "github/github-mcp-server", + connectionUrl: "https://api.githubcopilot.com/mcp/", + connectionToken: null, + metadata: { provider: "github", requiresOAuth: true, official: true }, + }, + + vercel: { + title: "Vercel", + description: "Frontend deployment and preview environments", + icon: "https://vercel.com/favicon.ico", + appName: "Vercel", + connectionUrl: "https://mcp.vercel.com", + connectionToken: null, + metadata: { provider: "vercel", requiresOAuth: true, official: true }, + }, + + supabase: { + title: "Supabase", + description: "Backend-as-a-service with real-time capabilities", + icon: "https://supabase.com/favicon.ico", + appName: "Supabase", + connectionUrl: "https://mcp.supabase.com/mcp", + connectionToken: null, + metadata: { provider: "supabase", requiresOAuth: true, official: true }, + }, + + prisma: { + title: "Prisma", + description: "ORM and database toolkit for schema management", + icon: "https://prismalens.vercel.app/header/logo-dark.svg", + appName: "Prisma", + connectionUrl: "https://mcp.prisma.io/sse", + connectionToken: null, + metadata: { provider: "prisma", requiresOAuth: true, official: true }, + }, + + cloudflare: { + title: "Cloudflare", + description: "Manage DNS, Workers, R2, and edge infrastructure", + icon: "https://www.cloudflare.com/favicon.ico", + appName: "Cloudflare", + connectionUrl: "https://mcp.cloudflare.com/sse", + connectionToken: null, + metadata: { provider: "cloudflare", requiresApiKey: true, official: true }, + }, + + aws: { + title: "AWS", + description: "Amazon Web Services cloud infrastructure management", + icon: "https://assets.decocache.com/mcp/ece686cd-c380-41e8-97c8-34616a3bf5ba/AWS.svg", + appName: "deco/aws", + connectionUrl: "https://api.decocms.com/apps/deco/aws/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + // AI & LLM + openrouter: { + title: "OpenRouter", + description: + "Access 100+ LLM models from a single API with unified pricing", + icon: "https://assets.decocache.com/decocms/b2e2f64f-6025-45f7-9e8c-3b3ebdd073d8/openrouter_logojpg.jpg", + appName: "deco/openrouter", + connectionUrl: "https://sites-openrouter.decocache.com/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + perplexity: { + title: "Perplexity", + description: "AI-powered search and research assistant", + icon: "https://assets.decocache.com/mcp/1b3b7880-e7a5-413b-8db2-601e84b22bcd/Perplexity.svg", + appName: "deco/mcp-perplexity", + connectionUrl: "https://api.decocms.com/apps/deco/mcp-perplexity/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + elevenlabs: { + title: "ElevenLabs", + description: "AI voice generation and text-to-speech", + icon: "https://assets.decocache.com/mcp/d5b8b14e-7611-4cdd-8453-cad6a4c23703/ElevenLabs.svg", + appName: "deco/elevenlabs", + connectionUrl: "https://api.decocms.com/apps/deco/elevenlabs/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + // Google Workspace + gmail: { + title: "Gmail", + description: "Send, read, and manage emails in Gmail", + icon: "https://assets.decocache.com/mcp/b4dbd04f-2d03-4e29-a881-f924f5946c4e/Gmail.svg", + appName: "deco/google-gmail", + connectionUrl: "https://api.decocms.com/apps/deco/google-gmail/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + googleCalendar: { + title: "Google Calendar", + description: + "Integrate and manage your Google Calendar. Create, edit and delete events, check availability and sync your calendars.", + icon: "https://assets.decocache.com/mcp/b5fffe71-647a-461c-aa39-3da07b86cc96/Google-Meets.svg", + appName: "deco/google-calendar", + connectionUrl: "https://sites-google-calendar.decocache.com/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + googleSheets: { + title: "Google Sheets", + description: "Create and edit spreadsheets, manage data", + icon: "https://assets.decocache.com/mcp/0b05c082-ce9d-4879-9258-1acbecf9bf68/Google-Sheets.svg", + appName: "deco/google-sheets", + connectionUrl: "https://api.decocms.com/apps/deco/google-sheets/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + googleDocs: { + title: "Google Docs", + description: "Read, write, and edit content in Google Docs.", + icon: "https://assets.decocache.com/mcp/e0a00fae-ba76-487a-9f62-7b21bbb08d50/Google-Docs.svg", + appName: "deco/google-docs", + connectionUrl: "https://api.decocms.com/apps/deco/google-docs/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + googleDrive: { + title: "Google Drive", + description: "Store, share, and manage files in the cloud", + icon: "https://assets.decocache.com/mcp/bc609f7d-e7c7-433d-b432-93639c5c84bf/Google-Drive.svg", + appName: "deco/google-drive", + connectionUrl: "https://api.decocms.com/apps/deco/google-drive/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + googleTagManager: { + title: "Google Tag Manager", + description: "Manage marketing tags and tracking pixels", + icon: "https://img.icons8.com/color/1200/google-tag-manager.jpg", + appName: "deco/google-tag-manager", + connectionUrl: "https://sites-google-tag-manager.decocache.com/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + youtube: { + title: "YouTube", + description: "Manage YouTube videos and channels", + icon: "https://assets.decocache.com/mcp/cac50532-150e-437d-a996-91fd9a0c115e/YouTube.svg", + appName: "deco/google-youtube", + connectionUrl: "https://api.decocms.com/apps/deco/google-youtube/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + // Communication + discord: { + title: "Discord Bot", + description: + "Discord Bot integration for sending messages, managing channels, and server moderation", + icon: "https://support.discord.com/hc/user_images/PRywUXcqg0v5DD6s7C3LyQ.jpeg", + appName: "deco/discordbot", + connectionUrl: "https://api.decocms.com/apps/deco/discordbot/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + discordWebhook: { + title: "Discord Webhook", + description: "Send rich, formatted messages to Discord channels.", + icon: "https://assets.decocache.com/mcp/a626d828-e641-4931-8557-850276e91702/DiscordWebhook.svg", + appName: "deco/discohook", + connectionUrl: "https://api.decocms.com/apps/deco/discohook/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + slack: { + title: "Slack", + description: "Send messages and manage Slack workspaces", + icon: "https://assets.decocache.com/mcp/f7e005a9-1c53-48f7-989b-955baa422be1/Slack.svg", + appName: "deco/slack", + connectionUrl: "https://api.decocms.com/apps/deco/slack/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + resend: { + title: "Resend", + description: "Send transactional emails with modern API", + icon: "https://assets.decocache.com/mcp/932e4c3a-6045-40af-9fd1-42894bdd138e/Resend.svg", + appName: "deco/resend", + connectionUrl: "https://api.decocms.com/apps/deco/resend/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + // Productivity & Documentation + notion: { + title: "Notion", + description: "Manage pages and databases in Notion workspaces", + icon: "https://www.notion.so/images/logo-ios.png", + appName: "Notion", + connectionUrl: "https://mcp.notion.com/mcp", + connectionToken: null, + metadata: { provider: "notion", requiresOAuth: true, official: true }, + }, + + grain: { + title: "Grain", + description: "Meeting recording, transcription, and compliance archival", + icon: "https://assets.decocache.com/mcp/1bfc7176-e7be-487c-83e6-4b9e970a8e10/Grain.svg", + appName: "grain/grain-mcp", + connectionUrl: "https://api.grain.com/_/mcp", + connectionToken: null, + metadata: { provider: "grain", official: true }, + }, + + airtable: { + title: "Airtable", + description: "Manage databases, records, and collaborative workflows", + icon: "https://assets.decocache.com/mcp/e724f447-3b98-46c4-9194-6b79841305a2/Airtable.svg", + appName: "deco/airtable", + connectionUrl: "https://api.decocms.com/apps/deco/airtable/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + jira: { + title: "Jira", + description: "Project management and issue tracking", + icon: "https://assets.decocache.com/mcp/7bae17a9-cfdb-4969-99ca-436b7a4dcf40/Jira.svg", + appName: "deco/jira", + connectionUrl: "https://api.decocms.com/apps/deco/jira/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + hubspot: { + title: "HubSpot", + description: "CRM, marketing, and sales automation", + icon: "https://www.hubspot.com/hubfs/HubSpot_Logos/HubSpot-Inversed-Favicon.png", + appName: "deco/hubspot", + connectionUrl: "https://api.decocms.com/apps/deco/hubspot/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + // Automation & Scraping + apify: { + title: "Apify", + description: "Web scraping and automation for data collection", + icon: "https://assets.decocache.com/mcp/4eda8c60-503f-4001-9edb-89de961ab7f0/Apify.svg", + appName: "deco/apify", + connectionUrl: "https://api.decocms.com/apps/deco/apify/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + browserUse: { + title: "Browser Use", + description: "Browser automation for testing and scraping", + icon: "https://assets.decocache.com/mcp/1a7a2573-023c-43ed-82a2-95d77adca3db/Browser-Use.svg", + appName: "deco/browser-use", + connectionUrl: "https://api.decocms.com/apps/deco/browser-use/mcp", + connectionToken: null, + metadata: { provider: "deco", decoHosted: true }, + }, + + // Payments + stripe: { + title: "Stripe", + description: "Payment processing and subscription management", + icon: "https://stripe.com/favicon.ico", + appName: "Stripe", + connectionUrl: "https://mcp.stripe.com/", + connectionToken: null, + metadata: { provider: "stripe", requiresOAuth: true, official: true }, + }, + + // Brazilian APIs + brasilApi: { + title: "Brasil API", + description: "CEP, CNPJ, banks, holidays, and Brazilian public data", + icon: "https://assets.decocache.com/mcp/bd684c47-0525-4659-a298-97fa60ba24f1/BrasilAPI.svg", + appName: "deco/brasilapi", + connectionUrl: "https://api.decocms.com/apps/deco/brasilapi/mcp", + connectionToken: null, + metadata: { provider: "deco", decoHosted: true }, + }, + + queridoDiario: { + title: "Querido Diário", + description: "Access Brazilian official gazettes data", + icon: "https://assets.decocache.com/mcp/0bb451a6-db7c-4f9a-9720-8f87b8898da5/QueridoDirio.svg", + appName: "deco/querido-diario", + connectionUrl: "https://api.decocms.com/apps/deco/querido-diario/mcp", + connectionToken: null, + metadata: { provider: "deco", decoHosted: true }, + }, + + datajud: { + title: "Datajud", + description: "Brazilian judicial data from CNJ", + icon: "https://www.cnj.jus.br/wp-content/uploads/2023/09/logo-cnj-portal-20-09-1.svg", + appName: "deco/datajud", + connectionUrl: "https://api.decocms.com/apps/deco/datajud/mcp", + connectionToken: null, + metadata: { provider: "deco", decoHosted: true }, + }, + + // Design & Media + figma: { + title: "Figma", + description: "Design collaboration and prototyping", + icon: "https://assets.decocache.com/mcp/eb714f8a-404b-4b8e-bfc4-f3ce5bde6f51/Figma.svg", + appName: "deco/figma", + connectionUrl: "https://api.decocms.com/apps/deco/figma/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresOAuth: true, decoHosted: true }, + }, + + // E-commerce + shopify: { + title: "Shopify", + description: "E-commerce platform management", + icon: "https://assets.decocache.com/mcp/37122d09-6ceb-4d25-a641-11ce4af8a19b/Shopify.svg", + appName: "deco/shopify-mcp", + connectionUrl: "https://api.decocms.com/apps/deco/shopify-mcp/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + vtex: { + title: "VTEX", + description: "E-commerce platform for Latin America", + icon: "https://assets.decocache.com/mcp/0d6e795b-cefd-4853-9a51-93b346c52c3f/VTEX.svg", + appName: "deco-team/mcp-vtex", + connectionUrl: "https://api.decocms.com/apps/deco-team/mcp-vtex/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, + + // Logistics + superfrete: { + title: "SuperFrete", + description: "Brazilian shipping and logistics", + icon: "https://assets.decocache.com/mcp/2fdb628e-c10c-4fac-8985-b55e383a64b2/SuperFrete.svg", + appName: "deco/superfrete", + connectionUrl: "https://api.decocms.com/apps/deco/superfrete/mcp", + connectionToken: null, + metadata: { provider: "deco", requiresApiKey: true, decoHosted: true }, + }, +} as const satisfies Record; + +export type ConnectionKey = keyof typeof CONNECTIONS; + +// ============================================================================= +// Gateway Templates +// ============================================================================= + +export const GATEWAYS = { + // Default Hub - matches production behavior (exclusion mode = includes all connections) + defaultHub: { + title: "Default Hub", + description: "Auto-created Hub for organization", + toolSelectionStrategy: "passthrough", + toolSelectionMode: "exclusion", + icon: null, + isDefault: true, + connections: [], // Empty with exclusion mode = include all + }, + + simpleDefault: { + title: "My First Gateway", + description: "Default gateway for getting started with MCP tools", + toolSelectionStrategy: "passthrough", + toolSelectionMode: "inclusion", + icon: null, + isDefault: false, + connections: ["github", "openrouter", "notion"], + }, + + llm: { + title: "LLM Gateway", + description: + "AI model access with cost tracking (OpenRouter + GitHub Copilot)", + toolSelectionStrategy: "passthrough", + toolSelectionMode: "inclusion", + icon: "https://assets.decocache.com/decocms/b2e2f64f-6025-45f7-9e8c-3b3ebdd073d8/openrouter_logojpg.jpg", + isDefault: false, + connections: ["openrouter", "github", "perplexity", "elevenlabs"], + }, + + devGateway: { + title: "Development & Deployments", + description: "Complete development workflow: code, deployments, database", + toolSelectionStrategy: "passthrough", + toolSelectionMode: "inclusion", + icon: "https://assets.decocache.com/mcp/02e06fe6-a820-4c42-b960-bce022362702/GitHub.svg", + isDefault: false, + connections: ["github", "vercel", "supabase", "aws", "cloudflare"], + }, + + compliance: { + title: "Knowledge & Compliance", + description: "Documentation, meeting records, and audit trails", + toolSelectionStrategy: "passthrough", + toolSelectionMode: "inclusion", + icon: "https://assets.decocache.com/mcp/1bfc7176-e7be-487c-83e6-4b9e970a8e10/Grain.svg", + isDefault: false, + connections: ["notion", "grain", "airtable", "jira"], + }, + + dataGateway: { + title: "Data & Automation", + description: "Web scraping, data collection, and automation", + toolSelectionStrategy: "passthrough", + toolSelectionMode: "inclusion", + icon: "https://assets.decocache.com/mcp/4eda8c60-503f-4001-9edb-89de961ab7f0/Apify.svg", + isDefault: false, + connections: ["apify", "browserUse", "brasilApi"], + }, + + allAccess: { + title: "All Access Gateway", + description: "Full access to all connected tools (restricted to admins)", + toolSelectionStrategy: "passthrough", + toolSelectionMode: "inclusion", + icon: null, + isDefault: false, + connections: [ + "github", + "openrouter", + "perplexity", + "notion", + "grain", + "stripe", + "vercel", + "supabase", + "apify", + "gmail", + "googleCalendar", + "googleSheets", + "googleDocs", + "googleDrive", + "googleTagManager", + "discord", + "slack", + "airtable", + "brasilApi", + "cloudflare", + "jira", + "hubspot", + "figma", + "shopify", + ], + }, +} as const satisfies Record; + +export type GatewayKey = keyof typeof GATEWAYS; + +// ============================================================================= +// Picker Helpers +// ============================================================================= + +/** + * Get all well-known connections (Mesh MCP, MCP Registry, Deco Store) + * These are installed by default in production environments. + */ +export function getWellKnownConnections(): Record< + WellKnownConnectionKey, + Connection +> { + return { ...WELL_KNOWN_CONNECTIONS }; +} + +export function pickConnections( + keys: K[], +): Record { + const result = {} as Record; + for (const key of keys) { + const conn = CONNECTIONS[key]; + if (conn) result[key] = conn; + } + return result; +} + +export function pickGateways( + keys: K[], +): Record { + const result = {} as Record; + for (const key of keys) { + const gw = GATEWAYS[key]; + if (gw) result[key] = gw; + } + return result; +} diff --git a/apps/mesh/migrations/seeds/demo/index.ts b/apps/mesh/migrations/seeds/demo/index.ts new file mode 100644 index 0000000000..cc67095a31 --- /dev/null +++ b/apps/mesh/migrations/seeds/demo/index.ts @@ -0,0 +1,99 @@ +/** + * Demo Seed + * + * Creates two complete demo environments: + * + * 1. **Onboarding** - Clean slate matching production behavior + * - 1 owner user + * - 3 well-known connections (Mesh MCP, MCP Registry, Deco Store) + * - 1 default gateway + * - No monitoring logs (start from scratch) + * + * 2. **Deco Bank** - Mature corporate banking environment + * - 12 users in realistic hierarchy + * - 10 real MCP connections (all verified) + * - 5 specialized gateways + * - ~2,500 logs over 90 days + * + * Demo Flow: + * 1. Login as carlos.mendes@decobank.com (password: demo123) + * 2. Start with "Onboarding" org -> show first steps + * 3. Switch to "Deco Bank" org -> show mature usage + */ + +import type { Kysely } from "kysely"; +import type { Database } from "../../../src/storage/types"; +import { createBetterAuthTables } from "../../../src/storage/test-helpers"; + +import type { OrgSeedResult } from "./seeder"; +import { cleanupOrgs, createMemberRecord } from "./seeder"; +import { seedOnboarding, ONBOARDING_SLUG } from "./orgs/onboarding"; +import { seedDecoBank, DECO_BANK_SLUG } from "./orgs/deco-bank"; + +// ============================================================================= +// Types +// ============================================================================= + +export interface DemoSeedResult { + onboarding: OrgSeedResult; + decoBank: OrgSeedResult; +} + +// ============================================================================= +// Main Seed Function +// ============================================================================= + +export async function seed(db: Kysely): Promise { + // Create Better Auth tables (not created by Kysely migrations) + await createBetterAuthTables(db); + + // Clean up any existing demo orgs (makes seed idempotent) + await cleanupOrgs(db, [ONBOARDING_SLUG, DECO_BANK_SLUG]); + + // Create Onboarding organization + console.log("🌱 Creating Onboarding organization..."); + const onboarding = await seedOnboarding(db); + console.log(` ✅ Onboarding: ${onboarding.logCount} logs`); + + // Create Deco Bank organization + console.log("🏦 Creating Deco Bank organization..."); + const decoBank = await seedDecoBank(db); + console.log(` ✅ Deco Bank: ${decoBank.logCount} logs`); + + // Cross-org access: Add Deco Bank CTO as member of Onboarding + console.log("🔗 Creating cross-org access..."); + const now = new Date().toISOString(); + + await db + .insertInto("member") + .values( + createMemberRecord( + onboarding.organizationId, + decoBank.userIds.cto!, + "owner", + now, + ), + ) + .execute(); + + const ctoEmail = decoBank.userEmails.cto!; + console.log(` ✅ ${ctoEmail} now has access to both orgs`); + + // Display demo credentials + console.log("\n📋 Demo Credentials:"); + console.log(" Password for all users: demo123\n"); + console.log(" 🌟 RECOMMENDED FOR DEMO (access to both orgs):"); + console.log(` - ${ctoEmail}`); + console.log(""); + console.log(" Onboarding only:"); + console.log(` - ${onboarding.userEmails.admin} (owner)`); + console.log(" Deco Bank only:"); + console.log(` - ${decoBank.userEmails.techLead} (admin)`); + console.log(` - ${decoBank.userEmails.seniorDev1} (member)`); + console.log(` - ... and more users`); + + return { onboarding, decoBank }; +} + +// Re-export types +export type { OrgSeedResult } from "./seeder"; diff --git a/apps/mesh/migrations/seeds/demo/orgs/deco-bank.ts b/apps/mesh/migrations/seeds/demo/orgs/deco-bank.ts new file mode 100644 index 0000000000..ba063f7c9a --- /dev/null +++ b/apps/mesh/migrations/seeds/demo/orgs/deco-bank.ts @@ -0,0 +1,879 @@ +/** + * Deco Bank Organization + * + * Large corporate banking environment simulating 30 days of high-volume usage: + * - 12 users across multiple departments + * - 24 connections (3 well-known + verified MCPs from Deco Store) + * - 6 gateways (Default Hub + 5 specialized) + * - ~1M synthetic + static monitoring logs with realistic activity patterns: + * - Peak hours: 9-11am (morning) and 2-4pm (afternoon) + * - Weekends with 80% reduced activity (visible valleys) + * - Special spike days on deployments (days 2, 7, 14, 21, 28) + * - Night/lunch hours with minimal activity (realistic lows) + */ + +import type { Kysely } from "kysely"; +import type { Database } from "../../../../src/storage/types"; +import type { + OrgConfig, + OrgSeedResult, + OrgUser, + MonitoringLog, +} from "../seeder"; +import { createOrg, TIME, USER_AGENTS } from "../seeder"; +import { + getWellKnownConnections, + pickConnections, + pickGateways, +} from "../catalog"; + +// ============================================================================= +// Configuration +// ============================================================================= + +const EMAIL_DOMAIN = "@decobank.com"; + +const USERS: Record = { + cto: { + role: "admin", + memberRole: "owner", + name: "Carlos Mendes", + email: `carlos.mendes${EMAIL_DOMAIN}`, + }, + techLead: { + role: "admin", + memberRole: "admin", + name: "Ana Silva", + email: `ana.silva${EMAIL_DOMAIN}`, + }, + seniorDev1: { + role: "user", + memberRole: "user", + name: "Pedro Costa", + email: `pedro.costa${EMAIL_DOMAIN}`, + }, + seniorDev2: { + role: "user", + memberRole: "user", + name: "Mariana Santos", + email: `mariana.santos${EMAIL_DOMAIN}`, + }, + midDev1: { + role: "user", + memberRole: "user", + name: "Rafael Oliveira", + email: `rafael.oliveira${EMAIL_DOMAIN}`, + }, + junior: { + role: "user", + memberRole: "user", + name: "Gabriel Lima", + email: `gabriel.lima${EMAIL_DOMAIN}`, + }, + analyst: { + role: "user", + memberRole: "user", + name: "Lucas Fernandes", + email: `lucas.fernandes${EMAIL_DOMAIN}`, + }, + dataEngineer: { + role: "user", + memberRole: "user", + name: "Beatriz Rodrigues", + email: `beatriz.rodrigues${EMAIL_DOMAIN}`, + }, + security: { + role: "admin", + memberRole: "admin", + name: "Roberto Alves", + email: `roberto.alves${EMAIL_DOMAIN}`, + }, + compliance: { + role: "user", + memberRole: "user", + name: "Julia Ferreira", + email: `julia.ferreira${EMAIL_DOMAIN}`, + }, + productManager: { + role: "user", + memberRole: "user", + name: "Fernanda Souza", + email: `fernanda.souza${EMAIL_DOMAIN}`, + }, + qa: { + role: "user", + memberRole: "user", + name: "Ricardo Martins", + email: `ricardo.martins${EMAIL_DOMAIN}`, + }, +}; + +const USER_ACTIVITY_WEIGHTS: Record = { + techLead: 0.18, + seniorDev1: 0.15, + seniorDev2: 0.14, + midDev1: 0.12, + analyst: 0.11, + dataEngineer: 0.1, + junior: 0.08, + security: 0.06, + productManager: 0.04, + qa: 0.01, + cto: 0.01, + compliance: 0.0, +}; + +// Include well-known connections (Mesh MCP, MCP Registry, Deco Store) + business connections +const CONNECTIONS = { + ...getWellKnownConnections(), + ...pickConnections([ + "openrouter", + "github", + "cloudflare", + "aws", + "gmail", + "googleCalendar", + "googleDocs", + "googleDrive", + "googleSheets", + "googleTagManager", + "jira", + "brasilApi", + "apify", + "airtable", + "slack", + "discord", + "discordWebhook", + "figma", + "grain", + "notion", + "perplexity", + ]), +}; + +// Default Hub (production-like) + specialized gateways +const GATEWAYS = { + ...pickGateways([ + "llm", + "devGateway", + "compliance", + "dataGateway", + "allAccess", + ]), + // Override defaultHub to include only well-known connections (production behavior) + defaultHub: { + title: "Default Hub", + description: "Auto-created Hub for organization", + toolSelectionStrategy: "passthrough" as const, + toolSelectionMode: "inclusion" as const, + icon: null, + isDefault: true, + connections: ["meshMcp", "mcpRegistry", "decoStore"], + }, +}; + +// ============================================================================= +// Static Logs - Hand-crafted story moments over 30 days +// ============================================================================= + +const STATIC_LOGS: MonitoringLog[] = [ + // 30 days ago: Monthly planning + { + connectionKey: "notion", + toolName: "create_page", + input: { parent_id: "workspace_root", title: "Monthly OKRs" }, + output: { page_id: "page_monthly_okrs" }, + isError: false, + durationMs: 345, + offsetMs: -30 * TIME.DAY, + userKey: "cto", + userAgent: USER_AGENTS.notionDesktop, + gatewayKey: "compliance", + }, + { + connectionKey: "grain", + toolName: "get_transcript", + input: { meeting_id: "meet_monthly_review" }, + output: { transcript: "Monthly Review...", duration_minutes: 120 }, + isError: false, + durationMs: 1847, + offsetMs: -30 * TIME.DAY, + userKey: "cto", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "compliance", + }, + + // 28 days ago: Security PR + { + connectionKey: "github", + toolName: "create_pull_request", + input: { repo: "payment-gateway", title: "SECURITY: SQL injection patch" }, + output: { number: 1892, state: "open" }, + isError: false, + durationMs: 456, + offsetMs: -28 * TIME.DAY, + userKey: "security", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "devGateway", + }, + + // 25 days ago: Infrastructure check + { + connectionKey: "cloudflare", + toolName: "list_zones", + input: {}, + output: { zones: [{ name: "decobank.com", status: "active" }] }, + isError: false, + durationMs: 234, + offsetMs: -25 * TIME.DAY, + userKey: "techLead", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "devGateway", + }, + + // 23 days ago: AWS resources check + { + connectionKey: "aws", + toolName: "list_s3_buckets", + input: {}, + output: { buckets: [{ name: "decobank-prod-assets" }] }, + isError: false, + durationMs: 345, + offsetMs: -23 * TIME.DAY, + userKey: "seniorDev1", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "devGateway", + }, + + // 22 days ago: AI code review + { + connectionKey: "openrouter", + toolName: "chat_completion", + input: { + model: "anthropic/claude-3.5-sonnet", + messages: [{ role: "user", content: "Review this code..." }], + }, + output: { response: "Security concerns found...", tokens_used: 1847 }, + isError: false, + durationMs: 3456, + offsetMs: -22 * TIME.DAY, + userKey: "seniorDev2", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "llm", + properties: { cost_usd: "0.047" }, + }, + + // 20 days ago: Brasil API integration + { + connectionKey: "brasilApi", + toolName: "get_bank_info", + input: { code: "341" }, + output: { name: "Itaú", fullName: "Itaú Unibanco" }, + isError: false, + durationMs: 456, + offsetMs: -20 * TIME.DAY, + userKey: "analyst", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "dataGateway", + }, + + // 18 days ago: Postmortem + { + connectionKey: "notion", + toolName: "create_page", + input: { title: "Postmortem: Payment Gateway Timeout" }, + output: { page_id: "page_postmortem" }, + isError: false, + durationMs: 432, + offsetMs: -18 * TIME.DAY, + userKey: "techLead", + userAgent: USER_AGENTS.notionDesktop, + gatewayKey: "compliance", + }, + + // 16 days ago: Gmail automation + { + connectionKey: "gmail", + toolName: "send_email", + input: { to: "support@decobank.com", subject: "Weekly Report" }, + output: { id: "msg_123", threadId: "thread_456" }, + isError: false, + durationMs: 567, + offsetMs: -16 * TIME.DAY, + userKey: "analyst", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "allAccess", + }, + + // 14 days ago: LLM cost analysis + { + connectionKey: "openrouter", + toolName: "get_usage_stats", + input: { period: "last_30_days" }, + output: { total_requests: 45678, total_cost: 1234.56 }, + isError: false, + durationMs: 234, + offsetMs: -14 * TIME.DAY, + userKey: "cto", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "llm", + }, + + // 12 days ago: Bug investigation + { + connectionKey: "github", + toolName: "search_code", + input: { query: "processRefund", org: "decobank" }, + output: { + total_count: 8, + items: [{ path: "src/services/refund-processor.ts" }], + }, + isError: false, + durationMs: 567, + offsetMs: -12 * TIME.DAY, + userKey: "seniorDev2", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "devGateway", + }, + + // 10 days ago: Compliance audit + { + connectionKey: "grain", + toolName: "get_transcript", + input: { meeting_id: "meet_compliance_audit" }, + output: { transcript: "BACEN Compliance Audit...", duration_minutes: 87 }, + isError: false, + durationMs: 2134, + offsetMs: -10 * TIME.DAY, + userKey: "compliance", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "compliance", + }, + + // 8 days ago: Slack notification + { + connectionKey: "slack", + toolName: "post_message", + input: { channel: "#engineering", text: "Deployment complete" }, + output: { ok: true, ts: "1234567890.123456" }, + isError: false, + durationMs: 234, + offsetMs: -8 * TIME.DAY, + userKey: "qa", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "allAccess", + }, + + // 5 days ago: AI code generation + { + connectionKey: "openrouter", + toolName: "chat_completion", + input: { + model: "openai/gpt-4-turbo", + messages: [{ role: "user", content: "Generate TypeScript types..." }], + }, + output: { response: "Here are the types...", tokens_used: 687 }, + isError: false, + durationMs: 2345, + offsetMs: -5 * TIME.DAY, + userKey: "junior", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "llm", + }, + + // 3 days ago: Repository maintenance + { + connectionKey: "github", + toolName: "list_repositories", + input: { org: "decobank", type: "private" }, + output: { repositories: [{ name: "payment-gateway" }], total: 523 }, + isError: false, + durationMs: 223, + offsetMs: -2 * TIME.DAY, + userKey: "techLead", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "devGateway", + }, + + // Today: Daily standup + { + connectionKey: "notion", + toolName: "create_page", + input: { parent_id: "db_standups", title: "Daily Standup" }, + output: { page_id: "page_standup_today" }, + isError: false, + durationMs: 267, + offsetMs: -2 * TIME.HOUR, + userKey: "techLead", + userAgent: USER_AGENTS.notionDesktop, + gatewayKey: "compliance", + }, +]; + +// ============================================================================= +// Synthetic Log Generator +// ============================================================================= + +interface ToolTemplate { + toolName: string; + connectionKey: string; + weight: number; + avgDurationMs: number; + durationVariance: number; + sampleInputs: object[]; + sampleOutputs: object[]; + properties?: Record; +} + +const TOOL_TEMPLATES: ToolTemplate[] = [ + // GitHub (25%) + { + toolName: "list_repositories", + connectionKey: "github", + weight: 0.08, + avgDurationMs: 210, + durationVariance: 90, + sampleInputs: [{ org: "decobank" }], + sampleOutputs: [{ repositories: [], total: 523 }], + }, + { + toolName: "create_pull_request", + connectionKey: "github", + weight: 0.05, + avgDurationMs: 340, + durationVariance: 120, + sampleInputs: [{ repo: "payment-gateway", title: "Feature" }], + sampleOutputs: [{ number: 1234, state: "open" }], + }, + { + toolName: "get_pr_status", + connectionKey: "github", + weight: 0.06, + avgDurationMs: 180, + durationVariance: 70, + sampleInputs: [{ repo: "payment-gateway", pr_number: 1234 }], + sampleOutputs: [{ state: "open", checks: { passed: 7 } }], + }, + { + toolName: "search_code", + connectionKey: "github", + weight: 0.03, + avgDurationMs: 420, + durationVariance: 180, + sampleInputs: [{ query: "processPayment" }], + sampleOutputs: [{ items: [], total: 127 }], + }, + + // OpenRouter (22%) + { + toolName: "chat_completion", + connectionKey: "openrouter", + weight: 0.15, + avgDurationMs: 1850, + durationVariance: 1200, + sampleInputs: [{ model: "anthropic/claude-3.5-sonnet" }], + sampleOutputs: [{ response: "...", tokens_used: 856 }], + properties: { cost_usd: "0.018" }, + }, + { + toolName: "list_models", + connectionKey: "openrouter", + weight: 0.04, + avgDurationMs: 180, + durationVariance: 60, + sampleInputs: [{}], + sampleOutputs: [{ models: [], total: 127 }], + }, + { + toolName: "get_usage_stats", + connectionKey: "openrouter", + weight: 0.03, + avgDurationMs: 220, + durationVariance: 80, + sampleInputs: [{ period: "last_7_days" }], + sampleOutputs: [{ total_requests: 12847, total_cost: 2456.78 }], + }, + + // Notion (18%) + { + toolName: "search_pages", + connectionKey: "notion", + weight: 0.07, + avgDurationMs: 320, + durationVariance: 140, + sampleInputs: [{ query: "API documentation" }], + sampleOutputs: [{ results: [], total: 234 }], + }, + { + toolName: "get_page", + connectionKey: "notion", + weight: 0.06, + avgDurationMs: 240, + durationVariance: 90, + sampleInputs: [{ page_id: "page_123" }], + sampleOutputs: [{ title: "Documentation", version: 34 }], + }, + { + toolName: "create_page", + connectionKey: "notion", + weight: 0.03, + avgDurationMs: 380, + durationVariance: 140, + sampleInputs: [{ title: "New Page" }], + sampleOutputs: [{ page_id: "page_new" }], + }, + + // Grain (12%) + { + toolName: "list_meetings", + connectionKey: "grain", + weight: 0.05, + avgDurationMs: 280, + durationVariance: 100, + sampleInputs: [{ date_from: "2024-01-01" }], + sampleOutputs: [{ meetings: [], total: 234 }], + }, + { + toolName: "get_transcript", + connectionKey: "grain", + weight: 0.04, + avgDurationMs: 420, + durationVariance: 180, + sampleInputs: [{ meeting_id: "meet_123" }], + sampleOutputs: [{ transcript: "...", duration_minutes: 87 }], + }, + { + toolName: "search_meetings", + connectionKey: "grain", + weight: 0.03, + avgDurationMs: 380, + durationVariance: 150, + sampleInputs: [{ query: "compliance" }], + sampleOutputs: [{ results: [], total: 67 }], + }, + + // Slack (8%) + { + toolName: "post_message", + connectionKey: "slack", + weight: 0.03, + avgDurationMs: 240, + durationVariance: 90, + sampleInputs: [{ channel: "#engineering", text: "Update..." }], + sampleOutputs: [{ ok: true, ts: "1234567890.123456" }], + }, + { + toolName: "list_channels", + connectionKey: "slack", + weight: 0.02, + avgDurationMs: 180, + durationVariance: 60, + sampleInputs: [{}], + sampleOutputs: [{ channels: [], total: 42 }], + }, + { + toolName: "get_channel_history", + connectionKey: "slack", + weight: 0.02, + avgDurationMs: 210, + durationVariance: 80, + sampleInputs: [{ channel: "C123456" }], + sampleOutputs: [{ messages: [], has_more: true }], + }, + + // Gmail (9%) + { + toolName: "send_email", + connectionKey: "gmail", + weight: 0.04, + avgDurationMs: 340, + durationVariance: 120, + sampleInputs: [{ to: "user@example.com", subject: "Report" }], + sampleOutputs: [{ id: "msg_123", threadId: "thread_456" }], + }, + { + toolName: "list_emails", + connectionKey: "gmail", + weight: 0.03, + avgDurationMs: 280, + durationVariance: 100, + sampleInputs: [{ query: "is:unread" }], + sampleOutputs: [{ messages: [], resultSizeEstimate: 42 }], + }, + { + toolName: "get_email", + connectionKey: "gmail", + weight: 0.02, + avgDurationMs: 210, + durationVariance: 80, + sampleInputs: [{ id: "msg_123" }], + sampleOutputs: [{ subject: "...", from: "..." }], + }, + + // Brasil API (6%) + { + toolName: "get_bank_info", + connectionKey: "brasilApi", + weight: 0.02, + avgDurationMs: 180, + durationVariance: 70, + sampleInputs: [{ code: "341" }], + sampleOutputs: [{ name: "Itaú", fullName: "Itaú Unibanco" }], + }, + { + toolName: "get_cep", + connectionKey: "brasilApi", + weight: 0.02, + avgDurationMs: 150, + durationVariance: 50, + sampleInputs: [{ cep: "01310100" }], + sampleOutputs: [{ street: "Av. Paulista", city: "São Paulo" }], + }, + { + toolName: "get_cnpj", + connectionKey: "brasilApi", + weight: 0.02, + avgDurationMs: 210, + durationVariance: 80, + sampleInputs: [{ cnpj: "00000000000191" }], + sampleOutputs: [{ razao_social: "Banco do Brasil" }], + }, + + // Jira (8%) + { + toolName: "create_issue", + connectionKey: "jira", + weight: 0.03, + avgDurationMs: 340, + durationVariance: 120, + sampleInputs: [{ project: "DECO", summary: "Bug fix" }], + sampleOutputs: [{ id: "10001", key: "DECO-123" }], + }, + { + toolName: "search_issues", + connectionKey: "jira", + weight: 0.03, + avgDurationMs: 280, + durationVariance: 100, + sampleInputs: [{ jql: "project = DECO" }], + sampleOutputs: [{ issues: [], total: 234 }], + }, + { + toolName: "get_issue", + connectionKey: "jira", + weight: 0.02, + avgDurationMs: 210, + durationVariance: 80, + sampleInputs: [{ key: "DECO-123" }], + sampleOutputs: [{ summary: "...", status: "In Progress" }], + }, + + // Apify (5%) + { + toolName: "run_actor", + connectionKey: "apify", + weight: 0.02, + avgDurationMs: 8500, + durationVariance: 3000, + sampleInputs: [{ actor_id: "apify/web-scraper" }], + sampleOutputs: [{ run_id: "run_123", status: "SUCCEEDED" }], + }, + { + toolName: "get_dataset", + connectionKey: "apify", + weight: 0.015, + avgDurationMs: 450, + durationVariance: 180, + sampleInputs: [{ dataset_id: "dataset_rates" }], + sampleOutputs: [{ items: [], total: 1247 }], + }, +]; + +const CONNECTION_TO_GATEWAY: Record = { + github: "devGateway", + cloudflare: "devGateway", + aws: "devGateway", + openrouter: "llm", + perplexity: "llm", + notion: "compliance", + grain: "compliance", + apify: "dataGateway", + brasilApi: "dataGateway", + gmail: "allAccess", + slack: "allAccess", + jira: "allAccess", +}; + +function weightedRandom(items: T[]): T { + const total = items.reduce((sum, item) => sum + item.weight, 0); + let r = Math.random() * total; + for (const item of items) { + r -= item.weight; + if (r <= 0) return item; + } + return items[items.length - 1]!; +} + +function generateSyntheticLogs(targetCount: number): MonitoringLog[] { + const logs: MonitoringLog[] = []; + const userWeights = Object.entries(USER_ACTIVITY_WEIGHTS).map( + ([key, weight]) => ({ key, weight }), + ); + + // Pre-calculate logs per day with realistic distribution + const logsPerDay: number[] = []; + const totalDays = 30; + + for (let day = 0; day < totalDays; day++) { + const now = new Date(); + const targetDate = new Date(now.getTime() - day * TIME.DAY); + const dayOfWeek = targetDate.getDay(); + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + + // Special spike days (deployments, incidents) + const isSpikeDay = [2, 7, 14, 21, 28].includes(day); + + // Calculate base activity for this day + let dayMultiplier = 1.0; + if (isWeekend) { + dayMultiplier = 0.3; // 30% activity on weekends (visible valley) + } else if (isSpikeDay) { + dayMultiplier = 2.5; // 250% activity on spike days (visible peak) + } else { + // Normal weekdays with slight variation + dayMultiplier = 0.85 + Math.random() * 0.3; // 85-115% of base + } + + logsPerDay.push(dayMultiplier); + } + + // Normalize to hit target count + const totalMultiplier = logsPerDay.reduce((sum, m) => sum + m, 0); + const logsPerDayFinal = logsPerDay.map((m) => + Math.floor((m / totalMultiplier) * targetCount), + ); + + // Generate logs for each day + for (let day = 0; day < totalDays; day++) { + const logsForThisDay = logsPerDayFinal[day] || 0; + + for (let logIdx = 0; logIdx < logsForThisDay; logIdx++) { + const template = weightedRandom(TOOL_TEMPLATES); + const userEntry = weightedRandom(userWeights); + const isError = Math.random() < 0.08; + + // Create realistic hourly patterns with clear peaks + // Peak hours: 9-11am (morning peak), 2-4pm (afternoon peak) + // Low activity: 12-1pm (lunch), 5pm-7am (evening/night) + let hourOffset: number; + const hourPattern = Math.random(); + + if (hourPattern < 0.25) { + // 25% - Morning peak (9-11am) - HIGHEST activity + hourOffset = 9 + Math.random() * 2; + } else if (hourPattern < 0.45) { + // 20% - Afternoon peak (2-4pm) - HIGH activity + hourOffset = 14 + Math.random() * 2; + } else if (hourPattern < 0.6) { + // 15% - Early morning ramp-up (7-9am) + hourOffset = 7 + Math.random() * 2; + } else if (hourPattern < 0.75) { + // 15% - Late afternoon (4-6pm) + hourOffset = 16 + Math.random() * 2; + } else if (hourPattern < 0.85) { + // 10% - Lunch dip (12-2pm) - LOWER activity + hourOffset = 12 + Math.random() * 2; + } else if (hourPattern < 0.93) { + // 8% - Evening (6-10pm) - LOW activity + hourOffset = 18 + Math.random() * 4; + } else { + // 7% - Night/early morning (10pm-7am) - MINIMAL activity + const nightHour = Math.random() * 9; + hourOffset = nightHour < 2 ? 22 + nightHour : nightHour - 2; + } + + // Add minute/second variation for spreading within the hour + const minuteOffset = Math.random() * 60; + const secondOffset = Math.random() * 60; + + // Calculate total offset in milliseconds + const randomOffset = + day * TIME.DAY + + hourOffset * TIME.HOUR + + minuteOffset * TIME.MINUTE + + secondOffset * 1000; + + const totalOffsetMs = -randomOffset; + + const duration = + template.avgDurationMs + + (Math.random() - 0.5) * 2 * template.durationVariance; + const input = template.sampleInputs[ + Math.floor(Math.random() * template.sampleInputs.length) + ] as Record; + const output = ( + isError + ? { error: "Internal error" } + : template.sampleOutputs[ + Math.floor(Math.random() * template.sampleOutputs.length) + ] + ) as Record; + + logs.push({ + connectionKey: template.connectionKey, + toolName: template.toolName, + input, + output, + isError, + durationMs: Math.max(50, Math.round(duration)), + offsetMs: totalOffsetMs, + userKey: userEntry.key, + userAgent: "mesh-client/1.0", + gatewayKey: + CONNECTION_TO_GATEWAY[template.connectionKey] || "allAccess", + properties: template.properties, + }); + } + } + + return logs.sort((a, b) => a.offsetMs - b.offsetMs); +} + +// ============================================================================= +// Seed Function +// ============================================================================= + +export const DECO_BANK_SLUG = "deco-bank"; + +export async function seedDecoBank( + db: Kysely, +): Promise { + // Generate ~1M synthetic logs + static story logs + // This simulates high-volume production usage (200k/day peak, 1M over 30 days) + const syntheticLogs = generateSyntheticLogs(1_000_000); + const allLogs = [...STATIC_LOGS, ...syntheticLogs]; + + const config: OrgConfig = { + orgName: "Deco Bank", + orgSlug: DECO_BANK_SLUG, + users: USERS, + apiKeys: [ + { userKey: "cto", name: "CTO API Key" }, + { userKey: "techLead", name: "Tech Lead API Key" }, + ], + connections: CONNECTIONS, + gateways: GATEWAYS, + gatewayConnections: [ + // Default Hub with well-known connections (production-like) + { gatewayKey: "defaultHub", connectionKey: "meshMcp" }, + { gatewayKey: "defaultHub", connectionKey: "mcpRegistry" }, + { gatewayKey: "defaultHub", connectionKey: "decoStore" }, + // LLM Gateway + { gatewayKey: "llm", connectionKey: "openrouter" }, + ], + logs: allLogs, + ownerUserKey: "cto", + }; + + return createOrg(db, config); +} diff --git a/apps/mesh/migrations/seeds/demo/orgs/onboarding.ts b/apps/mesh/migrations/seeds/demo/orgs/onboarding.ts new file mode 100644 index 0000000000..3f1fab183e --- /dev/null +++ b/apps/mesh/migrations/seeds/demo/orgs/onboarding.ts @@ -0,0 +1,412 @@ +/** + * Onboarding Organization + * + * Clean slate organization for onboarding - shows early usage: + * - 1 user (owner) + * - 3 well-known connections (Mesh MCP, MCP Registry, Deco Store) + * - 1 default gateway + * - ~300 monitoring logs over 3 days (light usage pattern) + */ + +import type { Kysely } from "kysely"; +import type { Database } from "../../../../src/storage/types"; +import type { + OrgConfig, + OrgSeedResult, + OrgUser, + MonitoringLog, +} from "../seeder"; +import { createOrg, TIME, USER_AGENTS } from "../seeder"; +import { getWellKnownConnections } from "../catalog"; + +// ============================================================================= +// Configuration +// ============================================================================= + +const EMAIL_DOMAIN = "@onboarding.local"; + +// Single owner user - matches production organization creation +const USERS: Record = { + admin: { + role: "admin", + memberRole: "owner", + name: "Alice Admin", + email: `admin${EMAIL_DOMAIN}`, + }, +}; + +// Only well-known connections - matches production seedOrgDb behavior +const CONNECTIONS = { + ...getWellKnownConnections(), +}; + +// Default Hub with only well-known connections - matches production +const GATEWAYS = { + defaultHub: { + title: "Default Hub", + description: "Auto-created Hub for organization", + toolSelectionStrategy: "passthrough" as const, + toolSelectionMode: "inclusion" as const, + icon: null, + isDefault: true, + connections: ["meshMcp", "mcpRegistry", "decoStore"], + }, +}; + +// ============================================================================= +// Static Logs - Hand-crafted onboarding journey over 3 days +// ============================================================================= + +const STATIC_LOGS: MonitoringLog[] = [ + // 3 days ago: First connection exploration + { + connectionKey: "mcpRegistry", + toolName: "search_servers", + input: { query: "github" }, + output: { results: [], total: 45 }, + isError: false, + durationMs: 234, + offsetMs: -3 * TIME.DAY, + userKey: "admin", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "defaultHub", + }, + { + connectionKey: "decoStore", + toolName: "list_verified_mcps", + input: {}, + output: { mcps: [], total: 127 }, + isError: false, + durationMs: 189, + offsetMs: -3 * TIME.DAY, + userKey: "admin", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "defaultHub", + }, + + // 2 days ago: Exploring Mesh capabilities + { + connectionKey: "meshMcp", + toolName: "list_connections", + input: {}, + output: { connections: [] }, + isError: false, + durationMs: 145, + offsetMs: -2 * TIME.DAY, + userKey: "admin", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "defaultHub", + }, + { + connectionKey: "meshMcp", + toolName: "list_gateways", + input: {}, + output: { gateways: [] }, + isError: false, + durationMs: 123, + offsetMs: -2 * TIME.DAY, + userKey: "admin", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "defaultHub", + }, + { + connectionKey: "mcpRegistry", + toolName: "get_server_details", + input: { server: "github" }, + output: { name: "GitHub MCP", version: "1.0.0" }, + isError: false, + durationMs: 267, + offsetMs: -2 * TIME.DAY, + userKey: "admin", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "defaultHub", + }, + + // Yesterday: Active exploration + { + connectionKey: "decoStore", + toolName: "search_mcps", + input: { query: "notion" }, + output: { results: [], total: 12 }, + isError: false, + durationMs: 198, + offsetMs: -1 * TIME.DAY, + userKey: "admin", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "defaultHub", + }, + { + connectionKey: "meshMcp", + toolName: "get_organization_info", + input: {}, + output: { name: "Onboarding", members: 1 }, + isError: false, + durationMs: 112, + offsetMs: -1 * TIME.DAY, + userKey: "admin", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "defaultHub", + }, + + // Today: Getting started + { + connectionKey: "mcpRegistry", + toolName: "list_categories", + input: {}, + output: { categories: [] }, + isError: false, + durationMs: 156, + offsetMs: -3 * TIME.HOUR, + userKey: "admin", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "defaultHub", + }, +]; + +// ============================================================================= +// Synthetic Log Generator for Onboarding +// ============================================================================= + +interface ToolTemplate { + toolName: string; + connectionKey: string; + weight: number; + avgDurationMs: number; + durationVariance: number; + sampleInputs: object[]; + sampleOutputs: object[]; +} + +// Simple tools for onboarding exploration +const TOOL_TEMPLATES: ToolTemplate[] = [ + // Mesh MCP (40%) + { + toolName: "list_connections", + connectionKey: "meshMcp", + weight: 0.15, + avgDurationMs: 145, + durationVariance: 50, + sampleInputs: [{}], + sampleOutputs: [{ connections: [] }], + }, + { + toolName: "list_gateways", + connectionKey: "meshMcp", + weight: 0.12, + avgDurationMs: 123, + durationVariance: 40, + sampleInputs: [{}], + sampleOutputs: [{ gateways: [] }], + }, + { + toolName: "get_organization_info", + connectionKey: "meshMcp", + weight: 0.08, + avgDurationMs: 112, + durationVariance: 35, + sampleInputs: [{}], + sampleOutputs: [{ name: "Onboarding", members: 1 }], + }, + { + toolName: "get_monitoring_stats", + connectionKey: "meshMcp", + weight: 0.05, + avgDurationMs: 167, + durationVariance: 55, + sampleInputs: [{}], + sampleOutputs: [{ total_calls: 234 }], + }, + + // MCP Registry (35%) + { + toolName: "search_servers", + connectionKey: "mcpRegistry", + weight: 0.15, + avgDurationMs: 234, + durationVariance: 80, + sampleInputs: [ + { query: "github" }, + { query: "notion" }, + { query: "slack" }, + ], + sampleOutputs: [{ results: [], total: 45 }], + }, + { + toolName: "get_server_details", + connectionKey: "mcpRegistry", + weight: 0.1, + avgDurationMs: 267, + durationVariance: 90, + sampleInputs: [{ server: "github" }, { server: "notion" }], + sampleOutputs: [{ name: "GitHub MCP", version: "1.0.0" }], + }, + { + toolName: "list_categories", + connectionKey: "mcpRegistry", + weight: 0.1, + avgDurationMs: 156, + durationVariance: 50, + sampleInputs: [{}], + sampleOutputs: [{ categories: [] }], + }, + + // Deco Store (25%) + { + toolName: "list_verified_mcps", + connectionKey: "decoStore", + weight: 0.12, + avgDurationMs: 189, + durationVariance: 60, + sampleInputs: [{}], + sampleOutputs: [{ mcps: [], total: 127 }], + }, + { + toolName: "search_mcps", + connectionKey: "decoStore", + weight: 0.08, + avgDurationMs: 198, + durationVariance: 65, + sampleInputs: [ + { query: "github" }, + { query: "notion" }, + { query: "slack" }, + ], + sampleOutputs: [{ results: [], total: 12 }], + }, + { + toolName: "get_mcp_details", + connectionKey: "decoStore", + weight: 0.05, + avgDurationMs: 212, + durationVariance: 70, + sampleInputs: [{ id: "github-mcp" }], + sampleOutputs: [{ name: "GitHub MCP", verified: true }], + }, +]; + +function weightedRandom(items: T[]): T { + const total = items.reduce((sum, item) => sum + item.weight, 0); + let r = Math.random() * total; + for (const item of items) { + r -= item.weight; + if (r <= 0) return item; + } + return items[items.length - 1]!; +} + +function generateOnboardingLogs(targetCount: number): MonitoringLog[] { + const logs: MonitoringLog[] = []; + const totalDays = 3; + + // Pre-calculate logs per day - progressive increase (learning curve) + const logsPerDay = [ + Math.floor(targetCount * 0.25), // Day 3: 25% (just starting) + Math.floor(targetCount * 0.35), // Day 2: 35% (exploring more) + Math.floor(targetCount * 0.4), // Day 1: 40% (getting comfortable) + ]; + + // Generate logs for each day + for (let day = 0; day < totalDays; day++) { + const logsForThisDay = logsPerDay[day] || 0; + + for (let logIdx = 0; logIdx < logsForThisDay; logIdx++) { + const template = weightedRandom(TOOL_TEMPLATES); + const isError = Math.random() < 0.03; // Very few errors for onboarding + + // Distributed hourly pattern - more activity during business hours + let hourOffset: number; + const hourPattern = Math.random(); + + if (hourPattern < 0.3) { + // 30% - Morning (9am-12pm) + hourOffset = 9 + Math.random() * 3; + } else if (hourPattern < 0.6) { + // 30% - Afternoon (2pm-5pm) + hourOffset = 14 + Math.random() * 3; + } else if (hourPattern < 0.8) { + // 20% - Late afternoon (5pm-7pm) + hourOffset = 17 + Math.random() * 2; + } else if (hourPattern < 0.9) { + // 10% - Evening (7pm-10pm) + hourOffset = 19 + Math.random() * 3; + } else { + // 10% - Off hours + hourOffset = Math.random() * 24; + } + + const minuteOffset = Math.random() * 60; + const secondOffset = Math.random() * 60; + + const randomOffset = + day * TIME.DAY + + hourOffset * TIME.HOUR + + minuteOffset * TIME.MINUTE + + secondOffset * 1000; + + const totalOffsetMs = -randomOffset; + + const duration = + template.avgDurationMs + + (Math.random() - 0.5) * 2 * template.durationVariance; + const input = template.sampleInputs[ + Math.floor(Math.random() * template.sampleInputs.length) + ] as Record; + const output = ( + isError + ? { error: "Request failed" } + : template.sampleOutputs[ + Math.floor(Math.random() * template.sampleOutputs.length) + ] + ) as Record; + + logs.push({ + connectionKey: template.connectionKey, + toolName: template.toolName, + input, + output, + isError, + durationMs: Math.max(50, Math.round(duration)), + offsetMs: totalOffsetMs, + userKey: "admin", + userAgent: USER_AGENTS.meshClient, + gatewayKey: "defaultHub", + }); + } + } + + return logs.sort((a, b) => a.offsetMs - b.offsetMs); +} + +// ============================================================================= +// Seed Function +// ============================================================================= + +export const ONBOARDING_SLUG = "onboarding"; + +export async function seedOnboarding( + db: Kysely, +): Promise { + // Generate ~300 synthetic logs + static story logs + // This simulates 3 days of light onboarding usage (~100/day average) + const syntheticLogs = generateOnboardingLogs(300); + const allLogs = [...STATIC_LOGS, ...syntheticLogs]; + + const config: OrgConfig = { + orgName: "Onboarding", + orgSlug: ONBOARDING_SLUG, + users: USERS, + apiKeys: [{ userKey: "admin", name: "Onboarding Admin Key" }], + connections: CONNECTIONS, + gateways: GATEWAYS, + gatewayConnections: [ + // Default Hub with well-known connections (production behavior) + { gatewayKey: "defaultHub", connectionKey: "meshMcp" }, + { gatewayKey: "defaultHub", connectionKey: "mcpRegistry" }, + { gatewayKey: "defaultHub", connectionKey: "decoStore" }, + ], + logs: allLogs, + ownerUserKey: "admin", + }; + + return createOrg(db, config); +} diff --git a/apps/mesh/migrations/seeds/demo/seeder.ts b/apps/mesh/migrations/seeds/demo/seeder.ts new file mode 100644 index 0000000000..07a87c4512 --- /dev/null +++ b/apps/mesh/migrations/seeds/demo/seeder.ts @@ -0,0 +1,600 @@ +/** + * Demo Seeder + * + * Consolidated module containing: + * - Type definitions + * - Record factory functions + * - Generic organization seeder + * - Shared configuration + */ + +import type { Kysely } from "kysely"; +import type { Database } from "../../../src/storage/types"; +import { hashPassword } from "better-auth/crypto"; +import { fetchToolsFromMCP } from "../../../src/tools/connection/fetch-tools"; + +// ============================================================================= +// Shared Configuration +// ============================================================================= + +export const CONFIG = { + PASSWORD: "demo123", + USER_AGENT_DEFAULT: "mesh-demo-client/1.0", +} as const; + +export const USER_AGENTS = { + meshClient: "mesh-demo-client/1.0", + cursorAgent: "cursor-agent/0.42.0", + claudeDesktop: "claude-desktop/1.2.0", + vscode: "vscode-mcp/1.0.0", + ghCli: "gh-cli/2.40.0", + slackBot: "slack-mcp-bot/1.0", + notionDesktop: "notion-desktop/3.5.0", + grainDesktop: "grain-desktop/2.1.0", +} as const; + +export const TIME = { + MINUTE: 60 * 1000, + HOUR: 60 * 60 * 1000, + DAY: 24 * 60 * 60 * 1000, +} as const; + +// ============================================================================= +// Type Definitions +// ============================================================================= + +export type MemberRole = "owner" | "admin" | "user"; + +export interface OrgUser { + role: "admin" | "user"; + memberRole: MemberRole; + name: string; + email: string; +} + +export interface Connection { + title: string; + description: string; + icon: string; + appName: string; + connectionUrl: string; + connectionToken: string | null; + metadata: { + provider: string; + requiresOAuth?: boolean; + requiresApiKey?: boolean; + official?: boolean; + decoHosted?: boolean; + }; + tools?: Array<{ + name: string; + description?: string; + inputSchema?: Record; + outputSchema?: Record; + }>; +} + +export interface Gateway { + title: string; + description: string; + toolSelectionStrategy: "passthrough" | "code_execution"; + toolSelectionMode: "inclusion" | "exclusion"; + icon: string | null; + isDefault: boolean; + connections: string[]; +} + +export interface MonitoringLog { + connectionKey: string; + toolName: string; + input: Record; + output: Record; + isError: boolean; + errorMessage?: string; + durationMs: number; + offsetMs: number; + userKey: string; + userAgent: string; + gatewayKey: string | null; + properties?: Record; +} + +export interface OrgConfig { + orgName: string; + orgSlug: string; + users: Record; + apiKeys?: { userKey: string; name: string }[]; + connections: Record; + gateways: Record; + gatewayConnections?: { gatewayKey: string; connectionKey: string }[]; + logs: MonitoringLog[]; + ownerUserKey: string; +} + +export interface OrgSeedResult { + organizationId: string; + organizationName: string; + organizationSlug: string; + userIds: Record; + userEmails: Record; + apiKeys: Record; + connectionIds: Record; + gatewayIds: Record; + logCount: number; +} + +// ============================================================================= +// ID Generator +// ============================================================================= + +export function generateId(prefix: string): string { + return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; +} + +// ============================================================================= +// Record Factories (internal) +// ============================================================================= + +function createUserRecord( + userId: string, + email: string, + name: string, + role: string, + timestamp: string, +) { + return { + id: userId, + email, + emailVerified: 1, + name, + image: null, + role, + banned: null, + banReason: null, + banExpires: null, + createdAt: timestamp, + updatedAt: timestamp, + }; +} + +function createAccountRecord( + userId: string, + email: string, + passwordHash: string, + timestamp: string, +) { + return { + id: generateId("account"), + userId, + accountId: email, + providerId: "credential", + password: passwordHash, + accessToken: null, + refreshToken: null, + accessTokenExpiresAt: null, + refreshTokenExpiresAt: null, + scope: null, + idToken: null, + createdAt: timestamp, + updatedAt: timestamp, + }; +} + +export function createMemberRecord( + organizationId: string, + userId: string, + role: MemberRole, + timestamp: string, +) { + return { + id: generateId("member"), + organizationId, + userId, + role, + createdAt: timestamp, + }; +} + +function createApiKeyRecord( + userId: string, + name: string, + key: string, + timestamp: string, +) { + return { + id: generateId("apikey"), + name, + userId, + key, + createdAt: timestamp, + updatedAt: timestamp, + }; +} + +function createConnectionRecord( + connectionId: string, + organizationId: string, + createdBy: string, + conn: Connection, + timestamp: string, + tools: unknown[] | null = null, +) { + return { + id: connectionId, + organization_id: organizationId, + created_by: createdBy, + title: conn.title, + description: conn.description, + icon: conn.icon, + app_name: conn.appName, + app_id: null, + connection_type: "HTTP" as const, + connection_url: conn.connectionUrl, + connection_token: conn.connectionToken, + connection_headers: null, + oauth_config: null, + configuration_state: null, + configuration_scopes: null, + metadata: JSON.stringify(conn.metadata), + tools: tools ? JSON.stringify(tools) : null, + bindings: null, + status: "active" as const, + created_at: timestamp, + updated_at: timestamp, + }; +} + +function createGatewayRecord( + gatewayId: string, + organizationId: string, + gateway: Gateway, + createdBy: string, + timestamp: string, +) { + return { + id: gatewayId, + organization_id: organizationId, + title: gateway.title, + description: gateway.description, + tool_selection_strategy: gateway.toolSelectionStrategy, + tool_selection_mode: gateway.toolSelectionMode, + icon: gateway.icon, + status: "active" as const, + is_default: gateway.isDefault ? 1 : 0, + created_at: timestamp, + updated_at: timestamp, + created_by: createdBy, + updated_by: null, + }; +} + +function createGatewayConnectionRecord( + gatewayId: string, + connectionId: string, + timestamp: string, +) { + return { + id: generateId("gtw_conn"), + gateway_id: gatewayId, + connection_id: connectionId, + selected_tools: null, + selected_resources: null, + selected_prompts: null, + created_at: timestamp, + }; +} + +function createMonitoringLogRecord( + organizationId: string, + connectionId: string, + connectionTitle: string, + log: MonitoringLog, + timestamp: string, + userId: string, + gatewayId: string | null, + logIndex: number, +) { + // Use index to ensure unique IDs when generating many logs quickly + const timestampMs = Date.now(); + return { + id: `log_${timestampMs}_${logIndex.toString().padStart(7, "0")}`, + organization_id: organizationId, + connection_id: connectionId, + connection_title: connectionTitle, + tool_name: log.toolName, + input: JSON.stringify(log.input), + output: JSON.stringify(log.output), + is_error: log.isError ? 1 : 0, + error_message: log.errorMessage ?? null, + duration_ms: log.durationMs, + timestamp, + user_id: userId, + request_id: `req_${timestampMs}_${logIndex.toString().padStart(7, "0")}`, + user_agent: log.userAgent, + gateway_id: gatewayId, + properties: log.properties ? JSON.stringify(log.properties) : null, + }; +} + +// ============================================================================= +// Generic Organization Seeder +// ============================================================================= + +export async function createOrg( + db: Kysely, + config: OrgConfig, +): Promise { + const now = new Date().toISOString(); + const orgId = generateId("org"); + + // Generate user IDs + const userIds: Record = {}; + const userEmails: Record = {}; + for (const [key, user] of Object.entries(config.users)) { + userIds[key] = generateId("user"); + userEmails[key] = user.email; + } + + // 1. Create Organization + await db + .insertInto("organization") + .values({ + id: orgId, + slug: config.orgSlug, + name: config.orgName, + createdAt: now, + }) + .execute(); + + // 2. Create Users + const passwordHash = await hashPassword(CONFIG.PASSWORD); + for (const [key, user] of Object.entries(config.users)) { + await db + // @ts-ignore: Better Auth user table + .insertInto("user") + .values( + createUserRecord(userIds[key]!, user.email, user.name, user.role, now), + ) + .execute(); + } + + // 3. Create Credential Accounts + const accountRecords = Object.entries(config.users).map(([key, user]) => + createAccountRecord(userIds[key]!, user.email, passwordHash, now), + ); + // @ts-ignore: Better Auth account table + await db.insertInto("account").values(accountRecords).execute(); + + // 4. Link Users to Organization + const memberRecords = Object.entries(config.users).map(([key, user]) => + createMemberRecord(orgId, userIds[key]!, user.memberRole, now), + ); + await db.insertInto("member").values(memberRecords).execute(); + + // 5. Create API Keys + const apiKeyResults: Record = {}; + if (config.apiKeys?.length) { + const apiKeyRecords = config.apiKeys.map((apiKey) => { + const keyHash = `${config.orgSlug}_${apiKey.userKey}_${generateId("key")}`; + apiKeyResults[apiKey.userKey] = keyHash; + return createApiKeyRecord( + userIds[apiKey.userKey]!, + apiKey.name, + keyHash, + now, + ); + }); + // @ts-ignore: Better Auth apikey table + await db.insertInto("apikey").values(apiKeyRecords).execute(); + } + + // 6. Create Connections + const connectionIds: Record = {}; + for (const key of Object.keys(config.connections)) { + connectionIds[key] = generateId("conn"); + } + const ownerUserId = userIds[config.ownerUserKey]!; + + // Fetch tools dynamically for connections that don't have them defined + const connectionRecords = await Promise.all( + Object.entries(config.connections).map(async ([key, conn]) => { + let tools = conn.tools ?? null; + + // If tools not provided, fetch them from the MCP server (production behavior) + if (!tools) { + const fetchedTools = await fetchToolsFromMCP({ + id: connectionIds[key]!, + title: conn.title, + connection_type: "HTTP", + connection_url: conn.connectionUrl, + connection_token: conn.connectionToken, + connection_headers: null, + }).catch((error) => { + console.warn( + `Failed to fetch tools for ${conn.title} (${conn.connectionUrl}):`, + error instanceof Error ? error.message : error, + ); + return null; + }); + + tools = fetchedTools; + } + + return createConnectionRecord( + connectionIds[key]!, + orgId, + ownerUserId, + conn, + now, + tools, + ); + }), + ); + await db.insertInto("connections").values(connectionRecords).execute(); + + // 7. Create Gateways + const gatewayIds: Record = {}; + for (const key of Object.keys(config.gateways)) { + gatewayIds[key] = generateId("gtw"); + } + const gatewayRecords = Object.entries(config.gateways).map(([key, gateway]) => + createGatewayRecord(gatewayIds[key]!, orgId, gateway, ownerUserId, now), + ); + await db.insertInto("gateways").values(gatewayRecords).execute(); + + // 8. Link Gateways to Connections + if (config.gatewayConnections?.length) { + const gwConnRecords = config.gatewayConnections.map((link) => + createGatewayConnectionRecord( + gatewayIds[link.gatewayKey]!, + connectionIds[link.connectionKey]!, + now, + ), + ); + await db.insertInto("gateway_connections").values(gwConnRecords).execute(); + } + + // 9. Create Monitoring Logs + if (config.logs.length > 0) { + const BATCH_SIZE = 500; // SQLite has a strict limit on SQL variables per query + const totalLogs = config.logs.length; + + console.log( + ` 📊 Inserting ${totalLogs.toLocaleString()} monitoring logs...`, + ); + + for (let i = 0; i < totalLogs; i += BATCH_SIZE) { + const batch = config.logs.slice(i, i + BATCH_SIZE); + const logRecords = batch.map((log, batchIndex) => { + const timestamp = new Date(Date.now() + log.offsetMs).toISOString(); + const gatewayId = log.gatewayKey + ? (gatewayIds[log.gatewayKey] ?? null) + : null; + // Use global index to ensure unique IDs across all batches + const logIndex = i + batchIndex; + return createMonitoringLogRecord( + orgId, + connectionIds[log.connectionKey]!, + config.connections[log.connectionKey]!.title, + log, + timestamp, + userIds[log.userKey]!, + gatewayId, + logIndex, + ); + }); + await db.insertInto("monitoring_logs").values(logRecords).execute(); + + if (totalLogs > BATCH_SIZE) { + const progress = Math.min(i + BATCH_SIZE, totalLogs); + const percentage = ((progress / totalLogs) * 100).toFixed(1); + console.log( + ` → ${progress.toLocaleString()}/${totalLogs.toLocaleString()} (${percentage}%)`, + ); + } + } + + console.log(` ✅ Inserted ${totalLogs.toLocaleString()} logs`); + } + + // 10. Create Organization Settings + await db + .insertInto("organization_settings") + .values({ + organizationId: orgId, + sidebar_items: null, + createdAt: now, + updatedAt: now, + }) + .execute(); + + return { + organizationId: orgId, + organizationName: config.orgName, + organizationSlug: config.orgSlug, + userIds, + userEmails, + apiKeys: apiKeyResults, + connectionIds, + gatewayIds, + logCount: config.logs.length, + }; +} + +// ============================================================================= +// Cleanup Helper +// ============================================================================= + +export async function cleanupOrgs( + db: Kysely, + slugs: string[], +): Promise { + const existingOrgs = await db + .selectFrom("organization") + .select(["id", "slug"]) + .where("slug", "in", slugs) + .execute(); + + if (existingOrgs.length === 0) return; + + console.log(`🧹 Cleaning up ${existingOrgs.length} existing demo org(s)...`); + const orgIds = existingOrgs.map((org) => org.id); + + await db + .deleteFrom("monitoring_logs") + .where("organization_id", "in", orgIds) + .execute(); + + const gatewayIds = await db + .selectFrom("gateways") + .select("id") + .where("organization_id", "in", orgIds) + .execute() + .then((rows) => rows.map((r) => r.id)); + + if (gatewayIds.length > 0) { + await db + .deleteFrom("gateway_connections") + .where("gateway_id", "in", gatewayIds) + .execute(); + } + + await db + .deleteFrom("gateways") + .where("organization_id", "in", orgIds) + .execute(); + await db + .deleteFrom("connections") + .where("organization_id", "in", orgIds) + .execute(); + await db + .deleteFrom("organization_settings") + .where("organizationId", "in", orgIds) + .execute(); + + const memberUserIds = await db + .selectFrom("member") + .select("userId") + .where("organizationId", "in", orgIds) + .execute() + .then((rows) => rows.map((r) => r.userId)); + + await db.deleteFrom("member").where("organizationId", "in", orgIds).execute(); + + if (memberUserIds.length > 0) { + // biome-ignore format: keep on single line for ts-ignore + // @ts-ignore: Better Auth tables not in Database type + await db.deleteFrom("apikey").where("userId", "in", memberUserIds).execute(); + + // biome-ignore format: keep on single line for ts-ignore + // @ts-ignore: Better Auth tables not in Database type + await db.deleteFrom("account").where("userId", "in", memberUserIds).execute(); + + await db.deleteFrom("user").where("id", "in", memberUserIds).execute(); + } + + await db.deleteFrom("organization").where("id", "in", orgIds).execute(); + console.log( + ` ✅ Cleaned up: ${existingOrgs.map((o) => o.slug).join(", ")}`, + ); +} diff --git a/apps/mesh/migrations/seeds/index.ts b/apps/mesh/migrations/seeds/index.ts index 9d25c6b0b1..aefd3b60c0 100644 --- a/apps/mesh/migrations/seeds/index.ts +++ b/apps/mesh/migrations/seeds/index.ts @@ -9,6 +9,7 @@ import type { Kysely } from "kysely"; import type { Database } from "../../src/storage/types"; export type { BenchmarkSeedResult } from "./benchmark"; +export type { DemoSeedResult } from "./demo/index"; /** * Seed function signature @@ -20,6 +21,7 @@ export type SeedFunction = (db: Kysely) => Promise; */ const seeds = { benchmark: () => import("./benchmark").then((m) => m.seed), + demo: () => import("./demo").then((m) => m.seed), } as const; export type SeedName = keyof typeof seeds; diff --git a/apps/mesh/src/database/migrate.ts b/apps/mesh/src/database/migrate.ts index 434de34adf..7b04fae11a 100644 --- a/apps/mesh/src/database/migrate.ts +++ b/apps/mesh/src/database/migrate.ts @@ -88,9 +88,12 @@ export async function migrateToLatest( keepOpen = false, database: customDb, skipBetterAuth = false, - seed, + seed: seedOption, } = options ?? {}; + // Check for seed from environment variable + const seed = seedOption || (process.env.SEED as SeedName | undefined); + // Run Better Auth migrations (unless skipped or using custom db) if (!skipBetterAuth && !customDb) { await migrateBetterAuth(); @@ -119,6 +122,8 @@ export async function migrateToLatest( // Run seed if specified let seedResult: T | undefined; if (seed) { + const source = seedOption ? "option" : "SEED environment variable"; + console.log(`🌱 Running seed "${seed}" (from ${source})...`); seedResult = await runSeed(database.db, seed); }