From f7802fc02dc2918464cc3261acac3cf229085d00 Mon Sep 17 00:00:00 2001 From: Tommi Leinamo Date: Mon, 13 Nov 2023 09:35:11 +0200 Subject: [PATCH 1/9] Remove the hello world example code Not really necessary at this point. --- README.md | 1 - .../fi/hsl/jore4/timetables/api/HelloWorld.kt | 34 ------------ .../timetables/config/WebSecurityConfig.kt | 2 - .../service/ScheduleFrameService.kt | 24 --------- .../jore4/timetables/api/HelloWorldTest.kt | 52 ------------------- 5 files changed, 113 deletions(-) delete mode 100644 src/main/kotlin/fi/hsl/jore4/timetables/api/HelloWorld.kt delete mode 100644 src/main/kotlin/fi/hsl/jore4/timetables/service/ScheduleFrameService.kt delete mode 100644 src/test/kotlin/fi/hsl/jore4/timetables/api/HelloWorldTest.kt diff --git a/README.md b/README.md index 2751a1ed..5c3ff044 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,6 @@ When the submodule is updated, to get the newest version of inserter you need to ## API structure -- `GET /hello` Hello world example interface returns http 200 - `POST /timetables/replace`: Import staging timetables to target priority by replacing matching currently active vehicle schedule frames. Example request body: diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/HelloWorld.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/HelloWorld.kt deleted file mode 100644 index 3cb86271..00000000 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/HelloWorld.kt +++ /dev/null @@ -1,34 +0,0 @@ -package fi.hsl.jore4.timetables.api - -import fi.hsl.jore4.timetables.service.ScheduleFrameService -import mu.KotlinLogging -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController - -private val LOGGER = KotlinLogging.logger {} - -@RestController -@RequestMapping("/hello") -class HelloWorld( - private val scheduleFrameService: ScheduleFrameService -) { - - @GetMapping - fun helloWorld(): ResponseEntity { - return ResponseEntity - .status(HttpStatus.OK) - .body("hello") - } - - @GetMapping("test") - fun vehicleScheduleFrames(): ResponseEntity { - LOGGER.info("Request for frames") - val frames = scheduleFrameService.getServiceFrames() - return ResponseEntity - .status(HttpStatus.OK) - .body(frames) - } -} diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/config/WebSecurityConfig.kt b/src/main/kotlin/fi/hsl/jore4/timetables/config/WebSecurityConfig.kt index 418c552e..a2764443 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/config/WebSecurityConfig.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/config/WebSecurityConfig.kt @@ -25,8 +25,6 @@ class WebSecurityConfig { HttpMethod.GET, "/actuator/health", "/error", - "/hello", - "/hello/test", "/timetables/to-replace" ) .permitAll() diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/service/ScheduleFrameService.kt b/src/main/kotlin/fi/hsl/jore4/timetables/service/ScheduleFrameService.kt deleted file mode 100644 index 6c815056..00000000 --- a/src/main/kotlin/fi/hsl/jore4/timetables/service/ScheduleFrameService.kt +++ /dev/null @@ -1,24 +0,0 @@ -package fi.hsl.jore4.timetables.service - -import fi.hsl.jore.jore4.jooq.vehicle_schedule.tables.VehicleScheduleFrame -import mu.KotlinLogging -import org.jooq.DSLContext -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional - -private val LOGGER = KotlinLogging.logger {} - -@Service -class ScheduleFrameService( - private val dsl: DSLContext -) { - - @Transactional(readOnly = true) - fun getServiceFrames(): String { - LOGGER.trace("GetServiceFrames request") - return dsl.selectFrom(VehicleScheduleFrame.VEHICLE_SCHEDULE_FRAME) - .fetch() - .map { it.toString() } - .joinToString(",") - } -} diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/HelloWorldTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/HelloWorldTest.kt deleted file mode 100644 index 828de73a..00000000 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/HelloWorldTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -package fi.hsl.jore4.timetables.api - -import fi.hsl.jore4.timetables.service.ScheduleFrameService -import io.mockk.every -import io.mockk.junit5.MockKExtension -import io.mockk.mockk -import io.mockk.verify -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -@DisplayName("Test Hello World API") -@MockKExtension.ConfirmVerification -class HelloWorldTest { - - private val scheduleFrameService = mockk() - - @BeforeEach - fun setup() { - every { scheduleFrameService.getServiceFrames() } returns "frame1, frame2" - } - - @Test - @DisplayName("Test the API returns hello") - fun helloWorldApiTest() { - val expected = "hello" - val api = HelloWorld(scheduleFrameService) - - val returned = api.helloWorld() - - assertNotNull(returned.body) - assertEquals(expected, returned.body, "Body content was not what was expected") - - verify(exactly = 0) { scheduleFrameService.getServiceFrames() } - } - - @Test - @DisplayName("Test the API returns hello") - fun scheduleFrameLabelsTest() { - val expected = "frame1, frame2" - val api = HelloWorld(scheduleFrameService) - - val returned = api.vehicleScheduleFrames() - - assertNotNull(returned.body) - assertEquals(expected, returned.body, "Body did not contain expected frames") - - verify { scheduleFrameService.getServiceFrames() } - } -} From af8b883c2eda1e838039a335bad2d9ffe3a8b118 Mon Sep 17 00:00:00 2001 From: Tommi Leinamo Date: Wed, 15 Nov 2023 09:09:54 +0200 Subject: [PATCH 2/9] Unify MockMvcResultMatchers import style Imports were done differently between files. --- .../api/TimetablesCombineApiTest.kt | 43 ++++++++++--------- .../api/TimetablesToReplaceApiTest.kt | 10 ++--- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt index 8dce318c..0e7c9779 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt @@ -21,7 +21,8 @@ import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.ResultActions import org.springframework.test.web.servlet.request.MockMvcRequestBuilders -import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.UUID @ExtendWith(MockKExtension::class) @@ -70,10 +71,10 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { } answers { defaultTargetFrameIds } executeCombineTimetablesRequest() - .andExpect(MockMvcResultMatchers.status().isOk) - .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect( - MockMvcResultMatchers.content().json( + content().json( """ { "combinedIntoVehicleScheduleFrameIds": $defaultTargetFrameIds @@ -98,10 +99,10 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { } throws RuntimeException("something unexpected happened") executeCombineTimetablesRequest() - .andExpect(MockMvcResultMatchers.status().isConflict) - .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isConflict) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect( - MockMvcResultMatchers.content().json( + content().json( """ { "message": "something unexpected happened", @@ -126,8 +127,8 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { // TODO: this could maybe use better handling. executeCombineTimetablesRequest(targetPriority = invalidTargetPriority) - .andExpect(MockMvcResultMatchers.status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().string("")) + .andExpect(status().isBadRequest) + .andExpect(content().string("")) verify(exactly = 0) { combineTimetablesService.combineTimetables(any(), any()) } } @@ -144,10 +145,10 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { } throws StagingVehicleScheduleFrameNotFoundException(errorMessage, missingStagingFrameId) executeCombineTimetablesRequest() - .andExpect(MockMvcResultMatchers.status().isNotFound) - .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect( - MockMvcResultMatchers.content().json( + content().json( """ { "message": "$errorMessage", @@ -178,10 +179,10 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { } throws InvalidTargetPriorityException(errorMessage, invalidTargetPriority) executeCombineTimetablesRequest(targetPriority = invalidTargetPriority.value) - .andExpect(MockMvcResultMatchers.status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect( - MockMvcResultMatchers.content().json( + content().json( """ { "message": "$errorMessage", @@ -211,10 +212,10 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { } throws TargetFrameNotFoundException(errorMessage, defaultStagingFrameIds[0]) executeCombineTimetablesRequest() - .andExpect(MockMvcResultMatchers.status().isNotFound) - .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect( - MockMvcResultMatchers.content().json( + content().json( """ { "message": "$errorMessage", @@ -244,10 +245,10 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { } throws MultipleTargetFramesFoundException(errorMessage, defaultStagingFrameIds[0], defaultTargetFrameIds) executeCombineTimetablesRequest() - .andExpect(MockMvcResultMatchers.status().isConflict) - .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isConflict) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect( - MockMvcResultMatchers.content().json( + content().json( """ { "message": "$errorMessage", diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToReplaceApiTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToReplaceApiTest.kt index 6b689bca..f941fe73 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToReplaceApiTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToReplaceApiTest.kt @@ -17,7 +17,7 @@ import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.ResultActions import org.springframework.test.web.servlet.request.MockMvcRequestBuilders -import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.time.LocalDate import java.util.UUID @@ -73,9 +73,9 @@ class TimetablesToReplaceApiTest(@Autowired val mockMvc: MockMvc) { executeToReplaceTimetablesRequest(stagingVehicleScheduleFrameId, targetPriority) .andExpect(status().isOk) - .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect( - MockMvcResultMatchers.content().json( + content().json( """ { "toReplaceVehicleScheduleFrameIds": $defaultToReplaceIds @@ -101,9 +101,9 @@ class TimetablesToReplaceApiTest(@Autowired val mockMvc: MockMvc) { executeToReplaceTimetablesRequest(stagingVehicleScheduleFrameId, invalidTargetPriorityInput) .andExpect(status().isBadRequest) - .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect( - MockMvcResultMatchers.content().json( + content().json( """ { "message": "$errorMessage", From a8d9ae01f94e1b9f0afac80d16fc6643c9955b01 Mon Sep 17 00:00:00 2001 From: Tommi Leinamo Date: Wed, 15 Nov 2023 11:53:09 +0200 Subject: [PATCH 3/9] Update Hasura image, generated sources and hasura submodule To include sequential integrity validation. --- docker/docker-compose.custom.yml | 2 +- jore4-hasura | 2 +- .../PassingTimesSequenceAlreadyValidated.kt | 32 -- ...les.kt => ValidatePassingTimeSequences.kt} | 2 +- .../routines/references/Routines.kt | 32 +- .../jooq/vehicle_service/VehicleService.kt | 38 +++ .../ValidateServiceSequentialIntegrity.kt | 22 ++ .../routines/references/Routines.kt | 42 +++ .../tables/GetVehicleServiceTimingData.kt | 249 ++++++++++++++ .../pojos/GetVehicleServiceTimingData.kt | 207 +++++++++++ .../GetVehicleServiceTimingDataRecord.kt | 320 ++++++++++++++++++ .../tables/references/Tables.kt | 35 ++ 12 files changed, 920 insertions(+), 63 deletions(-) delete mode 100644 src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/PassingTimesSequenceAlreadyValidated.kt rename src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/{CreateValidatePassingTimesSequenceQueueTempTables.kt => ValidatePassingTimeSequences.kt} (63%) create mode 100644 src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/routines/ValidateServiceSequentialIntegrity.kt create mode 100644 src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/GetVehicleServiceTimingData.kt create mode 100644 src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/pojos/GetVehicleServiceTimingData.kt create mode 100644 src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/records/GetVehicleServiceTimingDataRecord.kt diff --git a/docker/docker-compose.custom.yml b/docker/docker-compose.custom.yml index 9cd288f3..9a5e2a55 100644 --- a/docker/docker-compose.custom.yml +++ b/docker/docker-compose.custom.yml @@ -29,7 +29,7 @@ services: # locking hasura image so that we can develop against a static graphql API # Link to available jore4-hasura images in Docker Hub: # https://hub.docker.com/r/hsldevcom/jore4-hasura/tags?page=1&ordering=last_updated - image: "hsldevcom/jore4-hasura:hsl-main--20231019-667942672c4ff4f7493a49903f6ed2bbe35a7615" + image: "hsldevcom/jore4-hasura:hsl-main--20231115-5c29d7e8103145ac12554a01da6e04a9b08be784" # Waiting for database to be ready to avoid startup delay due to hasura crashing at startup if db is offline # Note: this should only be done in development setups as Kubernetes does not allow waiting for services to be ready depends_on: diff --git a/jore4-hasura b/jore4-hasura index 66794267..5c29d7e8 160000 --- a/jore4-hasura +++ b/jore4-hasura @@ -1 +1 @@ -Subproject commit 667942672c4ff4f7493a49903f6ed2bbe35a7615 +Subproject commit 5c29d7e8103145ac12554a01da6e04a9b08be784 diff --git a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/PassingTimesSequenceAlreadyValidated.kt b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/PassingTimesSequenceAlreadyValidated.kt deleted file mode 100644 index 5892cd53..00000000 --- a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/PassingTimesSequenceAlreadyValidated.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * This file is generated by jOOQ. - */ -package fi.hsl.jore.jore4.jooq.passing_times.routines - - -import fi.hsl.jore.jore4.jooq.passing_times.PassingTimes - -import org.jooq.Parameter -import org.jooq.impl.AbstractRoutine -import org.jooq.impl.Internal -import org.jooq.impl.SQLDataType - - -/** - * This class is generated by jOOQ. - */ -@Suppress("UNCHECKED_CAST") -open class PassingTimesSequenceAlreadyValidated : AbstractRoutine("passing_times_sequence_already_validated", PassingTimes.PASSING_TIMES, SQLDataType.BOOLEAN) { - companion object { - - /** - * The parameter - * passing_times.passing_times_sequence_already_validated.RETURN_VALUE. - */ - val RETURN_VALUE: Parameter = Internal.createParameter("RETURN_VALUE", SQLDataType.BOOLEAN, false, false) - } - - init { - returnParameter = RETURN_VALUE - } -} diff --git a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/CreateValidatePassingTimesSequenceQueueTempTables.kt b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/ValidatePassingTimeSequences.kt similarity index 63% rename from src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/CreateValidatePassingTimesSequenceQueueTempTables.kt rename to src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/ValidatePassingTimeSequences.kt index 7a92ddbb..2f31ad61 100644 --- a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/CreateValidatePassingTimesSequenceQueueTempTables.kt +++ b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/ValidatePassingTimeSequences.kt @@ -13,7 +13,7 @@ import org.jooq.impl.AbstractRoutine * This class is generated by jOOQ. */ @Suppress("UNCHECKED_CAST") -open class CreateValidatePassingTimesSequenceQueueTempTables : AbstractRoutine("create_validate_passing_times_sequence_queue_temp_tables", PassingTimes.PASSING_TIMES) { +open class ValidatePassingTimeSequences : AbstractRoutine("validate_passing_time_sequences", PassingTimes.PASSING_TIMES) { companion object { } diff --git a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/references/Routines.kt b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/references/Routines.kt index befacaac..95f683b3 100644 --- a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/references/Routines.kt +++ b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/passing_times/routines/references/Routines.kt @@ -4,8 +4,7 @@ package fi.hsl.jore.jore4.jooq.passing_times.routines.references -import fi.hsl.jore.jore4.jooq.passing_times.routines.CreateValidatePassingTimesSequenceQueueTempTables -import fi.hsl.jore.jore4.jooq.passing_times.routines.PassingTimesSequenceAlreadyValidated +import fi.hsl.jore.jore4.jooq.passing_times.routines.ValidatePassingTimeSequences import fi.hsl.jore.jore4.jooq.passing_times.tables.GetPassingTimeOrderValidityData import fi.hsl.jore.jore4.jooq.passing_times.tables.records.GetPassingTimeOrderValidityDataRecord @@ -18,39 +17,16 @@ import org.jooq.Result /** - * Call - * passing_times.create_validate_passing_times_sequence_queue_temp_tables + * Call passing_times.validate_passing_time_sequences */ -fun createValidatePassingTimesSequenceQueueTempTables( +fun validatePassingTimeSequences( configuration: Configuration ): Unit { - val p = CreateValidatePassingTimesSequenceQueueTempTables() + val p = ValidatePassingTimeSequences() p.execute(configuration) } -/** - * Call passing_times.passing_times_sequence_already_validated - */ -fun passingTimesSequenceAlreadyValidated( - configuration: Configuration -): Boolean? { - val f = PassingTimesSequenceAlreadyValidated() - - f.execute(configuration) - return f.returnValue -} - -/** - * Get passing_times.passing_times_sequence_already_validated as a - * field. - */ -fun passingTimesSequenceAlreadyValidated(): Field { - val f = PassingTimesSequenceAlreadyValidated() - - return f.asField() -} - /** * Call passing_times.get_passing_time_order_validity_data. */ diff --git a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/VehicleService.kt b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/VehicleService.kt index 3655611b..77fd0cc9 100644 --- a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/VehicleService.kt +++ b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/VehicleService.kt @@ -9,7 +9,9 @@ import fi.hsl.jore.jore4.jooq.return_value.tables.records.TimetableVersionRecord import fi.hsl.jore.jore4.jooq.vehicle_service.tables.Block import fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetTimetableVersionsByJourneyPatternIds import fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetTimetablesAndSubstituteOperatingDays +import fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData import fi.hsl.jore.jore4.jooq.vehicle_service.tables.JourneyPatternsInVehicleService +import fi.hsl.jore.jore4.jooq.vehicle_service.tables.records.GetVehicleServiceTimingDataRecord import java.time.LocalDate import java.util.UUID @@ -154,6 +156,41 @@ open class VehicleService : SchemaImpl("vehicle_service", DefaultCatalog.DEFAULT endDate ) + /** + * The table vehicle_service.get_vehicle_service_timing_data. + */ + val GET_VEHICLE_SERVICE_TIMING_DATA: GetVehicleServiceTimingData get() = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA + + /** + * Call vehicle_service.get_vehicle_service_timing_data. + */ + fun GET_VEHICLE_SERVICE_TIMING_DATA( + configuration: Configuration + , vehicleServiceIds: Array? + ): Result = configuration.dsl().selectFrom(fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.call( + vehicleServiceIds + )).fetch() + + /** + * Get vehicle_service.get_vehicle_service_timing_data as a + * table. + */ + fun GET_VEHICLE_SERVICE_TIMING_DATA( + vehicleServiceIds: Array? + ): GetVehicleServiceTimingData = fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.call( + vehicleServiceIds + ) + + /** + * Get vehicle_service.get_vehicle_service_timing_data as a + * table. + */ + fun GET_VEHICLE_SERVICE_TIMING_DATA( + vehicleServiceIds: Field?> + ): GetVehicleServiceTimingData = fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.call( + vehicleServiceIds + ) + /** * A denormalized table containing relationships between vehicle_services * and journey_patterns (via journey_pattern_ref.journey_pattern_id). @@ -177,6 +214,7 @@ open class VehicleService : SchemaImpl("vehicle_service", DefaultCatalog.DEFAULT Block.BLOCK, GetTimetableVersionsByJourneyPatternIds.GET_TIMETABLE_VERSIONS_BY_JOURNEY_PATTERN_IDS, GetTimetablesAndSubstituteOperatingDays.GET_TIMETABLES_AND_SUBSTITUTE_OPERATING_DAYS, + GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA, JourneyPatternsInVehicleService.JOURNEY_PATTERNS_IN_VEHICLE_SERVICE, fi.hsl.jore.jore4.jooq.vehicle_service.tables.VehicleService.VEHICLE_SERVICE_ ) diff --git a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/routines/ValidateServiceSequentialIntegrity.kt b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/routines/ValidateServiceSequentialIntegrity.kt new file mode 100644 index 00000000..b067dc9c --- /dev/null +++ b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/routines/ValidateServiceSequentialIntegrity.kt @@ -0,0 +1,22 @@ +/* + * This file is generated by jOOQ. + */ +package fi.hsl.jore.jore4.jooq.vehicle_service.routines + + +import fi.hsl.jore.jore4.jooq.vehicle_service.VehicleService + +import org.jooq.impl.AbstractRoutine + + +/** + * This class is generated by jOOQ. + */ +@Suppress("UNCHECKED_CAST") +open class ValidateServiceSequentialIntegrity : AbstractRoutine("validate_service_sequential_integrity", VehicleService.VEHICLE_SERVICE) { + companion object { + } + + init { + } +} diff --git a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/routines/references/Routines.kt b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/routines/references/Routines.kt index 275ea644..d9ce423a 100644 --- a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/routines/references/Routines.kt +++ b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/routines/references/Routines.kt @@ -6,8 +6,11 @@ package fi.hsl.jore.jore4.jooq.vehicle_service.routines.references import fi.hsl.jore.jore4.jooq.return_value.tables.records.TimetableVersionRecord import fi.hsl.jore.jore4.jooq.vehicle_service.routines.RefreshJourneyPatternsInVehicleService +import fi.hsl.jore.jore4.jooq.vehicle_service.routines.ValidateServiceSequentialIntegrity import fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetTimetableVersionsByJourneyPatternIds import fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetTimetablesAndSubstituteOperatingDays +import fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData +import fi.hsl.jore.jore4.jooq.vehicle_service.tables.records.GetVehicleServiceTimingDataRecord import java.time.LocalDate import java.util.UUID @@ -29,6 +32,17 @@ fun refreshJourneyPatternsInVehicleService( p.execute(configuration) } +/** + * Call vehicle_service.validate_service_sequential_integrity + */ +fun validateServiceSequentialIntegrity( + configuration: Configuration +): Unit { + val p = ValidateServiceSequentialIntegrity() + + p.execute(configuration) +} + /** * Call * vehicle_service.get_timetable_versions_by_journey_pattern_ids. @@ -122,3 +136,31 @@ fun getTimetablesAndSubstituteOperatingDays( startDate, endDate ) + +/** + * Call vehicle_service.get_vehicle_service_timing_data. + */ +fun getVehicleServiceTimingData( + configuration: Configuration + , vehicleServiceIds: Array? +): Result = configuration.dsl().selectFrom(fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.call( + vehicleServiceIds +)).fetch() + +/** + * Get vehicle_service.get_vehicle_service_timing_data as a table. + */ +fun getVehicleServiceTimingData( + vehicleServiceIds: Array? +): GetVehicleServiceTimingData = fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.call( + vehicleServiceIds +) + +/** + * Get vehicle_service.get_vehicle_service_timing_data as a table. + */ +fun getVehicleServiceTimingData( + vehicleServiceIds: Field?> +): GetVehicleServiceTimingData = fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.call( + vehicleServiceIds +) diff --git a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/GetVehicleServiceTimingData.kt b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/GetVehicleServiceTimingData.kt new file mode 100644 index 00000000..79c5b584 --- /dev/null +++ b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/GetVehicleServiceTimingData.kt @@ -0,0 +1,249 @@ +/* + * This file is generated by jOOQ. + */ +package fi.hsl.jore.jore4.jooq.vehicle_service.tables + + +import fi.hsl.jore.jore4.jooq.vehicle_service.VehicleService +import fi.hsl.jore.jore4.jooq.vehicle_service.tables.records.GetVehicleServiceTimingDataRecord + +import java.util.UUID +import java.util.function.Function + +import org.jooq.Field +import org.jooq.ForeignKey +import org.jooq.Name +import org.jooq.Record +import org.jooq.Records +import org.jooq.Row18 +import org.jooq.Schema +import org.jooq.SelectField +import org.jooq.Table +import org.jooq.TableField +import org.jooq.TableOptions +import org.jooq.impl.DSL +import org.jooq.impl.SQLDataType +import org.jooq.impl.TableImpl +import org.jooq.types.YearToSecond + + +/** + * This class is generated by jOOQ. + */ +@Suppress("UNCHECKED_CAST") +open class GetVehicleServiceTimingData( + alias: Name, + child: Table?, + path: ForeignKey?, + aliased: Table?, + parameters: Array?>? +): TableImpl( + alias, + VehicleService.VEHICLE_SERVICE, + child, + path, + aliased, + parameters, + DSL.comment(""), + TableOptions.function() +) { + companion object { + + /** + * The reference instance of + * vehicle_service.get_vehicle_service_timing_data + */ + val GET_VEHICLE_SERVICE_TIMING_DATA: GetVehicleServiceTimingData = GetVehicleServiceTimingData() + } + + /** + * The class holding records for this type + */ + override fun getRecordType(): Class = GetVehicleServiceTimingDataRecord::class.java + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.vehicle_service_id. + */ + val VEHICLE_SERVICE_ID: TableField = createField(DSL.name("vehicle_service_id"), SQLDataType.UUID, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.service_start. + */ + val SERVICE_START: TableField = createField(DSL.name("service_start"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.service_end. + */ + val SERVICE_END: TableField = createField(DSL.name("service_end"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.block_id. + */ + val BLOCK_ID: TableField = createField(DSL.name("block_id"), SQLDataType.UUID, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.block_start. + */ + val BLOCK_START: TableField = createField(DSL.name("block_start"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.block_end. + */ + val BLOCK_END: TableField = createField(DSL.name("block_end"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.preparing_time. + */ + val PREPARING_TIME: TableField = createField(DSL.name("preparing_time"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.finishing_time. + */ + val FINISHING_TIME: TableField = createField(DSL.name("finishing_time"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.vehicle_journey_id. + */ + val VEHICLE_JOURNEY_ID: TableField = createField(DSL.name("vehicle_journey_id"), SQLDataType.UUID, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.journey_start. + */ + val JOURNEY_START: TableField = createField(DSL.name("journey_start"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.journey_end. + */ + val JOURNEY_END: TableField = createField(DSL.name("journey_end"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.journey_first_stop_departure. + */ + val JOURNEY_FIRST_STOP_DEPARTURE: TableField = createField(DSL.name("journey_first_stop_departure"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.journey_last_stop_arrival. + */ + val JOURNEY_LAST_STOP_ARRIVAL: TableField = createField(DSL.name("journey_last_stop_arrival"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.turnaround_time. + */ + val TURNAROUND_TIME: TableField = createField(DSL.name("turnaround_time"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.layover_time. + */ + val LAYOVER_TIME: TableField = createField(DSL.name("layover_time"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.timetabled_passing_time_id. + */ + val TIMETABLED_PASSING_TIME_ID: TableField = createField(DSL.name("timetabled_passing_time_id"), SQLDataType.UUID, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.stop_departure_time. + */ + val STOP_DEPARTURE_TIME: TableField = createField(DSL.name("stop_departure_time"), SQLDataType.INTERVAL, this, "") + + /** + * The column + * vehicle_service.get_vehicle_service_timing_data.stop_arrival_time. + */ + val STOP_ARRIVAL_TIME: TableField = createField(DSL.name("stop_arrival_time"), SQLDataType.INTERVAL, this, "") + + private constructor(alias: Name, aliased: Table?): this(alias, null, null, aliased, arrayOf( + DSL.value(null, SQLDataType.UUID.array()) + )) + private constructor(alias: Name, aliased: Table?, parameters: Array?>?): this(alias, null, null, aliased, parameters) + + /** + * Create an aliased + * vehicle_service.get_vehicle_service_timing_data table + * reference + */ + constructor(alias: String): this(DSL.name(alias)) + + /** + * Create an aliased + * vehicle_service.get_vehicle_service_timing_data table + * reference + */ + constructor(alias: Name): this(alias, null) + + /** + * Create a vehicle_service.get_vehicle_service_timing_data + * table reference + */ + constructor(): this(DSL.name("get_vehicle_service_timing_data"), null) + override fun getSchema(): Schema? = if (aliased()) null else VehicleService.VEHICLE_SERVICE + override fun `as`(alias: String): GetVehicleServiceTimingData = GetVehicleServiceTimingData(DSL.name(alias), this, parameters) + override fun `as`(alias: Name): GetVehicleServiceTimingData = GetVehicleServiceTimingData(alias, this, parameters) + override fun `as`(alias: Table<*>): GetVehicleServiceTimingData = GetVehicleServiceTimingData(alias.getQualifiedName(), this, parameters) + + /** + * Rename this table + */ + override fun rename(name: String): GetVehicleServiceTimingData = GetVehicleServiceTimingData(DSL.name(name), null, parameters) + + /** + * Rename this table + */ + override fun rename(name: Name): GetVehicleServiceTimingData = GetVehicleServiceTimingData(name, null, parameters) + + /** + * Rename this table + */ + override fun rename(name: Table<*>): GetVehicleServiceTimingData = GetVehicleServiceTimingData(name.getQualifiedName(), null, parameters) + + // ------------------------------------------------------------------------- + // Row18 type methods + // ------------------------------------------------------------------------- + override fun fieldsRow(): Row18 = super.fieldsRow() as Row18 + + /** + * Call this table-valued function + */ + fun call( + vehicleServiceIds: Array? + ): GetVehicleServiceTimingData = GetVehicleServiceTimingData(DSL.name("get_vehicle_service_timing_data"), null, arrayOf( + DSL.value(vehicleServiceIds, SQLDataType.UUID.array()) + )).let { if (aliased()) it.`as`(unqualifiedName) else it } + + /** + * Call this table-valued function + */ + fun call( + vehicleServiceIds: Field?> + ): GetVehicleServiceTimingData = GetVehicleServiceTimingData(DSL.name("get_vehicle_service_timing_data"), null, arrayOf( + vehicleServiceIds + )).let { if (aliased()) it.`as`(unqualifiedName) else it } + + /** + * Convenience mapping calling {@link SelectField#convertFrom(Function)}. + */ + fun mapping(from: (UUID?, YearToSecond?, YearToSecond?, UUID?, YearToSecond?, YearToSecond?, YearToSecond?, YearToSecond?, UUID?, YearToSecond?, YearToSecond?, YearToSecond?, YearToSecond?, YearToSecond?, YearToSecond?, UUID?, YearToSecond?, YearToSecond?) -> U): SelectField = convertFrom(Records.mapping(from)) + + /** + * Convenience mapping calling {@link SelectField#convertFrom(Class, + * Function)}. + */ + fun mapping(toType: Class, from: (UUID?, YearToSecond?, YearToSecond?, UUID?, YearToSecond?, YearToSecond?, YearToSecond?, YearToSecond?, UUID?, YearToSecond?, YearToSecond?, YearToSecond?, YearToSecond?, YearToSecond?, YearToSecond?, UUID?, YearToSecond?, YearToSecond?) -> U): SelectField = convertFrom(toType, Records.mapping(from)) +} diff --git a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/pojos/GetVehicleServiceTimingData.kt b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/pojos/GetVehicleServiceTimingData.kt new file mode 100644 index 00000000..edb11537 --- /dev/null +++ b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/pojos/GetVehicleServiceTimingData.kt @@ -0,0 +1,207 @@ +/* + * This file is generated by jOOQ. + */ +package fi.hsl.jore.jore4.jooq.vehicle_service.tables.pojos + + +import java.io.Serializable +import java.util.UUID + +import org.jooq.types.YearToSecond + + +/** + * This class is generated by jOOQ. + */ +@Suppress("UNCHECKED_CAST") +data class GetVehicleServiceTimingData( + var vehicleServiceId: UUID? = null, + var serviceStart: YearToSecond? = null, + var serviceEnd: YearToSecond? = null, + var blockId: UUID? = null, + var blockStart: YearToSecond? = null, + var blockEnd: YearToSecond? = null, + var preparingTime: YearToSecond? = null, + var finishingTime: YearToSecond? = null, + var vehicleJourneyId: UUID? = null, + var journeyStart: YearToSecond? = null, + var journeyEnd: YearToSecond? = null, + var journeyFirstStopDeparture: YearToSecond? = null, + var journeyLastStopArrival: YearToSecond? = null, + var turnaroundTime: YearToSecond? = null, + var layoverTime: YearToSecond? = null, + var timetabledPassingTimeId: UUID? = null, + var stopDepartureTime: YearToSecond? = null, + var stopArrivalTime: YearToSecond? = null +): Serializable { + + + override fun equals(other: Any?): Boolean { + if (this === other) + return true + if (other == null) + return false + if (this::class != other::class) + return false + val o: GetVehicleServiceTimingData = other as GetVehicleServiceTimingData + if (this.vehicleServiceId == null) { + if (o.vehicleServiceId != null) + return false + } + else if (this.vehicleServiceId != o.vehicleServiceId) + return false + if (this.serviceStart == null) { + if (o.serviceStart != null) + return false + } + else if (this.serviceStart != o.serviceStart) + return false + if (this.serviceEnd == null) { + if (o.serviceEnd != null) + return false + } + else if (this.serviceEnd != o.serviceEnd) + return false + if (this.blockId == null) { + if (o.blockId != null) + return false + } + else if (this.blockId != o.blockId) + return false + if (this.blockStart == null) { + if (o.blockStart != null) + return false + } + else if (this.blockStart != o.blockStart) + return false + if (this.blockEnd == null) { + if (o.blockEnd != null) + return false + } + else if (this.blockEnd != o.blockEnd) + return false + if (this.preparingTime == null) { + if (o.preparingTime != null) + return false + } + else if (this.preparingTime != o.preparingTime) + return false + if (this.finishingTime == null) { + if (o.finishingTime != null) + return false + } + else if (this.finishingTime != o.finishingTime) + return false + if (this.vehicleJourneyId == null) { + if (o.vehicleJourneyId != null) + return false + } + else if (this.vehicleJourneyId != o.vehicleJourneyId) + return false + if (this.journeyStart == null) { + if (o.journeyStart != null) + return false + } + else if (this.journeyStart != o.journeyStart) + return false + if (this.journeyEnd == null) { + if (o.journeyEnd != null) + return false + } + else if (this.journeyEnd != o.journeyEnd) + return false + if (this.journeyFirstStopDeparture == null) { + if (o.journeyFirstStopDeparture != null) + return false + } + else if (this.journeyFirstStopDeparture != o.journeyFirstStopDeparture) + return false + if (this.journeyLastStopArrival == null) { + if (o.journeyLastStopArrival != null) + return false + } + else if (this.journeyLastStopArrival != o.journeyLastStopArrival) + return false + if (this.turnaroundTime == null) { + if (o.turnaroundTime != null) + return false + } + else if (this.turnaroundTime != o.turnaroundTime) + return false + if (this.layoverTime == null) { + if (o.layoverTime != null) + return false + } + else if (this.layoverTime != o.layoverTime) + return false + if (this.timetabledPassingTimeId == null) { + if (o.timetabledPassingTimeId != null) + return false + } + else if (this.timetabledPassingTimeId != o.timetabledPassingTimeId) + return false + if (this.stopDepartureTime == null) { + if (o.stopDepartureTime != null) + return false + } + else if (this.stopDepartureTime != o.stopDepartureTime) + return false + if (this.stopArrivalTime == null) { + if (o.stopArrivalTime != null) + return false + } + else if (this.stopArrivalTime != o.stopArrivalTime) + return false + return true + } + + override fun hashCode(): Int { + val prime = 31 + var result = 1 + result = prime * result + (if (this.vehicleServiceId == null) 0 else this.vehicleServiceId.hashCode()) + result = prime * result + (if (this.serviceStart == null) 0 else this.serviceStart.hashCode()) + result = prime * result + (if (this.serviceEnd == null) 0 else this.serviceEnd.hashCode()) + result = prime * result + (if (this.blockId == null) 0 else this.blockId.hashCode()) + result = prime * result + (if (this.blockStart == null) 0 else this.blockStart.hashCode()) + result = prime * result + (if (this.blockEnd == null) 0 else this.blockEnd.hashCode()) + result = prime * result + (if (this.preparingTime == null) 0 else this.preparingTime.hashCode()) + result = prime * result + (if (this.finishingTime == null) 0 else this.finishingTime.hashCode()) + result = prime * result + (if (this.vehicleJourneyId == null) 0 else this.vehicleJourneyId.hashCode()) + result = prime * result + (if (this.journeyStart == null) 0 else this.journeyStart.hashCode()) + result = prime * result + (if (this.journeyEnd == null) 0 else this.journeyEnd.hashCode()) + result = prime * result + (if (this.journeyFirstStopDeparture == null) 0 else this.journeyFirstStopDeparture.hashCode()) + result = prime * result + (if (this.journeyLastStopArrival == null) 0 else this.journeyLastStopArrival.hashCode()) + result = prime * result + (if (this.turnaroundTime == null) 0 else this.turnaroundTime.hashCode()) + result = prime * result + (if (this.layoverTime == null) 0 else this.layoverTime.hashCode()) + result = prime * result + (if (this.timetabledPassingTimeId == null) 0 else this.timetabledPassingTimeId.hashCode()) + result = prime * result + (if (this.stopDepartureTime == null) 0 else this.stopDepartureTime.hashCode()) + result = prime * result + (if (this.stopArrivalTime == null) 0 else this.stopArrivalTime.hashCode()) + return result + } + + override fun toString(): String { + val sb = StringBuilder("GetVehicleServiceTimingData (") + + sb.append(vehicleServiceId) + sb.append(", ").append(serviceStart) + sb.append(", ").append(serviceEnd) + sb.append(", ").append(blockId) + sb.append(", ").append(blockStart) + sb.append(", ").append(blockEnd) + sb.append(", ").append(preparingTime) + sb.append(", ").append(finishingTime) + sb.append(", ").append(vehicleJourneyId) + sb.append(", ").append(journeyStart) + sb.append(", ").append(journeyEnd) + sb.append(", ").append(journeyFirstStopDeparture) + sb.append(", ").append(journeyLastStopArrival) + sb.append(", ").append(turnaroundTime) + sb.append(", ").append(layoverTime) + sb.append(", ").append(timetabledPassingTimeId) + sb.append(", ").append(stopDepartureTime) + sb.append(", ").append(stopArrivalTime) + + sb.append(")") + return sb.toString() + } +} diff --git a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/records/GetVehicleServiceTimingDataRecord.kt b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/records/GetVehicleServiceTimingDataRecord.kt new file mode 100644 index 00000000..f0b52144 --- /dev/null +++ b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/records/GetVehicleServiceTimingDataRecord.kt @@ -0,0 +1,320 @@ +/* + * This file is generated by jOOQ. + */ +package fi.hsl.jore.jore4.jooq.vehicle_service.tables.records + + +import fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData + +import java.util.UUID + +import org.jooq.Field +import org.jooq.Record18 +import org.jooq.Row18 +import org.jooq.impl.TableRecordImpl +import org.jooq.types.YearToSecond + + +/** + * This class is generated by jOOQ. + */ +@Suppress("UNCHECKED_CAST") +open class GetVehicleServiceTimingDataRecord private constructor() : TableRecordImpl(GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA), Record18 { + + open var vehicleServiceId: UUID? + set(value): Unit = set(0, value) + get(): UUID? = get(0) as UUID? + + open var serviceStart: YearToSecond? + set(value): Unit = set(1, value) + get(): YearToSecond? = get(1) as YearToSecond? + + open var serviceEnd: YearToSecond? + set(value): Unit = set(2, value) + get(): YearToSecond? = get(2) as YearToSecond? + + open var blockId: UUID? + set(value): Unit = set(3, value) + get(): UUID? = get(3) as UUID? + + open var blockStart: YearToSecond? + set(value): Unit = set(4, value) + get(): YearToSecond? = get(4) as YearToSecond? + + open var blockEnd: YearToSecond? + set(value): Unit = set(5, value) + get(): YearToSecond? = get(5) as YearToSecond? + + open var preparingTime: YearToSecond? + set(value): Unit = set(6, value) + get(): YearToSecond? = get(6) as YearToSecond? + + open var finishingTime: YearToSecond? + set(value): Unit = set(7, value) + get(): YearToSecond? = get(7) as YearToSecond? + + open var vehicleJourneyId: UUID? + set(value): Unit = set(8, value) + get(): UUID? = get(8) as UUID? + + open var journeyStart: YearToSecond? + set(value): Unit = set(9, value) + get(): YearToSecond? = get(9) as YearToSecond? + + open var journeyEnd: YearToSecond? + set(value): Unit = set(10, value) + get(): YearToSecond? = get(10) as YearToSecond? + + open var journeyFirstStopDeparture: YearToSecond? + set(value): Unit = set(11, value) + get(): YearToSecond? = get(11) as YearToSecond? + + open var journeyLastStopArrival: YearToSecond? + set(value): Unit = set(12, value) + get(): YearToSecond? = get(12) as YearToSecond? + + open var turnaroundTime: YearToSecond? + set(value): Unit = set(13, value) + get(): YearToSecond? = get(13) as YearToSecond? + + open var layoverTime: YearToSecond? + set(value): Unit = set(14, value) + get(): YearToSecond? = get(14) as YearToSecond? + + open var timetabledPassingTimeId: UUID? + set(value): Unit = set(15, value) + get(): UUID? = get(15) as UUID? + + open var stopDepartureTime: YearToSecond? + set(value): Unit = set(16, value) + get(): YearToSecond? = get(16) as YearToSecond? + + open var stopArrivalTime: YearToSecond? + set(value): Unit = set(17, value) + get(): YearToSecond? = get(17) as YearToSecond? + + // ------------------------------------------------------------------------- + // Record18 type implementation + // ------------------------------------------------------------------------- + + override fun fieldsRow(): Row18 = super.fieldsRow() as Row18 + override fun valuesRow(): Row18 = super.valuesRow() as Row18 + override fun field1(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.VEHICLE_SERVICE_ID + override fun field2(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.SERVICE_START + override fun field3(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.SERVICE_END + override fun field4(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.BLOCK_ID + override fun field5(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.BLOCK_START + override fun field6(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.BLOCK_END + override fun field7(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.PREPARING_TIME + override fun field8(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.FINISHING_TIME + override fun field9(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.VEHICLE_JOURNEY_ID + override fun field10(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.JOURNEY_START + override fun field11(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.JOURNEY_END + override fun field12(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.JOURNEY_FIRST_STOP_DEPARTURE + override fun field13(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.JOURNEY_LAST_STOP_ARRIVAL + override fun field14(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.TURNAROUND_TIME + override fun field15(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.LAYOVER_TIME + override fun field16(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.TIMETABLED_PASSING_TIME_ID + override fun field17(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.STOP_DEPARTURE_TIME + override fun field18(): Field = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.STOP_ARRIVAL_TIME + override fun component1(): UUID? = vehicleServiceId + override fun component2(): YearToSecond? = serviceStart + override fun component3(): YearToSecond? = serviceEnd + override fun component4(): UUID? = blockId + override fun component5(): YearToSecond? = blockStart + override fun component6(): YearToSecond? = blockEnd + override fun component7(): YearToSecond? = preparingTime + override fun component8(): YearToSecond? = finishingTime + override fun component9(): UUID? = vehicleJourneyId + override fun component10(): YearToSecond? = journeyStart + override fun component11(): YearToSecond? = journeyEnd + override fun component12(): YearToSecond? = journeyFirstStopDeparture + override fun component13(): YearToSecond? = journeyLastStopArrival + override fun component14(): YearToSecond? = turnaroundTime + override fun component15(): YearToSecond? = layoverTime + override fun component16(): UUID? = timetabledPassingTimeId + override fun component17(): YearToSecond? = stopDepartureTime + override fun component18(): YearToSecond? = stopArrivalTime + override fun value1(): UUID? = vehicleServiceId + override fun value2(): YearToSecond? = serviceStart + override fun value3(): YearToSecond? = serviceEnd + override fun value4(): UUID? = blockId + override fun value5(): YearToSecond? = blockStart + override fun value6(): YearToSecond? = blockEnd + override fun value7(): YearToSecond? = preparingTime + override fun value8(): YearToSecond? = finishingTime + override fun value9(): UUID? = vehicleJourneyId + override fun value10(): YearToSecond? = journeyStart + override fun value11(): YearToSecond? = journeyEnd + override fun value12(): YearToSecond? = journeyFirstStopDeparture + override fun value13(): YearToSecond? = journeyLastStopArrival + override fun value14(): YearToSecond? = turnaroundTime + override fun value15(): YearToSecond? = layoverTime + override fun value16(): UUID? = timetabledPassingTimeId + override fun value17(): YearToSecond? = stopDepartureTime + override fun value18(): YearToSecond? = stopArrivalTime + + override fun value1(value: UUID?): GetVehicleServiceTimingDataRecord { + set(0, value) + return this + } + + override fun value2(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(1, value) + return this + } + + override fun value3(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(2, value) + return this + } + + override fun value4(value: UUID?): GetVehicleServiceTimingDataRecord { + set(3, value) + return this + } + + override fun value5(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(4, value) + return this + } + + override fun value6(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(5, value) + return this + } + + override fun value7(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(6, value) + return this + } + + override fun value8(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(7, value) + return this + } + + override fun value9(value: UUID?): GetVehicleServiceTimingDataRecord { + set(8, value) + return this + } + + override fun value10(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(9, value) + return this + } + + override fun value11(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(10, value) + return this + } + + override fun value12(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(11, value) + return this + } + + override fun value13(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(12, value) + return this + } + + override fun value14(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(13, value) + return this + } + + override fun value15(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(14, value) + return this + } + + override fun value16(value: UUID?): GetVehicleServiceTimingDataRecord { + set(15, value) + return this + } + + override fun value17(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(16, value) + return this + } + + override fun value18(value: YearToSecond?): GetVehicleServiceTimingDataRecord { + set(17, value) + return this + } + + override fun values(value1: UUID?, value2: YearToSecond?, value3: YearToSecond?, value4: UUID?, value5: YearToSecond?, value6: YearToSecond?, value7: YearToSecond?, value8: YearToSecond?, value9: UUID?, value10: YearToSecond?, value11: YearToSecond?, value12: YearToSecond?, value13: YearToSecond?, value14: YearToSecond?, value15: YearToSecond?, value16: UUID?, value17: YearToSecond?, value18: YearToSecond?): GetVehicleServiceTimingDataRecord { + this.value1(value1) + this.value2(value2) + this.value3(value3) + this.value4(value4) + this.value5(value5) + this.value6(value6) + this.value7(value7) + this.value8(value8) + this.value9(value9) + this.value10(value10) + this.value11(value11) + this.value12(value12) + this.value13(value13) + this.value14(value14) + this.value15(value15) + this.value16(value16) + this.value17(value17) + this.value18(value18) + return this + } + + /** + * Create a detached, initialised GetVehicleServiceTimingDataRecord + */ + constructor(vehicleServiceId: UUID? = null, serviceStart: YearToSecond? = null, serviceEnd: YearToSecond? = null, blockId: UUID? = null, blockStart: YearToSecond? = null, blockEnd: YearToSecond? = null, preparingTime: YearToSecond? = null, finishingTime: YearToSecond? = null, vehicleJourneyId: UUID? = null, journeyStart: YearToSecond? = null, journeyEnd: YearToSecond? = null, journeyFirstStopDeparture: YearToSecond? = null, journeyLastStopArrival: YearToSecond? = null, turnaroundTime: YearToSecond? = null, layoverTime: YearToSecond? = null, timetabledPassingTimeId: UUID? = null, stopDepartureTime: YearToSecond? = null, stopArrivalTime: YearToSecond? = null): this() { + this.vehicleServiceId = vehicleServiceId + this.serviceStart = serviceStart + this.serviceEnd = serviceEnd + this.blockId = blockId + this.blockStart = blockStart + this.blockEnd = blockEnd + this.preparingTime = preparingTime + this.finishingTime = finishingTime + this.vehicleJourneyId = vehicleJourneyId + this.journeyStart = journeyStart + this.journeyEnd = journeyEnd + this.journeyFirstStopDeparture = journeyFirstStopDeparture + this.journeyLastStopArrival = journeyLastStopArrival + this.turnaroundTime = turnaroundTime + this.layoverTime = layoverTime + this.timetabledPassingTimeId = timetabledPassingTimeId + this.stopDepartureTime = stopDepartureTime + this.stopArrivalTime = stopArrivalTime + resetChangedOnNotNull() + } + + /** + * Create a detached, initialised GetVehicleServiceTimingDataRecord + */ + constructor(value: fi.hsl.jore.jore4.jooq.vehicle_service.tables.pojos.GetVehicleServiceTimingData?): this() { + if (value != null) { + this.vehicleServiceId = value.vehicleServiceId + this.serviceStart = value.serviceStart + this.serviceEnd = value.serviceEnd + this.blockId = value.blockId + this.blockStart = value.blockStart + this.blockEnd = value.blockEnd + this.preparingTime = value.preparingTime + this.finishingTime = value.finishingTime + this.vehicleJourneyId = value.vehicleJourneyId + this.journeyStart = value.journeyStart + this.journeyEnd = value.journeyEnd + this.journeyFirstStopDeparture = value.journeyFirstStopDeparture + this.journeyLastStopArrival = value.journeyLastStopArrival + this.turnaroundTime = value.turnaroundTime + this.layoverTime = value.layoverTime + this.timetabledPassingTimeId = value.timetabledPassingTimeId + this.stopDepartureTime = value.stopDepartureTime + this.stopArrivalTime = value.stopArrivalTime + resetChangedOnNotNull() + } + } +} diff --git a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/references/Tables.kt b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/references/Tables.kt index d105a616..59b06497 100644 --- a/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/references/Tables.kt +++ b/src/generated-sources/jooq/fi/hsl/jore/jore4/jooq/vehicle_service/tables/references/Tables.kt @@ -8,8 +8,10 @@ import fi.hsl.jore.jore4.jooq.return_value.tables.records.TimetableVersionRecord import fi.hsl.jore.jore4.jooq.vehicle_service.tables.Block import fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetTimetableVersionsByJourneyPatternIds import fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetTimetablesAndSubstituteOperatingDays +import fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData import fi.hsl.jore.jore4.jooq.vehicle_service.tables.JourneyPatternsInVehicleService import fi.hsl.jore.jore4.jooq.vehicle_service.tables.VehicleService +import fi.hsl.jore.jore4.jooq.vehicle_service.tables.records.GetVehicleServiceTimingDataRecord import java.time.LocalDate import java.util.UUID @@ -135,6 +137,39 @@ fun GET_TIMETABLES_AND_SUBSTITUTE_OPERATING_DAYS( endDate ) +/** + * The table vehicle_service.get_vehicle_service_timing_data. + */ +val GET_VEHICLE_SERVICE_TIMING_DATA: GetVehicleServiceTimingData = GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA + +/** + * Call vehicle_service.get_vehicle_service_timing_data. + */ +fun GET_VEHICLE_SERVICE_TIMING_DATA( + configuration: Configuration + , vehicleServiceIds: Array? +): Result = configuration.dsl().selectFrom(fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.call( + vehicleServiceIds +)).fetch() + +/** + * Get vehicle_service.get_vehicle_service_timing_data as a table. + */ +fun GET_VEHICLE_SERVICE_TIMING_DATA( + vehicleServiceIds: Array? +): GetVehicleServiceTimingData = fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.call( + vehicleServiceIds +) + +/** + * Get vehicle_service.get_vehicle_service_timing_data as a table. + */ +fun GET_VEHICLE_SERVICE_TIMING_DATA( + vehicleServiceIds: Field?> +): GetVehicleServiceTimingData = fi.hsl.jore.jore4.jooq.vehicle_service.tables.GetVehicleServiceTimingData.GET_VEHICLE_SERVICE_TIMING_DATA.call( + vehicleServiceIds +) + /** * A denormalized table containing relationships between vehicle_services and * journey_patterns (via journey_pattern_ref.journey_pattern_id). From 29b809ac7135445357b7ff5e7d025cda1f428a28 Mon Sep 17 00:00:00 2001 From: Tommi Leinamo Date: Wed, 15 Nov 2023 12:23:01 +0200 Subject: [PATCH 4/9] Fix sequential integrity issues in test data --- src/test/resources/datasets/combine.json | 12 ++++++------ .../resources/datasets/combine_multiple_targets.json | 12 ++++++------ .../datasets/replace_multiple_replaced.json | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/test/resources/datasets/combine.json b/src/test/resources/datasets/combine.json index 64bccedc..ac51489a 100644 --- a/src/test/resources/datasets/combine.json +++ b/src/test/resources/datasets/combine.json @@ -44,21 +44,21 @@ { "_scheduled_stop_point_label": "H2204", "arrival_time": null, - "departure_time": "PT7H5M" + "departure_time": "PT8H5M" }, { "_scheduled_stop_point_label": "H2203", - "arrival_time": "PT7H15M", - "departure_time": "PT7H15M" + "arrival_time": "PT8H15M", + "departure_time": "PT8H15M" }, { "_scheduled_stop_point_label": "H2202", - "arrival_time": "PT7H19M", - "departure_time": "PT7H19M" + "arrival_time": "PT8H19M", + "departure_time": "PT8H19M" }, { "_scheduled_stop_point_label": "H2201", - "arrival_time": "PT7H25M", + "arrival_time": "PT8H25M", "departure_time": null } ] diff --git a/src/test/resources/datasets/combine_multiple_targets.json b/src/test/resources/datasets/combine_multiple_targets.json index ea02c68e..e5842031 100644 --- a/src/test/resources/datasets/combine_multiple_targets.json +++ b/src/test/resources/datasets/combine_multiple_targets.json @@ -43,21 +43,21 @@ { "_scheduled_stop_point_label": "H2204", "arrival_time": null, - "departure_time": "PT7H5M" + "departure_time": "PT8H5M" }, { "_scheduled_stop_point_label": "H2203", - "arrival_time": "PT7H15M", - "departure_time": "PT7H15M" + "arrival_time": "PT8H15M", + "departure_time": "PT8H15M" }, { "_scheduled_stop_point_label": "H2202", - "arrival_time": "PT7H19M", - "departure_time": "PT7H19M" + "arrival_time": "PT8H19M", + "departure_time": "PT8H19M" }, { "_scheduled_stop_point_label": "H2201", - "arrival_time": "PT7H25M", + "arrival_time": "PT8H25M", "departure_time": null } ] diff --git a/src/test/resources/datasets/replace_multiple_replaced.json b/src/test/resources/datasets/replace_multiple_replaced.json index b42e6276..fc790273 100644 --- a/src/test/resources/datasets/replace_multiple_replaced.json +++ b/src/test/resources/datasets/replace_multiple_replaced.json @@ -43,21 +43,21 @@ { "_scheduled_stop_point_label": "H2204", "arrival_time": null, - "departure_time": "PT7H5M" + "departure_time": "PT8H5M" }, { "_scheduled_stop_point_label": "H2203", - "arrival_time": "PT7H15M", - "departure_time": "PT7H15M" + "arrival_time": "PT8H15M", + "departure_time": "PT8H15M" }, { "_scheduled_stop_point_label": "H2202", - "arrival_time": "PT7H19M", - "departure_time": "PT7H19M" + "arrival_time": "PT8H19M", + "departure_time": "PT8H19M" }, { "_scheduled_stop_point_label": "H2201", - "arrival_time": "PT7H25M", + "arrival_time": "PT8H25M", "departure_time": null } ] From ebc27255bcfd279b74e615bf151f7b8404cd4229 Mon Sep 17 00:00:00 2001 From: Tommi Leinamo Date: Fri, 10 Nov 2023 14:15:33 +0200 Subject: [PATCH 5/9] Add an exception for SQL commit exceptions So we can customize them a bit more, currently showing just as "JDBC commit failed" in UI. --- .../timetables/api/TimetablesController.kt | 5 +++ .../api/util/HasuraErrorExtensions.kt | 14 +++++++ .../api/TimetablesCombineApiTest.kt | 38 +++++++++++++++++++ .../api/TimetablesReplaceApiTest.kt | 38 +++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt index 407c145e..77542886 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt @@ -8,6 +8,7 @@ import fi.hsl.jore4.timetables.api.util.PlainStatusExtensions import fi.hsl.jore4.timetables.api.util.StagingVehicleScheduleFrameNotFoundExtensions import fi.hsl.jore4.timetables.api.util.TargetPriorityParsingExtensions import fi.hsl.jore4.timetables.api.util.TargetVehicleScheduleFrameNotFoundExtensions +import fi.hsl.jore4.timetables.api.util.TransactionSystemExtensions import fi.hsl.jore4.timetables.enumerated.TimetablesPriority import fi.hsl.jore4.timetables.service.CombineTimetablesService import fi.hsl.jore4.timetables.service.InvalidTargetPriorityException @@ -20,6 +21,7 @@ import jakarta.validation.constraints.AssertTrue import mu.KotlinLogging import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity +import org.springframework.transaction.TransactionSystemException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping @@ -128,6 +130,9 @@ class TimetablesController( is TargetPriorityParsingException -> TargetPriorityParsingExtensions.from(ex) + // Occurs on Commit / Rollback errors. + is TransactionSystemException -> TransactionSystemExtensions.from(ex) + else -> { LOGGER.error { "Exception during request:$ex" } LOGGER.error(ex.stackTraceToString()) diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt index c4797b9d..0dcf077e 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt @@ -7,6 +7,7 @@ import fi.hsl.jore4.timetables.service.MultipleTargetFramesFoundException import fi.hsl.jore4.timetables.service.StagingVehicleScheduleFrameNotFoundException import fi.hsl.jore4.timetables.service.TargetFrameNotFoundException import org.springframework.http.HttpStatus +import org.springframework.transaction.TransactionSystemException import java.util.UUID sealed interface HasuraErrorExtensions { @@ -86,3 +87,16 @@ data class TargetPriorityParsingExtensions( ) } } + +data class TransactionSystemExtensions( + override val code: Int, + val sqlErrorMessage: String +) : HasuraErrorExtensions { + + companion object { + fun from(ex: TransactionSystemException) = TransactionSystemExtensions( + HttpStatus.CONFLICT.value(), + ex.cause?.message ?: "unknown error on transaction commit or rollback" + ) + } +} diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt index 0e7c9779..93368e65 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt @@ -23,6 +23,7 @@ import org.springframework.test.web.servlet.ResultActions import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.transaction.TransactionSystemException import java.util.UUID @ExtendWith(MockKExtension::class) @@ -120,6 +121,43 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { } } + @Test + fun `throws a 409 when service transaction fails to commit`() { + val sqlErrorMessage = "ERROR: conflicting schedules detected: vehicle schedule frame Where: PL/pgSQL function vehicle_schedule.validate_queued_schedules_uniqueness()" + + every { + combineTimetablesService.combineTimetables( + defaultStagingFrameIds, + defaultTargetPriority + ) + } throws TransactionSystemException( + "JDBC Commit Failed", + Exception(sqlErrorMessage) + ) + + executeCombineTimetablesRequest() + .andExpect(status().isConflict) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content().json( + """ + { + "message": "JDBC Commit Failed", + "extensions": { + "code": 409, + "sqlErrorMessage": "$sqlErrorMessage" + } + } + """.trimIndent(), + true + ) + ) + + verify(exactly = 1) { + combineTimetablesService.combineTimetables(defaultStagingFrameIds, defaultTargetPriority) + } + } + @Test fun `throws an error when called with an invalid priority value`() { val invalidTargetPriority = 99 diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt index bb8b09a2..562f0621 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt @@ -22,6 +22,7 @@ import org.springframework.test.web.servlet.ResultActions import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.transaction.TransactionSystemException import java.util.UUID private val LOGGER = KotlinLogging.logger {} @@ -128,6 +129,43 @@ class TimetablesReplaceApiTest(@Autowired val mockMvc: MockMvc) { } } + @Test + fun `throws a 409 when service transaction fails to commit`() { + val sqlErrorMessage = "ERROR: conflicting schedules detected: vehicle schedule frame Where: PL/pgSQL function vehicle_schedule.validate_queued_schedules_uniqueness()" + + every { + replaceTimetablesService.replaceTimetables( + defaultStagingFrameIds, + defaultTargetPriority + ) + } throws TransactionSystemException( + "JDBC Commit Failed", + Exception(sqlErrorMessage) + ) + + executeReplaceTimetablesRequest() + .andExpect(status().isConflict) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content().json( + """ + { + "message": "JDBC Commit Failed", + "extensions": { + "code": 409, + "sqlErrorMessage": "$sqlErrorMessage" + } + } + """.trimIndent(), + true + ) + ) + + verify(exactly = 1) { + replaceTimetablesService.replaceTimetables(defaultStagingFrameIds, defaultTargetPriority) + } + } + @Test fun `throws an error when called with an invalid priority value`() { val invalidTargetPriority = 99 From b334519a527ff4a5d929d228948517576285c8c7 Mon Sep 17 00:00:00 2001 From: Tommi Leinamo Date: Fri, 10 Nov 2023 15:02:01 +0200 Subject: [PATCH 6/9] Add types to Hasura exceptions To be used in UI for displaying proper error messages for each case. --- .../api/util/HasuraErrorExtensions.kt | 25 +++++++++++++++++-- .../timetables/api/util/HasuraErrorType.kt | 11 ++++++++ .../api/TimetablesCombineApiTest.kt | 8 +++++- .../api/TimetablesReplaceApiTest.kt | 6 ++++- .../api/TimetablesToReplaceApiTest.kt | 1 + 5 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt index 0dcf077e..0e98e0ae 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt @@ -14,21 +14,32 @@ sealed interface HasuraErrorExtensions { // code must be 4xx val code: Int + + // Not required by Hasura, but we want to always pass one. + val type: HasuraErrorType } -data class PlainStatusExtensions(override val code: Int) : HasuraErrorExtensions { +data class PlainStatusExtensions( + override val code: Int, + override val type: HasuraErrorType +) : HasuraErrorExtensions { - constructor(httpStatus: HttpStatus) : this(httpStatus.value()) + constructor(httpStatus: HttpStatus) : this( + httpStatus.value(), + HasuraErrorType.UnknownError + ) } data class InvalidTargetPriorityExtensions( override val code: Int, + override val type: HasuraErrorType, val targetPriority: TimetablesPriority ) : HasuraErrorExtensions { companion object { fun from(ex: InvalidTargetPriorityException) = InvalidTargetPriorityExtensions( HttpStatus.BAD_REQUEST.value(), + HasuraErrorType.InvalidTargetPriorityError, ex.targetPriority ) } @@ -36,12 +47,14 @@ data class InvalidTargetPriorityExtensions( data class StagingVehicleScheduleFrameNotFoundExtensions( override val code: Int, + override val type: HasuraErrorType, val stagingVehicleScheduleFrameId: UUID ) : HasuraErrorExtensions { companion object { fun from(ex: StagingVehicleScheduleFrameNotFoundException) = StagingVehicleScheduleFrameNotFoundExtensions( HttpStatus.NOT_FOUND.value(), + HasuraErrorType.StagingVehicleScheduleFrameNotFoundError, ex.stagingVehicleScheduleFrameId ) } @@ -49,12 +62,14 @@ data class StagingVehicleScheduleFrameNotFoundExtensions( data class TargetVehicleScheduleFrameNotFoundExtensions( override val code: Int, + override val type: HasuraErrorType, val stagingVehicleScheduleFrameId: UUID ) : HasuraErrorExtensions { companion object { fun from(ex: TargetFrameNotFoundException) = TargetVehicleScheduleFrameNotFoundExtensions( HttpStatus.NOT_FOUND.value(), + HasuraErrorType.TargetVehicleScheduleFrameNotFoundError, ex.stagingVehicleScheduleFrameId ) } @@ -62,6 +77,7 @@ data class TargetVehicleScheduleFrameNotFoundExtensions( data class MultipleTargetFramesFoundExtensions( override val code: Int, + override val type: HasuraErrorType, val stagingVehicleScheduleFrameId: UUID, val targetVehicleScheduleFrameIds: List ) : HasuraErrorExtensions { @@ -69,6 +85,7 @@ data class MultipleTargetFramesFoundExtensions( companion object { fun from(ex: MultipleTargetFramesFoundException) = MultipleTargetFramesFoundExtensions( HttpStatus.CONFLICT.value(), + HasuraErrorType.MultipleTargetFramesFoundError, ex.stagingVehicleScheduleFrameId, ex.targetVehicleScheduleFrameIds ) @@ -77,12 +94,14 @@ data class MultipleTargetFramesFoundExtensions( data class TargetPriorityParsingExtensions( override val code: Int, + override val type: HasuraErrorType, val targetPriority: Int ) : HasuraErrorExtensions { companion object { fun from(ex: TargetPriorityParsingException) = TargetPriorityParsingExtensions( HttpStatus.BAD_REQUEST.value(), + HasuraErrorType.TargetPriorityParsingError, ex.targetPriority ) } @@ -90,12 +109,14 @@ data class TargetPriorityParsingExtensions( data class TransactionSystemExtensions( override val code: Int, + override val type: HasuraErrorType, val sqlErrorMessage: String ) : HasuraErrorExtensions { companion object { fun from(ex: TransactionSystemException) = TransactionSystemExtensions( HttpStatus.CONFLICT.value(), + HasuraErrorType.TransactionSystemError, ex.cause?.message ?: "unknown error on transaction commit or rollback" ) } diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt new file mode 100644 index 00000000..c9f60eb1 --- /dev/null +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt @@ -0,0 +1,11 @@ +package fi.hsl.jore4.timetables.api.util + +enum class HasuraErrorType { + UnknownError, + InvalidTargetPriorityError, + MultipleTargetFramesFoundError, + StagingVehicleScheduleFrameNotFoundError, + TargetPriorityParsingError, + TargetVehicleScheduleFrameNotFoundError, + TransactionSystemError +} diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt index 93368e65..832fe4a2 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt @@ -108,7 +108,8 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { { "message": "something unexpected happened", "extensions": { - "code": 409 + "code": 409, + "type": "UnknownError" } } """.trimIndent(), @@ -145,6 +146,7 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { "message": "JDBC Commit Failed", "extensions": { "code": 409, + "type": "TransactionSystemError", "sqlErrorMessage": "$sqlErrorMessage" } } @@ -192,6 +194,7 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { "message": "$errorMessage", "extensions": { "code": 404, + "type": "StagingVehicleScheduleFrameNotFoundError", "stagingVehicleScheduleFrameId": "$missingStagingFrameId" } } @@ -226,6 +229,7 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { "message": "$errorMessage", "extensions": { "code": 400, + "type": "InvalidTargetPriorityError", "targetPriority": $invalidTargetPriority } } @@ -259,6 +263,7 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { "message": "$errorMessage", "extensions": { "code": 404, + "type": "TargetVehicleScheduleFrameNotFoundError", "stagingVehicleScheduleFrameId": ${defaultStagingFrameIds[0]} } } @@ -292,6 +297,7 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { "message": "$errorMessage", "extensions": { "code": 409, + "type": "MultipleTargetFramesFoundError", "stagingVehicleScheduleFrameId": ${defaultStagingFrameIds[0]}, "targetVehicleScheduleFrameIds": $defaultTargetFrameIds } diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt index 562f0621..6932e689 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt @@ -113,7 +113,8 @@ class TimetablesReplaceApiTest(@Autowired val mockMvc: MockMvc) { { "message": "something unexpected happened", "extensions": { - "code": 409 + "code": 409, + "type": "UnknownError" } } """.trimIndent(), @@ -153,6 +154,7 @@ class TimetablesReplaceApiTest(@Autowired val mockMvc: MockMvc) { "message": "JDBC Commit Failed", "extensions": { "code": 409, + "type": "TransactionSystemError", "sqlErrorMessage": "$sqlErrorMessage" } } @@ -200,6 +202,7 @@ class TimetablesReplaceApiTest(@Autowired val mockMvc: MockMvc) { "message": "$errorMessage", "extensions": { "code": 404, + "type": "StagingVehicleScheduleFrameNotFoundError", "stagingVehicleScheduleFrameId": "$missingStagingFrameId" } } @@ -237,6 +240,7 @@ class TimetablesReplaceApiTest(@Autowired val mockMvc: MockMvc) { "message": "$errorMessage", "extensions": { "code": 400, + "type": "InvalidTargetPriorityError", "targetPriority": $invalidTargetPriority } } diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToReplaceApiTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToReplaceApiTest.kt index f941fe73..8e27054d 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToReplaceApiTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToReplaceApiTest.kt @@ -109,6 +109,7 @@ class TimetablesToReplaceApiTest(@Autowired val mockMvc: MockMvc) { "message": "$errorMessage", "extensions": { "code": 400, + "type": "TargetPriorityParsingError", "targetPriority": $invalidTargetPriorityInput } } From af2a0e5391cdadbc3f6efea79ba5ab1ba7ac959a Mon Sep 17 00:00:00 2001 From: Tommi Leinamo Date: Mon, 13 Nov 2023 09:39:30 +0200 Subject: [PATCH 7/9] Add fine grained exceptions for known commit error situations To allow better error messages in UI. Note: most of these shouldn't actually be possible in replace or combine APIs, because the staging frames would run into these constraints already when being created in Hastus import. In addition there are lots of plain DB constraints, from simple value checks to foreign key constraints etc, but I don't see it as beneficial to add custom exceptions for all of these. Can always add these if users actually encounter them. --- .../api/util/HasuraErrorExtensions.kt | 21 +++- .../timetables/api/util/HasuraErrorType.kt | 7 ++ .../api/TimetablesCombineApiTest.kt | 7 +- .../api/TimetablesReplaceApiTest.kt | 7 +- .../util/TransactionSystemExtensionsTest.kt | 116 ++++++++++++++++++ .../service/CombineTimetablesServiceTest.kt | 38 ++++++ 6 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 src/test/kotlin/fi/hsl/jore4/timetables/api/util/TransactionSystemExtensionsTest.kt diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt index 0e98e0ae..60a8e778 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt @@ -116,8 +116,27 @@ data class TransactionSystemExtensions( companion object { fun from(ex: TransactionSystemException) = TransactionSystemExtensions( HttpStatus.CONFLICT.value(), - HasuraErrorType.TransactionSystemError, + resolveHasuraErrorType(ex), ex.cause?.message ?: "unknown error on transaction commit or rollback" ) + + private fun resolveHasuraErrorType(ex: TransactionSystemException): HasuraErrorType { + val errorMessage = ex.cause?.message ?: "" // Should always be defined though. + + // Attempt to detect from the error message (because there's no other data really) which error case triggered the failure. + // Mainly using either the triggering validation function name or some known part of the error message for this. + return errorTypesWithMatchingSubstrings.find { it.second in errorMessage }?.first + ?: HasuraErrorType.TransactionSystemError + } + + private val errorTypesWithMatchingSubstrings = listOf( + HasuraErrorType.PassingTimeStopPointMatchingOrderError to "passing times and their matching stop points must be in same order", + HasuraErrorType.PassingTimeFirstArrivalTimeError to "first passing time must not have arrival_time set", + HasuraErrorType.PassingTimeLastDepartureTimeError to "last passing time must not have departure_time set", + HasuraErrorType.PassingTimeNullError to "all passing time that are not first or last in the sequence must have both departure and arrival time defined", + HasuraErrorType.PassingTimesMixedJourneyPatternRefsError to "inconsistent journey_pattern_ref within vehicle journey, all timetabled_passing_times must reference same journey_pattern_ref as the vehicle_journey", + HasuraErrorType.ConflictingSchedulesError to "validate_queued_schedules_uniqueness", + HasuraErrorType.SequentialIntegrityError to "validate_service_sequential_integrity" + ) } } diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt index c9f60eb1..169807d3 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt @@ -2,8 +2,15 @@ package fi.hsl.jore4.timetables.api.util enum class HasuraErrorType { UnknownError, + ConflictingSchedulesError, InvalidTargetPriorityError, MultipleTargetFramesFoundError, + PassingTimeFirstArrivalTimeError, + PassingTimeLastDepartureTimeError, + PassingTimeNullError, + PassingTimeStopPointMatchingOrderError, + PassingTimesMixedJourneyPatternRefsError, + SequentialIntegrityError, StagingVehicleScheduleFrameNotFoundError, TargetPriorityParsingError, TargetVehicleScheduleFrameNotFoundError, diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt index 832fe4a2..02d90200 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesCombineApiTest.kt @@ -124,6 +124,11 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { @Test fun `throws a 409 when service transaction fails to commit`() { + // Note: there are many kinds of transaction system errors (see `TransactionSystemException`). + // Just using `ConflictingSchedulesError` here as an example instead of plain `TransactionSystemError` + // to get better test coverage by involving error type detection. + // Other transaction system errors are handled similarly, + // type detection for each error case is tested in more detail in a separate suite. val sqlErrorMessage = "ERROR: conflicting schedules detected: vehicle schedule frame Where: PL/pgSQL function vehicle_schedule.validate_queued_schedules_uniqueness()" every { @@ -146,7 +151,7 @@ class TimetablesCombineApiTest(@Autowired val mockMvc: MockMvc) { "message": "JDBC Commit Failed", "extensions": { "code": 409, - "type": "TransactionSystemError", + "type": "ConflictingSchedulesError", "sqlErrorMessage": "$sqlErrorMessage" } } diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt index 6932e689..907fcbc1 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesReplaceApiTest.kt @@ -132,6 +132,11 @@ class TimetablesReplaceApiTest(@Autowired val mockMvc: MockMvc) { @Test fun `throws a 409 when service transaction fails to commit`() { + // Note: there are many kinds of transaction system errors (see `TransactionSystemException`). + // Just using `ConflictingSchedulesError` here as an example instead of plain `TransactionSystemError` + // to get better test coverage by involving error type detection. + // Other transaction system errors are handled similarly, + // type detection for each error case is tested in more detail in a separate suite. val sqlErrorMessage = "ERROR: conflicting schedules detected: vehicle schedule frame Where: PL/pgSQL function vehicle_schedule.validate_queued_schedules_uniqueness()" every { @@ -154,7 +159,7 @@ class TimetablesReplaceApiTest(@Autowired val mockMvc: MockMvc) { "message": "JDBC Commit Failed", "extensions": { "code": 409, - "type": "TransactionSystemError", + "type": "ConflictingSchedulesError", "sqlErrorMessage": "$sqlErrorMessage" } } diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/util/TransactionSystemExtensionsTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/util/TransactionSystemExtensionsTest.kt new file mode 100644 index 00000000..ff07503f --- /dev/null +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/util/TransactionSystemExtensionsTest.kt @@ -0,0 +1,116 @@ +package fi.hsl.jore4.timetables.api.util + +import org.junit.jupiter.api.Test +import org.springframework.transaction.TransactionSystemException +import kotlin.test.assertEquals + +class TransactionSystemExtensionsTest { + + private fun createTransactionSystemExceptionWithCause(message: String): TransactionSystemException { + return TransactionSystemException("test exception", Exception(message)) + } + + @Test + fun `resolves PassingTimeStopPointMatchingOrderError`() { + val exception = TransactionSystemExtensions.from( + createTransactionSystemExceptionWithCause( + """ + ERROR: passing times and their matching stop points must be in same order + Where: PL/pgSQL function passing_times.validate_passing_time_sequences() + """.trimIndent() + ) + ) + assertEquals(exception.type, HasuraErrorType.PassingTimeStopPointMatchingOrderError) + } + + @Test + fun `resolves PassingTimeFirstArrivalTimeError`() { + val exception = TransactionSystemExtensions.from( + createTransactionSystemExceptionWithCause( + """ + ERROR: first passing time must not have arrival_time set + Where: PL/pgSQL function passing_times.validate_passing_time_sequences() + """.trimIndent() + ) + ) + assertEquals(exception.type, HasuraErrorType.PassingTimeFirstArrivalTimeError) + } + + @Test + fun `resolves PassingTimeLastDepartureTimeError`() { + val exception = TransactionSystemExtensions.from( + createTransactionSystemExceptionWithCause( + """ + ERROR: last passing time must not have departure_time set + Where: PL/pgSQL function passing_times.validate_passing_time_sequences() + """.trimIndent() + ) + ) + assertEquals(exception.type, HasuraErrorType.PassingTimeLastDepartureTimeError) + } + + @Test + fun `resolves PassingTimeNullError`() { + val exception = TransactionSystemExtensions.from( + createTransactionSystemExceptionWithCause( + """ + ERROR: all passing time that are not first or last in the sequence must have both departure and arrival time defined + Where: PL/pgSQL function passing_times.validate_passing_time_sequences() + """.trimIndent() + ) + ) + assertEquals(exception.type, HasuraErrorType.PassingTimeNullError) + } + + @Test + fun `resolves PassingTimesMixedJourneyPatternRefsError`() { + val exception = TransactionSystemExtensions.from( + createTransactionSystemExceptionWithCause( + """ + ERROR: inconsistent journey_pattern_ref within vehicle journey, all timetabled_passing_times must reference same journey_pattern_ref as the vehicle_journey + Where: PL/pgSQL function passing_times.validate_passing_time_sequences() + """.trimIndent() + ) + ) + assertEquals(exception.type, HasuraErrorType.PassingTimesMixedJourneyPatternRefsError) + } + + @Test + fun `resolves ConflictingSchedulesError`() { + val exception = TransactionSystemExtensions.from( + createTransactionSystemExceptionWithCause( + """ + ERROR: conflicting schedules detected: vehicle schedule frame + Where: PL/pgSQL function vehicle_schedule.validate_queued_schedules_uniqueness() + """.trimIndent() + ) + ) + assertEquals(exception.type, HasuraErrorType.ConflictingSchedulesError) + } + + @Test + fun `resolves SequentialIntegrityError`() { + val exception = TransactionSystemExtensions.from( + createTransactionSystemExceptionWithCause( + """ + ERROR: Sequential integrity issues detected: + Where: PL/pgSQL function vehicle_service.validate_service_sequential_integrity() + """.trimIndent() + ) + ) + assertEquals(exception.type, HasuraErrorType.SequentialIntegrityError) + } + + @Test + fun `resolves an unknown error as TransactionSystemError`() { + val exception = TransactionSystemExtensions.from( + createTransactionSystemExceptionWithCause( + """ + ERROR: something else: + Where: PL/pgSQL function somewhere_else() + """.trimIndent() + ) + ) + assertEquals(exception.type, HasuraErrorType.TransactionSystemError) + } +} diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/service/CombineTimetablesServiceTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/service/CombineTimetablesServiceTest.kt index 3aa8a503..1259f55a 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/service/CombineTimetablesServiceTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/service/CombineTimetablesServiceTest.kt @@ -8,11 +8,13 @@ import fi.hsl.jore4.timetables.extensions.deepClone import fi.hsl.jore4.timetables.extensions.getNested import fi.hsl.jore4.timetables.repository.VehicleScheduleFrameRepository import fi.hsl.jore4.timetables.repository.VehicleServiceRepository +import mu.KotlinLogging import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired +import org.springframework.transaction.TransactionSystemException import java.util.UUID import kotlin.test.assertContains import kotlin.test.assertEquals @@ -20,6 +22,8 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNotNull import kotlin.test.assertNull +private val LOGGER = KotlinLogging.logger {} + @IntTest class CombineTimetablesServiceTest @Autowired constructor( val combineTimetablesService: CombineTimetablesService, @@ -332,6 +336,40 @@ class CombineTimetablesServiceTest @Autowired constructor( assertEquals(listOf(UUID.fromString("bb2abc90-8e91-4b0b-a7e4-751a04e81ba3")), result) assertNull(vehicleScheduleFrameRepository.fetchOneByVehicleScheduleFrameId(stagingFrameId)) } + + @Test + fun `fails when combining would result in invalid data`() { + // There are not too many cases that can produce a commit fail here, + // since the frames have already been (mostly) validated when created as staging. + // Setup for one such case here: + // Change target2 validity times to differ from staging -> not detected as target. + // But since we have another target ("target" in dataset), we can still combine. + // However, since the staging frame overlaps with target2 (timetables active for same routes on same days), + // the commit will fail on DB constraints. + // There might be more realistic cases to reproduce similar error with, I think, + // but this is easiest with current datasets. + val testData = TimetablesDataset.createFromResource("datasets/combine_multiple_targets.json") + testData.getNested("_vehicle_schedule_frames.target2")["validity_start"] = "2022-07-15" + testData.getNested("_vehicle_schedule_frames.target2")["validity_end"] = "2023-05-15" + + timetablesDataInserterRunner.truncateAndInsertDataset(testData.toJSONString()) + + val stagingFrameId = UUID.fromString("e8d07c0d-575f-4cbe-bddb-ead5b2943638") + val exception = assertFailsWith { + combineTimetablesService.combineTimetables( + listOf(stagingFrameId), + TimetablesPriority.STANDARD + ) + } + + // Check that the error messages are somewhat in the format we expect. TransactionSystemExtensions depends on these. + assertEquals(exception.message, "JDBC commit failed") + val causeMessage = exception?.cause?.message + assertNotNull(causeMessage) + assertContains(causeMessage, "ERROR: conflicting schedules detected: vehicle schedule frame") + assertContains(causeMessage, "Where: PL/pgSQL function vehicle_schedule.validate_queued_schedules_uniqueness()") + assertContains(causeMessage, "SQL statement \"SELECT vehicle_schedule.validate_queued_schedules_uniqueness()") + } } @Nested From ec6f76072984e5955518982864cfc31da8a5538f Mon Sep 17 00:00:00 2001 From: Tommi Leinamo Date: Thu, 14 Dec 2023 07:47:41 +0200 Subject: [PATCH 8/9] Separate type from HasuraErrorExtensions and Response To be more explicit that it's not related to Hasura itself, just our own extension property. --- .../timetables/api/TimetablesController.kt | 12 +++++------ .../api/util/HasuraErrorExtensions.kt | 20 ++++++++++--------- .../api/util/HasuraErrorResponse.kt | 9 +++++++-- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt index 77542886..5e631b63 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt @@ -1,8 +1,8 @@ package fi.hsl.jore4.timetables.api -import fi.hsl.jore4.timetables.api.util.HasuraErrorExtensions -import fi.hsl.jore4.timetables.api.util.HasuraErrorResponse import fi.hsl.jore4.timetables.api.util.InvalidTargetPriorityExtensions +import fi.hsl.jore4.timetables.api.util.JoreErrorExtensions +import fi.hsl.jore4.timetables.api.util.JoreErrorResponse import fi.hsl.jore4.timetables.api.util.MultipleTargetFramesFoundExtensions import fi.hsl.jore4.timetables.api.util.PlainStatusExtensions import fi.hsl.jore4.timetables.api.util.StagingVehicleScheduleFrameNotFoundExtensions @@ -118,8 +118,8 @@ class TimetablesController( } @ExceptionHandler(RuntimeException::class) - fun handleRuntimeException(ex: RuntimeException): ResponseEntity { - val hasuraExtensions: HasuraErrorExtensions = when (ex) { + fun handleRuntimeException(ex: RuntimeException): ResponseEntity { + val errorExtensions: JoreErrorExtensions = when (ex) { is InvalidTargetPriorityException -> InvalidTargetPriorityExtensions.from(ex) is StagingVehicleScheduleFrameNotFoundException -> StagingVehicleScheduleFrameNotFoundExtensions.from(ex) @@ -141,7 +141,7 @@ class TimetablesController( } } - val httpStatus: HttpStatus = hasuraExtensions.run { + val httpStatus: HttpStatus = errorExtensions.run { if (code !in 400..499) { LOGGER.warn { "Violating Hasura error response contract by returning code not like 4xx: $code" } } @@ -153,6 +153,6 @@ class TimetablesController( } } - return ResponseEntity(HasuraErrorResponse(ex.message, hasuraExtensions), httpStatus) + return ResponseEntity(JoreErrorResponse(ex.message, errorExtensions), httpStatus) } } diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt index 60a8e778..c03193e6 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt @@ -11,18 +11,20 @@ import org.springframework.transaction.TransactionSystemException import java.util.UUID sealed interface HasuraErrorExtensions { - // code must be 4xx val code: Int +} - // Not required by Hasura, but we want to always pass one. +sealed interface JoreErrorExtensions : HasuraErrorExtensions { + // Not required by Hasura, but we always want to provide one + // so that the UI can present nice detailed error messages. val type: HasuraErrorType } data class PlainStatusExtensions( override val code: Int, override val type: HasuraErrorType -) : HasuraErrorExtensions { +) : JoreErrorExtensions { constructor(httpStatus: HttpStatus) : this( httpStatus.value(), @@ -34,7 +36,7 @@ data class InvalidTargetPriorityExtensions( override val code: Int, override val type: HasuraErrorType, val targetPriority: TimetablesPriority -) : HasuraErrorExtensions { +) : JoreErrorExtensions { companion object { fun from(ex: InvalidTargetPriorityException) = InvalidTargetPriorityExtensions( @@ -49,7 +51,7 @@ data class StagingVehicleScheduleFrameNotFoundExtensions( override val code: Int, override val type: HasuraErrorType, val stagingVehicleScheduleFrameId: UUID -) : HasuraErrorExtensions { +) : JoreErrorExtensions { companion object { fun from(ex: StagingVehicleScheduleFrameNotFoundException) = StagingVehicleScheduleFrameNotFoundExtensions( @@ -64,7 +66,7 @@ data class TargetVehicleScheduleFrameNotFoundExtensions( override val code: Int, override val type: HasuraErrorType, val stagingVehicleScheduleFrameId: UUID -) : HasuraErrorExtensions { +) : JoreErrorExtensions { companion object { fun from(ex: TargetFrameNotFoundException) = TargetVehicleScheduleFrameNotFoundExtensions( @@ -80,7 +82,7 @@ data class MultipleTargetFramesFoundExtensions( override val type: HasuraErrorType, val stagingVehicleScheduleFrameId: UUID, val targetVehicleScheduleFrameIds: List -) : HasuraErrorExtensions { +) : JoreErrorExtensions { companion object { fun from(ex: MultipleTargetFramesFoundException) = MultipleTargetFramesFoundExtensions( @@ -96,7 +98,7 @@ data class TargetPriorityParsingExtensions( override val code: Int, override val type: HasuraErrorType, val targetPriority: Int -) : HasuraErrorExtensions { +) : JoreErrorExtensions { companion object { fun from(ex: TargetPriorityParsingException) = TargetPriorityParsingExtensions( @@ -111,7 +113,7 @@ data class TransactionSystemExtensions( override val code: Int, override val type: HasuraErrorType, val sqlErrorMessage: String -) : HasuraErrorExtensions { +) : JoreErrorExtensions { companion object { fun from(ex: TransactionSystemException) = TransactionSystemExtensions( diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorResponse.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorResponse.kt index 4e3db89c..0808b316 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorResponse.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorResponse.kt @@ -1,9 +1,14 @@ package fi.hsl.jore4.timetables.api.util // See https://hasura.io/docs/latest/actions/action-handlers/#returning-an-error-response -class HasuraErrorResponse( +sealed class HasuraErrorResponse( nullableMessage: String?, - val extensions: HasuraErrorExtensions + open val extensions: HasuraErrorExtensions ) { val message = nullableMessage ?: "An error occurred." } + +class JoreErrorResponse( + nullableMessage: String?, + override val extensions: JoreErrorExtensions +) : HasuraErrorResponse(nullableMessage, extensions) From b11ca33045bba71e08f95e8ef32a6f9440bc5df7 Mon Sep 17 00:00:00 2001 From: Tommi Leinamo Date: Thu, 14 Dec 2023 07:50:29 +0200 Subject: [PATCH 9/9] Rename the error type enum Since this doesn't have anything to do with Hasura itself, other than that it is included in responses that go through Hasura. --- .../api/util/HasuraErrorExtensions.kt | 48 +++++++++---------- ...ErrorType.kt => TimetablesApiErrorType.kt} | 2 +- .../util/TransactionSystemExtensionsTest.kt | 16 +++---- 3 files changed, 33 insertions(+), 33 deletions(-) rename src/main/kotlin/fi/hsl/jore4/timetables/api/util/{HasuraErrorType.kt => TimetablesApiErrorType.kt} (93%) diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt index c03193e6..a10cab13 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorExtensions.kt @@ -18,30 +18,30 @@ sealed interface HasuraErrorExtensions { sealed interface JoreErrorExtensions : HasuraErrorExtensions { // Not required by Hasura, but we always want to provide one // so that the UI can present nice detailed error messages. - val type: HasuraErrorType + val type: TimetablesApiErrorType } data class PlainStatusExtensions( override val code: Int, - override val type: HasuraErrorType + override val type: TimetablesApiErrorType ) : JoreErrorExtensions { constructor(httpStatus: HttpStatus) : this( httpStatus.value(), - HasuraErrorType.UnknownError + TimetablesApiErrorType.UnknownError ) } data class InvalidTargetPriorityExtensions( override val code: Int, - override val type: HasuraErrorType, + override val type: TimetablesApiErrorType, val targetPriority: TimetablesPriority ) : JoreErrorExtensions { companion object { fun from(ex: InvalidTargetPriorityException) = InvalidTargetPriorityExtensions( HttpStatus.BAD_REQUEST.value(), - HasuraErrorType.InvalidTargetPriorityError, + TimetablesApiErrorType.InvalidTargetPriorityError, ex.targetPriority ) } @@ -49,14 +49,14 @@ data class InvalidTargetPriorityExtensions( data class StagingVehicleScheduleFrameNotFoundExtensions( override val code: Int, - override val type: HasuraErrorType, + override val type: TimetablesApiErrorType, val stagingVehicleScheduleFrameId: UUID ) : JoreErrorExtensions { companion object { fun from(ex: StagingVehicleScheduleFrameNotFoundException) = StagingVehicleScheduleFrameNotFoundExtensions( HttpStatus.NOT_FOUND.value(), - HasuraErrorType.StagingVehicleScheduleFrameNotFoundError, + TimetablesApiErrorType.StagingVehicleScheduleFrameNotFoundError, ex.stagingVehicleScheduleFrameId ) } @@ -64,14 +64,14 @@ data class StagingVehicleScheduleFrameNotFoundExtensions( data class TargetVehicleScheduleFrameNotFoundExtensions( override val code: Int, - override val type: HasuraErrorType, + override val type: TimetablesApiErrorType, val stagingVehicleScheduleFrameId: UUID ) : JoreErrorExtensions { companion object { fun from(ex: TargetFrameNotFoundException) = TargetVehicleScheduleFrameNotFoundExtensions( HttpStatus.NOT_FOUND.value(), - HasuraErrorType.TargetVehicleScheduleFrameNotFoundError, + TimetablesApiErrorType.TargetVehicleScheduleFrameNotFoundError, ex.stagingVehicleScheduleFrameId ) } @@ -79,7 +79,7 @@ data class TargetVehicleScheduleFrameNotFoundExtensions( data class MultipleTargetFramesFoundExtensions( override val code: Int, - override val type: HasuraErrorType, + override val type: TimetablesApiErrorType, val stagingVehicleScheduleFrameId: UUID, val targetVehicleScheduleFrameIds: List ) : JoreErrorExtensions { @@ -87,7 +87,7 @@ data class MultipleTargetFramesFoundExtensions( companion object { fun from(ex: MultipleTargetFramesFoundException) = MultipleTargetFramesFoundExtensions( HttpStatus.CONFLICT.value(), - HasuraErrorType.MultipleTargetFramesFoundError, + TimetablesApiErrorType.MultipleTargetFramesFoundError, ex.stagingVehicleScheduleFrameId, ex.targetVehicleScheduleFrameIds ) @@ -96,14 +96,14 @@ data class MultipleTargetFramesFoundExtensions( data class TargetPriorityParsingExtensions( override val code: Int, - override val type: HasuraErrorType, + override val type: TimetablesApiErrorType, val targetPriority: Int ) : JoreErrorExtensions { companion object { fun from(ex: TargetPriorityParsingException) = TargetPriorityParsingExtensions( HttpStatus.BAD_REQUEST.value(), - HasuraErrorType.TargetPriorityParsingError, + TimetablesApiErrorType.TargetPriorityParsingError, ex.targetPriority ) } @@ -111,34 +111,34 @@ data class TargetPriorityParsingExtensions( data class TransactionSystemExtensions( override val code: Int, - override val type: HasuraErrorType, + override val type: TimetablesApiErrorType, val sqlErrorMessage: String ) : JoreErrorExtensions { companion object { fun from(ex: TransactionSystemException) = TransactionSystemExtensions( HttpStatus.CONFLICT.value(), - resolveHasuraErrorType(ex), + resolveErrorType(ex), ex.cause?.message ?: "unknown error on transaction commit or rollback" ) - private fun resolveHasuraErrorType(ex: TransactionSystemException): HasuraErrorType { + private fun resolveErrorType(ex: TransactionSystemException): TimetablesApiErrorType { val errorMessage = ex.cause?.message ?: "" // Should always be defined though. // Attempt to detect from the error message (because there's no other data really) which error case triggered the failure. // Mainly using either the triggering validation function name or some known part of the error message for this. return errorTypesWithMatchingSubstrings.find { it.second in errorMessage }?.first - ?: HasuraErrorType.TransactionSystemError + ?: TimetablesApiErrorType.TransactionSystemError } private val errorTypesWithMatchingSubstrings = listOf( - HasuraErrorType.PassingTimeStopPointMatchingOrderError to "passing times and their matching stop points must be in same order", - HasuraErrorType.PassingTimeFirstArrivalTimeError to "first passing time must not have arrival_time set", - HasuraErrorType.PassingTimeLastDepartureTimeError to "last passing time must not have departure_time set", - HasuraErrorType.PassingTimeNullError to "all passing time that are not first or last in the sequence must have both departure and arrival time defined", - HasuraErrorType.PassingTimesMixedJourneyPatternRefsError to "inconsistent journey_pattern_ref within vehicle journey, all timetabled_passing_times must reference same journey_pattern_ref as the vehicle_journey", - HasuraErrorType.ConflictingSchedulesError to "validate_queued_schedules_uniqueness", - HasuraErrorType.SequentialIntegrityError to "validate_service_sequential_integrity" + TimetablesApiErrorType.PassingTimeStopPointMatchingOrderError to "passing times and their matching stop points must be in same order", + TimetablesApiErrorType.PassingTimeFirstArrivalTimeError to "first passing time must not have arrival_time set", + TimetablesApiErrorType.PassingTimeLastDepartureTimeError to "last passing time must not have departure_time set", + TimetablesApiErrorType.PassingTimeNullError to "all passing time that are not first or last in the sequence must have both departure and arrival time defined", + TimetablesApiErrorType.PassingTimesMixedJourneyPatternRefsError to "inconsistent journey_pattern_ref within vehicle journey, all timetabled_passing_times must reference same journey_pattern_ref as the vehicle_journey", + TimetablesApiErrorType.ConflictingSchedulesError to "validate_queued_schedules_uniqueness", + TimetablesApiErrorType.SequentialIntegrityError to "validate_service_sequential_integrity" ) } } diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/TimetablesApiErrorType.kt similarity index 93% rename from src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt rename to src/main/kotlin/fi/hsl/jore4/timetables/api/util/TimetablesApiErrorType.kt index 169807d3..947c8d0a 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/util/HasuraErrorType.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/util/TimetablesApiErrorType.kt @@ -1,6 +1,6 @@ package fi.hsl.jore4.timetables.api.util -enum class HasuraErrorType { +enum class TimetablesApiErrorType { UnknownError, ConflictingSchedulesError, InvalidTargetPriorityError, diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/util/TransactionSystemExtensionsTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/util/TransactionSystemExtensionsTest.kt index ff07503f..6af77631 100644 --- a/src/test/kotlin/fi/hsl/jore4/timetables/api/util/TransactionSystemExtensionsTest.kt +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/util/TransactionSystemExtensionsTest.kt @@ -20,7 +20,7 @@ class TransactionSystemExtensionsTest { """.trimIndent() ) ) - assertEquals(exception.type, HasuraErrorType.PassingTimeStopPointMatchingOrderError) + assertEquals(exception.type, TimetablesApiErrorType.PassingTimeStopPointMatchingOrderError) } @Test @@ -33,7 +33,7 @@ class TransactionSystemExtensionsTest { """.trimIndent() ) ) - assertEquals(exception.type, HasuraErrorType.PassingTimeFirstArrivalTimeError) + assertEquals(exception.type, TimetablesApiErrorType.PassingTimeFirstArrivalTimeError) } @Test @@ -46,7 +46,7 @@ class TransactionSystemExtensionsTest { """.trimIndent() ) ) - assertEquals(exception.type, HasuraErrorType.PassingTimeLastDepartureTimeError) + assertEquals(exception.type, TimetablesApiErrorType.PassingTimeLastDepartureTimeError) } @Test @@ -59,7 +59,7 @@ class TransactionSystemExtensionsTest { """.trimIndent() ) ) - assertEquals(exception.type, HasuraErrorType.PassingTimeNullError) + assertEquals(exception.type, TimetablesApiErrorType.PassingTimeNullError) } @Test @@ -72,7 +72,7 @@ class TransactionSystemExtensionsTest { """.trimIndent() ) ) - assertEquals(exception.type, HasuraErrorType.PassingTimesMixedJourneyPatternRefsError) + assertEquals(exception.type, TimetablesApiErrorType.PassingTimesMixedJourneyPatternRefsError) } @Test @@ -85,7 +85,7 @@ class TransactionSystemExtensionsTest { """.trimIndent() ) ) - assertEquals(exception.type, HasuraErrorType.ConflictingSchedulesError) + assertEquals(exception.type, TimetablesApiErrorType.ConflictingSchedulesError) } @Test @@ -98,7 +98,7 @@ class TransactionSystemExtensionsTest { """.trimIndent() ) ) - assertEquals(exception.type, HasuraErrorType.SequentialIntegrityError) + assertEquals(exception.type, TimetablesApiErrorType.SequentialIntegrityError) } @Test @@ -111,6 +111,6 @@ class TransactionSystemExtensionsTest { """.trimIndent() ) ) - assertEquals(exception.type, HasuraErrorType.TransactionSystemError) + assertEquals(exception.type, TimetablesApiErrorType.TransactionSystemError) } }