mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
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:
parent
d484170e32
commit
e2c8d0490a
18 changed files with 1307 additions and 390 deletions
|
|
@ -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 { 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 { 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 { 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 { authCheck } from "./auth.actions";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Message } from "@/app/lib/types/types";
|
import { Message } from "@/app/lib/types/types";
|
||||||
|
|
@ -15,6 +16,7 @@ const listRecurringJobRulesController = container.resolve<IListRecurringJobRules
|
||||||
const fetchRecurringJobRuleController = container.resolve<IFetchRecurringJobRuleController>('fetchRecurringJobRuleController');
|
const fetchRecurringJobRuleController = container.resolve<IFetchRecurringJobRuleController>('fetchRecurringJobRuleController');
|
||||||
const toggleRecurringJobRuleController = container.resolve<IToggleRecurringJobRuleController>('toggleRecurringJobRuleController');
|
const toggleRecurringJobRuleController = container.resolve<IToggleRecurringJobRuleController>('toggleRecurringJobRuleController');
|
||||||
const deleteRecurringJobRuleController = container.resolve<IDeleteRecurringJobRuleController>('deleteRecurringJobRuleController');
|
const deleteRecurringJobRuleController = container.resolve<IDeleteRecurringJobRuleController>('deleteRecurringJobRuleController');
|
||||||
|
const updateRecurringJobRuleController = container.resolve<IUpdateRecurringJobRuleController>('updateRecurringJobRuleController');
|
||||||
|
|
||||||
export async function createRecurringJobRule(request: {
|
export async function createRecurringJobRule(request: {
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
|
@ -89,3 +91,23 @@ export async function deleteRecurringJobRule(request: {
|
||||||
ruleId: request.ruleId,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 { 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 { 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 { authCheck } from "./auth.actions";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Message } from "@/app/lib/types/types";
|
import { Message } from "@/app/lib/types/types";
|
||||||
|
|
@ -13,6 +14,7 @@ const createScheduledJobRuleController = container.resolve<ICreateScheduledJobRu
|
||||||
const listScheduledJobRulesController = container.resolve<IListScheduledJobRulesController>('listScheduledJobRulesController');
|
const listScheduledJobRulesController = container.resolve<IListScheduledJobRulesController>('listScheduledJobRulesController');
|
||||||
const fetchScheduledJobRuleController = container.resolve<IFetchScheduledJobRuleController>('fetchScheduledJobRuleController');
|
const fetchScheduledJobRuleController = container.resolve<IFetchScheduledJobRuleController>('fetchScheduledJobRuleController');
|
||||||
const deleteScheduledJobRuleController = container.resolve<IDeleteScheduledJobRuleController>('deleteScheduledJobRuleController');
|
const deleteScheduledJobRuleController = container.resolve<IDeleteScheduledJobRuleController>('deleteScheduledJobRuleController');
|
||||||
|
const updateScheduledJobRuleController = container.resolve<IUpdateScheduledJobRuleController>('updateScheduledJobRuleController');
|
||||||
|
|
||||||
export async function createScheduledJobRule(request: {
|
export async function createScheduledJobRule(request: {
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
|
@ -73,3 +75,23 @@ export async function deleteScheduledJobRule(request: {
|
||||||
ruleId: request.ruleId,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -222,10 +222,12 @@ function AssistantMessage({
|
||||||
// Remove autoApplyEnabled and useEffect for auto-apply
|
// Remove autoApplyEnabled and useEffect for auto-apply
|
||||||
|
|
||||||
const triggersRef = useRef<CopilotTriggerType[] | undefined>(triggers);
|
const triggersRef = useRef<CopilotTriggerType[] | undefined>(triggers);
|
||||||
|
const pendingTriggerEditsRef = useRef<Map<string, CopilotTriggerType>>(new Map());
|
||||||
const triggerUpdateCallbackRef = useRef<typeof onTriggersUpdated>(onTriggersUpdated);
|
const triggerUpdateCallbackRef = useRef<typeof onTriggersUpdated>(onTriggersUpdated);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
triggersRef.current = triggers;
|
triggersRef.current = triggers;
|
||||||
|
pendingTriggerEditsRef.current.clear();
|
||||||
}, [triggers]);
|
}, [triggers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -378,14 +380,46 @@ function AssistantMessage({
|
||||||
return false;
|
return false;
|
||||||
}, [dispatch, workflow.agents, workflow.tools]);
|
}, [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 configType = action.config_type;
|
||||||
const actionType = action.action;
|
const actionType = action.action;
|
||||||
const triggerList = triggersRef.current ?? [];
|
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 {
|
try {
|
||||||
if (configType === 'one_time_trigger') {
|
if (configType === 'one_time_trigger') {
|
||||||
if (actionType === 'create_new') {
|
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 || {};
|
const { scheduledTime, input } = action.config_changes || {};
|
||||||
if (!scheduledTime || !input) {
|
if (!scheduledTime || !input) {
|
||||||
console.error('Missing scheduledTime or input for one-time trigger', action);
|
console.error('Missing scheduledTime or input for one-time trigger', action);
|
||||||
|
|
@ -410,9 +444,20 @@ function AssistantMessage({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { fetchScheduledJobRule, deleteScheduledJobRule, createScheduledJobRule } = await loadScheduledJobActions();
|
const {
|
||||||
|
fetchScheduledJobRule,
|
||||||
|
deleteScheduledJobRule,
|
||||||
|
updateScheduledJobRule,
|
||||||
|
} = await loadScheduledJobActions();
|
||||||
|
|
||||||
if (actionType === 'delete') {
|
if (actionType === 'delete') {
|
||||||
|
if (hasUpcomingReplacement()) {
|
||||||
|
pendingTriggerEditsRef.current.set(key, target);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingTriggerEditsRef.current.delete(key);
|
||||||
|
|
||||||
await deleteScheduledJobRule({ projectId, ruleId: target.id });
|
await deleteScheduledJobRule({ projectId, ruleId: target.id });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -432,27 +477,63 @@ function AssistantMessage({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await createScheduledJobRule({
|
await updateScheduledJobRule({
|
||||||
projectId,
|
projectId,
|
||||||
|
ruleId: target.id,
|
||||||
scheduledTime,
|
scheduledTime,
|
||||||
input,
|
input,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove the previous rule only after successfully creating the updated one
|
return true;
|
||||||
await deleteScheduledJobRule({ projectId, ruleId: target.id });
|
|
||||||
|
|
||||||
return Boolean(created?.id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configType === 'recurring_trigger') {
|
if (configType === 'recurring_trigger') {
|
||||||
if (actionType === 'create_new') {
|
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 || {};
|
const { cron, input } = action.config_changes || {};
|
||||||
if (!cron || !input) {
|
if (!cron || !input) {
|
||||||
console.error('Missing cron or input for recurring trigger', action);
|
console.error('Missing cron or input for recurring trigger', action);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const { createRecurringJobRule } = await loadRecurringJobActions();
|
|
||||||
await createRecurringJobRule({
|
await createRecurringJobRule({
|
||||||
projectId,
|
projectId,
|
||||||
cron,
|
cron,
|
||||||
|
|
@ -474,11 +555,18 @@ function AssistantMessage({
|
||||||
const {
|
const {
|
||||||
fetchRecurringJobRule,
|
fetchRecurringJobRule,
|
||||||
deleteRecurringJobRule,
|
deleteRecurringJobRule,
|
||||||
createRecurringJobRule,
|
|
||||||
toggleRecurringJobRule,
|
toggleRecurringJobRule,
|
||||||
|
updateRecurringJobRule,
|
||||||
} = await loadRecurringJobActions();
|
} = await loadRecurringJobActions();
|
||||||
|
|
||||||
if (actionType === 'delete') {
|
if (actionType === 'delete') {
|
||||||
|
if (hasUpcomingReplacement()) {
|
||||||
|
pendingTriggerEditsRef.current.set(key, target);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingTriggerEditsRef.current.delete(key);
|
||||||
|
|
||||||
await deleteRecurringJobRule({ projectId, ruleId: target.id });
|
await deleteRecurringJobRule({ projectId, ruleId: target.id });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -513,16 +601,15 @@ function AssistantMessage({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await createRecurringJobRule({
|
const updatedRule = await updateRecurringJobRule({
|
||||||
projectId,
|
projectId,
|
||||||
|
ruleId: target.id,
|
||||||
cron,
|
cron,
|
||||||
input,
|
input,
|
||||||
});
|
});
|
||||||
|
|
||||||
await deleteRecurringJobRule({ projectId, ruleId: target.id });
|
if (hasDisabledToggle && desiredDisabled !== updatedRule.disabled) {
|
||||||
|
await toggleRecurringJobRule({ ruleId: target.id, disabled: desiredDisabled });
|
||||||
if (desiredDisabled !== created.disabled) {
|
|
||||||
await toggleRecurringJobRule({ ruleId: created.id, disabled: desiredDisabled });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -559,7 +646,7 @@ function AssistantMessage({
|
||||||
|
|
||||||
console.warn('Unhandled trigger action from Copilot applyAction', action);
|
console.warn('Unhandled trigger action from Copilot applyAction', action);
|
||||||
return false;
|
return false;
|
||||||
}, [projectId]);
|
}, [projectId, parsed]);
|
||||||
|
|
||||||
const refreshTriggers = useCallback(async () => {
|
const refreshTriggers = useCallback(async () => {
|
||||||
const callback = triggerUpdateCallbackRef.current;
|
const callback = triggerUpdateCallbackRef.current;
|
||||||
|
|
@ -589,7 +676,7 @@ function AssistantMessage({
|
||||||
try {
|
try {
|
||||||
const isTrigger = action.config_type === 'one_time_trigger' || action.config_type === 'recurring_trigger' || action.config_type === 'external_trigger';
|
const isTrigger = action.config_type === 'one_time_trigger' || action.config_type === 'recurring_trigger' || action.config_type === 'external_trigger';
|
||||||
const success = isTrigger
|
const success = isTrigger
|
||||||
? await handleTriggerAction(action)
|
? await handleTriggerAction(action, actionIndex)
|
||||||
: applyAction(action);
|
: applyAction(action);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|
@ -625,7 +712,7 @@ function AssistantMessage({
|
||||||
try {
|
try {
|
||||||
const isTrigger = action.config_type === 'one_time_trigger' || action.config_type === 'recurring_trigger' || action.config_type === 'external_trigger';
|
const isTrigger = action.config_type === 'one_time_trigger' || action.config_type === 'recurring_trigger' || action.config_type === 'external_trigger';
|
||||||
const success = isTrigger
|
const success = isTrigger
|
||||||
? await handleTriggerAction(action)
|
? await handleTriggerAction(action, actionIndex)
|
||||||
: applyAction(action);
|
: applyAction(action);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Panel } from "@/components/common/panel-common";
|
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 { ArrowLeftIcon, PlusIcon, TrashIcon, InfoIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
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
|
// Define a simpler message type for the form that only includes the fields we need
|
||||||
type FormMessage = {
|
type FormMessage = {
|
||||||
|
|
@ -14,6 +17,29 @@ type FormMessage = {
|
||||||
content: string;
|
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 = [
|
const commonCronExamples = [
|
||||||
{ label: "Every minute", value: "* * * * *" },
|
{ label: "Every minute", value: "* * * * *" },
|
||||||
{ label: "Every 5 minutes", value: "*/5 * * * *" },
|
{ label: "Every 5 minutes", value: "*/5 * * * *" },
|
||||||
|
|
@ -25,86 +51,112 @@ const commonCronExamples = [
|
||||||
{ label: "Monthly on the 1st at midnight", value: "0 0 1 * *" },
|
{ label: "Monthly on the 1st at midnight", value: "0 0 1 * *" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function CreateRecurringJobRuleForm({
|
const createEmptyMessage = (): FormMessage => ({ role: "user", content: "" });
|
||||||
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: "" }
|
|
||||||
]);
|
|
||||||
const [cronExpression, setCronExpression] = useState("* * * * *");
|
|
||||||
const [showCronHelp, setShowCronHelp] = useState(false);
|
|
||||||
|
|
||||||
const addMessage = () => {
|
const normaliseMessages = (messages?: FormMessage[]): FormMessage[] => {
|
||||||
setMessages([...messages, { role: "user", content: "" }]);
|
if (!messages || messages.length === 0) {
|
||||||
};
|
return [createEmptyMessage()];
|
||||||
|
|
||||||
const removeMessage = (index: number) => {
|
|
||||||
if (messages.length > 1) {
|
|
||||||
setMessages(messages.filter((_, i) => i !== index));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMessage = (index: number, field: keyof FormMessage, value: string) => {
|
|
||||||
const newMessages = [...messages];
|
|
||||||
newMessages[index] = { ...newMessages[index], [field]: value };
|
|
||||||
setMessages(newMessages);
|
|
||||||
};
|
|
||||||
|
|
||||||
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())) {
|
return messages.map((message) => ({ ...message }));
|
||||||
alert("Please fill in all message content");
|
};
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
const convertFormMessagesToMessages = (messages: FormMessage[]): z.infer<typeof Message>[] => {
|
||||||
try {
|
return messages.map((msg) => {
|
||||||
// Convert FormMessage to the expected Message type
|
|
||||||
const convertedMessages = messages.map(msg => {
|
|
||||||
if (msg.role === "assistant") {
|
if (msg.role === "assistant") {
|
||||||
return {
|
return {
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
agentName: null,
|
agentName: null,
|
||||||
responseType: "internal" as const,
|
responseType: "internal" as const,
|
||||||
timestamp: undefined
|
timestamp: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
timestamp: undefined
|
timestamp: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
await createRecurringJobRule({
|
function RecurringJobRuleFormBase({
|
||||||
projectId,
|
title,
|
||||||
input: { messages: convertedMessages },
|
description,
|
||||||
cron: cronExpression,
|
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 [showCronHelp, setShowCronHelp] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMessages(normaliseMessages(initialMessages));
|
||||||
|
}, [initialMessages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCronExpression(initialCron ?? "* * * * *");
|
||||||
|
}, [initialCron]);
|
||||||
|
|
||||||
|
const addMessage = () => {
|
||||||
|
setMessages((prev) => [...prev, createEmptyMessage()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMessage = (index: number) => {
|
||||||
|
setMessages((prev) => {
|
||||||
|
if (prev.length <= 1) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return prev.filter((_, i) => i !== index);
|
||||||
});
|
});
|
||||||
if (onBack) {
|
};
|
||||||
onBack();
|
|
||||||
} else {
|
const updateMessage = (index: number, field: keyof FormMessage, value: string) => {
|
||||||
router.push(`/projects/${projectId}/manage-triggers?tab=recurring`);
|
setMessages((prev) => {
|
||||||
|
const next = [...prev];
|
||||||
|
next[index] = { ...next[index], [field]: value };
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!cronExpression.trim()) {
|
||||||
|
alert("Please enter a cron expression");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.some((msg) => !msg.content?.trim())) {
|
||||||
|
alert("Please fill in all message content");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await onSubmit({
|
||||||
|
cron: cronExpression,
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess(result);
|
||||||
|
} else if (successHref) {
|
||||||
|
router.push(successHref);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create recurring job rule:", error);
|
console.error(errorMessage, error);
|
||||||
alert("Failed to create recurring job rule");
|
alert(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -114,30 +166,39 @@ export function CreateRecurringJobRuleForm({
|
||||||
<Panel
|
<Panel
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{hasExistingTriggers && onBack ? (
|
{backButton ? (
|
||||||
|
'onClick' in backButton ? (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
startContent={<ArrowLeftIcon className="w-4 h-4" />}
|
startContent={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
className="whitespace-nowrap"
|
className="whitespace-nowrap"
|
||||||
onClick={onBack}
|
onClick={backButton.onClick}
|
||||||
>
|
>
|
||||||
Back
|
{backButton.label}
|
||||||
</Button>
|
</Button>
|
||||||
) : hasExistingTriggers ? (
|
) : (
|
||||||
<Link href={`/projects/${projectId}/manage-triggers?tab=recurring`}>
|
<Link href={backButton.href}>
|
||||||
<Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
|
<Button
|
||||||
Back
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
startContent={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{backButton.label}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
)
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
CREATE RECURRING JOB RULE
|
{title}
|
||||||
</div>
|
</div>
|
||||||
|
{description ? (
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<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.
|
{description}
|
||||||
</p>
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +323,7 @@ export function CreateRecurringJobRuleForm({
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
className="px-6 py-2 whitespace-nowrap"
|
className="px-6 py-2 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{loading ? "Creating..." : "Create Rule"}
|
{loading ? submittingLabel : submitLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -271,3 +332,99 @@ export function CreateRecurringJobRuleForm({
|
||||||
</Panel>
|
</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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ import { useRouter } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Panel } from "@/components/common/panel-common";
|
import { Panel } from "@/components/common/panel-common";
|
||||||
import { fetchRecurringJobRule, toggleRecurringJobRule, deleteRecurringJobRule } from "@/app/actions/recurring-job-rules.actions";
|
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 Link from "next/link";
|
||||||
import { RecurringJobRule } from "@/src/entities/models/recurring-job-rule";
|
import { RecurringJobRule } from "@/src/entities/models/recurring-job-rule";
|
||||||
import { Spinner } from "@heroui/react";
|
import { Spinner } from "@heroui/react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { JobsList } from "@/app/projects/[projectId]/jobs/components/jobs-list";
|
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 }) {
|
export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string; ruleId: string }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -19,6 +20,7 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
|
||||||
const [updating, setUpdating] = useState(false);
|
const [updating, setUpdating] = useState(false);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
const jobsFilters = useMemo(() => ({ recurringJobRuleId: ruleId }), [ruleId]);
|
const jobsFilters = useMemo(() => ({ recurringJobRuleId: ruleId }), [ruleId]);
|
||||||
|
|
||||||
|
|
@ -145,6 +147,26 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
|
||||||
}
|
}
|
||||||
rightActions={
|
rightActions={
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{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
|
<Button
|
||||||
onClick={handleToggleStatus}
|
onClick={handleToggleStatus}
|
||||||
disabled={updating}
|
disabled={updating}
|
||||||
|
|
@ -165,11 +187,22 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="h-full overflow-auto px-4 py-4">
|
<div className="h-full overflow-auto px-4 py-4">
|
||||||
<div className="max-w-[800px] mx-auto space-y-6">
|
<div className="max-w-[800px] mx-auto space-y-6">
|
||||||
|
{editing ? (
|
||||||
|
<EditRecurringJobRuleForm
|
||||||
|
projectId={projectId}
|
||||||
|
rule={rule}
|
||||||
|
onCancel={() => setEditing(false)}
|
||||||
|
onUpdated={(updatedRule) => setRule(updatedRule)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
<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="flex items-center gap-2 mb-2">
|
||||||
|
|
@ -267,6 +300,8 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,197 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Panel } from "@/components/common/panel-common";
|
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 { ArrowLeftIcon, PlusIcon, TrashIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { DatePicker } from "@heroui/react";
|
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 = {
|
type FormMessage = {
|
||||||
role: "system" | "user" | "assistant";
|
role: "system" | "user" | "assistant";
|
||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTriggers = true }: { projectId: string; onBack?: () => void; hasExistingTriggers?: boolean }) {
|
type BackButtonConfig =
|
||||||
const router = useRouter();
|
| { label: string; onClick: () => void }
|
||||||
const [loading, setLoading] = useState(false);
|
| { label: string; href: string };
|
||||||
const [messages, setMessages] = useState<FormMessage[]>([
|
|
||||||
{ role: "user", content: "" }
|
type FormSubmitPayload = {
|
||||||
]);
|
messages: FormMessage[];
|
||||||
// Set default to 30 minutes from now with timezone info
|
scheduledDateTime: ZonedDateTime;
|
||||||
const getDefaultDateTime = () => {
|
|
||||||
const localTimeZone = getLocalTimeZone();
|
|
||||||
const currentTime = now(localTimeZone);
|
|
||||||
const thirtyMinutesFromNow = currentTime.add({ minutes: 30 });
|
|
||||||
return thirtyMinutesFromNow;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [scheduledDateTime, setScheduledDateTime] = useState<ZonedDateTime | null>(getDefaultDateTime());
|
type ScheduledJobRuleFormBaseProps = {
|
||||||
|
title: string;
|
||||||
const addMessage = () => {
|
description?: string;
|
||||||
setMessages([...messages, { role: "user", content: "" }]);
|
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 removeMessage = (index: number) => {
|
const createEmptyMessage = (): FormMessage => ({ role: "user", content: "" });
|
||||||
if (messages.length > 1) {
|
|
||||||
setMessages(messages.filter((_, i) => i !== index));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMessage = (index: number, field: keyof FormMessage, value: string) => {
|
const normaliseMessages = (messages?: FormMessage[]): FormMessage[] => {
|
||||||
const newMessages = [...messages];
|
if (!messages || messages.length === 0) {
|
||||||
newMessages[index] = { ...newMessages[index], [field]: value };
|
return [createEmptyMessage()];
|
||||||
setMessages(newMessages);
|
|
||||||
};
|
|
||||||
|
|
||||||
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())) {
|
return messages.map((message) => ({ ...message }));
|
||||||
alert("Please fill in all message content");
|
};
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
const convertFormMessagesToMessages = (messages: FormMessage[]): z.infer<typeof Message>[] => {
|
||||||
try {
|
return messages.map((msg) => {
|
||||||
// Convert FormMessage to the expected Message type
|
|
||||||
const convertedMessages = messages.map(msg => {
|
|
||||||
if (msg.role === "assistant") {
|
if (msg.role === "assistant") {
|
||||||
return {
|
return {
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
agentName: null,
|
agentName: null,
|
||||||
responseType: "internal" as const,
|
responseType: "internal" as const,
|
||||||
timestamp: undefined
|
timestamp: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
timestamp: undefined
|
timestamp: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Convert ZonedDateTime to ISO string (already in UTC)
|
function ScheduledJobRuleFormBase({
|
||||||
const scheduledTimeString = scheduledDateTime.toDate().toISOString();
|
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);
|
||||||
|
|
||||||
await createScheduledJobRule({
|
useEffect(() => {
|
||||||
projectId,
|
setMessages(normaliseMessages(initialMessages));
|
||||||
input: { messages: convertedMessages },
|
}, [initialMessages]);
|
||||||
scheduledTime: scheduledTimeString,
|
|
||||||
|
useEffect(() => {
|
||||||
|
setScheduledDateTime(initialDateTime ?? placeholderDateTime);
|
||||||
|
}, [initialDateTime, placeholderDateTime]);
|
||||||
|
|
||||||
|
const addMessage = () => {
|
||||||
|
setMessages((prev) => [...prev, createEmptyMessage()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMessage = (index: number) => {
|
||||||
|
setMessages((prev) => {
|
||||||
|
if (prev.length <= 1) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return prev.filter((_, i) => i !== index);
|
||||||
});
|
});
|
||||||
if (onBack) {
|
};
|
||||||
onBack();
|
|
||||||
} else {
|
const updateMessage = (index: number, field: keyof FormMessage, value: string) => {
|
||||||
router.push(`/projects/${projectId}/manage-triggers?tab=scheduled`);
|
setMessages((prev) => {
|
||||||
|
const next = [...prev];
|
||||||
|
next[index] = { ...next[index], [field]: value };
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!scheduledDateTime) {
|
||||||
|
alert("Please select date and time");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.some((msg) => !msg.content?.trim())) {
|
||||||
|
alert("Please fill in all message content");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await onSubmit({
|
||||||
|
messages,
|
||||||
|
scheduledDateTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess(result);
|
||||||
|
} else if (successHref) {
|
||||||
|
router.push(successHref);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create scheduled job rule:", error);
|
console.error(errorMessage, error);
|
||||||
alert("Failed to create scheduled job rule");
|
alert(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{hasExistingTriggers && onBack ? (
|
{backButton ? (
|
||||||
<Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap" onClick={onBack}>
|
'onClick' in backButton ? (
|
||||||
Back
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
startContent={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
onClick={backButton.onClick}
|
||||||
|
>
|
||||||
|
{backButton.label}
|
||||||
</Button>
|
</Button>
|
||||||
) : hasExistingTriggers ? (
|
) : (
|
||||||
<Link href={`/projects/${projectId}/manage-triggers?tab=scheduled`}>
|
<Link href={backButton.href}>
|
||||||
<Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
|
<Button
|
||||||
Back
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
startContent={<ArrowLeftIcon className="w-4 h-4" />}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{backButton.label}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
)
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
CREATE SCHEDULED JOB RULE
|
{title}
|
||||||
</div>
|
</div>
|
||||||
|
{description ? (
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<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.
|
{description}
|
||||||
</p>
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -142,8 +207,8 @@ export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTrigg
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={scheduledDateTime}
|
value={scheduledDateTime}
|
||||||
onChange={setScheduledDateTime}
|
onChange={setScheduledDateTime}
|
||||||
placeholderValue={getDefaultDateTime()}
|
placeholderValue={placeholderDateTime}
|
||||||
minValue={now(getLocalTimeZone())}
|
minValue={minDateTime}
|
||||||
granularity="minute"
|
granularity="minute"
|
||||||
isRequired
|
isRequired
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|
@ -214,7 +279,7 @@ export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTrigg
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
className="px-6 py-2 whitespace-nowrap"
|
className="px-6 py-2 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{loading ? "Creating..." : "Create Rule"}
|
{loading ? submittingLabel : submitLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -223,3 +288,111 @@ export function CreateScheduledJobRuleForm({ projectId, onBack, hasExistingTrigg
|
||||||
</Panel>
|
</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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@ import { ScheduledJobRule } from "@/src/entities/models/scheduled-job-rule";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { MessageDisplay } from "@/app/lib/components/message-display";
|
||||||
|
import { EditScheduledJobRuleForm } from "./create-scheduled-job-rule-form";
|
||||||
|
|
||||||
export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string; ruleId: string; }) {
|
export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string; ruleId: string; }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -18,6 +19,7 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let ignore = false;
|
let ignore = false;
|
||||||
|
|
@ -92,6 +94,26 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
|
||||||
}
|
}
|
||||||
rightActions={
|
rightActions={
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{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
|
<Button
|
||||||
onClick={() => setShowDeleteConfirm(true)}
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
@ -101,6 +123,8 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -114,6 +138,15 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
|
||||||
)}
|
)}
|
||||||
{!loading && rule && (
|
{!loading && rule && (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
|
{editing ? (
|
||||||
|
<EditScheduledJobRuleForm
|
||||||
|
projectId={projectId}
|
||||||
|
rule={rule}
|
||||||
|
onCancel={() => setEditing(false)}
|
||||||
|
onUpdated={(updatedRule) => setRule(updatedRule)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{/* Rule Metadata */}
|
{/* 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="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 className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
|
@ -182,6 +215,8 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { 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
|
// Recurring Job Rules
|
||||||
import { MongoDBRecurringJobRulesRepository } from "@/src/infrastructure/repositories/mongodb.recurring-job-rules.repository";
|
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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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
|
// API Keys
|
||||||
import { CreateApiKeyUseCase } from "@/src/application/use-cases/api-keys/create-api-key.use-case";
|
import { CreateApiKeyUseCase } from "@/src/application/use-cases/api-keys/create-api-key.use-case";
|
||||||
|
|
@ -238,10 +242,12 @@ container.register({
|
||||||
createScheduledJobRuleUseCase: asClass(CreateScheduledJobRuleUseCase).singleton(),
|
createScheduledJobRuleUseCase: asClass(CreateScheduledJobRuleUseCase).singleton(),
|
||||||
fetchScheduledJobRuleUseCase: asClass(FetchScheduledJobRuleUseCase).singleton(),
|
fetchScheduledJobRuleUseCase: asClass(FetchScheduledJobRuleUseCase).singleton(),
|
||||||
listScheduledJobRulesUseCase: asClass(ListScheduledJobRulesUseCase).singleton(),
|
listScheduledJobRulesUseCase: asClass(ListScheduledJobRulesUseCase).singleton(),
|
||||||
|
updateScheduledJobRuleUseCase: asClass(UpdateScheduledJobRuleUseCase).singleton(),
|
||||||
deleteScheduledJobRuleUseCase: asClass(DeleteScheduledJobRuleUseCase).singleton(),
|
deleteScheduledJobRuleUseCase: asClass(DeleteScheduledJobRuleUseCase).singleton(),
|
||||||
createScheduledJobRuleController: asClass(CreateScheduledJobRuleController).singleton(),
|
createScheduledJobRuleController: asClass(CreateScheduledJobRuleController).singleton(),
|
||||||
fetchScheduledJobRuleController: asClass(FetchScheduledJobRuleController).singleton(),
|
fetchScheduledJobRuleController: asClass(FetchScheduledJobRuleController).singleton(),
|
||||||
listScheduledJobRulesController: asClass(ListScheduledJobRulesController).singleton(),
|
listScheduledJobRulesController: asClass(ListScheduledJobRulesController).singleton(),
|
||||||
|
updateScheduledJobRuleController: asClass(UpdateScheduledJobRuleController).singleton(),
|
||||||
deleteScheduledJobRuleController: asClass(DeleteScheduledJobRuleController).singleton(),
|
deleteScheduledJobRuleController: asClass(DeleteScheduledJobRuleController).singleton(),
|
||||||
|
|
||||||
// recurring job rules
|
// recurring job rules
|
||||||
|
|
@ -251,11 +257,13 @@ container.register({
|
||||||
fetchRecurringJobRuleUseCase: asClass(FetchRecurringJobRuleUseCase).singleton(),
|
fetchRecurringJobRuleUseCase: asClass(FetchRecurringJobRuleUseCase).singleton(),
|
||||||
listRecurringJobRulesUseCase: asClass(ListRecurringJobRulesUseCase).singleton(),
|
listRecurringJobRulesUseCase: asClass(ListRecurringJobRulesUseCase).singleton(),
|
||||||
toggleRecurringJobRuleUseCase: asClass(ToggleRecurringJobRuleUseCase).singleton(),
|
toggleRecurringJobRuleUseCase: asClass(ToggleRecurringJobRuleUseCase).singleton(),
|
||||||
|
updateRecurringJobRuleUseCase: asClass(UpdateRecurringJobRuleUseCase).singleton(),
|
||||||
deleteRecurringJobRuleUseCase: asClass(DeleteRecurringJobRuleUseCase).singleton(),
|
deleteRecurringJobRuleUseCase: asClass(DeleteRecurringJobRuleUseCase).singleton(),
|
||||||
createRecurringJobRuleController: asClass(CreateRecurringJobRuleController).singleton(),
|
createRecurringJobRuleController: asClass(CreateRecurringJobRuleController).singleton(),
|
||||||
fetchRecurringJobRuleController: asClass(FetchRecurringJobRuleController).singleton(),
|
fetchRecurringJobRuleController: asClass(FetchRecurringJobRuleController).singleton(),
|
||||||
listRecurringJobRulesController: asClass(ListRecurringJobRulesController).singleton(),
|
listRecurringJobRulesController: asClass(ListRecurringJobRulesController).singleton(),
|
||||||
toggleRecurringJobRuleController: asClass(ToggleRecurringJobRuleController).singleton(),
|
toggleRecurringJobRuleController: asClass(ToggleRecurringJobRuleController).singleton(),
|
||||||
|
updateRecurringJobRuleController: asClass(UpdateRecurringJobRuleController).singleton(),
|
||||||
deleteRecurringJobRuleController: asClass(DeleteRecurringJobRuleController).singleton(),
|
deleteRecurringJobRuleController: asClass(DeleteRecurringJobRuleController).singleton(),
|
||||||
|
|
||||||
// projects
|
// projects
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,15 @@ export const ListedRecurringRuleItem = RecurringJobRule.omit({
|
||||||
input: true,
|
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.
|
* 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>>;
|
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.
|
* Deletes a recurring job rule by its unique identifier.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export const UpdateJobSchema = ScheduledJobRule.pick({
|
||||||
output: true,
|
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.
|
* 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>>;
|
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.
|
* Releases a scheduled job rule after it has been executed.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { IProjectActionAuthorizationPolicy } from '../../policies/project-action
|
||||||
import { IRecurringJobRulesRepository } from '../../repositories/recurring-job-rules.repository.interface';
|
import { IRecurringJobRulesRepository } from '../../repositories/recurring-job-rules.repository.interface';
|
||||||
import { RecurringJobRule } from '@/src/entities/models/recurring-job-rule';
|
import { RecurringJobRule } from '@/src/entities/models/recurring-job-rule';
|
||||||
import { Message } from '@/app/lib/types/types';
|
import { Message } from '@/app/lib/types/types';
|
||||||
|
import { isValidCronExpression } from '@/src/application/lib/utils/is-valid-cron-expression';
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
caller: z.enum(["user", "api"]),
|
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>> {
|
async execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof RecurringJobRule>> {
|
||||||
// Validate cron expression
|
// 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');
|
throw new BadRequestError('Invalid cron expression. Expected format: minute hour day month dayOfWeek');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,31 +67,4 @@ export class CreateRecurringJobRuleUseCase implements ICreateRecurringJobRuleUse
|
||||||
|
|
||||||
return rule;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Filter, ObjectId } from "mongodb";
|
import { Filter, ObjectId } from "mongodb";
|
||||||
import { db } from "@/app/lib/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 { RecurringJobRule } from "@/src/entities/models/recurring-job-rule";
|
||||||
import { NotFoundError } from "@/src/entities/errors/common";
|
import { NotFoundError } from "@/src/entities/errors/common";
|
||||||
import { PaginatedList } from "@/src/entities/common/paginated-list";
|
import { PaginatedList } from "@/src/entities/common/paginated-list";
|
||||||
|
|
@ -208,6 +208,31 @@ export class MongoDBRecurringJobRulesRepository implements IRecurringJobRulesRep
|
||||||
return await this.updateNextRunAt(id, result.cron);
|
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.
|
* Deletes a recurring job rule by its unique identifier.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Filter, ObjectId } from "mongodb";
|
import { Filter, ObjectId } from "mongodb";
|
||||||
import { db } from "@/app/lib/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 { ScheduledJobRule } from "@/src/entities/models/scheduled-job-rule";
|
||||||
import { NotFoundError } from "@/src/entities/errors/common";
|
import { NotFoundError } from "@/src/entities/errors/common";
|
||||||
import { PaginatedList } from "@/src/entities/common/paginated-list";
|
import { PaginatedList } from "@/src/entities/common/paginated-list";
|
||||||
|
|
@ -138,6 +138,41 @@ export class MongoDBScheduledJobRulesRepository implements IScheduledJobRulesRep
|
||||||
return this.convertDocToModel(result);
|
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.
|
* Updates a scheduled job rule with new status and output data.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue