Files
windmill/frontend/src/lib/rawAppDeploy.ts
T

173 lines
6.4 KiB
TypeScript

/**
* Deploy a raw app (code-based app), from its server-side draft or from an
* explicit value. Raw apps can't be deployed through the normal
* AppService.updateApp/createApp path: their source `files` must be bundled to
* js/css and saved via the raw-app endpoints.
*
* This mirrors how the global AI chat deploys raw apps
* (`copilot/chat/global/core.ts` → deployDraft, case 'app'): read the item with
* its draft, normalise to an AppDraftValue, recompute the policy, bundle the
* files, then createAppRaw/updateAppRaw. The source→AppDraftValue projection is
* shared via `rawAppDraftValue` so the two deploy paths can't drift.
*/
import { get } from 'svelte/store'
import { AppService } from '$lib/gen'
import type { Policy } from '$lib/gen'
import { userStore } from '$lib/stores'
import { bundleRawAppDraft } from '$lib/components/copilot/chat/global/rawAppBundlerBridge'
import type { AppDraftValue } from '$lib/components/copilot/chat/global/workspaceItems'
import { updateRawAppPolicy } from '$lib/components/raw_apps/rawAppPolicy'
import { DEFAULT_DATA as DEFAULT_RAW_APP_DATA } from '$lib/components/raw_apps/dataTableRefUtils'
import { appSourceToDraftValue } from '$lib/components/raw_apps/rawAppDraftValue'
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
/**
* Deploy an explicit raw-app value — one the user edited as JSON, or one
* restored from a previous version — onto an already deployed raw app. Going
* through AppService.updateApp instead would write a version flagged low-code
* and leave the bundle behind on the old version, so the app stops rendering;
* the backend rejects that mismatch.
*/
export async function deployRawAppValue({
workspace,
path,
value,
summary,
policy: currentPolicy,
customPath,
deploymentMessage,
allowKindChange
}: {
workspace: string
path: string
value: any
summary?: string
policy?: Policy
customPath?: string | null
deploymentMessage?: string
/** Let this deploy turn a low-code app into a raw one (restoring a version
* from before an accidental conversion). Off for every ordinary deploy. */
allowKindChange?: boolean
}): Promise<void> {
// The value often comes straight out of a `$state` field, and the bundler
// runs in an iframe: postMessage refuses to clone a state proxy.
const plainValue = stateSnapshot(value) as any
const files = (plainValue?.files ?? {}) as Record<string, string>
const runnables = plainValue?.runnables ?? {}
// The value carries the runnables, so the policy's triggerables have to be
// recomputed from it or the deployed app can't call what it now contains.
const policy = (await updateRawAppPolicy(runnables, currentPolicy)) as Policy
if (!policy.execution_mode) {
policy.execution_mode = 'publisher'
}
const bundle = await bundleRawAppDraft({ workspace, files })
const isAdmin = !!(get(userStore)?.is_admin || get(userStore)?.is_super_admin)
await AppService.updateAppRaw({
workspace,
path,
formData: {
app: {
value: { files, runnables, data: plainValue?.data ?? { ...DEFAULT_RAW_APP_DATA } },
summary: summary ?? '',
policy,
deployment_message: deploymentMessage,
// custom_path changes require admin (see deployRawAppDraft).
custom_path: isAdmin ? (customPath ?? '') : undefined,
preserve_on_behalf_of: policy.on_behalf_of ? true : undefined,
allow_kind_change: allowKindChange || undefined
},
js: bundle.js,
css: bundle.css
}
})
}
/**
* Promote a raw app's draft to deployed. Throws on failure (caller wraps into a
* DeployResult). The matching draft row is deleted server-side by the raw-app
* create/update handler, like the other deploy paths.
*/
export async function deployRawAppDraft(
workspace: string,
path: string,
deploymentMessage?: string
): Promise<void> {
// `rawApp: true` so a never-deployed raw app (no `app` row) resolves to
// the raw_app draft kind server-side instead of 404ing.
const app = await AppService.getAppByPath({ workspace, path, getDraft: true, rawApp: true })
const draft = (app as any).draft
// Deploy at the draft's intended path. A raw-app draft carries the user-typed
// path in `draft_path` (a never-deployed app is parked at a synthetic
// `u/{user}/draft_{uuid}` storage key); the URL `path` below stays that storage
// key. Falls back to `path` for an unrenamed draft on a deployed app.
const targetPath = draft?.draft_path ?? draft?.path ?? path
const value = appSourceToDraftValue(draft ?? app, app)
const policy = (await updateRawAppPolicy(
value.runnables as any,
value.policy as any
)) as NonNullable<AppDraftValue['policy']> & Policy
if (!policy.execution_mode) {
policy.execution_mode = 'publisher'
}
const bundle = await bundleRawAppDraft({ workspace, files: value.files })
const rawAppValue = {
files: value.files,
runnables: value.runnables,
data: value.data ?? { ...DEFAULT_RAW_APP_DATA }
}
const summary = value.summary ?? ''
if (await AppService.existsApp({ workspace, path })) {
// custom_path changes require admin. Mirror RawAppEditorHeader's update path:
// admins send the draft's value (`''` to clear), non-admins send undefined so
// the backend ignores it and preserves the existing route — otherwise a
// non-admin deploying a draft for an app that has a custom route would hit
// RequireAdmin (the deployed custom_path is sent via the appSourceToDraftValue
// fallback even when unchanged).
const isAdmin = !!(get(userStore)?.is_admin || get(userStore)?.is_super_admin)
await AppService.updateAppRaw({
workspace,
path,
formData: {
app: {
path: targetPath,
value: rawAppValue,
summary,
policy,
deployment_message: deploymentMessage,
custom_path: isAdmin ? (value.custom_path ?? '') : undefined,
// Preserve the policy's on_behalf_of: this draft-deploy path has no
// on-behalf-of selector, so without the flag the backend resets it to
// the deploying user (gated server-side by can_preserve_on_behalf_of).
preserve_on_behalf_of: policy.on_behalf_of ? true : undefined
},
js: bundle.js,
css: bundle.css
}
})
} else {
await AppService.createAppRaw({
workspace,
formData: {
app: {
path: targetPath,
value: rawAppValue,
summary,
policy,
deployment_message: deploymentMessage,
custom_path: value.custom_path,
// Preserve the policy's on_behalf_of (see update branch above).
preserve_on_behalf_of: policy.on_behalf_of ? true : undefined
},
js: bundle.js,
css: bundle.css
}
})
}
}