@convex-dev/agent
Version:
A agent component for Convex.
182 lines • 5.83 kB
JavaScript
import { paginator } from "convex-helpers/server/pagination";
import { mutation, query } from "./_generated/server.js";
import { schema, v } from "./schema.js";
import { paginationOptsValidator } from "convex/server";
const addFileArgs = v.object({
storageId: v.string(),
hash: v.string(),
filename: v.optional(v.string()),
mimeType: v.string(),
});
export const addFile = mutation({
args: addFileArgs,
handler: addFileHandler,
returns: {
fileId: v.id("files"),
storageId: v.string(),
},
});
export async function addFileHandler(ctx, args) {
const existingFile = await ctx.db
.query("files")
.withIndex("hash", (q) => q.eq("hash", args.hash))
.filter((q) => q.eq(q.field("filename"), args.filename))
.first();
if (existingFile) {
// increment the refcount
await ctx.db.patch(existingFile._id, {
refcount: existingFile.refcount + 1,
lastTouchedAt: Date.now(),
});
return {
fileId: existingFile._id,
storageId: existingFile.storageId,
};
}
const fileId = await ctx.db.insert("files", {
...args,
// We start out with it unused - when it's saved in a message we increment.
refcount: 0,
lastTouchedAt: Date.now(),
});
return {
fileId,
storageId: args.storageId,
};
}
export const get = query({
args: {
fileId: v.id("files"),
},
returns: v.union(v.null(), v.doc("files")),
handler: async (ctx, args) => {
return ctx.db.get(args.fileId);
},
});
/**
* If you plan to have the same file added over and over without a reference to
* the fileId, you can use this query to get the fileId of the existing file.
* Note: this will not increment the refcount. only saving messages does that.
* It will only match if the filename is the same (or both are undefined).
*/
export const useExistingFile = mutation({
args: {
hash: v.string(),
filename: v.optional(v.string()),
},
handler: async (ctx, args) => {
const file = await ctx.db
.query("files")
.withIndex("hash", (q) => q.eq("hash", args.hash))
.filter((q) => q.eq(q.field("filename"), args.filename))
.first();
if (!file) {
return null;
}
await ctx.db.patch(file._id, {
lastTouchedAt: Date.now(),
});
return { fileId: file._id, storageId: file.storageId };
},
returns: v.union(v.null(), v.object({
fileId: v.id("files"),
storageId: v.string(),
})),
});
export async function changeRefcount(ctx, prev, next) {
const prevSet = new Set(prev);
const nextSet = new Set(next);
for (const fileId of prevSet) {
if (!nextSet.has(fileId)) {
const file = await ctx.db.get(fileId);
if (file) {
await ctx.db.patch(fileId, {
refcount: file.refcount - 1,
});
}
else {
console.error(`File ${fileId} not found when decrementing refcount`);
}
}
}
for (const fileId of nextSet) {
if (!prevSet.has(fileId)) {
const file = await ctx.db.get(fileId);
if (file) {
await ctx.db.patch(fileId, {
refcount: file.refcount + 1,
});
}
else {
throw new Error(`File ${fileId} not found when incrementing refcount`);
}
}
}
}
export const copyFile = mutation({
args: {
fileId: v.id("files"),
},
handler: copyFileHandler,
returns: v.null(),
});
export async function copyFileHandler(ctx, args) {
const file = await ctx.db.get(args.fileId);
if (!file) {
throw new Error("File not found");
}
await ctx.db.patch(args.fileId, {
refcount: file.refcount + 1,
lastTouchedAt: Date.now(),
});
}
/**
* Get files that are unused and can be deleted.
* This is useful for cleaning up files that are no longer needed.
* Note: recently added files that have not been saved yet will show up here.
* You can inspect the `lastTouchedAt` field to see how recently it was used.
* I'd recommend not deleting anything touched in the last 24 hours.
*/
export const getFilesToDelete = query({
args: {
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
const files = await paginator(ctx.db, schema)
.query("files")
.withIndex("refcount", (q) => q.eq("refcount", 0))
.paginate(args.paginationOpts);
return files;
},
returns: v.object({
page: v.array(v.doc("files")),
continueCursor: v.string(),
isDone: v.boolean(),
}),
});
export const deleteFiles = mutation({
args: {
fileIds: v.array(v.id("files")),
force: v.optional(v.boolean()),
},
returns: v.array(v.id("files")),
handler: async (ctx, args) => {
const deletedFileIds = await Promise.all(args.fileIds.map(async (fileId) => {
const file = await ctx.db.get(fileId);
if (!file) {
console.error(`File ${fileId} not found when deleting, skipping...`);
return null;
}
if (file.refcount && file.refcount > 0) {
if (!args.force) {
console.error(`File ${fileId} has refcount ${file.refcount} > 0, skipping...`);
return null;
}
}
await ctx.db.delete(fileId);
return fileId;
}));
return deletedFileIds.filter((fileId) => fileId !== null);
},
});
//# sourceMappingURL=files.js.map