mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
feat(web): automations contracts, API client, atoms and hooks
Foundation for the v1 automations UI. Mirrors backend Pydantic schemas into Zod and wires the data layer end-to-end so feature surfaces can be built on top. contracts/types/automation.types.ts: - AutomationStatus, TriggerType, RunStatus enums. - AutomationDefinition envelope (PlanStep, TriggerSpec, Execution, Metadata, Inputs). - AutomationCreate/Update/Detail/Summary/List + listParams. - TriggerCreate/Update/Detail. - RunSummary/Detail/List + runListParams. lib/apis/automations-api.service.ts: - list/get/create/update/delete automations. - add/update/remove triggers (sub-resource). - list/get runs (read-only sub-resource). - safeParse on every write, 204-safe deletes. atoms/automations/: - automationsListAtom (active search space, first page). - 6 mutation atoms with toast + cache invalidation. hooks/: - use-automations.ts wraps the list atom. - use-automation.ts: parameterized detail by id. - use-automation-runs.ts: useAutomationRuns + useAutomationRun. lib/query-client/cache-keys.ts: automations namespace (list, detail, runs, run) keyed by (id, limit, offset) where relevant. Smoke: zod round-trip OK on backend-shape payloads (Automation, AutomationCreate, Trigger, Run); typecheck clean for new files; biome clean.
This commit is contained in:
parent
d48bb2033b
commit
b18a5fdca9
8 changed files with 548 additions and 0 deletions
127
surfsense_web/atoms/automations/automations-mutation.atoms.ts
Normal file
127
surfsense_web/atoms/automations/automations-mutation.atoms.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { atomWithMutation } from "jotai-tanstack-query";
|
||||
import { toast } from "sonner";
|
||||
import type {
|
||||
AutomationCreateRequest,
|
||||
AutomationUpdateRequest,
|
||||
TriggerCreateRequest,
|
||||
TriggerUpdateRequest,
|
||||
} from "@/contracts/types/automation.types";
|
||||
import { automationsApiService } from "@/lib/apis/automations-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { queryClient } from "@/lib/query-client/client";
|
||||
|
||||
// Cache invalidation strategy:
|
||||
// - Automation writes invalidate the search-space list + the touched detail.
|
||||
// - Trigger writes only invalidate the parent automation detail (triggers
|
||||
// come back inline in AutomationDetail).
|
||||
// We deliberately invalidate the whole "automations" prefix on the list side
|
||||
// because list is keyed by (searchSpaceId, limit, offset) and we don't track
|
||||
// the active pagination in this layer.
|
||||
|
||||
function invalidateList(searchSpaceId: number) {
|
||||
queryClient.invalidateQueries({ queryKey: ["automations", "list", searchSpaceId] });
|
||||
}
|
||||
|
||||
function invalidateDetail(automationId: number) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.automations.detail(automationId),
|
||||
});
|
||||
}
|
||||
|
||||
export const createAutomationMutationAtom = atomWithMutation(() => ({
|
||||
meta: { suppressGlobalErrorToast: true },
|
||||
mutationFn: async (request: AutomationCreateRequest) => {
|
||||
return automationsApiService.createAutomation(request);
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
invalidateList(variables.search_space_id);
|
||||
toast.success("Automation created");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error("Error creating automation:", error);
|
||||
toast.error("Failed to create automation");
|
||||
},
|
||||
}));
|
||||
|
||||
export const updateAutomationMutationAtom = atomWithMutation(() => ({
|
||||
meta: { suppressGlobalErrorToast: true },
|
||||
mutationFn: async (vars: { automationId: number; patch: AutomationUpdateRequest }) => {
|
||||
return automationsApiService.updateAutomation(vars.automationId, vars.patch);
|
||||
},
|
||||
onSuccess: (automation, vars) => {
|
||||
invalidateDetail(vars.automationId);
|
||||
invalidateList(automation.search_space_id);
|
||||
toast.success("Automation updated");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error("Error updating automation:", error);
|
||||
toast.error("Failed to update automation");
|
||||
},
|
||||
}));
|
||||
|
||||
export const deleteAutomationMutationAtom = atomWithMutation(() => ({
|
||||
meta: { suppressGlobalErrorToast: true },
|
||||
mutationFn: async (vars: { automationId: number; searchSpaceId: number }) => {
|
||||
await automationsApiService.deleteAutomation(vars.automationId);
|
||||
return vars;
|
||||
},
|
||||
onSuccess: (vars) => {
|
||||
invalidateList(vars.searchSpaceId);
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Automation deleted");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error("Error deleting automation:", error);
|
||||
toast.error("Failed to delete automation");
|
||||
},
|
||||
}));
|
||||
|
||||
export const addTriggerMutationAtom = atomWithMutation(() => ({
|
||||
meta: { suppressGlobalErrorToast: true },
|
||||
mutationFn: async (vars: { automationId: number; payload: TriggerCreateRequest }) => {
|
||||
return automationsApiService.addTrigger(vars.automationId, vars.payload);
|
||||
},
|
||||
onSuccess: (_, vars) => {
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Trigger added");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error("Error adding trigger:", error);
|
||||
toast.error("Failed to add trigger");
|
||||
},
|
||||
}));
|
||||
|
||||
export const updateTriggerMutationAtom = atomWithMutation(() => ({
|
||||
meta: { suppressGlobalErrorToast: true },
|
||||
mutationFn: async (vars: {
|
||||
automationId: number;
|
||||
triggerId: number;
|
||||
patch: TriggerUpdateRequest;
|
||||
}) => {
|
||||
return automationsApiService.updateTrigger(vars.automationId, vars.triggerId, vars.patch);
|
||||
},
|
||||
onSuccess: (_, vars) => {
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Trigger updated");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error("Error updating trigger:", error);
|
||||
toast.error("Failed to update trigger");
|
||||
},
|
||||
}));
|
||||
|
||||
export const removeTriggerMutationAtom = atomWithMutation(() => ({
|
||||
meta: { suppressGlobalErrorToast: true },
|
||||
mutationFn: async (vars: { automationId: number; triggerId: number }) => {
|
||||
await automationsApiService.removeTrigger(vars.automationId, vars.triggerId);
|
||||
return vars;
|
||||
},
|
||||
onSuccess: (vars) => {
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Trigger removed");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error("Error removing trigger:", error);
|
||||
toast.error("Failed to remove trigger");
|
||||
},
|
||||
}));
|
||||
31
surfsense_web/atoms/automations/automations-query.atoms.ts
Normal file
31
surfsense_web/atoms/automations/automations-query.atoms.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { atomWithQuery } from "jotai-tanstack-query";
|
||||
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { automationsApiService } from "@/lib/apis/automations-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
// First page of the active search space's automations.
|
||||
// Detail + paginated/parameterized reads live in hooks (see use-automation.ts,
|
||||
// use-automation-runs.ts) so atoms stay tied to "current scope" and don't
|
||||
// proliferate atom families for every (id, limit, offset) tuple.
|
||||
const DEFAULT_LIMIT = 50;
|
||||
const DEFAULT_OFFSET = 0;
|
||||
|
||||
export const automationsListAtom = atomWithQuery((get) => {
|
||||
const searchSpaceId = get(activeSearchSpaceIdAtom);
|
||||
|
||||
return {
|
||||
queryKey: cacheKeys.automations.list(Number(searchSpaceId ?? 0), DEFAULT_LIMIT, DEFAULT_OFFSET),
|
||||
enabled: !!searchSpaceId,
|
||||
staleTime: 60 * 1000,
|
||||
queryFn: async () => {
|
||||
if (!searchSpaceId) {
|
||||
return { items: [], total: 0 };
|
||||
}
|
||||
return automationsApiService.listAutomations({
|
||||
search_space_id: Number(searchSpaceId),
|
||||
limit: DEFAULT_LIMIT,
|
||||
offset: DEFAULT_OFFSET,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
193
surfsense_web/contracts/types/automation.types.ts
Normal file
193
surfsense_web/contracts/types/automation.types.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import { z } from "zod";
|
||||
|
||||
// =============================================================================
|
||||
// Enums — mirror app/automations/persistence/enums/*
|
||||
// =============================================================================
|
||||
|
||||
export const automationStatus = z.enum(["active", "paused", "archived"]);
|
||||
export type AutomationStatus = z.infer<typeof automationStatus>;
|
||||
|
||||
export const triggerType = z.enum(["schedule", "manual"]);
|
||||
export type TriggerType = z.infer<typeof triggerType>;
|
||||
|
||||
export const runStatus = z.enum([
|
||||
"pending",
|
||||
"running",
|
||||
"succeeded",
|
||||
"failed",
|
||||
"cancelled",
|
||||
"timed_out",
|
||||
]);
|
||||
export type RunStatus = z.infer<typeof runStatus>;
|
||||
|
||||
// =============================================================================
|
||||
// Definition envelope — mirror app/automations/schemas/definition/*
|
||||
// =============================================================================
|
||||
|
||||
export const planStep = z.object({
|
||||
step_id: z.string().min(1),
|
||||
action: z.string().min(1),
|
||||
when: z.string().nullable().optional(),
|
||||
params: z.record(z.string(), z.any()).default({}),
|
||||
output_as: z.string().nullable().optional(),
|
||||
max_retries: z.number().int().min(0).nullable().optional(),
|
||||
timeout_seconds: z.number().int().positive().nullable().optional(),
|
||||
});
|
||||
export type PlanStep = z.infer<typeof planStep>;
|
||||
|
||||
export const definitionTriggerSpec = z.object({
|
||||
type: z.string().min(1),
|
||||
params: z.record(z.string(), z.any()).default({}),
|
||||
});
|
||||
export type DefinitionTriggerSpec = z.infer<typeof definitionTriggerSpec>;
|
||||
|
||||
export const execution = z.object({
|
||||
timeout_seconds: z.number().int().positive().default(600),
|
||||
max_retries: z.number().int().min(0).default(2),
|
||||
retry_backoff: z.enum(["exponential", "linear", "none"]).default("exponential"),
|
||||
concurrency: z.enum(["drop_if_running", "queue", "always"]).default("drop_if_running"),
|
||||
on_failure: z.array(planStep).default([]),
|
||||
});
|
||||
export type Execution = z.infer<typeof execution>;
|
||||
|
||||
// Backend ``Metadata`` is ``extra="allow"`` — keep ``tags`` typed, accept arbitrary keys.
|
||||
export const metadata = z.object({ tags: z.array(z.string()).default([]) }).catchall(z.any());
|
||||
export type Metadata = z.infer<typeof metadata>;
|
||||
|
||||
// Backend ``Inputs`` serializes its ``schema_`` field as ``schema`` (alias).
|
||||
export const inputs = z.object({
|
||||
schema: z.record(z.string(), z.any()),
|
||||
});
|
||||
export type Inputs = z.infer<typeof inputs>;
|
||||
|
||||
export const automationDefinition = z.object({
|
||||
schema_version: z.string().default("1.0"),
|
||||
name: z.string().min(1).max(200),
|
||||
goal: z.string().nullable().optional(),
|
||||
inputs: inputs.nullable().optional(),
|
||||
triggers: z.array(definitionTriggerSpec).default([]),
|
||||
plan: z.array(planStep).min(1),
|
||||
execution: execution.default(execution.parse({})),
|
||||
metadata: metadata.default(metadata.parse({})),
|
||||
});
|
||||
export type AutomationDefinition = z.infer<typeof automationDefinition>;
|
||||
|
||||
// =============================================================================
|
||||
// Triggers (sub-resource) — mirror app/automations/schemas/api/trigger.py
|
||||
// =============================================================================
|
||||
|
||||
export const triggerCreateRequest = z.object({
|
||||
type: triggerType,
|
||||
params: z.record(z.string(), z.any()).default({}),
|
||||
static_inputs: z.record(z.string(), z.any()).default({}),
|
||||
enabled: z.boolean().default(true),
|
||||
});
|
||||
export type TriggerCreateRequest = z.infer<typeof triggerCreateRequest>;
|
||||
|
||||
export const triggerUpdateRequest = z.object({
|
||||
enabled: z.boolean().nullable().optional(),
|
||||
params: z.record(z.string(), z.any()).nullable().optional(),
|
||||
static_inputs: z.record(z.string(), z.any()).nullable().optional(),
|
||||
});
|
||||
export type TriggerUpdateRequest = z.infer<typeof triggerUpdateRequest>;
|
||||
|
||||
export const trigger = z.object({
|
||||
id: z.number(),
|
||||
type: triggerType,
|
||||
params: z.record(z.string(), z.any()),
|
||||
static_inputs: z.record(z.string(), z.any()),
|
||||
enabled: z.boolean(),
|
||||
last_fired_at: z.string().nullable().optional(),
|
||||
next_fire_at: z.string().nullable().optional(),
|
||||
created_at: z.string(),
|
||||
});
|
||||
export type Trigger = z.infer<typeof trigger>;
|
||||
|
||||
// =============================================================================
|
||||
// Automations — mirror app/automations/schemas/api/automation.py
|
||||
// =============================================================================
|
||||
|
||||
export const automationCreateRequest = z.object({
|
||||
search_space_id: z.number(),
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().nullable().optional(),
|
||||
definition: automationDefinition,
|
||||
triggers: z.array(triggerCreateRequest).default([]),
|
||||
});
|
||||
export type AutomationCreateRequest = z.infer<typeof automationCreateRequest>;
|
||||
|
||||
export const automationUpdateRequest = z.object({
|
||||
name: z.string().min(1).max(200).nullable().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
status: automationStatus.nullable().optional(),
|
||||
definition: automationDefinition.nullable().optional(),
|
||||
});
|
||||
export type AutomationUpdateRequest = z.infer<typeof automationUpdateRequest>;
|
||||
|
||||
export const automationSummary = z.object({
|
||||
id: z.number(),
|
||||
search_space_id: z.number(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
status: automationStatus,
|
||||
version: z.number(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
});
|
||||
export type AutomationSummary = z.infer<typeof automationSummary>;
|
||||
|
||||
export const automation = automationSummary.extend({
|
||||
definition: automationDefinition,
|
||||
triggers: z.array(trigger).default([]),
|
||||
});
|
||||
export type Automation = z.infer<typeof automation>;
|
||||
|
||||
export const automationListResponse = z.object({
|
||||
items: z.array(automationSummary),
|
||||
total: z.number(),
|
||||
});
|
||||
export type AutomationListResponse = z.infer<typeof automationListResponse>;
|
||||
|
||||
export const automationListParams = z.object({
|
||||
search_space_id: z.number(),
|
||||
limit: z.number().int().min(1).max(200).default(50),
|
||||
offset: z.number().int().min(0).default(0),
|
||||
});
|
||||
export type AutomationListParams = z.infer<typeof automationListParams>;
|
||||
|
||||
// =============================================================================
|
||||
// Runs (sub-resource) — mirror app/automations/schemas/api/run.py
|
||||
// =============================================================================
|
||||
|
||||
export const runSummary = z.object({
|
||||
id: z.number(),
|
||||
automation_id: z.number(),
|
||||
trigger_id: z.number().nullable().optional(),
|
||||
status: runStatus,
|
||||
started_at: z.string().nullable().optional(),
|
||||
finished_at: z.string().nullable().optional(),
|
||||
created_at: z.string(),
|
||||
});
|
||||
export type RunSummary = z.infer<typeof runSummary>;
|
||||
|
||||
export const run = runSummary.extend({
|
||||
definition_snapshot: z.record(z.string(), z.any()),
|
||||
inputs: z.record(z.string(), z.any()),
|
||||
step_results: z.array(z.record(z.string(), z.any())),
|
||||
output: z.record(z.string(), z.any()).nullable().optional(),
|
||||
artifacts: z.array(z.record(z.string(), z.any())),
|
||||
error: z.record(z.string(), z.any()).nullable().optional(),
|
||||
});
|
||||
export type Run = z.infer<typeof run>;
|
||||
|
||||
export const runListResponse = z.object({
|
||||
items: z.array(runSummary),
|
||||
total: z.number(),
|
||||
});
|
||||
export type RunListResponse = z.infer<typeof runListResponse>;
|
||||
|
||||
export const runListParams = z.object({
|
||||
limit: z.number().int().min(1).max(200).default(50),
|
||||
offset: z.number().int().min(0).default(0),
|
||||
});
|
||||
export type RunListParams = z.infer<typeof runListParams>;
|
||||
42
surfsense_web/hooks/use-automation-runs.ts
Normal file
42
surfsense_web/hooks/use-automation-runs.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Run, RunListResponse } from "@/contracts/types/automation.types";
|
||||
import { automationsApiService } from "@/lib/apis/automations-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
const DEFAULT_LIMIT = 50;
|
||||
const DEFAULT_OFFSET = 0;
|
||||
|
||||
export interface UseAutomationRunsOptions {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/** Paginated run history for one automation. Newest-first per backend. */
|
||||
export function useAutomationRuns(
|
||||
automationId: number | undefined,
|
||||
{ limit = DEFAULT_LIMIT, offset = DEFAULT_OFFSET, enabled = true }: UseAutomationRunsOptions = {}
|
||||
) {
|
||||
return useQuery<RunListResponse, Error>({
|
||||
queryKey: cacheKeys.automations.runs(automationId ?? 0, limit, offset),
|
||||
queryFn: () => automationsApiService.listRuns(automationId as number, { limit, offset }),
|
||||
enabled: enabled && !!automationId,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Single run with the full snapshot, step results, output and artifacts. */
|
||||
export function useAutomationRun(
|
||||
automationId: number | undefined,
|
||||
runId: number | undefined,
|
||||
options: { enabled?: boolean } = {}
|
||||
) {
|
||||
const { enabled = true } = options;
|
||||
return useQuery<Run, Error>({
|
||||
queryKey: cacheKeys.automations.run(automationId ?? 0, runId ?? 0),
|
||||
queryFn: () => automationsApiService.getRun(automationId as number, runId as number),
|
||||
enabled: enabled && !!automationId && !!runId,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
19
surfsense_web/hooks/use-automation.ts
Normal file
19
surfsense_web/hooks/use-automation.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Automation } from "@/contracts/types/automation.types";
|
||||
import { automationsApiService } from "@/lib/apis/automations-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
/**
|
||||
* Fetch a single automation with its definition and triggers.
|
||||
* Lives outside the jotai atom layer because it's keyed by id, not by the
|
||||
* "current scope" the atom layer assumes.
|
||||
*/
|
||||
export function useAutomation(automationId: number | undefined) {
|
||||
return useQuery<Automation, Error>({
|
||||
queryKey: cacheKeys.automations.detail(automationId ?? 0),
|
||||
queryFn: () => automationsApiService.getAutomation(automationId as number),
|
||||
enabled: !!automationId,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
24
surfsense_web/hooks/use-automations.ts
Normal file
24
surfsense_web/hooks/use-automations.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"use client";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { automationsListAtom } from "@/atoms/automations/automations-query.atoms";
|
||||
|
||||
/**
|
||||
* List automations in the active search space (first page).
|
||||
* Pagination knobs live in detail/list hooks below; v1 surfaces only the
|
||||
* first page since automation counts are expected to be small.
|
||||
*/
|
||||
export function useAutomations() {
|
||||
const { data, isLoading, error, refetch } = useAutomationsRaw();
|
||||
return {
|
||||
automations: data?.items ?? [],
|
||||
total: data?.total ?? 0,
|
||||
loading: isLoading,
|
||||
error,
|
||||
refresh: refetch,
|
||||
};
|
||||
}
|
||||
|
||||
// Exposed for callers that prefer the raw react-query result shape.
|
||||
export function useAutomationsRaw() {
|
||||
return useAtomValue(automationsListAtom);
|
||||
}
|
||||
102
surfsense_web/lib/apis/automations-api.service.ts
Normal file
102
surfsense_web/lib/apis/automations-api.service.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import {
|
||||
type AutomationCreateRequest,
|
||||
type AutomationListParams,
|
||||
type AutomationUpdateRequest,
|
||||
automation,
|
||||
automationCreateRequest,
|
||||
automationListResponse,
|
||||
automationUpdateRequest,
|
||||
type RunListParams,
|
||||
run,
|
||||
runListResponse,
|
||||
type TriggerCreateRequest,
|
||||
type TriggerUpdateRequest,
|
||||
trigger,
|
||||
triggerCreateRequest,
|
||||
triggerUpdateRequest,
|
||||
} from "@/contracts/types/automation.types";
|
||||
import { ValidationError } from "../error";
|
||||
import { baseApiService } from "./base-api.service";
|
||||
|
||||
const BASE = "/api/v1/automations";
|
||||
|
||||
function rejectIfInvalid<T>(
|
||||
parsed: { success: true; data: T } | { success: false; error: { issues: { message: string }[] } }
|
||||
): T {
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError(
|
||||
`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`
|
||||
);
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
class AutomationsApiService {
|
||||
// ---- Automations ---------------------------------------------------------
|
||||
|
||||
listAutomations = async (params: AutomationListParams) => {
|
||||
const qs = new URLSearchParams({
|
||||
search_space_id: String(params.search_space_id),
|
||||
limit: String(params.limit),
|
||||
offset: String(params.offset),
|
||||
});
|
||||
return baseApiService.get(`${BASE}?${qs.toString()}`, automationListResponse);
|
||||
};
|
||||
|
||||
getAutomation = async (automationId: number) => {
|
||||
return baseApiService.get(`${BASE}/${automationId}`, automation);
|
||||
};
|
||||
|
||||
createAutomation = async (request: AutomationCreateRequest) => {
|
||||
const data = rejectIfInvalid(automationCreateRequest.safeParse(request));
|
||||
return baseApiService.post(BASE, automation, { body: data });
|
||||
};
|
||||
|
||||
updateAutomation = async (automationId: number, request: AutomationUpdateRequest) => {
|
||||
const data = rejectIfInvalid(automationUpdateRequest.safeParse(request));
|
||||
return baseApiService.patch(`${BASE}/${automationId}`, automation, { body: data });
|
||||
};
|
||||
|
||||
// Server returns 204; baseApiService now resolves to null and skips schema validation.
|
||||
deleteAutomation = async (automationId: number) => {
|
||||
return baseApiService.delete(`${BASE}/${automationId}`);
|
||||
};
|
||||
|
||||
// ---- Triggers (sub-resource) --------------------------------------------
|
||||
|
||||
addTrigger = async (automationId: number, request: TriggerCreateRequest) => {
|
||||
const data = rejectIfInvalid(triggerCreateRequest.safeParse(request));
|
||||
return baseApiService.post(`${BASE}/${automationId}/triggers`, trigger, { body: data });
|
||||
};
|
||||
|
||||
updateTrigger = async (
|
||||
automationId: number,
|
||||
triggerId: number,
|
||||
request: TriggerUpdateRequest
|
||||
) => {
|
||||
const data = rejectIfInvalid(triggerUpdateRequest.safeParse(request));
|
||||
return baseApiService.patch(`${BASE}/${automationId}/triggers/${triggerId}`, trigger, {
|
||||
body: data,
|
||||
});
|
||||
};
|
||||
|
||||
removeTrigger = async (automationId: number, triggerId: number) => {
|
||||
return baseApiService.delete(`${BASE}/${automationId}/triggers/${triggerId}`);
|
||||
};
|
||||
|
||||
// ---- Runs (sub-resource, read-only) -------------------------------------
|
||||
|
||||
listRuns = async (automationId: number, params: RunListParams) => {
|
||||
const qs = new URLSearchParams({
|
||||
limit: String(params.limit),
|
||||
offset: String(params.offset),
|
||||
});
|
||||
return baseApiService.get(`${BASE}/${automationId}/runs?${qs.toString()}`, runListResponse);
|
||||
};
|
||||
|
||||
getRun = async (automationId: number, runId: number) => {
|
||||
return baseApiService.get(`${BASE}/${automationId}/runs/${runId}`, run);
|
||||
};
|
||||
}
|
||||
|
||||
export const automationsApiService = new AutomationsApiService();
|
||||
|
|
@ -126,4 +126,14 @@ export const cacheKeys = {
|
|||
batchUnreadCounts: (searchSpaceId: number | null) =>
|
||||
["notifications", "unread-counts-batch", searchSpaceId] as const,
|
||||
},
|
||||
automations: {
|
||||
// list endpoint is keyed by pagination too so distinct pages don't collide
|
||||
list: (searchSpaceId: number, limit: number, offset: number) =>
|
||||
["automations", "list", searchSpaceId, limit, offset] as const,
|
||||
detail: (automationId: number) => ["automations", "detail", automationId] as const,
|
||||
runs: (automationId: number, limit: number, offset: number) =>
|
||||
["automations", "runs", automationId, limit, offset] as const,
|
||||
run: (automationId: number, runId: number) =>
|
||||
["automations", "runs", automationId, runId] as const,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue