Štosdenfer
Štosdenfer7d ago

Check if document exists once?

I want to check if a document exists once from my client. I'm running a document management app and before I upload the documents to uploadthing, I want to check if a task containing said documents exists. I created a query, but when using it via "useQuery" I have to define the argument at hook call, but I don't know it until the user submitted it.
export const checkTaskExists = query({
args: {
taskFullId: v.string(),
},
handler: async (ctx, args) => {
const task = await ctx.db
.query("tasks")
.withIndex("by_taskFullId", (q) => q.eq("taskFullId", args.taskFullId))
.first();
return !!task;
},
});
export const checkTaskExists = query({
args: {
taskFullId: v.string(),
},
handler: async (ctx, args) => {
const task = await ctx.db
.query("tasks")
.withIndex("by_taskFullId", (q) => q.eq("taskFullId", args.taskFullId))
.first();
return !!task;
},
});
Now I somehow want to call checkTaskExists({taskFullId) from my onSubmit function Is that somehow possibe? Maybe via Actions?
22 Replies
Štosdenfer
ŠtosdenferOP7d ago
Also, if I'm using it wrong and should think of this differently, please say so, I'm still wrapping my head around Convex
Sara
Sara7d ago
I'd use this : https://docs.convex.dev/api/interfaces/server.OrderedQuery#unique that should return it via a query that only exists once in your table, else it will return null ^, I wouldn't use actions, I'd keep it in a query with a little work around by passing the "skip" into the function variables. can you share part of your client side code that needs updates?
Štosdenfer
ŠtosdenferOP7d ago
The problem isn't defining the query, the problem is calling it on the client. I have a form that asks for a taskId input. The user inputs it and than in that same form selects PDFs to upload under that taskId. The upload has to happen before DB persist of the task (because I need the key of the uploaded files in order to store them for later fetching). Once the user clicks submit I have access to the taskId they have input. Using that input I need to check if a task with that taskId is already persisted in the DB. How do I do that? Here is part of the form onSubmit where I would call it:
const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsLoading(true);
setProgressStep("Uploading...");
const { taskId, year, eBill, documentation, email } = values;

CHECK IF DOCUMENT WITH TASKID EXISTS
...
}
const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsLoading(true);
setProgressStep("Uploading...");
const { taskId, year, eBill, documentation, email } = values;

CHECK IF DOCUMENT WITH TASKID EXISTS
...
}
I can't use the hook useQuery because there I need to define the argument immediately and I don't have it Or maybe I can but just don't know how
Sara
Sara7d ago
don't quote me on this but it might help if you can use a state for say "chosen"
const [taskId, setTaskId] = useState<string|null>(null)
const data = useQuery(api.mydata.someQuery, taskId ? taskId:"skip" )

// in your function

const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsLoading(true);

setProgressStep("Uploading...");
const { taskId, year, eBill, documentation, email } = values;
setTaskId(taskId)
CHECK IF DOCUMENT WITH TASKID EXISTS
// use the query here
...
}
const [taskId, setTaskId] = useState<string|null>(null)
const data = useQuery(api.mydata.someQuery, taskId ? taskId:"skip" )

// in your function

const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsLoading(true);

setProgressStep("Uploading...");
const { taskId, year, eBill, documentation, email } = values;
setTaskId(taskId)
CHECK IF DOCUMENT WITH TASKID EXISTS
// use the query here
...
}
I used to do that with modals, I don't recommend this in forms cc: @mikeysee might be able to give some insights :panicBasket:
Štosdenfer
ŠtosdenferOP7d ago
Yeah, I could make a form.watch that will listen for changes on the taskId field and set the new data to the state and than use that state in the query definition, but that honestly seems horrible and like I'm missusing convex thank you! I know realtime always means that I can't have inconsistent data, but the ability to fetch something once seems necessary for these kinds of usecases I know I can check it during mutation, but I need the check before cause of the upload Also, if I set it to state, everytime a user changes the input it would run the query - seems like a perfect situation for a huge bill :))
erquhart
erquhart7d ago
Is there a reason not to create the task before uploads, then patch with the uploaded file ids? You can use "skip" in place of useQuery args as in Sara's example.
Štosdenfer
ŠtosdenferOP7d ago
The paradigm (don't know if correct phrase, EN is not my native lang) of that seems off. I create a task that is then in my DB and visible to the admins in the dashboard - but without documents. I get that it's for a split second in 99% of cases - but... What if the upload fails? If the client's (user who's uploading) connection drops and they close the browser and want to try again later - then they get the error that the task exists but without data. In that case, I guess I could check if the document fields in the DB are missing and skip the creation and just do patching - but that seems like a pain in the axx I might be overthinking this, but I don't think I am. Feel free to tell me and explain otherwise
erquhart
erquhart7d ago
This is the general approach for uploads, capture state somehow via mutation pending a successful upload (this allows you to communicate state in your UI as well, including across clients), then when upload succeeds via scheduled action, update the state with status, file ids, etc
Štosdenfer
ŠtosdenferOP7d ago
So far I thought I'd use actions - because I can call them without needing to know the arguments beforehand. However when I do this:
export const getTaskByFullId = internalQuery({
args: {
taskFullId: v.string(),
},
handler: async (ctx, args) => {
const task = await ctx.db
.query("tasks")
.withIndex("by_taskFullId", (q) => q.eq("taskFullId", args.taskFullId))
.first();
return task;
},
});

export const checkIfTaskExists = action({
args: {
taskFullId: v.string(),
},
handler: async (ctx, args) => {
const task = await ctx.runQuery(internal.tasks.getTaskByFullId, {
taskFullId: args.taskFullId,
});

return !!task;
},
});
export const getTaskByFullId = internalQuery({
args: {
taskFullId: v.string(),
},
handler: async (ctx, args) => {
const task = await ctx.db
.query("tasks")
.withIndex("by_taskFullId", (q) => q.eq("taskFullId", args.taskFullId))
.first();
return task;
},
});

export const checkIfTaskExists = action({
args: {
taskFullId: v.string(),
},
handler: async (ctx, args) => {
const task = await ctx.runQuery(internal.tasks.getTaskByFullId, {
taskFullId: args.taskFullId,
});

return !!task;
},
});
I get this error at checkIfTaskExists:
'task' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.ts(7022)
erquhart
erquhart7d ago
Checking for the id in the task creation mutation shouldn't be a pain, would be a very minimal thing to do, couple lines of code
Štosdenfer
ŠtosdenferOP7d ago
yeah, inside the mutation it's really easy and straightforward
erquhart
erquhart7d ago
One thing to be aware of is calling actions from the client is generally an anti-pattern. There are use cases for it, but they're rare. This isn't a complicated use case. Reading your code sample...
because I can call them without needing to know the arguments beforehand
Did you catch that you can use "skip" with useQuery
Štosdenfer
ŠtosdenferOP7d ago
I went back to check it... I did not catch that @Sara set the state for the taskId after the submit - my thought process was that I would need to update it as the input changes That actually might solve everything. I really don't like the idea of persisting a task in the DB and then uploading and then updating the document keys for fetching after - could be wrong for not liking that. Let me try this approach with skipping and updateing the state after submit. Sorry again for not catching that
Sara
Sara7d ago
😅 I did add what erquhart suggested indeed https://docs.convex.dev/client/react#skipping-queries
Štosdenfer
ŠtosdenferOP7d ago
reading this I noticed one-off queries - would that be what I'm looking for?
Sara
Sara7d ago
try it out, I guess it does
Štosdenfer
ŠtosdenferOP7d ago
tried it - it does exactly what I wanted
// convex/tasks.ts
export const checkTaskExists = query({
args: {
taskFullId: v.string(),
},
handler: async (ctx, args) => {
const task = await ctx.db
.query("tasks")
.withIndex("by_taskFullId", (q) => q.eq("taskFullId", args.taskFullId))
.first();

return !!task;
},
});


// task-upload-form.tsx
const exists = await convex.query(api.tasks.checkTaskExists, {
taskFullId: `Z/6936-${taskId}/${year}`,
});
console.log("exists", exists);
if (exists) {
toast.error("Task exists");
return;
}
// convex/tasks.ts
export const checkTaskExists = query({
args: {
taskFullId: v.string(),
},
handler: async (ctx, args) => {
const task = await ctx.db
.query("tasks")
.withIndex("by_taskFullId", (q) => q.eq("taskFullId", args.taskFullId))
.first();

return !!task;
},
});


// task-upload-form.tsx
const exists = await convex.query(api.tasks.checkTaskExists, {
taskFullId: `Z/6936-${taskId}/${year}`,
});
console.log("exists", exists);
if (exists) {
toast.error("Task exists");
return;
}
Sara
Sara7d ago
good stuff!
Štosdenfer
ŠtosdenferOP7d ago
what would be the caveats? I noticed you edited the reply but I'm still interested in case I'm using sth wrong 😓
Sara
Sara7d ago
I edited it because I don't know ;p
Štosdenfer
ŠtosdenferOP7d ago
hahaha okay thank you both so much! @Sara @erquhart this was very helpful
mikeysee
mikeysee7d ago
Glad you sorted it!

Did you find this page helpful?