1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 09:25:22 -04:00

move database from postgres/prisma to redis

This commit is contained in:
Jake Jarvis 2025-03-27 09:21:22 -04:00
parent e865d9d8e5
commit bbf6e9dc66
16 changed files with 283 additions and 651 deletions

View File

@ -8,7 +8,7 @@ CORE EXPERTISE:
- Server Actions - Server Actions
- Parallel and Intercepting Routes - Parallel and Intercepting Routes
- CSS Modules - CSS Modules
- Prisma as a Database ORM - Redis for KV storage
- MDX for blog content - MDX for blog content
- Zod for runtime type validation - Zod for runtime type validation
@ -23,8 +23,7 @@ CODE ARCHITECTURE:
│ ├── config/ # Configuration constants │ ├── config/ # Configuration constants
│ ├── helpers/ # Utility functions │ ├── helpers/ # Utility functions
├── notes/ # Blog posts in markdown/MDX format ├── notes/ # Blog posts in markdown/MDX format
├── static/ # Static files such as images and videos └── static/ # Static files such as images and videos
└── prisma/ # Database schema in Prisma format
2. Component Organization: 2. Component Organization:
- Keep reusable components in ./components/. - Keep reusable components in ./components/.

View File

@ -22,7 +22,6 @@
"extensions": [ "extensions": [
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"prisma.prisma",
"unifiedjs.vscode-mdx", "unifiedjs.vscode-mdx",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"stylelint.vscode-stylelint" "stylelint.vscode-stylelint"

View File

@ -1,8 +1,9 @@
# required. storage for hit counter's server component at app/notes/[slug]/counter.tsx and API endpoint at /api/hits. # required. redis storage credentials for hit counter's server component (app/notes/[slug]/counter.tsx) and API
# currently set automatically by Vercel's Neon integration, but this can be changed in prisma/schema.prisma. # endpoint. currently set automatically by Vercel's Upstash integration.
# https://www.prisma.io/docs/postgres/overview # https://upstash.com/docs/redis/sdks/ts/getstarted
# https://vercel.com/marketplace/neon # https://vercel.com/marketplace/upstash
DATABASE_URL= KV_REST_API_URL=
KV_REST_API_TOKEN=
# required. used for /projects grid, built with ISR. only needs the "public_repo" scope since we don't need/want to # required. used for /projects grid, built with ISR. only needs the "public_repo" scope since we don't need/want to
# showcase any private repositories, obviously. # showcase any private repositories, obviously.

View File

@ -22,10 +22,6 @@ updates:
- "@types/react-is" - "@types/react-is"
- "babel-plugin-react-compiler" - "babel-plugin-react-compiler"
- "eslint-plugin-react-compiler" - "eslint-plugin-react-compiler"
prisma:
patterns:
- "prisma"
- "@prisma/*"
mdx: mdx:
patterns: patterns:
- "remark-*" - "remark-*"

View File

@ -5,7 +5,7 @@
[![Licensed under CC-BY-4.0](https://img.shields.io/badge/license-CC--BY--4.0-fb7828?logo=creative-commons&logoColor=white)](LICENSE) [![Licensed under CC-BY-4.0](https://img.shields.io/badge/license-CC--BY--4.0-fb7828?logo=creative-commons&logoColor=white)](LICENSE)
[![GitHub repo size](https://img.shields.io/github/repo-size/jakejarvis/jarv.is?color=009cdf&label=repo%20size&logo=git&logoColor=white)](https://github.com/jakejarvis/jarv.is) [![GitHub repo size](https://img.shields.io/github/repo-size/jakejarvis/jarv.is?color=009cdf&label=repo%20size&logo=git&logoColor=white)](https://github.com/jakejarvis/jarv.is)
My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Vercel](https://vercel.com/), [Neon Postgres](https://neon.tech/), [Prisma](https://www.prisma.io/postgres), [Umami](https://umami.is/), [and more](https://jarv.is/humans.txt). My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Vercel](https://vercel.com/), [Upstash Redis](https://upstash.com/), [Giscus](https://giscus.app/), [Umami](https://umami.is/), [and more](https://jarv.is/humans.txt).
I keep an ongoing list of [post ideas](https://github.com/jakejarvis/jarv.is/issues/1) and [coding to-dos](https://github.com/jakejarvis/jarv.is/issues/714) as issues in this repo. Outside contributions, improvements, and/or corrections are welcome too! I keep an ongoing list of [post ideas](https://github.com/jakejarvis/jarv.is/issues/1) and [coding to-dos](https://github.com/jakejarvis/jarv.is/issues/714) as issues in this repo. Outside contributions, improvements, and/or corrections are welcome too!

View File

@ -1,31 +1,43 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { prisma } from "../../../lib/helpers/prisma"; import redis from "../../../lib/helpers/redis";
import type { hits as Hits } from "@prisma/client";
export const revalidate = 900; // 15 mins export const revalidate = 900; // 15 mins
export const GET = async (): Promise< export const GET = async (): Promise<
NextResponse<{ NextResponse<{
total: Pick<Hits, "hits">; total: {
pages: Hits[]; hits: number;
};
pages: Array<{
slug: string;
hits: number;
}>;
}> }>
> => { > => {
// fetch all rows from db sorted by most hits const slugs = await redis.scan(0, {
const pages = await prisma.hits.findMany({ count: 50,
orderBy: [
{
hits: "desc",
},
],
}); });
const total = { hits: 0 }; // fetch all rows from db sorted by most hits
const data = await Promise.all(
slugs[1].map(async (slug) => {
const hits = (await redis.get(slug)) as number;
return {
slug,
hits,
};
})
);
// sort by hits
data.sort((a, b) => b.hits - a.hits);
// calculate total hits // calculate total hits
pages.forEach((page) => { const total = { hits: 0 };
data.forEach((page) => {
// add these hits to running tally // add these hits to running tally
total.hits += page.hits; total.hits += page.hits;
}); });
return NextResponse.json({ total, pages }); return NextResponse.json({ total, pages: data });
}; };

View File

@ -1,20 +1,12 @@
import { connection } from "next/server"; import { connection } from "next/server";
import commaNumber from "comma-number"; import commaNumber from "comma-number";
import { prisma } from "../../../lib/helpers/prisma"; import redis from "../../../lib/helpers/redis";
const HitCounter = async ({ slug }: { slug: string }) => { const HitCounter = async ({ slug }: { slug: string }) => {
await connection(); await connection();
try { try {
const { hits } = await prisma.hits.upsert({ const hits = await redis.incr(slug);
where: { slug },
create: { slug },
update: {
hits: {
increment: 1,
},
},
});
// we have data! // we have data!
return <span title={`${commaNumber(hits)} ${hits === 1 ? "view" : "views"}`}>{commaNumber(hits)}</span>; return <span title={`${commaNumber(hits)} ${hits === 1 ? "view" : "views"}`}>{commaNumber(hits)}</span>;

View File

@ -22,9 +22,9 @@ For a likely excessive level of privacy and security, this website is also mirro
## Analytics ## Analytics
A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e. `hits = hits + 1`) in a [Neon Postgres](https://neon.tech/) database. Individual views and identifying (or non-identifying) details are **never stored or logged**. A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e. `hits = hits + 1`) in a [Upstash Redis](https://upstash.com/) database. Individual views and identifying (or non-identifying) details are **never stored or logged**.
The [database schema](https://github.com/jakejarvis/jarv.is/blob/main/prisma/schema.prisma), [serverless function](https://github.com/jakejarvis/jarv.is/blob/main/app/api/hits/route.ts) and [client script](https://github.com/jakejarvis/jarv.is/blob/main/app/notes/%5Bslug%5D/counter.tsx) are open source, and [snapshots of the database](https://github.com/jakejarvis/website-stats) are public. The [server component](https://github.com/jakejarvis/jarv.is/blob/main/app/notes/%5Bslug%5D/counter.tsx) is open source, and [snapshots of the database](https://github.com/jakejarvis/website-stats) are public.
A self-hosted [**Umami**](https://umami.is/) instance is also used to gain insights into referrers, search terms, etc. [without collecting anything identifiable](https://umami.is/blog/why-privacy-matters) about you. [The dashboard is even public!](/stats) A self-hosted [**Umami**](https://umami.is/) instance is also used to gain insights into referrers, search terms, etc. [without collecting anything identifiable](https://umami.is/blog/why-privacy-matters) about you. [The dashboard is even public!](/stats)

View File

@ -1,17 +0,0 @@
import { PrismaClient } from "@prisma/client";
// creating PrismaClient here prevents next.js from starting too many concurrent prisma instances and exhausting the
// number of connection pools available (especially when hot reloading from `next dev`).
// https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices
const prismaClientSingleton = () => {
return new PrismaClient();
};
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
export const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;

6
lib/helpers/redis.ts Normal file
View File

@ -0,0 +1,6 @@
import { Redis } from "@upstash/redis";
// Initialize Redis
const redis = Redis.fromEnv();
export default redis;

View File

@ -14,8 +14,7 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"typecheck": "tsc", "typecheck": "tsc"
"postinstall": "npx prisma generate"
}, },
"dependencies": { "dependencies": {
"@date-fns/tz": "^1.2.0", "@date-fns/tz": "^1.2.0",
@ -29,7 +28,7 @@
"@next/third-parties": "15.3.0-canary.24", "@next/third-parties": "15.3.0-canary.24",
"@octokit/graphql": "^8.2.1", "@octokit/graphql": "^8.2.1",
"@octokit/graphql-schema": "^15.26.0", "@octokit/graphql-schema": "^15.26.0",
"@prisma/client": "^6.5.0", "@upstash/redis": "^1.34.6",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"comma-number": "^2.1.0", "comma-number": "^2.1.0",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
@ -76,7 +75,7 @@
"@jakejarvis/eslint-config": "^4.0.7", "@jakejarvis/eslint-config": "^4.0.7",
"@types/comma-number": "^2.1.2", "@types/comma-number": "^2.1.2",
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"@types/node": "^22.13.13", "@types/node": "^22.13.14",
"@types/prop-types": "^15.7.14", "@types/prop-types": "^15.7.14",
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
@ -96,7 +95,6 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"lint-staged": "^15.5.0", "lint-staged": "^15.5.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prisma": "^6.5.0",
"schema-dts": "^1.1.5", "schema-dts": "^1.1.5",
"simple-git-hooks": "^2.12.1", "simple-git-hooks": "^2.12.1",
"stylelint": "^16.17.0", "stylelint": "^16.17.0",
@ -130,10 +128,6 @@
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@prisma/client",
"@prisma/engines",
"esbuild",
"prisma",
"sharp", "sharp",
"simple-git-hooks" "simple-git-hooks"
], ],

791
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
-- CreateTable
CREATE TABLE "hits" (
"slug" TEXT NOT NULL,
"hits" INTEGER NOT NULL DEFAULT 1,
CONSTRAINT "hits_pkey" PRIMARY KEY ("slug")
);

View File

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -1,14 +0,0 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model hits {
slug String @id
hits Int @default(1)
}

View File

@ -30,11 +30,10 @@
- Next.js - Next.js
- Vercel - Vercel
- Neon Postgres - Upstash Redis
- Prisma
- Umami
- Giscus - Giscus
- Resend - Resend
- Umami
- ...and more: https://jarv.is/uses - ...and more: https://jarv.is/uses
# VIEW SOURCE # VIEW SOURCE