Skip to content

feat: generate http route triggers from openapi spec #5857

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
nits
  • Loading branch information
dieriba committed Jun 5, 2025
commit 6f2e5632334ba25361f1af04f6812e13b7fb0bea
39 changes: 25 additions & 14 deletions backend/windmill-api/src/http_triggers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,11 +368,14 @@ async fn create_trigger_inner(
Ok(())
}

fn check_no_duplicates(new_http_triggers: &[NewTrigger]) -> Result<(), Error> {
fn check_no_duplicates<'trigger>(
new_http_triggers: &[NewTrigger],
route_path_key: &[Cow<'trigger, str>],
) -> Result<(), Error> {
let mut seen = HashSet::with_capacity(new_http_triggers.len());

for trigger in new_http_triggers {
if !seen.insert((trigger.workspaced_route, &trigger.route_path, trigger.http_method)) {
for (i, trigger) in new_http_triggers.iter().enumerate() {
if !seen.insert((&route_path_key[i], trigger.http_method, trigger.workspaced_route)) {
return Err(Error::BadRequest(format!(
"Duplicate HTTP route detected: '{}'. Each HTTP route must have a unique 'route_path'.",
&trigger.route_path
Expand Down Expand Up @@ -401,18 +404,30 @@ async fn create_many_http_trigger(
.into()
};

check_no_duplicates(&new_http_triggers)?;

let mut tx = user_db.begin(&authed).await?;
let mut route_path_keys = Vec::with_capacity(new_http_triggers.len());

for new_http_trigger in new_http_triggers.iter() {
let route_path_key = validate_http_trigger(&db, &w_id, new_http_trigger)
.await
.map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?;

create_trigger_inner(&mut tx, &w_id, &authed, new_http_trigger, &route_path_key)
.await
.map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?;
route_path_keys.push(route_path_key);
}

check_no_duplicates(&new_http_triggers, &route_path_keys)?;

let mut tx = user_db.begin(&authed).await?;

for (i, new_http_trigger) in new_http_triggers.iter().enumerate() {
create_trigger_inner(
&mut tx,
&w_id,
&authed,
new_http_trigger,
&route_path_keys[i],
)
.await
.map_err(|err| error_wrapper(&new_http_trigger.route_path, err))?;
}

tx.commit().await?;
Expand Down Expand Up @@ -975,11 +990,7 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
router
.insert(full_path.clone(), trigger.clone())
.unwrap_or_else(|e| {
tracing::warn!(
"Failed to consider HTTP route {}: {:?}",
full_path,
e,
);
tracing::warn!("Failed to consider HTTP route {}: {:?}", full_path, e,);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import Required from '$lib/components/Required.svelte'
import { Drawer, DrawerContent } from '$lib/components/common'
import { get } from 'svelte/store'
import { Pane, Splitpanes } from 'svelte-splitpanes'

type Props = {
closeFn: () => Promise<void>
Expand Down Expand Up @@ -143,7 +142,7 @@
startIcon={{ icon: Save }}
on:click={saveHttpTrigger}
>
Save HTTP routes
Save routes
</Button>
</svelte:fragment>
{@render config()}
Expand Down Expand Up @@ -257,70 +256,61 @@
</Button>
</div>
{/if}
{#if selected === 'OpenAPI' || (selected === 'OpenAPI_File' && !emptyStringTrimmed(openApiFile)) || (selected === 'OpenAPI_URL' && !emptyStringTrimmed(openApiUrl))}
{#key forceRerender}
<SimpleEditor class="h-96" {lang} bind:code />
{/key}
{/if}
<Button
spacingSize="sm"
size="xs"
btnClasses="h-8"
loading={isGeneratingHttpRoutes}
on:click={generateHttpTrigger}
disabled={code.length === 0}
color="light"
variant="border">Generate HTTP routes</Button
>
</div>
</Subsection>
<Splitpanes class="overflow-hidden flex flex-col" horizontal>
<Pane size={80} minSize={20}>
<div class="h-full flex flex-col gap-1">
{#if selected === 'OpenAPI' || (selected === 'OpenAPI_File' && !emptyStringTrimmed(openApiFile)) || (selected === 'OpenAPI_URL' && !emptyStringTrimmed(openApiUrl))}
{#key forceRerender}
<SimpleEditor class="min-h-0 flex-1" {lang} bind:code />
{/key}
{/if}
<Button
spacingSize="sm"
size="xs"
btnClasses="h-8"
loading={isGeneratingHttpRoutes}
on:click={generateHttpTrigger}
disabled={code.length === 0}
color="light"
variant="border">Generate HTTP routes</Button
>
</div>
</Pane>
<Pane class="flex-1 overflow-auto" minSize={20}>
<Subsection>
<div class="flex flex-col gap-1 mb-2">
{#each httpTriggers as httpTrigger, index}
<div
class="hover:bg-surface-hover w-full items-center px-4 py-2 gap-4 first-of-type:!border-t-0

<div class="flex flex-col gap-1 mb-2">
{#each httpTriggers as httpTrigger, index}
<div
class="hover:bg-surface-hover w-full items-center px-4 py-2 gap-4 first-of-type:!border-t-0
first-of-type:rounded-t-md last-of-type:rounded-b-md flex justify-between mt-2"
>
<div>
<div class="text-primary">
{httpTrigger.http_method.toUpperCase()}
{isCloudHosted() || httpTrigger.workspaced_route
? $workspaceStore! + '/' + httpTrigger.route_path
: httpTrigger.route_path}
</div>
<div class="text-secondary text-xs truncate text-left font-light">
{httpTrigger.path}
</div>
</div>
>
<div>
<div class="text-primary">
{httpTrigger.http_method.toUpperCase()}
{isCloudHosted() || httpTrigger.workspaced_route
? $workspaceStore! + '/' + httpTrigger.route_path
: httpTrigger.route_path}
</div>
<div class="text-secondary text-xs truncate text-left font-light">
{httpTrigger.path}
</div>
</div>

<div class="flex gap-2 items-center justify-end">
<Button
on:click={() => {
callback = (newHttpTrigger) => {
httpTriggers[index] = newHttpTrigger as NewHttpTrigger
httpTriggers = httpTriggers
}
openRouteEditor(httpTriggers[index].path, httpTriggers[index])
}}
size="xs"
startIcon={{ icon: Pen }}
color="gray"
>
Edit
</Button>
</div>
</div>
{/each}
<div class="flex gap-2 items-center justify-end">
<Button
on:click={() => {
callback = (newHttpTrigger) => {
httpTriggers[index] = newHttpTrigger as NewHttpTrigger
httpTriggers = httpTriggers
}
openRouteEditor(httpTriggers[index].path, httpTriggers[index])
}}
size="xs"
startIcon={{ icon: Pen }}
color="gray"
>
Edit
</Button>
</div>
</Subsection>
</Pane>
</Splitpanes>
</div>
{/each}
</div>
{/if}
</div>
</Section>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/routes/(root)/(logged)/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@
workspace: $workspaceStore ?? '',
path
})
sendUserToast(`Successfully deleted trigger: ${path}`)
sendUserToast(`Successfully deleted HTTP route: ${path}`)
loadTriggers()
} catch (error) {
sendUserToast(error.body || error.message, true)
Expand Down
Loading