Compare commits

...

2 commits

Author SHA1 Message Date
1d358a9ea9 Set DB interface 2025-04-27 17:14:16 +08:00
0745298257 postgres_cursor add 2025-04-25 11:45:55 +08:00
12 changed files with 174 additions and 81 deletions

BIN
bun.lockb Normal file → Executable file

Binary file not shown.

6
justfile Normal file
View file

@ -0,0 +1,6 @@
fake:
POSTGRES_URL="postgres://test:test@192.168.50.14:5432/posts" bun run ./tools/gen_fake.ts
fetch_post:
POSTGRES_URL="postgres://test:test@192.168.50.14:5432/posts" bun run ./tools/post_db.ts
create_schema:
POSTGRES_URL="postgres://test:test@192.168.50.14:5432/posts" bun run ./tools/create_schema.ts

View file

@ -9,6 +9,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@faker-js/faker": "^9.7.0",
"bun-types": "^1.2.9", "bun-types": "^1.2.9",
"next": "15.3.0", "next": "15.3.0",
"react": "^19.0.0", "react": "^19.0.0",

15
src/app/new_post/route.ts Normal file
View file

@ -0,0 +1,15 @@
import { insert_post, NewPost } from "@/db";
import { Blob } from "buffer";
import type { NextRequest } from "next/server";
export async function POST(req: NextRequest) {
let form = await req.formData();
let text = form.get("content")?.toString()
if (!text) {
return Response.error()
}
let request: NewPost = { content: text, image: [] };
let [id, hash] = await insert_post(request);
return Response.json({ id: id, hash: hash })
}

View file

@ -0,0 +1,6 @@
import { NextRequest } from "next/server";
export function Post(req: NextRequest) {
}

View file

@ -1,4 +1,3 @@
import { retrieve_post } from "@/db";
export default function Home() { export default function Home() {

View file

@ -1,13 +1,13 @@
import { Suspense } from "react"; import { Suspense } from "react";
import Image from "next/image"; import Image from "next/image";
import { Attachment } from "@/db"; import { Attachment, MultiMediaType } from "@/db";
export default function Post(post_content: string, attachments: Attachment[]) { export default function Post(post_content: string, attachments: Attachment[]) {
let images = []; let images = [];
let videos = []; let videos = [];
attachments.forEach(attachment => { attachments.forEach(attachment => {
if (attachment.type.type == "video") { if (attachment.type == MultiMediaType.video) {
attachment.urls.forEach(url => { attachment.urls.forEach(url => {
videos.push( videos.push(
<Suspense fallback={<p></p>}> <Suspense fallback={<p></p>}>
@ -18,7 +18,7 @@ export default function Post(post_content: string, attachments: Attachment[]) {
</Suspense> </Suspense>
) )
}) })
}else if (attachment.type.type="image"){ } else if (attachment.type == MultiMediaType.image) {
attachment.urls.forEach(url => { attachment.urls.forEach(url => {
images.push(<Image src={url} alt="Uploaded" width={300} height={200}></Image>) images.push(<Image src={url} alt="Uploaded" width={300} height={200}></Image>)
}) })

132
src/db.ts
View file

@ -1,57 +1,93 @@
import { main } from "bun"; import { env, ReservedSQL, CryptoHasher, sql } from 'bun'
import { sql } from "bun"; import { MIMEType } from 'util';
import { MIMEType } from "util"
export enum MultiMediaType {
video, image
}
export interface Attachment { export interface Attachment {
type: MIMEType; urls: string[],
urls: string[]; type: MultiMediaType
} };
export interface Post { export interface Post {
content: string, post: string,
post_time: string,
hash: string, hash: string,
attachments: Attachment[],
} }
interface SQLPostCast {
export async function setup_func() { hash: string, post: string, post_time: string, images: number[]
const rows = await sql` };
CREATE OR REPLACE FUNCTION fetch_post( const SQLPostCast2Post = (obj: SQLPostCast) => {
OUT p_text VARCHAR(1000), let x: Post =
OUT p_hash CHAR(32), {
OUT p_date TIMESTAMP, post: obj.post,
OUT p_pics INT[] hash: obj.hash,
post_time: obj.post_time,
attachments: []
};
if (obj.images && obj.images.length > 0) {
x.attachments.push(
{
type: MultiMediaType.image, urls: obj.images.map(img => {
return `/img/${obj.hash}_${img}`
})
}
) )
RETURNS SETOF RECORD AS }
$$ return x
DECLARE
post_cursor CURSOR FOR
SELECT content, hash, date
FROM posts;
post_record RECORD;
BEGIN
-- Open cursor
OPEN post_cursor;
-- Fetch rows and return
LOOP
FETCH NEXT FROM post_cursor INTO post_record;
EXIT WHEN NOT FOUND;
p_text = post_record.content;
p_date = post_record.date;
p_hash = post_record.hash;
RETURN NEXT;
END LOOP;
-- Close cursor
CLOSE post_cursor;
END;
$$
LANGUAGE PLPGSQL;`;
} }
/* retrieve the latest post with posts table */
export async function retrieve_post(offset: Number) { export interface NewPost {
const res = await sql`SELECT * FROM fetch_post();` image: Blob[]
console.log(res) content: string,
} }
setup_func()
retrieve_post(0) export async function insert_post(post: NewPost): Promise<[number, string]> {
let post_hash = new CryptoHasher("sha256");
post_hash.update(post.content);
post_hash.update(Date.now().toString())
let populated = post_hash.digest("base64");
let [{ id, hash }] = await sql`INSERT INTO niming.posts (post, hash) VALUES (${post.content}, ${populated}) RETURNING id, hash`
return [id, hash]
}
export async function init_db() {
await sql.begin(async sql => {
await sql`CREATE SCHEMA niming`
await sql`CREATE TABLE niming.posts (id SERIAL PRIMARY KEY, hash char(44) UNIQUE, post VARCHAR(500), post_time TIMESTAMPTZ DEFAULT now())`
await sql`CREATE TABLE niming.images (id INTEGER PRIMARY KEY REFERENCES niming.posts (id), fileid integer[] )`
})
}
export class PostFetcher {
conn: Promise<ReservedSQL>;
constructor() {
this.conn = sql.reserve();
}
async init() {
await this.conn.then(async e => {
await e`DECLARE post_ptr CURSOR WITH HOLD FOR SELECT niming.posts.*, niming.images.fileid
AS images FROM niming.posts
LEFT JOIN niming.images ON niming.images.hash=niming.posts.hash ORDER BY niming.posts.post_time DESC, niming.posts.hash ASC`
}).catch(err => {
console.log(err);
})
}
async postgres_fetch() {
let x: SQLPostCast[] = []
if (this.conn) {
x = await (await this.conn)`FETCH 10 IN post_ptr`;
}
return x.map(e => {
return SQLPostCast2Post(e)
})
}
}

3
tools/create_schema.ts Normal file
View file

@ -0,0 +1,3 @@
import { init_db } from "@/db";
init_db()

13
tools/gen_fake.ts Normal file
View file

@ -0,0 +1,13 @@
import { insert_post, NewPost } from '@/db';
import { faker } from '@faker-js/faker';
for (let i = 0; i < 100; i++) {
let text = faker.string.alpha(20);
let x: NewPost = { image: [], content: text };
let [p_id, p_hash] = await insert_post(x)
console.log(p_id, p_hash)
}

13
tools/post_db.ts Normal file
View file

@ -0,0 +1,13 @@
import { PostFetcher, Post } from "@/db"
import { sql } from "bun";
let x: Post[] = [];
const fetcher = new PostFetcher();
await fetcher.init()
for (let i = 0; i < 10; i++) {
x = await fetcher.postgres_fetch()
console.log(x)
}

View file

@ -8,7 +8,7 @@
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "Bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
@ -20,7 +20,8 @@
], ],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} },
"types":["bun-types"]
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]