-
Notifications
You must be signed in to change notification settings - Fork 8
Add soft delete #468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add soft delete #468
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -151,7 +151,9 @@ export class LocalPermissionsProvider implements PermissionsProvider { | |
|
|
||
| // Get owner | ||
| const owner = await this.db | ||
| .prepare("SELECT owner_id, created_at FROM notebooks WHERE id = ?") | ||
| .prepare( | ||
| "SELECT owner_id, created_at FROM notebooks WHERE id = ? AND deleted_at IS NULL" | ||
| ) | ||
| .bind(notebookId) | ||
| .first<{ owner_id: string; created_at: string }>(); | ||
|
|
||
|
|
@@ -192,7 +194,9 @@ export class LocalPermissionsProvider implements PermissionsProvider { | |
| async isOwner(userId: string, notebookId: string): Promise<boolean> { | ||
| try { | ||
| const result = await this.db | ||
| .prepare("SELECT 1 FROM notebooks WHERE id = ? AND owner_id = ?") | ||
| .prepare( | ||
| "SELECT 1 FROM notebooks WHERE id = ? AND owner_id = ? AND deleted_at IS NULL" | ||
| ) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same for this one. Someone can still own a deleted notebook. |
||
| .bind(notebookId, userId) | ||
| .first(); | ||
|
|
||
|
|
@@ -218,7 +222,9 @@ export class LocalPermissionsProvider implements PermissionsProvider { | |
| // Always include owned notebooks (owner implies all permissions) | ||
| if (!permissions || permissions.includes("owner")) { | ||
| const ownedNotebooks = await this.db | ||
| .prepare("SELECT id FROM notebooks WHERE owner_id = ?") | ||
| .prepare( | ||
| "SELECT id FROM notebooks WHERE owner_id = ? AND deleted_at IS NULL" | ||
| ) | ||
| .bind(userId) | ||
| .all<{ id: string }>(); | ||
|
|
||
|
|
@@ -229,7 +235,9 @@ export class LocalPermissionsProvider implements PermissionsProvider { | |
| if (!permissions || permissions.includes("writer")) { | ||
| const sharedNotebooks = await this.db | ||
| .prepare( | ||
| "SELECT notebook_id FROM notebook_permissions WHERE user_id = ? AND permission = 'writer'" | ||
| `SELECT np.notebook_id FROM notebook_permissions np | ||
| INNER JOIN notebooks n ON np.notebook_id = n.id | ||
| WHERE np.user_id = ? AND np.permission = 'writer' AND n.deleted_at IS NULL` | ||
| ) | ||
| .bind(userId) | ||
| .all<{ notebook_id: string }>(); | ||
|
|
@@ -263,7 +271,7 @@ export class LocalPermissionsProvider implements PermissionsProvider { | |
| const placeholders = resourceIds.map(() => "?").join(","); | ||
| const ownedNotebooks = await this.db | ||
| .prepare( | ||
| `SELECT id FROM notebooks WHERE owner_id = ? AND id IN (${placeholders})` | ||
| `SELECT id FROM notebooks WHERE owner_id = ? AND deleted_at IS NULL AND id IN (${placeholders})` | ||
| ) | ||
| .bind(userId, ...resourceIds) | ||
| .all<{ id: string }>(); | ||
|
|
@@ -273,8 +281,9 @@ export class LocalPermissionsProvider implements PermissionsProvider { | |
| // Check notebooks with writer permissions | ||
| const sharedNotebooks = await this.db | ||
| .prepare( | ||
| `SELECT notebook_id FROM notebook_permissions | ||
| WHERE user_id = ? AND permission = 'writer' AND notebook_id IN (${placeholders})` | ||
| `SELECT np.notebook_id FROM notebook_permissions np | ||
| INNER JOIN notebooks n ON np.notebook_id = n.id | ||
| WHERE np.user_id = ? AND np.permission = 'writer' AND n.deleted_at IS NULL AND np.notebook_id IN (${placeholders})` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do think the |
||
| ) | ||
| .bind(userId, ...resourceIds) | ||
| .all<{ notebook_id: string }>(); | ||
|
|
@@ -344,31 +353,31 @@ export class LocalPermissionsProvider implements PermissionsProvider { | |
| if (owned && !shared) { | ||
| // Only owned notebooks | ||
| query = ` | ||
| SELECT id, owner_id, title, created_at, updated_at | ||
| SELECT id, owner_id, title, created_at, updated_at, deleted_at | ||
| FROM notebooks | ||
| WHERE owner_id = ? | ||
| WHERE owner_id = ? AND deleted_at IS NULL | ||
| ORDER BY updated_at DESC | ||
| LIMIT ? OFFSET ? | ||
| `; | ||
| bindings = [userId, limit, offset]; | ||
| } else if (shared && !owned) { | ||
| // Only shared notebooks (writer permissions) | ||
| query = ` | ||
| SELECT n.id, n.owner_id, n.title, n.created_at, n.updated_at | ||
| SELECT n.id, n.owner_id, n.title, n.created_at, n.updated_at, n.deleted_at | ||
| FROM notebooks n | ||
| INNER JOIN notebook_permissions np ON n.id = np.notebook_id | ||
| WHERE np.user_id = ? AND np.permission = 'writer' | ||
| WHERE np.user_id = ? AND np.permission = 'writer' AND n.deleted_at IS NULL | ||
| ORDER BY n.updated_at DESC | ||
| LIMIT ? OFFSET ? | ||
| `; | ||
| bindings = [userId, limit, offset]; | ||
| } else { | ||
| // All accessible notebooks (owned + shared) | ||
| query = ` | ||
| SELECT DISTINCT n.id, n.owner_id, n.title, n.created_at, n.updated_at | ||
| SELECT DISTINCT n.id, n.owner_id, n.title, n.created_at, n.updated_at, n.deleted_at | ||
| FROM notebooks n | ||
| LEFT JOIN notebook_permissions np ON n.id = np.notebook_id | ||
| WHERE n.owner_id = ? OR (np.user_id = ? AND np.permission = 'writer') | ||
| WHERE (n.owner_id = ? OR (np.user_id = ? AND np.permission = 'writer')) AND n.deleted_at IS NULL | ||
| ORDER BY n.updated_at DESC | ||
| LIMIT ? OFFSET ? | ||
| `; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -225,7 +225,7 @@ export const appRouter = router({ | |
| } | ||
|
|
||
| const notebook = await DB.prepare( | ||
| "SELECT * FROM notebooks WHERE id = ?" | ||
| "SELECT * FROM notebooks WHERE id = ? AND deleted_at IS NULL" | ||
| ) | ||
| .bind(nbId) | ||
| .first<NotebookRow>(); | ||
|
|
@@ -267,8 +267,8 @@ export const appRouter = router({ | |
|
|
||
| const result = await DB.prepare( | ||
| ` | ||
| INSERT INTO notebooks (id, owner_id, title, created_at, updated_at) | ||
| VALUES (?, ?, ?, ?, ?) | ||
| INSERT INTO notebooks (id, owner_id, title, created_at, updated_at, deleted_at) | ||
| VALUES (?, ?, ?, ?, ?, NULL) | ||
| ` | ||
| ) | ||
| .bind(nbId, user.id, input.title, now, now) | ||
|
|
@@ -282,7 +282,7 @@ export const appRouter = router({ | |
| } | ||
|
|
||
| const notebook = await DB.prepare( | ||
| "SELECT * FROM notebooks WHERE id = ?" | ||
| "SELECT * FROM notebooks WHERE id = ? AND deleted_at IS NULL" | ||
| ) | ||
| .bind(nbId) | ||
| .first<NotebookRow>(); | ||
|
|
@@ -348,7 +348,7 @@ export const appRouter = router({ | |
| ` | ||
| UPDATE notebooks | ||
| SET ${updates.join(", ")} | ||
| WHERE id = ? | ||
| WHERE id = ? AND deleted_at IS NULL | ||
| ` | ||
| ) | ||
| .bind(...bindings) | ||
|
|
@@ -363,7 +363,7 @@ export const appRouter = router({ | |
|
|
||
| // Return updated notebook | ||
| const notebook = await DB.prepare( | ||
| "SELECT * FROM notebooks WHERE id = ?" | ||
| "SELECT * FROM notebooks WHERE id = ? AND deleted_at IS NULL" | ||
| ) | ||
| .bind(nbId) | ||
| .first<NotebookRow>(); | ||
|
|
@@ -378,7 +378,7 @@ export const appRouter = router({ | |
| } | ||
| }), | ||
|
|
||
| // Delete notebook | ||
| // Delete notebook (soft delete) | ||
| deleteNotebook: authedProcedure | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could probably call this |
||
| .input(z.object({ nbId: z.string() })) | ||
| .mutation(async (opts) => { | ||
|
|
@@ -400,15 +400,17 @@ export const appRouter = router({ | |
| }); | ||
| } | ||
|
|
||
| // Delete notebook (CASCADE will handle permissions) | ||
| const result = await DB.prepare("DELETE FROM notebooks WHERE id = ?") | ||
| .bind(nbId) | ||
| // Soft delete notebook by setting deleted_at timestamp | ||
| const result = await DB.prepare( | ||
| "UPDATE notebooks SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL" | ||
| ) | ||
| .bind(new Date().toISOString(), new Date().toISOString(), nbId) | ||
| .run(); | ||
|
|
||
| if (result.meta.changes === 0) { | ||
| throw new TRPCError({ | ||
| code: "NOT_FOUND", | ||
| message: "Notebook not found", | ||
| message: "Notebook not found or already deleted", | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -490,7 +492,7 @@ export const appRouter = router({ | |
|
|
||
| try { | ||
| const notebook = await DB.prepare( | ||
| "SELECT owner_id FROM notebooks WHERE id = ?" | ||
| "SELECT owner_id FROM notebooks WHERE id = ? AND deleted_at IS NULL" | ||
| ) | ||
| .bind(nbId) | ||
| .first<{ owner_id: string }>(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| -- Migration number: 0006 2025-08-28T21:06:37.003Z | ||
|
|
||
| -- Add deleted_at column to notebooks table for soft delete functionality | ||
| ALTER TABLE notebooks ADD COLUMN deleted_at TEXT DEFAULT NULL; | ||
|
|
||
| -- Create index for efficient filtering of non-deleted notebooks | ||
| CREATE INDEX idx_notebooks_deleted_at ON notebooks(deleted_at); | ||
|
|
||
| -- Create composite index for efficient queries that filter by owner and deleted status | ||
| CREATE INDEX idx_notebooks_owner_deleted ON notebooks(owner_id, deleted_at); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're listing permissions on this particular notebook, I feel like we don't have to do the delete check. That's metadata to worry about when choosing whether or not to show it in the notebook dashboard.