mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 16:05:42 +00:00
* feat(frontend): adapt AI-chat/sessions drafts to DB-backed model PR #9351 dropped UserDraft's localStorage layer; the chat adapter's synchronous save->read-back threw "Could not read written draft". The adapter now treats the backend as source of truth (in-tab cell used opportunistically for live-preview coherence) with conflict-on-save, and read tools fall back to the backend. Collapses the six writeXDraft functions onto one generic writeDraft + typed per-kind WriteSpec constants. Terminology: "local draft" -> "draft" (drafts are server-side). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(frontend): autosave indicator + draft-only diff guard in session editors Thread an explicit (workspace, path) autosave target to the cloud AutosaveIndicator in the Script/Flow/RawApp session previews so it watches the same key saves land on (it previously watched an empty path and never animated). Disable the Diff button with a hint for draft-only (no_deployed) items consistently across the three editors. Adjust the script topbar compact breakpoint/layout so the cloud icon is part of the bar, and stop splitpanes over-constraining session panes on reload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): session draft diff viewer for schedule/resource/variable Canonicalize both sides of the draft diff onto one field set and strip runtime-only fields so rows aren't spuriously marked all-changed; mask secret values. Map draft itemKinds to deploy-style kinds so the DiffRow shows the correct icon/label. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): uniform diff-viewer row height regardless of summary Diff-viewer leaf rows (WorkspaceItemRow) drew two lines when an item had a summary and one line otherwise, giving unequal heights. Add an opt-in `uniformHeight` prop that gives the text wrapper a shared min-height and vertically centers the one-line case; enable it only from the diff viewer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(frontend): address review nits on the drafts diff/guard changes - Reuse the exported TRIGGER_RUNTIME_IGNORE from utils_deployable instead of a verbatim copy, so the runtime-field ignore list has one source of truth. - Drop the now-redundant `(savedApp as any)` cast in RawAppEditorHeader; the prop type already carries `no_deployed`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): add description parameter to the write_flow chat tool write_flow had no way to set a flow's top-level description (the sibling of summary in OpenFlow); patch_flow_json only edits the compact value, so the field was unreachable from the AI chat. Thread an optional description end-to-end: tool schema -> persisted draft -> read-back -> deploy body. Structural patches (patch_flow_json/set_flow_module_code) pass no description, so a previously-set description is preserved. Adds a deployRequests regression test asserting a draft description reaches the deploy body, overriding the deployed one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): round-trip top-level fields in session preview draft sync The session preview's two-way draft sync dedups on a per-kind signature and mirrors fields between the editor store and the shared UserDraft cell. Both omitted fields the chat can set, so with the preview open a change to only that field was swallowed (identical signature) and then clobbered by the editor's outbound save: - flow: the signature and applyDraftToStore ignored top-level `description`. - script: the signature keyed on `content` alone, dropping `summary`/`language`. Add the missing fields to flowDraftSig and the script codec signature, and copy `description` in the flow codec's applyDraftToStore (mirroring `summary`). Raw-app already stringifies the whole draft, so it was unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): deploy draft-only flow from the session preview Deploying a draft-only flow (a draft with no deployed row) from the session preview hit two gaps the full-page flow editor already handled: - create vs update: newFlow keyed on `!savedFlow.val`, but a draft-only flow has a synthesized savedFlow (no_deployed=true), so deploy took updateFlow against the draft path and 404'd "Flow not found". Key it on no_deployed too. - friendly name: a brand-new flow is stored under a `draft_<uuid>` path with its intended name in `draft_path`. Seed the builder's initialPath from `draft_path` (as the full-page editor does) so the Path widget and deploy use the friendly name instead of creating a flow named draft_<uuid>. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): deploy draft-only raw app from the session preview Same create-vs-update bug as the flow session preview: newApp keyed on `!savedRawApp.val`, but a draft-only app has a truthy synthesized savedApp (getAppByPath with rawApp:true resolves to the draft kind instead of 404ing, carrying no_deployed=true), so deploy took updateApp against a path with no deployed row and 404'd "not found". Key newApp on no_deployed too so a never-deployed app deploys via createApp. More reachable than the flow case: it hit any never-deployed app, including chat-created ones at friendly paths. Keying newApp on no_deployed also exposed that newEditedPath (the breadcrumb path AND the createApp target) used newApp to mean "brand-new, generate a random name". A draft-only app is newApp=true but already has a real path (empty newPath at init, but appPath is set), so it showed and would deploy a random `*_app` name. Prefer the real appPath before the random fallback, so only a genuinely new app (appPath === '') still gets a generated suggestion; the full-page editor is unaffected (it always sets newPath). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): don't re-save a draft after deploying from the session preview Deploying from a session preview reloaded the editor (expected) but then immediately POSTed a fresh draft. The full-page editor guards deploy with discardDraftAfterDeploy (stopSync + arm-restart-on-first-interaction), but the shared editor header skips that in a session pane (inSessionPane) and routes post-deploy cleanup through sessionRuntime.syncPreviewWithDeployed, which did discard + reload without the stopSync guard. UserDraft.discard keeps the cell entry, so the reload's UserDraft.save fired the cell's reactive effect and re-POSTed the just-deployed value as a draft. Wrap the discard + reload in the same UserDraft.stopSync + armRestartOnFirst- Interaction bracket. One place fixes all three kinds (script/flow/raw_app), since they all funnel through syncPreviewWithDeployed; autosave resumes on the next genuine edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(frontend): address review findings on the session-preview drafts work - Type `no_deployed` via the GetXByPathResponse/UserDraftOverlay types instead of `(result as any)`/`(saved as any)` casts at the sites this branch added (sessionRuntime, ScriptBuilder, FlowBuilder, + widened the script/flow builder prop types). Pre-existing trigger/variable/resource-editor casts left untouched. - Drop a history-narrating comment parenthetical per the AGENTS.md comment policy (RawAppEditorView). - Add a unit test covering persistGlobalDraft's conflict-on-save / override path (conflict-capable updateDraft mock; inert for existing tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): keep the friendly generated path for a brand-new raw app The earlier draft-only newApp fix made newEditedPath prefer `appPath` before the random suggestion, but a brand-new app is parked at the storage placeholder `u/{user}/draft_{uuid}` (the /apps_raw/add redirect target), so it surfaced that uuid instead of a friendly `<adjective>_app` suggestion. Reject a `draft_` placeholder segment when choosing the path: a real named/draft-only path is still kept, a placeholder falls through to the generated suggestion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): show the Diff-button tooltip when it's disabled A disabled <button> fires no pointer events and browsers suppress its native title, so the "deploy once to compare" explanation never showed on hover for a draft-only item's Diff button. Wrap the button in a titled element and set pointer-events-none on the button when disabled, so the hover reaches the wrapper. Applied in ScriptBuilder, FlowBuilder, and RawAppEditorHeader (covers both the full-page editors and the session preview). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): surface draft save failures and conflicts in the AI chat tools Addresses Codex + Pi review findings on PR #9601 (three P1s, all in the DB-backed draft adapter reporting success when the write didn't land): - persistGlobalDraft reported {status:'saved'} even when UserDraftDbSyncer.save failed (it records network/5xx into a failure map instead of throwing). Check getState().state==='failed' after the save and return a new 'error' status; finishDraftWrite now emits success:false with a retry hint. - saveGlobalAppDraft dropped the conflict/error status (returned only the item), so write_app_file/patch_app_file/write_app_runnable reported every stale or failed write as saved. It now returns the full DraftPersistResult, and the six app write tools route through a shared finishAppDraftWrite helper. - fetchBackendDraftValue's catch{} swallowed non-404 errors (403/500/network), collapsing them to "no draft" so the write merged from the deployed item and lost in-progress draft edits. Narrow the catch to status===404; propagate the rest. Adds unit coverage: save-failure -> 'error', non-404 read -> propagates, raw-app stale write -> 'conflict'. 71/71 pass, check:fast + full build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): surface failed draft deletes + strip tool-only override from schedule drafts Addresses Codex's second-round review on PR #9601: - [P1] deleteGlobalDraft reported success even when the server delete failed or conflicted (UserDraftDbSyncer.save records failure state instead of throwing) — so discard_local_draft / deploy_workspace_item / delete_workspace_item / the /global_drafts delete could report a draft removed while the DB still had it. Check getState().state and getConflict() after the awaited null save and throw, mirroring the write-path guard. - [P2] writeScheduleDraft persisted the tool-only `override` conflict flag into the schedule draft value (mergeDraftConfig cloned every arg field). Strip `override` in SCHEDULE_SPEC.buildDraft before merging. Tests: failed server delete -> throws; schedule draft no longer contains `override`. 73/73 pass, check:fast + full build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): /global_drafts "Clear all" deletes persisted drafts, not just cells Codex review nit (P2): the dev-only global-drafts inspector's "Clear all" called clearGlobalDrafts(), which only iterates in-tab UserDraft cells — any persisted backend draft row not currently mounted as a cell survived, so the list re-showed it after refresh. Iterate the listed drafts and delete each via the backend-aware deleteGlobalDraft() (continue past per-row failures), matching the per-row delete, then clear local cells + ephemeral secrets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
561 lines
17 KiB
TypeScript
561 lines
17 KiB
TypeScript
import { minimatch } from 'minimatch'
|
|
import {
|
|
AzureTriggerService,
|
|
EmailTriggerService,
|
|
GcpTriggerService,
|
|
HttpTriggerService,
|
|
KafkaTriggerService,
|
|
MqttTriggerService,
|
|
NatsTriggerService,
|
|
PostgresTriggerService,
|
|
ScheduleService,
|
|
SqsTriggerService,
|
|
WebsocketTriggerService,
|
|
type GcpTriggerData,
|
|
type WorkspaceDeployUISettings
|
|
} from './gen'
|
|
import type { TriggerKind } from './components/triggers'
|
|
import { base } from './base'
|
|
|
|
type DeployUIType = 'script' | 'flow' | 'app' | 'resource' | 'variable' | 'secret' | 'trigger'
|
|
|
|
export type Kind =
|
|
| 'script'
|
|
| 'resource'
|
|
| 'schedule'
|
|
| 'variable'
|
|
| 'flow'
|
|
| 'app'
|
|
| 'raw_app'
|
|
| 'resource_type'
|
|
| 'folder'
|
|
// Per-kind trigger names returned by the backend's `compareWorkspaces` API.
|
|
| 'http_trigger'
|
|
| 'websocket_trigger'
|
|
| 'kafka_trigger'
|
|
| 'nats_trigger'
|
|
| 'postgres_trigger'
|
|
| 'mqtt_trigger'
|
|
| 'sqs_trigger'
|
|
| 'gcp_trigger'
|
|
| 'azure_trigger'
|
|
| 'email_trigger'
|
|
// Legacy generic kind used by the cross-workspace `DeployWorkspace` UI,
|
|
// which carries the trigger sub-kind in `additionalInformation`.
|
|
| 'trigger'
|
|
|
|
export const ALL_DEPLOYABLE: WorkspaceDeployUISettings = {
|
|
include_path: [],
|
|
include_type: ['script', 'flow', 'app', 'resource', 'variable', 'secret', 'trigger']
|
|
}
|
|
|
|
export type AdditionalInformation = {
|
|
triggers?: {
|
|
kind: TriggerKind
|
|
}
|
|
}
|
|
|
|
export function isDeployable(
|
|
type: DeployUIType,
|
|
path: string,
|
|
deployUiSettings: WorkspaceDeployUISettings | undefined
|
|
) {
|
|
if (deployUiSettings == undefined) {
|
|
return false
|
|
}
|
|
|
|
if (deployUiSettings.include_type != undefined && !deployUiSettings.include_type.includes(type)) {
|
|
return false
|
|
}
|
|
|
|
if (
|
|
deployUiSettings.include_path != undefined &&
|
|
deployUiSettings.include_path.length != 0 &&
|
|
deployUiSettings.include_path.every((x) => !minimatch(path, x))
|
|
) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
export async function existsTrigger(
|
|
data: { workspace: string; path: string },
|
|
triggerKind: TriggerKind
|
|
) {
|
|
if (triggerKind === 'routes') {
|
|
return await HttpTriggerService.existsHttpTrigger(data)
|
|
} else if (triggerKind === 'kafka') {
|
|
return await KafkaTriggerService.existsKafkaTrigger(data)
|
|
} else if (triggerKind === 'mqtt') {
|
|
return await MqttTriggerService.existsMqttTrigger(data)
|
|
} else if (triggerKind === 'postgres') {
|
|
return await PostgresTriggerService.existsPostgresTrigger(data)
|
|
} else if (triggerKind === 'sqs') {
|
|
return await SqsTriggerService.existsSqsTrigger(data)
|
|
} else if (triggerKind === 'gcp') {
|
|
return await GcpTriggerService.existsGcpTrigger(data)
|
|
} else if (triggerKind === 'websockets') {
|
|
return await WebsocketTriggerService.existsWebsocketTrigger(data)
|
|
} else if (triggerKind === 'nats') {
|
|
return await NatsTriggerService.existsNatsTrigger(data)
|
|
} else if (triggerKind === 'azure') {
|
|
return await AzureTriggerService.existsAzureTrigger(data)
|
|
} else if (triggerKind === 'emails') {
|
|
return await EmailTriggerService.existsEmailTrigger(data)
|
|
} else if (triggerKind === 'schedules') {
|
|
return await ScheduleService.existsSchedule(data)
|
|
}
|
|
|
|
throw new Error(
|
|
`Unexpected trigger kind ${triggerKind}. Allowed kinds are: routes, kafka, mqtt, postgres, sqs, gcp, websockets, nats, azure, emails, schedules.`
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Strip operational state (`mode`, `enabled`) from a trigger/schedule payload
|
|
* before sending it to an update endpoint via the merge UI. The backend's
|
|
* `update_trigger` handler preserves the target row's existing `mode` when
|
|
* both fields are absent from the request (`is_mode_unspecified()`), so
|
|
* stripping here lets a fork→parent (or parent→fork) deploy carry config
|
|
* changes without flipping the target's enabled/disabled state. Schedules'
|
|
* `EditSchedule` already lacks `enabled` on the backend, but stripping keeps
|
|
* the intent explicit and matches the YAML/CLI round-trip behavior.
|
|
*
|
|
* Used by the legacy `kind === 'trigger'` path in `utils_workspace_deploy.ts`
|
|
* (the cross-workspace deploy UI). The merge-UI deploy goes through the
|
|
* shared `deployItem` in `windmill-utils-internal`, which applies its own
|
|
* `stripOperationalStateOnUpdate` at the dispatch layer.
|
|
*/
|
|
export function stripOperationalState<T extends Record<string, any>>(
|
|
payload: T
|
|
): Omit<T, 'mode' | 'enabled'> {
|
|
const { mode: _mode, enabled: _enabled, ...rest } = payload
|
|
return rest
|
|
}
|
|
|
|
/**
|
|
* Get trigger deployment data with optional permissioned_as preservation.
|
|
* @param onBehalfOf - If set, the trigger will be deployed with this permissioned_as (u/username or g/group) and preserve_permissioned_as=true.
|
|
*/
|
|
export async function getTriggersDeployData(
|
|
kind: TriggerKind,
|
|
path: string,
|
|
workspace: string,
|
|
onBehalfOf?: string
|
|
) {
|
|
const preservePermissionedAs = onBehalfOf !== undefined
|
|
|
|
if (kind === 'sqs') {
|
|
const sqsTrigger = await SqsTriggerService.getSqsTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
return {
|
|
data: {
|
|
...sqsTrigger,
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
},
|
|
createFn: SqsTriggerService.createSqsTrigger,
|
|
updateFn: SqsTriggerService.updateSqsTrigger
|
|
}
|
|
} else if (kind === 'kafka') {
|
|
const kafkaTrigger = await KafkaTriggerService.getKafkaTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
return {
|
|
data: {
|
|
...kafkaTrigger,
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
},
|
|
createFn: KafkaTriggerService.createKafkaTrigger,
|
|
updateFn: KafkaTriggerService.updateKafkaTrigger
|
|
}
|
|
} else if (kind === 'mqtt') {
|
|
const mqttTrigger = await MqttTriggerService.getMqttTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
return {
|
|
data: {
|
|
...mqttTrigger,
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
},
|
|
createFn: MqttTriggerService.createMqttTrigger,
|
|
updateFn: MqttTriggerService.updateMqttTrigger
|
|
}
|
|
} else if (kind === 'nats') {
|
|
const natsTrigger = await NatsTriggerService.getNatsTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
return {
|
|
data: {
|
|
...natsTrigger,
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
},
|
|
createFn: NatsTriggerService.createNatsTrigger,
|
|
updateFn: NatsTriggerService.updateNatsTrigger
|
|
}
|
|
} else if (kind === 'gcp') {
|
|
const gcpTrigger = await GcpTriggerService.getGcpTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
gcpTrigger.subscription_id = ''
|
|
gcpTrigger.subscription_mode = 'create_update'
|
|
|
|
if (gcpTrigger.delivery_config) {
|
|
gcpTrigger.delivery_config.audience = ''
|
|
}
|
|
|
|
const data: GcpTriggerData = {
|
|
...gcpTrigger,
|
|
delivery_config: gcpTrigger.delivery_config ?? undefined,
|
|
base_endpoint:
|
|
gcpTrigger.delivery_type === 'push' ? `${window.location.origin}${base}` : undefined,
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
}
|
|
|
|
return {
|
|
data,
|
|
createFn: GcpTriggerService.createGcpTrigger,
|
|
updateFn: GcpTriggerService.updateGcpTrigger
|
|
}
|
|
} else if (kind === 'postgres') {
|
|
const postgresTrigger = await PostgresTriggerService.getPostgresTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
return {
|
|
data: {
|
|
...postgresTrigger,
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
},
|
|
createFn: PostgresTriggerService.createPostgresTrigger,
|
|
updateFn: PostgresTriggerService.updatePostgresTrigger
|
|
}
|
|
} else if (kind === 'websockets') {
|
|
const websocketTrigger = await WebsocketTriggerService.getWebsocketTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
return {
|
|
data: {
|
|
...websocketTrigger,
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
},
|
|
createFn: WebsocketTriggerService.createWebsocketTrigger,
|
|
updateFn: WebsocketTriggerService.updateWebsocketTrigger
|
|
}
|
|
} else if (kind === 'routes') {
|
|
const httpTrigger = await HttpTriggerService.getHttpTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
return {
|
|
data: {
|
|
...httpTrigger,
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
},
|
|
createFn: HttpTriggerService.createHttpTrigger,
|
|
updateFn: HttpTriggerService.updateHttpTrigger
|
|
}
|
|
} else if (kind === 'azure') {
|
|
const azureTrigger = await AzureTriggerService.getAzureTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
return {
|
|
data: {
|
|
...azureTrigger,
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
},
|
|
createFn: AzureTriggerService.createAzureTrigger,
|
|
updateFn: AzureTriggerService.updateAzureTrigger
|
|
}
|
|
} else if (kind === 'emails') {
|
|
const emailTrigger = await EmailTriggerService.getEmailTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
return {
|
|
data: {
|
|
...emailTrigger,
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
},
|
|
createFn: EmailTriggerService.createEmailTrigger,
|
|
updateFn: EmailTriggerService.updateEmailTrigger
|
|
}
|
|
} else if (kind === 'schedules') {
|
|
const schedulesTrigger = await ScheduleService.getSchedule({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
return {
|
|
data: {
|
|
...schedulesTrigger,
|
|
// permissioned_as is only set on create, not update
|
|
permissioned_as: onBehalfOf,
|
|
preserve_permissioned_as: preservePermissionedAs
|
|
},
|
|
createFn: ScheduleService.createSchedule,
|
|
updateFn: ScheduleService.updateSchedule
|
|
}
|
|
}
|
|
|
|
throw new Error(`Unexpected trigger kind got: ${kind}`)
|
|
}
|
|
|
|
/**
|
|
* Runtime fields stripped from the trigger/schedule diff so the drawer mirrors
|
|
* the backend's `compare_two_trigger_or_schedule` semantics — same set as
|
|
* `TRIGGER_COMPARE_IGNORE` and `stripTriggerOrScheduleRuntimeFields` in the
|
|
* shared deploy module.
|
|
*/
|
|
export const TRIGGER_RUNTIME_IGNORE = new Set([
|
|
'workspace_id',
|
|
'edited_by',
|
|
'edited_at',
|
|
'email',
|
|
'error',
|
|
'enabled',
|
|
'mode',
|
|
'server_id',
|
|
'last_server_ping',
|
|
'extra_perms',
|
|
'permissioned_as',
|
|
// Server-managed (kept in sync with backend `TRIGGER_COMPARE_IGNORE`).
|
|
'subscription_id',
|
|
'push_auth_config'
|
|
])
|
|
|
|
function stripTriggerRuntimeFields<T extends Record<string, any>>(row: T): Partial<T> {
|
|
const out: Record<string, any> = {}
|
|
for (const [k, v] of Object.entries(row)) {
|
|
if (!TRIGGER_RUNTIME_IGNORE.has(k)) out[k] = v
|
|
}
|
|
return out as Partial<T>
|
|
}
|
|
|
|
export async function getTriggerValue(kind: TriggerKind, path: string, workspace: string) {
|
|
let trigger: Record<string, any>
|
|
if (kind === 'sqs') {
|
|
trigger = await SqsTriggerService.getSqsTrigger({ workspace, path })
|
|
} else if (kind === 'kafka') {
|
|
trigger = await KafkaTriggerService.getKafkaTrigger({ workspace, path })
|
|
} else if (kind === 'mqtt') {
|
|
trigger = await MqttTriggerService.getMqttTrigger({ workspace, path })
|
|
} else if (kind === 'nats') {
|
|
trigger = await NatsTriggerService.getNatsTrigger({ workspace, path })
|
|
} else if (kind === 'postgres') {
|
|
trigger = await PostgresTriggerService.getPostgresTrigger({ workspace, path })
|
|
} else if (kind === 'gcp') {
|
|
trigger = await GcpTriggerService.getGcpTrigger({ workspace, path })
|
|
} else if (kind === 'websockets') {
|
|
trigger = await WebsocketTriggerService.getWebsocketTrigger({ workspace, path })
|
|
} else if (kind === 'routes') {
|
|
trigger = await HttpTriggerService.getHttpTrigger({ workspace, path })
|
|
} else if (kind === 'schedules') {
|
|
trigger = await ScheduleService.getSchedule({ workspace, path })
|
|
} else if (kind === 'azure') {
|
|
trigger = await AzureTriggerService.getAzureTrigger({ workspace, path })
|
|
} else if (kind === 'emails') {
|
|
trigger = await EmailTriggerService.getEmailTrigger({ workspace, path })
|
|
} else {
|
|
throw new Error(`Unexpected trigger kind got: ${kind}`)
|
|
}
|
|
return stripTriggerRuntimeFields(trigger)
|
|
}
|
|
|
|
/**
|
|
* Get the permissioned_as for a trigger (used for on_behalf_of during deployment).
|
|
*/
|
|
export async function getTriggerPermissionedAs(
|
|
kind: TriggerKind,
|
|
path: string,
|
|
workspace: string
|
|
): Promise<string | undefined> {
|
|
try {
|
|
if (kind === 'sqs') {
|
|
const trigger = await SqsTriggerService.getSqsTrigger({ workspace, path })
|
|
return trigger.permissioned_as
|
|
} else if (kind === 'kafka') {
|
|
const trigger = await KafkaTriggerService.getKafkaTrigger({ workspace, path })
|
|
return trigger.permissioned_as
|
|
} else if (kind === 'mqtt') {
|
|
const trigger = await MqttTriggerService.getMqttTrigger({ workspace, path })
|
|
return trigger.permissioned_as
|
|
} else if (kind === 'nats') {
|
|
const trigger = await NatsTriggerService.getNatsTrigger({ workspace, path })
|
|
return trigger.permissioned_as
|
|
} else if (kind === 'gcp') {
|
|
const trigger = await GcpTriggerService.getGcpTrigger({ workspace, path })
|
|
return trigger.permissioned_as
|
|
} else if (kind === 'postgres') {
|
|
const trigger = await PostgresTriggerService.getPostgresTrigger({ workspace, path })
|
|
return trigger.permissioned_as
|
|
} else if (kind === 'websockets') {
|
|
const trigger = await WebsocketTriggerService.getWebsocketTrigger({ workspace, path })
|
|
return trigger.permissioned_as
|
|
} else if (kind === 'routes') {
|
|
const trigger = await HttpTriggerService.getHttpTrigger({ workspace, path })
|
|
return trigger.permissioned_as
|
|
} else if (kind === 'schedules') {
|
|
const trigger = await ScheduleService.getSchedule({ workspace, path })
|
|
return trigger.permissioned_as
|
|
} else if (kind === 'azure') {
|
|
const trigger = await AzureTriggerService.getAzureTrigger({ workspace, path })
|
|
return trigger.permissioned_as
|
|
} else if (kind === 'emails') {
|
|
const trigger = await EmailTriggerService.getEmailTrigger({ workspace, path })
|
|
return trigger.permissioned_as
|
|
}
|
|
} catch {
|
|
// Trigger may not exist in the workspace
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function retrieveScriptOrFlowKind(path: string, is_flow: boolean): { kind: Kind; path: string } {
|
|
return {
|
|
kind: is_flow ? 'flow' : 'script',
|
|
path
|
|
}
|
|
}
|
|
|
|
function retrieveKindsValues({
|
|
resource_path,
|
|
script_path,
|
|
is_flow
|
|
}: {
|
|
resource_path?: string
|
|
script_path: string
|
|
is_flow: boolean
|
|
}) {
|
|
const result: { kind: Kind; path: string }[] = []
|
|
|
|
if (resource_path) {
|
|
result.push({ kind: 'resource', path: resource_path })
|
|
}
|
|
result.push(retrieveScriptOrFlowKind(script_path, is_flow))
|
|
return result
|
|
}
|
|
|
|
export async function getTriggerDependency(kind: TriggerKind, path: string, workspace: string) {
|
|
let result: { kind: Kind; path: string }[]
|
|
if (kind === 'sqs') {
|
|
const { aws_resource_path, script_path, is_flow } = await SqsTriggerService.getSqsTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
result = retrieveKindsValues({ resource_path: aws_resource_path, script_path, is_flow })
|
|
} else if (kind === 'kafka') {
|
|
const { kafka_resource_path, script_path, is_flow } = await KafkaTriggerService.getKafkaTrigger(
|
|
{
|
|
workspace: workspace!,
|
|
path: path
|
|
}
|
|
)
|
|
|
|
result = retrieveKindsValues({ resource_path: kafka_resource_path, script_path, is_flow })
|
|
} else if (kind === 'mqtt') {
|
|
const { mqtt_resource_path, script_path, is_flow } = await MqttTriggerService.getMqttTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
result = retrieveKindsValues({ resource_path: mqtt_resource_path, script_path, is_flow })
|
|
} else if (kind === 'nats') {
|
|
const { nats_resource_path, script_path, is_flow } = await NatsTriggerService.getNatsTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
result = retrieveKindsValues({ resource_path: nats_resource_path, script_path, is_flow })
|
|
} else if (kind === 'postgres') {
|
|
const { postgres_resource_path, script_path, is_flow } =
|
|
await PostgresTriggerService.getPostgresTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
result = retrieveKindsValues({ resource_path: postgres_resource_path, script_path, is_flow })
|
|
} else if (kind === 'gcp') {
|
|
const { gcp_resource_path, script_path, is_flow } = await GcpTriggerService.getGcpTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
result = retrieveKindsValues({ resource_path: gcp_resource_path, script_path, is_flow })
|
|
} else if (kind === 'websockets') {
|
|
const { script_path, is_flow, url, initial_messages } =
|
|
await WebsocketTriggerService.getWebsocketTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
result = retrieveKindsValues({ script_path, is_flow })
|
|
|
|
const SCRIPT_PREFIX = '$script:'
|
|
const FLOW_PREFIX = '$flow:'
|
|
|
|
if (url.startsWith(SCRIPT_PREFIX))
|
|
result.push(retrieveScriptOrFlowKind(url.substring(SCRIPT_PREFIX.length), false))
|
|
else if (url.startsWith(FLOW_PREFIX))
|
|
result.push(retrieveScriptOrFlowKind(url.substring(FLOW_PREFIX.length), true))
|
|
|
|
initial_messages?.map((message) => {
|
|
if ('runnable_result' in message) {
|
|
result.push(
|
|
retrieveScriptOrFlowKind(message.runnable_result.path, message.runnable_result.is_flow)
|
|
)
|
|
}
|
|
})
|
|
} else if (kind === 'routes') {
|
|
const { script_path, is_flow, authentication_resource_path } =
|
|
await HttpTriggerService.getHttpTrigger({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
result = retrieveKindsValues({
|
|
script_path,
|
|
is_flow,
|
|
resource_path: authentication_resource_path ?? undefined
|
|
})
|
|
} else if (kind === 'schedules') {
|
|
const { script_path, is_flow } = await ScheduleService.getSchedule({
|
|
workspace: workspace!,
|
|
path: path
|
|
})
|
|
|
|
result = retrieveKindsValues({ script_path, is_flow })
|
|
} else {
|
|
throw new Error(`Unexpected trigger kind got: ${kind}`)
|
|
}
|
|
return result
|
|
}
|