diff --git a/backend/.sqlx/query-0877f40f086fb5324af54701b29c1edc9d3afb796a06ae4c673aa4c54ad87ddb.json b/backend/.sqlx/query-0877f40f086fb5324af54701b29c1edc9d3afb796a06ae4c673aa4c54ad87ddb.json new file mode 100644 index 0000000000..1235f39f88 --- /dev/null +++ b/backend/.sqlx/query-0877f40f086fb5324af54701b29c1edc9d3afb796a06ae4c673aa4c54ad87ddb.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT path, value as \"value: _\", typ as \"typ: DraftType\"\n FROM draft\n WHERE path = $1 AND typ = $2 AND workspace_id = $3 AND email = $4", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "value: _", + "type_info": "Json" + }, + { + "ordinal": 2, + "name": "typ: DraftType", + "type_info": { + "Custom": { + "name": "draft_type", + "kind": { + "Enum": [ + "script", + "flow", + "app" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Text", + { + "Custom": { + "name": "draft_type", + "kind": { + "Enum": [ + "script", + "flow", + "app" + ] + } + } + }, + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "0877f40f086fb5324af54701b29c1edc9d3afb796a06ae4c673aa4c54ad87ddb" +} diff --git a/backend/.sqlx/query-67f91733c44e9db3124a713dd950e2bad22709254f20a2c385377b9a00e7d79f.json b/backend/.sqlx/query-67f91733c44e9db3124a713dd950e2bad22709254f20a2c385377b9a00e7d79f.json new file mode 100644 index 0000000000..5c39b240e5 --- /dev/null +++ b/backend/.sqlx/query-67f91733c44e9db3124a713dd950e2bad22709254f20a2c385377b9a00e7d79f.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT path, value as \"value: _\", typ as \"typ: DraftType\", created_at\n FROM draft\n WHERE workspace_id = $1 AND email = $2\n ORDER BY created_at DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "value: _", + "type_info": "Json" + }, + { + "ordinal": 2, + "name": "typ: DraftType", + "type_info": { + "Custom": { + "name": "draft_type", + "kind": { + "Enum": [ + "script", + "flow", + "app" + ] + } + } + } + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "67f91733c44e9db3124a713dd950e2bad22709254f20a2c385377b9a00e7d79f" +} diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 00616c56a3..6f444c1a48 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -7854,6 +7854,74 @@ paths: schema: type: string + /w/{workspace}/drafts/get/{kind}/{path}: + get: + summary: get draft + operationId: getDraft + tags: + - draft + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: kind + in: path + required: true + schema: + type: string + enum: + - script + - flow + - app + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: draft + content: + application/json: + schema: + type: object + properties: + path: + type: string + typ: + type: string + enum: ["flow", "script", "app"] + value: {} + required: + - path + - typ + + /w/{workspace}/drafts/list: + get: + summary: list drafts of the current user + operationId: listDrafts + tags: + - draft + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: list of drafts + content: + application/json: + schema: + type: array + items: + type: object + properties: + path: + type: string + typ: + type: string + enum: ["flow", "script", "app"] + value: {} + created_at: + type: string + format: date-time + required: + - path + - typ + - created_at + /w/{workspace}/scripts/create: post: summary: create script diff --git a/backend/windmill-api/src/drafts.rs b/backend/windmill-api/src/drafts.rs index bb91873d47..91c90a9dfb 100644 --- a/backend/windmill-api/src/drafts.rs +++ b/backend/windmill-api/src/drafts.rs @@ -13,17 +13,23 @@ use crate::{ use axum::{ extract::{Extension, Path}, - routing::{delete, post}, + routing::{delete, get, post}, Json, Router, }; use hyper::StatusCode; use serde::{Deserialize, Serialize}; -use windmill_common::{db::UserDB, error::Result, utils::StripPath}; +use windmill_common::{ + db::UserDB, + error::{JsonResult, Result}, + utils::{not_found_if_none, StripPath}, +}; pub fn workspaced_service() -> Router { Router::new() .route("/create", post(create_draft)) .route("/delete/{kind}/{*path}", delete(delete_draft)) + .route("/get/{kind}/{*path}", get(get_draft)) + .route("/list", get(list_drafts)) } #[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone)] @@ -117,24 +123,63 @@ async fn delete_draft( Ok(format!("deleted draft")) } -// async fn get_draft( -// authed: ApiAuthed, -// Extension(user_db): Extension, -// Path((w_id, path)): Path<(String, StripPath)>, -// ) -> JsonResult { -// let path = path.to_path(); -// let mut tx = user_db.begin(&authed).await?; +// Fetch a single draft (scoped to the requesting user). Used by the editors to +// (re)open a never-deployed item that lives only in the `draft` table. +async fn get_draft( + authed: ApiAuthed, + Extension(user_db): Extension, + Path((w_id, kind, path)): Path<(String, DraftType, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + let mut tx = user_db.begin(&authed).await?; -// let script_o = sqlx::query_as!( -// Draft, -// r#"SELECT path, value, typ as "typ: DraftType" FROM draft WHERE path = $1 AND workspace_id = $2"#, -// path, -// w_id -// ) -// .fetch_optional(&mut *tx) -// .await?; -// tx.commit().await?; + let draft_o = sqlx::query_as!( + Draft, + r#"SELECT path, value as "value: _", typ as "typ: DraftType" + FROM draft + WHERE path = $1 AND typ = $2 AND workspace_id = $3 AND email = $4"#, + path, + kind as DraftType, + w_id, + &authed.email, + ) + .fetch_optional(&mut *tx) + .await?; + tx.commit().await?; -// let draft = not_found_if_none(script_o, "draft", path)?; -// Ok(Json(draft)) -// } + let draft = not_found_if_none(draft_o, "draft", path)?; + Ok(Json(draft)) +} + +#[derive(Serialize, Debug)] +pub struct DraftWithMetadata { + pub path: String, + pub value: sqlx::types::Json>, + pub typ: DraftType, + pub created_at: chrono::DateTime, +} + +// List the requesting user's drafts. The editors/home use this to surface +// never-deployed items (which no longer have a row in script/flow/app). +async fn list_drafts( + authed: ApiAuthed, + Extension(user_db): Extension, + Path(w_id): Path, +) -> JsonResult> { + let mut tx = user_db.begin(&authed).await?; + + let drafts = sqlx::query_as!( + DraftWithMetadata, + r#"SELECT path, value as "value: _", typ as "typ: DraftType", created_at + FROM draft + WHERE workspace_id = $1 AND email = $2 + ORDER BY created_at DESC"#, + w_id, + &authed.email, + ) + .fetch_all(&mut *tx) + .await?; + tx.commit().await?; + + Ok(Json(drafts)) +}