big dev changes
This commit is contained in:
+98
-5
@@ -53,6 +53,10 @@ pub struct CreateNoteRequest {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
password: Option<String>,
|
||||
#[serde(default)]
|
||||
protect: bool,
|
||||
#[serde(default)]
|
||||
created_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -84,6 +88,8 @@ pub struct NoteListItem {
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
url: String,
|
||||
protected: bool,
|
||||
created_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -93,6 +99,7 @@ pub struct NoteInfo {
|
||||
slug: String,
|
||||
title: String,
|
||||
protected: bool,
|
||||
note_protected: bool,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
@@ -141,6 +148,8 @@ pub async fn open_workspace(
|
||||
title: note.title,
|
||||
created_at: db::normalize_timestamp(¬e.created_at),
|
||||
updated_at: db::normalize_timestamp(¬e.updated_at),
|
||||
protected: note.protected,
|
||||
created_by: note.created_by,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -163,7 +172,8 @@ pub async fn create_note(
|
||||
}
|
||||
|
||||
let slug = unique_note_slug(&state, workspace.id, &base).await?;
|
||||
let note = db::create_note(&state.db, workspace.id, &slug, title).await?;
|
||||
let created_by = payload.created_by.as_deref().map(str::trim).filter(|v| !v.is_empty()).map(|v| v.chars().take(40).collect::<String>());
|
||||
let note = db::create_note(&state.db, workspace.id, &slug, title, payload.protect, created_by.as_deref()).await?;
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(NoteListItem {
|
||||
@@ -172,6 +182,8 @@ pub async fn create_note(
|
||||
title: note.title,
|
||||
created_at: db::normalize_timestamp(¬e.created_at),
|
||||
updated_at: db::normalize_timestamp(¬e.updated_at),
|
||||
protected: note.protected,
|
||||
created_by: note.created_by,
|
||||
}),
|
||||
))
|
||||
}
|
||||
@@ -193,6 +205,7 @@ pub async fn note_info(
|
||||
slug: note.slug,
|
||||
title: note.title,
|
||||
protected: workspace.password_hash.is_some(),
|
||||
note_protected: note.protected,
|
||||
created_at: db::normalize_timestamp(¬e.created_at),
|
||||
updated_at: db::normalize_timestamp(¬e.updated_at),
|
||||
}))
|
||||
@@ -547,8 +560,30 @@ pub async fn upload_pad_file(
|
||||
stored = format!("{stem}-{}{}", db::random_suffix(6), ext);
|
||||
path = dir.join(&stored);
|
||||
}
|
||||
tokio::fs::write(&path, bytes).await.map_err(|_| ApiError::internal("Failed to save the file"))?;
|
||||
Ok(Json(serde_json::json!({"name": stored, "url": format!("/f/{}/{}", file_token, stored)})))
|
||||
tokio::fs::write(&path, &bytes).await.map_err(|_| ApiError::internal("Failed to save the file"))?;
|
||||
let url = format!("/f/{}/{}", file_token, stored);
|
||||
let mime = mime_guess::from_path(&stored).first_or_octet_stream().to_string();
|
||||
db::register_pad_file(&state.db, pad.id, &stored, &url, &mime, bytes.len() as i64).await?;
|
||||
Ok(Json(serde_json::json!({"name": stored, "url": url})))
|
||||
}
|
||||
|
||||
pub async fn pad_files(
|
||||
State(state): State<SharedState>,
|
||||
Path(slug): Path<String>,
|
||||
Json(payload): Json<PasswordRequest>,
|
||||
) -> Result<Json<Vec<db::NoteFile>>, ApiError> {
|
||||
let pad = authorized_pad(&state, &slug, payload.password.as_deref()).await?;
|
||||
let mut files = db::list_pad_files(&state.db, pad.id).await?;
|
||||
for file in &mut files {
|
||||
let attached = pad.content.contains(&file.url);
|
||||
if attached != file.is_attached {
|
||||
db::set_pad_file_attached(&state.db, file.id, attached).await?;
|
||||
file.is_attached = attached;
|
||||
file.detached_at = if attached { None } else { Some(chrono::Utc::now().to_rfc3339()) };
|
||||
}
|
||||
file.created_at = db::normalize_timestamp(&file.created_at);
|
||||
}
|
||||
Ok(Json(files))
|
||||
}
|
||||
|
||||
pub async fn upload_note_file(
|
||||
@@ -586,11 +621,69 @@ pub async fn upload_note_file(
|
||||
stored = format!("{stem}-{}{}", db::random_suffix(6), ext);
|
||||
path = dir.join(&stored);
|
||||
}
|
||||
tokio::fs::write(&path, bytes).await.map_err(|_| ApiError::internal("Failed to save the file"))?;
|
||||
Ok(Json(serde_json::json!({"name": stored, "url": format!("/f/{}/{}", file_token, stored)})))
|
||||
tokio::fs::write(&path, &bytes).await.map_err(|_| ApiError::internal("Failed to save the file"))?;
|
||||
let url = format!("/f/{}/{}", file_token, stored);
|
||||
let mime = mime_guess::from_path(&stored).first_or_octet_stream().to_string();
|
||||
db::register_note_file(&state.db, note.id, &stored, &url, &mime, bytes.len() as i64).await?;
|
||||
Ok(Json(serde_json::json!({"name": stored, "url": url})))
|
||||
}
|
||||
|
||||
|
||||
pub async fn delete_note(
|
||||
State(state): State<SharedState>,
|
||||
Path((workspace_slug, note_slug)): Path<(String, String)>,
|
||||
Json(payload): Json<PasswordRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let (_workspace, note) = authorized_note(&state, &workspace_slug, ¬e_slug, payload.password.as_deref()).await?;
|
||||
if note.protected { return Err(ApiError::bad_request("This note is protected and cannot be deleted")); }
|
||||
db::delete_note(&state.db, note.id).await?;
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
pub async fn note_files(
|
||||
State(state): State<SharedState>,
|
||||
Path((workspace_slug, note_slug)): Path<(String, String)>,
|
||||
Json(payload): Json<PasswordRequest>,
|
||||
) -> Result<Json<Vec<db::NoteFile>>, ApiError> {
|
||||
let (_workspace, note) = authorized_note(&state, &workspace_slug, ¬e_slug, payload.password.as_deref()).await?;
|
||||
let mut files = db::list_note_files(&state.db, note.id).await?;
|
||||
for file in &mut files {
|
||||
let attached = note.content.contains(&file.url);
|
||||
if attached != file.is_attached {
|
||||
db::set_note_file_attached(&state.db, file.id, attached).await?;
|
||||
file.is_attached = attached;
|
||||
file.detached_at = if attached { None } else { Some(chrono::Utc::now().to_rfc3339()) };
|
||||
}
|
||||
file.created_at = db::normalize_timestamp(&file.created_at);
|
||||
}
|
||||
Ok(Json(files))
|
||||
}
|
||||
|
||||
pub async fn delete_note_file(
|
||||
State(state): State<SharedState>,
|
||||
Path((workspace_slug, note_slug, file_id)): Path<(String, String, i64)>,
|
||||
Json(payload): Json<PasswordRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let (workspace, note) = authorized_note(&state, &workspace_slug, ¬e_slug, payload.password.as_deref()).await?;
|
||||
if workspace.password_hash.is_none() || payload.password.as_deref().unwrap_or_default().is_empty() {
|
||||
return Err(ApiError::unauthorized());
|
||||
}
|
||||
let file = db::find_note_file(&state.db, note.id, file_id).await?
|
||||
.ok_or_else(ApiError::not_found_file)?;
|
||||
let relative = file.url.trim_start_matches('/').split('/').collect::<Vec<_>>();
|
||||
if relative.len() == 3 && relative[0] == "f" {
|
||||
let directory = format!("{}_{}", note.id, relative[1]);
|
||||
let path = std::path::Path::new(&state.files_dir).join("notes").join(directory).join(sanitize_filename(relative[2]));
|
||||
match tokio::fs::remove_file(&path).await {
|
||||
Ok(()) => {},
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {},
|
||||
Err(_) => return Err(ApiError::internal("Failed to delete the file")),
|
||||
}
|
||||
}
|
||||
db::delete_note_file(&state.db, note.id, file_id).await?;
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
pub async fn download_file(
|
||||
State(state): State<SharedState>,
|
||||
Path((token, filename)): Path<(String, String)>,
|
||||
|
||||
+7
-3
@@ -25,14 +25,14 @@ pub fn router(state: SharedState, static_dir: &str, upload_max_size_bytes: usize
|
||||
.route("/api/pads/{slug}/history", post(api::pad_history))
|
||||
.route("/api/pads/{slug}/publish", post(api::publish_pad_page))
|
||||
.route("/api/pads/{slug}/restore", post(api::pad_restore))
|
||||
.route("/api/pads/{slug}/files", post(api::upload_pad_file))
|
||||
.route("/api/pads/{slug}/files", post(api::upload_pad_file).put(api::pad_files))
|
||||
.route("/api/workspaces", post(api::create_workspace))
|
||||
.route("/api/workspaces/{workspace_slug}", get(api::workspace_info))
|
||||
.route("/api/workspaces/{workspace_slug}/open", post(api::open_workspace))
|
||||
.route("/api/workspaces/{workspace_slug}/notes", post(api::create_note))
|
||||
.route(
|
||||
"/api/workspaces/{workspace_slug}/notes/{note_slug}",
|
||||
get(api::note_info),
|
||||
get(api::note_info).delete(api::delete_note),
|
||||
)
|
||||
.route(
|
||||
"/api/workspaces/{workspace_slug}/notes/{note_slug}/publish",
|
||||
@@ -48,7 +48,11 @@ pub fn router(state: SharedState, static_dir: &str, upload_max_size_bytes: usize
|
||||
)
|
||||
.route(
|
||||
"/api/workspaces/{workspace_slug}/notes/{note_slug}/files",
|
||||
post(api::upload_note_file),
|
||||
post(api::upload_note_file).put(api::note_files),
|
||||
)
|
||||
.route(
|
||||
"/api/workspaces/{workspace_slug}/notes/{note_slug}/files/{file_id}",
|
||||
axum::routing::delete(api::delete_note_file),
|
||||
)
|
||||
.route("/ws/p/{slug}", get(websocket::upgrade_pad))
|
||||
.route(
|
||||
|
||||
@@ -44,6 +44,39 @@ pub struct Note {
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub owner_map: String,
|
||||
pub protected: bool,
|
||||
pub created_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
struct SqliteNote {
|
||||
id: i64,
|
||||
workspace_id: i64,
|
||||
slug: String,
|
||||
title: String,
|
||||
content: String,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
owner_map: String,
|
||||
protected: i64,
|
||||
created_by: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SqliteNote> for Note {
|
||||
fn from(value: SqliteNote) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
workspace_id: value.workspace_id,
|
||||
slug: value.slug,
|
||||
title: value.title,
|
||||
content: value.content,
|
||||
created_at: value.created_at,
|
||||
updated_at: value.updated_at,
|
||||
owner_map: value.owner_map,
|
||||
protected: value.protected != 0,
|
||||
created_by: value.created_by,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, FromRow)]
|
||||
@@ -98,10 +131,19 @@ pub fn verify_workspace_password(workspace: &Workspace, password: Option<&str>)
|
||||
}
|
||||
|
||||
pub async fn list_notes(pool: &Database, workspace_id: i64) -> Result<Vec<Note>, sqlx::Error> {
|
||||
if pool.kind() == DatabaseKind::Sqlite {
|
||||
return Ok(sqlx::query_as::<_, SqliteNote>(queries::Q003)
|
||||
.bind(workspace_id)
|
||||
.fetch_all(pool.pool())
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Note::from)
|
||||
.collect());
|
||||
}
|
||||
sqlx::query_as::<_, Note>(queries::get(pool.kind(), queries::Q003))
|
||||
.bind(workspace_id)
|
||||
.fetch_all(pool.pool())
|
||||
.await
|
||||
.bind(workspace_id)
|
||||
.fetch_all(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_note(
|
||||
@@ -109,11 +151,19 @@ pub async fn find_note(
|
||||
workspace_id: i64,
|
||||
slug: &str,
|
||||
) -> Result<Option<Note>, sqlx::Error> {
|
||||
if pool.kind() == DatabaseKind::Sqlite {
|
||||
return Ok(sqlx::query_as::<_, SqliteNote>(queries::Q004)
|
||||
.bind(workspace_id)
|
||||
.bind(slug)
|
||||
.fetch_optional(pool.pool())
|
||||
.await?
|
||||
.map(Note::from));
|
||||
}
|
||||
sqlx::query_as::<_, Note>(queries::get(pool.kind(), queries::Q004))
|
||||
.bind(workspace_id)
|
||||
.bind(slug)
|
||||
.fetch_optional(pool.pool())
|
||||
.await
|
||||
.bind(workspace_id)
|
||||
.bind(slug)
|
||||
.fetch_optional(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_note(
|
||||
@@ -121,19 +171,21 @@ pub async fn create_note(
|
||||
workspace_id: i64,
|
||||
slug: &str,
|
||||
title: &str,
|
||||
protected: bool,
|
||||
created_by: Option<&str>,
|
||||
) -> Result<Note, sqlx::Error> {
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q005))
|
||||
.bind(workspace_id)
|
||||
.bind(slug)
|
||||
.bind(title)
|
||||
.bind(protected)
|
||||
.bind(created_by)
|
||||
.execute(pool.pool())
|
||||
.await?;
|
||||
|
||||
sqlx::query_as::<_, Note>(queries::get(pool.kind(), queries::Q004))
|
||||
.bind(workspace_id)
|
||||
.bind(slug)
|
||||
.fetch_one(pool.pool())
|
||||
.await
|
||||
find_note(pool, workspace_id, slug)
|
||||
.await?
|
||||
.ok_or(sqlx::Error::RowNotFound)
|
||||
}
|
||||
|
||||
pub async fn save_revision(
|
||||
@@ -453,3 +505,125 @@ pub async fn find_file_owner(pool: &Database, token: &str) -> Result<Option<File
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, FromRow)]
|
||||
pub struct NoteFile {
|
||||
pub id: i64,
|
||||
pub filename: String,
|
||||
pub url: String,
|
||||
pub mime_type: String,
|
||||
pub size_bytes: i64,
|
||||
pub created_at: String,
|
||||
pub is_attached: bool,
|
||||
pub detached_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
struct SqliteNoteFile {
|
||||
id: i64,
|
||||
filename: String,
|
||||
url: String,
|
||||
mime_type: String,
|
||||
size_bytes: i64,
|
||||
created_at: String,
|
||||
is_attached: i64,
|
||||
detached_at: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SqliteNoteFile> for NoteFile {
|
||||
fn from(value: SqliteNoteFile) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
filename: value.filename,
|
||||
url: value.url,
|
||||
mime_type: value.mime_type,
|
||||
size_bytes: value.size_bytes,
|
||||
created_at: value.created_at,
|
||||
is_attached: value.is_attached != 0,
|
||||
detached_at: value.detached_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_note(pool: &Database, note_id: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q031)).bind(note_id).execute(pool.pool()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn register_note_file(pool: &Database, note_id: i64, filename: &str, url: &str, mime_type: &str, size_bytes: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q032))
|
||||
.bind(note_id).bind(filename).bind(url).bind(mime_type).bind(size_bytes)
|
||||
.execute(pool.pool()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_note_files(pool: &Database, note_id: i64) -> Result<Vec<NoteFile>, sqlx::Error> {
|
||||
list_files(pool, queries::Q033, note_id).await
|
||||
}
|
||||
|
||||
async fn list_files(pool: &Database, query: &'static str, owner_id: i64) -> Result<Vec<NoteFile>, sqlx::Error> {
|
||||
if pool.kind() == DatabaseKind::Sqlite {
|
||||
return Ok(sqlx::query_as::<_, SqliteNoteFile>(query)
|
||||
.bind(owner_id)
|
||||
.fetch_all(pool.pool())
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(NoteFile::from)
|
||||
.collect());
|
||||
}
|
||||
sqlx::query_as::<_, NoteFile>(queries::get(pool.kind(), query))
|
||||
.bind(owner_id)
|
||||
.fetch_all(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_note_file_attached(pool: &Database, file_id: i64, attached: bool) -> Result<(), sqlx::Error> {
|
||||
let detached_at: Option<String> = if attached { None } else { Some(chrono::Utc::now().to_rfc3339()) };
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q034)).bind(attached).bind(detached_at).bind(file_id).execute(pool.pool()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub async fn register_pad_file(pool: &Database, pad_id: i64, filename: &str, url: &str, mime_type: &str, size_bytes: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q035))
|
||||
.bind(pad_id).bind(filename).bind(url).bind(mime_type).bind(size_bytes)
|
||||
.execute(pool.pool()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_pad_files(pool: &Database, pad_id: i64) -> Result<Vec<NoteFile>, sqlx::Error> {
|
||||
list_files(pool, queries::Q036, pad_id).await
|
||||
}
|
||||
|
||||
pub async fn set_pad_file_attached(pool: &Database, file_id: i64, attached: bool) -> Result<(), sqlx::Error> {
|
||||
let detached_at: Option<String> = if attached { None } else { Some(chrono::Utc::now().to_rfc3339()) };
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q037)).bind(attached).bind(detached_at).bind(file_id).execute(pool.pool()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub async fn find_note_file(pool: &Database, note_id: i64, file_id: i64) -> Result<Option<NoteFile>, sqlx::Error> {
|
||||
if pool.kind() == DatabaseKind::Sqlite {
|
||||
return Ok(sqlx::query_as::<_, SqliteNoteFile>(queries::Q038)
|
||||
.bind(file_id)
|
||||
.bind(note_id)
|
||||
.fetch_optional(pool.pool())
|
||||
.await?
|
||||
.map(NoteFile::from));
|
||||
}
|
||||
sqlx::query_as::<_, NoteFile>(queries::get(pool.kind(), queries::Q038))
|
||||
.bind(file_id)
|
||||
.bind(note_id)
|
||||
.fetch_optional(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_note_file(pool: &Database, note_id: i64, file_id: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q039))
|
||||
.bind(file_id)
|
||||
.bind(note_id)
|
||||
.execute(pool.pool())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+12
-3
@@ -3,9 +3,9 @@ use crate::database::DatabaseKind;
|
||||
|
||||
pub const Q001: &str = "SELECT id, slug, title, password_hash, created_at, updated_at FROM workspaces WHERE slug = ?";
|
||||
pub const Q002: &str = "INSERT INTO workspaces (slug, title, password_hash) VALUES (?, ?, ?)";
|
||||
pub const Q003: &str = "SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC";
|
||||
pub const Q004: &str = "SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map FROM notes WHERE workspace_id = ? AND slug = ?";
|
||||
pub const Q005: &str = "INSERT INTO notes (workspace_id, slug, title) VALUES (?, ?, ?)";
|
||||
pub const Q003: &str = "SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, protected, created_by FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC";
|
||||
pub const Q004: &str = "SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map, protected, created_by FROM notes WHERE workspace_id = ? AND slug = ?";
|
||||
pub const Q005: &str = "INSERT INTO notes (workspace_id, slug, title, protected, created_by) VALUES (?, ?, ?, ?, ?)";
|
||||
pub const Q006: &str = "UPDATE notes SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
||||
pub const Q007: &str = "UPDATE workspaces SET updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
||||
pub const Q008: &str = "INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES (?, ?, ?, ?)";
|
||||
@@ -31,6 +31,15 @@ pub const Q027: &str = "SELECT id FROM notes WHERE file_token = ?";
|
||||
pub const Q028: &str = "SELECT content FROM note_revisions WHERE id = ? AND note_id = ?";
|
||||
pub const Q029: &str = "SELECT content FROM revisions WHERE id = ? AND pad_id = ?";
|
||||
pub const Q030: &str = "SELECT owner_map FROM revisions WHERE id = ? AND pad_id = ?";
|
||||
pub const Q031: &str = "DELETE FROM notes WHERE id = ?";
|
||||
pub const Q032: &str = "INSERT INTO note_files (note_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)";
|
||||
pub const Q033: &str = "SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE note_id = ? ORDER BY id DESC";
|
||||
pub const Q034: &str = "UPDATE note_files SET is_attached = ?, detached_at = ? WHERE id = ?";
|
||||
pub const Q035: &str = "INSERT INTO pad_files (pad_id, filename, url, mime_type, size_bytes) VALUES (?, ?, ?, ?, ?)";
|
||||
pub const Q036: &str = "SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM pad_files WHERE pad_id = ? ORDER BY id DESC";
|
||||
pub const Q037: &str = "UPDATE pad_files SET is_attached = ?, detached_at = ? WHERE id = ?";
|
||||
pub const Q038: &str = "SELECT id, filename, url, mime_type, size_bytes, created_at, is_attached, detached_at FROM note_files WHERE id = ? AND note_id = ?";
|
||||
pub const Q039: &str = "DELETE FROM note_files WHERE id = ? AND note_id = ?";
|
||||
|
||||
static POSTGRES_QUERIES: OnceLock<Mutex<HashMap<&'static str, &'static str>>> = OnceLock::new();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user