feat: refine Evernote-style memo interactions
This commit is contained in:
1 parent
a9478eaf20
commit
d897523eff
5 files changed
+2148
-185
No files matched your search
@@ -5,6 +5,7 @@ import {
|
||||
docToText,
|
||||
emptyDoc,
|
||||
ApiTokenCreateSchema,
|
||||
DeleteMemosSchema,
|
||||
LoginSchema,
|
||||
markdownToDoc,
|
||||
MemoCreateSchema,
|
||||
@@ -718,6 +719,28 @@ app.post("/api/v1/memos/batch/move", zValidator("json", MoveMemosSchema), async
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/v1/memos/batch/delete", zValidator("json", DeleteMemosSchema), async (c) => {
|
||||
const denied = requireScopes(c, "write:memos");
|
||||
|
||||
if (denied) {
|
||||
return denied;
|
||||
}
|
||||
|
||||
const input = c.req.valid("json");
|
||||
const actor = getAuditActor(c);
|
||||
|
||||
try {
|
||||
const deleted = await deleteMemosRecord(c.env.DB, c.env.RESOURCES, input.memoIds, Boolean(input.permanent), actor);
|
||||
return c.json({ ok: true, deleted });
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return apiError(c, error.code, error.message, error.status);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/v1/memos/:id", async (c) => {
|
||||
const denied = requireScopes(c, "read:memos");
|
||||
|
||||
@@ -2460,6 +2483,88 @@ const getMemoDetail = async (db: D1Database, id: string, includeDeleted = false)
|
||||
return row ? mapMemoDetail(row) : null;
|
||||
};
|
||||
|
||||
const deleteMemosRecord = async (
|
||||
db: D1Database,
|
||||
resourcesBucket: R2Bucket,
|
||||
memoIds: string[],
|
||||
permanent: boolean,
|
||||
actor: { actorType: "user" | "agent"; actorId: string | null }
|
||||
) => {
|
||||
const uniqueMemoIds = Array.from(new Set(memoIds));
|
||||
|
||||
if (uniqueMemoIds.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const placeholders = uniqueMemoIds.map(() => "?").join(", ");
|
||||
const expectedDeletedState = permanent ? 1 : 0;
|
||||
const rows = await db
|
||||
.prepare(
|
||||
`SELECT id
|
||||
FROM memos
|
||||
WHERE is_deleted = ? AND id IN (${placeholders})`
|
||||
)
|
||||
.bind(expectedDeletedState, ...uniqueMemoIds)
|
||||
.all<{ id: string }>();
|
||||
|
||||
if (rows.results.length !== uniqueMemoIds.length) {
|
||||
throw new AppError(
|
||||
"missing_memos",
|
||||
permanent ? "One or more memos cannot be permanently deleted." : "One or more memos cannot be deleted.",
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const now = isoNow();
|
||||
const statements: D1PreparedStatement[] = [];
|
||||
|
||||
if (permanent) {
|
||||
const resourceRows = await db
|
||||
.prepare(
|
||||
`SELECT object_key
|
||||
FROM resources
|
||||
WHERE memo_id IN (${placeholders})`
|
||||
)
|
||||
.bind(...uniqueMemoIds)
|
||||
.all<{ object_key: string }>();
|
||||
const objectKeys = resourceRows.results.map((resource) => resource.object_key);
|
||||
|
||||
if (objectKeys.length > 0) {
|
||||
await resourcesBucket.delete(objectKeys);
|
||||
}
|
||||
|
||||
statements.push(
|
||||
db.prepare(`DELETE FROM memos_fts WHERE memo_id IN (${placeholders})`).bind(...uniqueMemoIds),
|
||||
db.prepare(`DELETE FROM resources WHERE memo_id IN (${placeholders})`).bind(...uniqueMemoIds),
|
||||
db.prepare(`DELETE FROM memo_revisions WHERE memo_id IN (${placeholders})`).bind(...uniqueMemoIds),
|
||||
db.prepare(`DELETE FROM memo_contents WHERE memo_id IN (${placeholders})`).bind(...uniqueMemoIds),
|
||||
db.prepare(`DELETE FROM memos WHERE is_deleted = 1 AND id IN (${placeholders})`).bind(...uniqueMemoIds)
|
||||
);
|
||||
|
||||
for (const memoId of uniqueMemoIds) {
|
||||
statements.push(auditStatement(db, actor.actorType, actor.actorId, "memo.delete_permanent", "memo", memoId, {}));
|
||||
}
|
||||
} else {
|
||||
statements.push(
|
||||
db
|
||||
.prepare(
|
||||
`UPDATE memos
|
||||
SET is_deleted = 1, deleted_at = ?, updated_at = ?
|
||||
WHERE is_deleted = 0 AND id IN (${placeholders})`
|
||||
)
|
||||
.bind(now, now, ...uniqueMemoIds),
|
||||
db.prepare(`DELETE FROM memos_fts WHERE memo_id IN (${placeholders})`).bind(...uniqueMemoIds)
|
||||
);
|
||||
|
||||
for (const memoId of uniqueMemoIds) {
|
||||
statements.push(auditStatement(db, actor.actorType, actor.actorId, "memo.delete", "memo", memoId, {}));
|
||||
}
|
||||
}
|
||||
|
||||
await db.batch(statements);
|
||||
return uniqueMemoIds.length;
|
||||
};
|
||||
|
||||
const moveMemosToNotebook = async (
|
||||
db: D1Database,
|
||||
memoIds: string[],
|
||||
|
||||
+1977
-185
File diff suppressed because it is too large.
Load diff
@@ -184,6 +184,12 @@ export const api = {
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
|
||||
deleteMemos: (payload: { memoIds: string[]; permanent?: boolean }) =>
|
||||
request<{ ok: true; deleted: number }>("/api/v1/memos/batch/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
|
||||
getMemo: (memoId: string, options?: { includeDeleted?: boolean }) => {
|
||||
const search = new URLSearchParams();
|
||||
|
||||
|
||||
@@ -459,6 +459,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/memos/batch/delete": {
|
||||
"post": {
|
||||
"summary": "Delete memos",
|
||||
"description": "Soft-deletes active memos, or permanently deletes trashed memos when permanent is true.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteMemosInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Delete result",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleted": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": ["ok", "deleted"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/memos/merge": {
|
||||
"post": {
|
||||
"summary": "Merge memos",
|
||||
@@ -1179,6 +1216,23 @@
|
||||
},
|
||||
"required": ["memoIds", "notebookId"]
|
||||
},
|
||||
"DeleteMemosInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memoIds": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 100,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"permanent": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["memoIds"]
|
||||
},
|
||||
"MergeMemosInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -32,6 +32,11 @@ export const MoveMemosSchema = z.object({
|
||||
notebookId: z.string().trim().min(1),
|
||||
});
|
||||
|
||||
export const DeleteMemosSchema = z.object({
|
||||
memoIds: z.array(z.string().trim().min(1)).min(1).max(100),
|
||||
permanent: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const MergeMemosSchema = z.object({
|
||||
memoIds: z.array(z.string().trim().min(1)).min(2).max(50),
|
||||
notebookId: z.string().trim().min(1).optional(),
|
||||
@@ -58,6 +63,7 @@ export type NotebookUpdateInput = z.infer<typeof NotebookUpdateSchema>;
|
||||
export type MemoCreateInput = z.infer<typeof MemoCreateSchema>;
|
||||
export type MemoUpdateInput = z.infer<typeof MemoUpdateSchema>;
|
||||
export type MoveMemosInput = z.infer<typeof MoveMemosSchema>;
|
||||
export type DeleteMemosInput = z.infer<typeof DeleteMemosSchema>;
|
||||
export type MergeMemosInput = z.infer<typeof MergeMemosSchema>;
|
||||
export type LoginInput = z.infer<typeof LoginSchema>;
|
||||
export type ApiTokenCreateInput = z.infer<typeof ApiTokenCreateSchema>;
|
||||
|
||||
Reference in new issue
Block a user