[Convex Ents] Asymmetrical self-directed 1:many edges

Is that possible with Convex Ents? doesnt see it in the docs. Point is that I want a reply functionality. Which means that there is one message that replies to another (asymmetrical) but you can only reply to one message. But one message can be replied to by many messages.
21 Replies
lee
leeβ€’5mo ago
The docs say
Every edge has two "ends", each being an Ent. Those Ents can be stored in the same or in 2 different tables.
So it sounds like "self-directed" edges are possible. However, there is a slight problem: you probably don't want to require every message to be a reply to some other message (that would require infinite messages or a cycle of replies). You want an optional 1:many edge, which is not possible
Making the singular end of the 1:many edge optional is not supported for now.
https://github.com/xixixao/convex-ents/issues/5 To work around this, you could use foreign references directly, so each message has a replyTo: v.optional(v.id("messages"))
GitHub
1:many edges require single cardinality end to be required Β· Issue ...
This will require more configuration, especially for cascading deletes. We'll hold off on adding this until we get people asking for it.
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
Thank you so much. I will try the workaround with putting the id directly into the field
Matt Luo
Matt Luoβ€’5mo ago
I think the hard thing about putting the ID directly in the field is that to do joins, I don't think you can use Ents. My understanding is that you could use vanilla Convex to do the join - but then you'd include the table to Pick and you'd always use vanilla Convex for that table. Maybe there is a way to keep the table defined as an Ent, but still use vanilla Convex or convex-helpers' relationships, but I haven't tried it personally
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
Hmmm, I think about it and share my research
ampp
amppβ€’5mo ago
I would think you could use ents, couldn't you just do this within the loop of results via map? I think i'm unaware if any benefit exists using convex helpers. πŸ™ƒ I went with ents so early on i never had any complex relationships with vanilla
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
Hi, if I try to set up the field like this:
.field("replyTo", v.id("messages"), { optional: true })
.field("replyTo", v.id("messages"), { optional: true })
I get this error:
No overload matches this call.
Overload 1 of 4, '(field: "replyTo", validator: VId<Id<"messages">, "required">, options: { index: true; }): EntDefinition<VObject<{ content: string; type: string; deleted: boolean; replyTo: Id<"messages">; }, { ...; }, "required", "type" | ... 2 more ... | "replyTo">, { ...; }, {}, {}, {}>', gave the following error.
Object literal may only specify known properties, and 'optional' does not exist in type '{ index: true; }'.
Overload 2 of 4, '(field: "replyTo", validator: VId<Id<"messages">, "required">, options: { unique: true; }): EntDefinition<VObject<{ content: string; type: string; deleted: boolean; replyTo: Id<"messages">; }, { ...; }, "required", "type" | ... 2 more ... | "replyTo">, { ...; }, {}, {}, {}>', gave the following error.
Object literal may only specify known properties, and 'optional' does not exist in type '{ unique: true; }'.
Overload 3 of 4, '(field: "replyTo", validator: VId<Id<"messages">, "required">, options: { default: Id<"messages">; }): EntDefinition<VObject<{ content: string; type: string; deleted: boolean; replyTo: Id<...>; }, { ...; }, "required", "type" | ... 2 more ... | "replyTo">, {}, {}, {}, {}>', gave the following error.
Object literal may only specify known properties, and 'optional' does not exist in type '{ default: Id<"messages">; }'.
No overload matches this call.
Overload 1 of 4, '(field: "replyTo", validator: VId<Id<"messages">, "required">, options: { index: true; }): EntDefinition<VObject<{ content: string; type: string; deleted: boolean; replyTo: Id<"messages">; }, { ...; }, "required", "type" | ... 2 more ... | "replyTo">, { ...; }, {}, {}, {}>', gave the following error.
Object literal may only specify known properties, and 'optional' does not exist in type '{ index: true; }'.
Overload 2 of 4, '(field: "replyTo", validator: VId<Id<"messages">, "required">, options: { unique: true; }): EntDefinition<VObject<{ content: string; type: string; deleted: boolean; replyTo: Id<"messages">; }, { ...; }, "required", "type" | ... 2 more ... | "replyTo">, { ...; }, {}, {}, {}>', gave the following error.
Object literal may only specify known properties, and 'optional' does not exist in type '{ unique: true; }'.
Overload 3 of 4, '(field: "replyTo", validator: VId<Id<"messages">, "required">, options: { default: Id<"messages">; }): EntDefinition<VObject<{ content: string; type: string; deleted: boolean; replyTo: Id<...>; }, { ...; }, "required", "type" | ... 2 more ... | "replyTo">, {}, {}, {}, {}>', gave the following error.
Object literal may only specify known properties, and 'optional' does not exist in type '{ default: Id<"messages">; }'.
ampp
amppβ€’5mo ago
field("replyTo", v.optional(v.id("messages"))) it happens πŸ™ƒ
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
So seems to work: schema:
messages: defineEnt({})
.field("content", v.string())
.field("type", v.string(), { default: "message" })
.field("deleted", v.boolean(), { default: false })
.field("replyTo", v.optional(v.id("messages")))
.edge("privateChat")
.edge("user")
.edges("readBy", {
to: "users",
inverseField: "readMessages",
table: "readMessages",
}),
messages: defineEnt({})
.field("content", v.string())
.field("type", v.string(), { default: "message" })
.field("deleted", v.boolean(), { default: false })
.field("replyTo", v.optional(v.id("messages")))
.edge("privateChat")
.edge("user")
.edges("readBy", {
to: "users",
inverseField: "readMessages",
table: "readMessages",
}),
query:
return chat.edge("messages").map(async (message) => ({
...message,
userId: undefined,
from: await ctx.table("users").getX(message.userId),
replyTo: message.replyTo
? ctx.table("messages").getX(message.replyTo)
: undefined,
readBy: await message.edge("readBy"),
sent: true,
}));
return chat.edge("messages").map(async (message) => ({
...message,
userId: undefined,
from: await ctx.table("users").getX(message.userId),
replyTo: message.replyTo
? ctx.table("messages").getX(message.replyTo)
: undefined,
readBy: await message.edge("readBy"),
sent: true,
}));
ampp
amppβ€’5mo ago
im actually working on our chat thing rn, i use v.union(v.id('messages'), v.null())
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
what is the benefit of v.union(v.id("messages"), v.null()) over v.optional(v.id("messages")
ampp
amppβ€’5mo ago
my understanding is if you were loading stuff from the ui, null is helpful to let you know if the data loaded empty, with undefined, it hasn't loaded yet. so its not as confusing
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
ok, hmmm, yeah
ampp
amppβ€’5mo ago
apparently the more code i write the more im letting nextjs do lots of small queries.
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
Hoigim Dang
YouTube
Null References The Billion Dollar Mistake
lecturer:Tony Hoare conference:QCon London 2009 topic:null reference in programming language design
ampp
amppβ€’5mo ago
yeah null with c is a totally different game. our dx protects us from πŸ’₯
Matt Luo
Matt Luoβ€’5mo ago
Thanks for sharing the approach to query directly in the replyTo property:
replyTo: message.replyTo
? ctx.table("messages").getX(message.replyTo)
: undefined,
replyTo: message.replyTo
? ctx.table("messages").getX(message.replyTo)
: undefined,
Do you prefer this approach over creating a asymmetrical self-directed many:many relationship between messages? https://labs.convex.dev/convex-ents/schema#asymmetrical-self-directed-manymany-edges
Ent Schema - Convex Ents
Relations, default values, unique fields and more for Convex
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
The thing is that I don't want many to many relations. I want that you can only reply to one message. many to many would mean that one message can reply to multiple messages With my method you can only insert one message to reply to
Matt Luo
Matt Luoβ€’5mo ago
Yeah, your business logic wouldn’t include many to many. I believe Michal wrote to do many to many as a workaround to the limitation of not being able to do an optional one to many. Maybe I need to re-read this whole post. I think when you first made this post, you were looking for an optional one:many, and then the post title got edited to the asymmetric self referencing. So maybe what I’m saying is no longer relevant .
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
It still needs to be optional. Not every message replies to another You can write messages that don't reply
Matt Luo
Matt Luoβ€’5mo ago
Right, so 1) does the β€œfollowers” example from the Convex Ents docs work for this use case of replying? 2) if not, does a fake, but technically many:many junction table work better than this nested query approach you wrote?
Michal Srb
Michal Srbβ€’5mo ago
Recap: Problem: One message can be a reply to 0 or 1 other message. 1 message can have many replies. This is an assymetrical, self-directed, optional 1:many edge. Not directly supported with Ents, but the workarounds are either: 1. Use a field as @FleetAdmiralJakob πŸ—• πŸ—— πŸ—™ has done, and read the "parent" message manually. Most likely you will want to index the field so that you can get a query for replies of message X. This is the cheapest approach, works well, and is what you'd do in vanilla Convex as well. 2. Use a many:many edge. Enforce the uniqueness when writing a reply. Then the "reply" relationship will be stored in a separate table. Here I'd probably recommend sticking with the first approach, unless you see a possibility of the edge changing from 1:many to many:many in the future.