mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
Fix browser file operations and persist visible save times
Preserve JSON Content-Type when custom idempotency headers are supplied. Expose the latest accepted folder operation timestamp so browser saves remain visible before an agent runs and after all files are deleted. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
23407236b4
commit
b4c3bf6961
8 files changed
+46
-6
No files matched your search
@@ -57,6 +57,18 @@ describe("durable work folders", () => {
|
||||
expect(await textContent(f, "memory.md")).toBe("second");
|
||||
await expect(svc.write(f, { ...first, body: Buffer.from("different") })).rejects.toMatchObject({ status: 409 });
|
||||
});
|
||||
it("retains the last accepted save time after all files are removed", async () => {
|
||||
const f = await folder();
|
||||
expect((await svc.list(f)).lastSavedAt).toBeNull();
|
||||
await svc.write(f, { path: "note", body: Buffer.from("saved"), operationId: "write" });
|
||||
const saved = (await svc.list(f)).lastSavedAt;
|
||||
expect(saved).toEqual(expect.any(String));
|
||||
await svc.remove(f, "note", "delete");
|
||||
const listing = await svc.list(f);
|
||||
expect(listing.files).toEqual([]);
|
||||
expect(Date.parse(listing.lastSavedAt!)).toBeGreaterThanOrEqual(Date.parse(saved!));
|
||||
expect((await svc.list(f, { trash: true })).lastSavedAt).toBe(listing.lastSavedAt);
|
||||
});
|
||||
it("retains a deleted copy after the same path is recreated", async () => {
|
||||
const f = await folder();
|
||||
await svc.write(f, { path: "note", body: Buffer.from("deleted"), operationId: "one" });
|
||||
|
||||
@@ -5276,7 +5276,7 @@ const workFolderErrors = { 400: r.badRequest, 401: r.unauthorized, 404: r.notFou
|
||||
registry.registerPath({ method: "get", path: workFolderPath, tags: ["work-folders"], summary: "List scoped sandbox files or recoverable trash",
|
||||
description: "User files require the owning user or an authorized run acting for that user. Company access alone does not grant access.",
|
||||
request: { params: workFolderParams, query: z.object({ trash: z.enum(["true", "false"]).optional(), cursor: z.uuid().optional(), limit: z.coerce.number().int().min(1).max(1000).optional() }) },
|
||||
responses: { ...workFolderErrors, 200: r.ok(z.object({ id: z.uuid(), owner: workFolderParams, files: z.array(workFileResponse), nextCursor: z.string().nullable() })) },
|
||||
responses: { ...workFolderErrors, 200: r.ok(z.object({ id: z.uuid(), owner: workFolderParams, files: z.array(workFileResponse), nextCursor: z.string().nullable(), lastSavedAt: z.string().nullable() })) },
|
||||
});
|
||||
registry.registerPath({ method: "get", path: `${workFolderPath}/content`, tags: ["work-folders"], summary: "Download a scoped file",
|
||||
request: { params: workFolderParams, query: z.object({ path: z.string() }) },
|
||||
|
||||
@@ -5,7 +5,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Readable, Transform } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { and, asc, eq, gt, isNull, isNotNull, sql } from "drizzle-orm";
|
||||
import { and, asc, eq, gt, isNull, isNotNull, max, sql } from "drizzle-orm";
|
||||
import { workFolders, workFiles, workFileOperations, workFolderObjects, type Db } from "@paperclipai/db";
|
||||
import { validateWorkFilePath, type WorkFile, type WorkFolderOwner } from "@paperclipai/shared";
|
||||
import { registerWorkFolderObject } from "./work-folder-garbage.js";
|
||||
@@ -41,7 +41,10 @@ export function workFolderService(db: Db, storage: StorageProvider) {
|
||||
const rows = await db.select().from(workFiles).where(and(eq(workFiles.folderId, folder.id),
|
||||
eq(workFiles.companyId, folder.companyId), options.trash ? isNotNull(workFiles.deletedAt) : isNull(workFiles.deletedAt),
|
||||
options.cursor ? gt(workFiles.id, options.cursor) : undefined)).orderBy(asc(workFiles.id)).limit(limit + 1);
|
||||
const [saved] = await db.select({ at: max(workFileOperations.createdAt) }).from(workFileOperations)
|
||||
.where(and(eq(workFileOperations.companyId, folder.companyId), eq(workFileOperations.folderId, folder.id)));
|
||||
return { id: folder.id, owner: { companyId: folder.companyId, scope: folder.scope, ownerId: folder.ownerId },
|
||||
lastSavedAt: saved?.at?.toISOString() ?? null,
|
||||
files: rows.slice(0, limit).map(workFileDto), nextCursor: rows.length > limit ? rows[limit - 1]!.id : null };
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user