Files
probably-rendering-one-more…/src/lib/write.ts
T
codex-bot 8b650fdf78
PROMPT CI and deployment / verify-and-deploy (push) Successful in 8m18s
Build PROMPT media library with masonry UI and persistent PostgreSQL S3 storage
2026-10-22 22:18:11 +08:00

28 lines
2.4 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { z } from "zod";
import { transaction } from "./db";
import { works } from "../content/registry";
export const postInput=z.object({
slug:z.string().regex(/^[a-z0-9][a-z0-9-]{0,119}$/).optional(),
kind:z.enum(["image","video","audio","article","motion"]),title:z.string().trim().min(1).max(200),
assets:z.array(z.object({id:z.uuid(),role:z.enum(["content","caption"]).default("content")})).max(100).default([]),
text:z.string().trim().max(100000).optional(),tags:z.array(z.uuid()).max(50).default([]),
coverAssetId:z.uuid().optional(),pageKey:z.string().max(120).optional(),
});
export async function createPost(input:z.infer<typeof postInput>) {
const id=randomUUID(); const slug=input.slug||id;
if(["article","motion"].includes(input.kind)&&(!input.pageKey||works[input.pageKey]?.kind!==input.kind)) throw new Error("Register the coded page before creating its post");
return transaction(async client=>{
const ids=[...new Set(input.assets.map(a=>a.id))];
const assets=ids.length?(await client.query('SELECT id,kind FROM assets WHERE id=ANY($1::uuid[])',[ids])).rows:[];
if(assets.length!==ids.length) throw new Error("Referenced asset does not exist");
if(["image","video","audio"].includes(input.kind)&&!input.assets.some(link=>link.role==='content'&&assets.some(a=>a.id===link.id&&a.kind===input.kind))) throw new Error("A media post needs matching content");
if(input.coverAssetId && !(await client.query("SELECT 1 FROM assets WHERE id=$1 AND kind='image'",[input.coverAssetId])).rowCount) throw new Error("Cover must reference an image");
await client.query('INSERT INTO posts(id,slug,kind,title,page_key,cover_asset_id) VALUES($1,$2,$3,$4,$5,$6)',[id,slug,input.kind,input.title,input.pageKey||null,input.coverAssetId||null]);
for(const [position,a] of input.assets.entries()) await client.query('INSERT INTO post_assets(post_id,asset_id,position,role) VALUES($1,$2,$3,$4)',[id,a.id,position,a.role]);
if(input.text){const textId=randomUUID();await client.query("INSERT INTO assets(id,kind,body) VALUES($1,'text',$2)",[textId,input.text]);await client.query("INSERT INTO post_assets(post_id,asset_id,position,role) VALUES($1,$2,$3,'caption')",[id,textId,input.assets.length]);}
for(const tag of new Set(input.tags)) await client.query('INSERT INTO post_tags(post_id,tag_id) VALUES($1,$2)',[id,tag]);
return {id,slug};
});
}