remove legacy RawAppService endpoints

This commit is contained in:
Ruben Fiszel
2026-02-08 23:59:40 +00:00
parent 5da830b1dd
commit 71db1ae68f
10 changed files with 11 additions and 557 deletions

View File

@@ -7769,23 +7769,6 @@ paths:
items:
$ref: "#/components/schemas/ListableRawApp"
/w/{workspace}/raw_apps/exists/{path}:
get:
summary: does an app exisst at path
operationId: existsRawApp
tags:
- raw_app
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: app exists
content:
application/json:
schema:
type: boolean
/w/{workspace}/apps/get_data/v/{secretWithExtension}:
get:
summary: get raw app data by
@@ -8215,88 +8198,6 @@ paths:
schema:
$ref: "#/components/schemas/AppWithLastVersion"
/w/{workspace}/raw_apps/create:
post:
summary: create raw app
operationId: createRawApp
tags:
- raw_app
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: new raw app
required: true
content:
application/json:
schema:
type: object
properties:
path:
type: string
value:
type: string
summary:
type: string
required:
- path
- value
- summary
responses:
"201":
description: raw app created
content:
text/plain:
schema:
type: string
/w/{workspace}/raw_apps/update/{path}:
post:
summary: update app
operationId: updateRawApp
tags:
- raw_app
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
requestBody:
description: updateraw app
required: true
content:
application/json:
schema:
type: object
properties:
path:
type: string
summary:
type: string
value:
type: string
responses:
"200":
description: app updated
content:
text/plain:
schema:
type: string
/w/{workspace}/raw_apps/delete/{path}:
delete:
summary: delete raw app
operationId: deleteRawApp
tags:
- raw_app
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: app deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/apps/delete/{path}:
delete:
summary: delete app

View File

@@ -5,42 +5,29 @@
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use crate::{
db::{ApiAuthed, DB},
users::require_owner_of_path,
utils::check_scopes,
webhook_util::{WebhookMessage, WebhookShared},
};
use crate::{db::ApiAuthed, utils::check_scopes};
use axum::{
body::Body,
extract::{Extension, Json, Path, Query},
response::Response,
routing::{delete, get, post},
routing::get,
Router,
};
use hyper::{header, StatusCode};
use hyper::header;
use serde::{Deserialize, Serialize};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::FromRow;
use std::str;
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_common::{
apps::ListAppQuery,
db::UserDB,
error::{Error, JsonResult, Result},
utils::{not_found_if_none, paginate, Pagination, StripPath},
worker::CLOUD_HOSTED,
};
pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_apps))
.route("/get_data/:version/*path", get(get_data))
.route("/exists/*path", get(exists_app))
.route("/update/*path", post(update_app))
.route("/delete/*path", delete(delete_app))
.route("/create", post(create_app))
}
#[derive(FromRow, Deserialize, Serialize)]
@@ -54,20 +41,6 @@ pub struct ListableApp {
pub version: i32,
}
#[derive(Deserialize)]
pub struct CreateApp {
pub path: String,
pub summary: String,
pub value: String,
}
#[derive(Deserialize)]
pub struct EditApp {
pub path: Option<String>,
pub summary: Option<String>,
pub value: Option<String>,
}
async fn list_apps(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -147,230 +120,3 @@ async fn get_data(
Ok(res.body(Body::from(app)).unwrap())
}
async fn create_app(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
if authed.is_operator {
return Err(Error::NotAuthorized(
"Operators cannot create raw apps for security reasons".to_string(),
));
}
check_scopes(&authed, || format!("raw_apps:write:{}", app.path))?;
if *CLOUD_HOSTED {
let nb_apps = sqlx::query_scalar!(
"SELECT COUNT(*) FROM raw_app WHERE workspace_id = $1",
&w_id
)
.fetch_one(&db)
.await?;
if nb_apps.unwrap_or(0) >= 1000 {
return Err(Error::BadRequest(
"You have reached the maximum number of apps (1000) on cloud. Contact support@windmill.dev to increase the limit"
.to_string(),
));
}
if app.summary.len() > 300 {
return Err(Error::BadRequest(
"Summary must be less than 300 characters on cloud".to_string(),
));
}
}
let mut tx = user_db.begin(&authed).await?;
if &app.path == "" {
return Err(Error::BadRequest("App path cannot be empty".to_string()));
}
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
app.path,
w_id
)
.fetch_one(&mut *tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"App with path {} already exists",
&app.path
)));
}
sqlx::query!(
"INSERT INTO raw_app
(workspace_id, path, summary, extra_perms, data)
VALUES ($1, $2, $3, '{}', $4)",
w_id,
app.path,
app.summary,
app.value,
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
"apps.create",
ActionKind::Create,
&w_id,
Some(&app.path),
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateApp { workspace: w_id, path: app.path.clone() },
);
Ok((StatusCode::CREATED, app.path))
}
async fn delete_app(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("raw_apps:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"DELETE FROM raw_app WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
"apps.delete",
ActionKind::Delete,
&w_id,
Some(path),
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::DeleteApp { workspace: w_id, path: path.to_owned() },
);
Ok(format!("app {} deleted", path))
}
async fn update_app(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(app): Json<EditApp>,
) -> Result<String> {
if authed.is_operator {
return Err(Error::NotAuthorized(
"Operators cannot update raw apps for security reasons".to_string(),
));
}
use sql_builder::prelude::*;
let path = path.to_path();
check_scopes(&authed, || format!("raw_apps:write:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let mut sqlb = SqlBuilder::update_table("raw_app");
sqlb.and_where_eq("path", "?".bind(&path));
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
let npath = &app.path;
if npath.is_some() || app.summary.is_some() {
if let Some(npath) = npath {
if npath != path {
require_owner_of_path(&authed, path)?;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)",
npath,
w_id
)
.fetch_one(&mut *tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"App with path {} already exists",
npath
)));
}
}
sqlb.set_str("path", npath);
}
if let Some(nsummary) = &app.summary {
sqlb.set_str("summary", nsummary);
}
}
if let Some(value) = &app.value {
sqlb.set_str("data", value);
sqlb.set("version", "version + 1");
}
sqlb.returning("path");
let sql = sqlb.sql().map_err(|e| Error::internal_err(e.to_string()))?;
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut *tx).await?;
not_found_if_none(npath_o, "Raw App", path)?;
let npath = app.path.clone().unwrap_or_else(|| path.to_owned());
audit_log(
&mut *tx,
&authed,
"apps.update",
ActionKind::Update,
&w_id,
Some(&path),
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateApp {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("app {} updated (npath: {:?})", path, npath))
}
async fn exists_app(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
// Note: exists_app doesn't require authentication, so no scope check needed
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}

View File

@@ -16,7 +16,6 @@
AppService,
FlowService,
FolderService,
RawAppService,
ResourceService,
ScheduleService,
ScriptService,
@@ -198,21 +197,6 @@
path: path
})
return resource.schema
} else if (kind == 'raw_app') {
throw new Error('Raw app deploy not implemented yet')
// const app = await RawAppService.getRawAppData({
// workspace: workspace,
// path: path
// })
// if (alreadyExists) {
// }
// await RawAppService.updateRawApp({
// workspace: workspace,
// path: path,
// requestBody: {
// path: path
// }
// })
} else if (kind == 'folder') {
const folder = await FolderService.getFolder({
workspace: workspace,
@@ -293,11 +277,6 @@
workspace: workspace,
path: path
})
} else if (kind == 'raw_app') {
exists = await RawAppService.existsRawApp({
workspace: workspace,
path: path
})
} else if (kind == 'variable') {
exists = await VariableService.existsVariable({
workspace: workspace,
@@ -573,21 +552,6 @@
}
})
}
} else if (kind == 'raw_app') {
throw new Error('Raw app deploy not implemented yet')
// const app = await RawAppService.getRawAppData({
// workspace: workspaceFrom,
// path: path
// })
// if (alreadyExists) {
// }
// await RawAppService.updateRawApp({
// workspace: workspaceFrom,
// path: path,
// requestBody: {
// path: path
// }
// })
} else if (kind == 'folder') {
await FolderService.createFolder({
workspace: workspaceToDeployTo,

View File

@@ -6,7 +6,6 @@
AppService,
FlowService,
FolderService,
RawAppService,
ResourceService,
ScheduleService,
ScriptService,
@@ -232,11 +231,6 @@
workspace: workspaceToDeployTo!,
path: path
})
} else if (kind == 'raw_app') {
exists = await RawAppService.existsRawApp({
workspace: workspaceToDeployTo!,
path: path
})
} else if (kind == 'variable') {
exists = await VariableService.existsVariable({
workspace: workspaceToDeployTo!,
@@ -498,21 +492,6 @@
}
})
}
} else if (kind == 'raw_app') {
throw new Error('Raw app deploy not implemented yet')
// const app = await RawAppService.getRawAppData({
// workspace: $workspaceStore!,
// path: path
// })
// if (alreadyExists) {
// }
// await RawAppService.updateRawApp({
// workspace: $workspaceStore!,
// path: path,
// requestBody: {
// path: path
// }
// })
} else if (kind == 'folder') {
await FolderService.createFolder({
workspace: workspaceToDeployTo!,
@@ -616,21 +595,6 @@
path: path
})
return resource.schema
} else if (kind == 'raw_app') {
throw new Error('Raw app deploy not implemented yet')
// const app = await RawAppService.getRawAppData({
// workspace: workspace,
// path: path
// })
// if (alreadyExists) {
// }
// await RawAppService.updateRawApp({
// workspace: workspace,
// path: path,
// requestBody: {
// path: path
// }
// })
} else if (kind == 'folder') {
const folder = await FolderService.getFolder({
workspace: workspace,

View File

@@ -4,12 +4,12 @@
import { Alert, Button, Drawer } from './common'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import Path from './Path.svelte'
import { AppService, FlowService, RawAppService, ScriptService } from '$lib/gen'
import { AppService, FlowService, ScriptService } from '$lib/gen'
import { isOwner } from '$lib/utils'
const dispatch = createEventDispatcher()
type Kind = 'script' | 'resource' | 'schedule' | 'variable' | 'flow' | 'app' | 'raw_app'
type Kind = 'script' | 'resource' | 'schedule' | 'variable' | 'flow' | 'app'
let kind: Kind
let initialPath: string = ''
@@ -83,15 +83,6 @@
summary
}
})
} else if (kind == 'raw_app') {
await RawAppService.updateRawApp({
workspace: $workspaceStore!,
path: initialPath,
requestBody: {
path: path != initialPath ? path : undefined,
summary
}
})
}
dispatch('update', path)
drawer.closeDrawer()

View File

@@ -1,20 +1,13 @@
<script lang="ts">
import { base } from '$lib/base'
import Dropdown from '$lib/components/DropdownV2.svelte'
import type MoveDrawer from '$lib/components/MoveDrawer.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import type ShareModal from '$lib/components/ShareModal.svelte'
import { RawAppService, type ListableRawApp } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import Button from '../button/Button.svelte'
import { type ListableRawApp } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import Row from './Row.svelte'
import Drawer from '../drawer/Drawer.svelte'
import DrawerContent from '../drawer/DrawerContent.svelte'
import FileInput from '../fileInput/FileInput.svelte'
import { goto } from '$lib/navigation'
import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
import { FolderOpen, Globe, Pen, Share, Trash } from 'lucide-svelte'
import { Globe, Share } from 'lucide-svelte'
import { isDeployable } from '$lib/utils_deployable'
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
@@ -22,8 +15,6 @@
app: ListableRawApp & { canWrite: boolean }
marked: string | undefined
shareModal: ShareModal
moveDrawer: MoveDrawer
deleteConfirmedCallback: (() => void) | undefined
deploymentDrawer: DeployWorkspaceDrawer
depth?: number
menuOpen?: boolean
@@ -33,39 +24,12 @@
app,
marked,
shareModal,
moveDrawer,
deleteConfirmedCallback = $bindable(),
deploymentDrawer,
depth = 0,
menuOpen = $bindable(false)
}: Props = $props()
let updateAppDrawer: Drawer | undefined = $state(undefined)
const dispatch = createEventDispatcher()
</script>
{#if menuOpen}
<Drawer bind:this={updateAppDrawer} size="800px">
<DrawerContent title="Update app" on:close={() => updateAppDrawer?.toggleDrawer?.()}>
<FileInput
accept={'.js'}
multiple={false}
convertTo={'text'}
iconSize={24}
class="text-sm py-4"
on:change={async ({ detail }) => {
await RawAppService.updateRawApp({
workspace: $workspaceStore ?? '',
path: app.path,
requestBody: { value: detail?.[0] }
})
goto(`/apps/get_raw/${app.version + 1}/${app.path}`)
}}
/>
</DrawerContent>
</Drawer>
{/if}
<Row
href="{base}/apps/get_raw/{app.version}/{app.path}"
kind="raw_app"
@@ -80,35 +44,11 @@
<SharedBadge canWrite={app.canWrite} extraPerms={app.extra_perms} />
{/snippet}
{#snippet actions()}
<span class="hidden md:inline-flex gap-x-1">
{#if !$userStore?.operator}
{#if app.canWrite}
<div>
<Button
variant="subtle"
unifiedSize="md"
startIcon={{ icon: Pen }}
on:click={() => updateAppDrawer?.toggleDrawer?.()}
>
Edit
</Button>
</div>
{/if}
{/if}
</span>
<Dropdown
items={async () => {
let { summary, path, canWrite } = app
let { path, canWrite } = app
return [
{
displayName: 'Move/Rename',
icon: FolderOpen,
action: () => {
moveDrawer.openDrawer(path, summary, 'raw_app')
},
disabled: !canWrite
},
...(isDeployable('app', path, await getDeployUiSettings())
? [
{
@@ -126,25 +66,6 @@
action: () => {
shareModal.openDrawer && shareModal.openDrawer(path, 'raw_app')
}
},
{
displayName: 'Delete',
icon: Trash,
action: async (event) => {
// TODO
// @ts-ignore
if (event?.shiftKey) {
await RawAppService.deleteRawApp({ workspace: $workspaceStore ?? '', path })
dispatch('change')
} else {
deleteConfirmedCallback = async () => {
await RawAppService.deleteRawApp({ workspace: $workspaceStore ?? '', path })
dispatch('change')
}
}
},
type: 'delete',
disabled: !canWrite
}
]
}}

View File

@@ -72,11 +72,8 @@
/>
{:else if item.type == 'raw_app'}
<RawAppRow
bind:deleteConfirmedCallback
marked={item.marked}
on:change={() => dispatch('rawAppChanged')}
app={item}
{moveDrawer}
{shareModal}
{deploymentDrawer}
{depth}

View File

@@ -9,8 +9,7 @@
type Script,
ScriptService,
type Flow,
type ListableRawApp,
RawAppService
type ListableRawApp
} from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import type uFuzzy from '@leeoniya/ufuzzy'
@@ -134,17 +133,7 @@
}
async function loadRawApps(): Promise<void> {
raw_apps = (await RawAppService.listRawApps({ workspace: $workspaceStore! })).map(
(app: ListableRawApp) => {
return {
canWrite:
canWrite(app.path!, app.extra_perms!, $userStore) &&
app.workspace_id == $workspaceStore &&
!$userStore?.operator,
...app
}
}
)
raw_apps = []
loading = false
}

View File

@@ -3,7 +3,6 @@
import {
AppService,
FlowService,
RawAppService,
ScriptService,
type Flow,
type ListableApp,
@@ -492,7 +491,6 @@
withoutDescription: true
})
const apps = await AppService.listApps({ workspace: $workspaceStore! })
const raw_apps = await RawAppService.listRawApps({ workspace: $workspaceStore! })
let combinedItems: (TableScript | TableFlow | TableApp | TableRawApp)[] | undefined = [
...flows.map((x) => ({
@@ -512,12 +510,6 @@
type: 'app' as 'app',
time: new Date(x.edited_at).getTime(),
search_id: x.path
})),
...raw_apps.map((x) => ({
...x,
type: 'raw_app' as 'raw_app',
time: new Date(x.edited_at).getTime(),
search_id: x.path
}))
].sort((a, b) => (a.starred != b.starred ? (a.starred ? -1 : 1) : a.time - b.time > 0 ? -1 : 1))

View File

@@ -6,7 +6,6 @@
AssetService,
FlowService,
OpenAPI,
RawAppService,
ScriptService,
SettingService,
UserService,
@@ -192,10 +191,6 @@
workspace: $workspaceStore ?? '',
starredOnly: true
})
const raw_apps = await RawAppService.listRawApps({
workspace: $workspaceStore ?? '',
starredOnly: true
})
const assets = await AssetService.listFavoriteAssets({ workspace: $workspaceStore ?? '' })
favoriteManager.current = [
...scripts.map((s) => ({
@@ -216,12 +211,6 @@
href: getFavoriteHref(f.path, 'app'),
kind: 'app' as const
})),
...raw_apps.map((f) => ({
label: f.summary || getFavoriteLabel(f.path, 'raw_app'),
path: f.path,
href: getFavoriteHref(f.path, 'raw_app'),
kind: 'raw_app' as const
})),
...assets.map((a) => ({
label: getFavoriteLabel(a.path, 'asset'),
path: a.path,