feat(web): add table attachments and a public entry form

Store attachment files in object storage and keep only resource references in the table. A separate public form link appends one row without showing existing records.
This commit is contained in:
tianma-if committed 2026-09-23 13:03:12 +08:00
1 parent 615a25f501
commit c7f6e342be
23 files changed
+1787 -17

No files matched your search

@@ -1,6 +1,7 @@
import { Database } from "bun:sqlite";
import { describe, expect, test } from "bun:test";
import { globSync, readFileSync } from "node:fs";
import { addTableField, createDefaultTableDocument, serializeTableDocument, updateTableCell } from "@edgeever/shared";
import {
acquireMaintenanceLease,
createMemoEditSession,
@@ -161,6 +162,55 @@ describe("database write optimizations", () => {
sqlite.close();
});
test("keeps a structured table intact and retires an attachment removed from it", async () => {
const { database, sqlite } = createDatabase();
const memo = getSeedMemo(sqlite);
let document = addTableField(createDefaultTableDocument(), { id: "fld_files", name: "附件", type: "attachment" });
document = updateTableCell(document, "rec_sample", "fld_files", [{
resourceId: "res_table_file",
filename: "brief.pdf",
mimeType: "application/pdf",
byteSize: 12,
}]);
sqlite.query(
`INSERT INTO resources (id, memo_id, object_key, kind, mime_type, filename, byte_size)
VALUES ('res_table_file', ?, 'workspaces/test/brief.pdf', 'attachment', 'application/pdf', 'brief.pdf', 12)`,
).run(memo.id);
const actor = { actorType: "user", actorId: "user_owner" };
const saved = await updateMemoRecord(
database,
memo.workspace_id,
memo.id,
{ contentMarkdown: serializeTableDocument(document) },
actor,
"owner",
);
expect(saved.memo.contentMarkdown).toContain("edgeever-table-v1");
const replaced = await updateMemoRecord(
database,
memo.workspace_id,
memo.id,
{ contentMarkdown: "普通笔记" },
actor,
"owner",
);
expect(replaced.error).toBe("table_update_required");
expect(sqlite.query("SELECT content_markdown FROM memo_contents WHERE memo_id = ?").get(memo.id).content_markdown).toContain("edgeever-table-v1");
const removed = await updateMemoRecord(
database,
memo.workspace_id,
memo.id,
{ contentMarkdown: serializeTableDocument(updateTableCell(document, "rec_sample", "fld_files", [])) },
actor,
"owner",
);
expect(removed.releasedResources?.map((resource) => resource.id)).toEqual(["res_table_file"]);
expect(sqlite.query("SELECT is_deleted FROM resources WHERE id = 'res_table_file'").get().is_deleted).toBe(1);
sqlite.close();
});
test("allows only one live maintenance lease owner", async () => {
const { database, sqlite } = createDatabase();
const first = await acquireMaintenanceLease(database, "demo-reset-test", 60_000);