Add the edit function that allows editing triggers and lets copilot edit triggers too without losing previous jobs

feat: Add update functionality for recurring and scheduled job rules

- Implemented update actions for recurring job rules and scheduled job rules, allowing users to modify existing rules with new input and scheduling configurations.
- Enhanced the UI components to support editing of job rules, including forms for both creating and updating rules.
- Updated the repository interfaces and MongoDB implementations to handle the new update operations for job rules.

This update improves the flexibility of managing job rules within the application.
This commit is contained in:
tusharmagar 2025-10-06 11:25:06 +08:00
parent d484170e32
commit e2c8d0490a
18 changed files with 1307 additions and 390 deletions

View file

@ -6,6 +6,7 @@ import { IListRecurringJobRulesController } from "@/src/interface-adapters/contr
import { IFetchRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/fetch-recurring-job-rule.controller";
import { IToggleRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/toggle-recurring-job-rule.controller";
import { IDeleteRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/delete-recurring-job-rule.controller";
import { IUpdateRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/update-recurring-job-rule.controller";
import { authCheck } from "./auth.actions";
import { z } from "zod";
import { Message } from "@/app/lib/types/types";
@ -15,6 +16,7 @@ const listRecurringJobRulesController = container.resolve<IListRecurringJobRules
const fetchRecurringJobRuleController = container.resolve<IFetchRecurringJobRuleController>('fetchRecurringJobRuleController');
const toggleRecurringJobRuleController = container.resolve<IToggleRecurringJobRuleController>('toggleRecurringJobRuleController');
const deleteRecurringJobRuleController = container.resolve<IDeleteRecurringJobRuleController>('deleteRecurringJobRuleController');
const updateRecurringJobRuleController = container.resolve<IUpdateRecurringJobRuleController>('updateRecurringJobRuleController');
export async function createRecurringJobRule(request: {
projectId: string,
@ -89,3 +91,23 @@ export async function deleteRecurringJobRule(request: {
ruleId: request.ruleId,
});
}
export async function updateRecurringJobRule(request: {
projectId: string,
ruleId: string,
input: {
messages: z.infer<typeof Message>[],
},
cron: string,
}) {
const user = await authCheck();
return await updateRecurringJobRuleController.execute({
caller: 'user',
userId: user.id,
projectId: request.projectId,
ruleId: request.ruleId,
input: request.input,
cron: request.cron,
});
}

View file

@ -5,6 +5,7 @@ import { ICreateScheduledJobRuleController } from "@/src/interface-adapters/cont
import { IListScheduledJobRulesController } from "@/src/interface-adapters/controllers/scheduled-job-rules/list-scheduled-job-rules.controller";
import { IFetchScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/fetch-scheduled-job-rule.controller";
import { IDeleteScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/delete-scheduled-job-rule.controller";
import { IUpdateScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/update-scheduled-job-rule.controller";
import { authCheck } from "./auth.actions";
import { z } from "zod";
import { Message } from "@/app/lib/types/types";
@ -13,6 +14,7 @@ const createScheduledJobRuleController = container.resolve<ICreateScheduledJobRu
const listScheduledJobRulesController = container.resolve<IListScheduledJobRulesController>('listScheduledJobRulesController');
const fetchScheduledJobRuleController = container.resolve<IFetchScheduledJobRuleController>('fetchScheduledJobRuleController');
const deleteScheduledJobRuleController = container.resolve<IDeleteScheduledJobRuleController>('deleteScheduledJobRuleController');
const updateScheduledJobRuleController = container.resolve<IUpdateScheduledJobRuleController>('updateScheduledJobRuleController');
export async function createScheduledJobRule(request: {
projectId: string,
@ -72,4 +74,24 @@ export async function deleteScheduledJobRule(request: {
projectId: request.projectId,
ruleId: request.ruleId,
});
}
}
export async function updateScheduledJobRule(request: {
projectId: string,
ruleId: string,
input: {
messages: z.infer<typeof Message>[],
},
scheduledTime: string,
}) {
const user = await authCheck();
return await updateScheduledJobRuleController.execute({
caller: 'user',
userId: user.id,
projectId: request.projectId,
ruleId: request.ruleId,
input: request.input,
scheduledTime: request.scheduledTime,
});
}

View file

@ -222,10 +222,12 @@ function AssistantMessage({
// Remove autoApplyEnabled and useEffect for auto-apply
const triggersRef = useRef<CopilotTriggerType[] | undefined>(triggers);
const pendingTriggerEditsRef = useRef<Map<string, CopilotTriggerType>>(new Map());
const triggerUpdateCallbackRef = useRef<typeof onTriggersUpdated>(onTriggersUpdated);
useEffect(() => {
triggersRef.current = triggers;
pendingTriggerEditsRef.current.clear();
}, [triggers]);
useEffect(() => {
@ -378,14 +380,46 @@ function AssistantMessage({
return false;
}, [dispatch, workflow.agents, workflow.tools]);
const handleTriggerAction = useCallback(async (action: any): Promise<boolean> => {
const handleTriggerAction = useCallback(async (action: any, actionIndex?: number): Promise<boolean> => {
const configType = action.config_type;
const actionType = action.action;
const triggerList = triggersRef.current ?? [];
const key = `${configType}:${action.name}`;
const hasUpcomingReplacement = () => parsed.some((part, idx) =>
idx > (actionIndex ?? -1) &&
part.type === 'action' &&
part.action.config_type === configType &&
part.action.name === action.name &&
part.action.action === 'create_new'
);
try {
if (configType === 'one_time_trigger') {
if (actionType === 'create_new') {
const pending = pendingTriggerEditsRef.current.get(key);
if (pending && pending.type === 'one_time') {
const scheduledTime = action.config_changes?.scheduledTime ?? pending.nextRunAt;
const input = action.config_changes?.input ?? pending.input;
if (!scheduledTime || !input) {
console.error('Missing data for one-time trigger update via replacement', action);
return false;
}
const { updateScheduledJobRule } = await loadScheduledJobActions();
await updateScheduledJobRule({
projectId,
ruleId: pending.id,
scheduledTime,
input,
});
pendingTriggerEditsRef.current.delete(key);
return true;
}
const { scheduledTime, input } = action.config_changes || {};
if (!scheduledTime || !input) {
console.error('Missing scheduledTime or input for one-time trigger', action);
@ -410,9 +444,20 @@ function AssistantMessage({
return false;
}
const { fetchScheduledJobRule, deleteScheduledJobRule, createScheduledJobRule } = await loadScheduledJobActions();
const {
fetchScheduledJobRule,
deleteScheduledJobRule,
updateScheduledJobRule,
} = await loadScheduledJobActions();
if (actionType === 'delete') {
if (hasUpcomingReplacement()) {
pendingTriggerEditsRef.current.set(key, target);
return true;
}
pendingTriggerEditsRef.current.delete(key);
await deleteScheduledJobRule({ projectId, ruleId: target.id });
return true;
}
@ -432,27 +477,63 @@ function AssistantMessage({
return false;
}
const created = await createScheduledJobRule({
await updateScheduledJobRule({
projectId,
ruleId: target.id,
scheduledTime,
input,
});
// Remove the previous rule only after successfully creating the updated one
await deleteScheduledJobRule({ projectId, ruleId: target.id });
return Boolean(created?.id);
return true;
}
}
if (configType === 'recurring_trigger') {
if (actionType === 'create_new') {
const pending = pendingTriggerEditsRef.current.get(key);
const {
createRecurringJobRule,
updateRecurringJobRule,
toggleRecurringJobRule,
} = await loadRecurringJobActions();
if (pending && pending.type === 'recurring') {
const cron = action.config_changes?.cron ?? pending.cron;
const input = action.config_changes?.input ?? pending.input;
if (!cron || !input) {
console.error('Missing data for recurring trigger update via replacement', action);
return false;
}
const updatedRule = await updateRecurringJobRule({
projectId,
ruleId: pending.id,
cron,
input,
});
const hasDisabledToggle = Object.prototype.hasOwnProperty.call(action.config_changes ?? {}, 'disabled');
if (hasDisabledToggle) {
const desiredDisabled = typeof action.config_changes?.disabled === 'boolean'
? action.config_changes.disabled
: pending.disabled;
if (typeof desiredDisabled === 'boolean' && desiredDisabled !== pending.disabled) {
await toggleRecurringJobRule({ ruleId: pending.id, disabled: desiredDisabled });
}
}
pendingTriggerEditsRef.current.delete(key);
return Boolean(updatedRule?.id);
}
const { cron, input } = action.config_changes || {};
if (!cron || !input) {
console.error('Missing cron or input for recurring trigger', action);
return false;
}
const { createRecurringJobRule } = await loadRecurringJobActions();
await createRecurringJobRule({
projectId,
cron,
@ -474,11 +555,18 @@ function AssistantMessage({
const {
fetchRecurringJobRule,
deleteRecurringJobRule,
createRecurringJobRule,
toggleRecurringJobRule,
updateRecurringJobRule,
} = await loadRecurringJobActions();
if (actionType === 'delete') {
if (hasUpcomingReplacement()) {
pendingTriggerEditsRef.current.set(key, target);
return true;
}
pendingTriggerEditsRef.current.delete(key);
await deleteRecurringJobRule({ projectId, ruleId: target.id });
return true;
}
@ -513,16 +601,15 @@ function AssistantMessage({
return false;
}
const created = await createRecurringJobRule({
const updatedRule = await updateRecurringJobRule({
projectId,
ruleId: target.id,
cron,
input,
});
await deleteRecurringJobRule({ projectId, ruleId: target.id });
if (desiredDisabled !== created.disabled) {
await toggleRecurringJobRule({ ruleId: created.id, disabled: desiredDisabled });
if (hasDisabledToggle && desiredDisabled !== updatedRule.disabled) {
await toggleRecurringJobRule({ ruleId: target.id, disabled: desiredDisabled });
}
return true;
@ -559,7 +646,7 @@ function AssistantMessage({
console.warn('Unhandled trigger action from Copilot applyAction', action);
return false;
}, [projectId]);
}, [projectId, parsed]);
const refreshTriggers = useCallback(async () => {
const callback = triggerUpdateCallbackRef.current;
@ -589,7 +676,7 @@ function AssistantMessage({
try {
const isTrigger = action.config_type === 'one_time_trigger' || action.config_type === 'recurring_trigger' || action.config_type === 'external_trigger';
const success = isTrigger
? await handleTriggerAction(action)
? await handleTriggerAction(action, actionIndex)
: applyAction(action);
if (success) {
@ -625,7 +712,7 @@ function AssistantMessage({
try {
const isTrigger = action.config_type === 'one_time_trigger' || action.config_type === 'recurring_trigger' || action.config_type === 'external_trigger';
const success = isTrigger
? await handleTriggerAction(action)
? await handleTriggerAction(action, actionIndex)
: applyAction(action);
if (success) {

View file

@ -1,12 +1,15 @@
'use client';
import { useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Panel } from "@/components/common/panel-common";
import { createRecurringJobRule } from "@/app/actions/recurring-job-rules.actions";
import { createRecurringJobRule, updateRecurringJobRule } from "@/app/actions/recurring-job-rules.actions";
import { ArrowLeftIcon, PlusIcon, TrashIcon, InfoIcon } from "lucide-react";
import Link from "next/link";
import { z } from "zod";
import { Message } from "@/app/lib/types/types";
import { RecurringJobRule } from "@/src/entities/models/recurring-job-rule";
// Define a simpler message type for the form that only includes the fields we need
type FormMessage = {
@ -14,6 +17,29 @@ type FormMessage = {
content: string;
};
type BackButtonConfig =
| { label: string; onClick: () => void }
| { label: string; href: string };
type FormSubmitPayload = {
messages: FormMessage[];
cron: string;
};
type RecurringJobRuleFormBaseProps = {
title: string;
description?: string;
submitLabel: string;
submittingLabel: string;
errorMessage: string;
backButton?: BackButtonConfig;
initialCron?: string;
initialMessages?: FormMessage[];
onSubmit: (payload: FormSubmitPayload) => Promise<unknown>;
onSuccess?: (result: unknown) => void;
successHref?: string;
};
const commonCronExamples = [
{ label: "Every minute", value: "* * * * *" },
{ label: "Every 5 minutes", value: "*/5 * * * *" },
@ -25,86 +51,112 @@ const commonCronExamples = [
{ label: "Monthly on the 1st at midnight", value: "0 0 1 * *" },
];
export function CreateRecurringJobRuleForm({
projectId,
onBack,
hasExistingTriggers = true
}: {
projectId: string;
onBack?: () => void;
hasExistingTriggers?: boolean;
}) {
const createEmptyMessage = (): FormMessage => ({ role: "user", content: "" });
const normaliseMessages = (messages?: FormMessage[]): FormMessage[] => {
if (!messages || messages.length === 0) {
return [createEmptyMessage()];
}
return messages.map((message) => ({ ...message }));
};
const convertFormMessagesToMessages = (messages: FormMessage[]): z.infer<typeof Message>[] => {
return messages.map((msg) => {
if (msg.role === "assistant") {
return {
role: msg.role,
content: msg.content,
agentName: null,
responseType: "internal" as const,
timestamp: undefined,
};
}
return {
role: msg.role,
content: msg.content,
timestamp: undefined,
};
});
};
function RecurringJobRuleFormBase({
title,
description,
submitLabel,
submittingLabel,
errorMessage,
backButton,
initialCron,
initialMessages,
onSubmit,
onSuccess,
successHref,
}: RecurringJobRuleFormBaseProps) {
const router = useRouter();
const [messages, setMessages] = useState<FormMessage[]>(normaliseMessages(initialMessages));
const [cronExpression, setCronExpression] = useState(initialCron ?? "* * * * *");
const [loading, setLoading] = useState(false);
const [messages, setMessages] = useState<FormMessage[]>([
{ role: "user", content: "" }
]);
const [cronExpression, setCronExpression] = useState("* * * * *");
const [showCronHelp, setShowCronHelp] = useState(false);
useEffect(() => {
setMessages(normaliseMessages(initialMessages));
}, [initialMessages]);
useEffect(() => {
setCronExpression(initialCron ?? "* * * * *");
}, [initialCron]);
const addMessage = () => {
setMessages([...messages, { role: "user", content: "" }]);
setMessages((prev) => [...prev, createEmptyMessage()]);
};
const removeMessage = (index: number) => {
if (messages.length > 1) {
setMessages(messages.filter((_, i) => i !== index));
}
setMessages((prev) => {
if (prev.length <= 1) {
return prev;
}
return prev.filter((_, i) => i !== index);
});
};
const updateMessage = (index: number, field: keyof FormMessage, value: string) => {
const newMessages = [...messages];
newMessages[index] = { ...newMessages[index], [field]: value };
setMessages(newMessages);
setMessages((prev) => {
const next = [...prev];
next[index] = { ...next[index], [field]: value };
return next;
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate required fields
if (!cronExpression.trim()) {
alert("Please enter a cron expression");
return;
}
if (messages.some(msg => !msg.content?.trim())) {
if (messages.some((msg) => !msg.content?.trim())) {
alert("Please fill in all message content");
return;
}
setLoading(true);
try {
// Convert FormMessage to the expected Message type
const convertedMessages = messages.map(msg => {
if (msg.role === "assistant") {
return {
role: msg.role,
content: msg.content,
agentName: null,
responseType: "internal" as const,
timestamp: undefined
};
}
return {
role: msg.role,
content: msg.content,
timestamp: undefined
};
});
await createRecurringJobRule({
projectId,
input: { messages: convertedMessages },
const result = await onSubmit({
cron: cronExpression,
messages,
});
if (onBack) {
onBack();
} else {
router.push(`/projects/${projectId}/manage-triggers?tab=recurring`);
if (onSuccess) {
onSuccess(result);
} else if (successHref) {
router.push(successHref);
}
} catch (error) {
console.error("Failed to create recurring job rule:", error);
alert("Failed to create recurring job rule");
console.error(errorMessage, error);
alert(errorMessage);
} finally {
setLoading(false);
}
@ -114,30 +166,39 @@ export function CreateRecurringJobRuleForm({
<Panel
title={
<div className="flex items-center gap-3">
{hasExistingTriggers && onBack ? (
<Button
variant="secondary"
size="sm"
startContent={<ArrowLeftIcon className="w-4 h-4" />}
className="whitespace-nowrap"
onClick={onBack}
>
Back
</Button>
) : hasExistingTriggers ? (
<Link href={`/projects/${projectId}/manage-triggers?tab=recurring`}>
<Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
Back
{backButton ? (
'onClick' in backButton ? (
<Button
variant="secondary"
size="sm"
startContent={<ArrowLeftIcon className="w-4 h-4" />}
className="whitespace-nowrap"
onClick={backButton.onClick}
>
{backButton.label}
</Button>
</Link>
) : (
<Link href={backButton.href}>
<Button
variant="secondary"
size="sm"
startContent={<ArrowLeftIcon className="w-4 h-4" />}
className="whitespace-nowrap"
>
{backButton.label}
</Button>
</Link>
)
) : null}
<div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
CREATE RECURRING JOB RULE
{title}
</div>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Note: Triggers run only on the published version of your workflow. Publish any changes to make them active.
</p>
{description ? (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{description}
</p>
) : null}
</div>
</div>
}
@ -262,7 +323,7 @@ export function CreateRecurringJobRuleForm({
isLoading={loading}
className="px-6 py-2 whitespace-nowrap"
>
{loading ? "Creating..." : "Create Rule"}
{loading ? submittingLabel : submitLabel}
</Button>
</div>
</form>
@ -271,3 +332,99 @@ export function CreateRecurringJobRuleForm({
</Panel>
);
}
export function CreateRecurringJobRuleForm({
projectId,
onBack,
hasExistingTriggers = true,
}: {
projectId: string;
onBack?: () => void;
hasExistingTriggers?: boolean;
}) {
const handleSubmit = async ({ cron, messages }: FormSubmitPayload) => {
const convertedMessages = convertFormMessagesToMessages(messages);
await createRecurringJobRule({
projectId,
input: { messages: convertedMessages },
cron,
});
};
const handleSuccess = onBack ? () => onBack() : undefined;
const backButton: BackButtonConfig | undefined = hasExistingTriggers
? onBack
? { label: "Back", onClick: onBack }
: { label: "Back", href: `/projects/${projectId}/manage-triggers?tab=recurring` }
: undefined;
return (
<RecurringJobRuleFormBase
title="CREATE RECURRING JOB RULE"
description="Note: Triggers run only on the published version of your workflow. Publish any changes to make them active."
submitLabel="Create Rule"
submittingLabel="Creating..."
errorMessage="Failed to create recurring job rule"
backButton={backButton}
onSubmit={handleSubmit}
onSuccess={handleSuccess}
successHref={onBack ? undefined : `/projects/${projectId}/manage-triggers?tab=recurring`}
/>
);
}
export function EditRecurringJobRuleForm({
projectId,
rule,
onCancel,
onUpdated,
}: {
projectId: string;
rule: z.infer<typeof RecurringJobRule>;
onCancel: () => void;
onUpdated?: (rule: z.infer<typeof RecurringJobRule>) => void;
}) {
const initialMessages = useMemo<FormMessage[]>(() => {
return rule.input.messages
.filter((message): message is Extract<z.infer<typeof Message>, { role: "system" | "user" | "assistant" }> => {
return message.role === "system" || message.role === "user" || message.role === "assistant";
})
.map((message) => ({
role: message.role,
content: message.content ?? "",
}));
}, [rule.input.messages]);
const handleSubmit = async ({ cron, messages }: FormSubmitPayload) => {
const convertedMessages = convertFormMessagesToMessages(messages);
const updatedRule = await updateRecurringJobRule({
projectId,
ruleId: rule.id,
input: { messages: convertedMessages },
cron,
});
return updatedRule;
};
const handleSuccess = (result: unknown) => {
if (result && typeof result === 'object' && onUpdated) {
onUpdated(result as z.infer<typeof RecurringJobRule>);
}
onCancel();
};
return (
<RecurringJobRuleFormBase
title="EDIT RECURRING JOB RULE"
description="Update the cron schedule and prompt messages for this trigger."
submitLabel="Save Changes"
submittingLabel="Saving..."
errorMessage="Failed to update recurring job rule"
backButton={{ label: "Cancel", onClick: onCancel }}
initialCron={rule.cron}
initialMessages={initialMessages}
onSubmit={handleSubmit}
onSuccess={handleSuccess}
/>
);
}

View file

@ -5,12 +5,13 @@ import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Panel } from "@/components/common/panel-common";
import { fetchRecurringJobRule, toggleRecurringJobRule, deleteRecurringJobRule } from "@/app/actions/recurring-job-rules.actions";
import { ArrowLeftIcon, PlayIcon, PauseIcon, ClockIcon, AlertCircleIcon, Trash2Icon } from "lucide-react";
import { ArrowLeftIcon, PlayIcon, PauseIcon, ClockIcon, AlertCircleIcon, Trash2Icon, PencilIcon } from "lucide-react";
import Link from "next/link";
import { RecurringJobRule } from "@/src/entities/models/recurring-job-rule";
import { Spinner } from "@heroui/react";
import { z } from "zod";
import { JobsList } from "@/app/projects/[projectId]/jobs/components/jobs-list";
import { EditRecurringJobRuleForm } from "./create-recurring-job-rule-form";
export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string; ruleId: string }) {
const router = useRouter();
@ -19,6 +20,7 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
const [updating, setUpdating] = useState(false);
const [deleting, setDeleting] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [editing, setEditing] = useState(false);
const jobsFilters = useMemo(() => ({ recurringJobRuleId: ruleId }), [ruleId]);
@ -145,128 +147,161 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
}
rightActions={
<div className="flex items-center gap-3">
<Button
onClick={handleToggleStatus}
disabled={updating}
variant={rule.disabled ? "primary" : "secondary"}
size="sm"
isLoading={updating}
startContent={rule.disabled ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
className="whitespace-nowrap"
>
{rule.disabled ? 'Activate' : 'Pause'}
</Button>
<Button
onClick={() => setShowDeleteConfirm(true)}
variant="secondary"
size="sm"
startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
>
Delete
</Button>
{editing ? (
<Button
onClick={() => setEditing(false)}
variant="secondary"
size="sm"
className="whitespace-nowrap"
>
Cancel Edit
</Button>
) : (
<>
<Button
onClick={() => setEditing(true)}
variant="secondary"
size="sm"
startContent={<PencilIcon className="w-4 h-4" />}
className="whitespace-nowrap"
>
Edit
</Button>
<Button
onClick={handleToggleStatus}
disabled={updating}
variant={rule.disabled ? "primary" : "secondary"}
size="sm"
isLoading={updating}
startContent={rule.disabled ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
className="whitespace-nowrap"
>
{rule.disabled ? 'Activate' : 'Pause'}
</Button>
<Button
onClick={() => setShowDeleteConfirm(true)}
variant="secondary"
size="sm"
startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
>
Delete
</Button>
</>
)}
</div>
}
>
<div className="h-full overflow-auto px-4 py-4">
<div className="max-w-[800px] mx-auto space-y-6">
{/* Status */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="flex items-center gap-2 mb-2">
<div className={`w-3 h-3 rounded-full ${rule.disabled ? 'bg-red-500' : 'bg-green-500'}`} />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
Status: {rule.disabled ? 'Disabled' : 'Active'}
</span>
</div>
{rule.lastError && (
<div className="flex items-start gap-2 mt-2 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded">
<AlertCircleIcon className="w-4 h-4 text-red-500 mt-0.5 flex-shrink-0" />
<div className="text-sm text-red-700 dark:text-red-300">
<strong>Last Error:</strong> {rule.lastError}
</div>
</div>
)}
</div>
{/* Schedule Information */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">
Schedule Information
</h3>
<div className="space-y-3">
<div className="flex items-center gap-2">
<ClockIcon className="w-4 h-4 text-gray-500" />
<span className="text-sm text-gray-600 dark:text-gray-400">Cron Expression:</span>
<code className="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm font-mono">
{rule.cron}
</code>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Human Readable:</strong> {formatCronExpression(rule.cron)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Next Run:</strong> {formatDate(rule.nextRunAt)}
</div>
{rule.lastProcessedAt && (
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Last Processed:</strong> {formatDate(rule.lastProcessedAt)}
</div>
)}
</div>
</div>
{/* Messages */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">
Messages
</h3>
<div className="space-y-3">
{rule.input.messages.map((message, index) => (
<div key={index} className="border border-gray-200 dark:border-gray-600 rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${
message.role === 'system'
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
: message.role === 'user'
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
}`}>
{message.role.charAt(0).toUpperCase() + message.role.slice(1)}
</span>
</div>
<div className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
{message.content}
</div>
</div>
))}
</div>
</div>
{/* Metadata */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">
Metadata
</h3>
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<div><strong>Created:</strong> {formatDate(rule.createdAt)}</div>
{rule.updatedAt && (
<div><strong>Last Updated:</strong> {formatDate(rule.updatedAt)}</div>
)}
<div><strong>Rule ID:</strong> <code className="bg-gray-100 dark:bg-gray-700 px-1 rounded">{rule.id}</code></div>
</div>
</div>
{/* Jobs Created by This Rule */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">
Jobs Created by This Rule
</h3>
<JobsList
projectId={projectId}
filters={jobsFilters}
showTitle={false}
{editing ? (
<EditRecurringJobRuleForm
projectId={projectId}
rule={rule}
onCancel={() => setEditing(false)}
onUpdated={(updatedRule) => setRule(updatedRule)}
/>
</div>
) : (
<>
{/* Status */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="flex items-center gap-2 mb-2">
<div className={`w-3 h-3 rounded-full ${rule.disabled ? 'bg-red-500' : 'bg-green-500'}`} />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
Status: {rule.disabled ? 'Disabled' : 'Active'}
</span>
</div>
{rule.lastError && (
<div className="flex items-start gap-2 mt-2 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded">
<AlertCircleIcon className="w-4 h-4 text-red-500 mt-0.5 flex-shrink-0" />
<div className="text-sm text-red-700 dark:text-red-300">
<strong>Last Error:</strong> {rule.lastError}
</div>
</div>
)}
</div>
{/* Schedule Information */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">
Schedule Information
</h3>
<div className="space-y-3">
<div className="flex items-center gap-2">
<ClockIcon className="w-4 h-4 text-gray-500" />
<span className="text-sm text-gray-600 dark:text-gray-400">Cron Expression:</span>
<code className="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm font-mono">
{rule.cron}
</code>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Human Readable:</strong> {formatCronExpression(rule.cron)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Next Run:</strong> {formatDate(rule.nextRunAt)}
</div>
{rule.lastProcessedAt && (
<div className="text-sm text-gray-600 dark:text-gray-400">
<strong>Last Processed:</strong> {formatDate(rule.lastProcessedAt)}
</div>
)}
</div>
</div>
{/* Messages */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">
Messages
</h3>
<div className="space-y-3">
{rule.input.messages.map((message, index) => (
<div key={index} className="border border-gray-200 dark:border-gray-600 rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${
message.role === 'system'
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
: message.role === 'user'
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
}`}>
{message.role.charAt(0).toUpperCase() + message.role.slice(1)}
</span>
</div>
<div className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
{message.content}
</div>
</div>
))}
</div>
</div>
{/* Metadata */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">
Metadata
</h3>
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<div><strong>Created:</strong> {formatDate(rule.createdAt)}</div>
{rule.updatedAt && (
<div><strong>Last Updated:</strong> {formatDate(rule.updatedAt)}</div>
)}
<div><strong>Rule ID:</strong> <code className="bg-gray-100 dark:bg-gray-700 px-1 rounded">{rule.id}</code></div>
</div>
</div>
{/* Jobs Created by This Rule */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">
Jobs Created by This Rule
</h3>
<JobsList
projectId={projectId}
filters={jobsFilters}
showTitle={false}
/>
</div>
</>
)}
</div>
</div>
</Panel>

View file

@ -1,132 +1,197 @@
'use client';
import { useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Panel } from "@/components/common/panel-common";
import { createScheduledJobRule } from "@/app/actions/scheduled-job-rules.actions";
import { createScheduledJobRule, updateScheduledJobRule } from "@/app/actions/scheduled-job-rules.actions";
import { ArrowLeftIcon, PlusIcon, TrashIcon } from "lucide-react";
import Link from "next/link";
import { DatePicker } from "@heroui/react";
import { ZonedDateTime, now, getLocalTimeZone } from "@internationalized/date";
import { ZonedDateTime, now, getLocalTimeZone, parseAbsoluteToLocal } from "@internationalized/date";
import { z } from "zod";
import { Message } from "@/app/lib/types/types";
import { ScheduledJobRule } from "@/src/entities/models/scheduled-job-rule";
// Define a simpler message type for the form that only includes the fields we need
type FormMessage = {
role: "system" | "user" | "assistant";
content: string;
};
export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTriggers = true }: { projectId: string; onBack?: () => void; hasExistingTriggers?: boolean }) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [messages, setMessages] = useState<FormMessage[]>([
{ role: "user", content: "" }
]);
// Set default to 30 minutes from now with timezone info
const getDefaultDateTime = () => {
const localTimeZone = getLocalTimeZone();
const currentTime = now(localTimeZone);
const thirtyMinutesFromNow = currentTime.add({ minutes: 30 });
return thirtyMinutesFromNow;
};
type BackButtonConfig =
| { label: string; onClick: () => void }
| { label: string; href: string };
const [scheduledDateTime, setScheduledDateTime] = useState<ZonedDateTime | null>(getDefaultDateTime());
type FormSubmitPayload = {
messages: FormMessage[];
scheduledDateTime: ZonedDateTime;
};
type ScheduledJobRuleFormBaseProps = {
title: string;
description?: string;
submitLabel: string;
submittingLabel: string;
errorMessage: string;
backButton?: BackButtonConfig;
initialMessages?: FormMessage[];
initialDateTime?: ZonedDateTime | null;
placeholderDateTime: ZonedDateTime;
minDateTime: ZonedDateTime;
onSubmit: (payload: FormSubmitPayload) => Promise<unknown>;
onSuccess?: (result: unknown) => void;
successHref?: string;
};
const createEmptyMessage = (): FormMessage => ({ role: "user", content: "" });
const normaliseMessages = (messages?: FormMessage[]): FormMessage[] => {
if (!messages || messages.length === 0) {
return [createEmptyMessage()];
}
return messages.map((message) => ({ ...message }));
};
const convertFormMessagesToMessages = (messages: FormMessage[]): z.infer<typeof Message>[] => {
return messages.map((msg) => {
if (msg.role === "assistant") {
return {
role: msg.role,
content: msg.content,
agentName: null,
responseType: "internal" as const,
timestamp: undefined,
};
}
return {
role: msg.role,
content: msg.content,
timestamp: undefined,
};
});
};
function ScheduledJobRuleFormBase({
title,
description,
submitLabel,
submittingLabel,
errorMessage,
backButton,
initialMessages,
initialDateTime,
placeholderDateTime,
minDateTime,
onSubmit,
onSuccess,
successHref,
}: ScheduledJobRuleFormBaseProps) {
const router = useRouter();
const [messages, setMessages] = useState<FormMessage[]>(normaliseMessages(initialMessages));
const [scheduledDateTime, setScheduledDateTime] = useState<ZonedDateTime | null>(initialDateTime ?? placeholderDateTime);
const [loading, setLoading] = useState(false);
useEffect(() => {
setMessages(normaliseMessages(initialMessages));
}, [initialMessages]);
useEffect(() => {
setScheduledDateTime(initialDateTime ?? placeholderDateTime);
}, [initialDateTime, placeholderDateTime]);
const addMessage = () => {
setMessages([...messages, { role: "user", content: "" }]);
setMessages((prev) => [...prev, createEmptyMessage()]);
};
const removeMessage = (index: number) => {
if (messages.length > 1) {
setMessages(messages.filter((_, i) => i !== index));
}
setMessages((prev) => {
if (prev.length <= 1) {
return prev;
}
return prev.filter((_, i) => i !== index);
});
};
const updateMessage = (index: number, field: keyof FormMessage, value: string) => {
const newMessages = [...messages];
newMessages[index] = { ...newMessages[index], [field]: value };
setMessages(newMessages);
setMessages((prev) => {
const next = [...prev];
next[index] = { ...next[index], [field]: value };
return next;
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate required fields
if (!scheduledDateTime) {
alert("Please select date and time");
return;
}
if (messages.some(msg => !msg.content?.trim())) {
if (messages.some((msg) => !msg.content?.trim())) {
alert("Please fill in all message content");
return;
}
setLoading(true);
try {
// Convert FormMessage to the expected Message type
const convertedMessages = messages.map(msg => {
if (msg.role === "assistant") {
return {
role: msg.role,
content: msg.content,
agentName: null,
responseType: "internal" as const,
timestamp: undefined
};
}
return {
role: msg.role,
content: msg.content,
timestamp: undefined
};
const result = await onSubmit({
messages,
scheduledDateTime,
});
// Convert ZonedDateTime to ISO string (already in UTC)
const scheduledTimeString = scheduledDateTime.toDate().toISOString();
await createScheduledJobRule({
projectId,
input: { messages: convertedMessages },
scheduledTime: scheduledTimeString,
});
if (onBack) {
onBack();
} else {
router.push(`/projects/${projectId}/manage-triggers?tab=scheduled`);
if (onSuccess) {
onSuccess(result);
} else if (successHref) {
router.push(successHref);
}
} catch (error) {
console.error("Failed to create scheduled job rule:", error);
alert("Failed to create scheduled job rule");
console.error(errorMessage, error);
alert(errorMessage);
} finally {
setLoading(false);
}
};
return (
<Panel
title={
<div className="flex items-center gap-3">
{hasExistingTriggers && onBack ? (
<Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap" onClick={onBack}>
Back
</Button>
) : hasExistingTriggers ? (
<Link href={`/projects/${projectId}/manage-triggers?tab=scheduled`}>
<Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
Back
{backButton ? (
'onClick' in backButton ? (
<Button
variant="secondary"
size="sm"
startContent={<ArrowLeftIcon className="w-4 h-4" />}
className="whitespace-nowrap"
onClick={backButton.onClick}
>
{backButton.label}
</Button>
</Link>
) : (
<Link href={backButton.href}>
<Button
variant="secondary"
size="sm"
startContent={<ArrowLeftIcon className="w-4 h-4" />}
className="whitespace-nowrap"
>
{backButton.label}
</Button>
</Link>
)
) : null}
<div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
CREATE SCHEDULED JOB RULE
{title}
</div>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Note: Triggers run only on the published version of your workflow. Publish any changes to make them active.
</p>
{description ? (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{description}
</p>
) : null}
</div>
</div>
}
@ -142,8 +207,8 @@ export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTrigg
<DatePicker
value={scheduledDateTime}
onChange={setScheduledDateTime}
placeholderValue={getDefaultDateTime()}
minValue={now(getLocalTimeZone())}
placeholderValue={placeholderDateTime}
minValue={minDateTime}
granularity="minute"
isRequired
className="w-full"
@ -214,7 +279,7 @@ export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTrigg
isLoading={loading}
className="px-6 py-2 whitespace-nowrap"
>
{loading ? "Creating..." : "Create Rule"}
{loading ? submittingLabel : submitLabel}
</Button>
</div>
</form>
@ -223,3 +288,111 @@ export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTrigg
</Panel>
);
}
export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTriggers = true }: { projectId: string; onBack?: () => void; hasExistingTriggers?: boolean }) {
const timeZone = useMemo(() => getLocalTimeZone(), []);
const minDateTime = useMemo(() => now(timeZone), [timeZone]);
const defaultDateTime = useMemo(() => now(timeZone).add({ minutes: 30 }), [timeZone]);
const handleSubmit = async ({ messages, scheduledDateTime }: FormSubmitPayload) => {
const convertedMessages = convertFormMessagesToMessages(messages);
const scheduledTimeString = scheduledDateTime.toDate().toISOString();
await createScheduledJobRule({
projectId,
input: { messages: convertedMessages },
scheduledTime: scheduledTimeString,
});
};
const handleSuccess = onBack ? () => onBack() : undefined;
const backButton: BackButtonConfig | undefined = hasExistingTriggers
? onBack
? { label: "Back", onClick: onBack }
: { label: "Back", href: `/projects/${projectId}/manage-triggers?tab=scheduled` }
: undefined;
return (
<ScheduledJobRuleFormBase
title="CREATE SCHEDULED JOB RULE"
description="Note: Triggers run only on the published version of your workflow. Publish any changes to make them active."
submitLabel="Create Rule"
submittingLabel="Creating..."
errorMessage="Failed to create scheduled job rule"
backButton={backButton}
initialDateTime={defaultDateTime}
placeholderDateTime={defaultDateTime}
minDateTime={minDateTime}
onSubmit={handleSubmit}
onSuccess={handleSuccess}
successHref={onBack ? undefined : `/projects/${projectId}/manage-triggers?tab=scheduled`}
/>
);
}
export function EditScheduledJobRuleForm({
projectId,
rule,
onCancel,
onUpdated,
}: {
projectId: string;
rule: z.infer<typeof ScheduledJobRule>;
onCancel: () => void;
onUpdated?: (rule: z.infer<typeof ScheduledJobRule>) => void;
}) {
const timeZone = useMemo(() => getLocalTimeZone(), []);
const initialDateTime = useMemo(() => parseAbsoluteToLocal(rule.nextRunAt), [rule.nextRunAt]);
const nowDateTime = useMemo(() => now(timeZone), [timeZone]);
const minDateTime = useMemo(() => {
return initialDateTime.compare(nowDateTime) < 0 ? initialDateTime : nowDateTime;
}, [initialDateTime, nowDateTime]);
const initialMessages = useMemo<FormMessage[]>(() => {
return rule.input.messages
.filter((message): message is Extract<z.infer<typeof Message>, { role: "system" | "user" | "assistant" }> => {
return message.role === "system" || message.role === "user" || message.role === "assistant";
})
.map((message) => ({
role: message.role,
content: message.content ?? "",
}));
}, [rule.input.messages]);
const handleSubmit = async ({ messages, scheduledDateTime }: FormSubmitPayload) => {
const convertedMessages = convertFormMessagesToMessages(messages);
const scheduledTimeString = scheduledDateTime.toDate().toISOString();
const updatedRule = await updateScheduledJobRule({
projectId,
ruleId: rule.id,
input: { messages: convertedMessages },
scheduledTime: scheduledTimeString,
});
return updatedRule;
};
const handleSuccess = (result: unknown) => {
if (result && typeof result === 'object' && onUpdated) {
onUpdated(result as z.infer<typeof ScheduledJobRule>);
}
onCancel();
};
return (
<ScheduledJobRuleFormBase
title="EDIT SCHEDULED JOB RULE"
description="Update the scheduled run time and prompt messages for this trigger."
submitLabel="Save Changes"
submittingLabel="Saving..."
errorMessage="Failed to update scheduled job rule"
backButton={{ label: "Cancel", onClick: onCancel }}
initialMessages={initialMessages}
initialDateTime={initialDateTime}
placeholderDateTime={initialDateTime}
minDateTime={minDateTime}
onSubmit={handleSubmit}
onSuccess={handleSuccess}
/>
);
}

View file

@ -9,8 +9,9 @@ import { ScheduledJobRule } from "@/src/entities/models/scheduled-job-rule";
import { z } from "zod";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { ArrowLeftIcon, Trash2Icon } from "lucide-react";
import { ArrowLeftIcon, Trash2Icon, PencilIcon } from "lucide-react";
import { MessageDisplay } from "@/app/lib/components/message-display";
import { EditScheduledJobRuleForm } from "./create-scheduled-job-rule-form";
export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string; ruleId: string; }) {
const router = useRouter();
@ -18,6 +19,7 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
const [loading, setLoading] = useState<boolean>(true);
const [deleting, setDeleting] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [editing, setEditing] = useState(false);
useEffect(() => {
let ignore = false;
@ -92,15 +94,37 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
}
rightActions={
<div className="flex items-center gap-3">
<Button
onClick={() => setShowDeleteConfirm(true)}
variant="secondary"
size="sm"
startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
>
Delete
</Button>
{editing ? (
<Button
onClick={() => setEditing(false)}
variant="secondary"
size="sm"
className="whitespace-nowrap"
>
Cancel Edit
</Button>
) : (
<>
<Button
onClick={() => setEditing(true)}
variant="secondary"
size="sm"
startContent={<PencilIcon className="w-4 h-4" />}
className="whitespace-nowrap"
>
Edit
</Button>
<Button
onClick={() => setShowDeleteConfirm(true)}
variant="secondary"
size="sm"
startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
>
Delete
</Button>
</>
)}
</div>
}
>
@ -114,74 +138,85 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
)}
{!loading && rule && (
<div className="flex flex-col gap-6">
{/* Rule Metadata */}
<div className="bg-gray-50 dark:bg-gray-800/50 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Rule ID:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{rule.id}</span>
</div>
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Status:</span>
<span className={`ml-2 font-mono ${getStatusColor(rule.status, rule.processedAt || null)}`}>
{getStatusText(rule.status, rule.processedAt || null)}
</span>
</div>
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Next Run:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
{formatDateTime(rule.nextRunAt)}
</span>
</div>
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Created:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
{formatDateTime(rule.createdAt)}
</span>
</div>
{rule.processedAt && (
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Processed:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
{formatDateTime(rule.processedAt)}
</span>
{editing ? (
<EditScheduledJobRuleForm
projectId={projectId}
rule={rule}
onCancel={() => setEditing(false)}
onUpdated={(updatedRule) => setRule(updatedRule)}
/>
) : (
<>
{/* Rule Metadata */}
<div className="bg-gray-50 dark:bg-gray-800/50 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Rule ID:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{rule.id}</span>
</div>
)}
{rule.output?.jobId && (
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Job ID:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
<Link
href={`/projects/${projectId}/jobs/${rule.output.jobId}`}
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
{rule.output.jobId}
</Link>
</span>
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Status:</span>
<span className={`ml-2 font-mono ${getStatusColor(rule.status, rule.processedAt || null)}`}>
{getStatusText(rule.status, rule.processedAt || null)}
</span>
</div>
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Next Run:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
{formatDateTime(rule.nextRunAt)}
</span>
</div>
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Created:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
{formatDateTime(rule.createdAt)}
</span>
</div>
{rule.processedAt && (
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Processed:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
{formatDateTime(rule.processedAt)}
</span>
</div>
)}
{rule.output?.jobId && (
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Job ID:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">
<Link
href={`/projects/${projectId}/jobs/${rule.output.jobId}`}
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
{rule.output.jobId}
</Link>
</span>
</div>
)}
{rule.workerId && (
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Worker ID:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{rule.workerId}</span>
</div>
)}
</div>
)}
{rule.workerId && (
<div>
<span className="font-semibold text-gray-700 dark:text-gray-300">Worker ID:</span>
<span className="ml-2 font-mono text-gray-600 dark:text-gray-400">{rule.workerId}</span>
</div>
)}
</div>
</div>
</div>
{/* Messages */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Messages
</h3>
<div className="space-y-4">
{rule.input.messages.map((message, index) => (
<div key={index} className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<MessageDisplay message={message} index={index} />
{/* Messages */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Messages
</h3>
<div className="space-y-4">
{rule.input.messages.map((message, index) => (
<div key={index} className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<MessageDisplay message={message} index={index} />
</div>
))}
</div>
))}
</div>
</div>
</div>
</>
)}
</div>
)}
</div>

View file

@ -73,10 +73,12 @@ import { CreateScheduledJobRuleUseCase } from "@/src/application/use-cases/sched
import { FetchScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/fetch-scheduled-job-rule.use-case";
import { ListScheduledJobRulesUseCase } from "@/src/application/use-cases/scheduled-job-rules/list-scheduled-job-rules.use-case";
import { DeleteScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/delete-scheduled-job-rule.use-case";
import { UpdateScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/update-scheduled-job-rule.use-case";
import { CreateScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/create-scheduled-job-rule.controller";
import { FetchScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/fetch-scheduled-job-rule.controller";
import { ListScheduledJobRulesController } from "@/src/interface-adapters/controllers/scheduled-job-rules/list-scheduled-job-rules.controller";
import { DeleteScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/delete-scheduled-job-rule.controller";
import { UpdateScheduledJobRuleController } from "@/src/interface-adapters/controllers/scheduled-job-rules/update-scheduled-job-rule.controller";
// Recurring Job Rules
import { MongoDBRecurringJobRulesRepository } from "@/src/infrastructure/repositories/mongodb.recurring-job-rules.repository";
@ -85,11 +87,13 @@ import { FetchRecurringJobRuleUseCase } from "@/src/application/use-cases/recurr
import { ListRecurringJobRulesUseCase } from "@/src/application/use-cases/recurring-job-rules/list-recurring-job-rules.use-case";
import { ToggleRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/toggle-recurring-job-rule.use-case";
import { DeleteRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/delete-recurring-job-rule.use-case";
import { UpdateRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/update-recurring-job-rule.use-case";
import { CreateRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/create-recurring-job-rule.controller";
import { FetchRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/fetch-recurring-job-rule.controller";
import { ListRecurringJobRulesController } from "@/src/interface-adapters/controllers/recurring-job-rules/list-recurring-job-rules.controller";
import { ToggleRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/toggle-recurring-job-rule.controller";
import { DeleteRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/delete-recurring-job-rule.controller";
import { UpdateRecurringJobRuleController } from "@/src/interface-adapters/controllers/recurring-job-rules/update-recurring-job-rule.controller";
// API Keys
import { CreateApiKeyUseCase } from "@/src/application/use-cases/api-keys/create-api-key.use-case";
@ -238,10 +242,12 @@ container.register({
createScheduledJobRuleUseCase: asClass(CreateScheduledJobRuleUseCase).singleton(),
fetchScheduledJobRuleUseCase: asClass(FetchScheduledJobRuleUseCase).singleton(),
listScheduledJobRulesUseCase: asClass(ListScheduledJobRulesUseCase).singleton(),
updateScheduledJobRuleUseCase: asClass(UpdateScheduledJobRuleUseCase).singleton(),
deleteScheduledJobRuleUseCase: asClass(DeleteScheduledJobRuleUseCase).singleton(),
createScheduledJobRuleController: asClass(CreateScheduledJobRuleController).singleton(),
fetchScheduledJobRuleController: asClass(FetchScheduledJobRuleController).singleton(),
listScheduledJobRulesController: asClass(ListScheduledJobRulesController).singleton(),
updateScheduledJobRuleController: asClass(UpdateScheduledJobRuleController).singleton(),
deleteScheduledJobRuleController: asClass(DeleteScheduledJobRuleController).singleton(),
// recurring job rules
@ -251,11 +257,13 @@ container.register({
fetchRecurringJobRuleUseCase: asClass(FetchRecurringJobRuleUseCase).singleton(),
listRecurringJobRulesUseCase: asClass(ListRecurringJobRulesUseCase).singleton(),
toggleRecurringJobRuleUseCase: asClass(ToggleRecurringJobRuleUseCase).singleton(),
updateRecurringJobRuleUseCase: asClass(UpdateRecurringJobRuleUseCase).singleton(),
deleteRecurringJobRuleUseCase: asClass(DeleteRecurringJobRuleUseCase).singleton(),
createRecurringJobRuleController: asClass(CreateRecurringJobRuleController).singleton(),
fetchRecurringJobRuleController: asClass(FetchRecurringJobRuleController).singleton(),
listRecurringJobRulesController: asClass(ListRecurringJobRulesController).singleton(),
toggleRecurringJobRuleController: asClass(ToggleRecurringJobRuleController).singleton(),
updateRecurringJobRuleController: asClass(UpdateRecurringJobRuleController).singleton(),
deleteRecurringJobRuleController: asClass(DeleteRecurringJobRuleController).singleton(),
// projects
@ -344,4 +352,4 @@ container.register({
// users
// ---
usersRepository: asClass(MongoDBUsersRepository).singleton(),
});
});

View file

@ -0,0 +1,70 @@
const RANGE_SEPARATOR = "-";
const STEP_SEPARATOR = "/";
export function isValidCronExpression(cron: string): boolean {
const parts = cron.trim().split(/\s+/);
if (parts.length !== 5) {
return false;
}
const [minute, hour, day, month, dayOfWeek] = parts;
const validatePart = (part: string, max: number): boolean => {
if (part === "*") {
return true;
}
if (part.includes(STEP_SEPARATOR)) {
const [range, step] = part.split(STEP_SEPARATOR);
if (!step) {
return false;
}
const stepValue = Number(step);
if (!Number.isInteger(stepValue) || stepValue <= 0) {
return false;
}
if (range === "*") {
return stepValue <= max;
}
return validatePart(range, max);
}
if (part.includes(RANGE_SEPARATOR)) {
const [start, end] = part.split(RANGE_SEPARATOR);
if (start === undefined || end === undefined) {
return false;
}
const startValue = Number(start);
const endValue = Number(end);
if (!Number.isInteger(startValue) || !Number.isInteger(endValue)) {
return false;
}
if (startValue > endValue) {
return false;
}
return startValue >= 0 && endValue <= max;
}
const value = Number(part);
if (!Number.isInteger(value)) {
return false;
}
return value >= 0 && value <= max;
};
return (
validatePart(minute, 59) &&
validatePart(hour, 23) &&
validatePart(day, 31) &&
validatePart(month, 12) &&
validatePart(dayOfWeek, 7)
);
}

View file

@ -17,6 +17,15 @@ export const ListedRecurringRuleItem = RecurringJobRule.omit({
input: true,
});
/**
* Schema for updating a recurring job rule.
*/
export const UpdateRecurringRuleSchema = RecurringJobRule
.pick({
input: true,
cron: true,
});
/**
* Repository interface for managing recurring job rules in the system.
*
@ -82,6 +91,16 @@ export interface IRecurringJobRulesRepository {
*/
toggle(id: string, disabled: boolean): Promise<z.infer<typeof RecurringJobRule>>;
/**
* Updates a recurring job rule with new input and cron expression.
*
* @param id - The unique identifier of the recurring job rule to update
* @param data - The update data containing input messages and cron expression
* @returns Promise resolving to the updated recurring job rule
* @throws {NotFoundError} if the recurring job rule doesn't exist
*/
update(id: string, data: z.infer<typeof UpdateRecurringRuleSchema>): Promise<z.infer<typeof RecurringJobRule>>;
/**
* Deletes a recurring job rule by its unique identifier.
*

View file

@ -24,6 +24,17 @@ export const UpdateJobSchema = ScheduledJobRule.pick({
output: true,
});
/**
* Schema for updating a scheduled job rule's next run configuration.
*/
export const UpdateScheduledRuleSchema = ScheduledJobRule
.pick({
input: true,
})
.extend({
scheduledTime: z.string().datetime(),
});
/**
* Repository interface for managing scheduled job rules in the system.
*
@ -69,6 +80,16 @@ export interface IScheduledJobRulesRepository {
*/
update(id: string, data: z.infer<typeof UpdateJobSchema>): Promise<z.infer<typeof ScheduledJobRule>>;
/**
* Updates a scheduled job rule with new input and scheduled time.
*
* @param id - The unique identifier of the scheduled job rule to update
* @param data - The update data containing input messages and scheduled time
* @returns Promise resolving to the updated scheduled job rule
* @throws {NotFoundError} if the scheduled job rule doesn't exist
*/
updateRule(id: string, data: z.infer<typeof UpdateScheduledRuleSchema>): Promise<z.infer<typeof ScheduledJobRule>>;
/**
* Releases a scheduled job rule after it has been executed.
*
@ -103,4 +124,4 @@ export interface IScheduledJobRulesRepository {
* @returns Promise resolving to void
*/
deleteByProjectId(projectId: string): Promise<void>;
}
}

View file

@ -5,6 +5,7 @@ import { IProjectActionAuthorizationPolicy } from '../../policies/project-action
import { IRecurringJobRulesRepository } from '../../repositories/recurring-job-rules.repository.interface';
import { RecurringJobRule } from '@/src/entities/models/recurring-job-rule';
import { Message } from '@/app/lib/types/types';
import { isValidCronExpression } from '@/src/application/lib/utils/is-valid-cron-expression';
const inputSchema = z.object({
caller: z.enum(["user", "api"]),
@ -42,7 +43,7 @@ export class CreateRecurringJobRuleUseCase implements ICreateRecurringJobRuleUse
async execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof RecurringJobRule>> {
// Validate cron expression
if (!this.isValidCronExpression(request.cron)) {
if (!isValidCronExpression(request.cron)) {
throw new BadRequestError('Invalid cron expression. Expected format: minute hour day month dayOfWeek');
}
@ -66,31 +67,4 @@ export class CreateRecurringJobRuleUseCase implements ICreateRecurringJobRuleUse
return rule;
}
private isValidCronExpression(cron: string): boolean {
const parts = cron.split(' ');
if (parts.length !== 5) {
return false;
}
// Basic validation - in production you'd want more sophisticated validation
const [minute, hour, day, month, dayOfWeek] = parts;
// Check if parts are valid
const isValidPart = (part: string) => {
if (part === '*') return true;
if (part.includes('/')) {
const [range, step] = part.split('/');
if (range === '*' || (parseInt(step) > 0 && parseInt(step) <= 59)) return true;
return false;
}
if (part.includes('-')) {
const [start, end] = part.split('-');
return !isNaN(parseInt(start)) && !isNaN(parseInt(end)) && parseInt(start) <= parseInt(end);
}
return !isNaN(parseInt(part));
};
return isValidPart(minute) && isValidPart(hour) && isValidPart(day) && isValidPart(month) && isValidPart(dayOfWeek);
}
}

View file

@ -0,0 +1,69 @@
import { BadRequestError, NotFoundError } from '@/src/entities/errors/common';
import { z } from "zod";
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
import { IRecurringJobRulesRepository } from '../../repositories/recurring-job-rules.repository.interface';
import { RecurringJobRule } from '@/src/entities/models/recurring-job-rule';
import { Message } from '@/app/lib/types/types';
import { isValidCronExpression } from '@/src/application/lib/utils/is-valid-cron-expression';
const inputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
ruleId: z.string(),
input: z.object({
messages: z.array(Message),
}),
cron: z.string(),
});
export interface IUpdateRecurringJobRuleUseCase {
execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof RecurringJobRule>>;
}
export class UpdateRecurringJobRuleUseCase implements IUpdateRecurringJobRuleUseCase {
private readonly recurringJobRulesRepository: IRecurringJobRulesRepository;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
constructor({
recurringJobRulesRepository,
usageQuotaPolicy,
projectActionAuthorizationPolicy,
}: {
recurringJobRulesRepository: IRecurringJobRulesRepository,
usageQuotaPolicy: IUsageQuotaPolicy,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
}) {
this.recurringJobRulesRepository = recurringJobRulesRepository;
this.usageQuotaPolicy = usageQuotaPolicy;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
}
async execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof RecurringJobRule>> {
if (!isValidCronExpression(request.cron)) {
throw new BadRequestError('Invalid cron expression. Expected format: minute hour day month dayOfWeek');
}
await this.projectActionAuthorizationPolicy.authorize({
caller: request.caller,
userId: request.userId,
apiKey: request.apiKey,
projectId: request.projectId,
});
await this.usageQuotaPolicy.assertAndConsumeProjectAction(request.projectId);
const rule = await this.recurringJobRulesRepository.fetch(request.ruleId);
if (!rule || rule.projectId !== request.projectId) {
throw new NotFoundError('Recurring job rule not found');
}
return await this.recurringJobRulesRepository.update(request.ruleId, {
input: request.input,
cron: request.cron,
});
}
}

View file

@ -0,0 +1,64 @@
import { NotFoundError } from '@/src/entities/errors/common';
import { z } from "zod";
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
import { IScheduledJobRulesRepository } from '../../repositories/scheduled-job-rules.repository.interface';
import { ScheduledJobRule } from '@/src/entities/models/scheduled-job-rule';
import { Message } from '@/app/lib/types/types';
const inputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
ruleId: z.string(),
input: z.object({
messages: z.array(Message),
}),
scheduledTime: z.string().datetime(),
});
export interface IUpdateScheduledJobRuleUseCase {
execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof ScheduledJobRule>>;
}
export class UpdateScheduledJobRuleUseCase implements IUpdateScheduledJobRuleUseCase {
private readonly scheduledJobRulesRepository: IScheduledJobRulesRepository;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
constructor({
scheduledJobRulesRepository,
usageQuotaPolicy,
projectActionAuthorizationPolicy,
}: {
scheduledJobRulesRepository: IScheduledJobRulesRepository,
usageQuotaPolicy: IUsageQuotaPolicy,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
}) {
this.scheduledJobRulesRepository = scheduledJobRulesRepository;
this.usageQuotaPolicy = usageQuotaPolicy;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
}
async execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof ScheduledJobRule>> {
await this.projectActionAuthorizationPolicy.authorize({
caller: request.caller,
userId: request.userId,
apiKey: request.apiKey,
projectId: request.projectId,
});
await this.usageQuotaPolicy.assertAndConsumeProjectAction(request.projectId);
const rule = await this.scheduledJobRulesRepository.fetch(request.ruleId);
if (!rule || rule.projectId !== request.projectId) {
throw new NotFoundError('Scheduled job rule not found');
}
return await this.scheduledJobRulesRepository.updateRule(request.ruleId, {
input: request.input,
scheduledTime: request.scheduledTime,
});
}
}

View file

@ -1,7 +1,7 @@
import { z } from "zod";
import { Filter, ObjectId } from "mongodb";
import { db } from "@/app/lib/mongodb";
import { CreateRecurringRuleSchema, IRecurringJobRulesRepository, ListedRecurringRuleItem } from "@/src/application/repositories/recurring-job-rules.repository.interface";
import { CreateRecurringRuleSchema, IRecurringJobRulesRepository, ListedRecurringRuleItem, UpdateRecurringRuleSchema } from "@/src/application/repositories/recurring-job-rules.repository.interface";
import { RecurringJobRule } from "@/src/entities/models/recurring-job-rule";
import { NotFoundError } from "@/src/entities/errors/common";
import { PaginatedList } from "@/src/entities/common/paginated-list";
@ -208,6 +208,31 @@ export class MongoDBRecurringJobRulesRepository implements IRecurringJobRulesRep
return await this.updateNextRunAt(id, result.cron);
}
/**
* Updates a recurring job rule with new input and schedule.
*/
async update(id: string, data: z.infer<typeof UpdateRecurringRuleSchema>): Promise<z.infer<typeof RecurringJobRule>> {
const now = new Date().toISOString();
const result = await this.collection.findOneAndUpdate(
{ _id: new ObjectId(id) },
{
$set: {
input: data.input,
cron: data.cron,
updatedAt: now,
},
},
{ returnDocument: "after" },
);
if (!result) {
throw new NotFoundError(`Recurring job rule ${id} not found`);
}
return await this.updateNextRunAt(id, data.cron);
}
/**
* Deletes a recurring job rule by its unique identifier.
*/

View file

@ -1,7 +1,7 @@
import { z } from "zod";
import { Filter, ObjectId } from "mongodb";
import { db } from "@/app/lib/mongodb";
import { CreateRuleSchema, IScheduledJobRulesRepository, ListedRuleItem, UpdateJobSchema } from "@/src/application/repositories/scheduled-job-rules.repository.interface";
import { CreateRuleSchema, IScheduledJobRulesRepository, ListedRuleItem, UpdateJobSchema, UpdateScheduledRuleSchema } from "@/src/application/repositories/scheduled-job-rules.repository.interface";
import { ScheduledJobRule } from "@/src/entities/models/scheduled-job-rule";
import { NotFoundError } from "@/src/entities/errors/common";
import { PaginatedList } from "@/src/entities/common/paginated-list";
@ -138,6 +138,41 @@ export class MongoDBScheduledJobRulesRepository implements IScheduledJobRulesRep
return this.convertDocToModel(result);
}
/**
* Reconfigures a scheduled job rule's input and next run time.
*/
async updateRule(id: string, data: z.infer<typeof UpdateScheduledRuleSchema>): Promise<z.infer<typeof ScheduledJobRule>> {
const scheduledDate = new Date(data.scheduledTime);
const nextRunAtSeconds = Math.floor(scheduledDate.getTime() / 1000);
const nextRunAt = Math.floor(nextRunAtSeconds / 60) * 60;
const now = new Date().toISOString();
const result = await this.collection.findOneAndUpdate(
{ _id: new ObjectId(id) },
{
$set: {
input: data.input,
nextRunAt,
status: "pending",
workerId: null,
lastWorkerId: null,
updatedAt: now,
},
$unset: {
output: "",
processedAt: "",
},
},
{ returnDocument: "after" },
);
if (!result) {
throw new NotFoundError(`Scheduled job rule ${id} not found`);
}
return this.convertDocToModel(result);
}
/**
* Updates a scheduled job rule with new status and output data.
*/

View file

@ -0,0 +1,50 @@
import { BadRequestError } from "@/src/entities/errors/common";
import z from "zod";
import { IUpdateRecurringJobRuleUseCase } from "@/src/application/use-cases/recurring-job-rules/update-recurring-job-rule.use-case";
import { RecurringJobRule } from "@/src/entities/models/recurring-job-rule";
const inputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
ruleId: z.string(),
input: z.object({
messages: z.array(z.any()),
}),
cron: z.string(),
});
export interface IUpdateRecurringJobRuleController {
execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof RecurringJobRule>>;
}
export class UpdateRecurringJobRuleController implements IUpdateRecurringJobRuleController {
private readonly updateRecurringJobRuleUseCase: IUpdateRecurringJobRuleUseCase;
constructor({
updateRecurringJobRuleUseCase,
}: {
updateRecurringJobRuleUseCase: IUpdateRecurringJobRuleUseCase,
}) {
this.updateRecurringJobRuleUseCase = updateRecurringJobRuleUseCase;
}
async execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof RecurringJobRule>> {
const result = inputSchema.safeParse(request);
if (!result.success) {
throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`);
}
const { caller, userId, apiKey, projectId, ruleId, input, cron } = result.data;
return await this.updateRecurringJobRuleUseCase.execute({
caller,
userId,
apiKey,
projectId,
ruleId,
input,
cron,
});
}
}

View file

@ -0,0 +1,51 @@
import { BadRequestError } from "@/src/entities/errors/common";
import z from "zod";
import { IUpdateScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/update-scheduled-job-rule.use-case";
import { ScheduledJobRule } from "@/src/entities/models/scheduled-job-rule";
import { Message } from "@/app/lib/types/types";
const inputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
ruleId: z.string(),
input: z.object({
messages: z.array(Message),
}),
scheduledTime: z.string().datetime(),
});
export interface IUpdateScheduledJobRuleController {
execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof ScheduledJobRule>>;
}
export class UpdateScheduledJobRuleController implements IUpdateScheduledJobRuleController {
private readonly updateScheduledJobRuleUseCase: IUpdateScheduledJobRuleUseCase;
constructor({
updateScheduledJobRuleUseCase,
}: {
updateScheduledJobRuleUseCase: IUpdateScheduledJobRuleUseCase,
}) {
this.updateScheduledJobRuleUseCase = updateScheduledJobRuleUseCase;
}
async execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof ScheduledJobRule>> {
const result = inputSchema.safeParse(request);
if (!result.success) {
throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`);
}
const { caller, userId, apiKey, projectId, ruleId, input, scheduledTime } = result.data;
return await this.updateScheduledJobRuleUseCase.execute({
caller,
userId,
apiKey,
projectId,
ruleId,
input,
scheduledTime,
});
}
}