|
5 | 5 | namespace Test\Ecotone\EventSourcing\Projecting\Global; |
6 | 6 |
|
7 | 7 | use Doctrine\DBAL\Connection; |
| 8 | +use Ecotone\EventSourcing\Attribute\FromAggregateStream; |
8 | 9 | use Ecotone\EventSourcing\Attribute\FromStream; |
9 | 10 | use Ecotone\EventSourcing\Attribute\ProjectionDelete; |
10 | 11 | use Ecotone\EventSourcing\Attribute\ProjectionInitialization; |
11 | 12 | use Ecotone\EventSourcing\Attribute\ProjectionReset; |
12 | 13 | use Ecotone\EventSourcing\EventSourcingConfiguration; |
13 | 14 | use Ecotone\Lite\EcotoneLite; |
14 | 15 | use Ecotone\Lite\Test\FlowTestSupport; |
| 16 | +use Ecotone\Messaging\Config\ConfigurationException; |
15 | 17 | use Ecotone\Messaging\Config\ModulePackageList; |
16 | 18 | use Ecotone\Messaging\Config\ServiceConfiguration; |
17 | 19 | use Ecotone\Modelling\Attribute\EventHandler; |
|
32 | 34 | use Test\Ecotone\EventSourcing\Fixture\Ticket\Event\TicketWasRegistered; |
33 | 35 | use Test\Ecotone\EventSourcing\Fixture\Ticket\Ticket; |
34 | 36 | use Test\Ecotone\EventSourcing\Fixture\Ticket\TicketEventConverter; |
| 37 | +use Test\Ecotone\EventSourcing\Projecting\App\Ordering\Command\PlaceOrder; |
| 38 | +use Test\Ecotone\EventSourcing\Projecting\App\Ordering\Event\OrderWasPlaced; |
| 39 | +use Test\Ecotone\EventSourcing\Projecting\App\Ordering\EventsConverter; |
| 40 | +use Test\Ecotone\EventSourcing\Projecting\App\Ordering\Order; |
35 | 41 | use Test\Ecotone\EventSourcing\Projecting\ProjectingTestCase; |
36 | 42 |
|
37 | 43 | /** |
@@ -188,6 +194,132 @@ classesToResolve: [$projection::class, Ticket::class, TicketEventConverter::clas |
188 | 194 | self::assertEquals([['ticket_id' => '124', 'ticket_type' => 'info']], $ecotone->sendQueryWithRouting('getInProgressTickets')); |
189 | 195 | } |
190 | 196 |
|
| 197 | + public function test_building_global_projection_with_aggregate_stream_attribute(): void |
| 198 | + { |
| 199 | + $projection = $this->createOrderListProjectionWithAggregateStream(); |
| 200 | + |
| 201 | + $ecotone = EcotoneLite::bootstrapFlowTestingWithEventStore( |
| 202 | + classesToResolve: [$projection::class, Order::class, EventsConverter::class], |
| 203 | + containerOrAvailableServices: [$projection, new EventsConverter(), self::getConnectionFactory()], |
| 204 | + configuration: ServiceConfiguration::createWithDefaults() |
| 205 | + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ |
| 206 | + ModulePackageList::DBAL_PACKAGE, |
| 207 | + ModulePackageList::EVENT_SOURCING_PACKAGE, |
| 208 | + ModulePackageList::ASYNCHRONOUS_PACKAGE, |
| 209 | + ])), |
| 210 | + runForProductionEventStore: true, |
| 211 | + licenceKey: LicenceTesting::VALID_LICENCE, |
| 212 | + ); |
| 213 | + |
| 214 | + $ecotone->deleteProjection($projection::NAME) |
| 215 | + ->initializeProjection($projection::NAME); |
| 216 | + self::assertEquals([], $ecotone->sendQueryWithRouting('getOrders')); |
| 217 | + |
| 218 | + $ecotone->sendCommand(new PlaceOrder('order-1', 'laptop', 2)); |
| 219 | + self::assertEquals([ |
| 220 | + ['order_id' => 'order-1', 'product' => 'laptop', 'quantity' => '2'], |
| 221 | + ], $ecotone->sendQueryWithRouting('getOrders')); |
| 222 | + |
| 223 | + $ecotone->sendCommand(new PlaceOrder('order-2', 'phone', 1)); |
| 224 | + self::assertEquals([ |
| 225 | + ['order_id' => 'order-1', 'product' => 'laptop', 'quantity' => '2'], |
| 226 | + ['order_id' => 'order-2', 'product' => 'phone', 'quantity' => '1'], |
| 227 | + ], $ecotone->sendQueryWithRouting('getOrders')); |
| 228 | + |
| 229 | + // Test reset and catchup |
| 230 | + $ecotone->resetProjection($projection::NAME) |
| 231 | + ->triggerProjection($projection::NAME); |
| 232 | + |
| 233 | + self::assertEquals([ |
| 234 | + ['order_id' => 'order-1', 'product' => 'laptop', 'quantity' => '2'], |
| 235 | + ['order_id' => 'order-2', 'product' => 'phone', 'quantity' => '1'], |
| 236 | + ], $ecotone->sendQueryWithRouting('getOrders')); |
| 237 | + } |
| 238 | + |
| 239 | + public function test_aggregate_stream_throws_exception_for_non_event_sourcing_aggregate(): void |
| 240 | + { |
| 241 | + // Create a projection that references a non-EventSourcingAggregate class |
| 242 | + $projection = new #[ProjectionV2('invalid_projection'), FromAggregateStream(\stdClass::class)] class { |
| 243 | + #[EventHandler('*')] |
| 244 | + public function handle(array $event): void |
| 245 | + { |
| 246 | + } |
| 247 | + }; |
| 248 | + |
| 249 | + $this->expectException(ConfigurationException::class); |
| 250 | + $this->expectExceptionMessage('must be an EventSourcingAggregate'); |
| 251 | + |
| 252 | + EcotoneLite::bootstrapFlowTestingWithEventStore( |
| 253 | + classesToResolve: [$projection::class], |
| 254 | + containerOrAvailableServices: [$projection, self::getConnectionFactory()], |
| 255 | + configuration: ServiceConfiguration::createWithDefaults() |
| 256 | + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ |
| 257 | + ModulePackageList::DBAL_PACKAGE, |
| 258 | + ModulePackageList::EVENT_SOURCING_PACKAGE, |
| 259 | + ModulePackageList::ASYNCHRONOUS_PACKAGE, |
| 260 | + ])), |
| 261 | + runForProductionEventStore: true, |
| 262 | + licenceKey: LicenceTesting::VALID_LICENCE, |
| 263 | + ); |
| 264 | + } |
| 265 | + |
| 266 | + private function createOrderListProjectionWithAggregateStream(): object |
| 267 | + { |
| 268 | + $connection = $this->getConnection(); |
| 269 | + |
| 270 | + return new #[ProjectionV2(self::NAME), FromAggregateStream(Order::class)] class ($connection) { |
| 271 | + public const NAME = 'order_list_aggregate_stream'; |
| 272 | + |
| 273 | + public function __construct(private Connection $connection) |
| 274 | + { |
| 275 | + } |
| 276 | + |
| 277 | + #[QueryHandler('getOrders')] |
| 278 | + public function getOrders(): array |
| 279 | + { |
| 280 | + return $this->connection->executeQuery(<<<SQL |
| 281 | + SELECT * FROM order_list_aggregate_stream ORDER BY order_id ASC |
| 282 | + SQL)->fetchAllAssociative(); |
| 283 | + } |
| 284 | + |
| 285 | + #[EventHandler] |
| 286 | + public function addOrder(OrderWasPlaced $event): void |
| 287 | + { |
| 288 | + $this->connection->executeStatement(<<<SQL |
| 289 | + INSERT INTO order_list_aggregate_stream VALUES (?,?,?) |
| 290 | + SQL, [$event->orderId, $event->product, $event->quantity]); |
| 291 | + } |
| 292 | + |
| 293 | + #[ProjectionInitialization] |
| 294 | + public function initialization(): void |
| 295 | + { |
| 296 | + $this->connection->executeStatement(<<<SQL |
| 297 | + CREATE TABLE IF NOT EXISTS order_list_aggregate_stream ( |
| 298 | + order_id VARCHAR(36) PRIMARY KEY, |
| 299 | + product VARCHAR(255), |
| 300 | + quantity INT |
| 301 | + ) |
| 302 | + SQL); |
| 303 | + } |
| 304 | + |
| 305 | + #[ProjectionDelete] |
| 306 | + public function delete(): void |
| 307 | + { |
| 308 | + $this->connection->executeStatement(<<<SQL |
| 309 | + DROP TABLE IF EXISTS order_list_aggregate_stream |
| 310 | + SQL); |
| 311 | + } |
| 312 | + |
| 313 | + #[ProjectionReset] |
| 314 | + public function reset(): void |
| 315 | + { |
| 316 | + $this->connection->executeStatement(<<<SQL |
| 317 | + DELETE FROM order_list_aggregate_stream |
| 318 | + SQL); |
| 319 | + } |
| 320 | + }; |
| 321 | + } |
| 322 | + |
191 | 323 | private function createNotificationEventHandler(): object |
192 | 324 | { |
193 | 325 | return new class () { |
|
0 commit comments