Skip to content

Commit

Permalink
Fix bySlug version retrieval, add isSlugTaken check
Browse files Browse the repository at this point in the history
  • Loading branch information
vakila committed Aug 27, 2024
1 parent 02d5d6a commit 889e6b5
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 41 deletions.
67 changes: 58 additions & 9 deletions convex/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const getById = query({
})

type PostAugmented = Doc<'posts'> & {
publicVersion?: Doc<'versions'>;
draft?: Doc<'versions'>;
author?: Doc<'users'>;
};
Expand All @@ -96,20 +97,25 @@ export const getBySlug = query({
withAuthor: v.optional(v.boolean())
},
handler: async (ctx, args) => {
const versionsBySlug = ctx.db.query('versions')
.withIndex('by_slug', q => q.eq('slug', args.slug))

const publicVersion = await versionsBySlug
.filter(q => q.eq(q.field('published'), true))
.order('desc')
.first();

let post = await ctx.db.query('posts')
.withIndex('by_slug', q => q.eq('slug', args.slug))
.unique();
.order('desc')
.first();

if (!post) {
// The slug for this post may have changed
// try searching for this slug in old versions
const version = await ctx.db.query('versions')
.withIndex('by_slug', q => q.eq('slug', args.slug))
.first();

if (version) {
if (publicVersion) {
// The slug is outdated, lookup the postId
post = await ctx.db.get(version.postId);
post = await ctx.db.get(publicVersion.postId);
}
}
if (!post) return null; // The slug is unknown
Expand All @@ -121,7 +127,10 @@ export const getBySlug = query({
return null;
}

const result: PostAugmented = { ...post };
const result: PostAugmented = post;
if (publicVersion) {
result.publicVersion = publicVersion;
}

if (args.withDraft) {
// Find the most recent unpublished draft, if any,
Expand All @@ -133,7 +142,8 @@ export const getBySlug = query({
post.updateTime || post._creationTime),
q.eq(q.field('published'), false)
))
.first()
.order('desc')
.first();
if (draft) {
result.draft = draft;
}
Expand All @@ -150,3 +160,42 @@ export const getBySlug = query({
}
});


export const isSlugTaken = query({
args: {
slug: v.string(),
postId: v.optional(v.id('posts'))
},
handler: async (ctx, args) => {
const { slug, postId } = args;

// Find any existing post(s) with this slug and flag
// any whose postId doesn't match the one given
const posts = await ctx.db.query('posts')
.withIndex('by_slug', q => q.eq('slug', slug))
.collect();
const badPostIds = new Set(
posts.filter(p => p._id !== postId).map(p => p._id)
);

// It's possible that the slug is no longer in use on any posts,
// but had previously been used in an old version of another post.
// Collect all version(s) with this slug...
const versions = await ctx.db.query('versions')
.withIndex('by_slug', q => q.eq('slug', slug))
.collect();
// ...and flag any whose postId doesn't match the one given.
const badVersions = versions.filter(v => v.postId !== postId);
badVersions.map(v => badPostIds.add(v.postId))


if (badPostIds.size > 0) {
// This slug is unavailable (already/previously in use)
const msg = `Slug "${slug}" is unavailable, used on post(s) ${Array.from(badPostIds).toString()}`;
console.error(msg);
return msg;
} else {
return false;
}
}
})
6 changes: 5 additions & 1 deletion convex/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mutation, query, type QueryCtx } from "./_generated/server";
import { versions } from "./schema";
import type { Doc } from "./_generated/dataModel";
import { crud } from "convex-helpers/server";
import { create as createPost } from "./posts";
import { create as createPost, isSlugTaken } from "./posts";

export const {
create,
Expand All @@ -19,6 +19,10 @@ export const saveDraft = mutation({
},
handler: async (ctx, args) => {
const { postId, editorId, ...data } = args;
const slugTaken = await isSlugTaken(ctx,
{ slug: data.slug, postId: postId || undefined });
if (slugTaken) throw new Error(slugTaken);

let id = postId;
if (!id) {
const newPost = await createPost(ctx, data);
Expand Down
64 changes: 39 additions & 25 deletions src/components/Blog/Edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,27 @@ export function EditablePost({ version }: { version: Doc<'versions'> | null }) {
const publishPost = useMutation(api.posts.publish);

const zodSchema = z.object(versionsZod);
const defaultValues = version || versionDefaults;
type Schema = z.infer<typeof zodSchema>;

const defaultValues = version || versionDefaults;

const form = useForm<z.infer<typeof zodSchema>>({
const form = useForm<Schema>({
defaultValues,
resolver: zodResolver(zodSchema)
});

const { getValues, setValue } = form;
const { getValues, setValue, setError } = form;

const slugTaken = useQuery(api.posts.isSlugTaken, {
slug: getValues('slug'),
postId: getValues('postId')
});

useEffect(() => {
if (slugTaken) {
setError('slug', { message: 'Slug already in use' });
}
}, [slugTaken, setError])

useEffect(() => {
if (viewer) {
Expand Down Expand Up @@ -151,11 +163,11 @@ export function EditablePost({ version }: { version: Doc<'versions'> | null }) {

<div className={`flex gap-2 items-center`}>
<Button variant="secondary" type="reset"
onClick={() => {
const cancelled = !isDirty;
form.reset(defaultValues);
if (cancelled) navigate(-1);
}}>
onClick={() =>
isDirty ?
form.reset(defaultValues)
: navigate(-1)
}>
{isDirty ? 'Reset' : 'Cancel'}
</Button>

Expand All @@ -174,23 +186,25 @@ export function EditablePost({ version }: { version: Doc<'versions'> | null }) {
</Button>
</div>
</div>
</Toolbar>

{previewing
? (<div className="my-8" >
<DisplayPost post={{ ...version, ...form.getValues() } as PostOrVersion} />
</div>)
: <Form {...form}>
<form>
<div className="container">
<TextField name="title" form={form} />
<TextField name="slug" form={form} />
<TextField name="imageUrl" form={form} />
<MarkdownField name="summary" rows={3} form={form} />
<MarkdownField name="content" rows={10} form={form} />
</div>
</form>
</Form>}
</Toolbar >

{
previewing
? (<div className="my-8" >
<DisplayPost post={{ ...version, ...form.getValues() } as PostOrVersion} />
</div>)
: <Form {...form}>
<form>
<div className="container">
<TextField name="title" form={form} />
<TextField name="slug" form={form} />
<TextField name="imageUrl" form={form} />
<MarkdownField name="summary" rows={3} form={form} />
<MarkdownField name="content" rows={10} form={form} />
</div>
</form>
</Form>
}

</>)

Expand Down
15 changes: 12 additions & 3 deletions src/pages/__layout/$slug.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useQuery } from "convex/react";
import { Link, useParams } from "react-router-dom";
import { Link, useNavigate, useParams } from "react-router-dom";
import { api } from "../../../convex/_generated/api";
import { DisplayPost, type PostOrVersion } from "@/components/Blog/Post";
import { Toolbar } from "@/components/Toolbar";
import { Message } from "@/components/PageTitle";
import { Button } from "@/components/ui/button";
import { Pencil1Icon } from "@radix-ui/react-icons";
import { useEffect } from "react";


export default function PostPage() {

const { slug } = useParams();
const navigate = useNavigate();
const viewer = useQuery(api.users.viewer);
const loading = slug === undefined || viewer === undefined;

Expand All @@ -19,16 +21,23 @@ export default function PostPage() {
withAuthor: true,
withDraft: !!viewer
});
const publicVersion = post?.publicVersion?._id;
const draftVersion = post?.draft && post.draft._id;
const version = draftVersion || publicVersion;

useEffect(() => {
if (post?.slug && post.slug !== slug)
navigate(`/${post.slug}`);
}, [slug, post, navigate])

if (post === undefined) return <Message text="Loading..." />
if (post == null) return <Message text="Not found" />
return <>
<Toolbar >
<div className="flex grow justify-end items-center">
{post &&
<Link to={`/${post.slug}/edit${draftVersion
? `?v=${draftVersion}`
<Link to={`/${post.slug}/edit${version
? `?v=${version}`
: ''}`}
className={`flex gap-2 items-center`} >
<Button>
Expand Down
10 changes: 7 additions & 3 deletions src/pages/__layout/__authed/$slug.edit.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQuery } from "convex/react";
import { useParams, useSearchParams } from "react-router-dom";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { api } from "../../../../convex/_generated/api";
import { EditablePost } from "@/components/Blog/Edit";
import type { Id } from "../../../../convex/_generated/dataModel";
Expand All @@ -12,7 +12,9 @@ export function Message({ text }: { text: string }) {

export default function EditPostPage() {

const [searchParams, _] = useSearchParams()
const { slug } = useParams();
const [searchParams, _] = useSearchParams();
const navigate = useNavigate();

// If we navigated here from a link on the site,
// the searchParams should include a versionId
Expand All @@ -23,7 +25,6 @@ export default function EditPostPage() {

// If we navigated here manually or the versionId
// is otherwise missing, fall back to lookup by slug
const { slug } = useParams();
const postBySlug = useQuery(api.posts.getBySlug, versionId
? 'skip'
: {
Expand All @@ -36,6 +37,9 @@ export default function EditPostPage() {

if (post === undefined) return <Message text="Loading..." />;
if (post == null) return <Message text="Not found" />;
if (post.slug !== slug) {
return navigate(`/${post.slug}/edit`);
}
return <EditablePost version={post} />;
}

0 comments on commit 889e6b5

Please sign in to comment.