init: add code
This commit is contained in:
commit
79c4323666
26 changed files with 3684 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
use_nix
|
||||
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# prod
|
||||
dist/
|
||||
|
||||
# dev
|
||||
.yarn/
|
||||
!.yarn/releases
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/*.code-snippets
|
||||
.idea/workspace.xml
|
||||
.idea/usage.statistics.xml
|
||||
.idea/shelf
|
||||
|
||||
# deps
|
||||
node_modules/
|
||||
.wrangler
|
||||
|
||||
# env
|
||||
.env
|
||||
.env.production
|
||||
.dev.vars
|
||||
|
||||
# logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
/src/generated/prisma
|
||||
12
README.md
Normal file
12
README.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# anum
|
||||
|
||||
the unholy lovechild of Xitter and 4chan
|
||||
|
||||
## migration tutorial because i have dementia
|
||||
### if this makes it to prod im going to follow lowtiergod's advice
|
||||
1. `cp prisma/schema.prisma prisma/schema.prisma.old`
|
||||
2. touch schema.prisma like diddy touches children
|
||||
3. tell wrangler that we are Migrationing It
|
||||
4. get prisma to create a migration from the old ass motherfucker schema to the new babyoiled one and throw its output in wrangler's migration template
|
||||
5. tell wrangler that we have Migrationed It go apply this shit now you stinky ass motherfucker blackout revival flare looking ass
|
||||
6. `rm prisma/schema.prisma.old`
|
||||
166
ai_seed.sql
Normal file
166
ai_seed.sql
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
-- 1. CLEANUP (Optional: Remove if you want to keep existing data)
|
||||
DELETE FROM "Post";
|
||||
DELETE FROM "User";
|
||||
DELETE FROM "sqlite_sequence"; -- Resets autoincrement counters
|
||||
|
||||
-- 2. CREATE USERS
|
||||
-- (Passwords are all the same dummy bcrypt hash for "password123")
|
||||
INSERT INTO "User" (id, email, name, display_name, password) VALUES
|
||||
(1, 'admin@forum.com', 'admin', 'SysAdmin', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(2, 'steve@moderator.com', 'mod_steve', 'Steve [MOD]', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(3, 'jane.doe@gmail.com', 'janedoe', 'Jane D.', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(4, 'rust_evangelist@tech.com', 'rusty', 'RustInPeace', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(5, 'cpp_veteran@legacy.com', 'segfault', 'C++ Grandmaster', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(6, 'troll123@hotmail.com', 'u_mad_bro', 'Anonymous', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(7, 'helpme@student.edu', 'noob_coder', 'HelpMePls', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(8, 'linux_fan@gnu.org', 'sudo_make_me', 'Arch User', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(9, 'webdev@agency.com', 'div_center', 'CSS Wizard', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(10, 'gamer@twitch.tv', 'xx_sniper_xx', 'Pro Gamer', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(11, 'bot@bot.com', 'news_bot', 'News Aggregator', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(12, 'lurker@yahoo.com', 'lurker007', NULL, '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(13, 'vim@editor.com', 'vim_enthusiast', ':wq', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(14, 'emacs@editor.com', 'emacs_guru', 'M-x Butterfly', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7'),
|
||||
(15, 'react@fb.com', 'hook_master', 'UseEffect', '$2b$10$EpWaTgiK/H/t/0W.p/h.quu/L7C7jQe5nVqg.j/z.p/h.quu/L7C7');
|
||||
|
||||
|
||||
-- 3. CREATE THREADS (Top level posts)
|
||||
-- We manually assign IDs here to ensure the replies attach correctly later.
|
||||
|
||||
-- Thread 1: Welcome (Sticky)
|
||||
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
|
||||
(1, 'Welcome to the Community! READ FIRST', 'Welcome everyone. Please be nice. No spamming. Enjoy your stay!', 1, NULL, datetime('now', '-30 days'));
|
||||
|
||||
-- Thread 2: Flame war (Tabs vs Spaces)
|
||||
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
|
||||
(2, 'Unpopular Opinion: Spaces are objectively better than Tabs', 'I said what I said. If you use tabs, your formatting is at the mercy of the editor settings. Discuss.', 3, NULL, datetime('now', '-15 days'));
|
||||
|
||||
-- Thread 3: Tech Support
|
||||
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
|
||||
(3, 'URGENT: Cannot exit Vim?', 'I have been stuck in Vim for 4 days. I have tried Esc, Ctrl+C, Alt+F4. I am starving. Please help.', 7, NULL, datetime('now', '-2 days'));
|
||||
|
||||
-- Thread 4: Gaming
|
||||
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
|
||||
(4, 'Elden Ring DLC discussion', 'Has anyone beaten the final boss yet? No spoilers please, but that second phase is brutal.', 10, NULL, datetime('now', '-5 days'));
|
||||
|
||||
-- Thread 5: Programming Language War
|
||||
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
|
||||
(5, 'Why Rust will replace C++ in 5 years', 'Memory safety is not optional anymore. The borrow checker is your friend.', 4, NULL, datetime('now', '-10 days'));
|
||||
|
||||
-- Thread 6: Off Topic Megathread (This will house most of the "volume")
|
||||
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
|
||||
(6, 'MEGATHREAD: What are you listening to right now?', 'Post your current jam. Keep it clean.', 2, NULL, datetime('now', '-60 days'));
|
||||
|
||||
-- Thread 7: Random
|
||||
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
|
||||
(7, 'My cat walked on my keyboard and deployed to production', 'Fortunately, the tests passed. I think my cat is a better dev than me.', 9, NULL, datetime('now', '-1 day'));
|
||||
|
||||
-- Thread 8: News
|
||||
INSERT INTO "Post" (id, title, content, userId, parentId, createdAt) VALUES
|
||||
(8, 'Tech News: AI takes over toaster industry', 'Smart toasters are now requiring a subscription to toast bagels.', 11, NULL, datetime('now', '-20 days'));
|
||||
|
||||
|
||||
-- 4. CREATE REALISTIC REPLIES
|
||||
-- Thread 1 (Welcome) Replies
|
||||
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
|
||||
('Re: Welcome', 'Thanks admin! Glad to be here.', 3, 1, datetime('now', '-29 days')),
|
||||
('Re: Welcome', 'First!', 6, 1, datetime('now', '-29 days')),
|
||||
('Re: Welcome', 'Please do not post "First", this is not YouTube.', 2, 1, datetime('now', '-29 days')),
|
||||
('Re: Welcome', 'Hello world.', 5, 1, datetime('now', '-28 days'));
|
||||
|
||||
-- Thread 2 (Tabs vs Spaces) - The Fight
|
||||
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
|
||||
('Re: Tabs vs Spaces', 'You are wrong. Tabs save file size. Imagine a 1MB file, tabs save bytes!', 5, 2, datetime('now', '-14 days')),
|
||||
('Re: Tabs vs Spaces', 'Storage is cheap. Consistency is priceless. Spaces all the way.', 9, 2, datetime('now', '-14 days')),
|
||||
('Re: Tabs vs Spaces', 'I use a mix of both just to annoy my coworkers.', 6, 2, datetime('now', '-14 days')),
|
||||
('Re: Tabs vs Spaces', 'You monster.', 3, 2, datetime('now', '-13 days')),
|
||||
('Re: Tabs vs Spaces', 'Go style guide says tabs. I follow the Go style guide.', 4, 2, datetime('now', '-13 days')),
|
||||
('Re: Tabs vs Spaces', 'Python enforces indentation. Spaces are the standard there.', 8, 2, datetime('now', '-13 days'));
|
||||
|
||||
-- Thread 3 (Vim Help)
|
||||
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
|
||||
('Re: Vim', 'Type :q! and hit enter.', 13, 3, datetime('now', '-2 days')),
|
||||
('Re: Vim', 'Or just unplug your computer.', 6, 3, datetime('now', '-2 days')),
|
||||
('Re: Vim', 'Just switch to Emacs, we have a menu bar.', 14, 3, datetime('now', '-2 days')),
|
||||
('Re: Vim', 'OMG THANK YOU @vim_enthusiast you saved my life.', 7, 3, datetime('now', '-1 day'));
|
||||
|
||||
-- Thread 5 (Rust vs C++)
|
||||
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
|
||||
('Re: Rust', 'C++ is still faster if you know what you are doing.', 5, 5, datetime('now', '-9 days')),
|
||||
('Re: Rust', 'Most people do not know what they are doing though.', 4, 5, datetime('now', '-9 days')),
|
||||
('Re: Rust', 'Rewriting everything in Rust is not feasible for large legacy codebases.', 1, 5, datetime('now', '-8 days')),
|
||||
('Re: Rust', 'Agreed, but for greenfield projects it is a no brainer.', 9, 5, datetime('now', '-8 days')),
|
||||
('Re: Rust', 'I like Zig.', 8, 5, datetime('now', '-7 days'));
|
||||
|
||||
-- Thread 7 (Cat deployment)
|
||||
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
|
||||
('Re: Cat', 'Is the cat looking for a job? We are hiring.', 1, 7, datetime('now', '-23 hours')),
|
||||
('Re: Cat', 'Pics or it didnt happen.', 6, 7, datetime('now', '-20 hours')),
|
||||
('Re: Cat', 'Mine just sleeps on the warm laptop vent.', 15, 7, datetime('now', '-10 hours'));
|
||||
|
||||
|
||||
-- 5. GENERATE BULK VOLUME (The "Megathread")
|
||||
-- We use a recursive query or just a massive block of inserts to hit ~500 posts.
|
||||
-- We will simulate a "What are you listening to" thread (Parent ID 6).
|
||||
|
||||
INSERT INTO "Post" (title, content, userId, parentId, createdAt) VALUES
|
||||
('Song', 'Pink Floyd - Echoes', 8, 6, datetime('now', '-59 days')),
|
||||
('Song', 'Daft Punk - Discovery (Whole Album)', 9, 6, datetime('now', '-58 days')),
|
||||
('Song', 'Lo-fi beats to study to', 7, 6, datetime('now', '-58 days')),
|
||||
('Song', 'Metallica - Master of Puppets', 5, 6, datetime('now', '-57 days')),
|
||||
('Song', 'Darude - Sandstorm', 6, 6, datetime('now', '-56 days')),
|
||||
('Song', 'Rick Astley - Never Gonna Give You Up', 6, 6, datetime('now', '-56 days')),
|
||||
('Song', 'Bach - Cello Suite No. 1', 3, 6, datetime('now', '-55 days')),
|
||||
('Song', 'Kendrick Lamar - DNA', 15, 6, datetime('now', '-54 days')),
|
||||
('Song', 'Tool - Lateralus', 4, 6, datetime('now', '-53 days')),
|
||||
('Song', 'Taylor Swift', 10, 6, datetime('now', '-52 days')),
|
||||
('Song', '+1 for Tool', 8, 6, datetime('now', '-52 days')),
|
||||
('Song', 'Radiohead - OK Computer', 2, 6, datetime('now', '-50 days')),
|
||||
('Song', 'Video Game OSTs mostly', 10, 6, datetime('now', '-49 days')),
|
||||
('Song', 'Aphex Twin', 11, 6, datetime('now', '-48 days')),
|
||||
('Song', 'Silence. My fans are too loud.', 12, 6, datetime('now', '-47 days')),
|
||||
('Song', 'Jazz vibes', 14, 6, datetime('now', '-46 days'));
|
||||
|
||||
-- To get to 500 without writing 500 lines manually, we will use a cross join trick
|
||||
-- to multiply these generic comments into the megathread (ID 6).
|
||||
|
||||
INSERT INTO "Post" (title, content, userId, parentId, createdAt)
|
||||
SELECT
|
||||
'Re: Music',
|
||||
'Listening to track #' || (ABS(RANDOM()) % 1000),
|
||||
(ABS(RANDOM()) % 15) + 1, -- Random User ID 1-15
|
||||
6, -- Parent ID 6 (Music Thread)
|
||||
datetime('now', '-' || (ABS(RANDOM()) % 60) || ' days')
|
||||
FROM "User" u1, "User" u2, "User" u3
|
||||
LIMIT 300; -- Adds 300 generic posts
|
||||
|
||||
-- Add some random replies to the Rust vs C++ thread (ID 5) to make it look heated
|
||||
INSERT INTO "Post" (title, content, userId, parentId, createdAt)
|
||||
SELECT
|
||||
'Re: Rust vs C++',
|
||||
CASE (ABS(RANDOM()) % 5)
|
||||
WHEN 0 THEN 'I disagree strongly.'
|
||||
WHEN 1 THEN 'This is the way.'
|
||||
WHEN 2 THEN 'Have you read the documentation?'
|
||||
WHEN 3 THEN 'Benchmarks prove otherwise.'
|
||||
ELSE 'Interesting point.'
|
||||
END,
|
||||
(ABS(RANDOM()) % 15) + 1,
|
||||
5, -- Parent ID 5
|
||||
datetime('now', '-' || (ABS(RANDOM()) % 10) || ' days')
|
||||
FROM "User" u1, "User" u2
|
||||
LIMIT 50;
|
||||
|
||||
-- Add some "Bump" posts to random threads
|
||||
INSERT INTO "Post" (title, content, userId, parentId, createdAt)
|
||||
SELECT
|
||||
'Bump',
|
||||
'Bumping this thread.',
|
||||
(ABS(RANDOM()) % 15) + 1,
|
||||
(ABS(RANDOM()) % 8) + 1, -- Random Parent ID 1-8
|
||||
datetime('now', '-1 hour')
|
||||
FROM "User"
|
||||
LIMIT 20;
|
||||
|
||||
COMMIT;
|
||||
9
eslint.config.ts
Normal file
9
eslint.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
export default defineConfig([
|
||||
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
|
||||
tseslint.configs.recommended,
|
||||
]);
|
||||
59
flake.lock
generated
Normal file
59
flake.lock
generated
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767364772,
|
||||
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
15
flake.nix
Normal file
15
flake.nix
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
description = "anum backend";
|
||||
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem
|
||||
(system:
|
||||
let pkgs = nixpkgs.legacyPackages.${system}; in
|
||||
{
|
||||
devShells.default = import ./shell.nix { inherit pkgs; };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
23
migrations/0001_init.sql
Normal file
23
migrations/0001_init.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"display_name" TEXT,
|
||||
"password" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Post" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");
|
||||
17
migrations/0002_replies.sql
Normal file
17
migrations/0002_replies.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Post" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"parentId" INTEGER,
|
||||
CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "Post_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Post" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_Post" ("content", "id", "title", "userId") SELECT "content", "id", "title", "userId" FROM "Post";
|
||||
DROP TABLE "Post";
|
||||
ALTER TABLE "new_Post" RENAME TO "Post";
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
18
migrations/0003_post_creation_date.sql
Normal file
18
migrations/0003_post_creation_date.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Post" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"title" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"parentId" INTEGER,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "Post_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Post" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_Post" ("content", "id", "parentId", "title", "userId") SELECT "content", "id", "parentId", "title", "userId" FROM "Post";
|
||||
DROP TABLE "Post";
|
||||
ALTER TABLE "new_Post" RENAME TO "Post";
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
1
migrations/migration_lock.toml
Normal file
1
migrations/migration_lock.toml
Normal file
|
|
@ -0,0 +1 @@
|
|||
provider = "sqlite"
|
||||
28
package.json
Normal file
28
package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "anum",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wrangler dev",
|
||||
"deploy": "wrangler deploy --minify",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"@prisma/adapter-d1": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"bcrypt-ts": "^8.0.0",
|
||||
"hono": "^4.11.3",
|
||||
"jose": "^6.1.3",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"eslint": "^9.39.2",
|
||||
"globals": "^17.0.0",
|
||||
"jiti": "^2.6.1",
|
||||
"prisma": "^7.2.0",
|
||||
"typescript-eslint": "^8.51.0",
|
||||
"vscode-langservers-extracted": "^4.10.0",
|
||||
"wrangler": "^4.4.0"
|
||||
}
|
||||
}
|
||||
2782
pnpm-lock.yaml
generated
Normal file
2782
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
14
prisma.config.ts
Normal file
14
prisma.config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// This file was generated by Prisma, and assumes you have installed the following:
|
||||
// npm install --save-dev prisma dotenv
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
},
|
||||
datasource: {
|
||||
url: process.env["DATABASE_URL"],
|
||||
},
|
||||
});
|
||||
31
prisma/schema.prisma
Normal file
31
prisma/schema.prisma
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
name String @unique
|
||||
display_name String?
|
||||
password String
|
||||
posts Post[]
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
content String
|
||||
author User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
replies Post[] @relation("PostReplies")
|
||||
parent Post? @relation("PostReplies", fields: [parentId], references: [id])
|
||||
parentId Int?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
48
seed.sql
Normal file
48
seed.sql
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
-- 1. Generate 100 Users
|
||||
-- Password is a bcrypt hash for "password123"
|
||||
INSERT INTO "User" (email, name, display_name, password)
|
||||
WITH RECURSIVE
|
||||
cnt(n) AS (
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT n+1 FROM cnt WHERE n < 100
|
||||
)
|
||||
SELECT
|
||||
'user' || n || '@example.com',
|
||||
'user_' || n,
|
||||
'User ' || n,
|
||||
'$2b$10$EpjVIByL7WqitL96.t65he1usN8L6j0r6n/1q7R8U6O1Y2v6y/L2.' -- password123
|
||||
FROM cnt;
|
||||
|
||||
-- 2. Generate 700 Parent Posts
|
||||
-- Distributed across the 100 users
|
||||
INSERT INTO "Post" (title, content, userId, createdAt)
|
||||
WITH RECURSIVE
|
||||
cnt(n) AS (
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT n+1 FROM cnt WHERE n < 700
|
||||
)
|
||||
SELECT
|
||||
'Post Title #' || n,
|
||||
'This is the body content for sample post number ' || n || '. It contains some placeholder text for testing purposes.',
|
||||
(ABS(RANDOM()) % 100) + 1, -- Random userId between 1 and 100
|
||||
datetime('now', '-' || (ABS(RANDOM()) % 365) || ' days') -- Random date in the last year
|
||||
FROM cnt;
|
||||
|
||||
-- 3. Generate 200 Replies
|
||||
-- Linked to random parent posts (IDs 1-700)
|
||||
INSERT INTO "Post" (title, content, userId, parentId, createdAt)
|
||||
WITH RECURSIVE
|
||||
cnt(n) AS (
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT n+1 FROM cnt WHERE n < 200
|
||||
)
|
||||
SELECT
|
||||
'Reply #' || n,
|
||||
'I am replying to an existing post to test the self-relation logic.',
|
||||
(ABS(RANDOM()) % 100) + 1, -- Random author
|
||||
(ABS(RANDOM()) % 700) + 1, -- Random parent post ID
|
||||
datetime('now', '-' || (ABS(RANDOM()) % 30) || ' days') -- Random date in last 30 days
|
||||
FROM cnt;
|
||||
15
shell.nix
Normal file
15
shell.nix
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{ pkgs ? import <nixpkgs> { } }:
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
prisma_7
|
||||
pnpm_10
|
||||
nodejs_25
|
||||
vscode-langservers-extracted
|
||||
typescript
|
||||
];
|
||||
shellHook = ''
|
||||
export PRISMA_QUERY_ENGINE_LIBRARY=${pkgs.prisma-engines}/lib/libquery_engine.node
|
||||
export PRISMA_QUERY_ENGINE_BINARY=${pkgs.prisma-engines}/bin/query-engine
|
||||
export PRISMA_SCHEMA_ENGINE_BINARY=${pkgs.prisma-engines}/bin/schema-engine
|
||||
'';
|
||||
}
|
||||
49
shell.nix.bak
Normal file
49
shell.nix.bak
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
(pkgs.buildFHSEnv {
|
||||
name = "prisma-v7-shell";
|
||||
|
||||
targetPkgs = pkgs: (with pkgs; [
|
||||
nodejs
|
||||
pnpm
|
||||
openssl
|
||||
zlib
|
||||
]);
|
||||
|
||||
runScript = "bash";
|
||||
|
||||
profile = ''
|
||||
# 1. Define the correct version and commit for Prisma 7.2.0
|
||||
PRISMA_VERSION="7.2.0"
|
||||
COMMIT="0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
|
||||
|
||||
# 2. Setup cache directory
|
||||
CACHE_DIR="$HOME/.cache/prisma-nix-fix/$PRISMA_VERSION"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
|
||||
# 3. Only download schema-engine (REQUIRED for migrations)
|
||||
# Prisma 7 does not use the Rust query-engine by default, so we skip it to avoid 404s.
|
||||
if [ ! -f "$CACHE_DIR/schema-engine" ]; then
|
||||
echo ">> NixOS Fix: Downloading schema-engine for Prisma $PRISMA_VERSION..."
|
||||
# Use -f to fail silently if the file is missing (though it shouldn't be for schema-engine)
|
||||
curl -fL "https://binaries.prisma.sh/all_commits/$COMMIT/debian-openssl-3.0.x/schema-engine.gz" | gunzip > "$CACHE_DIR/schema-engine"
|
||||
chmod +x "$CACHE_DIR/schema-engine"
|
||||
fi
|
||||
|
||||
# 4. Export the specific variable for the Schema Engine
|
||||
export PRISMA_SCHEMA_ENGINE_BINARY="$CACHE_DIR/schema-engine"
|
||||
|
||||
# 5. UNSET the query engine variables.
|
||||
# Prisma 7 will fall back to its internal TypeScript engine (which works fine in FHS).
|
||||
# If we leave these set to broken files (from the 404s), Prisma crashes.
|
||||
unset PRISMA_QUERY_ENGINE_BINARY
|
||||
unset PRISMA_QUERY_ENGINE_LIBRARY
|
||||
unset PRISMA_FMT_BINARY
|
||||
|
||||
echo "------------------------------------------------------------"
|
||||
echo " Prisma 7.x Shell (NixOS)"
|
||||
echo " - Schema Engine: Pinned to Debian OpenSSL 3.0.x"
|
||||
echo " - Query Engine: Using Prisma Default (TypeScript)"
|
||||
echo "------------------------------------------------------------"
|
||||
'';
|
||||
}).env
|
||||
212
src/index.ts
Normal file
212
src/index.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// src/index.ts
|
||||
import { Hono } from 'hono'
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaD1 } from '@prisma/adapter-d1';
|
||||
import { Bindings, Variables, UserBody, LoginBody, PostCreationBody } from './types';
|
||||
import { genSalt, hash, compare } from "bcrypt-ts";
|
||||
import { sign_jwt } from './util/jwt'
|
||||
import { authMiddleware } from './middleware/auth';
|
||||
import { JsonObject } from '@prisma/client/runtime/client';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { userCreationSchema, userLoginSchema, postCreationSchema } from './middleware/schemas';
|
||||
import { cors } from 'hono/cors';
|
||||
import { setCookie } from 'hono/cookie';
|
||||
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
|
||||
|
||||
app.use('*', async (c, next) => {
|
||||
const adapter = new PrismaD1(c.env.DB);
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
c.set('prisma', prisma);
|
||||
await next();
|
||||
})
|
||||
|
||||
app.use('*', cors({
|
||||
origin: 'http://localhost:5173',
|
||||
allowHeaders: ['Content-Type', 'Authorization'], // Specify the headers your clients might send
|
||||
allowMethods: ['POST', 'GET', 'OPTIONS', 'DELETE', 'PATCH', 'PUT'], // Specify the methods you use
|
||||
maxAge: 600, // Optional: cache preflight response for 10 minutes
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
app.get('/', (c) => {
|
||||
return c.html("<h1>Hello!</h1>")
|
||||
})
|
||||
|
||||
app.post('/users/create', zValidator('json', userCreationSchema), async (c) => {
|
||||
const prisma = c.get('prisma');
|
||||
const body = await c.req.json<UserBody>();
|
||||
const salt = await genSalt(10);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: body.email,
|
||||
name: body.name,
|
||||
display_name: body.display_name,
|
||||
password: await hash(body.password, salt),
|
||||
},
|
||||
})
|
||||
|
||||
const jwt = await sign_jwt({ id: user.id });
|
||||
setCookie(c, 'auth_token', jwt, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'Lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
return c.json({ err: null, result: user })
|
||||
})
|
||||
|
||||
app.post('/users/login', zValidator('json', userLoginSchema), async (c) => {
|
||||
const prisma = c.get('prisma');
|
||||
const body = await c.req.json<LoginBody>();
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: body.email },
|
||||
});
|
||||
|
||||
if (!user) return c.body("404 Not Found\n");
|
||||
|
||||
const result = await compare(body.password, user.password);
|
||||
if (result) {
|
||||
const jwt = await sign_jwt({ id: user.id });
|
||||
setCookie(c, 'auth_token', jwt, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'Lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
});
|
||||
return c.json({ err: null, result: user })
|
||||
} else {
|
||||
c.status(403);
|
||||
return c.json({ err: 403 })
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/posts/add', zValidator('json', postCreationSchema), authMiddleware, async (c) => {
|
||||
const prisma = c.get('prisma');
|
||||
const userId = c.get('userId');
|
||||
const body = await c.req.json<PostCreationBody>();
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return c.text("403 forbidden")
|
||||
}
|
||||
|
||||
const post = await prisma.post.create({
|
||||
data: {
|
||||
title: body.title,
|
||||
content: body.content,
|
||||
author: { connect: { id: user.id } }
|
||||
}
|
||||
});
|
||||
|
||||
return c.json(post)
|
||||
})
|
||||
|
||||
app.post('/posts/:id/reply', zValidator('json', postCreationSchema), authMiddleware, async (c) => {
|
||||
const prisma = c.get('prisma');
|
||||
const userId = c.get('userId');
|
||||
const body = await c.req.json<PostCreationBody>();
|
||||
|
||||
// auth check
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return c.text("403 forbidden")
|
||||
}
|
||||
|
||||
const parentIdParameter = c.req.param("id");
|
||||
if (isNaN(Number(parentIdParameter))) {
|
||||
c.status(400)
|
||||
return c.text("400 bad request")
|
||||
}
|
||||
const parentId = Number(parentIdParameter)
|
||||
|
||||
const parent_post = await prisma.post.findUnique({
|
||||
where: {
|
||||
id: parentId
|
||||
}
|
||||
});
|
||||
|
||||
if (!parent_post) {
|
||||
return c.text("404 not found")
|
||||
}
|
||||
|
||||
// actually make the post
|
||||
|
||||
const post = await prisma.post.create({
|
||||
data: {
|
||||
title: body.title,
|
||||
content: body.content,
|
||||
author: { connect: { id: user.id } },
|
||||
parent: { connect: { id: parentId } }
|
||||
}
|
||||
});
|
||||
|
||||
return c.json(post)
|
||||
})
|
||||
|
||||
// Helper to generate nested includes with limits
|
||||
function getNestedReplies(depth: number, breadth: number): JsonObject {
|
||||
if (depth === 0) return {};
|
||||
return {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
name: true,
|
||||
display_name: true,
|
||||
id: true
|
||||
}
|
||||
},
|
||||
replies: {
|
||||
take: breadth, // limit breadth
|
||||
orderBy: { createdAt: 'desc' as const },
|
||||
...getNestedReplies(depth - 1, breadth),
|
||||
},
|
||||
_count: {
|
||||
select: { replies: true } // indicator of if there's more stuff to load
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
app.get('/posts/:id/view', async (c) => {
|
||||
const prisma = c.get('prisma');
|
||||
const post_id = c.req.param('id');
|
||||
|
||||
const post = await prisma.post.findMany({
|
||||
where: {
|
||||
id: Number(post_id),
|
||||
},
|
||||
...getNestedReplies(3, 5)
|
||||
})
|
||||
|
||||
return c.json(post)
|
||||
})
|
||||
|
||||
app.get('/feed', async (c) => {
|
||||
const prisma = c.get('prisma');
|
||||
const posts = await prisma.post.findMany({
|
||||
where: {
|
||||
parentId: null,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
take: 10,
|
||||
...getNestedReplies(2, 3)
|
||||
});
|
||||
return c.json(posts);
|
||||
})
|
||||
|
||||
export default app
|
||||
24
src/middleware/auth.ts
Normal file
24
src/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// src/middleware/auth.ts
|
||||
import { createMiddleware } from 'hono/factory'
|
||||
import { verify_jwt } from '../util/jwt'
|
||||
import { Bindings, Variables } from '../types'
|
||||
import { getCookie } from 'hono/cookie';
|
||||
|
||||
export const authMiddleware = createMiddleware<{
|
||||
Bindings: Bindings,
|
||||
Variables: Variables
|
||||
}>(async (c, next) => {
|
||||
const auth = getCookie(c, 'auth_token');
|
||||
|
||||
if (!auth) {
|
||||
return c.json({ err: 'Unauthorized' }, 401)
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await verify_jwt(auth)
|
||||
c.set('userId', payload.id)
|
||||
await next()
|
||||
} catch {
|
||||
return c.text("Invalid Token", 401)
|
||||
}
|
||||
})
|
||||
16
src/middleware/prisma.ts
Normal file
16
src/middleware/prisma.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { createMiddleware } from 'hono/factory'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import { PrismaD1 } from '@prisma/adapter-d1'
|
||||
import { Bindings, Variables } from '../types'
|
||||
|
||||
export const prismaMiddleware = createMiddleware<{
|
||||
Bindings: Bindings,
|
||||
Variables: Variables
|
||||
}>(async (c, next) => {
|
||||
const adapter = new PrismaD1(c.env.DB)
|
||||
const prisma = new PrismaClient({ adapter })
|
||||
|
||||
c.set('prisma', prisma)
|
||||
|
||||
await next()
|
||||
})
|
||||
21
src/middleware/schemas.ts
Normal file
21
src/middleware/schemas.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// src/middleware/schemas.ts
|
||||
import * as z from 'zod';
|
||||
|
||||
export const userCreationSchema = z.object({
|
||||
email: z.email().max(255),
|
||||
name: z.string().min(3).max(32),
|
||||
display_name: z.string().min(3).max(32),
|
||||
password: z.string().min(8).max(255),
|
||||
});
|
||||
|
||||
export const userLoginSchema = z.object({
|
||||
email: z.email().max(255),
|
||||
password: z.string().min(8).max(255)
|
||||
});
|
||||
|
||||
export const postCreationSchema = z.object({
|
||||
title: z.string().min(1).max(128),
|
||||
content: z.string().min(1).max(2147483647)
|
||||
})
|
||||
|
||||
|
||||
30
src/types.ts
Normal file
30
src/types.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// src/types.ts
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export interface Bindings {
|
||||
// @ts-expect-error Defined by God Himself. (idk ask Cloudflare)
|
||||
DB: D1Database;
|
||||
}
|
||||
|
||||
export interface Variables {
|
||||
prisma: PrismaClient;
|
||||
userId: number;
|
||||
postId: number;
|
||||
}
|
||||
|
||||
export interface UserBody {
|
||||
email: string
|
||||
name: string
|
||||
display_name: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginBody {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface PostCreationBody {
|
||||
title: string,
|
||||
content: string,
|
||||
}
|
||||
28
src/util/jwt.ts
Normal file
28
src/util/jwt.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import * as jose from 'jose'
|
||||
|
||||
const issuer = "Dingaling Industries LLC";
|
||||
// @ts-expect-error Defined by CF.
|
||||
const secret = new TextEncoder().encode("heilstalin");
|
||||
|
||||
interface PayloadBody {
|
||||
id: number
|
||||
}
|
||||
|
||||
export async function sign_jwt(payload: jose.JWTPayload) {
|
||||
|
||||
const alg = 'HS256';
|
||||
|
||||
const jwt = await new jose.SignJWT(payload)
|
||||
.setIssuedAt()
|
||||
.setIssuer(issuer)
|
||||
.setExpirationTime('2w')
|
||||
.setProtectedHeader({ alg })
|
||||
.sign(secret)
|
||||
|
||||
return jwt;
|
||||
}
|
||||
|
||||
export async function verify_jwt(jwt: string) {
|
||||
const { payload } = await jose.jwtVerify(jwt, secret);
|
||||
return payload as unknown as PayloadBody;
|
||||
}
|
||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx"
|
||||
},
|
||||
}
|
||||
16
wrangler.jsonc
Normal file
16
wrangler.jsonc
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "anum-backend",
|
||||
"compatibility_date": "2025-08-03",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"main": "./src/index.ts",
|
||||
"d1_databases": [
|
||||
{
|
||||
"binding": "DB",
|
||||
"database_name": "anum",
|
||||
"database_id": "to be filled when i get remote cloudflare workers up"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue