@@ -2,19 +2,24 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
22import { db } from '@sim/db'
33import { workflowSchedule } from '@sim/db/schema'
44import { createLogger } from '@sim/logger'
5- import {
6- assertWorkflowMutable ,
7- authorizeWorkflowByWorkspacePermission ,
8- WorkflowLockedError ,
9- } from '@sim/platform-authz/workflow'
5+ import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow'
106import { and , eq , isNull } from 'drizzle-orm'
117import { type NextRequest , NextResponse } from 'next/server'
128import { getScheduleByIdContract , updateScheduleContract } from '@/lib/api/contracts/schedules'
139import { parseRequest } from '@/lib/api/server'
10+ import {
11+ createInternalResourceConcealmentPolicy ,
12+ defineInternalJsonRoute ,
13+ internalErrorResponse ,
14+ internalOrchestrationErrorPolicy ,
15+ internalRateLimits ,
16+ internalSessionAuth ,
17+ } from '@/lib/api/server/routes'
1418import { getSession } from '@/lib/auth'
1519import { generateRequestId } from '@/lib/core/utils/request'
1620import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
17- import { validateCronExpression } from '@/lib/workflows/schedules/utils'
21+ import { workflowOperations } from '@/lib/workflows/application/operations'
22+ import { setWorkflowTriggerEnabled } from '@/lib/workflows/application/triggers'
1823
1924const logger = createLogger ( 'ScheduleAPI' )
2025
@@ -97,124 +102,48 @@ export const GET = withRouteHandler(
97102 }
98103)
99104
100- export const PUT = withRouteHandler (
101- async ( request : NextRequest , context : { params : Promise < { id : string } > } ) => {
102- const requestId = generateRequestId ( )
103-
104- try {
105- const session = await getSession ( )
106- if ( ! session ?. user ?. id ) {
107- logger . warn ( 'Unauthorized schedule update attempt' )
108- return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } )
109- }
110-
111- const parsed = await parseRequest ( updateScheduleContract , request , context , {
112- validationErrorResponse : ( ) =>
113- NextResponse . json ( { error : 'Invalid request body' } , { status : 400 } ) ,
114- } )
115- if ( ! parsed . success ) return parsed . response
116-
117- const { id : scheduleId } = parsed . data . params
118- const validatedBody = parsed . data . body
119-
120- const result = await fetchAndAuthorize ( requestId , scheduleId , session . user . id , 'write' )
121- if ( result instanceof NextResponse ) return result
122- const { schedule, workspaceId } = result
123- if ( schedule . workflowId ) {
124- await assertWorkflowMutable ( schedule . workflowId )
105+ /**
106+ * Disables or reactivates a workflow schedule through the same use case as
107+ * `PATCH /api/v2/workflows/{workflowId}/triggers/{blockId}`. The id must name
108+ * the live deployment's row: a schedule of an older version answers 404
109+ * instead of being revived.
110+ */
111+ export const PUT = defineInternalJsonRoute ( {
112+ contract : updateScheduleContract ,
113+ auth : internalSessionAuth ,
114+ operation : workflowOperations . setTriggerEnabled ,
115+ rateLimit : internalRateLimits . none ( {
116+ reason : 'Schedule enablement retains its existing session admission policy.' ,
117+ } ) ,
118+ errorPolicy : createInternalResourceConcealmentPolicy ( {
119+ notFoundMessage : 'Schedule not found' ,
120+ base : {
121+ project : internalOrchestrationErrorPolicy . project ,
122+ unhandled : ( ) => internalErrorResponse ( 500 , { error : 'Failed to update schedule' } ) ,
123+ } ,
124+ } ) ,
125+ parseOptions : {
126+ validationErrorResponse : ( ) =>
127+ NextResponse . json ( { error : 'Invalid request body' } , { status : 400 } ) ,
128+ } ,
129+ mapInput : ( { params, body } ) => ( {
130+ target : { kind : 'schedule_row' as const , scheduleId : params . id } ,
131+ enabled : body . action === 'reactivate' ,
132+ } ) ,
133+ useCase : setWorkflowTriggerEnabled ,
134+ present : ( { trigger, changed } , { input } ) => {
135+ if ( ! input . enabled ) {
136+ return {
137+ message : changed ? 'Schedule disabled successfully' : 'Schedule is already disabled' ,
125138 }
126-
127- const { action } = validatedBody
128-
129- if ( action === 'disable' ) {
130- if ( schedule . status === 'disabled' ) {
131- return NextResponse . json ( { message : 'Schedule is already disabled' } )
132- }
133-
134- await db
135- . update ( workflowSchedule )
136- . set ( { status : 'disabled' , nextRunAt : null , lastQueuedAt : null , updatedAt : new Date ( ) } )
137- . where ( and ( eq ( workflowSchedule . id , scheduleId ) , isNull ( workflowSchedule . archivedAt ) ) )
138-
139- logger . info ( `Disabled schedule: ${ scheduleId } ` )
140-
141- recordAudit ( {
142- workspaceId,
143- actorId : session . user . id ,
144- actorName : session . user . name ,
145- actorEmail : session . user . email ,
146- action : AuditAction . SCHEDULE_UPDATED ,
147- resourceType : AuditResourceType . SCHEDULE ,
148- resourceId : scheduleId ,
149- resourceName : schedule . jobTitle ?? undefined ,
150- description : `Disabled schedule "${ schedule . jobTitle ?? scheduleId } "` ,
151- metadata : {
152- operation : 'disable' ,
153- sourceType : schedule . sourceType ,
154- previousStatus : schedule . status ,
155- } ,
156- request,
157- } )
158-
159- return NextResponse . json ( { message : 'Schedule disabled successfully' } )
160- }
161-
162- // reactivate
163- if ( schedule . status === 'active' ) {
164- return NextResponse . json ( { message : 'Schedule is already active' } )
165- }
166-
167- if ( ! schedule . cronExpression ) {
168- logger . error ( `Schedule has no cron expression: ${ scheduleId } ` )
169- return NextResponse . json ( { error : 'Schedule has no cron expression' } , { status : 400 } )
170- }
171-
172- const cronResult = validateCronExpression ( schedule . cronExpression , schedule . timezone || 'UTC' )
173- if ( ! cronResult . isValid || ! cronResult . nextRun ) {
174- logger . error ( `Invalid cron expression for schedule: ${ scheduleId } ` )
175- return NextResponse . json ( { error : 'Schedule has invalid cron expression' } , { status : 400 } )
176- }
177-
178- const now = new Date ( )
179- const nextRunAt = cronResult . nextRun
180-
181- await db
182- . update ( workflowSchedule )
183- . set ( { status : 'active' , failedCount : 0 , infraRetryCount : 0 , updatedAt : now , nextRunAt } )
184- . where ( and ( eq ( workflowSchedule . id , scheduleId ) , isNull ( workflowSchedule . archivedAt ) ) )
185-
186- logger . info ( `Reactivated schedule: ${ scheduleId } ` )
187-
188- recordAudit ( {
189- workspaceId,
190- actorId : session . user . id ,
191- actorName : session . user . name ,
192- actorEmail : session . user . email ,
193- action : AuditAction . SCHEDULE_UPDATED ,
194- resourceType : AuditResourceType . SCHEDULE ,
195- resourceId : scheduleId ,
196- resourceName : schedule . jobTitle ?? undefined ,
197- description : `Reactivated schedule "${ schedule . jobTitle ?? scheduleId } "` ,
198- metadata : {
199- operation : 'reactivate' ,
200- sourceType : schedule . sourceType ,
201- cronExpression : schedule . cronExpression ,
202- timezone : schedule . timezone ,
203- } ,
204- request,
205- } )
206-
207- return NextResponse . json ( { message : 'Schedule activated successfully' , nextRunAt } )
208- } catch ( error ) {
209- if ( error instanceof WorkflowLockedError ) {
210- return NextResponse . json ( { error : error . message } , { status : error . status } )
211- }
212-
213- logger . error ( 'Error updating schedule' , error )
214- return NextResponse . json ( { error : 'Failed to update schedule' } , { status : 500 } )
215139 }
216- }
217- )
140+ if ( ! changed ) return { message : 'Schedule is already active' }
141+ return {
142+ message : 'Schedule activated successfully' ,
143+ ...( trigger . nextRunAt ? { nextRunAt : trigger . nextRunAt . toISOString ( ) } : { } ) ,
144+ }
145+ } ,
146+ } )
218147
219148export const DELETE = withRouteHandler (
220149 async ( request : NextRequest , { params } : { params : Promise < { id : string } > } ) => {
0 commit comments