gabrielw
gabrielw5w ago

Filtering a many:many ENT Edge

Hey all, having some issues with filtering an edge with ents. I am on convex-ents@latest, Typescript 5.7 and convex 1.13.2. here is my query:
export const getEventsByTeamId = query({
args: {
teamId: v.id("teams"),
paginationOpts: paginationOptsValidator,
status: vEventStatus,
},
async handler(ctx, { teamId, paginationOpts, status }) {
return await ctx
.table("teams")
.getX(teamId)
.edge("events")
.filter((q) => q.eq(q.field("status"), status))
.order("asc", "startAtInMsSinceEpoch")
.paginate(paginationOpts);
},
});
export const getEventsByTeamId = query({
args: {
teamId: v.id("teams"),
paginationOpts: paginationOptsValidator,
status: vEventStatus,
},
async handler(ctx, { teamId, paginationOpts, status }) {
return await ctx
.table("teams")
.getX(teamId)
.edge("events")
.filter((q) => q.eq(q.field("status"), status))
.order("asc", "startAtInMsSinceEpoch")
.paginate(paginationOpts);
},
});
The q argument in the filter is throwing a Type Error: Parameter 'q' implicitly has an 'any' type. When I apply the filter on the "teams" table, the type inference is working as expected, but not when traversing the many:many edge. Here is a more detailed error on the actual filter, it seems like the filter is trying to read a Team document, and not the Event edge.
Property 'filter' does not exist on type 'PromiseEdgeEnts<EntDataModelFromSchema<SchemaDefinition<{ teams: EntDefinition<VObject<{ name: string; isPersonal: boolean; slug: string; deletionTime?: number | undefined; }, { name: VString<string, "required">; isPersonal: VBoolean<boolean, "required">; FieldName: never; }, "required", "name" |
Property 'filter' does not exist on type 'PromiseEdgeEnts<EntDataModelFromSchema<SchemaDefinition<{ teams: EntDefinition<VObject<{ name: string; isPersonal: boolean; slug: string; deletionTime?: number | undefined; }, { name: VString<string, "required">; isPersonal: VBoolean<boolean, "required">; FieldName: never; }, "required", "name" |
Thanks all.
4 Replies
Convex Bot
Convex Bot5w ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
deen
deen5w ago
I believe it's because you're trying to order with an index, which you can't do through an edge - the "withIndex" has already been used traversing the edge, and the ents API doesn't allow for tacking your own index fields onto that. However I'd expect you'd be getting a more specific typescript error for that. Are you really still on convex 1.13.2? That's pretty old in convex years.
gabrielw
gabrielwOP5w ago
Just updated convex version, good catch. Hm the error seems to persist even after removing the ordering. Interestingly the .order with the index is working as I intended, returning the expected based on "asc" or "desc." I think the issue may have to do with this: A 1:many edge query is inferring a type of PromiseQuery which can chain a filter in the same manner as a ctx.db.query call would in vanilla convex. (screenshot attached) However, a many:many edge is inferring a type of PromiseEdgeEnts , which may require a fundamentally different approach for filtering, perhaps with an await and a .map. (2nd screenshot attached) Filtering, ordering, and paginating a many:many edge might be a bit ambitious, though I think can be done with updating my mental model around the filtering of a many:many ent.
No description
No description
gabrielw
gabrielwOP4w ago
brief update here: I solved this problem with an abstraction of the many:many edge (a new table), with a new property startAtinMsSinceEpoch, and two additional indeces (1 of them a compound index). This solution allows me to do a descending and ascending paginated sort on a moment in time that is not the default _created_at system field, but rather, the utc time of an event.
export const getPerformancesByTeamId = query({
args: {
teamId: v.id("teams"),
paginationOpts: paginationOptsValidator,
type: v.optional(v.union(v.literal("upcoming"), v.literal("past"))),
},
async handler(ctx, { teamId, paginationOpts, type }) {
if (type === "upcoming") {
return await ctx
.table("performances", "by_teamId_startAtInMsSinceEpoch", (q) =>
q.eq("teamId", teamId)
)
.filter((q) => q.gte(q.field("startAtInMsSinceEpoch"), Date.now()))
.paginate(paginationOpts)
.map(async (performance) => {
const event = await performance.edge("event");
if (event.imgStorageId) {
const imgUrl = await ctx.storage.getUrl(event.imgStorageId);
if (imgUrl) {
return { ...event, imgUrl };
}
}
});
} else {
return await ctx
.table("performances")
.order("desc", "startAtInMsSinceEpoch")
.filter((q) =>
q.and(
q.eq(q.field("teamId"), teamId),
q.lt(q.field("startAtInMsSinceEpoch"), Date.now())
)
)
.paginate(paginationOpts)
.map(async (performance) => {
const event = await performance.edge("event");
if (event.imgStorageId) {
const imgUrl = await ctx.storage.getUrl(event.imgStorageId);
if (imgUrl) {
return { ...event, imgUrl };
}
}
});
}
},
});
export const getPerformancesByTeamId = query({
args: {
teamId: v.id("teams"),
paginationOpts: paginationOptsValidator,
type: v.optional(v.union(v.literal("upcoming"), v.literal("past"))),
},
async handler(ctx, { teamId, paginationOpts, type }) {
if (type === "upcoming") {
return await ctx
.table("performances", "by_teamId_startAtInMsSinceEpoch", (q) =>
q.eq("teamId", teamId)
)
.filter((q) => q.gte(q.field("startAtInMsSinceEpoch"), Date.now()))
.paginate(paginationOpts)
.map(async (performance) => {
const event = await performance.edge("event");
if (event.imgStorageId) {
const imgUrl = await ctx.storage.getUrl(event.imgStorageId);
if (imgUrl) {
return { ...event, imgUrl };
}
}
});
} else {
return await ctx
.table("performances")
.order("desc", "startAtInMsSinceEpoch")
.filter((q) =>
q.and(
q.eq(q.field("teamId"), teamId),
q.lt(q.field("startAtInMsSinceEpoch"), Date.now())
)
)
.paginate(paginationOpts)
.map(async (performance) => {
const event = await performance.edge("event");
if (event.imgStorageId) {
const imgUrl = await ctx.storage.getUrl(event.imgStorageId);
if (imgUrl) {
return { ...event, imgUrl };
}
}
});
}
},
});
will mark this as resolved

Did you find this page helpful?