feat: share trigger drafts with global ai

This commit is contained in:
centdix
2026-05-13 14:28:31 +02:00
parent 972c93a718
commit c8418ece27
2 changed files with 287 additions and 13 deletions
@@ -50,6 +50,46 @@ vi.mock('$lib/gen', async () => {
existsFlowByPath: vi.fn(async () => false),
listFlows: vi.fn(async () => [])
}),
ScheduleService: wrapService(actual.ScheduleService, {
existsSchedule: vi.fn(async () => false),
listSchedules: vi.fn(async () => [])
}),
HttpTriggerService: wrapService(actual.HttpTriggerService, {
existsHttpTrigger: vi.fn(async () => false),
listHttpTriggers: vi.fn(async () => [])
}),
WebsocketTriggerService: wrapService(actual.WebsocketTriggerService, {
existsWebsocketTrigger: vi.fn(async () => false),
listWebsocketTriggers: vi.fn(async () => [])
}),
KafkaTriggerService: wrapService(actual.KafkaTriggerService, {
existsKafkaTrigger: vi.fn(async () => false),
listKafkaTriggers: vi.fn(async () => [])
}),
NatsTriggerService: wrapService(actual.NatsTriggerService, {
existsNatsTrigger: vi.fn(async () => false),
listNatsTriggers: vi.fn(async () => [])
}),
PostgresTriggerService: wrapService(actual.PostgresTriggerService, {
existsPostgresTrigger: vi.fn(async () => false),
listPostgresTriggers: vi.fn(async () => [])
}),
MqttTriggerService: wrapService(actual.MqttTriggerService, {
existsMqttTrigger: vi.fn(async () => false),
listMqttTriggers: vi.fn(async () => [])
}),
SqsTriggerService: wrapService(actual.SqsTriggerService, {
existsSqsTrigger: vi.fn(async () => false),
listSqsTriggers: vi.fn(async () => [])
}),
GcpTriggerService: wrapService(actual.GcpTriggerService, {
existsGcpTrigger: vi.fn(async () => false),
listGcpTriggers: vi.fn(async () => [])
}),
AzureTriggerService: wrapService(actual.AzureTriggerService, {
existsAzureTrigger: vi.fn(async () => false),
listAzureTriggers: vi.fn(async () => [])
}),
AppService: wrapService(actual.AppService, {
existsApp: vi.fn(async () => false)
}),
@@ -156,6 +196,83 @@ describe('global AI tools', () => {
])
})
it('writes schedule drafts into the shared UserDraft store', async () => {
await callGlobalTool('write_schedule', {
path: 'f/schedules/daily',
summary: 'Daily schedule',
schedule: '0 0 12 * * *',
timezone: 'UTC',
script_path: 'f/scripts/hello',
is_flow: false,
args: { name: 'Ada' },
enabled: true
})
expect(
UserDraft.get<any>('trigger_schedule', 'f/schedules/daily', { workspace: WORKSPACE })
).toMatchObject({
path: 'f/schedules/daily',
summary: 'Daily schedule',
script_path: 'f/scripts/hello',
is_flow: false
})
const raw = await callGlobalTool('read_workspace_item', {
type: 'schedule',
path: 'f/schedules/daily'
})
expect(JSON.parse(raw)).toMatchObject({
type: 'schedule',
path: 'f/schedules/daily',
summary: 'Daily schedule',
value: expect.objectContaining({
schedule: '0 0 12 * * *',
timezone: 'UTC'
}),
isDraft: true
})
})
it('writes trigger drafts into the shared trigger UserDraft kind', async () => {
await callGlobalTool('write_trigger', {
kind: 'http',
config: {
path: 'f/triggers/hook',
summary: 'Hook trigger',
script_path: 'f/scripts/hello',
route_path: 'api/hook',
is_flow: false,
http_method: 'post',
authentication_method: 'none',
is_static_website: false
}
})
expect(
UserDraft.get<any>('trigger_http', 'f/triggers/hook', { workspace: WORKSPACE })
).toMatchObject({
path: 'f/triggers/hook',
summary: 'Hook trigger',
route_path: 'api/hook',
is_flow: false
})
const raw = await callGlobalTool('list_workspace_items', {
types: ['trigger']
})
expect(JSON.parse(raw)).toEqual([
expect.objectContaining({
type: 'trigger',
triggerKind: 'http',
path: 'f/triggers/hook',
summary: 'Hook trigger',
isDraft: true
})
])
})
it('redacts variable draft values when reading workspace items', async () => {
await callGlobalTool('write_variable', {
path: 'f/secrets/api_key',
@@ -1,5 +1,5 @@
import { FlowService, ScriptService } from '$lib/gen'
import type { Flow, NewScript, Script, ScriptLang } from '$lib/gen/types.gen'
import type { Flow, NewSchedule, NewScript, Script, ScriptLang } from '$lib/gen/types.gen'
import { UserDraft, type UserDraftItemKind, type UserDraftListEntry } from '$lib/userDraft.svelte'
import { emptySchema } from '$lib/utils'
import {
@@ -7,14 +7,53 @@ import {
globalDraftStore,
type AppDraftValue,
type FlowDraftValue,
type TriggerRequestBody,
type TriggerKind,
type WorkspaceItem,
type WorkspaceItemType
} from './draftStore.svelte'
type SharedWorkspaceItemType = 'script' | 'flow' | 'app'
type SharedWorkspaceItemType = 'script' | 'flow' | 'app' | 'schedule' | 'trigger'
const SHARED_DRAFT_KINDS = ['script', 'flow', 'raw_app'] as const satisfies UserDraftItemKind[]
const TRIGGER_DRAFT_KIND_BY_TRIGGER_KIND = {
http: 'trigger_http',
websocket: 'trigger_websocket',
kafka: 'trigger_kafka',
nats: 'trigger_nats',
postgres: 'trigger_postgres',
mqtt: 'trigger_mqtt',
sqs: 'trigger_sqs',
gcp: 'trigger_gcp',
azure: 'trigger_azure'
} as const satisfies Record<TriggerKind, UserDraftItemKind>
const TRIGGER_KIND_BY_DRAFT_KIND = {
trigger_http: 'http',
trigger_websocket: 'websocket',
trigger_kafka: 'kafka',
trigger_nats: 'nats',
trigger_postgres: 'postgres',
trigger_mqtt: 'mqtt',
trigger_sqs: 'sqs',
trigger_gcp: 'gcp',
trigger_azure: 'azure'
} as const satisfies Partial<Record<UserDraftItemKind, TriggerKind>>
const SHARED_DRAFT_KINDS = [
'script',
'flow',
'raw_app',
'trigger_schedule',
'trigger_http',
'trigger_websocket',
'trigger_kafka',
'trigger_nats',
'trigger_postgres',
'trigger_mqtt',
'trigger_sqs',
'trigger_gcp',
'trigger_azure'
] as const satisfies UserDraftItemKind[]
const DEFAULT_SCRIPT_LANGUAGE: ScriptLang = 'bun'
const DEFAULT_APP_DATA = { tables: [], datatable: undefined, schema: undefined }
@@ -23,11 +62,30 @@ function clone<T>(value: T): T {
}
function isSharedWorkspaceItemType(type: WorkspaceItemType): type is SharedWorkspaceItemType {
return type === 'script' || type === 'flow' || type === 'app'
return (
type === 'script' ||
type === 'flow' ||
type === 'app' ||
type === 'schedule' ||
type === 'trigger'
)
}
function sharedDraftKind(type: SharedWorkspaceItemType): UserDraftItemKind {
return type === 'app' ? 'raw_app' : type
function sharedDraftKind(
type: SharedWorkspaceItemType,
triggerKind?: TriggerKind
): UserDraftItemKind | undefined {
switch (type) {
case 'script':
case 'flow':
return type
case 'app':
return 'raw_app'
case 'schedule':
return 'trigger_schedule'
case 'trigger':
return triggerKind ? TRIGGER_DRAFT_KIND_BY_TRIGGER_KIND[triggerKind] : undefined
}
}
function normalizeAppDraftValue(value: AppDraftValue): AppDraftValue {
@@ -39,6 +97,18 @@ function normalizeAppDraftValue(value: AppDraftValue): AppDraftValue {
}
}
function getItemSummary(value: unknown): string | undefined {
return ((value as { summary?: string | null } | undefined)?.summary ?? undefined) || undefined
}
function applyItemSummary<T extends object>(value: T, summary: string | undefined): T {
const draft = value as T & { summary?: string | null }
if (draft.summary === undefined && summary !== undefined) {
draft.summary = summary
}
return value
}
function scriptDraftToWorkspaceItem(path: string, draft: NewScript): WorkspaceItem {
return {
type: 'script',
@@ -75,6 +145,31 @@ function appDraftToWorkspaceItem(path: string, draft: AppDraftValue): WorkspaceI
}
}
function scheduleDraftToWorkspaceItem(path: string, draft: NewSchedule): WorkspaceItem {
return {
type: 'schedule',
path,
summary: draft.summary ?? undefined,
value: clone(draft),
isDraft: true
}
}
function triggerDraftToWorkspaceItem(
kind: TriggerKind,
path: string,
draft: TriggerRequestBody
): WorkspaceItem {
return {
type: 'trigger',
triggerKind: kind,
path,
summary: getItemSummary(draft),
value: clone(draft),
isDraft: true
}
}
function sharedDraftEntryToWorkspaceItem(entry: UserDraftListEntry): WorkspaceItem | undefined {
switch (entry.itemKind) {
case 'script':
@@ -83,8 +178,13 @@ function sharedDraftEntryToWorkspaceItem(entry: UserDraftListEntry): WorkspaceIt
return flowDraftToWorkspaceItem(entry.path, entry.value as Flow)
case 'raw_app':
return appDraftToWorkspaceItem(entry.path, entry.value as AppDraftValue)
case 'trigger_schedule':
return scheduleDraftToWorkspaceItem(entry.path, entry.value as NewSchedule)
default:
return undefined
const triggerKind = TRIGGER_KIND_BY_DRAFT_KIND[entry.itemKind]
return triggerKind
? triggerDraftToWorkspaceItem(triggerKind, entry.path, entry.value as TriggerRequestBody)
: undefined
}
}
@@ -160,6 +260,33 @@ function appItemToUserDraft(item: WorkspaceItem): AppDraftValue {
})
}
function scheduleItemToUserDraft(item: WorkspaceItem): NewSchedule {
const value = item.value as NewSchedule | undefined
if (!value) {
throw new Error(`Draft schedule "${item.path}" is missing value.`)
}
const draft = {
...clone(value),
path: item.path
}
return applyItemSummary(draft, item.summary)
}
function triggerItemToUserDraft(item: WorkspaceItem): TriggerRequestBody {
const value = item.value as TriggerRequestBody | undefined
if (!item.triggerKind) {
throw new Error(`Draft trigger "${item.path}" is missing trigger kind.`)
}
if (!value) {
throw new Error(`Draft trigger "${item.path}" is missing value.`)
}
const draft = {
...clone(value),
path: item.path
}
return applyItemSummary(draft, item.summary)
}
async function loadExistingScript(
workspace: string,
path: string,
@@ -181,9 +308,11 @@ async function loadExistingFlow(
function getSharedDraft(
workspace: string,
type: SharedWorkspaceItemType,
path: string
path: string,
triggerKind?: TriggerKind
): WorkspaceItem | undefined {
const itemKind = sharedDraftKind(type)
const itemKind = sharedDraftKind(type, triggerKind)
if (!itemKind) return undefined
const draft = UserDraft.get(itemKind, path, { workspace })
if (draft === undefined) return undefined
@@ -194,6 +323,12 @@ function getSharedDraft(
return flowDraftToWorkspaceItem(path, draft as Flow)
case 'app':
return appDraftToWorkspaceItem(path, draft as AppDraftValue)
case 'schedule':
return scheduleDraftToWorkspaceItem(path, draft as NewSchedule)
case 'trigger':
return triggerKind
? triggerDraftToWorkspaceItem(triggerKind, path, draft as TriggerRequestBody)
: undefined
}
}
@@ -224,13 +359,35 @@ async function setSharedDraft(
UserDraft.save('raw_app', item.path, draft, { workspace })
return appDraftToWorkspaceItem(item.path, draft)
}
case 'schedule': {
const draft = scheduleItemToUserDraft(item)
UserDraft.save('trigger_schedule', item.path, draft, { workspace })
return scheduleDraftToWorkspaceItem(item.path, draft)
}
case 'trigger': {
if (!item.triggerKind) {
throw new Error(`Draft trigger "${item.path}" is missing trigger kind.`)
}
const draft = triggerItemToUserDraft(item)
UserDraft.save(TRIGGER_DRAFT_KIND_BY_TRIGGER_KIND[item.triggerKind], item.path, draft, {
workspace
})
return triggerDraftToWorkspaceItem(item.triggerKind, item.path, draft)
}
default:
throw new Error(`Unsupported shared draft type: ${item.type}`)
}
}
function deleteSharedDraft(workspace: string, type: SharedWorkspaceItemType, path: string): void {
UserDraft.remove(sharedDraftKind(type), path, { workspace })
function deleteSharedDraft(
workspace: string,
type: SharedWorkspaceItemType,
path: string,
triggerKind?: TriggerKind
): void {
const itemKind = sharedDraftKind(type, triggerKind)
if (!itemKind) return
UserDraft.remove(itemKind, path, { workspace })
}
export function getGlobalDraft(
@@ -240,7 +397,7 @@ export function getGlobalDraft(
triggerKind?: TriggerKind
): WorkspaceItem | undefined {
if (isSharedWorkspaceItemType(type)) {
const shared = getSharedDraft(workspace, type, path)
const shared = getSharedDraft(workspace, type, path, triggerKind)
if (shared) return shared
}
return globalDraftStore.getDraft(workspace, type, path, triggerKind)
@@ -290,7 +447,7 @@ export function deleteGlobalDraft(
triggerKind?: TriggerKind
): void {
if (isSharedWorkspaceItemType(type)) {
deleteSharedDraft(workspace, type, path)
deleteSharedDraft(workspace, type, path, triggerKind)
}
globalDraftStore.deleteDraft(workspace, type, path, triggerKind)
}