From e44d419b589e770b3d8a79c6a063c9caa9b05f81 Mon Sep 17 00:00:00 2001 From: NexVeridian Date: Mon, 19 Feb 2024 08:58:07 -0800 Subject: [PATCH] postgres --- README.md | 29 ++++-- docker-compose-postgres.yml | 36 +++++++ ...ev.yml => docker-compose-surrealdb.dev.yml | 0 ...ompose.yml => docker-compose-surrealdb.yml | 0 package-lock.json | 17 +++- package.json | 4 +- src/app/[slug]/db.tsx | 35 +++++-- src/app/create/db.tsx | 95 ++++++++++++++----- src/app/global-error.tsx | 10 +- src/app/not-found.tsx | 2 +- src/app/stats/db.tsx | 32 +++++-- src/app/stats/loading.tsx | 2 +- src/app/stats/page.tsx | 2 +- src/components/card-grid.tsx | 33 +++---- src/components/db-utils.tsx | 18 +++- 15 files changed, 233 insertions(+), 82 deletions(-) create mode 100644 docker-compose-postgres.yml rename docker-compose.dev.yml => docker-compose-surrealdb.dev.yml (100%) rename docker-compose.yml => docker-compose-surrealdb.yml (100%) diff --git a/README.md b/README.md index 3ad29b8..e8bff19 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,37 @@ +# next-url-shortener +A simple URL shortener using [Next.js](https://nextjs.org/) 14 server actions, [Postgres](https://www.postgresql.org/) or [SurrealDB](https://surrealdb.com/), [Shadcn/ui](http://ui.shadcn.com/) and [Tailwind](https://tailwindcss.com/). + # Install -Copy [docker-compose.yml](./docker-compose.yml) +### Copy one +- [docker-compose-postgres.yml](./docker-compose-postgres.yml) +- [docker-compose-surrealdb.yml](./docker-compose-surrealdb.yml) Create data folder next to docker-compose.yml and .env, and set the data type in .env ``` ├── data │ └── surrealdb -├── docker-compose.yml +├── docker-compose-postgres.yml └── .env ``` - -`docker compose up --pull always -d` +### Then run: +- `docker compose -f docker-compose-postgres.yml up --pull always -d` +- `docker compose -f docker-compose-surrealdb.yml up --pull always -d` ## Example .env ``` -# If not using docker, use 0.0.0.0:8000 -DB_URL_PORT=surrealdb:8000 +# postgres or surrealdb +DB_TYPE=postgres + +# For surrealdb: If using docker surrealdb:8000, if not use 0.0.0.0:8000 +# For postgres: If using docker postgres:5432, if not use 0.0.0.0:5432 +DB_URL_PORT=postgres:5432 + +# postgres +POSTGRES_USER=root +POSTGRES_PASSWORD=root +POSTGRES_DB=url + +# surrealdb DB_USER=root DB_PASSWORD=root ``` diff --git a/docker-compose-postgres.yml b/docker-compose-postgres.yml new file mode 100644 index 0000000..47a2fee --- /dev/null +++ b/docker-compose-postgres.yml @@ -0,0 +1,36 @@ +version: "3" +services: + next-url-shortener: + container_name: next-url-shortener + build: . + env_file: + - .env + ports: + - 3000:3000 + depends_on: + - postgres + networks: + - postgres + + postgres: + container_name: postgres + image: postgres:latest + env_file: + - .env + restart: always + deploy: + resources: + reservations: + cpus: 1 + ports: + - 5432:5432 + volumes: + - ./data/postgres:/var/lib/postgresql/data + networks: + - postgres + +volumes: + data: + +networks: + postgres: diff --git a/docker-compose.dev.yml b/docker-compose-surrealdb.dev.yml similarity index 100% rename from docker-compose.dev.yml rename to docker-compose-surrealdb.dev.yml diff --git a/docker-compose.yml b/docker-compose-surrealdb.yml similarity index 100% rename from docker-compose.yml rename to docker-compose-surrealdb.yml diff --git a/package-lock.json b/package-lock.json index 0436252..ba661f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,9 @@ "dotenv": "^16.4.4", "next": "14.1.0", "next-themes": "^0.2.1", - "react": "^18", - "react-dom": "^18", + "postgres": "^3.4.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-hook-form": "^7.50.1", "surrealdb.js": "^0.11.0", "swr": "^2.2.5", @@ -4465,6 +4466,18 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/postgres": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.3.tgz", + "integrity": "sha512-iHJn4+M9vbTdHSdDzNkC0crHq+1CUdFhx+YqCE+SqWxPjm+Zu63jq7yZborOBF64c8pc58O5uMudyL1FQcHacA==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 04a6a1c..8906d4d 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "dotenv": "^16.4.4", "next": "14.1.0", "next-themes": "^0.2.1", - "react": "^18", - "react-dom": "^18", + "postgres": "^3.4.3", + "react": "^18.2.0", "react-hook-form": "^7.50.1", "surrealdb.js": "^0.11.0", "swr": "^2.2.5", diff --git a/src/app/[slug]/db.tsx b/src/app/[slug]/db.tsx index c3d6617..cd1064f 100644 --- a/src/app/[slug]/db.tsx +++ b/src/app/[slug]/db.tsx @@ -1,16 +1,35 @@ "use server"; -import { initConnection } from "@/components/db-utils"; +import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils"; export async function querydb(slug: string) { + let long_url = undefined; try { - let db = await initConnection(); - let long_url = await db.query(` - update url:[$id] set clicks = clicks + 1 ; - select * from url:[$id]; - `, { id: slug }); + if (process.env.DB_TYPE === "surrealdb") { - // @ts-ignore - long_url = long_url[0][0].long_url; + let db = await initConnectionSurreal(); + let long_url = await db.query(` + update url:[$id] + set clicks = clicks + 1; + select * from url:[$id]; + `, { id: slug }); + + // @ts-ignore + long_url = long_url[0][0].long_url; + } + + if (process.env.DB_TYPE === "postgres") { + let sql = await initConnectionPostgres(); + long_url = await sql` + update url set + clicks = clicks + 1, + date_accessed = now() + where id = ${slug} + returning long_url; + `; + long_url = long_url[0].long_url; + } + + console.log(long_url); return long_url; } catch (e) { return; diff --git a/src/app/create/db.tsx b/src/app/create/db.tsx index 3ab2ca8..502333c 100644 --- a/src/app/create/db.tsx +++ b/src/app/create/db.tsx @@ -1,30 +1,73 @@ "use server"; -import { initConnection } from "@/components/db-utils"; +import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils"; import { formSchema } from "./schema"; -export async function querydb(prevState: any, formData: FormData) { - try { - const values = formSchema.safeParse(formData) - if (!values.success) { - return { error: values.error }; - } - const long_url = values.data.url; - - let db = await initConnection(); - let url = await db.query(` - create url:[rand::string(8)] CONTENT { - long_url: string::replace(string::replace($long_url, "https://", ""), "http://", ""), - clicks: 0, - date_added: time::now(), - date_accessed: { time::now() } - } return id[0]; - `, { long_url: long_url }); - - // @ts-ignore - url = url[0][0].id; - - return { url: url }; - } catch (e) { - return; - } +function generateRandomString(length: number) { + const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let randomString = ''; + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * charset.length); + randomString += charset[randomIndex]; + } + return randomString; +} + +export async function querydb(prevState: any, formData: FormData) { + const values = formSchema.safeParse(formData) + if (!values.success) { + return { error: values.error }; + } + const long_url = values.data.url; + let url = undefined; + + try { + if (process.env.DB_TYPE === "surrealdb") { + let db = await initConnectionSurreal(); + url = await db.query(` + create url:[rand::string(8)] CONTENT { + long_url: $long_url, + clicks: 0, + date_added: time::now(), + date_accessed: { time::now() } + } return id[0]; + `, { + long_url: long_url.replace("https://", "").replace("http://", "") + }); + // @ts-ignore + url = url[0][0].id; + } + + console.log(long_url); + if (process.env.DB_TYPE === "postgres") { + let sql = await initConnectionPostgres(); + await sql` + create table if not exists url ( + id text primary key, + clicks integer not null, + long_url text not null, + date_added timestamp not null, + date_accessed timestamp not null + ); + `; + + url = await sql` + insert into url (id, long_url, clicks, date_added, date_accessed) + values ( + ${generateRandomString(8)}, + ${long_url.replace("https://", "").replace("http://", "")}, + 0, + now(), + now() + ) + returning id; + `; + + url = url[0].id; + } + console.log(url); + + return { url: url }; + } catch (e) { + return; + } } diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index ac63cdc..c4a167a 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -6,12 +6,16 @@ import { CardTitle } from "@/components/ui/card"; -export default function GlobalError() { +export default function GlobalError({ + error, +}: { + error: Error & { digest?: string } +}) { return ( - + - Error + Error + {String(error)} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 9b65bbe..445abf5 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -8,7 +8,7 @@ import { export default function NotFound() { return ( - + 404 - Not Found diff --git a/src/app/stats/db.tsx b/src/app/stats/db.tsx index 310a74a..766d271 100644 --- a/src/app/stats/db.tsx +++ b/src/app/stats/db.tsx @@ -1,18 +1,30 @@ "use server"; -import { initConnection } from "@/components/db-utils"; +import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils"; export async function querydb() { try { - let db = await initConnection(); - // console.log(db); - let stats = await db.query(` - select * from url - order by clicks desc - limit 50; - `); + let stats = []; + if (process.env.DB_TYPE === "surrealdb") { + let db = await initConnectionSurreal(); + // console.log(db); + stats = await db.query(` + select * from url + order by clicks desc + limit 50; + `); - // @ts-ignore - stats = stats[0]; + // @ts-ignore + stats = stats[0]; + } + + if (process.env.DB_TYPE === "postgres") { + let sql = await initConnectionPostgres(); + stats = await sql` + select * from url + order by clicks desc + limit 50; + `; + } return stats; } catch (e) { diff --git a/src/app/stats/loading.tsx b/src/app/stats/loading.tsx index 75f4650..64b508a 100644 --- a/src/app/stats/loading.tsx +++ b/src/app/stats/loading.tsx @@ -8,7 +8,7 @@ import { export default function Loading() { return ( - + Loading... diff --git a/src/app/stats/page.tsx b/src/app/stats/page.tsx index 4be0f15..8256a45 100644 --- a/src/app/stats/page.tsx +++ b/src/app/stats/page.tsx @@ -44,7 +44,7 @@ export default function StatsPage() { return data.length === 0 ? ( ) : ( - + {/* @ts-ignore */} diff --git a/src/components/card-grid.tsx b/src/components/card-grid.tsx index efc5875..d7ecc42 100644 --- a/src/components/card-grid.tsx +++ b/src/components/card-grid.tsx @@ -1,37 +1,30 @@ "use client"; export default function CardGrid({ - max_rows = 4, + maxCols: maxCols = 4, children, className, ...props }: { - max_rows?: number; + maxCols?: number; children?: React.ReactNode; className?: string; }) { - let baseClassName = `hidden items-start justify-center gap-6 rounded-lg p-8 md:grid`; + let baseClassName = `hidden items-start justify-center gap-6 rounded-lg p-8 md:grid md:grid-cols-1`; - switch (max_rows) { - case 1: - baseClassName += " md:grid-cols-1 "; - break; - case 2: - baseClassName += " md:grid-cols-1 lg:grid-cols-2 "; - break; - case 3: - baseClassName += " md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 "; - break; - case 4: - baseClassName += " md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 "; - break; - default: - break; - }; + if (maxCols >= 2) { + baseClassName += " lg:grid-cols-2"; + } + if (maxCols >= 3) { + baseClassName += " xl:grid-cols-3"; + } + if (maxCols >= 4) { + baseClassName += " 2xl:grid-cols-4"; + } if (className == undefined) { className = baseClassName; } else { - className = baseClassName + className; + className = baseClassName + " " + className; } return (
{ +export async function initConnectionSurreal(): Promise { try { db.connect("ws://" + process.env.DB_URL_PORT + "/rpc", { namespace: "url", @@ -19,3 +19,17 @@ export async function initConnection(): Promise { return db; } + +export async function initConnectionPostgres(): Promise> { + const DB_URL_PORT = (process.env.DB_URL_PORT || "postgres:5432").split(":"); + const sql = postgres({ + host: DB_URL_PORT[0], + port: parseInt(DB_URL_PORT[1]), + user: process.env.POSTGRES_USER || "root", + password: process.env.POSTGRES_PASSWORD || "root", + database: process.env.POSTGRES_DB || "url", + max: 100, + onnotice: () => { }, + }); + return sql; +}