#Fix tasks output type

13 messages · Page 1 of 1 (latest)

sterile gateBOT
#

Hi @sharp jasper. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

I notice you're having an issue with the undefined value in your Convex mutation. This is a common problem when working with Convex because undefined values cannot be properly serialized in JSON when sent to the backend.

The specific issue is in your patch operation where you're conditionally setting progressPourcent: status === "COMPLETED" ? 100 : undefined. When status is not "COMPLETED", you're trying to set progressPourcent to undefined, which Convex cannot handle properly in patches sent from clients.

According to the Convex documentation and support discussions, there are a few ways to fix this:

  1. Use null instead of undefined when sending data to Convex, then convert it to undefined in your mutation:
export const _internal = internalMutation({
  args,
  handler: async (ctx, args): Promise<DataModel['tasks']> => {
    const status = args.status ?? 'COMPLETED';
    const completedAt = args.completedAt ?? Date.now();

    const results = Array.from(new Set([...args.resultIds].filter(notEmpty)));

    // Create a patch object
    const patch: any = {
      completedAt: completedAt,
      resultIds: results,
      status: status,
      ended: true,
    };
    
    // Only add progressPourcent if status is COMPLETED
    if (status === "COMPLETED") {
      patch.progressPourcent = 100;
    }

    await ctx.db.patch(args.id, patch);

    const task = await ctx.db.get(args.id);

    if (!task) throw new Error("Task not found");

    // If the task is a temporary task (no projectId), we delete it
    // to avoid cluttering the database with old tasks
    if (!task.workspaceId) await ctx.db.delete(args.id);

    return task;
  }
})

This approach avoids using undefined entirely by only including the fields you want to update in the patch object.

#

As explained in the Discord support thread, undefined is not a value that can be stringified in JSON, which is why it causes issues when sending data to Convex:

The arguments object can't carry undefined values to the backend, as it needs to be stringified first:

JSON.stringify({ foo: 'bar', baz: undefined })
'{"foo":"bar"}'

Convex Discord Support

By constructing your patch object without the fields you don't want to update, you avoid this serialization issue entirely.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@sharp jasper Kapa.ai is still learning and improving, please let me know how I did by reacting below.

sharp jasper
#

@sterile gate the problem is that it lacks Type '{ _id: Id<"tasks">; _creationTime: number; workspaceId?: Id<"workspaces"> | undefined; completedAt?: number | undefined; type: "PHOTOSHOOT" | "REMOVE_BACKGROUND" | "QUICK_PHOTOSHOOT" | "GENERATE_IMAGE" | "UPSCALE" | "MAGIC_ERASER"; ... 6 more ...; updatedAt: number; }' is missing the following properties from type '{ document: { _id: Id<"tasks">; _creationTime: number; workspaceId?: Id<"workspaces"> | undefined; completedAt?: number | undefined; type: "PHOTOSHOOT" | "REMOVE_BACKGROUND" | "QUICK_PHOTOSHOOT" | "GENERATE_IMAGE" | "UPSCALE" | "MAGIC_ERASER"; ... 6 more ...; updatedAt: number; }; fieldPaths: ExtractFieldPaths<...> ...': document, fieldPaths, indexes, searchIndexes, vectorIndexes

sterile gateBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

I see the issue now. The error is related to TypeScript types in your Convex function. The problem is that your function is returning a document directly, but the return type DataModel['tasks'] is expecting a more complex type that includes document metadata.

Based on the error message, it looks like DataModel['tasks'] is a complex type that includes properties like document, fieldPaths, indexes, etc., but your function is just returning the document itself.

There are a few ways to fix this:

  1. Use the correct return type from the generated types:
export const _internal = internalMutation({
  args,
  handler: async (ctx, args): Promise<Doc<"tasks">> => {
    // Your existing code...
    return task;
  }
})

The Doc<"tasks"> type represents just the document itself, which matches what your function is returning.

This approach is supported by several documents in the knowledge sources. According to the TypeScript best practices documentation:

The Doc TypeScript type from dataModel.d.ts provides document types for all of your tables.

TypeScript types documentation

Additionally, from the Types Cookbook:

Convex code generation automatically creates types for all the documents in your tables, exposed via the Doc<"tablename"> generic type from convex/_generated/dataModel.

Types Cookbook

If you need to work with document fields without system fields, you can use WithoutSystemFields:

import { WithoutSystemFields } from "convex/server";
import { Doc } from "./_generated/dataModel";
#

// For creating new documents
type TaskInput = WithoutSystemFields<Doc<"tasks">>;


[TypeScript best practices](<https://docs.convex.dev/understanding/best-practices/typescript#document-types-without-system-fields>)

This should resolve your type error while maintaining proper type safety in your Convex function.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
#

@sharp jasper Kapa.ai is still learning and improving, please let me know how I did by reacting below.

sharp jasper
#

@sterile gate
import { v } from "convex/values";
import { serverMutation } from "../../procedures";
import { internalMutation } from "../../_generated/server";
import { internal } from "../../_generated/api";
import { TASK_ENDED_STATUS } from "../../schema/task";
import { Doc, Id } from "../../_generated/dataModel";
import { notEmpty } from "@utils/typescript.helpers";

const args = {
id: v.id("tasks"),
resultIds: v.array(v.id("workspaceImages")),
progressPourcent: v.number(),
status: TASK_ENDED_STATUS,
completedAt: v.optional(v.number()),
}

export const _internal = internalMutation({
args,
handler: async (ctx, args): Promise<Doc<'tasks'>> => {
const status = args.status ?? 'COMPLETED';
const completedAt = args.completedAt ?? Date.now();

const results = Array.from(new Set([...args.resultIds].filter(notEmpty)));

await ctx.db.patch(args.id, {
  progressPourcent: status === "COMPLETED" ? 100 : undefined,
  completedAt: completedAt,
  resultIds: results,
  status: status,
  ended: true,
})

const task = await ctx.db.get(args.id);

if (!task) throw new Error("Task not found");

// If the task is a temporary task (no projectId), we delete it
// to avoid cluttering the database with old tasks
if (!task.workspaceId) await ctx.db.delete(args.id);

return task;

}
})

export const server = serverMutation({
args,
handler: async (ctx, args): Promise<void> => {
const x = await ctx.runMutation(internal.router.tasks.updateTask._internal, args)
}
})

x is a string somehow..

sterile gateBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

I see the issue you're encountering. The problem is related to type inference when using ctx.runMutation in Convex.

Looking at your code, you're running a mutation from within another mutation using ctx.runMutation, but the return type isn't being properly inferred. Even though your _internal mutation returns Promise<Doc<'tasks'>>, the variable x is somehow being typed as a string.

This appears to be related to a known issue with the TypeScript type system in Convex when using zod wrappers or when dealing with return types from runMutation. According to a Discord support thread, there was a problem where the output validator wasn't being passed through to the Convex returns validator:

Trusted Support Staff 53: thanks for catching that - I wasn't passing through the output validator to the convex returns validator. I was worried about missing fields causing the convex validator to choke, but I can make that change.

Discord support thread

To fix this issue, you have a few options:

  1. Explicitly type the return value of ctx.runMutation:
export const server = serverMutation({
  args,
  handler: async (ctx, args): Promise<void> => {
    const x: Doc<'tasks'> = await ctx.runMutation(internal.router.tasks.updateTask._internal, args);
    // Now x is properly typed
  }
})
  1. Add a returns validator to your mutation to explicitly define the return type:
export const _internal = internalMutation({
  args,
  returns: v.object(/* define your task object schema here */),
  handler: async (ctx, args): Promise<Doc<'tasks'>> => {
    // Your existing code...
  }
})

According to the Discord thread, there was a change from using output to returns in a newer version:

#

Trusted Support Staff 53: If you use the alpha release, note you'll have to change your validators from output to returns - I made it a new key so it would be less likely to break any existing apps, and so it matches the convex naming.

Discord support thread

The FunctionReturnType utility from Convex should be correctly inferring the return type of your mutation, but there seems to be an issue with the type inference in this specific case. Explicitly typing the return value or using a returns validator should help resolve this issue.

Remember that when using ctx.runMutation, you're calling a mutation within the same transaction, and the return type should match what's defined in the target mutation.

Also, be aware that there's a note in the documentation about using runMutation:

NOTE: often you can call the mutation's function directly instead of using this. runMutation incurs overhead of running argument and return value validation, and creating a new isolated JS context.

Convex Documentation

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: