Soft-delete for notes — Daniel asked for "deleted notes go to trash"
so a slip of the finger doesn't lose work.
Schema: migrations/1777090000000_notes-trash.js adds a deleted_at
timestamptz column to personal_notes (NULL = active) plus an index
on (user_id, deleted_at).
Server (src/routes/notes.js):
GET /api/notes now filters deleted_at IS NULL
GET /api/notes/trash new — list trashed items, newest-
deleted first
DELETE /api/notes/:id now soft-deletes (sets deleted_at)
DELETE /api/notes/:id?hard=1 hard-delete, only allowed on items
already in trash (UI bug can't
erase an active note)
POST /api/notes/:id/restore pull a note out of trash
POST /api/notes/trash/empty hard-delete every trashed note for
the user
Frontend (public/components/notes.html + public/js/notes.js +
public/css/styles.css):
• Sidebar gets two tabs — "Notes" / "Trash (n)" with live count
• Trash tab shows deleted-at timestamps, Restore + delete-forever
per row, Empty-trash button at the bottom
• Active list and trash count refresh in parallel after every
save / delete / restore
• Delete button in the editor now says "Move to trash" and uses
the showConfirm helper (no native dialogs)
Sanitizer swap (public/js/notes.js):
Replaced the homegrown allowlist walker with DOMPurify (already
loaded from cdnjs in index.html, used by learningHub.js too).
Custom HTML sanitizers historically have bypasses; DOMPurify is
the right primitive.
Tests (test/notes-sanitize.test.js — node:test + jsdom + dompurify
as new dev deps):
9 contract tests covering script-tag stripping, inline event
handlers, img onerror, iframe/object, style attributes, every
preserved tag in the allowlist, javascript: URI rejection,
null/undefined input, and nested-script-inside-paragraph. Total
test count: 37 → 46 passing.
SW cache bumped to pedscribe-v12-notes5.
21 lines
746 B
JavaScript
21 lines
746 B
JavaScript
/**
|
|
* Soft-delete for personal_notes — Daniel asked for "deleted notes go to
|
|
* trash" so a slip of the finger doesn't lose work. Adds a deleted_at
|
|
* timestamp; NULL means active. Trash listing filters by NOT NULL,
|
|
* regular listing filters by NULL.
|
|
*
|
|
* Restore = clear deleted_at. Empty Trash = real DELETE. No retention
|
|
* policy yet — items stay in trash until the user empties it.
|
|
*/
|
|
|
|
exports.up = (pgm) => {
|
|
pgm.addColumn('personal_notes', {
|
|
deleted_at: { type: 'timestamptz', notNull: false, default: null },
|
|
});
|
|
pgm.createIndex('personal_notes', ['user_id', 'deleted_at']);
|
|
};
|
|
|
|
exports.down = (pgm) => {
|
|
pgm.dropIndex('personal_notes', ['user_id', 'deleted_at']);
|
|
pgm.dropColumn('personal_notes', 'deleted_at');
|
|
};
|