Adding tags

Hey there, I'm trying to make tags for my project, basically every post has tags and then you can filter through those tags which will show all posts with that tag. Though every time I try and filter through it I get nothing back, It's probably a Syntax issue but I was wondering if I could check if an array includes something ? Here's my code:
schema.ts:
posts: defineTable({
title: v.string(),
userId: v.string(),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
author: v.optional(v.string()),
likes: v.number(),
likeIds: v.optional(v.array(v.string())),
tags: v.array(v.string())
}).index("by_tagName", ["tags"]),
schema.ts:
posts: defineTable({
title: v.string(),
userId: v.string(),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
author: v.optional(v.string()),
likes: v.number(),
likeIds: v.optional(v.array(v.string())),
tags: v.array(v.string())
}).index("by_tagName", ["tags"]),
posts.ts
export const postsByTagName = query({
args: {
tagName: v.string(),
paginationOpts: paginationOptsValidator
},
handler: async (ctx, args) => {
return await ctx.db
.query("posts")
.filter((q) => q.eq(q.field("tags"), args.tagName))
.order("desc")
.paginate(args.paginationOpts);
},
});
posts.ts
export const postsByTagName = query({
args: {
tagName: v.string(),
paginationOpts: paginationOptsValidator
},
handler: async (ctx, args) => {
return await ctx.db
.query("posts")
.filter((q) => q.eq(q.field("tags"), args.tagName))
.order("desc")
.paginate(args.paginationOpts);
},
});
I'm using Next 14 btw and this is the code for it:
const params = useParams<{ tagName: string }>();
const tagName = params.tagName;

const {
results: blogs,
status,
loadMore,
isLoading,
} = usePaginatedQuery(
api.posts.tagPagePosts,
{ tagName },
{ initialNumItems: 10 }
);
const params = useParams<{ tagName: string }>();
const tagName = params.tagName;

const {
results: blogs,
status,
loadMore,
isLoading,
} = usePaginatedQuery(
api.posts.tagPagePosts,
{ tagName },
{ initialNumItems: 10 }
);
So I'm wondering if there are any solutions to this. Thanks in advance and I apologize if this isn't the right channel for this.
15 Replies
Hārūn | هارون
Result from console
console.log("Tag Name:", tagName);
console.log("Params:", params);
console.log("Blogs:", blogs);
Tag Name: typescript
page.tsx:22 Params: {tagName: 'typescript'}
page.tsx:35 Blogs: []
console.log("Tag Name:", tagName);
console.log("Params:", params);
console.log("Blogs:", blogs);
Tag Name: typescript
page.tsx:22 Params: {tagName: 'typescript'}
page.tsx:35 Blogs: []
ian
ian13mo ago
https://discord.com/channels/1019350475847499849/1138800227416014951 has some good info and if you are willing to have your tags be a space-separated string, https://discord.com/channels/1019350475847499849/1207072218744492072 would be a good option too
Hārūn | هارون
Alright , so something like this ?
posts: defineTable({
title: v.string(),
userId: v.string(),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
author: v.optional(v.string()),
likes: v.number(),
likeIds: v.optional(v.array(v.string())),
tags: v.array(v.string()),
}),
postTags: defineTable({
postId: v.id('posts'),
tagId: v.id('tags'),
}).index('postId_tags', ['postId', 'tagId']),
posts: defineTable({
title: v.string(),
userId: v.string(),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
author: v.optional(v.string()),
likes: v.number(),
likeIds: v.optional(v.array(v.string())),
tags: v.array(v.string()),
}),
postTags: defineTable({
postId: v.id('posts'),
tagId: v.id('tags'),
}).index('postId_tags', ['postId', 'tagId']),
export const createPost = internalMutation({
args: {
title: v.string(),
userId: v.id("users"),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
likes: v.number(),
likeIds: v.array(v.string()),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);

if (!user) {
throw new ConvexError("User not found.");
}

const id = await ctx.db.insert("posts", {
title: args.title,
userId: user._id,
image: args.image,
profileImage: user.profileImage,
description: args.description,
author: user.name,
likes: 0,
likeIds: [],
tags: args.tags,
});

return id;
},
});
export const createPost = internalMutation({
args: {
title: v.string(),
userId: v.id("users"),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
likes: v.number(),
likeIds: v.array(v.string()),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);

if (!user) {
throw new ConvexError("User not found.");
}

const id = await ctx.db.insert("posts", {
title: args.title,
userId: user._id,
image: args.image,
profileImage: user.profileImage,
description: args.description,
author: user.name,
likes: 0,
likeIds: [],
tags: args.tags,
});

return id;
},
});
export const createPostAction = authAction({
args: {
title: v.string(),
image: v.string(),
description: v.string(),
tags: v.array(v.string())
},
handler: async (ctx, args) => {
const postId: Id<"posts"> = await ctx.runMutation(
internal.posts.createPost,
{
image: args.image,
title: args.title,
description: args.description,
userId: ctx.user._id,
likes: 0,
likeIds: [],
tags: args.tags
}
);

return postId;
},
});
export const createPostAction = authAction({
args: {
title: v.string(),
image: v.string(),
description: v.string(),
tags: v.array(v.string())
},
handler: async (ctx, args) => {
const postId: Id<"posts"> = await ctx.runMutation(
internal.posts.createPost,
{
image: args.image,
title: args.title,
description: args.description,
userId: ctx.user._id,
likes: 0,
likeIds: [],
tags: args.tags
}
);

return postId;
},
});
@ian Or would this still be incorrect if you know First time doing something like this so, sorry if it's obvious to make
ian
ian13mo ago
Take out the tags array and add another table called “tags” that matches your ID in the postTags table. Create a tag document for every name you want, then for every post, add a document to postTags with the post’s ID and tag’s ID
Hārūn | هارون
posts: defineTable({
title: v.string(),
userId: v.string(),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
author: v.optional(v.string()),
likes: v.number(),
likeIds: v.optional(v.array(v.string())),
}),
tags: defineTable({
tagId: v.string(),
name: v.string(),
}),
postTags: defineTable({
postId: v.id("posts"),
tagId: v.id("tags"),
}).index("postId_tags", ["postId", "tagId"]),
posts: defineTable({
title: v.string(),
userId: v.string(),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
author: v.optional(v.string()),
likes: v.number(),
likeIds: v.optional(v.array(v.string())),
}),
tags: defineTable({
tagId: v.string(),
name: v.string(),
}),
postTags: defineTable({
postId: v.id("posts"),
tagId: v.id("tags"),
}).index("postId_tags", ["postId", "tagId"]),
Something like this? And what would I need to change here? I'm assuming a forloop and I've tried but no success so far:
export const createPost = internalMutation({
args: {
title: v.string(),
userId: v.id("users"),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
likes: v.number(),
likeIds: v.array(v.string()),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);

if (!user) {
throw new ConvexError("User not found.");
}

const id = await ctx.db.insert("posts", {
title: args.title,
userId: user._id,
image: args.image,
profileImage: user.profileImage,
description: args.description,
author: user.name,
likes: 0,
likeIds: [],
});

return id;
},
});
export const createPost = internalMutation({
args: {
title: v.string(),
userId: v.id("users"),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
likes: v.number(),
likeIds: v.array(v.string()),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);

if (!user) {
throw new ConvexError("User not found.");
}

const id = await ctx.db.insert("posts", {
title: args.title,
userId: user._id,
image: args.image,
profileImage: user.profileImage,
description: args.description,
author: user.name,
likes: 0,
likeIds: [],
});

return id;
},
});
export const createPostAction = authAction({
args: {
title: v.string(),
image: v.string(),
description: v.string(),
tags: v.array(v.string())
},
handler: async (ctx, args) => {
const postId: Id<"posts"> = await ctx.runMutation(
internal.posts.createPost,
{
image: args.image,
title: args.title,
description: args.description,
userId: ctx.user._id,
likes: 0,
likeIds: [],
tags: args.tags
}
);

return postId;
},
});
export const createPostAction = authAction({
args: {
title: v.string(),
image: v.string(),
description: v.string(),
tags: v.array(v.string())
},
handler: async (ctx, args) => {
const postId: Id<"posts"> = await ctx.runMutation(
internal.posts.createPost,
{
image: args.image,
title: args.title,
description: args.description,
userId: ctx.user._id,
likes: 0,
likeIds: [],
tags: args.tags
}
);

return postId;
},
});
My post action as well
ian
ian13mo ago
I don't see you using the args.tags yet, and I think tagId: v.string() isn't needed in the tags table if you can pass up tags by their ID:
export const createPost = authMutation({
args: {
title: v.string(),
userId: v.id("users"),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
likes: v.number(),
likeIds: v.array(v.string()),
tags: v.array(v.id("tags")),
},
handler: async (ctx, args) => {
//... like before
const postId = await ctx.db.insert("posts", {
//... like before
});
for (const tagId of args.tags) {
const existing = await ctx.db.query("authorProfiles")
.withIndex("postId_tags", q =>
q.eq("postId", postId).eq("tagId", tagId))
.unique();
if (existing) continue;
await ctx.db.insert("postTags", { postId, tagId });
}
return id;
},
});
export const createPost = authMutation({
args: {
title: v.string(),
userId: v.id("users"),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
likes: v.number(),
likeIds: v.array(v.string()),
tags: v.array(v.id("tags")),
},
handler: async (ctx, args) => {
//... like before
const postId = await ctx.db.insert("posts", {
//... like before
});
for (const tagId of args.tags) {
const existing = await ctx.db.query("authorProfiles")
.withIndex("postId_tags", q =>
q.eq("postId", postId).eq("tagId", tagId))
.unique();
if (existing) continue;
await ctx.db.insert("postTags", { postId, tagId });
}
return id;
},
});
otherwise if you pass up the names, something like:
tags: defineTable({
name: v.string(),
}).index("name", ["name"]),
tags: defineTable({
name: v.string(),
}).index("name", ["name"]),
...
export const createPost = authMutation({
args: {
title: v.string(),
userId: v.id("users"),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
likes: v.number(),
likeIds: v.array(v.string()),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
//... like before
const postId = await ctx.db.insert("posts", {
//... like before
});
for (const name of args.tags) {
const tag = await ctx.db.query("tags").withIndex("name", q => q.eq("name", name)).unique();
const tagId = tag._id || await ctx.db.insert("tags", {name});
const existing = await ctx.db.query("authorProfiles")
.withIndex("postId_tags", q =>
q.eq("postId", postId).eq("tagId", tagId))
.unique();
if (existing) continue;
await ctx.db.insert("postTags", { postId, tagId });
}
return id;
},
});
export const createPost = authMutation({
args: {
title: v.string(),
userId: v.id("users"),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
likes: v.number(),
likeIds: v.array(v.string()),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
//... like before
const postId = await ctx.db.insert("posts", {
//... like before
});
for (const name of args.tags) {
const tag = await ctx.db.query("tags").withIndex("name", q => q.eq("name", name)).unique();
const tagId = tag._id || await ctx.db.insert("tags", {name});
const existing = await ctx.db.query("authorProfiles")
.withIndex("postId_tags", q =>
q.eq("postId", postId).eq("tagId", tagId))
.unique();
if (existing) continue;
await ctx.db.insert("postTags", { postId, tagId });
}
return id;
},
});
Just wrote that in Discord, so apologies for any syntax snafoos
Hārūn | هارون
Hey there, I tried this out:
export const createPost = internalMutation({
args: {
title: v.string(),
userId: v.id("users"),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
likes: v.number(),
likeIds: v.array(v.string()),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);

if (!user) {
throw new ConvexError("User not found.");
}

const postId = await ctx.db.insert("posts", {
title: args.title,
userId: user._id,
image: args.image,
profileImage: user.profileImage,
description: args.description,
author: user.name,
likes: 0,
likeIds: [],
});

for (const name of args.tags) {
const tag = await ctx.db
.query("tags")
.withIndex("name", (q) => q.eq("name", name))
.unique();

// if (!tag) {
// throw new ConvexError("Tag cannot be empty.");
// }

const tagId = tag!._id || (await ctx.db.insert("tags", { name }));
const existing = await ctx.db
.query("postTags")
.withIndex("postId_tags", (q) =>
q.eq("postId", postId).eq("tagId", tagId)
)
.unique();
if (existing) continue;
await ctx.db.insert("postTags", { postId, tagId });
}

return postId;
},
});
export const createPost = internalMutation({
args: {
title: v.string(),
userId: v.id("users"),
image: v.string(),
profileImage: v.optional(v.string()),
description: v.string(),
likes: v.number(),
likeIds: v.array(v.string()),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);

if (!user) {
throw new ConvexError("User not found.");
}

const postId = await ctx.db.insert("posts", {
title: args.title,
userId: user._id,
image: args.image,
profileImage: user.profileImage,
description: args.description,
author: user.name,
likes: 0,
likeIds: [],
});

for (const name of args.tags) {
const tag = await ctx.db
.query("tags")
.withIndex("name", (q) => q.eq("name", name))
.unique();

// if (!tag) {
// throw new ConvexError("Tag cannot be empty.");
// }

const tagId = tag!._id || (await ctx.db.insert("tags", { name }));
const existing = await ctx.db
.query("postTags")
.withIndex("postId_tags", (q) =>
q.eq("postId", postId).eq("tagId", tagId)
)
.unique();
if (existing) continue;
await ctx.db.insert("postTags", { postId, tagId });
}

return postId;
},
});
But tags aren't being saved in the tags table nor the postTags table despite me passing in them in my form. They do however get saved in the posts table which is the same as before.
No description
No description
No description
No description
Hārūn | هارون
export const createPostAction = authAction({
args: {
title: v.string(),
image: v.string(),
description: v.string(),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
const postId: Id<"posts"> = await ctx.runMutation(
internal.posts.createPost,
{
image: args.image,
title: args.title,
description: args.description,
userId: ctx.user._id,
likes: 0,
likeIds: [],
tags: args.tags,
}
);

return postId;
},
});
export const createPostAction = authAction({
args: {
title: v.string(),
image: v.string(),
description: v.string(),
tags: v.array(v.string()),
},
handler: async (ctx, args) => {
const postId: Id<"posts"> = await ctx.runMutation(
internal.posts.createPost,
{
image: args.image,
title: args.title,
description: args.description,
userId: ctx.user._id,
likes: 0,
likeIds: [],
tags: args.tags,
}
);

return postId;
},
});
And this is my createPostAction @ian If you could look into this it'd be super helpful as I cannot figure out what's wrong
ian
ian13mo ago
I think your code hasn’t been updated if it’s still saving the tags in the table. You probably need to clear that table since it has the tags values that don’t exist anymore And it should be tag?._id ||
Hārūn | هارون
I don't know what I changed but I can confirm that it works, I think it was this that caused the difference and it works! Just wanted to ask how I would filter now through these as the filter I've written doesn't work 😅 and was wondering if it's possible or just a syntax error:
export const getTagsForPost = query({
args: { postId: v.id("posts") },
handler: async (ctx, args) => {
const postTags = await ctx.db
.query("postTags")
.withIndex("postId_tags" ,(q) => q.eq("postId", args.postId))
.collect();

const tagIds = postTags.map((postTag) => postTag.tagId);
const tags = await ctx.db
.query("tags")
.take(5)
// .filter((q) => q.eq(q.field("_id"), tagIds))

return tags.map((tag) => tag.name);
},
});
export const getTagsForPost = query({
args: { postId: v.id("posts") },
handler: async (ctx, args) => {
const postTags = await ctx.db
.query("postTags")
.withIndex("postId_tags" ,(q) => q.eq("postId", args.postId))
.collect();

const tagIds = postTags.map((postTag) => postTag.tagId);
const tags = await ctx.db
.query("tags")
.take(5)
// .filter((q) => q.eq(q.field("_id"), tagIds))

return tags.map((tag) => tag.name);
},
});
using .take(5) at the moment until the filter works.
ian
ian13mo ago
I think what you want is to get all of the tags by the ids you now have There are good blog posts on stack about relationship structures with code examples. If you could drop a link here to the one, that is the most useful, I would appreciate it.
ian
ian13mo ago
Yes, thanks. In particular, what you've made with the postTags table is a "many to many" relationship with a "join" table.
Hārūn | هارون
export const getTagsForPost = query({
args: { postId: v.id("posts") },
handler: async (ctx, args) => {

const tags = await getManyVia(
ctx.db,
"postTags",
"tagId",
"postId_tags",
args.postId,
"postId"
);

return tags.map((tag) => tag?.name);
},
});
export const getTagsForPost = query({
args: { postId: v.id("posts") },
handler: async (ctx, args) => {

const tags = await getManyVia(
ctx.db,
"postTags",
"tagId",
"postId_tags",
args.postId,
"postId"
);

return tags.map((tag) => tag?.name);
},
});
This is what I was able to make , and it is functional from what I've noticed So now, if I wanted to get all posts by x tag name e.g. /tag/typescript , then I'd need a one to many , correct ?
ian
ian13mo ago
Nice! 👏

Did you find this page helpful?