diff --git a/backend/windmill-api/src/folders.rs b/backend/windmill-api/src/folders.rs index cb10f45b8d..5115670e5a 100644 --- a/backend/windmill-api/src/folders.rs +++ b/backend/windmill-api/src/folders.rs @@ -16,10 +16,9 @@ use axum::{ routing::{delete, get, post}, Json, Router, }; -use itertools::Itertools; use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ - error::{self, Error, JsonResult, Result}, + error::{self, to_anyhow, Error, JsonResult, Result}, users::username_to_permissioned_as, utils::{not_found_if_none, paginate, Pagination}, }; @@ -262,13 +261,26 @@ async fn update_folder( sqlb.and_where_eq("workspace_id", "?".bind(&w_id)); if let Some(display_name) = ng.display_name { - sqlb.set("display_name", display_name); + sqlb.set("display_name", "?".bind(&display_name)); } if let Some(owners) = ng.owners { - sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(","))); + sqlb.set( + "owners", + "?".bind(&format!( + "{{{}}}", + owners + .iter() + .map(|x| format!("\"{x}\"")) + .collect::>() + .join(","), + )), + ); } if let Some(extra_perms) = ng.extra_perms { - sqlb.set_str("extra_perms", extra_perms.to_string()); + sqlb.set( + "extra_perms", + "?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?), + ); } sqlb.returning("*"); diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index dd2ee22f34..049ac59e63 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -1073,6 +1073,7 @@ struct ScriptMetadata { schema: Option, is_template: bool, lock: Vec, + kind: String, } enum ArchiveImpl { @@ -1235,6 +1236,7 @@ async fn tarball_workspace( description: script.description, schema: script.schema, is_template: script.is_template, + kind: script.kind.to_string(), lock, }; let metadata_str = serde_json::to_string_pretty(&metadata).unwrap(); diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs index 5a4a556cd3..36b1466919 100644 --- a/backend/windmill-common/src/scripts.rs +++ b/backend/windmill-common/src/scripts.rs @@ -7,7 +7,7 @@ */ use std::{ - fmt::Display, + fmt::{self, Display}, hash::{Hash, Hasher}, }; @@ -103,6 +103,18 @@ pub enum ScriptKind { Approval, } +impl Display for ScriptKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(match self { + ScriptKind::Trigger => "trigger", + ScriptKind::Failure => "failure", + ScriptKind::Script => "script", + ScriptKind::Approval => "approval", + })?; + Ok(()) + } +} + #[derive(Serialize)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct Script { diff --git a/cli/apps.ts b/cli/apps.ts index 46d0dd4456..72f1204bd3 100644 --- a/cli/apps.ts +++ b/cli/apps.ts @@ -1,7 +1,6 @@ import { Any, model, property } from "./decoverto.ts"; import { AppService, - AppWithLastVersion, colors, microdiff, Policy, @@ -31,7 +30,7 @@ export class AppFile implements Resource, PushDiffs { if (await AppService.existsApp({ workspace, path: remotePath })) { console.log( colors.bold.yellow( - `Applying ${diffs.length} diffs to existing app...`, + `Applying ${diffs.length} diffs to existing app... ${remotePath}`, ), ); const changeset: { @@ -87,19 +86,10 @@ export class AppFile implements Resource, PushDiffs { } } async push(workspace: string, remotePath: string): Promise { - let existing: AppWithLastVersion | undefined; - try { - existing = await AppService.getAppByPath({ - workspace: workspace, - path: remotePath, - }); - } catch { - existing = undefined; - } await this.pushDiffs( workspace, remotePath, - microdiff(existing ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } } diff --git a/cli/flow.ts b/cli/flow.ts index ca4a2661ec..eb455c0dda 100644 --- a/cli/flow.ts +++ b/cli/flow.ts @@ -94,6 +94,7 @@ export class FlowFile implements Resource, PushDiffs { ...changeset, ...base_changeset, } + await FlowService.updateFlow({ workspace: workspace, path: remotePath, @@ -114,20 +115,11 @@ export class FlowFile implements Resource, PushDiffs { } } async push(workspace: string, remotePath: string): Promise { - let remote: Flow | undefined; - try { - remote = await FlowService.getFlowByPath({ - workspace, - path: remotePath, - }); - } catch { - remote = undefined; - } await this.pushDiffs( workspace, remotePath, - microdiff(remote ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } } diff --git a/cli/folder.ts b/cli/folder.ts index ccaf6e2a01..548f950906 100644 --- a/cli/folder.ts +++ b/cli/folder.ts @@ -1,4 +1,4 @@ -import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts"; +import { colors, Command, FolderService, microdiff } from "./deps.ts"; import { requireLogin, resolveWorkspace, validatePath } from "./context.ts"; import { Difference, @@ -22,7 +22,9 @@ export class FolderFile implements Resource, PushDiffs { owners: Array | undefined; @property(map(() => String, () => Boolean, { shape: MapShape.Object })) extra_perms: Map | undefined; - + @property(() => String) + display_name: string| undefined; + async push(workspace: string, remotePath: string): Promise { if (remotePath.startsWith("/")) { remotePath = remotePath.substring(1); @@ -31,16 +33,10 @@ export class FolderFile implements Resource, PushDiffs { remotePath = remotePath.substring(2); } - let existing: Folder | undefined; - try { - existing = await FolderService.getFolder({ workspace, name: remotePath }); - } catch { - existing = undefined; - } await this.pushDiffs( workspace, remotePath, - microdiff(existing ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } @@ -66,22 +62,24 @@ export class FolderFile implements Resource, PushDiffs { if (exists) { console.log( colors.bold.yellow( - `Applying ${diffs.length} diffs to existing folder...`, + `Applying ${diffs.length} diffs to existing folder... ${remotePath}`, ), ); const changeset: { owners?: string[] | undefined; extra_perms?: any; + display_name?: string | undefined; } = {}; for (const diff of diffs) { if ( diff.type !== "REMOVE" && ( diff.path.length !== 1 || - !["owners", "extra_perms"].includes(diff.path[0] as string) + !["owners", "extra_perms", "display_name"].includes(diff.path[0] as string) ) ) { + console.log(diff.path) throw new Error("Invalid folder diff with path " + diff.path); } if (diff.type === "CREATE" || diff.type === "CHANGE") { @@ -97,11 +95,10 @@ export class FolderFile implements Resource, PushDiffs { if (!hasChanges) { return; } - await FolderService.updateFolder({ workspace: workspace, name: remotePath, - requestBody: changeset, + requestBody: {...changeset, extra_perms: changeset.extra_perms ? Object.fromEntries(this.extra_perms?.entries() ?? []) : undefined} }); } else { console.log(colors.bold.yellow("Creating new folder: " + remotePath)); diff --git a/cli/resource-type.ts b/cli/resource-type.ts index c0040c3bbc..031328cbd3 100644 --- a/cli/resource-type.ts +++ b/cli/resource-type.ts @@ -13,7 +13,6 @@ import { EditResourceType, microdiff, ResourceService, - ResourceType, Table, } from "./deps.ts"; import { Any, decoverto, model, property } from "./decoverto.ts"; @@ -26,19 +25,10 @@ export class ResourceTypeFile implements ResourceI, PushDiffs { description?: string; async push(workspace: string, remotePath: string): Promise { - let existing: ResourceType | undefined; - try { - existing = await ResourceService.getResourceType({ - workspace, - path: remotePath, - }); - } catch { - existing = undefined; - } - this.pushDiffs( + await this.pushDiffs( workspace, remotePath, - microdiff(existing ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } @@ -65,8 +55,8 @@ export class ResourceTypeFile implements ResourceI, PushDiffs { return; } console.log( - colors.yellow( - `Applying ${diffs.length} diffs to existing resource type...`, + colors.yellow.bold( + `Applying ${diffs.length} diffs to existing resource type... ${remotePath}`, ), ); const changeset: EditResourceType = {}; diff --git a/cli/resource.ts b/cli/resource.ts index c417a00ea2..f532b8877a 100644 --- a/cli/resource.ts +++ b/cli/resource.ts @@ -43,7 +43,7 @@ export class ResourceFile implements Resource2, PushDiffs { }) ) { console.log( - colors.yellow(`Applying ${diffs.length} diffs to existing resource...`), + colors.yellow.bold(`Applying ${diffs.length} diffs to existing resource... ${remotePath}`), ); const changeset: EditResource = { @@ -64,9 +64,10 @@ export class ResourceFile implements Resource2, PushDiffs { diff.path[0] !== "value" && ( diff.path.length !== 1 || diff.path[0] !== "description" - ) + ) && diff.path[0] !== "resource_type" ) ) { + console.log(colors.red("Invalid variable diff with path " + diff.path)); throw new Error("Invalid folder diff with path " + diff.path); } if (diff.type === "CREATE" || diff.type === "CHANGE") { @@ -110,19 +111,10 @@ export class ResourceFile implements Resource2, PushDiffs { } } async push(workspace: string, remotePath: string): Promise { - let existing: Resource | undefined; - try { - existing = await ResourceService.getResource({ - workspace, - path: remotePath, - }); - } catch { - existing = undefined; - } await this.pushDiffs( workspace, remotePath, - microdiff(existing ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } } diff --git a/cli/script.ts b/cli/script.ts index fc0bf22ac0..33c075366d 100644 --- a/cli/script.ts +++ b/cli/script.ts @@ -42,6 +42,7 @@ export class ScriptFile { }, toPlain: (data) => data, }) + @property(() => String) kind?: "script" | "failure" | "trigger" | "command" | "approval"; constructor(summary: string, description: string) { @@ -110,6 +111,11 @@ export async function handleFile(path: string, content: string, workspace: strin workspace, path: remotePath, }); + + if (typed.description === remote.description && content === remote.content && typed.summary === remote.summary && typed.is_template === remote.is_template && typed.kind == remote.kind && typed.lock == remote.lock && JSON.stringify(typed.schema) == JSON.stringify(remote.schema)) { + console.log(colors.yellow.bold(`Skipping script ${remotePath}`)) + return true + } await ScriptService.createScript({ workspace, requestBody: { @@ -123,8 +129,9 @@ export async function handleFile(path: string, content: string, workspace: strin lock: typed.lock, parent_hash: remote.hash, schema: typed.schema, - }, + } }); + console.log(colors.yellow.bold(`Creating script with a parent ${remotePath}`)) } catch { // no parent hash @@ -144,7 +151,6 @@ export async function handleFile(path: string, content: string, workspace: strin }, }); console.log(colors.yellow.bold(`Creating script without parent ${remotePath}`)) - } return true } diff --git a/cli/sync.ts b/cli/sync.ts index b0c7335787..7cadb0ab52 100644 --- a/cli/sync.ts +++ b/cli/sync.ts @@ -33,8 +33,8 @@ import { ResourceFile } from "./resource.ts"; import { FlowFile } from "./flow.ts"; import { VariableFile } from "./variable.ts"; import { handleFile } from "./script.ts"; -import { equal } from "https://deno.land/x/equal/mod.ts"; -import { diffCharacters } from "https://deno.land/x/diff/mod.ts"; +import { equal } from "https://deno.land/x/equal@v1.5.0/mod.ts"; +import { diffCharacters } from "https://deno.land/x/diff@v0.3.5/mod.ts"; type DynFSElement = { isDirectory: boolean; path: string; @@ -179,13 +179,13 @@ async function elementsToMap(els: DynFSElement, ignore: (path: string, isDirecto return map; } async function compareDynFSElement( - els1: DynFSElement, els2: DynFSElement, + els1: DynFSElement, els2: DynFSElement | undefined, ignore: (path: string, isDirectory: boolean) => boolean, - raw: boolean ): Promise { - const [m1, m2] = raw ? [await elementsToMap(els1, ignore), {}] : - await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)]); + const [m1, m2] = els2 + ? await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)]) + : [await elementsToMap(els1, ignore), {}]; const changes: Change[] = []; @@ -266,8 +266,8 @@ async function pull( console.log(colors.gray("Computing the files to update locally to match remote (taking .wmillignore into account)")); const remote = ZipFSElement((await downloadZip(workspace))!) - const local = await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill")) - const changes = await compareDynFSElement(remote, local, await ignoreF(), opts.raw) + const local = opts.raw ? undefined : await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill")) + const changes = await compareDynFSElement(remote, local, await ignoreF()) console.log(`remote -> local: ${changes.length} changes to apply`); @@ -289,7 +289,7 @@ async function pull( try { const currentLocal = await Deno.readTextFile(target) - if (currentLocal !== change.before) { + if (currentLocal !== change.before && currentLocal !== change.after) { console.log(colors.red(`Conflict detected on ${change.path}\nBoth local and remote have been modified.`)) if (opts.failConflicts) { conflicts.push({ local: currentLocal, change, path: change.path }) @@ -459,7 +459,6 @@ function removeSuffix(str: string, suffix: string) { async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull: boolean, failConflicts: boolean }) { - if (!opts.raw) { if (!opts.skipPull) { console.log(colors.gray("You need to be up-to-date before pushing, pulling first.")) @@ -474,9 +473,9 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull console.log(colors.gray("Computing the files to update on the remote to match local (taking .wmillignore into account)")); - const remote = ZipFSElement((await downloadZip(workspace))!) + const remote = opts.raw ? undefined : ZipFSElement((await downloadZip(workspace))!) const local = await FSFSElement(path.join(Deno.cwd(), "")) - const changes = await compareDynFSElement(local, remote, await ignoreF(), opts.raw) + const changes = await compareDynFSElement(local, remote, await ignoreF()) console.log(`remote <- local: ${changes.length} changes to apply`); if (changes.length > 0) { @@ -575,7 +574,7 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull break; } try { - Deno.remove(stateTarget) + await Deno.remove(stateTarget) } catch { } } } @@ -616,7 +615,7 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull await file.pushDiffs(workspace, remotePath, diffs); } catch (e) { console.error("Failing to apply diffs to " + remotePath) - console.error(e.body) + console.error(JSON.stringify(e)) } } } @@ -635,7 +634,7 @@ const command = new Command() "Push any local changes and apply them remotely. Use --raw for usage without local state tracking.", ) .option("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)") - .option("--skip-pull", "Push without pulling first") + .option("--skip-pull", "Push without pulling first (you have pulled prior)") .option("--yes", "Push without needing confirmation") .option("--raw", "Push without using state, just overwrite.") .action(push as any); diff --git a/cli/variable.ts b/cli/variable.ts index 6c6e001a44..3c1b4527ce 100644 --- a/cli/variable.ts +++ b/cli/variable.ts @@ -11,7 +11,6 @@ import { colors, Command, EditVariable, - ListableVariable, microdiff, Table, VariableService, @@ -67,7 +66,7 @@ export class VariableFile implements Resource, PushDiffs { if (await VariableService.existsVariable({ workspace, path: remotePath })) { console.log( colors.bold.yellow( - `Applying ${diffs.length} diffs to existing variable...`, + `Applying ${diffs.length} diffs to existing variable... ${remotePath}`, ), ); const changeset: EditVariable = {}; @@ -76,11 +75,12 @@ export class VariableFile implements Resource, PushDiffs { diff.type !== "REMOVE" && ( diff.path.length !== 1 || - !["path", "value", "is_secret", "description"].includes( + !["path", "value", "is_secret", "description", "account", "is_oauth"].includes( diff.path[0] as string, ) ) ) { + console.log(colors.red("Invalid variable diff with path " + diff.path)); throw new Error("Invalid variable diff with path " + diff.path); } if (diff.type === "CREATE" || diff.type === "CHANGE") { @@ -96,6 +96,7 @@ export class VariableFile implements Resource, PushDiffs { if (!hasChanges) { return; } + await VariableService.updateVariable({ workspace, path: remotePath, @@ -103,7 +104,6 @@ export class VariableFile implements Resource, PushDiffs { requestBody: changeset, }); - console.log(changeset); } else { console.log(colors.yellow.bold("Creating new variable...")); await VariableService.createVariable({ @@ -121,19 +121,10 @@ export class VariableFile implements Resource, PushDiffs { } } async push(workspace: string, remotePath: string): Promise { - let existing: ListableVariable | undefined; - try { - existing = await VariableService.getVariable({ - workspace: workspace, - path: remotePath, - }); - } catch { - existing = undefined; - } await this.pushDiffs( workspace, remotePath, - microdiff(existing ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } }