fawwaz
fawwaz•15mo ago

Trying to implement 404 but getting validator error

Hello all 👋 , in my app the ID for a resource ("campaign") is taken from the url, most the time things works as expected. However, if the user try to input the url and have typo or hit a deleted resource, In stead of seeing a 404 or my error page, the app crashes with this error. I tried adding .catch to the db.get call but it seems that the error is coming from the validator. is there is away to return null when campaign with the id provided does not exist?
No description
No description
8 Replies
ballingt
ballingt•15mo ago
You can use v.string() in your validator if you're not sure the string will be a valid ID and check in the Convex function with ctx.db.normalizeId https://docs.convex.dev/database/document-ids#serializing-ids, then return null if it's not
Document IDs | Convex Developer Hub
Create complex, relational data models using IDs.
ballingt
ballingt•15mo ago
Or you can catch this useQuery error with an errorboundary and show a 404 Do you know it will always be a string? You can accept more values if not (and then TypeScript will remind you to check for them in the body of your query)
fawwaz
fawwazOP•15mo ago
Hey @ballingt , so I just changed the validator to be string and did type casting and that seems to work find. Does adding the ctx.db.normalizeId add any important benefit over the current implementation?
No description
ballingt
ballingt•15mo ago
Mostly no it pretty much does the same thing as what you've written. It will let you know if that ID is actually from the table you think it is, and it might be easier to read
const campaignId = await ctx.db.normalizeId('campaigns', id)
if (!campaignId) return null;
const campaignId = await ctx.db.normalizeId('campaigns', id)
if (!campaignId) return null;
the code you could do surprising things if someone passed e.g. an Id<"user"> instead
fawwaz
fawwazOP•15mo ago
what! 😳 really ?, so if say a userId was passed to the bellow line, we are not always going to get null? const campaign = await ctx.db.get(args.id as Id<"campaigns">).catch(() => null);
ballingt
ballingt•15mo ago
Yep, there'd be a User record stored in the campaign variable. as Id<"campaigns"> is just a TypeScript thing, it doesn't exist at runtime as is always a way to promise something to just TypeScript, it never does anything at runtime. Here the promise/lie we're telling TypeScript is "just pretend that args.id is a Id<"campaigns">, I promise it will be." But if the code doesn't actually check that, it might not be true. That's the reason to use normalizeId here, that's the way to ask Convex if something is a real Id and whether it's from the table you think it is.
ian
ian•15mo ago
That's also the benefit of using args validators rather than just doing:
mutation({
handler: async (ctx, args: {campaignId: Id<'campaigns'>}) => {
}
});
mutation({
handler: async (ctx, args: {campaignId: Id<'campaigns'>}) => {
}
});
As you saw earlier, it's doing runtime validation
tiernacity
tiernacity•10mo ago
Stumbled across this thread while trying to work out how to handle an array of IDs, which could belong to more than one table. Taking inspiration from the RLS code in convex-helpers (linked in another thread) I wrote a little helper function to help me determine which table an ID belongs to. Sharing here, in case it's useful to anyone else
import { MutationCtx, QueryCtx } from '../_generated/server'
import { Id, TableNames } from '../_generated/dataModel'

const castId = <T extends TableNames>(
ctx: QueryCtx | MutationCtx,
tables: T[],
id: Id<T> | string,
) => {
return tables.reduce(
(acc, table) => {
return {
...acc,
[table]: ctx.db.normalizeId(table, id),
}
},
{} as { [K in T]: Id<K> | null },
)
}

export { castId }
import { MutationCtx, QueryCtx } from '../_generated/server'
import { Id, TableNames } from '../_generated/dataModel'

const castId = <T extends TableNames>(
ctx: QueryCtx | MutationCtx,
tables: T[],
id: Id<T> | string,
) => {
return tables.reduce(
(acc, table) => {
return {
...acc,
[table]: ctx.db.normalizeId(table, id),
}
},
{} as { [K in T]: Id<K> | null },
)
}

export { castId }
Can be used like this:
const { table1, table2 } = castId(ctx, ['table1', 'table2'], id)
if (table1) {
// You have an Id<'table1'> in variable table1, do something with it
}
if (table2) {
// You have an Id<'table2'> in variable table2, do something with it
}
}
const { table1, table2 } = castId(ctx, ['table1', 'table2'], id)
if (table1) {
// You have an Id<'table1'> in variable table1, do something with it
}
if (table2) {
// You have an Id<'table2'> in variable table2, do something with it
}
}

Did you find this page helpful?