├── .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 |
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 |
61 |
62 |
63 |
64 |
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/apps/web/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useRef, useState } from 'react'
4 | import { useRouter } from 'next/navigation'
5 | import Image from 'next/image'
6 | import { UploadIcon } from 'lucide-react'
7 | import { toast } from 'sonner'
8 |
9 | import { cn } from '@/lib/utils'
10 | import { isSupportedImageType, toBase64 } from '@/utils'
11 |
12 | import { useSchemaStore } from '@/store'
13 |
14 | import { Header } from '@/components/header'
15 | import { DatabasePicker } from '@/components/database-picker'
16 |
17 | const LIMIT_MB = 2 * 1024 * 1024
18 |
19 | export default function Page() {
20 | const [isDraggingOver, setIsDraggingOver] = useState(false)
21 | const inputRef = useRef(null)
22 | const [blobURL, setBlobURL] = useState(null)
23 | const [isLoading, setIsLoading] = useState(false)
24 | const [databaseFormat, setDatabaseFormat] = useState('postgresql')
25 | const setSchema = useSchemaStore((state) => state.setSchema)
26 | const router = useRouter()
27 |
28 | const getGenerationAI = async (base64: string) => {
29 | const toastId = toast.loading('Generation Database Schema')
30 |
31 | try {
32 | setIsLoading(true)
33 | const response = await fetch('api/ai-generation', {
34 | method: 'POST',
35 | body: JSON.stringify({ prompt: base64, databaseFormat }),
36 | headers: {
37 | 'Content-type': 'application/json'
38 | }
39 | })
40 | if (response.status === 429) {
41 | throw new Error('You have reached your request limit for the day.')
42 | } else if (response.status !== 200) {
43 | throw new Error('An error has ocurred while generation database schema')
44 | }
45 |
46 | const results = await response.json()
47 | const { sqlSchema, tables } = results.data
48 |
49 | if (
50 | sqlSchema.trim() === 'Invalid SQL diagram.' ||
51 | !sqlSchema.includes('CREATE TABLE')
52 | // !sqlSchema.includes('--TABLE')
53 | ) {
54 | toast.error('This is not a valid SQL diagram. Please try again.')
55 | return
56 | }
57 |
58 | const schema = {
59 | sqlSchema,
60 | tables,
61 | databaseFormat: databaseFormat as string
62 | }
63 |
64 | setSchema(schema)
65 | setTimeout(() => {
66 | router.push('/results')
67 | }, 1000)
68 | } catch (error) {
69 | if (error instanceof Error) {
70 | toast.error(error.message)
71 | }
72 | } finally {
73 | setIsLoading(false)
74 | toast.dismiss(toastId)
75 | }
76 | }
77 |
78 | const submit = async (file?: File | Blob) => {
79 | if (!file) return
80 |
81 | if (!isSupportedImageType(file.type)) {
82 | return toast.error('Unsupported format. Only JPEG, PNG, and WEBP files are supported.')
83 | }
84 |
85 | if (file.size > LIMIT_MB) return toast.error('Image too large, maximum file size is 1MB.')
86 |
87 | const base64 = await toBase64(file)
88 |
89 | if (!databaseFormat) {
90 | toast.error(`You haven't selected a database format`)
91 | return
92 | }
93 |
94 | setBlobURL(URL.createObjectURL(file))
95 | await getGenerationAI(base64)
96 | }
97 |
98 | const handleDragLeave = () => {
99 | setIsDraggingOver(false)
100 | }
101 |
102 | const handleDragOver = (e: DragEvent) => {
103 | setIsDraggingOver(true)
104 | e.preventDefault()
105 | e.stopPropagation()
106 | if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
107 | }
108 |
109 | const handleDrop = (e: DragEvent) => {
110 | e.preventDefault()
111 | e.stopPropagation()
112 | setIsDraggingOver(false)
113 |
114 | const file = e.dataTransfer?.files?.[0]
115 | submit(file)
116 | }
117 |
118 | const handlePaste = (e: ClipboardEvent) => {
119 | const file = e.clipboardData?.files?.[0]
120 | submit(file)
121 | }
122 |
123 | const handleInputChange = (e: React.ChangeEvent) => {
124 | const file = e.target.files?.[0]
125 | submit(file)
126 | }
127 |
128 | useEffect(() => {
129 | addEventListener('paste', handlePaste)
130 | addEventListener('drop', handleDrop)
131 | addEventListener('dragover', handleDragOver)
132 | addEventListener('dragleave', handleDragLeave)
133 |
134 | return () => {
135 | removeEventListener('paste', handlePaste)
136 | removeEventListener('drop', handleDrop)
137 | removeEventListener('dragover', handleDragOver)
138 | removeEventListener('dragleave', handleDragLeave)
139 | }
140 | }, [])
141 |
142 | return (
143 |
144 |
145 |
146 |
147 |
inputRef.current?.click()}
159 | >
160 | {blobURL ? (
161 |
168 | ) : (
169 |
174 |
175 |
176 |
177 |
178 | Drop or paste anywhere, or click to upload
179 |
180 |
181 | Supports PNG, JPEG, and JPG (max 2MB)
182 |
183 |
184 |
187 | e.preventDefault()}
190 | placeholder='Hold to paste'
191 | onClick={(e) => e.stopPropagation()}
192 | className='text-center w-full rounded-full py-3 bg-gray-200 dark:bg-gray-800 placeholder-black dark:placeholder-white focus:bg-white dark:focus:bg-black focus:placeholder-gray-700 dark:focus:placeholder-gray-300 transition-colors ease-in-out focus:outline-none border-2 focus:border-green-300 dark:focus:border-green-700 border-transparent'
193 | />
194 |
195 |
196 | )}
197 |
204 |
205 |
206 |
207 | )
208 | }
209 |
--------------------------------------------------------------------------------
/apps/web/app/results/page.tsx:
--------------------------------------------------------------------------------
1 | import { UploadIcon } from 'lucide-react'
2 | import Link from 'next/link'
3 | import { Header } from '@/components/header'
4 | import { OptionsResults } from '@/components/options-results'
5 | import { SchemaResults } from '@/components/schema-results'
6 | import { Button } from '@/components/ui/button'
7 |
8 | export default function Results() {
9 | return (
10 |
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/apps/web/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/apps/web/components/app-logo.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | export function AppLogo() {
4 | return (
5 |
6 |
7 |
8 | Snap2SQL
9 |
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/components/code-editor.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useEffect } from 'react'
3 | import dynamic from 'next/dynamic'
4 | import { useMonaco } from '@monaco-editor/react'
5 | import { LoaderIcon } from 'lucide-react'
6 |
7 | const Monaco = dynamic(() => import('@monaco-editor/react'), { ssr: false })
8 |
9 | type CodeEditorProps = {
10 | code: string
11 | }
12 |
13 | export function CodeEditor({ code }: CodeEditorProps) {
14 | const monaco = useMonaco()
15 |
16 | useEffect(() => {
17 | if (!monaco) return
18 | // https://github.com/brijeshb42/monaco-themes/tree/master/src
19 | monaco.editor.defineTheme('vs-dark', {
20 | base: 'vs-dark',
21 | inherit: true,
22 | rules: [
23 | {
24 | background: '141414',
25 | token: ''
26 | },
27 | {
28 | foreground: '5f5a60',
29 | fontStyle: 'italic',
30 | token: 'comment'
31 | },
32 | {
33 | foreground: 'cf6a4c',
34 | token: 'constant'
35 | },
36 | {
37 | foreground: '9b703f',
38 | token: 'entity'
39 | },
40 | {
41 | foreground: 'cda869',
42 | token: 'keyword'
43 | },
44 | {
45 | foreground: 'f9ee98',
46 | token: 'storage'
47 | },
48 | {
49 | foreground: '8f9d6a',
50 | token: 'string'
51 | },
52 | {
53 | foreground: '9b859d',
54 | token: 'support'
55 | },
56 | {
57 | foreground: '7587a6',
58 | token: 'variable'
59 | },
60 | {
61 | foreground: 'daefa3',
62 | token: 'string source'
63 | },
64 | {
65 | foreground: 'ddf2a4',
66 | token: 'string constant'
67 | },
68 | {
69 | foreground: 'e9c062',
70 | token: 'string.regexp'
71 | }
72 | ],
73 | colors: {
74 | 'editor.foreground': '#F8F8F8',
75 | 'editor.background': '#111010',
76 | 'editor.selectionBackground': '#DDF0FF33',
77 | 'editor.lineHighlightBackground': '#FFFFFF08',
78 | 'editorCursor.foreground': '#A7A7A7',
79 | 'editorWhitespace.foreground': '#FFFFFF40'
80 | }
81 | })
82 | }, [monaco])
83 |
84 | useEffect(() => {
85 | if (!monaco) return
86 | monaco.editor.getModels()[0]?.setValue(code || '')
87 | }, [code])
88 |
89 | return (
90 |
97 |
98 |
99 | }
100 | options={{
101 | readOnly: true,
102 | padding: {
103 | top: 20
104 | },
105 | cursorSmoothCaretAnimation: 'off',
106 | language: 'sql',
107 | cursorBlinking: 'solid',
108 | fontSize: 16,
109 | formatOnType: true,
110 | formatOnPaste: true,
111 | automaticLayout: true,
112 | wordWrap: 'wordWrapColumn',
113 | wordWrapColumn: 80,
114 | minimap: {
115 | enabled: false
116 | },
117 | tabSize: 2
118 | }}
119 | defaultLanguage='sql'
120 | />
121 | )
122 | }
123 |
--------------------------------------------------------------------------------
/apps/web/components/database-deployments.tsx:
--------------------------------------------------------------------------------
1 | import React, { FormEvent, useState } from 'react'
2 | import { AlertCircleIcon, Loader2Icon } from 'lucide-react'
3 | import { toast } from 'sonner'
4 | import { Button } from '@/components/ui/button'
5 | import { Input } from '@/components/ui/input'
6 | import { Label } from '@/components/ui/label'
7 | import {
8 | Card,
9 | CardHeader,
10 | CardTitle,
11 | CardDescription,
12 | CardContent,
13 | CardFooter
14 | } from '@/components/ui/card'
15 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
16 | import { SupabaseIc, NeonIc } from '@/components/icons'
17 | import { useSchemaStore } from '@/store'
18 | import { triggerConfetti } from '@/utils'
19 | import {
20 | validateNeonConnectionString,
21 | validateSupabaseConnectionString
22 | } from '@/utils/connection-string-validations'
23 |
24 | const providers = [
25 | {
26 | id: 'neon',
27 | name: 'Neon',
28 | icon: NeonIc,
29 | description: 'Ship faster with Postgres',
30 | color: 'text-green-600 dark:text-green-400',
31 | bgColor: 'bg-green-50 dark:bg-green-900/20',
32 | borderColor: 'border-green-200 dark:border-green-800'
33 | },
34 | {
35 | id: 'supabase',
36 | name: 'Supabase',
37 | icon: SupabaseIc,
38 | description: 'Open source Firebase alternative',
39 | color: 'text-emerald-600 dark:text-emerald-400',
40 | bgColor: 'bg-emerald-50 dark:bg-emerald-900/20',
41 | borderColor: 'border-emerald-200 dark:border-emerald-800'
42 | }
43 | // {
44 | // id: 'planetscale',
45 | // name: 'PlanetScale',
46 | // icon: GlobeIcon,
47 | // description: `The world's fastest relational database`,
48 | // color: 'text-purple-600 dark:text-purple-400',
49 | // bgColor: 'bg-purple-50 dark:bg-purple-900/20',
50 | // borderColor: 'border-purple-200 dark:border-purple-800'
51 | // },
52 | // {
53 | // id: 'turso',
54 | // name: 'Turso',
55 | // icon: TursoIc,
56 | // description: 'SQLite Databases for all Apps',
57 | // color: 'text-blue-600 dark:text-blue-400',
58 | // bgColor: 'bg-blue-50 dark:bg-blue-900/20',
59 | // borderColor: 'border-blue-200 dark:border-blue-800'
60 | // }
61 | ]
62 |
63 | export function DatabaseDeployments() {
64 | const [selectedProvider, setSelectedProvider] = useState('neon')
65 | const [isDeploying, setIsDeploying] = useState(false)
66 | const schema = useSchemaStore((store) => store.schema)
67 |
68 | if (schema && schema.databaseFormat !== 'postgresql') return null
69 |
70 | const isConnectionStringValid = (connectionString: string) => {
71 | if (!connectionString.trim() || !schema) {
72 | toast.error('Database connection string is missing.')
73 | return false
74 | }
75 |
76 | if (selectedProvider === 'neon') {
77 | const { isValid, errorMessage } = validateNeonConnectionString(connectionString)
78 | if (!isValid) {
79 | toast.error(errorMessage)
80 | return false
81 | }
82 | } else if (selectedProvider === 'supabase') {
83 | const { isValid, errorMessage } = validateSupabaseConnectionString(connectionString)
84 | if (!isValid) {
85 | toast.error(errorMessage)
86 | return false
87 | }
88 | }
89 |
90 | return true
91 | }
92 |
93 | const handleDeploy = async (e: FormEvent) => {
94 | e.preventDefault()
95 |
96 | const form = e.currentTarget
97 | const connectionStringInput = form.elements.namedItem('connectionString') as HTMLInputElement
98 | const connectionString = connectionStringInput.value
99 |
100 | const isValid = isConnectionStringValid(connectionString)
101 | if (!isValid) return
102 |
103 | setIsDeploying(true)
104 |
105 | try {
106 | const response = await fetch('api/deploy', {
107 | method: 'POST',
108 | body: JSON.stringify({
109 | url: connectionString,
110 | sqlSchema: schema!.sqlSchema
111 | }),
112 | headers: {
113 | 'Content-type': 'application/json'
114 | }
115 | })
116 | const data = await response.json()
117 | const { error, message } = data
118 | if (error) {
119 | throw new Error(error)
120 | }
121 |
122 | toast.message(message)
123 |
124 | triggerConfetti()
125 | } catch (error) {
126 | if (error instanceof Error) {
127 | toast.error(error.message)
128 | }
129 | } finally {
130 | setIsDeploying(false)
131 | form.reset()
132 | }
133 | }
134 |
135 | const getPlaceholder = () => {
136 | switch (selectedProvider) {
137 | case 'neon':
138 | return 'postgresql://[ROLE]:[PASSWORD]@[INSTANCE].west-001.aws.neon.tech/neondb'
139 | case 'supabase':
140 | return 'postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-SUPABASE-ID].supabase.co:5432/postgres'
141 | default:
142 | return 'Select a provider first'
143 | }
144 | }
145 |
146 | // const testConnection = async () => {
147 | // const isValid = isConnectionStringValid()
148 | // if (!isValid) return
149 |
150 | // const connectionString = inputRef.current?.value
151 |
152 | // setIsTesting(true)
153 |
154 | // try {
155 | // const response = await fetch('api/test-connection', {
156 | // method: 'POST',
157 | // body: JSON.stringify({
158 | // url: connectionString,
159 | // provider: selectedProvider
160 | // }),
161 | // headers: {
162 | // 'Content-type': 'application/json'
163 | // }
164 | // })
165 | // const data = await response.json()
166 | // console.log(data)
167 | // } catch (error) {
168 | // toast.error('An error has ocurred while deploying data')
169 | // } finally {
170 | // setIsTesting(false)
171 | // }
172 | // }
173 |
174 | return (
175 |
248 | )
249 | }
250 |
--------------------------------------------------------------------------------
/apps/web/components/database-picker.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Dispatch, SetStateAction } from 'react'
3 | import { MySQLIc, PostgreSQLIc, SQLiteIc } from '@/components/icons'
4 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5 |
6 | const schemas = [
7 | {
8 | id: 'postgresql',
9 | title: 'PostgreSQL',
10 | Icon: ,
11 | description: 'Advanced open-source relational database with strong SQL compliance.'
12 | },
13 | {
14 | id: 'mysql',
15 | title: 'MySQL',
16 | Icon: ,
17 | description: 'Popular open-source relational database management system.'
18 | },
19 | {
20 | id: 'sqlite',
21 | title: 'SQLite',
22 | Icon: ,
23 | description: 'Popular open-source relational database management system.'
24 | }
25 | ]
26 |
27 | type DatabasePickerProps = {
28 | databaseFormat: string
29 | setDatabaseFormat: Dispatch>
30 | }
31 |
32 | export function DatabasePicker({ databaseFormat, setDatabaseFormat }: DatabasePickerProps) {
33 | return (
34 |
35 |
36 | Choose Database Schema
37 | Choose a database provider to deploy your schema.
38 |
39 |
40 |
41 | {schemas.map((option) => (
42 |
setDatabaseFormat(option.id)}
50 | >
51 |
52 | {option.Icon}
53 |
{option.title}
54 |
{option.description}
55 |
56 |
57 | ))}
58 |
59 |
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/apps/web/components/footer.tsx:
--------------------------------------------------------------------------------
1 | export function Footer() {
2 | return (
3 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/components/header.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { AppLogo } from '@/components/app-logo'
3 |
4 | export function Header({ children }: { children?: ReactNode }) {
5 | return (
6 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/components/icons.tsx:
--------------------------------------------------------------------------------
1 | import { SVGProps } from 'react'
2 |
3 | export function PostgreSQLIc(props: SVGProps) {
4 | return (
5 |
73 | )
74 | }
75 |
76 | export function MySQLIc(props: SVGProps) {
77 | return (
78 |
92 | )
93 | }
94 |
95 | export function SupabaseIc(props: SVGProps) {
96 | return (
97 |
143 | )
144 | }
145 |
146 | // export function TursoIc (props: SVGProps) {
147 | // return (
148 | //
149 |
150 | // )
151 | // }
152 |
153 | export function NeonIc(props: SVGProps) {
154 | return (
155 |
191 | )
192 | }
193 |
194 | export function SQLiteIc(props: SVGProps) {
195 | return (
196 |
228 | )
229 | }
230 |
--------------------------------------------------------------------------------
/apps/web/components/options-results.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Card } from '@/components/ui/card'
3 | import { useSchemaStore } from '@/store'
4 | import { DatabaseDeployments } from '@/components/database-deployments'
5 |
6 | export function OptionsResults() {
7 | const schema = useSchemaStore((store) => store.schema)
8 | const { tables } = schema ?? {}
9 |
10 | return (
11 |
12 |
13 | Database Schema Information
14 |
15 | {tables && (
16 |
17 |
18 | Detected Tables({tables.length})
19 |
20 |
21 | {tables.map(({ name, numberOfColumns }) => (
22 | -
23 | {name}
24 | {numberOfColumns} columns
25 |
26 | ))}
27 |
28 |
29 | )}
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/apps/web/components/schema-results.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useState } from 'react'
3 | import { toast } from 'sonner'
4 | import { CheckIcon, CopyIcon, DownloadIcon } from 'lucide-react'
5 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
6 | import { CodeEditor } from '@/components/code-editor'
7 | import { Button } from '@/components/ui/button'
8 | import { useSchemaStore } from '@/store'
9 | import { copyToClipboard } from '@/utils'
10 |
11 | export function SchemaResults() {
12 | const [copied, setCopied] = useState(false)
13 | const schema = useSchemaStore((store) => store.schema)
14 | const { sqlSchema } = schema ?? {}
15 |
16 | const handleCopy = async () => {
17 | if (!sqlSchema) return
18 |
19 | await copyToClipboard(sqlSchema)
20 | setCopied(true)
21 | setTimeout(() => setCopied(false), 2000)
22 | toast.info('SQL has been copied to your clipboard')
23 | }
24 |
25 | const handleDownload = () => {
26 | if (!sqlSchema) return
27 |
28 | const blob = new Blob([sqlSchema], { type: 'text/plain' })
29 | const url = URL.createObjectURL(blob)
30 | const a = document.createElement('a')
31 | a.href = url
32 | a.download = `schema.sql`
33 | document.body.appendChild(a)
34 | a.click()
35 | document.body.removeChild(a)
36 | URL.revokeObjectURL(url)
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 | Generated SQL
44 | Database Schema
45 |
46 |
47 |
51 |
55 |
56 |
57 | {sqlSchema && }
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/apps/web/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils'
5 |
6 | const alertVariants = cva(
7 | 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
8 | {
9 | variants: {
10 | variant: {
11 | default: 'bg-background text-foreground',
12 | destructive:
13 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive'
14 | }
15 | },
16 | defaultVariants: {
17 | variant: 'default'
18 | }
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
27 | ))
28 | Alert.displayName = 'Alert'
29 |
30 | const AlertTitle = React.forwardRef>(
31 | ({ className, ...props }, ref) => (
32 |
37 | )
38 | )
39 | AlertTitle.displayName = 'AlertTitle'
40 |
41 | const AlertDescription = React.forwardRef<
42 | HTMLParagraphElement,
43 | React.HTMLAttributes
44 | >(({ className, ...props }, ref) => (
45 |
46 | ))
47 | AlertDescription.displayName = 'AlertDescription'
48 |
49 | export { Alert, AlertTitle, AlertDescription }
50 |
--------------------------------------------------------------------------------
/apps/web/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
13 | destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
14 | outline:
15 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
16 | secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
17 | ghost: 'hover:bg-accent hover:text-accent-foreground',
18 | link: 'text-primary underline-offset-4 hover:underline'
19 | },
20 | size: {
21 | default: 'h-9 px-4 py-2',
22 | sm: 'h-8 rounded-md px-3 text-xs',
23 | lg: 'h-10 rounded-md px-8',
24 | icon: 'h-9 w-9'
25 | }
26 | },
27 | defaultVariants: {
28 | variant: 'default',
29 | size: 'default'
30 | }
31 | }
32 | )
33 |
34 | export interface ButtonProps
35 | extends React.ButtonHTMLAttributes,
36 | VariantProps {
37 | asChild?: boolean
38 | }
39 |
40 | const Button = React.forwardRef(
41 | ({ className, variant, size, asChild = false, ...props }, ref) => {
42 | const Comp = asChild ? Slot : 'button'
43 | return (
44 |
45 | )
46 | }
47 | )
48 | Button.displayName = 'Button'
49 |
50 | export { Button, buttonVariants }
51 |
--------------------------------------------------------------------------------
/apps/web/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | const Card = React.forwardRef>(
6 | ({ className, ...props }, ref) => (
7 |
12 | )
13 | )
14 | Card.displayName = 'Card'
15 |
16 | const CardHeader = React.forwardRef>(
17 | ({ className, ...props }, ref) => (
18 |
19 | )
20 | )
21 | CardHeader.displayName = 'CardHeader'
22 |
23 | const CardTitle = React.forwardRef>(
24 | ({ className, ...props }, ref) => (
25 |
30 | )
31 | )
32 | CardTitle.displayName = 'CardTitle'
33 |
34 | const CardDescription = React.forwardRef<
35 | HTMLParagraphElement,
36 | React.HTMLAttributes
37 | >(({ className, ...props }, ref) => (
38 |
39 | ))
40 | CardDescription.displayName = 'CardDescription'
41 |
42 | const CardContent = React.forwardRef>(
43 | ({ className, ...props }, ref) => (
44 |
45 | )
46 | )
47 | CardContent.displayName = 'CardContent'
48 |
49 | const CardFooter = React.forwardRef>(
50 | ({ className, ...props }, ref) => (
51 |
52 | )
53 | )
54 | CardFooter.displayName = 'CardFooter'
55 |
56 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
57 |
--------------------------------------------------------------------------------
/apps/web/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface InputProps extends React.InputHTMLAttributes {}
6 |
7 | const Input = React.forwardRef(
8 | ({ className, type, ...props }, ref) => {
9 | return (
10 |
19 | )
20 | }
21 | )
22 | Input.displayName = 'Input'
23 |
24 | export { Input }
25 |
--------------------------------------------------------------------------------
/apps/web/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as LabelPrimitive from '@radix-ui/react-label'
5 | import { cva, type VariantProps } from 'class-variance-authority'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const labelVariants = cva(
10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef & VariantProps
16 | >(({ className, ...props }, ref) => (
17 |
18 | ))
19 | Label.displayName = LabelPrimitive.Root.displayName
20 |
21 | export { Label }
22 |
--------------------------------------------------------------------------------
/apps/web/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useTheme } from 'next-themes'
4 | import { Toaster as Sonner } from 'sonner'
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = 'system' } = useTheme()
10 |
11 | return (
12 |
26 | )
27 | }
28 |
29 | export { Toaster }
30 |
--------------------------------------------------------------------------------
/apps/web/constants.ts:
--------------------------------------------------------------------------------
1 | export const APP_URL =
2 | process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : 'https://snap2sql.app/'
3 |
--------------------------------------------------------------------------------
/apps/web/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/web/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {};
3 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "snap2sql",
3 | "version": "2.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "eslint . --max-warnings 0"
10 | },
11 | "dependencies": {
12 | "@ai-sdk/google": "1.2.12",
13 | "@ai-sdk/openai": "1.3.16",
14 | "@ai-sdk/react": "1.2.9",
15 | "@monaco-editor/react": "4.7.0",
16 | "@radix-ui/react-label": "2.1.4",
17 | "@radix-ui/react-slot": "1.2.0",
18 | "@upstash/ratelimit": "2.0.5",
19 | "@upstash/redis": "1.34.8",
20 | "@vercel/analytics": "1.5.0",
21 | "ai": "4.3.9",
22 | "canvas-confetti": "1.9.3",
23 | "class-variance-authority": "0.7.1",
24 | "clsx": "2.1.1",
25 | "drizzle-orm": "0.43.1",
26 | "lucide-react": "0.501.0",
27 | "monaco-editor": "0.52.2",
28 | "nanoid": "5.1.5",
29 | "next": "15.3.1",
30 | "next-themes": "0.4.6",
31 | "postgres": "3.4.5",
32 | "react": "19.1.0",
33 | "react-dom": "19.1.0",
34 | "sonner": "2.0.3",
35 | "tailwind-merge": "3.2.0",
36 | "tailwindcss-animate": "1.0.7",
37 | "zod": "3.24.1",
38 | "zustand": "4.5.6"
39 | },
40 | "devDependencies": {
41 | "@next/eslint-plugin-next": "15.3.1",
42 | "@repo/eslint-config": "workspace:*",
43 | "@repo/typescript-config": "workspace:*",
44 | "@types/canvas-confetti": "1.9.0",
45 | "@types/eslint": "8.56.5",
46 | "@types/node": "22.14.1",
47 | "@types/react": "19.1.2",
48 | "@types/react-dom": "19.1.2",
49 | "autoprefixer": "10.4.21",
50 | "drizzle-kit": "0.31.0",
51 | "eslint": "8.57.1",
52 | "postcss": "8.4.49",
53 | "tailwindcss": "3.4.17",
54 | "typescript": "5.8.3"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/prompt.ts:
--------------------------------------------------------------------------------
1 | /*
2 | 1.Add the following comment in uppercase at the top of each table: --TABLE
3 | 3.Utilize these PostgreSQL column types for Supabase: int2, int4, int8, float4, float8, numeric, json, jsonb, text, varchar, uuid, date, time, timetz, timestamp, timestamptz, bool.
4 |
5 | Ensure the generated SQL code accurately represents the visual schema for Supabase, including table relationships where present.
6 | */
7 |
8 | // const OLD_PG_PROMPT = `You're a PostgreSQL expert specializing in SQL diagram construction and need to follow specific guidelines:
9 |
10 | // 1.Analyze each column carefully. If column types aren't specified, use your expertise to select the appropriate type based on the column name.
11 | // 3.Don't add any extra column, just create those that are in the diagram.
12 | // 4.Regarding relationships, there are two approaches:
13 | // - If there are relationships in the diagram: Ensure to generate the corresponding SQL relationships between tables as depicted in the diagram.
14 | // - If no relationships are depicted: Utilize your expertise to infer and generate relationships between tables based on their structure or other available information.
15 |
16 | // Here is an example of table:
17 |
18 | // --TABLE
19 | // CREATE TABLE "public"."users" (
20 | // id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
21 | // name text,
22 | // email text,
23 | // created_at timestamp with time zone
24 | // );
25 |
26 | // Add always the schema name "public" before the table's name.
27 | // Important: Arrange table creation in the SQL script in a logical order to avoid reference errors. Tables that reference other tables should be created after the tables they reference.
28 | // If you identify that the input does not contain valid SQL diagram information (e.g., lacks tables and relationships), return the message: "Invalid SQL diagram."
29 | // Return the SQL code directly without adding any extra characters like backticks, explanations, or formatting.`
30 |
31 | export const PG_PROMPT = `You're a PostgreSQL expert specializing in SQL schema construction and need to follow these specific guidelines:
32 |
33 | 1. Add the following comment in uppercase at the top of each table: --TABLE
34 |
35 | 2. Naming conventions:
36 | • Use snake_case for all table and column names.
37 | • Enclose every identifier in double quotes (e.g. "public"."my_table").
38 | • Always prefix tables with the schema name "public".
39 |
40 | 3. Analyze each column carefully. If column types aren't specified, use your expertise to select the appropriate PostgreSQL-specific type based on the column name:
41 | - For IDs and keys: Use bigint, integer, or UUID as appropriate
42 | - For text fields: Use text for variable length or varchar(n) when length constraint is needed
43 | - For dates: Use date, timestamp, or timestamp with time zone
44 | - For numeric values: Use numeric, real, integer, smallint, or bigint as appropriate
45 | - For boolean values: Use boolean
46 | - For binary data: Use bytea
47 | - For JSON data: Use jsonb (preferred over json for efficiency)
48 | - For enum values: Create proper ENUM types when appropriate
49 |
50 | 4. Don't add any extra column, just create those that are in the diagram.
51 |
52 | 5. For primary keys:
53 | - Use GENERATED ALWAYS AS IDENTITY for auto-incrementing keys
54 | - For composite primary keys, declare them with CONSTRAINT at the end of the CREATE TABLE statement
55 |
56 | 6. For columns that should never be NULL, explicitly add NOT NULL constraint.
57 |
58 | 7. Constraints:
59 | - Name all constraints explicitly:
60 | CONSTRAINT pk_ PRIMARY KEY (...),
61 | CONSTRAINT uq__ UNIQUE (...),
62 | CONSTRAINT fk__ FOREIGN KEY (...) REFERENCES "public"."" (...)
63 | - For each FOREIGN KEY, include ON DELETE NO ACTION and ON UPDATE NO ACTION (or CASCADE if diagram indicates).
64 |
65 | 8. Regarding relationships:
66 | - If there are relationships in the diagram: Generate the corresponding SQL relationships between tables exactly as depicted.
67 | - If no relationships are depicted: Infer and generate relationships between tables based on column names and table structure.
68 |
69 | 9. Create appropriate indexes:
70 | - Automatically create indexes on foreign key columns
71 | - Create unique indexes/constraints where appropriate based on column names (email, username, etc.)
72 |
73 | Here is an example of table:
74 |
75 | --TABLE
76 | CREATE TABLE "public"."users" (
77 | "id" bigint GENERATED ALWAYS AS IDENTITY,
78 | "name" text NOT NULL,
79 | "email" text NOT NULL,
80 | "password_hash" text NOT NULL,
81 | "created_at" timestamp with time zone DEFAULT now(),
82 | "updated_at" timestamp with time zone DEFAULT now(),
83 |
84 | CONSTRAINT pk_users PRIMARY KEY ("id"),
85 | CONSTRAINT uq_users_email UNIQUE ("email")
86 | );
87 |
88 | CREATE INDEX idx_users_name ON "public"."users" ("name");
89 |
90 | 10. Always add the schema name "public" before the table's name.
91 |
92 | 11. For tables that store temporal data, include created_at and updated_at columns with appropriate defaults.
93 |
94 | 12. Ensure the generated SQL code accurately represents the visual schema for PostgreSQL, including all table relationships where present.
95 |
96 | 13. For text fields that should have a maximum length, use varchar(n) instead of text.
97 |
98 | 14. Arrange table creation in the SQL script in a logical order to avoid reference errors. Tables that reference other tables should be created after the tables they reference.
99 |
100 | 15. Add proper data constraints where applicable (CHECK constraints, NOT NULL, UNIQUE).
101 |
102 | 16. If you identify that the input does not contain valid SQL diagram information (e.g., lacks tables and relationships), return the message: "Invalid SQL diagram."
103 |
104 | Return the SQL code directly without adding any extra characters like backticks, explanations, or formatting, and the list of tables along with its number of columns.`
105 |
106 | export const MYSQL_PROMPT = `You're a MySQL expert specializing in SQL schema construction and need to follow these specific guidelines:
107 |
108 | 1. Add the following comment in uppercase at the top of each table: -- TABLE
109 |
110 | 2. Naming conventions:
111 | • Use snake_case for all table and column names.
112 | • Do not use quotes around identifiers unless absolutely necessary.
113 | • Use backticks ('table_name') only when the identifier is a reserved word.
114 |
115 | 3. Analyze each column carefully. If column types aren't specified, use your expertise to select the appropriate MySQL-specific type based on the column name:
116 | - For IDs and keys: Use INT, BIGINT with AUTO_INCREMENT, or CHAR(36) for UUID as appropriate
117 | - For text fields: Use VARCHAR(n) for variable length with reasonable limits, TEXT for longer content
118 | - For dates: Use DATE, DATETIME, or TIMESTAMP with appropriate precision
119 | - For numeric values: Use INT, BIGINT, DECIMAL(m,n), FLOAT or DOUBLE as appropriate
120 | - For boolean values: Use TINYINT(1) (MySQL standard for boolean)
121 | - For binary data: Use BLOB, MEDIUMBLOB, or LONGBLOB depending on size
122 | - For JSON data: Use JSON type (for MySQL 5.7+)
123 | - For enum values: Use ENUM type with appropriate values
124 |
125 | 4. Don't add any extra column, just create those that are in the diagram.
126 |
127 | 5. For primary keys:
128 | - Use AUTO_INCREMENT for auto-incrementing keys
129 | - For composite primary keys, declare them with PRIMARY KEY at the end of the CREATE TABLE statement
130 |
131 | 6. For columns that should never be NULL, explicitly add NOT NULL constraint.
132 |
133 | 7. Constraints:
134 | - Name all constraints explicitly:
135 | CONSTRAINT 'pk_table' PRIMARY KEY (...),
136 | CONSTRAINT 'uq_table_column' UNIQUE (...),
137 | CONSTRAINT 'fk_child_parent' FOREIGN KEY (...) REFERENCES 'parent (...)
138 | - For each FOREIGN KEY, include ON DELETE and ON UPDATE actions (RESTRICT, CASCADE, SET NULL, etc.)
139 |
140 | 8. Regarding relationships:
141 | - If there are relationships in the diagram: Generate the corresponding SQL relationships between tables exactly as depicted.
142 | - If no relationships are depicted: Infer and generate relationships between tables based on column names and table structure.
143 |
144 | 9. Create appropriate indexes:
145 | - Add INDEX to foreign key columns that aren't already part of an index
146 | - Create unique indexes/constraints where appropriate based on column names (email, username, etc.)
147 |
148 | 10. Include proper table comments using COMMENT = 'description' at the table level and column level.
149 |
150 | Here is an example of table:
151 |
152 | -- TABLE
153 | CREATE TABLE 'users' (
154 | 'id' INT AUTO_INCREMENT,
155 | 'name' VARCHAR(255) NOT NULL,
156 | 'email' VARCHAR(255) NOT NULL,
157 | 'password_hash' VARCHAR(255) NOT NULL,
158 | 'created_at' TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
159 | 'updated_at' TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
160 |
161 | CONSTRAINT 'pk_users' PRIMARY KEY ('id'),
162 | CONSTRAINT 'uq_users_email' UNIQUE ('email')
163 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Application users';
164 |
165 | CREATE INDEX 'idx_users_name' ON 'users ('name');
166 |
167 | 11. Always add proper ENGINE specification (typically InnoDB) and CHARACTER SET/COLLATION declarations.
168 |
169 | 12. For tables that store temporal data, include created_at and updated_at columns with appropriate defaults:
170 | - For created_at: DEFAULT CURRENT_TIMESTAMP
171 | - For updated_at: DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
172 |
173 | 13. Ensure the generated SQL code accurately represents the visual schema for MySQL, including all table relationships where present.
174 |
175 | 14. For all text fields, always specify a maximum length using VARCHAR(n) or appropriate TEXT type.
176 |
177 | 15. Arrange table creation in the SQL script in a logical order to avoid reference errors. Tables that reference other tables should be created after the tables they reference.
178 |
179 | 16. Add proper data constraints where applicable (CHECK constraints for MySQL 8.0+, NOT NULL, UNIQUE).
180 |
181 | 17. If you identify that the input does not contain valid SQL diagram information (e.g., lacks tables and relationships), return the message: "Invalid SQL diagram."
182 |
183 | Return the SQL code directly without adding any extra characters like triple backticks, explanations, or formatting, and the list of tables along with its number of columns.`
184 |
185 | export const SQLITE_PROMPT = `You're a SQLite expert specializing in SQL schema construction and need to follow these specific guidelines:
186 |
187 | 1. Add the following comment in uppercase at the top of each table: -- TABLE
188 |
189 | 2. Naming conventions:
190 | • Use snake_case for all table and column names.
191 | • Do not use quotes around identifiers unless absolutely necessary.
192 | • Only use quotes when the identifier is a reserved word.
193 |
194 | 3. Analyze each column carefully. If column types aren't specified, use your expertise to select the appropriate SQLite-specific type based on the column name:
195 | - For IDs and keys: Use INTEGER PRIMARY KEY AUTOINCREMENT for auto-incrementing primary keys
196 | - For text fields: Use TEXT for variable length strings
197 | - For dates: Use TEXT for ISO8601 strings ("YYYY-MM-DD HH:MM:SS.SSS") or INTEGER for Unix time
198 | - For numeric values: Use INTEGER for whole numbers, REAL for floating-point values
199 | - For boolean values: Use INTEGER (0 for false, 1 for true)
200 | - For binary data: Use BLOB
201 | - Keep in mind SQLite uses dynamic typing with only 5 storage classes: NULL, INTEGER, REAL, TEXT, and BLOB
202 |
203 | 4. Don't add any extra column, just create those that are in the diagram.
204 |
205 | 5. For primary keys:
206 | - Use INTEGER PRIMARY KEY AUTOINCREMENT for single-column auto-incrementing keys
207 | - For composite primary keys, declare them with PRIMARY KEY at the end of the CREATE TABLE statement
208 |
209 | 6. For columns that should never be NULL, explicitly add NOT NULL constraint.
210 |
211 | 7. Constraints:
212 | - Name all constraints explicitly:
213 | CONSTRAINT pk_table PRIMARY KEY (...),
214 | CONSTRAINT uq_table_column UNIQUE (...),
215 | CONSTRAINT fk_child_parent FOREIGN KEY (...) REFERENCES parent (...)
216 | - For each FOREIGN KEY, include ON DELETE and ON UPDATE actions (RESTRICT, CASCADE, SET NULL, etc.)
217 |
218 | 8. Regarding relationships:
219 | - If there are relationships in the diagram: Generate the corresponding SQL relationships between tables exactly as depicted.
220 | - If no relationships are depicted: Infer and generate relationships between tables based on column names and table structure.
221 |
222 | 9. Create appropriate indexes:
223 | - Add CREATE INDEX statements for foreign key columns that aren't already part of an index
224 | - Create unique indexes/constraints where appropriate based on column names (email, username, etc.)
225 |
226 | 10. Include table comments as SQL comments above each table.
227 |
228 | Here is an example of table:
229 |
230 | -- TABLE
231 | -- Users table to store application user data
232 | CREATE TABLE users (
233 | id INTEGER PRIMARY KEY AUTOINCREMENT,
234 | name TEXT NOT NULL,
235 | email TEXT NOT NULL,
236 | password_hash TEXT NOT NULL,
237 | created_at TEXT DEFAULT (datetime('now')),
238 | updated_at TEXT DEFAULT (datetime('now')),
239 |
240 | CONSTRAINT uq_users_email UNIQUE (email)
241 | );
242 |
243 | CREATE INDEX idx_users_name ON users (name);
244 |
245 | 11. For tables that store temporal data, include created_at and updated_at columns with appropriate defaults:
246 | - For created_at: DEFAULT (datetime('now'))
247 | - For updated_at: DEFAULT (datetime('now'))
248 |
249 | 12. Ensure the generated SQL code accurately represents the visual schema for SQLite, including all table relationships where present.
250 |
251 | 13. SQLite specific considerations:
252 | - Remember that SQLite does not enforce column data types strictly
253 | - Include CHECK constraints where appropriate for data validation
254 | - Use built-in date and time functions like datetime('now') for defaults
255 |
256 | 14. Arrange table creation in the SQL script in a logical order to avoid reference errors. Tables that reference other tables should be created after the tables they reference.
257 |
258 | 15. Add proper data constraints where applicable (CHECK constraints, NOT NULL, UNIQUE).
259 |
260 | 16. If you identify that the input does not contain valid SQL diagram information (e.g., lacks tables and relationships), return the message: "Invalid SQL diagram."
261 |
262 | Return the SQL code directly without adding any extra characters like triple backticks, explanations, or formatting, and the list of tables along with its number of columns.`
263 |
--------------------------------------------------------------------------------
/apps/web/public/banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xavimondev/vdbs/379a05d3a85922586c4bb3eb6b7cbc0e05d03efc/apps/web/public/banner.jpg
--------------------------------------------------------------------------------
/apps/web/public/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xavimondev/vdbs/379a05d3a85922586c4bb3eb6b7cbc0e05d03efc/apps/web/public/logo.webp
--------------------------------------------------------------------------------
/apps/web/public/medal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xavimondev/vdbs/379a05d3a85922586c4bb3eb6b7cbc0e05d03efc/apps/web/public/medal.png
--------------------------------------------------------------------------------
/apps/web/services/deploy.ts:
--------------------------------------------------------------------------------
1 | type SchemaDeploy = {
2 | sqlSchema: string
3 | url: string
4 | }
5 |
6 | export const schemaDeploy = async (
7 | schemaDeploy: SchemaDeploy
8 | ): Promise<{ message?: string; error?: string }> => {
9 | try {
10 | const request = await fetch('api/deploy', {
11 | method: 'POST',
12 | body: JSON.stringify(schemaDeploy),
13 | headers: {
14 | 'Content-type': 'application/json'
15 | }
16 | })
17 | const result = await request.json()
18 | return result
19 | } catch (error) {
20 | return {
21 | error: 'An error has ocurred while deploying.'
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/store.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 |
3 | type Table = {
4 | name: string
5 | numberOfColumns: number
6 | }
7 |
8 | type SchemaData = {
9 | sqlSchema: string
10 | tables: Table[]
11 | databaseFormat: string
12 | }
13 |
14 | type SchemaState = {
15 | schema: SchemaData | undefined
16 | setSchema: (schema: SchemaData) => void
17 | }
18 |
19 | export const useSchemaStore = create()((set) => ({
20 | schema: undefined,
21 | setSchema: (schema) => set({ schema })
22 | }))
23 |
--------------------------------------------------------------------------------
/apps/web/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config = {
4 | darkMode: ['class'],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}'
10 | ],
11 | prefix: '',
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: '2rem',
16 | screens: {
17 | '2xl': '1400px'
18 | }
19 | },
20 | extend: {
21 | colors: {
22 | border: 'hsl(var(--border))',
23 | input: 'hsl(var(--input))',
24 | ring: 'hsl(var(--ring))',
25 | background: 'hsl(var(--background))',
26 | foreground: 'hsl(var(--foreground))',
27 | primary: {
28 | DEFAULT: 'hsl(var(--primary))',
29 | foreground: 'hsl(var(--primary-foreground))'
30 | },
31 | secondary: {
32 | DEFAULT: 'hsl(var(--secondary))',
33 | foreground: 'hsl(var(--secondary-foreground))'
34 | },
35 | destructive: {
36 | DEFAULT: 'hsl(var(--destructive))',
37 | foreground: 'hsl(var(--destructive-foreground))'
38 | },
39 | muted: {
40 | DEFAULT: 'hsl(var(--muted))',
41 | foreground: 'hsl(var(--muted-foreground))'
42 | },
43 | accent: {
44 | DEFAULT: 'hsl(var(--accent))',
45 | foreground: 'hsl(var(--accent-foreground))'
46 | },
47 | popover: {
48 | DEFAULT: 'hsl(var(--popover))',
49 | foreground: 'hsl(var(--popover-foreground))'
50 | },
51 | card: {
52 | DEFAULT: 'hsl(var(--card))',
53 | foreground: 'hsl(var(--card-foreground))'
54 | }
55 | },
56 | borderRadius: {
57 | lg: 'var(--radius)',
58 | md: 'calc(var(--radius) - 2px)',
59 | sm: 'calc(var(--radius) - 4px)'
60 | },
61 | keyframes: {
62 | 'accordion-down': {
63 | from: { height: '0' },
64 | to: { height: 'var(--radix-accordion-content-height)' }
65 | },
66 | 'accordion-up': {
67 | from: { height: 'var(--radix-accordion-content-height)' },
68 | to: { height: '0' }
69 | },
70 | 'fade-slide': {
71 | '0%': { opacity: '0', transform: 'translateY(20px)' },
72 | '100%': { opacity: '1' }
73 | },
74 | blink: {
75 | '0%, 100%': {
76 | 'box-shadow': 'inset 0 0 150px rgba(34, 197, 95, 0.3)',
77 | 'border-color': 'rgba(34, 197, 95, 0.4)'
78 | },
79 | '50%': {
80 | 'box-shadow': 'inset 0 0 180px rgba(34, 197, 95, 0.7)',
81 | 'border-color': 'rgba(34, 197, 95, 1)'
82 | }
83 | }
84 | },
85 | animation: {
86 | 'accordion-down': 'accordion-down 0.2s ease-out',
87 | 'accordion-up': 'accordion-up 0.2s ease-out',
88 | 'fade-slide': 'fade-slide 0.3s forwards',
89 | blink: 'blink 3s infinite'
90 | }
91 | }
92 | },
93 | plugins: [require('tailwindcss-animate')]
94 | } satisfies Config
95 |
96 | export default config
97 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [
5 | {
6 | "name": "next"
7 | }
8 | ],
9 | "paths": {
10 | "@/*": ["./*"]
11 | }
12 | },
13 | "include": [
14 | "next-env.d.ts",
15 | "next.config.js",
16 | "**/*.ts",
17 | "**/*.tsx",
18 | ".next/types/**/*.ts"
19 | ],
20 | "exclude": ["node_modules"],
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/apps/web/utils.ts:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from 'nanoid'
2 | import confetti from 'canvas-confetti'
3 |
4 | type SupportedImageTypes = 'image/jpeg' | 'image/png' | 'image/webp'
5 |
6 | export const isSupportedImageType = (type: string): type is SupportedImageTypes => {
7 | return ['image/jpeg', 'image/png', 'image/webp'].includes(type)
8 | }
9 |
10 | export const toBase64 = (file: File | Blob): Promise => {
11 | return new Promise((resolve, reject) => {
12 | const reader = new FileReader()
13 | reader.readAsDataURL(file)
14 | reader.onload = () => {
15 | if (typeof reader.result !== 'string') return
16 | resolve(reader.result)
17 | }
18 | reader.onerror = (error) => reject(error)
19 | })
20 | }
21 |
22 | export const copyToClipboard = async (content: string) => {
23 | if (navigator.clipboard) navigator.clipboard.writeText(content)
24 | }
25 |
26 | export const nanoid = customAlphabet(
27 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
28 | 7
29 | )
30 |
31 | const count = 200
32 | const defaults = {
33 | origin: { y: 0.7 }
34 | }
35 |
36 | const fire = (particleRatio: number, opts: any) => {
37 | confetti({
38 | ...defaults,
39 | ...opts,
40 | particleCount: Math.floor(count * particleRatio)
41 | })
42 | }
43 |
44 | export const triggerConfetti = () => {
45 | fire(0.25, {
46 | spread: 26,
47 | startVelocity: 55
48 | })
49 | fire(0.2, {
50 | spread: 60
51 | })
52 | fire(0.35, {
53 | spread: 100,
54 | decay: 0.91,
55 | scalar: 0.8
56 | })
57 | fire(0.1, {
58 | spread: 120,
59 | startVelocity: 25,
60 | decay: 0.92,
61 | scalar: 1.2
62 | })
63 | fire(0.1, {
64 | spread: 120,
65 | startVelocity: 45
66 | })
67 | }
68 |
69 | export const getReferenceId = (connectionString: string) => {
70 | const chunks = connectionString.split('.')
71 | if (chunks.length >= 2) {
72 | const referenceId = chunks[1]?.split(':')[0]
73 | return referenceId
74 | }
75 | return
76 | }
77 |
78 | export const extractTableNames = (sqlScript: string) => {
79 | const regex = /CREATE TABLE "public"\."([^"]+)"/g
80 | const tableNames = []
81 | let match
82 |
83 | while ((match = regex.exec(sqlScript)) !== null) {
84 | tableNames.push(match[1])
85 | }
86 |
87 | return tableNames
88 | }
89 |
--------------------------------------------------------------------------------
/apps/web/utils/ai.ts:
--------------------------------------------------------------------------------
1 | import { MYSQL_PROMPT, PG_PROMPT, SQLITE_PROMPT } from '@/prompt'
2 | import { z } from 'zod'
3 |
4 | export const DB_SCHEMA = z.object({
5 | results: z.object({
6 | sqlSchema: z.string(),
7 | tables: z.array(
8 | z.object({
9 | name: z.string(),
10 | numberOfColumns: z.number()
11 | })
12 | )
13 | })
14 | })
15 |
16 | export const prompts: Record = {
17 | mysql: MYSQL_PROMPT,
18 | postgresql: PG_PROMPT,
19 | sqlite: SQLITE_PROMPT
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/utils/connection-string-validations.ts:
--------------------------------------------------------------------------------
1 | type ValidationResult = {
2 | isValid: boolean
3 | errorMessage: string
4 | }
5 |
6 | export const validateNeonConnectionString = (connectionString: string): ValidationResult => {
7 | // postgresql://[ROLE]:[PASSWORD]@[INSTANCE].[REGION].[PROVIDER].neon.tech/neondb?sslmode=require
8 | const neonPattern = /^postgresql:\/\/([^:]+):([^@]+)@/
9 |
10 | if (!connectionString) {
11 | return {
12 | isValid: false,
13 | errorMessage: 'Connection string cannot be empty.'
14 | }
15 | }
16 |
17 | if (!neonPattern.test(connectionString)) {
18 | return {
19 | isValid: false,
20 | errorMessage: 'Invalid format. Must follow the pattern.'
21 | }
22 | }
23 |
24 | // Validaciones adicionales específicas para Neon
25 | if (
26 | connectionString.includes('[ROLE]') ||
27 | connectionString.includes('[PASSWORD]') ||
28 | connectionString.includes('[INSTANCE]') ||
29 | connectionString.includes('[REGION]') ||
30 | connectionString.includes('[PROVIDER]')
31 | ) {
32 | return {
33 | isValid: false,
34 | errorMessage:
35 | 'Replace the placeholders [ROLE], [PASSWORD], [INSTANCE], [REGION] y [PROVIDER] con tus datos reales'
36 | }
37 | }
38 |
39 | return {
40 | isValid: true,
41 | errorMessage: ''
42 | }
43 | }
44 |
45 | export const validateSupabaseConnectionString = (connectionString: string): ValidationResult => {
46 | // postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-SUPABASE-ID].supabase.co:5432/postgres
47 | const supabasePattern = /^postgresql:\/\/postgres:[^@]+@db\.[^.]+\.supabase\.co:5432\/postgres$/
48 |
49 | if (!connectionString) {
50 | return {
51 | isValid: false,
52 | errorMessage: 'Connection string cannot be empty'
53 | }
54 | }
55 |
56 | if (!supabasePattern.test(connectionString)) {
57 | return {
58 | isValid: false,
59 | errorMessage:
60 | 'Invalid format. Must follow the pattern: postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-SUPABASE-ID].supabase.co:5432/postgres'
61 | }
62 | }
63 |
64 | // Validaciones adicionales específicas para Supabase
65 | if (
66 | connectionString.includes('[YOUR-PASSWORD]') ||
67 | connectionString.includes('[YOUR-SUPABASE-ID]')
68 | ) {
69 | return {
70 | isValid: false,
71 | errorMessage:
72 | 'Replace the placeholders [YOUR-PASSWORD] and [YOUR-SUPABASE-ID] with your actual data'
73 | }
74 | }
75 |
76 | return {
77 | isValid: true,
78 | errorMessage: ''
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/apps/web/utils/database.ts:
--------------------------------------------------------------------------------
1 | import { drizzle } from 'drizzle-orm/postgres-js'
2 | import postgres from 'postgres'
3 | import { sql } from 'drizzle-orm'
4 |
5 | // type Provider = 'neon' | 'supabase' | 'turso'
6 |
7 | export async function deploySchema(connectionString: string, sqlSchema: string) {
8 | try {
9 | // Disable prefetch as it is not supported for "Transaction" pool mode
10 | const client = postgres(connectionString, { prepare: false })
11 | const db = drizzle(client)
12 | await db.execute(sql`SELECT NOW()`)
13 |
14 | // Execute the migration
15 | await db.execute(sql.raw(sqlSchema))
16 |
17 | return { success: true }
18 | } catch (error) {
19 | // @ts-ignore
20 | let message = error.code
21 | if (message === 'SASL_SIGNATURE_MISMATCH') {
22 | message = 'Database password is missing.'
23 | } else if (message === 'ENOTFOUND') {
24 | message =
25 | 'Your connection URL is invalid. Please double-check it and make the necessary corrections.'
26 | } else {
27 | message = 'Unknown error occurred'
28 | }
29 |
30 | return {
31 | success: false,
32 | message
33 | // error: error instanceof Error ? error.message : 'Unknown error occurred'
34 | }
35 | }
36 | }
37 |
38 | // export async function testConnection(connectionString: string) {
39 | // try {
40 | // // Disable prefetch as it is not supported for "Transaction" pool mode
41 | // const client = postgres(connectionString, { prepare: false })
42 | // const db = drizzle(client)
43 | // await db.execute('SELECT NOW()')
44 | // // if (provider === 'supabase' || provider === 'neon') {
45 | // // } else {
46 | // // throw new Error(`Unsupported provider: ${provider}`)
47 | // // }
48 |
49 | // return { success: true }
50 | // } catch (error) {
51 | // console.log(error)
52 | // return {
53 | // success: false,
54 | // error: error instanceof Error ? error.message : 'Unknown error occurred'
55 | // }
56 | // }
57 | // }
58 |
--------------------------------------------------------------------------------
/apps/web/utils/rate-limit.ts:
--------------------------------------------------------------------------------
1 | import { Ratelimit } from '@upstash/ratelimit'
2 | import { Redis } from '@upstash/redis'
3 |
4 | export const MAX_REQUESTS = 3
5 |
6 | export const uptash = new Ratelimit({
7 | redis: Redis.fromEnv(),
8 | limiter: Ratelimit.slidingWindow(MAX_REQUESTS, '1440 m'), // 3 per day
9 | analytics: true,
10 | prefix: '@ratelimit/snap2sql'
11 | })
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "code2sql",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo build",
6 | "dev": "turbo dev",
7 | "lint": "turbo lint",
8 | "prettier": "prettier --write \"**/*.{ts,tsx,md}\"",
9 | "lint:cli": "turbo lint --filter=vdbs",
10 | "build:cli": "turbo --filter=vdbs build",
11 | "dev:cli": "turbo --filter=vdbs dev",
12 | "release": "changeset version",
13 | "changeset:add": "changeset add",
14 | "changeset:status": "changeset status --verbose",
15 | "release:cli": "changeset publish",
16 | "dev:web": "turbo --filter=web dev"
17 | },
18 | "devDependencies": {
19 | "@repo/eslint-config": "workspace:*",
20 | "@repo/typescript-config": "workspace:*",
21 | "prettier": "^3.2.5",
22 | "turbo": "latest"
23 | },
24 | "packageManager": "pnpm@8.15.9",
25 | "engines": {
26 | "node": ">=20"
27 | },
28 | "dependencies": {
29 | "@changesets/cli": "2.27.1"
30 | }
31 | }
--------------------------------------------------------------------------------
/packages/cli/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
--------------------------------------------------------------------------------
/packages/cli/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # vdbs
2 |
3 | ## 0.0.5
4 |
5 | ### Patch Changes
6 |
7 | - patch supabase version cli
8 |
9 | ## 0.0.4
10 |
11 | ### Patch Changes
12 |
13 | - add error handling
14 |
15 | ## 0.0.3
16 |
17 | ### Patch Changes
18 |
19 | - add node-fetch for backwards compability
20 |
21 | ## 0.0.2
22 |
23 | ### Patch Changes
24 |
25 | - update fetch
26 |
--------------------------------------------------------------------------------
/packages/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vdbs",
3 | "version": "0.0.5",
4 | "description": "Generate migrations from database diagrams",
5 | "license": "MIT",
6 | "keywords": [
7 | "Supabase",
8 | "SQL",
9 | "Database",
10 | "Migrations"
11 | ],
12 | "type": "module",
13 | "exports": "./dist/index.js",
14 | "bin": "./dist/index.js",
15 | "main": "./dist/index.js",
16 | "module": "./dist/index.mjs",
17 | "types": "./dist/index.d.ts",
18 | "scripts": {
19 | "dev": "tsup --watch",
20 | "build": "tsup",
21 | "lint": "tsc --noEmit",
22 | "start": "node dist/index.js",
23 | "clean": "rimraf dist",
24 | "pub:beta": "pnpm build && pnpm publish --no-git-checks --tag beta",
25 | "pub:release": "pnpm build && pnpm publish"
26 | },
27 | "author": {
28 | "name": "xavimondev",
29 | "url": "https://twitter.com/xavimondev"
30 | },
31 | "devDependencies": {
32 | "@types/node": "20.11.24",
33 | "@types/prompts": "2.4.9",
34 | "rimraf": "5.0.7",
35 | "tsup": "8.1.0",
36 | "type-fest": "4.20.1",
37 | "typescript": "5.3.3",
38 | "zod": "3.23.8"
39 | },
40 | "dependencies": {
41 | "@antfu/ni": "0.21.12",
42 | "chalk": "5.3.0",
43 | "commander": "12.1.0",
44 | "execa": "9.3.0",
45 | "glob": "10.4.2",
46 | "node-fetch": "3.3.2",
47 | "ora": "8.0.1",
48 | "prompts": "2.4.2"
49 | },
50 | "engines": {
51 | "node": ">=18"
52 | }
53 | }
--------------------------------------------------------------------------------
/packages/cli/src/commands/add.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 | import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
3 | import { Command } from 'commander'
4 | import chalk from 'chalk'
5 | import ora from 'ora'
6 | import { execa } from 'execa'
7 | import prompts from 'prompts'
8 | import * as z from 'zod'
9 | import { glob } from 'glob'
10 | import { logger } from '@/utils/logger.js'
11 | import { listCommands } from '@/utils/list-commands.js'
12 | import { showNextSteps } from '@/utils/show-next-steps.js'
13 | import { getSchema } from '@/utils/get-schema.js'
14 | import { handleError } from '@/utils/handleError.js'
15 |
16 | const addArgumentsSchema = z.object({
17 | generation: z.string().optional()
18 | })
19 |
20 | const promptSchema = z.object({
21 | path: z.string().trim().min(1)
22 | })
23 |
24 | export const add = new Command()
25 | .name('add')
26 | .arguments('[generation]')
27 | .description('Add Database Migration')
28 | .action(async (generation) => {
29 | try {
30 | const options = addArgumentsSchema.parse({ generation })
31 | let generationCode = options.generation
32 | if (!options.generation) {
33 | const { generation } = await prompts([
34 | {
35 | type: 'text',
36 | name: 'generation',
37 | message: 'Enter your generation code'
38 | }
39 | ])
40 | generationCode = generation
41 | }
42 | if (!generationCode) {
43 | logger.warn('Exiting due to no generation code provided')
44 | process.exit(0)
45 | }
46 |
47 | const schema = await getSchema(generationCode)
48 | const { data: schemaSql, error } = schema
49 | if (!schemaSql || error) {
50 | logger.error('The given generation code is wrong. Enter one valid.')
51 | process.exit(0)
52 | }
53 |
54 | // Looking for supabase path. I assume this cli is being running on a supabase project
55 | const cwd = path.resolve(process.cwd())
56 | let defaultDirectory = `${cwd}/supabase`
57 | // config.toml is the core file in supabase project
58 | const search = path.join(cwd, '**/supabase/config.toml')
59 | const result = await glob(search)
60 | const pathFound = result.at(0)
61 | if (pathFound) {
62 | defaultDirectory = path.dirname(pathFound)
63 | }
64 | const relativePath = path.relative(cwd, defaultDirectory)
65 | const config = await promptForConfig(relativePath)
66 | const pickedPath = config.path ?? relativePath
67 |
68 | // Let's check supabase folder existence
69 | if (!existsSync(pickedPath) && pickedPath !== 'supabase') {
70 | mkdirSync(pickedPath, {
71 | recursive: true
72 | })
73 | }
74 |
75 | // So, there's no supabase project, let's run supabase init to initialize supabse project
76 | if (!pathFound) {
77 | const spinner = ora(`Initializing Supabase project...`).start()
78 | // 1. Initialize supabase project
79 | const executeCommand = await listCommands(cwd)
80 | await execa(executeCommand, ['supabase@1.162.4', 'init'], {
81 | cwd: pickedPath === 'supabase' ? cwd : pickedPath
82 | })
83 | spinner.succeed()
84 | }
85 |
86 | const migrationSpinner = ora(`Adding migrations...`).start()
87 | // TODO: Find a better way to do this
88 | // 2. Create a folder migration...
89 | const migrationPath = path.join(
90 | `${
91 | pickedPath === 'supabase'
92 | ? `${cwd}/supabase`
93 | : pathFound
94 | ? pickedPath
95 | : `${pickedPath}/supabase`
96 | }`,
97 | 'migrations'
98 | )
99 |
100 | if (!existsSync(migrationPath)) {
101 | mkdirSync(migrationPath, {
102 | recursive: true
103 | })
104 | }
105 |
106 | // 3. Add tables sql to migration file
107 | const now = new Date()
108 | const formattedTimestamp = now.toISOString().replace(/\D/g, '').slice(0, 14)
109 | const fileMigrationName = `${formattedTimestamp}_initial_state_schema.sql`
110 |
111 | writeFileSync(path.join(migrationPath, fileMigrationName), schemaSql, 'utf-8')
112 | migrationSpinner.succeed()
113 | logger.info(`${chalk.green('Success!')} Migration added successfully.`)
114 | logger.break()
115 | // 4. In order to deploy the migration remotely, user has to do the following...
116 | showNextSteps(Boolean(pathFound))
117 | } catch (error) {
118 | handleError(error)
119 | }
120 | })
121 |
122 | export const promptForConfig = async (defaultDirectory: string) => {
123 | const highlight = (text: string) => chalk.cyan(text)
124 | const options = await prompts([
125 | {
126 | type: 'text',
127 | name: 'path',
128 | message: `Where would you like to add your ${highlight('migrations')} ?`,
129 | initial: defaultDirectory
130 | }
131 | ])
132 | const config = promptSchema.parse(options)
133 | return config
134 | }
135 |
--------------------------------------------------------------------------------
/packages/cli/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { Command } from 'commander'
3 | import { getPackageInfo } from '@/utils/package-info.js'
4 | import { add } from '@/commands/add.js'
5 |
6 | process.on('SIGINT', () => process.exit(0))
7 | process.on('SIGTERM', () => process.exit(0))
8 |
9 | async function main() {
10 | const packageInfo = getPackageInfo()
11 |
12 | const program = new Command()
13 | .name('vdbs')
14 | .description('Add a migration schema to your Supabase project')
15 | .version(packageInfo.version || '0.0.1', '-v, --version', 'display the version number')
16 |
17 | program.addCommand(add)
18 | program.parse()
19 | }
20 |
21 | main()
22 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/get-schema.ts:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch'
2 |
3 | const ENDPOINT = `https://vdbs.vercel.app/api/get-generation?code=`
4 |
5 | type DataResponse = { data?: string; error?: string }
6 |
7 | export const getSchema = async (generationCode: string): Promise => {
8 | try {
9 | const response = await fetch(`${ENDPOINT}${generationCode}`)
10 | const data = (await response.json()) as DataResponse
11 | return data
12 | } catch (error) {
13 | console.error(error)
14 | return {
15 | error: 'An error has ocurred.'
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/handleError.ts:
--------------------------------------------------------------------------------
1 | import { logger } from '@/utils/logger.js'
2 |
3 | export function handleError(error: unknown) {
4 | if (typeof error === 'string') {
5 | logger.error(error)
6 | process.exit(1)
7 | }
8 |
9 | if (error instanceof Error) {
10 | logger.error(error.message)
11 | process.exit(1)
12 | }
13 |
14 | logger.error('Something went wrong. Please try again.')
15 | process.exit(1)
16 | }
17 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/list-commands.ts:
--------------------------------------------------------------------------------
1 | import { detect } from '@antfu/ni'
2 |
3 | type CommandExecute = 'yarn dlx' | 'pnpm dlx' | 'bunx' | 'npx'
4 |
5 | export const listCommands = async (targetDir: string): Promise => {
6 | const packageManager = await detect({ programmatic: true, cwd: targetDir })
7 |
8 | if (packageManager === 'yarn@berry') return 'yarn dlx'
9 | if (packageManager === 'pnpm@6') return 'pnpm dlx'
10 | if (packageManager === 'bun') return 'bunx'
11 |
12 | return 'npx'
13 | }
14 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/logger.ts:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 |
3 | export const logger = {
4 | error(...args: unknown[]) {
5 | console.log(chalk.red(...args))
6 | },
7 | warn(...args: unknown[]) {
8 | console.log(chalk.yellow(...args))
9 | },
10 | info(...args: unknown[]) {
11 | console.log(chalk.cyan(...args))
12 | },
13 | success(...args: unknown[]) {
14 | console.log(chalk.green(...args))
15 | },
16 | break() {
17 | console.log('')
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/package-info.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 | import fs from 'node:fs'
3 | import { type PackageJson } from 'type-fest'
4 |
5 | export const getPackageInfo = (): any => {
6 | const packageJsonPath = path.join('package.json')
7 | return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as PackageJson
8 | }
9 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/show-next-steps.ts:
--------------------------------------------------------------------------------
1 | import { logger } from '@/utils/logger.js'
2 |
3 | export const showNextSteps = (projectExists: boolean) => {
4 | logger.info('Next Steps:')
5 | if (!projectExists) {
6 | console.log('supabase login')
7 | console.log('supabase link --project-ref YOUR_PROJECT_ID')
8 | }
9 |
10 | // In case the user has already set up a Supabase project, simply push the migration
11 | console.log('supabase db push --linked')
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "../typescript-config/base.json",
4 | "compilerOptions": {
5 | "isolatedModules": false,
6 | "noEmit": true,
7 | "target": "ESNext",
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["src/*"]
11 | }
12 | },
13 | "include": ["src/**/*.ts"],
14 | "exclude": ["node_modules", "dist"]
15 | }
16 |
--------------------------------------------------------------------------------
/packages/cli/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 |
3 | export default defineConfig({
4 | entry: ['src/index.ts'],
5 | clean: true,
6 | dts: true,
7 | format: ['esm', 'cjs'],
8 | sourcemap: true,
9 | minify: true,
10 | tsconfig: 'tsconfig.json'
11 | })
12 |
--------------------------------------------------------------------------------
/packages/eslint-config/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/packages/eslint-config/library.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /** @type {import("eslint").Linter.Config} */
6 | module.exports = {
7 | extends: ["eslint:recommended", "prettier", "eslint-config-turbo"],
8 | plugins: ["only-warn"],
9 | globals: {
10 | React: true,
11 | JSX: true,
12 | },
13 | env: {
14 | node: true,
15 | },
16 | settings: {
17 | "import/resolver": {
18 | typescript: {
19 | project,
20 | },
21 | },
22 | },
23 | ignorePatterns: [
24 | // Ignore dotfiles
25 | ".*.js",
26 | "node_modules/",
27 | "dist/",
28 | ],
29 | overrides: [
30 | {
31 | files: ["*.js?(x)", "*.ts?(x)"],
32 | },
33 | ],
34 | };
35 |
--------------------------------------------------------------------------------
/packages/eslint-config/next.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /** @type {import("eslint").Linter.Config} */
6 | module.exports = {
7 | extends: [
8 | "eslint:recommended",
9 | "prettier",
10 | require.resolve("@vercel/style-guide/eslint/next"),
11 | "eslint-config-turbo",
12 | ],
13 | globals: {
14 | React: true,
15 | JSX: true,
16 | },
17 | env: {
18 | node: true,
19 | browser: true,
20 | },
21 | plugins: ["only-warn"],
22 | settings: {
23 | "import/resolver": {
24 | typescript: {
25 | project,
26 | },
27 | },
28 | },
29 | ignorePatterns: [
30 | // Ignore dotfiles
31 | ".*.js",
32 | "node_modules/",
33 | ],
34 | overrides: [{ files: ["*.js?(x)", "*.ts?(x)"] }],
35 | };
36 |
--------------------------------------------------------------------------------
/packages/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/eslint-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "files": [
6 | "library.js",
7 | "next.js",
8 | "react-internal.js"
9 | ],
10 | "devDependencies": {
11 | "@vercel/style-guide": "^5.2.0",
12 | "eslint-config-turbo": "^1.12.4",
13 | "eslint-config-prettier": "^9.1.0",
14 | "eslint-plugin-only-warn": "^1.1.0",
15 | "@typescript-eslint/parser": "^7.1.0",
16 | "@typescript-eslint/eslint-plugin": "^7.1.0",
17 | "typescript": "^5.3.3"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/eslint-config/react-internal.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * internal (bundled by their consumer) libraries
8 | * that utilize React.
9 | *
10 | * This config extends the Vercel Engineering Style Guide.
11 | * For more information, see https://github.com/vercel/style-guide
12 | *
13 | */
14 |
15 | /** @type {import("eslint").Linter.Config} */
16 | module.exports = {
17 | extends: ["eslint:recommended", "prettier", "eslint-config-turbo"],
18 | plugins: ["only-warn"],
19 | globals: {
20 | React: true,
21 | JSX: true,
22 | },
23 | env: {
24 | browser: true,
25 | },
26 | settings: {
27 | "import/resolver": {
28 | typescript: {
29 | project,
30 | },
31 | },
32 | },
33 | ignorePatterns: [
34 | // Ignore dotfiles
35 | ".*.js",
36 | "node_modules/",
37 | "dist/",
38 | ],
39 | overrides: [
40 | // Force ESLint to detect .tsx files
41 | { files: ["*.js?(x)", "*.ts?(x)"] },
42 | ],
43 | };
44 |
--------------------------------------------------------------------------------
/packages/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "declaration": true,
6 | "declarationMap": true,
7 | "esModuleInterop": true,
8 | "incremental": false,
9 | "isolatedModules": true,
10 | "lib": ["es2022", "DOM", "DOM.Iterable"],
11 | "module": "NodeNext",
12 | "moduleDetection": "force",
13 | "moduleResolution": "NodeNext",
14 | "noUncheckedIndexedAccess": true,
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "ES2022",
19 | "forceConsistentCasingInFileNames": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "plugins": [{ "name": "next" }],
7 | "module": "ESNext",
8 | "moduleResolution": "Bundler",
9 | "allowJs": true,
10 | "jsx": "preserve",
11 | "noEmit": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/typescript-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "apps/*"
3 | - "packages/*"
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/base.json"
3 | }
4 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalDependencies": ["**/.env.*local"],
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "outputs": [".next/**", "!.next/cache/**"],
8 | "env": ["OPENAI_API_KEY", "UPSTASH_REDIS_REST_URL", "UPSTASH_REDIS_REST_TOKEN"]
9 | },
10 | "web#build": {
11 | "dependsOn": ["^build"],
12 | "env": [
13 | "OPENAI_API_KEY",
14 | "UPSTASH_REDIS_REST_URL",
15 | "UPSTASH_REDIS_REST_TOKEN"
16 | ],
17 | "outputs": [".next/**", "!.next/cache/**"]
18 | },
19 | "lint": {
20 | "dependsOn": ["^lint"]
21 | },
22 | "dev": {
23 | "cache": false,
24 | "persistent": true,
25 | "inputs": [".env.development.local", ".env.local", ".env.development"]
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------