2023-08-17 15:55:05 +00:00
|
|
|
module data;
|
|
|
|
|
|
|
|
import handy_httpd;
|
|
|
|
import d2sqlite3;
|
|
|
|
|
|
|
|
import std.path;
|
|
|
|
import std.file;
|
|
|
|
import std.stdio;
|
|
|
|
import std.typecons;
|
|
|
|
import std.string;
|
|
|
|
import std.json;
|
|
|
|
|
|
|
|
static UserDataSource userDataSource;
|
|
|
|
|
|
|
|
static this() {
|
|
|
|
userDataSource = new FsSqliteDataSource();
|
2023-08-18 01:38:10 +00:00
|
|
|
import slf4d;
|
|
|
|
import d2sqlite3.library;
|
|
|
|
infoF!"Sqlite version %s"(versionString());
|
2023-08-17 15:55:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
struct User {
|
|
|
|
string username;
|
|
|
|
string email;
|
|
|
|
string passwordHash;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct NoteList {
|
|
|
|
ulong id;
|
|
|
|
string name;
|
|
|
|
uint ordinality;
|
|
|
|
string description;
|
|
|
|
Note[] notes;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Note {
|
|
|
|
ulong id;
|
|
|
|
ulong noteListId;
|
|
|
|
uint ordinality;
|
|
|
|
string content;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface UserDataSource {
|
|
|
|
User createUser(string username, string email, string passwordHash);
|
|
|
|
void deleteUser(string username);
|
|
|
|
Nullable!User getUser(string username);
|
|
|
|
NoteList[] getLists(string username);
|
2023-08-18 01:38:10 +00:00
|
|
|
Nullable!NoteList getList(string username, ulong id);
|
2023-08-17 15:55:05 +00:00
|
|
|
NoteList createNoteList(string username, string name, string description = null);
|
|
|
|
void deleteNoteList(string username, ulong id);
|
2023-08-18 01:38:10 +00:00
|
|
|
NoteList updateNoteList(string username, ulong id, NoteList newData);
|
2023-08-17 15:55:05 +00:00
|
|
|
Note createNote(string username, ulong noteListId, string content);
|
2023-08-18 01:38:10 +00:00
|
|
|
Note updateNote(string username, ulong id, Note newData);
|
2023-08-17 15:55:05 +00:00
|
|
|
void deleteNote(string username, ulong id);
|
|
|
|
}
|
|
|
|
|
|
|
|
private immutable string USERS_DIR = "users";
|
|
|
|
private immutable string DATA_FILE = "user.json";
|
|
|
|
private immutable string DB_FILE = "notes.sqlite";
|
|
|
|
|
|
|
|
class FsSqliteDataSource : UserDataSource {
|
|
|
|
User createUser(string username, string email, string passwordHash) {
|
|
|
|
string dirPath = buildPath(USERS_DIR, username);
|
|
|
|
if (exists(dirPath)) throw new Exception("User already has a directory.");
|
|
|
|
mkdir(dirPath);
|
|
|
|
string dataPath = buildPath(dirPath, DATA_FILE);
|
|
|
|
JSONValue userObj = JSONValue(string[string].init);
|
|
|
|
userObj.object["username"] = username;
|
|
|
|
userObj.object["email"] = email;
|
|
|
|
userObj.object["passwordHash"] = passwordHash;
|
|
|
|
std.file.write(dataPath, userObj.toPrettyString());
|
|
|
|
|
|
|
|
// Set up a default list.
|
|
|
|
NoteList defaultList = this.createNoteList(username, "Default", "Your default list of notes.");
|
|
|
|
this.createNote(username, defaultList.id, "Here's an example note that was added to the Default list.");
|
|
|
|
|
|
|
|
return User(username, email, passwordHash);
|
|
|
|
}
|
|
|
|
|
|
|
|
void deleteUser(string username) {
|
|
|
|
string dirPath = buildPath(USERS_DIR, username);
|
|
|
|
if (exists(dirPath)) rmdirRecurse(dirPath);
|
|
|
|
}
|
|
|
|
|
|
|
|
Nullable!User getUser(string username) {
|
|
|
|
string dataPath = buildPath(USERS_DIR, username, DATA_FILE);
|
|
|
|
if (exists(dataPath) && isFile(dataPath)) {
|
|
|
|
JSONValue userObj = parseJSON(strip(readText(dataPath)));
|
|
|
|
return nullable(User(
|
|
|
|
userObj.object["username"].str,
|
|
|
|
userObj.object["email"].str,
|
|
|
|
userObj.object["passwordHash"].str
|
|
|
|
));
|
|
|
|
}
|
|
|
|
return Nullable!User.init;
|
|
|
|
}
|
|
|
|
|
|
|
|
NoteList[] getLists(string username) {
|
|
|
|
Database db = getDb(username);
|
|
|
|
ResultRange results = db.execute("SELECT * FROM note_list ORDER BY ordinality ASC");
|
|
|
|
NoteList[] lists;
|
|
|
|
foreach (Row row; results) {
|
|
|
|
lists ~= parseNoteList(row);
|
|
|
|
}
|
|
|
|
// Now eager-fetch notes for each list.
|
|
|
|
Statement stmt = db.prepare("SELECT * FROM note WHERE note_list_id = ? ORDER BY ordinality ASC");
|
|
|
|
foreach (ref list; lists) {
|
|
|
|
stmt.bind(1, list.id);
|
|
|
|
ResultRange noteResult = stmt.execute();
|
|
|
|
foreach (row; noteResult) list.notes ~= parseNote(row);
|
|
|
|
stmt.reset();
|
|
|
|
}
|
|
|
|
return lists;
|
|
|
|
}
|
|
|
|
|
2023-08-18 01:38:10 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-08-17 15:55:05 +00:00
|
|
|
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();
|
2023-08-18 01:38:10 +00:00
|
|
|
if (existsResult.oneValue!int() > 0) {
|
|
|
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "List already exists.");
|
|
|
|
}
|
2023-08-17 15:55:05 +00:00
|
|
|
|
|
|
|
Nullable!uint ordResult = db.execute("SELECT MAX(ordinality) + 1 FROM note_list").oneValue!(Nullable!uint);
|
|
|
|
uint ordinality = 0;
|
|
|
|
if (!ordResult.isNull) ordinality = ordResult.get();
|
|
|
|
Statement stmt = db.prepare("INSERT INTO note_list (name, ordinality, description) VALUES (?, ?, ?)");
|
|
|
|
stmt.bind(1, name);
|
|
|
|
stmt.bind(2, ordinality);
|
|
|
|
stmt.bind(3, description);
|
|
|
|
stmt.execute();
|
|
|
|
return NoteList(db.lastInsertRowid(), name, ordinality, description, []);
|
|
|
|
}
|
|
|
|
|
|
|
|
void deleteNoteList(string username, ulong id) {
|
|
|
|
Database db = getDb(username);
|
2023-08-18 01:38:10 +00:00
|
|
|
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, []);
|
2023-08-17 15:55:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Note createNote(string username, ulong noteListId, string content) {
|
|
|
|
Database db = getDb(username);
|
|
|
|
|
|
|
|
Statement ordStmt = db.prepare("SELECT MAX(ordinality) + 1 FROM note WHERE note_list_id = ?");
|
|
|
|
ordStmt.bind(1, noteListId);
|
|
|
|
Nullable!uint ordResult = ordStmt.execute().oneValue!(Nullable!uint);
|
|
|
|
uint ordinality = 0;
|
|
|
|
if (!ordResult.isNull) ordinality = ordResult.get();
|
|
|
|
|
|
|
|
Statement insertStmt = db.prepare("INSERT INTO note (note_list_id, ordinality, content) VALUES (?, ?, ?)");
|
|
|
|
insertStmt.bind(1, noteListId);
|
|
|
|
insertStmt.bind(2, ordinality);
|
|
|
|
insertStmt.bind(3, content);
|
|
|
|
insertStmt.execute();
|
|
|
|
return Note(
|
|
|
|
db.lastInsertRowid(),
|
|
|
|
noteListId,
|
|
|
|
ordinality,
|
|
|
|
content
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-08-18 01:38:10 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-08-17 15:55:05 +00:00
|
|
|
void deleteNote(string username, ulong id) {
|
|
|
|
Database db = getDb(username);
|
2023-08-18 01:38:10 +00:00
|
|
|
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();
|
2023-08-17 15:55:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private NoteList parseNoteList(Row row) {
|
|
|
|
NoteList list;
|
|
|
|
list.id = row["id"].as!ulong;
|
|
|
|
list.name = row["name"].as!string;
|
|
|
|
list.ordinality = row["ordinality"].as!uint;
|
|
|
|
list.description = row["description"].as!string;
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
private Note parseNote(Row row) {
|
|
|
|
Note note;
|
|
|
|
note.id = row["id"].as!ulong;
|
|
|
|
note.noteListId = row["note_list_id"].as!ulong;
|
|
|
|
note.ordinality = row["ordinality"].as!uint;
|
|
|
|
note.content = row["content"].as!string;
|
|
|
|
return note;
|
|
|
|
}
|
|
|
|
|
|
|
|
private Database getDb(string username) {
|
|
|
|
string dbPath = buildPath(USERS_DIR, username, DB_FILE);
|
|
|
|
if (!exists(dbPath)) initDb(dbPath);
|
|
|
|
return Database(dbPath);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void initDb(string path) {
|
|
|
|
if (exists(path)) std.file.remove(path);
|
|
|
|
Database db = Database(path);
|
|
|
|
db.run(q"SQL
|
|
|
|
CREATE TABLE note_list (
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
name TEXT NOT NULL,
|
|
|
|
ordinality INTEGER NOT NULL DEFAULT 0,
|
|
|
|
description TEXT NULL
|
|
|
|
);
|
|
|
|
|
|
|
|
CREATE TABLE note (
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
note_list_id INTEGER NOT NULL,
|
|
|
|
ordinality INTEGER NOT NULL DEFAULT 0,
|
|
|
|
content TEXT NOT NULL
|
|
|
|
);
|
|
|
|
SQL"
|
|
|
|
);
|
|
|
|
db.close();
|
|
|
|
}
|
|
|
|
}
|