From 43be4d8b2d12c4bb8434def11ddc370dac59b1e2 Mon Sep 17 00:00:00 2001 From: centdix Date: Wed, 13 May 2026 15:39:55 +0200 Subject: [PATCH] refactor: remove global ai draft fallback store --- .../copilot/chat/global/core.test.ts | 29 +-- .../components/copilot/chat/global/core.ts | 117 +---------- .../chat/global/deployRequests.test.ts | 2 +- .../copilot/chat/global/deployRequests.ts | 2 +- .../copilot/chat/global/draftStore.svelte.ts | 192 ------------------ .../copilot/chat/global/draftStore.test.ts | 56 ----- .../copilot/chat/global/userDraftAdapter.ts | 19 +- .../copilot/chat/global/workspaceItems.ts | 102 ++++++++++ 8 files changed, 140 insertions(+), 379 deletions(-) delete mode 100644 frontend/src/lib/components/copilot/chat/global/draftStore.svelte.ts delete mode 100644 frontend/src/lib/components/copilot/chat/global/draftStore.test.ts create mode 100644 frontend/src/lib/components/copilot/chat/global/workspaceItems.ts diff --git a/frontend/src/lib/components/copilot/chat/global/core.test.ts b/frontend/src/lib/components/copilot/chat/global/core.test.ts index 79f0256afa..b5fd9e31be 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.test.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.test.ts @@ -94,13 +94,19 @@ vi.mock('$lib/gen', async () => { existsApp: vi.fn(async () => false) }), VariableService: wrapService(actual.VariableService, { - existsVariable: vi.fn(async () => false) + existsVariable: vi.fn(async () => false), + getVariable: vi.fn(async ({ path }: { path: string }) => ({ + path, + value: 'super-secret-token', + is_secret: true, + description: 'API key' + })), + listVariable: vi.fn(async () => []) }) } }) import { globalTools, prepareGlobalUserMessage } from './core' -import { globalDraftStore } from './draftStore.svelte' import { UserDraft, __resetUserDraftForTesting } from '$lib/userDraft.svelte' import type { Tool, ToolCallbacks } from '../shared' @@ -135,12 +141,18 @@ async function callGlobalTool( describe('global AI tools', () => { beforeEach(() => { - globalDraftStore.clearDrafts(WORKSPACE) __resetUserDraftForTesting() localStorage.clear() vi.clearAllMocks() }) + it('does not expose resource or variable draft writers until their UserDraft contracts are complete', () => { + const toolNames = globalTools.map((tool) => tool.def.function.name) + + expect(toolNames).not.toContain('write_resource') + expect(toolNames).not.toContain('write_variable') + }) + it('writes script drafts into the shared UserDraft store', async () => { const content = 'export async function main() {\n\treturn "hello"\n}' @@ -273,14 +285,7 @@ describe('global AI tools', () => { ]) }) - it('redacts variable draft values when reading workspace items', async () => { - await callGlobalTool('write_variable', { - path: 'f/secrets/api_key', - value: 'super-secret-token', - is_secret: true, - description: 'API key' - }) - + it('redacts existing variable values when reading workspace items', async () => { const raw = await callGlobalTool('read_workspace_item', { type: 'variable', path: 'f/secrets/api_key' @@ -292,7 +297,7 @@ describe('global AI tools', () => { type: 'variable', path: 'f/secrets/api_key', summary: 'API key', - isDraft: true + isDraft: false }) }) diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index 23b5afe6a1..d03dc5e491 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -61,14 +61,11 @@ import { } from '../shared' import type { ContextElement } from '../context' import { - resourceRequestSchema, scheduleRequestSchema, - triggerRequestSchemas, - variableRequestSchema + triggerRequestSchemas } from '../workspaceToolsZod.gen' import { getWorkspaceItemKey, - globalDraftStore, TRIGGER_KINDS, type AppDraftValue, type FlowDraftValue, @@ -76,7 +73,7 @@ import { type TriggerKind, type WorkspaceItem, type WorkspaceItemType -} from './draftStore.svelte' +} from './workspaceItems' import { buildFlowDeployRequestBody, buildScriptDeployRequestBody } from './deployRequests' import { deleteGlobalDraft, @@ -279,10 +276,6 @@ const writeTriggerSchema = z.object({ ) }) -const writeResourceSchema = resourceRequestSchema - -const writeVariableSchema = variableRequestSchema - const searchResourceTypesSchema = z.object({ query: z.string().describe('Substring to match against resource type names.'), limit: z @@ -474,10 +467,10 @@ const initAppSchema = z.object({ const GLOBAL_SYSTEM_PROMPT = `You are Windmill's global workspace assistant. -You can inspect workspace scripts, flows, schedules, triggers, resources, variables, and apps, then create draft changes in the frontend AI draft store. +You can inspect workspace scripts, flows, schedules, triggers, resources, variables, and apps, then create draft changes in the frontend AI draft store for supported item types. Important rules: -- write_{script,flow,schedule,trigger,resource,variable} create or overwrite drafts. They do not save, deploy, or mutate workspace items. +- write_{script,flow,schedule,trigger} create or overwrite drafts. They do not save, deploy, or mutate workspace items. - edit_script and patch_flow_json apply small exact-text edits and save the result as a draft. Prefer them for localized changes; use write_* for large rewrites. - For flows specifically: read_workspace_item and patch_flow_json work on a COMPACT view where rawscript module bodies are replaced with the placeholder "inline_script.". Use read_flow_module_code / set_flow_module_code to inspect or overwrite an inline script body; use patch_flow_json for structural edits. - deploy_workspace_item persists a draft to the workspace via the real backend create/update API and removes the draft. Requires user confirmation. Only call after the user has reviewed the draft and explicitly asked to deploy. @@ -943,31 +936,6 @@ const triggerServices: Record = { } } -async function workspaceItemExists( - type: WorkspaceItemType, - path: string, - workspace: string, - triggerKind?: TriggerKind -): Promise { - switch (type) { - case 'script': - return ScriptService.existsScriptByPath({ workspace, path }) - case 'flow': - return FlowService.existsFlowByPath({ workspace, path }) - case 'schedule': - return ScheduleService.existsSchedule({ workspace, path }) - case 'trigger': - if (!triggerKind) return false - return triggerServices[triggerKind].exists({ workspace, path }) - case 'resource': - return ResourceService.existsResource({ workspace, path }) - case 'variable': - return VariableService.existsVariable({ workspace, path }) - case 'app': - return AppService.existsApp({ workspace, path }) - } -} - async function readWorkspaceItem( type: WorkspaceItemType, path: string, @@ -1168,14 +1136,11 @@ ${getRawAppPrompt()}` } function getResourceInstructions(): string { - return `# Global draft resource & variable instructions + return `# Resource & variable reference -- Global mode writes complete draft payloads only; it does not save, deploy, run, scaffold local files, or generate metadata. -- A resource draft is a workspace item: \`{ type: 'resource', path, summary?, value, isDraft }\`. \`value\` is a CreateResource body: \`{ path, value, description?, resource_type, labels? }\` where the inner \`value\` is the resource type's data shape. -- A variable draft is a workspace item: \`{ type: 'variable', path, summary?, value, isDraft }\`. \`value\` is a CreateVariable body: \`{ path, value, is_secret, description, account?, is_oauth?, expires_at?, labels? }\`. -- For secret fields in a resource value, do NOT inline the raw secret. Create a Variable first with \`is_secret: true\`, then in the resource value reference it as \`"$var:path/to/variable"\`. +- Global mode can inspect resources and variables, but cannot create resource or variable drafts until their editor-native UserDraft contracts are complete. +- For secret fields in a resource value, do NOT inline the raw secret. Reference variables as \`"$var:path/to/variable"\`. - Reference formats inside resource values: \`$var:g/all/name\` (global), \`$var:u/user/name\` (user), \`$var:f/folder/name\` (folder). Reference another resource with \`$res:path/to/resource\`. -- When deploying drafts that depend on each other (e.g., a resource and the variables it references), deploy the variables first. - Use \`search_resource_types\` to discover valid \`resource_type\` names and their JSON Schemas. Match the resource value to that schema. - For OAuth resources, the \`is_oauth: true\` flag is managed by Windmill's OAuth flow; global mode generally creates manual resources, not OAuth ones. @@ -1476,59 +1441,13 @@ export const globalTools: Tool<{}>[] = [ return deleteWorkspaceItem(parsed, ctx) } }, - { - def: createToolDef( - writeResourceSchema, - 'write_resource', - 'Create or overwrite an AI draft resource. Does not save or deploy. Reference secret values via $var:path/to/variable; create the variable separately with write_variable.', - { strict: false } - ), - showDetails: true, - streamArguments: true, - showFade: true, - fn: async (ctx) => { - const parsed = writeResourceSchema.parse(ctx.args) - return writeFallbackDraft( - { - type: 'resource', - path: parsed.path, - summary: parsed.description, - value: parsed, - isDraft: true - }, - ctx - ) - } - }, - { - def: createToolDef( - writeVariableSchema, - 'write_variable', - 'Create or overwrite an AI draft variable. Does not save or deploy. Use is_secret: true for secret values. After deploy, reference from a resource as $var:path/to/variable.', - { strict: false } - ), - showDetails: true, - streamArguments: true, - showFade: true, - fn: async (ctx) => { - const parsed = writeVariableSchema.parse(ctx.args) - return writeFallbackDraft( - { - type: 'variable', - path: parsed.path, - summary: parsed.description, - value: parsed, - isDraft: true - }, - ctx - ) - } - }, + // TODO: Re-enable write_resource/write_variable tools after resource and + // variable use self-contained editor-native UserDraft values. { def: createToolDef( searchResourceTypesSchema, 'search_resource_types', - 'Search for resource types in the workspace by substring. Returns names, descriptions, and JSON Schemas — use this before write_resource to know what shape value should have.' + 'Search for resource types in the workspace by substring. Returns names, descriptions, and JSON Schemas.' ), fn: async ({ args, workspace, toolId, toolCallbacks }) => { const parsed = searchResourceTypesSchema.parse(args) @@ -1705,7 +1624,7 @@ function finishDraftWrite(item: WorkspaceItem, exists: boolean, ctx: WriteDraftC { success: true, message: `${verb} AI draft ${item.type} "${item.path}". The workspace was not saved or deployed.`, - item: item.type === 'variable' ? serializeWorkspaceItemForRead(item) : item + item }, null, 2 @@ -1871,20 +1790,6 @@ async function writeTriggerDraft( ) } -async function writeFallbackDraft(item: WorkspaceItem, ctx: WriteDraftCtx): Promise { - const { workspace } = ctx - startDraftWrite(ctx, item.type, item.path) - - const existingDraft = - getGlobalDraft(workspace, item.type, item.path, item.triggerKind) !== undefined - const backendExists = existingDraft - ? false - : await workspaceItemExists(item.type, item.path, workspace, item.triggerKind) - const stored = globalDraftStore.setDraft(workspace, item) - - return finishDraftWrite(stored, existingDraft || backendExists, ctx) -} - function saveAppDraft(workspace: string, path: string, value: AppDraftValue): WorkspaceItem { UserDraft.save('raw_app', path, normalizeAppDraftValue(value), { workspace }) return getRequiredGlobalDraft(workspace, 'app', path) diff --git a/frontend/src/lib/components/copilot/chat/global/deployRequests.test.ts b/frontend/src/lib/components/copilot/chat/global/deployRequests.test.ts index f79bbd2a4e..88cbafe205 100644 --- a/frontend/src/lib/components/copilot/chat/global/deployRequests.test.ts +++ b/frontend/src/lib/components/copilot/chat/global/deployRequests.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import type { Flow, NewScript, Script } from '$lib/gen/types.gen' import { buildFlowDeployRequestBody, buildScriptDeployRequestBody } from './deployRequests' -import type { WorkspaceItem } from './draftStore.svelte' +import type { WorkspaceItem } from './workspaceItems' describe('global AI deploy request builders', () => { it('preserves existing script metadata while replacing draft-controlled fields', () => { diff --git a/frontend/src/lib/components/copilot/chat/global/deployRequests.ts b/frontend/src/lib/components/copilot/chat/global/deployRequests.ts index c9b2630c51..9e779779f6 100644 --- a/frontend/src/lib/components/copilot/chat/global/deployRequests.ts +++ b/frontend/src/lib/components/copilot/chat/global/deployRequests.ts @@ -1,5 +1,5 @@ import type { Flow, NewScript, OpenFlowWPath, Script } from '$lib/gen/types.gen' -import type { FlowDraftValue, WorkspaceItem } from './draftStore.svelte' +import type { FlowDraftValue, WorkspaceItem } from './workspaceItems' type ScriptWithDeployMetadata = Script & Partial> diff --git a/frontend/src/lib/components/copilot/chat/global/draftStore.svelte.ts b/frontend/src/lib/components/copilot/chat/global/draftStore.svelte.ts deleted file mode 100644 index c0fb8985f5..0000000000 --- a/frontend/src/lib/components/copilot/chat/global/draftStore.svelte.ts +++ /dev/null @@ -1,192 +0,0 @@ -import type { - AzureTriggerData, - CreateResource, - CreateVariable, - FlowValue, - GcpTriggerData, - NewHttpTrigger, - NewKafkaTrigger, - NewMqttTrigger, - NewNatsTrigger, - NewPostgresTrigger, - NewSchedule, - NewSqsTrigger, - NewWebsocketTrigger, - Policy, - ScriptLang -} from '$lib/gen/types.gen' - -/** - * Flow draft value. Mirrors what the backend's create/update flow API expects - * — the OpenFlow value, plus the inputs schema and (optional) groups. - * - * Schema and groups are split out from FlowValue intentionally so that - * deploy_workspace_item can preserve them through the draft → workspace - * round-trip; an earlier version dropped them on every deploy. - */ -export type FlowDraftValue = { - value: FlowValue - schema?: Record | null - groups?: NonNullable | null -} - -export const TRIGGER_KINDS = [ - 'http', - 'websocket', - 'kafka', - 'nats', - 'postgres', - 'mqtt', - 'sqs', - 'gcp', - 'azure' -] as const - -export type TriggerKind = (typeof TRIGGER_KINDS)[number] - -export type TriggerRequestBody = - | NewHttpTrigger - | NewWebsocketTrigger - | NewKafkaTrigger - | NewNatsTrigger - | NewPostgresTrigger - | NewMqttTrigger - | NewSqsTrigger - | GcpTriggerData - | AzureTriggerData - -export type WorkspaceItemType = - | 'script' - | 'flow' - | 'schedule' - | 'trigger' - | 'resource' - | 'variable' - | 'app' - -export type AppDraftValue = { - summary?: string - files: Record - runnables: Record - data?: any - policy?: Policy - custom_path?: string -} - -export type WorkspaceItem = { - type: WorkspaceItemType - path: string - summary?: string - language?: ScriptLang - triggerKind?: TriggerKind - value?: - | string - | FlowDraftValue - | NewSchedule - | TriggerRequestBody - | CreateResource - | CreateVariable - | AppDraftValue - isDraft: boolean -} - -export function getWorkspaceItemKey( - type: WorkspaceItemType, - path: string, - triggerKind?: TriggerKind -): string { - if (type === 'trigger') { - return `trigger:${triggerKind ?? ''}:${path}` - } - return `${type}:${path}` -} - -function clone(value: T): T { - return structuredClone($state.snapshot(value)) as T -} - -class GlobalDraftStore { - private drafts = $state>>({}) - - private getWorkspaceDrafts(workspace: string): Record { - return this.drafts[workspace] ?? {} - } - - private ensureWorkspaceDrafts(workspace: string): Record { - if (!this.drafts[workspace]) { - this.drafts[workspace] = {} - } - return this.drafts[workspace] - } - - listDrafts(workspace: string): WorkspaceItem[] { - return Object.values(this.getWorkspaceDrafts(workspace)).map(clone) - } - - getDraft( - workspace: string, - type: WorkspaceItemType, - path: string, - triggerKind?: TriggerKind - ): WorkspaceItem | undefined { - const draft = this.getWorkspaceDrafts(workspace)[getWorkspaceItemKey(type, path, triggerKind)] - return draft ? clone(draft) : undefined - } - - setDraft(workspace: string, item: WorkspaceItem): WorkspaceItem { - const stored: WorkspaceItem = { ...clone(item), isDraft: true } - this.ensureWorkspaceDrafts(workspace)[ - getWorkspaceItemKey(item.type, item.path, item.triggerKind) - ] = stored - return clone(stored) - } - - deleteDraft( - workspace: string, - type: WorkspaceItemType, - path: string, - triggerKind?: TriggerKind - ): void { - const drafts = this.drafts[workspace] - if (!drafts) return - - delete drafts[getWorkspaceItemKey(type, path, triggerKind)] - if (Object.keys(drafts).length === 0) { - delete this.drafts[workspace] - } - } - - clearDrafts(workspace: string): void { - delete this.drafts[workspace] - } - - getScriptDraft(workspace: string, path: string): WorkspaceItem | undefined { - return this.getDraft(workspace, 'script', path) - } - - getFlowDraft(workspace: string, path: string): WorkspaceItem | undefined { - return this.getDraft(workspace, 'flow', path) - } - - getScheduleDraft(workspace: string, path: string): WorkspaceItem | undefined { - return this.getDraft(workspace, 'schedule', path) - } - - getTriggerDraft(workspace: string, kind: TriggerKind, path: string): WorkspaceItem | undefined { - return this.getDraft(workspace, 'trigger', path, kind) - } - - getResourceDraft(workspace: string, path: string): WorkspaceItem | undefined { - return this.getDraft(workspace, 'resource', path) - } - - getVariableDraft(workspace: string, path: string): WorkspaceItem | undefined { - return this.getDraft(workspace, 'variable', path) - } - - getAppDraft(workspace: string, path: string): WorkspaceItem | undefined { - return this.getDraft(workspace, 'app', path) - } -} - -export const globalDraftStore = new GlobalDraftStore() diff --git a/frontend/src/lib/components/copilot/chat/global/draftStore.test.ts b/frontend/src/lib/components/copilot/chat/global/draftStore.test.ts deleted file mode 100644 index a05126a568..0000000000 --- a/frontend/src/lib/components/copilot/chat/global/draftStore.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest' -import { globalDraftStore } from './draftStore.svelte' - -const WORKSPACE_A = 'draft-store-test-a' -const WORKSPACE_B = 'draft-store-test-b' - -function clearTestDrafts() { - globalDraftStore.clearDrafts(WORKSPACE_A) - globalDraftStore.clearDrafts(WORKSPACE_B) -} - -describe('globalDraftStore', () => { - beforeEach(clearTestDrafts) - - it('lists and reads drafts only from the requested workspace', () => { - globalDraftStore.setDraft(WORKSPACE_A, { - type: 'script', - path: 'f/shared/path', - language: 'bun', - value: 'export async function main() {}', - isDraft: true - }) - - expect(globalDraftStore.getDraft(WORKSPACE_A, 'script', 'f/shared/path')?.value).toBe( - 'export async function main() {}' - ) - expect(globalDraftStore.getDraft(WORKSPACE_B, 'script', 'f/shared/path')).toBeUndefined() - expect(globalDraftStore.listDrafts(WORKSPACE_A)).toHaveLength(1) - expect(globalDraftStore.listDrafts(WORKSPACE_B)).toHaveLength(0) - }) - - it('deletes and clears drafts only from the requested workspace', () => { - globalDraftStore.setDraft(WORKSPACE_A, { - type: 'flow', - path: 'f/shared/path', - value: { value: { modules: [] }, schema: null, groups: null }, - isDraft: true - }) - globalDraftStore.setDraft(WORKSPACE_B, { - type: 'flow', - path: 'f/shared/path', - value: { value: { modules: [] }, schema: { workspace: WORKSPACE_B }, groups: null }, - isDraft: true - }) - - globalDraftStore.deleteDraft(WORKSPACE_A, 'flow', 'f/shared/path') - - expect(globalDraftStore.getDraft(WORKSPACE_A, 'flow', 'f/shared/path')).toBeUndefined() - expect(globalDraftStore.getDraft(WORKSPACE_B, 'flow', 'f/shared/path')).toBeDefined() - - globalDraftStore.clearDrafts(WORKSPACE_B) - - expect(globalDraftStore.listDrafts(WORKSPACE_A)).toHaveLength(0) - expect(globalDraftStore.listDrafts(WORKSPACE_B)).toHaveLength(0) - }) -}) diff --git a/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts b/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts index 8b2bc6fcbb..4e8be9c5e5 100644 --- a/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts +++ b/frontend/src/lib/components/copilot/chat/global/userDraftAdapter.ts @@ -2,13 +2,12 @@ import type { Flow, NewSchedule, NewScript } from '$lib/gen/types.gen' import { UserDraft, type UserDraftItemKind, type UserDraftListEntry } from '$lib/userDraft.svelte' import { getWorkspaceItemKey, - globalDraftStore, type AppDraftValue, type TriggerRequestBody, type TriggerKind, type WorkspaceItem, type WorkspaceItemType -} from './draftStore.svelte' +} from './workspaceItems' type SharedWorkspaceItemType = 'script' | 'flow' | 'app' | 'schedule' | 'trigger' @@ -53,6 +52,11 @@ const SHARED_DRAFT_KINDS = [ ] as const satisfies UserDraftItemKind[] const DEFAULT_APP_DATA = { tables: [], datatable: undefined, schema: undefined } +// TODO: Add resource and variable here only after their editor-native +// UserDraft contracts are self-contained. Resource drafts currently need +// resource_type outside the draft value, and new variable drafts do not have +// a complete persisted/live empty-path contract. + function clone(value: T): T { return structuredClone(value) as T } @@ -221,19 +225,14 @@ export function getGlobalDraft( triggerKind?: TriggerKind ): WorkspaceItem | undefined { if (isSharedWorkspaceItemType(type)) { - const shared = getSharedDraft(workspace, type, path, triggerKind) - if (shared) return shared + return getSharedDraft(workspace, type, path, triggerKind) } - return globalDraftStore.getDraft(workspace, type, path, triggerKind) + return undefined } export function listGlobalDrafts(workspace: string): WorkspaceItem[] { const drafts = new Map() - for (const draft of globalDraftStore.listDrafts(workspace)) { - drafts.set(getWorkspaceItemKey(draft.type, draft.path, draft.triggerKind), draft) - } - for (const entry of UserDraft.list({ workspace, itemKinds: [...SHARED_DRAFT_KINDS] })) { const draft = sharedDraftEntryToWorkspaceItem(entry) if (!draft) continue @@ -252,14 +251,12 @@ export function deleteGlobalDraft( if (isSharedWorkspaceItemType(type)) { deleteSharedDraft(workspace, type, path, triggerKind) } - globalDraftStore.deleteDraft(workspace, type, path, triggerKind) } export function clearGlobalDrafts(workspace: string): void { for (const draft of UserDraft.list({ workspace, itemKinds: [...SHARED_DRAFT_KINDS] })) { UserDraft.remove(draft.itemKind, draft.path, { workspace }) } - globalDraftStore.clearDrafts(workspace) } export function triggerKindToUserDraftKind(kind: TriggerKind): UserDraftItemKind { diff --git a/frontend/src/lib/components/copilot/chat/global/workspaceItems.ts b/frontend/src/lib/components/copilot/chat/global/workspaceItems.ts new file mode 100644 index 0000000000..a8036cc1c1 --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/global/workspaceItems.ts @@ -0,0 +1,102 @@ +import type { + AzureTriggerData, + CreateResource, + CreateVariable, + FlowValue, + GcpTriggerData, + NewHttpTrigger, + NewKafkaTrigger, + NewMqttTrigger, + NewNatsTrigger, + NewPostgresTrigger, + NewSchedule, + NewSqsTrigger, + NewWebsocketTrigger, + Policy, + ScriptLang +} from '$lib/gen/types.gen' + +/** + * Flow draft value. Mirrors what the backend's create/update flow API expects + * -- the OpenFlow value, plus the inputs schema and (optional) groups. + * + * Schema and groups are split out from FlowValue intentionally so that + * deploy_workspace_item can preserve them through the draft -> workspace + * round-trip; an earlier version dropped them on every deploy. + */ +export type FlowDraftValue = { + value: FlowValue + schema?: Record | null + groups?: NonNullable | null +} + +export const TRIGGER_KINDS = [ + 'http', + 'websocket', + 'kafka', + 'nats', + 'postgres', + 'mqtt', + 'sqs', + 'gcp', + 'azure' +] as const + +export type TriggerKind = (typeof TRIGGER_KINDS)[number] + +export type TriggerRequestBody = + | NewHttpTrigger + | NewWebsocketTrigger + | NewKafkaTrigger + | NewNatsTrigger + | NewPostgresTrigger + | NewMqttTrigger + | NewSqsTrigger + | GcpTriggerData + | AzureTriggerData + +export type WorkspaceItemType = + | 'script' + | 'flow' + | 'schedule' + | 'trigger' + | 'resource' + | 'variable' + | 'app' + +export type AppDraftValue = { + summary?: string + files: Record + runnables: Record + data?: any + policy?: Policy + custom_path?: string +} + +export type WorkspaceItem = { + type: WorkspaceItemType + path: string + summary?: string + language?: ScriptLang + triggerKind?: TriggerKind + value?: + | string + | FlowDraftValue + | NewSchedule + | TriggerRequestBody + | CreateResource + | CreateVariable + | AppDraftValue + isDraft: boolean +} + +export function getWorkspaceItemKey( + type: WorkspaceItemType, + path: string, + triggerKind?: TriggerKind +): string { + if (type === 'trigger') { + return `trigger:${triggerKind ?? ''}:${path}` + } + return `${type}:${path}` +}