Skip to content

Commit a3f11ca

Browse files
committed
cleanup datetime types; added received timestamp for events.
1 parent af82cb6 commit a3f11ca

File tree

16 files changed

+119
-51
lines changed

16 files changed

+119
-51
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Status
44

55
[![build](https://github.com/jvorhauer/konomas/actions/workflows/gradle.yml/badge.svg)](https://github.com/jvorhauer/noviblog/actions/workflows/gradle.yml)
6-
[![coverage](https://codecov.io/gh/jvorhauer/noviblog/branch/main/graph/badge.svg?token=Nn5OmNCOEY)](https://codecov.io/gh/jvorhauer/noviblog)
6+
[![coverage](https://codecov.io/gh/jvorhauer/konomas/branch/main/graph/badge.svg?token=Nn5OmNCOEY)](https://codecov.io/gh/jvorhauer/noviblog)
77

88
An Event Sourced version of the backend for the FrontEnd solution.
99

build.gradle.kts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ repositories {
3737
maven {
3838
url = uri("https://oss.sonatype.org/content/repositories/releases")
3939
}
40+
maven {
41+
url = uri("https://jitpack.io")
42+
}
4043
}
4144

4245
dependencies {
@@ -91,8 +94,8 @@ tasks.withType<KotlinCompile> {
9194
kotlinOptions {
9295
freeCompilerArgs = listOf("-Xjsr305=strict")
9396
jvmTarget = "21"
94-
languageVersion = "1.9"
95-
allWarningsAsErrors = true
97+
languageVersion = "2.0"
98+
allWarningsAsErrors = false
9699
}
97100
}
98101
tasks.withType<JavaCompile> {

src/main/kotlin/blog/Main.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import blog.read.Reader
2626
import blog.read.info
2727
import blog.write.Processor
2828

29-
const val pid: String = "28"
29+
const val pid: String = "29"
3030

3131
object Main {
3232
private val kfg: Konfig = ConfigFactory.load("application.conf").extract("konomas")

src/main/kotlin/blog/model/Note.kt

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,31 @@ data class DeleteNote(val id: String, val rt: ActorRef<StatusReply<Done>>): Comm
4343
val toEvent get() = NoteDeleted(id)
4444
}
4545

46-
data class NoteCreated(val id: String, val user: String, val title: String, val body: String) : Event {
46+
47+
data class NoteCreated(
48+
val id: String,
49+
val user: String,
50+
val title: String,
51+
val body: String,
52+
override val received: ZonedDateTime = znow
53+
) : Event {
4754
val toEntity get() = Note(id, user, title, title.slug, body)
48-
val toResponse get() = this.toEntity.toResponse()
55+
val toResponse get() = this.toEntity.toResponse
4956
}
5057

51-
data class NoteUpdated(val id: String, val user: String, val title: String?, val body: String?): Event {
52-
val timestamp: ZonedDateTime get() = TSID.from(id).instant.atZone(CET)
53-
}
58+
data class NoteUpdated(
59+
val id: String,
60+
val user: String,
61+
val title: String?,
62+
val body: String?,
63+
override val received: ZonedDateTime = znow
64+
): Event
5465

55-
data class NoteDeleted(val id: String): Event
66+
data class NoteDeleted(
67+
val id: String,
68+
override val received: ZonedDateTime = znow
69+
): Event
5670

57-
data class NoteDelta(val updated: ZonedDateTime, val what: String)
5871

5972
data class Note(
6073
override val id: String,
@@ -63,19 +76,19 @@ data class Note(
6376
val slug: String,
6477
val body: String,
6578
val created: ZonedDateTime = TSID.from(id).instant.atZone(CET),
66-
val updated: ZonedDateTime = znow,
67-
val events: List<NoteUpdated> = listOf()
79+
val updated: ZonedDateTime = znow
6880
): Entity {
6981
constructor(id: String, user: String, title: String, body: String): this(id, user, title, title.slug, body)
7082
fun update(nu: NoteUpdated): Note = this.copy(
71-
title = nu.title ?: this.title, slug = nu.title?.slug ?: this.slug, body = nu.body ?: this.body, updated = znow, events = this.events + nu
83+
title = nu.title ?: this.title, slug = nu.title?.slug ?: this.slug, body = nu.body ?: this.body, updated = nu.received
7284
)
73-
fun toResponse() = NoteResponse(id, user, DTF.format(created), DTF.format(updated), title, slug, body, events.map { NoteDelta(it.timestamp, "???") })
85+
val toResponse get() = NoteResponse(id, user, created.fmt, updated.fmt, title, slug, body)
7486

7587
override fun equals(other: Any?): Boolean = equals(this, other)
7688
override fun hashCode(): Int = id.hashCode()
7789
}
7890

91+
7992
data class NoteResponse(
8093
val id: String,
8194
val user: String,
@@ -84,7 +97,6 @@ data class NoteResponse(
8497
val title: String,
8598
val slug: String,
8699
val body: String,
87-
val deltas: List<NoteDelta>
88100
)
89101

90102
fun Route.notesRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Scheduler, kfg: Konfig) =
@@ -105,12 +117,12 @@ fun Route.notesRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
105117
get {
106118
val rows = call.request.queryParameters["rows"]?.toInt() ?: 10
107119
val start = call.request.queryParameters["start"]?.toInt() ?: 0
108-
call.respond(reader.allNotes(rows, start).map { it.toResponse() })
120+
call.respond(reader.allNotes(rows, start).map { it.toResponse })
109121
}
110122
get("{id?}") {
111123
val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.NotFound, "note id not specified")
112124
val note = reader.findNote(id) ?: return@get call.respond(HttpStatusCode.NotFound, "note not found for $id")
113-
call.respond(note.toResponse())
125+
call.respond(note.toResponse)
114126
}
115127
put {
116128
val unr = call.receive<UpdateNoteRequest>()

src/main/kotlin/blog/model/Tag.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package blog.model
22

3+
import java.time.ZonedDateTime
34
import akka.actor.typed.ActorRef
45
import akka.actor.typed.Scheduler
56
import akka.actor.typed.javadsl.AskPattern.ask
@@ -30,7 +31,11 @@ data class CreateTag(val id: String, val label: String, val replyTo: ActorRef<St
3031
val toEvent get() = TagCreated(id, label)
3132
}
3233

33-
data class TagCreated(val id: String, val label: String) : Event {
34+
data class TagCreated(
35+
val id: String,
36+
val label: String,
37+
override val received: ZonedDateTime = znow
38+
) : Event {
3439
val toEntity get() = Tag(id, label)
3540
}
3641

src/main/kotlin/blog/model/Task.kt

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,19 @@ data class Task(
2828
val title: String,
2929
val slug: String,
3030
val body: String,
31-
val due: LocalDateTime,
31+
val due: ZonedDateTime,
3232
val status: TaskStatus = TaskStatus.TODO,
3333
val private: Boolean = true,
3434
val created: ZonedDateTime = TSID.from(id).instant.atZone(CET),
35-
val updated: ZonedDateTime = znow
35+
val updated: ZonedDateTime = TSID.from(id).instant.atZone(CET)
3636
) : Entity {
3737
fun update(tu: TaskUpdated): Task = this.copy(
3838
title = tu.title ?: this.title,
3939
slug = tu.title?.slug ?: this.slug,
4040
body = tu.body ?: this.body,
4141
due = tu.due ?: this.due,
4242
status = tu.status ?: this.status,
43-
updated = znow
43+
updated = tu.received
4444
)
4545

4646
val toResponse get() = TaskResponse(id, created.fmt, updated.fmt, user, title, body, due.fmt, status.name)
@@ -49,19 +49,19 @@ data class Task(
4949
}
5050

5151
data class CreateTaskRequest(val title: String, val body: String, val due: LocalDateTime): Request {
52-
fun toCommand(user: String, replyTo: ActorRef<StatusReply<TaskResponse>>) = CreateTask(user, title.encode, body.encode, due, replyTo)
52+
fun toCommand(user: String, replyTo: ActorRef<StatusReply<TaskResponse>>) = CreateTask(user, title.encode, body.encode, due.atZone(CET), replyTo)
5353
}
5454

5555
data class UpdateTaskRequest(val id: String, val title: String?, val body: String?, val due: LocalDateTime?, val status: TaskStatus?): Request {
56-
fun toCommand(user: String, replyTo: ActorRef<StatusReply<TaskResponse>>) = UpdateTask(user, id, title.mencode, body.mencode, due, status, replyTo)
56+
fun toCommand(user: String, replyTo: ActorRef<StatusReply<TaskResponse>>) = UpdateTask(user, id, title.mencode, body.mencode, due?.atZone(CET), status, replyTo)
5757
}
5858

5959

6060
data class CreateTask(
6161
val user: String,
6262
val title: String,
6363
val body: String,
64-
val due: LocalDateTime,
64+
val due: ZonedDateTime,
6565
val replyTo: ActorRef<StatusReply<TaskResponse>>,
6666
val id: String = nextId
6767
) : Command {
@@ -73,7 +73,7 @@ data class UpdateTask(
7373
val id: String,
7474
val title: String?,
7575
val body: String?,
76-
val due: LocalDateTime?,
76+
val due: ZonedDateTime?,
7777
val status: TaskStatus?,
7878
val replyTo: ActorRef<StatusReply<TaskResponse>>
7979
) : Command {
@@ -90,7 +90,8 @@ data class TaskCreated(
9090
val user: String,
9191
val title: String,
9292
val body: String,
93-
val due: LocalDateTime
93+
val due: ZonedDateTime,
94+
override val received: ZonedDateTime = znow
9495
) : Event {
9596
val toEntity get() = Task(id, user, title, title.slug, body, due)
9697
val toResponse get() = toEntity.toResponse
@@ -101,11 +102,15 @@ data class TaskUpdated(
101102
val id: String,
102103
val title: String?,
103104
val body: String?,
104-
val due: LocalDateTime?,
105+
val due: ZonedDateTime?,
105106
val status: TaskStatus?,
107+
override val received: ZonedDateTime = znow
106108
) : Event
107109

108-
data class TaskDeleted(val id: String): Event
110+
data class TaskDeleted(
111+
val id: String,
112+
override val received: ZonedDateTime = znow
113+
): Event
109114

110115

111116
data class TaskResponse(

src/main/kotlin/blog/model/User.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,22 @@ data class UserCreated(
6969
val email: String,
7070
val name: String,
7171
val password: String,
72+
override val received: ZonedDateTime = znow
7273
) : Event {
7374
val toEntity: User get() = User(id, email, name, password)
7475
}
7576

7677
data class UserUpdated(
7778
val id: String,
7879
val name: String?,
79-
val password: String?
80+
val password: String?,
81+
override val received: ZonedDateTime = znow
8082
): Event
8183

82-
data class UserDeleted(val id: String) : Event
84+
data class UserDeleted(
85+
val id: String,
86+
override val received: ZonedDateTime = znow
87+
) : Event
8388

8489
// Entitites
8590

@@ -157,7 +162,7 @@ fun Route.usersRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
157162
}
158163
get("/notes") {
159164
val userId = userIdFromJWT(call) ?: return@get call.respond(Unauthorized, "Unauthorized")
160-
call.respond(reader.findNotesForUser(userId).sortedBy { it.created }.map { it.toResponse() })
165+
call.respond(reader.findNotesForUser(userId).sortedBy { it.created }.map { it.toResponse })
161166
}
162167
put {
163168
val userId = userIdFromJWT(call) ?: return@put call.respond(Unauthorized, "Unauthorized")

src/main/kotlin/blog/model/model.kt

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,35 @@ import io.hypersistence.tsid.TSID
1616
import io.ktor.server.application.*
1717
import io.ktor.server.auth.*
1818
import io.ktor.server.auth.jwt.*
19+
import blog.model.Constants.idNode
20+
import blog.model.Constants.mdi
21+
import blog.model.Constants.randm
1922

2023
interface Request : Serializable
24+
2125
interface Command : Serializable
22-
interface Event : Serializable
26+
27+
interface Event : Serializable {
28+
val received: ZonedDateTime
29+
}
30+
2331
interface Entity : Serializable {
2432
val id: String
2533
}
34+
2635
interface Response : Serializable {
2736
val id: String
2837
}
2938

39+
object Constants {
40+
val randm: SecureRandom = SecureRandom.getInstance("SHA1PRNG", "SUN")
41+
val idNode: Int = InetAddress.getLocalHost().address[3].toInt()and(0xFF)
42+
val mdi: MessageDigest = MessageDigest.getInstance("SHA-256")
43+
}
44+
3045
object Hasher {
31-
private val md = MessageDigest.getInstance("SHA-256")
3246
private fun toHex(ba: ByteArray) = ba.joinToString(separator = "") { String.format(Locale.US, "%02x", it) }
33-
fun hash(s: String): String = toHex(md.digest(s.toByteArray(StandardCharsets.UTF_8)))
47+
fun hash(s: String): String = toHex(mdi.digest(s.toByteArray(StandardCharsets.UTF_8)))
3448
}
3549
val String.hashed: String get() = Hasher.hash(this)
3650
val String.gravatar: String get() = this.trim().lowercase().hashed
@@ -44,10 +58,7 @@ val inow: Instant get() = Instant.now()
4458
val znow: ZonedDateTime get() = inow.atZone(CET)
4559
val now: LocalDateTime get() = LocalDateTime.ofInstant(inow, CET)
4660

47-
private val idFactory = TSID.Factory.builder()
48-
.withRandom(SecureRandom.getInstance("SHA1PRNG", "SUN"))
49-
.withNodeBits(8)
50-
.withNode(InetAddress.getLocalHost().address[3].toInt()and(0xFF)).build()
61+
private val idFactory = TSID.Factory.builder().withRandom(randm).withNodeBits(8).withNode(idNode).build()
5162
val nextTSID: TSID get() = idFactory.generate()
5263
val nextId: String get() = nextTSID.toString()
5364

src/main/kotlin/blog/read/Reader.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class Reader(
7373
is TagCreated -> tags[e.id] = e.toEntity
7474
else -> logger.warn("could not processEvent {}", e)
7575
}
76+
logger.info("processEvent: $e")
7677
}
7778

7879
companion object {

src/main/kotlin/blog/write/Processor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class Processor(pid: PersistenceId, private val reader: Reader) : EventSourcedBe
8888
Effect().none().thenReply(cmd.replyTo) { StatusReply.error("Note with id ${cmd.id} not found for user with id ${cmd.user}") }
8989
} else {
9090
cmd.toEvent.let {
91-
Effect().persist(it).thenReply(cmd.replyTo) { st -> StatusReply.success(st.findNote(it.id)?.toResponse()) }
91+
Effect().persist(it).thenReply(cmd.replyTo) { st -> StatusReply.success(st.findNote(it.id)?.toResponse) }
9292
}
9393
}
9494

src/test/kotlin/blog/ApiTests.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ class ApiTests {
163163
assertThat(response.status.value).isEqualTo(200)
164164
val ur = response.body<UserResponse>()
165165
assertThat(ur.name).isEqualTo("Anders")
166+
167+
response = client.get("http://localhost:8181/api/users/me") {
168+
header("Authorization", "Bearer ${token.token}")
169+
}
170+
assertThat(response.status.value).isEqualTo(200)
171+
val updatedUser: UserResponse = response.body<UserResponse>()
172+
assertThat(updatedUser.name).isEqualTo("Anders")
166173
}
167174
}
168175

src/test/kotlin/blog/model/NoteTests.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class NoteTests {
3535
assertThat(nc.user).isEqualTo(userId)
3636
assertThat(nc.title).isEqualTo("title")
3737
assertThat(nc.body).isEqualTo("body")
38+
assertThat(nc.received).isNotNull
39+
assertThat(nc.received).isBefore(znow)
3840

3941
val note = nc.toEntity
4042
assertThat(note.id).isNotNull()

src/test/kotlin/blog/model/StateTests.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package blog.model
22

33
import org.assertj.core.api.Assertions.assertThat
44
import org.junit.jupiter.api.Test
5-
import java.time.LocalDateTime
65

76
class StateTests {
87

@@ -75,7 +74,7 @@ class StateTests {
7574
val state = State()
7675
val user = User(nextId, "[email protected]", "Tester", pw)
7776
val state2 = state.save(user)
78-
val task = Task(nextId, user.id, "Test", "test", "Tasking, 1.. 2..", LocalDateTime.now().plusHours(1))
77+
val task = Task(nextId, user.id, "Test", "test", "Tasking, 1.. 2..", znow.plusHours(1))
7978
val state3 = state2.save(task)
8079
assertThat(state3.taskCount()).isEqualTo(1)
8180
assertThat(state3.findTask(task.id)).isNotNull

0 commit comments

Comments
 (0)