#help

5 messages · Page 1 of 1 (latest)

oblique lance
#

export const getOrphanedFiles = internalQuery({
args: {},
handler: async (ctx) => {
const ONE_HOUR_MS = 1000 * 60 * 60;
const cutoffTimeMs = Date.now() - ONE_HOUR_MS;
const orphanedFiles = await ctx.db
.query("files")
.withIndex("by_is_uploaded_created_at", q =>
q.eq("isUploaded", false).lte("createdAt", cutoffTimeMs)
)
.take(100);
return orphanedFiles;
},
});

export const deleteFile = internalMutation({
args: {
fileId: v.id("files"),
},
handler: async (ctx, args) => {
await ctx.db.delete(args.fileId);
},
});

export const purgeOrphanedFiles = internalAction({
args: {},
handler: async (ctx) => {
const orphanedFiles = await ctx.runQuery(internal.files.getOrphanedFiles);
for (const file of orphanedFiles) {
try {
const command = new DeleteObjectCommand({
Bucket: ${process.env.PROJECT_NAME!}-${process.env.ENV_SUFFIX!}-${process.env.AWS_REGION!}-s3-${process.env.AWS_PRIVATE_S3_BUCKET_NAME!},
Key: file._id,
});
await s3Client.send(command);
await ctx.runMutation(internal.files.deleteFile, {
fileId: file._id,
});
} catch (error) {
console.error(Failed to purge orphaned file ${file._id}: ${error});
}
}
},
});

is there a cleaner way of writing this so i dont have to have multiple function declared

balmy tigerBOT
#

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

#

You’re already very close to the “recommended” pattern; the extra functions are mostly good structure, not avoidable boilerplate.

From the docs and Discord threads, there are two relevant points:

  1. Actions cannot touch ctx.db directly – they must call queries/mutations via ctx.runQuery / ctx.runMutation. There is “no way around that.” [Inline queries]
  2. Shared logic should live in plain TypeScript helpers, not in extra Convex functions. Use helpers to avoid ctx.runQuery / ctx.runMutation where you don’t need a separate transaction or separate function. [Helper functions; Best practices]

Given that, the only “cleaner” refactor you can do (without changing behavior) is to:

  • Keep getOrphanedFiles as an internalQuery (the action must call a query).
  • Inline deleteFile into a helper function instead of a separate internalMutation, since it’s only used from this action and doesn’t need its own transaction boundary.

Example:

// convex/files.ts
import { internalQuery, internalAction, type MutationCtx } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
import { DeleteObjectCommand } from "@aws-sdk/client-s3";
import { s3Client } from "../lib/s3";

export const getOrphanedFiles = internalQuery({
  args: {},
  handler: async (ctx) => {
    const ONE_HOUR_MS = 1000 * 60 * 60;
    const cutoffTimeMs = Date.now() - ONE_HOUR_MS;
    return await ctx.db
      .query("files")
      .withIndex("by_is_uploaded_created_at", (q) =>
        q.eq("isUploaded", false).lte("createdAt", cutoffTimeMs),
      )
      .take(100);
  },
});
#

// plain TS helper, not a Convex function
async function deleteFileHelper(ctx: MutationCtx, fileId: Id<"files">) {
await ctx.db.delete(fileId);
}

export const purgeOrphanedFiles = internalAction({
args: {},
handler: async (ctx) => {
const orphanedFiles = await ctx.runQuery(internal.files.getOrphanedFiles);
for (const file of orphanedFiles) {
try {
const command = new DeleteObjectCommand({
Bucket: ${process.env.PROJECT_NAME!}-${process.env.ENV_SUFFIX!}-${process.env.AWS_REGION!}-s3-${process.env.AWS_PRIVATE_S3_BUCKET_NAME!},
Key: file._id,
});
await s3Client.send(command);
// still need a mutation call from the action
await ctx.runMutation(internal.files.deleteFileInternal, {
fileId: file._id,
});
} catch (error) {
console.error(Failed to purge orphaned file ${file._id}: ${error});
}
}
},
});

// small internal mutation that just delegates to the helper
export const deleteFileInternal = internalMutation({
args: { fileId: v.id("files") },
handler: async (ctx, { fileId }) => {
await deleteFileHelper(ctx, fileId);
},
});


This follows the docs’ guidance:

- Most logic in helpers (`deleteFileHelper`), thin Convex wrappers. [[Best practices](<https://docs.convex.dev/understanding/best-practices/#why-7>)]
- Action uses `runQuery` / `runMutation` as required; no “inline db access” from actions is supported. [[Action context](<https://docs.convex.dev/functions/actions#action-context>); [Inline queries](<https://discord.com/channels/1019350475847499849/1426156768634863716>)]
- You avoid duplicating logic if you ever need to delete a file from another mutation/query. [[Helper reuse](<https://docs.convex.dev/functions/mutation-functions#splitting-up-mutation-code-via-helpers>)]
#

If you truly never need deleteFile anywhere else and don’t care about re‑use, your current version is already about as minimal as Convex allows; you can’t collapse the query + mutation + action into fewer Convex functions without losing the ability to call S3 safely from an action and touch the DB in a transaction.

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