mirror of
https://github.com/jordienr/zenblog.git
synced 2026-08-24 10:14:46 -05:00
feat(authors): migrate author data and relations to api
This commit is contained in:
@@ -7,19 +7,24 @@ import { handle } from "hono/vercel";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { createClient } from "@/lib/server/supabase";
|
||||
import {
|
||||
addPostAuthor,
|
||||
createBlogCategory,
|
||||
createBlogTag,
|
||||
createDb,
|
||||
createUserBlog,
|
||||
deleteBlogAuthor,
|
||||
deleteBlogCategory,
|
||||
deleteBlogTag,
|
||||
deleteUserBlog,
|
||||
getUserBlogById,
|
||||
listBlogAuthors,
|
||||
listBlogCategories,
|
||||
listBlogCategoriesWithPostCount,
|
||||
listBlogTags,
|
||||
listBlogTagUsageCounts,
|
||||
listPostAuthors,
|
||||
listUserBlogs,
|
||||
removePostAuthor,
|
||||
updateBlogCategory,
|
||||
updateBlogTag,
|
||||
updateUserBlog,
|
||||
@@ -558,6 +563,90 @@ const api = new Hono()
|
||||
|
||||
return c.json({ ok: !!deleted }, 200);
|
||||
})
|
||||
.get("/blogs/:blog_id/authors", async (c) => {
|
||||
const blogId = c.req.param("blog_id");
|
||||
const { user } = await getUser();
|
||||
|
||||
if (!user?.id || !(await getBlogOwnership(blogId, user.id))) {
|
||||
return c.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const db = createDb();
|
||||
const authors = await listBlogAuthors(db, blogId);
|
||||
|
||||
return c.json(authors, 200);
|
||||
})
|
||||
.delete("/blogs/:blog_id/authors/:author_id", async (c) => {
|
||||
const blogId = c.req.param("blog_id");
|
||||
const authorId = Number(c.req.param("author_id"));
|
||||
const { user } = await getUser();
|
||||
|
||||
if (!user?.id || !(await getBlogOwnership(blogId, user.id))) {
|
||||
return c.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const db = createDb();
|
||||
const deleted = await deleteBlogAuthor(db, { blogId, authorId });
|
||||
|
||||
return c.json({ ok: !!deleted }, 200);
|
||||
})
|
||||
.get("/blogs/:blog_id/posts/:post_id/authors", async (c) => {
|
||||
const blogId = c.req.param("blog_id");
|
||||
const postId = c.req.param("post_id");
|
||||
const { user } = await getUser();
|
||||
|
||||
if (!user?.id || !(await getBlogOwnership(blogId, user.id))) {
|
||||
return c.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const db = createDb();
|
||||
const authors = await listPostAuthors(db, { blogId, postId });
|
||||
|
||||
return c.json(authors, 200);
|
||||
})
|
||||
.post(
|
||||
"/blogs/:blog_id/posts/:post_id/authors",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
author_id: z.number(),
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const blogId = c.req.param("blog_id");
|
||||
const postId = c.req.param("post_id");
|
||||
const { user } = await getUser();
|
||||
|
||||
if (!user?.id || !(await getBlogOwnership(blogId, user.id))) {
|
||||
return c.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const payload = await c.req.json();
|
||||
const db = createDb();
|
||||
const relation = await addPostAuthor(db, {
|
||||
blogId,
|
||||
postId,
|
||||
authorId: payload.author_id,
|
||||
});
|
||||
|
||||
return c.json(relation, 200);
|
||||
}
|
||||
)
|
||||
.delete("/blogs/:blog_id/posts/:post_id/authors/:author_id", async (c) => {
|
||||
const blogId = c.req.param("blog_id");
|
||||
const postId = c.req.param("post_id");
|
||||
const authorId = Number(c.req.param("author_id"));
|
||||
const { user } = await getUser();
|
||||
|
||||
if (!user?.id || !(await getBlogOwnership(blogId, user.id))) {
|
||||
return c.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const db = createDb();
|
||||
const deleted = await removePostAuthor(db, { postId, authorId });
|
||||
|
||||
return c.json({ ok: !!deleted }, 200);
|
||||
})
|
||||
.get(
|
||||
"/blogs/:blog_id/usage",
|
||||
zValidator(
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { createSupabaseBrowserClient } from "@/lib/supabase";
|
||||
import { Database } from "@/types/supabase";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { API } from "app/utils/api-client";
|
||||
|
||||
const sb = createSupabaseBrowserClient();
|
||||
|
||||
export type Author = Omit<
|
||||
Database["public"]["Tables"]["authors"]["Row"],
|
||||
"id" | "created_at" | "updated_at" | "blog_id"
|
||||
@@ -19,11 +16,14 @@ export function useAuthorsQuery() {
|
||||
return useQuery({
|
||||
queryKey: keys.authors,
|
||||
queryFn: async () => {
|
||||
const { data } = await sb
|
||||
.from("authors")
|
||||
.select("id, slug, name, bio, twitter, website")
|
||||
.throwOnError();
|
||||
return data;
|
||||
return [] as Array<{
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
bio: string | null;
|
||||
twitter: string | null;
|
||||
website: string | null;
|
||||
}>;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -32,16 +32,24 @@ export function useAuthors({ blogId }: { blogId: string }) {
|
||||
return useQuery({
|
||||
queryKey: keys.authors,
|
||||
queryFn: async () => {
|
||||
const { data, error } = await sb
|
||||
.from("authors")
|
||||
.select("id, slug, name, created_at, bio, twitter, website, image_url")
|
||||
.eq("blog_id", blogId)
|
||||
.throwOnError();
|
||||
const res = await api.v2.blogs[":blog_id"].authors.$get({
|
||||
param: { blog_id: blogId },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to load authors");
|
||||
}
|
||||
return data;
|
||||
|
||||
return (await res.json()) as Array<{
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
bio: string | null;
|
||||
twitter: string | null;
|
||||
website: string | null;
|
||||
image_url: string | null;
|
||||
}>;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -63,23 +71,21 @@ export function useCreateAuthor() {
|
||||
|
||||
export function useDeleteAuthorMutation(blogId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
const supa = createSupabaseBrowserClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (authorId: number) => {
|
||||
const res = await supa
|
||||
.from("authors")
|
||||
.delete()
|
||||
.eq("blog_id", blogId)
|
||||
.eq("id", authorId)
|
||||
.throwOnError();
|
||||
const res = await api.v2.blogs[":blog_id"].authors[":author_id"].$delete({
|
||||
param: {
|
||||
blog_id: blogId,
|
||||
author_id: String(authorId),
|
||||
},
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
console.log(res.error);
|
||||
throw new Error(res.error.message);
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to delete author");
|
||||
}
|
||||
|
||||
return res;
|
||||
return { error: null, data: await res.json() };
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -132,20 +138,24 @@ export function usePostAuthorsQuery({
|
||||
queryKey: keys.postAuthors,
|
||||
enabled: !!postId && !!blogId,
|
||||
queryFn: async () => {
|
||||
const { data } = await sb
|
||||
.from("post_authors")
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
post_id,
|
||||
author_id,
|
||||
author:authors(name, slug, image_url)
|
||||
`
|
||||
)
|
||||
.eq("post_id", postId)
|
||||
.eq("blog_id", blogId);
|
||||
const res = await api.v2.blogs[":blog_id"].posts[":post_id"].authors.$get({
|
||||
param: { blog_id: blogId, post_id: postId },
|
||||
});
|
||||
|
||||
return data;
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to load post authors");
|
||||
}
|
||||
|
||||
return (await res.json()) as Array<{
|
||||
id: number;
|
||||
post_id: string;
|
||||
author_id: number;
|
||||
author: {
|
||||
name: string;
|
||||
slug: string;
|
||||
image_url: string | null;
|
||||
};
|
||||
}>;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -159,12 +169,21 @@ export function useAddPostAuthorMutation() {
|
||||
author_id: number;
|
||||
blog_id: string;
|
||||
}) => {
|
||||
const res = await sb.from("post_authors").insert({
|
||||
post_id: payload.post_id,
|
||||
author_id: payload.author_id,
|
||||
blog_id: payload.blog_id,
|
||||
const res = await api.v2.blogs[":blog_id"].posts[":post_id"].authors.$post({
|
||||
param: {
|
||||
blog_id: payload.blog_id,
|
||||
post_id: payload.post_id,
|
||||
},
|
||||
json: {
|
||||
author_id: payload.author_id,
|
||||
},
|
||||
});
|
||||
return res;
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to add post author");
|
||||
}
|
||||
|
||||
return { error: null, data: await res.json() };
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -179,12 +198,22 @@ export function useRemovePostAuthorMutation() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payload: { post_id: string; author_id: number }) => {
|
||||
const res = await sb
|
||||
.from("post_authors")
|
||||
.delete()
|
||||
.eq("post_id", payload.post_id)
|
||||
.eq("author_id", payload.author_id);
|
||||
return res;
|
||||
const blogId = window.location.pathname.split("/")[2] || "";
|
||||
const res = await api.v2.blogs[":blog_id"].posts[":post_id"].authors[
|
||||
":author_id"
|
||||
].$delete({
|
||||
param: {
|
||||
blog_id: blogId,
|
||||
post_id: payload.post_id,
|
||||
author_id: String(payload.author_id),
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to remove post author");
|
||||
}
|
||||
|
||||
return { error: null, data: await res.json() };
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { and, asc, count, desc, eq } from "drizzle-orm";
|
||||
import type { DbClient } from "./client";
|
||||
import {
|
||||
authors,
|
||||
blogs,
|
||||
categories,
|
||||
categoryPostCount,
|
||||
postAuthors,
|
||||
tags,
|
||||
tagUsageCountV2,
|
||||
} from "./schema";
|
||||
@@ -285,3 +287,89 @@ export async function countBlogTags(db: DbClient, blogId: string) {
|
||||
|
||||
return rows[0]?.value || 0;
|
||||
}
|
||||
|
||||
export async function listBlogAuthors(db: DbClient, blogId: string) {
|
||||
return db
|
||||
.select({
|
||||
id: authors.id,
|
||||
slug: authors.slug,
|
||||
name: authors.name,
|
||||
created_at: authors.createdAt,
|
||||
bio: authors.bio,
|
||||
twitter: authors.twitter,
|
||||
website: authors.website,
|
||||
image_url: authors.imageUrl,
|
||||
})
|
||||
.from(authors)
|
||||
.where(eq(authors.blogId, blogId))
|
||||
.orderBy(asc(authors.createdAt));
|
||||
}
|
||||
|
||||
export async function deleteBlogAuthor(
|
||||
db: DbClient,
|
||||
input: { blogId: string; authorId: number }
|
||||
) {
|
||||
const rows = await db
|
||||
.delete(authors)
|
||||
.where(and(eq(authors.blogId, input.blogId), eq(authors.id, input.authorId)))
|
||||
.returning({ id: authors.id });
|
||||
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function listPostAuthors(
|
||||
db: DbClient,
|
||||
input: { blogId: string; postId: string }
|
||||
) {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: postAuthors.id,
|
||||
post_id: postAuthors.postId,
|
||||
author_id: postAuthors.authorId,
|
||||
author_name: authors.name,
|
||||
author_slug: authors.slug,
|
||||
author_image_url: authors.imageUrl,
|
||||
})
|
||||
.from(postAuthors)
|
||||
.innerJoin(authors, eq(postAuthors.authorId, authors.id))
|
||||
.where(and(eq(postAuthors.blogId, input.blogId), eq(postAuthors.postId, input.postId)));
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
post_id: row.post_id,
|
||||
author_id: row.author_id,
|
||||
author: {
|
||||
name: row.author_name,
|
||||
slug: row.author_slug,
|
||||
image_url: row.author_image_url,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export async function addPostAuthor(
|
||||
db: DbClient,
|
||||
input: { blogId: string; postId: string; authorId: number }
|
||||
) {
|
||||
const rows = await db
|
||||
.insert(postAuthors)
|
||||
.values({
|
||||
blogId: input.blogId,
|
||||
postId: input.postId,
|
||||
authorId: input.authorId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function removePostAuthor(
|
||||
db: DbClient,
|
||||
input: { postId: string; authorId: number }
|
||||
) {
|
||||
const rows = await db
|
||||
.delete(postAuthors)
|
||||
.where(and(eq(postAuthors.postId, input.postId), eq(postAuthors.authorId, input.authorId)))
|
||||
.returning({ id: postAuthors.id });
|
||||
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user