From 3a1092d4681f5d1fd44820d3e22d5666b63315a8 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Thu, 17 Aug 2023 21:38:10 -0400 Subject: [PATCH] Add note and add list functionality. --- litelist-api/source/app.d | 5 + litelist-api/source/data.d | 115 +++++++++++++-- litelist-api/source/lists.d | 52 ++++++- litelist-app/src/App.vue | 2 +- litelist-app/src/api/auth.ts | 4 + litelist-app/src/api/lists.ts | 56 +++++++- litelist-app/src/assets/base.css | 4 + litelist-app/src/assets/trash-emoji.svg | 50 +++++++ litelist-app/src/main.ts | 1 + litelist-app/src/router/index.ts | 20 ++- litelist-app/src/stores/auth.ts | 9 +- litelist-app/src/util.ts | 8 ++ litelist-app/src/views/ListsView.vue | 116 +++++++++++++-- litelist-app/src/views/LoginView.vue | 68 +++++++-- litelist-app/src/views/SingleListView.vue | 164 ++++++++++++++++++++++ 15 files changed, 625 insertions(+), 49 deletions(-) create mode 100644 litelist-app/src/assets/base.css create mode 100644 litelist-app/src/assets/trash-emoji.svg create mode 100644 litelist-app/src/util.ts create mode 100644 litelist-app/src/views/SingleListView.vue diff --git a/litelist-api/source/app.d b/litelist-api/source/app.d index 7351874..2fb91cc 100644 --- a/litelist-api/source/app.d +++ b/litelist-api/source/app.d @@ -4,6 +4,7 @@ import slf4d.default_provider; void main() { auto provider = new shared DefaultProvider(true, Levels.INFO); + provider.getLoggerFactory().setModuleLevelPrefix("handy_httpd", Levels.WARN); configureLoggingProvider(provider); HttpServer server = initServer(); @@ -23,6 +24,7 @@ private HttpServer initServer() { config.connectionQueueSize = 10; config.defaultHeaders["Access-Control-Allow-Origin"] = "*"; config.defaultHeaders["Access-Control-Allow-Credentials"] = "true"; + config.defaultHeaders["Access-Control-Allow-Methods"] = "*"; config.defaultHeaders["Vary"] = "origin"; config.defaultHeaders["Access-Control-Allow-Headers"] = "Authorization"; @@ -44,7 +46,10 @@ private HttpServer initServer() { mainHandler.addMapping(Method.GET, "/lists", &getNoteLists); mainHandler.addMapping(Method.POST, "/lists", &createNoteList); + mainHandler.addMapping(Method.GET, "/lists/{id}", &getNoteList); mainHandler.addMapping(Method.DELETE, "/lists/{id}", &deleteNoteList); + mainHandler.addMapping(Method.POST, "/lists/{listId}/notes", &createNote); + mainHandler.addMapping(Method.DELETE, "/lists/{listId}/notes/{noteId}", &deleteNote); return new HttpServer(mainHandler, config); } diff --git a/litelist-api/source/data.d b/litelist-api/source/data.d index 207aa48..0435084 100644 --- a/litelist-api/source/data.d +++ b/litelist-api/source/data.d @@ -14,6 +14,9 @@ static UserDataSource userDataSource; static this() { userDataSource = new FsSqliteDataSource(); + import slf4d; + import d2sqlite3.library; + infoF!"Sqlite version %s"(versionString()); } struct User { @@ -42,9 +45,12 @@ interface UserDataSource { void deleteUser(string username); Nullable!User getUser(string username); NoteList[] getLists(string username); + Nullable!NoteList getList(string username, ulong id); NoteList createNoteList(string username, string name, string description = null); void deleteNoteList(string username, ulong id); + NoteList updateNoteList(string username, ulong id, NoteList newData); Note createNote(string username, ulong noteListId, string content); + Note updateNote(string username, ulong id, Note newData); void deleteNote(string username, ulong id); } @@ -107,13 +113,26 @@ class FsSqliteDataSource : UserDataSource { return lists; } + Nullable!NoteList getList(string username, ulong id) { + Database db = getDb(username); + ResultRange results = db.execute("SELECT * FROM note_list WHERE id = ?", id); + if (results.empty()) return Nullable!NoteList.init; + NoteList list = parseNoteList(results.front()); + ResultRange noteResults = db.execute("SELECT * FROM note WHERE note_list_id = ? ORDER BY ordinality ASC", id); + foreach (Row row; noteResults) { + list.notes ~= parseNote(row); + } + return nullable(list); + } + NoteList createNoteList(string username, string name, string description = null) { Database db = getDb(username); - Statement existsStatement = db.prepare("SELECT COUNT(name) FROM note_list WHERE name = ?"); existsStatement.bind(1, name); ResultRange existsResult = existsStatement.execute(); - if (existsResult.oneValue!int() > 0) throw new HttpStatusException(HttpStatus.BAD_REQUEST, "List already exists."); + if (existsResult.oneValue!int() > 0) { + throw new HttpStatusException(HttpStatus.BAD_REQUEST, "List already exists."); + } Nullable!uint ordResult = db.execute("SELECT MAX(ordinality) + 1 FROM note_list").oneValue!(Nullable!uint); uint ordinality = 0; @@ -128,12 +147,47 @@ class FsSqliteDataSource : UserDataSource { void deleteNoteList(string username, ulong id) { Database db = getDb(username); - Statement stmt1 = db.prepare("DELETE FROM note WHERE note_list_id = ?"); - stmt1.bind(1, id); - stmt1.execute(); - Statement stmt2 = db.prepare("DELETE FROM note_list WHERE id = ?"); - stmt2.bind(1, id); - stmt2.execute(); + db.begin(); + db.execute("DELETE FROM note WHERE note_list_id = ?", id); + Nullable!uint ordinality = db.execute( + "SELECT ordinality FROM note_list WHERE id = ?", id + ).oneValue!(Nullable!uint)(); + db.execute("DELETE FROM note_list WHERE id = ?", id); + if (!ordinality.isNull) { + db.execute("UPDATE note_list SET ordinality = ordinality - 1 WHERE ordinality > ?", ordinality.get); + } + db.commit(); + } + + NoteList updateNoteList(string username, ulong id, NoteList newData) { + Database db = getDb(username); + ResultRange result = db.execute("SELECT * FROM note_list WHERE id = ?", id); + if (result.empty()) throw new HttpStatusException(HttpStatus.NOT_FOUND); + NoteList list = parseNoteList(result.front()); + db.begin(); + if (list.ordinality != newData.ordinality) { + if (newData.ordinality > list.ordinality) { + // Decrement all lists between the old index and the new one. + db.execute( + "UPDATE note_list SET ordinality = ordinality - 1 WHERE ordinality > ? AND ordinality <= ?", + list.ordinality, + newData.ordinality + ); + } else { + // Increment all lists between the old index and the new one. + db.execute( + "UPDATE note_list SET ordinality = ordinality + 1 WHERE ordinality >= ? AND ordinality < ?", + newData.ordinality, + list.ordinality + ); + } + } + db.execute( + "UPDATE note_list SET name = ?, description = ?, ordinality = ? WHERE id = ?", + newData.name, newData.description, newData.ordinality, id + ); + db.commit(); + return NoteList(id, newData.name, newData.ordinality, newData.description, []); } Note createNote(string username, ulong noteListId, string content) { @@ -158,11 +212,50 @@ class FsSqliteDataSource : UserDataSource { ); } + Note updateNote(string username, ulong id, Note newData) { + Database db = getDb(username); + ResultRange result = db.execute("SELECT * FROM note WHERE id = ?", id); + if (result.empty()) throw new HttpStatusException(HttpStatus.NOT_FOUND); + Note note = parseNote(result.front()); + db.begin(); + if (note.ordinality != newData.ordinality) { + if (newData.ordinality > note.ordinality) { + // Decrement all notes between the old index and the new one. + db.execute( + "UPDATE note SET ordinality = ordinality - 1 WHERE ordinality > ? AND ordinality <= ?", + note.ordinality, + newData.ordinality + ); + } else { + // Increment all notes between the old index and the new one. + db.execute( + "UPDATE note SET ordinality = ordinality + 1 WHERE ordinality >= ? AND ordinality < ?", + newData.ordinality, + note.ordinality + ); + } + } + db.execute( + "UPDATE note SET ordinality = ?, content = ? WHERE id = ?", + newData.ordinality, + newData.content, + id + ); + db.commit(); + return Note(id, note.noteListId, newData.ordinality, newData.content); + } + void deleteNote(string username, ulong id) { Database db = getDb(username); - Statement stmt = db.prepare("DELETE FROM note WHERE id = ?"); - stmt.bind(1, id); - stmt.execute(); + db.begin(); + Nullable!uint ordinality = db.execute( + "SELECT ordinality FROM note WHERE id = ?", id + ).oneValue!(Nullable!uint)(); + db.execute("DELETE FROM note WHERE id = ?", id); + if (!ordinality.isNull) { + db.execute("UPDATE note SET ordinality = ordinality - 1 WHERE ordinality > ?", ordinality.get); + } + db.commit(); } private NoteList parseNoteList(Row row) { diff --git a/litelist-api/source/lists.d b/litelist-api/source/lists.d index d619206..649ec87 100644 --- a/litelist-api/source/lists.d +++ b/litelist-api/source/lists.d @@ -2,6 +2,8 @@ module lists; import handy_httpd; import std.json; +import std.typecons; +import std.string; import auth; import data; @@ -17,22 +19,68 @@ void getNoteLists(ref HttpRequestContext ctx) { ctx.response.writeBodyString(listsArray.toString(), "application/json"); } +void getNoteList(ref HttpRequestContext ctx) { + if (!validateAuthenticatedRequest(ctx)) return; + AuthContext auth = AuthContextHolder.getOrThrow(); + ulong id = ctx.request.getPathParamAs!ulong("id"); + Nullable!NoteList optionalList = userDataSource.getList(auth.user.username, id); + if (!optionalList.isNull) { + ctx.response.writeBodyString(serializeList(optionalList.get()).toString(), "application/json"); + } else { + ctx.response.setStatus(HttpStatus.NOT_FOUND); + } +} + void createNoteList(ref HttpRequestContext ctx) { if (!validateAuthenticatedRequest(ctx)) return; AuthContext auth = AuthContextHolder.getOrThrow(); JSONValue requestBody = ctx.request.readBodyAsJson(); - string listName = requestBody.object["name"].str; - string description = requestBody.object["description"].str; + if ("name" !in requestBody.object) { + ctx.response.setStatus(HttpStatus.BAD_REQUEST); + ctx.response.writeBodyString("Missing required name for creating a new list."); + return; + } + string listName = strip(requestBody.object["name"].str); + if (listName.length < 3) { + ctx.response.setStatus(HttpStatus.BAD_REQUEST); + ctx.response.writeBodyString("List name is too short. Should be at least 3 characters."); + } + string description = null; + if ("description" in requestBody.object) { + description = strip(requestBody.object["description"].str); + } NoteList list = userDataSource.createNoteList(auth.user.username, listName, description); ctx.response.writeBodyString(serializeList(list).toString(), "application/json"); } +void createNote(ref HttpRequestContext ctx) { + if (!validateAuthenticatedRequest(ctx)) return; + AuthContext auth = AuthContextHolder.getOrThrow(); + ulong listId = ctx.request.getPathParamAs!ulong("listId"); + JSONValue requestBody = ctx.request.readBodyAsJson(); + if ("content" !in requestBody || requestBody.object["content"].type != JSONType.STRING || requestBody.object["content"].str.length < 1) { + ctx.response.setStatus(HttpStatus.BAD_REQUEST); + ctx.response.writeBodyString("Missing string content."); + return; + } + string content = requestBody.object["content"].str; + Note note = userDataSource.createNote(auth.user.username, listId, content); + ctx.response.writeBodyString(serializeNote(note).toString(), "application/json"); +} + void deleteNoteList(ref HttpRequestContext ctx) { if (!validateAuthenticatedRequest(ctx)) return; AuthContext auth = AuthContextHolder.getOrThrow(); userDataSource.deleteNoteList(auth.user.username, ctx.request.getPathParamAs!ulong("id")); } +void deleteNote(ref HttpRequestContext ctx) { + if (!validateAuthenticatedRequest(ctx)) return; + AuthContext auth = AuthContextHolder.getOrThrow(); + ulong noteId = ctx.request.getPathParamAs!ulong("noteId"); + userDataSource.deleteNote(auth.user.username, noteId); +} + private JSONValue serializeList(NoteList list) { JSONValue listObj = JSONValue(string[string].init); listObj.object["id"] = JSONValue(list.id); diff --git a/litelist-app/src/App.vue b/litelist-app/src/App.vue index d2c1397..d6a3e5e 100644 --- a/litelist-app/src/App.vue +++ b/litelist-app/src/App.vue @@ -1,5 +1,5 @@