├── .changeset ├── README.md └── config.json ├── .eslintrc.js ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── apps └── web │ ├── .env.example │ ├── .eslintrc.js │ ├── README.md │ ├── app │ ├── api │ │ ├── ai-generation │ │ │ └── route.ts │ │ ├── deploy │ │ │ └── route.ts │ │ └── gemini-generation │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── results │ │ └── page.tsx │ ├── components.json │ ├── components │ ├── app-logo.tsx │ ├── code-editor.tsx │ ├── database-deployments.tsx │ ├── database-picker.tsx │ ├── footer.tsx │ ├── header.tsx │ ├── icons.tsx │ ├── options-results.tsx │ ├── schema-results.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ └── sonner.tsx │ ├── constants.ts │ ├── lib │ └── utils.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── prompt.ts │ ├── public │ ├── banner.jpg │ ├── logo.webp │ └── medal.png │ ├── services │ └── deploy.ts │ ├── store.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── utils.ts │ └── utils │ ├── ai.ts │ ├── connection-string-validations.ts │ ├── database.ts │ └── rate-limit.ts ├── package.json ├── packages ├── cli │ ├── .gitignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── commands │ │ │ └── add.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── get-schema.ts │ │ │ ├── handleError.ts │ │ │ ├── list-commands.ts │ │ │ ├── logger.ts │ │ │ ├── package-info.ts │ │ │ └── show-next-steps.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── eslint-config │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js └── typescript-config │ ├── base.json │ ├── nextjs.json │ └── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["vdb-web"] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This configuration only applies to the package manager root. 2 | /** @type {import("eslint").Linter.Config} */ 3 | module.exports = { 4 | ignorePatterns: ["apps/**", "packages/**"], 5 | extends: ["@repo/eslint-config/library.js"], 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | project: true, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: pnpm/action-setup@v2 13 | with: 14 | version: 8 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 18.x 18 | cache: "pnpm" 19 | 20 | - run: pnpm install 21 | - run: pnpm run lint:cli && pnpm run build:cli -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup PNPM 19 | uses: pnpm/action-setup@v2.2.4 20 | with: 21 | node-version: 8.9.2 22 | 23 | - name: Use Node.js 18 24 | uses: actions/setup-node@v3 25 | with: 26 | version: 8.9.2 27 | node-version: 18 28 | cache: "pnpm" 29 | 30 | - name: Install Dependencies 31 | run: pnpm install 32 | 33 | - name: Build the package 34 | run: pnpm build:cli 35 | 36 | - name: Create Version PR or Publish to NPM 37 | id: changesets 38 | uses: changesets/action@v1.4.1 39 | with: 40 | publish: pnpm run release:cli 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.18.1 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build 5 | .vscode 6 | .turbo 7 | .DS_Store 8 | .github -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "jsxSingleQuote": true, 4 | "printWidth": 100, 5 | "semi": false, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "trailingComma": "none" 9 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Xavier Alfaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | App banner 4 | 5 |
6 | 7 | ## Introduction 8 | 9 | Snap2SQL lets you instantly convert database diagrams into clean SQL schemas using AI. Support for MySQL and PostgreSQL. 10 | 11 | ## Requirements 12 | 13 | - Node >= 20 14 | - pnpm >= 9 15 | 16 | ## Stack 17 | 18 | - [next](https://www.npmjs.com/package/next): A framework for server-rendered React applications. 19 | - [shadcn/ui](https://ui.shadcn.com/): Provides beautifully designed components for UI. 20 | - [monaco-editor/react](https://www.npmjs.com/package/monaco-editor): A Monaco Editor wrapper for React applications. 21 | - [zustand](https://www.npmjs.com/package/zustand): A small, fast, and scalable state management library for React. 22 | - [typescript](https://www.npmjs.com/package/typescript): A typed superset of JavaScript that compiles to plain JavaScript. 23 | 24 | ## Setting Up 25 | 26 | ### OPENAI_API_TOKEN 27 | 28 | - Go to the [OpenAI web](https://openai.com/). 29 | - Sign in to your account or create a new one. 30 | - Navigate to your [API settings](https://platform.openai.com/account/api-keys). 31 | - Generate an Secret key. 32 | - Copy the generated Secret key. 33 | 34 | ### GOOGLE_GENERATIVE_AI_API_KEY 35 | 36 | - Go to the [Google AI Studio](https://aistudio.google.com/app/apikey). 37 | - Sign in to your account or create a new one. 38 | - Generate an Secret key. 39 | - Copy the generated Secret key. 40 | 41 | ### UPSTASH_REDIS_REST_URL - UPSTASH_REDIS_REST_TOKEN 42 | 43 | - Go to the Uptash [console](https://console.upstash.com/). 44 | - Sign in to your account or create a new one. 45 | - Navigate to your database. 46 | - Copy the generated keys. 47 | 48 | ## Run Locally 49 | 50 | 1.Clone the snap2sql repository: 51 | 52 | ```sh 53 | git clone https://github.com/xavimondev/snap2sql 54 | ``` 55 | 56 | 2.Install the dependencies: 57 | 58 | ```bash 59 | pnpm install 60 | ``` 61 | 62 | 3.Start the development: 63 | 64 | ```bash 65 | pnpm dev 66 | ``` 67 | 68 | ## Contributors 69 | 70 | 71 | 72 | 73 | 74 | ## License 75 | 76 | [**MIT**](https://github.com/xavimondev/snap2sql/blob/main/LICENSE). 77 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | GOOGLE_GENERATIVE_AI_API_KEY= 3 | DEFAULT_PROVIDER=openai 4 | 5 | UPSTASH_REDIS_REST_URL= 6 | UPSTASH_REDIS_REST_TOKEN= -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This is the web for **vdbs**. Built using the following stack: 4 | 5 | - [next](https://www.npmjs.com/package/next): A framework for server-rendered React applications. 6 | - [shadcn/ui](https://ui.shadcn.com/): Provides beautifully designed components for UI. 7 | - [monaco-editor/react](https://www.npmjs.com/package/monaco-editor): A Monaco Editor wrapper for React applications. 8 | - [zustand](https://www.npmjs.com/package/zustand): A small, fast, and scalable state management library for React. 9 | - [typescript](https://www.npmjs.com/package/typescript): A typed superset of JavaScript that compiles to plain JavaScript. 10 | -------------------------------------------------------------------------------- /apps/web/app/api/ai-generation/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { generateObject } from 'ai' 3 | import { openai } from '@ai-sdk/openai' 4 | import { uptash } from '@/utils/rate-limit' 5 | import { headers } from 'next/headers' 6 | 7 | import { DB_SCHEMA, prompts } from '@/utils/ai' 8 | 9 | const ratelimit = 10 | process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN ? uptash : false 11 | 12 | export async function POST(req: Request) { 13 | if ( 14 | process.env.NODE_ENV === 'development' && 15 | (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === '') 16 | ) { 17 | return NextResponse.json( 18 | { 19 | data: undefined, 20 | message: 'Missing OPENAI_API_KEY – make sure to add it to your .env file.' 21 | }, 22 | { status: 400 } 23 | ) 24 | } 25 | 26 | if (process.env.NODE_ENV === 'production') { 27 | if (ratelimit) { 28 | const ip = (await headers()).get('x-forwarded-for') ?? 'local' 29 | 30 | const { success } = await ratelimit.limit(ip) 31 | if (!success) { 32 | return NextResponse.json( 33 | { message: 'You have reached your request limit for the day.' }, 34 | { status: 429 } 35 | ) 36 | } 37 | } 38 | } 39 | 40 | const { prompt: base64, databaseFormat } = await req.json() 41 | 42 | try { 43 | const result = await generateObject({ 44 | model: openai('gpt-4.1-mini'), 45 | schema: DB_SCHEMA, 46 | messages: [ 47 | { 48 | role: 'user', 49 | content: [ 50 | { 51 | type: 'text', 52 | text: prompts[databaseFormat] as string 53 | }, 54 | { 55 | type: 'image', 56 | image: base64 57 | } 58 | ] 59 | } 60 | ], 61 | temperature: 0.2 62 | }) 63 | 64 | return NextResponse.json({ 65 | data: result.object.results 66 | }) 67 | } catch (error) { 68 | // @ts-ignore 69 | const statusCode = error?.lastError?.statusCode ?? error.statusCode 70 | let errorMessage = 'An error has ocurred with API Completions. Please try again.' 71 | 72 | if (statusCode === 401) { 73 | errorMessage = 'The provided API Key is invalid. Please enter a valid API Key.' 74 | } /*else if (statusCode === 429) { 75 | errorMessage = 'You exceeded your current quota, please check your plan and billing details.' 76 | }*/ 77 | 78 | return NextResponse.json( 79 | { 80 | message: errorMessage 81 | }, 82 | { status: statusCode } 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /apps/web/app/api/deploy/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { deploySchema } from '@/utils/database' 3 | 4 | type ResponseJson = { 5 | url: string 6 | sqlSchema: string 7 | } 8 | 9 | export async function POST(req: Request) { 10 | const { url, sqlSchema } = (await req.json()) as ResponseJson 11 | 12 | if (url === '' || !sqlSchema) { 13 | return NextResponse.json( 14 | { 15 | error: 16 | "We couldn't find a connection URL or a SQL Schema. Please try again with the correct information." 17 | }, 18 | { status: 400 } 19 | ) 20 | } 21 | 22 | const response = await deploySchema(url, sqlSchema) 23 | if (!response.success) { 24 | return NextResponse.json({ error: response.message }, { status: 500 }) 25 | } 26 | 27 | return NextResponse.json({ 28 | message: 'Database Schema deployed successfully' 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/app/api/gemini-generation/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { generateObject } from 'ai' 3 | import { google } from '@ai-sdk/google' 4 | import { uptash } from '@/utils/rate-limit' 5 | import { headers } from 'next/headers' 6 | 7 | import { DB_SCHEMA, prompts } from '@/utils/ai' 8 | 9 | const ratelimit = 10 | process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN ? uptash : false 11 | 12 | export async function POST(req: Request) { 13 | if (process.env.NODE_ENV === 'development' && !process.env.GOOGLE_GENERATIVE_AI_API_KEY) { 14 | return NextResponse.json( 15 | { 16 | data: undefined, 17 | message: 'Missing GOOGLE_GENERATIVE_AI_API_KEY – make sure to add it to your .env file.' 18 | }, 19 | { status: 400 } 20 | ) 21 | } 22 | 23 | if (process.env.NODE_ENV === 'production') { 24 | if (ratelimit) { 25 | const ip = (await headers()).get('x-forwarded-for') ?? 'local' 26 | 27 | const { success } = await ratelimit.limit(ip) 28 | if (!success) { 29 | return NextResponse.json( 30 | { message: 'You have reached your request limit for the day.' }, 31 | { status: 429 } 32 | ) 33 | } 34 | } 35 | } 36 | 37 | const { prompt: base64, databaseFormat } = await req.json() 38 | 39 | try { 40 | const result = await generateObject({ 41 | model: google('gemini-2.0-flash-001'), 42 | schema: DB_SCHEMA, 43 | messages: [ 44 | { 45 | role: 'user', 46 | content: [ 47 | { 48 | type: 'text', 49 | text: prompts[databaseFormat] as string 50 | }, 51 | { 52 | type: 'image', 53 | image: base64 54 | } 55 | ] 56 | } 57 | ], 58 | temperature: 0.2 59 | }) 60 | 61 | return NextResponse.json({ 62 | data: result.object.results 63 | }) 64 | } catch (error) { 65 | let errorMessage = 'An error has ocurred with API Completions. Please try again.' 66 | // @ts-ignore 67 | if (error.status === 401) { 68 | errorMessage = 'The provided API Key is invalid. Please enter a valid API Key.' 69 | } 70 | // @ts-ignore 71 | const { name, status, headers } = error 72 | return NextResponse.json({ name, status, headers, message: errorMessage }, { status }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /apps/web/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xavimondev/vdbs/379a05d3a85922586c4bb3eb6b7cbc0e05d03efc/apps/web/app/favicon.ico -------------------------------------------------------------------------------- /apps/web/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | @layer base { 7 | :root { 8 | --background: 0 0% 100%; 9 | --foreground: 240 10% 3.9%; 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | --popover: 0 0% 100%; 13 | --popover-foreground: 240 10% 3.9%; 14 | --primary: 142.1 76.2% 36.3%; 15 | --primary-foreground: 355.7 100% 97.3%; 16 | --secondary: 240 4.8% 95.9%; 17 | --secondary-foreground: 240 5.9% 10%; 18 | --muted: 240 4.8% 95.9%; 19 | --muted-foreground: 240 3.8% 46.1%; 20 | --accent: 240 4.8% 95.9%; 21 | --accent-foreground: 240 5.9% 10%; 22 | --destructive: 0 84.2% 60.2%; 23 | --destructive-foreground: 0 0% 98%; 24 | --border: 240 5.9% 90%; 25 | --input: 240 5.9% 90%; 26 | --ring: 142.1 76.2% 36.3%; 27 | --radius: 0.5rem; 28 | } 29 | 30 | .dark { 31 | --background: 20 14.3% 4.1%; 32 | --foreground: 0 0% 95%; 33 | --card: 24 9.8% 10%; 34 | --card-foreground: 0 0% 95%; 35 | --popover: 0 0% 9%; 36 | --popover-foreground: 0 0% 95%; 37 | --primary: 142.1 70.6% 45.3%; 38 | --primary-foreground: 144.9 80.4% 10%; 39 | --secondary: 240 3.7% 15.9%; 40 | --secondary-foreground: 0 0% 98%; 41 | --muted: 0 0% 15%; 42 | --muted-foreground: 240 5% 64.9%; 43 | --accent: 12 6.5% 15.1%; 44 | --accent-foreground: 0 0% 98%; 45 | --destructive: 0 62.8% 30.6%; 46 | --destructive-foreground: 0 85.7% 97.3%; 47 | --border: 240 3.7% 15.9%; 48 | --input: 240 3.7% 15.9%; 49 | --ring: 142.4 71.8% 29.2%; 50 | } 51 | } 52 | 53 | @layer base { 54 | * { 55 | @apply border-border; 56 | } 57 | body { 58 | @apply bg-background text-foreground; 59 | } 60 | } -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import type { Metadata } from 'next' 3 | import { Inter } from 'next/font/google' 4 | import { Analytics } from '@vercel/analytics/react' 5 | import { cn } from '@/lib/utils' 6 | import { APP_URL } from '@/constants' 7 | import { Toaster } from '@/components/ui/sonner' 8 | import { Footer } from '@/components/footer' 9 | 10 | const inter = Inter({ subsets: ['latin'] }) 11 | 12 | const title = 'Snap2SQL - Convert diagrams to SQL with AI' 13 | const description = 14 | 'Snap2SQL lets you instantly convert database diagrams into clean SQL schemas using AI. Support for MySQL and PostgreSQL. Try your first scan free!' 15 | 16 | export const metadata: Metadata = { 17 | metadataBase: new URL(APP_URL), 18 | title, 19 | description, 20 | keywords: [ 21 | 'ERD to SQL', 22 | 'diagram to SQL', 23 | 'convert ERD', 24 | 'SQL schema generator', 25 | 'AI SQL builder', 26 | 'database diagram OCR', 27 | 'MySQL generator', 28 | 'PostgreSQL schema', 29 | 'Snap2SQL', 30 | 'ER diagram parser' 31 | ], 32 | openGraph: { 33 | title, 34 | description, 35 | url: '/', 36 | siteName: 'snap2sql', 37 | locale: 'en_US', 38 | type: 'website', 39 | images: [ 40 | { 41 | url: '/banner.jpg', 42 | width: 1835, 43 | height: 1000, 44 | type: 'image/jpeg' 45 | } 46 | ] 47 | } 48 | } 49 | 50 | export default function RootLayout({ children }: { children: React.ReactNode }) { 51 | return ( 52 | 53 | 56 |
57 |
58 | {children} 59 |
60 |