diff --git a/apps/api/.dev.vars.example b/apps/api/.dev.vars.example index 640bcb8..e26a94e 100644 --- a/apps/api/.dev.vars.example +++ b/apps/api/.dev.vars.example @@ -1,4 +1,7 @@ COOKIE_SECRET= DATABASE_URL= NEON_API_KEY= -APP_URL=http://localhost:3000 \ No newline at end of file +APP_URL=http://localhost:3000 +CLOUDFLARE_TURNSTILE_SECRET_KEY= +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 080b396..1a5f605 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,7 +10,7 @@ "build": "esbuild --bundle src/index.ts --format=esm --outfile=dist/_worker.js", "deploy": "wrangler pages deploy dist", "format": "biome format --write ./src", - "lint": "biome lint --apply ./src", + "lint": "biome lint --write ./src", "start": "wrangler dev", "typecheck": "tsc --noEmit" }, diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index a459f25..7ab8755 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -3,9 +3,7 @@ import { getSignedCookie, setSignedCookie } from "hono/cookie"; import { cors } from "hono/cors"; import { db } from "@instant-postgres/db"; import { projects } from "@instant-postgres/db/schema"; -import { neon, type schema } from "@instant-postgres/neon"; -import { Ratelimit } from "@upstash/ratelimit"; -import { Redis } from "@upstash/redis/cloudflare"; +import { neon } from "@instant-postgres/neon"; import type { TurnstileServerValidationResponse } from "@instant-postgres/turnstile"; import { findClosestRegion, @@ -13,38 +11,13 @@ import { } from "@instant-postgres/neon/regions"; import { z } from "zod"; import { zValidator } from "@hono/zod-validator"; - -type Bindings = { - DATABASE_URL: string; - NEON_API_KEY: string; - COOKIE_SECRET: string; - APP_URL: string; - CLOUDFLARE_TURNSTILE_SECRET_KEY: string; - UPSTASH_REDIS_REST_URL: string; - UPSTASH_REDIS_REST_TOKEN: string; -}; - -type SuccessResponse = { - result: ResultType; - success: true; - error: null; -}; - -type ErrorResponse = { - result: null; - success: false; - error: { - message: string; - code: string; - }; -}; - -type ProjectProvision = { - connectionUri: string | undefined; - project: schema.components["schemas"]["Project"]; - hasCreatedProject: boolean; - timeToProvision: number; -}; +import type { + Bindings, + ErrorResponse, + ProjectProvision, + SuccessResponse, +} from "./types"; +import { ratelimiter } from "./middleware"; const app = new Hono<{ Bindings: Bindings }>(); @@ -63,9 +36,9 @@ app.use("*", async (c, next) => { return await corsMiddleware(c, next); }); -// Todo: add ratelmiting middleware first const route = app.post( "/postgres", + ratelimiter, zValidator( "json", z.object({ @@ -73,39 +46,9 @@ const route = app.post( }), ), async (c) => { - const ip = c.req.raw.headers.get("CF-Connecting-IP") as string; - - const redis = new Redis({ - url: c.env.UPSTASH_REDIS_REST_URL, - token: c.env.UPSTASH_REDIS_REST_TOKEN, - }); - - const ratelimit = new Ratelimit({ - redis, - limiter: Ratelimit.slidingWindow(10, "10 s"), // Todo: check if we should increase this - analytics: true, - }); + const responseStartTime = Date.now(); - const { success, limit, reset, remaining } = await ratelimit.limit(ip); - - if (!success) { - return c.json( - { - result: null, - success: false, - error: { - message: "Rate limit exceeded", - code: "429", - }, - }, - 429, - { - "X-RateLimit-Limit": limit.toString(), - "X-RateLimit-Remaining": remaining.toString(), - "X-RateLimit-Reset": reset.toString(), - }, - ); - } + const ip = c.req.raw.headers.get("CF-Connecting-IP") as string; const projectData = await getSignedCookie( c, @@ -125,7 +68,7 @@ const route = app.post( 200, ); } - // do validation here instead + const body = await c.req.json(); const token = body.cfTurnstileResponse; @@ -185,7 +128,7 @@ const route = app.post( default_endpoint_settings: { autoscaling_limit_min_cu: 0.25, autoscaling_limit_max_cu: 0.25, - suspend_timeout_seconds: 120, // TODO: check with Em if this can be lower + suspend_timeout_seconds: 120, }, }, }, @@ -248,9 +191,11 @@ const route = app.post( }, ); + const responseTime = Date.now() - responseStartTime; + return c.json, 201>( { - result: newProjectData, + result: { ...newProjectData, responseTime }, success: true, error: null, }, diff --git a/apps/api/src/middleware.ts b/apps/api/src/middleware.ts new file mode 100644 index 0000000..bd18d9d --- /dev/null +++ b/apps/api/src/middleware.ts @@ -0,0 +1,44 @@ +import { Ratelimit } from "@upstash/ratelimit"; +import { Redis } from "@upstash/redis/cloudflare"; +import { createMiddleware } from "hono/factory"; +import type { ErrorResponse } from "./types"; + +const TOKENS = 10; // Number of requests +const WINDOW = "10 s"; + +export const ratelimiter = createMiddleware(async (c, next) => { + const ip = c.req.raw.headers.get("CF-Connecting-IP") as string; + + const redis = new Redis({ + url: c.env.UPSTASH_REDIS_REST_URL, + token: c.env.UPSTASH_REDIS_REST_TOKEN, + }); + + const ratelimit = new Ratelimit({ + redis, + limiter: Ratelimit.slidingWindow(TOKENS, WINDOW), + analytics: true, + }); + + const { success, limit, reset, remaining } = await ratelimit.limit(ip); + + if (!success) { + return c.json( + { + result: null, + success: false, + error: { + message: "Rate limit exceeded", + code: "429", + }, + }, + 429, + { + "X-RateLimit-Limit": limit.toString(), + "X-RateLimit-Remaining": remaining.toString(), + "X-RateLimit-Reset": reset.toString(), + }, + ); + } + await next(); +}); diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts new file mode 100644 index 0000000..32499cc --- /dev/null +++ b/apps/api/src/types.ts @@ -0,0 +1,34 @@ +import type { schema } from "@instant-postgres/neon"; + +export type Bindings = { + DATABASE_URL: string; + NEON_API_KEY: string; + COOKIE_SECRET: string; + APP_URL: string; + CLOUDFLARE_TURNSTILE_SECRET_KEY: string; + UPSTASH_REDIS_REST_URL: string; + UPSTASH_REDIS_REST_TOKEN: string; +}; + +export type SuccessResponse = { + result: ResultType; + success: true; + error: null; +}; + +export type ErrorResponse = { + result: null; + success: false; + error: { + message: string; + code: string; + }; +}; + +export type ProjectProvision = { + connectionUri: string | undefined; + project: schema.components["schemas"]["Project"]; + hasCreatedProject: boolean; + timeToProvision: number; + responseTime?: number; +}; diff --git a/apps/web/.env.example b/apps/web/.env.example index 7572c94..1ff97ff 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1 +1,2 @@ -VITE_API_URL=http://localhost:8788 \ No newline at end of file +VITE_API_URL=http://localhost:8788 +VITE_CLOUDFLARE_TURNSTILE_SITE_KEY= \ No newline at end of file diff --git a/apps/web/app/components/connection-string.tsx b/apps/web/app/components/connection-string.tsx index 9083c4b..bb05a71 100644 --- a/apps/web/app/components/connection-string.tsx +++ b/apps/web/app/components/connection-string.tsx @@ -3,16 +3,14 @@ import { Button } from "./ui/button"; import { useScramble } from "use-scramble"; import { cn } from "~/lib/cn"; import { maskPassword } from "~/lib/mask-password"; +import type { clientAction } from "~/routes/actions.deploy"; +import { useFetcher } from "@remix-run/react"; -type ConnectionStringProps = { - hasCreatedProject: boolean; - connectionUri: string; -}; +export const ConnectionString = () => { + const fetcher = useFetcher({ key: "deploy" }); + const hasCreatedProject = fetcher?.data?.result?.hasCreatedProject ?? false; + const connectionUri = fetcher?.data?.result?.connectionUri ?? ""; -export const ConnectionString = ({ - hasCreatedProject, - connectionUri, -}: ConnectionStringProps) => { const clipboard = useClipboard({ copiedTimeout: 600, }); diff --git a/apps/web/app/components/deploy-button.tsx b/apps/web/app/components/deploy-button.tsx index 6112322..27be84e 100644 --- a/apps/web/app/components/deploy-button.tsx +++ b/apps/web/app/components/deploy-button.tsx @@ -1,23 +1,23 @@ -import { Form } from "@remix-run/react"; +import { useFetcher } from "@remix-run/react"; import { Turnstile } from "@instant-postgres/turnstile"; import { useState } from "react"; import { cn } from "~/lib/cn"; +import type { clientAction } from "~/routes/actions.deploy"; -type DeployButtonProps = { - isLoading: boolean; - hasCreatedProject: boolean; -}; +const CLOUDFLARE_TURNSTILE_SITE_KEY = import.meta.env + .VITE_CLOUDFLARE_TURNSTILE_SITE_KEY; -export const DeployButton = ({ - isLoading, - hasCreatedProject, -}: DeployButtonProps) => { +export const DeployButton = () => { const [token, setToken] = useState(null); + const fetcher = useFetcher({ key: "deploy" }); + const isLoading = fetcher.state !== "idle"; + const hasCreatedProject = fetcher?.data?.result?.hasCreatedProject ?? false; return ( -
+ + { + + } + + + )} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length > 0 + ? table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + : null} + +
+
+ + ); +}; diff --git a/apps/web/app/components/sql-editor.tsx b/apps/web/app/components/sql-editor.tsx index 93d7ff1..c8a6bbc 100644 --- a/apps/web/app/components/sql-editor.tsx +++ b/apps/web/app/components/sql-editor.tsx @@ -1,36 +1,16 @@ import { useCallback, useState } from "react"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "~/components/ui/table"; -import { - flexRender, - getCoreRowModel, - getPaginationRowModel, - useReactTable, -} from "@tanstack/react-table"; import { Button } from "~/components/ui/button"; import { tags as t } from "@lezer/highlight"; import createTheme from "@uiw/codemirror-themes"; import { useFetcher } from "@remix-run/react"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; -import type { clientAction } from "~/routes/query"; import Editor from "@uiw/react-codemirror"; import { sql } from "@codemirror/lang-sql"; +import type { clientAction as queryAction } from "~/routes/actions.query"; +import type { clientAction as deployAction } from "~/routes/actions.deploy"; +import { Results } from "./results"; -type CodeEditorProps = { - hasCreatedProject: boolean; - connectionUri: string; -}; - -export const SqlEditor = ({ - hasCreatedProject, - connectionUri, -}: CodeEditorProps) => { +export const SqlEditor = () => { const [query, setQuery] = useState( "CREATE TABLE playing_with_neon(id SERIAL PRIMARY KEY, name TEXT NOT NULL, value REAL);\nINSERT INTO playing_with_neon(name, value)\nSELECT LEFT(md5(i::TEXT), 10), random() FROM generate_series(1, 10) s(i);\nSELECT * FROM playing_with_neon;", ); @@ -38,14 +18,16 @@ export const SqlEditor = ({ setQuery(val); }, []); - const fetcher = useFetcher(); - const isLoading = fetcher.state !== "idle"; - - const queryResult = fetcher.data?.result.result ?? { - rows: [], - rowCount: 0, - columns: [], - }; + const queryFetcher = useFetcher({ key: "query" }); + const isLoading = queryFetcher.state !== "idle"; + const results = queryFetcher.data?.result; + console.log({ + results, + }); + const deployFetcher = useFetcher({ key: "deploy" }); + const hasCreatedProject = + deployFetcher?.data?.result?.hasCreatedProject ?? false; + const connectionUri = deployFetcher?.data?.result?.connectionUri ?? ""; return (
@@ -94,10 +76,10 @@ export const SqlEditor = ({ margin: "1rem", }} /> - - +
@@ -182,61 +164,7 @@ export const SqlEditor = ({ maxSize={50} >
- {fetcher.data?.result.error ? ( -
- - - - - -

{fetcher.data?.result.error}

- - {fetcher.data.queryTime} ms - -
- ) : ( -
- {fetcher.data?.result.result && ( -
- - - - -

Query ran successfully

- - {fetcher.data.queryTime} ms - -
- )} - -
- )} +
@@ -247,149 +175,6 @@ export const SqlEditor = ({ ); }; -export const Result = ({ - queryResult, -}: { - queryResult: { - // biome-ignore lint/suspicious/noExplicitAny: - rows: any[]; - rowCount: number; - columns: string[]; - }; -}) => { - const table = useReactTable({ - data: queryResult?.rows, - columns: queryResult?.columns.map((key) => { - return { - accessorKey: key, - header: key, - indexed: true, - }; - }), - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - }); - - return ( -
- {queryResult ? ( -
- {table.getRowModel().rows?.length > 0 && ( - - )} -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length > 0 - ? table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - : null} - -
-
-
- ) : ( -
- - - - - - - -

- The results of your query will appear here -

-
- )} -
- ); -}; - const darkTheme = createTheme({ theme: "dark", settings: { diff --git a/apps/web/app/lib/constants.ts b/apps/web/app/lib/seo.ts similarity index 79% rename from apps/web/app/lib/constants.ts rename to apps/web/app/lib/seo.ts index f9448a1..3a55614 100644 --- a/apps/web/app/lib/constants.ts +++ b/apps/web/app/lib/seo.ts @@ -1,5 +1,3 @@ -import coverImage from "../../public/instant-postgres.png"; - export const SEO = [ { title: "Neon | Instant Postgres" }, { @@ -13,7 +11,7 @@ export const SEO = [ property: "og:description", content: "Provision a Postgres database on Neon in seconds.", }, - { property: "og:image", content: `https://neon.tech${coverImage}` }, + { property: "og:image", content: "/instant-postgres.png?url" }, { property: "og:url", content: "https://neon.tech/demos/instant-postgres" }, { property: "og:type", content: "website" }, @@ -24,6 +22,6 @@ export const SEO = [ name: "twitter:description", content: "Provision a Postgres database on Neon in seconds.", }, - { name: "twitter:image", content: `https://neon.tech${coverImage}` }, + { name: "twitter:image", content: "/instant-postgres.png?url" }, { name: "twitter:site", content: "@neondatabase" }, ]; diff --git a/apps/web/app/routes/_index.tsx b/apps/web/app/routes/_index.tsx index 19244b3..404ed68 100644 --- a/apps/web/app/routes/_index.tsx +++ b/apps/web/app/routes/_index.tsx @@ -1,65 +1,14 @@ import type { MetaFunction } from "@remix-run/cloudflare"; -import { json } from "@remix-run/cloudflare"; -import { - type ClientActionFunctionArgs, - useActionData, - useNavigation, -} from "@remix-run/react"; -import { SEO } from "~/lib/constants"; -import { hc } from "hono/client"; -import type { AppType } from "../../../api/src"; +import { SEO } from "~/lib/seo"; import { ConnectionString } from "~/components/connection-string"; import { DeployButton } from "~/components/deploy-button"; import { SqlEditor } from "~/components/sql-editor"; import { Message } from "~/components/message"; -import { regions } from "@instant-postgres/neon/regions"; +import RequestInfo from "~/components/request-info"; export const meta: MetaFunction = () => SEO; -export const clientAction = async ({ request }: ClientActionFunctionArgs) => { - const formdata = await request.formData(); - const cfTurnstileResponse = formdata.get("cf-turnstile-response") as string; - - const API_URL = import.meta.env.VITE_API_URL; - - const client = hc(API_URL, { - headers: { - "Content-Type": "application/json", - }, - init: { - credentials: "include", - }, - }); - - const res = await client.postgres.$post({ - json: { - cfTurnstileResponse, - }, - }); - - const data = await res.json(); - - if (data.error) { - console.log(data.error.message); - return json(data, { status: 500 }); - } - - return json(data); -}; - export default function Index() { - const navigation = useNavigation(); - const isLoading = navigation.state !== "idle"; - - const actionData = useActionData(); - - const hasCreatedProject = actionData?.result?.hasCreatedProject ?? false; - const connectionUri = actionData?.result?.connectionUri ?? ""; - const timeToProvision = actionData?.result?.timeToProvision ?? 0; - const project = actionData?.result?.project; - const regionId = project?.region_id as keyof typeof regions; - const region = regions[regionId]?.name; - return (
@@ -71,7 +20,7 @@ export default function Index() { Instantly provison a Postgres database on{" "} Neon @@ -80,30 +29,15 @@ export default function Index() {
- +
-
- {hasCreatedProject && ( -

- Provisioned in {timeToProvision} ms, {region} -

- )} -
- -
{hasCreatedProject && }
+ + +
- +
); diff --git a/apps/web/app/routes/actions.deploy.tsx b/apps/web/app/routes/actions.deploy.tsx new file mode 100644 index 0000000..9440f10 --- /dev/null +++ b/apps/web/app/routes/actions.deploy.tsx @@ -0,0 +1,40 @@ +import { type ClientActionFunctionArgs, json } from "@remix-run/react"; +import type { AppType } from "../../../api/src"; +import { hc } from "hono/client"; + +export const clientAction = async ({ request }: ClientActionFunctionArgs) => { + // @ts-ignore + if (window.zaraz) { + // @ts-ignore + window.zaraz.track("Button Clicked", { text: "Deploy Postgres" }); + } + + const formdata = await request.formData(); + const cfTurnstileResponse = formdata.get("cf-turnstile-response") as string; + + const API_URL = import.meta.env.VITE_API_URL; + + const client = hc(API_URL, { + headers: { + "Content-Type": "application/json", + }, + init: { + credentials: "include", + }, + }); + + const res = await client.postgres.$post({ + json: { + cfTurnstileResponse, + }, + }); + + const data = await res.json(); + + if (data.error) { + console.log(data.error.message); + return json(data, { status: 500 }); + } + + return json(data); +}; diff --git a/apps/web/app/routes/actions.query.tsx b/apps/web/app/routes/actions.query.tsx new file mode 100644 index 0000000..a3f8838 --- /dev/null +++ b/apps/web/app/routes/actions.query.tsx @@ -0,0 +1,32 @@ +import { runQuery } from "@instant-postgres/db/run-query"; +import { json, type ClientActionFunctionArgs } from "@remix-run/react"; +import * as semicolons from "postgres-semicolons"; + +export const clientAction = async ({ request }: ClientActionFunctionArgs) => { + // @ts-ignore + if (window.zaraz) { + // @ts-ignore + window.zaraz.track("Button Clicked", { + text: "Instant Postgres run query", + }); + } + + const form = await request.formData(); + const query = form.get("query") as string; + const connectionUri = form.get("connectionUri") as string; + + const splits = semicolons.parseSplits(query, true); + const queries = semicolons.nonEmptyStatements(query, splits.positions); + + const results: Awaited>[] = []; + + for (const query of queries) { + const result = await runQuery({ + connectionUri, + query, + }); + results.push(result); + } + + return json({ result: results }); +}; diff --git a/apps/web/app/routes/query.tsx b/apps/web/app/routes/query.tsx deleted file mode 100644 index 3cdcdc1..0000000 --- a/apps/web/app/routes/query.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { runQuery } from "@instant-postgres/db/run-query"; -import { json, type ClientActionFunctionArgs } from "@remix-run/react"; - -export const clientAction = async ({ request }: ClientActionFunctionArgs) => { - const form = await request.formData(); - const query = form.get("query") as string; - const connectionUri = form.get("connectionUri") as string; - - const executionTime = Date.now(); - const result = await runQuery({ - connectionUri, - query, - }); - - const queryTime = Date.now() - executionTime; - - return json({ result, queryTime }); -}; diff --git a/apps/web/package.json b/apps/web/package.json index 32b300a..86be7c7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,65 +1,68 @@ { - "name": "web", - "private": true, - "sideEffects": false, - "type": "module", - "scripts": { - "build": "remix vite:build", - "cf-typegen": "wrangler types", - "deploy": "bun run build && wrangler pages deploy ./build/client", - "dev": "remix vite:dev", - "format": "biome format ./app --write", - "lint": "biome lint ./app --apply", - "preview": "bun run build && wrangler pages dev ./build/client", - "start": "wrangler pages dev ./build/client", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@codemirror/lang-sql": "^6.6.4", - "@instant-postgres/neon": "*", - "@instant-postgres/db": "*", - "@lezer/highlight": "^1.2.0", - "@instant-postgres/turnstile": "*", - "@neondatabase/serverless": "^0.9.3", - "@remix-run/cloudflare": "^2.8.1", - "@remix-run/cloudflare-pages": "^2.8.1", - "@remix-run/react": "^2.8.1", - "@tanstack/react-table": "^8.17.3", - "@uiw/codemirror-theme-github": "^4.22.1", - "@uiw/codemirror-themes": "^4.22.1", - "@uiw/react-codemirror": "^4.22.1", - "clsx": "^2.1.1", - "hono": "^4.3.6", - "isbot": "^4.1.0", - "miniflare": "^3.20240404.0", - "react": "^18.2.0", - "react-aria-components": "^1.2.0", - "react-dom": "^18.2.0", - "react-resizable-panels": "^2.0.19", - "tailwind-merge": "^2.3.0", - "use-clipboard-copy": "^0.2.0", - "use-scramble": "^2.2.15" - }, - "devDependencies": { - "@biomejs/biome": "^1.7.3", - "@cloudflare/workers-types": "^4.20240502.0", - "@instant-postgres/tsconfig": "*", - "@remix-run/dev": "^2.8.1", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", - "autoprefixer": "^10.4.19", - "drizzle-kit": "^0.21.1", - "node-fetch": "^3.3.2", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.3", - "tailwindcss-animate": "^1.0.7", - "tailwindcss-react-aria-components": "^1.1.2", - "typescript": "^5.1.6", - "vite": "^5.1.0", - "vite-tsconfig-paths": "^4.2.1", - "wrangler": "^3.48.0" - }, - "engines": { - "node": ">=21.7.2" - } + "name": "web", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "remix vite:build", + "cf-typegen": "wrangler types", + "deploy": "bun run build && wrangler pages deploy ./build/client", + "dev": "remix vite:dev", + "format": "biome format ./app --write", + "lint": "biome lint ./app --write", + "preview": "bun run build && wrangler pages dev ./build/client", + "start": "wrangler pages dev ./build/client", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@codemirror/lang-sql": "^6.6.4", + "@instant-postgres/db": "*", + "@instant-postgres/neon": "*", + "@instant-postgres/turnstile": "*", + "@lezer/highlight": "^1.2.0", + "@neondatabase/serverless": "^0.9.3", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", + "@remix-run/cloudflare": "^2.8.1", + "@remix-run/cloudflare-pages": "^2.8.1", + "@remix-run/react": "^2.8.1", + "@tanstack/react-table": "^8.17.3", + "@uiw/codemirror-theme-github": "^4.22.1", + "@uiw/codemirror-themes": "^4.22.1", + "@uiw/react-codemirror": "^4.22.1", + "clsx": "^2.1.1", + "hono": "^4.3.6", + "isbot": "^4.1.0", + "miniflare": "^3.20240404.0", + "postgres-semicolons": "^0.1.2", + "react": "^18.2.0", + "react-aria-components": "^1.2.0", + "react-dom": "^18.2.0", + "react-resizable-panels": "^2.0.19", + "tailwind-merge": "^2.3.0", + "use-clipboard-copy": "^0.2.0", + "use-scramble": "^2.2.15" + }, + "devDependencies": { + "@biomejs/biome": "^1.7.3", + "@cloudflare/workers-types": "^4.20240502.0", + "@instant-postgres/tsconfig": "*", + "@remix-run/dev": "^2.8.1", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "autoprefixer": "^10.4.19", + "drizzle-kit": "^0.21.1", + "node-fetch": "^3.3.2", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "tailwindcss-animate": "^1.0.7", + "tailwindcss-react-aria-components": "^1.1.2", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1", + "wrangler": "^3.48.0" + }, + "engines": { + "node": ">=21.7.2" + } } diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index c269c2f..0c146fa 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -6,10 +6,23 @@ export default { content: ["./app/**/*.{js,jsx,ts,tsx}"], theme: { extend: { - animation: { - shimmer: "shimmer 3.5s linear infinite", - }, keyframes: { + slideDownAndFade: { + from: { opacity: '0', transform: 'translateY(-2px)' }, + to: { opacity: '1', transform: 'translateY(0)' }, + }, + slideLeftAndFade: { + from: { opacity: '0', transform: 'translateX(2px)' }, + to: { opacity: '1', transform: 'translateX(0)' }, + }, + slideUpAndFade: { + from: { opacity: '0', transform: 'translateY(2px)' }, + to: { opacity: '1', transform: 'translateY(0)' }, + }, + slideRightAndFade: { + from: { opacity: '0', transform: 'translateX(-2px)' }, + to: { opacity: '1', transform: 'translateX(0)' }, + }, shimmer: { from: { backgroundPosition: "0 0", @@ -19,6 +32,12 @@ export default { }, }, }, + animation: { + shimmer: "shimmer 3.5s linear infinite", + slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)', + slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)', + slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)', + slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)', }, }, }, plugins: [animatePlugin, reactAriaComponentsPlugin], diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index e64ca11..ef22f4f 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -6,11 +6,10 @@ import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ - // base: "/demos/instant-postgres/", plugins: [ remixCloudflareDevProxy(), remix({ - basename: "/demos/instant-postgres", + basename: process.env.NODE_ENV === "production" ? "/demos/instant-postgres" : "/", }), tsconfigPaths(), ], @@ -18,7 +17,7 @@ export default defineConfig({ port: 3000, }, build: { - assetsDir: "demos/instant-postgres/assets", + assetsDir: process.env.NODE_ENV === "production" ? "demos/instant-postgres/assets" : "assets", outDir: "build", }, ssr: { diff --git a/bun.lockb b/bun.lockb index 2e0cb79..0ac2fe3 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/db/package.json b/packages/db/package.json index a164be1..8aa4392 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,34 +1,34 @@ { - "name": "@instant-postgres/db", - "version": "0.1.0", - "private": true, - "main": "./src/index.ts", - "types": "./src/index.ts", - "license": "MIT", - "exports": { - ".": "./src/index.ts", - "./schema": "./src/schema/index.ts", - "./run-query": "./src/run-query.ts", - "./cleanup-projects":"./src/cleanup-projects.ts" - }, - "scripts": { - "clean": "rm -rf .turbo node_modules", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "lint": "biome lint --apply .", - "format": "biome format --write .", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "drizzle-orm": "^0.30.10", - "@instant-postgres/neon": "*" - }, - "devDependencies": { - "@biomejs/biome": "^1.5.3", - "@instant-postgres/tsconfig": "*", - "dotenv": "^16.4.5", - "dotenv-cli": "^7.3.0", - "drizzle-kit": "^0.21.2", - "typescript": "^5.2.2" - } + "name": "@instant-postgres/db", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts", + "license": "MIT", + "exports": { + ".": "./src/index.ts", + "./schema": "./src/schema/index.ts", + "./run-query": "./src/run-query.ts", + "./cleanup-projects": "./src/cleanup-projects.ts" + }, + "scripts": { + "clean": "rm -rf .turbo node_modules", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "lint": "biome lint --write .", + "format": "biome format --write .", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@instant-postgres/neon": "*", + "drizzle-orm": "^0.30.10" + }, + "devDependencies": { + "@biomejs/biome": "^1.5.3", + "@instant-postgres/tsconfig": "*", + "dotenv": "^16.4.5", + "dotenv-cli": "^7.3.0", + "drizzle-kit": "^0.21.2", + "typescript": "^5.2.2" + } } diff --git a/packages/db/src/run-query.ts b/packages/db/src/run-query.ts index 95e4924..f840d13 100644 --- a/packages/db/src/run-query.ts +++ b/packages/db/src/run-query.ts @@ -9,46 +9,46 @@ type SuccessResponse = { result: ResultType; success: true; error: null; + queryTime: number; }; type ErrorResponse = { result: null; success: false; error: string; + queryTime: number; }; export const runQuery = async ({ connectionUri, query }: queryOptions) => { + const executionTime = Date.now(); + try { const client = new Pool({ connectionString: connectionUri, }); - const { rows, rowCount, fields } = await client.query(query); + const result = await client.query(query); client.end(); - const response: SuccessResponse<{ - // biome-ignore lint/suspicious/noExplicitAny: - rows: any[]; - rowCount: number; - columns: string[]; - }> = { - result: { - rows: rows ?? [], - rowCount: rowCount ?? 0, - columns: fields?.map((field) => field.name) ?? [], - }, + const queryTime = Date.now() - executionTime; + + const response: SuccessResponse = { + result, + queryTime, success: true, error: null, }; + return response; } catch (error) { - console.log("error", error); + const queryTime = Date.now() - executionTime; const response: ErrorResponse = { result: null, success: false, error: `${error}`, + queryTime, }; return response; diff --git a/packages/neon/package.json b/packages/neon/package.json index 7c74dec..2554cae 100644 --- a/packages/neon/package.json +++ b/packages/neon/package.json @@ -12,7 +12,7 @@ }, "scripts": { "clean": "rm -rf .turbo node_modules", - "lint": "biome lint --apply .", + "lint": "biome lint --write .", "format": "biome format --write .", "typecheck": "tsc --noEmit", "typegen": "bunx openapi-typescript https://neon.tech/api_spec/release/v2.json -o ./src/schema.ts" diff --git a/packages/turnstile/package.json b/packages/turnstile/package.json index 8139fba..54549ef 100644 --- a/packages/turnstile/package.json +++ b/packages/turnstile/package.json @@ -10,7 +10,7 @@ }, "scripts": { "clean": "rm -rf .turbo node_modules", - "lint": "biome lint --apply .", + "lint": "biome lint --write .", "format": "biome format --write .", "typecheck": "tsc --noEmit" },