-
Couldn't load subscription status.
- Fork 2
Bkit 140 jpa #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bkit 140 jpa #29
Changes from all commits
612da0a
5be0eae
d2f45a2
2c40f07
36a3098
e025d77
2209298
4bd6776
4a0a861
ab4df26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,13 +4,12 @@ import com.buildit.bookit.auth.UserPrincipal | |
| import com.buildit.bookit.v1.booking.dto.Booking | ||
| import com.buildit.bookit.v1.booking.dto.BookingRequest | ||
| import com.buildit.bookit.v1.booking.dto.interval | ||
| import com.buildit.bookit.v1.booking.dto.maskSubjectIfOtherUser | ||
| import com.buildit.bookit.v1.location.LocationRepository | ||
| import com.buildit.bookit.v1.location.bookable.BookableRepository | ||
| import com.buildit.bookit.v1.location.bookable.InvalidBookable | ||
| import com.buildit.bookit.v1.location.bookable.dto.Bookable | ||
| import com.buildit.bookit.v1.location.dto.Location | ||
| import com.buildit.bookit.v1.user.UserService | ||
| import com.buildit.bookit.v1.user.dto.maskSubjectIfOtherUser | ||
| import org.springframework.format.annotation.DateTimeFormat | ||
| import org.springframework.http.HttpStatus | ||
| import org.springframework.http.ResponseEntity | ||
|
|
@@ -31,7 +30,6 @@ import java.net.URI | |
| import java.time.Clock | ||
| import java.time.LocalDate | ||
| import java.time.LocalDateTime | ||
| import java.time.ZoneId | ||
| import java.time.temporal.ChronoUnit.MINUTES | ||
| import javax.validation.Valid | ||
|
|
||
|
|
@@ -58,7 +56,6 @@ class BookableNotAvailable : RuntimeException("Bookable is not available. Pleas | |
| @Transactional | ||
| class BookingController(private val bookingRepository: BookingRepository, | ||
| private val bookableRepository: BookableRepository, | ||
| private val locationRepository: LocationRepository, | ||
| private val userService: UserService, | ||
| private val clock: Clock | ||
| ) { | ||
|
|
@@ -79,16 +76,13 @@ class BookingController(private val bookingRepository: BookingRepository, | |
| throw EndBeforeStartException() | ||
| } | ||
|
|
||
| val allBookings = bookingRepository.getAllBookings() | ||
| val allBookings = bookingRepository.findAll().toList() | ||
| if (start == LocalDate.MIN && end == LocalDate.MAX) | ||
| return allBookings.map { maskSubjectIfOtherUser(it, user) } | ||
|
|
||
| val locationTimezones = locationRepository.getLocations().associate { Pair(it.id, ZoneId.of(it.timeZone)) } | ||
| val bookableTimezones = bookableRepository.getAllBookables().associate { Pair(it.id, locationTimezones[it.locationId]) } | ||
|
|
||
| return allBookings | ||
| .filter { booking -> | ||
| val timezone = bookableTimezones[booking.bookableId] ?: throw IllegalStateException("Encountered an incomplete booking: $booking. Not able to determine booking's timezone.") | ||
| val timezone = booking.bookable.location.timeZone | ||
| val desiredInterval = Interval.of( | ||
| start.atStartOfDay(timezone).toInstant(), | ||
| end.atStartOfDay(timezone).toInstant() | ||
|
|
@@ -99,45 +93,45 @@ class BookingController(private val bookingRepository: BookingRepository, | |
| } | ||
|
|
||
| @GetMapping("/{id}") | ||
| fun getBooking(@PathVariable("id") bookingId: String, @AuthenticationPrincipal user: UserPrincipal): Booking = | ||
| bookingRepository.getAllBookings().find { it.id == bookingId }.let { maskSubjectIfOtherUser(it ?: throw BookingNotFound(), user) } | ||
| fun getBooking(@PathVariable("id") booking: Booking?, @AuthenticationPrincipal user: UserPrincipal): Booking = | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What witchcraft is this? Am looking at the diffs in the browser, so probably just need context. |
||
| booking?.let { maskSubjectIfOtherUser(it, user) } ?: throw BookingNotFound() | ||
|
|
||
| @DeleteMapping("/{id}") | ||
| fun deleteBooking(@PathVariable("id") id: String, @AuthenticationPrincipal userPrincipal: UserPrincipal): ResponseEntity<Unit> { | ||
| val booking = bookingRepository.getAllBookings().find { it.id == id } | ||
| if (booking != null && booking.user.externalId != userPrincipal.subject) { | ||
| return ResponseEntity.status(HttpStatus.FORBIDDEN).build() | ||
| fun deleteBooking(@PathVariable("id") booking: Booking?, @AuthenticationPrincipal userPrincipal: UserPrincipal): ResponseEntity<Unit> = | ||
| when { | ||
| booking == null -> ResponseEntity.noContent().build() | ||
| booking.user.externalId != userPrincipal.subject -> ResponseEntity.status(HttpStatus.FORBIDDEN).build() | ||
| else -> { | ||
| bookingRepository.delete(booking) | ||
| ResponseEntity.noContent().build() | ||
| } | ||
| } | ||
| bookingRepository.delete(id) | ||
| return ResponseEntity.noContent().build() | ||
| } | ||
|
|
||
| @Suppress("UnsafeCallOnNullableType") | ||
| @PostMapping() | ||
| fun createBooking(@Valid @RequestBody bookingRequest: BookingRequest, @AuthenticationPrincipal userPrincipal: UserPrincipal, errors: Errors? = null): ResponseEntity<Booking> { | ||
|
|
||
| val bookable = bookableRepository.getAllBookables().find { it.id == bookingRequest.bookableId } ?: throw InvalidBookable() | ||
| val location = locationRepository.getLocations().single { it.id == bookable.locationId } | ||
|
|
||
| if (errors?.hasErrors() == true) { | ||
| val errorMessage = errors.allErrors.joinToString(",", transform = { it.defaultMessage }) | ||
|
|
||
| throw InvalidBooking(errorMessage) | ||
| } | ||
| val bookable = bookableRepository.findOne(bookingRequest.bookableId) ?: throw InvalidBookable() | ||
|
|
||
| val startDateTimeTruncated = bookingRequest.start!!.truncatedTo(MINUTES) | ||
| val endDateTimeTruncated = bookingRequest.end!!.truncatedTo(MINUTES) | ||
|
|
||
| validateBooking(location, startDateTimeTruncated, endDateTimeTruncated, bookable) | ||
| validateBooking(bookable.location, startDateTimeTruncated, endDateTimeTruncated, bookable) | ||
|
|
||
| val user = userService.register(userPrincipal) | ||
|
|
||
| val booking = bookingRepository.insertBooking( | ||
| bookingRequest.bookableId!!, | ||
| bookingRequest.subject!!, | ||
| startDateTimeTruncated, | ||
| endDateTimeTruncated, | ||
| user | ||
| val booking = bookingRepository.save( | ||
| Booking( | ||
| bookable, | ||
| bookingRequest.subject!!, | ||
| startDateTimeTruncated, | ||
| endDateTimeTruncated, | ||
| user | ||
| ) | ||
| ) | ||
|
|
||
| return ResponseEntity | ||
|
|
@@ -146,7 +140,7 @@ class BookingController(private val bookingRepository: BookingRepository, | |
| } | ||
|
|
||
| private fun validateBooking(location: Location, startDateTimeTruncated: LocalDateTime, endDateTimeTruncated: LocalDateTime, bookable: Bookable) { | ||
| val now = LocalDateTime.now(clock.withZone(ZoneId.of(location.timeZone))) | ||
| val now = LocalDateTime.now(clock.withZone(location.timeZone)) | ||
| if (!startDateTimeTruncated.isAfter(now)) { | ||
| throw StartInPastException() | ||
| } | ||
|
|
@@ -156,17 +150,16 @@ class BookingController(private val bookingRepository: BookingRepository, | |
| } | ||
|
|
||
| val interval = Interval.of( | ||
| startDateTimeTruncated.atZone(ZoneId.of(location.timeZone)).toInstant(), | ||
| endDateTimeTruncated.atZone(ZoneId.of(location.timeZone)).toInstant() | ||
| startDateTimeTruncated.atZone(location.timeZone).toInstant(), | ||
| endDateTimeTruncated.atZone(location.timeZone).toInstant() | ||
| ) | ||
|
|
||
| val unavailable = bookingRepository.getAllBookings() | ||
| .filter { it.bookableId == bookable.id } | ||
| val unavailable = bookingRepository.findByBookable(bookable) | ||
| .any { | ||
| interval.overlaps( | ||
| Interval.of( | ||
| it.start.atZone(ZoneId.of(location.timeZone)).toInstant(), | ||
| it.end.atZone(ZoneId.of(location.timeZone)).toInstant())) | ||
| it.start.atZone(location.timeZone).toInstant(), | ||
| it.end.atZone(location.timeZone).toInstant())) | ||
| } | ||
|
|
||
| if (unavailable) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,84 +1,9 @@ | ||
| package com.buildit.bookit.v1.booking | ||
|
|
||
| import com.buildit.bookit.v1.booking.dto.Booking | ||
| import com.buildit.bookit.v1.booking.dto.User | ||
| import org.springframework.jdbc.core.JdbcTemplate | ||
| import org.springframework.jdbc.core.simple.SimpleJdbcInsert | ||
| import org.springframework.stereotype.Repository | ||
| import java.sql.ResultSet | ||
| import java.time.LocalDateTime | ||
| import java.util.UUID | ||
| import com.buildit.bookit.v1.location.bookable.dto.Bookable | ||
| import org.springframework.data.repository.CrudRepository | ||
|
|
||
| interface BookingRepository { | ||
| fun getAllBookings(): Collection<Booking> | ||
| /** | ||
| * @param bookableId The id for the pre-exiting <code>Bookable</code> that is being booked. | ||
| * @param subject The purpose of the booking. | ||
| * @param startDateTime The start date of the booking. | ||
| * @param endDateTime The end date of the booking. | ||
| * @param creatingUser A pre-existing, fully-formed <code>User</code> object representing the person creating the booking. | ||
| */ | ||
| fun insertBooking(bookableId: String, subject: String, startDateTime: LocalDateTime, endDateTime: LocalDateTime, creatingUser: User): Booking | ||
|
|
||
| fun delete(id: String): Boolean | ||
| } | ||
|
|
||
| @Repository | ||
| class BookingDatabaseRepository(private val jdbcTemplate: JdbcTemplate) : BookingRepository { | ||
|
|
||
| private val tableName = "BOOKING" | ||
|
|
||
| override fun getAllBookings(): Collection<Booking> { | ||
| val sql = | ||
| """ | ||
| |SELECT b.BOOKING_ID, | ||
| | b.BOOKABLE_ID, | ||
| | b.SUBJECT, | ||
| | b.START_DATE, | ||
| | b.END_DATE, | ||
| | u.USER_ID, | ||
| | u.GIVEN_NAME, | ||
| | u.FAMILY_NAME, | ||
| | u.EXTERNAL_USER_ID | ||
| |FROM $tableName b | ||
| |LEFT JOIN USER u on b.USER_ID = u.USER_ID""".trimMargin() | ||
|
|
||
| return jdbcTemplate.query(sql) { rs, _ -> | ||
| Booking( | ||
| rs.getString("BOOKING_ID"), | ||
| rs.getString("BOOKABLE_ID"), | ||
| rs.getString("SUBJECT"), | ||
| rs.getObject("START_DATE", LocalDateTime::class.java), | ||
| rs.getObject("END_DATE", LocalDateTime::class.java), | ||
| makeUser(rs) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| override fun insertBooking(bookableId: String, subject: String, startDateTime: LocalDateTime, endDateTime: LocalDateTime, creatingUser: User): Booking { | ||
| val bookingId = UUID.randomUUID().toString() | ||
|
|
||
| SimpleJdbcInsert(jdbcTemplate).withTableName(tableName).apply { | ||
| execute( | ||
| mapOf("BOOKING_ID" to bookingId, | ||
| "BOOKABLE_ID" to bookableId, | ||
| "SUBJECT" to subject, | ||
| "START_DATE" to startDateTime, | ||
| "END_DATE" to endDateTime, | ||
| "USER_ID" to creatingUser.id | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| return Booking(bookingId, bookableId, subject, startDateTime, endDateTime, creatingUser) | ||
| } | ||
|
|
||
| override fun delete(id: String): Boolean = jdbcTemplate.update("DELETE FROM $tableName WHERE BOOKING_ID = ?", id) == 1 | ||
|
|
||
| private fun makeUser(rs: ResultSet): User = | ||
| User( | ||
| rs.getString("USER_ID"), | ||
| "${rs.getString("GIVEN_NAME")} ${rs.getString("FAMILY_NAME")}", | ||
| rs.getString("EXTERNAL_USER_ID") | ||
| ) | ||
| interface BookingRepository : CrudRepository<Booking, String> { | ||
| fun findByBookable(bookable: Bookable): List<Booking> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,18 @@ | ||
| package com.buildit.bookit.v1.booking.dto | ||
|
|
||
| import com.buildit.bookit.auth.UserPrincipal | ||
| import com.buildit.bookit.v1.location.bookable.dto.Bookable | ||
| import com.buildit.bookit.v1.user.dto.User | ||
| import com.fasterxml.jackson.annotation.JsonFormat | ||
| import org.hibernate.annotations.GenericGenerator | ||
| import org.hibernate.validator.constraints.NotBlank | ||
| import org.threeten.extra.Interval | ||
| import java.time.LocalDateTime | ||
| import java.time.ZoneId | ||
| import javax.persistence.Column | ||
| import javax.persistence.Entity | ||
| import javax.persistence.GeneratedValue | ||
| import javax.persistence.Id | ||
| import javax.persistence.ManyToOne | ||
| import javax.validation.constraints.NotNull | ||
|
|
||
| /** | ||
|
|
@@ -22,31 +29,23 @@ data class BookingRequest( | |
| val end: LocalDateTime? | ||
| ) | ||
|
|
||
| const val MASKED_STRING = "**********" | ||
| fun maskSubjectIfOtherUser(booking: Booking, otherUser: UserPrincipal): Booking = | ||
| when { | ||
| booking.user.externalId != otherUser.subject -> booking.copy(subject = MASKED_STRING) | ||
| else -> booking | ||
| } | ||
|
|
||
| /** | ||
| * Booking response | ||
| */ | ||
| @Entity | ||
| data class Booking( | ||
| val id: String, | ||
| val bookableId: String, | ||
| @ManyToOne(optional = false) | ||
| val bookable: Bookable, | ||
| @Column(nullable = false) | ||
| val subject: String, | ||
| @field:JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm") | ||
| @Column(nullable = false) @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm") | ||
| val start: LocalDateTime, | ||
| @field:JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm") | ||
| @Column(nullable = false) @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm") | ||
| val end: LocalDateTime, | ||
| val user: User | ||
| ) | ||
|
|
||
| data class User( | ||
| val id: String, | ||
| val name: String, | ||
| val externalId: String | ||
| @ManyToOne(optional = false) | ||
| val user: User, | ||
| @Id @GeneratedValue(generator = "uuid2") @GenericGenerator(name = "uuid2", strategy = "uuid2") | ||
| val id: String? = null | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. generally moved optional IDs to end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I slightly prefer a degree of isomorphism between the schema and the code, and like PKs first in a schema, but I can't what JPA will do in the schema anyway with regard to order (maybe it sorts keys to the top?). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fair point... i'll have a looksie as to what it actually does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so looks like it does the right thing i'd like to see from a schema perspective wrt order |
||
| ) | ||
|
|
||
| fun Booking.interval(timeZone: ZoneId): Interval = | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,5 @@ | ||
| package com.buildit.bookit.v1.location | ||
|
|
||
| import com.buildit.bookit.v1.location.dto.Location | ||
| import org.springframework.jdbc.core.JdbcTemplate | ||
| import org.springframework.stereotype.Repository | ||
|
|
||
| interface LocationRepository { | ||
| fun getLocations(): Collection<Location> | ||
| } | ||
|
|
||
| @Repository | ||
| class LocationDatabaseRepository(private val jdbcTemplate: JdbcTemplate) : LocationRepository { | ||
|
|
||
| override fun getLocations(): Collection<Location> = jdbcTemplate.query( | ||
| "SELECT LOCATION_ID, LOCATION_NAME, LOCATION_TZ FROM LOCATION") { rs, _ -> | ||
| Location( | ||
| rs.getString("LOCATION_ID"), | ||
| rs.getString("LOCATION_NAME"), | ||
| rs.getString("LOCATION_TZ") | ||
| ) | ||
| } | ||
| } | ||
| interface LocationRepository : org.springframework.data.repository.CrudRepository<Location, String> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed due to constraints guaranteeing presence?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes