Skip to content

Commit

Permalink
Support deleted notes filter for frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
mcndt committed Nov 21, 2022
1 parent 855e089 commit 017c318
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 35 deletions.
4 changes: 4 additions & 0 deletions server/src/controllers/note/note.delete.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { validateOrReject, ValidationError } from "class-validator";
import { NextFunction, Request, Response } from "express";
import { deleteNote, getNote } from "../../db/note.dao";
import checkId from "../../lib/checkUserId";
import { getNoteFilter } from "../../lib/expiredNoteFilter";
import EventLogger, { WriteEvent } from "../../logging/EventLogger";
import { getConnectingIp, getNoteSize } from "../../util";
import { NoteDeleteRequest } from "../../validation/Request";
Expand Down Expand Up @@ -70,4 +71,7 @@ export async function deleteNoteController(
await EventLogger.deleteEvent(event);
next(err);
}

const filter = await getNoteFilter("deletedNotes");
await filter.addNoteIds([note.id]);
}
18 changes: 14 additions & 4 deletions server/src/controllers/note/note.get.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express";
import { getExpiredNoteFilter } from "../../lib/expiredNoteFilter";
import { getNoteFilter } from "../../lib/expiredNoteFilter";
import EventLogger from "../../logging/EventLogger";
import { getConnectingIp, getNoteSize } from "../../util";
import { getNote } from "../../db/note.dao";
Expand All @@ -21,8 +21,18 @@ export async function getNoteController(
res.send(note);
} else {
// check the expired filter to see if the note was expired
const expiredFilter = await getExpiredNoteFilter();
if (expiredFilter.hasNoteId(req.params.id)) {
const deletedFilter = await getNoteFilter("deletedNotes");
const expiredFilter = await getNoteFilter("expiredNotes");

if (deletedFilter.hasNoteId(req.params.id)) {
await EventLogger.readEvent({
success: false,
host: ip,
note_id: req.params.id,
error: "Note deleted",
});
res.status(410).send("Note deleted");
} else if (expiredFilter.hasNoteId(req.params.id)) {
await EventLogger.readEvent({
success: false,
host: ip,
Expand All @@ -37,7 +47,7 @@ export async function getNoteController(
note_id: req.params.id,
error: "Note not found",
});
res.status(404).send();
res.status(404).send("Note not found");
}
}
})
Expand Down
45 changes: 27 additions & 18 deletions server/src/lib/expiredNoteFilter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { ScalableBloomFilter } from "bloom-filters";
import { getFilter, upsertFilter } from "../db/bloomFilter.dao";

export class ExpiredNoteFilter {
export const EXPIRED_NOTES_FILTER_NAME = "expiredNotes" as const;
export const DELETED_NOTES_FILTER_NAME = "deletedNotes" as const;

type FilterName =
| typeof EXPIRED_NOTES_FILTER_NAME
| typeof DELETED_NOTES_FILTER_NAME;
export class NoteIdFilter {
_filter: ScalableBloomFilter;
static FILTER_NAME = "expiredNotes";
_name: string;

private constructor(filter: ScalableBloomFilter) {
private constructor(name: string, filter: ScalableBloomFilter) {
this._filter = filter;
this._name = name;
}

public static async deserializeFromDb(): Promise<ExpiredNoteFilter> {
return ExpiredNoteFilter._deserializeFilter()
public static async deserializeFromDb(name: string): Promise<NoteIdFilter> {
return NoteIdFilter._deserializeFilter(name)
.catch((err) => {
if (err.message === "No BloomFilter found") {
return new ScalableBloomFilter();
Expand All @@ -19,7 +26,7 @@ export class ExpiredNoteFilter {
}
})
.then((filter) => {
return new ExpiredNoteFilter(filter);
return new NoteIdFilter(name, filter);
});
}

Expand All @@ -35,24 +42,26 @@ export class ExpiredNoteFilter {
}

private _serialize(): Promise<void> {
return upsertFilter(ExpiredNoteFilter.FILTER_NAME, this._filter);
return upsertFilter(this._name, this._filter);
}

private static _deserializeFilter(): Promise<ScalableBloomFilter> {
return getFilter<ScalableBloomFilter>(
this.FILTER_NAME,
ScalableBloomFilter
);
private static _deserializeFilter(
name: string
): Promise<ScalableBloomFilter> {
return getFilter<ScalableBloomFilter>(name, ScalableBloomFilter);
}
}

let _filter: ExpiredNoteFilter;
let _filters: Record<FilterName, NoteIdFilter | null> = {
expiredNotes: null,
deletedNotes: null,
};

export async function getExpiredNoteFilter(): Promise<ExpiredNoteFilter> {
if (_filter) {
return _filter;
export async function getNoteFilter(name: FilterName): Promise<NoteIdFilter> {
if (_filters[name] !== null) {
return _filters[name] as NoteIdFilter;
} else {
_filter = await ExpiredNoteFilter.deserializeFromDb();
return _filter;
_filters[name] = await NoteIdFilter.deserializeFromDb(name);
return _filters[name] as NoteIdFilter;
}
}
14 changes: 7 additions & 7 deletions server/src/lib/expiredNoteFilter.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { ExpiredNoteFilter } from "./expiredNoteFilter";
import { NoteIdFilter } from "./expiredNoteFilter";
import { ScalableBloomFilter } from "bloom-filters";

import * as dao from "../db/bloomFilter.dao";
Expand All @@ -16,12 +16,12 @@ describe("Deserialization from database", () => {
mockedDao.getFilter.mockRejectedValue(new Error("No BloomFilter found"));

// test instatiation
const testFilter = await ExpiredNoteFilter.deserializeFromDb();
const testFilter = await NoteIdFilter.deserializeFromDb();
expect(mockedDao.getFilter).toHaveBeenCalledWith(
"expiredNotes",
ScalableBloomFilter
);
expect(testFilter).toBeInstanceOf(ExpiredNoteFilter);
expect(testFilter).toBeInstanceOf(NoteIdFilter);

// expect the _filter property to be a fresh ScalableBloomFilter (capacity 8)
expect(testFilter._filter).toBeInstanceOf(ScalableBloomFilter);
Expand All @@ -37,7 +37,7 @@ describe("Deserialization from database", () => {
mockedDao.getFilter.mockResolvedValue(bloomFilter);

// test instatiation
const testFilter = await ExpiredNoteFilter.deserializeFromDb();
const testFilter = await NoteIdFilter.deserializeFromDb();
expect(mockedDao.getFilter).toHaveBeenCalledWith(
"expiredNotes",
ScalableBloomFilter
Expand All @@ -51,7 +51,7 @@ describe("Deserialization from database", () => {
});

describe("Filter operations and serialization", () => {
let testFilter: ExpiredNoteFilter;
let testFilter: NoteIdFilter;

beforeEach(async () => {
const mockedDao = vi.mocked(dao);
Expand All @@ -63,7 +63,7 @@ describe("Filter operations and serialization", () => {
});

it("should add multiple noteIds to the filter", async () => {
testFilter = await ExpiredNoteFilter.deserializeFromDb();
testFilter = await NoteIdFilter.deserializeFromDb();
testFilter.addNoteIds(["test", "test2"]);
expect(testFilter.hasNoteId("test")).toBe(true);
expect(testFilter.hasNoteId("test2")).toBe(true);
Expand All @@ -77,7 +77,7 @@ describe("Filter operations and serialization", () => {
});

it("Should have an error rate <1% for 1000 elements", async () => {
testFilter = await ExpiredNoteFilter.deserializeFromDb();
testFilter = await NoteIdFilter.deserializeFromDb();
const elements = Array.from({ length: 1000 }, (_, i) => i.toString());
testFilter.addNoteIds(elements);

Expand Down
4 changes: 2 additions & 2 deletions server/src/tasks/deleteExpiredNotes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { deleteNotes, getExpiredNotes } from "../db/note.dao";
import { getExpiredNoteFilter } from "../lib/expiredNoteFilter";
import { getNoteFilter } from "../lib/expiredNoteFilter";
import EventLogger from "../logging/EventLogger";
import logger from "../logging/logger";
import { getNoteSize } from "../util";
Expand All @@ -23,7 +23,7 @@ export async function deleteExpiredNotes(): Promise<number> {
});
});
await Promise.all(logs);
const filter = await getExpiredNoteFilter();
const filter = await getNoteFilter("expiredNotes");
await filter.addNoteIds(toDelete.map((n) => n.id));
logger.info(`[Cleanup] Deleted ${deleteCount} expired notes.`);
return deleteCount;
Expand Down
2 changes: 1 addition & 1 deletion server/src/tasks/deleteExpiredNotes.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe("deleteExpiredNotes", () => {
mockedDao.deleteNotes.mockResolvedValue(1);

// mock ExpiredNoteFilter
const mockedFilter = vi.mocked(await filter.getExpiredNoteFilter());
const mockedFilter = vi.mocked(await filter.getNoteFilter());
mockedFilter.addNoteIds.mockResolvedValue();

// test task call
Expand Down
11 changes: 9 additions & 2 deletions webapp/src/routes/+error.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
<script lang="ts">
import { page } from '$app/stores';
console.log($page);
</script>

<div class="prose max-w-2xl prose-zinc dark:prose-invert">
{#if $page.status === 404}
<h1>404: No note found 🕵️</h1>
<p class="prose-xl">No note was found at this link. Are you from the future?</p>
{:else if $page.status === 410}
{:else if $page.status === 410 && $page.error?.message === 'Note expired'}
<h1>📝💨 This note is no longer here!</h1>
<p class="prose-xl">
Notes are stored for a limited amount of time. The note at this link was either set to expire,
or deleted due to inactivity. Sorry!
</p>
{:else if $page.status === 410 && $page.error?.message === 'Note deleted'}
<h1>📝🗑 This note has been deleted.</h1>
<p class="prose-xl">The note at this link has been deleted by the user who shared it. Sorry!</p>
{:else}
<h1>Something went wrong 🤔</h1>
<p class="prose-xl">
Expand All @@ -24,8 +29,10 @@
{/if}

<div class="not-prose w-full flex justify-center mt-16">
{#if $page.status === 404 || $page.status === 410}
{#if $page.status === 404 || ($page.status === 410 && $page.error?.message === 'Note expired')}
<img src="/expired_note.svg" alt="encrypted-art" class="w-80" />
{:else if $page.status === 410 && $page.error?.message === 'Note deleted'}
<img src="/deleted_note.svg" alt="encrypted-art" class="w-80" />
{/if}
</div>
</div>
4 changes: 3 additions & 1 deletion webapp/src/routes/note/[id]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const load: PageServerLoad = async ({ request, params, setHeaders, getCli
throw error(500, response.statusText);
}
} else {
throw error(response.status, response.statusText);
// get the response body (the reason why the request failed)
const body = await response.text();
throw error(response.status, body);
}
};
1 change: 1 addition & 0 deletions webapp/static/deleted_note.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 017c318

Please sign in to comment.