Best practices for joining data in query
I am attempting to join a project's notes to this query, I get no errors however the type inference insists that the property does not exist when I try to access it in the response.
export const getProjectById = query({
args: { projectId: v.id("projects") },
handler: withUser(async ({ db, user }, { projectId }) => {
const project = await db.get(projectId);
if (!project) {
throw new Error("Project not found");
}
if (project.user !== user._id) {
throw new Error("Unauthorized");
}
const notes = await db
.query("notes")
.withIndex("by_project", (q) => q.eq("project", project._id))
.collect();
return {
...project,
notes: notes,
};
}),
});
29 Replies
@Luke what's the schema for projects?
Also where is the error in the above code? The property
notes
does not exist on the client?The schema for projects is
projects: defineTable({
title: v.string(),
user: v.id("users"),
isArchived: v.boolean(),
parentProject: v.optional(v.id("projects")),
icon: v.optional(v.string()),
isFavorited: v.boolean(),
})
.index("by_user", ["user"])
.index("by_user_parent", ["user", "parentProject"]),
And yes, its that it doesn't exist
However I see similar queries in this example (https://github.com/get-convex/convex-demos/blob/6fc30f0841274711c6cd2ca3d5df13dce3506e8e/users-and-auth/convex/messages.ts#L4) so not sure whats going wrong
@Luke could you show as example of this error? looks fine to me, notes should be a property that exists and is an array of (an unknown number of, maybe 0) note records.
Of course, so I query it like this
const project = useQuery(api.projects.getById, {
projectId: params.projectId,
});
and then try to access notes via
project?.notes
and I get this error
Property 'notes' does not exist on type '{ _id: Id<"projects">; _creationTime: number; parentProject?: Id<"projects"> | undefined; icon?: string | undefined; title: string; user: Id<"users">; isArchived: boolean; isFavorited: boolean; } | { ...; }'.
Property 'notes' does not exist on type '{ _id: Id<"projects">; _creationTime: number; parentProject?: Id<"projects"> | undefined; icon?: string | undefined; title: string; user: Id<"users">; isArchived: boolean; isFavorited: boolean; }'.ts(2339)
anygetById
and getProjectById
don't match, could that be it?I just renamed the function in the meantime
Apologies, should've mentioned
Intellisense also doesn't recognise the notes list

I also tried removing the "withUser" wrapper, which also made no difference
The wrapper was copied directly from the helpers repo
huh, I don't see the issue. I'm curious about that union, of the three union member s (the one we can see, the middle one that's collapsed here, and undefined,) do you know how the first and second differ?
I'm not explicitly defining a union type, so not sure where its getting it from...

Which doesn't make sense, as the type of the "notes" value can be seem here

Could you try setting
"noErrorTruncation": true
in your tsconfig.json
not sure if it's the top level one or the convex/tsconfig.json that would impact thisI'll change both
(property) getById: FunctionReference<"query", "public", {
projectId: Id<"projects">;
}, {
_id: Id<"projects">;
_creationTime: number;
parentProject?: Id<"projects"> | undefined;
icon?: string | undefined;
title: string;
user: Id<"users">;
isArchived: boolean;
isFavorited: boolean;
} | {
notes: {
_id: Id<"notes">;
_creationTime: number;
project?: Id<"projects"> | undefined;
content?: string | undefined;
title: string;
user: Id<"users">;
isArchived: boolean;
}[];
_id: Id<"projects">;
_creationTime: number;
parentProject?: Id<"projects"> | undefined;
icon?: string | undefined;
title: string;
user: Id<"users">;
isArchived: boolean;
isFavorited: boolean;
}>
maybe save it to a variable to see where this happens,
Same issue
if this is happening in the return value and you still see this without
withUser
then I'd love to get a repro, sounds like a bug or at least something pretty confusingI'll try removing withUser and using the intermediate variable
curious too if hovering over the function definition shows this too or not
export const getById = query({
args: { projectId: v.id("projects") },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
const project = await ctx.db.get(args.projectId);
if (!project) {
throw new Error("Not found");
}
if (!project.isArchived) {
return project;
}
if (!identity) {
throw new Error("Not authenticated");
}
const userId = identity.subject;
if (project.user !== userId) {
throw new Error("Unauthorized");
}
const notes = await ctx.db
.query("notes")
.withIndex("by_project", (q) => q.eq("project", project._id))
.collect();
const x = {
...project,
notes: notes,
};
return x;
},
});
So this is the raw query without any wrappers/anything special
projects: defineTable({
title: v.string(),
user: v.id("users"),
isArchived: v.boolean(),
parentProject: v.optional(v.id("projects")),
icon: v.optional(v.string()),
isFavorited: v.boolean(),
})
.index("by_user", ["user"])
.index("by_user_parent", ["user", "parentProject"]),
notes: defineTable({
title: v.string(),
content: v.optional(v.string()),
user: v.id("users"),
project: v.optional(v.id("projects")),
isArchived: v.boolean(),
})
.index("by_user", ["user"])
.index("by_project", ["project"]),
This is the relevant part of the schemathere's a
return project;
in thereOh wow, very good spot!
Thank you!
Let me just verify this
That explains the union
Can confirm that sorted it
sweet
Glad it wasn't a bug
TypeScript being the hero for once
Love to see it
noice 🙂
PS, only been playing around with Convex for a few days but really enjoying it so far
Coming from a background of several Firebase projects and most recently a Supabase project
Thanks for the support
Good to hear! Let us know how things go, feedback is great