Skip to content

Commit 0b827e8

Browse files
Merge pull request #23 from MaximumTrainer/copilot/add-post-job-action-service
feat: Post-Job Actions — PUT endpoint + full frontend CRUD
2 parents 9cc8038 + b4fd81a commit 0b827e8

11 files changed

Lines changed: 479 additions & 6 deletions

File tree

backend/src/main/kotlin/com/opendatamask/controller/PostJobActionController.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.opendatamask.controller
22

3+
import com.opendatamask.dto.PostJobActionRequest
34
import com.opendatamask.model.PostJobAction
45
import com.opendatamask.service.PostJobActionService
56
import org.springframework.http.HttpStatus
@@ -18,12 +19,25 @@ class PostJobActionController(
1819
@PostMapping
1920
fun createAction(
2021
@PathVariable workspaceId: Long,
21-
@RequestBody action: PostJobAction
22+
@RequestBody request: PostJobActionRequest
2223
): ResponseEntity<PostJobAction> {
23-
val toSave = action.copy(workspaceId = workspaceId)
24+
val toSave = PostJobAction(
25+
workspaceId = workspaceId,
26+
actionType = request.actionType,
27+
config = request.config,
28+
enabled = request.enabled
29+
)
2430
return ResponseEntity.status(HttpStatus.CREATED).body(service.createAction(toSave))
2531
}
2632

33+
@PutMapping("/{actionId}")
34+
fun updateAction(
35+
@PathVariable workspaceId: Long,
36+
@PathVariable actionId: Long,
37+
@RequestBody request: PostJobActionRequest
38+
): ResponseEntity<PostJobAction> =
39+
ResponseEntity.ok(service.updateAction(workspaceId, actionId, request))
40+
2741
@DeleteMapping("/{actionId}")
2842
fun deleteAction(
2943
@PathVariable workspaceId: Long,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.opendatamask.dto
2+
3+
import com.opendatamask.model.ActionType
4+
5+
data class PostJobActionRequest(
6+
val actionType: ActionType,
7+
val config: String = "{}",
8+
val enabled: Boolean = true
9+
)

backend/src/main/kotlin/com/opendatamask/service/PostJobActionService.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,16 @@ class PostJobActionService(
103103

104104
fun createAction(action: PostJobAction): PostJobAction = repository.save(action)
105105
fun listActions(workspaceId: Long): List<PostJobAction> = repository.findByWorkspaceId(workspaceId)
106+
fun updateAction(workspaceId: Long, id: Long, request: com.opendatamask.dto.PostJobActionRequest): PostJobAction {
107+
val existing = repository.findById(id)
108+
.orElseThrow { NoSuchElementException("PostJobAction not found: $id") }
109+
if (existing.workspaceId != workspaceId) {
110+
throw NoSuchElementException("PostJobAction $id does not belong to workspace $workspaceId")
111+
}
112+
existing.actionType = request.actionType
113+
existing.config = request.config
114+
existing.enabled = request.enabled
115+
return repository.save(existing)
116+
}
106117
fun deleteAction(id: Long) = repository.deleteById(id)
107118
}

backend/src/test/kotlin/com/opendatamask/controller/PostJobActionControllerTest.kt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.opendatamask.controller
22

33
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
44
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
5+
import com.opendatamask.dto.PostJobActionRequest
56
import com.opendatamask.model.ActionType
67
import com.opendatamask.model.PostJobAction
78
import com.opendatamask.security.JwtAuthenticationFilter
@@ -51,12 +52,13 @@ class PostJobActionControllerTest {
5152

5253
@Test
5354
fun `POST create action returns 201`() {
54-
val action = PostJobAction(workspaceId = 1L, actionType = ActionType.WEBHOOK, config = """{"url":"http://example.com"}""")
55-
whenever(service.createAction(any<PostJobAction>())).thenReturn(action.copy(id = 1L))
55+
val request = PostJobActionRequest(actionType = ActionType.WEBHOOK, config = """{"url":"http://example.com"}""")
56+
val saved = PostJobAction(id = 1L, workspaceId = 1L, actionType = ActionType.WEBHOOK, config = """{"url":"http://example.com"}""")
57+
whenever(service.createAction(any<PostJobAction>())).thenReturn(saved)
5658
mockMvc.perform(
5759
post("/api/workspaces/1/actions")
5860
.contentType(MediaType.APPLICATION_JSON)
59-
.content(mapper.writeValueAsString(action))
61+
.content(mapper.writeValueAsString(request))
6062
).andExpect(status().isCreated)
6163
}
6264

@@ -66,4 +68,30 @@ class PostJobActionControllerTest {
6668
.andExpect(status().isNoContent)
6769
verify(service).deleteAction(42L)
6870
}
71+
72+
@Test
73+
fun `PUT update action returns 200 with correct response body`() {
74+
val request = PostJobActionRequest(
75+
actionType = ActionType.WEBHOOK,
76+
config = """{"url":"http://example.com"}"""
77+
)
78+
val returned = PostJobAction(
79+
id = 42L,
80+
workspaceId = 1L,
81+
actionType = ActionType.WEBHOOK,
82+
config = """{"url":"http://example.com"}"""
83+
)
84+
whenever(service.updateAction(eq(1L), eq(42L), any<PostJobActionRequest>())).thenReturn(returned)
85+
mockMvc.perform(
86+
put("/api/workspaces/1/actions/42")
87+
.contentType(MediaType.APPLICATION_JSON)
88+
.content(mapper.writeValueAsString(request))
89+
)
90+
.andExpect(status().isOk)
91+
.andExpect(jsonPath("$.id").value(42))
92+
.andExpect(jsonPath("$.workspaceId").value(1))
93+
.andExpect(jsonPath("$.actionType").value("WEBHOOK"))
94+
.andExpect(jsonPath("$.config").value("""{"url":"http://example.com"}"""))
95+
verify(service).updateAction(eq(1L), eq(42L), any<PostJobActionRequest>())
96+
}
6997
}

backend/src/test/kotlin/com/opendatamask/service/PostJobActionServiceTest.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.opendatamask.service
22

3+
import com.opendatamask.dto.PostJobActionRequest
34
import com.opendatamask.model.ActionType
45
import com.opendatamask.model.Job
56
import com.opendatamask.model.JobStatus
67
import com.opendatamask.model.PostJobAction
78
import com.opendatamask.repository.PostJobActionRepository
89
import org.junit.jupiter.api.Test
910
import org.junit.jupiter.api.Assertions.*
11+
import org.junit.jupiter.api.assertThrows
1012
import org.mockito.kotlin.*
1113
import java.time.LocalDateTime
1214

@@ -68,4 +70,39 @@ class PostJobActionServiceTest {
6870
service.deleteAction(42L)
6971
verify(repository).deleteById(42L)
7072
}
73+
74+
@Test
75+
fun `updateAction updates fields and saves`() {
76+
val existing = PostJobAction(
77+
id = 1L, workspaceId = 1L,
78+
actionType = ActionType.EMAIL,
79+
config = """{"to":"old@example.com"}"""
80+
)
81+
val request = PostJobActionRequest(
82+
actionType = ActionType.WEBHOOK,
83+
config = """{"url":"http://new.example.com"}""",
84+
enabled = false
85+
)
86+
whenever(repository.findById(1L)).thenReturn(java.util.Optional.of(existing))
87+
whenever(repository.save(any<PostJobAction>())).thenReturn(existing)
88+
service.updateAction(1L, 1L, request)
89+
verify(repository).save(existing)
90+
assertEquals(ActionType.WEBHOOK, existing.actionType)
91+
assertEquals("""{"url":"http://new.example.com"}""", existing.config)
92+
assertEquals(false, existing.enabled)
93+
}
94+
95+
@Test
96+
fun `updateAction throws when action belongs to different workspace`() {
97+
val existing = PostJobAction(
98+
id = 1L, workspaceId = 99L,
99+
actionType = ActionType.EMAIL,
100+
config = """{"to":"other@example.com"}"""
101+
)
102+
whenever(repository.findById(1L)).thenReturn(java.util.Optional.of(existing))
103+
assertThrows<NoSuchElementException> {
104+
service.updateAction(1L, 1L, PostJobActionRequest(actionType = ActionType.EMAIL, config = "{}"))
105+
}
106+
verify(repository, never()).save(any())
107+
}
71108
}

frontend/src/api/actions.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import apiClient from './client'
2+
import type { PostJobAction, PostJobActionRequest } from '@/types'
3+
4+
export async function listActions(workspaceId: number): Promise<PostJobAction[]> {
5+
const { data } = await apiClient.get<PostJobAction[]>(`/workspaces/${workspaceId}/actions`)
6+
return data
7+
}
8+
9+
export async function createAction(
10+
workspaceId: number,
11+
payload: PostJobActionRequest
12+
): Promise<PostJobAction> {
13+
const { data } = await apiClient.post<PostJobAction>(
14+
`/workspaces/${workspaceId}/actions`,
15+
payload
16+
)
17+
return data
18+
}
19+
20+
export async function updateAction(
21+
workspaceId: number,
22+
actionId: number,
23+
payload: PostJobActionRequest
24+
): Promise<PostJobAction> {
25+
const { data } = await apiClient.put<PostJobAction>(
26+
`/workspaces/${workspaceId}/actions/${actionId}`,
27+
payload
28+
)
29+
return data
30+
}
31+
32+
export async function deleteAction(workspaceId: number, actionId: number): Promise<void> {
33+
await apiClient.delete(`/workspaces/${workspaceId}/actions/${actionId}`)
34+
}

frontend/src/router/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import WorkspaceDetailView from '@/views/WorkspaceDetailView.vue'
88
import ConnectionsView from '@/views/ConnectionsView.vue'
99
import TablesView from '@/views/TablesView.vue'
1010
import JobsView from '@/views/JobsView.vue'
11+
import ActionsView from '@/views/ActionsView.vue'
1112

1213
const router = createRouter({
1314
history: createWebHistory(import.meta.env.BASE_URL),
@@ -60,6 +61,12 @@ const router = createRouter({
6061
name: 'jobs',
6162
component: JobsView,
6263
meta: { requiresAuth: true }
64+
},
65+
{
66+
path: '/workspaces/:id/actions',
67+
name: 'actions',
68+
component: ActionsView,
69+
meta: { requiresAuth: true }
6370
}
6471
]
6572
})

frontend/src/types/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,29 @@ export interface JobLog {
204204
timestamp: string
205205
}
206206

207+
// ── Post-Job Action ───────────────────────────────────────────────────────
208+
209+
export enum ActionType {
210+
WEBHOOK = 'WEBHOOK',
211+
EMAIL = 'EMAIL',
212+
SCRIPT = 'SCRIPT'
213+
}
214+
215+
export interface PostJobAction {
216+
id: number
217+
workspaceId: number
218+
actionType: ActionType
219+
config: string
220+
enabled: boolean
221+
createdAt: string
222+
}
223+
224+
export interface PostJobActionRequest {
225+
actionType: ActionType
226+
config: string
227+
enabled?: boolean
228+
}
229+
207230
// ── Pagination ────────────────────────────────────────────────────────────
208231

209232
export interface Page<T> {

0 commit comments

Comments
 (0)