├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .idea
└── .gitignore
├── LICENSE
├── README.md
├── images
├── AggregareStateTransitions.png
├── AggregationSagaExample.PNG
├── CommandResultsInEvent.png
├── CommandResultsInEventNew.png
├── ConfigClassDiagramm.png
├── EventInsertion.png
├── Example1.png
├── GeneralAggregateUpdate.png
├── NestedSagaExample.PNG
├── SagaExample.PNG
└── SagaProcessing.PNG
├── pom.xml
├── tiny-event-sourcing-app-base
├── pom.xml
└── src
│ └── main
│ └── kotlin
│ └── ru.quipy
│ └── TinyEsLibConfig.kt
├── tiny-event-sourcing-app
├── pom.xml
└── src
│ ├── main
│ ├── kotlin
│ │ └── ru
│ │ │ └── quipy
│ │ │ ├── Application.kt
│ │ │ ├── application
│ │ │ ├── App.kt
│ │ │ ├── component
│ │ │ │ ├── BaseComponent.kt
│ │ │ │ └── Component.kt
│ │ │ ├── config
│ │ │ │ └── TinyEsDependencyConfig.kt
│ │ │ └── context
│ │ │ │ └── Context.kt
│ │ │ ├── bankDemo
│ │ │ ├── BankContext.kt
│ │ │ ├── accounts
│ │ │ │ ├── api
│ │ │ │ │ ├── AccountAggregate.kt
│ │ │ │ │ └── AccountEvents.kt
│ │ │ │ ├── config
│ │ │ │ │ └── AccountBoundedContextConfig.kt
│ │ │ │ ├── logic
│ │ │ │ │ └── Account.kt
│ │ │ │ └── subscribers
│ │ │ │ │ └── TransactionsSubscriber.kt
│ │ │ └── transfers
│ │ │ │ ├── api
│ │ │ │ ├── TransferTransactionAggregate.kt
│ │ │ │ └── TransferTransactionEvents.kt
│ │ │ │ ├── config
│ │ │ │ └── TransactionCoundedContextConfig.kt
│ │ │ │ ├── db
│ │ │ │ ├── BankAccountCacheRepository.kt
│ │ │ │ ├── BankAccountCacheRepositoryImpl.kt
│ │ │ │ └── entity
│ │ │ │ │ └── BankAccount.kt
│ │ │ │ ├── logic
│ │ │ │ └── TransaferTransaction.kt
│ │ │ │ ├── projections
│ │ │ │ └── BankAccountsExistenceCache.kt
│ │ │ │ ├── service
│ │ │ │ └── TransactionService.kt
│ │ │ │ └── subscribers
│ │ │ │ └── BankAccountsSubscriber.kt
│ │ │ └── projectDemo
│ │ │ ├── ProjectContext.kt
│ │ │ ├── api
│ │ │ ├── ProjectAggregate.kt
│ │ │ └── ProjectAggregateDomainEvents.kt
│ │ │ ├── config
│ │ │ └── ProjectDemoConfig.kt
│ │ │ ├── logic
│ │ │ ├── ProjectAggregateCommands.kt
│ │ │ └── ProjectAggregateState.kt
│ │ │ └── projections
│ │ │ ├── AnnotationBasedProjectEventsSubscriber.kt
│ │ │ └── ProjectEventsSubscriber.kt
│ └── resources
│ │ ├── application.properties
│ │ └── logback.xml
│ └── test
│ └── kotlin
│ ├── ActiveStreamReaderTest.kt
│ ├── BankAggregateStateTest.kt
│ ├── BaseTest.kt
│ ├── EventStreamsTest.kt
│ ├── StreamEventOrderingTest.kt
│ ├── TransferTransactionAggregateStateTest.kt
│ └── config
│ └── DockerPostgresDataSourceInitialize.kt
├── tiny-event-sourcing-lib
├── pom.xml
└── src
│ └── main
│ └── kotlin
│ └── ru
│ └── quipy
│ ├── core
│ ├── AggregateRegistry.kt
│ ├── BasicAggregateRegistry.kt
│ ├── EventSourcingProperties.kt
│ ├── EventSourcingService.kt
│ ├── EventSourcingServiceFactory.kt
│ ├── SeekingForSuitableClassesAggregateRegistry.kt
│ ├── annotations
│ │ ├── AggregateType.kt
│ │ ├── DomainEvent.kt
│ │ └── StateTransitionFunc.kt
│ └── exceptions
│ │ ├── DuplicateEventIdException.kt
│ │ └── EventRecordOptimisticLockException.kt
│ ├── database
│ └── EventStore.kt
│ ├── domain
│ └── EventSourcingDomain.kt
│ ├── mapper
│ └── EventMapper.kt
│ ├── saga
│ ├── SagaInfo.kt
│ ├── SagaManager.kt
│ └── aggregate
│ │ ├── api
│ │ ├── SagaStepAggregate.kt
│ │ └── SagaStepEvents.kt
│ │ ├── logic
│ │ └── SagaStepAggregateState.kt
│ │ └── stream
│ │ └── SagaEventStream.kt
│ ├── streams
│ ├── AggregateEventStream.kt
│ ├── AggregateEventStreamManager.kt
│ ├── AggregateSubscriptionsManager.kt
│ ├── BufferedAggregateEventStream.kt
│ ├── EventStoreReader.kt
│ ├── EventStreamReaderManager.kt
│ ├── EventStreamSubscriber.kt
│ ├── EventsChannel.kt
│ └── annotation
│ │ ├── AggregateSubscriber.kt
│ │ └── SubscribeEvent.kt
│ └── utils
│ ├── Batcher.kt
│ ├── NamedThreadFactory.kt
│ └── PeriodicalJob.kt
├── tiny-event-sourcing-sagas-projections
├── pom.xml
└── src
│ └── main
│ ├── kotlin
│ └── ru
│ │ └── quipy
│ │ ├── SagasProjectionsApplication.kt
│ │ └── projections
│ │ ├── SagaDefaultProjections.kt
│ │ └── SagaProjections.kt
│ └── resources
│ └── application.properties
├── tiny-event-sourcing-spring-app
├── .env
├── docker-compose.yml
├── mongo
│ └── Dockerfile
├── pom.xml
└── src
│ ├── main
│ ├── kotlin
│ │ └── ru.quipy
│ │ │ ├── Application.kt
│ │ │ ├── bankDemo
│ │ │ ├── accounts
│ │ │ │ ├── api
│ │ │ │ │ ├── AccountAggregate.kt
│ │ │ │ │ └── AccountEvents.kt
│ │ │ │ ├── config
│ │ │ │ │ └── AccountBoundedContextConfig.kt
│ │ │ │ ├── controller
│ │ │ │ │ └── AccountController.kt
│ │ │ │ ├── logic
│ │ │ │ │ └── Account.kt
│ │ │ │ └── subscribers
│ │ │ │ │ └── TransactionsSubscriber.kt
│ │ │ └── transfers
│ │ │ │ ├── api
│ │ │ │ ├── TransferTransactionAggregate.kt
│ │ │ │ └── TransferTransactionEvents.kt
│ │ │ │ ├── config
│ │ │ │ └── TransactionCoundedContextConfig.kt
│ │ │ │ ├── logic
│ │ │ │ └── TransaferTransaction.kt
│ │ │ │ ├── projections
│ │ │ │ └── BankAccountsExistenceCache.kt
│ │ │ │ ├── service
│ │ │ │ └── TransactionService.kt
│ │ │ │ └── subscribers
│ │ │ │ └── BankAccountsSubscriber.kt
│ │ │ ├── projectDemo
│ │ │ ├── api
│ │ │ │ ├── ProjectAggregate.kt
│ │ │ │ └── ProjectAggregateDomainEvents.kt
│ │ │ ├── config
│ │ │ │ └── ProjectDemoConfig.kt
│ │ │ ├── logic
│ │ │ │ ├── ProjectAggregateCommands.kt
│ │ │ │ └── ProjectAggregateState.kt
│ │ │ └── projections
│ │ │ │ ├── AnnotationBasedProjectEventsSubscriber.kt
│ │ │ │ └── ProjectEventsSubscriber.kt
│ │ │ └── sagaDemo
│ │ │ ├── flights
│ │ │ ├── api
│ │ │ │ ├── FlightAggregate.kt
│ │ │ │ └── FlightsEvents.kt
│ │ │ ├── config
│ │ │ │ └── FlightBoundedContextConfig.kt
│ │ │ ├── logic
│ │ │ │ └── Flight.kt
│ │ │ └── subscribers
│ │ │ │ └── PaymentSubscriber.kt
│ │ │ ├── payment
│ │ │ ├── api
│ │ │ │ ├── PaymentAggregate.kt
│ │ │ │ └── PaymentEvents.kt
│ │ │ ├── config
│ │ │ │ └── PaymentBoundedContextConfig.kt
│ │ │ ├── logic
│ │ │ │ └── Payment.kt
│ │ │ └── subscribers
│ │ │ │ └── TripSubscriber.kt
│ │ │ └── trips
│ │ │ ├── api
│ │ │ ├── TripAggregate.kt
│ │ │ └── TripEvents.kt
│ │ │ ├── config
│ │ │ └── TripBoundedContextConfig.kt
│ │ │ ├── controller
│ │ │ └── TripController.kt
│ │ │ ├── logic
│ │ │ └── Trip.kt
│ │ │ └── subscribers
│ │ │ ├── FlightSubscriber.kt
│ │ │ └── PaymentTripSubscriber.kt
│ └── resources
│ │ └── application.properties
│ └── test
│ ├── kotlin
│ └── ru.quipy
│ │ ├── ActiveStreamReaderTest.kt
│ │ ├── BankAggregateStateTest.kt
│ │ ├── BaseTest.kt
│ │ ├── EventStreamsTest.kt
│ │ ├── StreamEventOrderingTest.kt
│ │ ├── TransferTransactionAggregateStateTest.kt
│ │ └── config
│ │ └── DockerPostgresDataSourceInitialize.kt
│ └── resources
│ └── application-test.yml
├── tiny-event-sourcing-spring-boot-starter
├── pom.xml
└── src
│ └── main
│ ├── kotlin
│ └── ru
│ │ └── quipy
│ │ └── config
│ │ └── EventSourcingLibConfig.kt
│ └── resources
│ └── META-INF
│ ├── spring-configuration-metadata.json
│ └── spring.factories
├── tiny-mongo-event-store-spring-boot-starter
├── pom.xml
└── src
│ ├── main
│ ├── kotlin
│ │ └── ru
│ │ │ └── quipy
│ │ │ ├── MongoTemplateEventStore.kt
│ │ │ └── autoconfigure
│ │ │ └── MongoEventStoreAutoConfiguration.kt
│ └── resources
│ │ └── META-INF
│ │ └── spring.factories
│ └── test
│ ├── kotlin
│ └── ru
│ │ └── quipy
│ │ └── updateSerial
│ │ ├── TestAggregate.kt
│ │ ├── UpdateSerialTest.kt
│ │ └── UpdateSerialTestConfiguration.kt
│ └── resources
│ └── application-test.yml
├── tiny-mongo-event-store
├── pom.xml
└── src
│ └── main
│ └── kotlin
│ └── ru
│ └── quipy
│ └── eventstore
│ ├── MongoClientEventStore.kt
│ ├── converter
│ ├── BsonConverter.kt
│ ├── JacksonMongoEntityConverter.kt
│ ├── MongoEntityConverter.kt
│ └── UuidConverter.kt
│ ├── exception
│ ├── MongoClientException.kt
│ ├── MongoClientExceptionTranslator.kt
│ └── MongoDuplicateKeyException.kt
│ └── factory
│ ├── MongoClientFactory.kt
│ └── MongoClientFactoryImpl.kt
├── tiny-postgres-event-store-spring-boot-starter
├── pom.xml
└── src
│ ├── main
│ ├── kotlin
│ │ └── ru
│ │ │ └── quipy
│ │ │ ├── PostgresTemplateEventStore.kt
│ │ │ ├── autoconfigure
│ │ │ └── PostgresEventStoreAutoConfiguration.kt
│ │ │ ├── config
│ │ │ ├── DatabaseConfig.kt
│ │ │ └── LiquibaseSpringConfig.kt
│ │ │ └── mappers
│ │ │ ├── ActiveEventStreamReaderRowMapper.kt
│ │ │ ├── EventRowMapper.kt
│ │ │ ├── EventStreamReadIndexRowMapper.kt
│ │ │ ├── MapperFactory.kt
│ │ │ ├── MapperFactoryImpl.kt
│ │ │ └── SnapshotRowMapper.kt
│ └── resources
│ │ ├── META-INF
│ │ └── spring.factories
│ │ ├── application.properties
│ │ └── database
│ │ ├── .env
│ │ └── docker-compose.yml
│ └── test
│ ├── kotlin
│ └── ru
│ │ └── quipy
│ │ ├── PostgresEventStoreTest.kt
│ │ └── config
│ │ └── TestDbConfig.kt
│ └── resources
│ └── application.properties
└── tiny-postgresql-event-store
├── pom.xml
└── src
├── main
├── kotlin
│ └── ru
│ │ └── quipy
│ │ ├── PostgresClientEventStore.kt
│ │ ├── config
│ │ └── LiquibaseConfig.kt
│ │ ├── converter
│ │ ├── EntityConverter.kt
│ │ ├── JsonEntityConverter.kt
│ │ ├── ResultSetToEntityMapper.kt
│ │ ├── ResultSetToEntityMapperImpl.kt
│ │ └── exception
│ │ │ └── NoMapperForClass.kt
│ │ ├── db
│ │ ├── DataSourceProvider.kt
│ │ ├── DatasourceProviderImpl.kt
│ │ ├── HikariDatasourceProvider.kt
│ │ └── factory
│ │ │ ├── ConnectionFactory.kt
│ │ │ └── DataSourceConnectionFactoryImpl.kt
│ │ ├── exception
│ │ └── UnknownEntityClassException.kt
│ │ ├── executor
│ │ ├── ExceptionLoggingSqlQueriesExecutor.kt
│ │ └── QueryExecutor.kt
│ │ ├── query
│ │ ├── BasicQuery.kt
│ │ ├── Query.kt
│ │ ├── QueryBuilder.kt
│ │ ├── delete
│ │ │ └── DeleteQuery.kt
│ │ ├── exception
│ │ │ ├── InvalidQueryStateException.kt
│ │ │ └── UnmappedDtoType.kt
│ │ ├── insert
│ │ │ ├── BatchInsertQuery.kt
│ │ │ ├── InsertQuery.kt
│ │ │ └── OnDuplicateKeyUpdateInsertQuery.kt
│ │ ├── select
│ │ │ └── SelectQuery.kt
│ │ └── update
│ │ │ └── UpdateQuery.kt
│ │ └── tables
│ │ ├── Column.kt
│ │ ├── Dto.kt
│ │ ├── DtoCreator.kt
│ │ └── Tables.kt
└── resources
│ └── liquibase
│ └── changelog.sql
└── test
└── kotlin
└── ru
└── quipy
└── query
├── InsertQueryTest.kt
└── SelectQueryTest.kt
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release to Maven Central
2 |
3 | on:
4 | push:
5 | branches: ["master", "master-2.0"]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 |
13 | - name: Set up JDK 11
14 | uses: actions/setup-java@v3
15 | with:
16 | distribution: 'temurin'
17 | java-version: '11'
18 | server-id: ossrh
19 | server-username: MAVEN_USERNAME
20 | server-password: MAVEN_PASSWORD
21 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
22 | gpg-passphrase: MAVEN_GPG_PASSPHRASE
23 |
24 | - name: Deploy to Maven Central
25 | run: mvn -B deploy -P release
26 | env:
27 | MAVEN_USERNAME: ${{ secrets.OSSRH_USER }}
28 | MAVEN_PASSWORD: ${{ secrets.OSSRH_PWD }}
29 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project exclude paths
2 | **/target/
3 | *.iml
4 | /.idea/
5 | # IDEA files
6 |
7 | .idea/libraries/**
8 | .idea/compiler.xml
9 | .idea/jarRepositories.xml
10 | .idea/kotlinc.xml
11 | .idea/vcs.xml
12 | .idea/misc.xml
13 | .idea/modules.xml
14 |
15 | **/*.iml
16 |
17 | *.versionsBackup
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 | # GitHub Copilot persisted chat sessions
10 | /copilot/chatSessions
11 |
--------------------------------------------------------------------------------
/images/AggregareStateTransitions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/AggregareStateTransitions.png
--------------------------------------------------------------------------------
/images/AggregationSagaExample.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/AggregationSagaExample.PNG
--------------------------------------------------------------------------------
/images/CommandResultsInEvent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/CommandResultsInEvent.png
--------------------------------------------------------------------------------
/images/CommandResultsInEventNew.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/CommandResultsInEventNew.png
--------------------------------------------------------------------------------
/images/ConfigClassDiagramm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/ConfigClassDiagramm.png
--------------------------------------------------------------------------------
/images/EventInsertion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/EventInsertion.png
--------------------------------------------------------------------------------
/images/Example1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/Example1.png
--------------------------------------------------------------------------------
/images/GeneralAggregateUpdate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/GeneralAggregateUpdate.png
--------------------------------------------------------------------------------
/images/NestedSagaExample.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/NestedSagaExample.PNG
--------------------------------------------------------------------------------
/images/SagaExample.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/SagaExample.PNG
--------------------------------------------------------------------------------
/images/SagaProcessing.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrsuh/tiny-event-sourcing/880796f04bb6e27aab615c8efccd709d5ca22193/images/SagaProcessing.PNG
--------------------------------------------------------------------------------
/tiny-event-sourcing-app-base/pom.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 | 4.0.0
6 | jar
7 |
8 |
9 | ru.quipy
10 | tiny-event-sourcing
11 | 2.7.4
12 |
13 |
14 | tiny-event-sourcing-app-base
15 | tiny-event-sourcing-app-base
16 |
17 |
18 | 11
19 | true
20 |
21 |
22 |
23 |
24 | ru.quipy
25 | tiny-event-sourcing-lib
26 | ${project.version}
27 |
28 |
29 |
30 | ru.quipy
31 | tiny-postgresql-event-store
32 | ${project.version}
33 |
34 |
35 |
36 | org.slf4j
37 | slf4j-api
38 | 2.0.9
39 |
40 |
41 |
42 | ch.qos.logback
43 | logback-classic
44 | 1.4.14
45 |
46 |
47 |
48 | ch.qos.logback
49 | logback-core
50 | 1.4.14
51 |
52 |
53 |
54 |
55 | src/main/kotlin
56 |
57 |
58 | org.springframework.boot
59 | spring-boot-maven-plugin
60 |
61 |
62 | org.jetbrains.kotlin
63 | kotlin-maven-plugin
64 | ${kotlin.version}
65 |
66 |
67 | compile
68 | compile
69 |
70 | compile
71 |
72 |
73 |
74 |
75 |
76 | -Xjsr305=strict
77 |
78 |
79 | spring
80 |
81 |
82 |
83 |
84 | org.jetbrains.kotlin
85 | kotlin-maven-allopen
86 | 1.6.10
87 |
88 |
89 |
90 |
91 | org.apache.maven.plugins
92 | maven-compiler-plugin
93 |
94 | 8
95 | 8
96 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-app-base/src/main/kotlin/ru.quipy/TinyEsLibConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import ru.quipy.converter.EntityConverter
5 | import ru.quipy.converter.ResultSetToEntityMapper
6 | import ru.quipy.core.AggregateRegistry
7 | import ru.quipy.core.EventSourcingProperties
8 | import ru.quipy.core.EventSourcingService
9 | import ru.quipy.core.EventSourcingServiceFactory
10 | import ru.quipy.database.EventStore
11 | import ru.quipy.db.DataSourceProvider
12 | import ru.quipy.mapper.EventMapper
13 | import ru.quipy.saga.SagaManager
14 | import ru.quipy.saga.aggregate.api.SagaStepAggregate
15 | import ru.quipy.saga.aggregate.logic.SagaStepAggregateState
16 | import ru.quipy.saga.aggregate.stream.SagaEventStream
17 | import ru.quipy.streams.AggregateEventStreamManager
18 | import ru.quipy.streams.AggregateSubscriptionsManager
19 | import ru.quipy.streams.EventStreamReaderManager
20 | import java.util.UUID
21 |
22 | class TinyEsLibConfig (
23 | val objectMapper: ObjectMapper,
24 | val eventMapper: EventMapper,
25 | val eventSourcingProperties: EventSourcingProperties,
26 | val aggregateRegistry: AggregateRegistry,
27 | val eventStreamReaderManager: EventStreamReaderManager,
28 | val eventStreamManager: AggregateEventStreamManager,
29 | val subscriptionsManager: AggregateSubscriptionsManager,
30 | val eventSourcingServiceFactory: EventSourcingServiceFactory,
31 | val sagaStepEsService: EventSourcingService,
32 | val sagaManager: SagaManager,
33 | val sagaEventStream: SagaEventStream,
34 | val entityConverter: EntityConverter,
35 | val resultSetToEntityMapper: ResultSetToEntityMapper,
36 | val datasourceProvider: DataSourceProvider,
37 | val eventStore: EventStore,
38 | // val databaseConnectionFactory: ConnectionFactory
39 | ) {}
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/Application.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy
2 |
3 | import ru.quipy.application.App
4 |
5 | class Application
6 | fun main(args: Array) {
7 | App.start()
8 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/application/App.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.application
2 |
3 | import ru.quipy.application.context.Context
4 | import ru.quipy.core.EventSourcingProperties
5 | import java.util.Properties
6 |
7 | class App private constructor(val context: Context) {
8 |
9 | companion object {
10 | private var app: App? = null
11 | fun start() {
12 | val properties = Properties();
13 | properties.load(App::class.java.classLoader.getResourceAsStream("application.properties"))
14 | if (app == null) {
15 | app = App(Context(properties, eventSourcingProperties = EventSourcingProperties()))
16 | }
17 | }
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/application/component/BaseComponent.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.application.component
2 |
3 | open class BaseComponent : Component {
4 | override fun postConstruct() {
5 | // no op
6 | }
7 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/application/component/Component.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.application.component
2 |
3 | interface Component {
4 | fun postConstruct()
5 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/application/context/Context.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.application.context
2 |
3 | import com.mongodb.ConnectionString
4 | import com.mongodb.MongoClientSettings
5 | import com.mongodb.client.MongoClients
6 | import com.mongodb.client.MongoDatabase
7 | import com.zaxxer.hikari.HikariConfig
8 | import com.zaxxer.hikari.HikariDataSource
9 | import org.bson.UuidRepresentation
10 | import ru.quipy.TinyEsLibConfig
11 | import ru.quipy.application.config.TinyEsDependencyConfig
12 | import ru.quipy.bankDemo.BankContext
13 | import ru.quipy.config.LiquibaseConfig
14 | import ru.quipy.core.EventSourcingProperties
15 | import ru.quipy.projectDemo.ProjectContext
16 | import java.util.Properties
17 |
18 | class Context private constructor() {
19 | lateinit var tinyEsLibConfig: TinyEsLibConfig
20 |
21 | lateinit var dataSource: HikariDataSource
22 | lateinit var mongoDatabase: MongoDatabase
23 |
24 | lateinit var projectContext: ProjectContext
25 | lateinit var bankContext: BankContext
26 | constructor(properties: Properties, eventSourcingProperties: EventSourcingProperties) : this() {
27 | this.dataSource = dataSource(properties)
28 | LiquibaseConfig().liquibase(dataSource, properties.getProperty("event.sourcing.db-schema"))
29 |
30 | this.mongoDatabase = mongoDatabase(properties)
31 |
32 | tinyEsLibConfig = TinyEsDependencyConfig(properties, dataSource, eventSourcingProperties)
33 | .tinyEsLibConfig
34 |
35 | projectContext = ProjectContext(tinyEsLibConfig)
36 | bankContext = BankContext(tinyEsLibConfig, mongoDatabase)
37 | }
38 |
39 |
40 | private fun dataSource(properties: Properties) : HikariDataSource {
41 | val hikariConfig = HikariConfig()
42 | hikariConfig.jdbcUrl = properties.getProperty("datasource.jdbc-url")
43 | hikariConfig.username = properties.getProperty("datasource.username")
44 | hikariConfig.password = properties.getProperty("datasource.password")
45 |
46 | return HikariDataSource(hikariConfig)
47 | }
48 |
49 | private fun mongoDatabase(properties: Properties) : MongoDatabase {
50 | val clientSettings = MongoClientSettings.builder()
51 | .uuidRepresentation(UuidRepresentation.STANDARD)
52 | .applyConnectionString(ConnectionString(properties.getProperty("mongodb.url")))
53 | .build()
54 |
55 | return MongoClients.create(clientSettings)
56 | .getDatabase("tiny-es")
57 | }
58 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/BankContext.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo
2 |
3 | import com.mongodb.client.MongoDatabase
4 | import ru.quipy.TinyEsLibConfig
5 | import ru.quipy.application.component.Component
6 | import ru.quipy.bankDemo.accounts.api.AccountAggregate
7 | import ru.quipy.bankDemo.accounts.config.AccountBoundedContextConfig
8 | import ru.quipy.bankDemo.accounts.logic.Account
9 | import ru.quipy.bankDemo.accounts.subscribers.TransactionsSubscriber
10 | import ru.quipy.bankDemo.transfers.api.TransferTransactionAggregate
11 | import ru.quipy.bankDemo.transfers.config.TransactionCoundedContextConfig
12 | import ru.quipy.bankDemo.transfers.db.BankAccountCacheRepository
13 | import ru.quipy.bankDemo.transfers.db.BankAccountCacheRepositoryImpl
14 | import ru.quipy.bankDemo.transfers.logic.TransferTransaction
15 | import ru.quipy.bankDemo.transfers.projections.BankAccountsExistenceCache
16 | import ru.quipy.bankDemo.transfers.service.TransactionService
17 | import ru.quipy.bankDemo.transfers.subscribers.BankAccountsSubscriber
18 | import ru.quipy.core.EventSourcingService
19 | import java.util.UUID
20 |
21 | class BankContext private constructor() {
22 | lateinit var components: List
23 | lateinit var accountBoundedContextConfig: AccountBoundedContextConfig
24 | lateinit var transactionsSubscriber: TransactionsSubscriber
25 | lateinit var accountEventSourcingService: EventSourcingService
26 |
27 | lateinit var transactionCoundedContextConfig: TransactionCoundedContextConfig
28 | lateinit var bankAccountsExistenceCache: BankAccountsExistenceCache
29 |
30 | lateinit var bankAccountCacheRepository: BankAccountCacheRepository
31 | lateinit var transactionService : TransactionService
32 | lateinit var bankAccountsSubscriber: BankAccountsSubscriber
33 | lateinit var transactionEventSourcingService: EventSourcingService
34 | constructor(tinyEsLibConfig: TinyEsLibConfig, mongoDatabase: MongoDatabase) : this() {
35 | accountBoundedContextConfig = AccountBoundedContextConfig(tinyEsLibConfig.eventSourcingServiceFactory)
36 | accountEventSourcingService = accountBoundedContextConfig.accountEsService()
37 | transactionsSubscriber = TransactionsSubscriber(tinyEsLibConfig.subscriptionsManager, accountEventSourcingService)
38 |
39 | transactionCoundedContextConfig = TransactionCoundedContextConfig(tinyEsLibConfig.eventSourcingServiceFactory)
40 | bankAccountCacheRepository = BankAccountCacheRepositoryImpl(mongoDatabase)
41 |
42 | bankAccountsExistenceCache = BankAccountsExistenceCache(bankAccountCacheRepository, tinyEsLibConfig.subscriptionsManager)
43 |
44 | transactionEventSourcingService = transactionCoundedContextConfig.transactionEsService()
45 | transactionService = TransactionService(bankAccountCacheRepository, transactionEventSourcingService)
46 | bankAccountsSubscriber = BankAccountsSubscriber(tinyEsLibConfig.subscriptionsManager, transactionEventSourcingService)
47 |
48 | components = mutableListOf(transactionsSubscriber, bankAccountsExistenceCache, transactionService, bankAccountsSubscriber)
49 | components.forEach { it.postConstruct() }
50 | }
51 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/accounts/api/AccountAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.accounts.api
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.domain.Aggregate
5 |
6 | @AggregateType(aggregateEventsTableName = "accounts")
7 | class AccountAggregate: Aggregate
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/accounts/api/AccountEvents.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.accounts.api
2 |
3 | import ru.quipy.core.annotations.DomainEvent
4 | import ru.quipy.domain.Event
5 | import java.math.BigDecimal
6 | import java.util.UUID
7 |
8 | const val ACCOUNT_CREATED = "ACCOUNT_CREATED_EVENT"
9 | const val BANK_ACCOUNT_CREATED = "BANK_ACCOUNT_CREATED_EVENT"
10 | const val BANK_ACCOUNT_DEPOSIT = "BANK_ACCOUNT_DEPOSIT_EVENT"
11 | const val BANK_ACCOUNT_WITHDRAWAL = "BANK_ACCOUNT_WITHDRAWAL_EVENT"
12 | const val INTERNAL_ACCOUNT_TRANSFER = "INTERNAL_ACCOUNT_TRANSFER_EVENT"
13 |
14 | const val TRANSFER_TRANSACTION_ACCEPTED = "TRANSFER_TRANSACTION_ACCEPTED"
15 | const val TRANSFER_TRANSACTION_DECLINED = "TRANSFER_TRANSACTION_DECLINED"
16 | const val TRANSFER_TRANSACTION_PROCESSED = "TRANSFER_TRANSACTION_PROCESSED"
17 | const val TRANSFER_TRANSACTION_ROLLBACKED = "TRANSFER_TRANSACTION_ROLLBACKED"
18 |
19 |
20 | @DomainEvent(name = ACCOUNT_CREATED)
21 | data class AccountCreatedEvent(
22 | val accountId: UUID,
23 | val userId: UUID,
24 | ) : Event(
25 | name = ACCOUNT_CREATED,
26 | )
27 |
28 | @DomainEvent(name = BANK_ACCOUNT_CREATED)
29 | data class BankAccountCreatedEvent(
30 | val accountId: UUID,
31 | val bankAccountId: UUID,
32 | ) : Event(
33 | name = BANK_ACCOUNT_CREATED,
34 | )
35 |
36 | @DomainEvent(name = BANK_ACCOUNT_DEPOSIT)
37 | data class BankAccountDepositEvent(
38 | val accountId: UUID,
39 | val bankAccountId: UUID,
40 | val amount: BigDecimal,
41 | ) : Event(
42 | name = BANK_ACCOUNT_DEPOSIT,
43 | )
44 |
45 | @DomainEvent(name = BANK_ACCOUNT_WITHDRAWAL)
46 | data class BankAccountWithdrawalEvent(
47 | val accountId: UUID,
48 | val bankAccountId: UUID,
49 | val amount: BigDecimal,
50 | ) : Event(
51 | name = BANK_ACCOUNT_WITHDRAWAL,
52 | )
53 |
54 | @DomainEvent(name = INTERNAL_ACCOUNT_TRANSFER)
55 | data class InternalAccountTransferEvent(
56 | val accountId: UUID,
57 | val bankAccountIdFrom: UUID,
58 | val bankAccountIdTo: UUID,
59 | val amount: BigDecimal,
60 | ) : Event(
61 | name = INTERNAL_ACCOUNT_TRANSFER,
62 | )
63 |
64 | @DomainEvent(name = TRANSFER_TRANSACTION_ACCEPTED)
65 | data class TransferTransactionAcceptedEvent(
66 | val accountId: UUID,
67 | val bankAccountId: UUID,
68 | val transactionId: UUID,
69 | val transferAmount: BigDecimal,
70 | val isDeposit: Boolean
71 | ) : Event(
72 | name = TRANSFER_TRANSACTION_ACCEPTED,
73 | )
74 |
75 | @DomainEvent(name = TRANSFER_TRANSACTION_DECLINED)
76 | data class TransferTransactionDeclinedEvent(
77 | val accountId: UUID,
78 | val bankAccountId: UUID,
79 | val transactionId: UUID,
80 | val reason: String
81 | ) : Event(
82 | name = TRANSFER_TRANSACTION_DECLINED,
83 | )
84 |
85 | @DomainEvent(name = TRANSFER_TRANSACTION_PROCESSED)
86 | data class TransferTransactionProcessedEvent(
87 | val accountId: UUID,
88 | val bankAccountId: UUID,
89 | val transactionId: UUID,
90 | ) : Event(
91 | name = TRANSFER_TRANSACTION_PROCESSED,
92 | )
93 |
94 | @DomainEvent(name = TRANSFER_TRANSACTION_ROLLBACKED)
95 | data class TransferTransactionRollbackedEvent(
96 | val accountId: UUID,
97 | val bankAccountId: UUID,
98 | val transactionId: UUID,
99 | ) : Event(
100 | name = TRANSFER_TRANSACTION_ROLLBACKED,
101 | )
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/accounts/config/AccountBoundedContextConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.accounts.config
2 |
3 | import ru.quipy.bankDemo.accounts.api.AccountAggregate
4 | import ru.quipy.bankDemo.accounts.logic.Account
5 | import ru.quipy.core.EventSourcingService
6 | import ru.quipy.core.EventSourcingServiceFactory
7 | import java.util.UUID
8 | class AccountBoundedContextConfig(val eventSourcingServiceFactory: EventSourcingServiceFactory) {
9 | fun accountEsService(): EventSourcingService =
10 | eventSourcingServiceFactory.create()
11 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/accounts/subscribers/TransactionsSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.accounts.subscribers
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import ru.quipy.application.component.Component
6 | import ru.quipy.bankDemo.accounts.api.AccountAggregate
7 | import ru.quipy.bankDemo.accounts.logic.Account
8 | import ru.quipy.bankDemo.transfers.api.TransactionConfirmedEvent
9 | import ru.quipy.bankDemo.transfers.api.TransferTransactionAggregate
10 | import ru.quipy.bankDemo.transfers.api.TransferTransactionCreatedEvent
11 | import ru.quipy.core.EventSourcingService
12 | import ru.quipy.streams.AggregateSubscriptionsManager
13 | import java.util.UUID
14 |
15 | class TransactionsSubscriber(
16 | private val subscriptionsManager: AggregateSubscriptionsManager,
17 | private val accountEsService: EventSourcingService
18 | ) : Component {
19 | private val logger: Logger = LoggerFactory.getLogger(TransactionsSubscriber::class.java)
20 |
21 | override fun postConstruct() {
22 | subscriptionsManager.createSubscriber(TransferTransactionAggregate::class, "accounts::transaction-processing-subscriber") {
23 | `when`(TransferTransactionCreatedEvent::class) { event ->
24 | logger.info("Got transaction to process: $event")
25 |
26 | val transactionOutcome1 = accountEsService.update(event.sourceAccountId) { // todo sukhoa idempotence!
27 | it.performTransferFrom(
28 | event.sourceBankAccountId,
29 | event.transferId,
30 | event.transferAmount
31 | )
32 | }
33 |
34 | val transactionOutcome2 = accountEsService.update(event.destinationAccountId) { // todo sukhoa idempotence!
35 | it.performTransferTo(
36 | event.destinationBankAccountId,
37 | event.transferId,
38 | event.transferAmount
39 | )
40 | }
41 |
42 | logger.info("Transaction: ${event.transferId}. Outcomes: $transactionOutcome1, $transactionOutcome2")
43 | }
44 | `when`(TransactionConfirmedEvent::class) { event ->
45 | logger.info("Got transaction confirmed event: $event")
46 |
47 | val transactionOutcome1 = accountEsService.update(event.sourceAccountId) { // todo sukhoa idempotence!
48 | it.processPendingTransaction(event.sourceBankAccountId, event.transferId)
49 | }
50 |
51 | val transactionOutcome2 = accountEsService.update(event.destinationAccountId) { // todo sukhoa idempotence!
52 | it.processPendingTransaction(event.destinationBankAccountId, event.transferId)
53 | }
54 |
55 | logger.info("Transaction: ${event.transferId}. Outcomes: $transactionOutcome1, $transactionOutcome2")
56 | }
57 | // todo sukhoa bank account deleted event
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/transfers/api/TransferTransactionAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.api
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.domain.Aggregate
5 |
6 | @AggregateType(aggregateEventsTableName = "transfers")
7 | class TransferTransactionAggregate: Aggregate
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/transfers/api/TransferTransactionEvents.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.api
2 |
3 | import ru.quipy.core.annotations.DomainEvent
4 | import ru.quipy.domain.Event
5 | import java.math.BigDecimal
6 | import java.util.UUID
7 |
8 | const val TRANSFER_TRANSACTION_CREATED = "TRANSFER_TRANSACTION_CREATED"
9 | const val TRANSFER_PARTICIPANT_ACCEPTED = "TRANSFER_PARTICIPANT_ACCEPTED"
10 | const val TRANSFER_CONFIRMED = "TRANSFER_CONFIRMED"
11 | const val TRANSFER_NOT_CONFIRMED = "TRANSFER_NOT_CONFIRMED"
12 | const val TRANSFER_PARTICIPANT_COMMITTED = "TRANSFER_PARTICIPANT_COMMITTED"
13 | const val TRANSFER_SUCCEEDED = "TRANSFER_SUCCEEDED"
14 | const val TRANSFER_PARTICIPANT_ROLLBACKED = "TRANSFER_PARTICIPANT_ROLLBACKED"
15 | const val TRANSFER_FAILED = "TRANSFER_FAILED"
16 | const val NOOP = "NOOP"
17 | const val TRANSFER_CANCELLED = "TRANSFER_CANCELLED"
18 |
19 | @DomainEvent(name = TRANSFER_TRANSACTION_CREATED)
20 | data class TransferTransactionCreatedEvent(
21 | val transferId: UUID,
22 | val sourceAccountId: UUID,
23 | val sourceBankAccountId: UUID,
24 | val destinationAccountId: UUID,
25 | val destinationBankAccountId: UUID,
26 | val transferAmount: BigDecimal,
27 | ) : Event(
28 | name = TRANSFER_TRANSACTION_CREATED,
29 | )
30 |
31 | @DomainEvent(name = TRANSFER_PARTICIPANT_ACCEPTED)
32 | data class TransferParticipantAcceptedEvent(
33 | val transferId: UUID,
34 | val participantBankAccountId: UUID,
35 | ) : Event(
36 | name = TRANSFER_PARTICIPANT_ACCEPTED,
37 | )
38 |
39 | @DomainEvent(name = TRANSFER_CONFIRMED)
40 | data class TransactionConfirmedEvent(
41 | val transferId: UUID,
42 | val sourceAccountId: UUID,
43 | val sourceBankAccountId: UUID,
44 | val destinationAccountId: UUID,
45 | val destinationBankAccountId: UUID,
46 | ) : Event(
47 | name = TRANSFER_CONFIRMED,
48 | )
49 |
50 | @DomainEvent(name = TRANSFER_NOT_CONFIRMED)
51 | data class TransactionNotConfirmedEvent(
52 | val transferId: UUID,
53 | val sourceAccountId: UUID,
54 | val sourceBankAccountId: UUID,
55 | val destinationAccountId: UUID,
56 | val destinationBankAccountId: UUID,
57 | ) : Event(
58 | name = TRANSFER_NOT_CONFIRMED,
59 | )
60 |
61 | @DomainEvent(name = NOOP)
62 | data class NoopEvent(
63 | val transferId: UUID,
64 | ) : Event(
65 | name = NOOP,
66 | )
67 |
68 | @DomainEvent(name = TRANSFER_PARTICIPANT_COMMITTED)
69 | data class TransferParticipantCommittedEvent(
70 | val transferId: UUID,
71 | val participantBankAccountId: UUID,
72 | ) : Event(
73 | name = TRANSFER_PARTICIPANT_COMMITTED,
74 | )
75 |
76 | @DomainEvent(name = TRANSFER_PARTICIPANT_ROLLBACKED)
77 | data class TransferParticipantRollbackedEvent(
78 | val transferId: UUID,
79 | val participantBankAccountId: UUID,
80 | ) : Event(
81 | name = TRANSFER_PARTICIPANT_ROLLBACKED,
82 | )
83 |
84 |
85 | @DomainEvent(name = TRANSFER_SUCCEEDED)
86 | data class TransactionSucceededEvent(
87 | val transferId: UUID,
88 | ) : Event(
89 | name = TRANSFER_SUCCEEDED,
90 | )
91 |
92 | @DomainEvent(name = TRANSFER_FAILED)
93 | data class TransactionFailedEvent(
94 | val transferId: UUID,
95 | ) : Event(
96 | name = TRANSFER_FAILED,
97 | )
98 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/transfers/config/TransactionCoundedContextConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.config
2 |
3 | import ru.quipy.bankDemo.transfers.api.TransferTransactionAggregate
4 | import ru.quipy.bankDemo.transfers.logic.TransferTransaction
5 | import ru.quipy.core.EventSourcingService
6 | import ru.quipy.core.EventSourcingServiceFactory
7 | import java.util.UUID
8 |
9 | class TransactionCoundedContextConfig(val eventSourcingServiceFactory: EventSourcingServiceFactory) {
10 |
11 | fun transactionEsService(): EventSourcingService =
12 | eventSourcingServiceFactory.create()
13 |
14 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/transfers/db/BankAccountCacheRepository.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.db
2 |
3 | import ru.quipy.bankDemo.transfers.db.entity.BankAccount
4 | import java.util.Optional
5 | import java.util.UUID
6 |
7 | interface BankAccountCacheRepository {
8 | fun save(bankAccount: BankAccount)
9 | fun findById(id: UUID) : Optional
10 | fun existsById(bankAccountId: UUID) : Boolean
11 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/transfers/db/BankAccountCacheRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.db
2 |
3 | import com.mongodb.client.MongoCollection
4 | import com.mongodb.client.MongoDatabase
5 | import com.mongodb.client.model.Filters.eq
6 | import org.bson.Document
7 | import ru.quipy.bankDemo.transfers.db.entity.BankAccount
8 | import java.util.Optional
9 | import java.util.UUID
10 |
11 | class BankAccountCacheRepositoryImpl(private val mongoDatabase: MongoDatabase): BankAccountCacheRepository {
12 | override fun save(bankAccount: BankAccount) {
13 | val collection: MongoCollection = mongoDatabase.getCollection("bank-account")
14 | val document = Document(mapOf(Pair("_id", UUID.randomUUID()),
15 | Pair("accountId", bankAccount.accountId),
16 | Pair("bankAccountId", bankAccount.bankAccountId)))
17 | collection.insertOne(document)
18 | }
19 |
20 | override fun findById(id: UUID): Optional {
21 | val collection: MongoCollection = mongoDatabase.getCollection("bank-account")
22 | val document = collection.find(eq("bankAccountId", id)).first()
23 | document?: return Optional.empty()
24 | return Optional.of(BankAccount(document["bankAccountId"] as UUID, document["accountId"] as UUID))
25 | }
26 |
27 | override fun existsById(bankAccountId: UUID): Boolean {
28 | val collection: MongoCollection = mongoDatabase.getCollection("bank-account")
29 | collection.find(eq("bankAccountId", bankAccountId)).first() ?: return false
30 | return true
31 | }
32 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/transfers/db/entity/BankAccount.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.db.entity
2 |
3 | import java.util.UUID
4 |
5 | data class BankAccount(
6 | val bankAccountId: UUID,
7 | var accountId: UUID
8 | )
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/transfers/projections/BankAccountsExistenceCache.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.projections
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import ru.quipy.application.component.Component
6 | import ru.quipy.bankDemo.accounts.api.AccountAggregate
7 | import ru.quipy.bankDemo.accounts.api.BankAccountCreatedEvent
8 | import ru.quipy.bankDemo.transfers.db.BankAccountCacheRepository
9 | import ru.quipy.bankDemo.transfers.db.entity.BankAccount
10 | import ru.quipy.streams.AggregateSubscriptionsManager
11 |
12 | class BankAccountsExistenceCache(
13 | private val bankAccountCacheRepository: BankAccountCacheRepository,
14 | private val subscriptionsManager: AggregateSubscriptionsManager
15 | ) : Component {
16 | private val logger: Logger = LoggerFactory.getLogger(BankAccountsExistenceCache::class.java)
17 |
18 | override fun postConstruct() {
19 | subscriptionsManager.createSubscriber(AccountAggregate::class, "transactions::accounts-cache") {
20 | `when`(BankAccountCreatedEvent::class) { event ->
21 | bankAccountCacheRepository.save(BankAccount(event.bankAccountId, event.accountId)) // todo sukhoa idempotence!
22 | logger.info("Update bank account cache, create account ${event.accountId}-${event.bankAccountId}")
23 | }
24 | // todo sukhoa bank account deleted event
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/transfers/service/TransactionService.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.service
2 |
3 | import ru.quipy.application.component.BaseComponent
4 | import ru.quipy.bankDemo.transfers.api.TransferTransactionAggregate
5 | import ru.quipy.bankDemo.transfers.api.TransferTransactionCreatedEvent
6 | import ru.quipy.bankDemo.transfers.db.BankAccountCacheRepository
7 | import ru.quipy.bankDemo.transfers.logic.TransferTransaction
8 | import ru.quipy.core.EventSourcingService
9 | import java.math.BigDecimal
10 | import java.util.UUID
11 |
12 | class TransactionService(
13 | private val bankAccountCacheRepository: BankAccountCacheRepository,
14 | private val transactionEsService: EventSourcingService
15 | ) : BaseComponent() {
16 | fun initiateTransferTransaction(
17 | sourceBankAccountId: UUID,
18 | destinationBankAccountId: UUID,
19 | transferAmount: BigDecimal
20 | ): TransferTransactionCreatedEvent {
21 | val srcBankAccount = bankAccountCacheRepository.findById(sourceBankAccountId).orElseThrow {
22 | IllegalArgumentException("Cannot create transaction. There is no source bank account: $sourceBankAccountId")
23 | }
24 |
25 | val dstBankAccount = bankAccountCacheRepository.findById(destinationBankAccountId).orElseThrow {
26 | IllegalArgumentException("Cannot create transaction. There is no destination bank account: $destinationBankAccountId")
27 | }
28 |
29 | return transactionEsService.create {
30 | it.initiateTransferTransaction(
31 | sourceAccountId = srcBankAccount.accountId,
32 | sourceBankAccountId = srcBankAccount.bankAccountId,
33 | destinationAccountId = dstBankAccount.accountId,
34 | destinationBankAccountId = dstBankAccount.bankAccountId,
35 | transferAmount = transferAmount
36 | )
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/bankDemo/transfers/subscribers/BankAccountsSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.subscribers
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import ru.quipy.application.component.Component
6 | import ru.quipy.bankDemo.accounts.api.AccountAggregate
7 | import ru.quipy.bankDemo.accounts.api.TransferTransactionAcceptedEvent
8 | import ru.quipy.bankDemo.accounts.api.TransferTransactionDeclinedEvent
9 | import ru.quipy.bankDemo.accounts.api.TransferTransactionProcessedEvent
10 | import ru.quipy.bankDemo.accounts.api.TransferTransactionRollbackedEvent
11 | import ru.quipy.bankDemo.transfers.api.TransferTransactionAggregate
12 | import ru.quipy.bankDemo.transfers.logic.TransferTransaction
13 | import ru.quipy.core.EventSourcingService
14 | import ru.quipy.streams.AggregateSubscriptionsManager
15 | import java.util.UUID
16 |
17 | class BankAccountsSubscriber(
18 | private val subscriptionsManager: AggregateSubscriptionsManager,
19 | private val transactionEsService: EventSourcingService
20 | ) : Component {
21 | private val logger: Logger = LoggerFactory.getLogger(BankAccountsSubscriber::class.java)
22 | override fun postConstruct() {
23 | subscriptionsManager.createSubscriber(AccountAggregate::class, "transactions::bank-accounts-subscriber") {
24 | `when`(TransferTransactionAcceptedEvent::class) { event ->
25 | transactionEsService.update(event.transactionId) {
26 | it.processParticipantAccept(event.bankAccountId)
27 | }
28 | }
29 | `when`(TransferTransactionDeclinedEvent::class) { event ->
30 | transactionEsService.update(event.transactionId) {
31 | it.processParticipantDecline(event.bankAccountId)
32 | }
33 | }
34 | `when`(TransferTransactionProcessedEvent::class) { event ->
35 | transactionEsService.update(event.transactionId) {
36 | it.participantCommitted(event.bankAccountId)
37 | }
38 | }
39 | `when`(TransferTransactionRollbackedEvent::class) { event ->
40 | transactionEsService.update(event.transactionId) {
41 | it.participantRollbacked(event.bankAccountId)
42 | }
43 | }
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/projectDemo/ProjectContext.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo
2 |
3 | import ru.quipy.TinyEsLibConfig
4 | import ru.quipy.application.component.Component
5 | import ru.quipy.projectDemo.config.ProjectDemoConfig
6 | import ru.quipy.projectDemo.projections.AnnotationBasedProjectEventsSubscriber
7 | import ru.quipy.projectDemo.projections.ProjectEventsSubscriber
8 |
9 | class ProjectContext private constructor() {
10 | lateinit var annotationBasedProjectEventsSubscriber: AnnotationBasedProjectEventsSubscriber
11 | lateinit var components: List
12 | lateinit var projectEventsSubscriber: ProjectEventsSubscriber
13 | lateinit var projectDemoConfig: ProjectDemoConfig
14 |
15 | constructor(tinyEsLibConfig: TinyEsLibConfig) : this() {
16 | annotationBasedProjectEventsSubscriber = AnnotationBasedProjectEventsSubscriber()
17 | projectDemoConfig = projectDemoConfig(tinyEsLibConfig)
18 | projectEventsSubscriber = ProjectEventsSubscriber(tinyEsLibConfig.subscriptionsManager)
19 |
20 | components = mutableListOf(projectDemoConfig, projectEventsSubscriber, annotationBasedProjectEventsSubscriber)
21 | components.forEach { it.postConstruct() }
22 | }
23 |
24 | private fun projectDemoConfig(config: TinyEsLibConfig) : ProjectDemoConfig {
25 | return ProjectDemoConfig(
26 | config.subscriptionsManager,
27 | annotationBasedProjectEventsSubscriber,
28 | config.eventSourcingServiceFactory,
29 | config.eventStreamManager,
30 | config.aggregateRegistry
31 | )
32 | }
33 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/projectDemo/api/ProjectAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.api
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.domain.Aggregate
5 |
6 | // API
7 | @AggregateType(aggregateEventsTableName = "aggregate-project")
8 | class ProjectAggregate : Aggregate
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/projectDemo/api/ProjectAggregateDomainEvents.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.api
2 |
3 | import ru.quipy.core.annotations.DomainEvent
4 | import ru.quipy.domain.Event
5 | import java.util.UUID
6 |
7 | const val PROJECT_CREATED_EVENT = "PROJECT_CREATED_EVENT"
8 | const val TAG_CREATED_EVENT = "TAG_CREATED_EVENT"
9 | const val TAG_ASSIGNED_TO_TASK_EVENT = "TAG_ASSIGNED_TO_TASK_EVENT"
10 | const val TASK_CREATED_EVENT = "TASK_CREATED_EVENT"
11 |
12 | // API
13 | @DomainEvent(name = PROJECT_CREATED_EVENT)
14 | class ProjectCreatedEvent(
15 | val projectId: String,
16 | createdAt: Long = System.currentTimeMillis(),
17 | ) : Event(
18 | name = PROJECT_CREATED_EVENT,
19 | createdAt = createdAt,
20 | )
21 |
22 | @DomainEvent(name = TAG_CREATED_EVENT)
23 | class TagCreatedEvent(
24 | val projectId: String,
25 | val tagId: UUID,
26 | val tagName: String,
27 | createdAt: Long = System.currentTimeMillis(),
28 | ) : Event(
29 | name = TAG_CREATED_EVENT,
30 | createdAt = createdAt,
31 | )
32 |
33 | @DomainEvent(name = TASK_CREATED_EVENT)
34 | class TaskCreatedEvent(
35 | val projectId: String,
36 | val taskId: UUID,
37 | val taskName: String,
38 | createdAt: Long = System.currentTimeMillis(),
39 | ) : Event(
40 | name = TASK_CREATED_EVENT,
41 | createdAt = createdAt
42 | )
43 |
44 | @DomainEvent(name = TAG_ASSIGNED_TO_TASK_EVENT)
45 | class TagAssignedToTaskEvent(
46 | val projectId: String,
47 | val taskId: UUID,
48 | val tagId: UUID,
49 | createdAt: Long = System.currentTimeMillis(),
50 | ) : SomeBaseEventWithoutAnnotation(
51 | name = TAG_ASSIGNED_TO_TASK_EVENT,
52 | createdAt = createdAt
53 | )
54 |
55 | // for testing and demo purposes
56 | open class SomeBaseEventWithoutAnnotation(
57 | name: String,
58 | createdAt: Long = System.currentTimeMillis(),
59 | ) : Event(name = name, createdAt = createdAt)
60 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/projectDemo/config/ProjectDemoConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.config
2 |
3 | import org.slf4j.LoggerFactory
4 | import ru.quipy.application.component.Component
5 | import ru.quipy.core.AggregateRegistry
6 | import ru.quipy.core.EventSourcingServiceFactory
7 | import ru.quipy.projectDemo.api.ProjectAggregate
8 | import ru.quipy.projectDemo.logic.ProjectAggregateState
9 | import ru.quipy.projectDemo.projections.AnnotationBasedProjectEventsSubscriber
10 | import ru.quipy.streams.AggregateEventStreamManager
11 | import ru.quipy.streams.AggregateSubscriptionsManager
12 |
13 | class ProjectDemoConfig(
14 | var subscriptionsManager: AggregateSubscriptionsManager,
15 | var projectEventSubscriber: AnnotationBasedProjectEventsSubscriber,
16 | var eventSourcingServiceFactory: EventSourcingServiceFactory,
17 | var eventStreamManager: AggregateEventStreamManager,
18 | var aggregateRegistry: AggregateRegistry
19 | ) : Component {
20 |
21 | private val logger = LoggerFactory.getLogger(ProjectDemoConfig::class.java)
22 |
23 | override fun postConstruct() {
24 | subscriptionsManager.subscribe(projectEventSubscriber)
25 |
26 | eventStreamManager.maintenance {
27 | onRecordHandledSuccessfully { streamName, eventName ->
28 | logger.info("Stream $streamName successfully processed record of $eventName")
29 | }
30 |
31 | onBatchRead { streamName, batchSize ->
32 | logger.info("Stream $streamName read batch size: $batchSize")
33 | }
34 | }
35 | }
36 |
37 | fun demoESService() = eventSourcingServiceFactory.create()
38 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/projectDemo/logic/ProjectAggregateCommands.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo
2 |
3 | import ru.quipy.projectDemo.api.ProjectCreatedEvent
4 | import ru.quipy.projectDemo.api.TagAssignedToTaskEvent
5 | import ru.quipy.projectDemo.api.TagCreatedEvent
6 | import ru.quipy.projectDemo.api.TaskCreatedEvent
7 | import ru.quipy.projectDemo.logic.ProjectAggregateState
8 | import java.util.UUID
9 |
10 |
11 | // Commands : takes something -> returns event
12 | fun ProjectAggregateState.create(id: String): ProjectCreatedEvent {
13 | return ProjectCreatedEvent(projectId = id)
14 | }
15 |
16 | fun ProjectAggregateState.addTask(name: String): TaskCreatedEvent {
17 | return TaskCreatedEvent(projectId = this.getId(), taskId = UUID.randomUUID(), taskName = name)
18 | }
19 |
20 | fun ProjectAggregateState.createTag(name: String): TagCreatedEvent {
21 | if (projectTags.values.any { it.name == name }) {
22 | throw IllegalArgumentException("Tag already exists: $name")
23 | }
24 | return TagCreatedEvent(projectId = this.getId(), tagId = UUID.randomUUID(), tagName = name)
25 | }
26 |
27 | fun ProjectAggregateState.assignTagToTask(tagId: UUID, taskId: UUID): TagAssignedToTaskEvent {
28 | if (!projectTags.containsKey(tagId)) {
29 | throw IllegalArgumentException("Tag doesn't exists: $tagId")
30 | }
31 |
32 | if (!tasks.containsKey(taskId)) {
33 | throw IllegalArgumentException("Task doesn't exists: $taskId")
34 | }
35 |
36 | return TagAssignedToTaskEvent(projectId = this.getId(), tagId = tagId, taskId = taskId)
37 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/projectDemo/logic/ProjectAggregateState.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.logic
2 |
3 | import ru.quipy.core.annotations.StateTransitionFunc
4 | import ru.quipy.domain.AggregateState
5 | import ru.quipy.projectDemo.api.ProjectAggregate
6 | import ru.quipy.projectDemo.api.ProjectCreatedEvent
7 | import ru.quipy.projectDemo.api.TagAssignedToTaskEvent
8 | import ru.quipy.projectDemo.api.TagCreatedEvent
9 | import ru.quipy.projectDemo.api.TaskCreatedEvent
10 | import java.util.UUID
11 |
12 | // Service's business logic
13 | class ProjectAggregateState: AggregateState {
14 | lateinit var projectId: String
15 | var createdAt: Long = System.currentTimeMillis()
16 | var updatedAt: Long = System.currentTimeMillis()
17 |
18 | var tasks = mutableMapOf()
19 | var projectTags = mutableMapOf()
20 |
21 | override fun getId() = projectId
22 |
23 | // State transition functions
24 | @StateTransitionFunc
25 | fun projectCreatedApply(event: ProjectCreatedEvent) {
26 | projectId = event.projectId
27 | updatedAt = createdAt
28 | }
29 |
30 | @StateTransitionFunc
31 | fun tagCreatedApply(event: TagCreatedEvent) {
32 | projectTags[event.tagId] = ProjectTag(event.tagId, event.tagName)
33 | updatedAt = createdAt
34 | }
35 |
36 | @StateTransitionFunc
37 | fun taskCreatedApply(event: TaskCreatedEvent) {
38 | tasks[event.taskId] = TaskEntity(event.taskId, event.taskName, mutableSetOf())
39 | updatedAt = createdAt
40 | }
41 | }
42 |
43 | data class TaskEntity(
44 | val id: UUID = UUID.randomUUID(),
45 | val name: String,
46 | val tagsAssigned: MutableSet
47 | )
48 |
49 | data class ProjectTag(
50 | val id: UUID = UUID.randomUUID(),
51 | val name: String
52 | )
53 |
54 | @StateTransitionFunc
55 | fun ProjectAggregateState.tagAssignedApply(event: TagAssignedToTaskEvent) {
56 | tasks[event.taskId]?.tagsAssigned?.add(event.tagId)
57 | ?: throw IllegalArgumentException("No such task: ${event.taskId}") // todo sukhoa exception or not?
58 | updatedAt = createdAt
59 | }
60 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/projectDemo/projections/AnnotationBasedProjectEventsSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.projections
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import ru.quipy.application.component.BaseComponent
6 | import ru.quipy.projectDemo.api.ProjectAggregate
7 | import ru.quipy.projectDemo.api.TagCreatedEvent
8 | import ru.quipy.projectDemo.api.TaskCreatedEvent
9 | import ru.quipy.streams.annotation.AggregateSubscriber
10 | import ru.quipy.streams.annotation.SubscribeEvent
11 |
12 | @AggregateSubscriber(
13 | aggregateClass = ProjectAggregate::class, subscriberName = "demo-subs-stream"
14 | )
15 | class AnnotationBasedProjectEventsSubscriber : BaseComponent() {
16 |
17 | val logger: Logger = LoggerFactory.getLogger(AnnotationBasedProjectEventsSubscriber::class.java)
18 |
19 | @SubscribeEvent
20 | fun taskCreatedSubscriber(event: TaskCreatedEvent) {
21 | logger.info("Task created: {}", event.taskName)
22 | }
23 |
24 | @SubscribeEvent
25 | fun tagCreatedSubscriber(event: TagCreatedEvent) {
26 | logger.info("Tag created: {}", event.tagName)
27 | }
28 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/kotlin/ru/quipy/projectDemo/projections/ProjectEventsSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.projections
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import ru.quipy.application.component.Component
6 | import ru.quipy.projectDemo.api.ProjectAggregate
7 | import ru.quipy.projectDemo.api.TagAssignedToTaskEvent
8 | import ru.quipy.projectDemo.api.TagCreatedEvent
9 | import ru.quipy.projectDemo.api.TaskCreatedEvent
10 | import ru.quipy.streams.AggregateSubscriptionsManager
11 |
12 | class ProjectEventsSubscriber(var subscriptionsManager: AggregateSubscriptionsManager) : Component {
13 |
14 | val logger: Logger = LoggerFactory.getLogger(ProjectEventsSubscriber::class.java)
15 |
16 | override fun postConstruct() {
17 | subscriptionsManager.createSubscriber(ProjectAggregate::class, "some-meaningful-name") {
18 |
19 | `when`(TaskCreatedEvent::class) { event ->
20 | logger.info("Task created: {}", event.taskName)
21 | }
22 |
23 | `when`(TagCreatedEvent::class) { event ->
24 | logger.info("Tag created: {}", event.tagName)
25 | }
26 |
27 | `when`(TagAssignedToTaskEvent::class) { event ->
28 | logger.info("Tag {} assigned to task {}: ", event.tagId, event.taskId)
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | event.sourcing.db-schema=event_sourcing_store
2 |
3 | # datasource configuration
4 | datasource.jdbc-url=jdbc:postgresql://localhost:5432/tiny_es
5 | datasource.username=tiny_es
6 | datasource.password=tiny_es
7 |
8 |
9 | mongodb.url=mongodb://localhost:27017
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | System.out
4 |
5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %msg%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/test/kotlin/StreamEventOrderingTest.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy
2 |
3 | import org.awaitility.kotlin.await
4 | import org.junit.jupiter.api.BeforeAll
5 | import org.junit.jupiter.api.BeforeEach
6 | import org.junit.jupiter.api.Test
7 | import ru.quipy.core.EventSourcingProperties
8 | import ru.quipy.projectDemo.api.ProjectAggregate
9 | import ru.quipy.projectDemo.api.TagCreatedEvent
10 | import ru.quipy.projectDemo.create
11 | import ru.quipy.projectDemo.createTag
12 | import java.util.concurrent.TimeUnit
13 |
14 | class StreamEventOrderingTest: BaseTest(testId) {
15 | companion object {
16 | const val testId = "StreamEventOrderingTest"
17 | @BeforeAll
18 | @JvmStatic
19 | fun configure() {
20 | configure(eventSourcingProperties = EventSourcingProperties(streamBatchSize = 3))
21 | }
22 | }
23 |
24 | private val sb = StringBuilder()
25 |
26 | @BeforeEach
27 | fun init() {
28 | cleanDatabase()
29 | }
30 |
31 | @Test
32 | fun testEventOrder() {
33 | demoESService.create {
34 | it.create(testId)
35 | }
36 |
37 | demoESService.update(testId) {
38 | it.createTag("1")
39 | }
40 | demoESService.update(testId) {
41 | it.createTag("2")
42 | }
43 | demoESService.update(testId) {
44 | it.createTag("3")
45 | }
46 | demoESService.update(testId) {
47 | it.createTag("4")
48 | }
49 | demoESService.update(testId) {
50 | it.createTag("5")
51 | }
52 | demoESService.update(testId) {
53 | it.createTag("6")
54 | }
55 |
56 | subscriptionsManager.createSubscriber(ProjectAggregate::class, "StreamEventOrderingTest") {
57 | `when`(TagCreatedEvent::class) { event ->
58 | sb.append(event.tagName).also {
59 | println(sb.toString())
60 | }
61 | }
62 | }
63 |
64 | await.atMost(10, TimeUnit.MINUTES).until {
65 | sb.toString() == "123456"
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-app/src/test/kotlin/config/DockerPostgresDataSourceInitialize.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.config
2 |
3 | import org.testcontainers.containers.PostgreSQLContainer
4 | import org.testcontainers.utility.DockerImageName
5 | import java.util.Properties
6 |
7 | class DockerPostgresDataSourceInitializer {
8 | private val postgresContainer = PostgreSQLContainer(DockerImageName.parse("postgres:14.9-alpine")).apply {
9 | withDatabaseName("tiny_es")
10 | withUsername("tiny_es")
11 | withPassword("tiny_es")
12 | }
13 |
14 | fun initialize(properties: Properties) {
15 | postgresContainer.start()
16 |
17 | properties.setProperty("datasource.jdbc-url", postgresContainer.jdbcUrl)
18 | properties.setProperty("datasource.username", postgresContainer.username)
19 | properties.setProperty("datasource.password", postgresContainer.password)
20 | properties.setProperty("event.sourcing.db-schema", "event_sourcing_store")
21 | }
22 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/core/BasicAggregateRegistry.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.core
2 |
3 | import org.springframework.core.annotation.AnnotationUtils
4 | import ru.quipy.core.AggregateRegistry.*
5 | import ru.quipy.core.annotations.AggregateType
6 | import ru.quipy.domain.Aggregate
7 | import ru.quipy.domain.AggregateState
8 | import java.util.concurrent.ConcurrentHashMap
9 | import kotlin.reflect.KClass
10 |
11 | @Suppress("UNCHECKED_CAST")
12 | class BasicAggregateRegistry : AggregateRegistry {
13 | private val aggregatesInfo = ConcurrentHashMap, AggregateInfo>()
14 |
15 | override fun register(
16 | aggregateClass: KClass,
17 | eventRegistrationBlock: EventRegistrar.() -> Unit
18 | ){
19 | val aggregateInfo = AnnotationUtils.findAnnotation(aggregateClass.java, AggregateType::class.java)
20 | ?: throw IllegalStateException("No annotation ${aggregateClass.simpleName} provided on aggregate")
21 |
22 | aggregatesInfo.computeIfAbsent(aggregateClass) {
23 | AggregateInfo().also {
24 | it.eventInfo = EventInfoImpl(aggregateClass, aggregateInfo.aggregateEventsTableName)
25 | }
26 | }.also {
27 | eventRegistrationBlock(it.eventInfo as EventRegistrar)
28 | }
29 | }
30 |
31 | override fun > register(
32 | aggregateClass: KClass,
33 | aggregateStateClass: KClass,
34 | eventRegistrationBlock: StateTransitionsRegistrar.() -> Unit
35 | ) {
36 | val aggregateInfo = AnnotationUtils.findAnnotation(aggregateClass.java, AggregateType::class.java)
37 | ?: throw IllegalStateException("No annotation ${aggregateClass.simpleName} provided on aggregate")
38 |
39 |
40 | aggregatesInfo.computeIfAbsent(aggregateClass) {
41 | AggregateInfo().also {
42 | it.stateInfo = AggregateStateInfoImpl(aggregateClass, aggregateStateClass, aggregateInfo.aggregateEventsTableName)
43 | }
44 | }.also {
45 | eventRegistrationBlock(it.stateInfo as StateTransitionsRegistrar)
46 | }
47 | }
48 |
49 | override fun basicAggregateInfo(clazz: KClass): BasicAggregateInfo? {
50 | return aggregatesInfo[clazz]?.let { (it.eventInfo ?: it.stateInfo) as BasicAggregateInfo }
51 | }
52 |
53 |
54 | override fun getEventInfo(clazz: KClass): EventInfo? {
55 | return aggregatesInfo[clazz]?.let { (it.eventInfo ?: it.stateInfo) as EventInfo }
56 | }
57 |
58 | override fun > getStateTransitionInfo(clazz: KClass): AggregateStateInfo? {
59 | return aggregatesInfo[clazz]?.let { it.stateInfo as AggregateStateInfo }
60 | }
61 |
62 | override fun getAllAggregates(): List> {
63 | return aggregatesInfo.toMap().keys.map { it as KClass }
64 | }
65 |
66 | class AggregateInfo {
67 | var eventInfo: EventInfo<*>? = null
68 | var stateInfo: AggregateStateInfo<*, *, *>? = null
69 | }
70 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/core/EventSourcingProperties.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.core
2 |
3 | import kotlin.time.Duration
4 | import kotlin.time.Duration.Companion.seconds
5 |
6 | class EventSourcingProperties (
7 | var snapshotFrequency: Int = 10,
8 | var snapshotsEnabled: Boolean = false,
9 | var snapshotTableName: String = "snapshots", // todo sukhoa should be per aggregate
10 | var streamReadPeriod: Long = 1_000,
11 | var streamBatchSize: Int = 200,
12 | var autoScanEnabled: Boolean = false,
13 | var scanPackage: String? = null,
14 | var spinLockMaxAttempts: Int = 25,
15 | var maxActiveReaderInactivityPeriod: Duration = 5.seconds,
16 | var readerCommitPeriodMessages: Int = 100,
17 | var readerCommitPeriodMillis: Long = 1000,
18 | val eventReaderHealthCheckPeriod: Duration = 3.seconds,
19 | var sagasEnabled: Boolean = true,
20 | var batchEnabled: Boolean = false,
21 | var batchSize: Int = 1,
22 | var batchPeriodMillis: Long = 50,
23 | var batchMode: String = BatchMode.STORED_PROCEDURE.name,
24 | var dbSchema: String = "event_sourcing_store"
25 | )
26 |
27 | enum class BatchMode {
28 | JDBC_BATCH, STORED_PROCEDURE
29 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/core/EventSourcingServiceFactory.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.core
2 |
3 | import ru.quipy.database.EventStore
4 | import ru.quipy.domain.*
5 | import ru.quipy.mapper.EventMapper
6 |
7 |
8 | class EventSourcingServiceFactory(
9 | val aggregateRegistry: AggregateRegistry,
10 | val eventMapper: EventMapper,
11 | val eventStore: EventStore,
12 | val eventSourcingProperties: EventSourcingProperties
13 | ) {
14 |
15 | inline fun > create(): EventSourcingService {
16 | return EventSourcingService(
17 | A::class, aggregateRegistry, eventMapper, eventSourcingProperties, eventStore
18 | )
19 | }
20 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/core/annotations/AggregateType.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.core.annotations
2 |
3 | @Target(AnnotationTarget.CLASS)
4 | @Retention(AnnotationRetention.RUNTIME)
5 | annotation class AggregateType(
6 | val aggregateEventsTableName: String,
7 | )
8 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/core/annotations/DomainEvent.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.core.annotations
2 |
3 | @Target(AnnotationTarget.CLASS)
4 | @Retention(AnnotationRetention.RUNTIME)
5 | annotation class DomainEvent(
6 | val name: String,
7 | )
8 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/core/annotations/StateTransitionFunc.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.core.annotations
2 |
3 | @Target(AnnotationTarget.FUNCTION)
4 | @Retention(AnnotationRetention.RUNTIME)
5 | annotation class StateTransitionFunc
6 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/core/exceptions/DuplicateEventIdException.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.core.exceptions
2 |
3 | class DuplicateEventIdException(
4 | override val message: String,
5 | override val cause: Throwable?
6 | ) : RuntimeException(
7 | message, cause
8 | )
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/core/exceptions/EventRecordOptimisticLockException.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.core.exceptions
2 |
3 | import ru.quipy.domain.EventRecord
4 |
5 | internal class EventRecordOptimisticLockException(
6 | override val message: String,
7 | override val cause: Throwable?,
8 | val eventRecords: List
9 | ) : RuntimeException(
10 | message, cause
11 | )
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/database/EventStore.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.database
2 |
3 | import ru.quipy.core.exceptions.DuplicateEventIdException
4 | import ru.quipy.domain.*
5 |
6 | /**
7 | * Abstracts away the DB access. Provides the operations for event sourcing functioning.
8 | * You can provide your own implementation of [EventStore] and run event sourcing app
9 | * working on any DB you wish under the hood.
10 | */
11 | interface EventStore {
12 |
13 | /**
14 | * Appends event record in aggregate event log.
15 | *
16 | * Throws [DuplicateEventIdException] if there is already event with same.
17 | *
18 | * This is used to handle concurrency If one of the insertion operation running in parallel managed to insert
19 | * event, then others should retry their attempt including create aggregate state again, performing validations
20 | * and insertion the resulting event. Some kind of optimistic concurrency control.
21 | *
22 | * We suggest using same (aggregateId, version) combination as a sign of the event is duplicate (version is a
23 | * monotonically increasing sequence always incremented by one). In this case there will be no two events with same
24 | * version within single aggregate instance. That allows you to tackle concurrency issues withing same aggregate instance.
25 | * So that execution will be linearized for every single aggregate instance.
26 | *
27 | */
28 | @Throws(exceptionClasses = [DuplicateEventIdException::class])
29 | fun insertEventRecord(aggregateTableName: String, eventRecord: EventRecord)
30 |
31 | @Throws(exceptionClasses = [DuplicateEventIdException::class])
32 | fun insertEventRecords(aggregateTableName: String, eventRecords: List)
33 |
34 | /**
35 | * Aggregate state version is the number of events that should be applied to empty aggregate state to get current state.
36 | *
37 | * Each event stores the number of the aggregate state version. This is the version of the state after the event is applied.
38 | */
39 | fun findEventRecordsWithAggregateVersionGraterThan(
40 | aggregateTableName: String, aggregateId: Any, aggregateVersion: Long
41 | ): List
42 |
43 | /**
44 | * Returns a batch of events that has their sequence number greater than passed
45 | */
46 | fun findBatchOfEventRecordAfter(
47 | aggregateTableName: String, eventSequenceNum: Long, batchSize: Int
48 | ): List
49 |
50 | fun tableExists(aggregateTableName: String): Boolean
51 |
52 | fun updateSnapshotWithLatestVersion(tableName: String, snapshot: Snapshot)
53 |
54 | fun findSnapshotByAggregateId(snapshotsTableName: String, aggregateId: Any): Snapshot?
55 |
56 | fun findStreamReadIndex(streamName: String): EventStreamReadIndex?
57 |
58 | fun getActiveStreamReader(streamName: String): ActiveEventStreamReader?
59 |
60 | fun tryUpdateActiveStreamReader(updatedActiveReader: ActiveEventStreamReader): Boolean
61 |
62 | fun tryReplaceActiveStreamReader(expectedVersion: Long, newActiveReader: ActiveEventStreamReader): Boolean
63 |
64 | fun commitStreamReadIndex(readIndex: EventStreamReadIndex): Boolean
65 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/domain/EventSourcingDomain.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.domain
2 |
3 | import ru.quipy.saga.SagaContext
4 | import java.util.*
5 |
6 | interface Versioned {
7 | var version: Long
8 | }
9 |
10 | interface Unique { // todo sukhoa rename this stuff
11 | val id: ID
12 | }
13 |
14 | interface Aggregate
15 |
16 | interface AggregateState { // todo sukhoa add version of the state
17 | /**
18 | * Returns the ID of the aggregate or null if state is empty
19 | */
20 | fun getId(): ID?
21 | }
22 |
23 | fun interface AggregateStateTransitionFunction, S : AggregateState<*, A>> {
24 | fun performTransition(state: S, event: E)
25 | }
26 |
27 | abstract class Event(
28 | override val id: UUID = UUID.randomUUID(),
29 | val name: String,
30 | var sagaContext: SagaContext? = null,
31 | override var version: Long = 0L, // this is aggregate version actually or the event count number
32 | var createdAt: Long = System.currentTimeMillis(),
33 | ) : Versioned, Unique
34 |
35 | @Suppress("unused")
36 | data class EventRecord(
37 | override val id: String,
38 | val aggregateId: Any, // todo sukhoa weird?
39 | val aggregateVersion: Long,
40 | val eventTitle: String,
41 | val payload: String,
42 | var sagaContext: SagaContext? = null,
43 | val createdAt: Long = System.currentTimeMillis()
44 | ) : Unique
45 |
46 | @Suppress("UNCHECKED_CAST")
47 | class Snapshot(
48 | override val id : Any, // todo sukhoa weird ANY, should it be parametrized?
49 | val snapshot : Any, // todo sukhoa weird ANY, should it be parametrized?
50 | override var version: Long
51 | ) : Versioned, Unique
52 |
53 |
54 | data class EventStreamReadIndex(
55 | override val id: String, // name of the stream
56 | val readIndex: Long,
57 | override var version: Long
58 | ) : Unique, Versioned {
59 | override fun toString(): String {
60 | return "EventStreamReadIndex(id='$id', readIndex=$readIndex, version=$version)"
61 | }
62 | }
63 |
64 | class ActiveEventStreamReader(
65 | override val id: String, // Represents the stream name.
66 | override var version: Long,
67 | val readerId: String,
68 | val readPosition: Long,
69 | val lastInteraction: Long
70 | ) : Unique, Versioned
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/mapper/EventMapper.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.mapper
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import ru.quipy.domain.Aggregate
5 | import ru.quipy.domain.Event
6 | import kotlin.reflect.KClass
7 |
8 | /**
9 | * Allows to map some row representation (json String by default) of event to instance of [Event] class and vise versa
10 | */
11 | interface EventMapper {
12 |
13 | fun toEvent(payload: String, eventClass: KClass>): Event
14 |
15 | fun eventToString(event: Event): String
16 | }
17 |
18 | class JsonEventMapper(
19 | val jsonObjectMapper: ObjectMapper
20 | ) : EventMapper {
21 |
22 | override fun toEvent(payload: String, eventClass: KClass>): Event {
23 | return jsonObjectMapper.readValue(
24 | payload,
25 | eventClass.java
26 | ) as Event
27 | }
28 |
29 | override fun eventToString(event: Event): String {
30 | return jsonObjectMapper.writeValueAsString(event)
31 | }
32 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/saga/SagaManager.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.saga
2 |
3 | import ru.quipy.core.EventSourcingService
4 | import ru.quipy.saga.aggregate.api.SagaStepAggregate
5 | import ru.quipy.saga.aggregate.logic.SagaStepAggregateState
6 | import java.util.*
7 |
8 | class SagaManager(
9 | private val sagaStepEsService: EventSourcingService
10 | ) {
11 | fun withContextGiven(sagaContext: SagaContext?) = SagaInvoker(sagaContext)
12 |
13 | fun launchSaga(sagaName: String, stepName: String) =
14 | SagaInvoker(SagaContext()).launchSaga(sagaName, stepName)
15 |
16 | private fun launchSaga(
17 | sagaName: String,
18 | stepName: String,
19 | sagaStepId: UUID?,
20 | sagaContext: SagaContext
21 | ): SagaContext {
22 | if (sagaContext.ctx.containsKey(sagaName))
23 | throw IllegalArgumentException("The name of the saga $sagaName is already in the context")
24 |
25 | val sagaStep = SagaStep(sagaName, stepName, sagaStepId ?: UUID.randomUUID())
26 |
27 | sagaStepEsService.create { it.launchSagaStep(sagaStep) }
28 |
29 | return processContext(sagaContext, sagaName, sagaStep)
30 | }
31 |
32 | private fun performSagaStep(
33 | sagaName: String,
34 | stepName: String,
35 | sagaStepId: UUID?,
36 | sagaContext: SagaContext,
37 | ): SagaContext {
38 | if (!sagaContext.ctx.containsKey(sagaName))
39 | throw IllegalArgumentException("The name of the saga $sagaName does not match the context")
40 |
41 | val sagaInfo = sagaContext.ctx[sagaName]
42 |
43 | val sagaStep = SagaStep(
44 | sagaName,
45 | stepName,
46 | sagaStepId ?: UUID.randomUUID(),
47 | sagaInstanceId = sagaInfo!!.sagaInstanceId,
48 | prevSteps = sagaInfo.stepIdPrevStepsIdsAssociation.keys
49 | )
50 |
51 | sagaStepEsService.update(sagaStep.sagaInstanceId) { it.initiateSagaStep(sagaStep) }
52 |
53 | return processContext(sagaContext, sagaName, sagaStep)
54 | }
55 |
56 | private fun processContext(sagaContext: SagaContext, sagaName: String, sagaStep: SagaStep): SagaContext {
57 | val processedContext = SagaContext(sagaContext.ctx.toMutableMap().also {
58 | it[sagaName] = SagaInfo(
59 | sagaStep.sagaInstanceId,
60 | sagaStep.stepName,
61 | sagaStep.sagaStepId,
62 | sagaStep.prevSteps,
63 | mapOf(sagaStep.sagaStepId to sagaStep.prevSteps)
64 | )
65 | })
66 | processedContext.correlationId = sagaContext.correlationId
67 | processedContext.causationId = sagaContext.causationId
68 | processedContext.currentEventId = sagaContext.currentEventId
69 |
70 | return processedContext
71 | }
72 |
73 | inner class SagaInvoker(
74 | private val sagaContext: SagaContext?,
75 | private var currentSagaStepId: UUID? = null
76 | ) {
77 | fun launchSaga(sagaName: String, stepName: String): SagaInvoker {
78 | val updatedContext = launchSaga(sagaName, stepName, currentSagaStepId, sagaContext ?: SagaContext())
79 | currentSagaStepId = updatedContext.ctx[sagaName]!!.sagaStepId
80 | return SagaInvoker(updatedContext, currentSagaStepId)
81 | }
82 |
83 | fun performSagaStep(sagaName: String, stepName: String): SagaInvoker {
84 | if (sagaContext == null)
85 | throw IllegalArgumentException("The saga context is not initialized")
86 |
87 | val updatedContext = performSagaStep(sagaName, stepName, currentSagaStepId, sagaContext)
88 | currentSagaStepId = updatedContext.ctx[sagaName]!!.sagaStepId
89 | return SagaInvoker(updatedContext, currentSagaStepId)
90 | }
91 |
92 | fun sagaContext() = sagaContext ?: SagaContext()
93 | }
94 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/saga/aggregate/api/SagaStepAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.saga.aggregate.api
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.domain.Aggregate
5 |
6 | @AggregateType("sagas")
7 | class SagaStepAggregate : Aggregate
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/saga/aggregate/api/SagaStepEvents.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.saga.aggregate.api
2 |
3 | import ru.quipy.core.annotations.DomainEvent
4 | import ru.quipy.domain.Event
5 | import java.util.UUID
6 |
7 | const val SAGA_STEP_LAUNCHED = "SAGA_STEP_LAUNCHED_EVENT"
8 | const val SAGA_STEP_INITIATED = "SAGA_STEP_INITIATED_EVENT"
9 | const val SAGA_STEP_PROCESSED = "SAGA_STEP_PROCESSED_EVENT"
10 | const val DEFAULT_SAGA_PROCESSED = "DEFAULT_SAGA_PROCESSED_EVENT"
11 |
12 | @DomainEvent(SAGA_STEP_LAUNCHED)
13 | data class SagaStepLaunchedEvent(
14 | val sagaName: String,
15 | val stepName: String,
16 | val sagaStepId: UUID,
17 | val sagaInstanceId: UUID,
18 | val prevSteps: Set = setOf()
19 | ) : Event(
20 | name = SAGA_STEP_LAUNCHED,
21 | )
22 |
23 | @DomainEvent(SAGA_STEP_INITIATED)
24 | data class SagaStepInitiatedEvent(
25 | val sagaName: String,
26 | val stepName: String,
27 | val sagaStepId: UUID,
28 | val sagaInstanceId: UUID,
29 | val prevSteps: Set = setOf()
30 | ) : Event(
31 | name = SAGA_STEP_INITIATED,
32 | )
33 |
34 | @DomainEvent(SAGA_STEP_PROCESSED)
35 | data class SagaStepProcessedEvent(
36 | val sagaName: String,
37 | val stepName: String,
38 | val sagaStepId: UUID,
39 | val sagaInstanceId: UUID,
40 | val prevSteps: Set = setOf(),
41 | val eventName: String
42 | ) : Event(
43 | name = SAGA_STEP_PROCESSED,
44 | )
45 |
46 | @DomainEvent(DEFAULT_SAGA_PROCESSED)
47 | data class DefaultSagaProcessedEvent(
48 | val correlationId: UUID,
49 | val currentEventId: UUID,
50 | val causationId: UUID?,
51 | val eventName: String
52 | ) : Event(
53 | name = DEFAULT_SAGA_PROCESSED,
54 | )
55 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/saga/aggregate/logic/SagaStepAggregateState.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.saga.aggregate.logic
2 |
3 | import ru.quipy.core.annotations.StateTransitionFunc
4 | import ru.quipy.domain.AggregateState
5 | import ru.quipy.saga.SagaStep
6 | import ru.quipy.saga.aggregate.api.*
7 | import java.util.UUID
8 |
9 | class SagaStepAggregateState : AggregateState {
10 | private lateinit var sagaName: String
11 | private lateinit var sagaInstanceId: UUID
12 | private var sagaSteps = mutableListOf()
13 | private var processedSagaSteps = mutableListOf()
14 | override fun getId() = sagaInstanceId
15 |
16 | fun launchSagaStep(sagaStep: SagaStep): SagaStepLaunchedEvent {
17 | return SagaStepLaunchedEvent(
18 | sagaStep.sagaName,
19 | sagaStep.stepName,
20 | sagaStep.sagaStepId,
21 | sagaStep.sagaInstanceId,
22 | sagaStep.prevSteps
23 | )
24 | }
25 |
26 | fun initiateSagaStep(sagaStep: SagaStep): SagaStepInitiatedEvent {
27 | if (sagaSteps.contains(sagaStep.sagaStepId)) {
28 | throw IllegalStateException("Duplicate step: $sagaStep")
29 | }
30 |
31 | return SagaStepInitiatedEvent(
32 | sagaStep.sagaName,
33 | sagaStep.stepName,
34 | sagaStep.sagaStepId,
35 | sagaStep.sagaInstanceId,
36 | sagaStep.prevSteps
37 | )
38 | }
39 |
40 | fun processSagaStep(sagaStep: SagaStep, eventName: String): SagaStepProcessedEvent {
41 | if (processedSagaSteps.contains(sagaStep.sagaStepId)) {
42 | throw IllegalStateException("Duplicate step: $sagaStep")
43 | }
44 |
45 | return SagaStepProcessedEvent(
46 | sagaStep.sagaName,
47 | sagaStep.stepName,
48 | sagaStep.sagaStepId,
49 | sagaStep.sagaInstanceId,
50 | sagaStep.prevSteps,
51 | eventName
52 | )
53 | }
54 |
55 | fun containsProcessedSagaStep(sagaStepId: UUID): Boolean {
56 | return processedSagaSteps.contains(sagaStepId)
57 | }
58 |
59 | fun processDefaultSaga(
60 | correlationId: UUID,
61 | currentEventId: UUID,
62 | eventName: String,
63 | causationId: UUID? = null
64 | ): DefaultSagaProcessedEvent {
65 | if (processedSagaSteps.contains(currentEventId)) {
66 | throw IllegalStateException("Duplicate step: $correlationId")
67 | }
68 |
69 | return DefaultSagaProcessedEvent(
70 | correlationId,
71 | currentEventId,
72 | causationId,
73 | eventName
74 | )
75 | }
76 |
77 | @StateTransitionFunc
78 | fun launchSagaStep(sagaStepEvent: SagaStepLaunchedEvent) {
79 | sagaName = sagaStepEvent.sagaName
80 | sagaInstanceId = sagaStepEvent.sagaInstanceId
81 | sagaSteps.add(sagaStepEvent.sagaStepId)
82 | }
83 |
84 | @StateTransitionFunc
85 | fun initiateSagaStep(sagaStepEvent: SagaStepInitiatedEvent) {
86 | sagaSteps.add(sagaStepEvent.sagaStepId)
87 | }
88 |
89 | @StateTransitionFunc
90 | fun processSagaStep(sagaStepEvent: SagaStepProcessedEvent) {
91 | processedSagaSteps.add(sagaStepEvent.sagaStepId)
92 | }
93 |
94 | @StateTransitionFunc
95 | fun processDefaultSaga(sagaEvent: DefaultSagaProcessedEvent) {
96 | if (!this::sagaInstanceId.isInitialized) {
97 | sagaInstanceId = sagaEvent.correlationId
98 | }
99 | processedSagaSteps.add(sagaEvent.currentEventId)
100 | }
101 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/streams/AggregateEventStreamManager.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.streams
2 |
3 | import kotlinx.coroutines.asCoroutineDispatcher
4 | import ru.quipy.core.AggregateRegistry
5 | import ru.quipy.core.EventSourcingProperties
6 | import ru.quipy.database.EventStore
7 | import ru.quipy.domain.Aggregate
8 | import ru.quipy.streams.annotation.RetryConf
9 | import ru.quipy.streams.annotation.RetryFailedStrategy
10 | import ru.quipy.utils.NamedThreadFactory
11 | import java.util.concurrent.ConcurrentHashMap
12 | import java.util.concurrent.Executors
13 | import kotlin.reflect.KClass
14 |
15 |
16 | class AggregateEventStreamManager(
17 | private val aggregateRegistry: AggregateRegistry,
18 | private val eventStore: EventStore,
19 | private val eventSourcingProperties: EventSourcingProperties,
20 | private val streamManager: EventStreamReaderManager
21 | ) {
22 | private val eventStreamListener: EventStreamListenerImpl = EventStreamListenerImpl()// todo sukhoa make injectable
23 |
24 | private val eventStreams = ConcurrentHashMap>()
25 |
26 | fun createEventStream(
27 | streamName: String,
28 | aggregateClass: KClass,
29 | retryConfig: RetryConf = RetryConf(3, RetryFailedStrategy.SKIP_EVENT)
30 | ): AggregateEventStream {
31 | val aggregateInfo = (aggregateRegistry.basicAggregateInfo(aggregateClass)
32 | ?: throw IllegalArgumentException("Aggregate $aggregateClass is not registered"))
33 |
34 | val eventStoreReader = streamManager.createStreamReader(
35 | eventStore,
36 | streamName,
37 | aggregateInfo as AggregateRegistry.BasicAggregateInfo,
38 | eventSourcingProperties,
39 | eventStreamListener,
40 | Executors.newSingleThreadExecutor(NamedThreadFactory("$streamName-store-reader")).asCoroutineDispatcher() // eventStoreReaderDispatcher
41 | )
42 |
43 | val eventsChannel = EventsChannel()
44 |
45 | val existing = eventStreams.putIfAbsent(
46 | streamName, BufferedAggregateEventStream(
47 | streamName,
48 | eventSourcingProperties.streamReadPeriod,
49 | eventSourcingProperties.streamBatchSize,
50 | eventsChannel,
51 | eventStoreReader,
52 | retryConfig,
53 | eventStreamListener,
54 | Executors.newSingleThreadExecutor(NamedThreadFactory("$streamName-stream-reader")).asCoroutineDispatcher() // eventStreamsDispatcher
55 | )
56 | )
57 |
58 | if (existing != null) throw IllegalStateException("There is already stream $streamName for aggregate ${aggregateClass.simpleName}")
59 |
60 | return eventStreams[streamName] as AggregateEventStream
61 | }
62 |
63 | fun destroy() {
64 | eventStreams.values.forEach {
65 | it.stopAndDestroy()
66 | }
67 | }
68 |
69 | fun maintenance(block: EventStreamListener.() -> Unit) { // todo sukhoa naming
70 | block(eventStreamListener)
71 | }
72 |
73 | fun streamsInfo() = eventStreams.map { (_, stream) ->
74 | StreamInfo(stream.streamName)
75 | }.toList()
76 |
77 | fun getStreamByName(name: String) = eventStreams[name]
78 |
79 | data class StreamInfo(
80 | val streamName: String
81 | )
82 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/streams/EventsChannel.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.streams
2 |
3 | import kotlinx.coroutines.channels.BufferOverflow
4 | import kotlinx.coroutines.channels.Channel
5 | import ru.quipy.domain.EventRecord
6 |
7 | class EventsChannel {
8 | private val eventsChannel: Channel = Channel(
9 | capacity = Channel.RENDEZVOUS,
10 | onBufferOverflow = BufferOverflow.SUSPEND
11 | )
12 |
13 | private val acknowledgesChannel: Channel = Channel(
14 | capacity = Channel.RENDEZVOUS,
15 | onBufferOverflow = BufferOverflow.SUSPEND
16 | )
17 |
18 | suspend fun sendEvent(eventRecord: EventRecord) {
19 | eventsChannel.send(eventRecord)
20 | }
21 |
22 | suspend fun receiveEvent(): EventRecord {
23 | return eventsChannel.receive()
24 | }
25 |
26 | suspend fun sendConfirmation(isConfirmed: Boolean) {
27 | acknowledgesChannel.send(isConfirmed)
28 | }
29 |
30 | suspend fun receiveConfirmation(): Boolean {
31 | return acknowledgesChannel.receive()
32 | }
33 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/streams/annotation/AggregateSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.streams.annotation
2 |
3 | import ru.quipy.domain.Aggregate
4 | import kotlin.reflect.KClass
5 |
6 | @Target(AnnotationTarget.CLASS)
7 | @Retention(AnnotationRetention.RUNTIME)
8 | annotation class AggregateSubscriber(
9 | val aggregateClass: KClass,
10 | val subscriberName: String,
11 | val retry: RetryConf = RetryConf(3, RetryFailedStrategy.SKIP_EVENT)
12 | )
13 |
14 | annotation class RetryConf(
15 | val maxAttempts: Int,
16 | val lastAttemptFailedStrategy: RetryFailedStrategy
17 | )
18 |
19 | /**
20 | * In case event handling failure (when subscriber failed to process it correct - exception or just returning false for some reason)
21 | * we can choose between two strategies:
22 | * - suspend the whole stream and wait till the situation is soled manually somehow
23 | * - skip the event and take the next one sacrificing the projection data consistency but continuing pocessing events
24 | */
25 | enum class RetryFailedStrategy {
26 | SUSPEND,
27 | SKIP_EVENT
28 | }
29 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/streams/annotation/SubscribeEvent.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.streams.annotation
2 |
3 | @Target(AnnotationTarget.FUNCTION)
4 | @Retention(AnnotationRetention.RUNTIME)
5 | annotation class SubscribeEvent
6 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/utils/Batcher.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.utils
2 |
3 | import org.slf4j.LoggerFactory
4 | import ru.quipy.core.exceptions.DuplicateEventIdException
5 | import java.util.concurrent.CompletableFuture
6 | import java.util.concurrent.Executors
7 | import java.util.concurrent.locks.ReentrantLock
8 | import kotlin.concurrent.withLock
9 | import kotlin.time.Duration
10 | import kotlin.time.Duration.Companion.milliseconds
11 |
12 | class Batcher(
13 | private val batchCommandSize: Int,
14 | private val batchWindow: Duration,
15 | private val batchUpdater: (List) -> Array
16 | ) {
17 | private val lock = ReentrantLock()
18 |
19 | private val collectingBatch = mutableListOf()
20 | private val idSet = mutableSetOf()
21 | private var collectingStartedAt = System.currentTimeMillis()
22 |
23 | private val batchExecJob = PeriodicalJob(
24 | "event-store",
25 | "batch-execute",
26 | Executors.newSingleThreadExecutor(NamedThreadFactory("batcher-executor")),
27 | delayer = Delayer.InvocationRatePreservingDelay(2.milliseconds),
28 | ) { _, _ ->
29 | if (collectingBatch.size >= batchCommandSize || System.currentTimeMillis() - collectingStartedAt >= batchWindow.inWholeMilliseconds) {
30 | executeBatch()
31 | }
32 | }
33 |
34 | fun delayedExecution(id: String, statement: String): CompletableFuture {
35 | val command = Command(statement, CompletableFuture())
36 | lock.withLock {
37 | if (!idSet.add(id)) {
38 | throw DuplicateEventIdException(
39 | "There is record with such an id in batch. Record cannot be saved $id",
40 | null
41 | )
42 | }
43 | collectingBatch.add(command)
44 | }
45 | return command.completableFuture
46 | }
47 |
48 | private fun executeBatch() {
49 | var copyCommands: List
50 | lock.withLock {
51 | copyCommands = collectingBatch.toList()
52 | if (collectingBatch.isNotEmpty()) {
53 | collectingBatch.clear()
54 | collectingStartedAt = System.currentTimeMillis()
55 | idSet.clear()
56 | }
57 |
58 | if (copyCommands.isEmpty()) return
59 |
60 | try {
61 | val errored = batchUpdater(copyCommands.map { it.statement }.toList()).toSet()
62 | copyCommands.forEachIndexed { i, c ->
63 | c.completableFuture.complete(!errored.contains(i))
64 | }
65 | } catch (e: Exception) {
66 | val batchStatement = copyCommands.joinToString(";") { it.statement }
67 | logger.error("Batch execution failed Statement: $batchStatement", e)
68 | copyCommands.forEach { it.completableFuture.completeExceptionally(e) }
69 | }
70 | }
71 | }
72 |
73 | class Command(
74 | val statement: String,
75 | val completableFuture: CompletableFuture
76 | )
77 |
78 | companion object {
79 | private val logger = LoggerFactory.getLogger(Batcher::class.java)
80 | }
81 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-lib/src/main/kotlin/ru/quipy/utils/NamedThreadFactory.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.utils
2 |
3 | import java.util.concurrent.ThreadFactory
4 | import java.util.concurrent.atomic.AtomicInteger
5 |
6 | class NamedThreadFactory(private val prefix: String) : ThreadFactory {
7 | private val sequence = AtomicInteger(1)
8 |
9 | override fun newThread(r: Runnable): Thread {
10 | val thread = Thread(r)
11 | val seq = sequence.getAndIncrement()
12 | thread.name = prefix + (if (seq > 1) "-$seq" else "")
13 | thread.isDaemon = true
14 | return thread
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-sagas-projections/src/main/kotlin/ru/quipy/SagasProjectionsApplication.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 | @SpringBootApplication
6 | class SagasProjectionsApplication
7 |
8 | fun main(args: Array) {
9 | runApplication(*args)
10 | }
11 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-sagas-projections/src/main/kotlin/ru/quipy/projections/SagaDefaultProjections.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projections
2 |
3 | import org.springframework.data.annotation.Id
4 | import org.springframework.data.mongodb.core.mapping.Document
5 | import org.springframework.data.mongodb.repository.MongoRepository
6 | import org.springframework.stereotype.Component
7 | import org.springframework.stereotype.Repository
8 | import ru.quipy.saga.aggregate.api.DefaultSagaProcessedEvent
9 | import ru.quipy.saga.aggregate.api.SagaStepAggregate
10 | import ru.quipy.streams.AggregateSubscriptionsManager
11 | import javax.annotation.PostConstruct
12 |
13 | @Component
14 | class SagaDefaultProjections(
15 | private val sagaDefaultProjectionsRepository: SagaDefaultProjectionsRepository,
16 | private val subscriptionsManager: AggregateSubscriptionsManager
17 | ) {
18 | @PostConstruct
19 | fun init() {
20 | subscriptionsManager.createSubscriber(SagaStepAggregate::class, "local-sagas::sagas-default-projections") {
21 | `when`(DefaultSagaProcessedEvent::class) { event ->
22 | val projectionOptional = sagaDefaultProjectionsRepository.findById(event.correlationId.toString())
23 |
24 | val currentEventId = event.currentEventId.toString()
25 | val causationId = event.causationId?.toString()
26 |
27 | val newStep = SagaDefaultStep(
28 | currentEventId,
29 | causationId,
30 | event.createdAt.toString(),
31 | event.eventName
32 | )
33 |
34 | val saga: SagaDefault
35 | if (projectionOptional.isEmpty) {
36 | saga = SagaDefault(event.correlationId.toString())
37 | insertNextStep(saga.sagaSteps, newStep)
38 | } else {
39 | saga = projectionOptional.get()
40 | if (!saga.sagaSteps.contains(newStep)) {
41 | insertNextStep(saga.sagaSteps, newStep)
42 | }
43 | }
44 |
45 | sagaDefaultProjectionsRepository.save(saga)
46 | }
47 | }
48 | }
49 |
50 | private fun insertNextStep(sagaSteps: MutableList, sagaStep: SagaDefaultStep) {
51 | var indexToInsert = 0
52 | if (sagaStep.causationId != null) {
53 | indexToInsert = sagaSteps.indices.findLast {
54 | sagaStep.causationId == sagaSteps[it].currentEventId
55 | }?.inc()
56 | ?: sagaSteps.size
57 | }
58 |
59 | if (indexToInsert < sagaSteps.size) {
60 | sagaSteps.add(indexToInsert, sagaStep)
61 | } else {
62 | sagaSteps.add(sagaStep)
63 | }
64 | }
65 | }
66 |
67 | @Document("sagas-default-projections")
68 | data class SagaDefault(
69 | @Id
70 | val correlationId: String,
71 | val sagaSteps: MutableList = mutableListOf()
72 | )
73 |
74 | data class SagaDefaultStep(
75 | val currentEventId: String,
76 | val causationId: String? = null,
77 | val processedAt: String,
78 | var eventName: String
79 | )
80 |
81 | @Repository
82 | interface SagaDefaultProjectionsRepository : MongoRepository
--------------------------------------------------------------------------------
/tiny-event-sourcing-sagas-projections/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.data.mongodb.host=localhost
2 | spring.data.mongodb.port=27017
3 | spring.data.mongodb.database=tiny-es
4 | spring.main.allow-bean-definition-overriding=true
5 |
6 | management.endpoints.web.exposure.include=health,metrics,prometheus
7 | management.endpoint.health.probes.enabled=true
8 | management.metrics.export.defaults.enabled=false
9 | management.metrics.export.prometheus.enabled=true
10 | management.metrics.tags.application=${spring.application.name}
11 |
12 | event.sourcing.snapshot-frequency=100
13 | event.sourcing.auto-scan-enabled=true
14 | event.sourcing.scan-package=ru.quipy
15 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/.env:
--------------------------------------------------------------------------------
1 | POSTGRES_DB=tiny_es
2 | POSTGRES_USER=tiny_es
3 | POSTGRES_PASSWORD=tiny_es
4 | POSTGRES_DB_PORT=5432
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | mongodb:
5 | build: mongo/
6 | ports:
7 | - "27017:27017"
8 |
9 | # volumes:
10 | # - ~/apps/mongo:/data/db
11 |
12 | volumes:
13 | pg_data:
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/mongo/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mongo:5.0
2 | RUN echo "rs.initiate({'_id':'rs0','members':[{'_id':0,'host':'127.0.0.1:27017'}]});" > /docker-entrypoint-initdb.d/replica-init.js
3 | RUN cat /docker-entrypoint-initdb.d/replica-init.js
4 | CMD [ "--bind_ip_all", "--replSet", "rs0" ]
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/Application.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class Application
8 |
9 | fun main(args: Array) {
10 | runApplication(*args)
11 | }
12 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/bankDemo/accounts/api/AccountAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.accounts.api
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.domain.Aggregate
5 |
6 | @AggregateType(aggregateEventsTableName = "accounts")
7 | class AccountAggregate: Aggregate
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/bankDemo/accounts/api/AccountEvents.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.accounts.api
2 |
3 | import ru.quipy.core.annotations.DomainEvent
4 | import ru.quipy.domain.Event
5 | import java.math.BigDecimal
6 | import java.util.*
7 |
8 | const val ACCOUNT_CREATED = "ACCOUNT_CREATED_EVENT"
9 | const val BANK_ACCOUNT_CREATED = "BANK_ACCOUNT_CREATED_EVENT"
10 | const val BANK_ACCOUNT_DEPOSIT = "BANK_ACCOUNT_DEPOSIT_EVENT"
11 | const val BANK_ACCOUNT_WITHDRAWAL = "BANK_ACCOUNT_WITHDRAWAL_EVENT"
12 | const val INTERNAL_ACCOUNT_TRANSFER = "INTERNAL_ACCOUNT_TRANSFER_EVENT"
13 |
14 | const val TRANSFER_TRANSACTION_ACCEPTED = "TRANSFER_TRANSACTION_ACCEPTED"
15 | const val TRANSFER_TRANSACTION_DECLINED = "TRANSFER_TRANSACTION_DECLINED"
16 | const val TRANSFER_TRANSACTION_PROCESSED = "TRANSFER_TRANSACTION_PROCESSED"
17 | const val TRANSFER_TRANSACTION_ROLLBACKED = "TRANSFER_TRANSACTION_ROLLBACKED"
18 |
19 |
20 | @DomainEvent(name = ACCOUNT_CREATED)
21 | data class AccountCreatedEvent(
22 | val accountId: UUID,
23 | val userId: UUID,
24 | ) : Event(
25 | name = ACCOUNT_CREATED,
26 | )
27 |
28 | @DomainEvent(name = BANK_ACCOUNT_CREATED)
29 | data class BankAccountCreatedEvent(
30 | val accountId: UUID,
31 | val bankAccountId: UUID,
32 | ) : Event(
33 | name = BANK_ACCOUNT_CREATED,
34 | )
35 |
36 | @DomainEvent(name = BANK_ACCOUNT_DEPOSIT)
37 | data class BankAccountDepositEvent(
38 | val accountId: UUID,
39 | val bankAccountId: UUID,
40 | val amount: BigDecimal,
41 | ) : Event(
42 | name = BANK_ACCOUNT_DEPOSIT,
43 | )
44 |
45 | @DomainEvent(name = BANK_ACCOUNT_WITHDRAWAL)
46 | data class BankAccountWithdrawalEvent(
47 | val accountId: UUID,
48 | val bankAccountId: UUID,
49 | val amount: BigDecimal,
50 | ) : Event(
51 | name = BANK_ACCOUNT_WITHDRAWAL,
52 | )
53 |
54 | @DomainEvent(name = INTERNAL_ACCOUNT_TRANSFER)
55 | data class InternalAccountTransferEvent(
56 | val accountId: UUID,
57 | val bankAccountIdFrom: UUID,
58 | val bankAccountIdTo: UUID,
59 | val amount: BigDecimal,
60 | ) : Event(
61 | name = INTERNAL_ACCOUNT_TRANSFER,
62 | )
63 |
64 | @DomainEvent(name = TRANSFER_TRANSACTION_ACCEPTED)
65 | data class TransferTransactionAcceptedEvent(
66 | val accountId: UUID,
67 | val bankAccountId: UUID,
68 | val transactionId: UUID,
69 | val transferAmount: BigDecimal,
70 | val isDeposit: Boolean
71 | ) : Event(
72 | name = TRANSFER_TRANSACTION_ACCEPTED,
73 | )
74 |
75 | @DomainEvent(name = TRANSFER_TRANSACTION_DECLINED)
76 | data class TransferTransactionDeclinedEvent(
77 | val accountId: UUID,
78 | val bankAccountId: UUID,
79 | val transactionId: UUID,
80 | val reason: String
81 | ) : Event(
82 | name = TRANSFER_TRANSACTION_DECLINED,
83 | )
84 |
85 | @DomainEvent(name = TRANSFER_TRANSACTION_PROCESSED)
86 | data class TransferTransactionProcessedEvent(
87 | val accountId: UUID,
88 | val bankAccountId: UUID,
89 | val transactionId: UUID,
90 | ) : Event(
91 | name = TRANSFER_TRANSACTION_PROCESSED,
92 | )
93 |
94 | @DomainEvent(name = TRANSFER_TRANSACTION_ROLLBACKED)
95 | data class TransferTransactionRollbackedEvent(
96 | val accountId: UUID,
97 | val bankAccountId: UUID,
98 | val transactionId: UUID,
99 | ) : Event(
100 | name = TRANSFER_TRANSACTION_ROLLBACKED,
101 | )
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/bankDemo/accounts/config/AccountBoundedContextConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.accounts.config
2 |
3 | import org.springframework.beans.factory.annotation.Autowired
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import ru.quipy.bankDemo.accounts.api.AccountAggregate
7 | import ru.quipy.bankDemo.accounts.logic.Account
8 | import ru.quipy.core.EventSourcingService
9 | import ru.quipy.core.EventSourcingServiceFactory
10 | import java.util.*
11 |
12 | @Configuration
13 | class AccountBoundedContextConfig {
14 |
15 | @Autowired
16 | private lateinit var eventSourcingServiceFactory: EventSourcingServiceFactory
17 |
18 | @Bean
19 | fun accountEsService(): EventSourcingService =
20 | eventSourcingServiceFactory.create()
21 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/bankDemo/accounts/controller/AccountController.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.accounts.controller
2 |
3 | import org.springframework.web.bind.annotation.GetMapping
4 | import org.springframework.web.bind.annotation.PathVariable
5 | import org.springframework.web.bind.annotation.PostMapping
6 | import org.springframework.web.bind.annotation.RequestMapping
7 | import org.springframework.web.bind.annotation.RestController
8 | import ru.quipy.bankDemo.accounts.api.AccountAggregate
9 | import ru.quipy.bankDemo.accounts.api.AccountCreatedEvent
10 | import ru.quipy.bankDemo.accounts.api.BankAccountCreatedEvent
11 | import ru.quipy.bankDemo.accounts.logic.Account
12 | import ru.quipy.bankDemo.accounts.logic.BankAccount
13 | import ru.quipy.core.EventSourcingService
14 | import java.util.*
15 |
16 | @RestController
17 | @RequestMapping("/accounts")
18 | class AccountController(
19 | val accountEsService: EventSourcingService
20 | ) {
21 |
22 | @PostMapping("/{holderId}")
23 | fun createAccount(@PathVariable holderId: UUID) : AccountCreatedEvent {
24 | return accountEsService.create { it.createNewAccount(holderId = holderId) }
25 | }
26 |
27 | @GetMapping("/{accountId}")
28 | fun getAccount(@PathVariable accountId: UUID) : Account? {
29 | return accountEsService.getState(accountId)
30 | }
31 |
32 | @PostMapping("/{accountId}/bankAccount")
33 | fun createBankAccount(@PathVariable accountId: UUID) : BankAccountCreatedEvent {
34 | return accountEsService.update(accountId) { it.createNewBankAccount() }
35 | }
36 |
37 | @GetMapping("/{accountId}/bankAccount/{bankAccountId}")
38 | fun getBankAccount(@PathVariable accountId: UUID, @PathVariable bankAccountId: UUID) : BankAccount? {
39 | return accountEsService.getState(accountId)?.bankAccounts?.get(bankAccountId)
40 | }
41 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/bankDemo/accounts/subscribers/TransactionsSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.accounts.subscribers
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import org.springframework.stereotype.Component
6 | import ru.quipy.bankDemo.accounts.api.AccountAggregate
7 | import ru.quipy.bankDemo.transfers.api.TransferTransactionAggregate
8 | import ru.quipy.bankDemo.transfers.api.TransferTransactionCreatedEvent
9 | import ru.quipy.bankDemo.accounts.logic.Account
10 | import ru.quipy.bankDemo.transfers.api.TransactionConfirmedEvent
11 | import ru.quipy.core.EventSourcingService
12 | import ru.quipy.streams.AggregateSubscriptionsManager
13 | import java.util.*
14 | import javax.annotation.PostConstruct
15 |
16 | @Component
17 | class TransactionsSubscriber(
18 | private val subscriptionsManager: AggregateSubscriptionsManager,
19 | private val accountEsService: EventSourcingService
20 | ) {
21 | private val logger: Logger = LoggerFactory.getLogger(TransactionsSubscriber::class.java)
22 |
23 | @PostConstruct
24 | fun init() {
25 | subscriptionsManager.createSubscriber(TransferTransactionAggregate::class, "accounts::transaction-processing-subscriber") {
26 | `when`(TransferTransactionCreatedEvent::class) { event ->
27 | logger.info("Got transaction to process: $event")
28 |
29 | val transactionOutcome1 = accountEsService.update(event.sourceAccountId) { // todo sukhoa idempotence!
30 | it.performTransferFrom(
31 | event.sourceBankAccountId,
32 | event.transferId,
33 | event.transferAmount
34 | )
35 | }
36 |
37 | val transactionOutcome2 = accountEsService.update(event.destinationAccountId) { // todo sukhoa idempotence!
38 | it.performTransferTo(
39 | event.destinationBankAccountId,
40 | event.transferId,
41 | event.transferAmount
42 | )
43 | }
44 |
45 | logger.info("Transaction: ${event.transferId}. Outcomes: $transactionOutcome1, $transactionOutcome2")
46 | }
47 | `when`(TransactionConfirmedEvent::class) { event ->
48 | logger.info("Got transaction confirmed event: $event")
49 |
50 | val transactionOutcome1 = accountEsService.update(event.sourceAccountId) { // todo sukhoa idempotence!
51 | it.processPendingTransaction(event.sourceBankAccountId, event.transferId)
52 | }
53 |
54 | val transactionOutcome2 = accountEsService.update(event.destinationAccountId) { // todo sukhoa idempotence!
55 | it.processPendingTransaction(event.destinationBankAccountId, event.transferId)
56 | }
57 |
58 | logger.info("Transaction: ${event.transferId}. Outcomes: $transactionOutcome1, $transactionOutcome2")
59 | }
60 | // todo sukhoa bank account deleted event
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/bankDemo/transfers/api/TransferTransactionAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.api
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.domain.Aggregate
5 |
6 | @AggregateType(aggregateEventsTableName = "transfers")
7 | class TransferTransactionAggregate: Aggregate
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/bankDemo/transfers/config/TransactionCoundedContextConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.config
2 |
3 | import org.springframework.beans.factory.annotation.Autowired
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import ru.quipy.bankDemo.transfers.api.TransferTransactionAggregate
7 | import ru.quipy.bankDemo.transfers.logic.TransferTransaction
8 | import ru.quipy.core.EventSourcingService
9 | import ru.quipy.core.EventSourcingServiceFactory
10 | import java.util.*
11 |
12 | @Configuration
13 | class TransactionCoundedContextConfig {
14 |
15 | @Autowired
16 | private lateinit var eventSourcingServiceFactory: EventSourcingServiceFactory
17 |
18 | @Bean
19 | fun transactionEsService(): EventSourcingService =
20 | eventSourcingServiceFactory.create()
21 |
22 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/bankDemo/transfers/projections/BankAccountsExistenceCache.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.projections
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import org.springframework.data.annotation.Id
6 | import org.springframework.data.mongodb.core.mapping.Document
7 | import org.springframework.data.mongodb.repository.MongoRepository
8 | import org.springframework.stereotype.Component
9 | import org.springframework.stereotype.Repository
10 | import ru.quipy.bankDemo.accounts.api.AccountAggregate
11 | import ru.quipy.bankDemo.accounts.api.BankAccountCreatedEvent
12 | import ru.quipy.streams.AggregateSubscriptionsManager
13 | import java.util.*
14 | import javax.annotation.PostConstruct
15 |
16 | @Component
17 | class BankAccountsExistenceCache(
18 | private val bankAccountCacheRepository: BankAccountCacheRepository,
19 | private val subscriptionsManager: AggregateSubscriptionsManager
20 | ) {
21 | private val logger: Logger = LoggerFactory.getLogger(BankAccountsExistenceCache::class.java)
22 |
23 | @PostConstruct
24 | fun init() {
25 | subscriptionsManager.createSubscriber(AccountAggregate::class, "transactions::accounts-cache") {
26 | `when`(BankAccountCreatedEvent::class) { event ->
27 | bankAccountCacheRepository.save(BankAccount(event.bankAccountId, event.accountId)) // todo sukhoa idempotence!
28 | logger.info("Update bank account cache, create account ${event.accountId}-${event.bankAccountId}")
29 | }
30 | // todo sukhoa bank account deleted event
31 | }
32 | }
33 | }
34 |
35 | @Document("transactions-accounts-cache")
36 | data class BankAccount(
37 | @Id
38 | val bankAccountId: UUID,
39 | var accountId: UUID
40 | )
41 |
42 | @Repository
43 | interface BankAccountCacheRepository: MongoRepository
44 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/bankDemo/transfers/service/TransactionService.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.service
2 |
3 | import org.springframework.stereotype.Service
4 | import ru.quipy.bankDemo.transfers.api.TransferTransactionAggregate
5 | import ru.quipy.bankDemo.transfers.api.TransferTransactionCreatedEvent
6 | import ru.quipy.bankDemo.transfers.logic.TransferTransaction
7 | import ru.quipy.bankDemo.transfers.projections.BankAccountCacheRepository
8 | import ru.quipy.core.EventSourcingService
9 | import java.math.BigDecimal
10 | import java.util.*
11 |
12 | @Service
13 | class TransactionService(
14 | private val bankAccountCacheRepository: BankAccountCacheRepository,
15 | private val transactionEsService: EventSourcingService
16 | ) {
17 | fun initiateTransferTransaction(
18 | sourceBankAccountId: UUID,
19 | destinationBankAccountId: UUID,
20 | transferAmount: BigDecimal
21 | ): TransferTransactionCreatedEvent {
22 | val srcBankAccount = bankAccountCacheRepository.findById(sourceBankAccountId).orElseThrow {
23 | IllegalArgumentException("Cannot create transaction. There is no source bank account: $sourceBankAccountId")
24 | }
25 |
26 | val dstBankAccount = bankAccountCacheRepository.findById(destinationBankAccountId).orElseThrow {
27 | IllegalArgumentException("Cannot create transaction. There is no destination bank account: $destinationBankAccountId")
28 | }
29 |
30 | return transactionEsService.create {
31 | it.initiateTransferTransaction(
32 | sourceAccountId = srcBankAccount.accountId,
33 | sourceBankAccountId = srcBankAccount.bankAccountId,
34 | destinationAccountId = dstBankAccount.accountId,
35 | destinationBankAccountId = dstBankAccount.bankAccountId,
36 | transferAmount = transferAmount
37 | )
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/bankDemo/transfers/subscribers/BankAccountsSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.bankDemo.transfers.subscribers
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import org.springframework.stereotype.Component
6 | import ru.quipy.bankDemo.accounts.api.*
7 | import ru.quipy.bankDemo.transfers.api.TransferTransactionAggregate
8 | import ru.quipy.bankDemo.transfers.api.TransferTransactionCreatedEvent
9 | import ru.quipy.bankDemo.accounts.logic.Account
10 | import ru.quipy.bankDemo.transfers.logic.TransferTransaction
11 | import ru.quipy.core.EventSourcingService
12 | import ru.quipy.streams.AggregateSubscriptionsManager
13 | import java.util.*
14 | import javax.annotation.PostConstruct
15 |
16 | @Component
17 | class BankAccountsSubscriber(
18 | private val subscriptionsManager: AggregateSubscriptionsManager,
19 | private val transactionEsService: EventSourcingService
20 | ) {
21 | private val logger: Logger = LoggerFactory.getLogger(BankAccountsSubscriber::class.java)
22 |
23 | @PostConstruct
24 | fun init() {
25 | subscriptionsManager.createSubscriber(AccountAggregate::class, "transactions::bank-accounts-subscriber") {
26 | `when`(TransferTransactionAcceptedEvent::class) { event ->
27 | transactionEsService.update(event.transactionId) {
28 | it.processParticipantAccept(event.bankAccountId)
29 | }
30 | }
31 | `when`(TransferTransactionDeclinedEvent::class) { event ->
32 | transactionEsService.update(event.transactionId) {
33 | it.processParticipantDecline(event.bankAccountId)
34 | }
35 | }
36 | `when`(TransferTransactionProcessedEvent::class) { event ->
37 | transactionEsService.update(event.transactionId) {
38 | it.participantCommitted(event.bankAccountId)
39 | }
40 | }
41 | `when`(TransferTransactionRollbackedEvent::class) { event ->
42 | transactionEsService.update(event.transactionId) {
43 | it.participantRollbacked(event.bankAccountId)
44 | }
45 | }
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/projectDemo/api/ProjectAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.api
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.domain.Aggregate
5 |
6 | // API
7 | @AggregateType(aggregateEventsTableName = "aggregate-project")
8 | class ProjectAggregate : Aggregate
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/projectDemo/api/ProjectAggregateDomainEvents.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.api
2 |
3 | import ru.quipy.core.annotations.DomainEvent
4 | import ru.quipy.domain.Event
5 | import java.util.*
6 |
7 | const val PROJECT_CREATED_EVENT = "PROJECT_CREATED_EVENT"
8 | const val TAG_CREATED_EVENT = "TAG_CREATED_EVENT"
9 | const val TAG_ASSIGNED_TO_TASK_EVENT = "TAG_ASSIGNED_TO_TASK_EVENT"
10 | const val TASK_CREATED_EVENT = "TASK_CREATED_EVENT"
11 |
12 | // API
13 | @DomainEvent(name = PROJECT_CREATED_EVENT)
14 | class ProjectCreatedEvent(
15 | val projectId: String,
16 | createdAt: Long = System.currentTimeMillis(),
17 | ) : Event(
18 | name = PROJECT_CREATED_EVENT,
19 | createdAt = createdAt,
20 | )
21 |
22 | @DomainEvent(name = TAG_CREATED_EVENT)
23 | class TagCreatedEvent(
24 | val projectId: String,
25 | val tagId: UUID,
26 | val tagName: String,
27 | createdAt: Long = System.currentTimeMillis(),
28 | ) : Event(
29 | name = TAG_CREATED_EVENT,
30 | createdAt = createdAt,
31 | )
32 |
33 | @DomainEvent(name = TASK_CREATED_EVENT)
34 | class TaskCreatedEvent(
35 | val projectId: String,
36 | val taskId: UUID,
37 | val taskName: String,
38 | createdAt: Long = System.currentTimeMillis(),
39 | ) : Event(
40 | name = TASK_CREATED_EVENT,
41 | createdAt = createdAt
42 | )
43 |
44 | @DomainEvent(name = TAG_ASSIGNED_TO_TASK_EVENT)
45 | class TagAssignedToTaskEvent(
46 | val projectId: String,
47 | val taskId: UUID,
48 | val tagId: UUID,
49 | createdAt: Long = System.currentTimeMillis(),
50 | ) : SomeBaseEventWithoutAnnotation(
51 | name = TAG_ASSIGNED_TO_TASK_EVENT,
52 | createdAt = createdAt
53 | )
54 |
55 | // for testing and demo purposes
56 | open class SomeBaseEventWithoutAnnotation(
57 | name: String,
58 | createdAt: Long = System.currentTimeMillis(),
59 | ) : Event(name = name, createdAt = createdAt)
60 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/projectDemo/config/ProjectDemoConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.config
2 |
3 | import org.slf4j.LoggerFactory
4 | import org.springframework.beans.factory.annotation.Autowired
5 | import org.springframework.context.annotation.Bean
6 | import org.springframework.context.annotation.Configuration
7 | import ru.quipy.core.AggregateRegistry
8 | import ru.quipy.core.EventSourcingServiceFactory
9 | import ru.quipy.projectDemo.api.ProjectAggregate
10 | import ru.quipy.projectDemo.logic.ProjectAggregateState
11 | import ru.quipy.projectDemo.projections.AnnotationBasedProjectEventsSubscriber
12 | import ru.quipy.streams.AggregateEventStreamManager
13 | import ru.quipy.streams.AggregateSubscriptionsManager
14 | import javax.annotation.PostConstruct
15 |
16 | @Configuration
17 | class ProjectDemoConfig {
18 |
19 | private val logger = LoggerFactory.getLogger(ProjectDemoConfig::class.java)
20 |
21 | @Autowired
22 | private lateinit var subscriptionsManager: AggregateSubscriptionsManager
23 |
24 | @Autowired
25 | private lateinit var projectEventSubscriber: AnnotationBasedProjectEventsSubscriber
26 |
27 | @Autowired
28 | private lateinit var eventSourcingServiceFactory: EventSourcingServiceFactory
29 |
30 | @Autowired
31 | private lateinit var eventStreamManager: AggregateEventStreamManager
32 |
33 | @Autowired
34 | private lateinit var aggregateRegistry: AggregateRegistry
35 |
36 | @PostConstruct
37 | fun init() {
38 | // autoscan enabled see event.sourcing.auto-scan-enabled property
39 | // aggregateRegistry.register(ProjectAggregate::class, ProjectAggregateState::class) {
40 | // registerStateTransition(TagCreatedEvent::class, ProjectAggregateState::tagCreatedApply)
41 | // registerStateTransition(TaskCreatedEvent::class, ProjectAggregateState::taskCreatedApply)
42 | // registerStateTransition(TagAssignedToTaskEvent::class, ProjectAggregateState::tagAssignedApply)
43 | // }
44 |
45 | subscriptionsManager.subscribe(projectEventSubscriber)
46 |
47 | eventStreamManager.maintenance {
48 | onRecordHandledSuccessfully { streamName, eventName ->
49 | logger.info("Stream $streamName successfully processed record of $eventName")
50 | }
51 |
52 | onBatchRead { streamName, batchSize ->
53 | logger.info("Stream $streamName read batch size: $batchSize")
54 | }
55 | }
56 | }
57 |
58 | @Bean
59 | fun demoESService() = eventSourcingServiceFactory.create()
60 |
61 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/projectDemo/logic/ProjectAggregateCommands.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo
2 |
3 | import ru.quipy.projectDemo.api.ProjectCreatedEvent
4 | import ru.quipy.projectDemo.api.TagAssignedToTaskEvent
5 | import ru.quipy.projectDemo.api.TagCreatedEvent
6 | import ru.quipy.projectDemo.api.TaskCreatedEvent
7 | import ru.quipy.projectDemo.logic.ProjectAggregateState
8 | import java.util.*
9 |
10 |
11 | // Commands : takes something -> returns event
12 | fun ProjectAggregateState.create(id: String): ProjectCreatedEvent {
13 | return ProjectCreatedEvent(projectId = id)
14 | }
15 |
16 | fun ProjectAggregateState.addTask(name: String): TaskCreatedEvent {
17 | return TaskCreatedEvent(projectId = this.getId(), taskId = UUID.randomUUID(), taskName = name)
18 | }
19 |
20 | fun ProjectAggregateState.createTag(name: String): TagCreatedEvent {
21 | if (projectTags.values.any { it.name == name }) {
22 | throw IllegalArgumentException("Tag already exists: $name")
23 | }
24 | return TagCreatedEvent(projectId = this.getId(), tagId = UUID.randomUUID(), tagName = name)
25 | }
26 |
27 | fun ProjectAggregateState.assignTagToTask(tagId: UUID, taskId: UUID): TagAssignedToTaskEvent {
28 | if (!projectTags.containsKey(tagId)) {
29 | throw IllegalArgumentException("Tag doesn't exists: $tagId")
30 | }
31 |
32 | if (!tasks.containsKey(taskId)) {
33 | throw IllegalArgumentException("Task doesn't exists: $taskId")
34 | }
35 |
36 | return TagAssignedToTaskEvent(projectId = this.getId(), tagId = tagId, taskId = taskId)
37 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/projectDemo/logic/ProjectAggregateState.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.logic
2 |
3 | import ru.quipy.core.annotations.StateTransitionFunc
4 | import ru.quipy.domain.AggregateState
5 | import ru.quipy.projectDemo.api.ProjectAggregate
6 | import ru.quipy.projectDemo.api.ProjectCreatedEvent
7 | import ru.quipy.projectDemo.api.TagAssignedToTaskEvent
8 | import ru.quipy.projectDemo.api.TagCreatedEvent
9 | import ru.quipy.projectDemo.api.TaskCreatedEvent
10 | import java.util.UUID
11 |
12 | // Service's business logic
13 | class ProjectAggregateState: AggregateState {
14 | lateinit var projectId: String
15 | var createdAt: Long = System.currentTimeMillis()
16 | var updatedAt: Long = System.currentTimeMillis()
17 |
18 | var tasks = mutableMapOf()
19 | var projectTags = mutableMapOf()
20 |
21 | override fun getId() = projectId
22 |
23 | // State transition functions
24 | @StateTransitionFunc
25 | fun projectCreatedApply(event: ProjectCreatedEvent) {
26 | projectId = event.projectId
27 | updatedAt = createdAt
28 | }
29 |
30 | @StateTransitionFunc
31 | fun tagCreatedApply(event: TagCreatedEvent) {
32 | projectTags[event.tagId] = ProjectTag(event.tagId, event.tagName)
33 | updatedAt = createdAt
34 | }
35 |
36 | @StateTransitionFunc
37 | fun taskCreatedApply(event: TaskCreatedEvent) {
38 | tasks[event.taskId] = TaskEntity(event.taskId, event.taskName, mutableSetOf())
39 | updatedAt = createdAt
40 | }
41 | }
42 |
43 | data class TaskEntity(
44 | val id: UUID = UUID.randomUUID(),
45 | val name: String,
46 | val tagsAssigned: MutableSet
47 | )
48 |
49 | data class ProjectTag(
50 | val id: UUID = UUID.randomUUID(),
51 | val name: String
52 | )
53 |
54 | @StateTransitionFunc
55 | fun ProjectAggregateState.tagAssignedApply(event: TagAssignedToTaskEvent) {
56 | tasks[event.taskId]?.tagsAssigned?.add(event.tagId)
57 | ?: throw IllegalArgumentException("No such task: ${event.taskId}") // todo sukhoa exception or not?
58 | updatedAt = createdAt
59 | }
60 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/projectDemo/projections/AnnotationBasedProjectEventsSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.projections
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import org.springframework.stereotype.Service
6 | import ru.quipy.projectDemo.api.ProjectAggregate
7 | import ru.quipy.projectDemo.api.TagCreatedEvent
8 | import ru.quipy.projectDemo.api.TaskCreatedEvent
9 | import ru.quipy.streams.annotation.AggregateSubscriber
10 | import ru.quipy.streams.annotation.SubscribeEvent
11 |
12 | @Service
13 | @AggregateSubscriber(
14 | aggregateClass = ProjectAggregate::class, subscriberName = "demo-subs-stream"
15 | )
16 | class AnnotationBasedProjectEventsSubscriber {
17 |
18 | val logger: Logger = LoggerFactory.getLogger(AnnotationBasedProjectEventsSubscriber::class.java)
19 |
20 | @SubscribeEvent
21 | fun taskCreatedSubscriber(event: TaskCreatedEvent) {
22 | logger.info("Task created: {}", event.taskName)
23 | }
24 |
25 | @SubscribeEvent
26 | fun tagCreatedSubscriber(event: TagCreatedEvent) {
27 | logger.info("Tag created: {}", event.tagName)
28 | }
29 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/projectDemo/projections/ProjectEventsSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.projectDemo.projections
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import org.springframework.beans.factory.annotation.Autowired
6 | import org.springframework.stereotype.Service
7 | import ru.quipy.projectDemo.api.ProjectAggregate
8 | import ru.quipy.projectDemo.api.TagAssignedToTaskEvent
9 | import ru.quipy.projectDemo.api.TagCreatedEvent
10 | import ru.quipy.projectDemo.api.TaskCreatedEvent
11 | import ru.quipy.streams.AggregateSubscriptionsManager
12 | import javax.annotation.PostConstruct
13 |
14 | @Service
15 | class ProjectEventsSubscriber {
16 |
17 | val logger: Logger = LoggerFactory.getLogger(ProjectEventsSubscriber::class.java)
18 |
19 | @Autowired
20 | lateinit var subscriptionsManager: AggregateSubscriptionsManager
21 |
22 | @PostConstruct
23 | fun init() {
24 | subscriptionsManager.createSubscriber(ProjectAggregate::class, "some-meaningful-name") {
25 |
26 | `when`(TaskCreatedEvent::class) { event ->
27 | logger.info("Task created: {}", event.taskName)
28 | }
29 |
30 | `when`(TagCreatedEvent::class) { event ->
31 | logger.info("Tag created: {}", event.tagName)
32 | }
33 |
34 | `when`(TagAssignedToTaskEvent::class) { event ->
35 | logger.info("Tag {} assigned to task {}: ", event.tagId, event.taskId)
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/flights/api/FlightAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.flights.api
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.domain.Aggregate
5 |
6 | @AggregateType("flights")
7 | class FlightAggregate : Aggregate
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/flights/api/FlightsEvents.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.flights.api
2 |
3 | import ru.quipy.core.annotations.DomainEvent
4 | import ru.quipy.domain.Event
5 | import java.util.UUID
6 |
7 | const val FLIGHTS_RESERVED = "FLIGHTS_RESERVED_EVENT"
8 | const val FLIGHTS_RESERVATION_FAILED = "FLIGHTS_RESERVATION_FAILED_EVENT"
9 |
10 | @DomainEvent(FLIGHTS_RESERVED)
11 | data class FlightReservedEvent (
12 | val flightReservationId: UUID
13 | ) : Event(
14 | name = FLIGHTS_RESERVED,
15 | )
16 |
17 | @DomainEvent(FLIGHTS_RESERVATION_FAILED)
18 | data class FlightReservationCanceledEvent (
19 | val flightReservationId: UUID
20 | ): Event(
21 | name = FLIGHTS_RESERVATION_FAILED,
22 | )
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/flights/config/FlightBoundedContextConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.flights.config
2 |
3 | import org.springframework.beans.factory.annotation.Autowired
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import ru.quipy.core.EventSourcingService
7 | import ru.quipy.core.EventSourcingServiceFactory
8 | import ru.quipy.sagaDemo.flights.api.FlightAggregate
9 | import ru.quipy.sagaDemo.flights.logic.Flight
10 | import java.util.*
11 |
12 | @Configuration
13 | class FlightBoundedContextConfig {
14 |
15 | @Autowired
16 | private lateinit var eventSourcingServiceFactory: EventSourcingServiceFactory
17 |
18 | @Bean
19 | fun flightEsService(): EventSourcingService =
20 | eventSourcingServiceFactory.create()
21 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/flights/logic/Flight.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.flights.logic
2 |
3 | import ru.quipy.core.annotations.StateTransitionFunc
4 | import ru.quipy.domain.AggregateState
5 | import ru.quipy.sagaDemo.flights.api.FlightAggregate
6 | import ru.quipy.sagaDemo.flights.api.FlightReservationCanceledEvent
7 | import ru.quipy.sagaDemo.flights.api.FlightReservedEvent
8 | import java.util.*
9 |
10 | class Flight : AggregateState {
11 | lateinit var flightId: UUID
12 | private var canceled: Boolean = false
13 |
14 | override fun getId() = flightId
15 |
16 | fun reserveFlight(id: UUID = UUID.randomUUID()): FlightReservedEvent {
17 | return FlightReservedEvent(id)
18 | }
19 |
20 | fun canselFlight(id: UUID): FlightReservationCanceledEvent {
21 | return FlightReservationCanceledEvent(id)
22 | }
23 |
24 | @StateTransitionFunc
25 | fun reserveFlight(event: FlightReservedEvent) {
26 | flightId = event.flightReservationId
27 | }
28 |
29 | @StateTransitionFunc
30 | fun canselFlight(event: FlightReservationCanceledEvent) {
31 | canceled = true
32 | }
33 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/flights/subscribers/PaymentSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.flights.subscribers
2 |
3 | import org.springframework.stereotype.Component
4 | import ru.quipy.core.EventSourcingService
5 | import ru.quipy.saga.SagaManager
6 | import ru.quipy.sagaDemo.flights.api.FlightAggregate
7 | import ru.quipy.sagaDemo.flights.logic.Flight
8 | import ru.quipy.sagaDemo.payment.api.PaymentAggregate
9 | import ru.quipy.sagaDemo.payment.api.PaymentSucceededEvent
10 | import ru.quipy.streams.AggregateSubscriptionsManager
11 | import java.util.*
12 | import javax.annotation.PostConstruct
13 |
14 | @Component
15 | class PaymentSubscriber (
16 | private val subscriptionsManager: AggregateSubscriptionsManager,
17 | private val flightEsService: EventSourcingService,
18 | private val sagaManager: SagaManager
19 | ) {
20 |
21 | @PostConstruct
22 | fun init() {
23 | subscriptionsManager.createSubscriber(PaymentAggregate::class, "flights::payment-subscriber") {
24 | `when`(PaymentSucceededEvent::class) { event ->
25 | val sagaContext = sagaManager
26 | .withContextGiven(event.sagaContext)
27 | .launchSaga("TRIP_RESERVATION3", "reservation flight3")
28 | .performSagaStep("TRIP_RESERVATION", "reservation flight")
29 | .performSagaStep("TRIP_RESERVATION2", "reservation flight2")
30 | .sagaContext()
31 |
32 | flightEsService.create(sagaContext) { it.reserveFlight(event.paymentId) }
33 | }
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/payment/api/PaymentAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.payment.api
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.domain.Aggregate
5 |
6 | @AggregateType("payment")
7 | class PaymentAggregate : Aggregate
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/payment/api/PaymentEvents.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.payment.api
2 |
3 | import ru.quipy.core.annotations.DomainEvent
4 | import ru.quipy.domain.Event
5 | import java.util.UUID
6 |
7 | const val PAYMENT_SUCCEEDED = "PAYMENT_SUCCEEDED_EVENT"
8 | const val PAYMENT_CANCELED = "PAYMENT_CANCELED_EVENT"
9 |
10 | @DomainEvent(PAYMENT_SUCCEEDED)
11 | data class PaymentSucceededEvent (
12 | val paymentId: UUID,
13 | val amount: Int
14 | ) : Event(
15 | name = PAYMENT_SUCCEEDED,
16 | )
17 |
18 | @DomainEvent(PAYMENT_CANCELED)
19 | data class PaymentCanceledEvent (
20 | val paymentId: UUID,
21 | ) : Event(
22 | name = PAYMENT_CANCELED,
23 | )
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/payment/config/PaymentBoundedContextConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.payment.config
2 |
3 | import org.springframework.beans.factory.annotation.Autowired
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import ru.quipy.core.EventSourcingService
7 | import ru.quipy.core.EventSourcingServiceFactory
8 | import ru.quipy.sagaDemo.payment.api.PaymentAggregate
9 | import ru.quipy.sagaDemo.payment.logic.Payment
10 | import java.util.*
11 |
12 | @Configuration
13 | class PaymentBoundedContextConfig {
14 |
15 | @Autowired
16 | private lateinit var eventSourcingServiceFactory: EventSourcingServiceFactory
17 |
18 | @Bean
19 | fun paymentEsService(): EventSourcingService =
20 | eventSourcingServiceFactory.create()
21 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/payment/logic/Payment.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.payment.logic
2 |
3 | import ru.quipy.core.annotations.StateTransitionFunc
4 | import ru.quipy.domain.AggregateState
5 | import ru.quipy.domain.Event
6 | import ru.quipy.sagaDemo.payment.api.PaymentAggregate
7 | import ru.quipy.sagaDemo.payment.api.PaymentCanceledEvent
8 | import ru.quipy.sagaDemo.payment.api.PaymentSucceededEvent
9 | import java.util.*
10 |
11 | class Payment : AggregateState {
12 | lateinit var paymentId: UUID
13 | private var failed: Boolean = false
14 |
15 | override fun getId() = paymentId
16 |
17 | fun processPayment(id: UUID = UUID.randomUUID(), amount: Int) : Event {
18 | return if (amount > 0)
19 | PaymentSucceededEvent(id, amount)
20 | else
21 | PaymentCanceledEvent(id)
22 | }
23 |
24 | fun canselPayment(id: UUID) : PaymentCanceledEvent {
25 | return PaymentCanceledEvent(id)
26 | }
27 |
28 | @StateTransitionFunc
29 | fun processPayment(event: PaymentSucceededEvent) {
30 | paymentId = event.paymentId
31 | }
32 |
33 | @StateTransitionFunc
34 | fun canselPayment(event: PaymentCanceledEvent) {
35 | paymentId = event.paymentId
36 | failed = true
37 | }
38 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/payment/subscribers/TripSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.payment.subscribers
2 |
3 | import org.springframework.stereotype.Component
4 |
5 | import ru.quipy.core.EventSourcingService
6 | import ru.quipy.saga.SagaManager
7 | import ru.quipy.sagaDemo.payment.api.PaymentAggregate
8 | import ru.quipy.sagaDemo.payment.logic.Payment
9 | import ru.quipy.sagaDemo.trips.api.TripAggregate
10 | import ru.quipy.sagaDemo.trips.api.TripReservationStartedEvent
11 | import ru.quipy.streams.AggregateSubscriptionsManager
12 | import java.util.*
13 | import javax.annotation.PostConstruct
14 |
15 | @Component
16 | class TripSubscriber(
17 | private val subscriptionsManager: AggregateSubscriptionsManager,
18 | private val paymentEsService: EventSourcingService,
19 | private val sagaManager: SagaManager
20 | ) {
21 |
22 | @PostConstruct
23 | fun init() {
24 | subscriptionsManager.createSubscriber(TripAggregate::class, "payment::trips-subscriber") {
25 | `when`(TripReservationStartedEvent::class) { event ->
26 | val sagaContext = sagaManager
27 | .withContextGiven(event.sagaContext)
28 | .launchSaga("TRIP_RESERVATION2", "process payment2")
29 | .performSagaStep("TRIP_RESERVATION", "process payment")
30 | .sagaContext()
31 |
32 | paymentEsService.create(sagaContext) { it.processPayment(event.tripId,100) }
33 | }
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/trips/api/TripAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.trips.api
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.domain.Aggregate
5 |
6 | @AggregateType("trips")
7 | class TripAggregate : Aggregate
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/trips/api/TripEvents.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.trips.api
2 |
3 | import ru.quipy.core.annotations.DomainEvent
4 | import ru.quipy.domain.Event
5 | import java.util.UUID
6 |
7 | const val TRIP_RESERVATION_STARTED = "TRIP_RESERVATION_STARTED_EVENT"
8 | const val TRIP_RESERVATION_FAILED = "TRIP_RESERVATION_FAILED_EVENT"
9 | const val TRIP_RESERVATION_CONFIRMED = "TRIP_RESERVATION_CONFIRMED_EVENT"
10 |
11 | @DomainEvent(TRIP_RESERVATION_STARTED)
12 | data class TripReservationStartedEvent (
13 | val tripId: UUID
14 | ) : Event(
15 | name = TRIP_RESERVATION_STARTED,
16 | )
17 |
18 | @DomainEvent(TRIP_RESERVATION_FAILED)
19 | data class TripReservationFailedEvent (
20 | val tripId: UUID
21 | ) : Event(
22 | name = TRIP_RESERVATION_FAILED,
23 | )
24 |
25 | @DomainEvent(TRIP_RESERVATION_CONFIRMED)
26 | data class TripReservationConfirmedEvent (
27 | val tripId: UUID
28 | ) : Event(
29 | name = TRIP_RESERVATION_CONFIRMED,
30 | )
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/trips/config/TripBoundedContextConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.trips.config
2 |
3 | import org.springframework.beans.factory.annotation.Autowired
4 | import org.springframework.context.annotation.Bean
5 | import org.springframework.context.annotation.Configuration
6 | import ru.quipy.core.EventSourcingService
7 | import ru.quipy.core.EventSourcingServiceFactory
8 | import ru.quipy.sagaDemo.trips.api.TripAggregate
9 | import ru.quipy.sagaDemo.trips.logic.Trip
10 | import java.util.*
11 |
12 | @Configuration
13 | class TripBoundedContextConfig {
14 |
15 | @Autowired
16 | private lateinit var eventSourcingServiceFactory: EventSourcingServiceFactory
17 |
18 | @Bean
19 | fun tripEsService(): EventSourcingService =
20 | eventSourcingServiceFactory.create()
21 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/trips/controller/TripController.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.trips.controller
2 |
3 | import org.springframework.web.bind.annotation.*
4 | import ru.quipy.core.EventSourcingService
5 | import ru.quipy.saga.SagaManager
6 | import ru.quipy.sagaDemo.trips.api.TripAggregate
7 | import ru.quipy.sagaDemo.trips.api.TripReservationStartedEvent
8 | import ru.quipy.sagaDemo.trips.api.TripReservationFailedEvent
9 | import ru.quipy.sagaDemo.trips.logic.Trip
10 | import java.util.*
11 |
12 | @RestController
13 | class TripController(
14 | val tripEsService: EventSourcingService,
15 | val sagaManager: SagaManager,
16 | ) {
17 |
18 | @GetMapping
19 | fun reserveTrip() : TripReservationStartedEvent {
20 | val sagaContext = sagaManager
21 | .launchSaga("TRIP_RESERVATION", "start reservation")
22 | .sagaContext()
23 |
24 | return tripEsService.create(sagaContext) { it.startReservationTrip() }
25 | }
26 |
27 | @DeleteMapping("/{id}")
28 | fun cancelTrip(@PathVariable id: UUID) : TripReservationFailedEvent {
29 | return tripEsService.update(id) { it.cancelTrip(id) }
30 | }
31 |
32 | @GetMapping("/{id}")
33 | fun getAccount(@PathVariable id: UUID) : Trip? {
34 | return tripEsService.getState(id)
35 | }
36 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/trips/logic/Trip.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.trips.logic
2 |
3 | import ru.quipy.core.annotations.StateTransitionFunc
4 | import ru.quipy.domain.AggregateState
5 | import ru.quipy.sagaDemo.trips.api.TripAggregate
6 | import ru.quipy.sagaDemo.trips.api.TripReservationConfirmedEvent
7 | import ru.quipy.sagaDemo.trips.api.TripReservationStartedEvent
8 | import ru.quipy.sagaDemo.trips.api.TripReservationFailedEvent
9 | import java.util.*
10 |
11 | class Trip : AggregateState {
12 | private lateinit var tripId: UUID
13 | private var canceled: Boolean = false
14 | private var confirmed: Boolean = false
15 |
16 | override fun getId() = tripId
17 |
18 | fun startReservationTrip(id: UUID = UUID.randomUUID()) : TripReservationStartedEvent {
19 | return TripReservationStartedEvent(id)
20 | }
21 |
22 | fun cancelTrip(id: UUID) : TripReservationFailedEvent {
23 | return TripReservationFailedEvent(id)
24 | }
25 |
26 | fun confirmTrip(id: UUID) : TripReservationConfirmedEvent {
27 | return TripReservationConfirmedEvent(id)
28 | }
29 |
30 | @StateTransitionFunc
31 | fun startReservationTrip(event: TripReservationStartedEvent) {
32 | tripId = event.tripId
33 | }
34 |
35 | @StateTransitionFunc
36 | fun cancelTrip(event: TripReservationFailedEvent) {
37 | canceled = true
38 | }
39 |
40 | @StateTransitionFunc
41 | fun confirmTrip(event: TripReservationConfirmedEvent) {
42 | confirmed = true
43 | }
44 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/trips/subscribers/FlightSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.trips.subscribers
2 |
3 | import org.springframework.stereotype.Component
4 | import ru.quipy.core.EventSourcingService
5 | import ru.quipy.saga.SagaManager
6 | import ru.quipy.sagaDemo.flights.api.FlightAggregate
7 | import ru.quipy.sagaDemo.flights.api.FlightReservedEvent
8 | import ru.quipy.sagaDemo.trips.api.TripAggregate
9 | import ru.quipy.sagaDemo.trips.logic.Trip
10 | import ru.quipy.streams.AggregateSubscriptionsManager
11 | import java.util.*
12 | import javax.annotation.PostConstruct
13 |
14 | @Component
15 | class FlightSubscriber (
16 | private val subscriptionsManager: AggregateSubscriptionsManager,
17 | private val tripEsService: EventSourcingService,
18 | private val sagaManager: SagaManager
19 | ) {
20 |
21 | @PostConstruct
22 | fun init() {
23 | subscriptionsManager.createSubscriber(FlightAggregate::class, "trips::flights-subscriber") {
24 | `when`(FlightReservedEvent::class) { event ->
25 | val sagaContext = sagaManager
26 | .withContextGiven(event.sagaContext)
27 | .performSagaStep("TRIP_RESERVATION", "finish reservation")
28 | .performSagaStep("TRIP_RESERVATION2", "finish reservation2")
29 | .performSagaStep("TRIP_RESERVATION3", "finish reservation3")
30 | .sagaContext()
31 |
32 | tripEsService.update(event.flightReservationId, sagaContext) { it.confirmTrip(event.flightReservationId) }
33 | }
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/kotlin/ru.quipy/sagaDemo/trips/subscribers/PaymentTripSubscriber.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.sagaDemo.trips.subscribers
2 |
3 | import org.springframework.stereotype.Component
4 | import ru.quipy.core.EventSourcingService
5 | import ru.quipy.saga.SagaManager
6 | import ru.quipy.sagaDemo.payment.api.PaymentAggregate
7 | import ru.quipy.sagaDemo.payment.api.PaymentCanceledEvent
8 | import ru.quipy.sagaDemo.trips.api.TripAggregate
9 | import ru.quipy.sagaDemo.trips.logic.Trip
10 | import ru.quipy.streams.AggregateSubscriptionsManager
11 | import java.util.*
12 | import javax.annotation.PostConstruct
13 |
14 | @Component
15 | class PaymentTripSubscriber (
16 | private val subscriptionsManager: AggregateSubscriptionsManager,
17 | private val tripEsService: EventSourcingService,
18 | private val sagaManager: SagaManager
19 | ) {
20 |
21 | @PostConstruct
22 | fun init() {
23 | subscriptionsManager.createSubscriber(PaymentAggregate::class, "trips::payment-subscriber") {
24 | `when`(PaymentCanceledEvent::class) { event ->
25 | val sagaContext = sagaManager
26 | .withContextGiven(event.sagaContext)
27 | .performSagaStep("TRIP_RESERVATION", "payment failed").sagaContext()
28 |
29 | tripEsService.update(event.paymentId, sagaContext) { it.cancelTrip(event.paymentId) }
30 | }
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.data.mongodb.host=localhost
2 | spring.data.mongodb.port=27017
3 | spring.data.mongodb.database=tiny-es
4 |
5 | spring.main.allow-bean-definition-overriding=true
6 |
7 | management.endpoints.web.exposure.include=health,metrics,prometheus
8 | management.endpoint.health.probes.enabled=true
9 | management.metrics.export.defaults.enabled=false
10 | management.metrics.export.prometheus.enabled=true
11 | management.metrics.tags.application=${spring.application.name}
12 |
13 | event.sourcing.snapshot-frequency=100
14 | event.sourcing.auto-scan-enabled=true
15 | event.sourcing.scan-package=ru.quipy
16 |
17 | # pg events storage schema
18 | event.sourcing.db-schema=event_sourcing_store
19 |
20 | spring.datasource.hikari.idle-timeout=30000
21 | spring.datasource.hikari.maximum-pool-size=20
22 |
23 | # datasource configuration
24 | spring.datasource.hikari.jdbc-url=jdbc:postgresql://localhost:5432/tiny_es
25 | spring.datasource.hikari.username=tiny_es
26 | spring.datasource.hikari.password=tiny_es
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/test/kotlin/ru.quipy/BaseTest.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy
2 |
3 | import org.springframework.beans.factory.annotation.Autowired
4 | import org.springframework.beans.factory.annotation.Value
5 | import org.springframework.data.mongodb.core.MongoTemplate
6 | import org.springframework.data.mongodb.core.query.Criteria
7 | import org.springframework.data.mongodb.core.query.Query
8 | import ru.quipy.tables.EventRecordTable
9 | import ru.quipy.tables.EventStreamActiveReadersTable
10 | import ru.quipy.tables.EventStreamReadIndexTable
11 | import ru.quipy.tables.SnapshotTable
12 |
13 | open class BaseTest(private val testId: String) {
14 | @Autowired
15 | lateinit var mongoTemplate: MongoTemplate
16 |
17 | @Value("\${event.sourcing.db-schema:event_sourcing_store}")
18 | private lateinit var schema: String
19 |
20 | @Autowired
21 | private lateinit var databaseConnectionFactory: ru.quipy.db.factory.ConnectionFactory
22 | open fun cleanDatabase() {
23 | mongoTemplate.remove(Query.query(Criteria.where("aggregateId").`is`(testId)), "aggregate-project")
24 | mongoTemplate.remove(Query.query(Criteria.where("_id").`is`(testId)), "snapshots")
25 |
26 | databaseConnectionFactory.getDatabaseConnection().use { connection -> connection.createStatement().execute(
27 | "truncate ${schema}.${EventRecordTable.name};" +
28 | "truncate ${schema}.${SnapshotTable.name};" +
29 | "truncate ${schema}.${EventStreamReadIndexTable.name};" +
30 | "truncate ${schema}.${EventStreamActiveReadersTable.name};")
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/test/kotlin/ru.quipy/StreamEventOrderingTest.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy
2 |
3 | import org.awaitility.kotlin.await
4 | import org.junit.jupiter.api.BeforeEach
5 | import org.junit.jupiter.api.Test
6 | import org.springframework.beans.factory.annotation.Autowired
7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration
8 | import org.springframework.boot.test.context.SpringBootTest
9 | import org.springframework.test.annotation.DirtiesContext
10 | import org.springframework.test.context.ActiveProfiles
11 | import org.springframework.test.context.ContextConfiguration
12 | import ru.quipy.config.DockerPostgresDataSourceInitializer
13 | import ru.quipy.core.EventSourcingProperties
14 | import ru.quipy.core.EventSourcingService
15 | import ru.quipy.projectDemo.api.ProjectAggregate
16 | import ru.quipy.projectDemo.api.TagCreatedEvent
17 | import ru.quipy.projectDemo.create
18 | import ru.quipy.projectDemo.createTag
19 | import ru.quipy.projectDemo.logic.ProjectAggregateState
20 | import ru.quipy.streams.AggregateSubscriptionsManager
21 | import java.util.concurrent.TimeUnit
22 |
23 | @SpringBootTest
24 | @ActiveProfiles("test")
25 | @ContextConfiguration(
26 | initializers = [DockerPostgresDataSourceInitializer::class])
27 | @EnableAutoConfiguration
28 | @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
29 | class StreamEventOrderingTest: BaseTest(testId) {
30 | companion object {
31 | const val testId = "StreamEventOrderingTest"
32 | }
33 | private val properties: EventSourcingProperties = EventSourcingProperties(
34 | streamBatchSize = 3
35 | )
36 | @Autowired
37 | private lateinit var esService: EventSourcingService
38 |
39 | @Autowired
40 | private lateinit var subscriptionsManager: AggregateSubscriptionsManager
41 |
42 | private val sb = StringBuilder()
43 |
44 | @BeforeEach
45 | fun init() {
46 | cleanDatabase()
47 | }
48 |
49 | @Test
50 | fun testEventOrder() {
51 | esService.create {
52 | it.create(testId)
53 | }
54 |
55 | esService.update(testId) {
56 | it.createTag("1")
57 | }
58 | esService.update(testId) {
59 | it.createTag("2")
60 | }
61 | esService.update(testId) {
62 | it.createTag("3")
63 | }
64 | esService.update(testId) {
65 | it.createTag("4")
66 | }
67 | esService.update(testId) {
68 | it.createTag("5")
69 | }
70 | esService.update(testId) {
71 | it.createTag("6")
72 | }
73 |
74 | subscriptionsManager.createSubscriber(ProjectAggregate::class, "StreamEventOrderingTest") {
75 | `when`(TagCreatedEvent::class) { event ->
76 | sb.append(event.tagName).also {
77 | println(sb.toString())
78 | }
79 | }
80 | }
81 |
82 | await.atMost(10, TimeUnit.MINUTES).until {
83 | sb.toString() == "123456"
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/test/kotlin/ru.quipy/config/DockerPostgresDataSourceInitialize.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.config
2 |
3 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
4 | import org.springframework.context.ApplicationContextInitializer
5 | import org.springframework.context.ConfigurableApplicationContext
6 | import org.springframework.test.context.support.TestPropertySourceUtils
7 | import org.testcontainers.containers.PostgreSQLContainer
8 | import org.testcontainers.utility.DockerImageName
9 |
10 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
11 | class DockerPostgresDataSourceInitializer : ApplicationContextInitializer {
12 | private val postgresContainer = PostgreSQLContainer(DockerImageName.parse("postgres:14.9-alpine")).apply {
13 | withDatabaseName("tiny_es")
14 | withUsername("tiny_es")
15 | withPassword("tiny_es")
16 | }
17 |
18 | override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) {
19 | postgresContainer.start()
20 |
21 | TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
22 | configurableApplicationContext,
23 | "spring.datasource.hikari.jdbc-url=" + postgresContainer.jdbcUrl,
24 | "spring.datasource.hikari.username=" + postgresContainer.username,
25 | "spring.datasource.hikari.password=" + postgresContainer.password
26 | )
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-app/src/test/resources/application-test.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | data:
3 | mongodb:
4 | host: localhost
5 | port: 27019
6 | database: tiny-es-test
7 | mongodb:
8 | embedded:
9 | version: 4.0.12
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-boot-starter/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 |
8 | ru.quipy
9 | tiny-event-sourcing
10 | 2.7.4
11 |
12 |
13 | tiny-event-sourcing-spring-boot-starter
14 | Tiny event sourcing spring boot starter
15 | tiny-event-sourcing-spring-boot-starter
16 | 2.7.4
17 |
18 |
19 | 17
20 | 17
21 | UTF-8
22 | 1.6.10
23 |
24 |
25 |
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-dependencies
30 | 2.7.5
31 | pom
32 | import
33 |
34 |
35 |
36 |
37 |
38 |
39 | tiny-event-sourcing-lib
40 | ru.quipy
41 | ${project.version}
42 |
43 |
44 |
45 | org.springframework.boot
46 | spring-boot-starter
47 | 2.7.5
48 |
49 |
50 |
51 |
52 | src/main/kotlin
53 | ${project.basedir}/src/test/kotlin
54 |
55 |
56 | org.springframework.boot
57 | spring-boot-maven-plugin
58 |
59 |
60 | org.jetbrains.kotlin
61 | kotlin-maven-plugin
62 | ${kotlin.verion}
63 |
64 |
65 | compile
66 | compile
67 |
68 | compile
69 |
70 |
71 |
72 |
73 |
74 | -Xjsr305=strict
75 |
76 |
77 | spring
78 |
79 |
80 |
81 |
82 | org.jetbrains.kotlin
83 | kotlin-maven-allopen
84 | ${kotlin.verion}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/tiny-event-sourcing-spring-boot-starter/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2 | ru.quipy.config.EventSourcingLibConfig
--------------------------------------------------------------------------------
/tiny-mongo-event-store-spring-boot-starter/src/main/kotlin/ru/quipy/autoconfigure/MongoEventStoreAutoConfiguration.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.autoconfigure
2 |
3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
5 | import org.springframework.context.annotation.Bean
6 | import org.springframework.context.annotation.Configuration
7 | import org.springframework.data.mongodb.MongoDatabaseFactory
8 | import org.springframework.data.mongodb.MongoTransactionManager
9 | import org.springframework.data.mongodb.core.MongoTemplate
10 | import ru.quipy.MongoTemplateEventStore
11 | import ru.quipy.database.EventStore
12 | import ru.quipy.eventstore.MongoClientEventStore
13 | import ru.quipy.eventstore.converter.JacksonMongoEntityConverter
14 | import ru.quipy.eventstore.factory.MongoClientFactory
15 |
16 | @Configuration
17 | class MongoEventStoreAutoConfiguration {
18 | @Bean("mongoTemplateEventStore")
19 | @ConditionalOnBean(MongoTemplate::class)
20 | @ConditionalOnMissingBean
21 | fun mongoTemplateEventStore(): EventStore = MongoTemplateEventStore()
22 |
23 | @Bean("mongoClientEventStore")
24 | @ConditionalOnBean(MongoClientFactory::class)
25 | @ConditionalOnMissingBean
26 | fun mongoClientEventStore(databaseFactory: MongoClientFactory): EventStore {
27 | return MongoClientEventStore(JacksonMongoEntityConverter(), databaseFactory)
28 | }
29 |
30 | @Bean
31 | @ConditionalOnBean(MongoTransactionManager::class)
32 | @ConditionalOnMissingBean
33 | fun transactionManager(dbFactory: MongoDatabaseFactory): MongoTransactionManager {
34 | return MongoTransactionManager(dbFactory)
35 | }
36 | }
--------------------------------------------------------------------------------
/tiny-mongo-event-store-spring-boot-starter/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2 | ru.quipy.autoconfigure.MongoEventStoreAutoConfiguration
3 | spring.autoconfigure.exclude=\
4 | org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, \
5 | org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
--------------------------------------------------------------------------------
/tiny-mongo-event-store-spring-boot-starter/src/test/kotlin/ru/quipy/updateSerial/TestAggregate.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.updateSerial
2 |
3 | import ru.quipy.core.annotations.AggregateType
4 | import ru.quipy.core.annotations.DomainEvent
5 | import ru.quipy.core.annotations.StateTransitionFunc
6 | import ru.quipy.domain.Aggregate
7 | import ru.quipy.domain.AggregateState
8 | import ru.quipy.domain.Event
9 | import ru.quipy.updateSerial.UpdateSerialTest.Companion.CREATED_EVENT_NAME
10 | import ru.quipy.updateSerial.UpdateSerialTest.Companion.TEST_EVENT_NAME_1
11 | import ru.quipy.updateSerial.UpdateSerialTest.Companion.TEST_EVENT_NAME_2
12 | import ru.quipy.updateSerial.UpdateSerialTest.Companion.TEST_TABLE_NAME
13 | import java.util.*
14 |
15 | @AggregateType(aggregateEventsTableName = TEST_TABLE_NAME)
16 | class TestAggregate : Aggregate
17 |
18 | class TestAggregateState : AggregateState {
19 | private lateinit var id: UUID
20 | private var order: Int = 0
21 |
22 | override fun getId() = id
23 |
24 | fun create(id: UUID) = TestCreatedEvent(id)
25 |
26 | @StateTransitionFunc
27 | fun create(event: TestCreatedEvent) {
28 | id = event.testId
29 | }
30 |
31 | /**
32 | * This command generates the list of different type of events like it can be in a real world
33 | */
34 | fun testUpdateSerial(size: Int) = List(size) {index ->
35 | when (index % 2) {
36 | 0 -> TestEvent_1(order + index + 1)
37 | else -> TestEvent_2(order + index + 1)
38 | }
39 | }
40 |
41 | @StateTransitionFunc
42 | fun testUpdateSerial(event: TestEvent_1) {
43 | order = event.order
44 | }
45 |
46 | @StateTransitionFunc
47 | fun testUpdateSerial(event: TestEvent_2) {
48 | order = event.order
49 | }
50 | }
51 |
52 | @DomainEvent(name = CREATED_EVENT_NAME)
53 | class TestCreatedEvent(
54 | val testId: UUID,
55 | ) : Event(
56 | name = CREATED_EVENT_NAME,
57 | createdAt = System.currentTimeMillis(),
58 | )
59 |
60 | @DomainEvent(name = TEST_EVENT_NAME_1)
61 | class TestEvent_1(
62 | val order: Int
63 | ) : Event(
64 | name = TEST_EVENT_NAME_1,
65 | createdAt = System.currentTimeMillis(),
66 | )
67 |
68 | @DomainEvent(name = TEST_EVENT_NAME_2)
69 | class TestEvent_2(
70 | val order: Int
71 | ) : Event(
72 | name = TEST_EVENT_NAME_2,
73 | createdAt = System.currentTimeMillis(),
74 | )
--------------------------------------------------------------------------------
/tiny-mongo-event-store-spring-boot-starter/src/test/kotlin/ru/quipy/updateSerial/UpdateSerialTestConfiguration.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.updateSerial
2 |
3 | import com.mongodb.client.MongoClient
4 | import org.springframework.beans.factory.annotation.Qualifier
5 | import org.springframework.beans.factory.annotation.Value
6 | import org.springframework.context.annotation.Bean
7 | import org.springframework.context.annotation.Configuration
8 | import org.springframework.context.annotation.Primary
9 | import org.springframework.test.context.ActiveProfiles
10 | import ru.quipy.MongoTemplateEventStore
11 | import ru.quipy.core.AggregateRegistry
12 | import ru.quipy.core.EventSourcingProperties
13 | import ru.quipy.core.EventSourcingService
14 | import ru.quipy.core.EventSourcingServiceFactory
15 | import ru.quipy.database.EventStore
16 | import ru.quipy.eventstore.MongoClientEventStore
17 | import ru.quipy.eventstore.converter.JacksonMongoEntityConverter
18 | import ru.quipy.eventstore.factory.MongoClientFactoryImpl
19 | import ru.quipy.mapper.JsonEventMapper
20 | import java.util.*
21 |
22 | @Configuration
23 | @ActiveProfiles("test")
24 | class UpdateSerialTestConfiguration {
25 | @Bean
26 | fun service(
27 | eventSourcingServiceFactory: EventSourcingServiceFactory
28 | ): EventSourcingService = eventSourcingServiceFactory.create()
29 |
30 | @Bean
31 | @Primary
32 | fun mongoTemplateEventStore(): EventStore = MongoTemplateEventStore()
33 |
34 | @Value("\${spring.data.mongodb.database}")
35 | private lateinit var databaseName: String
36 |
37 | @Bean
38 | fun mongoClientEventStore(mongoClient: MongoClient): EventStore = MongoClientEventStore(
39 | JacksonMongoEntityConverter(),
40 | MongoClientFactoryImpl(mongoClient, databaseName)
41 | )
42 |
43 | @Bean
44 | @Qualifier("service-with-mongo-template")
45 | fun serviceWithMongoTemplateEventStore(
46 | eventSourcingProperties: EventSourcingProperties,
47 | aggregateRegistry: AggregateRegistry,
48 | eventMapper: JsonEventMapper,
49 | ): EventSourcingService {
50 | return EventSourcingServiceFactory(
51 | aggregateRegistry, eventMapper, mongoTemplateEventStore(), eventSourcingProperties
52 | ).create()
53 | }
54 |
55 | @Bean
56 | @Qualifier("service-with-mongo-client")
57 | fun serviceWithMongoClientEventStore(
58 | eventSourcingProperties: EventSourcingProperties,
59 | aggregateRegistry: AggregateRegistry,
60 | eventMapper: JsonEventMapper,
61 | mongoClient: MongoClient
62 | ): EventSourcingService {
63 | return EventSourcingServiceFactory(
64 | aggregateRegistry, eventMapper, mongoClientEventStore(mongoClient), eventSourcingProperties
65 | ).create()
66 | }
67 |
68 | }
--------------------------------------------------------------------------------
/tiny-mongo-event-store-spring-boot-starter/src/test/resources/application-test.yml:
--------------------------------------------------------------------------------
1 | event:
2 | sourcing:
3 | auto-scan-enabled: true
4 | scan-package: ru.quipy.updateSerial
5 | spin-lock-max-attempts: 11000
6 | snapshot-frequency: 10000
7 | spring:
8 | data:
9 | mongodb:
10 | host: localhost
11 | port: 27018
12 | database: tiny-es-test
13 | mongodb:
14 | embedded:
15 | version: 4.0.12
--------------------------------------------------------------------------------
/tiny-mongo-event-store/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 | jar
7 |
8 |
9 | ru.quipy
10 | tiny-event-sourcing
11 | 2.7.4
12 |
13 |
14 | tiny-mongo-event-store
15 | Tiny event sourcing mongodb event-store operations
16 | tiny-mongo-event-store
17 |
18 |
19 | 11
20 | 2.6.6
21 | 1.6.21
22 | 1.5.2
23 | 4.2.0
24 | true
25 |
26 |
27 |
28 |
29 | tiny-event-sourcing-lib
30 | ru.quipy
31 | ${project.version}
32 |
33 |
34 |
35 | org.mongodb
36 | mongodb-driver-sync
37 | 4.7.1
38 |
39 |
40 |
41 |
42 | src/main/kotlin
43 | ${project.basedir}/src/test/kotlin
44 |
45 |
46 | org.jetbrains.kotlin
47 | kotlin-maven-plugin
48 | ${kotlin.version}
49 |
50 |
51 | compile
52 | compile
53 |
54 | compile
55 |
56 |
57 |
58 |
59 |
60 | -Xjsr305=strict
61 |
62 |
63 | spring
64 |
65 |
66 |
67 |
68 | org.jetbrains.kotlin
69 | kotlin-maven-allopen
70 | ${kotlin.version}
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/tiny-mongo-event-store/src/main/kotlin/ru/quipy/eventstore/converter/BsonConverter.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.eventstore.converter
2 |
3 | interface BsonConverter {
4 | fun convertToBsonType(value: Any): T?
5 | fun convertFromBsonType(value: Any): V?
6 | }
--------------------------------------------------------------------------------
/tiny-mongo-event-store/src/main/kotlin/ru/quipy/eventstore/converter/JacksonMongoEntityConverter.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.eventstore.converter
2 |
3 | import com.fasterxml.jackson.annotation.JsonAutoDetect
4 | import com.fasterxml.jackson.annotation.JsonTypeInfo
5 | import com.fasterxml.jackson.annotation.PropertyAccessor
6 | import com.fasterxml.jackson.databind.MapperFeature
7 | import com.fasterxml.jackson.databind.ObjectMapper
8 | import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator
9 | import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator
10 | import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder
11 | import com.fasterxml.jackson.module.kotlin.KotlinModule
12 | import org.bson.Document
13 | import kotlin.reflect.KClass
14 |
15 |
16 | private const val TYPE_PROPERTY_KEY = "_class"
17 | private const val BASE_TYPE_PREFIX = "java"
18 | private const val EXPECTED_ID_KEY = "id"
19 | private const val TARGET_ID_KEY = "_id"
20 |
21 | class JacksonMongoEntityConverter : MongoEntityConverter {
22 |
23 | private val objectMapper: ObjectMapper = initMapper()
24 |
25 | private fun initMapper(): ObjectMapper {
26 | val mapper = ObjectMapper()
27 | mapper.configure(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS, true)
28 | mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
29 |
30 | val polymorphicTypeValidator: PolymorphicTypeValidator = BasicPolymorphicTypeValidator.builder()
31 | .allowIfBaseType(BASE_TYPE_PREFIX)
32 | .build()
33 | val typeResolver: TypeResolverBuilder<*> = ObjectMapper.DefaultTypeResolverBuilder(
34 | ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT,
35 | polymorphicTypeValidator
36 | )
37 | typeResolver.init(JsonTypeInfo.Id.CLASS, null)
38 | typeResolver.inclusion(JsonTypeInfo.As.PROPERTY)
39 | typeResolver.typeProperty(TYPE_PROPERTY_KEY)
40 |
41 | mapper.setDefaultTyping(typeResolver)
42 | mapper.registerModule(KotlinModule())
43 |
44 | return mapper
45 | }
46 |
47 | private val converters: List> = listOf(
48 | UuidConverter()
49 | )
50 |
51 | override fun convertObjectToBsonDocument(obj: T): Document {
52 | val document = Document.parse(objectMapper.writeValueAsString(obj))
53 | if (document.containsKey(EXPECTED_ID_KEY)) {
54 | document[TARGET_ID_KEY] = document.remove(EXPECTED_ID_KEY)
55 | }
56 | documentBypass(document) { converter, value ->
57 | converter.convertToBsonType(value)
58 | }
59 |
60 | return document
61 | }
62 |
63 | override fun convertBsonDocumentToObject(document: Document, clazz: KClass): T {
64 | if (document.containsKey(TARGET_ID_KEY)) {
65 | document[EXPECTED_ID_KEY] = document.remove(TARGET_ID_KEY)
66 | }
67 | documentBypass(document) { converter, value ->
68 | converter.convertFromBsonType(value)
69 | }
70 | return objectMapper.readValue(document.toJson(), clazz.java)
71 | }
72 |
73 | private fun documentBypass(document: Document, handler: (converter: BsonConverter<*, *>, value: Any) -> Any?) {
74 | document.forEach {
75 | if (it.value is Document) {
76 | documentBypass(it.value as Document, handler)
77 | } else {
78 | for (converter in converters) {
79 | val newValue = handler(converter, it.value)
80 | if (newValue != null) {
81 | document[it.key] = newValue
82 | break
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 | }
--------------------------------------------------------------------------------
/tiny-mongo-event-store/src/main/kotlin/ru/quipy/eventstore/converter/MongoEntityConverter.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.eventstore.converter
2 |
3 | import org.bson.Document
4 | import kotlin.reflect.KClass
5 |
6 | interface MongoEntityConverter {
7 | fun convertObjectToBsonDocument(obj: T): Document
8 | fun convertBsonDocumentToObject(document: Document, clazz: KClass): T
9 | }
--------------------------------------------------------------------------------
/tiny-mongo-event-store/src/main/kotlin/ru/quipy/eventstore/converter/UuidConverter.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.eventstore.converter
2 |
3 | import java.util.UUID
4 |
5 | class UuidConverter : BsonConverter> {
6 |
7 | override fun convertFromBsonType(value: Any): List? {
8 | if (value !is UUID) return null
9 | return listOf(
10 | UUID::class.qualifiedName,
11 | value.toString()
12 | )
13 | }
14 |
15 | override fun convertToBsonType(value: Any): UUID? {
16 | if (value !is List<*>
17 | || value.size != 2
18 | || value[1] !is String
19 | ) return null
20 | if (UUID::class.qualifiedName != value[0]) return null
21 | return UUID.fromString(value[1] as String)
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/tiny-mongo-event-store/src/main/kotlin/ru/quipy/eventstore/exception/MongoClientException.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.eventstore.exception
2 |
3 | open class MongoClientException(
4 | override val message: String?,
5 | override val cause: Throwable?
6 | ) : RuntimeException(
7 | message, cause
8 | )
9 |
--------------------------------------------------------------------------------
/tiny-mongo-event-store/src/main/kotlin/ru/quipy/eventstore/exception/MongoClientExceptionTranslator.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.eventstore.exception
2 |
3 | import com.mongodb.*
4 |
5 | class MongoClientExceptionTranslator {
6 |
7 | private fun translateException(ex: Exception): MongoClientException? {
8 | when (ex) {
9 | is DuplicateKeyException -> {
10 | return MongoDuplicateKeyException(ex.message, ex)
11 | }
12 | is MongoWriteException -> {
13 | if (ErrorCategory.fromErrorCode(ex.code) == ErrorCategory.DUPLICATE_KEY) {
14 | return MongoDuplicateKeyException(ex.message, ex)
15 | }
16 | }
17 | is MongoBulkWriteException -> {
18 | for (x in ex.writeErrors) {
19 | if (x.code == 11000) {
20 | return MongoDuplicateKeyException(ex.message, ex)
21 | }
22 | }
23 | }
24 | is MongoServerException -> {
25 | if (ex.code == 11000) {
26 | return MongoDuplicateKeyException(ex.message, ex)
27 | }
28 | }
29 | }
30 | return null
31 | }
32 |
33 | fun withTranslation(action: () -> T): T {
34 | try {
35 | return action()
36 | } catch (ex: Exception) {
37 | throw this.translateException(ex) ?: ex
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/tiny-mongo-event-store/src/main/kotlin/ru/quipy/eventstore/exception/MongoDuplicateKeyException.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.eventstore.exception
2 |
3 | open class MongoDuplicateKeyException(
4 | override val message: String?,
5 | override val cause: Throwable?
6 | ) : MongoClientException(
7 | message, cause
8 | ) {
9 | constructor(cause: Throwable?) : this(null, cause)
10 | }
--------------------------------------------------------------------------------
/tiny-mongo-event-store/src/main/kotlin/ru/quipy/eventstore/factory/MongoClientFactory.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.eventstore.factory
2 |
3 | import com.mongodb.client.MongoClient
4 | import com.mongodb.client.MongoDatabase
5 |
6 | interface MongoClientFactory {
7 | fun getDatabase(): MongoDatabase
8 | fun getClient(): MongoClient
9 | }
--------------------------------------------------------------------------------
/tiny-mongo-event-store/src/main/kotlin/ru/quipy/eventstore/factory/MongoClientFactoryImpl.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.eventstore.factory
2 |
3 | import com.mongodb.ConnectionString
4 | import com.mongodb.MongoClientSettings
5 | import com.mongodb.client.MongoClient
6 | import com.mongodb.client.MongoClients
7 | import com.mongodb.client.MongoDatabase
8 | import org.bson.UuidRepresentation
9 |
10 | class MongoClientFactoryImpl : MongoClientFactory {
11 | private val client: MongoClient
12 | private val databaseName: String
13 |
14 | constructor(databaseName: String) : this("mongodb://localhost", databaseName)
15 |
16 | constructor(connectionString: String, databaseName: String)
17 | : this(ConnectionString(connectionString), databaseName)
18 |
19 | constructor(connectionString: ConnectionString, databaseName: String) {
20 | this.client = MongoClients.create(
21 | MongoClientSettings.builder()
22 | .uuidRepresentation(UuidRepresentation.JAVA_LEGACY)
23 | .applyConnectionString(connectionString)
24 | .build()
25 | )
26 | this.databaseName = databaseName
27 | }
28 |
29 | constructor(mongoClient: MongoClient, databaseName: String) {
30 | this.client = mongoClient
31 | this.databaseName = databaseName
32 | }
33 |
34 | override fun getDatabase(): MongoDatabase {
35 | return client.getDatabase(databaseName)
36 | }
37 |
38 |
39 | override fun getClient(): MongoClient {
40 | return this.client
41 | }
42 | }
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/kotlin/ru/quipy/config/DatabaseConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.config
2 |
3 | import com.zaxxer.hikari.HikariConfig
4 | import com.zaxxer.hikari.HikariDataSource
5 | import org.springframework.beans.factory.annotation.Value
6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
7 | import org.springframework.context.annotation.Bean
8 | import org.springframework.context.annotation.Configuration
9 | import org.springframework.context.annotation.Primary
10 |
11 | @Configuration
12 | class DatabaseConfig {
13 | @Bean
14 | @Primary
15 | @ConditionalOnProperty("spring.datasource.hikari.jdbc-url")
16 | fun dataSource(@Value("\${spring.datasource.hikari.jdbc-url}") databaseUrl: String,
17 | @Value("\${spring.datasource.hikari.username:}") username: String,
18 | @Value("\${spring.datasource.hikari.password:}") password: String,
19 | @Value("\${spring.datasource.hikari.idleTimeout:30000}") idleTimeout: Long,
20 | @Value("\${spring.datasource.hikari.maximumPoolSize:20}") maxPoolSize: Int): HikariDataSource {
21 | val hikariConfig = HikariConfig()
22 | hikariConfig.maximumPoolSize = maxPoolSize
23 | hikariConfig.idleTimeout = idleTimeout
24 | hikariConfig.jdbcUrl = databaseUrl
25 | hikariConfig.username = username
26 | hikariConfig.password = password
27 |
28 | return HikariDataSource(hikariConfig)
29 | }
30 | }
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/kotlin/ru/quipy/config/LiquibaseSpringConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.config
2 |
3 | import liquibase.integration.spring.SpringLiquibase
4 | import org.springframework.beans.factory.annotation.Value
5 | import org.springframework.context.annotation.Bean
6 | import org.springframework.context.annotation.Configuration
7 | import org.springframework.context.annotation.Primary
8 | import java.sql.SQLException
9 | import javax.sql.DataSource
10 |
11 | @Configuration
12 | class LiquibaseSpringConfig {
13 |
14 | @Bean
15 | @Primary
16 | fun liquibase(dataSource: DataSource,
17 | @Value("\${event.sourcing.db-schema:event_sourcing_store}") schema: String): SpringLiquibase {
18 | try {
19 | dataSource.connection.use { connection ->
20 | connection.createStatement()
21 | .execute("CREATE SCHEMA IF NOT EXISTS $schema;")
22 | }
23 | } catch (e: SQLException) {
24 | throw RuntimeException(e)
25 | }
26 | val liquibase = SpringLiquibase()
27 | liquibase.resourceLoader = LiquibaseConfig().getResourceReader()
28 | liquibase.liquibaseSchema = schema
29 | liquibase.defaultSchema = schema
30 | liquibase.changeLog = "classpath:liquibase/changelog.sql"
31 | liquibase.dataSource = dataSource
32 | liquibase.setChangeLogParameters(
33 | mapOf(
34 | Pair("schema", schema)
35 | )
36 | )
37 | return liquibase
38 | }
39 | }
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/kotlin/ru/quipy/mappers/ActiveEventStreamReaderRowMapper.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.mappers
2 |
3 | import ru.quipy.converter.ResultSetToEntityMapper
4 | import org.springframework.jdbc.core.RowMapper
5 | import ru.quipy.domain.ActiveEventStreamReader
6 | import java.sql.ResultSet
7 |
8 | class ActiveEventStreamReaderRowMapper(private val entityMapper: ResultSetToEntityMapper) : RowMapper {
9 | override fun mapRow(rs: ResultSet, rowNum: Int): ActiveEventStreamReader? {
10 | return entityMapper.convert(rs, ActiveEventStreamReader::class, false)
11 | }
12 | }
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/kotlin/ru/quipy/mappers/EventRowMapper.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.mappers
2 |
3 | import ru.quipy.converter.ResultSetToEntityMapper
4 | import org.springframework.jdbc.core.RowMapper
5 | import ru.quipy.domain.EventRecord
6 | import java.sql.ResultSet
7 |
8 | class EventRowMapper(private val entityMapper: ResultSetToEntityMapper) : RowMapper {
9 | override fun mapRow(rs: ResultSet, rowNum: Int): EventRecord? {
10 | return entityMapper.convert(rs, EventRecord::class, false)
11 | }
12 | }
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/kotlin/ru/quipy/mappers/EventStreamReadIndexRowMapper.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.mappers
2 |
3 | import ru.quipy.converter.ResultSetToEntityMapper
4 | import org.springframework.jdbc.core.RowMapper
5 | import ru.quipy.domain.EventStreamReadIndex
6 | import java.sql.ResultSet
7 |
8 | class EventStreamReadIndexRowMapper(private val entityMapper: ResultSetToEntityMapper) : RowMapper {
9 | override fun mapRow(rs: ResultSet, rowNum: Int): EventStreamReadIndex? {
10 | return entityMapper.convert(rs, EventStreamReadIndex::class, false)
11 | }
12 | }
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/kotlin/ru/quipy/mappers/MapperFactory.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.mappers
2 |
3 | import org.springframework.jdbc.core.RowMapper
4 | import kotlin.reflect.KClass
5 |
6 | interface MapperFactory {
7 | fun getMapper(clazz: KClass): RowMapper
8 | }
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/kotlin/ru/quipy/mappers/MapperFactoryImpl.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.mappers
2 |
3 | import ru.quipy.converter.ResultSetToEntityMapper
4 | import org.springframework.jdbc.core.RowMapper
5 | import ru.quipy.domain.ActiveEventStreamReader
6 | import ru.quipy.domain.EventRecord
7 | import ru.quipy.domain.EventStreamReadIndex
8 | import ru.quipy.domain.Snapshot
9 | import kotlin.reflect.KClass
10 |
11 | class MapperFactoryImpl(private val entityMapper: ResultSetToEntityMapper) : MapperFactory {
12 | override fun getMapper(clazz: KClass): RowMapper {
13 | return when (clazz) {
14 | EventRecord::class -> EventRowMapper(entityMapper)
15 | Snapshot::class -> SnapshotRowMapper(entityMapper)
16 | EventStreamReadIndex::class -> EventStreamReadIndexRowMapper(entityMapper)
17 | ActiveEventStreamReader::class -> ActiveEventStreamReaderRowMapper(entityMapper)
18 | else -> throw IllegalStateException("No mapper defined for entity ${clazz.simpleName}")
19 | } as RowMapper
20 | }
21 | }
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/kotlin/ru/quipy/mappers/SnapshotRowMapper.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.mappers
2 |
3 | import ru.quipy.converter.ResultSetToEntityMapper
4 | import org.springframework.jdbc.core.RowMapper
5 | import ru.quipy.domain.Snapshot
6 | import java.sql.ResultSet
7 |
8 | class SnapshotRowMapper(private val entityMapper: ResultSetToEntityMapper) : RowMapper {
9 | override fun mapRow(rs: ResultSet, rowNum: Int): Snapshot? {
10 | return entityMapper.convert(rs, Snapshot::class, false)
11 | }
12 | }
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2 | ru.quipy.autoconfigure.PostgresEventStoreAutoConfiguration
3 | spring.autoconfigure.exclude=\
4 | org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.liquibase.change-log=classpath:liquibase/changelog.sql
2 |
3 | spring.datasource.hikari.driver-class-name=org.postgresql.Driver
4 | spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/resources/database/.env:
--------------------------------------------------------------------------------
1 | POSTGRES_DB=tiny_es
2 | POSTGRES_USER=tiny_es
3 | POSTGRES_PASSWORD=tiny_es
4 | POSTGRES_DB_PORT=5432
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/main/resources/database/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 | services:
3 | db:
4 | hostname: event_sourcing_db
5 | image: postgres:14.9-alpine
6 | env_file:
7 | - ./.env
8 | environment:
9 | POSTGRES_DB: ${POSTGRES_DB}
10 | POSTGRES_USER: ${POSTGRES_USER}
11 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
12 | ports:
13 | - ${POSTGRES_DB_PORT}:${POSTGRES_DB_PORT}
14 | restart: unless-stopped
15 |
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/test/kotlin/ru/quipy/config/TestDbConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.config
2 |
3 | import com.zaxxer.hikari.HikariConfig
4 | import com.zaxxer.hikari.HikariDataSource
5 | import org.springframework.beans.factory.annotation.Value
6 | import org.springframework.context.annotation.Bean
7 | import org.springframework.context.annotation.Configuration
8 | import org.testcontainers.containers.PostgreSQLContainer
9 | import org.testcontainers.utility.DockerImageName
10 |
11 | @Configuration
12 | class TestDbConfig {
13 | @Bean
14 | fun dataSource(
15 | @Value("\${jdbc.dbName:}") dbName: String,
16 | @Value("\${jdbc.username:}") username: String,
17 | @Value("\${jdbc.password:}") password: String,
18 | @Value("\${schema:event_sourcing_store}") schema: String)
19 | : HikariDataSource {
20 | val container = PostgreSQLContainer(DockerImageName.parse("postgres:14.9-alpine")).apply {
21 | withDatabaseName(dbName)
22 | withUsername(username)
23 | withPassword(password)
24 | }
25 | if (!container.isRunning) {
26 | container.start()
27 | }
28 | val hikariConfig = HikariConfig()
29 | hikariConfig.maximumPoolSize = 20
30 | hikariConfig.idleTimeout = 30000
31 | hikariConfig.jdbcUrl = container.jdbcUrl
32 | hikariConfig.username = username
33 | hikariConfig.password = password
34 |
35 | return HikariDataSource(hikariConfig)
36 | }
37 | }
--------------------------------------------------------------------------------
/tiny-postgres-event-store-spring-boot-starter/src/test/resources/application.properties:
--------------------------------------------------------------------------------
1 | jdbc.dbName=tiny_es
2 | jdbc.username=tiny_es
3 | jdbc.password=tiny_es
4 |
5 | event.sourcing.db-schema=event_sourcing_store
6 |
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/config/LiquibaseConfig.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.config
2 |
3 | import liquibase.Liquibase
4 | import liquibase.database.jvm.JdbcConnection
5 | import liquibase.resource.FileSystemResourceAccessor
6 | import org.springframework.core.io.FileSystemResourceLoader
7 | import java.io.File
8 | import java.sql.SQLException
9 | import javax.sql.DataSource
10 |
11 | class LiquibaseConfig {
12 | fun liquibase(dataSource: DataSource, schema: String): Liquibase {
13 | try {
14 | dataSource.connection.use { connection ->
15 | connection.createStatement()
16 | .execute("CREATE SCHEMA IF NOT EXISTS $schema;")
17 | }
18 | } catch (e: SQLException) {
19 | throw RuntimeException(e)
20 | }
21 | dataSource.connection.use { connection ->
22 | val databaseConnection = JdbcConnection(connection)
23 | val chageset = LiquibaseConfig::class.java.classLoader.getResource("liquibase").path
24 | val fileSystemResourceAccessor = FileSystemResourceAccessor(File(chageset))
25 | val liquibase = Liquibase("changelog.sql", fileSystemResourceAccessor, databaseConnection)
26 | liquibase.setChangeLogParameter("schema", schema)
27 | liquibase.update("")
28 | return liquibase
29 | }
30 | }
31 |
32 | fun getResourceReader() : FileSystemResourceLoader {
33 | return FileSystemResourceLoader()
34 | }
35 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/converter/EntityConverter.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.converter
2 |
3 | import kotlin.reflect.KClass
4 |
5 | interface EntityConverter {
6 | fun serialize(obj: T): String
7 | fun toObject(converted: String, clazz: KClass): T
8 | fun toNullableObject(converted: String, clazz: KClass): T?
9 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/converter/JsonEntityConverter.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.converter
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import kotlin.reflect.KClass
5 |
6 | class JsonEntityConverter(private val objectMapper: ObjectMapper) : EntityConverter {
7 | override fun serialize(obj: T): String {
8 | return objectMapper.writeValueAsString(obj)
9 | }
10 |
11 | override fun toObject(converted: String, clazz: KClass): T {
12 | return objectMapper.readValue(converted, clazz.java)
13 | }
14 |
15 | override fun toNullableObject(converted: String, clazz: KClass): T? {
16 | return objectMapper.readValue(converted, clazz.java)
17 | }
18 |
19 |
20 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/converter/ResultSetToEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.converter
2 |
3 | import java.sql.ResultSet
4 | import kotlin.reflect.KClass
5 |
6 | interface ResultSetToEntityMapper {
7 | fun convert(resultSet: ResultSet?, clazz: KClass, scroll: Boolean = true) : T?
8 | fun convertMany(resultSet: ResultSet?, clazz: KClass) : List
9 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/converter/ResultSetToEntityMapperImpl.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.converter
2 |
3 | import ru.quipy.converter.exception.NoMapperForClass
4 | import ru.quipy.domain.ActiveEventStreamReader
5 | import ru.quipy.domain.EventRecord
6 | import ru.quipy.domain.EventStreamReadIndex
7 | import ru.quipy.domain.Snapshot
8 | import ru.quipy.saga.SagaContext
9 | import ru.quipy.tables.EventRecordTable
10 | import ru.quipy.tables.EventStreamActiveReadersTable
11 | import ru.quipy.tables.EventStreamReadIndexTable
12 | import ru.quipy.tables.SnapshotTable
13 | import java.sql.ResultSet
14 | import kotlin.reflect.KClass
15 |
16 | @Suppress("UNCHECKED_CAST")
17 | class ResultSetToEntityMapperImpl(private val entityConverter: EntityConverter) : ResultSetToEntityMapper {
18 | override fun convert(resultSet: ResultSet?, clazz: KClass, scroll: Boolean) : T? {
19 | resultSet ?: return null
20 | if (scroll && !resultSet.next()) return null
21 | return when(clazz) {
22 | EventRecord::class -> mapToEventRecord(resultSet)
23 | Snapshot::class -> mapToSnapshot(resultSet)
24 | EventStreamReadIndex::class -> mapToEventStreamReader(resultSet)
25 | ActiveEventStreamReader::class -> mapToActiveEventStreamReader(resultSet)
26 | else -> throw NoMapperForClass(clazz.simpleName)
27 | } as T
28 | }
29 |
30 | override fun convertMany(resultSet: ResultSet?, clazz: KClass) : List {
31 | resultSet ?: return listOf()
32 | val result = mutableListOf()
33 | var res: T?;
34 | do {
35 | res = convert(resultSet, clazz)
36 | if (res != null) result.add(res)
37 | } while (res != null)
38 |
39 | return result
40 | }
41 | private fun mapToEventRecord(resultSet: ResultSet): EventRecord {
42 | return EventRecord(
43 | resultSet.getString(EventRecordTable.id.index),
44 | resultSet.getString(EventRecordTable.aggregateId.index),
45 | resultSet.getLong(EventRecordTable.aggregateVersion.index),
46 | resultSet.getString(EventRecordTable.eventTitle.index),
47 | resultSet.getString(EventRecordTable.payload.index),
48 | entityConverter.toNullableObject(resultSet.getString(EventRecordTable.sagaContext.index), SagaContext::class),
49 | resultSet.getLong(EventRecordTable.createdAt.index)
50 | )
51 | }
52 |
53 | private fun mapToSnapshot(resultSet: ResultSet) : Snapshot {
54 | return Snapshot(
55 | resultSet.getString(SnapshotTable.id.index),
56 | entityConverter.toObject(resultSet.getString(SnapshotTable.snapshot.index),
57 | Class.forName(resultSet.getString(SnapshotTable.aggregateStateClassName.index)).kotlin),
58 | resultSet.getLong(SnapshotTable.version.index)
59 | )
60 | }
61 |
62 | private fun mapToEventStreamReader(resultSet: ResultSet) : EventStreamReadIndex {
63 | return EventStreamReadIndex(
64 | resultSet.getString(EventStreamReadIndexTable.id.index),
65 | resultSet.getLong(EventStreamReadIndexTable.readIndex.index),
66 | resultSet.getLong(EventStreamReadIndexTable.version.index)
67 | )
68 | }
69 |
70 | private fun mapToActiveEventStreamReader(resultSet: ResultSet) : ActiveEventStreamReader {
71 | return ActiveEventStreamReader(
72 | resultSet.getString(EventStreamActiveReadersTable.id.index),
73 | resultSet.getLong(EventStreamActiveReadersTable.version.index),
74 | resultSet.getString(EventStreamActiveReadersTable.readerId.index),
75 | resultSet.getLong(EventStreamActiveReadersTable.readPosition.index),
76 | resultSet.getLong(EventStreamActiveReadersTable.lastInteraction.index)
77 | )
78 | }
79 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/converter/exception/NoMapperForClass.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.converter.exception
2 |
3 | class NoMapperForClass(message: String?): Exception(message)
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/db/DataSourceProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.db
2 |
3 | import javax.sql.DataSource
4 |
5 | interface DataSourceProvider {
6 | fun dataSource() : DataSource
7 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/db/DatasourceProviderImpl.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.db
2 |
3 | import javax.sql.DataSource
4 |
5 | class DatasourceProviderImpl(private val dataSource: DataSource) : DataSourceProvider {
6 | override fun dataSource(): DataSource {
7 | return dataSource
8 | }
9 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/db/HikariDatasourceProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.db
2 |
3 | import com.zaxxer.hikari.HikariDataSource
4 |
5 | class HikariDatasourceProvider(private val datasource: HikariDataSource) : DataSourceProvider {
6 | override fun dataSource(): HikariDataSource {
7 | return datasource
8 | }
9 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/db/factory/ConnectionFactory.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.db.factory
2 |
3 | import java.sql.Connection
4 |
5 | interface ConnectionFactory {
6 | fun getDatabaseConnection() : Connection
7 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/db/factory/DataSourceConnectionFactoryImpl.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.db.factory
2 |
3 | import ru.quipy.db.DataSourceProvider
4 | import java.sql.Connection
5 |
6 | class DataSourceConnectionFactoryImpl(private val dataSourceProvider: DataSourceProvider) : ConnectionFactory {
7 | override fun getDatabaseConnection(): Connection {
8 | return this.dataSourceProvider.dataSource().connection
9 | }
10 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/exception/UnknownEntityClassException.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.exception
2 |
3 | class UnknownEntityClassException(message: String?) : Exception(message)
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/executor/ExceptionLoggingSqlQueriesExecutor.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.executor
2 |
3 | import org.slf4j.Logger
4 | import ru.quipy.db.factory.ConnectionFactory
5 | import ru.quipy.query.BasicQuery
6 | import ru.quipy.query.Query
7 | import ru.quipy.query.insert.BatchInsertQuery
8 | import java.sql.ResultSet
9 | import java.sql.SQLException
10 |
11 | open class ExceptionLoggingSqlQueriesExecutor(
12 | private val connectionFactory: ConnectionFactory,
13 | private val logger: Logger) : QueryExecutor {
14 | override fun executeReturningBoolean(query: BasicQuery): Boolean {
15 | return try {
16 | executeDependingOnQueryType(query)
17 | true
18 | } catch (ex: SQLException) {
19 | logger.error(ex.message)
20 | false
21 | }
22 | }
23 |
24 | override fun execute(query: BasicQuery) {
25 | executeDependingOnQueryType(query)
26 | }
27 |
28 | override fun executeAndProcessResultSet(query: BasicQuery,
29 | processFunction: (ResultSet?) -> E?): E? {
30 | if (query is BatchInsertQuery) {
31 | throw UnsupportedOperationException("Cannot return result set executing batch insert")
32 | }
33 | return connectionFactory.getDatabaseConnection().use { connection ->
34 | processFunction(
35 | connection.prepareStatement(query.build()).executeQuery()
36 | )
37 | }
38 | }
39 |
40 | open fun executeDependingOnQueryType(query: BasicQuery) {
41 | when (query) {
42 | is BatchInsertQuery -> executeBatchInsert(query)
43 | else -> {
44 | connectionFactory.getDatabaseConnection().use { connection ->
45 | connection.prepareStatement(query.build())
46 | .execute()
47 | }
48 | }
49 | }
50 | }
51 |
52 | open fun executeBatchInsert(query: BatchInsertQuery) {
53 | var sqls = query.build().split("\n")
54 | connectionFactory.getDatabaseConnection().use { connection ->
55 | val prepared = connection.createStatement()
56 | for (sql in sqls) {
57 | prepared.addBatch(sql)
58 | }
59 | prepared.executeBatch()
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/executor/QueryExecutor.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.executor
2 |
3 | import ru.quipy.query.BasicQuery
4 | import ru.quipy.query.Query
5 | import java.sql.ResultSet
6 |
7 | interface QueryExecutor {
8 | fun execute(query: BasicQuery)
9 | fun executeAndProcessResultSet(query: BasicQuery, processFunction: (ResultSet?) -> E?): E?
10 | fun executeReturningBoolean(query: BasicQuery): Boolean
11 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/query/BasicQuery.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query
2 |
3 | import ru.quipy.query.exception.InvalidQueryStateException
4 | import java.sql.PreparedStatement
5 |
6 | abstract class BasicQuery(protected val schema: String, protected val relation: String) : Query {
7 | protected val columns: MutableList = mutableListOf()
8 | protected val values: MutableList = mutableListOf()
9 | protected val conditions = mutableListOf()
10 | protected var returnEntity: Boolean = false
11 | open fun withColumns(vararg columns: String): T {
12 | this.columns.clear()
13 | for (t in columns)
14 | this.columns.add(t)
15 | return this as T
16 | }
17 |
18 | open fun withValues(vararg values: Any): T {
19 | this.values.clear()
20 | for (t in values)
21 | this.values.add(t)
22 | return this as T
23 | }
24 |
25 | fun andWhere(condition: String) : T {
26 | conditions.add(condition)
27 | return this as T
28 | }
29 |
30 | fun returningEntity() : T {
31 | returnEntity = true
32 | return this as T
33 | }
34 |
35 | protected open fun validate() {
36 | if (columns.size != values.size)
37 | throw InvalidQueryStateException("Columns size doesn't match values size" +
38 | "\ncolumns[${columns.joinToString(", " )}]" +
39 | "\nvalues[${values.joinToString(", " )}]")
40 | }
41 |
42 | protected fun convertValueToString(value: Any) : String {
43 | return when (value) {
44 | is Long -> value.toString()
45 | is String -> "'$value'"
46 | else -> throw Exception("Unknown type")
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/query/Query.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query
2 |
3 | import java.sql.Connection
4 |
5 | interface Query {
6 | fun build() : String
7 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/query/delete/DeleteQuery.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query.delete
2 |
3 | import ru.quipy.query.BasicQuery
4 | import java.sql.Connection
5 |
6 | class DeleteQuery(schema:String, relation: String) : BasicQuery(schema, relation) {
7 | override fun build(): String {
8 | var sql = String.format(
9 | "delete from %s.%s where %s",
10 | schema,
11 | relation,
12 | conditions.joinToString { " and " }
13 | )
14 |
15 | if (returnEntity) {
16 | sql = "$sql returning *"
17 | }
18 | return sql
19 | }
20 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/query/exception/InvalidQueryStateException.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query.exception
2 |
3 | class InvalidQueryStateException(message: String) : Exception(message) {
4 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/query/exception/UnmappedDtoType.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query.exception
2 |
3 | class UnmappedDtoType(message: String?) : Exception(message)
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/query/insert/BatchInsertQuery.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query.insert
2 |
3 | import ru.quipy.query.BasicQuery
4 | import ru.quipy.query.exception.InvalidQueryStateException
5 |
6 | class BatchInsertQuery(schema: String, relation: String)
7 | : BasicQuery(schema, relation) {
8 | private val batches = ArrayList>()
9 | override fun withValues(vararg values: Any): BatchInsertQuery {
10 | val batch = ArrayList()
11 | for (value in values) {
12 | batch.add(value)
13 | }
14 | batches.add(batch)
15 | return this
16 | }
17 |
18 | fun getTemplate() : String {
19 | return String.format(
20 | "insert into %s.%s (%s) values (%s)",
21 | schema, relation,
22 | columns.joinToString(),
23 | columns.joinToString {"?"}
24 | )
25 | }
26 | override fun validate() {
27 | for (batch in batches) {
28 | if (batch.size != columns.size) {
29 | throw InvalidQueryStateException("Columns size doesn't match batch values size" +
30 | "\ncolumns[${columns.joinToString( )}]" +
31 | "\nvalues[${values.joinToString()}]")
32 | }
33 | }
34 | }
35 | override fun build(): String {
36 | validate()
37 | val resultQueries = mutableListOf()
38 | for (batch in batches) {
39 | super.withValues(values = batch.toTypedArray())
40 | resultQueries.add(String.format(
41 | "insert into %s.%s (%s) values (%s)",
42 | schema, relation,
43 | columns.joinToString(),
44 | values.joinToString {convertValueToString(it)}
45 | ))
46 | }
47 |
48 | return resultQueries.joinToString(";\n")
49 | }
50 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/query/insert/InsertQuery.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query.insert
2 |
3 | import ru.quipy.query.BasicQuery
4 | import java.sql.Connection
5 |
6 | open class InsertQuery(schema: String, relation: String) : BasicQuery(schema, relation) {
7 | override fun build(): String {
8 | validate()
9 | return String.format(
10 | "insert into %s.%s (%s) values (%s)",
11 | schema, relation,
12 | columns.joinToString(),
13 | values.joinToString { convertValueToString(it) }
14 | )
15 | }
16 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/query/insert/OnDuplicateKeyUpdateInsertQuery.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query.insert
2 |
3 | import ru.quipy.query.BasicQuery
4 | import ru.quipy.query.exception.InvalidQueryStateException
5 |
6 | class OnDuplicateKeyUpdateInsertQuery(schema: String, relation: String)
7 | : BasicQuery(schema, relation) {
8 | private val duplicateKeyUpdateColumns: MutableList = mutableListOf()
9 | private val conflictingColumns: MutableList = mutableListOf()
10 | fun onDuplicateKeyUpdateColumns(vararg columns: String) : OnDuplicateKeyUpdateInsertQuery {
11 | duplicateKeyUpdateColumns.addAll(columns)
12 | return this
13 | }
14 |
15 | fun withPossiblyConflictingColumns(vararg columns: String) : OnDuplicateKeyUpdateInsertQuery {
16 | conflictingColumns.addAll(columns)
17 | return this
18 | }
19 |
20 | override fun validate() {
21 | super.validate()
22 | val unknownColumns = duplicateKeyUpdateColumns.filter { !columns.contains(it) }
23 | if (unknownColumns.isNotEmpty()) {
24 | throw InvalidQueryStateException(
25 | "Unknown columns for updating on duplicated key: [${unknownColumns.joinToString { ", " }}]")
26 | }
27 | }
28 |
29 | override fun build(): String {
30 | validate()
31 | val duplicateColumnToValue = mutableMapOf()
32 | for (column in duplicateKeyUpdateColumns) {
33 | val value = values[columns.indexOf(column)]
34 | duplicateColumnToValue[column] = convertValueToString(value)
35 | }
36 | var sql = "insert into ${schema}.${relation} (${columns.joinToString()}) values (${values.joinToString {convertValueToString(it)}}) " +
37 | "on conflict (${conflictingColumns.joinToString()}) do update " +
38 | "set ${duplicateColumnToValue.entries.joinToString {"${it.key}=${it.value}"}}"
39 | if (conditions.isNotEmpty()) {
40 | sql += " where ${conditions.joinToString(" and ")}"
41 | }
42 | return sql
43 | }
44 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/query/select/SelectQuery.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query.select
2 |
3 | import ru.quipy.query.BasicQuery
4 | import ru.quipy.query.exception.InvalidQueryStateException
5 |
6 | class SelectQuery(schema: String, relation: String) : BasicQuery(schema, relation) {
7 | private var limit: Int = -1
8 | private var orderByColumn: String? = null
9 | private var sortingOrder: SortingOrder = SortingOrder.ASCENDING
10 | fun limit(limit: Int) : SelectQuery {
11 | if (limit > 0) {
12 | this.limit = limit
13 | }
14 | return this
15 | }
16 |
17 | override fun build(): String {
18 | // no need to validate query state
19 | var columnsStr = "*"
20 | if (columns.isNotEmpty()) {
21 | columnsStr = columns.joinToString()
22 | }
23 | var sql = String.format("select $columnsStr from ${schema}.${relation}")
24 | if (conditions.isNotEmpty()) {
25 | sql += " where ${conditions.joinToString( " and " )}"
26 | }
27 | if (orderByColumn != null) sql += " order by $orderByColumn ${sortingOrder.sqlName}"
28 | if (limit > 0) sql = "$sql limit $limit"
29 | return sql
30 | }
31 |
32 | override fun withValues(vararg values: Any): SelectQuery {
33 | throw UnsupportedOperationException()
34 | }
35 |
36 | fun orderBy(column: String, mode: SortingOrder = SortingOrder.ASCENDING): SelectQuery {
37 | orderByColumn = column
38 | sortingOrder = sortingOrder
39 | return this
40 | }
41 |
42 | override fun validate() {
43 | super.validate()
44 | if (orderByColumn != null && !columns.contains(orderByColumn)) {
45 | throw InvalidQueryStateException("Colunm $orderByColumn is not present in select columns " +
46 | "[${columns.joinToString(", " )}]")
47 | }
48 | }
49 |
50 | enum class SortingOrder(val sqlName: String) {
51 | ASCENDING("asc"),
52 | DESCENDING("desc");
53 | }
54 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/query/update/UpdateQuery.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query.update
2 |
3 | import ru.quipy.query.BasicQuery
4 | import java.sql.Connection
5 | import java.sql.ResultSet
6 |
7 | class UpdateQuery(schema: String, relation: String) : BasicQuery(schema, relation) {
8 | private var columnValueMap = mutableMapOf()
9 |
10 | fun set(column: String, value: Any) : UpdateQuery {
11 | columnValueMap[column] = value
12 | return this
13 | }
14 |
15 | override fun build(): String {
16 | validate()
17 | var sql = String.format(
18 | "update %s.%s set %s where %s",
19 | schema,
20 | relation,
21 | columnValueMap.map { "${it.key} = ${convertValueToString(it.value)}" }.joinToString(),
22 | conditions.joinToString(" and ")
23 | )
24 |
25 | if (returnEntity) {
26 | sql = "$sql returning *"
27 | }
28 | return sql
29 | }
30 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/tables/Column.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.tables
2 |
3 | import kotlin.reflect.KClass
4 |
5 | class Column {
6 | val index: Int
7 | val name: String
8 | val type: KClass
9 |
10 | constructor(index: Int, name: String, type: KClass) {
11 | this.index = index
12 | this.name = name
13 | this.type = type
14 | }
15 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/tables/Dto.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.tables
2 |
3 | import ru.quipy.converter.EntityConverter
4 | import ru.quipy.domain.ActiveEventStreamReader
5 | import ru.quipy.domain.EventRecord
6 | import ru.quipy.domain.EventStreamReadIndex
7 | import ru.quipy.domain.Snapshot
8 | import kotlin.reflect.jvm.jvmName
9 |
10 | interface Dto {
11 | fun values() : Array
12 | }
13 | class EventRecordDto(
14 | val id: String,
15 | val aggregateTableName: String,
16 | val aggregateId: String,
17 | val aggregateVersion: Long,
18 | val eventTitle: String,
19 | val payload: String,
20 | var sagaContext: String
21 | ) : Dto {
22 | constructor(eventRecord: EventRecord, aggregateTableName: String, entityConverter: EntityConverter)
23 | : this(
24 | eventRecord.id,
25 | aggregateTableName,
26 | eventRecord.aggregateId.toString(),
27 | eventRecord.aggregateVersion,
28 | eventRecord.eventTitle,
29 | eventRecord.payload,
30 | eventRecord.sagaContext?.let { entityConverter.serialize(it)} ?: "null"
31 | )
32 |
33 | override fun values(): Array {
34 | return arrayOf(
35 | id, aggregateTableName, aggregateId, aggregateVersion, eventTitle, payload, sagaContext
36 | )
37 | }
38 | }
39 |
40 | class SnapshotDto(
41 | val id : String,
42 | val snapshotTableName: String,
43 | val aggregateStateClassName: String,
44 | val snapshot : String,
45 | var version: Long
46 | ) : Dto {
47 | constructor(snapshot: Snapshot, snapshotTableName: String, entityConverter: EntityConverter)
48 | : this(snapshot.id.toString(),
49 | snapshotTableName,
50 | snapshot.snapshot::class.jvmName,
51 | entityConverter.serialize(snapshot.snapshot),
52 | snapshot.version)
53 |
54 | override fun values(): Array {
55 | return arrayOf(id, snapshotTableName, aggregateStateClassName, snapshot, version)
56 | }
57 | }
58 |
59 |
60 | class EventStreamReadIndexDto(
61 | val id: String,
62 | val readIndex: Long,
63 | var version: Long
64 | ) : Dto {
65 | constructor(eventStreamReadIndex: EventStreamReadIndex)
66 | : this(eventStreamReadIndex.id,
67 | eventStreamReadIndex.readIndex,
68 | eventStreamReadIndex.version)
69 |
70 | override fun values(): Array {
71 | return arrayOf(id, readIndex, version)
72 | }
73 | }
74 |
75 | class ActiveEventStreamReaderDto(
76 | val id: String,
77 | var version: Long,
78 | val readerId: String,
79 | val readPosition: Long,
80 | val lastInteraction: Long
81 | ) : Dto {
82 | constructor(activeEventStreamReader: ActiveEventStreamReader)
83 | : this(activeEventStreamReader.id,
84 | activeEventStreamReader.version,
85 | activeEventStreamReader.readerId,
86 | activeEventStreamReader.readPosition,
87 | activeEventStreamReader.lastInteraction)
88 |
89 | override fun values(): Array {
90 | return arrayOf(id, version, readerId, readPosition, lastInteraction)
91 | }
92 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/kotlin/ru/quipy/tables/DtoCreator.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.tables
2 |
3 | import ru.quipy.converter.EntityConverter
4 | import ru.quipy.domain.ActiveEventStreamReader
5 | import ru.quipy.domain.EventRecord
6 | import ru.quipy.domain.EventStreamReadIndex
7 | import ru.quipy.domain.Snapshot
8 |
9 | class DtoCreator {
10 | companion object {
11 | fun create(entity: E, tableName: String, entityConverter: EntityConverter) : Dto {
12 | return when (entity) {
13 | is EventRecord -> EventRecordDto(entity as EventRecord, tableName, entityConverter)
14 | is Snapshot -> SnapshotDto(entity as Snapshot, tableName, entityConverter)
15 | is EventStreamReadIndex -> EventStreamReadIndexDto(entity as EventStreamReadIndex)
16 | is ActiveEventStreamReader -> ActiveEventStreamReaderDto(entity as ActiveEventStreamReader)
17 | else -> throw IllegalStateException("No dto defined for entity ${entity!!::class.simpleName}")
18 | }
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/main/resources/liquibase/changelog.sql:
--------------------------------------------------------------------------------
1 | --liquibase formatted sql
2 | --changeset vekajp:es-001
3 |
4 | create sequence if not exists ${schema}.event_record_created_at_sequence;
5 |
6 | create table if not exists ${schema}.event_record
7 | (
8 | id text primary key,
9 | aggregate_table_name text,
10 | aggregate_id text,
11 | aggregate_version bigint,
12 | event_title text,
13 | payload text,
14 | saga_context text,
15 | created_at bigint default nextval('${schema}.event_record_created_at_sequence'),
16 | unique (aggregate_id, aggregate_version)
17 | );
18 |
19 | CREATE INDEX idx_created_at ON ${schema}.event_record(aggregate_table_name, created_at);
20 | CREATE INDEX idx_aggregate_id_version ON ${schema}.event_record(aggregate_id, aggregate_version);
21 |
22 | create table if not exists ${schema}.snapshot
23 | (
24 | id text primary key,
25 | snapshot_table_name text,
26 | aggregate_state_class_name text,
27 | snapshot text,
28 | version bigint
29 | );
30 |
31 | create table if not exists ${schema}.event_stream_read_index
32 | (
33 | id text primary key,
34 | read_index bigint,
35 | version bigint
36 | );
37 |
38 | create table if not exists ${schema}.event_stream_active_readers
39 | (
40 | id text primary key,
41 | version bigint,
42 | reader_id text,
43 | read_position bigint,
44 | last_interaction bigint
45 | );
46 |
47 | CREATE OR REPLACE FUNCTION ${schema}.execute_batch(
48 | p_commands TEXT[]
49 | ) RETURNS INT[] LANGUAGE plpgsql AS '
50 | DECLARE
51 | p_results INT[] := ''{}'';
52 | i INT;
53 | BEGIN
54 | FOR i IN array_lower(p_commands, 1) .. array_upper(p_commands, 1) LOOP
55 | BEGIN
56 | EXECUTE p_commands[i];
57 | EXCEPTION
58 | WHEN OTHERS THEN
59 | p_results := array_append(p_results, i);
60 | END;
61 | END LOOP;
62 | RETURN p_results;
63 | END;';
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/test/kotlin/ru/quipy/query/InsertQueryTest.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query
2 |
3 | import ru.quipy.query.insert.InsertQuery
4 | import ru.quipy.query.insert.OnDuplicateKeyUpdateInsertQuery
5 | import org.junit.jupiter.api.Assertions
6 | import org.junit.jupiter.api.Test
7 |
8 | class InsertQueryTest {
9 | private val schema = "schema"
10 | private val relation = "relation"
11 | private val columns = arrayOf("a", "b", "c")
12 | private val onDuplicateKeyUpdateColumns = arrayOf("a", "b")
13 | private val values = arrayOf("a1", "b1", "c1")
14 | @Test
15 | fun testSimpleInsertQuery() {
16 | val query = InsertQuery(schema, relation)
17 | .withColumns(columns = columns)
18 | .withValues(values = values)
19 |
20 | Assertions.assertEquals(query.build(),
21 | "insert into schema.relation (a, b, c) values ('a1', 'b1', 'c1')"
22 | )
23 | }
24 |
25 | @Test
26 | fun testOnDuplicateKeyUpdateQuery() {
27 | val query = OnDuplicateKeyUpdateInsertQuery(schema, relation)
28 | .withColumns(columns = columns)
29 | .withValues(values = columns)
30 | .withPossiblyConflictingColumns("c")
31 | .onDuplicateKeyUpdateColumns(columns = onDuplicateKeyUpdateColumns)
32 |
33 | Assertions.assertEquals(query.build(),
34 | "insert into schema.relation (a, b, c) values ('a', 'b', 'c')" +
35 | " on conflict (c) do update set a='a', b='b'"
36 | )
37 | }
38 |
39 | // TODO append tests for every query and case
40 | }
--------------------------------------------------------------------------------
/tiny-postgresql-event-store/src/test/kotlin/ru/quipy/query/SelectQueryTest.kt:
--------------------------------------------------------------------------------
1 | package ru.quipy.query
2 |
3 | import ru.quipy.query.select.SelectQuery
4 | import org.junit.jupiter.api.Assertions
5 | import org.junit.jupiter.api.Test
6 |
7 | class SelectQueryTest {
8 | private val schema = "schema"
9 | private val relation = "relation"
10 | private val columns = arrayOf("a", "b", "c")
11 | @Test
12 | fun testSelectQuerySqlWithoutConditionsWithColumnsAndLimit() {
13 | val limit = 101;
14 | val query = SelectQuery(schema, relation)
15 | .withColumns(columns = columns)
16 | .limit(limit)
17 |
18 | Assertions.assertEquals(query.build(),
19 | String.format("select %s from %s.%s limit %d", columns.joinToString(), schema, relation, limit)
20 | )
21 | }
22 |
23 | @Test
24 | fun testSelectQuerySqlWithConditionsWithoutColumnsAndNoLimit() {
25 | val query = SelectQuery(schema, relation)
26 | .andWhere("a = b")
27 | .andWhere("b > c")
28 |
29 | Assertions.assertEquals(query.build(),
30 | String.format("select * from %s.%s where a = b and b > c", schema, relation)
31 | )
32 | }
33 | }
--------------------------------------------------------------------------------