From 2cfa4afe92593c677684ae45e1319190f8e9bb7c Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Mon, 20 Jul 2026 17:48:14 -0400 Subject: [PATCH] Rename Space to Project, add desktop document creation --- package.json | 2 +- server/src/db/postgres.rs | 31 ++- server/src/db/sqlite.rs | 31 ++- server/src/desktop.rs | 226 ++++++++++-------- server/src/handlers.rs | 36 +-- server/src/main.rs | 24 +- server/src/models.rs | 16 +- server/src/packages.rs | 18 +- server/src/{spaces.rs => projects.rs} | 206 ++++++++-------- src/lib/components/PublishPackageModal.svelte | 8 +- ...Modal.svelte => CreateProjectModal.svelte} | 22 +- src/lib/components/dashboard/Navbar.svelte | 2 +- .../{SpaceCard.svelte => ProjectCard.svelte} | 36 +-- .../{space => project}/FileTree.svelte | 14 +- .../ProjectToolbar.svelte} | 54 ++--- src/lib/ts/typst-api.ts | 8 +- src/lib/ts/{yjs-space.ts => yjs-project.ts} | 14 +- src/routes/dashboard/+page.svelte | 26 +- src/routes/packages/+page.svelte | 4 +- .../{space => project}/[id]/+page.svelte | 66 ++--- src/routes/{spaces => projects}/+page.svelte | 78 +++--- 21 files changed, 494 insertions(+), 428 deletions(-) rename server/src/{spaces.rs => projects.rs} (68%) rename src/lib/components/dashboard/{CreateSpaceModal.svelte => CreateProjectModal.svelte} (75%) rename src/lib/components/dashboard/{SpaceCard.svelte => ProjectCard.svelte} (64%) rename src/lib/components/{space => project}/FileTree.svelte (93%) rename src/lib/components/{space/SpaceToolbar.svelte => project/ProjectToolbar.svelte} (93%) rename src/lib/ts/{yjs-space.ts => yjs-project.ts} (92%) rename src/routes/{space => project}/[id]/+page.svelte (78%) rename src/routes/{spaces => projects}/+page.svelte (74%) diff --git a/package.json b/package.json index cb9cacc..f1fc0a0 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@tailwindcss/forms": "^0.5.11", "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4.3.3", - "svelte": "^5.56.6", + "svelte": "^5.56.7", "svelte-check": "^4.7.3", "tailwindcss": "^4.3.3", "typescript": "^5.9.3", diff --git a/server/src/db/postgres.rs b/server/src/db/postgres.rs index ecfb871..4cba4f5 100644 --- a/server/src/db/postgres.rs +++ b/server/src/db/postgres.rs @@ -1,6 +1,21 @@ use sqlx::AnyPool; pub async fn init_schema(pool: &AnyPool) { + // Rename the legacy "space" tables/columns to the "project" vocabulary on + // existing databases. Best-effort: on a fresh database (or one already + // migrated) the old names don't exist, so these fail silently and the + // CREATE TABLE IF NOT EXISTS statements below take over. + let rename_migrations = [ + "ALTER TABLE IF EXISTS spaces RENAME TO projects", + "ALTER TABLE IF EXISTS space_files RENAME TO project_files", + "ALTER TABLE IF EXISTS space_collaborators RENAME TO project_collaborators", + "ALTER TABLE IF EXISTS project_files RENAME COLUMN space_id TO project_id", + "ALTER TABLE IF EXISTS project_collaborators RENAME COLUMN space_id TO project_id", + ]; + for stmt in &rename_migrations { + let _ = sqlx::query(stmt).execute(pool).await; + } + let statements = [ "CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, @@ -105,7 +120,7 @@ pub async fn init_schema(pool: &AnyPool) { count INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(key_id, minute) )", - "CREATE TABLE IF NOT EXISTS spaces ( + "CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id), folder_id TEXT REFERENCES folders(id), @@ -116,23 +131,23 @@ pub async fn init_schema(pool: &AnyPool) { created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'), updated_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') )", - "CREATE TABLE IF NOT EXISTS space_files ( + "CREATE TABLE IF NOT EXISTS project_files ( id TEXT PRIMARY KEY, - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, path TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'text', content BYTEA, mime_type TEXT NOT NULL DEFAULT 'text/plain', created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'), - UNIQUE(space_id, path) + UNIQUE(project_id, path) )", - "CREATE TABLE IF NOT EXISTS space_collaborators ( + "CREATE TABLE IF NOT EXISTS project_collaborators ( id TEXT PRIMARY KEY, - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, role TEXT NOT NULL, created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'), - UNIQUE(space_id, user_id) + UNIQUE(project_id, user_id) )", "CREATE TABLE IF NOT EXISTS packages ( id TEXT PRIMARY KEY, @@ -181,7 +196,7 @@ pub async fn init_schema(pool: &AnyPool) { "ALTER TABLE documents ADD COLUMN IF NOT EXISTS public_role TEXT", "ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE", "ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')", - "ALTER TABLE space_files ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')", + "ALTER TABLE project_files ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')", ]; for stmt in &migrations { sqlx::query(stmt).execute(pool).await.unwrap_or_else(|_| Default::default()); diff --git a/server/src/db/sqlite.rs b/server/src/db/sqlite.rs index 993eb8a..1662da9 100644 --- a/server/src/db/sqlite.rs +++ b/server/src/db/sqlite.rs @@ -6,6 +6,21 @@ pub async fn init_schema(pool: &AnyPool) { .await .expect("Failed to enable SQLite foreign keys"); + // Rename the legacy "space" tables/columns to the "project" vocabulary on + // existing databases. Best-effort: on a fresh database (or one already + // migrated) the old names don't exist, so these fail silently and the + // CREATE TABLE IF NOT EXISTS statements below take over. + let rename_migrations = [ + "ALTER TABLE spaces RENAME TO projects", + "ALTER TABLE space_files RENAME TO project_files", + "ALTER TABLE space_collaborators RENAME TO project_collaborators", + "ALTER TABLE project_files RENAME COLUMN space_id TO project_id", + "ALTER TABLE project_collaborators RENAME COLUMN space_id TO project_id", + ]; + for stmt in &rename_migrations { + let _ = sqlx::query(stmt).execute(pool).await; + } + let statements = [ "CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, @@ -110,7 +125,7 @@ pub async fn init_schema(pool: &AnyPool) { count INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(key_id, minute) )", - "CREATE TABLE IF NOT EXISTS spaces ( + "CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id), folder_id TEXT REFERENCES folders(id), @@ -121,23 +136,23 @@ pub async fn init_schema(pool: &AnyPool) { created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')) )", - "CREATE TABLE IF NOT EXISTS space_files ( + "CREATE TABLE IF NOT EXISTS project_files ( id TEXT PRIMARY KEY, - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, path TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'text', content BLOB, mime_type TEXT NOT NULL DEFAULT 'text/plain', created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), - UNIQUE(space_id, path) + UNIQUE(project_id, path) )", - "CREATE TABLE IF NOT EXISTS space_collaborators ( + "CREATE TABLE IF NOT EXISTS project_collaborators ( id TEXT PRIMARY KEY, - space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, role TEXT NOT NULL, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), - UNIQUE(space_id, user_id) + UNIQUE(project_id, user_id) )", "CREATE TABLE IF NOT EXISTS packages ( id TEXT PRIMARY KEY, @@ -185,7 +200,7 @@ pub async fn init_schema(pool: &AnyPool) { let migrations = [ "ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0", "ALTER TABLE users ADD COLUMN created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))", - "ALTER TABLE space_files ADD COLUMN updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))", + "ALTER TABLE project_files ADD COLUMN updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))", ]; for stmt in &migrations { let _ = sqlx::query(stmt).execute(pool).await; diff --git a/server/src/desktop.rs b/server/src/desktop.rs index c11c366..c984d2f 100644 --- a/server/src/desktop.rs +++ b/server/src/desktop.rs @@ -14,8 +14,8 @@ use argon2::{ }; use crate::{ - models::{Space, User}, - spaces::{decode_text_blob, encode_text_blob}, + models::{Project, User}, + projects::{decode_text_blob, encode_text_blob}, AppState, }; @@ -81,28 +81,28 @@ pub async fn authenticate( Ok(user_id) } -async fn owned_space( +async fn owned_project( state: &AppState, - space_id: &str, + project_id: &str, user_id: &str, -) -> Result { - let space = sqlx::query_as::<_, Space>( - "SELECT s.id, s.owner_id, s.folder_id, s.name, s.entrypoint, s.thumbnail_svg, \ - s.public_role, s.created_at, s.updated_at FROM spaces s \ - WHERE s.id = ? AND (s.owner_id = ? OR EXISTS ( \ - SELECT 1 FROM space_collaborators c \ - WHERE c.space_id = s.id AND c.user_id = ? AND c.role = 'editor'))", +) -> Result { + let project = sqlx::query_as::<_, Project>( + "SELECT p.id, p.owner_id, p.folder_id, p.name, p.entrypoint, p.thumbnail_svg, \ + p.public_role, p.created_at, p.updated_at FROM projects p \ + WHERE p.id = ? AND (p.owner_id = ? OR EXISTS ( \ + SELECT 1 FROM project_collaborators c \ + WHERE c.project_id = p.id AND c.user_id = ? AND c.role = 'editor'))", ) - .bind(space_id) + .bind(project_id) .bind(user_id) .bind(user_id) .fetch_optional(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - space.ok_or(( + project.ok_or(( StatusCode::NOT_FOUND, - "Space not found or not writable".to_string(), + "Project not found or not writable".to_string(), )) } @@ -224,7 +224,7 @@ pub async fn logout( } #[derive(Serialize)] -pub struct SpaceSummary { +pub struct ProjectSummary { pub id: String, pub name: String, pub entrypoint: String, @@ -232,14 +232,14 @@ pub struct SpaceSummary { pub updated_at: String, } -pub async fn list_spaces( +pub async fn list_projects( State(state): State, headers: HeaderMap, -) -> Result>, (StatusCode, String)> { +) -> Result>, (StatusCode, String)> { let user_id = authenticate(&state, &headers).await?; let owned = sqlx::query_as::<_, (String, String, String, String)>( - "SELECT id, name, entrypoint, updated_at FROM spaces WHERE owner_id = ? ORDER BY updated_at DESC", + "SELECT id, name, entrypoint, updated_at FROM projects WHERE owner_id = ? ORDER BY updated_at DESC", ) .bind(&user_id) .fetch_all(&state.db) @@ -247,18 +247,18 @@ pub async fn list_spaces( .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let shared = sqlx::query_as::<_, (String, String, String, String, String)>( - "SELECT s.id, s.name, s.entrypoint, s.updated_at, c.role FROM spaces s \ - INNER JOIN space_collaborators c ON c.space_id = s.id AND c.user_id = ? \ - ORDER BY s.updated_at DESC", + "SELECT p.id, p.name, p.entrypoint, p.updated_at, c.role FROM projects p \ + INNER JOIN project_collaborators c ON c.project_id = p.id AND c.user_id = ? \ + ORDER BY p.updated_at DESC", ) .bind(&user_id) .fetch_all(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - let mut spaces: Vec = owned + let mut projects: Vec = owned .into_iter() - .map(|(id, name, entrypoint, updated_at)| SpaceSummary { + .map(|(id, name, entrypoint, updated_at)| ProjectSummary { id, name, entrypoint, @@ -267,10 +267,10 @@ pub async fn list_spaces( }) .collect(); - spaces.extend( + projects.extend( shared .into_iter() - .map(|(id, name, entrypoint, updated_at, role)| SpaceSummary { + .map(|(id, name, entrypoint, updated_at, role)| ProjectSummary { id, name, entrypoint, @@ -279,36 +279,36 @@ pub async fn list_spaces( }), ); - Ok(Json(spaces)) + Ok(Json(projects)) } #[derive(Deserialize)] -pub struct CreateSpaceBody { +pub struct CreateProjectBody { pub name: String, pub entrypoint: Option, } -pub async fn create_space( +pub async fn create_project( State(state): State, headers: HeaderMap, - Json(payload): Json, -) -> Result, (StatusCode, String)> { + Json(payload): Json, +) -> Result, (StatusCode, String)> { let user_id = authenticate(&state, &headers).await?; if payload.name.trim().is_empty() { return Err((StatusCode::BAD_REQUEST, "Name cannot be empty".to_string())); } - let space_id = Uuid::new_v4().to_string(); + let project_id = Uuid::new_v4().to_string(); let entrypoint = payload .entrypoint .unwrap_or_else(|| "main.typ".to_string()); - let space = sqlx::query_as::<_, Space>( - "INSERT INTO spaces (id, owner_id, name, entrypoint) VALUES (?, ?, ?, ?) \ + let project = sqlx::query_as::<_, Project>( + "INSERT INTO projects (id, owner_id, name, entrypoint) VALUES (?, ?, ?, ?) \ RETURNING id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at", ) - .bind(&space_id) + .bind(&project_id) .bind(&user_id) .bind(payload.name.trim()) .bind(&entrypoint) @@ -316,36 +316,36 @@ pub async fn create_space( .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - Ok(Json(SpaceSummary { - id: space.id, - name: space.name, - entrypoint: space.entrypoint, + Ok(Json(ProjectSummary { + id: project.id, + name: project.name, + entrypoint: project.entrypoint, role: "owner".to_string(), - updated_at: space.updated_at, + updated_at: project.updated_at, })) } -pub async fn delete_space( +pub async fn delete_project( State(state): State, headers: HeaderMap, - Path(space_id): Path, + Path(project_id): Path, ) -> Result { let user_id = authenticate(&state, &headers).await?; - let _ = sqlx::query("DELETE FROM space_files WHERE space_id = ?") - .bind(&space_id) + let _ = sqlx::query("DELETE FROM project_files WHERE project_id = ?") + .bind(&project_id) .execute(&state.db) .await; - let result = sqlx::query("DELETE FROM spaces WHERE id = ? AND owner_id = ?") - .bind(&space_id) + let result = sqlx::query("DELETE FROM projects WHERE id = ? AND owner_id = ?") + .bind(&project_id) .bind(&user_id) .execute(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if result.rows_affected() == 0 { - return Err((StatusCode::NOT_FOUND, "Space not found".to_string())); + return Err((StatusCode::NOT_FOUND, "Project not found".to_string())); } Ok(StatusCode::NO_CONTENT) @@ -361,8 +361,8 @@ pub struct ManifestEntry { } #[derive(Serialize)] -pub struct SpaceManifest { - pub space_id: String, +pub struct ProjectManifest { + pub project_id: String, pub name: String, pub entrypoint: String, pub updated_at: String, @@ -371,12 +371,12 @@ pub struct SpaceManifest { async fn plain_contents( state: &AppState, - space_id: &str, + project_id: &str, ) -> Result, String)>, (StatusCode, String)> { let rows = sqlx::query_as::<_, (String, String, Option>, Option)>( - "SELECT path, kind, content, updated_at FROM space_files WHERE space_id = ? ORDER BY path ASC", + "SELECT path, kind, content, updated_at FROM project_files WHERE project_id = ? ORDER BY path ASC", ) - .bind(space_id) + .bind(project_id) .fetch_all(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -398,12 +398,12 @@ async fn plain_contents( pub async fn get_manifest( State(state): State, headers: HeaderMap, - Path(space_id): Path, -) -> Result, (StatusCode, String)> { + Path(project_id): Path, +) -> Result, (StatusCode, String)> { let user_id = authenticate(&state, &headers).await?; - let space = owned_space(&state, &space_id, &user_id).await?; + let project = owned_project(&state, &project_id, &user_id).await?; - let files = plain_contents(&state, &space_id) + let files = plain_contents(&state, &project_id) .await? .into_iter() .map(|(path, kind, plain, updated_at)| ManifestEntry { @@ -415,11 +415,11 @@ pub async fn get_manifest( }) .collect(); - Ok(Json(SpaceManifest { - space_id: space.id, - name: space.name, - entrypoint: space.entrypoint, - updated_at: space.updated_at, + Ok(Json(ProjectManifest { + project_id: project.id, + name: project.name, + entrypoint: project.entrypoint, + updated_at: project.updated_at, files, })) } @@ -452,16 +452,16 @@ fn encode_for_transport(kind: &str, plain: Vec) -> (String, String) { pub async fn pull_file( State(state): State, headers: HeaderMap, - Path(space_id): Path, + Path(project_id): Path, Query(query): Query, ) -> Result, (StatusCode, String)> { let user_id = authenticate(&state, &headers).await?; - owned_space(&state, &space_id, &user_id).await?; + owned_project(&state, &project_id, &user_id).await?; let row = sqlx::query_as::<_, (String, Option>)>( - "SELECT kind, content FROM space_files WHERE space_id = ? AND path = ?", + "SELECT kind, content FROM project_files WHERE project_id = ? AND path = ?", ) - .bind(&space_id) + .bind(&project_id) .bind(&query.path) .fetch_optional(&state.db) .await @@ -530,11 +530,11 @@ impl axum::response::IntoResponse for PushOutcome { pub async fn push_file( State(state): State, headers: HeaderMap, - Path(space_id): Path, + Path(project_id): Path, Json(payload): Json, ) -> Result { let user_id = authenticate(&state, &headers).await?; - owned_space(&state, &space_id, &user_id).await?; + owned_project(&state, &project_id, &user_id).await?; let incoming = match payload.encoding.as_deref() { Some("base64") => BASE64 @@ -544,9 +544,9 @@ pub async fn push_file( }; let existing = sqlx::query_as::<_, (String, Option>)>( - "SELECT kind, content FROM space_files WHERE space_id = ? AND path = ?", + "SELECT kind, content FROM project_files WHERE project_id = ? AND path = ?", ) - .bind(&space_id) + .bind(&project_id) .bind(&payload.path) .fetch_optional(&state.db) .await @@ -600,13 +600,13 @@ pub async fn push_file( let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(); sqlx::query( - "INSERT INTO space_files (id, space_id, path, kind, content, mime_type, updated_at) \ + "INSERT INTO project_files (id, project_id, path, kind, content, mime_type, updated_at) \ VALUES (?, ?, ?, ?, ?, ?, ?) \ - ON CONFLICT (space_id, path) DO UPDATE SET content = excluded.content, \ + ON CONFLICT (project_id, path) DO UPDATE SET content = excluded.content, \ kind = excluded.kind, mime_type = excluded.mime_type, updated_at = excluded.updated_at", ) .bind(Uuid::new_v4().to_string()) - .bind(&space_id) + .bind(&project_id) .bind(&payload.path) .bind(kind) .bind(&stored) @@ -616,9 +616,9 @@ pub async fn push_file( .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - let _ = sqlx::query("UPDATE spaces SET updated_at = ? WHERE id = ?") + let _ = sqlx::query("UPDATE projects SET updated_at = ? WHERE id = ?") .bind(&now) - .bind(&space_id) + .bind(&project_id) .execute(&state.db) .await; @@ -632,14 +632,14 @@ pub async fn push_file( pub async fn delete_file( State(state): State, headers: HeaderMap, - Path(space_id): Path, + Path(project_id): Path, Query(query): Query, ) -> Result { let user_id = authenticate(&state, &headers).await?; - owned_space(&state, &space_id, &user_id).await?; + owned_project(&state, &project_id, &user_id).await?; - let result = sqlx::query("DELETE FROM space_files WHERE space_id = ? AND path = ?") - .bind(&space_id) + let result = sqlx::query("DELETE FROM project_files WHERE project_id = ? AND path = ?") + .bind(&project_id) .bind(&query.path) .execute(&state.db) .await @@ -662,22 +662,22 @@ pub struct BundleFile { } #[derive(Serialize)] -pub struct SpaceBundle { - pub space_id: String, +pub struct ProjectBundle { + pub project_id: String, pub name: String, pub entrypoint: String, pub files: Vec, } -pub async fn pull_space( +pub async fn pull_project( State(state): State, headers: HeaderMap, - Path(space_id): Path, -) -> Result, (StatusCode, String)> { + Path(project_id): Path, +) -> Result, (StatusCode, String)> { let user_id = authenticate(&state, &headers).await?; - let space = owned_space(&state, &space_id, &user_id).await?; + let project = owned_project(&state, &project_id, &user_id).await?; - let files = plain_contents(&state, &space_id) + let files = plain_contents(&state, &project_id) .await? .into_iter() .map(|(path, kind, plain, _)| { @@ -693,10 +693,10 @@ pub async fn pull_space( }) .collect(); - Ok(Json(SpaceBundle { - space_id: space.id, - name: space.name, - entrypoint: space.entrypoint, + Ok(Json(ProjectBundle { + project_id: project.id, + name: project.name, + entrypoint: project.entrypoint, files, })) } @@ -789,7 +789,7 @@ pub async fn list_documents( #[derive(Serialize)] pub struct SharedItems { pub documents: Vec, - pub spaces: Vec, + pub projects: Vec, } pub async fn list_shared( @@ -808,10 +808,10 @@ pub async fn list_shared( .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - let spaces = sqlx::query_as::<_, (String, String, String, String, String)>( - "SELECT s.id, s.name, s.entrypoint, s.updated_at, c.role FROM spaces s \ - INNER JOIN space_collaborators c ON c.space_id = s.id AND c.user_id = ? \ - ORDER BY s.updated_at DESC", + let projects = sqlx::query_as::<_, (String, String, String, String, String)>( + "SELECT p.id, p.name, p.entrypoint, p.updated_at, c.role FROM projects p \ + INNER JOIN project_collaborators c ON c.project_id = p.id AND c.user_id = ? \ + ORDER BY p.updated_at DESC", ) .bind(&user_id) .fetch_all(&state.db) @@ -829,9 +829,9 @@ pub async fn list_shared( updated_at, }) .collect(), - spaces: spaces + projects: projects .into_iter() - .map(|(id, name, entrypoint, updated_at, role)| SpaceSummary { + .map(|(id, name, entrypoint, updated_at, role)| ProjectSummary { id, name, entrypoint, @@ -974,6 +974,42 @@ pub async fn push_document( }))) } +#[derive(Deserialize)] +pub struct CreateDocumentRequest { + pub title: String, + pub content: String, + pub folder_id: Option, +} + +pub async fn create_document( + State(state): State, + headers: HeaderMap, + Json(payload): Json, +) -> Result, (StatusCode, String)> { + let user_id = authenticate(&state, &headers).await?; + let document_id = Uuid::new_v4().to_string(); + + sqlx::query( + "INSERT INTO documents (id, owner_id, folder_id, title, content) VALUES (?, ?, ?, ?, ?)", + ) + .bind(&document_id) + .bind(&user_id) + .bind(&payload.folder_id) + .bind(&payload.title) + .bind(encode_text_blob(&payload.content)) + .execute(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + Ok(Json(DocumentContent { + id: document_id, + title: payload.title, + role: "owner".to_string(), + hash: content_hash(payload.content.as_bytes()), + content: payload.content, + })) +} + #[derive(Serialize)] pub struct CloudFile { pub id: String, diff --git a/server/src/handlers.rs b/server/src/handlers.rs index 49cf030..5cbba95 100644 --- a/server/src/handlers.rs +++ b/server/src/handlers.rs @@ -53,7 +53,7 @@ pub struct CompileRequest { #[serde(default)] pub text: Option, pub document_id: Option, - pub space_id: Option, + pub project_id: Option, #[serde(default)] pub files: Option>, } @@ -102,21 +102,21 @@ pub async fn yjs_handler( // (table, row_id) the autosave task persists into; None means no persistence. let mut save_target: Option<(&'static str, String)> = None; - if let Some(rest) = id.strip_prefix("space:") { - if let Some((space_id, file_id)) = rest.split_once(':') { - if let Some((_space, role)) = crate::spaces::space_role(&state, space_id, &user_id_opt).await { + if let Some(rest) = id.strip_prefix("project:") { + if let Some((project_id, file_id)) = rest.split_once(':') { + if let Some((_project, role)) = crate::projects::project_role(&state, project_id, &user_id_opt).await { is_viewer = role == "viewer"; if let Ok(Some((content,))) = sqlx::query_as::<_, (Option>,)>( - "SELECT content FROM space_files WHERE id = ? AND space_id = ?" + "SELECT content FROM project_files WHERE id = ? AND project_id = ?" ) .bind(file_id) - .bind(space_id) + .bind(project_id) .fetch_optional(&state.db) .await { initial_content = content; } - save_target = Some(("space_files", file_id.to_string())); + save_target = Some(("project_files", file_id.to_string())); } } } else { @@ -177,8 +177,8 @@ pub async fn yjs_handler( interval.tick().await; let doc = save_awareness.read().await; let content = doc.doc().transact().encode_state_as_update_v1(&yrs::StateVector::default()); - let query = if table == "space_files" { - "UPDATE space_files SET content = ? WHERE id = ?" + let query = if table == "project_files" { + "UPDATE project_files SET content = ? WHERE id = ?" } else { "UPDATE documents SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?" }; @@ -222,8 +222,8 @@ pub async fn compile_handler( let mut can_save_thumbnail = false; let user_id_opt = jar.get("session_user_id").map(|c| c.value().to_string()); - if let Some(space_id) = &payload.space_id { - let (space, role) = match crate::spaces::space_role(&state, space_id, &user_id_opt).await { + if let Some(project_id) = &payload.project_id { + let (project, role) = match crate::projects::project_role(&state, project_id, &user_id_opt).await { Some(v) => v, None => { return Json(CompileResponse { @@ -240,7 +240,7 @@ pub async fn compile_handler( }; let overrides = payload.files.clone().unwrap_or_default(); - let input = crate::spaces::assemble_project(&state, &space, overrides).await; + let input = crate::projects::assemble_project(&state, &project, overrides).await; let can_save = role == "owner" || role == "editor"; let compiler = state.compiler.lock().await; @@ -250,9 +250,9 @@ pub async fn compile_handler( return match result { Ok((svgs, thumbnail, stats)) => { if can_save { - let _ = sqlx::query("UPDATE spaces SET thumbnail_svg = ? WHERE id = ?") + let _ = sqlx::query("UPDATE projects SET thumbnail_svg = ? WHERE id = ?") .bind(&thumbnail) - .bind(&space.id) + .bind(&project.id) .execute(&state.db) .await; } @@ -430,11 +430,11 @@ pub async fn export_handler( } } - let input = if let Some(space_id) = &payload.space_id { - match crate::spaces::space_role(&state, space_id, &user_id_opt).await { - Some((space, _)) => { + let input = if let Some(project_id) = &payload.project_id { + match crate::projects::project_role(&state, project_id, &user_id_opt).await { + Some((project, _)) => { let overrides = payload.files.clone().unwrap_or_default(); - crate::spaces::assemble_project(&state, &space, overrides).await + crate::projects::assemble_project(&state, &project, overrides).await } None => return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(), } diff --git a/server/src/main.rs b/server/src/main.rs index a5a9828..c2e44f5 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -24,9 +24,9 @@ mod files; mod handlers; mod models; mod packages; +mod projects; mod public_api; mod setup; -mod spaces; mod world; mod collab; @@ -131,12 +131,12 @@ async fn main() { .route("/keys/usage", get(api_keys::get_aggregate_usage)) .route("/keys/{id}", delete(api_keys::delete_key)) .route("/keys/{id}/regenerate", post(api_keys::regenerate_key)) - .route("/spaces/shared", get(spaces::list_shared_spaces)) - .route("/spaces", get(spaces::list_spaces).post(spaces::create_space)) - .route("/spaces/{id}", get(spaces::get_space).delete(spaces::delete_space).patch(spaces::update_space)) - .route("/spaces/{id}/files", get(spaces::list_space_files).post(spaces::create_space_file)) - .route("/spaces/{id}/files/upload", post(spaces::upload_space_file)) - .route("/spaces/{id}/files/{fid}", get(spaces::get_space_file).patch(spaces::update_space_file).delete(spaces::delete_space_file)) + .route("/projects/shared", get(projects::list_shared_projects)) + .route("/projects", get(projects::list_projects).post(projects::create_project)) + .route("/projects/{id}", get(projects::get_project).delete(projects::delete_project).patch(projects::update_project)) + .route("/projects/{id}/files", get(projects::list_project_files).post(projects::create_project_file)) + .route("/projects/{id}/files/upload", post(projects::upload_project_file)) + .route("/projects/{id}/files/{fid}", get(projects::get_project_file).patch(projects::update_project_file).delete(projects::delete_project_file)) .route("/packages", get(packages::list_packages)) .route("/packages/publish", post(packages::publish_package)) .route("/packages/{name}", get(packages::list_versions).delete(packages::delete_package)); @@ -145,16 +145,16 @@ async fn main() { .route("/auth/login", post(desktop::login)) .route("/auth/logout", post(desktop::logout)) .route("/auth/me", get(desktop::me)) - .route("/spaces", get(desktop::list_spaces).post(desktop::create_space)) - .route("/spaces/{id}", get(desktop::pull_space).delete(desktop::delete_space)) - .route("/spaces/{id}/manifest", get(desktop::get_manifest)) + .route("/projects", get(desktop::list_projects).post(desktop::create_project)) + .route("/projects/{id}", get(desktop::pull_project).delete(desktop::delete_project)) + .route("/projects/{id}/manifest", get(desktop::get_manifest)) .route("/folders", get(desktop::list_folders)) - .route("/documents", get(desktop::list_documents)) + .route("/documents", get(desktop::list_documents).post(desktop::create_document)) .route("/documents/{id}", get(desktop::pull_document).put(desktop::push_document)) .route("/shared", get(desktop::list_shared)) .route("/files", get(desktop::list_account_files)) .route("/files/{id}", get(desktop::pull_account_file)) - .route("/spaces/{id}/file", get(desktop::pull_file).put(desktop::push_file).delete(desktop::delete_file)); + .route("/projects/{id}/file", get(desktop::pull_file).put(desktop::push_file).delete(desktop::delete_file)); let v1_routes = Router::new() .route("/render", post(public_api::render_handler)); diff --git a/server/src/models.rs b/server/src/models.rs index 78b832d..8b3c087 100644 --- a/server/src/models.rs +++ b/server/src/models.rs @@ -73,7 +73,7 @@ pub struct Document { } #[derive(Debug, Serialize, Deserialize, FromRow)] -pub struct Space { +pub struct Project { pub id: String, pub owner_id: String, pub folder_id: Option, @@ -89,9 +89,9 @@ pub struct Space { } #[derive(Debug, Serialize, Deserialize, FromRow)] -pub struct SpaceFile { +pub struct ProjectFile { pub id: String, - pub space_id: String, + pub project_id: String, pub path: String, pub kind: String, #[serde(skip_serializing)] @@ -127,14 +127,14 @@ pub struct PackageVersion { } #[derive(Debug, Serialize, Deserialize)] -pub struct CreateSpaceRequest { +pub struct CreateProjectRequest { pub name: String, pub folder_id: Option, pub template: Option, } #[derive(Debug, Serialize, Deserialize)] -pub struct UpdateSpaceRequest { +pub struct UpdateProjectRequest { pub name: Option, pub folder_id: Option, pub entrypoint: Option, @@ -142,20 +142,20 @@ pub struct UpdateSpaceRequest { } #[derive(Debug, Serialize, Deserialize)] -pub struct CreateSpaceFileRequest { +pub struct CreateProjectFileRequest { pub path: String, pub kind: Option, pub content: Option, } #[derive(Debug, Serialize, Deserialize)] -pub struct UpdateSpaceFileRequest { +pub struct UpdateProjectFileRequest { pub path: String, } #[derive(Debug, Serialize, Deserialize)] pub struct PublishPackageRequest { - pub space_id: String, + pub project_id: String, pub version: Option, } diff --git a/server/src/packages.rs b/server/src/packages.rs index 90ecb65..f16d818 100644 --- a/server/src/packages.rs +++ b/server/src/packages.rs @@ -8,8 +8,8 @@ use serde::Deserialize; use uuid::Uuid; use crate::{ - models::{Package, PackageVersion, PublishPackageRequest, Space}, - spaces::decode_text_blob, + models::{Package, PackageVersion, Project, PublishPackageRequest}, + projects::decode_text_blob, AppState, }; @@ -45,20 +45,20 @@ pub async fn publish_package( let user_id = jar.get("session_user_id").map(|c| c.value().to_string()) .ok_or((StatusCode::UNAUTHORIZED, "Not logged in".to_string()))?; - let space = sqlx::query_as::<_, Space>( - "SELECT id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at FROM spaces WHERE id = ? AND owner_id = ?" + let project = sqlx::query_as::<_, Project>( + "SELECT id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at FROM projects WHERE id = ? AND owner_id = ?" ) - .bind(&payload.space_id) + .bind(&payload.project_id) .bind(&user_id) .fetch_optional(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? - .ok_or((StatusCode::NOT_FOUND, "Space not found".to_string()))?; + .ok_or((StatusCode::NOT_FOUND, "Project not found".to_string()))?; let files = sqlx::query_as::<_, (String, String, Option>)>( - "SELECT path, kind, content FROM space_files WHERE space_id = ?" + "SELECT path, kind, content FROM project_files WHERE project_id = ?" ) - .bind(&space.id) + .bind(&project.id) .fetch_all(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -78,7 +78,7 @@ pub async fn publish_package( } let manifest_text = manifest_text - .ok_or((StatusCode::BAD_REQUEST, "Space has no typst.toml manifest".to_string()))?; + .ok_or((StatusCode::BAD_REQUEST, "Project has no typst.toml manifest".to_string()))?; let manifest: Manifest = toml::from_str(&manifest_text) .map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid typst.toml: {}", e)))?; diff --git a/server/src/spaces.rs b/server/src/projects.rs similarity index 68% rename from server/src/spaces.rs rename to server/src/projects.rs index ef681e4..e695c73 100644 --- a/server/src/spaces.rs +++ b/server/src/projects.rs @@ -14,8 +14,8 @@ use yrs::Update; use crate::{ compiler::ProjectInput, models::{ - CreateSpaceFileRequest, CreateSpaceRequest, Space, SpaceFile, UpdateSpaceFileRequest, - UpdateSpaceRequest, + CreateProjectFileRequest, CreateProjectRequest, Project, ProjectFile, UpdateProjectFileRequest, + UpdateProjectRequest, }, AppState, }; @@ -61,44 +61,44 @@ fn slugify(name: &str) -> String { .collect(); let trimmed = slug.trim_matches('-').replace("--", "-"); if trimmed.is_empty() { - "my-space".to_string() + "my-project".to_string() } else { trimmed } } -pub async fn space_role( +pub async fn project_role( state: &AppState, - space_id: &str, + project_id: &str, user_id_opt: &Option, -) -> Option<(Space, String)> { - let space = sqlx::query_as::<_, Space>( - "SELECT id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at FROM spaces WHERE id = ?" +) -> Option<(Project, String)> { + let project = sqlx::query_as::<_, Project>( + "SELECT id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at FROM projects WHERE id = ?" ) - .bind(space_id) + .bind(project_id) .fetch_optional(&state.db) .await .ok()??; if let Some(uid) = user_id_opt { - if &space.owner_id == uid { - return Some((space, "owner".to_string())); + if &project.owner_id == uid { + return Some((project, "owner".to_string())); } if let Ok(Some((role,))) = sqlx::query_as::<_, (String,)>( - "SELECT role FROM space_collaborators WHERE space_id = ? AND user_id = ?", + "SELECT role FROM project_collaborators WHERE project_id = ? AND user_id = ?", ) - .bind(space_id) + .bind(project_id) .bind(uid) .fetch_optional(&state.db) .await { - return Some((space, role)); + return Some((project, role)); } } - if let Some(pr) = space.public_role.clone() { + if let Some(pr) = project.public_role.clone() { if pr == "viewer" || pr == "editor" { - return Some((space, pr)); + return Some((project, pr)); } } @@ -128,17 +128,17 @@ pub async fn load_local_packages(state: &AppState) -> HashMap, ) -> ProjectInput { let mut files: HashMap> = HashMap::new(); // Account-level uploaded files (fonts, images) come first as a base layer so - // they are available inside spaces; space files below override them by name. + // they are available inside projects; project files below override them by name. if let Ok(account_files) = sqlx::query_as::<_, (String, Vec)>( "SELECT name, data FROM files WHERE owner_id = ?", ) - .bind(&space.owner_id) + .bind(&project.owner_id) .fetch_all(&state.db) .await { @@ -148,9 +148,9 @@ pub async fn assemble_project( } let rows = sqlx::query_as::<_, (String, String, Option>)>( - "SELECT path, kind, content FROM space_files WHERE space_id = ?", + "SELECT path, kind, content FROM project_files WHERE project_id = ?", ) - .bind(&space.id) + .bind(&project.id) .fetch_all(&state.db) .await .unwrap_or_default(); @@ -170,36 +170,36 @@ pub async fn assemble_project( } ProjectInput { - entrypoint: space.entrypoint.clone(), + entrypoint: project.entrypoint.clone(), files, packages: load_local_packages(state).await, } } #[derive(serde::Deserialize)] -pub struct ListSpacesQuery { +pub struct ListProjectsQuery { pub folder_id: Option, } -pub async fn list_spaces( - Query(query): Query, +pub async fn list_projects( + Query(query): Query, State(state): State, jar: SignedCookieJar, -) -> Result>, (StatusCode, String)> { +) -> Result>, (StatusCode, String)> { let user_id = jar.get("session_user_id").map(|c| c.value().to_string()) .ok_or((StatusCode::UNAUTHORIZED, "Not logged in".to_string()))?; - let spaces = if let Some(folder_id) = query.folder_id { - sqlx::query_as::<_, Space>( - "SELECT id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at FROM spaces WHERE owner_id = ? AND folder_id = ? ORDER BY updated_at DESC" + let projects = if let Some(folder_id) = query.folder_id { + sqlx::query_as::<_, Project>( + "SELECT id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at FROM projects WHERE owner_id = ? AND folder_id = ? ORDER BY updated_at DESC" ) .bind(&user_id) .bind(&folder_id) .fetch_all(&state.db) .await } else { - sqlx::query_as::<_, Space>( - "SELECT id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at FROM spaces WHERE owner_id = ? AND folder_id IS NULL ORDER BY updated_at DESC" + sqlx::query_as::<_, Project>( + "SELECT id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at FROM projects WHERE owner_id = ? AND folder_id IS NULL ORDER BY updated_at DESC" ) .bind(&user_id) .fetch_all(&state.db) @@ -207,45 +207,45 @@ pub async fn list_spaces( } .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - Ok(Json(spaces)) + Ok(Json(projects)) } -pub async fn list_shared_spaces( +pub async fn list_shared_projects( State(state): State, jar: SignedCookieJar, -) -> Result>, (StatusCode, String)> { +) -> Result>, (StatusCode, String)> { let user_id = jar.get("session_user_id").map(|c| c.value().to_string()) .ok_or((StatusCode::UNAUTHORIZED, "Not logged in".to_string()))?; - let spaces = sqlx::query_as::<_, Space>( - "SELECT s.id, s.owner_id, s.folder_id, s.name, s.entrypoint, s.thumbnail_svg, \ - s.public_role, s.created_at, s.updated_at, c.role as effective_role \ - FROM spaces s \ - INNER JOIN space_collaborators c ON c.space_id = s.id AND c.user_id = ? \ - ORDER BY s.updated_at DESC" + let projects = sqlx::query_as::<_, Project>( + "SELECT p.id, p.owner_id, p.folder_id, p.name, p.entrypoint, p.thumbnail_svg, \ + p.public_role, p.created_at, p.updated_at, c.role as effective_role \ + FROM projects p \ + INNER JOIN project_collaborators c ON c.project_id = p.id AND c.user_id = ? \ + ORDER BY p.updated_at DESC" ) .bind(&user_id) .fetch_all(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - Ok(Json(spaces)) + Ok(Json(projects)) } -pub async fn create_space( +pub async fn create_project( State(state): State, jar: SignedCookieJar, - Json(payload): Json, -) -> Result, (StatusCode, String)> { + Json(payload): Json, +) -> Result, (StatusCode, String)> { let user_id = jar.get("session_user_id").map(|c| c.value().to_string()) .ok_or((StatusCode::UNAUTHORIZED, "Not logged in".to_string()))?; - let space_id = Uuid::new_v4().to_string(); + let project_id = Uuid::new_v4().to_string(); - let space = sqlx::query_as::<_, Space>( - "INSERT INTO spaces (id, owner_id, folder_id, name, entrypoint) VALUES (?, ?, ?, ?, 'main.typ') RETURNING id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at" + let project = sqlx::query_as::<_, Project>( + "INSERT INTO projects (id, owner_id, folder_id, name, entrypoint) VALUES (?, ?, ?, ?, 'main.typ') RETURNING id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at" ) - .bind(&space_id) + .bind(&project_id) .bind(&user_id) .bind(&payload.folder_id) .bind(&payload.name) @@ -255,91 +255,91 @@ pub async fn create_space( let seeds = [ ("typst.toml", default_manifest(&slugify(&payload.name))), - ("main.typ", "= New Space\n\nStart writing here.\n".to_string()), + ("main.typ", "= New Project\n\nStart writing here.\n".to_string()), ]; for (path, content) in seeds { let _ = sqlx::query( - "INSERT INTO space_files (id, space_id, path, kind, content, mime_type) VALUES (?, ?, ?, 'text', ?, 'text/plain')" + "INSERT INTO project_files (id, project_id, path, kind, content, mime_type) VALUES (?, ?, ?, 'text', ?, 'text/plain')" ) .bind(Uuid::new_v4().to_string()) - .bind(&space_id) + .bind(&project_id) .bind(path) .bind(encode_text_blob(&content)) .execute(&state.db) .await; } - Ok(Json(space)) + Ok(Json(project)) } -pub async fn get_space( +pub async fn get_project( State(state): State, Path(id): Path, jar: SignedCookieJar, -) -> Result, (StatusCode, String)> { +) -> Result, (StatusCode, String)> { let user_id_opt = jar.get("session_user_id").map(|c| c.value().to_string()); - let (mut space, role) = space_role(&state, &id, &user_id_opt) + let (mut project, role) = project_role(&state, &id, &user_id_opt) .await .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized".to_string()))?; - space.effective_role = Some(role); - Ok(Json(space)) + project.effective_role = Some(role); + Ok(Json(project)) } -pub async fn update_space( +pub async fn update_project( State(state): State, Path(id): Path, jar: SignedCookieJar, - Json(payload): Json, -) -> Result, (StatusCode, String)> { + Json(payload): Json, +) -> Result, (StatusCode, String)> { let user_id = jar.get("session_user_id").map(|c| c.value().to_string()) .ok_or((StatusCode::UNAUTHORIZED, "Not logged in".to_string()))?; - let mut space = sqlx::query_as::<_, Space>( - "SELECT id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at FROM spaces WHERE id = ? AND owner_id = ?" + let mut project = sqlx::query_as::<_, Project>( + "SELECT id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at FROM projects WHERE id = ? AND owner_id = ?" ) .bind(&id) .bind(&user_id) .fetch_optional(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? - .ok_or((StatusCode::NOT_FOUND, "Space not found".to_string()))?; + .ok_or((StatusCode::NOT_FOUND, "Project not found".to_string()))?; if let Some(name) = payload.name { - space.name = name; + project.name = name; } if let Some(entrypoint) = payload.entrypoint { - space.entrypoint = entrypoint; + project.entrypoint = entrypoint; } if let Some(folder_id) = payload.folder_id { - space.folder_id = if folder_id.is_empty() { None } else { Some(folder_id) }; + project.folder_id = if folder_id.is_empty() { None } else { Some(folder_id) }; } if let Some(public_role) = payload.public_role { - space.public_role = if public_role == "none" || public_role.is_empty() { + project.public_role = if public_role == "none" || public_role.is_empty() { None } else { Some(public_role) }; } - let space = sqlx::query_as::<_, Space>( - "UPDATE spaces SET name = ?, entrypoint = ?, folder_id = ?, public_role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND owner_id = ? RETURNING id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at" + let project = sqlx::query_as::<_, Project>( + "UPDATE projects SET name = ?, entrypoint = ?, folder_id = ?, public_role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND owner_id = ? RETURNING id, owner_id, folder_id, name, entrypoint, thumbnail_svg, public_role, created_at, updated_at" ) - .bind(&space.name) - .bind(&space.entrypoint) - .bind(&space.folder_id) - .bind(&space.public_role) + .bind(&project.name) + .bind(&project.entrypoint) + .bind(&project.folder_id) + .bind(&project.public_role) .bind(&id) .bind(&user_id) .fetch_one(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - Ok(Json(space)) + Ok(Json(project)) } -pub async fn delete_space( +pub async fn delete_project( State(state): State, Path(id): Path, jar: SignedCookieJar, @@ -347,12 +347,12 @@ pub async fn delete_space( let user_id = jar.get("session_user_id").map(|c| c.value().to_string()) .ok_or((StatusCode::UNAUTHORIZED, "Not logged in".to_string()))?; - let _ = sqlx::query("DELETE FROM space_files WHERE space_id = ?") + let _ = sqlx::query("DELETE FROM project_files WHERE project_id = ?") .bind(&id) .execute(&state.db) .await; - let result = sqlx::query("DELETE FROM spaces WHERE id = ? AND owner_id = ?") + let result = sqlx::query("DELETE FROM projects WHERE id = ? AND owner_id = ?") .bind(&id) .bind(&user_id) .execute(&state.db) @@ -360,24 +360,24 @@ pub async fn delete_space( .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if result.rows_affected() == 0 { - return Err((StatusCode::NOT_FOUND, "Space not found or unauthorized".to_string())); + return Err((StatusCode::NOT_FOUND, "Project not found or unauthorized".to_string())); } Ok(StatusCode::NO_CONTENT) } -pub async fn list_space_files( +pub async fn list_project_files( State(state): State, Path(id): Path, jar: SignedCookieJar, -) -> Result>, (StatusCode, String)> { +) -> Result>, (StatusCode, String)> { let user_id_opt = jar.get("session_user_id").map(|c| c.value().to_string()); - space_role(&state, &id, &user_id_opt) + project_role(&state, &id, &user_id_opt) .await .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized".to_string()))?; - let files = sqlx::query_as::<_, SpaceFile>( - "SELECT id, space_id, path, kind, mime_type, created_at FROM space_files WHERE space_id = ? ORDER BY path ASC" + let files = sqlx::query_as::<_, ProjectFile>( + "SELECT id, project_id, path, kind, mime_type, created_at FROM project_files WHERE project_id = ? ORDER BY path ASC" ) .bind(&id) .fetch_all(&state.db) @@ -387,14 +387,14 @@ pub async fn list_space_files( Ok(Json(files)) } -pub async fn create_space_file( +pub async fn create_project_file( State(state): State, Path(id): Path, jar: SignedCookieJar, - Json(payload): Json, -) -> Result, (StatusCode, String)> { + Json(payload): Json, +) -> Result, (StatusCode, String)> { let user_id_opt = jar.get("session_user_id").map(|c| c.value().to_string()); - let (_, role) = space_role(&state, &id, &user_id_opt) + let (_, role) = project_role(&state, &id, &user_id_opt) .await .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized".to_string()))?; if role == "viewer" { @@ -405,8 +405,8 @@ pub async fn create_space_file( let content = payload.content.unwrap_or_default(); let file_id = Uuid::new_v4().to_string(); - let file = sqlx::query_as::<_, SpaceFile>( - "INSERT INTO space_files (id, space_id, path, kind, content, mime_type) VALUES (?, ?, ?, ?, ?, 'text/plain') RETURNING id, space_id, path, kind, mime_type, created_at" + let file = sqlx::query_as::<_, ProjectFile>( + "INSERT INTO project_files (id, project_id, path, kind, content, mime_type) VALUES (?, ?, ?, ?, ?, 'text/plain') RETURNING id, project_id, path, kind, mime_type, created_at" ) .bind(&file_id) .bind(&id) @@ -420,14 +420,14 @@ pub async fn create_space_file( Ok(Json(file)) } -pub async fn upload_space_file( +pub async fn upload_project_file( State(state): State, Path(id): Path, jar: SignedCookieJar, mut multipart: Multipart, ) -> Result, (StatusCode, String)> { let user_id_opt = jar.get("session_user_id").map(|c| c.value().to_string()); - let (_, role) = space_role(&state, &id, &user_id_opt) + let (_, role) = project_role(&state, &id, &user_id_opt) .await .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized".to_string()))?; if role == "viewer" { @@ -449,8 +449,8 @@ pub async fn upload_space_file( }; let _ = sqlx::query( - "INSERT INTO space_files (id, space_id, path, kind, content, mime_type) VALUES (?, ?, ?, ?, ?, ?) \ - ON CONFLICT (space_id, path) DO UPDATE SET content = excluded.content, kind = excluded.kind, mime_type = excluded.mime_type" + "INSERT INTO project_files (id, project_id, path, kind, content, mime_type) VALUES (?, ?, ?, ?, ?, ?) \ + ON CONFLICT (project_id, path) DO UPDATE SET content = excluded.content, kind = excluded.kind, mime_type = excluded.mime_type" ) .bind(Uuid::new_v4().to_string()) .bind(&id) @@ -468,18 +468,18 @@ pub async fn upload_space_file( Ok(Json(serde_json::json!({ "files": uploaded }))) } -pub async fn get_space_file( +pub async fn get_project_file( State(state): State, Path((id, file_id)): Path<(String, String)>, jar: SignedCookieJar, ) -> Result { let user_id_opt = jar.get("session_user_id").map(|c| c.value().to_string()); - space_role(&state, &id, &user_id_opt) + project_role(&state, &id, &user_id_opt) .await .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized".to_string()))?; let file = sqlx::query_as::<_, (String, String, Option>)>( - "SELECT kind, mime_type, content FROM space_files WHERE id = ? AND space_id = ?" + "SELECT kind, mime_type, content FROM project_files WHERE id = ? AND project_id = ?" ) .bind(&file_id) .bind(&id) @@ -498,21 +498,21 @@ pub async fn get_space_file( } } -pub async fn update_space_file( +pub async fn update_project_file( State(state): State, Path((id, file_id)): Path<(String, String)>, jar: SignedCookieJar, - Json(payload): Json, + Json(payload): Json, ) -> Result { let user_id_opt = jar.get("session_user_id").map(|c| c.value().to_string()); - let (_, role) = space_role(&state, &id, &user_id_opt) + let (_, role) = project_role(&state, &id, &user_id_opt) .await .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized".to_string()))?; if role == "viewer" { return Err((StatusCode::FORBIDDEN, "Read-only access".to_string())); } - let result = sqlx::query("UPDATE space_files SET path = ? WHERE id = ? AND space_id = ?") + let result = sqlx::query("UPDATE project_files SET path = ? WHERE id = ? AND project_id = ?") .bind(&payload.path) .bind(&file_id) .bind(&id) @@ -527,20 +527,20 @@ pub async fn update_space_file( Ok(StatusCode::NO_CONTENT) } -pub async fn delete_space_file( +pub async fn delete_project_file( State(state): State, Path((id, file_id)): Path<(String, String)>, jar: SignedCookieJar, ) -> Result { let user_id_opt = jar.get("session_user_id").map(|c| c.value().to_string()); - let (_, role) = space_role(&state, &id, &user_id_opt) + let (_, role) = project_role(&state, &id, &user_id_opt) .await .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized".to_string()))?; if role == "viewer" { return Err((StatusCode::FORBIDDEN, "Read-only access".to_string())); } - let result = sqlx::query("DELETE FROM space_files WHERE id = ? AND space_id = ?") + let result = sqlx::query("DELETE FROM project_files WHERE id = ? AND project_id = ?") .bind(&file_id) .bind(&id) .execute(&state.db) diff --git a/src/lib/components/PublishPackageModal.svelte b/src/lib/components/PublishPackageModal.svelte index 2e83f84..84e567c 100644 --- a/src/lib/components/PublishPackageModal.svelte +++ b/src/lib/components/PublishPackageModal.svelte @@ -2,10 +2,10 @@ import Icon from '@iconify/svelte'; let { - spaceId, + projectId, onClose }: { - spaceId: string; + projectId: string; onClose: () => void; } = $props(); @@ -22,7 +22,7 @@ const res = await fetch('/api/packages/publish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ space_id: spaceId, version: version.trim() || undefined }) + body: JSON.stringify({ project_id: projectId, version: version.trim() || undefined }) }); if (!res.ok) { error = await res.text(); @@ -45,7 +45,7 @@

- Snapshots this space's files into an immutable package version, importable instance-wide as + Snapshots this project's files into an immutable package version, importable instance-wide as @typstdrive/<name>:<version>. The name, version and entrypoint come from your typst.toml.

diff --git a/src/lib/components/dashboard/CreateSpaceModal.svelte b/src/lib/components/dashboard/CreateProjectModal.svelte similarity index 75% rename from src/lib/components/dashboard/CreateSpaceModal.svelte rename to src/lib/components/dashboard/CreateProjectModal.svelte index cfd73d1..c8fc255 100644 --- a/src/lib/components/dashboard/CreateSpaceModal.svelte +++ b/src/lib/components/dashboard/CreateProjectModal.svelte @@ -1,24 +1,24 @@