postgres
This commit is contained in:
parent
b0f3e6c576
commit
e44d419b58
15 changed files with 233 additions and 82 deletions
29
README.md
29
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
|
# 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
|
Create data folder next to docker-compose.yml and .env, and set the data type in .env
|
||||||
```
|
```
|
||||||
├── data
|
├── data
|
||||||
│ └── surrealdb
|
│ └── surrealdb
|
||||||
├── docker-compose.yml
|
├── docker-compose-postgres.yml
|
||||||
└── .env
|
└── .env
|
||||||
```
|
```
|
||||||
|
### Then run:
|
||||||
`docker compose up --pull always -d`
|
- `docker compose -f docker-compose-postgres.yml up --pull always -d`
|
||||||
|
- `docker compose -f docker-compose-surrealdb.yml up --pull always -d`
|
||||||
|
|
||||||
## Example .env
|
## Example .env
|
||||||
```
|
```
|
||||||
# If not using docker, use 0.0.0.0:8000
|
# postgres or surrealdb
|
||||||
DB_URL_PORT=surrealdb:8000
|
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_USER=root
|
||||||
DB_PASSWORD=root
|
DB_PASSWORD=root
|
||||||
```
|
```
|
||||||
|
|
36
docker-compose-postgres.yml
Normal file
36
docker-compose-postgres.yml
Normal file
|
@ -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:
|
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -22,8 +22,9 @@
|
||||||
"dotenv": "^16.4.4",
|
"dotenv": "^16.4.4",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "^18",
|
"postgres": "^3.4.3",
|
||||||
"react-dom": "^18",
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.50.1",
|
"react-hook-form": "^7.50.1",
|
||||||
"surrealdb.js": "^0.11.0",
|
"surrealdb.js": "^0.11.0",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
|
@ -4465,6 +4466,18 @@
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
"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": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|
|
@ -23,8 +23,8 @@
|
||||||
"dotenv": "^16.4.4",
|
"dotenv": "^16.4.4",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "^18",
|
"postgres": "^3.4.3",
|
||||||
"react-dom": "^18",
|
"react": "^18.2.0",
|
||||||
"react-hook-form": "^7.50.1",
|
"react-hook-form": "^7.50.1",
|
||||||
"surrealdb.js": "^0.11.0",
|
"surrealdb.js": "^0.11.0",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
|
|
|
@ -1,16 +1,35 @@
|
||||||
"use server";
|
"use server";
|
||||||
import { initConnection } from "@/components/db-utils";
|
import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils";
|
||||||
|
|
||||||
export async function querydb(slug: string) {
|
export async function querydb(slug: string) {
|
||||||
|
let long_url = undefined;
|
||||||
try {
|
try {
|
||||||
let db = await initConnection();
|
if (process.env.DB_TYPE === "surrealdb") {
|
||||||
|
|
||||||
|
let db = await initConnectionSurreal();
|
||||||
let long_url = await db.query(`
|
let long_url = await db.query(`
|
||||||
update url:[$id] set clicks = clicks + 1 ;
|
update url:[$id]
|
||||||
|
set clicks = clicks + 1;
|
||||||
select * from url:[$id];
|
select * from url:[$id];
|
||||||
`, { id: slug });
|
`, { id: slug });
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
long_url = long_url[0][0].long_url;
|
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;
|
return long_url;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,27 +1,70 @@
|
||||||
"use server";
|
"use server";
|
||||||
import { initConnection } from "@/components/db-utils";
|
import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils";
|
||||||
import { formSchema } from "./schema";
|
import { formSchema } from "./schema";
|
||||||
|
|
||||||
|
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) {
|
export async function querydb(prevState: any, formData: FormData) {
|
||||||
try {
|
|
||||||
const values = formSchema.safeParse(formData)
|
const values = formSchema.safeParse(formData)
|
||||||
if (!values.success) {
|
if (!values.success) {
|
||||||
return { error: values.error };
|
return { error: values.error };
|
||||||
}
|
}
|
||||||
const long_url = values.data.url;
|
const long_url = values.data.url;
|
||||||
|
let url = undefined;
|
||||||
|
|
||||||
let db = await initConnection();
|
try {
|
||||||
let url = await db.query(`
|
if (process.env.DB_TYPE === "surrealdb") {
|
||||||
|
let db = await initConnectionSurreal();
|
||||||
|
url = await db.query(`
|
||||||
create url:[rand::string(8)] CONTENT {
|
create url:[rand::string(8)] CONTENT {
|
||||||
long_url: string::replace(string::replace($long_url, "https://", ""), "http://", ""),
|
long_url: $long_url,
|
||||||
clicks: 0,
|
clicks: 0,
|
||||||
date_added: time::now(),
|
date_added: time::now(),
|
||||||
date_accessed: <future> { time::now() }
|
date_accessed: <future> { time::now() }
|
||||||
} return id[0];
|
} return id[0];
|
||||||
`, { long_url: long_url });
|
`, {
|
||||||
|
long_url: long_url.replace("https://", "").replace("http://", "")
|
||||||
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
url = url[0][0].id;
|
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 };
|
return { url: url };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -6,12 +6,16 @@ import {
|
||||||
CardTitle
|
CardTitle
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
export default function GlobalError() {
|
export default function GlobalError({
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string }
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<CardGrid max_rows={1}>
|
<CardGrid maxCols={1}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-center text-2xl text-red-400">Error</CardTitle>
|
<CardTitle className="text-center text-2xl text-red-400">Error + {String(error)}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
</CardGrid>
|
</CardGrid>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<CardGrid max_rows={1}>
|
<CardGrid maxCols={1}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-center text-2xl text-red-400">404 - Not Found</CardTitle>
|
<CardTitle className="text-center text-2xl text-red-400">404 - Not Found</CardTitle>
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
"use server";
|
"use server";
|
||||||
import { initConnection } from "@/components/db-utils";
|
import { initConnectionPostgres, initConnectionSurreal } from "@/components/db-utils";
|
||||||
|
|
||||||
export async function querydb() {
|
export async function querydb() {
|
||||||
try {
|
try {
|
||||||
let db = await initConnection();
|
let stats = [];
|
||||||
|
if (process.env.DB_TYPE === "surrealdb") {
|
||||||
|
let db = await initConnectionSurreal();
|
||||||
// console.log(db);
|
// console.log(db);
|
||||||
let stats = await db.query(`
|
stats = await db.query(`
|
||||||
select * from url
|
select * from url
|
||||||
order by clicks desc
|
order by clicks desc
|
||||||
limit 50;
|
limit 50;
|
||||||
|
@ -13,6 +15,16 @@ export async function querydb() {
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
stats = stats[0];
|
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;
|
return stats;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
<CardGrid max_rows={1}>
|
<CardGrid maxCols={1}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-1xl text-amber-400">Loading...</CardTitle>
|
<CardTitle className="text-1xl text-amber-400">Loading...</CardTitle>
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default function StatsPage() {
|
||||||
return data.length === 0 ? (
|
return data.length === 0 ? (
|
||||||
<Loading />
|
<Loading />
|
||||||
) : (
|
) : (
|
||||||
<CardGrid max_rows={1}>
|
<CardGrid maxCols={1}>
|
||||||
<Card>
|
<Card>
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<DataTable columns={columns} data={data} />
|
<DataTable columns={columns} data={data} />
|
||||||
|
|
|
@ -1,37 +1,30 @@
|
||||||
"use client";
|
"use client";
|
||||||
export default function CardGrid({
|
export default function CardGrid({
|
||||||
max_rows = 4,
|
maxCols: maxCols = 4,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
max_rows?: number;
|
maxCols?: number;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
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) {
|
if (maxCols >= 2) {
|
||||||
case 1:
|
baseClassName += " lg:grid-cols-2";
|
||||||
baseClassName += " md:grid-cols-1 ";
|
}
|
||||||
break;
|
if (maxCols >= 3) {
|
||||||
case 2:
|
baseClassName += " xl:grid-cols-3";
|
||||||
baseClassName += " md:grid-cols-1 lg:grid-cols-2 ";
|
}
|
||||||
break;
|
if (maxCols >= 4) {
|
||||||
case 3:
|
baseClassName += " 2xl:grid-cols-4";
|
||||||
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 (className == undefined) {
|
if (className == undefined) {
|
||||||
className = baseClassName;
|
className = baseClassName;
|
||||||
} else {
|
} else {
|
||||||
className = baseClassName + className;
|
className = baseClassName + " " + className;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
"use server";
|
"use server";
|
||||||
import "dotenv";
|
import postgres, { Sql } from 'postgres';
|
||||||
import { Surreal } from "surrealdb.js";
|
import { Surreal } from "surrealdb.js";
|
||||||
const db = new Surreal();
|
const db = new Surreal();
|
||||||
|
|
||||||
export async function initConnection(): Promise<Surreal> {
|
export async function initConnectionSurreal(): Promise<Surreal> {
|
||||||
try {
|
try {
|
||||||
db.connect("ws://" + process.env.DB_URL_PORT + "/rpc", {
|
db.connect("ws://" + process.env.DB_URL_PORT + "/rpc", {
|
||||||
namespace: "url",
|
namespace: "url",
|
||||||
|
@ -19,3 +19,17 @@ export async function initConnection(): Promise<Surreal> {
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function initConnectionPostgres(): Promise<Sql<{}>> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue