36 lines
3.3 KiB
TypeScript
36 lines
3.3 KiB
TypeScript
import type {PoolClient} from 'pg';
|
|
import { db } from "./db";
|
|
import { decodeCursor, encodeCursor, type FeedData, type Post, type PostKind, type Tag } from "./model";
|
|
|
|
export async function tags(client?:PoolClient): Promise<Tag[]> { return (await (client||db()).query('SELECT *,slug AS path FROM tags ORDER BY name,id')).rows; }
|
|
const expand = `SELECT p.*,
|
|
COALESCE((SELECT jsonb_agg(ae ORDER BY ae.position) FROM (SELECT a.*,pa.position,pa.role,COALESCE((SELECT jsonb_agg(v.derivation) FROM assets v WHERE v.source_asset_id=a.id),'[]') AS variants FROM post_assets pa JOIN assets a ON a.id=pa.asset_id WHERE pa.post_id=p.id) ae),'[]') AS assets,
|
|
COALESCE((SELECT jsonb_agg(t) FROM tags t JOIN post_tags pt ON pt.tag_id=t.id WHERE pt.post_id=p.id),'[]') AS tags FROM posts p`;
|
|
function serialize(posts: Post[], allTags: Tag[]): Post[] {
|
|
return posts.map(p => ({...p, created_at: new Date(p.created_at).toISOString(), updated_at:new Date(p.updated_at).toISOString(), tags:p.tags.map(t=>allTags.find(a=>a.id===t.id)!).filter(Boolean)}));
|
|
}
|
|
export async function getPost(slug: string): Promise<Post | null> {
|
|
const result = await db().query(`${expand} WHERE p.slug=$1`,[slug]);
|
|
return serialize(result.rows,await tags())[0] || null;
|
|
}
|
|
export async function getPostById(id:string,client?:PoolClient):Promise<Post|null>{
|
|
const result=await (client||db()).query(`${expand} WHERE ${/^[0-9a-f]{24}$/.test(id)?'p.public_id':'p.id'}=$1`,[id]);
|
|
return serialize(result.rows,await tags(client))[0]||null;
|
|
}
|
|
export async function feed(options: {kind?: PostKind; path?: string; cursor?: string; q?: string} = {}): Promise<FeedData> {
|
|
const allTags=await tags(); const target=options.path ? allTags.find(t=>t.path===options.path) : undefined;
|
|
if(options.path&&!target) throw new Error("Tag not found");
|
|
const cursor=decodeCursor(options.cursor); const values: unknown[]=[];
|
|
const where=[`EXISTS(SELECT 1 FROM post_assets pa JOIN assets a ON a.id=pa.asset_id WHERE pa.post_id=p.id AND pa.role IN ('content','caption') AND ((a.storage_key IS NOT NULL AND a.byte_size>0) OR (a.kind='text' AND a.body IS NOT NULL)))`];
|
|
if(options.kind){values.push(options.kind);where.push(`p.kind=$${values.length}`);}
|
|
if(target){values.push([target.id]);where.push(`EXISTS(SELECT 1 FROM post_tags WHERE post_id=p.id AND tag_id=ANY($${values.length}::uuid[]))`);}
|
|
if(options.q){values.push(`%${options.q.replace(/[\\%_]/g,"\\$&")}%`);where.push(`p.title ILIKE $${values.length}`);}
|
|
if(cursor){values.push(...cursor);where.push(`(p.created_at,p.id)<($${values.length-1}::timestamptz,$${values.length}::uuid)`);}
|
|
const rows=(await db().query(`${expand} WHERE ${where.join(' AND ')} ORDER BY p.created_at DESC,p.id DESC LIMIT 25`,values)).rows as Post[];
|
|
const hasMore=rows.length>24; const posts=serialize(rows.slice(0,24),allTags); const last=posts.at(-1);
|
|
const contributions=(await db().query(`SELECT to_char(occurred_at AT TIME ZONE 'Asia/Shanghai','YYYY-MM-DD') AS day,sum(amount)::integer AS total FROM contribution_events WHERE occurred_at>=now()-interval '366 days' GROUP BY day ORDER BY day`)).rows;
|
|
return {posts,tags:allTags,contributions,nextCursor:hasMore&&last?encodeCursor(last.created_at,last.id):null};
|
|
}
|
|
|
|
export async function publishedPublicIds():Promise<string[]>{return (await db().query("SELECT public_id FROM posts WHERE public_id IS NOT NULL")).rows.map(p=>p.public_id);}
|