Files
edgeever/apps/api/src/database-write-optimizations.test.mjs
T
tianma-if c7f6e342be 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.
2026-09-23 13:03:12 +08:00

353 lines
14 KiB
JavaScript

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,
fetchEdgeEverApp,
mergeMemosRecord,
updateMemoRecord,
} from "./index.ts";
const executionContext = {
passThroughOnException() {},
waitUntil() {},
};
const sha256 = async (value) => {
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
};
class SqlitePreparedStatement {
constructor(sqlite, sql, bindings = []) {
this.sqlite = sqlite;
this.sql = sql;
this.bindings = bindings;
}
bind(...bindings) {
return new SqlitePreparedStatement(this.sqlite, this.sql, bindings);
}
async all() {
return { results: this.sqlite.query(this.sql).all(...this.bindings) };
}
async first() {
return this.sqlite.query(this.sql).get(...this.bindings) ?? null;
}
async run() {
this.sqlite.query(this.sql).run(...this.bindings);
return { success: true, meta: {} };
}
}
class SqliteD1Database {
constructor(sqlite) {
this.sqlite = sqlite;
}
prepare(sql) {
return new SqlitePreparedStatement(this.sqlite, sql);
}
async batch(statements) {
const results = [];
this.sqlite.transaction(() => {
for (const statement of statements) {
results.push(this.sqlite.query(statement.sql).run(...statement.bindings));
}
})();
return results;
}
}
const createDatabase = () => {
const sqlite = new Database(":memory:");
sqlite.exec("PRAGMA foreign_keys = ON");
for (const path of globSync("migrations/*.sql").sort()) {
sqlite.exec(readFileSync(path, "utf8"));
}
return { database: new SqliteD1Database(sqlite), sqlite };
};
const getSeedMemo = (sqlite) => sqlite.query(
`SELECT m.id, m.workspace_id, m.title, m.tags_json, m.notebook_id, m.is_pinned,
c.revision, c.content_hash
FROM memos m
INNER JOIN memo_contents c ON c.memo_id = m.id
WHERE m.is_deleted = 0
ORDER BY m.id
LIMIT 1`,
).get();
describe("database write optimizations", () => {
test("returns the completed merge when the same source set is retried", async () => {
const { database, sqlite } = createDatabase();
const first = getSeedMemo(sqlite);
const secondId = "memo_merge_retry_source";
sqlite.query(
`INSERT INTO memos (id, workspace_id, notebook_id, title, excerpt, tags_json, created_by, updated_by)
VALUES (?, ?, ?, 'Retry source', 'retry body', '[]', 'user', 'user')`,
).run(secondId, first.workspace_id, first.notebook_id);
sqlite.query(
`INSERT INTO memo_contents (memo_id, content_json, content_markdown, content_text, content_hash)
VALUES (?, '{"type":"doc","content":[]}', 'retry body', 'retry body', 'retry-hash')`,
).run(secondId);
const input = { memoIds: [first.id, secondId], title: "Idempotent merge" };
const actor = { actorType: "user", actorId: "user_owner" };
const completed = await mergeMemosRecord(database, first.workspace_id, input, actor, "owner");
const retried = await mergeMemosRecord(database, first.workspace_id, input, actor, "owner");
expect(retried.id).toBe(completed.id);
expect(retried.sourceMemoIds).toEqual(input.memoIds);
expect(sqlite.query("SELECT COUNT(*) AS count FROM memos WHERE is_deleted = 0 AND title = 'Idempotent merge'").get().count).toBe(1);
expect(sqlite.query("SELECT COUNT(*) AS count FROM audit_events WHERE action = 'memo.merge' AND entity_id = ?").get(completed.id).count).toBe(1);
sqlite.close();
});
test("reuses a matching live edit session without another insert", async () => {
const { database, sqlite } = createDatabase();
const memo = getSeedMemo(sqlite);
const auth = {
kind: "user",
actorType: "user",
actorId: "user_owner",
username: "owner",
displayName: "Owner",
scopes: [],
workspaceId: memo.workspace_id,
role: "owner",
};
const context = {
env: { storage: { db: database, resources: {} } },
get: (name) => name === "auth" ? auth : undefined,
};
const first = await createMemoEditSession(context, memo.id);
const countAfterFirst = sqlite.query("SELECT COUNT(*) AS count FROM memo_edit_sessions").get().count;
const second = await createMemoEditSession(context, memo.id);
expect(second).toEqual(first);
expect(sqlite.query("SELECT COUNT(*) AS count FROM memo_edit_sessions").get().count).toBe(countAfterFirst);
sqlite.close();
});
test("returns an unchanged memo without revision, audit, or search writes", async () => {
const { database, sqlite } = createDatabase();
const memo = getSeedMemo(sqlite);
const auditCount = sqlite.query("SELECT COUNT(*) AS count FROM audit_events").get().count;
const searchRow = sqlite.query(
"SELECT id, title, content_text, tags FROM memo_search_documents WHERE memo_id = ?",
).get(memo.id);
const result = await updateMemoRecord(
database,
memo.workspace_id,
memo.id,
{ title: memo.title },
{ actorType: "user", actorId: "user_owner" },
"owner",
);
expect(result.memo.revision).toBe(memo.revision);
expect(sqlite.query("SELECT COUNT(*) AS count FROM audit_events").get().count).toBe(auditCount);
expect(sqlite.query(
"SELECT id, title, content_text, tags FROM memo_search_documents WHERE memo_id = ?",
).get(memo.id)).toEqual(searchRow);
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);
const second = await acquireMaintenanceLease(database, "demo-reset-test", 60_000);
expect(first).toStartWith("lease_");
expect(second).toBeNull();
sqlite.query(
"UPDATE maintenance_leases SET expires_at = '2000-01-01T00:00:00.000Z' WHERE name = 'demo-reset-test'",
).run();
expect(await acquireMaintenanceLease(database, "demo-reset-test", 60_000)).toStartWith("lease_");
sqlite.close();
});
test("does not report a demo reset as successful when another reset owns the lease", async () => {
const { database, sqlite } = createDatabase();
expect(await acquireMaintenanceLease(database, "demo-reset", 60_000)).toStartWith("lease_");
const response = await fetchEdgeEverApp(
new Request("https://notes.example.com/api/v1/demo/reset", { method: "POST" }),
{
storage: { db: database, resources: {} },
EDGE_EVER_ALLOW_UNAUTHENTICATED: "true",
EDGE_EVER_DEMO_MODE: "true",
},
executionContext,
);
expect(response.status).toBe(409);
expect(response.headers.get("Retry-After")).toBe("60");
expect(await response.json()).toMatchObject({
error: { code: "demo_reset_in_progress" },
});
sqlite.close();
});
test("does not rewrite current demo seed data while listing notebooks", async () => {
const { database, sqlite } = createDatabase();
const resources = {
put: async () => {},
};
const request = () => fetchEdgeEverApp(
new Request("https://notes.example.com/api/v1/notebooks"),
{
storage: { db: database, resources },
EDGE_EVER_ALLOW_UNAUTHENTICATED: "true",
EDGE_EVER_DEMO_MODE: "true",
},
executionContext,
);
expect((await request()).status).toBe(200);
expect(sqlite.query(
"SELECT COUNT(*) AS count FROM memos WHERE id IN ('memo_demo_overview', 'memo_demo_overview_en')",
).get().count).toBe(2);
const changesAfterSeed = sqlite.query("SELECT total_changes() AS count").get().count;
expect((await request()).status).toBe(200);
expect((await request()).status).toBe(200);
expect(sqlite.query("SELECT total_changes() AS count").get().count).toBe(changesAfterSeed);
sqlite.query("UPDATE memos SET title = 'Changed demo title' WHERE id = 'memo_demo_overview'").run();
const changesBeforeRepair = sqlite.query("SELECT total_changes() AS count").get().count;
expect((await request()).status).toBe(200);
expect(sqlite.query("SELECT title FROM memos WHERE id = 'memo_demo_overview'").get().title).not.toBe("Changed demo title");
expect(sqlite.query("SELECT total_changes() AS count").get().count).toBeGreaterThan(changesBeforeRepair);
const changesAfterRepair = sqlite.query("SELECT total_changes() AS count").get().count;
expect((await request()).status).toBe(200);
expect(sqlite.query("SELECT total_changes() AS count").get().count).toBe(changesAfterRepair);
sqlite.close();
});
test("touches API tokens at most once per hour", async () => {
const { database, sqlite } = createDatabase();
const token = "edgeever_api_test_token";
const recent = new Date(Date.now() - 5 * 60_000).toISOString();
sqlite.query(
`INSERT INTO api_tokens (
id, workspace_id, name, token_hash, token_value, scopes_json, last_used_at
) VALUES ('tok_touch_test', 'ws_default', 'Touch test', ?, ?, '["read:notebooks"]', ?)`,
).run(await sha256(token), token, recent);
const request = () => fetchEdgeEverApp(
new Request("https://notes.example.com/api/v1/notebooks", {
headers: { Authorization: `Bearer ${token}` },
}),
{ storage: { db: database, resources: {} }, EDGE_EVER_AUTH_PASSWORD: "configured" },
executionContext,
);
expect((await request()).status).toBe(200);
expect(sqlite.query("SELECT last_used_at FROM api_tokens WHERE id = 'tok_touch_test'").get().last_used_at).toBe(recent);
const stale = new Date(Date.now() - 2 * 60 * 60_000).toISOString();
sqlite.query("UPDATE api_tokens SET last_used_at = ? WHERE id = 'tok_touch_test'").run(stale);
expect((await request()).status).toBe(200);
expect(sqlite.query("SELECT last_used_at FROM api_tokens WHERE id = 'tok_touch_test'").get().last_used_at).not.toBe(stale);
sqlite.close();
});
test("touches login sessions at most once per hour", async () => {
const { database, sqlite } = createDatabase();
const token = "edgeever_session_test_token";
const recent = new Date(Date.now() - 5 * 60_000).toISOString();
const now = new Date().toISOString();
sqlite.query(
`INSERT INTO users (id, username, password_hash, display_name)
VALUES ('user_touch_test', 'touch-test', 'scrypt$placeholder', 'Touch Test')`,
).run();
sqlite.query(
`INSERT INTO workspace_members (workspace_id, user_id, role)
VALUES ('ws_default', 'user_touch_test', 'owner')`,
).run();
sqlite.query(
`INSERT INTO sessions (
id, user_id, token_hash, device_id, expires_at, created_at, last_seen_at
) VALUES ('session_touch_test', 'user_touch_test', ?, 'test-device', ?, ?, ?)`,
).run(await sha256(token), new Date(Date.now() + 60 * 60_000).toISOString(), now, recent);
const request = () => fetchEdgeEverApp(
new Request("https://notes.example.com/api/v1/notebooks", {
headers: { Authorization: `Bearer ${token}` },
}),
{ storage: { db: database, resources: {} }, EDGE_EVER_AUTH_PASSWORD: "configured" },
executionContext,
);
expect((await request()).status).toBe(200);
expect(sqlite.query("SELECT last_seen_at FROM sessions WHERE id = 'session_touch_test'").get().last_seen_at).toBe(recent);
const stale = new Date(Date.now() - 61 * 60_000).toISOString();
sqlite.query("UPDATE sessions SET last_seen_at = ? WHERE id = 'session_touch_test'").run(stale);
expect((await request()).status).toBe(200);
expect(sqlite.query("SELECT last_seen_at FROM sessions WHERE id = 'session_touch_test'").get().last_seen_at).not.toBe(stale);
sqlite.close();
});
});